UNPKG

paqad-ai

Version:

Spec-driven development framework — AI agents that think before they type

643 lines (634 loc) • 19.7 kB
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url); import { RULE_SCRIPT_MAP_SCHEMA_VERSION, computeRuleScriptsDigest, executeRuleScript, loadRuleScriptMap, missingBinaries, parseScriptHeader, readCapabilityDigest, readReport, ruleScriptMapPath, serializeRuleScriptMap, writeCapabilityDigest } from "./chunk-SS625UO5.js"; import { require_out } from "./chunk-P4RPU2KB.js"; import { toPosixPath } from "./chunk-LYNDVGNE.js"; import { embedRuleMarker, mintRuleId, ruleTextHash, sha256, stripRuleMarker } from "./chunk-ACJQW7SB.js"; import { PATHS } from "./chunk-IHMBVS6H.js"; import { __toESM, init_esm_shims } from "./chunk-IDFRWBMN.js"; // src/rule-scripts/rule-file.ts init_esm_shims(); var BULLET_RE = /^(\s*)([-*])\s+(\S.*)$/; var FENCE_RE = /^\s*(```|~~~)/; function parseRuleFile(content) { const lines = content.split("\n"); const rules = []; let inFence = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (FENCE_RE.test(line)) { inFence = !inFence; continue; } if (inFence) { continue; } const m = BULLET_RE.exec(line); if (!m) { continue; } const { text, id } = stripRuleMarker(line); const bulletBody = stripRuleMarker(m[3]).text; rules.push({ line: i, indent: m[1], text: bulletBody, text_hash: ruleTextHash(bulletBody), id }); void text; } return rules; } function embedRuleIds(source, content, takenIds) { const lines = content.split("\n"); const parsed = parseRuleFile(content); const rules = []; for (const rule of parsed) { let id = rule.id; const isNew = id === null; if (id === null) { id = mintRuleId(source, rule.text, takenIds); } takenIds.add(id); lines[rule.line] = embedRuleMarker(lines[rule.line], id); rules.push({ id, text: rule.text, text_hash: rule.text_hash, isNew }); } return { content: lines.join("\n"), rules }; } // src/rule-scripts/analyzer.ts init_esm_shims(); var import_fast_glob = __toESM(require_out(), 1); import { readFileSync, writeFileSync } from "fs"; import { join } from "path"; function collectRuleFiles(projectRoot) { const rel = import_fast_glob.default.sync("**/*.md", { cwd: join(projectRoot, PATHS.RULES_DIR), onlyFiles: true }); return rel.map((r) => toPosixPath(join(PATHS.RULES_DIR, r))).sort(); } function computeRuleFilesHash(projectRoot, files) { const parts = files.slice().sort().map((rel) => `${rel} ${readFileSync(join(projectRoot, rel), "utf8")}`); return `sha256-${sha256(parts.join("\n"))}`; } function scanAndEmbedIds(projectRoot) { const files = collectRuleFiles(projectRoot); const taken = /* @__PURE__ */ new Set(); const inventory = []; const changed = []; for (const rel of files) { const abs = join(projectRoot, rel); const before = readFileSync(abs, "utf8"); const { content, rules } = embedRuleIds(rel, before, taken); if (content !== before) { writeFileSync(abs, content, "utf8"); changed.push(rel); } for (const r of rules) { inventory.push({ id: r.id, source: rel, text: r.text, text_hash: r.text_hash, isNew: r.isNew }); } } return { inventory, files, rule_files_hash: computeRuleFilesHash(projectRoot, files), changed_files: changed }; } function assembleMap(inventory, classifications, rule_files_hash, priorMap, now = /* @__PURE__ */ new Date()) { const priorById = /* @__PURE__ */ new Map(); for (const r of priorMap?.rules ?? []) { priorById.set(r.id, r); } const rules = inventory.map((item) => { const cls = classifications.get(item.id); const prior = priorById.get(item.id); const carryScripts = prior !== void 0 && prior.text_hash === item.text_hash ? prior.scripts : []; return { id: item.id, source: item.source, text: item.text, text_hash: item.text_hash, verifiability: cls?.verifiability ?? prior?.verifiability ?? { kind: "heuristic" }, enforced_by: cls?.enforced_by ?? prior?.enforced_by ?? [], scripts: carryScripts }; }); return { schema_version: RULE_SCRIPT_MAP_SCHEMA_VERSION, generated_at: now.toISOString(), rule_files_hash, rules }; } // src/rule-scripts/apply.ts init_esm_shims(); import { appendFileSync, existsSync, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs"; import { dirname, join as join2 } from "path"; function historyDir(projectRoot) { return join2(projectRoot, PATHS.RULE_SCRIPT_MAP_HISTORY_DIR); } function eventsLog(projectRoot) { return join2(projectRoot, PATHS.RULE_SCRIPT_MAP_EVENTS_LOG); } function sanitiseVia(via) { return via.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80); } function timestampForFilename(now) { return now.toISOString().replace(/[:.]/g, "-"); } var snapshotSeq = 0; function snapshotRuleScriptMap(projectRoot, via, now = /* @__PURE__ */ new Date()) { const mapPath = ruleScriptMapPath(projectRoot); const histDir = historyDir(projectRoot); mkdirSync(histDir, { recursive: true }); const unique = `${process.pid}-${(snapshotSeq++).toString(36)}`; const snapPath = join2(histDir, `${timestampForFilename(now)}-${sanitiseVia(via)}-${unique}.yml`); const existing = existsSync(mapPath) ? readFileSync2(mapPath, "utf8") : ""; writeFileSync2(snapPath, existing, "utf8"); return toPosixPath(snapPath); } function atomicWriteRuleScriptMap(projectRoot, yaml) { const mapPath = ruleScriptMapPath(projectRoot); mkdirSync(dirname(mapPath), { recursive: true }); const tmpPath = `${mapPath}.tmp-${process.pid}-${Date.now()}`; writeFileSync2(tmpPath, yaml, "utf8"); renameSync(tmpPath, mapPath); } function appendEvent(projectRoot, event) { const log = eventsLog(projectRoot); mkdirSync(dirname(log), { recursive: true }); appendFileSync(log, `${JSON.stringify(event)} `, "utf8"); } function applyRuleScriptMap(opts) { const now = /* @__PURE__ */ new Date(); const snapshot = snapshotRuleScriptMap(opts.projectRoot, opts.via, now); atomicWriteRuleScriptMap(opts.projectRoot, serializeRuleScriptMap(opts.map)); appendEvent(opts.projectRoot, { ts: now.toISOString(), via: opts.via, ...opts.event }); const digest = opts.keepDigest === true ? null : computeRuleScriptsDigest(opts.projectRoot); if (digest !== null) { writeCapabilityDigest(opts.projectRoot, "rule-scripts", digest, now); } return { snapshot_path: snapshot, applied_at: now.toISOString() }; } // src/rule-scripts/mutate.ts init_esm_shims(); function cloneMap(map) { return JSON.parse(JSON.stringify(map)); } function findRule(map, ruleId) { return map.rules.find((r) => r.id === ruleId); } function upsertScriptEntry(map, ruleId, entry) { const next = cloneMap(map); const rule = findRule(next, ruleId); if (!rule) { throw new Error(`upsertScriptEntry: rule ${ruleId} not found in map`); } const idx = rule.scripts.findIndex((s) => s.path === entry.path); if (idx >= 0) { rule.scripts[idx] = entry; } else { rule.scripts.push(entry); } rule.scripts.sort((a, b) => a.path.localeCompare(b.path)); return next; } function addRuleEntry(map, entry) { const next = cloneMap(map); if (next.rules.some((r) => r.id === entry.id)) { throw new Error(`addRuleEntry: rule ${entry.id} already in map`); } next.rules.push({ id: entry.id, source: entry.source, text: entry.text, text_hash: entry.text_hash, verifiability: { kind: "heuristic" }, enforced_by: [], scripts: [] }); return next; } function setRuleText(map, ruleId, text, textHash) { const next = cloneMap(map); const rule = findRule(next, ruleId); if (!rule) { throw new Error(`setRuleText: rule ${ruleId} not found in map`); } rule.text = text; rule.text_hash = textHash; rule.scripts = []; return next; } function clearRuleScripts(map, ruleId) { const next = cloneMap(map); const rule = findRule(next, ruleId); if (rule) { rule.scripts = []; } return next; } function setVerifiability(map, ruleId, verifiability) { const next = cloneMap(map); const rule = findRule(next, ruleId); if (!rule) { throw new Error(`setVerifiability: rule ${ruleId} not found in map`); } rule.verifiability = verifiability; if (verifiability.kind === "unverifiable") { rule.scripts = []; } return next; } function archiveRule(map, ruleId) { const next = cloneMap(map); const idx = next.rules.findIndex((r) => r.id === ruleId); if (idx < 0) { return next; } const [removed] = next.rules.splice(idx, 1); next.archived = [...next.archived ?? [], removed]; return next; } // src/rule-scripts/compile.ts init_esm_shims(); function isRuleScriptMapStale(projectRoot) { const map = loadRuleScriptMap(projectRoot); if (!map) return true; return map.rule_files_hash !== computeRuleFilesHash(projectRoot, collectRuleFiles(projectRoot)); } function isRuleScriptMapCurrent(projectRoot) { try { return !isRuleScriptMapStale(projectRoot); } catch { return false; } } function dropRuleScriptMapSources(projectRoot, sources, mapWasCurrent, repairedTexts = []) { const map = loadRuleScriptMap(projectRoot); if (!map) return; const dropped = map.rules.filter((rule) => sources.includes(rule.source)); const edits = repairedTexts.filter((edit) => map.rules.some((rule) => rule.id === edit.id)); const ruleFilesHash = mapWasCurrent ? computeRuleFilesHash(projectRoot, collectRuleFiles(projectRoot)) : map.rule_files_hash; if (dropped.length === 0 && edits.length === 0 && ruleFilesHash === map.rule_files_hash) return; const digestWasValid = readCapabilityDigest(projectRoot, "rule-scripts") === computeRuleScriptsDigest(projectRoot); const edited = edits.reduce( (current, edit) => setRuleText(current, edit.id, edit.text, ruleTextHash(edit.text)), map ); applyRuleScriptMap({ projectRoot, map: { ...edited, rule_files_hash: ruleFilesHash, rules: edited.rules.filter((rule) => !sources.includes(rule.source)) }, via: "update-cleanup", event: dropped.length > 0 ? { action: "remove", rule_ids: dropped.map((rule) => rule.id), note: sources.join(", ") } : { action: edits.length > 0 ? "edit" : "reconcile", rule_ids: edits.map((edit) => edit.id), note: "refreshed the rule file hash after update" }, keepDigest: !digestWasValid }); } function compileRuleScripts(projectRoot) { const scan = scanAndEmbedIds(projectRoot); const prior = loadRuleScriptMap(projectRoot); const map = assembleMap(scan.inventory, /* @__PURE__ */ new Map(), scan.rule_files_hash, prior); const applied = applyRuleScriptMap({ projectRoot, map, via: "rules-compile", event: { action: "generate", rule_ids: map.rules.map((rule) => rule.id) } }); return { map, ruleCount: map.rules.length, scriptedCount: map.rules.filter((rule) => rule.scripts.length > 0).length, changedFiles: scan.changed_files, snapshotPath: applied.snapshot_path, appliedAt: applied.applied_at }; } // src/rule-scripts/fixture-runner.ts init_esm_shims(); var import_fast_glob2 = __toESM(require_out(), 1); import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs"; import { basename, dirname as dirname2, join as join3 } from "path"; function fixturesRoot(scriptPath) { const dir = dirname2(scriptPath); const stem = basename(scriptPath).replace(/\.mjs$/, ""); return join3(dir, stem, "__fixtures__"); } function listFixtures(dir) { if (!existsSync2(dir)) { return []; } return import_fast_glob2.default.sync("**/*", { cwd: dir, onlyFiles: true, dot: false }).sort(); } function runFixtures(scriptPath) { const root = fixturesRoot(scriptPath); const passDir = join3(root, "pass"); const failDir = join3(root, "fail"); const passFiles = listFixtures(passDir); const failFiles = listFixtures(failDir); if (passFiles.length === 0 && failFiles.length === 0) { return { passed: false, pass_fixtures: 0, fail_fixtures: 0, failures: [], missing_fixtures: true }; } if (existsSync2(scriptPath)) { const header = parseScriptHeader(readFileSync3(scriptPath, "utf8")); const missing = missingBinaries(header.header?.requires?.binaries); if (missing.length > 0) { return { passed: false, pass_fixtures: passFiles.length, fail_fixtures: failFiles.length, failures: [], missing_fixtures: false, missing_binaries: missing }; } } const failures = []; for (const rel of passFiles) { const result = executeRuleScript(scriptPath, { projectRoot: passDir, files: [rel] }); if (!result.ok) { failures.push({ fixture: `pass/${rel}`, expected: "zero-findings", actual: -1, detail: result.error }); continue; } const count = result.report?.findings.length ?? 0; if (count !== 0) { failures.push({ fixture: `pass/${rel}`, expected: "zero-findings", actual: count }); } } for (const rel of failFiles) { const result = executeRuleScript(scriptPath, { projectRoot: failDir, files: [rel] }); if (!result.ok) { failures.push({ fixture: `fail/${rel}`, expected: "at-least-one-finding", actual: -1, detail: result.error }); continue; } const count = result.report?.findings.length ?? 0; if (count < 1) { failures.push({ fixture: `fail/${rel}`, expected: "at-least-one-finding", actual: count }); } } return { passed: failures.length === 0, pass_fixtures: passFiles.length, fail_fixtures: failFiles.length, failures, missing_fixtures: false }; } // src/rule-scripts/reconciler.ts init_esm_shims(); import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs"; import { dirname as dirname3, join as join4 } from "path"; function emptyCounts() { return { "RS-RULE-ADDED": 0, "RS-RULE-EDITED": 0, "RS-RULE-REMOVED": 0, "RS-SCRIPT-STALE": 0, "RS-FIXTURE-FAIL": 0, "RS-CONFLICT": 0, "RS-CACHE-INVALID": 0 }; } function driftPath(projectRoot) { return join4(projectRoot, PATHS.RULE_SCRIPTS_DRIFT); } var CONFLICT_CODES = /* @__PURE__ */ new Set(["RS-CONFLICT"]); function mergeDrift(projectRoot, ownedCodes, ownFindings) { const path = driftPath(projectRoot); if (existsSync3(path) && readDrift(projectRoot) === null) { try { renameSync2(path, `${path}.corrupt-${Date.now()}`); } catch { } } const existing = readDrift(projectRoot); const preserved = (existing?.findings ?? []).filter((f) => !ownedCodes.has(f.code)); const findings = [...preserved, ...ownFindings]; const counts = emptyCounts(); for (const f of findings) { counts[f.code]++; } const report = { generated_at: (/* @__PURE__ */ new Date()).toISOString(), findings, counts, blocked: findings.some((f) => f.code !== "RS-CACHE-INVALID") }; writeDrift(projectRoot, report); return report; } function reconcileRuleScripts(projectRoot) { const findings = []; const map = loadRuleScriptMap(projectRoot); const files = collectRuleFiles(projectRoot); const onDisk = /* @__PURE__ */ new Map(); for (const rel of files) { const parsed = parseRuleFile(readFileSync4(join4(projectRoot, rel), "utf8")); for (const rule of parsed) { if (rule.id === null) { findings.push({ code: "RS-RULE-ADDED", source: rel, message: `Unmarked rule bullet in ${rel}: "${rule.text}". Run \`analyze rules\`.` }); continue; } onDisk.set(rule.id, { source: rel, text_hash: rule.text_hash }); } } const mapById = new Map((map?.rules ?? []).map((r) => [r.id, r])); for (const [id, disk] of onDisk) { const entry = mapById.get(id); if (!entry) { findings.push({ code: "RS-RULE-ADDED", rule_id: id, source: disk.source, message: `Rule ${id} is marked on disk but absent from the map. Run \`analyze rules\`.` }); continue; } if (entry.text_hash !== disk.text_hash) { findings.push({ code: "RS-RULE-EDITED", rule_id: id, source: disk.source, message: `Rule ${id} text changed since the map was written. Run \`edit rule ${id}\` or \`generate rule scripts\`.` }); if (entry.scripts.length > 0) { findings.push({ code: "RS-SCRIPT-STALE", rule_id: id, source: disk.source, message: `Rule ${id} was edited but its ${entry.scripts.length} script(s) were not regenerated.` }); } } } for (const entry of map?.rules ?? []) { if (!onDisk.has(entry.id)) { findings.push({ code: "RS-RULE-REMOVED", rule_id: entry.id, source: entry.source, message: `Rule ${entry.id} is in the map but its marker is gone from ${entry.source}. Run \`remove rule ${entry.id}\`.` }); } } for (const entry of map?.rules ?? []) { for (const script of entry.scripts) { const abs = join4(projectRoot, script.path); if (!existsSync3(abs)) { continue; } const result = runFixtures(abs); if (!result.passed) { findings.push({ code: "RS-FIXTURE-FAIL", rule_id: entry.id, source: script.path, message: `Script ${script.path} no longer passes its fixtures. Run \`regenerate scripts for rule ${entry.id}\`.` }); } } } const report = readReport(projectRoot); if (report && files.length > 0 && findings.length === 0) { const currentHash = computeRuleFilesHash(projectRoot, files); if (report.rule_files_hash && report.rule_files_hash !== currentHash) { findings.push({ code: "RS-CACHE-INVALID", message: "Cached findings report is stale (rule_files_hash mismatch). It will be recomputed." }); } } const reconcilerOwned = new Set( Object.keys(emptyCounts()).filter((c) => !CONFLICT_CODES.has(c)) ); return mergeDrift(projectRoot, reconcilerOwned, findings); } function writeDrift(projectRoot, report) { const path = driftPath(projectRoot); mkdirSync2(dirname3(path), { recursive: true }); writeFileSync3(path, `${JSON.stringify(report, null, 2)} `, "utf8"); } function readDrift(projectRoot) { const path = driftPath(projectRoot); if (!existsSync3(path)) { return null; } try { return JSON.parse(readFileSync4(path, "utf8")); } catch { return null; } } function recordConflictFindings(projectRoot, conflicts) { const findings = conflicts.filter((c) => Array.isArray(c.rule_ids) && c.rule_ids.length > 0).map((c) => ({ code: "RS-CONFLICT", rule_id: c.rule_ids[0], message: `${c.message} (conflicting rules: ${c.rule_ids.join(", ")})` })); return mergeDrift(projectRoot, CONFLICT_CODES, findings); } export { parseRuleFile, embedRuleIds, collectRuleFiles, computeRuleFilesHash, scanAndEmbedIds, assembleMap, snapshotRuleScriptMap, atomicWriteRuleScriptMap, applyRuleScriptMap, upsertScriptEntry, addRuleEntry, setRuleText, clearRuleScripts, setVerifiability, archiveRule, isRuleScriptMapStale, isRuleScriptMapCurrent, dropRuleScriptMapSources, compileRuleScripts, fixturesRoot, runFixtures, reconcileRuleScripts, readDrift, recordConflictFindings };