@mrciphersmith/keryx 0.3.28 → 0.3.30

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/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 AC_LINE = /^\s*[-*]\s*(AC\d+)\s*:\s?(.*)$/i;
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 = AC_LINE.exec(line);
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
  },
@@ -37931,6 +38108,12 @@ var HELP_GROUPS = [
37931
38108
  group: "Automation",
37932
38109
  summary: "Read-only report over already-recorded spend, confirmations, signatures and gate outcomes."
37933
38110
  },
38111
+ {
38112
+ kind: "cli",
38113
+ name: "product",
38114
+ group: "Automation",
38115
+ summary: "The product's intent as a derived index, and the intents closed in code that nobody looked back at."
38116
+ },
37934
38117
  {
37935
38118
  kind: "cli",
37936
38119
  name: "hooks",
@@ -37967,6 +38150,12 @@ var HELP_GROUPS = [
37967
38150
  group: "Automation",
37968
38151
  summary: "Show the last governance report, or run one in the background."
37969
38152
  },
38153
+ {
38154
+ kind: "slash",
38155
+ name: "/product",
38156
+ group: "Automation",
38157
+ summary: "Intents closed in code that nobody looked back at, with their outcome criteria."
38158
+ },
37970
38159
  {
37971
38160
  kind: "cli",
37972
38161
  name: "acp",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
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": {
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc --noEmit",
46
46
  "typecheck:scripts": "tsc --project tsconfig.scripts.json --noEmit",
47
47
  "test": "bun test",
48
- "test:core": "bun test src/cli src/core src/impact-evidence/ src/shell-source-audits.test.ts src/acp/ src/assets/ src/bundle/ src/capability/ src/commands/ src/contracts/ src/ctx/ src/eval/ src/flow/ src/forgetting/ src/gdgraph/ src/gdskills/ src/governance/ src/health/ src/integrations/ src/job/ src/learning/ src/lib/ src/mcp/ src/memory/ src/metrics/ src/retention/ src/review/ src/rules/ src/sac/ src/security/ src/stack/ src/standard/ src/sync/ src/testing/ src/trigger/ src/wiki/ scripts/",
48
+ "test:core": "bun test src/cli src/core src/impact-evidence/ src/shell-source-audits.test.ts src/acp/ src/assets/ src/bundle/ src/capability/ src/commands/ src/contracts/ src/ctx/ src/eval/ src/flow/ src/forgetting/ src/gdgraph/ src/gdskills/ src/governance/ src/product/ src/health/ src/integrations/ src/job/ src/learning/ src/lib/ src/mcp/ src/memory/ src/metrics/ src/retention/ src/review/ src/rules/ src/sac/ src/security/ src/stack/ src/standard/ src/sync/ src/testing/ src/trigger/ src/wiki/ scripts/",
49
49
  "test:client:terminal": "bun test src/tui/ src/commands/shell",
50
50
  "test:client:streaming": "bun test src/harness/provider/",
51
51
  "test:client:cancel-resume": "bun test src/harness/run/ src/harness/resume/ src/harness/session/ src/session/ src/bus/ src/commands/sessions",
@@ -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
- recommendation.
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`.