devflow-kit 3.11.0 → 3.12.0

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.
Files changed (204) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/agents/architect.md +87 -0
  3. package/dist/agents/code.md +21 -25
  4. package/dist/agents/debug.md +21 -49
  5. package/dist/agents/design.md +9 -43
  6. package/dist/agents/fix.md +15 -29
  7. package/dist/agents/git.md +19 -22
  8. package/dist/agents/harden.md +14 -19
  9. package/dist/agents/knowledge.md +11 -19
  10. package/dist/agents/refactor.md +9 -14
  11. package/dist/agents/research.md +11 -34
  12. package/dist/agents/review.md +22 -52
  13. package/dist/agents/skim.md +7 -17
  14. package/dist/agents/triage.md +7 -14
  15. package/dist/commands/bug-analysis.md +31 -62
  16. package/dist/commands/code-review.md +38 -87
  17. package/dist/commands/debug.md +32 -82
  18. package/dist/commands/dynamic-build.md +155 -211
  19. package/dist/commands/dynamic-plan.md +71 -93
  20. package/dist/commands/dynamic-profile.md +7 -155
  21. package/dist/commands/dynamic-tickets.md +65 -186
  22. package/dist/commands/explore.md +27 -70
  23. package/dist/commands/implement.md +98 -186
  24. package/dist/commands/plan.md +60 -107
  25. package/dist/commands/release.md +25 -91
  26. package/dist/commands/research.md +34 -103
  27. package/dist/commands/resolve.md +64 -128
  28. package/dist/commands/self-review.md +35 -99
  29. package/dist/compliance-off/agents/git.md +15 -18
  30. package/dist/compliance-off/agents/triage.md +7 -14
  31. package/dist/compliance-off/commands/bug-analysis.md +31 -62
  32. package/dist/compliance-off/commands/code-review.md +34 -82
  33. package/dist/compliance-off/commands/dynamic-build.md +149 -205
  34. package/dist/compliance-off/commands/dynamic-tickets.md +65 -186
  35. package/dist/compliance-off/commands/implement.md +94 -180
  36. package/dist/compliance-off/commands/plan.md +60 -107
  37. package/dist/compliance-off/commands/release.md +25 -90
  38. package/dist/compliance-off/commands/resolve.md +54 -128
  39. package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
  40. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
  41. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
  42. package/dist/core/plugins.js +3 -3
  43. package/dist/curation-compliance-off/agents/triage.md +7 -13
  44. package/dist/curation-compliance-off/commands/bug-analysis.md +21 -41
  45. package/dist/curation-compliance-off/commands/code-review.md +25 -62
  46. package/dist/curation-compliance-off/commands/dynamic-build.md +142 -194
  47. package/dist/curation-compliance-off/commands/dynamic-tickets.md +54 -173
  48. package/dist/curation-compliance-off/commands/implement.md +86 -161
  49. package/dist/curation-compliance-off/commands/plan.md +51 -86
  50. package/dist/curation-compliance-off/commands/release.md +17 -78
  51. package/dist/curation-compliance-off/commands/resolve.md +45 -108
  52. package/dist/curation-off/agents/architect.md +81 -0
  53. package/dist/curation-off/agents/code.md +20 -23
  54. package/dist/curation-off/agents/debug.md +19 -47
  55. package/dist/curation-off/agents/design.md +8 -41
  56. package/dist/curation-off/agents/fix.md +14 -27
  57. package/dist/curation-off/agents/harden.md +13 -18
  58. package/dist/curation-off/agents/knowledge.md +9 -17
  59. package/dist/curation-off/agents/refactor.md +8 -12
  60. package/dist/curation-off/agents/research.md +9 -32
  61. package/dist/curation-off/agents/review.md +11 -40
  62. package/dist/curation-off/agents/skim.md +7 -17
  63. package/dist/curation-off/agents/triage.md +7 -13
  64. package/dist/curation-off/commands/bug-analysis.md +21 -41
  65. package/dist/curation-off/commands/code-review.md +29 -67
  66. package/dist/curation-off/commands/debug.md +24 -63
  67. package/dist/curation-off/commands/dynamic-build.md +148 -200
  68. package/dist/curation-off/commands/dynamic-plan.md +60 -80
  69. package/dist/curation-off/commands/dynamic-tickets.md +54 -173
  70. package/dist/curation-off/commands/explore.md +18 -50
  71. package/dist/curation-off/commands/implement.md +90 -167
  72. package/dist/curation-off/commands/plan.md +51 -86
  73. package/dist/curation-off/commands/release.md +17 -79
  74. package/dist/curation-off/commands/research.md +23 -81
  75. package/dist/curation-off/commands/resolve.md +55 -108
  76. package/dist/curation-off/commands/self-review.md +26 -78
  77. package/dist/skills/git/references/pr/update-pr-evidence.md +2 -1
  78. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
  79. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
  80. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
  81. package/dist/targets/claude-code/curation-install.js +2 -2
  82. package/dist/variant-axes.json +5 -1
  83. package/package.json +1 -1
  84. package/src/assets/agents/_partials/_code.mds +3 -5
  85. package/src/assets/agents/_partials/_design.mds +86 -0
  86. package/src/assets/agents/architect.mds +41 -0
  87. package/src/assets/agents/code.mds +18 -20
  88. package/src/assets/agents/curate.md +26 -35
  89. package/src/assets/agents/debug.mds +21 -49
  90. package/src/assets/agents/design.mds +11 -114
  91. package/src/assets/agents/evaluate.md +6 -16
  92. package/src/assets/agents/explore.md +1 -2
  93. package/src/assets/agents/fix.mds +12 -24
  94. package/src/assets/agents/git.mds +20 -23
  95. package/src/assets/agents/harden.mds +14 -19
  96. package/src/assets/agents/knowledge.mds +10 -38
  97. package/src/assets/agents/polish.md +13 -39
  98. package/src/assets/agents/refactor.mds +6 -9
  99. package/src/assets/agents/research.mds +11 -34
  100. package/src/assets/agents/review.mds +12 -84
  101. package/src/assets/agents/skim.mds +7 -17
  102. package/src/assets/agents/synthesize.md +3 -21
  103. package/src/assets/agents/test.md +31 -32
  104. package/src/assets/agents/tracker.md +116 -229
  105. package/src/assets/agents/triage.mds +7 -26
  106. package/src/assets/agents/validate.md +1 -3
  107. package/src/assets/commands/_partials/_compliance.mds +2 -2
  108. package/src/assets/commands/_partials/_decisions.mds +17 -24
  109. package/src/assets/commands/_partials/_docs_root.mds +31 -10
  110. package/src/assets/commands/_partials/_engine.mds +34 -56
  111. package/src/assets/commands/_partials/_evidence_policy.mds +1 -1
  112. package/src/assets/commands/_partials/_factory.mds +10 -104
  113. package/src/assets/commands/_partials/_knowledge.mds +47 -51
  114. package/src/assets/commands/_partials/_plan_contract.mds +2 -2
  115. package/src/assets/commands/_partials/_preamble.mds +21 -21
  116. package/src/assets/commands/_partials/_publication.mds +3 -3
  117. package/src/assets/commands/_partials/_roster.mds +12 -13
  118. package/src/assets/commands/_partials/_settings.mds +6 -3
  119. package/src/assets/commands/_partials/_ticket_template.mds +3 -7
  120. package/src/assets/commands/_partials/_tracker.mds +10 -5
  121. package/src/assets/commands/_partials/_wave.mds +23 -32
  122. package/src/assets/commands/bug-analysis.mds +3 -7
  123. package/src/assets/commands/code-review.mds +12 -33
  124. package/src/assets/commands/debug.mds +5 -47
  125. package/src/assets/commands/dynamic-build.mds +48 -70
  126. package/src/assets/commands/dynamic-plan.mds +23 -42
  127. package/src/assets/commands/dynamic-profile.mds +7 -57
  128. package/src/assets/commands/dynamic-tickets.mds +9 -29
  129. package/src/assets/commands/explore.mds +5 -44
  130. package/src/assets/commands/implement.mds +75 -117
  131. package/src/assets/commands/plan.mds +27 -57
  132. package/src/assets/commands/release.mds +4 -62
  133. package/src/assets/commands/research.mds +7 -53
  134. package/src/assets/commands/resolve.mds +40 -87
  135. package/src/assets/commands/self-review.mds +5 -45
  136. package/src/assets/mds/git/_ops_pr.mds +1 -0
  137. package/src/assets/mds/git/_ops_steps.mds +4 -4
  138. package/src/assets/mds/git/_pr.mds +1 -1
  139. package/src/assets/rules/engineering.md +1 -1
  140. package/src/assets/rules/quality.md +2 -2
  141. package/src/assets/scripts/ci-wait.cjs +150 -18
  142. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +3 -3
  143. package/src/assets/scripts/hooks/preamble +2 -3
  144. package/src/assets/scripts/pr-evidence.cjs +1 -0
  145. package/src/assets/scripts/resolve-evidence-policy.cjs +14 -13
  146. package/src/assets/scripts/resolve-root.cjs +186 -0
  147. package/src/assets/scripts/resolve-settings.cjs +14 -9
  148. package/src/assets/scripts/verify-evidence.cjs +355 -22
  149. package/src/assets/skills/accessibility/SKILL.md +28 -95
  150. package/src/assets/skills/agent-references/SKILL.md +2 -4
  151. package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +1 -1
  152. package/src/assets/skills/agent-references/references/commands/implement-levels.md +4 -7
  153. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +1 -1
  154. package/src/assets/skills/agent-references/references/commands/plan-levels.md +2 -2
  155. package/src/assets/skills/apply-decisions/SKILL.md +5 -6
  156. package/src/assets/skills/apply-feature-knowledge/SKILL.md +3 -13
  157. package/src/assets/skills/architecture/SKILL.md +29 -105
  158. package/src/assets/skills/boundary-validation/SKILL.md +27 -78
  159. package/src/assets/skills/complexity/SKILL.md +8 -104
  160. package/src/assets/skills/compliance/SKILL.md +9 -67
  161. package/src/assets/skills/consistency/SKILL.md +7 -107
  162. package/src/assets/skills/database/SKILL.md +7 -72
  163. package/src/assets/skills/dependencies/SKILL.md +9 -109
  164. package/src/assets/skills/dependencies/references/detection.md +10 -0
  165. package/src/assets/skills/dependency-research/SKILL.md +9 -63
  166. package/src/assets/skills/design-review/SKILL.md +1 -35
  167. package/src/assets/skills/docs-framework/SKILL.md +4 -47
  168. package/src/assets/skills/documentation/SKILL.md +7 -71
  169. package/src/assets/skills/documentation/references/patterns.md +1 -1
  170. package/src/assets/skills/documentation/references/violations.md +1 -1
  171. package/src/assets/skills/feature-knowledge/SKILL.md +19 -222
  172. package/src/assets/skills/feature-knowledge/references/new-kb.md +128 -0
  173. package/src/assets/skills/gap-analysis/SKILL.md +1 -31
  174. package/src/assets/skills/git/SKILL.md +2 -22
  175. package/src/assets/skills/go/SKILL.md +24 -113
  176. package/src/assets/skills/java/SKILL.md +12 -98
  177. package/src/assets/skills/patterns/SKILL.md +8 -93
  178. package/src/assets/skills/performance/SKILL.md +29 -88
  179. package/src/assets/skills/python/SKILL.md +22 -120
  180. package/src/assets/skills/qa/SKILL.md +16 -54
  181. package/src/assets/skills/qa/references/browser-testing.md +0 -24
  182. package/src/assets/skills/qa/references/sources.md +2 -2
  183. package/src/assets/skills/quality-gates/SKILL.md +13 -100
  184. package/src/assets/skills/react/SKILL.md +33 -95
  185. package/src/assets/skills/regression/SKILL.md +8 -73
  186. package/src/assets/skills/reliability/SKILL.md +11 -120
  187. package/src/assets/skills/research-codebase/SKILL.md +7 -49
  188. package/src/assets/skills/research-competitor/SKILL.md +4 -39
  189. package/src/assets/skills/research-external/SKILL.md +5 -43
  190. package/src/assets/skills/research-market/SKILL.md +5 -43
  191. package/src/assets/skills/research-technology/SKILL.md +8 -68
  192. package/src/assets/skills/review-methodology/SKILL.md +10 -56
  193. package/src/assets/skills/rust/SKILL.md +19 -109
  194. package/src/assets/skills/security/SKILL.md +39 -66
  195. package/src/assets/skills/software-design/SKILL.md +20 -105
  196. package/src/assets/skills/software-design/references/patterns.md +84 -2
  197. package/src/assets/skills/software-design/references/violations.md +1 -1
  198. package/src/assets/skills/test-driven-development/SKILL.md +32 -107
  199. package/src/assets/skills/test-driven-development/references/rationalization-prevention.md +33 -1
  200. package/src/assets/skills/testing/SKILL.md +35 -53
  201. package/src/assets/skills/typescript/SKILL.md +23 -102
  202. package/src/assets/skills/ui-design/SKILL.md +26 -74
  203. package/src/assets/skills/worktree-support/SKILL.md +4 -10
  204. package/dist/curation-off/commands/dynamic-profile.md +0 -214
