@mrciphersmith/keryx 0.3.28 → 0.3.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -623,9 +623,12 @@ Grouped by what you are trying to do, not by internal module layout.
|
|
|
623
623
|
rounds-with-cost/rounds-total count for partial coverage), project-wide
|
|
624
624
|
trigger spend (never attributed to a flow — the run record carries no flow
|
|
625
625
|
reference), who confirmed each acceptance criterion and who signed
|
|
626
|
-
completion (with identity basis),
|
|
627
|
-
outcomes
|
|
628
|
-
|
|
626
|
+
completion (with identity basis), every `flow complete` attempt's gate
|
|
627
|
+
outcomes, and each flow's acceptance coverage — how many criteria carry a
|
|
628
|
+
runnable `[verify: exec ...]` or `[verify: invariant ...]` marker (a flow
|
|
629
|
+
with no recorded kinds counts as fully unclassified; see `keryx flow ac kinds`
|
|
630
|
+
in the [CLI reference](docs/docs/cli-reference.md#verification-kinds)). A
|
|
631
|
+
figure nobody recorded is reported as "not recorded", never as zero. Writes `.metaproject/data/governance/artifacts/latest.{md,json}`, the
|
|
629
632
|
same convention `keryx health run` uses; `--all-projects` also covers every
|
|
630
633
|
project in the user-global registry. In the TUI, the sidebar's
|
|
631
634
|
**Governance** row shows `no report — click to run`,
|
package/dist/cli.js
CHANGED
|
@@ -13447,7 +13447,7 @@ var package_default;
|
|
|
13447
13447
|
var init_package = __esm(() => {
|
|
13448
13448
|
package_default = {
|
|
13449
13449
|
name: "@mrciphersmith/keryx",
|
|
13450
|
-
version: "0.3.
|
|
13450
|
+
version: "0.3.29",
|
|
13451
13451
|
description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
13452
13452
|
private: false,
|
|
13453
13453
|
publishConfig: {
|
|
@@ -49602,6 +49602,162 @@ var init_confirm_token = __esm(() => {
|
|
|
49602
49602
|
CONFIRMATION_CAVEAT = "The token proves an interactive confirmation step ran outside the agent's tool roster, within its TTL, " + "for this criteria checksum \u2014 not that a human ran it, and not who.";
|
|
49603
49603
|
});
|
|
49604
49604
|
|
|
49605
|
+
// src/flow/ac-kinds.ts
|
|
49606
|
+
function maskCodeSpans(text) {
|
|
49607
|
+
return text.replace(/`[^`]*`/g, (span) => " ".repeat(span.length));
|
|
49608
|
+
}
|
|
49609
|
+
function scanMarkers(text) {
|
|
49610
|
+
const masked = maskCodeSpans(text);
|
|
49611
|
+
let occurrences = 0;
|
|
49612
|
+
let lastStart = -1;
|
|
49613
|
+
for (let at = masked.indexOf(MARKER_OPEN);at !== -1; at = masked.indexOf(MARKER_OPEN, at + MARKER_OPEN.length)) {
|
|
49614
|
+
occurrences += 1;
|
|
49615
|
+
lastStart = at;
|
|
49616
|
+
}
|
|
49617
|
+
const end = text.trimEnd().length;
|
|
49618
|
+
return { occurrences, lastStart, trailing: occurrences > 0 && end > lastStart && text[end - 1] === "]", end };
|
|
49619
|
+
}
|
|
49620
|
+
function stripVerifyMarker(text) {
|
|
49621
|
+
const scan = scanMarkers(text);
|
|
49622
|
+
if (!scan.trailing || scan.occurrences !== 1)
|
|
49623
|
+
return text;
|
|
49624
|
+
return text.slice(0, scan.lastStart).trimEnd();
|
|
49625
|
+
}
|
|
49626
|
+
function unclassified(error) {
|
|
49627
|
+
return error === undefined ? { record: { kind: "unclassified" } } : { record: { kind: "unclassified" }, error };
|
|
49628
|
+
}
|
|
49629
|
+
function parseAcKindText(text) {
|
|
49630
|
+
const scan = scanMarkers(text);
|
|
49631
|
+
if (scan.occurrences === 0)
|
|
49632
|
+
return unclassified();
|
|
49633
|
+
if (scan.occurrences > 1) {
|
|
49634
|
+
return unclassified("a second [verify: \u2026] marker \u2014 a criterion carries exactly one, as the last thing on its line");
|
|
49635
|
+
}
|
|
49636
|
+
if (!scan.trailing) {
|
|
49637
|
+
return unclassified("the [verify: \u2026] marker must be the last thing on the line and close with `]`");
|
|
49638
|
+
}
|
|
49639
|
+
const payload = text.slice(scan.lastStart + MARKER_OPEN.length, scan.end - 1).trim();
|
|
49640
|
+
if (payload.length === 0)
|
|
49641
|
+
return unclassified(`an empty [verify: \u2026] marker \u2014 expected ${KIND_WORDS}`);
|
|
49642
|
+
const word = /^[A-Za-z]+/.exec(payload)?.[0] ?? "";
|
|
49643
|
+
const rest = payload.slice(word.length).trim();
|
|
49644
|
+
switch (word) {
|
|
49645
|
+
case "judged":
|
|
49646
|
+
return rest.length === 0 ? { record: { kind: "judged" } } : unclassified("`judged` takes no arguments \u2014 write [verify: judged]");
|
|
49647
|
+
case "exec":
|
|
49648
|
+
case "invariant": {
|
|
49649
|
+
if (rest.length === 0)
|
|
49650
|
+
return unclassified(`\`${word}\` requires a backticked command, e.g. [verify: ${word} \`bun test path/to.test.ts\`]`);
|
|
49651
|
+
const command = /^`([^`]*)`$/.exec(rest);
|
|
49652
|
+
if (command === null)
|
|
49653
|
+
return unclassified(`\`${word}\` requires exactly one backticked command after the kind, e.g. [verify: ${word} \`bun test path/to.test.ts\`]`);
|
|
49654
|
+
const check = (command[1] ?? "").trim();
|
|
49655
|
+
if (check.length === 0)
|
|
49656
|
+
return unclassified(`\`${word}\` has an empty backticked command`);
|
|
49657
|
+
return { record: { kind: word, check } };
|
|
49658
|
+
}
|
|
49659
|
+
case "none": {
|
|
49660
|
+
if (rest.length === 0)
|
|
49661
|
+
return unclassified("`none` requires a reason after an em dash, e.g. [verify: none \u2014 <reason>]");
|
|
49662
|
+
const reason = /^\u2014\s*([\s\S]*)$/.exec(rest);
|
|
49663
|
+
if (reason === null)
|
|
49664
|
+
return unclassified("`none` requires its reason after an em dash (\u2014), e.g. [verify: none \u2014 <reason>]");
|
|
49665
|
+
const why = (reason[1] ?? "").trim();
|
|
49666
|
+
if (why.length === 0)
|
|
49667
|
+
return unclassified("`none` has an empty reason after the em dash");
|
|
49668
|
+
return { record: { kind: "none", reason: why } };
|
|
49669
|
+
}
|
|
49670
|
+
default:
|
|
49671
|
+
return unclassified(`unknown verification kind "${word.length > 0 ? word : payload}" \u2014 expected ${KIND_WORDS}`);
|
|
49672
|
+
}
|
|
49673
|
+
}
|
|
49674
|
+
function parseAcKinds(content) {
|
|
49675
|
+
const criteria = [];
|
|
49676
|
+
const errors = [];
|
|
49677
|
+
for (const line of content.split(/\r?\n/)) {
|
|
49678
|
+
const match = AC_LINE.exec(line);
|
|
49679
|
+
if (!match?.[1])
|
|
49680
|
+
continue;
|
|
49681
|
+
const id = match[1].toUpperCase();
|
|
49682
|
+
const rawText = (match[2] ?? "").trim();
|
|
49683
|
+
const parsed = parseAcKindText(rawText);
|
|
49684
|
+
if (parsed.error !== undefined)
|
|
49685
|
+
errors.push({ id, message: parsed.error });
|
|
49686
|
+
criteria.push({ id, text: stripVerifyMarker(rawText), rawText, record: parsed.record });
|
|
49687
|
+
}
|
|
49688
|
+
return { criteria, errors };
|
|
49689
|
+
}
|
|
49690
|
+
function emptyCounts() {
|
|
49691
|
+
return { exec: 0, invariant: 0, judged: 0, none: 0, unclassified: 0 };
|
|
49692
|
+
}
|
|
49693
|
+
function buildAcKindReport(flowId, content) {
|
|
49694
|
+
const parsed = parseAcKinds(content);
|
|
49695
|
+
const criteria = {};
|
|
49696
|
+
for (const criterion of parsed.criteria) {
|
|
49697
|
+
if (!(criterion.id in criteria))
|
|
49698
|
+
criteria[criterion.id] = criterion.record;
|
|
49699
|
+
}
|
|
49700
|
+
return { report: reportFromRecords(flowId, criteria), errors: parsed.errors };
|
|
49701
|
+
}
|
|
49702
|
+
function reportFromRecords(flowId, criteria) {
|
|
49703
|
+
const counts = emptyCounts();
|
|
49704
|
+
for (const record of Object.values(criteria))
|
|
49705
|
+
counts[record.kind] += 1;
|
|
49706
|
+
return {
|
|
49707
|
+
flowId,
|
|
49708
|
+
criteria,
|
|
49709
|
+
counts,
|
|
49710
|
+
runnable: counts.exec + counts.invariant,
|
|
49711
|
+
total: Object.keys(criteria).length
|
|
49712
|
+
};
|
|
49713
|
+
}
|
|
49714
|
+
function renderAcKindDistribution(report) {
|
|
49715
|
+
const { counts } = report;
|
|
49716
|
+
const percent = report.total === 0 ? 0 : Math.round(report.runnable / report.total * 100);
|
|
49717
|
+
const tail = [`${counts.none} accepted as unverifiable`, `${counts.judged} judged`, ...counts.unclassified > 0 ? [`${counts.unclassified} unclassified`] : []];
|
|
49718
|
+
return [
|
|
49719
|
+
`acceptance kinds: exec ${counts.exec} invariant ${counts.invariant} judged ${counts.judged} none ${counts.none} unclassified ${counts.unclassified} (${report.total} criteria)`,
|
|
49720
|
+
`coverage: ${report.runnable}/${report.total} runnable (${percent}%) \xB7 ${tail.join(" \xB7 ")}`
|
|
49721
|
+
];
|
|
49722
|
+
}
|
|
49723
|
+
function describeAcKind(record) {
|
|
49724
|
+
switch (record.kind) {
|
|
49725
|
+
case "exec":
|
|
49726
|
+
case "invariant":
|
|
49727
|
+
return `${record.kind} \`${record.check}\``;
|
|
49728
|
+
case "none":
|
|
49729
|
+
return `none \u2014 ${record.reason}`;
|
|
49730
|
+
default:
|
|
49731
|
+
return record.kind;
|
|
49732
|
+
}
|
|
49733
|
+
}
|
|
49734
|
+
function readAcKindRecords(value) {
|
|
49735
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
49736
|
+
return;
|
|
49737
|
+
const out = {};
|
|
49738
|
+
for (const [id, raw] of Object.entries(value)) {
|
|
49739
|
+
if (typeof raw !== "object" || raw === null)
|
|
49740
|
+
return;
|
|
49741
|
+
const record = raw;
|
|
49742
|
+
const kind = record["kind"];
|
|
49743
|
+
if ((kind === "exec" || kind === "invariant") && typeof record["check"] === "string")
|
|
49744
|
+
out[id] = { kind, check: record["check"] };
|
|
49745
|
+
else if (kind === "judged")
|
|
49746
|
+
out[id] = { kind };
|
|
49747
|
+
else if (kind === "none" && typeof record["reason"] === "string")
|
|
49748
|
+
out[id] = { kind, reason: record["reason"] };
|
|
49749
|
+
else if (kind === "unclassified")
|
|
49750
|
+
out[id] = { kind };
|
|
49751
|
+
else
|
|
49752
|
+
return;
|
|
49753
|
+
}
|
|
49754
|
+
return out;
|
|
49755
|
+
}
|
|
49756
|
+
var AC_LINE, MARKER_OPEN = "[verify:", KIND_WORDS = "exec, invariant, judged or none";
|
|
49757
|
+
var init_ac_kinds = __esm(() => {
|
|
49758
|
+
AC_LINE = /^\s*[-*]\s*(AC\d+)\s*:\s?(.*)$/i;
|
|
49759
|
+
});
|
|
49760
|
+
|
|
49605
49761
|
// src/review/cost.ts
|
|
49606
49762
|
function estimateTokens2(text) {
|
|
49607
49763
|
return Math.ceil(text.length / 4);
|
|
@@ -49676,7 +49832,7 @@ import path120 from "path";
|
|
|
49676
49832
|
function parseAcceptanceCriteria(content) {
|
|
49677
49833
|
const out = [];
|
|
49678
49834
|
for (const line of content.split(/\r?\n/)) {
|
|
49679
|
-
const match =
|
|
49835
|
+
const match = AC_LINE2.exec(line);
|
|
49680
49836
|
if (match?.[1]) {
|
|
49681
49837
|
out.push({ id: match[1].toUpperCase(), text: (match[2] ?? "").trim() });
|
|
49682
49838
|
}
|
|
@@ -49715,6 +49871,9 @@ function classifyNotCheckable(criterionText) {
|
|
|
49715
49871
|
reason: `not-checkable: every clause of this criterion names a condition no diff records (${labels.join(", ")}) \u2014 assigned without asking Jev.`
|
|
49716
49872
|
};
|
|
49717
49873
|
}
|
|
49874
|
+
function classifyAcCriterionNotCheckable(criterion) {
|
|
49875
|
+
return classifyNotCheckable(stripVerifyMarker(criterion.text));
|
|
49876
|
+
}
|
|
49718
49877
|
function extractCriterionTokens(text) {
|
|
49719
49878
|
const tokens = new Set;
|
|
49720
49879
|
for (const match of text.matchAll(/`([^`]+)`/g)) {
|
|
@@ -49732,7 +49891,7 @@ function extractCriterionTokens(text) {
|
|
|
49732
49891
|
return [...tokens];
|
|
49733
49892
|
}
|
|
49734
49893
|
function computeAcFacts(criterion, diffText, changedFiles) {
|
|
49735
|
-
const tokens = extractCriterionTokens(criterion.text);
|
|
49894
|
+
const tokens = extractCriterionTokens(stripVerifyMarker(criterion.text));
|
|
49736
49895
|
const tokensPresent = tokens.filter((token) => diffText.includes(token) || changedFiles.some((file) => file.includes(token)));
|
|
49737
49896
|
const tokensAbsent = tokens.filter((token) => !tokensPresent.includes(token));
|
|
49738
49897
|
const testFiles = changedFiles.filter((file) => /\.(test|spec)\.[a-z]+$/i.test(file));
|
|
@@ -49757,7 +49916,7 @@ function computeAcFacts(criterion, diffText, changedFiles) {
|
|
|
49757
49916
|
function questionFor(criterion) {
|
|
49758
49917
|
return {
|
|
49759
49918
|
type: "noul",
|
|
49760
|
-
instructions: `Given the deterministic facts and the redacted diff evidence above, does this change LIKELY SATISFY this frozen ` + `acceptance criterion? Criterion ${criterion.id}: ${redactSensitiveText(criterion.text)}`
|
|
49919
|
+
instructions: `Given the deterministic facts and the redacted diff evidence above, does this change LIKELY SATISFY this frozen ` + `acceptance criterion? Criterion ${criterion.id}: ${redactSensitiveText(stripVerifyMarker(criterion.text))}`
|
|
49761
49920
|
};
|
|
49762
49921
|
}
|
|
49763
49922
|
function selectMatchedHunks(facts, regions) {
|
|
@@ -49918,12 +50077,13 @@ function renderAcCheckAdvisoryNotice(verdicts) {
|
|
|
49918
50077
|
const inDoubt = verdicts.filter((v) => v.status === "not-evident").map((v) => v.id);
|
|
49919
50078
|
return `acceptance-criteria check (advisory): ${counts.likelyMet} likely met, ${counts.notEvident} not evident, ${counts.notCheckable} not checkable` + (inDoubt.length > 0 ? ` \u2014 in doubt: ${inDoubt.join(", ")}` : "") + ` (\`keryx flow check-ac\` for detail)`;
|
|
49920
50079
|
}
|
|
49921
|
-
var
|
|
50080
|
+
var AC_LINE2, NOT_CHECKABLE_MARKERS, AC_CHECK_TOKEN_BUDGET = 64000, MAX_MATCHED_HUNKS_PER_ITEM = 8, BATCH_BUDGET_FRACTION = 0.5, MAX_ITEMS_PER_BATCH = 6, DEFAULT_AC_CHECK_THRESHOLD = 0.5, STATUS_LABEL;
|
|
49922
50081
|
var init_check_ac = __esm(() => {
|
|
49923
50082
|
init_service2();
|
|
49924
50083
|
init_fs();
|
|
49925
50084
|
init_review_gate();
|
|
49926
|
-
|
|
50085
|
+
init_ac_kinds();
|
|
50086
|
+
AC_LINE2 = /^\s*[-*]\s*(AC\d+)\s*:\s?(.*)$/i;
|
|
49927
50087
|
NOT_CHECKABLE_MARKERS = [
|
|
49928
50088
|
{ label: "live check", re: /\blive[- ]?check\b|\brun(?:s|ning)? with\b.*\benv\b|\blive[- ]?run\b/i },
|
|
49929
50089
|
{ label: "CI green", re: /\bCI (?:is )?green\b|\bCI passes\b/i },
|
|
@@ -50385,6 +50545,10 @@ var init_templates3 = __esm(() => {
|
|
|
50385
50545
|
// src/flow/service.ts
|
|
50386
50546
|
import { mkdir as mkdir30, readFile as readFile58 } from "fs/promises";
|
|
50387
50547
|
import path122 from "path";
|
|
50548
|
+
async function deriveAcKinds(cwd, dir, flowId) {
|
|
50549
|
+
const { report } = buildAcKindReport(flowId, await readFile58(acPath(cwd, dir), "utf8"));
|
|
50550
|
+
return { ...report.criteria };
|
|
50551
|
+
}
|
|
50388
50552
|
function recordAttempt(task, outcome, at, detail) {
|
|
50389
50553
|
const attempts = task.attempts ?? { count: 0, log: [] };
|
|
50390
50554
|
attempts.count += 1;
|
|
@@ -50579,6 +50743,7 @@ function createFlowService(deps) {
|
|
|
50579
50743
|
throw new Error("Cannot freeze: acceptance-criteria.md must contain at least one real `- ACn:` criterion.");
|
|
50580
50744
|
}
|
|
50581
50745
|
flow.acChecksum = await acChecksum(cwd, dir);
|
|
50746
|
+
flow.acKinds = await deriveAcKinds(cwd, dir, flow.id);
|
|
50582
50747
|
flow.status = "ready";
|
|
50583
50748
|
return save(cwd, dir, flow, "frozen", `${criteria.length} criteria; checksum recorded`);
|
|
50584
50749
|
});
|
|
@@ -50723,10 +50888,15 @@ function createFlowService(deps) {
|
|
|
50723
50888
|
detail = `${normalizedCriterion}: "${previousText ?? "(new)"}" -> "${normalizedText}" (${reason.trim()})`;
|
|
50724
50889
|
}
|
|
50725
50890
|
flow.acChecksum = await acChecksum(cwd, dir);
|
|
50891
|
+
flow.acKinds = await deriveAcKinds(cwd, dir, flow.id);
|
|
50726
50892
|
flow.acConfirmed = {};
|
|
50727
50893
|
return save(cwd, dir, flow, "ac-updated", detail);
|
|
50728
50894
|
});
|
|
50729
50895
|
},
|
|
50896
|
+
async acKinds({ cwd, id }) {
|
|
50897
|
+
const { dir, flow } = await load(cwd, id);
|
|
50898
|
+
return buildAcKindReport(flow.id, await readFile58(acPath(cwd, dir), "utf8"));
|
|
50899
|
+
},
|
|
50730
50900
|
async acReseal({ cwd, id, reason }) {
|
|
50731
50901
|
if (!reason?.trim()) {
|
|
50732
50902
|
throw new Error('flow ac reseal requires --reason "<why the checksum is stale>"');
|
|
@@ -50745,6 +50915,7 @@ function createFlowService(deps) {
|
|
|
50745
50915
|
throw new Error(`flow ac reseal: refusing \u2014 ${evidence.reason}. ` + "Reseal only re-seals a stale checksum over an unchanged file; it is not a way to " + 'accept an edit. If the criteria really changed, use `keryx flow ac update <id> --reason "..."`, ' + "which re-seals AND voids the prior confirmations.");
|
|
50746
50916
|
}
|
|
50747
50917
|
flow.acChecksum = current;
|
|
50918
|
+
flow.acKinds = await deriveAcKinds(cwd, dir, flow.id);
|
|
50748
50919
|
return save(cwd, dir, flow, "ac-resealed", reason);
|
|
50749
50920
|
});
|
|
50750
50921
|
},
|
|
@@ -51418,6 +51589,8 @@ var init_service9 = __esm(() => {
|
|
|
51418
51589
|
init_confirm_token();
|
|
51419
51590
|
init_confirm_token();
|
|
51420
51591
|
init_store3();
|
|
51592
|
+
init_ac_kinds();
|
|
51593
|
+
init_ac_kinds();
|
|
51421
51594
|
init_review_gate();
|
|
51422
51595
|
init_check_ac();
|
|
51423
51596
|
init_store3();
|
|
@@ -94059,6 +94232,25 @@ var init_modal_host = __esm(() => {
|
|
|
94059
94232
|
hosts = new WeakMap;
|
|
94060
94233
|
});
|
|
94061
94234
|
|
|
94235
|
+
// src/tui/ac-kinds-surface.ts
|
|
94236
|
+
function formatAcKindLines(item) {
|
|
94237
|
+
if (item.acKinds === undefined) {
|
|
94238
|
+
if (item.acFrozen === false) {
|
|
94239
|
+
return ["Verification kinds: not frozen yet (kinds are read when the criteria are frozen)."];
|
|
94240
|
+
}
|
|
94241
|
+
return [
|
|
94242
|
+
"Verification kinds: not recorded (this flow was frozen before kinds existed; every criterion reads unclassified)."
|
|
94243
|
+
];
|
|
94244
|
+
}
|
|
94245
|
+
const report = reportFromRecords(item.id, item.acKinds);
|
|
94246
|
+
const rows = Object.entries(report.criteria).map(([id, record]) => `${id.padEnd(ID_WIDTH)}${describeAcKind(record)}`);
|
|
94247
|
+
return [...renderAcKindDistribution(report), "", ...rows.length === 0 ? ["(no criteria)"] : rows];
|
|
94248
|
+
}
|
|
94249
|
+
var ID_WIDTH = 5;
|
|
94250
|
+
var init_ac_kinds_surface = __esm(() => {
|
|
94251
|
+
init_service9();
|
|
94252
|
+
});
|
|
94253
|
+
|
|
94062
94254
|
// src/review/scope.ts
|
|
94063
94255
|
function classifyPath2(path, config = DEFAULT_REVIEW_SCOPE_CONFIG) {
|
|
94064
94256
|
const normalized = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -94333,7 +94525,7 @@ function isCommentOnly(hunk, block, path) {
|
|
|
94333
94525
|
}
|
|
94334
94526
|
return sawComment;
|
|
94335
94527
|
}
|
|
94336
|
-
function
|
|
94528
|
+
function emptyCounts2() {
|
|
94337
94529
|
const droppedByReason = Object.fromEntries(SCOPE_DROP_REASONS.map((reason) => [reason, 0]));
|
|
94338
94530
|
return {
|
|
94339
94531
|
filesSeen: 0,
|
|
@@ -94376,7 +94568,7 @@ function windowsFor(blocks, contextLines) {
|
|
|
94376
94568
|
}
|
|
94377
94569
|
function buildReviewScope(diff, config) {
|
|
94378
94570
|
const resolved = resolveConfig(config);
|
|
94379
|
-
const counts =
|
|
94571
|
+
const counts = emptyCounts2();
|
|
94380
94572
|
const drops = [];
|
|
94381
94573
|
const regions = [];
|
|
94382
94574
|
const files = [];
|
|
@@ -94488,7 +94680,7 @@ function buildReviewScope(diff, config) {
|
|
|
94488
94680
|
}
|
|
94489
94681
|
function buildPathScope(paths, config) {
|
|
94490
94682
|
const resolved = resolveConfig(config);
|
|
94491
|
-
const counts =
|
|
94683
|
+
const counts = emptyCounts2();
|
|
94492
94684
|
const drops = [];
|
|
94493
94685
|
const files = [];
|
|
94494
94686
|
for (const path of paths) {
|
|
@@ -95192,7 +95384,7 @@ async function runCheckAc(cwd, id, opts = {}, deps = {}) {
|
|
|
95192
95384
|
const changedFiles = scope.files;
|
|
95193
95385
|
const notCheckable = [];
|
|
95194
95386
|
const checkableCriteria = criteria.filter((criterion) => {
|
|
95195
|
-
const classification =
|
|
95387
|
+
const classification = classifyAcCriterionNotCheckable(criterion);
|
|
95196
95388
|
if (classification === undefined)
|
|
95197
95389
|
return true;
|
|
95198
95390
|
notCheckable.push(notCheckableVerdict(criterion, classification.reason));
|
|
@@ -95992,6 +96184,11 @@ async function loadInspectorFlows(cwd) {
|
|
|
95992
96184
|
if (await isCompletionInterrupted(cwd, flow, dir)) {
|
|
95993
96185
|
item.interrupted = interruptedCompletionLine(flow.id);
|
|
95994
96186
|
}
|
|
96187
|
+
const kinds = readAcKindRecords(flow.acKinds);
|
|
96188
|
+
item.acFrozen = flow.acChecksum !== null;
|
|
96189
|
+
if (kinds !== undefined) {
|
|
96190
|
+
item.acKinds = kinds;
|
|
96191
|
+
}
|
|
95995
96192
|
try {
|
|
95996
96193
|
const cache = await readAcCheckCache(acCheckCachePath(cwd, dir));
|
|
95997
96194
|
if (cache !== undefined) {
|
|
@@ -96209,6 +96406,7 @@ function presentFlows(openModal, otui, chrome, options) {
|
|
|
96209
96406
|
let selected = 0;
|
|
96210
96407
|
let listScroll = 0;
|
|
96211
96408
|
let detailScroll = 0;
|
|
96409
|
+
let acScroll = 0;
|
|
96212
96410
|
let listNode;
|
|
96213
96411
|
let detailNode;
|
|
96214
96412
|
let acNode;
|
|
@@ -96231,7 +96429,7 @@ function presentFlows(openModal, otui, chrome, options) {
|
|
|
96231
96429
|
if (checking) {
|
|
96232
96430
|
return ["Checking\u2026", "", "Running `keryx flow check-ac` against the frozen criteria \u2014 this can take a few seconds."];
|
|
96233
96431
|
}
|
|
96234
|
-
const raw = item !== undefined ? formatAcCheckLines(item) : ["No flow selected."];
|
|
96432
|
+
const raw = item !== undefined ? [...formatAcKindLines(item), "", ...formatAcCheckLines(item)] : ["No flow selected."];
|
|
96235
96433
|
const withError = checkError !== undefined ? [...raw, "", `Error: ${checkError}`] : raw;
|
|
96236
96434
|
return wrapLines(withError.join(`
|
|
96237
96435
|
`), tabWidth).split(`
|
|
@@ -96250,7 +96448,8 @@ function presentFlows(openModal, otui, chrome, options) {
|
|
|
96250
96448
|
`);
|
|
96251
96449
|
}
|
|
96252
96450
|
if (acNode !== undefined) {
|
|
96253
|
-
|
|
96451
|
+
acScroll = clampScroll(acScroll, acLines().length, bodyRows);
|
|
96452
|
+
acNode.content = windowLines(acLines(), acScroll, bodyRows).join(`
|
|
96254
96453
|
`);
|
|
96255
96454
|
}
|
|
96256
96455
|
};
|
|
@@ -96264,6 +96463,7 @@ function presentFlows(openModal, otui, chrome, options) {
|
|
|
96264
96463
|
}
|
|
96265
96464
|
selected = clamped;
|
|
96266
96465
|
detailScroll = 0;
|
|
96466
|
+
acScroll = 0;
|
|
96267
96467
|
paintSelection();
|
|
96268
96468
|
};
|
|
96269
96469
|
const handle = openModal(otui, chrome, {
|
|
@@ -96284,7 +96484,8 @@ function presentFlows(openModal, otui, chrome, options) {
|
|
|
96284
96484
|
return;
|
|
96285
96485
|
}
|
|
96286
96486
|
if (tabId === "ac") {
|
|
96287
|
-
|
|
96487
|
+
acScroll = clampScroll(acScroll, acLines().length, bodyRows);
|
|
96488
|
+
acNode = paintLines(otui, renderer, body, windowLines(acLines(), acScroll, bodyRows));
|
|
96288
96489
|
return;
|
|
96289
96490
|
}
|
|
96290
96491
|
detailScroll = clampScroll(detailScroll, detailLines().length, bodyRows);
|
|
@@ -96354,6 +96555,12 @@ function presentFlows(openModal, otui, chrome, options) {
|
|
|
96354
96555
|
}
|
|
96355
96556
|
return;
|
|
96356
96557
|
}
|
|
96558
|
+
if (handle.activeTab() === "ac" && (token === "pageup" || token === "pagedown")) {
|
|
96559
|
+
const step = token === "pageup" ? -bodyRows : bodyRows;
|
|
96560
|
+
acScroll = clampScroll(acScroll + step, acLines().length, bodyRows);
|
|
96561
|
+
paintSelection();
|
|
96562
|
+
return;
|
|
96563
|
+
}
|
|
96357
96564
|
if (onDetail && (token === "pageup" || token === "pagedown")) {
|
|
96358
96565
|
const step = token === "pageup" ? -bodyRows : bodyRows;
|
|
96359
96566
|
detailScroll = clampScroll(detailScroll + step, detailLines().length, bodyRows);
|
|
@@ -96369,6 +96576,7 @@ function openFlows(otui, chrome, options) {
|
|
|
96369
96576
|
var FLOWS_COMMAND = "/flows", AC_COMMAND = "/ac", FLOWS_FOOTER, MARKER_GLYPH;
|
|
96370
96577
|
var init_flow_inspector = __esm(() => {
|
|
96371
96578
|
init_modal_host();
|
|
96579
|
+
init_ac_kinds_surface();
|
|
96372
96580
|
init_inspector_sources();
|
|
96373
96581
|
FLOWS_FOOTER = [
|
|
96374
96582
|
{ key: "[/]", label: "flow" },
|
|
@@ -97271,12 +97479,30 @@ function summarizeConfirmations(flow) {
|
|
|
97271
97479
|
}
|
|
97272
97480
|
};
|
|
97273
97481
|
}
|
|
97482
|
+
function summarizeAcceptance(flow, criteriaInFile) {
|
|
97483
|
+
const records = readAcKindRecords(flow.acKinds);
|
|
97484
|
+
if (records === undefined) {
|
|
97485
|
+
const total = criteriaInFile;
|
|
97486
|
+
return {
|
|
97487
|
+
recorded: false,
|
|
97488
|
+
frozen: flow.acChecksum !== null,
|
|
97489
|
+
total,
|
|
97490
|
+
counts: { exec: 0, invariant: 0, judged: 0, none: 0, unclassified: total ?? 0 },
|
|
97491
|
+
runnable: 0
|
|
97492
|
+
};
|
|
97493
|
+
}
|
|
97494
|
+
const report = reportFromRecords(flow.id, records);
|
|
97495
|
+
return { recorded: true, total: report.total, counts: { ...report.counts }, runnable: report.runnable };
|
|
97496
|
+
}
|
|
97274
97497
|
function summarizeGateOutcomes(flow) {
|
|
97275
97498
|
if (flow.completionAttempts === undefined) {
|
|
97276
97499
|
return { recorded: false };
|
|
97277
97500
|
}
|
|
97278
97501
|
return { recorded: true, attempts: flow.completionAttempts };
|
|
97279
97502
|
}
|
|
97503
|
+
var init_accountability = __esm(() => {
|
|
97504
|
+
init_ac_kinds();
|
|
97505
|
+
});
|
|
97280
97506
|
|
|
97281
97507
|
// src/governance/aggregate.ts
|
|
97282
97508
|
async function collectFlowGovernance(cwd, ledger) {
|
|
@@ -97285,6 +97511,14 @@ async function collectFlowGovernance(cwd, ledger) {
|
|
|
97285
97511
|
for (const dir of dirs) {
|
|
97286
97512
|
const flow = await readFlow(cwd, dir);
|
|
97287
97513
|
const manifests = await readFlowReviewManifests(cwd, dir);
|
|
97514
|
+
let criteriaInFile;
|
|
97515
|
+
if (readAcKindRecords(flow.acKinds) === undefined) {
|
|
97516
|
+
try {
|
|
97517
|
+
criteriaInFile = (await readAcCriteria(cwd, dir)).length;
|
|
97518
|
+
} catch {
|
|
97519
|
+
criteriaInFile = undefined;
|
|
97520
|
+
}
|
|
97521
|
+
}
|
|
97288
97522
|
flows.push({
|
|
97289
97523
|
id: flow.id,
|
|
97290
97524
|
dir,
|
|
@@ -97297,6 +97531,7 @@ async function collectFlowGovernance(cwd, ledger) {
|
|
|
97297
97531
|
spend: summarizeReviewSpend(manifests),
|
|
97298
97532
|
confirmations: summarizeConfirmations(flow),
|
|
97299
97533
|
gateOutcomes: summarizeGateOutcomes(flow),
|
|
97534
|
+
acceptance: summarizeAcceptance(flow, criteriaInFile),
|
|
97300
97535
|
dispatch: collectFlowDispatch(ledger, flow.id)
|
|
97301
97536
|
});
|
|
97302
97537
|
}
|
|
@@ -97335,9 +97570,11 @@ async function collectProjectGovernance(root, filters, displayName) {
|
|
|
97335
97570
|
}
|
|
97336
97571
|
var POLICY_DECISIONS_NOT_RECORDED;
|
|
97337
97572
|
var init_aggregate = __esm(() => {
|
|
97573
|
+
init_ac_kinds();
|
|
97338
97574
|
init_store3();
|
|
97339
97575
|
init_record2();
|
|
97340
97576
|
init_spend();
|
|
97577
|
+
init_accountability();
|
|
97341
97578
|
POLICY_DECISIONS_NOT_RECORDED = {
|
|
97342
97579
|
recorded: false,
|
|
97343
97580
|
reason: "no durable log of an interactive session's own allow/ask/deny policy decisions exists in this build " + "(unattended dispatch runs are different: their denials are recorded per run, and reported under each " + "flow's own dispatch section below)"
|
|
@@ -97419,6 +97656,19 @@ function identityLine(identity) {
|
|
|
97419
97656
|
}
|
|
97420
97657
|
return `${identity.value ?? "unknown"} [${identity.basis}]`;
|
|
97421
97658
|
}
|
|
97659
|
+
function renderAcceptanceLine(acceptance) {
|
|
97660
|
+
if (acceptance === undefined || !acceptance.recorded) {
|
|
97661
|
+
const total = acceptance?.total;
|
|
97662
|
+
if (acceptance?.frozen === false) {
|
|
97663
|
+
return `acceptance coverage: not frozen yet (${total ?? 0} criteria drafted; kinds are read at freeze)`;
|
|
97664
|
+
}
|
|
97665
|
+
return total === undefined ? "acceptance coverage: not recorded (predates verification kinds; every criterion reads unclassified)" : `acceptance coverage: 0/${total} runnable (0%) \u2014 all ${total} unclassified (predates verification kinds)`;
|
|
97666
|
+
}
|
|
97667
|
+
const { counts } = acceptance;
|
|
97668
|
+
const total = acceptance.total ?? 0;
|
|
97669
|
+
const percent = total === 0 ? 0 : Math.round(acceptance.runnable / total * 100);
|
|
97670
|
+
return `acceptance coverage: ${acceptance.runnable}/${total} runnable (${percent}%) \u2014 ` + `exec ${counts.exec}, invariant ${counts.invariant}, judged ${counts.judged}, none ${counts.none}, unclassified ${counts.unclassified}`;
|
|
97671
|
+
}
|
|
97422
97672
|
function renderGovernanceMarkdown(report) {
|
|
97423
97673
|
const lines = [
|
|
97424
97674
|
"# Governance report",
|
|
@@ -97451,6 +97701,7 @@ function renderGovernanceMarkdown(report) {
|
|
|
97451
97701
|
lines.push(`status: ${flow.status}`);
|
|
97452
97702
|
lines.push(`owner: ${identityLine(flow.owner)}`);
|
|
97453
97703
|
lines.push(`review spend: spent=${usd(flow.spend.spentUsd)} (rounds_with_spent=${flow.spend.roundsWithSpentUsd}), ` + `input_tokens=${tokens(flow.spend.inputTokens)} (rounds=${flow.spend.roundsWithInputTokens}), ` + `output_tokens=${tokens(flow.spend.outputTokens)} (rounds=${flow.spend.roundsWithOutputTokens}), ` + `rounds_with_cost=${flow.spend.roundsWithCost}/${flow.spend.roundsTotal}`);
|
|
97704
|
+
lines.push(renderAcceptanceLine(flow.acceptance));
|
|
97454
97705
|
if (!flow.confirmations.recorded) {
|
|
97455
97706
|
lines.push("confirmations: not recorded (predates signing)");
|
|
97456
97707
|
} else {
|
|
@@ -121586,6 +121837,47 @@ async function runSimple(args, action) {
|
|
|
121586
121837
|
const id = requireId(args);
|
|
121587
121838
|
const flow = await getService2()[action]({ cwd: process.cwd(), id });
|
|
121588
121839
|
console.log(` ${style.green(symbols.ok)} Flow ${flow.id} ${style.cyan(symbols.arrow)} ${flowStatusLabel(flow.status)}`);
|
|
121840
|
+
if (action === "freeze") {
|
|
121841
|
+
await printAcKindDistribution(process.cwd(), flow.id);
|
|
121842
|
+
}
|
|
121843
|
+
}
|
|
121844
|
+
async function printAcKindDistribution(cwd, id) {
|
|
121845
|
+
try {
|
|
121846
|
+
const { report, errors } = await getService2().acKinds({ cwd, id });
|
|
121847
|
+
for (const line of renderAcKindDistribution(report)) {
|
|
121848
|
+
console.log(` ${style.dim(line)}`);
|
|
121849
|
+
}
|
|
121850
|
+
for (const error of errors) {
|
|
121851
|
+
console.log(` ${style.yellow(WARN)} ${error.id}: ${error.message} ${style.dim("(read as unclassified)")}`);
|
|
121852
|
+
}
|
|
121853
|
+
if (errors.length > 0) {
|
|
121854
|
+
note(`\`keryx flow ac kinds ${id}\` names each malformed marker. The freeze went through: a kind never refuses one.`);
|
|
121855
|
+
}
|
|
121856
|
+
} catch {}
|
|
121857
|
+
}
|
|
121858
|
+
async function runAcKinds(args) {
|
|
121859
|
+
const asJson = args.includes("--json");
|
|
121860
|
+
const { positionals } = parseAcArgs(args.filter((arg) => arg !== "--json"), "kinds", [], 1);
|
|
121861
|
+
const id = requireId(positionals);
|
|
121862
|
+
const { report, errors } = await getService2().acKinds({ cwd: process.cwd(), id });
|
|
121863
|
+
if (errors.length > 0) {
|
|
121864
|
+
process.exitCode = 1;
|
|
121865
|
+
}
|
|
121866
|
+
if (asJson) {
|
|
121867
|
+
console.log(JSON.stringify({ ...report, errors }, null, 2));
|
|
121868
|
+
return;
|
|
121869
|
+
}
|
|
121870
|
+
heading(`Flow ${report.flowId}: acceptance kinds`);
|
|
121871
|
+
for (const [criterionId, record] of Object.entries(report.criteria)) {
|
|
121872
|
+
console.log(` ${style.bold(criterionId.padEnd(5))} ${describeAcKind(record)}`);
|
|
121873
|
+
}
|
|
121874
|
+
console.log("");
|
|
121875
|
+
for (const line of renderAcKindDistribution(report)) {
|
|
121876
|
+
console.log(` ${line}`);
|
|
121877
|
+
}
|
|
121878
|
+
for (const error of errors) {
|
|
121879
|
+
console.error(`${style.red(symbols.cross)} ${error.id}: ${error.message}`);
|
|
121880
|
+
}
|
|
121589
121881
|
}
|
|
121590
121882
|
async function runTask(args) {
|
|
121591
121883
|
const sub = args[0];
|
|
@@ -121731,6 +122023,9 @@ async function runAc(args) {
|
|
|
121731
122023
|
}
|
|
121732
122024
|
return;
|
|
121733
122025
|
}
|
|
122026
|
+
if (sub === "kinds") {
|
|
122027
|
+
return await runAcKinds(args.slice(1));
|
|
122028
|
+
}
|
|
121734
122029
|
if (sub === "reseal") {
|
|
121735
122030
|
const { positionals, values } = parseAcArgs(args.slice(1), "reseal", AC_RESEAL_FLAGS, 1);
|
|
121736
122031
|
const id = requireId(positionals);
|
|
@@ -121742,7 +122037,7 @@ async function runAc(args) {
|
|
|
121742
122037
|
console.log(` ${style.green(symbols.ok)} Checksum re-sealed over the unchanged file; ${style.dim(`${Object.keys(flow.acConfirmed).length} confirmation(s) kept`)}.`);
|
|
121743
122038
|
return;
|
|
121744
122039
|
}
|
|
121745
|
-
throw new Error('Usage: keryx flow ac <confirm <id> <ACn> | update <id> --reason "<why>" [--criterion ACn --text "<criterion>"] | reseal <id> --reason "<why>"> ...');
|
|
122040
|
+
throw new Error('Usage: keryx flow ac <confirm <id> <ACn> | update <id> --reason "<why>" [--criterion ACn --text "<criterion>"] | reseal <id> --reason "<why>" | kinds <id> [--json]> ...');
|
|
121746
122041
|
}
|
|
121747
122042
|
async function runCheckAcCommand(args) {
|
|
121748
122043
|
const id = requireId(args);
|
|
@@ -122030,6 +122325,7 @@ function printHelp5() {
|
|
|
122030
122325
|
'keryx flow ac update <id> --reason "<why>" (re-freeze the file as already edited; VOIDS prior confirmations)',
|
|
122031
122326
|
'keryx flow ac update <id> --criterion ACn --text "<criterion>" --reason "<why>" (rewrite/append that one criterion, then re-freeze; VOIDS prior confirmations)',
|
|
122032
122327
|
'keryx flow ac reseal <id> --reason "<why>" (checksum stale, file unchanged; KEEPS confirmations)',
|
|
122328
|
+
"keryx flow ac kinds <id> [--json] (verification kind per criterion; read-only, never gates)",
|
|
122033
122329
|
" every `flow ac` subcommand refuses an argument it does not use \u2014 an extra positional, an unknown flag, or --criterion/--text given alone",
|
|
122034
122330
|
"keryx flow check-ac <id> [--diff <ref>|--pr <n>] [--json] [--refresh] (ADVISORY: Jev vs. the frozen criteria; never changes flow state)",
|
|
122035
122331
|
"keryx flow implemented <id> --pr <url>",
|
|
@@ -158414,6 +158710,19 @@ var init_command_registry = __esm(() => {
|
|
|
158414
158710
|
read: false,
|
|
158415
158711
|
sideEffects: ["writes flow.json's acChecksum to match the file on disk; acConfirmed is left untouched"]
|
|
158416
158712
|
},
|
|
158713
|
+
{
|
|
158714
|
+
module: "tasks",
|
|
158715
|
+
command: "flow ac kinds",
|
|
158716
|
+
summary: "Read a flow's acceptance-criteria.md and report each criterion's verification kind \u2014 exec, invariant, " + "judged, none or unclassified \u2014 from its trailing `[verify: ...]` marker, with the distribution and the " + "runnable coverage. Reads only: never gates, never runs a check, never calls a model. Exits 1 when a " + "marker is malformed, naming the criterion; that criterion reads unclassified.",
|
|
158717
|
+
intent: ["\u0432\u0438\u0434\u044B \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0435\u0432", "flow ac kinds", "acceptance criteria verification kinds", "ac coverage"],
|
|
158718
|
+
args: [
|
|
158719
|
+
{ name: "<id>", type: "string", required: true, desc: "flow id" },
|
|
158720
|
+
{ name: "json", type: "bool", required: false, desc: "print the report as JSON, with the per-criterion records and any marker errors" }
|
|
158721
|
+
],
|
|
158722
|
+
json: true,
|
|
158723
|
+
read: true,
|
|
158724
|
+
sideEffects: []
|
|
158725
|
+
},
|
|
158417
158726
|
{
|
|
158418
158727
|
module: "tasks",
|
|
158419
158728
|
command: "flow complete",
|
|
@@ -177115,6 +177424,10 @@ ledger (.metaproject/data/trigger/runs.jsonl). Never re-runs a gate, never
|
|
|
177115
177424
|
calls a model or a network service. A figure nobody recorded is reported as
|
|
177116
177425
|
"not recorded", never as zero.
|
|
177117
177426
|
|
|
177427
|
+
Each flow also shows its acceptance coverage: how many criteria are runnable
|
|
177428
|
+
(exec or invariant) of all of them. A flow frozen before verification kinds
|
|
177429
|
+
existed reads as fully unclassified, not as zero criteria.
|
|
177430
|
+
|
|
177118
177431
|
\`report\` writes .metaproject/data/governance/artifacts/latest.md and
|
|
177119
177432
|
latest.json (schema-versioned), then prints the report. \`show\` reprints the
|
|
177120
177433
|
most recently written report without regenerating it.
|
package/dist/core.js
CHANGED
|
@@ -21627,6 +21627,7 @@ init_validator();
|
|
|
21627
21627
|
var exports_service2 = {};
|
|
21628
21628
|
__export(exports_service2, {
|
|
21629
21629
|
AC_CHECK_TOKEN_BUDGET: () => AC_CHECK_TOKEN_BUDGET,
|
|
21630
|
+
AC_KINDS: () => AC_KINDS,
|
|
21630
21631
|
CONFIRMATION_CAVEAT: () => CONFIRMATION_CAVEAT,
|
|
21631
21632
|
REVIEW_GATE_CONFIG_PATH: () => REVIEW_GATE_CONFIG_PATH,
|
|
21632
21633
|
acCheckCacheKey: () => acCheckCacheKey,
|
|
@@ -21636,10 +21637,13 @@ __export(exports_service2, {
|
|
|
21636
21637
|
assertAcIntact: () => assertAcIntact,
|
|
21637
21638
|
baseBranchCondition: () => baseBranchCondition,
|
|
21638
21639
|
batchAcCheckItems: () => batchAcCheckItems,
|
|
21640
|
+
buildAcKindReport: () => buildAcKindReport,
|
|
21641
|
+
classifyAcCriterionNotCheckable: () => classifyAcCriterionNotCheckable,
|
|
21639
21642
|
classifyNotCheckable: () => classifyNotCheckable,
|
|
21640
21643
|
computeAcFacts: () => computeAcFacts,
|
|
21641
21644
|
confirmPreconditionError: () => confirmPreconditionError,
|
|
21642
21645
|
createFlowService: () => createFlowService,
|
|
21646
|
+
describeAcKind: () => describeAcKind,
|
|
21643
21647
|
evaluatedVerdict: () => evaluatedVerdict,
|
|
21644
21648
|
extractCriterionTokens: () => extractCriterionTokens,
|
|
21645
21649
|
factsOnlyVerdict: () => factsOnlyVerdict,
|
|
@@ -21649,12 +21653,16 @@ __export(exports_service2, {
|
|
|
21649
21653
|
parseAcceptanceCriteria: () => parseAcceptanceCriteria,
|
|
21650
21654
|
readAcCheckCache: () => readAcCheckCache,
|
|
21651
21655
|
readAcCheckEnabled: () => readAcCheckEnabled,
|
|
21656
|
+
readAcKindRecords: () => readAcKindRecords,
|
|
21652
21657
|
readFlow: () => readFlow,
|
|
21653
21658
|
renderAcCheckAdvisoryNotice: () => renderAcCheckAdvisoryNotice,
|
|
21654
21659
|
renderAcCheckReport: () => renderAcCheckReport,
|
|
21660
|
+
renderAcKindDistribution: () => renderAcKindDistribution,
|
|
21661
|
+
reportFromRecords: () => reportFromRecords,
|
|
21655
21662
|
resolveFlowDir: () => resolveFlowDir,
|
|
21656
21663
|
selectMatchedHunks: () => selectMatchedHunks,
|
|
21657
21664
|
statusLabel: () => statusLabel,
|
|
21665
|
+
stripVerifyMarker: () => stripVerifyMarker,
|
|
21658
21666
|
summarizeVerdicts: () => summarizeVerdicts,
|
|
21659
21667
|
validateCriterionName: () => validateCriterionName,
|
|
21660
21668
|
writeAcCheckCache: () => writeAcCheckCache
|
|
@@ -23979,6 +23987,161 @@ async function readRequireConfirmationDefault(cwd) {
|
|
|
23979
23987
|
return false;
|
|
23980
23988
|
}
|
|
23981
23989
|
}
|
|
23990
|
+
// src/flow/ac-kinds.ts
|
|
23991
|
+
var AC_KINDS = ["exec", "invariant", "judged", "none", "unclassified"];
|
|
23992
|
+
var AC_LINE = /^\s*[-*]\s*(AC\d+)\s*:\s?(.*)$/i;
|
|
23993
|
+
var MARKER_OPEN = "[verify:";
|
|
23994
|
+
function maskCodeSpans(text) {
|
|
23995
|
+
return text.replace(/`[^`]*`/g, (span) => " ".repeat(span.length));
|
|
23996
|
+
}
|
|
23997
|
+
function scanMarkers(text) {
|
|
23998
|
+
const masked = maskCodeSpans(text);
|
|
23999
|
+
let occurrences = 0;
|
|
24000
|
+
let lastStart = -1;
|
|
24001
|
+
for (let at = masked.indexOf(MARKER_OPEN);at !== -1; at = masked.indexOf(MARKER_OPEN, at + MARKER_OPEN.length)) {
|
|
24002
|
+
occurrences += 1;
|
|
24003
|
+
lastStart = at;
|
|
24004
|
+
}
|
|
24005
|
+
const end = text.trimEnd().length;
|
|
24006
|
+
return { occurrences, lastStart, trailing: occurrences > 0 && end > lastStart && text[end - 1] === "]", end };
|
|
24007
|
+
}
|
|
24008
|
+
function stripVerifyMarker(text) {
|
|
24009
|
+
const scan = scanMarkers(text);
|
|
24010
|
+
if (!scan.trailing || scan.occurrences !== 1)
|
|
24011
|
+
return text;
|
|
24012
|
+
return text.slice(0, scan.lastStart).trimEnd();
|
|
24013
|
+
}
|
|
24014
|
+
var KIND_WORDS = "exec, invariant, judged or none";
|
|
24015
|
+
function unclassified(error) {
|
|
24016
|
+
return error === undefined ? { record: { kind: "unclassified" } } : { record: { kind: "unclassified" }, error };
|
|
24017
|
+
}
|
|
24018
|
+
function parseAcKindText(text) {
|
|
24019
|
+
const scan = scanMarkers(text);
|
|
24020
|
+
if (scan.occurrences === 0)
|
|
24021
|
+
return unclassified();
|
|
24022
|
+
if (scan.occurrences > 1) {
|
|
24023
|
+
return unclassified("a second [verify: \u2026] marker \u2014 a criterion carries exactly one, as the last thing on its line");
|
|
24024
|
+
}
|
|
24025
|
+
if (!scan.trailing) {
|
|
24026
|
+
return unclassified("the [verify: \u2026] marker must be the last thing on the line and close with `]`");
|
|
24027
|
+
}
|
|
24028
|
+
const payload = text.slice(scan.lastStart + MARKER_OPEN.length, scan.end - 1).trim();
|
|
24029
|
+
if (payload.length === 0)
|
|
24030
|
+
return unclassified(`an empty [verify: \u2026] marker \u2014 expected ${KIND_WORDS}`);
|
|
24031
|
+
const word = /^[A-Za-z]+/.exec(payload)?.[0] ?? "";
|
|
24032
|
+
const rest = payload.slice(word.length).trim();
|
|
24033
|
+
switch (word) {
|
|
24034
|
+
case "judged":
|
|
24035
|
+
return rest.length === 0 ? { record: { kind: "judged" } } : unclassified("`judged` takes no arguments \u2014 write [verify: judged]");
|
|
24036
|
+
case "exec":
|
|
24037
|
+
case "invariant": {
|
|
24038
|
+
if (rest.length === 0)
|
|
24039
|
+
return unclassified(`\`${word}\` requires a backticked command, e.g. [verify: ${word} \`bun test path/to.test.ts\`]`);
|
|
24040
|
+
const command = /^`([^`]*)`$/.exec(rest);
|
|
24041
|
+
if (command === null)
|
|
24042
|
+
return unclassified(`\`${word}\` requires exactly one backticked command after the kind, e.g. [verify: ${word} \`bun test path/to.test.ts\`]`);
|
|
24043
|
+
const check = (command[1] ?? "").trim();
|
|
24044
|
+
if (check.length === 0)
|
|
24045
|
+
return unclassified(`\`${word}\` has an empty backticked command`);
|
|
24046
|
+
return { record: { kind: word, check } };
|
|
24047
|
+
}
|
|
24048
|
+
case "none": {
|
|
24049
|
+
if (rest.length === 0)
|
|
24050
|
+
return unclassified("`none` requires a reason after an em dash, e.g. [verify: none \u2014 <reason>]");
|
|
24051
|
+
const reason = /^\u2014\s*([\s\S]*)$/.exec(rest);
|
|
24052
|
+
if (reason === null)
|
|
24053
|
+
return unclassified("`none` requires its reason after an em dash (\u2014), e.g. [verify: none \u2014 <reason>]");
|
|
24054
|
+
const why = (reason[1] ?? "").trim();
|
|
24055
|
+
if (why.length === 0)
|
|
24056
|
+
return unclassified("`none` has an empty reason after the em dash");
|
|
24057
|
+
return { record: { kind: "none", reason: why } };
|
|
24058
|
+
}
|
|
24059
|
+
default:
|
|
24060
|
+
return unclassified(`unknown verification kind "${word.length > 0 ? word : payload}" \u2014 expected ${KIND_WORDS}`);
|
|
24061
|
+
}
|
|
24062
|
+
}
|
|
24063
|
+
function parseAcKinds(content) {
|
|
24064
|
+
const criteria = [];
|
|
24065
|
+
const errors = [];
|
|
24066
|
+
for (const line of content.split(/\r?\n/)) {
|
|
24067
|
+
const match = AC_LINE.exec(line);
|
|
24068
|
+
if (!match?.[1])
|
|
24069
|
+
continue;
|
|
24070
|
+
const id = match[1].toUpperCase();
|
|
24071
|
+
const rawText = (match[2] ?? "").trim();
|
|
24072
|
+
const parsed = parseAcKindText(rawText);
|
|
24073
|
+
if (parsed.error !== undefined)
|
|
24074
|
+
errors.push({ id, message: parsed.error });
|
|
24075
|
+
criteria.push({ id, text: stripVerifyMarker(rawText), rawText, record: parsed.record });
|
|
24076
|
+
}
|
|
24077
|
+
return { criteria, errors };
|
|
24078
|
+
}
|
|
24079
|
+
function emptyCounts() {
|
|
24080
|
+
return { exec: 0, invariant: 0, judged: 0, none: 0, unclassified: 0 };
|
|
24081
|
+
}
|
|
24082
|
+
function buildAcKindReport(flowId, content) {
|
|
24083
|
+
const parsed = parseAcKinds(content);
|
|
24084
|
+
const criteria = {};
|
|
24085
|
+
for (const criterion of parsed.criteria) {
|
|
24086
|
+
if (!(criterion.id in criteria))
|
|
24087
|
+
criteria[criterion.id] = criterion.record;
|
|
24088
|
+
}
|
|
24089
|
+
return { report: reportFromRecords(flowId, criteria), errors: parsed.errors };
|
|
24090
|
+
}
|
|
24091
|
+
function reportFromRecords(flowId, criteria) {
|
|
24092
|
+
const counts = emptyCounts();
|
|
24093
|
+
for (const record of Object.values(criteria))
|
|
24094
|
+
counts[record.kind] += 1;
|
|
24095
|
+
return {
|
|
24096
|
+
flowId,
|
|
24097
|
+
criteria,
|
|
24098
|
+
counts,
|
|
24099
|
+
runnable: counts.exec + counts.invariant,
|
|
24100
|
+
total: Object.keys(criteria).length
|
|
24101
|
+
};
|
|
24102
|
+
}
|
|
24103
|
+
function renderAcKindDistribution(report) {
|
|
24104
|
+
const { counts } = report;
|
|
24105
|
+
const percent = report.total === 0 ? 0 : Math.round(report.runnable / report.total * 100);
|
|
24106
|
+
const tail = [`${counts.none} accepted as unverifiable`, `${counts.judged} judged`, ...counts.unclassified > 0 ? [`${counts.unclassified} unclassified`] : []];
|
|
24107
|
+
return [
|
|
24108
|
+
`acceptance kinds: exec ${counts.exec} invariant ${counts.invariant} judged ${counts.judged} none ${counts.none} unclassified ${counts.unclassified} (${report.total} criteria)`,
|
|
24109
|
+
`coverage: ${report.runnable}/${report.total} runnable (${percent}%) \xB7 ${tail.join(" \xB7 ")}`
|
|
24110
|
+
];
|
|
24111
|
+
}
|
|
24112
|
+
function describeAcKind(record) {
|
|
24113
|
+
switch (record.kind) {
|
|
24114
|
+
case "exec":
|
|
24115
|
+
case "invariant":
|
|
24116
|
+
return `${record.kind} \`${record.check}\``;
|
|
24117
|
+
case "none":
|
|
24118
|
+
return `none \u2014 ${record.reason}`;
|
|
24119
|
+
default:
|
|
24120
|
+
return record.kind;
|
|
24121
|
+
}
|
|
24122
|
+
}
|
|
24123
|
+
function readAcKindRecords(value) {
|
|
24124
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
24125
|
+
return;
|
|
24126
|
+
const out = {};
|
|
24127
|
+
for (const [id, raw] of Object.entries(value)) {
|
|
24128
|
+
if (typeof raw !== "object" || raw === null)
|
|
24129
|
+
return;
|
|
24130
|
+
const record = raw;
|
|
24131
|
+
const kind = record["kind"];
|
|
24132
|
+
if ((kind === "exec" || kind === "invariant") && typeof record["check"] === "string")
|
|
24133
|
+
out[id] = { kind, check: record["check"] };
|
|
24134
|
+
else if (kind === "judged")
|
|
24135
|
+
out[id] = { kind };
|
|
24136
|
+
else if (kind === "none" && typeof record["reason"] === "string")
|
|
24137
|
+
out[id] = { kind, reason: record["reason"] };
|
|
24138
|
+
else if (kind === "unclassified")
|
|
24139
|
+
out[id] = { kind };
|
|
24140
|
+
else
|
|
24141
|
+
return;
|
|
24142
|
+
}
|
|
24143
|
+
return out;
|
|
24144
|
+
}
|
|
23982
24145
|
// src/flow/check-ac.ts
|
|
23983
24146
|
import { createHash as createHash8 } from "crypto";
|
|
23984
24147
|
import { chmod as chmod4, mkdir as mkdir9, readFile as readFile21 } from "fs/promises";
|
|
@@ -23992,11 +24155,11 @@ function estimateTokens(text) {
|
|
|
23992
24155
|
// src/flow/check-ac.ts
|
|
23993
24156
|
init_service2();
|
|
23994
24157
|
init_fs();
|
|
23995
|
-
var
|
|
24158
|
+
var AC_LINE2 = /^\s*[-*]\s*(AC\d+)\s*:\s?(.*)$/i;
|
|
23996
24159
|
function parseAcceptanceCriteria(content) {
|
|
23997
24160
|
const out = [];
|
|
23998
24161
|
for (const line of content.split(/\r?\n/)) {
|
|
23999
|
-
const match =
|
|
24162
|
+
const match = AC_LINE2.exec(line);
|
|
24000
24163
|
if (match?.[1]) {
|
|
24001
24164
|
out.push({ id: match[1].toUpperCase(), text: (match[2] ?? "").trim() });
|
|
24002
24165
|
}
|
|
@@ -24043,6 +24206,9 @@ function classifyNotCheckable(criterionText) {
|
|
|
24043
24206
|
reason: `not-checkable: every clause of this criterion names a condition no diff records (${labels.join(", ")}) \u2014 assigned without asking Jev.`
|
|
24044
24207
|
};
|
|
24045
24208
|
}
|
|
24209
|
+
function classifyAcCriterionNotCheckable(criterion) {
|
|
24210
|
+
return classifyNotCheckable(stripVerifyMarker(criterion.text));
|
|
24211
|
+
}
|
|
24046
24212
|
function extractCriterionTokens(text) {
|
|
24047
24213
|
const tokens = new Set;
|
|
24048
24214
|
for (const match of text.matchAll(/`([^`]+)`/g)) {
|
|
@@ -24060,7 +24226,7 @@ function extractCriterionTokens(text) {
|
|
|
24060
24226
|
return [...tokens];
|
|
24061
24227
|
}
|
|
24062
24228
|
function computeAcFacts(criterion, diffText, changedFiles) {
|
|
24063
|
-
const tokens = extractCriterionTokens(criterion.text);
|
|
24229
|
+
const tokens = extractCriterionTokens(stripVerifyMarker(criterion.text));
|
|
24064
24230
|
const tokensPresent = tokens.filter((token) => diffText.includes(token) || changedFiles.some((file) => file.includes(token)));
|
|
24065
24231
|
const tokensAbsent = tokens.filter((token) => !tokensPresent.includes(token));
|
|
24066
24232
|
const testFiles = changedFiles.filter((file) => /\.(test|spec)\.[a-z]+$/i.test(file));
|
|
@@ -24086,7 +24252,7 @@ var AC_CHECK_TOKEN_BUDGET = 64000;
|
|
|
24086
24252
|
function questionFor(criterion) {
|
|
24087
24253
|
return {
|
|
24088
24254
|
type: "noul",
|
|
24089
|
-
instructions: `Given the deterministic facts and the redacted diff evidence above, does this change LIKELY SATISFY this frozen ` + `acceptance criterion? Criterion ${criterion.id}: ${redactSensitiveText(criterion.text)}`
|
|
24255
|
+
instructions: `Given the deterministic facts and the redacted diff evidence above, does this change LIKELY SATISFY this frozen ` + `acceptance criterion? Criterion ${criterion.id}: ${redactSensitiveText(stripVerifyMarker(criterion.text))}`
|
|
24090
24256
|
};
|
|
24091
24257
|
}
|
|
24092
24258
|
var MAX_MATCHED_HUNKS_PER_ITEM = 8;
|
|
@@ -24431,6 +24597,10 @@ function renderJournal(createdAt) {
|
|
|
24431
24597
|
}
|
|
24432
24598
|
|
|
24433
24599
|
// src/flow/service.ts
|
|
24600
|
+
async function deriveAcKinds(cwd, dir, flowId) {
|
|
24601
|
+
const { report } = buildAcKindReport(flowId, await readFile23(acPath(cwd, dir), "utf8"));
|
|
24602
|
+
return { ...report.criteria };
|
|
24603
|
+
}
|
|
24434
24604
|
function recordAttempt(task, outcome, at, detail) {
|
|
24435
24605
|
const attempts = task.attempts ?? { count: 0, log: [] };
|
|
24436
24606
|
attempts.count += 1;
|
|
@@ -24628,6 +24798,7 @@ function createFlowService(deps) {
|
|
|
24628
24798
|
throw new Error("Cannot freeze: acceptance-criteria.md must contain at least one real `- ACn:` criterion.");
|
|
24629
24799
|
}
|
|
24630
24800
|
flow.acChecksum = await acChecksum(cwd, dir);
|
|
24801
|
+
flow.acKinds = await deriveAcKinds(cwd, dir, flow.id);
|
|
24631
24802
|
flow.status = "ready";
|
|
24632
24803
|
return save(cwd, dir, flow, "frozen", `${criteria.length} criteria; checksum recorded`);
|
|
24633
24804
|
});
|
|
@@ -24772,10 +24943,15 @@ function createFlowService(deps) {
|
|
|
24772
24943
|
detail = `${normalizedCriterion}: "${previousText ?? "(new)"}" -> "${normalizedText}" (${reason.trim()})`;
|
|
24773
24944
|
}
|
|
24774
24945
|
flow.acChecksum = await acChecksum(cwd, dir);
|
|
24946
|
+
flow.acKinds = await deriveAcKinds(cwd, dir, flow.id);
|
|
24775
24947
|
flow.acConfirmed = {};
|
|
24776
24948
|
return save(cwd, dir, flow, "ac-updated", detail);
|
|
24777
24949
|
});
|
|
24778
24950
|
},
|
|
24951
|
+
async acKinds({ cwd, id }) {
|
|
24952
|
+
const { dir, flow } = await load(cwd, id);
|
|
24953
|
+
return buildAcKindReport(flow.id, await readFile23(acPath(cwd, dir), "utf8"));
|
|
24954
|
+
},
|
|
24779
24955
|
async acReseal({ cwd, id, reason }) {
|
|
24780
24956
|
if (!reason?.trim()) {
|
|
24781
24957
|
throw new Error('flow ac reseal requires --reason "<why the checksum is stale>"');
|
|
@@ -24794,6 +24970,7 @@ function createFlowService(deps) {
|
|
|
24794
24970
|
throw new Error(`flow ac reseal: refusing \u2014 ${evidence.reason}. ` + "Reseal only re-seals a stale checksum over an unchanged file; it is not a way to " + 'accept an edit. If the criteria really changed, use `keryx flow ac update <id> --reason "..."`, ' + "which re-seals AND voids the prior confirmations.");
|
|
24795
24971
|
}
|
|
24796
24972
|
flow.acChecksum = current;
|
|
24973
|
+
flow.acKinds = await deriveAcKinds(cwd, dir, flow.id);
|
|
24797
24974
|
return save(cwd, dir, flow, "ac-resealed", reason);
|
|
24798
24975
|
});
|
|
24799
24976
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.29",
|
|
4
4
|
"description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -62,11 +62,26 @@ document changes.
|
|
|
62
62
|
- problem;
|
|
63
63
|
- goal;
|
|
64
64
|
- users;
|
|
65
|
-
- requirements;
|
|
66
|
-
- success criteria;
|
|
65
|
+
- requirements, each carrying a verification field (see Verification Field below);
|
|
66
|
+
- success criteria, split into two lists (see Success Contract below);
|
|
67
67
|
- risks;
|
|
68
68
|
- recommendation.
|
|
69
69
|
|
|
70
|
+
### Success Contract
|
|
71
|
+
|
|
72
|
+
The PRD's success criteria are two lists, never one:
|
|
73
|
+
|
|
74
|
+
- **Release criteria** are checkable when the work ships: a test, a command, a
|
|
75
|
+
file, a count. Each is something a release can be blocked on.
|
|
76
|
+
- **Outcome criteria** are what should be true some time after release: adoption,
|
|
77
|
+
a measured change, a behavior seen in use. Each outcome criterion either names
|
|
78
|
+
the observation that would confirm it (what is looked at, where, and when) or
|
|
79
|
+
declares `not measured — <reason>`.
|
|
80
|
+
|
|
81
|
+
An outcome criterion with neither an observation nor a `not measured — <reason>`
|
|
82
|
+
is not a criterion. A list whose every entry is `not measured` is acceptable: it
|
|
83
|
+
is honest. A PRD with no outcome list at all is not.
|
|
84
|
+
|
|
70
85
|
## Specification Contract
|
|
71
86
|
|
|
72
87
|
`specification.md` must state:
|
|
@@ -79,6 +94,21 @@ document changes.
|
|
|
79
94
|
- integration points;
|
|
80
95
|
- acceptance criteria.
|
|
81
96
|
|
|
97
|
+
### Verification Field
|
|
98
|
+
|
|
99
|
+
Every requirement in the PRD (each `R<n>` under Requirements) carries one verification field, in the
|
|
100
|
+
same vocabulary a flow criterion's trailing marker uses:
|
|
101
|
+
|
|
102
|
+
```markdown
|
|
103
|
+
Verification: exec `<command>` # a command whose exit code decides it
|
|
104
|
+
Verification: invariant `<command>` # a command that must keep passing
|
|
105
|
+
Verification: judged # a person reads the evidence
|
|
106
|
+
Verification: none — <reason> # honestly unverifiable, with the reason
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`exec` and `invariant` name a backticked command; `none` names its reason after
|
|
110
|
+
an em dash. A requirement with no field is a defect, not a `none`.
|
|
111
|
+
|
|
82
112
|
## Verification Contract
|
|
83
113
|
|
|
84
114
|
Before finishing:
|
|
@@ -88,6 +118,11 @@ Before finishing:
|
|
|
88
118
|
- verify README links to all package files;
|
|
89
119
|
- verify schemas are referenced from specification when present;
|
|
90
120
|
- verify status is honest: `draft`, `spec ready`, `implemented`, or `future`;
|
|
121
|
+
- verify every PRD requirement carries a `Verification:` field, and
|
|
122
|
+
fail the package when one does not, naming that requirement;
|
|
123
|
+
- verify the PRD has both a release-criteria list and an outcome-criteria list,
|
|
124
|
+
and fail the package when the outcome list is missing (an outcome list whose
|
|
125
|
+
entries are all `not measured — <reason>` passes);
|
|
91
126
|
- update `docs/requirements/roadmap.md` when the package represents a module or
|
|
92
127
|
standard capability.
|
|
93
128
|
|
|
@@ -109,10 +109,8 @@ Add optional files only when justified:
|
|
|
109
109
|
Write concise Markdown with explicit headings. Required contracts:
|
|
110
110
|
|
|
111
111
|
- README: purpose, status, document index, scope, related modules.
|
|
112
|
-
- PRD: problem, goal, users, requirements, success criteria, risks,
|
|
113
|
-
|
|
114
|
-
- Specification: identity, structure, manifest/config, CLI/skill surface, data
|
|
115
|
-
contracts, integrations, acceptance criteria.
|
|
112
|
+
- PRD: problem, goal, users, requirements (each `R<n>` carries a `Verification:` field: `exec`/`invariant` with a backticked command, `judged`, or `none — <reason>`), success criteria (a release list and an outcome list; an outcome criterion names its observation or declares `not measured — <reason>`), risks, recommendation.
|
|
113
|
+
- Specification: identity, structure, manifest/config, CLI/skill surface, data contracts, integrations, acceptance criteria.
|
|
116
114
|
|
|
117
115
|
When updating existing docs, preserve useful content and bump versions.
|
|
118
116
|
|
|
@@ -125,6 +123,7 @@ Run a local verification pass:
|
|
|
125
123
|
- README links to every package file;
|
|
126
124
|
- schema files are valid JSON;
|
|
127
125
|
- specification references schemas when present;
|
|
126
|
+
- every PRD requirement has a `Verification:` field (a package with a requirement that omits it FAILS, naming that requirement); the PRD has a release list and an outcome list (no outcome list FAILS; an outcome list whose entries are all `not measured — <reason>` passes);
|
|
128
127
|
- roadmap is updated for new module/standard capabilities;
|
|
129
128
|
- no implementation status is overstated.
|
|
130
129
|
|
|
@@ -154,6 +153,7 @@ Do not emit the Final Response until all of these hold:
|
|
|
154
153
|
- Every Markdown file in the package carries a `Version`, and every file you changed has a bumped one.
|
|
155
154
|
- `README.md` links to every file in the package; the specification links to each schema it defines.
|
|
156
155
|
- Every `schemas/*.json` parses as valid JSON.
|
|
156
|
+
- Every specification requirement carries a `Verification:` field, and the PRD carries both a release list and an outcome list; a package missing either was reported as `verification: fail`, naming the requirement.
|
|
157
157
|
- `docs/requirements/roadmap.md` is updated when the package represents a new module or capability, or the report states why it does not.
|
|
158
158
|
- No implementation claim appears that code does not support; anything unbuilt is marked planned or future.
|
|
159
159
|
- `docpack-review` has been run in Phase 5 and reports zero blockers. Remaining warnings appear verbatim in `remaining_gaps`.
|