paqad-ai
Version:
Spec-driven development framework — AI agents that think before they type
262 lines (257 loc) • 7.99 kB
JavaScript
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url);
import {
require_out
} from "./chunk-P4RPU2KB.js";
import {
resolveFrameworkConfig
} from "./chunk-ACJQW7SB.js";
import {
PATHS
} from "./chunk-IHMBVS6H.js";
import {
__toESM,
init_esm_shims
} from "./chunk-IDFRWBMN.js";
// src/planning/rule-compiler.ts
init_esm_shims();
var import_fast_glob = __toESM(require_out(), 1);
import { mkdir, readFile, writeFile } from "fs/promises";
import { createHash } from "crypto";
import { join } from "path";
var RULE_SCHEMA_VERSION = 1;
async function compileRules(root) {
const files = await (0, import_fast_glob.default)("**/*.md", {
cwd: join(root, PATHS.RULES_DIR),
onlyFiles: true
});
const gateFlags = resolveGateFlags(root);
const kept = [];
for (const file of files.sort()) {
const raw = await readFile(join(root, PATHS.RULES_DIR, file), "utf8");
const gate = extractGate(raw);
if (gate !== void 0 && gateFlags[gate] !== true) {
continue;
}
kept.push({ file, raw });
}
const rules = kept.map(({ file, raw }, index) => compileRuleContent(file, raw, index + 1));
return {
schema_version: RULE_SCHEMA_VERSION,
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
source_hash: await computeSourceHash(root),
rules
};
}
function resolveGateFlags(root) {
try {
return resolveFrameworkConfig(root).features;
} catch {
return {};
}
}
function extractGate(raw) {
return raw.match(/<!--\s*gate:\s*([a-z0-9_]+)\s*-->/i)?.[1];
}
async function writeCompiledRules(root, store) {
const outputPath = join(root, PATHS.COMPILED_RULES);
await mkdir(join(root, ".paqad"), { recursive: true });
await writeFile(outputPath, JSON.stringify(store, null, 2) + "\n", "utf8");
return outputPath;
}
async function readCompiledRules(root) {
try {
const raw = await readFile(join(root, PATHS.COMPILED_RULES), "utf8");
return JSON.parse(raw);
} catch {
return null;
}
}
async function isCompiledRulesStale(root) {
const store = await readCompiledRules(root);
if (!store) {
return true;
}
return store.source_hash !== await computeSourceHash(root);
}
async function computeSourceHash(root) {
const files = await (0, import_fast_glob.default)("**/*.md", {
cwd: join(root, PATHS.RULES_DIR),
onlyFiles: true
});
const hash = createHash("sha256");
for (const file of files.sort()) {
const content = await readFile(join(root, PATHS.RULES_DIR, file), "utf8");
hash.update(file);
hash.update("\0");
hash.update(content);
hash.update("\0");
}
return `sha256:${hash.digest("hex")}`;
}
function compileRuleContent(file, raw, ordinal) {
const headingMatch = raw.match(/^#\s+(.+)$/m);
const title = headingMatch?.[1]?.trim() || join(PATHS.RULES_DIR, file);
const summary = raw.split("\n").map((line) => line.trim()).find((line) => line && !line.startsWith("#") && !line.startsWith("```"));
const triggerPatterns = extractTriggerPatterns(raw);
if (summary === void 0) {
return {
rule_id: `RULE-${ordinal}`,
title,
source_path: join(PATHS.RULES_DIR, file),
trigger_patterns: ["**"],
severity: "must",
summary: "Unparseable rule content; preserve raw text for planning context.",
raw_text: raw
};
}
return {
rule_id: `RULE-${ordinal}`,
title,
source_path: join(PATHS.RULES_DIR, file),
trigger_patterns: triggerPatterns.length > 0 ? triggerPatterns : ["**"],
severity: inferSeverity(raw),
summary,
raw_text: raw
};
}
function looksLikeTriggerPattern(candidate) {
if (!candidate || candidate.length > 120) return false;
if (/\s/.test(candidate)) return false;
if (!/^[A-Za-z0-9_./*?@{}[\]-]+$/.test(candidate)) return false;
return candidate.includes("/") || /[*?]/.test(candidate) || /\.[A-Za-z0-9]+$/.test(candidate);
}
function extractTriggerPatterns(raw) {
const explicitDirective = raw.match(/<!--\s*trigger:\s*([^>]+)\s*-->/i)?.[1];
if (explicitDirective) {
return [
...new Set(
explicitDirective.split(",").map((value) => value.trim()).filter(Boolean)
)
];
}
const withoutFences = raw.replace(/```[\s\S]*?```/g, "");
const candidates = Array.from(withoutFences.matchAll(/`([^`]+)`/g), (match) => match[1].trim());
return [...new Set(candidates.filter(looksLikeTriggerPattern))];
}
function inferSeverity(raw) {
const normalized = raw.toLowerCase();
if (/\bmust\b|\brequired\b|\bnever\b/.test(normalized)) {
return "must";
}
if (/\bshould\b|\brecommended\b/.test(normalized)) {
return "should";
}
return "could";
}
// src/planning/decision-detector.ts
init_esm_shims();
var SIGNALS = [
{
category: "create-vs-reuse",
signal: "reuse-vs-create",
patterns: [
/\b(reuse|existing)\b.{0,40}\b(create|build|make new|new)\b/i,
/\b(create|build|make new|new)\b.{0,40}\b(reuse|existing)\b/i
]
},
{
category: "component-reuse",
signal: "component-choice",
patterns: [
/\b(button|card|modal|drawer|tile|iconbutton|input|form)\b.{0,15}\bor\b.{0,15}\b(button|card|modal|drawer|tile|iconbutton|input|form)\b/i
]
},
{
category: "architecture-path",
signal: "explicit-path-fork",
patterns: [
/((?:[\w-]+\/)+[\w.-]+\.\w+)\s*(?:,\s*)?\b(?:or|vs\.?|versus)\b\s*((?:[\w-]+\/)+[\w.-]+\.\w+)/i
],
validate: (match) => match[1].toLowerCase() !== match[2].toLowerCase()
},
{
category: "architecture-path",
signal: "alternative-path",
patterns: [/\b(or|either|alternatively|we could)\b/i]
},
{
category: "architecture-path",
signal: "multiple-file-paths",
patterns: [
/(?:[\w-]+\/)+[\w.-]+\.\w+.{0,40}\bor\b.{0,40}(?:[\w-]+\/)+[\w.-]+\.\w+/i,
/(?:[\w-]+\/)+[\w.-]+\.\w+.{0,40}(?:[\w-]+\/)+[\w.-]+\.\w+/i
]
}
];
function detectDecisionForks(request) {
const forks = [];
for (const candidate of SIGNALS) {
for (const pattern of candidate.patterns) {
const match = request.match(pattern);
if (match && (!candidate.validate || candidate.validate(match))) {
forks.push({
category: candidate.category,
confidence: confidenceFor(candidate.signal),
signal: candidate.signal,
matched_text: match[0]
});
break;
}
}
}
return forks;
}
function confidenceFor(signal) {
switch (signal) {
case "reuse-vs-create":
return 0.92;
case "explicit-path-fork":
return 0.9;
case "component-choice":
return 0.84;
default:
return 0.64;
}
}
// src/pipeline/rule-trigger-matcher.ts
init_esm_shims();
async function matchRuleTriggers(root, modulePaths) {
const compiled = await readCompiledRules(root);
if (!compiled || modulePaths.length === 0) {
return [];
}
return compiled.rules.filter((rule) => ruleTriggersMatch(rule, modulePaths)).map((r) => r.rule_id);
}
function isAlwaysLoadRule(rule) {
return rule.trigger_patterns.length === 0 || rule.trigger_patterns.includes("**");
}
function ruleTriggersMatch(rule, paths) {
return rule.trigger_patterns.some(
(pattern) => paths.some((path) => matchesGlobish(path, pattern))
);
}
function matchesGlobish(value, pattern) {
const normalizedValue = value.replace(/\\/g, "/");
const normalizedPattern = pattern.replace(/\\/g, "/");
if (normalizedPattern === "**") {
return true;
}
const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(
`^${escaped.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, ".")}$`
);
return regex.test(normalizedValue) || normalizedValue.includes(normalizedPattern.replace(/\*/g, ""));
}
export {
compileRules,
writeCompiledRules,
readCompiledRules,
isCompiledRulesStale,
computeSourceHash,
looksLikeTriggerPattern,
detectDecisionForks,
matchRuleTriggers,
isAlwaysLoadRule,
ruleTriggersMatch,
matchesGlobish
};