@@ -63,7 +63,7 @@
63
63
  // value. The level comes from these two keys only — never from a task, a diff or
64
64
  // project.json.
65
65
  //
66
- // stdout is exactly one line plus "\n", or empty (D-SETTINGS-LINE):
66
+ // stdout is exactly one line plus "\n" (D-SETTINGS-LINE):
67
67
  // TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default>
68
68
  // TRACKER_WARN=<none|mismatch|invalid> SITE=<url|none> KEY=<KEY|none>
69
69
  // REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|id,…>
@@ -79,7 +79,7 @@
79
79
  //
80
80
  // Exit codes (a caller treats EVERY non-zero code as the fail-closed line):
81
81
  // 0 resolved — the line above, including the whole-file rule's line
82
- // 1 usage error — stdout entirely empty, usage on stderr
82
+ // 1 usage error — usage on stderr; prints SETTINGS_FAIL_CLOSED_LINE
83
83
  // 2 input unusable — <dir> missing or not a directory; prints
84
84
  // SETTINGS_FAIL_CLOSED_LINE
85
85
  // 3 never emitted — this script writes no file
@@ -192,10 +192,11 @@ const SETTINGS_LINE_RE = new RegExp(
192
192
  );
193
193
 
194
194
  /**
195
- * The line every refusal prints (exits 2, 4 and 5), what a consumer uses in place
195
+ * The line every refusal prints (exits 1, 2, 4 and 5), what a consumer uses in place
196
196
  * of any line it cannot accept, and the whole-file rule's line on a machine with
197
197
  * no compliance lens. A constant, so the boundary can never fail
198
- * to compose it. Each field is the conservative reading for its consumer:
198
+ * to compose it. The literal fallback of the _settings.mds partial is these bytes.
199
+ * Each field is the conservative reading for its consumer:
199
200
  * TRACKER_WARN=invalid the Git agent degrades rather than guessing a provider
200
201
  * REVIEW_PUBLICATION=off nothing is published (raised to a stub under required)
201
202
  * COMPLIANCE=generic the review lens still runs
@@ -1278,13 +1279,12 @@ function settleOutcome(outcome) {
1278
1279
  if (o === null || typeof o !== 'object' || typeof o.line !== 'string') {
1279
1280
  return { code: EXIT_CODES.INTERNAL_ERROR, line: SETTINGS_FAIL_CLOSED_LINE };
1280
1281
  }
1281
- if (o.code === EXIT_CODES.USAGE) return { code: EXIT_CODES.USAGE, line: '' };
1282
1282
  if (o.code === EXIT_CODES.RESOLVED) {
1283
1283
  return isCoherentSettingsLine(o.line)
1284
1284
  ? { code: o.code, line: o.line }
1285
1285
  : { code: EXIT_CODES.OUTPUT_GATE_REFUSED, line: SETTINGS_FAIL_CLOSED_LINE };
1286
1286
  }
1287
- if (o.code === EXIT_CODES.INPUT_UNUSABLE || o.code === EXIT_CODES.OUTPUT_GATE_REFUSED) {
1287
+ if (o.code === EXIT_CODES.USAGE || o.code === EXIT_CODES.INPUT_UNUSABLE || o.code === EXIT_CODES.OUTPUT_GATE_REFUSED) {
1288
1288
  return { code: o.code, line: SETTINGS_FAIL_CLOSED_LINE };
1289
1289
  }
1290
1290
  return { code: EXIT_CODES.INTERNAL_ERROR, line: SETTINGS_FAIL_CLOSED_LINE };
@@ -1374,7 +1374,7 @@ function main(argv, deps) {
1374
1374
  const args = parseArgs(argv);
1375
1375
  if (args.kind === 'usage') {
1376
1376
  process.stderr.write(args.usage + '\n');
1377
- return { code: EXIT_CODES.USAGE, line: '' };
1377
+ return { code: EXIT_CODES.USAGE, line: SETTINGS_FAIL_CLOSED_LINE };
1378
1378
  }
1379
1379
  if (!isUsableDirectory(args.dir)) {
1380
1380
  process.stderr.write('resolve-settings: not a usable directory: '
@@ -1430,13 +1430,15 @@ if (require.main === module) {
1430
1430
  outcome = { code: EXIT_CODES.INTERNAL_ERROR, line: SETTINGS_FAIL_CLOSED_LINE };
1431
1431
  }
1432
1432
  const settled = settleOutcome(outcome);
1433
- if (settled.line !== '') process.stdout.write(settled.line + '\n');
1433
+ process.stdout.write(settled.line + '\n');
1434
1434
  process.exitCode = settled.code;
1435
1435
  }
1436
1436
 
1437
1437
  // ---------------------------------------------------------------------------
1438
1438
  // Exports — the CLI seam (src/core/evidence-policy.ts), the capture hooks' gate read
1439
- // without jq (hooks/queue-append requires machineSwitchOn) and the unit tests
1439
+ // without jq (hooks/queue-append requires machineSwitchOn), the root resolver
1440
+ // (resolve-root.cjs takes the git half of its answer from gitToplevel, defaultExec and
1441
+ // ledgerRootOf) and the unit tests
1440
1442
  // ---------------------------------------------------------------------------
1441
1443
 
1442
1444
  module.exports = Object.freeze({
@@ -1451,10 +1453,13 @@ module.exports = Object.freeze({
1451
1453
  foldSettings,
1452
1454
  readRepoLayers,
1453
1455
  machineSwitchOn,
1456
+ defaultExec,
1457
+ gitToplevel,
1454
1458
  ledgerRootOf,
1455
1459
  resolveSettings,
1456
1460
  formatSettingsLine,
1457
1461
  isCoherentSettingsLine,
1458
1462
  serializeProjectSuggestion,
1463
+ settleOutcome,
1459
1464
  main,
1460
1465
  });
@@ -19,6 +19,8 @@
19
19
  // node verify-evidence.cjs publish --pr <n> [--publication auto|full|off|stub] [--evidence <file>]
20
20
  // node verify-evidence.cjs readiness --pr <n> [--require-non-author-approval true|false]
21
21
  // [--evidence <file>] [--out <file>]
22
+ // node verify-evidence.cjs claim --evidence <file> --head <sha> [--plan <file>]
23
+ // gate | TP-<n>=<PASS|FAIL|SKIP>[:<exit>] ...
22
24
  //
23
25
  // stdout (D-VERIFY-STDOUT) is empty or exactly one of these shapes, each closed-vocabulary
24
26
  // (the render block's TP lines come from the caller's own plan file):
@@ -33,10 +35,14 @@
33
35
  // posted:<yes|no|n/a> body:<same|changed> (one line; wrapped here)
34
36
  // splice SPLICE ok|resplice (exit 0) or SPLICE conflict|malformed|oversize (exit 5)
35
37
  // readback READBACK ok (exit 0) or READBACK mismatch (exit 5)
36
- // publish the `## PR Evidence` block — the heading, the EVIDENCE line, one
38
+ // publish the `## PR Evidence` block — the heading, the EVIDENCE line, the line
39
+ // `Test plan: <v>/<total> verified (VERIFIED-CI <a>, ATTESTED-LOCAL <b>)`
40
+ // (v is a + b, all three read from the EVIDENCE line), one
37
41
  // `**Body**: … · **Comment**: …` line — then one `TRACEABILITY: DEGRADED
38
42
  // (<reason>)` line per degraded part (exit 0), or exit 2 with one line:
39
- // `TRACEABILITY: DEGRADED (evidence unavailable)` when it could not run,
43
+ // `TRACEABILITY: DEGRADED (evidence unavailable)` when it could not run (a
44
+ // usage error, an internal error, a failed read and a block the output gate
45
+ // refuses all end this way, so a `publish` never ends with empty stdout),
40
46
  // `TRACEABILITY: DEGRADED (compliance off)` when the machine manifest does not
41
47
  // say compliance is enabled, or `TRACEABILITY: DEGRADED (compliance state
42
48
  // unreadable)` when that manifest cannot be read. The last two come before
@@ -55,10 +61,27 @@
55
61
  // is removed before the first read, and a block that does not parse or a
56
62
  // write that fails leaves no out file. The one exception is a path that
57
63
  // git tracks (below), which is left as it is
64
+ // claim D-CLAIM-APPEND: appends claim lines to the `## Claims` section of the
65
+ // `--evidence` file. `gate` writes `- gate:validate PASS sha:<head>
66
+ // by:validate`. Each `TP-<n>=<outcome>[:<exit>]` writes `- TP-<n> <outcome>
67
+ // sha:<head> by:test[ exit:<exit>]`. `--head` is one 40-hex SHA, else
68
+ // nothing is written. The `exit:` part is written only for a number from 0
69
+ // to 255. A TP that the plan does not list gets no line. The plan is the
70
+ // `## Test Plan` of the `--plan` file, or of the evidence file when `--plan`
71
+ // is absent. The `## Claims` section is the file's last section and is
72
+ // created when absent, and so is the file when its folder exists. The
73
+ // write is one append of every line, and the parser of pr-evidence.cjs
74
+ // accepts each line before it. It prints one line, exit 0:
75
+ // `CLAIM appended:<n> PASS:<n> FAIL:<n> SKIP:<n> skipped:<n>`
76
+ // (`skipped` counts the TPs that got no line), or exit 2 with
77
+ // `CLAIM refused (<reason>)`, the reason being `head not one sha`,
78
+ // `evidence file tracked` (git tracks it, or cannot say it does not),
79
+ // `evidence file unusable` or `plan file unusable`. A refusal writes nothing
58
80
  // A git-tracked file counts as absent, like a tracked `.devflow/config.json`
59
81
  // (D-TRACKED-RESULT). `verify`, `publish` and `readiness` ask git about an `--evidence`
60
82
  // file before they read it, and about `--block-out`, `--comment-out`, `--stale-out` and
61
- // `--out` before they write or remove one. Only a clear "not tracked" answer lets the
83
+ // `--out` before they write or remove one. `claim` asks about its `--evidence` and
84
+ // `--plan` files, and refuses both when either is not clearly untracked. Only a clear "not tracked" answer lets the
62
85
  // file be read or written. A tracked `--evidence` file, or one git cannot answer for, is
63
86
  // dropped: the run goes on exactly as if `--evidence` was not given, and the stderr
64
87
  // summary carries the note `evidence-ignored`. A tracked out path, or one git cannot
@@ -68,14 +91,16 @@
68
91
  //
69
92
  // Exit codes:
70
93
  // 0 ok
71
- // 1 usage error — stdout entirely empty, usage on stderr
94
+ // 1 usage error — stdout entirely empty, usage on stderr (a `publish` usage error is
95
+ // exit 2 with the evidence unavailable line instead)
72
96
  // 2 input unusable — a file could not be read (missing, not a regular file,
73
97
  // oversize, not UTF-8), the evidence file or the state directory is
74
98
  // malformed, or the PR body's test-plan block cannot serve as the plan.
75
99
  // For `publish` and `readiness` (D-OP-SCRIPT-POINTER) it is the DEGRADED
76
100
  // result: the operation could not read what it needs, and the stdout above
77
- // says so
78
- // 3 output write failed — no stdout line is printed
101
+ // says so. For `claim` it is the refusal line above
102
+ // 3 output write failed — no stdout line is printed (a `claim` write leaves the
103
+ // evidence file as it was)
79
104
  // 4 remote or internal failure before any verdict — `gh pr view` failed or
80
105
  // answered something unusable, or an unexpected internal error
81
106
  // 5 output gate refused — `check` found the file invalid, the EVIDENCE line
@@ -223,6 +248,12 @@ const DECISIVE_REVIEW_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMIS
223
248
  const SPLICE_LINE_RE = /^SPLICE (?<outcome>ok|resplice|conflict|malformed|oversize)$/;
224
249
  const READBACK_LINE_RE = /^READBACK (?<outcome>ok|mismatch)$/;
225
250
 
251
+ /** D-CLAIM-APPEND: the stdout shapes of `claim`, and the closed set of its refusal reasons. */
252
+ const CLAIM_APPENDED_RE = /^CLAIM appended:(?<n>[0-9]{1,3}) PASS:(?<pass>[0-9]{1,3}) FAIL:(?<fail>[0-9]{1,3}) SKIP:(?<skip>[0-9]{1,3}) skipped:(?<skipped>[0-9]{1,3})$/;
253
+ const CLAIM_REFUSAL_RE = /^CLAIM refused \((?<reason>head not one sha|evidence file tracked|evidence file unusable|plan file unusable)\)$/;
254
+ /** One `claim` argument: the TP, its outcome, and an exit the script keeps only as a number from 0 to 255. */
255
+ const CLAIM_ARG_RE = /^TP-(?<n>[1-9][0-9]{0,2})=(?<outcome>PASS|FAIL|SKIP)(?::(?<exit>[^\s]{1,12}))?$/;
256
+
226
257
  const USAGE = [
227
258
  'Usage: node verify-evidence.cjs check tp|block|exceptions|wave <file>',
228
259
  ' node verify-evidence.cjs check wave --links-only <file>',
@@ -234,6 +265,8 @@ const USAGE = [
234
265
  ' node verify-evidence.cjs publish --pr <n> [--publication auto|full|off|stub] [--evidence <file>]',
235
266
  ' node verify-evidence.cjs readiness --pr <n> [--require-non-author-approval true|false]',
236
267
  ' [--evidence <file>] [--out <file>]',
268
+ ' node verify-evidence.cjs claim --evidence <file> --head <sha> [--plan <file>]',
269
+ ' gate | TP-<n>=<PASS|FAIL|SKIP>[:<exit>] ...',
237
270
  ].join('\n');
238
271
 
239
272
  // ---------------------------------------------------------------------------
@@ -290,7 +323,9 @@ const USAGE = [
290
323
  * | { kind: 'readback', pr: number, expect: string }
291
324
  * | { kind: 'publish', pr: number, publication: string, evidence: string | null }
292
325
  * | { kind: 'readiness', pr: number, requireNonAuthor: 'true' | 'false' | 'unrecognised',
293
- * evidence: string | null, out: string | null }} ParsedArgs
326
+ * evidence: string | null, out: string | null }
327
+ * | { kind: 'claim', evidence: string, head: string, plan: string | null, gate: boolean,
328
+ * tps: readonly { id: number, outcome: 'PASS' | 'FAIL' | 'SKIP', exit: string | null }[] }} ParsedArgs
294
329
  */
