paqad-ai
Version:
Spec-driven development framework — AI agents that think before they type
9,152 lines • 328 kB
JavaScript
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url);
import {
formatStageAgentWriteSummary,
stageAgentWriteDecision
} from "./chunk-E4ZQOAV4.js";
import {
findAgentPaths,
findSkillPaths,
readSkillPathContext,
resolveFlooredMode,
shouldStripAiAttribution
} from "./chunk-KXN6TRQP.js";
import {
subtractUnchangedBaselineFiles
} from "./chunk-Z4GLUCC5.js";
import {
FREEZE_NEXT_LINE,
FREEZE_NEXT_V2_LINE,
GATE_NO_DESIGN_LINE,
gateBlockLine,
readHld
} from "./chunk-LGAFOBYN.js";
import {
readStackSnapshotSync
} from "./chunk-WRHJCBRI.js";
import {
expertBudget,
isExpertRole
} from "./chunk-IKAOR4NY.js";
import {
formatUnresolvedStageSummary,
unresolvedStageAgent
} from "./chunk-GCMANDHS.js";
import {
compileRules,
detectDecisionForks,
isAlwaysLoadRule,
isCompiledRulesStale,
readCompiledRules,
ruleTriggersMatch,
writeCompiledRules
} from "./chunk-7Q65STES.js";
import {
atomicWriteFile,
getPacksForFrameworks
} from "./chunk-K4E6ACLH.js";
import {
DecisionStore,
computeDecisionFingerprint,
createPendingDecision,
readContractDecisions,
recordDecisionReuse
} from "./chunk-4ZAWKFXT.js";
import {
appendHooksLog
} from "./chunk-4S3XFJAH.js";
import {
getPrimaryStack
} from "./chunk-HNYZUZ7D.js";
import {
readProjectProfile
} from "./chunk-AK3HYELM.js";
import {
DECISION_CATEGORY_DEFAULTS
} from "./chunk-6XDDIYNJ.js";
import {
recordProjectEvent
} from "./chunk-I2G54BH7.js";
import {
computeRuleScriptsDigest,
enforceRuleScripts,
executeRuleScriptAsync,
getCapability,
isOnPath,
loadRuleScriptMap,
missingBinaries,
parseScriptHeader,
projectRelativeTargets,
readCapabilityDigest,
readCapabilityVersions,
runPool
} from "./chunk-SS625UO5.js";
import {
hudLastBlockPath,
safeSessionId,
writeFileAtomic
} from "./chunk-3UV6TK6X.js";
import {
appendRuleRun,
readFeatureEvidence
} from "./chunk-5ETIY5I3.js";
import {
activeChangeFiles,
loadChangeEvidence
} from "./chunk-EQYAXXZO.js";
import {
execa
} from "./chunk-MDXUST3Z.js";
import {
markerBatchNarration
} from "./chunk-22I2D24D.js";
import {
bundleWriterFor,
classifyBundlePath,
normalizeArtifactPath,
parseAndRecordMarkers
} from "./chunk-OAEANL22.js";
import {
isFeatureDevelopmentRoute,
readWorkflowState
} from "./chunk-VOJO7HJ6.js";
import {
backfillFeatureSlug,
readPendingImpact,
resolveEffectiveLane
} from "./chunk-7D5MLTG3.js";
import {
levenshtein
} from "./chunk-Y3O45QYV.js";
import {
stripTrailingChars,
toPosixPath,
toProjectRelativePath
} from "./chunk-LYNDVGNE.js";
import {
appendFeatureStageRow,
currentFeature,
documentSessionId,
foldFeature,
isPaqadArtifactPath,
pendingTicketActions,
readChangeStartPoint,
readFeatureDelivery,
readFeatureStageUnit,
recordShip,
resolveDeliveryFeatureByBranch,
stampFeatureDocument,
withFileLockSync
} from "./chunk-5M55PDWL.js";
import {
releaseLock,
tryAcquireLock
} from "./chunk-TINPSX3N.js";
import {
PRE_CODE_STAGES,
SPEC_CORRECTION_KIND,
SPEC_STEP_KIND,
STAGE_AGENT_KIND,
STAGE_EVIDENCE_DOC_TYPE,
isArtifactBearingStage,
isBundleClosed,
isBundleMaterialized,
isStageAgent,
listFeatureDirs,
listInFlightFeatures,
readFeatureRecord,
requiredPreCodeStages,
updateFeatureRecord,
validatePlanRecord,
validateReviewRecord
} from "./chunk-7TBVBCDH.js";
import {
ENVELOPE_HEADER_KEYS,
buildDocumentEnvelope,
buildHeadingPath,
buildPlanRecord,
buildReviewRecord,
buildTextHeader,
classifySection,
makeDeterministicGeneratedId,
normalizeCell,
parseHeadings,
renderFrontMatter,
rowRecordedAt,
sha256Hex,
splitFrontMatter,
splitLines
} from "./chunk-RJMJYPHL.js";
import {
AUTHORING_BATCH_MODES,
AUTHORING_SECTION,
FRAMEWORK_CONFIG_SPECS,
configSaysPaqadDisabled,
envConfigValue,
layeredConfigMap,
loadGitWorkflow,
readConfigsDir,
readDotConfig,
require_ajv,
require_dist,
resolveFrameworkConfig,
resolveNumericConfig,
resolveRuleGateConfig,
setConfigValue
} from "./chunk-ACJQW7SB.js";
import {
renameWithRetrySync
} from "./chunk-Z3NGWHFX.js";
import {
getRuntimeRoot
} from "./chunk-YWAORJG6.js";
import {
readUnitFile,
sessionLedgerDir
} from "./chunk-Q353EDKY.js";
import {
resolveSessionId
} from "./chunk-SG6LXLDH.js";
import {
readSessionControl
} from "./chunk-FMQ5YRSP.js";
import {
CLARIFICATION_DOC_TYPE,
EXPERTS_DOC_TYPE,
FEATURE_BUNDLE_FILES,
FEATURE_DOC_SCHEMA_VERSION,
REQUEST_DOC_TYPE,
SCREENSHOTS_DIR,
SPECIFICATION_DOC_TYPE,
SPEC_SOURCE_DOC_TYPE,
dirname,
featureChangeKey,
featureDir,
featureFilePath,
isAbsolute,
join,
parseFeatureDirName,
relative,
resolve,
slugifyAscii
} from "./chunk-AZLA53A6.js";
import {
PATHS
} from "./chunk-IHMBVS6H.js";
import {
__toESM,
init_esm_shims
} from "./chunk-IDFRWBMN.js";
// src/compliance/constants.ts
init_esm_shims();
var COMPLIANCE_SCHEMA_VERSION = 1;
var SPEC_REVIEW_SCHEMA_VERSION = 1;
var DEFAULT_OBLIGATION_INDEX_PATH = ".paqad/compliance/obligation-index.json";
function slugifySpec(specFile) {
const segments = specFile.replace(/\.[^.]+$/, "").split(/[\\/]/);
const base = segments[segments.length - 1];
return slugifyAscii(base);
}
function specIndexPath(specFile) {
return `.paqad/compliance/${slugifySpec(specFile)}/obligations.json`;
}
function specReportPath(specFile) {
return `.paqad/compliance/${slugifySpec(specFile)}/report.json`;
}
function specReviewPath(specFile) {
return `.paqad/compliance/${slugifySpec(specFile)}/spec-review.json`;
}
// src/compliance/spec-review.ts
init_esm_shims();
// src/compliance/spec-review-detectors/registry.ts
init_esm_shims();
// src/compliance/spec-review-detectors/boundary-detector.ts
init_esm_shims();
// src/compliance/spec-review-detectors/shared.ts
init_esm_shims();
function normalizeText(value) {
return value.toLowerCase().replace(/[`*_]/g, "").replace(/\s+/g, " ").trim();
}
function makeLocation(line) {
return {
section: line.section,
line_range: [line.line, line.line],
text_excerpt: line.text.trim()
};
}
// src/compliance/spec-review-detectors/boundary-detector.ts
var boundaryDetector = {
name: "boundary_gap",
detect(context) {
const defects = [];
for (const line of context.review_lines) {
const normalized = normalizeText(line.text);
const limitMatch = /\b(?:maximum|max(?:imum)?|minimum|min|threshold|limit)\b.*?(\d[\d,]*)/.exec(normalized);
if (!limitMatch) continue;
if (/exactly|equal to|at the boundary|inclusive/.test(normalized)) continue;
const value = limitMatch[1].replace(/,/g, "");
const relatedLines = context.review_lines.filter((candidate) => {
const text = normalizeText(candidate.text);
return text.includes(value) || /\btruncate|truncation|append truncation marker|clamp|reject\b/.test(text);
});
const hasBoundaryRule = relatedLines.some(
(candidate) => /exactly|equal to|at the boundary|inclusive/.test(normalizeText(candidate.text))
);
const hasTransitionBehavior = relatedLines.some(
(candidate) => /\btruncate|truncation|append truncation marker|clamp|reject\b/.test(
normalizeText(candidate.text)
)
);
if (hasTransitionBehavior && !hasBoundaryRule) {
defects.push({
category: "boundary_gap",
severity: "major",
description: `The spec defines a ${value}-value limit but never states the exact boundary behavior.`,
locations: [makeLocation(line)],
suggested_resolution: `State what happens at exactly ${value}.`
});
}
}
return defects;
}
};
// src/compliance/spec-review-detectors/contradiction-detector.ts
init_esm_shims();
var contradictionDetector = {
name: "contradiction",
detect(context) {
const defects = [];
const exclusionLine = context.review_lines.find(
(line) => /exclude(?:d)? .*denominator|excluded from the denominator/.test(normalizeText(line.text))
);
const formulaLine = context.review_lines.find(
(line) => /(?:compliance_)?ratio\s*=.*\/\s*total\b|covered\s*\/\s*total\b/.test(
normalizeText(line.text)
)
);
if (exclusionLine && formulaLine) {
defects.push({
category: "contradiction",
severity: "critical",
description: "The denominator rule excludes obligations, but the stated formula still divides by total.",
locations: [makeLocation(exclusionLine), makeLocation(formulaLine)].sort(
(a, b) => a.line_range[0] - b.line_range[0]
),
suggested_resolution: "Clarify one denominator rule and align the formula with that rule."
});
}
const alwaysLines = context.review_lines.filter(
(line) => /\balways\b/.test(normalizeText(line.text))
);
const neverLines = context.review_lines.filter(
(line) => /\bnever\b/.test(normalizeText(line.text))
);
for (const left of alwaysLines) {
const leftText = normalizeText(left.text);
const quotedSubject = extractQuotedSubject(left.text);
for (const right of neverLines) {
const rightText = normalizeText(right.text);
const subject = quotedSubject ?? extractQuotedSubject(right.text) ?? inferSharedSubject(leftText, rightText);
if (!subject) continue;
if (left.line === right.line) continue;
if (!leftText.includes(subject) || !rightText.includes(subject)) continue;
defects.push({
category: "contradiction",
severity: "major",
description: `The spec gives both "always" and "never" guidance for ${subject}.`,
locations: [makeLocation(left), makeLocation(right)].sort(
(a, b) => a.line_range[0] - b.line_range[0]
),
suggested_resolution: `Choose one rule for ${subject} or define when each rule applies.`
});
break;
}
}
return defects;
}
};
function extractQuotedSubject(text) {
const match = /[`'"]([^`'"]+)[`'"]/.exec(text);
return match ? normalizeText(match[1]) : null;
}
function inferSharedSubject(left, right) {
const stopWords = /* @__PURE__ */ new Set([
"the",
"a",
"an",
"and",
"or",
"to",
"for",
"of",
"in",
"on",
"with",
"must",
"should",
"always",
"never",
"be",
"is",
"are",
"when",
"if",
"then"
]);
const leftTokens = tokenize(left).filter((token) => !stopWords.has(token));
const rightTokens = new Set(tokenize(right).filter((token) => !stopWords.has(token)));
const overlap = leftTokens.filter((token) => rightTokens.has(token));
if (overlap.length === 0) return null;
return overlap.sort((a, b) => b.length - a.length)[0];
}
function tokenize(text) {
return text.split(/[^a-z0-9_]+/g).map((token) => token.trim()).filter((token) => token.length >= 3);
}
// src/compliance/spec-review-detectors/formula-detector.ts
init_esm_shims();
var DIVISION_PATTERN = /\b([a-z_]\w*)\s*\/\s*(?:\(([^)]+)\)|([a-z_]\w*))/;
var ZERO_CASE_PATTERN = /all\s+\w+(?:\s+\w+)*\s+(?:may|can|could)\s+be\s+(?:indeterminate|zero|empty|null|absent)|all\s+\w+(?:\s+\w+)*\s+are\s+(?:indeterminate|zero|empty|absent)|(?:may|can|could)\s+have\s+(?:zero|no)\s+\w+|\bno\s+\w+\s+(?:exist|remain|are\s+present)/;
var formulaDetector = {
name: "formula_inconsistency",
detect(context) {
const defects = [];
const canonicalFormulaLine = context.review_lines.find(
(line) => /\/\s*\(\s*total\s*-\s*indeterminate\s*\)/.test(normalizeText(line.text))
);
const canonicalZeroCaseLine = context.review_lines.find(
(line) => /all obligations .* indeterminate|all .* may be indeterminate/.test(normalizeText(line.text))
);
if (canonicalFormulaLine && canonicalZeroCaseLine) {
defects.push({
category: "formula_inconsistency",
severity: "critical",
description: "The formula can divide by zero when the spec allows every obligation to be indeterminate.",
locations: [makeLocation(canonicalFormulaLine), makeLocation(canonicalZeroCaseLine)].sort(
(a, b) => a.line_range[0] - b.line_range[0]
),
suggested_resolution: "Define the zero-denominator behavior or revise the formula so that this input remains valid."
});
}
for (const line of context.review_lines) {
const normalized = normalizeText(line.text);
const divMatch = DIVISION_PATTERN.exec(normalized);
if (!divMatch) continue;
const denomExpr = (divMatch[2] ?? divMatch[3]).trim();
const denomWord = /\b([a-z_]\w*)\b/.exec(denomExpr)?.[1];
if (!denomWord || denomWord.length < 3) continue;
if (/http|url|path|file|true|false/.test(denomWord)) continue;
const zeroCaseLine = context.review_lines.find((candidate) => {
if (candidate.line === line.line) return false;
const candidateText = normalizeText(candidate.text);
return candidateText.includes(denomWord) && ZERO_CASE_PATTERN.test(candidateText);
});
if (zeroCaseLine) {
defects.push({
category: "formula_inconsistency",
severity: "critical",
description: `The formula divides by an expression containing "${denomWord}", but the spec allows a state where that value reaches zero.`,
locations: [makeLocation(line), makeLocation(zeroCaseLine)].sort(
(a, b) => a.line_range[0] - b.line_range[0]
),
suggested_resolution: `Guard the formula against a zero "${denomWord}" value, or define behavior for that edge case.`
});
}
}
return dedupe(defects);
}
};
function dedupe(items) {
const seen = /* @__PURE__ */ new Set();
return items.filter((item) => {
const key = item.locations.map((l) => l.line_range[0]).join(":");
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
// src/compliance/spec-review-detectors/goal-conflict-detector.ts
init_esm_shims();
var CONFLICT_RULES = [
{
description: "A lossless extraction goal conflicts with a rule that ignores partially matching tables.",
preservePattern: /\blossless\b|must preserve all data/,
discardPattern: /\bignore\b.*\btable\b|non-matching tables .* ignored/,
suggested_resolution: "Define which rule wins when a structure is partially recognized."
},
{
description: 'A "must preserve all" completeness requirement conflicts with a rule that excludes or skips items.',
preservePattern: /\bmust preserve all\b|\bno data (?:may be )?lost\b|\bcompleteness (?:is )?required\b|\bnothing (?:should|must) be (?:lost|dropped|discarded)\b/,
discardPattern: /(?:\b(?:exclud\w*|skip\w*|filter(?:\s+out)?|discard\w*|omit\w*)\b.*\b(?:items?|entries?|records?|rows?|fields?)\b|\b(?:items?|entries?|records?|rows?|fields?)\b.*\b(?:exclud\w*|skip\w*|filter(?:\s+out)?|discard\w*|omit\w*)\b)/,
suggested_resolution: "Specify what takes priority: completeness or the exclusion rule, and define behavior for items caught by both."
},
{
description: 'An all-or-nothing "must succeed for all items" requirement conflicts with language that allows partial or incomplete processing.',
preservePattern: /\bmust (?:succeed|complete|process)\s+(?:for\s+)?all\b|\ball\s+\w+\s+must\s+be\s+(?:processed|handled|covered)\b/,
discardPattern: /\bpartial(?:ly)?\b|\bmay\s+(?:fail|be\s+skipped|be\s+incomplete)\b|\bnot\s+all\s+\w+\s+(?:will|may|might)\b/,
suggested_resolution: "Reconcile the all-or-nothing requirement with the partial-processing allowance."
}
];
var goalConflictDetector = {
name: "goal_conflict",
detect(context) {
const defects = [];
for (const rule of CONFLICT_RULES) {
const preserveLine = context.review_lines.find(
(line) => rule.preservePattern.test(normalizeText(line.text))
);
const discardLine = context.review_lines.find(
(line) => rule.discardPattern.test(normalizeText(line.text))
);
if (preserveLine && discardLine) {
defects.push({
category: "goal_conflict",
severity: "major",
description: rule.description,
locations: [makeLocation(preserveLine), makeLocation(discardLine)].sort(
(a, b) => a.line_range[0] - b.line_range[0]
),
suggested_resolution: rule.suggested_resolution
});
}
}
return defects;
}
};
// src/compliance/spec-review-detectors/missing-negative-detector.ts
init_esm_shims();
var POSITIVE_PATH_PATTERN = /when[\s\S]*(100%|below threshold)/;
var INDETERMINATE_ALL_PATTERN = /all obligations[\s\S]*indeterminate[\s\S]*(return|warn|skip|pass|fail|must)/;
var NEGATIVE_PATH_PATTERN = /when not |otherwise|else|if not/;
var missingNegativeDetector = {
name: "missing_negative_case",
detect(context) {
const defects = [];
const positiveLine = context.review_lines.find((line) => {
const text = normalizeText(line.text);
return POSITIVE_PATH_PATTERN.test(text);
});
const negativeLine = context.review_lines.find(
(line) => hasNegativePath(normalizeText(line.text))
);
if (positiveLine && !negativeLine) {
defects.push({
category: "missing_negative_case",
severity: "major",
description: "The spec defines conditional success or failure behavior but leaves the negative path undefined.",
locations: [makeLocation(positiveLine)],
suggested_resolution: "Define what happens when the stated condition does not hold, especially for indeterminate or empty inputs."
});
}
return defects;
}
};
function hasNegativePath(text) {
return INDETERMINATE_ALL_PATTERN.test(text) || NEGATIVE_PATH_PATTERN.test(text);
}
// src/compliance/spec-review-detectors/reference-detector.ts
init_esm_shims();
var ID_PATTERN = /\b(?:FR|EC|AC|NFR|OQ)-[A-Z0-9][A-Z0-9.-]*\b/g;
var referenceDetector = {
name: "references",
detect(context) {
const defects = [];
const definedIds = collectDefinedIds(context.lines);
for (const line of context.review_lines) {
const normalized = normalizeText(line.text);
if (/\bsee\b .*?\bspec\b/.test(normalized) && ![...line.text.matchAll(ID_PATTERN)].length) {
defects.push({
category: "unresolvable_reference",
severity: "minor",
description: "The spec references an external document that cannot be validated locally.",
locations: [makeLocation(line)],
suggested_resolution: "Inline the required contract details or replace the external note with a local reference."
});
}
const references = [...line.text.matchAll(ID_PATTERN)].map((match) => match[0]);
for (const reference of references) {
if (definedIds.has(reference)) continue;
defects.push({
category: "dangling_reference",
severity: "major",
description: `The spec references ${reference}, but that identifier is not defined.`,
locations: [makeLocation(line)],
suggested_resolution: `Define ${reference} or update the reference to the correct identifier.`
});
}
}
return dedupeByLineAndCategory(defects);
}
};
function collectDefinedIds(lines) {
const defined = /* @__PURE__ */ new Set();
for (const text of lines) {
const trimmed = text.trim();
const leadingMatches = [
/^(?:#{1,6}\s+)?((?:FR|EC|AC|NFR|OQ)-[A-Z0-9][A-Z0-9.-]*)\b/,
/^\*\*((?:FR|EC|AC|NFR|OQ)-[A-Z0-9][A-Z0-9.-]*)\*\*/,
/^\|\s*((?:FR|EC|AC|NFR|OQ)-[A-Z0-9][A-Z0-9.-]*)\s*\|/
];
for (const pattern of leadingMatches) {
const match = pattern.exec(trimmed);
if (match) {
defined.add(match[1]);
}
}
}
return defined;
}
function dedupeByLineAndCategory(defects) {
const seen = /* @__PURE__ */ new Set();
return defects.filter((defect) => {
const key = `${defect.category}:${defect.locations[0]?.line_range[0] ?? 0}:${defect.description ?? ""}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
// src/compliance/spec-review-detectors/registry.ts
var SPEC_REVIEW_DETECTORS = [
contradictionDetector,
formulaDetector,
boundaryDetector,
goalConflictDetector,
referenceDetector,
missingNegativeDetector
];
// src/compliance/spec-review.ts
var SEVERITY_ORDER = {
critical: 0,
major: 1,
minor: 2
};
function reviewSpecification(options) {
const reviewedAt = options.reviewed_at ?? (/* @__PURE__ */ new Date()).toISOString();
const context = buildReviewContext(options.spec_file, options.spec_markdown);
const rawDefects = SPEC_REVIEW_DETECTORS.flatMap((detector) => detector.detect(context));
const nextDefects = rawDefects.map((defect) => hydrateDefect(defect));
const previousById = new Map(
(options.previous_report?.defects ?? []).map((defect) => [defect.defect_id, defect])
);
const defects = nextDefects.map((defect) => ({
...defect,
status: previousById.has(defect.defect_id) ? "existing" : "new"
}));
for (const previous of options.previous_report?.defects ?? []) {
if (defects.some((defect) => defect.defect_id === previous.defect_id)) continue;
defects.push({
...previous,
status: "resolved",
affected_obligation_ids: previous.affected_obligation_ids ?? []
});
}
defects.sort(compareDefects);
return {
metadata: {
spec_file: options.spec_file,
spec_hash: sha256Hex(options.spec_markdown),
reviewed_at: reviewedAt,
defect_count: defects.filter((defect) => defect.status !== "resolved").length,
schema_version: SPEC_REVIEW_SCHEMA_VERSION
},
defects,
pattern_advisories: []
};
}
function attachSpecDefectsToObligations(review, obligations) {
const defects = review.defects.map((defect) => {
if (defect.status === "resolved") {
return { ...defect, affected_obligation_ids: defect.affected_obligation_ids ?? [] };
}
const affected = obligations.filter(
(obligation) => defect.locations.some((location) => {
const [start, end] = location.line_range;
return obligation.source_line !== null && obligation.source_line >= start && obligation.source_line <= end;
})
).map((obligation) => obligation.obligation_id).sort((a, b) => a.localeCompare(b));
return {
...defect,
affected_obligation_ids: affected
};
});
return {
...review,
defects
};
}
function buildReviewContext(specFile, markdown) {
const lines = splitLines(markdown);
const headings = parseHeadings(lines);
const openQuestionRanges = findOpenQuestionRanges(headings, lines.length);
const reviewLines = lines.map((text, index) => ({
line: index + 1,
text,
section: buildHeadingPath(headings, index + 1) || "Spec"
})).filter((line) => isReviewableLine(line, openQuestionRanges));
return {
spec_file: specFile,
spec_markdown: markdown,
lines,
review_lines: reviewLines
};
}
function findOpenQuestionRanges(headings, lineCount) {
const ranges = [];
for (let index = 0; index < headings.length; index += 1) {
const heading = headings[index];
if (!/open questions/i.test(heading.text)) continue;
let endLine = lineCount;
for (let cursor = index + 1; cursor < headings.length; cursor += 1) {
const candidate = headings[cursor];
if (candidate.level <= heading.level) {
endLine = candidate.line - 1;
break;
}
}
ranges.push([heading.line, endLine]);
}
return ranges;
}
function isReviewableLine(line, openQuestionRanges) {
const trimmed = line.text.trim();
if (!trimmed) return false;
if (trimmed.startsWith("#")) return false;
if (trimmed.startsWith("|---")) return false;
if (openQuestionRanges.some(([start, end]) => line.line >= start && line.line <= end))
return false;
if (/\bTBD\b|to be determined/i.test(trimmed)) return false;
return true;
}
function hydrateDefect(defect) {
const defectId = sha256Hex(fingerprint(defect)).slice(0, 12).toUpperCase();
return {
defect_id: `SQ-${defectId}`,
category: defect.category,
severity: defect.severity,
description: defect.description,
locations: defect.locations.slice().sort((left, right) => left.line_range[0] - right.line_range[0]),
suggested_resolution: defect.suggested_resolution,
affected_obligation_ids: null,
status: "new"
};
}
function fingerprint(defect) {
return JSON.stringify({
category: defect.category,
severity: defect.severity,
description: defect.description,
locations: defect.locations.map((location) => ({
section: location.section,
line_range: location.line_range,
text_excerpt: location.text_excerpt
})),
suggested_resolution: defect.suggested_resolution
});
}
function compareDefects(left, right) {
const severityDelta = SEVERITY_ORDER[left.severity] - SEVERITY_ORDER[right.severity];
if (severityDelta !== 0) return severityDelta;
const lineDelta = (left.locations[0]?.line_range[0] ?? 0) - (right.locations[0]?.line_range[0] ?? 0);
if (lineDelta !== 0) return lineDelta;
return left.defect_id.localeCompare(right.defect_id);
}
// src/compliance/obligation-extractor.ts
init_esm_shims();
function extractObligationIndex(options) {
const extractedAt = options.extracted_at ?? (/* @__PURE__ */ new Date()).toISOString();
const warnings = [];
const lines = splitLines(options.spec_markdown);
const headings = parseHeadings(lines);
const obligations = [];
const seenIds = /* @__PURE__ */ new Set();
const tables = extractRecognizedTables(lines);
for (const row of tables.rows) {
const sectionPath = buildHeadingPath(headings, row.line);
const { category } = classifySection(sectionPath);
const obligation = obligationFromTableRow(row, options.spec_file, sectionPath, category);
if (!obligation) continue;
if (seenIds.has(obligation.obligation_id)) {
warnings.push(
`Duplicate obligation_id "${obligation.obligation_id}" (table row at line ${row.line})`
);
continue;
}
seenIds.add(obligation.obligation_id);
obligations.push(obligation);
}
const sectionItemCount = /* @__PURE__ */ new Map();
for (const item of extractNumberedListItems(lines)) {
const sectionPath = buildHeadingPath(headings, item.line);
const { category } = classifySection(sectionPath);
if (category !== "acceptance" || !isAcceptanceListSection(sectionPath)) {
continue;
}
const count = (sectionItemCount.get(sectionPath) ?? 0) + 1;
sectionItemCount.set(sectionPath, count);
const generatedId = makeDeterministicGeneratedId(sectionPath, count);
if (seenIds.has(generatedId)) {
continue;
}
obligations.push({
obligation_id: generatedId,
category,
description: normalizeCell(item.text),
pass_criteria: null,
source_section: sectionPath,
source_line: item.line,
spec_file: options.spec_file,
affected_by_spec_defects: []
});
seenIds.add(generatedId);
}
for (const match of extractExplicitIds(lines, tables.table_lines)) {
if (seenIds.has(match.id)) continue;
const sectionPath = buildHeadingPath(headings, match.line);
const { category } = classifySection(sectionPath);
obligations.push({
obligation_id: match.id,
category,
description: normalizeCell(match.text),
pass_criteria: null,
source_section: sectionPath,
source_line: match.line,
spec_file: options.spec_file,
affected_by_spec_defects: []
});
seenIds.add(match.id);
}
if (obligations.length === 0) {
warnings.push("No recognizable structured obligations found in spec.");
}
obligations.sort((left, right) => left.obligation_id.localeCompare(right.obligation_id));
const linkedReview = options.spec_review ? attachSpecDefectsToObligations(options.spec_review, obligations) : null;
const obligationsWithDefects = obligations.map((obligation) => ({
...obligation,
affected_by_spec_defects: linkedReview === null ? [] : linkedReview.defects.filter((defect) => defect.affected_obligation_ids.includes(obligation.obligation_id)).map((defect) => defect.defect_id).sort((a, b) => a.localeCompare(b))
}));
return {
metadata: {
spec_file: options.spec_file,
spec_hash: sha256Hex(options.spec_markdown),
extracted_at: extractedAt,
obligation_count: obligations.length,
schema_version: COMPLIANCE_SCHEMA_VERSION,
warnings
},
obligations: obligationsWithDefects
};
}
function obligationFromTableRow(row, specFile, sectionPath, category) {
if (row.cells.length < 2) return null;
const normalizedCells = row.cells.map(normalizeCell);
const headerCells = row.header_cells.map((cell) => cell.toLowerCase().trim());
const [first, second, third, fourth] = normalizedCells;
const obligationId = first || null;
if (!obligationId) return null;
const descriptionColumn = selectDescriptionColumn(headerCells, normalizedCells);
const passCriteriaColumn = selectPassCriteriaColumn(headerCells, normalizedCells);
const description = firstNonEmpty(descriptionColumn, second, third) ?? "";
const passCriteria = firstNonEmpty(passCriteriaColumn, fourth);
return {
obligation_id: obligationId,
category,
description,
pass_criteria: passCriteria,
source_section: sectionPath,
source_line: row.line,
spec_file: specFile,
affected_by_spec_defects: []
};
}
function extractRecognizedTables(lines) {
const rows = [];
const tableLines = /* @__PURE__ */ new Set();
for (let index = 0; index < lines.length; index += 1) {
const headerLine = lines[index];
if (!isTableRow(headerLine)) continue;
const headerCells = parseTableRow(headerLine);
const isRecognized = headerLooksLikeObligationTable(headerCells);
if (!isRecognized) continue;
const separatorLine = lines[index + 1];
if (!separatorLine || !looksLikeTableSeparator(separatorLine)) continue;
tableLines.add(index + 1);
tableLines.add(index + 2);
index += 2;
while (index < lines.length) {
const line = lines[index];
if (!isTableRow(line)) break;
const cells = parseTableRow(line);
if (cells.every((cell) => cell.trim().length === 0)) {
index += 1;
continue;
}
rows.push({ cells, line: index + 1, header_cells: headerCells });
tableLines.add(index + 1);
index += 1;
}
index -= 1;
}
return { rows, table_lines: tableLines };
}
function headerLooksLikeObligationTable(cells) {
const normalized = cells.map((cell) => cell.toLowerCase().trim());
const joined = normalized.join(" | ");
const hasId = /test id|obligation|id|#/.test(joined);
const hasConditionOrCriterion = /condition|scenario|criterion|requirement|case|description|method/.test(joined);
const hasPass = /pass criteria|expected|assert|measurement|result|outcome/.test(joined);
return hasId && hasConditionOrCriterion && hasPass;
}
function isTableRow(line) {
const trimmed = line.trim();
return trimmed.startsWith("|") && trimmed.endsWith("|");
}
function looksLikeTableSeparator(line) {
const trimmed = line.trim();
return isTableRow(trimmed) && /^\|(?:\s*:?-+:?\s*\|)+$/.test(trimmed);
}
function parseTableRow(line) {
const inner = line.trim().slice(1, -1);
return inner.split("|").map((cell) => cell.trim());
}
function extractNumberedListItems(lines) {
const items = [];
let currentIndex = 0;
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
const line = lines[lineIndex];
const match = /^\s*(\d+)\.\s+(.*\S)\s*$/.exec(line);
if (!match) continue;
currentIndex += 1;
items.push({ index: currentIndex, line: lineIndex + 1, text: match[2] });
}
return items;
}
function extractExplicitIds(lines, excludedLines) {
const matches = [];
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
if (excludedLines.has(lineIndex + 1)) continue;
const line = lines[lineIndex];
const id = extractLeadingExplicitId(line);
if (!id) continue;
matches.push({ id, line: lineIndex + 1, text: line });
}
return matches;
}
function isAcceptanceListSection(sectionPath) {
return /(?:^|> )(?:\d+(?:\.\d+)*\.?\s*)?(acceptance criteria|definition of done|ac)\b/i.test(
sectionPath
);
}
function selectDescriptionColumn(headerCells, rowCells) {
const descriptionIndex = headerCells.findIndex(
(cell) => /condition|scenario|criterion|requirement|case|description|method/.test(cell)
);
if (descriptionIndex > 0) {
return rowCells[descriptionIndex] || null;
}
return rowCells[1] || rowCells[2] || null;
}
function selectPassCriteriaColumn(headerCells, rowCells) {
const passIndex = headerCells.findIndex(
(cell) => /pass criteria|expected|assert|measurement|result|outcome/.test(cell)
);
if (passIndex > 0) {
return rowCells[passIndex] || null;
}
return rowCells[2] || rowCells[3] || null;
}
function extractLeadingExplicitId(line) {
const match = /^\s*(?:#{1,6}\s+|[-*+]\s+)?(?:\*\*)?(?<id>(?:FR|NFR)-\d+(?:\.\d+)?(?:-T\d+)?|AC-(?:TRACK-[A-Za-z0-9-]+|\d+)|EC-\d+(?:-T\d+)?)(?:\*\*)?(?=\b|:|\s)/.exec(
line
);
if (match?.groups?.id) {
return match.groups.id;
}
if (line.trim().startsWith("|")) {
const rowMatch = /^\|\s*(?<id>(?:FR|NFR)-\d+(?:\.\d+)?(?:-T\d+)?|AC-\d+|EC-\d+(?:-T\d+)?)\s*\|/.exec(line);
if (rowMatch?.groups?.id) {
return rowMatch.groups.id;
}
}
return null;
}
function firstNonEmpty(...values) {
for (const value of values) {
if (value && value.trim().length > 0) {
return value;
}
}
return null;
}
// src/core/framework-enabled.ts
init_esm_shims();
import { existsSync } from "fs";
import { join as join2 } from "path";
var ENV_TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
function isEnvDisabled(env = process.env) {
const raw = env.PAQAD_DISABLED;
return typeof raw === "string" && ENV_TRUTHY.has(raw.trim().toLowerCase());
}
function isManagedEnv(env = process.env) {
const raw = env.PAQAD_MANAGED;
return typeof raw === "string" && ENV_TRUTHY.has(raw.trim().toLowerCase());
}
function isFrameworkEnabled(profile, env = process.env) {
if (isEnvDisabled(env)) {
return false;
}
return profile?.paqad?.enabled !== false;
}
function isFrameworkEnabledForRoot(projectRoot, env = process.env) {
if (isEnvDisabled(env)) {
return false;
}
return !configSaysPaqadDisabled(projectRoot, env);
}
function isFrameworkDisabledForRoot(projectRoot, env = process.env) {
return !isFrameworkEnabledForRoot(projectRoot, env);
}
function setFrameworkEnabled(projectRoot, enabled) {
if (!existsSync(join2(projectRoot, PATHS.PROJECT_PROFILE))) {
throw new Error(
"paqad is not onboarded in this project (no .paqad/project-profile.yaml). Run `paqad-ai onboard` first."
);
}
const config_path = setConfigValue(projectRoot, "paqad_enable", enabled ? "true" : "false");
return { enabled, config_path };
}
// src/planning/decision-evidence.ts
init_esm_shims();
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
import { join as join6, relative as relative2 } from "path";
// src/code-knowledge/store.ts
init_esm_shims();
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
import { dirname as dirname2, join as join3 } from "path";
// src/code-knowledge/schema.ts
init_esm_shims();
var import_ajv = __toESM(require_ajv(), 1);
// src/validators/schemas/code-knowledge.schema.json
var code_knowledge_schema_default = {
$schema: "http://json-schema.org/draft-07/schema#",
$id: "paqad://schemas/code-knowledge.json",
title: "Code-knowledge index (issue #353)",
type: "object",
additionalProperties: false,
required: [
"schema_version",
"header",
"symbols",
"files",
"import_edges",
"reference_edges",
"dependencies"
],
properties: {
schema_version: { type: "integer", const: 1 },
header: {
type: "object",
additionalProperties: false,
required: ["generated_at", "branch", "head_commit", "schema_version", "entry_point_globs"],
properties: {
generated_at: { type: "string", minLength: 1 },
branch: { type: ["string", "null"] },
head_commit: { type: ["string", "null"] },
schema_version: { type: "integer", const: 1 },
entry_point_globs: { type: "array", items: { type: "string" } }
}
},
symbols: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: [
"name",
"kind",
"file",
"line",
"signature",
"exported",
"module_slug",
"extraction_tier",
"caller_count",
"orphan"
],
properties: {
name: { type: "string", minLength: 1 },
kind: { enum: ["function", "class", "component", "const", "type"] },
file: { type: "string", minLength: 1 },
line: { type: "integer", minimum: 1 },
signature: { type: "string" },
exported: { type: "boolean" },
module_slug: { type: ["string", "null"] },
extraction_tier: { enum: ["regex", "ast"] },
caller_count: { type: "integer", minimum: 0 },
orphan: { type: "boolean" }
}
}
},
files: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: ["path", "caller_count", "orphan", "entry_point"],
properties: {
path: { type: "string", minLength: 1 },
caller_count: { type: "integer", minimum: 0 },
orphan: { type: "boolean" },
entry_point: { type: "boolean" }
}
}
},
import_edges: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: ["from", "to"],
properties: {
from: { type: "string", minLength: 1 },
to: { type: "string", minLength: 1 }
}
}
},
reference_edges: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: ["from", "to", "symbol"],
properties: {
from: { type: "string", minLength: 1 },
to: { type: "string", minLength: 1 },
symbol: { type: "string", minLength: 1 }
}
}
},
dependencies: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: ["name", "ecosystem", "imported"],
properties: {
name: { type: "string", minLength: 1 },
ecosystem: { type: "string", minLength: 1 },
imported: { type: "boolean" }
}
}
}
}
};
// src/code-knowledge/schema.ts
var ajv = new import_ajv.default({ allErrors: true, allowUnionTypes: true });
var compiled;
function validator() {
if (!compiled) {
compiled = ajv.compile(code_knowledge_schema_default);
}
return compiled;
}
function validateCodeKnowledgeIndex(data) {
const validate = validator();
if (validate(data)) {
return { valid: true, errors: [] };
}
const rawErrors = validate.errors ?? [];
const errors = rawErrors.map((error) => {
const path = error.instancePath.length > 0 ? error.instancePath : "/";
const message = error.message ?? "invalid";
return `${path} ${message}`.trim();
});
return { valid: false, errors };
}
// src/code-knowledge/store.ts
function codeKnowledgeIndexPath(projectRoot) {
return join3(projectRoot, PATHS.CODE_KNOWLEDGE_INDEX);
}
function writeCodeKnowledgeIndex(projectRoot, index) {
const target = codeKnowledgeIndexPath(projectRoot);
mkdirSync(dirname2(target), { recursive: true });
const tmp = `${target}.tmp`;
writeFileSync(tmp, JSON.stringify(index, null, 2) + "\n", "utf8");
renameSync(tmp, target);
return target;
}
function readCodeKnowledgeIndex(projectRoot) {
const target = codeKnowledgeIndexPath(projectRoot);
try {
const parsed = JSON.parse(readFileSync(target, "utf8"));
return validateCodeKnowledgeIndex(parsed).valid ? parsed : null;
} catch {
return null;
}
}
// src/planning/decision-evidence-arm.ts
init_esm_shims();
import { statSync } from "fs";
import { join as join5 } from "path";
// src/duplication/decisions.ts
init_esm_shims();
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
import { join as join4 } from "path";
var DUPLICATION_DECISION_CATEGORY = "create-vs-reuse";
function findingKey(finding) {
return `${finding.file}:${finding.line_range.start}:${finding.matched_file}`;
}
function findingToken(key) {
return `[paqad-duplication ${key}]`;
}
function buildDuplicationDecisionContext(finding) {
const percent = Math.round(finding.similarity * 100);
return `Duplication finding accepted as a new copy.
- file: ${finding.file}:${finding.line_range.start}-${finding.line_range.end}
- similarity: ${percent}% of ${finding.matched_file}
- callers of the existing code: ${finding.matched_callers}
${findingToken(findingKey(finding))}`;
}
function applyResolvedDecisions(projectRoot, findings) {
if (findings.length === 0) {
return [];
}
const keys = findings.map(findingKey);
const resolvedDir = join4(projectRoot, PATHS.DECISIONS_RESOLVED_DIR);
if (!existsSync2(resolvedDir)) {
return [];
}
const results = [];
let files;
try {
files = readdirSync(resolvedDir).filter((name) => name.endsWith(".json"));
} catch {
return [];
}
for (const file of files.sort()) {
const packet = readPacket(join4(resolvedDir, file));
if (!packet || packet.category !== DUPLICATION_DECISION_CATEGORY) {
continue;
}
const covered = keys.filter((key) => packet.context.includes(findingToken(key)));
if (covered.length > 0) {
results.push({ decisionId: packet.id, coveredFindingKeys: covered });
}
}
return results;
}
function readPacket(path) {
try {
return JSON.parse(readFileSync2(path, "utf8"));
} catch {
return null;
}
}
// src/planning/decision-arm-config.ts
init_esm_shims();
var DECISION_ARM_MODES = ["off", "warn", "strict"];
var DEFAULT_DECISION_ARM_MODE = "warn";
var DEFAULT_PLAN_THRESHOLD = 0.85;
var DEFAULT_MAX_PER_CHANGE = 1;
function resolveDecisionArmConfig(projectRoot, env = process.env) {
return {
mode: resolveDecisionArmMode(projectRoot, env),
planThreshold: resolveNumericConfig(
projectRoot,
env,
"decision_arm_plan_threshold",
DEFAULT_PLAN_THRESHOLD,
(value) => value > 0 && value <= 1
),
maxPerChange: resolveNumericConfig(
projectRoot,
env,
"decision_arm_max_per_change",
DEFAULT_MAX_PER_CHANGE,
(value) => Number.isInteger(value) && value >= 0
)
};
}
function resolveDecisionArmMode(projectRoot, env = process.env) {
return resolveFlooredMode(
{
team: readConfigsDir(projectRoot).merged.get("decision_arm_mode"),
local: readDotConfig(projectRoot).get("decision_arm_mode"),
env: envConfigValue(env, "decision_arm_mode")
},
DECISION_ARM_MODES,
DEFAULT_DECISION_ARM_MODE
);
}
// src/planning/symbol-similarity.ts
init_esm_shims();
var HEAD_BONUS = 0.1;
var TAIL_BONUS = 0.1;
var MODULE_BONUS = 0.05;
function tokenizeSymbolName(name) {
return name.replace(/([a-z0-9])([A-Z])/gu, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/gu, "$1 $2").split(/[^A-Za-z0-9]+/u).filter((token) => token.length > 0).map((token) => token.toLowerCase());
}
function tokenDice(left, right) {
if (left.length === 0 || right.length === 0) {
return 0;
}
const rightSet = new Set(right);
const shared = new Set(left.filter((token) => rightSet.has(token))).size;
return 2 * shared / (new Set(left).size + new Set(right).size);
}
function editSimilarity(left, right) {
const longest = Math.max(left.length, right.length);
if (longest === 0) {
return 0;
}
return 1 - levenshtein(left, right, longest) / longest;
}
function symbolNameSimilarity(left, right, options = {}) {
if (left === right) {
return 1;
}
const leftTokens = tokenizeSymbolName(left);
const rightTokens = tokenizeSymbolName(right);
let tokenScore = tokenDice(leftTokens, rightTokens);
if (tokenScore > 0) {
if (leftTokens[0] === rightTokens[0]) {
tokenScore += HEAD_BONUS;
}
if (leftTokens[leftTokens.length - 1] === rightTokens[rightTokens.length - 1]) {
tokenScore += TAIL_BONUS;
}
}
const base = Math.max(tokenScore, editSimilarity(left, right));
const withModule = base + (options.sameModule === true ? MODULE_BONUS : 0);
return Number(Math.min(1, Math.max(0, withModule)).toFixed(2));
}
// src/planning/decision-evidence-arm.ts
var RECOMMEND_REUSE_MIN_CALLERS = 3;
var CREATE_NEW_OPTION_KEY = "create-new";
function reuseOptionKey(symbol) {
return `reuse:${symbol}`;
}
function forkToken(forkKey) {
return `[paqad-arm ${forkKey}]`;
}
function changeToken(changeKey) {
return `[paqad-arm-change ${changeKey}]`;
}
function planForkKey(newName, existingSymbol) {
return `plan:${newName}~${existingSymbol}`;
}
function duplicationForkKey(finding) {
return `dup:${findingKey(finding)}`;
}
function buildReuseForkEvidence(input) {
const evidence = {
file: input.file,
callers: input.callers,
similarity: Number(Math.min(1, Math.max(0, input.similarity)).toFixed(2))
};
try {
evidence.last_modified = statSync(join5(input.projectRoot, input.file)).mtime.toISOString();
} catch {
evidence.evidence_partial = true;
}
return evidence;
}
function findPlanTimeReuseForks(input) {
const index = input.index !== void 0 ? input.index : readCodeKnowledgeIndex(input.projectRoot);
if (!index || input.constructs.length === 0) {
return [];
}
const forks = [];
for (const planned of input.constructs) {
const best = closestSymbol(index.symbols, planned);
if (best === null || best.similarity < input.threshold) {
continue;
}
forks.push({
forkKey: planForkKey(planned.name, best.symbol.name),
newName: planned.name,
justification: planned.justification ?? "",
existing: best.symbol,
similarity: best.similarity
});
}
return forks.sort((a, b) => b.similarity - a.similarity);
}
function closestSymbol(symbols, planned) {
let best = null;
for (const symbol of symbols) {
if (symbol.name === planned.name) {
continue;
}
const similarity = symbolNameSimilarity(planned.name, symbol.name, {
sameModule: planned.module !== void 0 && planned.module !== null && planned.module === symbol.module_slug
});
if (best === null || similarity > best.similarity) {
best = { symbol, similarity };
}
}
return best;
}
var EMPTY_RESULT = { minted: [], warnings: [], reusedDecisions: [] };
function armDecisionFromPlan(input) {
return safely(() => {
const config = input.config ?? resolveDecisionArmConfig(input.projectRoot, input.env);
if (config.mode === "off") {
return EMPTY_RESULT;
}
const forks = findPlanTimeReuseForks({
projectRoot: input.projectRoot,
constructs: input.constructs,
threshold: config.planThreshold
});
return mintForks(
input,
config,
forks.map((fork) => toMintable(input.projectRoot, fork))
);
});
}
function armDecisionFromDuplicationFinding(input) {
return safely(() => {
const config = input.config ?? resolveDecisionArmConfig(input.projectRoot, input.env);
if (config.mode === "off") {
return EMPTY_RESULT;
}
return mintForks(input, config, [toMintableFinding(input.projectRoot, input.finding)]);
});
}
function toMintable(projectRoot, fork) {
const evidence = buildReuseForkEvidence({
projectRoot,
file: fork.existing.file,
callers: fork.existing.caller_count,
similarity: fork.similarity
});
const percent = Math.round(fork.similarity * 100);
return {
forkKey: fork.forkKey,
title: `Reuse ${fork.existing.name}, or build ${fork.newName}?`,
context: `The plan introduces ${fork.newName}, which is ${percent}% similar to existing ${fork.existing.name}.
- file: ${fork.existing.file}:${fork.existing.line}
- callers of the existing code: ${fork.existing.caller_count}
- the plan's justification: ${fork.justification || "(none given)"}`,
options: [
{
option_key: reuseOptionKey(fork.existing.name),
label: `Reuse ${fork.existing.name} (${fork.existing.file})`,
evidence
},
{
option_key: CREATE_NEW_OPTION_KEY,
label: `Build ${fork.newName} anyway \u2014 ${fork.justification || "no justification given"}`
}
],
recommendation: fork.existing.caller_count >= RECOMMEND_REUSE_MIN_CALLERS ? reuseOptionKey(fork.existing.name) : null,
score: fork.similarity,
warning: `${fork.newName} is ${percent}% similar to existing ${fork.existing.name} (${fork.existing.file}, ${fork.existing.caller_count} callers) \u2014 reuse it, or say why not`
};
}
function toMintableFinding(projectRoot, finding) {
const matchedName = finding.matched_symbol ?? finding.matched_file;
const evidence = buildReuseForkEvidence({
projectRoot,
file: finding.matched_file,
callers: finding.matched_callers,
similarity: finding.similarity
});
return {
forkKey: duplicationForkKey(finding),
title: `Reuse ${matchedName}, or keep the new copy in ${finding.file}?`,
context: buildDuplicationDecisionContext(finding),
options: [
{
option_key: reuseOptionKey(matchedName),
label: `Reuse ${matchedName} (${finding.matched_file})`,
evidence
},
{
option_key: CREATE_NEW_OPTION_KEY,
label: `Keep the new copy in ${finding.file}`
}
],
recommendation: finding.matched_callers >= RECOMMEND_REUSE_MIN_CALLERS ? reuseOptionKey(matchedName) : null,
score: finding.similarity,
warning: finding.message
};
}
function mintForks(input, config, forks) {
if (forks.length === 0) {
return EMPTY_RESULT;
}
const stored = readContractDecisions(input.projectRoot);
const result = { minted: [], warnings: [], reusedDecisions: [] };
let openedForChange = stored.filter(
(row) => row.packet.context.includes(changeToken(input.changeKey))
).length;
const ranked = [...forks].sort((a, b) => b.score - a.score);
for (const fork of ranked) {
const token = forkToken(fork.forkKey);
const prior = stored.find((row) => row.packet.context.includes(token));
if (prior !== void 0) {
if (prior.status === "resolved") {
recordDecisionReuse(
input.projectRoot,
{
decisionId: prior.packet.id,
category: prior.packet.category,
matchKind: "exact",
note: `evidence-armed fork ${fork.forkKey}`
},
{ sessionId: input.sessionId ?? null }
);
result.reusedDecisions.push(`reused_decision:${prior.packet.id}`);
}
continue;
}
if (config.mode !== "strict" || openedForChange >= config.maxPerChange) {
result.warnings.push(fork.warning);
continue;
}
const created = createPendingDecision(input.projectRoot, {
category: DUPLICATION_DECISION_CATEGORY,
title: fork.title,
context: `${fork.context}
${token}
${changeToken(input.changeKey)}`,
options: fork.options,
recommendation: fork.recommendation,
origin: "evidence-armed",
change: featureChangeKey(input.changeKey)
});
if (created.reused) {
result.reusedDecisions.push(`reused_decision:${created.id}`);
continue;
}
result.minted.push(created.id);
openedForChange += 1;
}
return result;
}
function safely(run) {
try {
return run();
} catch {
return EMPTY_RESULT;
}
}
// src/planning/decision-evidence.ts
var MIN_SIMILARITY = 0.01;
var MAX_SIMILARITY = 0.99;
function assembleDecisionEvidence(input) {
const absolutePath = join6(input.projectRoot, input.file);
const fileExists = existsSync3(absolutePath);
const indexedCallers = indexedCallerCount(input.projectRoot, input.file);
if (indexedCallers !== null) {
const evidence2 = buildReuseForkEvidence({
projectRoot: input.projectRoot,
file: input.file,
callers: indexedCallers,
similarity: clampSimilarity(
input.similarity ?? defaultSimilarityFor(input.category, fileExists, indexedCallers)
)
});
const ruleMatch2 = findSupportingRule(input.projectRoot, input.file);
if (ruleMatch2) {
evidence2.rule_match = ruleMatch2;
}
return evidence2;
}
const callers = fileExists ? countFileReferences(input.projectRoot, input.file) : 0;
const similarity = clampSimilarity(
input.similarity ?? defaultSimilarityFor(input.category, fileExists, callers)
);
const ruleMatch = findSupportingRule(input.projectRoot, input.file);
const evidence = {
file: input.file,
callers,
similarity
};
if (ruleMatch) {
evidence.rule_match = ruleMatch;
}
if (fileExists) {
evidence.last_modified = statSync2(absolutePath).mtime.toISOString();
} else {
evidence.evidence_partial = true;
}
return evidence;
}
function countFileReferences(projectRoot, file) {
const absoluteTarget = join6(projectRoot, file);
if (!existsSync3(absoluteTarget)) {
return 0;
}
const relativePath = normalizePath(relative2(projectRoot, absoluteTarget));
const baseName = relativePath.split("/").pop() ?? relativePath;
const stem = baseName.replace(/\.[^.]+$/, "");
const matcher = new RegExp(`\\b${escapeRegExp(stem)}\\b|${escapeRegExp(relativePath)}`, "g");
let count = 0;
for (const candidate of walkTextFiles(projectRoot)) {
if (normalizePath(candidate) === relativePath) {
count += 1;
continue;
}
const content = safeReadFile(join6(projectRoot, candidate));
if (matcher.test(content)) {
count += 1;
}
}
return count;
}
function defaultSimilarityFor(category, fileExists, callers) {
if (!fileExists) {
return category === "workflow-or-tool" ? 0.44 : 0.38;
}
const callerBoost = callers >= 3 ? 0.06 : callers > 0 ? 0.03 : 0;
switch (category) {
case "component-reuse":
case "create-vs-reuse":
return Number((0.86 + callerBoost).toFixed(2));
/* v8 ignore start -- individual category branches are exercised by integration; unit tests only hit the top two */
case "shared-abstraction":
return Number((0.78 + callerBoost).toFixed(2));
case "ux-pattern":
return Number((0.82 + callerBoost).toFixed(2));
case "architecture-path":
return Number((0.74 + callerBoost).toFixed(2));
case "workflow-or-tool":
return Number((0.7 + callerBoost).toFixed(2));
/* v8 ignore stop */
case "intake.requirement":
case "intake.confirm_auto_resolution":
case "intake.write_back":
case "delivery.open_pr":
case "delivery.ci_red":
case "spec.change":
case "spec.contradiction":
case "spec.expert_conflict":
case "fix.proof_method":
case "test.flaky_judgement":
case "finding.triage":
case "quality.ratchet_exception":
case "analytics.provider_version_mismatch":
case "analytics.taxonomy_violation":
case "analytics.pii_consent":
case "analytics.no_provider_flag":
case "analytics.architecture_conflict":
case "analytics.new_event":
case "authoring.overlap":
case "authoring.risky_script":
case "authoring.agent_permission":
case "authoring.intake_gap":
case "authoring.improvement":
case "authoring.retirement":
case "authoring.override":
case "rules.proposal":
return 0.5;
}
}
function indexedCallerCount(projectRoot, file) {
const index = readCodeKnowledgeIndex(projectRoot);
if (!index) {
return null;
}
const normalized = normalizePath(file);
return index.files.find((entry) => entry.path === normalized)?.caller_count ?? null;
}
function findSupportingRule(projectRoot, file) {
const compiledRules = readCompiledRulesSync(projectRoot);
if (!compiledRules) {
return void 0;
}
return compiledRules.rules.find(
(rule) => rule.trigger_patterns.some((pattern) => matchesGlobish(file, pattern))
)?.rule_id;
}
function readCompiledRulesSync(projectRoot) {
try {
return JSON.parse(readFileSync3(join6(projectRoot, PATHS.COMPILED_RULES), "utf8"));
} catch {
return null;
}
}
function walkTextFiles(projectRoot, currentDir = "") {
const absoluteDir = join6(projectRoot, currentDir);
let entries;
try {
entries = readdirSync2(absoluteDir);
} catch {
return [];
}
const files = [];
for (const entry of entries) {
if (entry === "node_modules" || entry === ".git" || entry === ".paqad") {
continue;
}
const relativeEntry = currentDir ? `${currentDir}/${entry}` : entry;
const absoluteEntry = join6(projectRoot, relativeEntry);
try {
const stat = statSync2(absoluteEntry);
if (stat.isDirectory()) {
files.push(...walkTextFiles(projectRoot, relativeEntry));
} else if (/\.(ts|tsx|js|jsx|md|json|yaml|yml|txt|mdc)$/.test(entry)) {
files.push(normalizePath(relativeEntry));
}
} catch {
continue;
}
}
return files;
}
function safeReadFile(path) {
try {
return readFileSync3(path, "utf8");
} catch {
return "";
}
}
function clampSimilarity(value) {
return Math.min(MAX_SIMILARITY, Math.max(MIN_SIMILARITY, Number(value.toFixed(2))));
}
function normalizePath(value) {
return value.replace(/\\/g, "/");
}
function matchesGlobish(value, pattern) {
const normalizedValue = normalizePath(value);
const normalizedPattern = normalizePath(pattern);
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, ""));
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// src/planning/decision-packet-builder.ts
init_esm_shims();
function buildDecisionPacket(input) {
const primaryTouch = input.context.current_slice.touches[0] ?? "src/unknown.ts";
const optionSet = decisionOptionsForCategory(input.projectRoot, input.category, primaryTouch);
const now = /* @__PURE__ */ new Date();
const linkedRequirements = input.context.current_slice.covers.filter(
(cover) => input.manifest.requirement_graph.some((requirement) => requirement.id === cover)
);
const recommendation = chooseRecommendation(optionSet.options);
const ttlDays = ttlDaysFor(input.projectRoot, input.category);
return {
decision_id: input.decisionId,
fingerprint: computeDecisionFingerprint({
category: input.category,
question: decisionQuestionForCategory(input.category),
option_keys: optionSet.options.map((option) => option.option_key),
repo_state: {
active_capabilities: ["coding"],
stack: input.context.manifest_header.classification.stack,
packs: input.context.manifest_header.classification.affected_modules
}
}),
category: input.category,
question: decisionQuestionForCategory(input.category),
context: `${input.context.current_slice.goal} This fork affects ${primaryTouch}. Choose the path before implementation continues.`,
options: optionSet.options,
recommendation,
recommendation_reason: recommendationReasonFor(recommendation, optionSet.options),
confidence: computePacketConfidence(
optionSet.options,
recommendation,
input.detectorConfidence
),
requested_by: input.requestedBy,
task_session_id: input.taskSessionId,
linked_requirements: linkedRequirements,
linked_slice_id: input.context.current_slice.slice_id,
created_at: now.toISOString(),
status: "pending",
ttl_until: new Date(now.getTime() + ttlDays * 24 * 60 * 60 * 1e3).toISOString(),
invalidation_watch: invalidationWatchFor(
input.category,
primaryTouch,
input.context.current_slice.touches,
optionSet.options
)
};
}
function computePacketConfidence(options, recommendation, detectorConfidence) {
const recommendedOption = options.find((option) => option.option_key === recommendation) ?? options[0];
const similarity = recommendedOption?.evidence.similarity ?? detectorConfidence;
const callerBonus = (recommendedOption?.evidence.callers ?? 0) > 0 ? 0.05 : 0;
const ruleBonus = recommendedOption?.evidence.rule_match ? 0.05 : 0;
const score = similarity * 0.75 + detectorConfidence * 0.15 + callerBonus + ruleBonus;
return Number(Math.min(0.99, Math.max(0, score)).toFixed(2));
}
function decisionQuestionForCategory(category) {
switch (category) {
case "component-reuse":
return "Reuse the component or make new?";
case "create-vs-reuse":
return "Use what exists or make new?";
case "shared-abstraction":
return "Keep this local or share it?";
case "ux-pattern":
return "Stay with this pattern?";
case "architecture-path":
return "Which path should we take?";
case "workflow-or-tool":
return "Which workflow fits best?";
case "intake.requirement":
return "How should we pin this open question from the refined ticket?";
case "intake.confirm_auto_resolution":
return "We resolved these decisions from priors and rules \u2014 accept or override?";
case "intake.write_back":
return "Update the source ticket with the refined description and acceptance criteria?";
case "delivery.open_pr":
return "Open a pull request now (yes / draft / no)?";
case "delivery.ci_red":
return "CI is red \u2014 retry the gate, override and continue, or abandon delivery?";
case "spec.change":
return "The goal changed mid-build \u2014 update the frozen spec and re-freeze?";
case "spec.contradiction":
return "Work conflicts with the frozen spec \u2014 fix the code or change the spec?";
case "spec.expert_conflict":
return "Two experts disagree about the same target \u2014 which claim should the spec take?";
case "fix.proof_method":
return "This problem cannot be auto-checked \u2014 how should we confirm it is fixed?";
case "test.flaky_judgement":
return "Is this a flaky test or a rare real fault?";
case "finding.triage":
return "Which pile does this finding belong in?";
case "quality.ratchet_exception":
return "A quality measure would worsen \u2014 approve a legitimate exception or hold the line?";
case "analytics.provider_version_mismatch":
return "I'm not sure which analytics tool (or version) this project uses \u2014 which is right?";
case "analytics.taxonomy_violation":
return "This event name breaks your naming convention \u2014 keep mine or rename?";
case "analytics.pii_consent":
return "This property looks like personal data \u2014 I won't auto-track it. Track it anyway?";
case "analytics.no_provider_flag":
return "Analytics is on but no provider is wired up \u2014 set one up, or stay dormant?";
case "analytics.architecture_conflict":
return "Your module docs say tracking belongs elsewhere than this change \u2014 where should it go?";
case "analytics.new_event":
return "This adds a new tracked event \u2014 approve the name, rename it, or skip tracking?";
case "authoring.overlap":
return "This new skill overlaps one you already have. Extend that one, or create a new skill?";
case "authoring.risky_script":
return "A script in this skill reaches the network, installs packages or deletes files. Allow it?";
case "authoring.agent_permission":
return "This agent asks for risky powers (bypassPermissions, network, package install, delete rights, or more than the tool limit). Allow these powers?";
case "authoring.intake_gap":
return "Only you can answer this part of what the new skill is for. What should it be?";
case "authoring.improvement":
return "A skill in use could learn this correction. Add it to the skill?";
case "authoring.retirement":
return "The agent now does this right without the skill. Retire the skill?";
case "authoring.override":
return "This skill skipped one of its checks. Add it unverified, kept in this project?";
case "rules.proposal":
return "Sign off the proposed rule changes?";
}
}
function decisionOptionsForCategory(projectRoot, category, targetFile) {
const targetDir = targetFile.replace(/\/[^/]+$/, "");
const extension = targetFile.includes(".") ? targetFile.slice(targetFile.lastIndexOf(".")) : ".ts";
const newFile = `${targetDir}/new-${targetFile.split("/").pop() ?? `option${extension}`}`;
const sharedFile = `${targetDir}/shared-${targetFile.split("/").pop() ?? `option${extension}`}`;
switch (category) {
case "shared-abstraction":
return {
options: [
buildOption(projectRoot, category, {
option_key: "keep-local",
label: "Keep it local",
one_line_preview: `If you pick this, we will keep the work in ${targetFile}.`,
trade_off: "You give up: one shared place for later reuse.",
technical_detail: `The first implementation stays in ${targetFile} and avoids new shared surface area.`,
file: targetFile,
similarity: 0.79
}),
buildOption(projectRoot, category, {
option_key: "extract-shared",
label: "Extract shared helper",
one_line_preview: `If you pick this, we will create ${sharedFile}.`,
trade_off: "You give up: the quickest path for this slice.",
technical_detail: `A shared helper in ${sharedFile} makes later reuse easier but broadens the change now.`,
file: sharedFile,
similarity: 0.41
})
]
};
case "architecture-path":
return {
options: [
buildOption(projectRoot, category, {
option_key: "keep-current-path",
label: "Keep current path",
one_line_preview: `If you pick this, we will update ${targetFile}.`,
trade_off: "You give up: a broader cleanup right now.",
technical_detail: "This path keeps implementation close to the current slice boundary.",
file: targetFile,
similarity: 0.74
}),
buildOption(projectRoot, category, {
option_key: "take-new-path",
label: "Take new path",
one_line_preview: `If you pick this, we will create ${newFile}.`,
trade_off: "You give up: the smallest possible diff.",
technical_detail: "This path opens a new implementation route and needs more follow-up.",
file: newFile,
similarity: 0.46
})
]
};
case "workflow-or-tool":
return {
options: [
buildOption(projectRoot, category, {
option_key: "use-current-workflow",
label: "Use current workflow",
one_line_preview: `If you pick this, we will keep the work inside ${targetFile}.`,
trade_off: "You give up: trying a brand new tool path.",
technical_detail: "This keeps the change aligned with the current workflow in the repo.",
file: targetFile,
similarity: 0.71
}),
buildOption(projectRoot, category, {
option_key: "switch-workflow",
label: "Switch workflow now",
one_line_preview: `If you pick this, we will introduce ${newFile}.`,
trade_off: "You give up: the simpler path for this task.",
technical_detail: "This introduces a new workflow artifact and changes future execution.",
file: newFile,
similarity: 0.44
})
]
};
case "ux-pattern":
return {
options: [
buildOption(projectRoot, category, {
option_key: "reuse-current-pattern",
label: "Reuse current pattern",
one_line_preview: `If you pick this, we will update ${targetFile}.`,
trade_off: "You give up: a fresh interaction pattern.",
technical_detail: "The current pattern already appears near this change.",
file: targetFile,
similarity: 0.83
}),
buildOption(projectRoot, category, {
option_key: "make-new-pattern",
label: "Make new pattern",
one_line_preview: `If you pick this, we will create ${newFile}.`,
trade_off: "You give up: the safest path for consistency.",
technical_detail: "A new pattern gives flexibility now but increases UX drift risk.",
file: newFile,
similarity: 0.47
})
]
};
case "component-reuse":
case "create-vs-reuse":
return {
options: [
buildOption(projectRoot, category, {
option_key: "reuse-existing",
label: "Reuse what exists",
one_line_preview: `If you pick this, we will update ${targetFile}.`,
trade_off: "You give up: a blank-slate implementation.",
technical_detail: `Reusing ${targetFile} keeps behavior aligned with the existing project structure.`,
file: targetFile,
similarity: 0.91
}),
buildOption(projectRoot, category, {
option_key: "make-new",
label: "Make a new one",
one_line_preview: `If you pick this, we will create ${newFile}.`,
trade_off: "You give up: the shared path that already exists.",
technical_detail: `Creating ${newFile} gives isolation but increases maintenance and consistency risk.`,
file: newFile,
similarity: 0.42
})
]
};
case "intake.requirement":
case "intake.confirm_auto_resolution":
case "intake.write_back":
case "delivery.open_pr":
case "delivery.ci_red":
case "spec.change":
case "spec.contradiction":
case "spec.expert_conflict":
case "fix.proof_method":
case "test.flaky_judgement":
case "finding.triage":
case "quality.ratchet_exception":
case "analytics.provider_version_mismatch":
case "analytics.taxonomy_violation":
case "analytics.pii_consent":
case "analytics.no_provider_flag":
case "analytics.architecture_conflict":
case "analytics.new_event":
case "authoring.overlap":
case "authoring.risky_script":
case "authoring.agent_permission":
case "authoring.intake_gap":
case "authoring.improvement":
case "authoring.retirement":
case "authoring.override":
case "rules.proposal":
return { options: [] };
}
}
function selectViableDecisionOptions(projectRoot, options) {
const floor = readProjectProfile(projectRoot)?.custom?.decisions?.ask_threshold === "permissive" ? 0.4 : 0.55;
const viable = options.filter(
(option) => option.evidence.similarity === void 0 || option.evidence.similarity >= floor
);
return viable.length > 0 ? viable : options;
}
function buildOption(projectRoot, category, input) {
return {
option_key: input.option_key,
label: input.label,
one_line_preview: input.one_line_preview,
trade_off: input.trade_off,
technical_detail: input.technical_detail,
evidence: assembleDecisionEvidence({
projectRoot,
file: input.file,
category,
similarity: input.similarity
})
};
}
function chooseRecommendation(options) {
const ranked = [...options].sort((left, right) => scoreOption(right) - scoreOption(left));
return ranked[0]?.option_key ?? null;
}
function recommendationReasonFor(recommendation, options) {
const option = options.find((entry) => entry.option_key === recommendation);
if (!option) {
return void 0;
}
return option.evidence.rule_match ? "A project rule already points to this path." : (option.evidence.callers ?? 0) > 0 ? "This path already matches the way the repo works today." : (
/* v8 ignore stop */
"This is the safer and cheaper path for the first change."
);
}
function scoreOption(option) {
return (option.evidence.similarity ?? 0) + ((option.evidence.callers ?? 0) > 0 ? 0.05 : 0) + (option.evidence.rule_match ? 0.1 : 0);
}
function invalidationWatchFor(category, primaryTouch, touches, options) {
const watch = /* @__PURE__ */ new Set([primaryTouch]);
switch (category) {
case "component-reuse":
for (const option of options) {
if (option.evidence.file) {
watch.add(option.evidence.file);
}
}
watch.add(PATHS.DESIGN_SYSTEM_DIR);
break;
case "create-vs-reuse":
for (const option of options) {
if (option.evidence.file) {
watch.add(option.evidence.file);
}
}
watch.add(PATHS.DESIGN_SYSTEM_DIR);
break;
case "architecture-path":
for (const touch of touches) {
watch.add(touch);
}
break;
case "shared-abstraction":
for (const option of options) {
if (option.evidence.file) {
watch.add(option.evidence.file);
}
}
watch.add(PATHS.DESIGN_SYSTEM_DIR);
break;
case "ux-pattern":
watch.add(PATHS.DESIGN_SYSTEM_DIR);
break;
default:
break;
}
return [...watch];
}
function ttlDaysFor(projectRoot, category) {
const override = readProjectProfile(projectRoot)?.custom?.decisions?.ttl_overrides_days?.[category];
return override ?? DECISION_CATEGORY_DEFAULTS[category].ttl_days;
}
// src/kernel/capability.ts
init_esm_shims();
import { readFileSync as readFileSync13 } from "fs";
// src/pipeline/session-ownership.ts
init_esm_shims();
var AGENT_AUTHORED_SOURCES = /* @__PURE__ */ new Set(["live-mark", "redo"]);
var PRE_EDIT_STAGES = /* @__PURE__ */ new Set([
"ticket_intake",
"planning",
"specification"
]);
function sessionOwnedRows(projectRoot, sessionId) {
return ownedRowsByChange(projectRoot, sessionId).flatMap(({ rows }) => rows);
}
function ownedRowsByChange(projectRoot, sessionId) {
let dirNames;
try {
dirNames = listInFlightFeatures(projectRoot);
} catch {
return [];
}
const ownBundle = readSessionControl(projectRoot, sessionId).active;
if (ownBundle !== null && !dirNames.includes(ownBundle) && isReopened(projectRoot, ownBundle)) {
dirNames = [...dirNames, ownBundle];
}
const lastPrompt = lastPromptAt(projectRoot, sessionId);
return dirNames.map((dirName) => ({
dirName,
rows: readFeatureStageUnit(projectRoot, dirName).filter(
(row) => row.session_id === sessionId && typeof row.evidence_source === "string" && AGENT_AUTHORED_SOURCES.has(row.evidence_source) || dirName === ownBundle && isStageAgentRow(row) && stageAgentRowIsOwn(row, sessionId, lastPrompt)
)
}));
}
function isReopened(projectRoot, dirName) {
return isBundleMaterialized(projectRoot, dirName) && !isBundleClosed(projectRoot, dirName);
}
function sessionWroteInEpisode(projectRoot, sessionId, dirName, episodeStartAt) {
const rows = readFeatureStageUnit(projectRoot, dirName);
const since = episodeStartAt === null ? NaN : Date.parse(episodeStartAt);
const wroteSince = rows.some((row) => {
if (row.session_id !== sessionId) return false;
if (Number.isNaN(since)) return true;
const at = Date.parse(rowRecordedAt(row) ?? "");
return !Number.isNaN(at) && at >= since;
});
return wroteSince || rows.some((row, index) => isOwnRunningStageStart(rows, row, index, sessionId));
}
function isOwnRunningStageStart(rows, row, index, sessionId) {
if (row.kind !== "stage_start" || row.session_id !== sessionId) return false;
return !rows.slice(index + 1).some((later) => later.kind === "stage_end" && later.stage === row.stage);
}
function lastPromptAt(projectRoot, sessionId) {
const stamp = readWorkflowState(projectRoot, sessionId).turn_started_at;
return stamp === void 0 ? NaN : Date.parse(stamp);
}
function stageAgentRowIsOwn(row, sessionId, lastPrompt) {
if (Number.isNaN(lastPrompt)) {
return true;
}
if (row.session_id !== sessionId) {
return false;
}
const at = Date.parse(rowRecordedAt(row) ?? "");
return !Number.isNaN(at) && at >= lastPrompt;
}
function isStageAgentRow(row) {
return row.kind === STAGE_AGENT_KIND || isStageAgent(row.agent);
}
function editedThisTurn(owned, state) {
const turnStart = state.turn_started_at === void 0 ? NaN : Date.parse(state.turn_started_at);
if (Number.isNaN(turnStart)) {
return owned.length > 0;
}
return owned.some((row) => {
if (row.session_source === "cache") {
return false;
}
const at = Date.parse(rowRecordedAt(row) ?? "");
return !Number.isNaN(at) && at >= turnStart;
});
}
function editedDevelopmentThisTurn(projectRoot, sessionId) {
const development = sessionOwnedRows(projectRoot, sessionId).filter(
(row) => row.stage === "development"
);
if (development.length === 0) {
return false;
}
return editedThisTurn(development, readWorkflowState(projectRoot, sessionId));
}
function hasRowPastSpecification(rows) {
return rows.some((row) => typeof row.stage === "string" && !PRE_EDIT_STAGES.has(row.stage));
}
function changePastSpecification(projectRoot, sessionId) {
const dirName = currentFeature(projectRoot, sessionId);
return dirName !== null && hasRowPastSpecification(readFeatureStageUnit(projectRoot, dirName));
}
function isEmptyChange(projectRoot, sessionId, state) {
const turnStart = state.turn_started_at === void 0 ? NaN : Date.parse(state.turn_started_at);
if (Number.isNaN(turnStart)) return false;
const dirName = currentFeature(projectRoot, sessionId);
if (!dirName) return false;
const elsewhere = ownedRowsByChange(projectRoot, sessionId).filter((change) => change.dirName !== dirName).flatMap((change) => change.rows);
if (editedThisTurn(elsewhere, state)) return false;
const rows = readFeatureStageUnit(projectRoot, dirName);
if (hasRowPastSpecification(rows)) return false;
const endedThisTurn = rows.some((row) => {
if (row.kind !== "stage_end") return false;
const at = Date.parse(rowRecordedAt(row) ?? "");
return Number.isNaN(at) || at >= turnStart;
});
if (endedThisTurn) return false;
return activeChangeFiles(projectRoot, sessionId)?.length === 0;
}
function classifyCompletionEnforcement(projectRoot, sessionId) {
let resolved;
try {
resolved = resolveSessionId(projectRoot, sessionId ?? null);
} catch {
return { enforce: false, reason: "not-owner", activeWorkflow: null };
}
const state = readWorkflowState(projectRoot, resolved);
const activeWorkflow = state.active?.workflow ?? null;
const owned = sessionOwnedRows(projectRoot, resolved);
if (owned.length === 0) {
return { enforce: false, reason: "not-owner", activeWorkflow };
}
if (isEmptyChange(projectRoot, resolved, state)) {
return { enforce: false, reason: "empty-change", activeWorkflow };
}
if (editedThisTurn(owned, state)) {
return { enforce: true, reason: "edited-this-turn", activeWorkflow };
}
if (activeWorkflow === null) {
return { enforce: true, reason: "owner-unknown-route", activeWorkflow };
}
if (isFeatureDevelopmentRoute(activeWorkflow)) {
return { enforce: true, reason: "owner-feature-dev", activeWorkflow };
}
return { enforce: false, reason: "detour", activeWorkflow };
}
// src/delivery/delivery-check.ts
init_esm_shims();
// src/delivery/ai-attribution.ts
init_esm_shims();
var AI_ATTRIBUTION_MARKERS = [
{
id: "claude-code",
vendor: "Claude Code",
pattern: /(?:co-authored-by:\s*claude\b|generated with \[?claude code)/i,
configuredByPaqad: true,
remediation: 'paqad writes `attribution: { commit: "", pr: "", sessionUrl: false }` into `.claude/settings.json` at onboarding. If the trailer is still appearing, that file was edited or a higher-precedence settings file overrides it (`.claude/settings.local.json`, then the command line, then managed settings).'
},
{
id: "codex-cli",
vendor: "Codex CLI",
pattern: /co-authored-by:\s*codex\b/i,
configuredByPaqad: false,
remediation: 'Codex only exposes this in your home directory, which paqad will not write. Set `commit_attribution = ""` in `~/.codex/config.toml`.'
},
{
id: "cursor",
vendor: "Cursor",
pattern: /(?:co-authored-by:\s*cursor\b|made[- ]with:?\s*cursor\b)/i,
configuredByPaqad: false,
remediation: "Cursor only exposes this in your home directory, which paqad will not write. Set `attribution.attributeCommitsToAgent` to false in `~/.cursor/cli-config.json`, or turn it off for everyone from the Cursor admin dashboard."
},
{
id: "aider",
vendor: "Aider",
pattern: /co-authored-by:\s*aider\b/i,
configuredByPaqad: true,
remediation: "paqad writes `attribute-author`, `attribute-committer` and `attribute-co-authored-by` as false into `.aider.conf.yml` at onboarding. If the trailer is still appearing, that file was edited or aider was run with an explicit `--attribute-\u2026` flag."
},
{
id: "github-copilot",
vendor: "GitHub Copilot",
pattern: /co-authored-by:\s*(?:copilot\b|copilot-swe-agent\b)/i,
configuredByPaqad: false,
remediation: "The Copilot coding agent commits under its own GitHub identity; there is no project file to set. Control it from your organisation\u2019s Copilot policy settings on GitHub."
},
{
id: "gemini-cli",
vendor: "Gemini CLI",
pattern: /(?:co-authored-by:\s*gemini\b|generated with gemini\b)/i,
configuredByPaqad: false,
remediation: "Gemini CLI adds no attribution by default, so this was added by hand or by a local commit template. Remove it from whatever added it."
}
];
function detectAiAttribution(text) {
if (!text) {
return [];
}
const matches = [];
for (const marker of AI_ATTRIBUTION_MARKERS) {
const found = marker.pattern.exec(text);
if (found) {
matches.push({ marker, matched: found[0] });
}
}
return matches;
}
// src/delivery/delivery-ledger.ts
init_esm_shims();
var DELIVERY_EVIDENCE_DOC_TYPE = "delivery-evidence";
var DELIVERY_EVIDENCE_SCHEMA_VERSION = 1;
// src/delivery/runner.ts
init_esm_shims();
// src/git-workflow/patterns.ts
init_esm_shims();
// src/delivery/templates.ts
init_esm_shims();
var PLACEHOLDER = /\{([a-z_]+)\}/g;
var SEPARATORS = /* @__PURE__ */ new Set(["-", "_", ":", "/", "|", " "]);
function slugify(value, maxLength) {
const slug = value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
if (slug.length <= maxLength) {
return slug;
}
return slug.slice(0, maxLength).replace(/-+$/, "");
}
function resolveBranchTypeName(config, ticketType) {
if (ticketType) {
const match = Object.entries(config.branches.types).filter(
([, type]) => type.ticket_types.includes(ticketType)
);
if (match.length === 1) return match[0][0];
}
return config.branches.default_type;
}
function known(vars, key) {
return Object.prototype.hasOwnProperty.call(vars, key);
}
function placeholdersIn(line, vars) {
return Array.from(line.matchAll(PLACEHOLDER), (match) => match[1]).filter(
(key) => known(vars, key)
);
}
function dropEmptyLines(template, vars) {
const lines = template.split("\n");
const kept = lines.filter((line, index) => {
if (index === 0) return true;
const keys = placeholdersIn(line, vars);
return keys.length === 0 || keys.some((key) => vars[key] !== "");
});
while (kept.length > 1 && kept[kept.length - 1].trim() === "") kept.pop();
return kept.join("\n");
}
function cleanEmptyPlaceholders(line, vars) {
let out = line;
for (; ; ) {
const match = Array.from(out.matchAll(PLACEHOLDER)).find(
(m) => known(vars, m[1]) && vars[m[1]] === ""
);
if (!match || match.index === void 0) return out;
const start = match.index;
const end = start + match[0].length;
let after = end;
while (after < out.length && SEPARATORS.has(out[after])) after += 1;
const rest = out.slice(end).replace(PLACEHOLDER, (m, key) => known(vars, key) && vars[key] === "" ? "" : m);
if ([...rest].every((ch) => SEPARATORS.has(ch))) {
let before = start;
while (before > 0 && SEPARATORS.has(out[before - 1])) before -= 1;
out = out.slice(0, before) + out.slice(end);
} else {
out = out.slice(0, start) + out.slice(after);
}
}
}
function removeEmptyBrackets(text) {
return text.replace(/ ?(\(\)|\[\])/g, "");
}
function cleanBranch(text) {
return text.replace(/-{2,}/g, "-").replace(/\/{2,}/g, "/").replace(/^[-_/]+|[-_/]+$/g, "");
}
function renderTemplate(template, vars, options = {}) {
const endsWithNewline = template.endsWith("\n");
const lines = dropEmptyLines(template, vars).split("\n").map((line) => removeEmptyBrackets(cleanEmptyPlaceholders(line, vars)));
let out = lines.join("\n").replace(PLACEHOLDER, (match, key) => known(vars, key) ? vars[key] : match);
if (options.branch) out = cleanBranch(out);
return endsWithNewline && !out.endsWith("\n") ? `${out}
` : out;
}
function withTechDebt(template, body, section) {
if (section === "" || template.includes("{tech_debt}")) {
return body;
}
const trimmed = stripTrailingChars(body, " \r\n");
return trimmed === "" ? `${section}
` : `${trimmed}
${section}
`;
}
function withSection(body, section) {
if (section === "") return body;
const trimmed = stripTrailingChars(body, " \r\n");
return trimmed === "" ? `${section}
` : `${trimmed}
${section}
`;
}
function renderVars(config, typeName, inputs) {
const branchType = config.branches.types[typeName];
return {
prefix: branchType?.prefix ?? typeName,
type: branchType?.commit_type ?? typeName,
ticket: inputs.ticket || "",
title: inputs.title,
title_slug: slugify(inputs.title, config.branches.slug_max_length),
summary: inputs.summary,
scope: inputs.scope ?? "",
tech_debt: inputs.tech_debt ?? ""
};
}
function renderBranchName(config, typeName, inputs) {
const vars = renderVars(config, typeName, inputs);
const template = config.branches.types[typeName]?.template ?? config.branches.template;
return renderTemplate(
template,
{ ...vars, ticket: vars.ticket.replace(/^#/, "") },
{ branch: true }
);
}
function renderDelivery(config, inputs, prBodyTemplate, typeName = resolveBranchTypeName(config, inputs.ticket_type)) {
const vars = renderVars(config, typeName, inputs);
return {
branch: renderBranchName(config, typeName, inputs),
commit: renderTemplate(config.commits.template, vars),
pr_title: renderTemplate(config.pull_requests.title_template, vars),
pr_body: withSection(
withTechDebt(prBodyTemplate, renderTemplate(prBodyTemplate, vars), vars.tech_debt),
inputs.design_sign_off ?? ""
)
};
}
// src/git-workflow/patterns.ts
var PLACEHOLDERS = [
"prefix",
"type",
"ticket",
"title",
"title_slug",
"summary",
"scope",
"tech_debt"
];
function escapeRegExp2(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function alternation(values, fallback) {
const unique = Array.from(new Set(values.filter((value) => value !== "")));
return unique.length === 0 ? fallback : `(?:${unique.map(escapeRegExp2).join("|")})`;
}
function ticketPatterns(config, branch = false) {
return Object.values(config.tickets.sources).map((source) => source.pattern).filter((pattern) => pattern !== "").map((pattern) => branch ? pattern.replace(/^#/, "") : pattern);
}
function pieces(config, options) {
const types = Object.values(config.branches.types);
const tickets = ticketPatterns(config, options.branch === true);
return {
prefix: alternation(options.prefixes ?? types.map((type) => type.prefix), "[^/\\s]+"),
type: alternation(
types.map((type) => type.commit_type),
"\\w+"
),
ticket: options.ticketCapture !== void 0 ? `(?<ticket>${options.ticketCapture})` : tickets.length === 0 ? "\\S+" : `(?:${tickets.map((p) => `(?:${p})`).join("|")})`,
title_slug: "[a-z0-9]+(?:-[a-z0-9]+)*",
scope: "[^()\\s]+",
title: ".+",
summary: ".+",
tech_debt: "[\\s\\S]*"
};
}
function subsets(items) {
return items.reduce(
(all, item) => [...all, ...all.map((subset) => [...subset, item])],
[[]]
);
}
function templateToRegExp(template, config, options = {}) {
const regexPieces = pieces(config, options);
const present = PLACEHOLDERS.filter((key) => template.includes(`{${key}}`));
const optional = (options.optional ?? []).filter((key) => present.includes(key));
const patterns = subsets(optional).map((emptied) => {
const vars = {};
for (const key of PLACEHOLDERS) {
vars[key] = emptied.includes(key) ? "" : `\uE000${key}\uE001`;
}
const rendered = renderTemplate(template, vars, { branch: options.branch === true });
const source = rendered.split(/(\uE000[a-z_]+\uE001)/).map((part) => {
const key = /^\uE000([a-z_]+)\uE001$/.exec(part)?.[1];
return key ? regexPieces[key] : escapeRegExp2(part);
}).join("");
return new RegExp(`^${source}$`);
});
return { patterns, test: (text) => patterns.some((pattern) => pattern.test(text)) };
}
function ticketOptional(config) {
return config.tickets.require_ticket ? ["scope"] : ["ticket", "scope"];
}
function firstLine(template) {
return template.split("\n")[0];
}
// src/git-workflow/summary-style.ts
init_esm_shims();
var FILE_PATH = /\b[\w.-]+\/[\w./-]+\b/;
var FILE_NAME = /\b\w+\.(ts|tsx|js|jsx|php|py|rb|go|java|kt|cs|vue|css|scss|sql|yml|yaml|json|md)\b/;
var CODE_SYMBOLS = ["::", "->", "=>", "()"];
var CAMEL = /[a-z][A-Z]/;
var SNAKE = /[A-Za-z]_[A-Za-z]/;
var VAGUE = /* @__PURE__ */ new Set([
"fix",
"fixes",
"fix bug",
"fix bugs",
"bug fix",
"bugfix",
"update",
"updates",
"change",
"changes",
"wip",
"misc",
"minor changes",
"cleanup",
"refactor",
"tweaks",
"small fixes"
]);
var IGNORED_SUBJECT = /^(Merge |Revert "|fixup! |squash! )/;
function isIgnoredSubject(subject) {
return IGNORED_SUBJECT.test(subject);
}
function escapeRegExp3(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function ticketRegExps(config) {
return Object.values(config.tickets.sources).flatMap((source) => {
try {
return [new RegExp(source.pattern, "g")];
} catch {
return [];
}
});
}
var SUMMARY_PIECES = {
ticket: "[^\\s()\\[\\]:]*",
scope: "[^()\\s]*",
prefix: "[^/\\s]*",
title: ".*?",
title_slug: "[a-z0-9-]*",
tech_debt: "[\\s\\S]*?"
};
function summaryPart(subject, template, config) {
const line = firstLine(template);
if (line.includes("{summary}")) {
const types = Object.values(config.branches.types).map(
(type) => escapeRegExp3(type.commit_type)
);
const typePiece = types.length > 0 ? `(?:${types.join("|")})` : "\\w+";
const tickets = ticketPatterns(config);
const ticketPiece = tickets.length > 0 ? `(?:${tickets.map((pattern) => `(?:${pattern})`).join("|")})?` : "";
const source = line.split(/(\{[a-z_]+\})/).map((part) => {
const key = /^\{([a-z_]+)\}$/.exec(part)?.[1];
if (key === "summary") return "(?<summary>.+?)";
if (key === "type") return typePiece;
if (key === "ticket") return ticketPiece;
if (key && SUMMARY_PIECES[key]) return SUMMARY_PIECES[key];
return escapeRegExp3(part).replace(/[ :([\]\\)]+/g, (sep2) => `(?:${sep2})?`);
}).join("");
const match = new RegExp(`^${source}$`).exec(subject);
const summary = match?.groups?.summary;
if (summary !== void 0) return withoutTickets(summary, config);
}
return withoutTickets(subject, config);
}
function withoutTickets(text, config) {
let out = text;
for (const pattern of ticketRegExps(config)) out = out.replace(pattern, "");
return out.replace(/\s{2,}/g, " ").trim();
}
function summaryStyleReasons(summary, maxLength) {
const reasons = [];
if (summary.includes("`")) reasons.push("it contains code formatting");
if (FILE_PATH.test(summary) || FILE_NAME.test(summary)) reasons.push("it names a file");
if (CODE_SYMBOLS.some((symbol) => summary.includes(symbol))) {
reasons.push("it uses code symbols");
}
const words = summary.split(/\s+/).filter((word) => word !== "");
if (words.some((word) => CAMEL.test(word) || SNAKE.test(word))) {
reasons.push("it names a function or variable");
}
if (summary.length > maxLength) reasons.push(`it's longer than ${maxLength} characters`);
const normalized = summary.toLowerCase().replace(/[.!?,;:]+$/, "").trim();
if (VAGUE.has(normalized)) {
reasons.push("it doesn't say what changed");
} else if (words.length < 3) {
reasons.push("it's too short to say what changed");
}
const first = (words[0] ?? "").replace(/[^A-Za-z]/g, "");
if (first.length > 4 && first.toLowerCase().endsWith("ing")) {
reasons.push('start with what it does, like "Show", not "Showing"');
}
return reasons;
}
function checkSummaryStyle(subject, template, config) {
if (config.commits.summary.style !== "plain" || isIgnoredSubject(subject)) return [];
return summaryStyleReasons(
summaryPart(subject, template, config),
config.commits.summary.max_length
);
}
function summaryStyleMessage(subject, reason, what = "commit") {
return `${what} "${subject}" reads like code, not plain language: ${reason}.`;
}
// src/delivery/runner.ts
var GIT_WORKFLOW_PATH = PATHS.GIT_WORKFLOW_DIR;
function stop(message) {
return { ok: false, message };
}
function done(value) {
return { ok: true, value };
}
function wholeMatch(pattern, ref) {
try {
return new RegExp(`^(?:${pattern})$`).test(ref);
} catch {
return false;
}
}
function requireTicket(config, ticket) {
if (config.tickets.require_ticket && ticket.trim() === "") {
return stop(
"This team's git workflow needs a ticket for every change. Re-run with --ticket <ref>."
);
}
return done(null);
}
function matchTicketSource(config, ticket) {
const sources = Object.entries(config.tickets.sources);
if (ticket.trim() === "" || sources.length === 0) return done(null);
const found = sources.find(([, source]) => wholeMatch(source.pattern, ticket));
if (found) return done({ name: found[0], source: found[1] });
const list = sources.map(([name, source]) => `${name}: ${source.pattern}`).join(", ");
return stop(
`${ticket} doesn't look like any ticket this team uses (${list}). Check the key or add a source in ${GIT_WORKFLOW_PATH}/tickets.yaml.`
);
}
function branchPattern(config, typeName) {
const type = config.branches.types[typeName];
return templateToRegExp(type?.template ?? config.branches.template, config, {
optional: ticketOptional(config),
branch: true,
prefixes: type ? [type.prefix] : void 0
});
}
function typeOfBranch(config, branch) {
return Object.keys(config.branches.types).filter(
(name) => branchPattern(config, name).test(branch)
);
}
function allows(config, typeName, source) {
const tickets = config.branches.types[typeName]?.tickets ?? [];
return source === null || tickets.length === 0 || tickets.includes(source);
}
function pickBranchType(config, input) {
const names = Object.keys(config.branches.types);
if (input.type) {
if (!names.includes(input.type)) {
return stop(
`There's no branch type called ${input.type}. This team's types are: ${names.join(", ")}.`
);
}
return done(input.type);
}
if (input.source) {
const bySource = names.filter(
(name) => config.branches.types[name].tickets.includes(input.source)
);
if (bySource.length === 1) return done(bySource[0]);
}
if (input.ticketType) {
const byTicketType = names.filter(
(name) => config.branches.types[name].ticket_types.includes(input.ticketType)
);
if (byTicketType.length === 1) return done(byTicketType[0]);
}
if (input.currentBranch) {
const byBranch = typeOfBranch(config, input.currentBranch);
if (byBranch.length === 1) return done(byBranch[0]);
}
const fallback = config.branches.default_type;
if (!input.ticketGiven || allows(config, fallback, input.source)) return done(fallback);
const candidates = names.filter((name) => allows(config, name, input.source));
return stop(
`This change could be ${(candidates.length > 0 ? candidates : names).join(", ")}. Re-run with --type <name>.`
);
}
function compareVersions(a, b) {
const split = (value) => value.split(/[.-]/);
const pa = split(a);
const pb = split(b);
for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
const x = pa[i];
const y = pb[i];
if (x === void 0) return -1;
if (y === void 0) return 1;
const nx = Number(x);
const ny = Number(y);
if (Number.isInteger(nx) && Number.isInteger(ny) && nx !== ny) return nx - ny;
if (!(Number.isInteger(nx) && Number.isInteger(ny)) && x !== y) return x < y ? -1 : 1;
}
return a < b ? -1 : a > b ? 1 : 0;
}
function highestVersionBranch(branches) {
const last = (name) => name.split("/").pop() ?? name;
return [...branches].sort((a, b) => compareVersions(last(a), last(b)) || (a < b ? -1 : 1)).pop() ?? null;
}
function isGlob(base) {
return base.includes("*");
}
function globMatches(glob, name) {
const source = glob.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
return new RegExp(`^${source}$`).test(name);
}
async function resolveBase(shell, base) {
if (!isGlob(base)) return done(base);
const listed = await shell.run("git", ["ls-remote", "--heads", "origin", base]);
const names = listed.exitCode === 0 ? listed.stdout.split("\n").map((line) => line.trim().split(/\s+/)[1] ?? "").filter((ref) => ref.startsWith("refs/heads/")).map((ref) => ref.slice("refs/heads/".length)).filter((name) => globMatches(base, name)) : [];
const picked = highestVersionBranch(names);
return picked ? done(picked) : stop(`No branch on origin matches ${base}.`);
}
function configuredBases(config) {
return Array.from(
/* @__PURE__ */ new Set([
config.branches.base_branch,
...Object.values(config.branches.types).map((type) => type.base_branch),
...config.pull_requests.also_open_against
])
);
}
function isConfiguredBase(config, branch) {
return configuredBases(config).some(
(base) => isGlob(base) ? globMatches(base, branch) : base === branch
);
}
function planBranch(config, current, base, rendered, typeName) {
if (current === base) return done({ branch: rendered, create: true });
if (typeOfBranch(config, current).length > 0) return done({ branch: current, create: false });
if (isConfiguredBase(config, current)) {
return stop(
`You're on ${current}, but this change should start from ${base}. Switch to ${base} and re-run paqad-ai deliver.`
);
}
const template = config.branches.types[typeName]?.template ?? config.branches.template;
return stop(
`You're on ${current}, which doesn't follow this team's branch pattern ${template}. Switch to ${base} or rename the branch, then re-run paqad-ai deliver.`
);
}
async function currentBranch(shell) {
const result = await shell.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
const name = result.stdout.trim();
return result.exitCode === 0 && name !== "" && name !== "HEAD" ? name : null;
}
async function hasStagedChanges(shell) {
const result = await shell.run("git", ["diff", "--cached", "--quiet"]);
return result.exitCode === 1;
}
async function commitsSince(shell, base) {
for (const ref of [base, `origin/${base}`]) {
const result = await shell.run("git", ["rev-list", "--count", `${ref}..HEAD`]);
if (result.exitCode === 0) return Number.parseInt(result.stdout.trim(), 10) || 0;
}
return 0;
}
function styleVerdict(config, subject, template, what) {
const reasons = checkSummaryStyle(subject, template, config);
return {
block: reasons.length > 0 && config.enforcement.agents === "block",
messages: reasons.map((reason) => summaryStyleMessage(subject, reason, what))
};
}
async function commitStep(shell, config, message, options) {
const subject = message.split("\n")[0];
if (await hasStagedChanges(shell)) {
const style = styleVerdict(config, subject, config.commits.template, "commit");
if (style.block) return stop(style.messages[0]);
const args = ["commit", ...config.commits.sign_off ? ["-s"] : [], "-m", message];
const result = await shell.run("git", args);
if (result.exitCode !== 0) {
return stop(
`git commit failed: ${(result.stderr || result.stdout).trim() || "unknown error"}. Fix it and re-run; do not skip pre-commit hooks.`
);
}
return done({ committed: true, output: result.stdout, warnings: style.messages });
}
if (!options.requireWork) {
return stop("Nothing is staged. Stage your changes with git add first.");
}
if (options.base && await commitsSince(shell, options.base) > 0) {
return done({ committed: false, output: "", warnings: [] });
}
return stop(
`Nothing to deliver: no staged changes and no commits on ${options.branch} since ${options.base ?? "the base"}.`
);
}
function placeholdersIn2(template) {
return Array.from(template.matchAll(/\{([a-z_]+)\}/g), (match) => match[1]);
}
function markdownHeadings(text) {
return text.split("\n").map((line) => line.trimEnd()).filter((line) => /^#{1,6}\s+\S/.test(line));
}
function missingHeadings(template, body) {
const present = new Set(markdownHeadings(body).map((line) => line.trim()));
return markdownHeadings(template).filter((heading) => !present.has(heading.trim()));
}
function linkTicket(body, ticket, matched, enabled) {
if (!enabled || ticket.trim() === "" || body.includes(ticket)) return body;
const line = matched?.source.tracker === "github-issues" ? `Closes ${ticket.startsWith("#") ? ticket : `#${ticket}`}` : `Ticket: ${ticket}`;
if (body.includes(line)) return body;
const trimmed = body.replace(/\s+$/, "");
return trimmed === "" ? `${line}
` : `${trimmed}
${line}
`;
}
function uniqueInOrder(...lists) {
return Array.from(new Set(lists.flat().filter((value) => value !== "")));
}
var MISSING_LABEL_HINT = `If a label is missing, create it on the repo or remove it from ${GIT_WORKFLOW_PATH}/pull-requests.yaml.`;
// src/delivery/workflow-findings.ts
init_esm_shims();
var FIELD_SEPARATOR = String.fromCharCode(31);
var RECORD_SEPARATOR = String.fromCharCode(30);
var BLOCKING_CODES = /* @__PURE__ */ new Set([
"on-base-branch",
"branch-shape",
"commit-shape",
"commit-summary-style",
"pr-base",
"pr-title-shape",
"pr-labels",
"pr-ticket-link"
]);
function invalidFinding(errors) {
return {
code: "git-workflow-invalid",
message: `${GIT_WORKFLOW_PATH}/ has errors, so nothing was checked: ${errors[0]?.message ?? "unknown error"}`
};
}
function onBaseFinding(branch) {
return {
code: "on-base-branch",
message: `you're committing on ${branch}. This team works on a branch and opens a PR from it.`
};
}
function followsBranchShape(config, branch) {
return Object.values(config.branches.types).some((type) => {
const template = type.template || config.branches.template;
const optional = template.includes("{prefix}") ? ticketOptional(config) : ["scope"];
return templateToRegExp(template, config, {
optional,
branch: true,
prefixes: [type.prefix]
}).test(branch);
});
}
function branchFinding(config, branch) {
if (isConfiguredBase(config, branch)) return null;
if (Object.keys(config.branches.types).length === 0) return null;
if (followsBranchShape(config, branch)) return null;
return {
code: "branch-shape",
message: `branch ${branch} doesn't follow this team's pattern ${config.branches.template}.`
};
}
function commitFindings(config, commit) {
const { subject } = commit;
if (commit.merge || isIgnoredSubject(subject)) return [];
const template = firstLine(config.commits.template);
const findings = [];
const shape = templateToRegExp(template, config, { optional: ticketOptional(config) });
if (!shape.test(subject)) {
findings.push({
code: "commit-shape",
message: `commit "${subject}" doesn't follow this team's pattern ${template}.`
});
}
for (const reason of checkSummaryStyle(subject, template, config)) {
findings.push({ code: "commit-summary-style", message: summaryStyleMessage(subject, reason) });
}
return findings;
}
function ticketInTemplateSlot(config, pattern, branch) {
const types = Object.values(config.branches.types);
const shapes = types.length > 0 ? types.map((type) => ({
template: type.template || config.branches.template,
prefixes: [type.prefix]
})) : [{ template: config.branches.template, prefixes: void 0 }];
for (const { template, prefixes } of shapes) {
const { patterns } = templateToRegExp(template, config, {
optional: ["scope", "title_slug"],
branch: true,
prefixes,
ticketCapture: pattern
});
for (const shape of patterns) {
const ticket = shape.exec(branch)?.groups?.ticket;
if (ticket) return `#${ticket}`;
}
}
return "";
}
function ticketInBranch(config, branch) {
for (const source of Object.values(config.tickets.sources)) {
try {
if (source.pattern.startsWith("#")) {
const ticket = ticketInTemplateSlot(config, source.pattern.slice(1), branch);
if (ticket) return ticket;
continue;
}
const found = new RegExp(`(?<![A-Za-z0-9])(?:${source.pattern})(?![A-Za-z0-9])`).exec(branch);
if (found) return found[0];
} catch {
continue;
}
}
return "";
}
function baseMatches(base, actual) {
return isGlob(base) ? globMatches(base, actual) : base === actual;
}
function prFindings(config, branch, pr) {
const findings = [];
const n = pr.number ?? "?";
const typeName = typeOfBranch(config, branch)[0] ?? config.branches.default_type;
const type = config.branches.types[typeName];
const expected = type?.base_branch || config.branches.base_branch;
if (pr.base) {
const accepted = baseMatches(expected, pr.base) || config.pull_requests.also_open_against.includes(pr.base) || typeOfBranch(config, pr.base).length > 0;
if (!accepted) {
findings.push({
code: "pr-base",
message: `PR #${n} targets ${pr.base}, but this team sends ${typeName} changes to ${expected}.`
});
}
}
if (pr.title !== null) {
const template = config.pull_requests.title_template;
const shape = templateToRegExp(template, config, { optional: ticketOptional(config) });
if (!shape.test(pr.title)) {
findings.push({
code: "pr-title-shape",
message: `PR #${n} title "${pr.title}" doesn't follow this team's pattern ${template}.`
});
}
}
const required = Array.from(/* @__PURE__ */ new Set([...config.pull_requests.labels, ...type?.labels ?? []]));
const missing = required.filter((label) => !pr.labels.includes(label));
if (missing.length > 0) {
findings.push({
code: "pr-labels",
message: `PR #${n} is missing the labels: ${missing.join(", ")}.`
});
}
const ticket = ticketInBranch(config, branch);
if (config.pull_requests.link_ticket && ticket !== "" && !pr.body.includes(ticket)) {
findings.push({ code: "pr-ticket-link", message: `PR #${n} doesn't link ${ticket}.` });
}
return findings;
}
function expectedBase(config, branch) {
const typeName = typeOfBranch(config, branch)[0] ?? config.branches.default_type;
return config.branches.types[typeName]?.base_branch || config.branches.base_branch;
}
var SUBJECT_FORMAT = "%s%x1f%P%x1e";
function parseSubjects(stdout) {
return stdout.split(RECORD_SEPARATOR).map((record) => record.replace(/^\n/, "")).filter((record) => record.includes(FIELD_SEPARATOR)).map((record) => {
const [subject, parents = ""] = record.split(FIELD_SEPARATOR);
return { subject, merge: parents.trim().split(/\s+/).length > 1 };
});
}
// src/delivery/delivery-check.ts
var GH_TIMEOUT_MS = 1e4;
var NO_CHECK = {
ran: false,
branch: null,
commit: null,
ghAvailable: false,
findings: []
};
var NO_OP = { ran: false, blocking: false, summary: "" };
async function branchSubjectFindings(run, projectRoot, policy, branch) {
const base = expectedBase(policy, branch);
if (isGlob(base)) return [];
for (const ref of [base, `origin/${base}`]) {
const result = await run("git", [
"-C",
projectRoot,
"log",
`${ref}..HEAD`,
`--format=${SUBJECT_FORMAT}`
]);
if (result.exitCode !== 0) continue;
return parseSubjects(result.stdout).flatMap((commit) => commitFindings(policy, commit));
}
return [];
}
async function committedOnBase(run, projectRoot, branch) {
const ahead = await readLine(run, "git", [
"-C",
projectRoot,
"rev-list",
"--count",
`origin/${branch}..HEAD`
]);
return ahead !== null && /^\d+$/.test(ahead) && Number.parseInt(ahead, 10) > 0;
}
function labelNames(value) {
if (!Array.isArray(value)) return [];
return value.map((entry) => entry.name).filter((name) => typeof name === "string");
}
async function readLine(run, command, args) {
const result = await run(command, args);
if (result.exitCode !== 0) {
return null;
}
const line = result.stdout.trim();
return line.length > 0 ? line : null;
}
async function readBranchCommitMessages(run, projectRoot, branch, base) {
const range = branch === base ? ["-1"] : [`${base}..HEAD`];
const result = await run("git", ["-C", projectRoot, "log", ...range, "--format=%B"]);
return result.exitCode === 0 ? result.stdout : "";
}
function attributionFindings(commitMessages, prBody) {
const inCommits = new Map(
detectAiAttribution(commitMessages).map((match) => [match.marker.id, match.marker])
);
const inPrBody = new Map(
detectAiAttribution(prBody).map((match) => [match.marker.id, match.marker])
);
const findings = [];
for (const marker of AI_ATTRIBUTION_MARKERS) {
const commits = inCommits.has(marker.id);
const body = inPrBody.has(marker.id);
if (!commits && !body) {
continue;
}
const where = commits && body ? "your commits and the PR body" : commits ? "your commits" : "the PR body";
findings.push({
code: "ai-attribution",
message: `${marker.vendor} attribution is on ${where}. That trailer is what makes the repo list an AI vendor as a contributor. ${marker.remediation}`
});
}
return findings;
}
var FAILING = /* @__PURE__ */ new Set(["FAILURE", "ERROR", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED"]);
function rollupHasFailure(rollup) {
if (!Array.isArray(rollup)) {
return false;
}
return rollup.some((entry) => {
const check = entry;
const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : "";
const state = typeof check.state === "string" ? check.state.toUpperCase() : "";
return FAILING.has(conclusion) || FAILING.has(state);
});
}
var PASSING = /* @__PURE__ */ new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
function upper(value) {
return typeof value === "string" ? value.toUpperCase() : "";
}
function textOrNull(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}
var FULL_SHA = /^[0-9a-f]{40}$/;
function shaOrNull(value) {
return typeof value === "string" && FULL_SHA.test(value) ? value : null;
}
function countOrNull(value) {
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
}
function rollupJobs(rollup) {
const entries = Array.isArray(rollup) ? rollup : [];
return entries.map((entry) => ({
status: textOrNull(entry.status) ?? textOrNull(entry.state),
conclusion: textOrNull(entry.conclusion),
started_at: textOrNull(entry.startedAt),
completed_at: textOrNull(entry.completedAt)
}));
}
var PR_STATES = {
OPEN: "open",
MERGED: "merged",
CLOSED: "closed"
};
function readPrState(value) {
return PR_STATES[upper(value)] ?? null;
}
function readRollup(rollup) {
const entries = Array.isArray(rollup) ? rollup : [];
if (entries.length === 0) {
return { state: "unknown", done: 0, total: 0, reason: "no checks reported", jobs: [] };
}
let done2 = 0;
let failed = false;
for (const entry of entries) {
const outcome = upper(entry.conclusion) || upper(entry.state);
if (FAILING.has(outcome)) failed = true;
if (FAILING.has(outcome) || PASSING.has(outcome)) done2 += 1;
}
const total = entries.length;
const state = failed ? "fail" : done2 === total ? "pass" : "pending";
return { state, done: done2, total, reason: null, jobs: rollupJobs(entries) };
}
function ghFailureReason(result) {
const text = `${result.stderr ?? ""}
${result.stdout}`.toLowerCase();
if (result.timedOut) return `gh timed out after ${GH_TIMEOUT_MS / 1e3} s`;
if (result.missing) return "gh is not installed";
if (text.includes("rate limit")) return "gh is rate-limited";
if (text.includes("auth login") || text.includes("not logged in")) return "gh is signed out";
if (text.includes("no pull requests found")) return "no pull request for this branch";
return "gh could not read the pull request";
}
async function evaluateDelivery(input) {
const { projectRoot, policy, run } = input;
if (!policy.enforcement.enabled) {
return NO_CHECK;
}
const branch = await readLine(run, "git", [
"-C",
projectRoot,
"rev-parse",
"--abbrev-ref",
"HEAD"
]);
if (!branch || branch === "HEAD") {
return NO_CHECK;
}
const commit = await readLine(run, "git", ["-C", projectRoot, "rev-parse", "HEAD"]);
const upstream = await readLine(run, "git", ["-C", projectRoot, "rev-parse", "@{upstream}"]);
if (input.errors && input.errors.length > 0) {
return {
ran: true,
branch,
commit,
ghAvailable: false,
findings: [invalidFinding(input.errors)],
upstream
};
}
const findings = [];
const base = policy.branches.base_branch;
const onBase = isConfiguredBase(policy, branch);
if (onBase) {
if (await committedOnBase(run, projectRoot, branch)) findings.push(onBaseFinding(branch));
} else {
const shape = branchFinding(policy, branch);
if (shape) findings.push(shape);
findings.push(...await branchSubjectFindings(run, projectRoot, policy, branch));
}
let ghAvailable = false;
let prBody = "";
let prReading = null;
let ciReading;
const prView = await run("gh", [
"pr",
"view",
branch,
"--json",
"number,url,state,headRefOid,statusCheckRollup,body,isDraft,changedFiles,additions,deletions,baseRefName,title,labels"
]);
if (prView.exitCode === 0) {
ghAvailable = true;
try {
const pr = JSON.parse(prView.stdout);
prBody = typeof pr.body === "string" ? pr.body : "";
prReading = {
number: typeof pr.number === "number" ? pr.number : null,
url: typeof pr.url === "string" ? pr.url : null,
draft: typeof pr.isDraft === "boolean" ? pr.isDraft : null,
state: readPrState(pr.state),
head_sha: shaOrNull(pr.headRefOid),
changed_files: countOrNull(pr.changedFiles),
additions: countOrNull(pr.additions),
deletions: countOrNull(pr.deletions)
};
ciReading = readRollup(pr.statusCheckRollup);
if (!onBase) {
findings.push(
...prFindings(policy, branch, {
number: prReading.number,
base: textOrNull(pr.baseRefName),
title: typeof pr.title === "string" ? pr.title : null,
labels: labelNames(pr.labels),
body: prBody
})
);
}
if (policy.pull_requests.ci.gate === "wait_for_green" && rollupHasFailure(pr.statusCheckRollup)) {
const label = typeof pr.number === "number" ? ` for PR #${pr.number}` : "";
findings.push({
code: "ci-red",
message: `CI is red${label} \u2014 the delivery policy gates merge on green (\`wait_for_green\`).`
});
}
} catch {
ghAvailable = false;
prReading = null;
ciReading = {
state: "unknown",
done: null,
total: null,
reason: "gh returned unreadable output"
};
}
} else {
ciReading = { state: "unknown", done: null, total: null, reason: ghFailureReason(prView) };
}
if (shouldStripAiAttribution(projectRoot)) {
const commitMessages = await readBranchCommitMessages(run, projectRoot, branch, base);
findings.push(...attributionFindings(commitMessages, prBody));
}
findings.push(...pendingMoveFindings(projectRoot, branch));
return {
ran: true,
branch,
commit,
ghAvailable,
findings,
pr: prReading,
ci: ciReading,
upstream
};
}
function pendingMoveFindings(projectRoot, branch) {
try {
const dirName = resolveDeliveryFeatureByBranch(projectRoot, branch);
if (!dirName) return [];
return pendingTicketActions(readFeatureDelivery(projectRoot, dirName)).map((action) => ({
code: "ticket-move-pending",
message: `ticket move to "${action.to}" not confirmed`
}));
} catch {
return [];
}
}
function pushedHead(result) {
const head = shaOrNull(result.commit);
if (!head) return null;
return result.upstream === head || result.pr?.head_sha === head ? head : null;
}
function ciReadOf(result) {
if (result.ghAvailable) return { ok: true, reason: null };
return { ok: false, reason: result.ci?.reason ?? "gh could not read the pull request" };
}
function saveDeliveryReading(projectRoot, result, now = () => /* @__PURE__ */ new Date(), options = {}) {
if (!result.ran || !result.branch) return;
try {
const dirName = resolveDeliveryFeatureByBranch(projectRoot, result.branch);
if (!dirName) return;
const patch = { ci_read: ciReadOf(result) };
if (options.startedAt) patch.read_at = options.startedAt;
const pushed = pushedHead(result);
if (pushed) patch.push = { state: "pushed", branch: result.branch, sha: pushed };
const keep = options.keepOnGhFailure === true && !result.ghAvailable;
if (result.pr && !keep) patch.pr = result.pr;
if (result.ci && !keep) {
const sha = shaOrNull(result.pr?.head_sha) ?? shaOrNull(result.commit);
patch.ci = { sha, ...result.ci, source: "stop-check" };
}
recordShip(projectRoot, dirName, patch, now().toISOString());
} catch (error) {
appendHooksLog(
projectRoot,
`delivery-check: delivery.json not updated: ${error instanceof Error ? error.message : String(error)}`
);
}
}
function storedOutcome(projectRoot, feature, refreshed, reason) {
const record = readFeatureDelivery(projectRoot, feature);
const ci = record.ci ?? null;
return {
refreshed,
feature,
reason,
push: record.push?.state ?? null,
ci: ci ? { state: ci.state, done: ci.done, total: ci.total } : null
};
}
function readIsFresh(read, minAgeSeconds, now) {
if (!read || !minAgeSeconds || minAgeSeconds <= 0) return false;
const age = now.getTime() - Date.parse(read.at);
return Number.isFinite(age) && age >= 0 && age < minAgeSeconds * 1e3;
}
async function refreshDelivery(input) {
const { projectRoot, run } = input;
const now = input.now ?? (() => /* @__PURE__ */ new Date());
const skip = (reason2) => ({
refreshed: false,
feature: null,
reason: reason2,
push: null,
ci: null
});
const policy = loadGitWorkflow(projectRoot).config;
if (!policy.enforcement.enabled) return skip("delivery policy is off");
const branch = await readLine(run, "git", [
"-C",
projectRoot,
"rev-parse",
"--abbrev-ref",
"HEAD"
]);
if (!branch || branch === "HEAD") return skip("no branch");
const feature = resolveDeliveryFeatureByBranch(projectRoot, branch);
if (!feature) return skip("no change on this branch");
const stored = readFeatureDelivery(projectRoot, feature).ci_read;
if (readIsFresh(stored, input.minAgeSeconds, now())) {
return storedOutcome(projectRoot, feature, false, "fresh");
}
const startedAt = now().toISOString();
const result = await evaluateDelivery({ projectRoot, policy, run });
if (!result.ran) return skip("no branch");
saveDeliveryReading(projectRoot, result, now, { startedAt, keepOnGhFailure: true });
const reason = ciReadOf(result).reason;
return storedOutcome(projectRoot, feature, result.ghAvailable, reason);
}
function isBlockingFinding(finding) {
return BLOCKING_CODES.has(finding.code);
}
function formatDeliverySummary(result, blocking = false) {
if (result.findings.length === 0) {
return "";
}
const lines = result.findings.map(
(finding) => `> - ${blocking && isBlockingFinding(finding) ? "\u{1F534}" : "\u{1F7E1}"} ${finding.message}`
);
const lead = blocking ? `> \u{1F534} Needs your attention: this team's git workflow (agents: block) holds this turn until these are fixed:
` : `> Heads up \u2014 a delivery convention to tidy before you push (not blocking):
`;
return `**\u25B8 paqad** \xB7 checked how this change would ship
` + lead + lines.join("\n");
}
async function runDeliveryCapability(projectRoot, seam, run) {
if (seam !== "completion") {
return NO_OP;
}
const loaded = loadGitWorkflow(projectRoot);
const policy = loaded.config;
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
const result = await evaluateDelivery({ projectRoot, policy, run, errors: loaded.errors });
if (!result.ran) {
return NO_OP;
}
if (loaded.valid) saveDeliveryReading(projectRoot, result, void 0, { startedAt });
recordProjectEvent(
projectRoot,
DELIVERY_EVIDENCE_DOC_TYPE,
{
kind: "delivery-check",
branch: result.branch,
commit: result.commit,
gh_available: result.ghAvailable,
findings: result.findings
},
DELIVERY_EVIDENCE_SCHEMA_VERSION
);
const blocking = loaded.valid && policy.enforcement.agents === "block" && result.findings.some((finding) => isBlockingFinding(finding));
const summary = formatDeliverySummary(result, blocking);
return summary ? { ran: true, blocking, summary } : NO_OP;
}
var execaCommandRunner = async (command, args) => {
const result = await execa(command, args, { reject: false, timeout: GH_TIMEOUT_MS });
const missing = result.code === "ENOENT" || result.exitCode !== 0 && !result.timedOut && !isOnPath(command);
return {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode ?? 1,
timedOut: result.timedOut,
missing
};
};
// src/planning/decision-selfarm.ts
init_esm_shims();
import { readFileSync as readFileSync4 } from "fs";
var ARMED_CATEGORIES = [
{ category: "create-vs-reuse", minConfidence: 0.92 },
{ category: "architecture-path", minConfidence: 0.9 }
];
var TTL_DAYS = 30;
var NO_OP2 = { ran: false, blocking: false, summary: "" };
function isTruthy(value) {
return value !== void 0 && ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
}
function isSelfArmEnabled(projectRoot, env, opts = {}) {
const envValue = env.PAQAD_DECISION_SELFARM;
if (envValue !== void 0 && envValue !== "") {
return isTruthy(envValue);
}
const configValue = readDotConfig(projectRoot).get("decision_selfarm");
if (configValue !== void 0 && configValue !== "") {
return isTruthy(configValue);
}
return opts.featureDevelopment === true;
}
function sessionOnFeatureDevelopment(projectRoot, sessionId) {
if (!sessionId) return false;
const state = readWorkflowState(projectRoot, resolveSessionId(projectRoot, sessionId));
if (state.active && isFeatureDevelopmentRoute(state.active.workflow)) return true;
return state.paused.some((entry) => isFeatureDevelopmentRoute(entry.workflow));
}
function lastUserPromptFromTranscript(transcriptText) {
const lines = transcriptText.split("\n");
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim();
if (!line) continue;
let entry;
try {
entry = JSON.parse(line);
} catch {
continue;
}
const record = entry;
const role = record.message?.role ?? record.role ?? record.type;
if (role === "user") {
const text = extractText(
record.message?.content ?? record.content
);
if (text) return text;
}
}
return "";
}
function extractText(content) {
if (typeof content === "string") return content;
if (Array.isArray(content)) {
return content.map((part) => {
const block = part;
return block.type === "text" && typeof block.text === "string" ? block.text : "";
}).join(" ").trim();
}
return "";
}
function selfArmDecision(input) {
const armedFork = detectDecisionForks(input.promptText).find(
(fork) => ARMED_CATEGORIES.some(
(armed) => armed.category === fork.category && fork.confidence >= armed.minConfidence
)
);
if (!armedFork) {
return { minted: null, reason: "no-fork", category: null };
}
if (!input.sessionId) {
return { minted: null, reason: "no-session", category: null };
}
const category = armedFork.category;
const store = input.store ?? new DecisionStore(input.projectRoot);
store.initialize();
if (store.listPendingDecisionIds().length > 0) {
return { minted: null, reason: "pending-exists", category };
}
const targetFile = input.targetPath || "src/unknown.ts";
const options = decisionOptionsForCategory(input.projectRoot, category, targetFile).options;
const question = decisionQuestionForCategory(category);
const fingerprint2 = computeDecisionFingerprint({
category,
question,
option_keys: options.map((option) => option.option_key),
repo_state: { active_capabilities: ["coding"] }
});
if (store.findReusableDecision({ fingerprint: fingerprint2, category, options })) {
return { minted: null, reason: "already-decided", category };
}
const now = input.now?.() ?? /* @__PURE__ */ new Date();
const packet = {
decision_id: store.nextDecisionId(),
fingerprint: fingerprint2,
category,
question,
context: contextForCategory(category, targetFile),
options,
confidence: armedFork.confidence,
requested_by: "paqad",
task_session_id: input.sessionId,
created_at: now.toISOString(),
status: "pending",
ttl_until: new Date(now.getTime() + TTL_DAYS * 24 * 60 * 60 * 1e3).toISOString(),
invalidation_watch: [targetFile]
};
try {
store.writePending(packet);
return { minted: packet.decision_id, reason: "minted", category };
} catch {
return { minted: null, reason: "write-failed", category };
}
}
function contextForCategory(category, targetFile) {
return category === "architecture-path" ? `This looks like a which-path choice that affects ${targetFile}. Pick the path before more is built on it.` : `This looks like a reuse-or-create choice that affects ${targetFile}. Pick the path before more is built on it.`;
}
function mintedSummary(category) {
const headline = category === "architecture-path" ? `I hit a which-path choice that's yours to make` : `I hit a reuse-or-create choice that's yours to make`;
return `**\u25B8 paqad** \xB7 ${headline}
> I paused and wrote it up so you can pick the path \u2014 answer the decision, then I'll continue.`;
}
function runDecisionSelfArm(input) {
if (input.seam !== "pre-mutation") return NO_OP2;
const featureDevelopment = sessionOnFeatureDevelopment(
input.projectRoot,
input.payload?.sessionId ?? input.env.CLAUDE_SESSION_ID ?? null
);
if (!isSelfArmEnabled(input.projectRoot, input.env, { featureDevelopment })) return NO_OP2;
const transcriptPath = input.payload?.transcriptPath;
const sessionId = input.payload?.sessionId ?? null;
if (!transcriptPath || !sessionId) return NO_OP2;
let promptText;
try {
const read = input.readTranscript ?? ((path) => readFileSync4(path, "utf8"));
promptText = lastUserPromptFromTranscript(read(transcriptPath));
} catch {
return NO_OP2;
}
if (!promptText) return NO_OP2;
const result = selfArmDecision({
projectRoot: input.projectRoot,
promptText,
sessionId,
targetPath: input.payload?.targetPath ?? null
});
return result.minted && result.category ? { ran: true, blocking: false, summary: mintedSummary(result.category) } : (
/* v8 ignore stop */
NO_OP2
);
}
// src/spec/spec-change-guard.ts
init_esm_shims();
import { readFileSync as readFileSync10 } from "fs";
import { join as join12 } from "path";
// src/feature-evidence/projections.ts
init_esm_shims();
// src/feature-evidence/artifacts.ts
init_esm_shims();
import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
import { dirname as dirname5, join as join10 } from "path";
// src/visual-evidence/readiness.ts
init_esm_shims();
// src/site-map/store.ts
init_esm_shims();
var import_yaml = __toESM(require_dist(), 1);
import {
existsSync as existsSync4,
mkdirSync as mkdirSync2,
readFileSync as readFileSync5,
readdirSync as readdirSync3,
renameSync as renameSync2,
rmSync,
writeFileSync as writeFileSync2
} from "fs";
import { dirname as dirname3, join as join7 } from "path";
// src/site-map/schema.ts
init_esm_shims();
var import_ajv2 = __toESM(require_ajv(), 1);
// src/validators/schemas/app-map.schema.json
var app_map_schema_default = {
$schema: "http://json-schema.org/draft-07/schema#",
$id: "paqad://schemas/app-map.json",
title: "Site map \u2014 application behavioural map (app-map.yaml)",
type: "object",
additionalProperties: false,
required: ["schema_version", "app", "surfaces"],
properties: {
schema_version: { type: "integer", const: 1 },
app: { $ref: "#/$defs/app" },
actors: { type: "array", items: { $ref: "#/$defs/actor" } },
guards: { type: "array", items: { $ref: "#/$defs/guard" } },
areas: { type: "array", items: { $ref: "#/$defs/area" } },
surfaces: { type: "array", items: { $ref: "#/$defs/surface" } },
journeys: { type: "array", items: { $ref: "#/$defs/journeyIndexEntry" } }
},
$defs: {
appKind: {
type: "string",
enum: ["web", "api", "cli", "mobile", "desktop", "service", "llm-workflows"]
},
visibility: { type: "string", enum: ["frontstage", "backstage"] },
derivation: { type: "string", enum: ["static", "convention", "agent", "human"] },
confidence: { type: "string", enum: ["high", "medium", "low"] },
trustTier: {
type: "string",
enum: ["unverified", "inferred", "proven-in-code", "proven-by-test", "human-confirmed"]
},
guardKind: {
type: "string",
enum: [
"permission",
"role",
"feature-flag",
"auth-state",
"data-state",
"capability",
"environment"
]
},
guardRef: {
oneOf: [
{ type: "string", minLength: 1 },
{ type: "array", items: { type: "string", minLength: 1 } }
]
},
evidenceItem: {
type: "object",
additionalProperties: false,
required: ["file"],
properties: {
file: { type: "string", minLength: 1 },
line: { type: "integer", minimum: 1 },
note: { type: "string" }
}
},
evidenceRef: {
oneOf: [
{ $ref: "#/$defs/evidenceItem" },
{ type: "array", items: { $ref: "#/$defs/evidenceItem" } }
]
},
endsValue: { type: ["boolean", "string"] },
ends: {
type: "object",
additionalProperties: false,
properties: {
success: { $ref: "#/$defs/endsValue" },
failure: { $ref: "#/$defs/endsValue" },
abandoned: { $ref: "#/$defs/endsValue" }
}
},
app: {
type: "object",
additionalProperties: false,
required: ["name", "kind"],
properties: {
name: { type: "string", minLength: 1 },
kind: {
oneOf: [
{ $ref: "#/$defs/appKind" },
{ type: "array", items: { $ref: "#/$defs/appKind" }, minItems: 1 }
]
},
frameworks: { type: "array", items: { type: "string" } },
generated_from: { type: "string" },
freshness: {
type: "object",
additionalProperties: false,
required: ["anchors_total", "anchors_resolved", "anchors_broken"],
properties: {
anchors_total: { type: "integer", minimum: 0 },
anchors_resolved: { type: "integer", minimum: 0 },
anchors_broken: { type: "integer", minimum: 0 }
}
},
language: {
oneOf: [
{ type: "string", minLength: 1 },
{
type: "object",
additionalProperties: false,
required: ["default_locale"],
properties: {
default_locale: { type: "string", minLength: 1 },
locales: { type: "array", items: { type: "string" } },
catalogs: { type: "array", items: { type: "string" } },
label_policy: { type: "string" }
}
}
]
}
}
},
actor: {
type: "object",
additionalProperties: false,
required: ["id", "label"],
properties: {
id: { type: "string", minLength: 1 },
label: { type: "string", minLength: 1 },
satisfies: { type: "array", items: { type: "string" } },
note: { type: "string" }
}
},
guard: {
type: "object",
additionalProperties: false,
required: ["id", "kind", "label"],
properties: {
id: { type: "string", minLength: 1 },
kind: { $ref: "#/$defs/guardKind" },
label: { type: "string", minLength: 1 },
requires: { type: "string" },
satisfy_via: { type: "string" },
evidence: { $ref: "#/$defs/evidenceRef" },
flag_key: { type: "string" },
required_variant: { type: "string" },
provider: { type: "string" },
owner: { type: "string" },
lifecycle: {
type: "string",
enum: ["new", "active", "launched", "inactive", "stale"]
},
introduced: { type: "string" },
note: { type: "string" },
trust: { $ref: "#/$defs/trustTier" }
}
},
area: {
type: "object",
additionalProperties: false,
required: ["id", "label"],
properties: {
id: { type: "string", minLength: 1 },
label: { type: "string", minLength: 1 },
visibility: { $ref: "#/$defs/visibility" },
guard: { $ref: "#/$defs/guardRef" }
}
},
surfaceEntry: {
type: "object",
additionalProperties: false,
required: ["kind"],
properties: {
kind: { type: "string", minLength: 1 },
value: { type: "string" },
deep_link: { type: "boolean" }
}
},
surfaceVariant: {
type: "object",
additionalProperties: false,
required: ["flag", "variant"],
properties: {
flag: { type: "string", minLength: 1 },
variant: { type: "string", minLength: 1 },
adds: { type: "string" },
changes: { type: "string" }
}
},
handOff: {
type: "object",
additionalProperties: false,
properties: {
carries: { type: "string" },
returns: { type: "boolean" }
}
},
flowStep: {
type: "object",
additionalProperties: false,
required: ["id"],
properties: {
id: { type: "string", minLength: 1 },
label: { type: "string" },
type: { type: "string" },
visibility: { $ref: "#/$defs/visibility" },
visible: { type: "string" },
artifact: { type: "string" },
presents: { type: "string" },
decisions: { type: "array", items: { type: "string" } },
resumable: { type: "boolean" },
inputs: { type: "array", items: { type: "string" } },
outputs: { type: "array", items: { type: "string" } }
}
},
transition: {
type: "object",
additionalProperties: false,
required: ["to", "trigger"],
properties: {
to: { type: "string", minLength: 1 },
trigger: { type: "string", minLength: 1 },
guard: { $ref: "#/$defs/guardRef" },
guard_not: { type: "string" },
note: { type: "string" },
via: { type: "string" },
carries: { type: "string" },
visibility: { $ref: "#/$defs/visibility" },
evidence: { $ref: "#/$defs/evidenceRef" },
confidence: { $ref: "#/$defs/confidence" },
trust: { $ref: "#/$defs/trustTier" },
mode: { type: "string", enum: ["link", "call"] },
guard_text: { type: "string" },
guard_mode: { type: "string", enum: ["deterministic", "llm-judged"] },
utterances: { type: "array", items: { type: "string" } },
fallback: { type: "boolean" },
routing_description: { type: "string" }
}
},
surface: {
type: "object",
additionalProperties: false,
required: ["id", "kind", "label"],
properties: {
id: { type: "string", minLength: 1 },
kind: {
type: "string",
enum: [
"page",
"screen",
"modal",
"action",
"api",
"cli-command",
"job",
"webhook",
"email",
"external-system",
"prompt-entry",
"router",
"llm-workflow",
"step",
"decision-pause",
"handoff",
"subflow",
"terminal"
]
},
label: { type: "string", minLength: 1 },
label_key: { type: "string" },
label_source: { type: "string" },
labels: { type: "object", additionalProperties: { type: "string" } },
area: { type: "string" },
entry: { $ref: "#/$defs/surfaceEntry" },
page_type: { type: "string" },
module: { type: "string" },
guard: { $ref: "#/$defs/guardRef" },
guard_not: { type: "string" },
evidence: { $ref: "#/$defs/evidenceRef" },
derivation: { $ref: "#/$defs/derivation" },
confidence: { $ref: "#/$defs/confidence" },
trust: { $ref: "#/$defs/trustTier" },
visibility: { $ref: "#/$defs/visibility" },
variants: { type: "array", items: { $ref: "#/$defs/surfaceVariant" } },
variant_group: { type: "string" },
variant_of: { type: "string" },
status: { type: "string" },
oracle: { type: "object", additionalProperties: { type: "string" } },
hand_off: { $ref: "#/$defs/handOff" },
ends: { $ref: "#/$defs/ends" },
note: { type: "string" },
actor: { type: "string" },
intent: { type: "string" },
routing_description: { type: "string" },
utterances: { type: "array", items: { type: "string" } },
steps: { type: "array", items: { $ref: "#/$defs/flowStep" } },
transitions: { type: "array", items: { $ref: "#/$defs/transition" } }
}
},
journeyIndexEntry: {
type: "object",
additionalProperties: false,
required: ["id", "label"],
properties: {
id: { type: "string", minLength: 1 },
label: { type: "string", minLength: 1 },
actor: { type: "string" },
priority: { type: "string" },
status: { type: "string", enum: ["proposed", "confirmed", "locked"] },
trust: { $ref: "#/$defs/trustTier" }
}
}
}
};
// src/validators/schemas/journey.schema.json
var journey_schema_default = {
$schema: "http://json-schema.org/draft-07/schema#",
$id: "paqad://schemas/journey.json",
title: "Site map \u2014 curated journey (journeys/<id>.journey.yaml)",
type: "object",
additionalProperties: false,
required: ["schema_version", "id", "label", "actor", "goal", "entry", "steps", "status"],
properties: {
schema_version: { type: "integer", const: 1 },
id: { type: "string", minLength: 1 },
label: { type: "string", minLength: 1 },
actor: { type: "string", minLength: 1 },
on_behalf_of: { type: "string" },
goal: { type: "string", minLength: 1 },
entry: { type: "string", minLength: 1 },
uses: { type: "array", items: { type: "string" } },
steps: { type: "array", items: { $ref: "#/$defs/journeyStep" } },
branches: { type: "array", items: { type: "object" } },
ends: { $ref: "#/$defs/journeyEnds" },
next_journeys: { type: "array", items: { type: "string" } },
tests: {
type: "array",
items: {
oneOf: [
{ type: "string" },
{ type: "null" },
{
type: "object",
additionalProperties: false,
properties: {
spec: { type: ["string", "null"] },
variant: { type: "string" }
}
}
]
}
},
priority: { type: "string" },
status: { type: "string", enum: ["proposed", "confirmed", "locked"] },
derivation: { type: "string", enum: ["static", "convention", "agent", "human"] },
evidence: {
oneOf: [
{ $ref: "#/$defs/evidenceItem" },
{ type: "array", items: { $ref: "#/$defs/evidenceItem" } }
]
}
},
$defs: {
evidenceItem: {
type: "object",
additionalProperties: false,
required: ["file"],
properties: {
file: { type: "string", minLength: 1 },
line: { type: "integer", minimum: 1 },
note: { type: "string" }
}
},
journeyStep: {
type: "object",
additionalProperties: false,
required: ["surface"],
properties: {
surface: { type: "string", minLength: 1 },
action: { type: "string" },
expect: { type: "string" },
step: { type: "string" },
type: { type: "string" },
variant: {
type: "object",
additionalProperties: false,
properties: {
group: { type: "string" },
when: { type: "string" }
}
}
}
},
journeyEnds: {
type: "object",
additionalProperties: false,
properties: {
success: {},
failure: {},
abandoned: {}
}
}
}
};
// src/validators/schemas/journey-capture.schema.json
var journey_capture_schema_default = {
$schema: "http://json-schema.org/draft-07/schema#",
$id: "paqad://schemas/journey-capture.json",
title: "Site map \u2014 journey capture script (journeys/<id>.capture.yaml)",
type: "object",
additionalProperties: false,
required: ["schema_version", "journey", "steps"],
properties: {
schema_version: { type: "integer", const: 1 },
journey: { type: "string", minLength: 1 },
setup: {
type: "array",
items: { $ref: "#/$defs/setupStep" }
},
steps: {
type: "array",
minItems: 1,
items: { $ref: "#/$defs/captureStep" }
}
},
$defs: {
captureAction: {
type: "object",
additionalProperties: false,
required: ["selector", "do"],
properties: {
selector: { type: "string", minLength: 1 },
do: { type: "string", enum: ["click", "fill", "select", "press", "wait_for"] },
value: { type: "string" }
}
},
setupStep: {
type: "object",
additionalProperties: false,
properties: {
goto: { type: "string" },
actions: { type: "array", items: { $ref: "#/$defs/captureAction" } }
}
},
captureStep: {
type: "object",
additionalProperties: false,
required: ["journey_step"],
properties: {
journey_step: { type: "integer", minimum: 1 },
goto: { type: "string" },
actions: { type: "array", items: { $ref: "#/$defs/captureAction" } },
screenshot: { type: "boolean" }
}
}
}
};
// src/validators/schemas/site-map-answers.schema.json
var site_map_answers_schema_default = {
$schema: "http://json-schema.org/draft-07/schema#",
$id: "paqad://schemas/site-map-answers.json",
title: "Site-map creation answers (issue #466, Part A)",
type: "object",
additionalProperties: false,
required: ["schema_version", "generated_by", "answers"],
properties: {
schema_version: { type: "integer", const: 1 },
generated_by: { type: "string", const: "paqad-ai" },
answers: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: ["question_id", "category", "question", "answer", "decided_by", "anchors"],
properties: {
question_id: { type: "string", minLength: 1 },
category: {
type: "string",
enum: [
"app-kind",
"actors-roles",
"sensitive-surface",
"journey-priority",
"labels-language",
"grouping",
"tool-access",
"journey-scope"
]
},
question: { type: "string", minLength: 1 },
answer: { type: "string" },
decided_by: { type: "string", enum: ["human", "default"] },
anchors: { type: "array", items: { type: "string" } }
}
}
}
}
};
// src/site-map/schema.ts
var ajv2 = new import_ajv2.default({ allErrors: true, allowUnionTypes: true });
var compiledAppMap;
var compiledJourney;
var compiledJourneyCapture;
var compiledAnswers;
function appMapValidator() {
if (!compiledAppMap) {
compiledAppMap = ajv2.compile(app_map_schema_default);
}
return compiledAppMap;
}
function journeyValidator() {
if (!compiledJourney) {
compiledJourney = ajv2.compile(journey_schema_default);
}
return compiledJourney;
}
function journeyCaptureValidator() {
if (!compiledJourneyCapture) {
compiledJourneyCapture = ajv2.compile(journey_capture_schema_default);
}
return compiledJourneyCapture;
}
function answersValidator() {
if (!compiledAnswers) {
compiledAnswers = ajv2.compile(site_map_answers_schema_default);
}
return compiledAnswers;
}
function toValidation(validate, data) {
if (validate(data)) {
return { valid: true, errors: [] };
}
const rawErrors = validate.errors ?? [];
const errors = rawErrors.map((error) => {
const path = error.instancePath.length > 0 ? error.instancePath : "/";
const message = error.message ?? "invalid";
return `${path} ${message}`.trim();
});
return { valid: false, errors };
}
function validateAppMap(data) {
return toValidation(appMapValidator(), data);
}
function validateJourney(data) {
return toValidation(journeyValidator(), data);
}
function validateJourneyCapture(data) {
return toValidation(journeyCaptureValidator(), data);
}
function validateSiteMapAnswers(data) {
return toValidation(answersValidator(), data);
}
// src/site-map/store.ts
var SiteMapSchemaError = class extends Error {
constructor(message, errors) {
super(message);
this.errors = errors;
this.name = "SiteMapSchemaError";
}
errors;
};
var JOURNEY_SUFFIX = ".journey.yaml";
function canonicalAppMapPath(projectRoot) {
return join7(projectRoot, PATHS.SITE_MAP_CANONICAL_APP_MAP);
}
function journeysDir(projectRoot) {
return join7(projectRoot, PATHS.SITE_MAP_CANONICAL_JOURNEYS_DIR);
}
function journeyPath(projectRoot, id) {
return join7(journeysDir(projectRoot), `${id}${JOURNEY_SUFFIX}`);
}
function writeYamlAtomic(target, data) {
mkdirSync2(dirname3(target), { recursive: true });
const tmp = `${target}.tmp`;
writeFileSync2(tmp, import_yaml.default.stringify(data), "utf8");
renameSync2(tmp, target);
return target;
}
function readYaml(target) {
try {
return import_yaml.default.parse(readFileSync5(target, "utf8"));
} catch {
return null;
}
}
function writeCanonicalSiteMap(projectRoot, map) {
const result = validateAppMap(map);
if (!result.valid) {
throw new SiteMapSchemaError("canonical app-map failed schema validation", result.errors);
}
return writeYamlAtomic(canonicalAppMapPath(projectRoot), map);
}
function readCanonicalSiteMap(projectRoot) {
const parsed = readYaml(canonicalAppMapPath(projectRoot));
if (parsed === null) return null;
return validateAppMap(parsed).valid ? parsed : null;
}
function writeJourney(projectRoot, journey) {
const result = validateJourney(journey);
if (!result.valid) {
throw new SiteMapSchemaError(`journey "${journey.id}" failed schema validation`, result.errors);
}
return writeYamlAtomic(journeyPath(projectRoot, journey.id), journey);
}
function removeJourney(projectRoot, id) {
const target = journeyPath(projectRoot, id);
if (!existsSync4(target)) return false;
rmSync(target, { force: true });
return true;
}
function readJourneyAt(path) {
const parsed = readYaml(path);
if (parsed === null) return null;
return validateJourney(parsed).valid ? parsed : null;
}
function listJourneyIdsIn(dir) {
let names;
try {
names = readdirSync3(dir);
} catch {
return [];
}
return names.filter((name) => name.endsWith(JOURNEY_SUFFIX)).map((name) => name.slice(0, -JOURNEY_SUFFIX.length)).sort();
}
function readAllJourneysIn(dir) {
const journeys = [];
for (const id of listJourneyIdsIn(dir)) {
const journey = readJourneyAt(join7(dir, `${id}${JOURNEY_SUFFIX}`));
if (journey !== null) {
journeys.push(journey);
}
}
return journeys;
}
function readJourney(projectRoot, id) {
return readJourneyAt(journeyPath(projectRoot, id));
}
function readAllJourneys(projectRoot) {
return readAllJourneysIn(journeysDir(projectRoot));
}
// src/visual-evidence/capture-script.ts
init_esm_shims();
import { existsSync as existsSync5, readdirSync as readdirSync4 } from "fs";
var CAPTURE_SUFFIX = ".capture.yaml";
function hasValue(value) {
return typeof value === "string" && value.length > 0;
}
function actionProblems(action, where) {
const problems = [];
const { do: verb, value } = action;
if ((verb === "fill" || verb === "select" || verb === "press") && !hasValue(value)) {
problems.push(`${where}: \`${verb}\` requires a value`);
}
if (verb === "wait_for" && value !== "visible" && value !== "hidden") {
problems.push(`${where}: \`wait_for\` requires a value of "visible" or "hidden"`);
}
return problems;
}
function crossCheck(script, basename, journey) {
const problems = [];
if (basename !== script.journey) {
problems.push(
`journey "${script.journey}" must equal the sibling file id "${basename}" (${basename}${CAPTURE_SUFFIX})`
);
}
if (!journey) {
problems.push(`journey "${script.journey}" does not resolve to an existing journey`);
return problems;
}
if (journey.status !== "confirmed") {
problems.push(
`journey "${script.journey}" is ${journey.status}, not confirmed \u2014 only confirmed journeys produce evidence`
);
}
const stepCount = journey.steps.length;
let previous = 0;
for (const step of script.steps) {
if (step.journey_step < 1 || step.journey_step > stepCount) {
problems.push(
`journey_step ${step.journey_step} does not exist in journey "${script.journey}" (${stepCount} steps)`
);
}
if (step.journey_step < previous) {
problems.push(
`steps must be ordered ascending by journey_step (saw ${step.journey_step} after ${previous})`
);
}
previous = step.journey_step;
for (const [i, action] of (step.actions ?? []).entries()) {
problems.push(...actionProblems(action, `step ${step.journey_step} action ${i + 1}`));
}
}
for (const [i, setup] of (script.setup ?? []).entries()) {
for (const [j, action] of (setup.actions ?? []).entries()) {
problems.push(...actionProblems(action, `setup ${i + 1} action ${j + 1}`));
}
}
return problems;
}
function listCaptureScriptIds(projectRoot) {
const dir = journeysDir(projectRoot);
if (!existsSync5(dir)) return [];
return readdirSync4(dir).filter((name) => name.endsWith(CAPTURE_SUFFIX)).map((name) => name.slice(0, -CAPTURE_SUFFIX.length)).sort();
}
function loadCaptureScripts(projectRoot) {
const scripts = [];
const errors = [];
const dir = journeysDir(projectRoot);
for (const basename of listCaptureScriptIds(projectRoot)) {
const file = `docs/site-map/journeys/${basename}${CAPTURE_SUFFIX}`;
const parsed = readYaml(join(dir, `${basename}${CAPTURE_SUFFIX}`));
if (parsed === null) {
errors.push({ file, errors: ["file is missing or not valid YAML"] });
continue;
}
const shape = validateJourneyCapture(parsed);
if (!shape.valid) {
errors.push({ file, errors: shape.errors });
continue;
}
const script = parsed;
const journey = readJourney(projectRoot, script.journey);
const problems = crossCheck(script, basename, journey);
if (problems.length > 0) {
errors.push({ file, errors: problems });
continue;
}
scripts.push({ file, script, journey });
}
return { scripts, errors };
}
function deriveCaption(journey, journeyStep) {
const step = journey.steps[journeyStep - 1];
const action = step?.action ?? "";
const expect = step?.expect;
return expect ? `${action}. ${expect}` : action;
}
// src/visual-evidence/provision.ts
init_esm_shims();
import { createRequire } from "module";
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
import { homedir } from "os";
import { join as join8 } from "path";
function resolveVeRuntimeDir() {
return join8(homedir(), ".paqad-ai", "ve-runtime");
}
function provisionRecordPath(veRuntime) {
return join8(veRuntime, "provision.json");
}
function browsersPath(veRuntime) {
return join8(veRuntime, "browsers");
}
function resolvePlaywrightModulePath(veRuntime) {
const req = createRequire(join8(veRuntime, "package.json"));
return req.resolve("playwright");
}
function browserStatus(veRuntime = resolveVeRuntimeDir()) {
let raw;
try {
raw = readFileSync6(provisionRecordPath(veRuntime), "utf8");
} catch {
return existsSync6(veRuntime) ? "broken" : "missing";
}
try {
JSON.parse(raw);
resolvePlaywrightModulePath(veRuntime);
return "provisioned";
} catch {
return "broken";
}
}
async function provisionBrowser(veRuntime = resolveVeRuntimeDir(), now = () => (/* @__PURE__ */ new Date()).toISOString()) {
mkdirSync3(veRuntime, { recursive: true });
const pkgPath = join8(veRuntime, "package.json");
writeFileSync3(pkgPath, `${JSON.stringify({ private: true }, null, 2)}
`, "utf8");
const env = { ...process.env, PLAYWRIGHT_BROWSERS_PATH: browsersPath(veRuntime) };
await execa("npm", ["install", "playwright@^1", "--no-audit", "--no-fund"], {
cwd: veRuntime,
env
});
await execa("npx", ["playwright", "install", "chromium"], { cwd: veRuntime, env });
const playwrightVersion = readInstalledPlaywrightVersion(veRuntime);
const record = {
schema_version: 1,
playwright_version: playwrightVersion,
provisioned_at: now()
};
writeFileSync3(provisionRecordPath(veRuntime), `${JSON.stringify(record, null, 2)}
`, "utf8");
return record;
}
function readInstalledPlaywrightVersion(veRuntime) {
const req = createRequire(join8(veRuntime, "package.json"));
const pkg = JSON.parse(readFileSync6(req.resolve("playwright/package.json"), "utf8"));
return pkg.version;
}
function veBrowsersPath(veRuntime = resolveVeRuntimeDir()) {
return browsersPath(veRuntime);
}
// src/visual-evidence/trigger.ts
init_esm_shims();
function frontendGlobToRegExp(glob) {
let out = "";
let braceDepth = 0;
for (let i = 0; i < glob.length; i += 1) {
const ch = glob[i];
const next = glob[i + 1];
if (ch === "*" && next === "*") {
if (glob[i + 2] === "/") {
out += "(?:.*/)?";
i += 2;
} else {
out += ".*";
i += 1;
}
continue;
}
switch (ch) {
case "*":
out += "[^/]*";
break;
case "?":
out += "[^/]";
break;
case "{":
out += "(?:";
braceDepth += 1;
break;
case "}":
if (braceDepth > 0) {
out += ")";
braceDepth -= 1;
} else {
out += "\\}";
}
break;
case ",":
out += braceDepth > 0 ? "|" : "\\,";
break;
case ".":
case "+":
case "^":
case "$":
case "(":
case ")":
case "[":
case "]":
case "|":
case "\\":
out += `\\${ch}`;
break;
default:
out += ch;
}
}
return new RegExp(`^${out}$`);
}
function matchesFrontendGlob(file, glob) {
return frontendGlobToRegExp(glob).test(file);
}
function toPosix(file) {
return file.replace(/\\/g, "/");
}
function activeFrameworks(projectRoot) {
const profile = readProjectProfile(projectRoot);
if (!profile) return [];
const frameworks = profile.stack_profile?.frameworks ?? [];
const primary = getPrimaryStack({ stack_profile: profile.stack_profile });
return [...new Set([...frameworks, primary].filter((name) => Boolean(name)))];
}
var PackRegistryEmptyError = class extends Error {
runtimeRoot;
frameworks;
constructor(runtimeRoot, frameworks) {
super(
`paqad could not load its built-in stack packs (looked in ${runtimeRoot}) for ${frameworks.join(", ")}.`
);
this.name = "PackRegistryEmptyError";
this.runtimeRoot = runtimeRoot;
this.frameworks = frameworks;
}
};
function packRegistryFaultDetail(runtimeRoot) {
return `paqad could not load its built-in stack packs (looked in ${runtimeRoot}). This is an install fault, not a clean change.`;
}
var PACK_REGISTRY_FAULT_REMEDIATION = "run `paqad-ai doctor` and reinstall paqad-ai.";
function activeFrontendGlobs(projectRoot) {
const frameworks = activeFrameworks(projectRoot);
const packs = getPacksForFrameworks(frameworks, projectRoot);
if (frameworks.length > 0 && packs.length === 0) {
throw new PackRegistryEmptyError(getRuntimeRoot(), frameworks);
}
const out = [];
for (const pack of packs) {
for (const glob of pack.manifest.visual_evidence?.frontend_globs ?? []) {
out.push({ pack: pack.manifest.name, glob });
}
}
return out;
}
function frontendFilesOf(projectRoot, changedFiles) {
const globs = activeFrontendGlobs(projectRoot);
const matched = changedFiles.map(toPosix).filter((file) => globs.some(({ glob }) => matchesFrontendGlob(file, glob)));
return [...new Set(matched)].sort();
}
function frontendTriggerOrFault(projectRoot, changedFiles) {
try {
const files = frontendFilesOf(projectRoot, changedFiles);
return { triggered: files.length > 0, files, fault: null };
} catch (error) {
if (error instanceof PackRegistryEmptyError) {
return { triggered: false, files: [], fault: { runtimeRoot: error.runtimeRoot } };
}
throw error;
}
}
async function evaluateFrontendTrigger(projectRoot) {
const change = await loadChangeEvidence(projectRoot);
const changed = change.files.map(toPosix);
const globs = activeFrontendGlobs(projectRoot);
const matchedFiles = /* @__PURE__ */ new Set();
const matchedGlobs = /* @__PURE__ */ new Set();
const packs = /* @__PURE__ */ new Set();
if (change.source !== "none") {
for (const file of changed) {
for (const { pack, glob } of globs) {
if (matchesFrontendGlob(file, glob)) {
matchedFiles.add(file);
matchedGlobs.add(glob);
packs.add(pack);
}
}
}
}
return {
triggered: matchedFiles.size > 0,
matched_files: [...matchedFiles].sort(),
matched_globs: [...matchedGlobs].sort(),
packs: [...packs].sort(),
changed_files: changed,
source: change.source
};
}
// src/visual-evidence/readiness.ts
function visualEvidenceReadiness(projectRoot) {
const problems = [];
if (!resolveFrameworkConfig(projectRoot).features.site_map) {
problems.push({
code: "site-map-off",
detail: "visual_evidence is on but site_map is off \u2014 capture scripts derive from confirmed site-map journeys, so nothing can be captured.",
remediation: "Turn on site_map (env PAQAD_SITE_MAP) and author a confirmed journey + capture script."
});
}
if (!readAllJourneys(projectRoot).some((journey) => journey.status === "confirmed")) {
problems.push({
code: "no-confirmed-journey",
detail: "visual_evidence is on but no confirmed journey exists under docs/site-map/journeys/ \u2014 only confirmed journeys produce evidence.",
remediation: "Confirm a journey (paqad-ai sitemap journey confirm) so it can back a capture script."
});
}
if (listCaptureScriptIds(projectRoot).length === 0) {
problems.push({
code: "no-capture-script",
detail: "visual_evidence is on but no *.capture.yaml exists under docs/site-map/journeys/ \u2014 there is nothing to capture.",
remediation: "Author a docs/site-map/journeys/<id>.capture.yaml for a confirmed journey (see docs/modules/visual-evidence)."
});
}
if (browserStatus() !== "provisioned") {
problems.push({
code: "browser-not-provisioned",
detail: "visual_evidence is on but the capture browser is not provisioned \u2014 a capture run would record playwright-not-provisioned.",
remediation: "Run `paqad-ai visual-evidence setup` to provision Playwright + Chromium under ~/.paqad-ai/ve-runtime."
});
}
return problems;
}
var READINESS_DECISION_TITLE = "Visual evidence is on, but I can't capture screenshots here yet";
var READINESS_WAIVE_OPTION = "waive";
function readinessToken(dirName) {
return `[paqad-ve-readiness ${dirName}]`;
}
function visualEvidenceFlagOn(projectRoot) {
if (!resolveFrameworkConfig(projectRoot).features.visual_evidence) return false;
return readProjectProfile(projectRoot)?.active_capabilities?.includes("coding") ?? false;
}
function openVisualEvidenceReadinessPause(input) {
const { projectRoot, dirName } = input;
if (!visualEvidenceFlagOn(projectRoot)) return null;
if (!frontendTriggerOrFault(projectRoot, [...input.files]).triggered) return null;
const problems = visualEvidenceReadiness(projectRoot);
if (problems.length === 0) return null;
const token = readinessToken(dirName);
if (readContractDecisions(projectRoot).some(({ packet }) => packet.context.includes(token))) {
return null;
}
const reasons = problems.map((problem) => `- ${problem.detail}`).join("\n");
const { id, reused } = createPendingDecision(projectRoot, {
category: "workflow-or-tool",
title: READINESS_DECISION_TITLE,
context: `This change touches frontend files and visual evidence is on, but a capture run cannot work here yet:
${reasons}
Pick how this change gets its visual evidence. ${token}`,
options: [
{ option_key: "setup", label: "Set it up now (site map journey, capture script, browser)" },
{
option_key: "attach",
label: "Attach my own screenshots with paqad-ai visual-evidence attach"
},
{ option_key: READINESS_WAIVE_OPTION, label: "Record a waiver for this change" }
],
recommendation: "setup",
change: featureChangeKey(dirName)
});
return reused ? null : id;
}
function visualAcRequiredFiles(projectRoot, files) {
if (!visualEvidenceFlagOn(projectRoot)) return [];
return frontendTriggerOrFault(projectRoot, [...new Set(files)]).files;
}
function findVisualEvidenceWaiver(projectRoot, dirName) {
const token = readinessToken(dirName);
for (const { packet, status } of readContractDecisions(projectRoot)) {
if (status !== "resolved" || !packet.context.includes(token)) continue;
if (packet.chosen === READINESS_WAIVE_OPTION) return packet.id;
}
return null;
}
function visualEvidenceWaiverHint(projectRoot, dirName) {
const token = readinessToken(dirName);
const pending = readContractDecisions(projectRoot).find(
({ packet, status }) => status === "pending" && packet.context.includes(token)
);
if (pending) {
return `resolve the pending readiness decision with \`paqad-ai decision resolve ${pending.packet.id} ${READINESS_WAIVE_OPTION}\``;
}
return `create one with \`paqad-ai decision create --category workflow-or-tool --title "Visual evidence waiver" --context "<why no screenshots> ${token}" --option ${READINESS_WAIVE_OPTION}="Waive visual evidence" --option attach="Attach screenshots"\` and resolve it with \`paqad-ai decision resolve <D-id> ${READINESS_WAIVE_OPTION}\``;
}
// src/feature-evidence/lane-resize.ts
init_esm_shims();
// src/authoring/config.ts
init_esm_shims();
var RANGE_OF = {
skill_orchestrator_enabled: "boolean",
authoring_trigger_threshold: "share",
authoring_trigger_min_runs: "count",
skill_eval_min_gain_points: "count",
skill_eval_max_token_multiple: "multiple",
authoring_runs_per_arm: "count",
authoring_max_iterations: "count",
authoring_fact_max_age_days: "count",
authoring_collision_jaccard: "share",
authoring_description_budget_chars: "count",
skill_body_max_tokens: "count",
authoring_tool_result_max_tokens: "count",
authoring_cache_hit_min: "share",
authoring_regression_tolerance: "share",
authoring_dark_window_days: "count",
authoring_batch_mode: "batch",
authoring_run_max_cost_usd: "cost",
authoring_otel_endpoint: "endpoint",
agent_orchestrator_enabled: "boolean",
agent_pass_k_threshold: "share",
agent_stage_pass_k_threshold: "share",
agent_delegation_threshold: "share",
agent_description_max_tokens: "count",
agent_report_max_tokens: "count",
agent_prompt_max_tokens: "count",
agent_max_tools: "count",
agent_max_cost_multiple: "multiple",
agent_catalog_description_max_tokens: "count",
agent_memory_max_lines: "count"
};
var ALLOWED_TEXT = {
boolean: "true or false",
share: "a number from 0 to 1",
count: "a positive whole number",
multiple: "a number of 1 or more",
cost: "a number above 0",
batch: AUTHORING_BATCH_MODES.join(" or "),
endpoint: "empty or an http(s) URL"
};
var TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
var FALSY = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
var AUTHORING_CONFIG_KEYS = Object.keys(
RANGE_OF
);
function defaultOf(key) {
const spec = FRAMEWORK_CONFIG_SPECS.find(
(entry) => entry.key === key && entry.section === AUTHORING_SECTION
);
if (spec?.default === void 0) {
throw new Error(`authoring config key ${key} has no spec in FRAMEWORK_CONFIG_SPECS`);
}
return spec.default;
}
function isHttpUrl(raw) {
try {
const url = new URL(raw);
return url.protocol === "http:" || url.protocol === "https:";
} catch {
return false;
}
}
function parseValue(kind, raw) {
const text = raw.trim();
if (kind === "boolean") {
const lower = text.toLowerCase();
if (TRUTHY.has(lower)) return true;
if (FALSY.has(lower)) return false;
return void 0;
}
if (kind === "batch") {
return AUTHORING_BATCH_MODES.includes(text) ? text : void 0;
}
if (kind === "endpoint") {
return text === "" || isHttpUrl(text) ? text : void 0;
}
if (text === "") return void 0;
const value = Number(text);
if (!Number.isFinite(value)) return void 0;
switch (kind) {
case "share":
return value >= 0 && value <= 1 ? value : void 0;
case "count":
return Number.isInteger(value) && value > 0 ? value : void 0;
case "multiple":
return value >= 1 ? value : void 0;
case "cost":
return value > 0 ? value : void 0;
}
}
function authoringConfigProblems(projectRoot, env = process.env) {
const map = layeredConfigMap(projectRoot, env);
const problems = [];
for (const key of AUTHORING_CONFIG_KEYS) {
const raw = map.get(key);
if (raw === void 0) continue;
if (parseValue(RANGE_OF[key], raw) === void 0) {
problems.push({ key, value: raw, allowed: ALLOWED_TEXT[RANGE_OF[key]] });
}
}
return problems;
}
function readAuthoringConfig(projectRoot, env = process.env) {
const map = layeredConfigMap(projectRoot, env);
const config = {};
for (const key of AUTHORING_CONFIG_KEYS) {
const raw = map.get(key);
const parsed = raw === void 0 ? void 0 : parseValue(RANGE_OF[key], raw);
config[key] = parsed ?? defaultOf(key);
}
return config;
}
function skillOrchestratorEnabled(projectRoot, env = process.env) {
return readAuthoringConfig(projectRoot, env).skill_orchestrator_enabled;
}
function agentOrchestratorEnabled(projectRoot, env = process.env) {
return readAuthoringConfig(projectRoot, env).agent_orchestrator_enabled;
}
// src/feature-evidence/lane-resize.ts
var SMALL_CHANGE_MAX_FILES = 8;
var RISKY_DATABASE_IMPACTS = /* @__PURE__ */ new Set(["schema-change", "data-migration"]);
var RISKY_API_IMPACTS = /* @__PURE__ */ new Set(["breaking-change"]);
function planFiles(steps) {
return [...new Set((steps ?? []).flatMap((step) => step.files ?? []))];
}
function sensitivePaths(projectRoot, files) {
return files.filter((file) => resolveEffectiveLane(projectRoot, file, "graduated") === "full");
}
function isRiskyImpact(impact) {
return impact === null || RISKY_DATABASE_IMPACTS.has(impact.database_impact) || RISKY_API_IMPACTS.has(impact.api_impact);
}
function editedFiles(changedFiles) {
return [...new Set(changedFiles)].filter((file) => !/^\.paqad(\/|$)/.test(file));
}
function fileCount(count) {
return `${count} file${count === 1 ? "" : "s"}`;
}
function planLaneResize(input) {
if (!input.changesKnown) return null;
const record = readFeatureRecord(input.projectRoot, input.dirName);
if (record?.lane !== "full" || record.lane_source !== "classifier" || record.lane_resized) {
return null;
}
const files = planFiles(input.steps);
if (files.length > SMALL_CHANGE_MAX_FILES || sensitivePaths(input.projectRoot, files).length > 0 || isRiskyImpact(readPendingImpact(input.projectRoot, input.sessionId)) || editedFiles(input.changedFiles).length > 0) {
return null;
}
const at = (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
const written = updateFeatureRecord(
input.projectRoot,
input.dirName,
{
resizeLaneFromPlan: {
reason: `the plan touches ${fileCount(files.length)}, none sensitive, no schema or breaking API change`,
files: files.length,
at
}
},
input.now
);
return written?.lane_source === "plan" && written.lane_resized?.at === at ? written.lane_resized : null;
}
function raiseFacts(input) {
const edited = editedFiles(input.changedFiles);
if (edited.length > SMALL_CHANGE_MAX_FILES) {
return { reason: `the change now touches ${fileCount(edited.length)}`, files: edited.length };
}
const sensitive = sensitivePaths(input.projectRoot, [
.../* @__PURE__ */ new Set([...edited, ...input.plannedFiles])
]);
return sensitive.length > 0 ? { reason: `the change touches a sensitive path: ${sensitive[0]}`, files: edited.length } : null;
}
function laneMayRise(projectRoot, dirName) {
const record = readFeatureRecord(projectRoot, dirName);
if (record?.lane !== "graduated" || record.lane_raised) return false;
return record.lane_source === "plan" || record.lane_raised_for_authoring !== void 0;
}
function raiseLaneIfGrown(input) {
if (!laneMayRise(input.projectRoot, input.dirName)) return null;
const facts = raiseFacts(input);
if (facts === null) return null;
const at = (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
const written = updateFeatureRecord(
input.projectRoot,
input.dirName,
{ raiseLaneFromChange: { ...facts, at } },
input.now
);
return written?.lane_raised?.at === at ? written.lane_raised : null;
}
var AUTHORING_KIND_WORDS = {
skill: { noun: "a skill", seat: "the Skill Orchestrator" },
agent: { noun: "an agent", seat: "the Agent Orchestrator" }
};
function authoringRaiseReason(match) {
const matches = typeof match === "string" ? [{ kind: "skill", path: match }] : match;
if (matches.length === 1) {
const words = AUTHORING_KIND_WORDS[matches[0].kind];
return `the change creates or edits ${words.noun} (${matches[0].path}), so ${words.seat} sits at the High Table.`;
}
const parts = matches.map((entry) => `${AUTHORING_KIND_WORDS[entry.kind].noun} (${entry.path})`);
const seats = matches.map((entry) => AUTHORING_KIND_WORDS[entry.kind].seat);
return `the change creates or edits ${parts.join(" and ")}, so ${seats.join(" and ")} sit at the High Table.`;
}
function authoringMatches(projectRoot, files) {
const skillsOn = skillOrchestratorEnabled(projectRoot);
const agentsOn = agentOrchestratorEnabled(projectRoot);
if (!skillsOn && !agentsOn) return [];
const context = readSkillPathContext(projectRoot);
const matches = [];
const skill = skillsOn ? findSkillPaths(files, context.adapters, context)[0] : void 0;
if (skill !== void 0) matches.push({ kind: "skill", path: skill });
const agent = agentsOn ? findAgentPaths(files, context.adapters, context)[0] : void 0;
if (agent !== void 0) matches.push({ kind: "agent", path: agent });
return matches;
}
function authoringRaiseEnabled(projectRoot) {
return skillOrchestratorEnabled(projectRoot) || agentOrchestratorEnabled(projectRoot);
}
function raiseLaneForAuthoring(input) {
const record = readFeatureRecord(input.projectRoot, input.dirName);
if (record?.lane !== "fast" || record.lane_raised_for_authoring !== void 0) return null;
if (!authoringRaiseEnabled(input.projectRoot)) return null;
const candidates = [.../* @__PURE__ */ new Set([...input.plannedFiles, ...editedFiles(input.changedFiles)])];
const paths = authoringMatches(input.projectRoot, candidates);
if (paths.length === 0) return null;
const at = (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
const written = updateFeatureRecord(
input.projectRoot,
input.dirName,
{
raiseLaneForAuthoring: {
reason: authoringRaiseReason(paths),
path: paths[0].path,
paths,
at
}
},
input.now
);
return written?.lane_raised_for_authoring?.at === at ? written.lane_raised_for_authoring : null;
}
// src/feature-evidence/reuse.ts
init_esm_shims();
// src/framework-api/query.ts
init_esm_shims();
// src/framework-api/adapters/shared.ts
init_esm_shims();
import { relative as relative3 } from "path";
var DYNAMIC_MEMBER_SUFFIX = ".*";
function projectRelative(projectRoot, target) {
return toPosixPath(relative3(projectRoot, target));
}
function dynamicRecord(container) {
return {
name: `${container}${DYNAMIC_MEMBER_SUFFIX}`,
kind: "member",
exists: true,
deprecated: false,
message: null,
since: null,
for_removal: false,
provenance: "unknown-dynamic"
};
}
var NAME_SEPARATORS = ["::", ".", "\\"];
function lastSeparator(name) {
let at = -1;
let width = 0;
for (const separator of NAME_SEPARATORS) {
const index = name.lastIndexOf(separator);
if (index > at) {
at = index;
width = separator.length;
}
}
return { at, width };
}
function lastNameSegment(name) {
const { at, width } = lastSeparator(name);
return at === -1 ? name : name.slice(at + width);
}
function containerOf(name) {
const { at } = lastSeparator(name);
return at === -1 ? null : name.slice(0, at);
}
// src/framework-api/query.ts
function findRecord(entry, symbol) {
const exact = entry.symbols.find((record) => record.name === symbol);
if (exact) {
return exact;
}
const wanted = lastNameSegment(symbol);
return entry.symbols.find(
(record) => !record.name.endsWith(DYNAMIC_MEMBER_SUFFIX) && lastNameSegment(record.name) === wanted
) ?? null;
}
function coveredByDynamicContainer(entry, symbol) {
const container = containerOf(symbol);
if (container === null) {
return false;
}
const wanted = lastNameSegment(container);
return entry.symbols.some((record) => {
if (!record.name.endsWith(DYNAMIC_MEMBER_SUFFIX)) {
return false;
}
const recorded = record.name.slice(0, -DYNAMIC_MEMBER_SUFFIX.length);
return recorded === container || lastNameSegment(recorded) === wanted;
});
}
function nearestFrameworkSymbol(symbol, known2) {
const wanted = lastNameSegment(symbol);
const bound = Math.max(2, Math.floor(wanted.length / 4));
let best = null;
let bestDistance = bound + 1;
for (const candidate of known2) {
const distance = levenshtein(wanted, lastNameSegment(candidate), bound);
if (distance < bestDistance) {
best = candidate;
bestDistance = distance;
}
}
return best;
}
function queryFrameworkApi(index, packageName, symbol, version) {
const forPackage = index.packages.filter((candidate) => candidate.package === packageName);
const entry = (version === void 0 ? forPackage[0] : forPackage.find((candidate) => candidate.version === version) ?? forPackage[0]) ?? null;
if (!entry) {
return {
package: packageName,
symbol,
verdict: "package-not-indexed",
version: null,
record: null,
nearest: null
};
}
const record = findRecord(entry, symbol);
if (record) {
return {
package: packageName,
symbol,
verdict: record.provenance === "asserted" ? record.deprecated ? "deprecated" : "live" : "unknown-dynamic",
version: entry.version,
record,
nearest: null
};
}
if (coveredByDynamicContainer(entry, symbol)) {
return {
package: packageName,
symbol,
verdict: "unknown-dynamic",
version: entry.version,
record: null,
nearest: null
};
}
const known2 = entry.symbols.filter((candidate) => !candidate.name.endsWith(DYNAMIC_MEMBER_SUFFIX)).map((candidate) => candidate.name);
return {
package: packageName,
symbol,
verdict: "absent",
version: entry.version,
record: null,
nearest: nearestFrameworkSymbol(symbol, known2)
};
}
// src/framework-api/store.ts
init_esm_shims();
import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
import { dirname as dirname4, join as join9 } from "path";
// src/framework-api/schema.ts
init_esm_shims();
var import_ajv3 = __toESM(require_ajv(), 1);
// src/validators/schemas/framework-api.schema.json
var framework_api_schema_default = {
$schema: "http://json-schema.org/draft-07/schema#",
$id: "paqad://schemas/framework-api.json",
title: "Installed framework-API index (issue #397)",
type: "object",
additionalProperties: false,
required: ["schema_version", "header", "packages", "blocked"],
properties: {
schema_version: { type: "integer", const: 1 },
header: {
type: "object",
additionalProperties: false,
required: ["generated_at", "schema_version"],
properties: {
generated_at: { type: "string", minLength: 1 },
schema_version: { type: "integer", const: 1 }
}
},
packages: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: [
"package",
"version",
"ecosystem",
"root",
"content_hash",
"sources",
"symbols"
],
properties: {
package: { type: "string", minLength: 1 },
version: { type: "string", minLength: 1 },
ecosystem: { type: "string", minLength: 1 },
root: { type: "string", minLength: 1 },
content_hash: { type: "string", minLength: 1 },
sources: { type: "array", items: { type: "string" } },
symbols: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: [
"name",
"kind",
"exists",
"deprecated",
"message",
"since",
"for_removal",
"provenance"
],
properties: {
name: { type: "string", minLength: 1 },
kind: {
type: "string",
enum: [
"function",
"class",
"interface",
"type",
"variable",
"enum",
"namespace",
"member",
"unknown"
]
},
exists: { type: "boolean" },
deprecated: { type: "boolean" },
message: { type: ["string", "null"] },
since: { type: ["string", "null"] },
for_removal: { type: "boolean" },
provenance: {
type: "string",
enum: ["asserted", "unknown-dynamic", "doc-derived"]
}
}
}
}
}
}
},
blocked: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: ["package", "ecosystem", "reason", "detail"],
properties: {
package: { type: "string", minLength: 1 },
ecosystem: { type: "string", minLength: 1 },
reason: {
type: "string",
enum: ["no-adapter", "not-installed", "no-types", "parser-unavailable"]
},
detail: { type: "string" }
}
}
}
}
};
// src/framework-api/schema.ts
var ajv3 = new import_ajv3.default({ allErrors: true, allowUnionTypes: true });
var compiled2;
function validator2() {
if (!compiled2) {
compiled2 = ajv3.compile(framework_api_schema_default);
}
return compiled2;
}
function validateFrameworkApiIndex(data) {
const validate = validator2();
if (validate(data)) {
return { valid: true, errors: [] };
}
const rawErrors = validate.errors ?? [];
const errors = rawErrors.map((error) => {
const path = error.instancePath.length > 0 ? error.instancePath : "/";
const message = error.message ?? "invalid";
return `${path} ${message}`.trim();
});
return { valid: false, errors };
}
// src/framework-api/store.ts
function frameworkApiIndexPath(projectRoot) {
return join9(projectRoot, PATHS.FRAMEWORK_API_INDEX);
}
function writeFrameworkApiIndex(projectRoot, index) {
const target = frameworkApiIndexPath(projectRoot);
mkdirSync4(dirname4(target), { recursive: true });
const tmp = `${target}.tmp`;
writeFileSync4(tmp, JSON.stringify(index, null, 2) + "\n", "utf8");
renameSync3(tmp, target);
return target;
}
function readFrameworkApiIndex(projectRoot) {
try {
const parsed = JSON.parse(readFileSync7(frameworkApiIndexPath(projectRoot), "utf8"));
return validateFrameworkApiIndex(parsed).valid ? parsed : null;
} catch {
return null;
}
}
// src/feature-evidence/reuse.ts
var CREATE_KEYWORDS = [
"add helper",
"add a helper",
"add util",
"add a util",
"add component",
"add a component",
"create helper",
"create a helper",
"create util",
"create a util",
"create utility",
"create component",
"create a component",
"new helper",
"new util",
"new component"
];
var EXPECTED_SHAPE = `"reuse": {
"consulted": [{ "source": "index-query", "query": "date formatting", "hits": 2 }],
"reusing": [{ "symbol": "formatIsoDate", "file": "src/utils/dates.ts", "how": "call as-is" }],
"new_constructs": [{ "name": "formatRelativeDate", "justification": "no existing helper handles the relative form" }]
}`;
var INDEX_ABSENT_WARNING = "reuse claims unverified: index not built";
var SNAPSHOT_ABSENT_WARNING = "framework reuse claims unverified: stack snapshot not built";
var FRAMEWORK_API_INDEX_ABSENT_WARNING = "framework reuse claims unverified: framework-api index not built";
function validateReuseSection(input) {
const shape = validateShape(input.reuse);
if (shape.errors.length > 0) {
return shape;
}
const reuse = input.reuse;
const errors = [];
const warnings = [];
const provenance = {};
checkDeclaredConstructs(reuse, input.steps, errors);
checkFirstPartySymbols(input.projectRoot, reuse, errors, warnings);
checkFrameworkClaims(input.projectRoot, reuse, errors, warnings);
verifyFrameworkSymbols(input.projectRoot, reuse, errors, warnings, provenance);
return { errors, warnings, provenance };
}
function frameworkClaimKey(packageName, symbol) {
return `${packageName}::${symbol}`;
}
function verifyFrameworkSymbols(projectRoot, reuse, errors, warnings, provenance) {
const claims = (reuse.reusing ?? []).filter(
(claim) => claim.package !== void 0
);
if (claims.length === 0) {
return;
}
const index = readFrameworkApiIndex(projectRoot);
if (!index) {
warnings.push(FRAMEWORK_API_INDEX_ABSENT_WARNING);
return;
}
for (const claim of claims) {
const result = queryFrameworkApi(index, claim.package, claim.symbol, claim.version);
const key = frameworkClaimKey(claim.package, claim.symbol);
switch (result.verdict) {
case "live":
provenance[key] = "asserted";
break;
case "deprecated":
provenance[key] = "asserted";
errors.push(deprecatedMessage(claim, result));
break;
case "absent":
provenance[key] = "asserted";
errors.push(
`reuse.reusing "${claim.symbol}" does not exist in ${claim.package}@${result.version}` + (result.nearest === null ? "" : ` \u2014 did you mean "${result.nearest}"?`)
);
break;
case "unknown-dynamic":
provenance[key] = "unknown-dynamic";
warnings.push(
`reuse.reusing "${claim.symbol}" is provided dynamically by ${claim.package}@${result.version} \u2014 paqad could not verify it statically`
);
break;
default:
warnings.push(
`reuse.reusing "${claim.symbol}" unverified: ${claim.package} is not in the framework-api index`
);
}
}
}
function deprecatedMessage(claim, result) {
const detail = result.record?.message ?? "no reason given";
const since = result.record?.since === null ? "" : ` (deprecated since ${result.record?.since})`;
const removal = result.record?.for_removal === true ? " It is slated for removal." : "";
return `reuse.reusing "${claim.symbol}" is deprecated in ${claim.package}@${result.version}${since}: ${detail}${removal}`;
}
function validateShape(reuse) {
if (reuse === void 0 || reuse === null) {
return {
errors: [
`plan template is missing the required "reuse" section \u2014 record what you checked before building.
Expected shape:
${EXPECTED_SHAPE}`
],
warnings: [],
provenance: {}
};
}
if (typeof reuse !== "object" || Array.isArray(reuse)) {
return {
errors: ['plan template "reuse" must be an object'],
warnings: [],
provenance: {}
};
}
const candidate = reuse;
const errors = [];
if (!Array.isArray(candidate.consulted) || candidate.consulted.length === 0) {
errors.push(
'a plan must record what existing code was checked: plan template "reuse.consulted" must list at least one thing you checked (run `paqad-ai index query <name>` or read the Existing surface section)'
);
}
if (candidate.reusing !== void 0 && !Array.isArray(candidate.reusing)) {
errors.push('plan template "reuse.reusing" must be an array');
}
if (candidate.new_constructs !== void 0 && !Array.isArray(candidate.new_constructs)) {
errors.push('plan template "reuse.new_constructs" must be an array');
}
return { errors, warnings: [], provenance: {} };
}
function matchedCreateKeyword(description) {
const haystack = description.toLowerCase();
return CREATE_KEYWORDS.find((keyword) => haystack.includes(keyword)) ?? null;
}
function checkDeclaredConstructs(reuse, steps, errors) {
if ((reuse.new_constructs ?? []).length > 0) {
return;
}
for (const step of steps) {
const keyword = matchedCreateKeyword(step.description ?? "");
if (keyword !== null) {
errors.push(
`plan step says "${keyword}" but "reuse.new_constructs" is empty \u2014 declare the new construct and justify it, or reword the step if nothing new is introduced`
);
return;
}
}
}
function checkFirstPartySymbols(projectRoot, reuse, errors, warnings) {
const firstParty = (reuse.reusing ?? []).filter((claim) => claim.package === void 0);
if (firstParty.length === 0) {
return;
}
const index = readCodeKnowledgeIndex(projectRoot);
if (!index) {
warnings.push(INDEX_ABSENT_WARNING);
return;
}
const known2 = index.symbols.map((symbol) => symbol.name);
const knownSet = new Set(known2);
for (const claim of firstParty) {
if (knownSet.has(claim.symbol)) {
continue;
}
const nearest = nearestSymbol(claim.symbol, known2);
errors.push(
nearest === null ? `reuse.reusing names "${claim.symbol}", which is not in the code-knowledge index \u2014 check the name, or declare it under new_constructs if you are creating it` : `reuse.reusing names "${claim.symbol}", which is not in the code-knowledge index \u2014 did you mean "${nearest}"?`
);
}
}
function nearestSymbol(symbol, known2) {
const bound = Math.max(2, Math.floor(symbol.length / 4));
let best = null;
let bestDistance = bound + 1;
for (const candidate of known2) {
const distance = levenshtein(symbol, candidate, bound);
if (distance < bestDistance) {
best = candidate;
bestDistance = distance;
}
}
return best;
}
function checkFrameworkClaims(projectRoot, reuse, errors, warnings) {
const frameworkClaims = (reuse.reusing ?? []).filter((claim) => claim.package !== void 0);
if (frameworkClaims.length > 0) {
checkFrameworkVersions(projectRoot, frameworkClaims, errors, warnings);
}
const frameworks = detectedFrameworks(projectRoot);
if (frameworks.length === 0) {
return;
}
for (const construct of reuse.new_constructs ?? []) {
if (construct.framework_checked !== void 0) {
continue;
}
if (justificationNamesFramework(construct.justification, frameworks)) {
continue;
}
errors.push(
`reuse.new_constructs "${construct.name}" must record the framework check \u2014 this project uses ${frameworks.join(", ")}. Add "framework_checked", or justify why no ${frameworks[0]} equivalent fits.`
);
}
}
function checkFrameworkVersions(projectRoot, claims, errors, warnings) {
for (const claim of claims) {
if (!claim.version) {
errors.push(
`reuse.reusing "${claim.symbol}" names package "${claim.package}" but sets no "version" \u2014 a framework claim must say which installed version it is made against`
);
}
}
const versioned = claims.filter((claim) => Boolean(claim.version));
if (versioned.length === 0) {
return;
}
const snapshot = readStackSnapshotSync(projectRoot);
if (!snapshot) {
warnings.push(SNAPSHOT_ABSENT_WARNING);
return;
}
for (const claim of versioned) {
const installed = snapshot.packages.find((pkg) => pkg.name === claim.package);
if (!installed) {
errors.push(
`reuse.reusing "${claim.symbol}" names package "${claim.package}", which is not in the stack snapshot \u2014 check the package name`
);
continue;
}
if (installed.locked_version !== claim.version) {
errors.push(
`reuse.reusing "${claim.symbol}" claims ${claim.package}@${claim.version}; stack has ${installed.locked_version}`
);
}
}
}
function detectedFrameworks(projectRoot) {
const profile = readProjectProfile(projectRoot);
const frameworks = profile?.stack_profile?.frameworks;
return Array.isArray(frameworks) ? frameworks : [];
}
function justificationNamesFramework(justification, frameworks) {
const haystack = justification.toLowerCase();
return frameworks.some((framework) => haystack.includes(framework.toLowerCase()));
}
function reuseCounts(plan) {
const reuse = plan?.reuse;
if (!reuse) {
return null;
}
return {
reused: (reuse.reusing ?? []).length,
newJustified: (reuse.new_constructs ?? []).length
};
}
// src/feature-evidence/artifacts.ts
var ReuseDeclarationError = class extends Error {
errors;
constructor(errors) {
super(errors.join("\n"));
this.name = "ReuseDeclarationError";
this.errors = errors;
}
};
var NoActiveFeatureError = class extends Error {
constructor() {
super("No active feature \u2014 run `paqad-ai stage start planning` first.");
this.name = "NoActiveFeatureError";
}
};
function atomicWriteJson(absPath, value) {
mkdirSync5(dirname5(absPath), { recursive: true });
const tmp = `${absPath}.tmp`;
writeFileSync5(tmp, `${JSON.stringify(value, null, 2)}
`, "utf8");
renameSync4(tmp, absPath);
}
function atomicWriteText(absPath, text) {
mkdirSync5(dirname5(absPath), { recursive: true });
const tmp = `${absPath}.tmp`;
writeFileSync5(tmp, text, "utf8");
renameSync4(tmp, absPath);
}
function readJson(absPath) {
try {
return JSON.parse(readFileSync8(absPath, "utf8"));
} catch {
return null;
}
}
function activeFeatureParts(projectRoot, sessionId) {
const dirName = currentFeature(projectRoot, sessionId);
if (!dirName) {
throw new NoActiveFeatureError();
}
const parts = parseFeatureDirName(dirName);
if (!parts) {
throw new Error(`Active feature dir name is not parseable: ${JSON.stringify(dirName)}`);
}
return { dirName, parts };
}
function writeFeaturePlan(projectRoot, sessionId, input, options = {}) {
const reuseCheck = validateReuseSection({
projectRoot,
reuse: input.reuse,
steps: input.steps ?? []
});
if (reuseCheck.errors.length > 0) {
throw new ReuseDeclarationError(reuseCheck.errors);
}
const reuse = applyComputedProvenance(input.reuse, reuseCheck.provenance);
let { dirName, parts } = activeFeatureParts(projectRoot, sessionId);
if (input.title !== void 0 && input.title.length > 0) {
const backfilled = backfillFeatureSlug(projectRoot, dirName, input.title, input.now);
if (backfilled.renamed) {
dirName = backfilled.dirName;
const renamedParts = parseFeatureDirName(dirName);
if (renamedParts) {
parts = renamedParts;
}
}
}
const record = buildPlanRecord({
change: parts.ulid,
session_id: sessionId,
summary: input.summary,
steps: input.steps,
modules_touched: input.modules_touched,
decisions: input.decisions,
risks: input.risks,
reuse,
now: input.now
});
const errors = validatePlanRecord(record);
if (errors.length > 0) {
throw new Error(`Invalid plan.json: ${errors.join("; ")}`);
}
const rel = featureFilePath(dirName, "plan");
atomicWriteJson(join10(projectRoot, rel), record);
updateFeatureRecord(
projectRoot,
dirName,
{ title: input.title ?? parts.slug, slug: parts.slug, issue: parts.issue },
input.now
);
const laneResized = planLaneResize({
projectRoot,
sessionId,
dirName,
steps: input.steps,
changedFiles: options.changedFiles ?? [],
changesKnown: options.changedFiles !== void 0 && options.gitReadable !== false,
now: input.now
});
const laneRaisedForAuthoring = raiseLaneForAuthoring({
projectRoot,
dirName,
plannedFiles: planFiles(input.steps),
changedFiles: options.changedFiles ?? [],
now: input.now
});
const laneRaised = laneResized ? null : raiseLaneIfGrown({
projectRoot,
dirName,
plannedFiles: planFiles(input.steps),
changedFiles: options.changedFiles ?? [],
now: input.now
});
const armed = armDecisionFromPlan({
projectRoot,
changeKey: dirName,
sessionId,
constructs: (input.reuse?.new_constructs ?? []).map((construct) => ({
name: construct.name,
justification: construct.justification
}))
});
const readinessDecision = openVisualEvidenceReadinessPause({
projectRoot,
dirName,
files: [
.../* @__PURE__ */ new Set([
...(input.steps ?? []).flatMap((step) => step.files ?? []),
...options.changedFiles ?? []
])
]
});
return {
dirName,
path: rel,
record,
warnings: [...reuseCheck.warnings, ...armed.warnings],
armedDecisions: armed.minted,
readinessDecision,
laneResized,
laneRaised,
laneRaisedForAuthoring
};
}
function applyComputedProvenance(reuse, computed) {
if (reuse === void 0 || Object.keys(computed).length === 0) {
return reuse;
}
return {
...reuse,
reusing: (reuse.reusing ?? []).map((claim) => {
if (claim.package === void 0) {
return claim;
}
const verdict = computed[frameworkClaimKey(claim.package, claim.symbol)];
return verdict === void 0 ? claim : { ...claim, provenance: verdict };
})
};
}
function readFeaturePlan(projectRoot, dirName) {
return readJson(join10(projectRoot, featureFilePath(dirName, "plan")));
}
function writeFeatureSpecification(projectRoot, sessionId, spec, markdown) {
const dirName = currentFeature(projectRoot, sessionId);
if (!dirName) {
throw new NoActiveFeatureError();
}
if (spec.frozen === null || spec.frozen === void 0) {
throw new Error(`Refusing to persist unfrozen spec ${spec.spec_id}: freeze it first.`);
}
if (sha256Hex(markdown) !== spec.spec_hash) {
throw new Error(
`Refusing to persist spec ${spec.spec_id}: the source does not hash to its spec_hash.`
);
}
const sessionForDocs = documentSessionId(projectRoot, dirName, sessionId);
const header = buildTextHeader({
docType: SPEC_SOURCE_DOC_TYPE,
change: featureChangeKey(dirName),
sessionId: sessionForDocs,
schemaVersion: FEATURE_DOC_SCHEMA_VERSION,
body: markdown
});
atomicWriteText(
join10(projectRoot, featureFilePath(dirName, "specMd")),
renderFrontMatter(header, markdown)
);
const body = { ...spec, spec_file: FEATURE_BUNDLE_FILES.specMd };
delete body.schema_version;
const record = stampFeatureDocument({
projectRoot,
dirName,
docType: SPECIFICATION_DOC_TYPE,
schemaVersion: FEATURE_DOC_SCHEMA_VERSION,
sessionId: sessionForDocs,
body
});
const rel = featureFilePath(dirName, "specification");
atomicWriteJson(join10(projectRoot, rel), record);
updateFeatureRecord(projectRoot, dirName, { spec_id: spec.spec_id });
return { dirName, path: rel, record };
}
function readFeatureSpecification(projectRoot, dirName) {
return readJson(join10(projectRoot, featureFilePath(dirName, "specification")));
}
function writeFeatureReview(projectRoot, sessionId, input) {
const { dirName, parts } = activeFeatureParts(projectRoot, sessionId);
const record = buildReviewRecord({
change: parts.ulid,
session_id: sessionId,
summary: input.summary,
verdict: input.verdict,
findings: input.findings,
checked: input.checked,
rollback: input.rollback,
now: input.now
});
const errors = validateReviewRecord(record);
if (errors.length > 0) {
throw new Error(`Invalid review.json: ${errors.join("; ")}`);
}
const rel = featureFilePath(dirName, "review");
atomicWriteJson(join10(projectRoot, rel), record);
return { dirName, path: rel, record };
}
function readFeatureReview(projectRoot, dirName) {
return readJson(join10(projectRoot, featureFilePath(dirName, "review")));
}
var SKILL_RECORD_DOC_TYPE = "paqad.skill-record";
var SKILL_RECORD_SCHEMA_VERSION = 1;
var SKILL_RECORD_WRITER = "paqad-ai skill release";
function skillRecordFilename(name) {
return `skill-${name}.json`;
}
function skillRecordPath(dirName, name) {
return join10(featureDir(dirName), skillRecordFilename(name));
}
function readSkillRecord(projectRoot, dirName, name) {
return readJson(
join10(projectRoot, skillRecordPath(dirName, name))
);
}
function sameSkillRecord(existing, body) {
if (existing === null) return false;
return JSON.stringify({ ...body }) === JSON.stringify({
skill: existing.skill,
status: existing.status,
version: existing.version,
released_path: existing.released_path,
folder_hash: existing.folder_hash,
evidence_path: existing.evidence_path,
overrides: existing.overrides,
labels: existing.labels
});
}
function writeSkillRecord(projectRoot, sessionId, body, now) {
const { dirName, parts } = activeFeatureParts(projectRoot, sessionId);
const rel = skillRecordPath(dirName, body.skill);
if (sameSkillRecord(readSkillRecord(projectRoot, dirName, body.skill), body)) {
return { dirName, path: rel, written: false };
}
const record = buildDocumentEnvelope({
docType: SKILL_RECORD_DOC_TYPE,
change: parts.ulid,
sessionId,
schemaVersion: SKILL_RECORD_SCHEMA_VERSION,
now,
body: { ...body }
});
atomicWriteJson(join10(projectRoot, rel), record);
return { dirName, path: rel, written: true };
}
var AGENT_RECORD_DOC_TYPE = "paqad.agent-record";
var AGENT_RECORD_SCHEMA_VERSION = 1;
var AGENT_RECORD_WRITER = "paqad-ai agent release";
function agentRecordFilename(name) {
return `agent-${name}.json`;
}
function agentRecordPath(dirName, name) {
return join10(featureDir(dirName), agentRecordFilename(name));
}
function readAgentRecord(projectRoot, dirName, name) {
return readJson(
join10(projectRoot, agentRecordPath(dirName, name))
);
}
function sameAgentRecord(existing, body) {
if (existing === null) return false;
const pick = (record) => ({
agent: record.agent,
status: record.status,
version: record.version,
released_path: record.released_path,
file_hash: record.file_hash,
hook_scripts: record.hook_scripts,
files: record.files,
evidence_path: record.evidence_path,
overrides: record.overrides,
labels: record.labels
});
return JSON.stringify(pick(body)) === JSON.stringify(pick(existing));
}
function writeAgentRecord(projectRoot, sessionId, body, now) {
const { dirName, parts } = activeFeatureParts(projectRoot, sessionId);
const rel = agentRecordPath(dirName, body.agent);
if (sameAgentRecord(readAgentRecord(projectRoot, dirName, body.agent), body)) {
return { dirName, path: rel, written: false };
}
const record = buildDocumentEnvelope({
docType: AGENT_RECORD_DOC_TYPE,
change: parts.ulid,
sessionId,
schemaVersion: AGENT_RECORD_SCHEMA_VERSION,
now,
body: { ...body }
});
atomicWriteJson(join10(projectRoot, rel), record);
return { dirName, path: rel, written: true };
}
// src/feature-evidence/projections.ts
function readAllFeatureStageRows(projectRoot) {
return listFeatureDirs(projectRoot).flatMap(
(dirName) => readUnitFile(projectRoot, featureFilePath(dirName, "stageEvidence"))
);
}
function readAllFeatureRuleRuns(projectRoot) {
return listFeatureDirs(projectRoot).flatMap(
(dirName) => readUnitFile(projectRoot, featureFilePath(dirName, "ruleRun"))
);
}
function readAllFeatureChangeMetrics(projectRoot) {
return listFeatureDirs(projectRoot).flatMap(
(dirName) => readUnitFile(projectRoot, featureFilePath(dirName, "changeMetrics"))
);
}
function readAllFeatureRag(projectRoot) {
return listFeatureDirs(projectRoot).flatMap(
(dirName) => readUnitFile(projectRoot, featureFilePath(dirName, "rag"))
);
}
function readFeatureChangeMetricsWindow(projectRoot, limit) {
const rows = readAllFeatureChangeMetrics(projectRoot).slice().sort((a, b) => (rowRecordedAt(a) ?? "").localeCompare(rowRecordedAt(b) ?? ""));
return limit >= rows.length ? rows : rows.slice(rows.length - limit);
}
function readAllFeatureEvidence(projectRoot) {
return listFeatureDirs(projectRoot).flatMap(
(dirName) => readFeatureEvidence(projectRoot, dirName)
);
}
function readAllFeatureSpecificationEntries(projectRoot) {
const entries = [];
for (const dirName of listFeatureDirs(projectRoot)) {
const spec = readFeatureSpecification(projectRoot, dirName);
if (spec && spec.frozen !== null && spec.frozen !== void 0) {
entries.push({ dirName, spec });
}
}
return entries;
}
// src/spec-pipeline/metrics.ts
init_esm_shims();
// src/spec/feature-spec-builder.ts
init_esm_shims();
var FEATURE_SPEC_SCHEMA_VERSION = "1";
function buildFeatureSpec(options) {
const index = extractObligationIndex({
spec_file: options.spec_file,
spec_markdown: options.spec_markdown,
spec_review: options.spec_review ?? null,
extracted_at: options.extracted_at
});
const behaviour = [];
const criteria = [];
let criterionSeq = 0;
for (const obligation of index.obligations) {
const description = stripLeadingId(obligation.description);
if (obligation.category === "functional" || obligation.category === "non-functional") {
behaviour.push(`${obligation.obligation_id}: ${description}`);
} else if (obligation.category === "acceptance") {
criterionSeq += 1;
criteria.push(criterionFromObligation(obligation.obligation_id, description, criterionSeq));
}
}
return {
schema_version: FEATURE_SPEC_SCHEMA_VERSION,
spec_id: options.spec_id,
spec_file: options.spec_file,
spec_hash: sha256Hex(options.spec_markdown),
behaviour,
acceptance_criteria: criteria,
invariants: collectInvariants(options.spec_markdown, options.suggested_invariants ?? []),
open_questions: extractOpenQuestions(options.spec_markdown),
frozen: null,
...(() => {
const nonGoals = extractNonGoals(options.spec_markdown);
return nonGoals.length > 0 ? { non_goals: nonGoals } : {};
})()
};
}
function criterionFromObligation(obligationId, description, sequence) {
const criterionId = /^AC-(?:TRACK-[A-Za-z0-9-]+|\d+)$/.test(obligationId) ? obligationId : `AC-${sequence}`;
const { given, when, then } = parseGivenWhenThen(description);
return {
criterion_id: criterionId,
given,
when,
then,
proof_type: detectProofType(description),
status: "uncovered",
source: "planned",
linked_requirement_ids: []
};
}
function stripLeadingId(text) {
return text.replace(/^(?:(?:FR|NFR|EC)-\d+(?:\.\d+)?|AC-(?:TRACK-[A-Za-z0-9-]+|\d+))\s*[:.)-]\s*/i, "").trim();
}
function parseGivenWhenThen(text) {
const match = /given\s+(.*?),?\s+when\s+(.*?),?\s+then\s+(.*?)\.?$/i.exec(text.trim());
if (match) {
return { given: match[1].trim(), when: match[2].trim(), then: match[3].trim() };
}
return { given: "", when: "", then: text.trim() };
}
function detectProofType(text) {
const match = /\(proof:\s*(automated|manual|visual)\)/i.exec(text);
if (match) {
return match[1].toLowerCase();
}
return "automated";
}
function collectInvariants(markdown, suggested) {
const invariants = [];
const seenStatements = /* @__PURE__ */ new Set();
let sequence = 0;
for (const authored of extractAuthoredInvariants(markdown)) {
sequence += 1;
seenStatements.add(authored.toLowerCase());
invariants.push({
invariant_id: `INV-${sequence}`,
statement: authored,
source: "authored",
confirmed: false
});
}
for (const suggestion of suggested) {
const normalized = suggestion.statement.trim();
if (normalized.length === 0 || seenStatements.has(normalized.toLowerCase())) {
continue;
}
sequence += 1;
seenStatements.add(normalized.toLowerCase());
invariants.push({
invariant_id: `INV-${sequence}`,
statement: normalized,
source: suggestion.source,
...suggestion.rule_id === void 0 ? {} : { rule_id: suggestion.rule_id },
confirmed: false
});
}
return invariants;
}
function extractAuthoredInvariants(markdown) {
const statements = [];
for (const line of splitLines(markdown)) {
const match = /^\s*(?:[-*+]\s+)?(?:\*\*)?INV-\d+(?:\*\*)?\s*[:.)-]\s*(.+\S)\s*$/.exec(line);
if (match) {
statements.push(match[1].trim());
}
}
return statements;
}
function extractOpenQuestions(markdown) {
const questions = [];
for (const line of splitLines(markdown)) {
const match = /^\s*(?:[-*+]\s+)?(Q\d+)\s*[:.)-]\s*(.+\S)\s*$/.exec(line);
if (match) {
questions.push(`${match[1]}: ${match[2].trim()}`);
}
}
return questions;
}
function extractNonGoals(markdown) {
const nonGoals = [];
let inSection = false;
for (const line of splitLines(markdown)) {
const heading = /^(#{1,6})\s+(.*\S)\s*$/.exec(line);
if (heading) {
inSection = /^non-?goals?$/i.test(heading[2].trim());
continue;
}
if (!inSection) {
continue;
}
const bullet = /^\s*[-*+]\s+(.+\S)\s*$/.exec(line);
if (bullet) {
nonGoals.push(bullet[1].trim());
}
}
return nonGoals;
}
// src/spec/spec-freeze.ts
init_esm_shims();
function evaluateSpecFreeze(spec, specReview, options = {}) {
const blockers = [];
if (spec.behaviour.length === 0) {
blockers.push("Spec has no behaviour statements.");
}
if (spec.acceptance_criteria.length === 0) {
blockers.push("Spec has no acceptance criteria.");
}
if (spec.invariants.length === 0) {
blockers.push("Spec has no invariants.");
}
for (const criterion of spec.acceptance_criteria) {
if (!criterion.proof_type) {
blockers.push(`Acceptance criterion ${criterion.criterion_id} has no proof_type.`);
}
}
for (const invariant of spec.invariants) {
if (!invariant.confirmed) {
blockers.push(`Invariant ${invariant.invariant_id} is not human-confirmed.`);
}
}
for (const question of spec.open_questions) {
blockers.push(`Open question unresolved: ${question}`);
}
const visual = options.requireVisualAc;
if (visual && !spec.acceptance_criteria.some((criterion) => criterion.proof_type === "visual")) {
blockers.push(
`Visual evidence is on and this change touches frontend files (${visual.files.join(", ")}), so at least one acceptance criterion needs (proof: visual).`
);
}
if (specReview) {
for (const defect of specReview.defects) {
if (defect.severity === "critical" && defect.status !== "resolved") {
blockers.push(`Critical spec-review defect open: ${defect.defect_id}`);
}
}
}
return { can_freeze: blockers.length === 0, blockers };
}
function freezeSpec(spec, input) {
const evaluation = evaluateSpecFreeze(spec, input.spec_review);
if (!evaluation.can_freeze) {
throw new Error(`Cannot freeze spec ${spec.spec_id}: ${evaluation.blockers.join("; ")}`);
}
const frozen = {
frozen_at: input.frozen_at,
spec_hash: spec.spec_hash,
signed_off_by: input.signed_off_by
};
const specReview = input.spec_review ? summarizeSpecReview(input.spec_review) : void 0;
return {
...spec,
frozen,
...specReview === void 0 ? {} : { spec_review: specReview },
...input.sections?.task === void 0 ? {} : { task: input.sections.task },
...input.sections?.grounding === void 0 ? {} : { grounding: input.sections.grounding },
...input.sections?.pipeline === void 0 ? {} : { pipeline: input.sections.pipeline },
...input.sections?.trace === void 0 ? {} : { trace: input.sections.trace }
};
}
function summarizeSpecReview(report) {
const open = report.defects.filter((defect) => defect.status !== "resolved");
return {
reviewed_at: report.metadata.reviewed_at,
defect_count: open.length,
by_severity: {
critical: open.filter((defect) => defect.severity === "critical").length,
major: open.filter((defect) => defect.severity === "major").length,
minor: open.filter((defect) => defect.severity === "minor").length
}
};
}
function isFrozenSpecStale(spec, currentSpecHash) {
return spec.frozen !== null && spec.frozen.spec_hash !== currentSpecHash;
}
// src/spec-pipeline/experts/assemble.ts
init_esm_shims();
// src/spec-pipeline/run-store.ts
init_esm_shims();
import { mkdirSync as mkdirSync6, readFileSync as readFileSync9, realpathSync, rmdirSync, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
// src/spec-pipeline/experts/types.ts
init_esm_shims();
var MAX_EXPERT_QUESTIONS = 5;
var QA_ROLE = "qa-engineer";
// src/spec-pipeline/finish.ts
init_esm_shims();
function decideFinish(config, a5Live) {
if (config.final_review === "strict") {
return {
outcome: "await-human-approval",
reason: "final-review is required \u2014 a human must approve before freeze"
};
}
if (!a5Live) {
return {
outcome: "non-blocking-review",
reason: "the spec-vs-code check (A5) is not live \u2014 showing a non-blocking readable review instead of freezing silently"
};
}
return {
outcome: "freeze",
reason: config.final_review === "warn" ? "A5 is live; final-review is advisory \u2014 showing the readable spec, then freezing" : "A5 is live \u2014 freezing automatically"
};
}
function buildProvenance(config, a5Live, answerRefs, questions, experts, metrics) {
return {
pipeline_produced: true,
answer_refs: answerRefs,
questions,
enforcement: config,
a5_live: a5Live,
outcome: decideFinish(config, a5Live).outcome,
...experts ? { experts } : {},
...metrics ? { metrics } : {}
};
}
function frozenPipelineSection(finish) {
const { provenance } = finish;
const outcome = finish.outcome ?? provenance.outcome;
const config = provenance.enforcement;
return {
produced: true,
...outcome === void 0 ? {} : { outcome },
...finish.reason === void 0 ? {} : { reason: finish.reason },
...provenance.a5_live === void 0 ? {} : { a5_live: provenance.a5_live },
...config === void 0 ? {} : {
enforcement: {
clarification: config.clarification,
final_review: config.final_review,
token_ceiling: config.token_ceiling,
experts_enabled: config.experts_enabled,
adoption: config.adoption,
standing_experts: config.standing_experts,
standing_experts_dropped: config.standing_experts_dropped
}
}
};
}
// src/spec-pipeline/run-store.ts
var SPEC_PIPELINE_STAGING_DIR = ".paqad/tmp/spec-pipeline";
var STAGED_FILES = {
grounding: "grounding.json",
task: "task.json",
trace: "trace.json",
craft: "spec.md",
finish: "finish.json",
expertQuestions: "expert-questions.json",
expertVerdicts: "expert-verdicts.json",
inputs: "inputs.json"
};
function stagingDir(dirName) {
return join(SPEC_PIPELINE_STAGING_DIR, featureChangeKey(dirName));
}
function stagedFilePath(dirName, file) {
return join(stagingDir(dirName), STAGED_FILES[file]);
}
function readText(abs) {
try {
return readFileSync9(abs, "utf8");
} catch {
return null;
}
}
function parseJson(raw) {
try {
return JSON.parse(raw);
} catch {
return void 0;
}
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function atomicWrite(abs, text) {
mkdirSync6(dirname(abs), { recursive: true });
const tmp = `${abs}.tmp-${process.pid}`;
writeFileSync6(tmp, text, "utf8");
renameWithRetrySync(tmp, abs);
}
function readStagedText(projectRoot, dirName, file) {
return readText(join(projectRoot, stagedFilePath(dirName, file)));
}
function writeStagedText(projectRoot, dirName, file, text) {
atomicWrite(join(projectRoot, stagedFilePath(dirName, file)), text);
}
function readStagedJson(projectRoot, dirName, file) {
const raw = readStagedText(projectRoot, dirName, file);
const parsed = raw === null ? void 0 : parseJson(raw);
return parsed === void 0 ? null : parsed;
}
function writeStagedJson(projectRoot, dirName, file, value) {
writeStagedText(projectRoot, dirName, file, `${JSON.stringify(value, null, 2)}
`);
}
function clearStaged(projectRoot, dirName, file) {
rmSync2(join(projectRoot, stagedFilePath(dirName, file)), { force: true });
}
function readBundleBody(projectRoot, dirName, file) {
const raw = readText(join(projectRoot, featureFilePath(dirName, file)));
const parsed = raw === null ? void 0 : parseJson(raw);
if (!isRecord(parsed)) return null;
const body = { ...parsed };
for (const key of ENVELOPE_HEADER_KEYS) delete body[key];
return body;
}
function writeBundleBody(projectRoot, dirName, file, docType, body, options) {
const doc = stampFeatureDocument({
projectRoot,
dirName,
docType,
schemaVersion: FEATURE_DOC_SCHEMA_VERSION,
sessionId: options.sessionId,
body,
now: options.now
});
atomicWrite(
join(projectRoot, featureFilePath(dirName, file)),
`${JSON.stringify(doc, null, 2)}
`
);
}
function writeRequest(projectRoot, dirName, text, options = {}) {
const header = buildTextHeader({
docType: REQUEST_DOC_TYPE,
change: featureChangeKey(dirName),
sessionId: documentSessionId(projectRoot, dirName, options.sessionId),
schemaVersion: FEATURE_DOC_SCHEMA_VERSION,
body: text,
now: options.now
});
atomicWrite(
join(projectRoot, featureFilePath(dirName, "request")),
renderFrontMatter(header, text)
);
}
function readRequest(projectRoot, dirName) {
const raw = readText(join(projectRoot, featureFilePath(dirName, "request")));
return raw === null ? "" : splitFrontMatter(raw).body;
}
function readClarification(projectRoot, dirName) {
const body = readBundleBody(projectRoot, dirName, "clarification");
if (body === null) return null;
return {
label: isRecord(body.label) ? body.label : null,
questions: isRecord(body.questions) ? body.questions : null
};
}
function writeClarification(projectRoot, dirName, patch, options) {
const current = readClarification(projectRoot, dirName) ?? { label: null, questions: null };
writeBundleBody(
projectRoot,
dirName,
"clarification",
CLARIFICATION_DOC_TYPE,
{ ...current, ...patch },
options
);
}
function writeLabel(projectRoot, dirName, label, options = {}) {
writeClarification(
projectRoot,
dirName,
{
label: {
value: label.label,
signals: label.signals,
question_budget: label.question_budget
}
},
options
);
}
function readLabel(projectRoot, dirName) {
const section = readClarification(projectRoot, dirName)?.label;
if (!section) return null;
return {
label: section.value,
signals: section.signals,
question_budget: section.question_budget
};
}
function writeQuestions(projectRoot, dirName, questions, options = {}) {
writeClarification(
projectRoot,
dirName,
{
questions: {
asked: questions.questions,
auto_answered: questions.auto_answered,
deferred: questions.deferred_from_experts ?? [],
counts: {
asked: questions.asked,
answered: questions.answered,
auto_answered: questions.auto_answered.length,
deferred: questions.deferred
}
}
},
options
);
}
function readQuestions(projectRoot, dirName) {
const section = readClarification(projectRoot, dirName)?.questions;
if (!section) return null;
return {
questions: section.asked,
auto_answered: section.auto_answered,
asked: section.counts.asked,
answered: section.counts.answered,
deferred: section.counts.deferred,
...section.deferred.length > 0 ? { deferred_from_experts: section.deferred } : {}
};
}
function clearClarificationSection(projectRoot, dirName, section, options = {}) {
writeClarification(projectRoot, dirName, { [section]: null }, options);
}
function readExperts(projectRoot, dirName) {
const body = readBundleBody(projectRoot, dirName, "experts");
if (body === null || !Array.isArray(body.roster)) return null;
return {
roster: body.roster,
findings: Array.isArray(body.findings) ? body.findings : null,
synthesis: isRecord(body.synthesis) ? body.synthesis : null,
...Array.isArray(body.voice_warnings) ? { voice_warnings: body.voice_warnings } : {}
};
}
function writeExperts(projectRoot, dirName, patch, options) {
const current = readExperts(projectRoot, dirName) ?? {
roster: [],
findings: null,
synthesis: null
};
writeBundleBody(
projectRoot,
dirName,
"experts",
EXPERTS_DOC_TYPE,
{ ...current, ...patch },
options
);
}
function writeExpertRoster(projectRoot, dirName, roster, options = {}) {
writeExperts(projectRoot, dirName, { roster }, options);
}
function readExpertNeed(projectRoot, dirName) {
const experts = readExperts(projectRoot, dirName);
if (experts === null) return null;
return {
experts: experts.roster.map(({ role, reason, origin }) => ({
role,
reason,
...origin === void 0 ? {} : { origin }
}))
};
}
function expertNotesLockDir(projectRoot, dirName) {
return join(projectRoot, PATHS.LOCKS_DIR, `experts-${featureChangeKey(dirName)}.lock`);
}
function warningBelongsTo(warning, role) {
const prefix = `EX-${role}-`;
return warning.id.startsWith(prefix) && /^\d+$/.test(warning.id.slice(prefix.length));
}
function inRosterOrder(findings, roster) {
const seat = new Map(roster.map((entry, index) => [entry.role, index]));
const rank = (role) => seat.get(role) ?? roster.length;
return findings.map((finding, index) => ({ finding, index })).sort((a, b) => rank(a.finding.role) - rank(b.finding.role) || a.index - b.index).map(({ finding }) => finding);
}
function writeExpertNotes(projectRoot, dirName, artifact, options = {}) {
withFileLockSync(
expertNotesLockDir(projectRoot, dirName),
() => mergeExpertNotesLocked(projectRoot, dirName, artifact, options)
);
}
function mergeExpertNotesLocked(projectRoot, dirName, artifact, options) {
const current = readExperts(projectRoot, dirName);
const noteRoles = new Set(artifact.notes.map((note) => note.role));
const tokenRoles = new Set(Object.keys(artifact.tokens));
const roles = /* @__PURE__ */ new Set([...noteRoles, ...tokenRoles]);
const kept = (current?.findings ?? []).filter((finding) => !roles.has(finding.role));
const incoming = artifact.notes.flatMap(
(note) => note.findings.map(({ id, ...finding }) => ({ id, role: note.role, ...finding }))
);
const roster = (current?.roster ?? []).map(
(entry) => roles.has(entry.role) ? { ...entry, tokens_used: artifact.tokens[entry.role] ?? null } : entry
);
const touched = [...roles];
const keptWarnings = (current?.voice_warnings ?? []).filter(
(warning) => !touched.some((role) => warningBelongsTo(warning, role))
);
const voiceWarnings = artifact.voice_warnings === void 0 && current?.voice_warnings === void 0 ? void 0 : [...keptWarnings, ...artifact.voice_warnings ?? []];
writeExperts(
projectRoot,
dirName,
{
roster,
findings: inRosterOrder([...kept, ...incoming], roster),
...voiceWarnings === void 0 ? {} : { voice_warnings: voiceWarnings }
},
options
);
const questions = [
...readStagedExpertQuestions(projectRoot, dirName).filter((note) => !roles.has(note.role)),
...artifact.notes.filter((note) => (note.questions ?? []).length > 0).map((note) => ({ role: note.role, questions: note.questions }))
];
if (questions.length > 0) {
writeStagedJson(projectRoot, dirName, "expertQuestions", { notes: questions });
} else {
clearStaged(projectRoot, dirName, "expertQuestions");
}
const verdicts = artifact.notes.find((note) => note.verdicts !== void 0)?.verdicts;
if (verdicts !== void 0) {
writeStagedJson(projectRoot, dirName, "expertVerdicts", { role: QA_ROLE, verdicts });
} else if (roles.has(QA_ROLE)) {
clearStaged(projectRoot, dirName, "expertVerdicts");
}
}
function readStagedQaVerdicts(projectRoot, dirName) {
return readStagedJson(projectRoot, dirName, "expertVerdicts")?.verdicts ?? null;
}
function readStagedExpertQuestions(projectRoot, dirName) {
return readStagedJson(projectRoot, dirName, "expertQuestions")?.notes ?? [];
}
function readExpertNotes(projectRoot, dirName) {
const experts = readExperts(projectRoot, dirName);
if (experts === null || experts.findings === null) return null;
const byRole = /* @__PURE__ */ new Map();
const noteFor = (role) => {
const existing = byRole.get(role);
if (existing) return existing;
const created = { role, findings: [] };
byRole.set(role, created);
return created;
};
const superseded = new Set(
experts.findings.flatMap(
(finding) => (finding.merged_from ?? []).filter((id) => id !== finding.id)
)
);
for (const { role, ...finding } of experts.findings) {
if (superseded.has(finding.id)) continue;
noteFor(role).findings.push(finding);
}
for (const { role, questions } of readStagedExpertQuestions(projectRoot, dirName)) {
noteFor(role).questions = questions;
}
const verdicts = readStagedQaVerdicts(projectRoot, dirName);
if (verdicts !== null) noteFor(QA_ROLE).verdicts = verdicts;
const tokens = {};
for (const entry of experts.roster) {
if (entry.tokens_used !== null) tokens[entry.role] = entry.tokens_used;
}
return { notes: [...byRole.values()], tokens };
}
function writeExpertSynthesis(projectRoot, dirName, synthesis, options = {}) {
writeExperts(projectRoot, dirName, { synthesis }, options);
}
function writeExpertFindings(projectRoot, dirName, findings, options = {}) {
writeExperts(projectRoot, dirName, { findings }, options);
}
function readExpertSynthesis(projectRoot, dirName) {
return readExperts(projectRoot, dirName)?.synthesis ?? null;
}
function clearExpertSynthesis(projectRoot, dirName, options = {}) {
writeExperts(projectRoot, dirName, { synthesis: null }, options);
}
function readGrounding(projectRoot, dirName) {
const staged = readStagedJson(projectRoot, dirName, "grounding");
if (staged) return staged;
const frozen = readBundleBody(projectRoot, dirName, "specification")?.grounding;
if (!isRecord(frozen) || !Array.isArray(frozen.references)) return null;
return { terms: [], ...frozen };
}
function readFrozenSpecificationBody(projectRoot, dirName) {
return readBundleBody(projectRoot, dirName, "specification");
}
function readFreezeSections(projectRoot, dirName) {
const finish = readStagedJson(projectRoot, dirName, "finish");
if (!isRecord(finish) || !isRecord(finish.provenance)) return null;
const task = readStagedJson(projectRoot, dirName, "task");
const grounding = readStagedJson(projectRoot, dirName, "grounding");
const trace = readStagedJson(projectRoot, dirName, "trace");
return {
...isRecord(task) && typeof task.intent === "string" ? { task: { intent: task.intent, scope: isRecord(task.scope) ? task.scope : {} } } : {},
...isRecord(grounding) ? {
grounding: {
path: grounding.path,
sparse: grounding.sparse,
references: grounding.references,
...Array.isArray(grounding.vocabulary) ? { vocabulary: grounding.vocabulary } : {}
}
} : {},
pipeline: withExpertFreezeSummary(frozenPipelineSection(finish), projectRoot, dirName),
...isRecord(trace) && Array.isArray(trace.entries) ? {
trace: Object.fromEntries(
trace.entries.map((entry) => [
entry.id,
entry.source
])
)
} : {}
};
}
function withExpertFreezeSummary(pipeline, projectRoot, dirName) {
const experts = readExperts(projectRoot, dirName);
if (!experts || !experts.synthesis) return pipeline;
const syn = experts.synthesis;
const acceptedIds = new Set(syn.accepted);
const findings = (experts.findings ?? []).filter((finding) => acceptedIds.has(finding.id)).map((finding) => ({
id: finding.id,
role: finding.role,
kind: finding.kind ?? "requirement",
severity: finding.severity ?? "should",
target: finding.target,
claim: finding.claim
}));
const renamed = (experts.findings ?? []).filter(
(finding) => finding.renamed_from !== void 0
).length;
return {
...pipeline,
experts: {
roles: experts.roster.map((entry) => entry.role),
accepted: syn.accepted.length,
declined: syn.declined.length,
conflicts: syn.conflicts.length,
auto_resolved: syn.auto_resolved?.length ?? 0,
standing: experts.roster.filter((entry) => entry.origin === "standing").length,
on_call: experts.roster.filter((entry) => entry.origin !== "standing").map((entry) => entry.role),
renamed
},
...findings.length > 0 ? { findings } : {}
};
}
function isTmpPath(rel) {
return rel.startsWith(".paqad/tmp/") && !rel.split("/").includes("..");
}
function clearFrozenRun(projectRoot, dirName, keep = []) {
for (const rel of readRememberedInputs(projectRoot, dirName)) {
if (isTmpPath(rel) && !keep.includes(rel)) rmSync2(join(projectRoot, rel), { force: true });
}
rmSync2(join(projectRoot, stagingDir(dirName)), { recursive: true, force: true });
try {
rmdirSync(join(projectRoot, SPEC_PIPELINE_STAGING_DIR));
} catch {
}
}
function rootForms(projectRoot) {
const given = resolve(projectRoot);
let real = given;
try {
real = resolve(realpathSync(projectRoot));
} catch {
}
return [.../* @__PURE__ */ new Set([given, real])];
}
function tmpInputPath(projectRoot, file) {
const abs = resolve(file);
for (const base of rootForms(projectRoot)) {
const rel = relative(base, abs);
if (rel.startsWith(".paqad/tmp/")) return rel;
}
return null;
}
function rememberInput(projectRoot, dirName, file) {
const rel = tmpInputPath(projectRoot, file);
if (rel === null) return;
const inputs = readRememberedInputs(projectRoot, dirName);
if (inputs.includes(rel)) return;
writeStagedJson(projectRoot, dirName, "inputs", { inputs: [...inputs, rel] });
}
function readRememberedInputs(projectRoot, dirName) {
return readStagedJson(projectRoot, dirName, "inputs")?.inputs ?? [];
}
function appendSpecStepRow(projectRoot, dirName, input, options = {}) {
return appendFeatureStageRow(
projectRoot,
documentSessionId(projectRoot, dirName, options.sessionId),
dirName,
{
kind: SPEC_STEP_KIND,
step: input.step,
outcome: input.outcome,
artifact_hash: input.artifactHash,
...input.tokens !== void 0 ? { tokens: input.tokens } : {}
},
options.now
);
}
function readSpecStepRows(projectRoot, dirName) {
return readFeatureStageUnit(projectRoot, dirName).filter((row) => row.kind === SPEC_STEP_KIND);
}
function appendSpecCorrectionRow(projectRoot, dirName, correction, options = {}) {
return appendFeatureStageRow(
projectRoot,
documentSessionId(projectRoot, dirName, options.sessionId),
dirName,
{
kind: SPEC_CORRECTION_KIND,
spec_id: correction.spec_id,
changed_sections: correction.changed_sections
},
options.now
);
}
function readSpecCorrectionRows(projectRoot, dirName) {
return readFeatureStageUnit(projectRoot, dirName).filter(
(row) => row.kind === SPEC_CORRECTION_KIND
);
}
// src/spec-pipeline/trace.ts
init_esm_shims();
var KINDS = ["FR", "NFR", "AC", "INV"];
function idNumber(id) {
const m = /-(\d+)$/.exec(id);
return m ? Number(m[1]) : 0;
}
function carryForwardIds(current, previous) {
const priorBySource = /* @__PURE__ */ new Map();
const maxByKind = /* @__PURE__ */ new Map();
for (const kind of KINDS) maxByKind.set(kind, 0);
for (const entry of previous?.entries ?? []) {
priorBySource.set(`${entry.kind}\0${entry.source}`, entry.id);
const n = idNumber(entry.id);
if (n > (maxByKind.get(entry.kind) ?? 0)) {
maxByKind.set(entry.kind, n);
}
}
const entries = [];
for (const req of current) {
const key = `${req.kind}\0${req.source}`;
const inherited = priorBySource.get(key);
if (inherited !== void 0) {
entries.push({ id: inherited, kind: req.kind, source: req.source });
continue;
}
const next = (maxByKind.get(req.kind) ?? 0) + 1;
maxByKind.set(req.kind, next);
entries.push({ id: `${req.kind}-${next}`, kind: req.kind, source: req.source });
}
return { entries };
}
function validateTrace(trace) {
const unsourced = trace.entries.filter((e) => typeof e.source !== "string" || e.source.trim().length === 0).map((e) => e.id);
return { ok: unsourced.length === 0, unsourced };
}
function extractRequirementIds(markdown) {
const ids = /* @__PURE__ */ new Set();
const re = /\b((?:FR|NFR|AC|INV)-\d+)\b/g;
let m;
while ((m = re.exec(markdown)) !== null) ids.add(m[1]);
return [...ids];
}
function validateCraftTrace(specMarkdown, trace, acceptedFindingIds = []) {
const errors = [];
const unsourced = validateTrace(trace);
for (const id of unsourced.unsourced) {
errors.push(
`line "${id}" has no source; every requirement must trace to the task, an answer, or an expert finding`
);
}
const tracedIds = new Set(trace.entries.map((entry) => entry.id));
for (const id of extractRequirementIds(specMarkdown)) {
if (!tracedIds.has(id)) {
errors.push(
`line "${id}" has no source; every requirement must trace to the task, an answer, or an expert finding`
);
}
}
const tracedSources = new Set(trace.entries.map((entry) => entry.source));
const unreflected = acceptedFindingIds.filter((id) => !tracedSources.has(id));
if (unreflected.length > 0) {
errors.push(`accepted expert findings not reflected in the spec: ${unreflected.join(", ")}`);
}
return { ok: errors.length === 0, errors };
}
function writeTrace(projectRoot, dirName, trace) {
writeStagedJson(projectRoot, dirName, "trace", trace);
}
function readTrace(projectRoot, dirName) {
const parsed = readStagedJson(projectRoot, dirName, "trace");
if (parsed !== null && Array.isArray(parsed.entries)) return parsed;
const frozen = readFrozenSpecificationBody(projectRoot, dirName);
const map = frozen?.trace;
if (typeof map === "object" && map !== null && !Array.isArray(map)) {
return traceFromMap(map);
}
const legacy = frozen?.provenance?.trace;
return legacy !== void 0 && Array.isArray(legacy.entries) ? legacy : null;
}
function traceFromMap(map) {
const entries = [];
for (const [id, source] of Object.entries(map)) {
const kind = kindOf(id);
if (kind !== null && typeof source === "string") entries.push({ id, kind, source });
}
return { entries };
}
function kindOf(id) {
const prefix = id.split("-")[0];
return KINDS.includes(prefix) ? prefix : null;
}
function parseTraceArtifact(raw) {
if (typeof raw !== "object" || raw === null) return null;
const entries = raw.entries;
if (!Array.isArray(entries)) return null;
const out = [];
for (const entry of entries) {
if (typeof entry !== "object" || entry === null) return null;
const { id, source } = entry;
if (typeof id !== "string" || typeof source !== "string") return null;
const kind = kindOf(id);
if (kind === null) return null;
out.push({ id, kind, source });
}
return { entries: out };
}
// src/spec-pipeline/experts/accounting.ts
init_esm_shims();
function buildExpertAccounting(input) {
const notesByRole = /* @__PURE__ */ new Map();
for (const note of input.notes) {
notesByRole.set(note.role, note);
}
const experts = input.needs.map((need) => {
const note = notesByRole.get(need.role);
const findings = note?.findings ?? [];
const changed = findings.some(
(finding) => finding.id !== void 0 && input.tracedFindingIds.has(finding.id)
);
const empty = findings.length === 0 || findings.length === 1 && findings[0].kind === "non-goal";
return {
role: need.role,
reason: need.reason,
tokens: input.tokens[need.role] ?? 0,
changed_spec: changed,
...need.origin === void 0 ? {} : { origin: need.origin },
empty
};
});
return {
experts,
total_tokens: experts.reduce((total, entry) => total + entry.tokens, 0),
warnings: [...input.warnings ?? []]
};
}
// src/spec-pipeline/experts/merge.ts
init_esm_shims();
function mergeExpertNotes(notes, voiceWarnings = []) {
const byTarget = /* @__PURE__ */ new Map();
const order = [];
for (const note of notes) {
for (const finding of note.findings) {
const key = normalize(finding.target);
if (!byTarget.has(key)) {
byTarget.set(key, []);
order.push(key);
}
byTarget.get(key).push({ role: note.role, finding });
}
}
const findings = [];
const conflicts = [];
for (const key of order) {
const group = collapseSameRole(byTarget.get(key));
const distinctClaims = [...new Set(group.map((entry) => normalize(entry.finding.claim)))];
if (distinctClaims.length <= 1) {
findings.push(group[0].finding);
continue;
}
conflicts.push({
target: group[0].finding.target,
roles: group.map((entry) => entry.role),
claims: group.map((entry) => entry.finding.claim),
finding_ids: group.map((entry) => entry.finding.id ?? ""),
...group.some((entry) => entry.finding.severity !== void 0) ? { severities: group.map((entry) => entry.finding.severity ?? null) } : {}
});
}
const qa = notes.find((note) => note.verdicts !== void 0)?.verdicts;
return {
findings,
conflicts,
...voiceWarnings.length > 0 ? { voice_warnings: [...voiceWarnings] } : {},
...qa === void 0 ? {} : { qa_verdicts: qa }
};
}
function collapseSameRole(group) {
const byRole = /* @__PURE__ */ new Map();
for (const entry of group) {
const list = byRole.get(entry.role);
if (list) list.push(entry);
else byRole.set(entry.role, [entry]);
}
return [...byRole.values()].map((entries) => {
if (entries.length === 1) return entries[0];
let kept = entries[0];
for (const entry of entries.slice(1)) {
if (entry.finding.claim.trim().length > kept.finding.claim.trim().length) kept = entry;
}
const mergedFrom = entries.flatMap((entry) => [entry.finding.id, ...entry.finding.merged_from ?? []]).filter((id) => typeof id === "string" && id.length > 0);
const findings = entries.map((entry) => entry.finding);
const severity = strongestSeverity(findings);
const kind = mergedKind(findings);
return {
role: kept.role,
finding: {
...kept.finding,
...severity ? { severity } : {},
...kind ? { kind } : {},
merged_from: [...new Set(mergedFrom)]
}
};
});
}
var SEVERITY_RANK = { must: 3, should: 2, could: 1 };
function strongestSeverity(findings) {
let best;
for (const finding of findings) {
if (finding.severity === void 0) continue;
if (best === void 0 || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[best]) {
best = finding.severity;
}
}
return best;
}
function mergedKind(findings) {
if (findings.some((finding) => finding.kind === "invariant")) return "invariant";
return findings.find((finding) => finding.kind !== void 0)?.kind;
}
function normalize(text) {
return text.trim().toLowerCase().replace(/\s+/g, " ");
}
// src/spec-pipeline/experts/need.ts
init_esm_shims();
function seatStandingExperts(detectorNeeds, standing) {
const named = new Set(detectorNeeds.map((need) => need.role));
const seated = detectorNeeds.map((need) => ({ ...need, origin: "detector" }));
for (const role of standing) {
if (named.has(role)) continue;
named.add(role);
seated.push({ role, reason: "standing expert (always at the table)", origin: "standing" });
}
return seated;
}
var SENSITIVE_PATH_EXPERT = "security-auditor";
var SKILL_PATH_EXPERT = "skill-orchestrator";
var AGENT_PATH_EXPERT = "agent-orchestrator";
function pathTriggeredSeats(input) {
const agentPath = input.agentPath ?? null;
return [
{
role: SENSITIVE_PATH_EXPERT,
path: input.standingSits ? null : input.sensitivePath,
reason: `a planned path is sensitive: ${input.sensitivePath}`
},
{
role: SKILL_PATH_EXPERT,
path: input.skillPath,
reason: `${input.skillPathEdited ? "an edited" : "a planned"} file is a skill: ${input.skillPath}`
},
{
role: AGENT_PATH_EXPERT,
path: agentPath,
reason: `${input.agentPathEdited ? "an edited" : "a planned"} file is an agent: ${agentPath}`
}
];
}
function seatExpertsForLane(input) {
const seated = seatStandingExperts(input.detectorNeeds, input.standingSits ? input.standing : []);
for (const seat of input.pathSeats) {
if (seat.path === null || seated.some((need) => need.role === seat.role)) continue;
seated.push({ role: seat.role, reason: seat.reason, origin: "detector" });
}
return seated;
}
function fail(error) {
return { ok: false, error };
}
function validateExpertNeed(raw) {
const parsed = typeof raw === "string" ? parseJson2(raw) : raw;
if (parsed === void 0) {
return fail("expert-need artifact is not valid JSON");
}
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
return fail("expert-need artifact must be an object with an experts[] array");
}
const experts = parsed.experts;
if (!Array.isArray(experts)) {
return fail("expert-need artifact needs an experts[] array");
}
const seen = /* @__PURE__ */ new Set();
const normalized = [];
for (const [index, entry] of experts.entries()) {
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
return fail(`experts[${index}] must be an object with role and reason`);
}
const { role, reason, origin } = entry;
if (role === "chief-architect") {
return fail('"chief-architect" is never picked: it runs automatically when any expert fires');
}
if (typeof role !== "string" || !isExpertRole(role)) {
return fail(
`experts[${index}].role "${String(role)}" is not an expert in the roster \u2014 the detector may not invent an expert (AC-8)`
);
}
if (typeof reason !== "string" || reason.trim().length === 0) {
return fail(`experts[${index}] ("${role}") needs a non-empty reason it fired (FR-7)`);
}
if (seen.has(role)) {
return fail(`experts names "${role}" twice \u2014 one decision per expert`);
}
seen.add(role);
normalized.push({
role,
reason: reason.trim(),
origin: origin === "standing" ? "standing" : "detector"
});
}
return { ok: true, artifact: { experts: normalized } };
}
function parseJson2(raw) {
try {
return JSON.parse(raw);
} catch {
return void 0;
}
}
// src/spec-pipeline/experts/notes.ts
init_esm_shims();
import { existsSync as existsSync7 } from "fs";
import { join as join11 } from "path";
// src/code-knowledge/query.ts
init_esm_shims();
var TOP_CALLERS_LIMIT = 10;
function distinctSorted(values) {
return [...new Set(values)].sort();
}
function symbolCard(index, symbol) {
const callers = index.reference_edges.filter((edge) => edge.to === symbol.file && edge.symbol === symbol.name).map((edge) => edge.from);
return {
kind: "symbol",
name: symbol.name,
file: symbol.file,
line: symbol.line,
signature: symbol.signature,
module_slug: symbol.module_slug,
extraction_tier: symbol.extraction_tier,
caller_count: symbol.caller_count,
orphan: symbol.orphan,
top_callers: distinctSorted(callers).slice(0, TOP_CALLERS_LIMIT)
};
}
function fileCard(index, file) {
const importers = index.import_edges.filter((edge) => edge.to === file.path).map((edge) => edge.from);
const symbols = index.symbols.filter((symbol) => symbol.file === file.path).map((symbol) => ({ name: symbol.name, caller_count: symbol.caller_count }));
return {
kind: "file",
path: file.path,
caller_count: file.caller_count,
orphan: file.orphan,
entry_point: file.entry_point,
importers: distinctSorted(importers).slice(0, TOP_CALLERS_LIMIT),
symbols
};
}
function queryCodeKnowledge(index, term) {
const symbolMatches = index.symbols.filter((symbol) => symbol.name === term).map((symbol) => symbolCard(index, symbol));
if (symbolMatches.length > 0) {
return { term, matches: symbolMatches };
}
const fileMatch = index.files.find((file) => file.path === term);
if (fileMatch) {
return { term, matches: [fileCard(index, fileMatch)] };
}
return { term, matches: [] };
}
// src/spec-pipeline/plain-language.ts
init_esm_shims();
var JARGON_TERMS = /* @__PURE__ */ new Set([
"endpoint",
"backoff",
"idempotent",
"idempotency",
"webhook",
"mutex",
"semaphore",
"cron",
"regex",
"middleware",
"polling",
"debounce",
"throttle",
"websocket",
"oauth",
"jwt",
"csrf",
"cors",
"orm",
"sdk",
"api",
"sql",
"nosql",
"cache",
"enum",
"boolean",
"null",
"async",
"thread",
"kubernetes",
"docker",
"redis",
"kafka"
]);
var JARGON_PHRASES = ["circuit breaker", "exponential backoff", "fail fast", "race condition"];
var HTTP_VERBS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
function looksTechnical(token) {
if (/[a-z][A-Z]/.test(token)) return true;
if (/_/.test(token)) return true;
if (/[/\\]/.test(token)) return true;
if (/\.[A-Za-z]{1,4}\b/.test(token)) return true;
if (/\w\(/.test(token)) return true;
if (/\d/.test(token) && /[A-Za-z]/.test(token)) return true;
if (HTTP_VERBS.has(token)) return true;
if (/^[1-5]\d\d$/.test(token)) return true;
if (JARGON_TERMS.has(token.toLowerCase())) return true;
return false;
}
function wordBag(text) {
const bag = /* @__PURE__ */ new Set();
for (const w of text.toLowerCase().split(/[^a-z0-9_]+/)) {
if (w.length > 0) bag.add(w);
}
return bag;
}
function checkPlainLanguage(question, sources) {
const haystack = [sources.prompt, ...sources.terms, ...sources.vocabulary ?? []].join(" ");
const allowed = wordBag(haystack);
const allowedLower = haystack.toLowerCase();
const flagged = /* @__PURE__ */ new Set();
const surfaces = [question.business_text, ...question.options];
const combined = surfaces.join(" ");
const combinedLower = combined.toLowerCase();
for (const phrase of JARGON_PHRASES) {
if (combinedLower.includes(phrase) && !allowedLower.includes(phrase)) {
flagged.add(phrase);
}
}
for (const surface of surfaces) {
for (const rawToken of surface.split(/\s+/)) {
const token = rawToken.replace(/^[^A-Za-z0-9_/\\.()]+|[^A-Za-z0-9_/\\.()]+$/g, "");
if (token.length === 0) continue;
if (!looksTechnical(token)) continue;
if (allowed.has(token.toLowerCase())) continue;
flagged.add(token);
}
}
const flaggedList = [...flagged];
return { ok: flaggedList.length === 0, flagged: flaggedList };
}
// src/spec-pipeline/experts/notes.ts
var FINDING_KINDS = [
"requirement",
"invariant",
"acceptance",
"risk",
"non-goal"
];
var FINDING_SEVERITIES = ["must", "should", "could"];
function normalizeTarget(text) {
return text.trim().toLowerCase().replace(/\s+/g, " ");
}
function closestWord(target, entries) {
for (const entry of entries) {
const term = normalizeTarget(entry.term);
if (term.length < 3) continue;
if (target.includes(term) || term.includes(target)) return entry;
}
for (const entry of entries) {
const shared = normalizeTarget(entry.term).split(/[^a-z0-9]+/).filter((token) => token.length >= 3);
if (shared.some((token) => target.includes(token))) return entry;
}
return null;
}
function checkFindingVoice(finding, sources) {
const target = normalizeTarget(finding.target);
if (target.startsWith("new ") || target === "this request") return { ok: true };
const inVocabulary = sources.vocabulary.some((entry) => normalizeTarget(entry.term) === target);
if (inVocabulary) return { ok: true };
if (normalizeTarget(sources.requestText).includes(target)) return { ok: true };
const stripped = finding.target.replace(/`/g, "").trim();
if (stripped.length > 0 && existsSync7(join11(sources.projectRoot, stripped))) return { ok: true };
if (sources.indexPresent) {
const index = readCodeKnowledgeIndex(sources.projectRoot);
if (index && queryCodeKnowledge(index, stripped).matches.length > 0) return { ok: true };
}
const business = closestWord(
target,
sources.vocabulary.filter((entry) => entry.kind !== "technical")
);
const technical = closestWord(
target,
sources.vocabulary.filter((entry) => entry.kind === "technical")
);
const parts = [];
if (business) parts.push(`the docs say "${business.term}" (${business.kind})`);
if (technical) parts.push(`the schema page names "${technical.term}"`);
const closeness = parts.length > 0 ? parts.join(" and ") : "no close match";
return {
ok: false,
hint: `target "${finding.target}" is not a name this project uses; ${closeness}`
};
}
function fail2(error) {
return { ok: false, error };
}
var QA_VERDICT_STATUSES = ["pass", "concerns", "fail"];
var QA_VERDICT_KEYS = ["spec_compliance", "quality"];
function checkVerdicts(role, raw, path, required) {
if (role === QA_ROLE && raw === void 0 && !required) return null;
if (role !== QA_ROLE) {
return raw === void 0 ? null : `${path} ("${role}") may not carry verdicts; only ${QA_ROLE} gives them`;
}
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
return `${path} ("${QA_ROLE}") needs verdicts with spec_compliance and quality`;
}
const out = {};
for (const key of QA_VERDICT_KEYS) {
const verdict = raw[key];
if (typeof verdict !== "object" || verdict === null || Array.isArray(verdict)) {
return `${path} ("${QA_ROLE}") needs a ${key} verdict`;
}
const { status, reason } = verdict;
if (!QA_VERDICT_STATUSES.includes(status)) {
return `${path}.verdicts.${key} has an unknown status "${String(status)}" (want ${QA_VERDICT_STATUSES.join(" | ")})`;
}
if (typeof reason !== "string" || reason.trim().length === 0) {
return `${path}.verdicts.${key} needs a non-empty reason`;
}
out[key] = { status, reason };
}
return out;
}
function validateExpertNotes(raw, sources, voiceSources, options = {}) {
const parsed = typeof raw === "string" ? parseJson3(raw) : raw;
if (parsed === void 0) return fail2("expert-notes artifact is not valid JSON");
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
return fail2("expert-notes artifact must be an object with a notes[] array");
}
const obj = parsed;
if (!Array.isArray(obj.notes)) return fail2("expert-notes artifact needs a notes[] array");
const notes = [];
const idCounters = /* @__PURE__ */ new Map();
for (const [index, entry] of obj.notes.entries()) {
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
return fail2(`notes[${index}] must be an object with role and findings`);
}
const { role, findings, questions, verdicts } = entry;
if (typeof role !== "string" || !isExpertRole(role)) {
return fail2(`notes[${index}].role "${String(role)}" is not an expert in the roster`);
}
const verdictCheck = checkVerdicts(
role,
verdicts,
`notes[${index}]`,
options.requireQaVerdicts !== false
);
if (typeof verdictCheck === "string") return fail2(verdictCheck);
if (!Array.isArray(findings)) {
return fail2(`notes[${index}] ("${role}") needs a findings[] array`);
}
const parsedFindings = [];
for (const [fi, finding] of findings.entries()) {
if (typeof finding !== "object" || finding === null || Array.isArray(finding)) {
return fail2(`notes[${index}].findings[${fi}] must be an object with target and claim`);
}
const { target, claim, kind, severity, evidence } = finding;
if (typeof target !== "string" || target.trim().length === 0) {
return fail2(`notes[${index}].findings[${fi}] needs a non-empty target`);
}
if (typeof claim !== "string" || claim.trim().length === 0) {
return fail2(`notes[${index}].findings[${fi}] needs a non-empty claim`);
}
if (kind !== void 0 && !FINDING_KINDS.includes(kind)) {
return fail2(
`notes[${index}].findings[${fi}] has an unknown kind "${String(kind)}" (want ${FINDING_KINDS.join(" | ")})`
);
}
if (severity !== void 0 && !FINDING_SEVERITIES.includes(severity)) {
return fail2(
`notes[${index}].findings[${fi}] has an unknown severity "${String(severity)}" (want ${FINDING_SEVERITIES.join(" | ")})`
);
}
if (evidence !== void 0 && typeof evidence !== "string") {
return fail2(`notes[${index}].findings[${fi}] evidence must be a string when present`);
}
const seq = (idCounters.get(role) ?? 0) + 1;
idCounters.set(role, seq);
parsedFindings.push({
id: `EX-${role}-${seq}`,
target,
claim,
kind: kind ?? "requirement",
severity: severity ?? "should",
...typeof evidence === "string" ? { evidence } : {}
});
}
const parsedQuestions = [];
if (questions !== void 0) {
if (!Array.isArray(questions)) {
return fail2(`notes[${index}] ("${role}") questions must be an array when present`);
}
for (const [qi, question] of questions.entries()) {
const validated = validateQuestion(question, `notes[${index}].questions[${qi}]`, sources);
if (!validated.ok) return fail2(validated.error);
parsedQuestions.push(validated.question);
}
}
notes.push({
role,
findings: parsedFindings,
...parsedQuestions.length > 0 ? { questions: parsedQuestions } : {},
...verdictCheck === null ? {} : { verdicts: verdictCheck }
});
}
const tokens = {};
if (obj.tokens !== void 0) {
if (typeof obj.tokens !== "object" || obj.tokens === null || Array.isArray(obj.tokens)) {
return fail2('expert-notes "tokens" must be an object of role -> number');
}
for (const [role, value] of Object.entries(obj.tokens)) {
if (!isExpertRole(role)) return fail2(`tokens names "${role}", which is not an expert role`);
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
return fail2(`tokens["${role}"] must be a non-negative number`);
}
tokens[role] = value;
}
}
let voice_warnings;
if (voiceSources) {
const warnings = [];
for (const note of notes) {
for (const finding of note.findings) {
const result = checkFindingVoice(finding, voiceSources);
if (!result.ok) {
warnings.push({ id: finding.id, target: finding.target, hint: result.hint });
}
}
}
voice_warnings = warnings;
}
return {
ok: true,
artifact: { notes, tokens, ...voice_warnings === void 0 ? {} : { voice_warnings } }
};
}
function validateQuestion(raw, path, sources) {
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
return { ok: false, error: `${path} must be a question object` };
}
const q = raw;
if (typeof q.business_text !== "string" || q.business_text.trim().length === 0) {
return { ok: false, error: `${path} needs a non-empty business_text` };
}
if (typeof q.why_it_matters !== "string" || q.why_it_matters.trim().length === 0) {
return { ok: false, error: `${path} needs a non-empty why_it_matters` };
}
if (!Array.isArray(q.options) || q.options.length === 0 || !q.options.every((o) => typeof o === "string" && o.trim().length > 0)) {
return { ok: false, error: `${path} needs a non-empty options[] of strings` };
}
if (q.grounded_in !== null && typeof q.grounded_in !== "string") {
return { ok: false, error: `${path} grounded_in must be a string or null` };
}
const question = {
business_text: q.business_text,
why_it_matters: q.why_it_matters,
options: q.options,
grounded_in: q.grounded_in ?? null,
...typeof q.technical_note === "string" ? { technical_note: q.technical_note } : {}
};
if (sources) {
const plain = checkPlainLanguage(question, sources);
if (!plain.ok) {
return {
ok: false,
error: `${path} is not plain language \u2014 flagged: ${plain.flagged.join(", ")}`
};
}
}
return { ok: true, question };
}
function parseJson3(raw) {
try {
return JSON.parse(raw);
} catch {
return void 0;
}
}
// src/spec-pipeline/experts/slice.ts
init_esm_shims();
function ceilingWarning(sum, ceiling, count) {
return `expert slices need ${sum} tokens but the run ceiling is ${ceiling}; scaled ${count} expert${count === 1 ? "" : "s"} to fit \u2014 none dropped`;
}
function planExpertSlices(roles, ceiling) {
if (roles.length === 0) {
return { slices: [], warnings: [] };
}
const budgets = roles.map((role) => ({ role, budget: expertBudget(role) }));
const sum = budgets.reduce((total, entry) => total + entry.budget, 0);
if (sum <= ceiling) {
return {
slices: budgets.map(({ role, budget }) => ({
role,
budget,
granted: budget,
clamped: false
})),
warnings: []
};
}
const slices = budgets.map(({ role, budget }) => {
const granted = Math.max(1, Math.floor(budget / sum * ceiling));
return { role, budget, granted, clamped: granted < budget };
});
return { slices, warnings: [ceilingWarning(sum, ceiling, roles.length)] };
}
// src/spec-pipeline/experts/assemble.ts
function assembleExpertRun(projectRoot, dirName, ceiling) {
const rawNeed = readExpertNeed(projectRoot, dirName);
if (rawNeed === null) return null;
const need = validateExpertNeed(rawNeed);
if (!need.ok || !need.artifact) return null;
const roles = need.artifact.experts.map((expert) => expert.role);
const slicePlan = planExpertSlices(roles, ceiling);
const rawNotes = readExpertNotes(projectRoot, dirName);
const notes = rawNotes === null ? { ok: true, artifact: { notes: [], tokens: {} } } : validateExpertNotes(rawNotes, void 0, void 0, { requireQaVerdicts: false });
const notesArtifact = notes.ok && notes.artifact ? notes.artifact : { notes: [], tokens: {} };
const merged = mergeExpertNotes(notesArtifact.notes);
const trace = readTrace(projectRoot, dirName);
const tracedFindingIds = new Set((trace?.entries ?? []).map((entry) => entry.source));
const accounting = buildExpertAccounting({
needs: need.artifact.experts,
notes: notesArtifact.notes,
tokens: notesArtifact.tokens,
tracedFindingIds,
warnings: slicePlan.warnings
});
return { accounting, conflicts: merged.conflicts };
}
// src/spec-pipeline/metrics.ts
function numberField(value) {
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
}
function buildRunMetrics(projectRoot, dirName, config, a5Live, a5Verdict) {
const grounding = readStagedJson(
projectRoot,
dirName,
"grounding"
);
const label = readLabel(projectRoot, dirName);
const questionsArtifact = readQuestions(projectRoot, dirName);
const task = readStagedJson(projectRoot, dirName, "task");
const trace = readStagedJson(projectRoot, dirName, "trace");
const spec = readSpec(projectRoot, dirName);
const questions = {
asked: questionsArtifact?.asked ?? 0,
answered: questionsArtifact?.answered ?? 0,
auto_answered: questionsArtifact?.auto_answered?.length ?? 0,
deferred: questionsArtifact?.deferred ?? 0
};
const tokensByStep = {};
const setStep = (step, value) => {
if (value !== void 0) tokensByStep[step] = value;
};
const questionsRow = readSpecStepRows(projectRoot, dirName).filter((row) => row.step === "questions" && row.outcome === "complete").at(-1);
setStep("questions", numberField(questionsRow?.tokens));
setStep("task", numberField(task?.tokens));
setStep("craft", numberField(trace?.tokens));
const tiersByStep = {};
const need = readNeed(projectRoot, dirName);
const expertCount = need?.experts.length ?? 0;
const ceilingWarnings = need ? planExpertSlices(
need.experts.map((expert) => expert.role),
config.token_ceiling
).warnings : [];
if (need) {
const notes = readExpertNotes(projectRoot, dirName);
for (const [role, value] of Object.entries(notes?.tokens ?? {})) {
const n = numberField(value);
if (n !== void 0) tokensByStep[role] = n;
}
const synthesis = readExpertSynthesis(projectRoot, dirName);
if (synthesis) {
setStep("experts", numberField(synthesis.tokens));
tiersByStep.experts = "reasoning";
}
}
const freezeChecksFired = spec ? evaluateSpecFreeze(spec.spec).blockers : [];
return {
grounding_sparse: grounding?.sparse ?? false,
grounding_path: grounding?.path === "rag" ? "rag" : "docs-fallback",
label: label?.label ?? "clear",
signal_count: label?.signals.length ?? 0,
questions,
expert_count: expertCount,
spec_words: spec ? spec.words : 0,
tokens_by_step: tokensByStep,
tiers_by_step: tiersByStep,
ceiling_warnings: ceilingWarnings,
a5_live: a5Live,
a5_verdict: a5Verdict,
freeze_checks_fired: freezeChecksFired
};
}
function readNeed(projectRoot, dirName) {
const raw = readExpertNeed(projectRoot, dirName);
if (raw === null) return null;
const validated = validateExpertNeed(raw);
return validated.ok && validated.artifact ? validated.artifact : null;
}
function readSpec(projectRoot, dirName) {
const markdown = readStagedText(projectRoot, dirName, "craft");
if (markdown === null) return null;
const words = markdown.split(/\s+/).filter((token) => token.length > 0).length;
try {
return {
spec: buildFeatureSpec({ spec_id: "metrics", spec_file: "spec.md", spec_markdown: markdown }),
words
};
} catch {
return null;
}
}
function recordSpecCorrection(projectRoot, dirName, correction) {
appendSpecCorrectionRow(
projectRoot,
dirName,
{ spec_id: correction.spec_id, changed_sections: correction.changed_sections },
{ now: () => new Date(correction.at) }
);
}
function readSpecCorrections(projectRoot, dirName) {
return readSpecCorrectionRows(projectRoot, dirName).map((row) => ({
spec_id: String(row.spec_id),
changed_sections: row.changed_sections,
at: String(row.recorded_at)
}));
}
function pipelineRun(projectRoot, dirName) {
if (readStagedJson(projectRoot, dirName, "finish")?.provenance) {
return { legacy: null };
}
const frozen = readFrozenSpecificationBody(projectRoot, dirName);
if (frozen?.pipeline?.produced === true) {
return { legacy: null };
}
const legacy = frozen?.provenance;
return legacy?.pipeline_produced === true ? { legacy } : null;
}
function stepTokens(projectRoot, dirName) {
const tokens = /* @__PURE__ */ new Map();
for (const row of readSpecStepRows(projectRoot, dirName)) {
const n = numberField(row.tokens);
if (row.outcome === "redone") tokens.delete(String(row.step));
else if (row.outcome === "complete" && n !== void 0) tokens.set(String(row.step), n);
}
return tokens;
}
function aggregateSpecPipelineMetrics(projectRoot, dirNames) {
const aggregate = {
runs: 0,
experts_fired: {},
changed_spec_rate: {},
tokens_by_role: {},
tokens_by_step: {},
conflicts: 0,
auto_resolved: 0,
corrections_by_section: {},
label_distribution: {},
grounding_sparse_runs: 0,
tier_by_role: {},
empty_by_role: {},
voice_warnings_by_role: {},
renames: 0
};
for (const dirName of dirNames) {
for (const correction of readSpecCorrections(projectRoot, dirName)) {
for (const section of correction.changed_sections) {
aggregate.corrections_by_section[section] = (aggregate.corrections_by_section[section] ?? 0) + 1;
}
}
const run = pipelineRun(projectRoot, dirName);
if (run === null) continue;
aggregate.runs += 1;
const experts = assembleExpertRun(projectRoot, dirName, Number.MAX_SAFE_INTEGER);
const fired = (role) => {
aggregate.experts_fired[role] = (aggregate.experts_fired[role] ?? 0) + 1;
const rate = aggregate.changed_spec_rate[role] ??= { fired: 0, changed: 0 };
rate.fired += 1;
return rate;
};
if (experts) {
for (const expert of experts.accounting.experts) {
const rate = fired(expert.role);
if (expert.changed_spec) rate.changed += 1;
aggregate.tokens_by_role[expert.role] = (aggregate.tokens_by_role[expert.role] ?? 0) + expert.tokens;
if (expert.origin === "standing") aggregate.tier_by_role[expert.role] = "standing";
else if (aggregate.tier_by_role[expert.role] === void 0)
aggregate.tier_by_role[expert.role] = "on-call";
const emptyRate = aggregate.empty_by_role[expert.role] ??= { fired: 0, empty: 0 };
emptyRate.fired += 1;
if (expert.empty) emptyRate.empty += 1;
}
aggregate.conflicts += experts.conflicts.length;
for (const warning of readExperts(projectRoot, dirName)?.voice_warnings ?? []) {
const role = roleOfFindingId(warning.id);
if (role) {
aggregate.voice_warnings_by_role[role] = (aggregate.voice_warnings_by_role[role] ?? 0) + 1;
}
}
} else if (run.legacy?.experts) {
for (const role of run.legacy.experts.roles) fired(role);
aggregate.conflicts += run.legacy.experts.conflicts;
}
const label = readLabel(projectRoot, dirName)?.label ?? run.legacy?.label;
if (label !== void 0) {
aggregate.label_distribution[label] = (aggregate.label_distribution[label] ?? 0) + 1;
}
const sparse = readGrounding(projectRoot, dirName)?.sparse ?? run.legacy?.grounding?.sparse;
if (sparse === true) aggregate.grounding_sparse_runs += 1;
const synthesis = readExpertSynthesis(projectRoot, dirName);
const tokens = stepTokens(projectRoot, dirName);
const synthesisTokens = numberField(synthesis?.tokens);
if (!tokens.has("experts") && synthesisTokens !== void 0) {
tokens.set("experts", synthesisTokens);
}
for (const [step, value] of tokens) {
aggregate.tokens_by_step[step] = (aggregate.tokens_by_step[step] ?? 0) + value;
}
aggregate.auto_resolved += synthesis?.auto_resolved?.length ?? 0;
aggregate.renames += synthesis?.renames?.length ?? 0;
}
return aggregate;
}
function roleOfFindingId(id) {
const match = /^EX-(.+)-\d+$/.exec(id);
return match ? match[1] : null;
}
function listRunDirs(projectRoot) {
return listFeatureDirs(projectRoot);
}
// src/spec/spec-decisions.ts
init_esm_shims();
var DEFAULT_REQUESTED_BY = "spec-stage";
function buildSpecChangePacket(input) {
return buildPacket("spec.change", input, {
question: "The goal changed mid-build. Update the frozen spec and re-freeze, or keep it as-is?",
options: [
{
option_key: "update-and-refreeze",
label: "Take the new goal",
one_line_preview: `If you pick this, we will update ${input.spec_file} to the new goal and re-freeze it.`,
trade_off: "You give up: the first frozen goal, so the new one becomes the bar."
},
{
option_key: "keep-current-spec",
label: "Keep the current spec",
one_line_preview: "If you pick this, we will keep building to the spec already frozen.",
trade_off: "You give up: capturing the new goal, so the change waits."
}
],
recommendation: "update-and-refreeze",
recommendation_reason: "A real goal change must update the spec and re-freeze before work continues."
});
}
function buildPacket(category, input, shape) {
const ttlDays = DECISION_CATEGORY_DEFAULTS[category].ttl_days;
const ttlUntil = new Date(
new Date(input.created_at).getTime() + ttlDays * 24 * 60 * 60 * 1e3
).toISOString();
const fingerprint2 = `sha256:${sha256Hex(`${category}:${input.spec_id}:${input.detail}`)}`;
const packet = {
decision_id: input.decision_id,
fingerprint: fingerprint2,
category,
question: shape.question,
context: `Spec ${input.spec_id} (${input.spec_file}). ${input.detail}`,
options: shape.options.map((option) => ({ ...option, evidence: {} })),
confidence: 0.5,
requested_by: input.requested_by ?? DEFAULT_REQUESTED_BY,
task_session_id: input.task_session_id,
linked_requirements: [],
created_at: input.created_at,
status: "pending",
ttl_until: ttlUntil,
invalidation_watch: [input.spec_file]
};
if (shape.recommendation !== null) {
packet.recommendation = shape.recommendation;
packet.recommendation_reason = shape.recommendation_reason;
}
return packet;
}
// src/spec/spec-change-guard.ts
function diffSpecSections(frozen, current) {
const changed = [];
const differs = (a, b) => JSON.stringify(a) !== JSON.stringify(b);
if (differs(frozen.behaviour, current.behaviour)) changed.push("behaviour");
if (differs(frozen.acceptance_criteria, current.acceptance_criteria)) {
changed.push("acceptance_criteria");
}
if (differs(frozen.invariants, current.invariants)) changed.push("invariants");
if (differs(frozen.non_goals ?? [], current.non_goals ?? [])) changed.push("non_goals");
return changed;
}
function pipelineProduced(spec) {
return spec.pipeline?.produced === true || spec.provenance?.pipeline_produced === true;
}
var NO_OP3 = { ran: false, blocking: false, summary: "" };
function watchedSpec(dirName, spec) {
return spec.spec_file === FEATURE_BUNDLE_FILES.specMd ? { spec, dirName, source: featureFilePath(dirName, "specMd"), bundled: true } : { spec, dirName, source: spec.spec_file, bundled: false };
}
var STALE_DETAIL = "The frozen spec source changed since it was frozen \u2014 the goal may have moved. Confirm the new goal before more is built on the old one.";
function mintedSummary2(specId) {
return `**\u25B8 paqad** \xB7 the frozen spec ${specId} changed mid-build \u2014 that's a goal call that's yours
> I paused and wrote it up so you can update-and-refreeze or hold the line \u2014 answer it, then I'll continue.`;
}
function runSpecChangeGuard(input) {
if (input.seam !== void 0 && input.seam !== "pre-mutation") return NO_OP3;
if (!input.sessionId) return NO_OP3;
const specs = input.frozenSpecs ? input.frozenSpecs.map((spec) => ({
spec,
dirName: null,
source: spec.spec_file,
bundled: false
})) : readAllFeatureSpecificationEntries(input.projectRoot).map(
(entry) => watchedSpec(entry.dirName, entry.spec)
);
if (specs.length === 0) return NO_OP3;
const store = input.store ?? new DecisionStore(input.projectRoot);
store.initialize();
if (store.listPendingDecisionIds().length > 0) return NO_OP3;
const readMarkdown = input.readMarkdown ?? ((specFile) => readFileSync10(join12(input.projectRoot, specFile), "utf8"));
const now = input.now?.() ?? /* @__PURE__ */ new Date();
for (const { spec, dirName, source, bundled } of specs) {
let currentMarkdown;
try {
const text = readMarkdown(source);
currentMarkdown = bundled ? splitFrontMatter(text).body : text;
} catch {
continue;
}
if (!isFrozenSpecStale(spec, sha256Hex(currentMarkdown))) continue;
const changedSections = diffSpecSections(
spec,
buildFeatureSpec({
spec_id: spec.spec_id,
spec_file: spec.spec_file,
spec_markdown: currentMarkdown
})
);
if (dirName !== null && pipelineProduced(spec)) {
recordSpecCorrection(input.projectRoot, dirName, {
spec_id: spec.spec_id,
changed_sections: changedSections,
at: now.toISOString()
});
}
const packet = buildSpecChangePacket({
decision_id: store.nextDecisionId(),
spec_id: spec.spec_id,
spec_file: source,
detail: changedSections.length > 0 ? `${STALE_DETAIL} Changed sections: ${changedSections.join(", ")}.` : STALE_DETAIL,
task_session_id: input.sessionId,
created_at: now.toISOString()
});
if (store.findReusableDecision(packet)) continue;
try {
store.writePending(packet);
return { ran: true, blocking: false, summary: mintedSummary2(spec.spec_id) };
} catch {
return NO_OP3;
}
}
return NO_OP3;
}
// src/rule-scripts/edit-result.ts
init_esm_shims();
import {
closeSync,
constants,
lstatSync,
mkdirSync as mkdirSync7,
mkdtempSync,
openSync,
readdirSync as readdirSync5,
readFileSync as readFileSync11,
realpathSync as realpathSync2,
rmSync as rmSync3,
symlinkSync,
writeFileSync as writeFileSync7
} from "fs";
import { tmpdir } from "os";
import { join as join13, sep } from "path";
// src/rule-scripts/edited-content.ts
init_esm_shims();
var EDIT_KEYS = /* @__PURE__ */ new Set(["file_path", "old_string", "new_string", "replace_all"]);
var WRITE_KEYS = /* @__PURE__ */ new Set(["file_path", "content"]);
var PATCH_BEGIN = "*** Begin Patch";
var PATCH_END = "*** End Patch";
var PATCH_UPDATE = "*** Update File: ";
function asObject(input) {
return typeof input === "object" && input !== null && !Array.isArray(input) ? input : null;
}
function onlyKeys(input, allowed) {
return Object.keys(input).every((key) => allowed.has(key));
}
function applyEdit(input, current) {
const { old_string: before, new_string: after, replace_all: replaceAll } = input;
if (!onlyKeys(input, EDIT_KEYS)) return null;
if (typeof before !== "string" || typeof after !== "string") return null;
if (before === "" || before === after) return null;
if (replaceAll !== void 0 && typeof replaceAll !== "boolean") return null;
const parts = current.split(before);
if (parts.length < 2) return null;
if (parts.length > 2 && replaceAll !== true) return null;
return parts.join(after);
}
function applyWrite(input) {
if (!onlyKeys(input, WRITE_KEYS)) return null;
return typeof input.content === "string" ? input.content : null;
}
function patchText(input) {
const object = asObject(input);
const values = object === null ? [input] : Object.values(object);
const patches = values.filter(
(value) => typeof value === "string" && value.includes(PATCH_BEGIN)
);
return patches.length === 1 ? patches[0] : null;
}
function readChunks(body) {
const chunks = [];
let open = null;
for (const line of body) {
if (line === "@@") {
open = { before: [], after: [] };
chunks.push(open);
continue;
}
const mark = line[0];
if (mark !== " " && mark !== "-" && mark !== "+") return null;
if (open === null) {
open = { before: [], after: [] };
chunks.push(open);
}
const text = line.slice(1);
if (mark !== "+") open.before.push(text);
if (mark !== "-") open.after.push(text);
}
return chunks.length > 0 && chunks.every((chunk) => chunk.before.length > 0) ? chunks : null;
}
function parsePatch(text) {
const lines = text.split("\n");
if (lines.at(-1) === "") lines.pop();
if (lines[0] !== PATCH_BEGIN || lines.at(-1) !== PATCH_END) return null;
if (!lines[1]?.startsWith(PATCH_UPDATE)) return null;
return readChunks(lines.slice(2, -1));
}
function matchesAt(lines, wanted, at) {
return wanted.every((line, offset) => lines[at + offset] === line);
}
function onlyMatch(lines, wanted) {
const found = [];
for (let at = 0; at + wanted.length <= lines.length; at += 1) {
if (matchesAt(lines, wanted, at)) found.push(at);
}
return found.length === 1 ? found[0] : null;
}
function applyPatch(input, current) {
const text = patchText(input);
const chunks = text === null ? null : parsePatch(text);
if (chunks === null) return null;
const lines = current.split("\n");
if (lines.at(-1) === "") lines.pop();
let cursor = 0;
for (const chunk of chunks) {
const at = onlyMatch(lines, chunk.before);
if (at === null || at < cursor) return null;
lines.splice(at, chunk.before.length, ...chunk.after);
cursor = at + chunk.after.length;
}
return `${lines.join("\n")}
`;
}
function editedContent(toolName, toolInput, current) {
if (toolName === "apply_patch") return applyPatch(toolInput, current);
const input = asObject(toolInput);
if (input === null) return null;
if (toolName === "Edit") return applyEdit(input, current);
if (toolName === "Write") return applyWrite(input);
return null;
}
// src/rule-scripts/edit-result.ts
var RULE_SOURCES_PREFIX = "docs/instructions/rules/";
var MAX_GROWN_RESULT_BYTES = 1024 * 1024;
var REWRITE_NOT_THROUGH_LINK = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW;
var NOT_A_CANDIDATE = { allowed: false, reason: null };
function refuse(reason) {
return { allowed: false, reason };
}
function spelledAsOnDisk(projectRoot, rel) {
let dir = projectRoot;
return rel.split("/").every((segment) => {
const found = readdirSync5(dir).includes(segment);
dir = join13(dir, segment);
return found;
});
}
function isRegularFileInProject(projectRoot, rel) {
const abs = join13(projectRoot, rel);
return lstatSync(abs).isFile() && realpathSync2(abs) === join13(realpathSync2(projectRoot), rel) && spelledAsOnDisk(projectRoot, rel);
}
function resultSizeAllowed(current, after) {
const size = Buffer.byteLength(after);
return size <= MAX_GROWN_RESULT_BYTES || size <= Buffer.byteLength(current);
}
function readEditedTarget(input, rel) {
if (!isRegularFileInProject(input.projectRoot, rel)) {
return { reason: "the target is not a plain file inside the project" };
}
const current = readFileSync11(join13(input.projectRoot, rel), "utf8");
const after = editedContent(input.toolName, input.toolInput, current);
if (after === null) return { reason: "the result of this edit could not be worked out" };
if (!resultSizeAllowed(current, after)) return { reason: "the result of this edit is too large" };
return { target: { rel, current, after } };
}
function rerunScripts(projectRoot) {
const scripts = [];
for (const rule of loadRuleScriptMap(projectRoot)?.rules ?? []) {
for (const script of rule.scripts) {
if (script.kind !== "deterministic" || script.scope !== "changed-files") continue;
const abs = join13(projectRoot, script.path);
let source;
try {
source = readFileSync11(abs, "utf8");
} catch {
return null;
}
const { header } = parseScriptHeader(source);
if (missingBinaries(header?.requires?.binaries).length > 0) return null;
scripts.push({
ruleId: rule.id,
path: script.path,
abs,
perFile: header?.per_file === true
});
}
}
return scripts;
}
function buildOverlay(projectRoot, rel, overlay) {
const segments = rel.split("/");
let from = projectRoot;
let to = overlay;
segments.forEach((segment, index) => {
for (const name of readdirSync5(from)) {
if (name !== segment) symlinkSync(join13(from, name), join13(to, name));
}
from = join13(from, segment);
to = join13(to, segment);
if (index < segments.length - 1) mkdirSync7(to);
});
}
function rewriteNotThroughLink(path, content) {
const file = openSync(path, REWRITE_NOT_THROUGH_LINK);
try {
writeFileSync7(file, content, "utf8");
} finally {
closeSync(file);
}
}
async function scanOverlay(overlay, scripts, target, changedFiles, input) {
const runs = await runPool(
scripts,
input.concurrency,
(script) => executeRuleScriptAsync(
script.abs,
{ projectRoot: overlay, files: script.perFile ? [target] : [...changedFiles] },
{ signal: input.signal }
)
);
if (runs.some((run) => !run.ok || !run.report)) return null;
return runs.flatMap(
(run, index) => (run.report?.findings ?? []).map((finding) => ({
...finding,
script: scripts[index].path,
rule_id: scripts[index].ruleId
}))
);
}
function counts(keys) {
const tally = /* @__PURE__ */ new Map();
for (const key of keys) tally.set(key, (tally.get(key) ?? 0) + 1);
return tally;
}
function sameCounts(left, right) {
const a = counts(left);
const b = counts(right);
return a.size === b.size && [...a].every(([key, count]) => b.get(key) === count);
}
function exactKey(finding) {
return JSON.stringify([
finding.script,
finding.rule_id,
finding.file,
finding.line ?? null,
finding.message,
finding.severity
]);
}
function comparableKey(finding, target, content) {
const onTarget = finding.file === target.rel;
const where = onTarget && finding.line !== void 0 ? content.split("\n")[finding.line - 1] ?? "" : finding.line ?? null;
return JSON.stringify([
finding.script,
finding.rule_id,
finding.file,
where,
finding.message,
finding.severity
]);
}
function matchesRealRun(baseline, scripts, target, violations) {
const byPath = new Map(scripts.map((script) => [script.path, script]));
const rerun = violations.filter((violation) => {
const script = byPath.get(violation.script);
return script !== void 0 && (!script.perFile || violation.file === target);
});
return sameCounts(baseline.map(exactKey), rerun.map(exactKey));
}
function compareResults(baseline, after, target) {
if (after.some((finding) => finding.file === target.rel && finding.line === void 0)) {
return { reason: "a finding left in the target has no line, so it cannot be told apart" };
}
const before = counts(baseline.map((finding) => comparableKey(finding, target, target.current)));
const result = counts(after.map((finding) => comparableKey(finding, target, target.after)));
for (const [key, count] of result) {
if (count > (before.get(key) ?? 0)) return { reason: "this edit adds a finding" };
}
const onTarget = (finding) => finding.file === target.rel;
const removed = baseline.filter(onTarget).length - after.filter(onTarget).length;
return removed > 0 ? { removed } : { reason: "this edit removes no finding from its target" };
}
function removedFindings(baseline, after, target, violations) {
const left = counts(after.map((finding) => comparableKey(finding, target, target.after)));
const removed = /* @__PURE__ */ new Set();
for (const finding of baseline) {
const key = comparableKey(finding, target, target.current);
const remaining = left.get(key) ?? 0;
if (remaining > 0) left.set(key, remaining - 1);
else if (finding.file === target.rel) removed.add(exactKey(finding));
}
return violations.filter((violation) => removed.has(exactKey(violation)));
}
async function compareOnOverlay(input, target, scripts, changedFiles) {
const overlay = realpathSync2(mkdtempSync(join13(tmpdir(), "paqad-edit-result-")));
try {
buildOverlay(input.projectRoot, target.rel, overlay);
const overlayTarget = join13(overlay, target.rel);
writeFileSync7(overlayTarget, target.current, { encoding: "utf8", flag: "wx" });
const baseline = await scanOverlay(overlay, scripts, target.rel, changedFiles, input);
if (baseline === null) return refuse("a rule script gave no answer");
if (!matchesRealRun(baseline, scripts, target.rel, input.violations)) {
return refuse("the comparison did not reproduce the findings of the real run");
}
rewriteNotThroughLink(overlayTarget, target.after);
const after = await scanOverlay(overlay, scripts, target.rel, changedFiles, input);
if (after === null) return refuse("a rule script gave no answer");
const compared = compareResults(baseline, after, target);
if ("reason" in compared) return refuse(compared.reason);
return {
allowed: true,
target: target.rel,
removed: removedFindings(baseline, after, target, input.violations),
remaining: input.violations.length - compared.removed
};
} finally {
rmSync3(overlay, { recursive: true, force: true });
}
}
async function judgeEditResult(input) {
const rels = projectRelativeTargets(input.projectRoot, [...input.targets]);
const rel = rels[0];
if (input.targets.length !== 1 || rel === void 0) return NOT_A_CANDIDATE;
if (sep !== "/" || rel.includes("\\")) return NOT_A_CANDIDATE;
if (!input.violations.some((violation) => violation.file === rel)) return NOT_A_CANDIDATE;
if (rel.startsWith(RULE_SOURCES_PREFIX)) return refuse("the target is a rule file");
const read = readEditedTarget(input, rel);
if ("reason" in read) return refuse(read.reason);
const scripts = rerunScripts(input.projectRoot);
if (scripts === null) return refuse("a rule script could not run");
const evidence = await loadChangeEvidence(input.projectRoot);
const changedFiles = Array.from(/* @__PURE__ */ new Set([...evidence.files, rel]));
return compareOnOverlay(input, read.target, scripts, changedFiles);
}
// src/rule-scripts/pending-marker.ts
init_esm_shims();
import { existsSync as existsSync8, rmSync as rmSync4 } from "fs";
import { join as join14 } from "path";
var WITNESS_FILE = ".pending-pre-mutation.json";
function pendingPreMutationPaths(projectRoot, sessionId) {
const safe = safeSessionId(sessionId);
return [
join14(projectRoot, PATHS.RULE_SCRIPTS_PENDING_DIR, `${safe}.json`),
join14(projectRoot, sessionLedgerDir(STAGE_EVIDENCE_DOC_TYPE, safe), WITNESS_FILE)
];
}
function writePendingPreMutation(projectRoot, marker) {
const body = `${JSON.stringify(marker, null, 2)}
`;
for (const path of pendingPreMutationPaths(projectRoot, marker.session_id)) {
writeFileAtomic(path, body);
}
}
function hasPendingPreMutation(projectRoot, sessionId) {
return pendingPreMutationPaths(projectRoot, sessionId).some((path) => existsSync8(path));
}
function clearPendingPreMutation(projectRoot, sessionId) {
for (const path of pendingPreMutationPaths(projectRoot, sessionId)) {
rmSync4(path, { force: true });
}
}
// src/hld/gate.ts
init_esm_shims();
// src/hld/applies.ts
init_esm_shims();
function designSignOffApplies(config, lane) {
return config.enabled && lane !== null && config.lanes.includes(lane);
}
function designSignOffWaits(config, lane) {
return designSignOffApplies(config, lane) && config.wait_for_approval;
}
function loadDesignSignOff(projectRoot) {
return loadGitWorkflow(projectRoot).config.design_sign_off;
}
function changeLane(projectRoot, sessionId, dirName) {
return resolveEffectiveLane(
projectRoot,
void 0,
foldFeature(projectRoot, sessionId, dirName).lane
);
}
function freezeNudge(projectRoot, sessionId, dirName) {
if (!designSignOffApplies(
loadDesignSignOff(projectRoot),
changeLane(projectRoot, sessionId, dirName)
)) {
return null;
}
return readHld(projectRoot, dirName)?.state === "changes_requested" ? FREEZE_NEXT_V2_LINE : FREEZE_NEXT_LINE;
}
// src/hld/gate.ts
var OPEN_STATES = /* @__PURE__ */ new Set(["approved", "skipped", "not_posted"]);
function designSignOffBlock(projectRoot, dirName, lane) {
const config = loadDesignSignOff(projectRoot);
if (!designSignOffWaits(config, lane)) return null;
const record = readHld(projectRoot, dirName);
if (record === null) return GATE_NO_DESIGN_LINE;
if (OPEN_STATES.has(record.state)) return null;
return gateBlockLine(record.ref, config.allow_skip_with_reason);
}
// src/feature-evidence/rules-loaded.ts
init_esm_shims();
import { mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
import { dirname as dirname6, join as join16 } from "path";
// src/context/rule-context.ts
init_esm_shims();
import { createHash } from "crypto";
import { existsSync as existsSync9 } from "fs";
import { join as join15 } from "path";
// src/context/rule-manifest.ts
init_esm_shims();
var SCRIPT_GLYPH = "\u2699";
var DEFAULT_SUMMARY_CHARS = 140;
function scriptedSourcePaths(map) {
const paths = /* @__PURE__ */ new Set();
if (!map) return paths;
for (const rule of map.rules ?? []) {
if ((rule.scripts?.length ?? 0) > 0) {
paths.add(rule.source);
}
}
return paths;
}
function oneLineSummary(summary, maxChars) {
const flattened = summary.replace(/`/g, "").replace(/\s+/g, " ").replace(/^[-*]\s*/, "").trim();
if (flattened.length <= maxChars) return flattened;
return `${flattened.slice(0, maxChars - 1).trimEnd()}\u2026`;
}
function renderTrigger(pattern) {
return `\`${pattern.replace(/`/g, "").replace(/\s+/g, "")}\``;
}
function manifestLine(rule, scripted, maxChars) {
const triggers = rule.trigger_patterns.length ? rule.trigger_patterns.map(renderTrigger).join(" ") : "`**`";
const script = scripted.has(rule.source_path) ? ` ${SCRIPT_GLYPH}` : "";
const summary = oneLineSummary(rule.summary, maxChars);
return `- **${rule.rule_id}** ${rule.title} \xB7 ${rule.severity} \xB7 triggers: ${triggers}${script}${summary ? ` \u2014 ${summary}` : ""}`;
}
function generateRuleManifest(store, options = {}) {
const scripted = options.scriptedSourcePaths ?? /* @__PURE__ */ new Set();
const maxChars = options.maxSummaryChars ?? DEFAULT_SUMMARY_CHARS;
const rules = store.rules ?? [];
const header = `## paqad rule manifest \u2014 ${rules.length} ${rules.length === 1 ? "rule" : "rules"}`;
const intro = `> Every rule that governs this repo. This is the index; the full text of the rules that apply to your changed files is below under "Loaded rule text" \u2014 read it, then run \`paqad-ai rules load\` to record that you did (required for a code change). ${SCRIPT_GLYPH} marks a script-enforced rule: paqad enforces it whether or not its text is loaded.`;
if (rules.length === 0) {
return `${header}
${intro}
_No rules compiled yet._
`;
}
const lines = rules.map((rule) => manifestLine(rule, scripted, maxChars));
return `${header}
${intro}
${lines.join("\n")}
`;
}
// src/context/rule-context.ts
var STALE_LOCK_MS = 10 * 60 * 1e3;
var RULES_MISSING_FALLBACK_MARKER = "> \u26A0\uFE0F No compiled rules in this artifact \u2014 load `docs/instructions/rules/` in full before relying on rules.";
var DECISION_PAUSE_REMINDER = "## Decision pause is active\n> Before a create-vs-reuse, shared-abstraction, architecture-path, component-reuse, workflow-or-tool, or ux-pattern choice, write a decision packet and pause for the human (`paqad-ai decision create \u2026`) \u2014 do not decide it unilaterally.";
function selectTriggeredRules(rules, changedPaths) {
const alwaysLoad = [];
const triggered = [];
for (const rule of rules) {
if (isAlwaysLoadRule(rule)) {
alwaysLoad.push(rule);
} else if (changedPaths.length > 0 && ruleTriggersMatch(rule, changedPaths)) {
triggered.push(rule);
}
}
return { alwaysLoad, triggered };
}
function ruleTextBlock(rule) {
const body = (rule.raw_text ?? rule.summary).trim();
return `### ${rule.rule_id} \xB7 ${rule.title}
${body}`;
}
function composeLoadedRuleText(rules) {
return rules.map(ruleTextBlock).join("\n\n");
}
function hashRuleText(text) {
return createHash("sha256").update(text).digest("hex");
}
function computeRuleApplicability(rules, changedPaths) {
const { alwaysLoad, triggered } = selectTriggeredRules(rules, changedPaths);
const applicable = [
...alwaysLoad.map((rule) => ({
rule_id: rule.rule_id,
title: rule.title,
always_load: true,
matched_paths: []
})),
...triggered.map((rule) => ({
rule_id: rule.rule_id,
title: rule.title,
always_load: false,
matched_paths: changedPaths.filter((path) => ruleTriggersMatch(rule, [path]))
}))
];
const loadedRuleText = composeLoadedRuleText([...alwaysLoad, ...triggered]);
return {
applicable,
loadedRuleText,
ruleTextHash: hashRuleText(loadedRuleText),
ruleSourcePaths: [...alwaysLoad, ...triggered].map((rule) => rule.source_path)
};
}
async function changeOwnPaths(projectRoot, sessionId, scope) {
const since = scope.dirName ? readChangeStartPoint(projectRoot, scope.dirName) : void 0;
const evidence = await loadChangeEvidence(projectRoot, since ? { since } : {});
const files = evidence.files.filter((filePath) => !isPaqadArtifactPath(filePath));
return sessionId === null ? files : subtractUnchangedBaselineFiles(projectRoot, sessionId, files);
}
async function resolveRuleApplicabilityForChange(projectRoot, sessionId = null, scope = {}) {
const store = await readCompiledRules(projectRoot);
const changedPaths = await changeOwnPaths(projectRoot, sessionId, scope);
const rules = store?.rules ?? [];
if (rules.length === 0) {
return {
applicable: [],
loadedRuleText: "",
ruleTextHash: hashRuleText(""),
ruleSourcePaths: [],
changedPaths,
hasStore: false
};
}
return { ...computeRuleApplicability(rules, changedPaths), changedPaths, hasStore: true };
}
function composeRuleContext(store, options = {}) {
const changedPaths = options.changedPaths ?? [];
const manifest = generateRuleManifest(store, { scriptedSourcePaths: options.scriptedPaths });
const { alwaysLoad, triggered } = selectTriggeredRules(store.rules ?? [], changedPaths);
const loaded = [...alwaysLoad, ...triggered];
if (loaded.length === 0) {
return manifest;
}
const heading = triggered.length > 0 ? `## Loaded rule text \u2014 ${loaded.length} rules apply to the files in play` : `## Loaded rule text \u2014 ${loaded.length} always-on rules`;
const blocks = composeLoadedRuleText(loaded);
return `${manifest}
${heading}
${blocks}
`;
}
async function writeRuleContext(projectRoot, options = {}) {
const loadRules = options.loadRules ?? true;
const store = loadRules ? await readCompiledRules(projectRoot) : null;
const existingSurfaceSection = loadRules ? options.existingSurfaceSection?.trim() ?? "" : "";
const memorySection = options.memorySection?.trim() ?? "";
const retrievalSection = options.retrievalSection?.trim() ?? "";
const driftSection = options.driftSection?.trim() ?? "";
if (!store && !existingSurfaceSection && !memorySection && !retrievalSection && !driftSection)
return null;
let markdown = "";
if (store) {
const changedPaths = (await loadChangeEvidence(projectRoot)).files;
const scriptedPaths = scriptedSourcePaths(loadRuleScriptMap(projectRoot));
markdown = composeRuleContext(store, { changedPaths, scriptedPaths });
if ((store.rules?.length ?? 0) > 0) {
markdown = `${markdown}
${DECISION_PAUSE_REMINDER}
`;
}
}
for (const section of [existingSurfaceSection, memorySection, retrievalSection, driftSection]) {
if (section) {
markdown = markdown ? `${markdown}
${section}
` : `${section}
`;
}
}
const hasRules = (store?.rules?.length ?? 0) > 0;
if (loadRules && !hasRules) {
markdown = `${RULES_MISSING_FALLBACK_MARKER}
${markdown}`;
}
const target = join15(projectRoot, PATHS.CONTEXT_SESSION_ARTIFACT);
await atomicWriteFile(target, markdown);
return target;
}
async function ensureCompiledRulesCurrent(projectRoot) {
if (!existsSync9(join15(projectRoot, PATHS.RULES_DIR))) {
return null;
}
try {
if (await isCompiledRulesStale(projectRoot)) {
await writeCompiledRules(projectRoot, await compileRules(projectRoot));
}
return null;
} catch (error) {
return error instanceof Error ? error : new Error(String(error));
}
}
function compileFailureReason(error) {
return error.message.replace(/\.+$/u, "");
}
async function refreshRuleContext(projectRoot, options = {}) {
const lockDir = join15(projectRoot, PATHS.LOCKS_DIR, "rule-context.lock");
const lock = tryAcquireLock(lockDir, { staleLockMs: STALE_LOCK_MS });
if (!lock.acquired) {
return null;
}
try {
const failure = await ensureCompiledRulesCurrent(projectRoot);
if (failure !== null) options.onCompileFailure?.(failure);
return await writeRuleContext(projectRoot, options);
} finally {
releaseLock(lockDir);
}
}
// src/feature-evidence/rules-loaded.ts
var RULES_LOADED_DOC_TYPE = "paqad.rules-loaded";
var RULES_LOADED_SCHEMA_VERSION = 2;
function atomicWriteJson2(absPath, value) {
mkdirSync8(dirname6(absPath), { recursive: true });
const tmp = `${absPath}.tmp`;
writeFileSync8(tmp, `${JSON.stringify(value, null, 2)}
`, "utf8");
renameWithRetrySync(tmp, absPath);
}
function buildRulesLoadedRecord(dirName, sessionId, input) {
return buildDocumentEnvelope({
docType: RULES_LOADED_DOC_TYPE,
change: featureChangeKey(dirName),
sessionId,
schemaVersion: RULES_LOADED_SCHEMA_VERSION,
now: input.now,
body: {
changed_files: [...input.changedPaths],
applicable_rules: input.applicable,
rule_text_hash: input.ruleTextHash,
...input.ruleSourceHashes ? { rule_source_hashes: { ...input.ruleSourceHashes } } : {},
artifact: PATHS.CONTEXT_SESSION_ARTIFACT
}
});
}
function writeRulesLoaded(projectRoot, sessionId, input) {
const dirName = currentFeature(projectRoot, sessionId);
if (!dirName) {
return null;
}
const record = buildRulesLoadedRecord(dirName, sessionId, input);
atomicWriteJson2(join16(projectRoot, featureFilePath(dirName, "rulesLoaded")), record);
return record;
}
function readRulesLoaded(projectRoot, dirName) {
try {
return JSON.parse(
readFileSync12(join16(projectRoot, featureFilePath(dirName, "rulesLoaded")), "utf8")
);
} catch {
return null;
}
}
function coverageGaps(record, currentApplicableIds) {
const recorded = new Set((record?.applicable_rules ?? []).map((rule) => rule.rule_id));
return currentApplicableIds.filter((id) => !recorded.has(id));
}
function currentSourceHash(projectRoot, sourcePath) {
try {
return hashRuleText(readFileSync12(join16(projectRoot, sourcePath), "utf8"));
} catch {
return null;
}
}
function hashRuleSources(projectRoot, sourcePaths) {
const hashes = {};
for (const sourcePath of sourcePaths) {
const hash = currentSourceHash(projectRoot, sourcePath);
if (hash !== null) {
hashes[sourcePath] = hash;
}
}
return hashes;
}
function changedLoadedRuleSources(projectRoot, record) {
const recorded = record.rule_source_hashes;
if (!recorded || typeof recorded !== "object") {
return [];
}
return Object.entries(recorded).filter(([sourcePath, hash]) => currentSourceHash(projectRoot, sourcePath) !== hash).map(([sourcePath]) => sourcePath).sort();
}
// src/stage-evidence/mode.ts
init_esm_shims();
var STAGES_MODES = ["off", "warn", "strict"];
var DEFAULT_STAGES_MODE = "strict";
function resolveStagesMode(projectRoot, env = process.env) {
return resolveFlooredMode(
{
team: readConfigsDir(projectRoot).merged.get("stages_mode"),
local: readDotConfig(projectRoot).get("stages_mode"),
env: envConfigValue(env, "stages_mode")
},
STAGES_MODES,
DEFAULT_STAGES_MODE
);
}
// src/stage-evidence/scope.ts
init_esm_shims();
function toRelativePosix(targetPath, projectRoot) {
const rel = projectRoot && isAbsolute(targetPath) ? relative(projectRoot, targetPath) : targetPath;
return rel.replace(/\\/g, "/").replace(/^\.\//, "");
}
function isFrameworkInternalPath(targetPath, projectRoot) {
const p = toRelativePosix(targetPath, projectRoot);
return p === "" || p === ".paqad" || p.startsWith(".paqad/") || p.startsWith("../");
}
function isDocumentationPath(targetPath, projectRoot) {
const p = toRelativePosix(targetPath, projectRoot);
if (p === "docs" || p.startsWith("docs/")) return true;
if (/\.(md|mdx|markdown|mdown|rst|adoc)$/i.test(p)) return true;
if (/^(readme|changelog|contributing|license|licence|notice|authors)(\.|$)/i.test(p)) return true;
return false;
}
var HOST_AGENT_CONFIG_DIRS = [
".claude",
".codex",
".gemini",
".junie",
".cursor",
".windsurf",
".continue",
".aider",
".aiassistant",
".ai"
];
var HOST_AGENT_CONFIG_FILES = [".windsurfrules"];
function isHostAgentConfigPath(targetPath, projectRoot) {
const p = toRelativePosix(targetPath, projectRoot);
if (HOST_AGENT_CONFIG_FILES.includes(p)) {
return true;
}
return HOST_AGENT_CONFIG_DIRS.some((dir) => p === dir || p.startsWith(`${dir}/`));
}
function isFeatureDevEdit(targetPath, projectRoot) {
if (!targetPath) return true;
if (isFrameworkInternalPath(targetPath, projectRoot)) return false;
if (isHostAgentConfigPath(targetPath, projectRoot)) return false;
if (isDocumentationPath(targetPath, projectRoot)) return false;
return true;
}
function changeIsFeatureDev(files, projectRoot) {
return files.some((file) => isFeatureDevEdit(file, projectRoot));
}
// src/kernel/compat.ts
init_esm_shims();
function evaluateCapabilityCompat(projectRoot, descriptor) {
const blessed = readCapabilityVersions(projectRoot, descriptor.id);
if (blessed === null) {
return "unversioned";
}
if (blessed.policy > descriptor.policySchemaVersion || blessed.record > descriptor.recordSchemaVersion) {
return "project-ahead";
}
if (blessed.policy < descriptor.policySchemaVersion || blessed.record < descriptor.recordSchemaVersion) {
return "project-behind";
}
return "current";
}
function isRefusedByCompat(compat) {
return compat === "project-ahead";
}
// src/kernel/capability.ts
var NO_OP4 = { ran: false, blocking: false, summary: "" };
var RULE_COMPLIANCE_MODES = ["off", "warn", "strict"];
var DEFAULT_RULE_COMPLIANCE = "warn";
function resolveRuleComplianceMode(projectRoot, env = process.env) {
return resolveFlooredMode(
{
team: readConfigsDir(projectRoot).merged.get("rule_compliance"),
local: readDotConfig(projectRoot).get("rule_compliance"),
env: envConfigValue(env, "rule_compliance")
},
RULE_COMPLIANCE_MODES,
DEFAULT_RULE_COMPLIANCE
);
}
function verifyRuleScriptsIntegrity(projectRoot) {
const digest = computeRuleScriptsDigest(projectRoot);
if (digest === null) {
return "ok";
}
const locked = readCapabilityDigest(projectRoot, "rule-scripts");
if (locked === null) {
return "unverified";
}
return locked === digest ? "ok" : "tampered";
}
function formatTamperSummary(mode) {
const blocking = mode === "strict";
const verb = blocking ? "Needs your attention" : "Heads up";
const glyph = blocking ? "\u{1F534}" : "\u{1F7E1}";
const tail = blocking ? "" : " (warn mode, not blocking)";
return `**\u25B8 paqad** \xB7 scripted-rule bindings changed outside the engine
> ${glyph} ${verb} \u2014 your rule-script bindings were edited outside paqad, so I can't trust this check; a binding may have been weakened. Re-bless them with \`analyze rules\` / \`generate rule scripts\`, then commit the updated lock.${tail}`;
}
function formatNoneArmedSummary() {
return `**\u25B8 paqad** \xB7 scripted rules: \u26AA none armed
> No rule-scripts are registered for this repo, so I ran no scripted enforcement. Your checks passed on the toolchain (format/tests/build), not on the team's rule-scripts \u2014 arm them with \`generate rule scripts\` to enforce the rules deterministically.`;
}
function formatUnverifiedSummary() {
return `**\u25B8 paqad** \xB7 scripted-rule bindings not yet attested
> \u{1F7E1} Heads up \u2014 your rule-script bindings have no integrity lock yet, so I can't attest they are unchanged. Re-run \`generate rule scripts\` to bless them. (advisory, not blocking)`;
}
function formatCompatRefusalSummary() {
return `**\u25B8 paqad** \xB7 scripted-rule enforcement paused \u2014 framework update pending
> \u{1F7E1} Heads up \u2014 this project's rule-script bindings were blessed by a newer paqad than the one installed here, so I'm holding off enforcing them rather than risk misreading a newer format. The framework self-heals on update, then I'll resume. (advisory, not blocking)`;
}
var RULE_GATE_BUDGET_CONTEXT = "paqad: the blocking rule checks did not finish in time for this edit. They will run again at the end of this turn and block there if anything fails.";
var RULE_GATE_REMOVAL_CONTEXT = "paqad: this edit removes a rule finding from its file and adds none, so it passed and was recorded. Other findings in this change still block at the end of the turn.";
var PRE_EDIT_FILTER = {
kinds: ["deterministic"],
scopes: ["changed-files"]
};
function formatPendingUnwritableSummary() {
return `**\u25B8 paqad** \xB7 blocking rule checks could not be deferred
> \u{1F534} Needs your attention \u2014 the blocking rule checks did not finish in time for this edit, and I could not record that they still have to run at the end of this turn, so I stopped the edit. Check that \`.paqad/scripts/rules/.cache/\` is writable, then re-try the edit.`;
}
function payloadSessionId(projectRoot, env, payload) {
return resolveSessionId(projectRoot, payload?.sessionId ?? env.CLAUDE_SESSION_ID ?? null);
}
function payloadTargets(payload) {
return payload?.targetPaths ?? (payload?.targetPath ? [payload.targetPath] : []);
}
async function withinGateBudget(context, gate, work) {
const now = context.now ?? Date.now;
const remaining = (context.startedAt ?? now()) + gate.budgetMs - now();
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), Math.max(0, remaining));
if (remaining <= 0) controller.abort();
try {
return await work(controller.signal);
} finally {
clearTimeout(timer);
}
}
function enforceBeforeEdit(context, mode, gate) {
return withinGateBudget(
context,
gate,
(signal) => enforceRuleScripts({
projectRoot: context.projectRoot,
mode,
scope: "whole-tree",
...gate.preEdit === "blocking-only" ? { ...PRE_EDIT_FILTER, useFileCache: true, editTargets: payloadTargets(context.payload) } : {},
concurrency: gate.concurrency,
signal
})
);
}
function recordAllowedEdit(context, verdict) {
const { projectRoot, env, payload } = context;
const sessionId = payloadSessionId(projectRoot, env, payload);
const removed = verdict.removed.length;
const row = appendRuleRun(projectRoot, sessionId, {
kind: "edit-allowed",
counts: { removed, remaining: verdict.remaining },
blocking: false,
note: `edit to ${verdict.target} removes ${removed} rule finding(s) and adds none`,
findings: verdict.removed.map((violation) => ({
rule_id: violation.rule_id,
file: violation.file,
line: violation.line ?? null,
message: violation.message,
severity: violation.severity
}))
});
if (row === null && currentFeature(projectRoot, sessionId)) return false;
appendHooksLog(
projectRoot,
`capability-gate rule-scripts edit-allowed session=${sessionId} file=${verdict.target} removed=${removed} remaining=${verdict.remaining}`
);
return true;
}
function refusalForFailedComparison(error) {
const code = error?.code;
if (code === "ENOENT" || code === "EEXIST") return { allowed: false, reason: null };
return { allowed: false, reason: `the comparison failed (${code ?? String(error)})` };
}
async function editOnlyRemovesFindings(context, gate, result) {
if (gate.preEdit !== "blocking-only") return false;
const { projectRoot, payload } = context;
const verdict = await withinGateBudget(
context,
gate,
(signal) => judgeEditResult({
projectRoot,
targets: payloadTargets(payload),
toolName: payload?.toolName,
toolInput: payload?.toolInput,
violations: result.violations,
concurrency: gate.concurrency,
signal
})
).catch(refusalForFailedComparison);
if (verdict.allowed) return recordAllowedEdit(context, verdict);
if (verdict.reason !== null) {
appendHooksLog(projectRoot, `capability-gate rule-scripts edit-refused ${verdict.reason}`);
}
return false;
}
function deferToTurnEnd(context) {
const { projectRoot, env, payload } = context;
const sessionId = payloadSessionId(projectRoot, env, payload);
const files = payload?.targetPaths?.length ?? (payload?.targetPath ? 1 : 0);
try {
writePendingPreMutation(projectRoot, {
session_id: sessionId,
change: currentFeature(projectRoot, sessionId) ?? null,
written_at: new Date((context.now ?? Date.now)()).toISOString(),
files
});
} catch (error) {
const code = error?.code ?? "unknown";
appendHooksLog(
projectRoot,
`capability-gate rule-scripts pending-unwritable ${code} session=${sessionId} files=${files}`
);
return { ran: true, blocking: true, summary: formatPendingUnwritableSummary() };
}
appendHooksLog(
projectRoot,
`capability-gate rule-scripts budget-exhausted session=${sessionId} files=${files}`
);
return { ran: true, blocking: false, summary: "", context: RULE_GATE_BUDGET_CONTEXT };
}
function payloadFeatureDev(payload, projectRoot) {
if (payload?.targetPaths && payload.targetPaths.length > 0) {
return changeIsFeatureDev(payload.targetPaths, projectRoot);
}
return isFeatureDevEdit(payload?.targetPath, projectRoot);
}
var ruleScriptsCapability = {
id: "rule-scripts",
async evaluate(context) {
const { projectRoot, seam, env, payload } = context;
if (seam === "pre-mutation" && !payloadFeatureDev(payload, projectRoot)) {
return NO_OP4;
}
const pendingSession = seam === "completion" ? payloadSessionId(projectRoot, env, payload) : null;
const pending = pendingSession !== null && hasPendingPreMutation(projectRoot, pendingSession);
if (seam === "completion" && !pending && !classifyCompletionEnforcement(
projectRoot,
payload?.sessionId ?? env.CLAUDE_SESSION_ID ?? null
).enforce) {
return NO_OP4;
}
const mode = resolveRuleComplianceMode(projectRoot, env);
if (mode === "off") {
return NO_OP4;
}
if (isRefusedByCompat(evaluateCapabilityCompat(projectRoot, getCapability("rule-scripts")))) {
return { ran: true, blocking: false, summary: formatCompatRefusalSummary() };
}
const integrity = verifyRuleScriptsIntegrity(projectRoot);
if (integrity === "tampered") {
return { ran: true, blocking: mode === "strict", summary: formatTamperSummary(mode) };
}
const result = seam === "pre-mutation" ? await enforceBeforeEdit(context, mode, resolveRuleGateConfig(projectRoot, env)) : await enforceRuleScripts({ projectRoot, mode, scope: "changed-files" });
if (result.aborted) {
return deferToTurnEnd(context);
}
if (pending && pendingSession !== null) {
clearPendingPreMutation(projectRoot, pendingSession);
}
if (!result.ran) {
if (seam === "completion" && result.armed === 0) {
return { ran: true, blocking: false, summary: formatNoneArmedSummary() };
}
return NO_OP4;
}
if (result.violations.length === 0) {
return integrity === "unverified" ? { ran: true, blocking: false, summary: formatUnverifiedSummary() } : NO_OP4;
}
if (seam === "pre-mutation" && result.blocking && await editOnlyRemovesFindings(context, resolveRuleGateConfig(projectRoot, env), result)) {
return { ran: true, blocking: false, summary: "", context: RULE_GATE_REMOVAL_CONTEXT };
}
return {
ran: true,
blocking: result.blocking,
summary: result.summary,
violations: result.violations
};
}
};
function formatMissingStageSummary(stage, mode) {
const blocking = mode === "strict";
const verb = blocking ? "Needs your attention" : "Heads up";
const glyph = blocking ? "\u{1F534}" : "\u{1F7E1}";
const tail = blocking ? "" : " (warn mode, not blocking)";
const endMarker = isArtifactBearingStage(stage) ? `\`paqad:stage ${stage} end -- <artifact-path>\` (a real, non-empty file \u2014 e.g. a plan/spec/findings file)` : `\`paqad:stage ${stage} end\``;
const endCli = isArtifactBearingStage(stage) ? `\`paqad-ai stage end ${stage} --artifact <artifact-path>\`` : `\`paqad-ai stage end ${stage}\``;
return `**\u25B8 paqad** \xB7 run ${stage} before you change code
> ${glyph} ${verb} \u2014 the feature-development workflow needs the **${stage}** stage recorded before this edit. Mark it: emit \`paqad:stage ${stage} start\` and ${endMarker} each on its own line (parsed before the next edit, so they clear this block in the same turn), or run \`paqad-ai stage start ${stage}\` then ${endCli}. Set stage_tracking: warn or off in .paqad/configs/feature-development/workflow.yaml to adopt the workflow before enforcing.${tail}`;
}
function sweepSameTurnMarkers(projectRoot, transcriptPath, sessionId, adapter) {
if (!transcriptPath) return "";
let transcriptText;
try {
transcriptText = readFileSync13(transcriptPath, "utf8");
} catch {
return "";
}
const recorded = parseAndRecordMarkers({ projectRoot, transcriptText, sessionId, adapter });
return markerBatchNarration(recorded);
}
function bundleFileWriter(filename) {
if (filename === SCREENSHOTS_DIR || filename.startsWith(`${SCREENSHOTS_DIR}/`)) {
return "paqad-ai visual-evidence run (or visual-evidence attach)";
}
return bundleWriterFor(filename);
}
function formatBundleWriteSummary(relPath, writer) {
const remedy = writer ? `This file is written by \`${writer}\`. Run that verb and hand it your content instead.` : `No verb writes this file, so it does not belong in the bundle. Keep your notes outside \`.paqad/ledger/feature-evidence/\`.`;
return `**\u25B8 paqad** \xB7 bundle files are written by paqad, not by hand
> \u{1F534} Needs your attention \u2014 \`${relPath}\` sits inside a feature evidence bundle, and every file there is created by a paqad verb so the record can be trusted. ${remedy}`;
}
function normalizedPayloadTargets(payload, projectRoot) {
const targets = payload?.targetPaths && payload.targetPaths.length > 0 ? payload.targetPaths : payload?.targetPath ? [payload.targetPath] : [];
const rels = [];
for (const target of targets) {
try {
rels.push(normalizeArtifactPath(projectRoot, target));
} catch {
continue;
}
}
return rels;
}
function firstBundleTarget(targets) {
for (const rel of targets) {
const classified = classifyBundlePath(rel);
if (classified) return { rel, filename: classified.filename };
}
return null;
}
function firstChecksRecordTarget(targets) {
return targets.find((rel) => rel === PATHS.CHECKS_DIR || rel.startsWith(`${PATHS.CHECKS_DIR}/`)) ?? null;
}
function formatChecksRecordWriteSummary(relPath) {
return `**\u25B8 paqad** \xB7 checks records are written by paqad, not by hand
> \u{1F534} Needs your attention \u2014 \`${relPath}\` is part of the checks record (the run history, the run snapshots and the last report). Only \`paqad-ai checks run\` writes it, so a green result there can be trusted. Run \`paqad-ai checks run\` instead.`;
}
function firstGitWorkflowTarget(targets) {
const dir = PATHS.GIT_WORKFLOW_DIR;
return targets.find((rel) => rel === dir || rel.startsWith(`${dir}/`)) ?? null;
}
function formatGitWorkflowWriteSummary(relPath) {
return `**\u25B8 paqad** \xB7 the git workflow rules are changed by the git-workflow skill
> \u{1F534} Needs your attention: \`${relPath}\` holds this team's git workflow rules. Only the git-workflow skill changes them, so every change is checked, explained and recorded. Use the git-workflow skill instead.`;
}
var stagesCapability = {
id: "stages",
async evaluate({ projectRoot, seam, env, payload }) {
if (seam !== "pre-mutation") return NO_OP4;
const targets = normalizedPayloadTargets(payload, projectRoot);
const workflowTarget = firstGitWorkflowTarget(targets);
if (workflowTarget) {
return { ran: true, blocking: true, summary: formatGitWorkflowWriteSummary(workflowTarget) };
}
const checksTarget = firstChecksRecordTarget(targets);
if (checksTarget) {
return { ran: true, blocking: true, summary: formatChecksRecordWriteSummary(checksTarget) };
}
const bundleTarget = firstBundleTarget(targets);
if (bundleTarget) {
return {
ran: true,
blocking: true,
summary: formatBundleWriteSummary(
bundleTarget.rel,
bundleFileWriter(bundleTarget.filename)
)
};
}
const writeDecision = stageAgentWriteDecision({
projectRoot,
agentType: payload?.agentType,
toolName: payload?.toolName,
targets
});
if (writeDecision.blocked) {
return { ran: true, blocking: true, summary: formatStageAgentWriteSummary(writeDecision) };
}
const unresolvedAgent = unresolvedStageAgent(payload?.agentType);
if (unresolvedAgent !== null) {
return { ran: true, blocking: true, summary: formatUnresolvedStageSummary(unresolvedAgent) };
}
const mode = resolveStagesMode(projectRoot, env);
if (mode === "off") return NO_OP4;
if (!payloadFeatureDev(payload, projectRoot)) return NO_OP4;
const blocking = mode === "strict";
const sessionId = resolveSessionId(
projectRoot,
payload?.sessionId ?? env.CLAUDE_SESSION_ID ?? null
);
const narration = sweepSameTurnMarkers(
projectRoot,
payload?.transcriptPath,
sessionId,
payload?.adapter
);
const dirName = currentFeature(projectRoot, sessionId);
const prefix = PRE_CODE_STAGES;
if (!dirName) {
const firstRequired = prefix[0] ?? "planning";
return {
ran: true,
blocking,
summary: formatMissingStageSummary(firstRequired, mode),
narration
};
}
const fold = foldFeature(projectRoot, sessionId, dirName);
const byStage = new Map(fold.stages.map((stage) => [stage.stage, stage]));
const effectiveLane = resolveEffectiveLane(projectRoot, payload?.targetPath, fold.lane);
const requiredStages = requiredPreCodeStages(effectiveLane);
for (const stage of requiredStages) {
const folded = byStage.get(stage);
const hasPair = Boolean(folded?.started_at && folded?.ended_at);
const hasArtifact = !isArtifactBearingStage(stage) || Boolean(folded?.artifact_digest);
if (!hasPair || !hasArtifact) {
return { ran: true, blocking, summary: formatMissingStageSummary(stage, mode), narration };
}
}
const signOff = designSignOffBlock(projectRoot, dirName, effectiveLane);
if (signOff !== null) return { ran: true, blocking, summary: signOff, narration };
return { ran: true, blocking: false, summary: "", narration };
}
};
function formatRulesNotLoadedSummary() {
return `**\u25B8 paqad** \xB7 rules not loaded for this change
> \u{1F534} Needs your attention \u2014 you are editing code but I have no record that the applicable project rules were loaded. Read them and record it: run \`paqad-ai rules load\` (it prints the rules that apply to your changed files and writes the evidence), then re-try the edit.`;
}
function formatRulesChangedSummary(changed) {
return `**\u25B8 paqad** \xB7 rules changed since you loaded them
> \u{1F534} Needs your attention \u2014 the text of ${changed.length} rule${changed.length === 1 ? "" : "s"} you loaded for this change has changed since: ${changed.join(", ")}. Read the new text: run \`paqad-ai rules load\`, then re-try the edit.`;
}
var rulesLoadedCapability = {
id: "rules-loaded",
async evaluate({ projectRoot, seam, env, payload }) {
if (seam !== "pre-mutation") return NO_OP4;
if (!payloadFeatureDev(payload, projectRoot)) return NO_OP4;
const store = await readCompiledRules(projectRoot);
if (!store || (store.rules?.length ?? 0) === 0) return NO_OP4;
const sessionId = resolveSessionId(
projectRoot,
payload?.sessionId ?? env.CLAUDE_SESSION_ID ?? null
);
const dirName = currentFeature(projectRoot, sessionId);
if (!dirName) return NO_OP4;
const record = readRulesLoaded(projectRoot, dirName);
if (!record) {
return { ran: true, blocking: true, summary: formatRulesNotLoadedSummary() };
}
const changed = changedLoadedRuleSources(projectRoot, record);
if (changed.length === 0) return NO_OP4;
return { ran: true, blocking: true, summary: formatRulesChangedSummary(changed) };
}
};
var deliveryCapability = {
id: "delivery",
evaluate({ projectRoot, seam }) {
return runDeliveryCapability(projectRoot, seam, execaCommandRunner);
}
};
var decisionPauseSelfArmCapability = {
id: "decision-pause",
async evaluate({ projectRoot, seam, env, payload }) {
const selfArm = runDecisionSelfArm({ projectRoot, seam, env, payload });
if (selfArm.ran) return selfArm;
return runSpecChangeGuard({ projectRoot, seam, sessionId: payload?.sessionId ?? null });
}
};
var CAPABILITY_IMPLS = /* @__PURE__ */ new Map([
[ruleScriptsCapability.id, ruleScriptsCapability],
[stagesCapability.id, stagesCapability],
[rulesLoadedCapability.id, rulesLoadedCapability],
[deliveryCapability.id, deliveryCapability],
[decisionPauseSelfArmCapability.id, decisionPauseSelfArmCapability]
]);
// src/hud/last-block.ts
init_esm_shims();
import { randomBytes } from "crypto";
import { readFileSync as readFileSync14, rmSync as rmSync5 } from "fs";
var LAST_BLOCK_DIFF_MAX_BYTES = 2048;
function asRecord(value) {
return typeof value === "object" && value !== null ? value : null;
}
function prefixed(text, mark) {
return typeof text === "string" && text !== "" ? text.split(/\r?\n/).map((line) => `${mark} ${line}`) : [];
}
function editLines(edit) {
if (!edit) return [];
return [...prefixed(edit.old_string, "-"), ...prefixed(edit.new_string, "+")];
}
function truncateUtf8(text, maxBytes) {
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
let out = "";
let used = 0;
for (const char of text) {
const size = Buffer.byteLength(char, "utf8");
if (used + size > maxBytes) break;
out += char;
used += size;
}
return out;
}
function shortDiff(toolInput, maxBytes = LAST_BLOCK_DIFF_MAX_BYTES) {
const input = asRecord(toolInput);
if (!input) return "";
const edits = Array.isArray(input.edits) ? input.edits.map(asRecord) : [input];
const lines = [
...edits.flatMap(editLines),
...prefixed(input.content, "+"),
...prefixed(input.new_source, "+")
];
return truncateUtf8(lines.join("\n"), maxBytes);
}
function writeRuleStopRecord(input) {
const record = {
rule: input.rule,
title: input.title ?? null,
file: input.file,
tool_use_id: input.toolUseId ?? null,
managed: input.managed,
sole: input.sole === true,
at: (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
diff: shortDiff(input.toolInput),
token: input.token ?? randomBytes(16).toString("hex"),
used: false,
allowed_once: false,
session: input.sessionId
};
writeFileAtomic(
hudLastBlockPath(input.projectRoot, input.sessionId),
`${JSON.stringify(record, null, 2)}
`
);
return record;
}
function str(value) {
return typeof value === "string" && value !== "" ? value : null;
}
function parseRuleStopRecord(value) {
const raw = asRecord(value);
const rule = str(raw?.rule);
const file = str(raw?.file);
const at = str(raw?.at);
const token = str(raw?.token);
const session = str(raw?.session);
if (!raw || !rule || !file || !at || !token || !session) return null;
return {
rule,
title: str(raw.title),
file,
tool_use_id: str(raw.tool_use_id),
managed: raw.managed === true,
sole: raw.sole === true,
at,
diff: typeof raw.diff === "string" ? raw.diff : "",
token,
used: raw.used === true,
allowed_once: raw.allowed_once === true,
session
};
}
function readRuleStopRecord(projectRoot, sessionId) {
let parsed;
try {
parsed = JSON.parse(readFileSync14(hudLastBlockPath(projectRoot, sessionId), "utf8"));
} catch {
return null;
}
const record = parseRuleStopRecord(parsed);
return record && record.session === sessionId ? record : null;
}
function writeRecord(projectRoot, record) {
writeFileAtomic(
hudLastBlockPath(projectRoot, record.session),
`${JSON.stringify(record, null, 2)}
`
);
}
function markRuleStopAllowedOnce(projectRoot, record) {
writeRecord(projectRoot, { ...record, used: true, allowed_once: true });
}
function takeAllowedOnce(projectRoot, sessionId, call) {
const record = readRuleStopRecord(projectRoot, sessionId);
if (!record?.allowed_once || !call.toolUseId || record.tool_use_id !== call.toolUseId) {
return false;
}
if (record.rule !== call.rule) return false;
const recordFile = toProjectRelativePath(projectRoot, record.file);
const callFile = toProjectRelativePath(projectRoot, call.file);
if (recordFile === null || callFile === null || recordFile !== callFile) return false;
writeRecord(projectRoot, { ...record, allowed_once: false });
return true;
}
function clearRuleStopRecord(projectRoot, sessionId) {
rmSync5(hudLastBlockPath(projectRoot, sessionId), { force: true });
}
export {
COMPLIANCE_SCHEMA_VERSION,
SPEC_REVIEW_SCHEMA_VERSION,
DEFAULT_OBLIGATION_INDEX_PATH,
slugifySpec,
specIndexPath,
specReportPath,
specReviewPath,
reviewSpecification,
attachSpecDefectsToObligations,
compareDefects,
extractObligationIndex,
planExpertSlices,
MAX_EXPERT_QUESTIONS,
decideFinish,
buildProvenance,
SPEC_PIPELINE_STAGING_DIR,
readStagedText,
writeStagedText,
readStagedJson,
clearStaged,
writeRequest,
readRequest,
writeLabel,
readLabel,
writeQuestions,
readQuestions,
clearClarificationSection,
readExperts,
writeExpertRoster,
readExpertNeed,
writeExpertNotes,
readStagedExpertQuestions,
readExpertNotes,
writeExpertSynthesis,
writeExpertFindings,
readExpertSynthesis,
clearExpertSynthesis,
readGrounding,
readFreezeSections,
clearFrozenRun,
rememberInput,
appendSpecStepRow,
appendSpecCorrectionRow,
validateSiteMapAnswers,
SiteMapSchemaError,
canonicalAppMapPath,
writeYamlAtomic,
readYaml,
writeCanonicalSiteMap,
readCanonicalSiteMap,
writeJourney,
removeJourney,
readJourney,
readAllJourneys,
loadCaptureScripts,
deriveCaption,
resolveVeRuntimeDir,
resolvePlaywrightModulePath,
browserStatus,
provisionBrowser,
veBrowsersPath,
PackRegistryEmptyError,
packRegistryFaultDetail,
PACK_REGISTRY_FAULT_REMEDIATION,
frontendTriggerOrFault,
evaluateFrontendTrigger,
visualEvidenceReadiness,
visualEvidenceFlagOn,
visualAcRequiredFiles,
findVisualEvidenceWaiver,
visualEvidenceWaiverHint,
isManagedEnv,
isFrameworkEnabled,
isFrameworkEnabledForRoot,
isFrameworkDisabledForRoot,
setFrameworkEnabled,
buildFeatureSpec,
sessionWroteInEpisode,
editedDevelopmentThisTurn,
changePastSpecification,
classifyCompletionEnforcement,
DELIVERY_EVIDENCE_DOC_TYPE,
renderDelivery,
GIT_WORKFLOW_PATH,
requireTicket,
matchTicketSource,
pickBranchType,
resolveBase,
isConfiguredBase,
planBranch,
currentBranch,
styleVerdict,
commitStep,
placeholdersIn2 as placeholdersIn,
missingHeadings,
linkTicket,
uniqueInOrder,
MISSING_LABEL_HINT,
branchFinding,
commitFindings,
ticketInBranch,
refreshDelivery,
execaCommandRunner,
validateCodeKnowledgeIndex,
writeCodeKnowledgeIndex,
readCodeKnowledgeIndex,
applyResolvedDecisions,
armDecisionFromDuplicationFinding,
assembleDecisionEvidence,
countFileReferences,
defaultSimilarityFor,
buildDecisionPacket,
computePacketConfidence,
decisionQuestionForCategory,
decisionOptionsForCategory,
selectViableDecisionOptions,
authoringConfigProblems,
readAuthoringConfig,
skillOrchestratorEnabled,
agentOrchestratorEnabled,
planFiles,
sensitivePaths,
editedFiles,
laneMayRise,
raiseLaneIfGrown,
authoringRaiseEnabled,
raiseLaneForAuthoring,
projectRelative,
dynamicRecord,
queryFrameworkApi,
validateFrameworkApiIndex,
writeFrameworkApiIndex,
readFrameworkApiIndex,
reuseCounts,
ReuseDeclarationError,
NoActiveFeatureError,
writeFeaturePlan,
readFeaturePlan,
writeFeatureSpecification,
readFeatureSpecification,
writeFeatureReview,
readFeatureReview,
SKILL_RECORD_WRITER,
skillRecordFilename,
readSkillRecord,
writeSkillRecord,
AGENT_RECORD_WRITER,
agentRecordFilename,
readAgentRecord,
writeAgentRecord,
readAllFeatureStageRows,
readAllFeatureRuleRuns,
readAllFeatureChangeMetrics,
readAllFeatureRag,
readFeatureChangeMetricsWindow,
readAllFeatureEvidence,
evaluateSpecFreeze,
freezeSpec,
carryForwardIds,
validateCraftTrace,
writeTrace,
readTrace,
parseTraceArtifact,
mergeExpertNotes,
strongestSeverity,
mergedKind,
pathTriggeredSeats,
seatExpertsForLane,
validateExpertNeed,
queryCodeKnowledge,
validateExpertNotes,
validateQuestion,
assembleExpertRun,
buildRunMetrics,
aggregateSpecPipelineMetrics,
listRunDirs,
buildSpecChangePacket,
designSignOffApplies,
loadDesignSignOff,
changeLane,
freezeNudge,
resolveRuleApplicabilityForChange,
writeRuleContext,
ensureCompiledRulesCurrent,
compileFailureReason,
refreshRuleContext,
writeRulesLoaded,
readRulesLoaded,
coverageGaps,
hashRuleSources,
resolveStagesMode,
changeIsFeatureDev,
resolveRuleComplianceMode,
CAPABILITY_IMPLS,
writeRuleStopRecord,
readRuleStopRecord,
markRuleStopAllowedOnce,
takeAllowedOnce,
clearRuleStopRecord
};