295
330
 
296
331
  /** @type {ParsedArgs} */
@@ -423,9 +458,45 @@ function parseArgs(argv) {
423
458
  // An absent flag is the ladder's "unrecognised": it can never read READY.
424
459
  return { kind: 'readiness', pr, requireNonAuthor: needed === null ? 'unrecognised' : needed, evidence, out };
425
460
  }
461
+ if (sub === 'claim') return parseClaimArgs(tail);
426
462
  return USAGE_ARGS;
427
463
  }
428
464
 
465
+ /**
466
+ * D-CLAIM-APPEND: `claim` takes its flags first, then either the one word `gate` or
467
+ * one to CAPS.TPS `TP-<n>=<outcome>[:<exit>]` arguments, each TP at most once. A bad
468
+ * flag or argument is a usage error. `--head` is not checked here: a value that is not
469
+ * one SHA is a refusal with a line, since the caller passes what its report shows.
470
+ *
471
+ * @param {readonly string[]} tail
472
+ * @returns {ParsedArgs}
473
+ */
474
+ function parseClaimArgs(tail) {
475
+ let flagCount = 0;
476
+ while (flagCount < tail.length && tail[flagCount].startsWith('--')) flagCount += 2;
477
+ const f = parseFlags(tail.slice(0, flagCount), { '--evidence': 'value', '--head': 'value', '--plan': 'value' });
478
+ const evidence = f === null ? null : flagValue(f, '--evidence');
479
+ const head = f === null ? null : flagValue(f, '--head');
480
+ const plan = f === null ? null : flagValue(f, '--plan');
481
+ const items = tail.slice(flagCount);
482
+ if (evidence === null || head === null || items.length === 0 || items.length > CAPS.TPS) return USAGE_ARGS;
483
+ if (items.length === 1 && items[0] === 'gate') {
484
+ return plan === null ? { kind: 'claim', evidence, head, plan, gate: true, tps: [] } : USAGE_ARGS;
485
+ }
486
+ const tps = [];
487
+ for (const item of items) {
488
+ const m = CLAIM_ARG_RE.exec(item);
489
+ if (m === null || m.groups === undefined) return USAGE_ARGS;
490
+ tps.push({
491
+ id: Number(m.groups.n),
492
+ outcome: /** @type {'PASS' | 'FAIL' | 'SKIP'} */ (m.groups.outcome),
493
+ exit: m.groups.exit === undefined ? null : m.groups.exit,
494
+ });
495
+ }
496
+ if (new Set(tps.map(tp => tp.id)).size !== tps.length) return USAGE_ARGS;
497
+ return { kind: 'claim', evidence, head, plan, gate: false, tps };
498
+ }
499
+
429
500
  // ---------------------------------------------------------------------------
430
501
  // Bounded file I/O
431
502
  // ---------------------------------------------------------------------------
@@ -516,6 +587,44 @@ function writeTextFile(filePath, text) {
516
587
  }
517
588
  }
518
589
 
590
+ /**
591
+ * Append `text` to `filePath` in one write, creating the file when it is absent. The same
592
+ * refusals as writeTextFile apply: a path that exists and is not a regular file, a link
593
+ * put in its place (O_NOFOLLOW), a FIFO (O_NONBLOCK). A write that does not complete
594
+ * cuts the file back to its length before the append, or removes the file when this call
595
+ * created it, so a failed append leaves no part of `text`. Returns whether it was written.
596
+ *
597
+ * @param {string} filePath
598
+ * @param {string} text
599
+ * @returns {boolean}
600
+ */
601
+ function appendTextFile(filePath, text) {
602
+ let fd;
603
+ let created = false;
604
+ try {
605
+ const st = fs.lstatSync(filePath, { throwIfNoEntry: false });
606
+ if (st !== undefined && !st.isFile()) return false;
607
+ created = st === undefined;
608
+ const c = fs.constants;
609
+ fd = fs.openSync(filePath, c.O_WRONLY | c.O_APPEND | c.O_CREAT | (c.O_NOFOLLOW || 0) | (c.O_NONBLOCK || 0), 0o600);
610
+ const before = fs.fstatSync(fd);
611
+ if (!before.isFile()) return false;
612
+ const bytes = Buffer.from(text, 'utf8');
613
+ try {
614
+ if (fs.writeSync(fd, bytes) === bytes.length) return true;
615
+ } catch (_) { /* cut back below */ }
616
+ try { fs.ftruncateSync(fd, before.size); } catch (_) { /* the write already failed */ }
617
+ if (created) fs.unlinkSync(filePath);
618
+ return false;
619
+ } catch (_) {
620
+ return false;
621
+ } finally {
622
+ if (fd !== undefined) {
623
+ try { fs.closeSync(fd); } catch (_) { /* the write already decided */ }
624
+ }
625
+ }
626
+ }
627
+
519
628
  /**
520
629
  * @param {string} dir
521
630
  * @returns {boolean}
@@ -753,13 +862,18 @@ function trackedState(io, file) {
753
862
  * the write failure (exit 3): no file is written. An `--evidence` file that is not
754
863
  * clearly untracked is dropped, so the run goes on exactly as if `--evidence` was not
755
864
  * given; the stderr summary then carries the closed note `evidence-ignored`. Stdout
756
- * keeps its grammar.
865
+ * keeps its grammar. A `claim` run has its own refusal line for each of these cases
866
+ * (claimRefusalReason).
757
867
  *
758
868
  * @param {Io} io
759
869
  * @param {ParsedArgs} args
760
870
  * @returns {{ args: ParsedArgs } | { outcome: Outcome }}
761
871
  */
762
872
  function admitPaths(io, args) {
873
+ if (args.kind === 'claim') {
874
+ const reason = claimRefusalReason(io, args);
875
+ return reason === null ? { args } : { outcome: claimRefusal(reason) };
876
+ }
763
877
  if (args.kind !== 'verify' && args.kind !== 'publish' && args.kind !== 'readiness') return { args };
764
878
  const outs = args.kind === 'verify' ? [args.blockOut, args.commentOut, args.staleOut]
765
879
  : args.kind === 'readiness' ? [args.out] : [];
@@ -1424,7 +1538,16 @@ function runsAt(io, memo, sha) {
1424
1538
  */
1425
1539
  function loadEvidence(file) {
1426
1540
  const text = readTextFile(file);
1427
- if (text === null) return null;
1541
+ return text === null ? null : parseEvidenceText(text);
1542
+ }
1543
+
1544
+ /**
1545
+ * The sections of an evidence file's text, parsed (see loadEvidence).
1546
+ *
1547
+ * @param {string} text
1548
+ * @returns {EvidenceInput | null} null when unusable
1549
+ */
1550
+ function parseEvidenceText(text) {
1428
1551
  const sections = PE.evidenceSections(text);
1429
1552
  if (!sections.ok) return null;
1430
1553
  let plan = null;
@@ -2026,6 +2149,21 @@ const COMPLIANCE_UNREADABLE_LINE = 'TRACEABILITY: DEGRADED (compliance state unr
2026
2149
  /** Every line a `publish` that did not run may print. The boundary admits each one with exit 2 only. */
2027
2150
  const PUBLISH_REFUSAL_LINES = Object.freeze([EVIDENCE_UNAVAILABLE_LINE, COMPLIANCE_OFF_LINE, COMPLIANCE_UNREADABLE_LINE]);
2028
2151
 
2152
+ /** The answer of every `publish` that could not give its block: the one line, exit 2. */
2153
+ const PUBLISH_UNAVAILABLE = Object.freeze({ code: EXIT_CODES.INPUT_UNUSABLE, stdout: EVIDENCE_UNAVAILABLE_LINE + '\n' });
2154
+
2155
+ /**
2156
+ * Whether argv asks for `publish`. The boundary reads it after any failure, a usage
2157
+ * error and a throw included, so settleOutcome can answer a failed `publish` with
2158
+ * PUBLISH_UNAVAILABLE.
2159
+ *
2160
+ * @param {readonly string[]} argv process.argv
2161
+ * @returns {boolean}
2162
+ */
2163
+ function isPublishRun(argv) {
2164
+ return Array.isArray(argv) && argv[2] === 'publish';
2165
+ }
2166
+
2029
2167
  /**
2030
2168
  * @typedef {{ status: string, reason: string | null }} Part
2031
2169
  * One half of the `publish` answer: a closed status, or `DEGRADED` with its reason.
@@ -2077,8 +2215,26 @@ function traceReasons(body, comment) {
2077
2215
  }
2078
2216
 
2079
2217
  /**
2080
- * The `## PR Evidence` block of the update-pr-evidence contract, then its TRACEABILITY
2081
- * lines.
2218
+ * The test-plan tally that both scripted blocks print: `<verified>/<total>`, the given
2219
+ * word, then the two counts that make up `verified`. The readiness block prints it with
2220
+ * no word and the publish block with ` verified`.
2221
+ *
2222
+ * @param {{ total: number, counts: Readonly<Record<string, number>> }} fields the parsed EVIDENCE line
2223
+ * @param {'' | ' verified'} word
2224
+ * @returns {string}
2225
+ */
2226
+ function planTally(fields, word) {
2227
+ const verifiedCi = fields.counts['VERIFIED-CI'];
2228
+ const attestedLocal = fields.counts['ATTESTED-LOCAL'];
2229
+ return (verifiedCi + attestedLocal) + '/' + fields.total + word
2230
+ + ' (VERIFIED-CI ' + verifiedCi + ', ATTESTED-LOCAL ' + attestedLocal + ')';
2231
+ }
2232
+
2233
+ /**
2234
+ * The `## PR Evidence` block of the update-pr-evidence contract: the EVIDENCE line, the
2235
+ * `Test plan:` line read from it, the Body and Comment line, then its TRACEABILITY lines.
2236
+ * The caller holds the precondition that `evidenceLine` parses; a line that does not is a
2237
+ * broken invariant, which the catch in main turns into "evidence unavailable".
2082
2238
  *
2083
2239
  * @param {string} evidenceLine
2084
2240
  * @param {Part} body
@@ -2086,9 +2242,12 @@ function traceReasons(body, comment) {
2086
2242
  * @returns {string}
2087
2243
  */
2088
2244
  function renderPublishBlock(evidenceLine, body, comment) {
2245
+ const evidence = PE.parseEvidenceLine(evidenceLine);
2246
+ if (!evidence.ok) throw new Error('verify-evidence: unchecked evidence line');
2089
2247
  const lines = [
2090
2248
  '## PR Evidence',
2091
2249
  evidenceLine,
2250
+ 'Test plan: ' + planTally(evidence.value, ' verified'),
2092
2251
  '**Body**: ' + partText(body) + ' · **Comment**: ' + partText(comment),
2093
2252
  ...traceReasons(body, comment).map(r => 'TRACEABILITY: DEGRADED (' + r + ')'),
2094
2253
  ];
@@ -2110,8 +2269,8 @@ function parsePart(text, statuses) {
2110
2269
 
2111
2270
  /**
2112
2271
  * D-OP-BLOCK-GATE: read a `publish` block back into its parts, or null when it is not
2113
- * exactly what renderPublishBlock prints. The boundary prints a block only when this
2114
- * accepts it.
2272
+ * exactly what renderPublishBlock prints, so a `Test plan:` line that disagrees with the
2273
+ * EVIDENCE line is refused. The boundary prints a block only when this accepts it.
2115
2274
  *
2116
2275
  * @param {string} text the whole stdout, its final newline included
2117
2276
  * @returns {{ evidenceLine: string, body: Part, comment: Part } | null}
@@ -2119,9 +2278,9 @@ function parsePart(text, statuses) {
2119
2278
  function parsePublishBlock(text) {
2120
2279
  if (typeof text !== 'string' || !text.endsWith('\n') || text.length > 8192) return null;
2121
2280
  const lines = text.slice(0, -1).split('\n');
2122
- if (lines.length < 3 || lines.length > 5 || lines[0] !== '## PR Evidence') return null;
2281
+ if (lines.length < 4 || lines.length > 6 || lines[0] !== '## PR Evidence') return null;
2123
2282
  if (!PE.parseEvidenceLine(lines[1]).ok) return null;
2124
- const m = PUBLISH_PARTS_RE.exec(lines[2]);
2283
+ const m = PUBLISH_PARTS_RE.exec(lines[3]);
2125
2284
  if (m === null || m.groups === undefined) return null;
2126
2285
  const body = parsePart(m.groups.body, BODY_STATUSES);
2127
2286
  const comment = parsePart(m.groups.comment, COMMENT_STATUSES);
@@ -2138,7 +2297,7 @@ function parsePublishBlock(text) {
2138
2297
  */
2139
2298
  function evidenceUnavailable(io, reason) {
2140
2299
  io.stderr('verify-evidence: publish: ' + reason + '\n');
2141
- return { code: EXIT_CODES.INPUT_UNUSABLE, stdout: EVIDENCE_UNAVAILABLE_LINE + '\n' };
2300
+ return PUBLISH_UNAVAILABLE;
2142
2301
  }
2143
2302
 
2144
2303
  /**
@@ -2826,9 +2985,7 @@ function readinessPass(io, args, deps) {
2826
2985
  : String(threads.value.unresolved),
2827
2986
  decision: decisionText,
2828
2987
  ci: ci.status,
2829
- plan: evidence === null ? 'unavailable'
2830
- : (evidence.verifiedCi + evidence.attestedLocal) + '/' + evidence.total
2831
- + ' (VERIFIED-CI ' + evidence.verifiedCi + ', ATTESTED-LOCAL ' + evidence.attestedLocal + ')',
2988
+ plan: fields === null ? 'unavailable' : planTally(fields, ''),
2832
2989
  approval: approvalText(args.requireNonAuthor, evidence),
2833
2990
  };
2834
2991
  return { outcome: { code: EXIT_CODES.OK, stdout: renderReadinessBlock(block) }, head };
@@ -2922,6 +3079,159 @@ function runReadiness(io, args, deps) {
2922
3079
  return args.out === null ? pass.outcome : writeReadinessOut(io, args.out, args.pr, pass.outcome, pass.head, planSource(args));
2923
3080
  }
2924
3081
 
3082
+ // ---------------------------------------------------------------------------
3083
+ // claim — D-CLAIM-APPEND
3084
+ // ---------------------------------------------------------------------------
3085
+ //
3086
+ // A Test or Validate report becomes claim lines in the evidence file. The caller passes
3087
+ // only the facts of its report (the HEAD, the gate, one outcome per TP). This script owns
3088
+ // the rest: the line shapes (the claim parser of pr-evidence.cjs accepts each one before
3089
+ // it is written), the HEAD rule, the exit range, the `## Claims` section and the append.
3090
+ // A claim is an input to `verify`, never a state: only pr-evidence.cjs and `verify`
3091
+ // assign states.
3092
+
3093
+ /**
3094
+ * @param {string} reason one of the CLAIM_REFUSAL_RE reasons
3095
+ * @returns {Outcome}
3096
+ */
3097
+ function claimRefusal(reason) {
3098
+ return { code: EXIT_CODES.INPUT_UNUSABLE, stdout: 'CLAIM refused (' + reason + ')\n' };
3099
+ }
3100
+
3101
+ /**
3102
+ * The reason a `claim` run must not start, or null. Nothing is read or written before
3103
+ * this: the HEAD is one 40-hex SHA, each path is a path the commands build, the evidence
3104
+ * file sits in a folder that exists, and git says clearly that neither file is tracked
3105
+ * (D-TRACKED-RESULT).
3106
+ *
3107
+ * @param {Io} io
3108
+ * @param {Extract<ParsedArgs, { kind: 'claim' }>} args
3109
+ * @returns {string | null}
3110
+ */
3111
+ function claimRefusalReason(io, args) {
3112
+ if (!SHA40_RE.test(args.head)) return 'head not one sha';
3113
+ if (!EVIDENCE_PATH_RE.test(args.evidence) || !isRealDirectory(path.dirname(path.resolve(args.evidence)))) return 'evidence file unusable';
3114
+ if (args.plan !== null && !EVIDENCE_PATH_RE.test(args.plan)) return 'plan file unusable';
3115
+ if (trackedState(io, args.evidence) !== 'untracked') return 'evidence file tracked';
3116
+ if (args.plan !== null && trackedState(io, args.plan) !== 'untracked') return 'plan file unusable';
3117
+ return null;
3118
+ }
3119
+
3120
+ /**
3121
+ * Whether one line is exactly one claim to the claim parser of pr-evidence.cjs.
3122
+ *
3123
+ * @param {string} line
3124
+ * @returns {boolean}
3125
+ */
3126
+ function claimLineOk(line) {
3127
+ const r = PE.parseClaims(line);
3128
+ if (!r.ok) return false;
3129
+ const gates = r.value.gates;
3130
+ return r.value.malformed === 0 && r.value.tp.size + (gates.validate === null ? 0 : 1) + (gates.qa === null ? 0 : 1) === 1;
3131
+ }
3132
+
3133
+ /**
3134
+ * Assert a line before it is written: one claim to the claim parser. Every caller builds
3135
+ * the line from parts that are already checked; this is that precondition held at the sink.
3136
+ *
3137
+ * @param {string} line
3138
+ * @returns {string}
3139
+ */
3140
+ function claimLine(line) {
3141
+ if (!claimLineOk(line)) throw new Error('verify-evidence: unchecked claim line');
3142
+ return line;
3143
+ }
3144
+
3145
+ /**
3146
+ * The text to append: the lines, behind the `## Claims` heading when the file has no such
3147
+ * section (a blank line first, when the file holds anything). The section is the last of
3148
+ * the file, so the lines go at its end.
3149
+ *
3150
+ * @param {string} text the evidence file as it is, '' when it is absent
3151
+ * @param {boolean} hasSection
3152
+ * @param {readonly string[]} lines
3153
+ * @returns {string}
3154
+ */
3155
+ function claimAppendText(text, hasSection, lines) {
3156
+ const body = lines.join('\n') + '\n';
3157
+ if (text === '') return PE.CLAIMS_HEADING + '\n' + body;
3158
+ const lead = text.endsWith('\n') ? '' : '\n';
3159
+ return hasSection ? lead + body : lead + '\n' + PE.CLAIMS_HEADING + '\n' + body;
3160
+ }
3161
+
3162
+ /**
3163
+ * The TP numbers the plan lists: the `## Test Plan` of the `--plan` file, else of the
3164
+ * evidence file. A plan with no TP line, or no plan section, lists none. Null when the
3165
+ * `--plan` file cannot serve as a plan.
3166
+ *
3167
+ * @param {Extract<ParsedArgs, { kind: 'claim' }>} args
3168
+ * @param {EvidenceInput} evidence
3169
+ * @returns {Set<number> | null}
3170
+ */
3171
+ function claimPlanIds(args, evidence) {
3172
+ let plan = evidence.plan;
3173
+ if (args.plan !== null) {
3174
+ const text = readTextFile(args.plan);
3175
+ const parsed = text === null ? null : PE.parsePlan(sectionOrWhole(text, 'testPlan'));
3176
+ if (parsed === null || (!parsed.ok && parsed.error.code !== 'empty')) return null;
3177
+ plan = parsed.ok ? parsed.value : { tps: [] };
3178
+ }
3179
+ return new Set((plan === null ? [] : plan.tps).map(tp => tp.id));
3180
+ }
3181
+
3182
+ /**
3183
+ * D-CLAIM-APPEND: `claim`. The checks of claimRefusalReason have passed (admitPaths).
3184
+ * The evidence file is read as `verify` reads it, and its `## Claims` section, when it
3185
+ * has one, must be its last section. Then every line is built and checked, and one append
3186
+ * writes them all. A run that writes no line (every TP off the plan) changes no file.
3187
+ *
3188
+ * @param {Io} io
3189
+ * @param {Extract<ParsedArgs, { kind: 'claim' }>} args
3190
+ * @returns {Outcome}
3191
+ */
3192
+ function runClaim(io, args) {
3193
+ const file = path.resolve(args.evidence);
3194
+ /** @type {fs.Stats | undefined} */
3195
+ let st;
3196
+ try {
3197
+ st = fs.lstatSync(file, { throwIfNoEntry: false });
3198
+ } catch (_) {
3199
+ return claimRefusal('evidence file unusable');
3200
+ }
3201
+ if (st !== undefined && !st.isFile()) return claimRefusal('evidence file unusable');
3202
+ const text = st === undefined ? '' : readTextFile(file);
3203
+ const evidence = text === null ? null : parseEvidenceText(text);
3204
+ if (text === null || evidence === null) return claimRefusal('evidence file unusable');
3205
+ const headings = text.split('\n').map(row => row.replace(/\r$/, '')).filter(row => row === '##' || row.startsWith('## '));
3206
+ const hasSection = headings.includes(PE.CLAIMS_HEADING);
3207
+ if (hasSection && headings[headings.length - 1] !== PE.CLAIMS_HEADING) return claimRefusal('evidence file unusable');
3208
+ const listed = args.gate ? new Set() : claimPlanIds(args, evidence);
3209
+ if (listed === null) return claimRefusal('plan file unusable');
3210
+
3211
+ /** @type {string[]} */
3212
+ const lines = [];
3213
+ const tally = { PASS: 0, FAIL: 0, SKIP: 0 };
3214
+ if (args.gate) {
3215
+ lines.push(claimLine('- gate:validate PASS sha:' + args.head + ' by:validate'));
3216
+ tally.PASS++;
3217
+ }
3218
+ for (const tp of args.tps) {
3219
+ if (!listed.has(tp.id)) continue;
3220
+ const bare = '- TP-' + tp.id + ' ' + tp.outcome + ' sha:' + args.head + ' by:test';
3221
+ const withExit = tp.exit === null ? bare : bare + ' exit:' + tp.exit;
3222
+ lines.push(claimLine(claimLineOk(withExit) ? withExit : bare));
3223
+ tally[tp.outcome]++;
3224
+ }
3225
+ if (lines.length > 0 && !appendTextFile(file, claimAppendText(text, hasSection, lines))) {
3226
+ return refuse(io, EXIT_CODES.WRITE_FAILED, 'an output file could not be written');
3227
+ }
3228
+ const skipped = args.gate ? 0 : args.tps.length - lines.length;
3229
+ return {
3230
+ code: EXIT_CODES.OK,
3231
+ stdout: 'CLAIM appended:' + lines.length + ' PASS:' + tally.PASS + ' FAIL:' + tally.FAIL + ' SKIP:' + tally.SKIP + ' skipped:' + skipped + '\n',
3232
+ };
3233
+ }
3234
+
2925
3235
  // ---------------------------------------------------------------------------
2926
3236
  // main — returns {code, stdout}; never calls process.exit
2927
3237
  // ---------------------------------------------------------------------------
@@ -3005,6 +3315,7 @@ function main(argv, deps) {
3005
3315
  else if (args.kind === 'splice') outcome = runSplice(io, args);
3006
3316
  else if (args.kind === 'publish') outcome = runPublish(io, args, d);
3007
3317
  else if (args.kind === 'readiness') outcome = runReadiness(io, args, d);
3318
+ else if (args.kind === 'claim') outcome = runClaim(io, args);
3008
3319
  else outcome = runReadback(io, args);
3009
3320
  }
3010
3321
  } catch (err) {
@@ -3038,12 +3349,26 @@ function errorLabel(err) {
3038
3349
  /**
3039
3350
  * D-VERIFY-STDOUT: settle whatever main() returned into what the boundary may
3040
3351
  * print. The code must be a known exit code and stdout must be empty or one of
3041
- * the closed shapes for that code; anything else prints nothing and exits 4.
3352
+ * the closed shapes for that code; anything else prints nothing and exits 4. A
3353
+ * `publish` run is the exception: it never settles to empty stdout, so what would
3354
+ * print nothing prints the evidence unavailable line with exit 2.
3355
+ *
3356
+ * @param {unknown} outcome
3357
+ * @param {boolean} [publish] true when the run is a `publish`
3358
+ * @returns {Outcome}
3359
+ */
3360
+ function settleOutcome(outcome, publish) {
3361
+ const settled = settleShape(outcome);
3362
+ return publish === true && settled.stdout === '' ? PUBLISH_UNAVAILABLE : settled;
3363
+ }
3364
+
3365
+ /**
3366
+ * The shape check of settleOutcome, for every subcommand.
3042
3367
  *
3043
3368
  * @param {unknown} outcome
3044
3369
  * @returns {Outcome}
3045
3370
  */
3046
- function settleOutcome(outcome) {
3371
+ function settleShape(outcome) {
3047
3372
  const o = /** @type {any} */ (outcome);
3048
3373
  const failed = { code: EXIT_CODES.REMOTE_FAILURE, stdout: '' };
3049
3374
  if (o === null || typeof o !== 'object' || typeof o.stdout !== 'string') return failed;
@@ -3073,6 +3398,14 @@ function settleOutcome(outcome) {
3073
3398
  const passing = readback.groups.outcome === 'ok';
3074
3399
  return o.code === (passing ? EXIT_CODES.OK : EXIT_CODES.OUTPUT_GATE_REFUSED) ? { code: o.code, stdout: o.stdout } : failed;
3075
3400
  }
3401
+ const claimRefused = CLAIM_REFUSAL_RE.exec(text);
3402
+ if (claimRefused !== null) return o.code === EXIT_CODES.INPUT_UNUSABLE ? pass : failed;
3403
+ const claimed = CLAIM_APPENDED_RE.exec(text);
3404
+ if (claimed !== null && claimed.groups !== undefined) {
3405
+ const g = claimed.groups;
3406
+ const sums = Number(g.n) === Number(g.pass) + Number(g.fail) + Number(g.skip);
3407
+ return o.code === EXIT_CODES.OK && sums && Number(g.n) <= CAPS.TPS && Number(g.skipped) <= CAPS.TPS ? pass : failed;
3408
+ }
3076
3409
  if (o.code !== EXIT_CODES.OK) return failed;
3077
3410
  if (PE.parseEvidenceLine(text).ok) return { code: o.code, stdout: o.stdout };
3078
3411
  const block = PE.parseBlock(text);
@@ -3094,7 +3427,7 @@ if (require.main === module) {
3094
3427
  process.stderr.write('verify-evidence: internal error (' + errorLabel(err) + ')\n');
3095
3428
  outcome = { code: EXIT_CODES.REMOTE_FAILURE, stdout: '' };
3096
3429
  }
3097
- const settled = settleOutcome(outcome);
3430
+ const settled = settleOutcome(outcome, isPublishRun(process.argv));
3098
3431
  if (settled.stdout !== '') process.stdout.write(settled.stdout);
3099
3432
  process.exitCode = settled.code;
3100
3433
  }