@pareto4/pp-core 1.12.1 → 1.17.1

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 (196) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +26 -9
  3. package/agents/pp-code-reviewer.md +3 -2
  4. package/agents/pp-debug-session-manager.md +9 -6
  5. package/agents/pp-executor.md +7 -21
  6. package/agents/pp-plan-checker.md +4 -4
  7. package/agents/pp-planner.md +23 -9
  8. package/agents/pp-vault-reviewer.md +274 -0
  9. package/agents/pp-verifier.md +7 -3
  10. package/bin/install.js +10 -3
  11. package/commands/pp-execute-phase.md +1 -3
  12. package/commands/pp-ns-review.md +2 -1
  13. package/commands/pp-plan-phase.md +1 -2
  14. package/commands/pp-vault-review.md +35 -0
  15. package/commands/pp-verify-work.md +1 -2
  16. package/gemini-extension.json +1 -1
  17. package/hooks/dist/pp-check-update.js +0 -0
  18. package/hooks/dist/pp-cursor-session-start.js +1 -1
  19. package/hooks/dist/pp-statusline.js +1 -1
  20. package/hooks/dist/pp-update-banner.js +0 -0
  21. package/hooks/pp-check-update.js +0 -0
  22. package/hooks/pp-cursor-session-start.js +1 -1
  23. package/hooks/pp-statusline.js +1 -1
  24. package/hooks/pp-update-banner.js +0 -0
  25. package/package.json +14 -19
  26. package/pareto-skills/_shared/create-project-board.sh +0 -0
  27. package/pareto-skills/pp-project-setup/SKILL.md +1 -1
  28. package/pp-core/bin/check-latest-version.cjs +0 -0
  29. package/pp-core/bin/detect-build-commands.sh +2 -2
  30. package/pp-core/bin/lib/capability-activation.cjs +83 -9
  31. package/pp-core/bin/lib/capability-registry.cjs +526 -22
  32. package/pp-core/bin/lib/capability-state.cjs +160 -4
  33. package/pp-core/bin/lib/capability-writer.cjs +19 -5
  34. package/pp-core/bin/lib/check-command-router.cjs +3867 -274
  35. package/pp-core/bin/lib/clusters.cjs +4 -0
  36. package/pp-core/bin/lib/command-aliases.cjs +0 -28
  37. package/pp-core/bin/lib/commands.cjs +9 -4
  38. package/pp-core/bin/lib/compose-bindings.cjs +253 -76
  39. package/pp-core/bin/lib/config-loader.cjs +7 -103
  40. package/pp-core/bin/lib/config-schema.cjs +1 -1
  41. package/pp-core/bin/lib/config.cjs +154 -62
  42. package/pp-core/bin/lib/decisions.cjs +104 -27
  43. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  44. package/pp-core/bin/lib/frontmatter.cjs +1 -1
  45. package/pp-core/bin/lib/git-base-branch.cjs +138 -10
  46. package/pp-core/bin/lib/git-provider.cjs +4 -4
  47. package/pp-core/bin/lib/init-command-router.cjs +5 -8
  48. package/pp-core/bin/lib/init-contract.cjs +653 -629
  49. package/pp-core/bin/lib/init-interactive.cjs +77 -21
  50. package/pp-core/bin/lib/init.cjs +24 -51
  51. package/pp-core/bin/lib/install-profiles.cjs +1 -1
  52. package/pp-core/bin/lib/io.cjs +8 -0
  53. package/pp-core/bin/lib/loop-resolver.cjs +169 -47
  54. package/pp-core/bin/lib/org-profile.cjs +544 -53
  55. package/pp-core/bin/lib/package-identity.cjs +3 -3
  56. package/pp-core/bin/lib/phase-command-router.cjs +1 -23
  57. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  58. package/pp-core/bin/lib/phase.cjs +11 -203
  59. package/pp-core/bin/lib/platform-bindings.cjs +40 -19
  60. package/pp-core/bin/lib/practices-review-check.cjs +15 -9
  61. package/pp-core/bin/lib/profile-output.cjs +1 -1
  62. package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
  63. package/pp-core/bin/lib/state.cjs +11 -85
  64. package/pp-core/bin/lib/template.cjs +1 -1
  65. package/pp-core/bin/lib/vault-audit.cjs +2594 -0
  66. package/pp-core/bin/lib/vault-command-router.cjs +2756 -60
  67. package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
  68. package/pp-core/bin/lib/vault-index.cjs +699 -0
  69. package/pp-core/bin/lib/vault-mode.cjs +235 -0
  70. package/pp-core/bin/lib/vault-practices.cjs +1194 -89
  71. package/pp-core/bin/lib/vault-review.cjs +2102 -0
  72. package/pp-core/bin/lib/vault-seal.cjs +1936 -0
  73. package/pp-core/bin/lib/vault-standards.cjs +1505 -0
  74. package/pp-core/bin/lib/vault-trace.cjs +3184 -0
  75. package/pp-core/bin/lib/vault-update.cjs +242 -0
  76. package/pp-core/bin/lib/vault-write-back.cjs +78 -31
  77. package/pp-core/bin/lib/vault.cjs +2713 -634
  78. package/pp-core/bin/lib/verify.cjs +0 -11
  79. package/pp-core/bin/portable-timeout.sh +1 -1
  80. package/pp-core/bin/pp-tools.cjs +184 -37
  81. package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
  82. package/pp-core/bin/shared/config-schema.manifest.json +0 -3
  83. package/pp-core/bin/shared/model-catalog.json +1 -0
  84. package/pp-core/bin/verify-reapply-patches.cjs +0 -0
  85. package/pp-core/org-profile.json +26 -0
  86. package/pp-core/references/autonomous-best-effort.md +8 -7
  87. package/pp-core/references/loop-hook-dispatch.md +0 -14
  88. package/pp-core/references/planner-gap-closure.md +0 -1
  89. package/pp-core/references/planner-tdd.md +0 -1
  90. package/pp-core/references/planner-vault-practices.md +31 -3
  91. package/pp-core/references/planner-vault-standards.md +320 -0
  92. package/pp-core/references/planning-config.md +0 -8
  93. package/pp-core/references/practice-arithmetic-contract.md +121 -0
  94. package/pp-core/references/tdd.md +0 -1
  95. package/pp-core/references/verifier-practice-verdicts.md +263 -0
  96. package/pp-core/templates/README.md +1 -0
  97. package/pp-core/templates/VAULT-REVIEW.md +202 -0
  98. package/pp-core/templates/phase-prompt.md +2 -6
  99. package/pp-core/templates/practices/README.md +137 -11
  100. package/pp-core/templates/project-charter.md +226 -0
  101. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  102. package/pp-core/workflows/add-tests.md +55 -0
  103. package/pp-core/workflows/audit-fix.md +63 -0
  104. package/pp-core/workflows/audit-milestone.md +1 -1
  105. package/pp-core/workflows/autonomous.md +85 -11
  106. package/pp-core/workflows/check-todos.md +13 -2
  107. package/pp-core/workflows/code-review-fix.md +59 -1
  108. package/pp-core/workflows/code-review.md +10 -1
  109. package/pp-core/workflows/complete-milestone.md +1 -1
  110. package/pp-core/workflows/debug.md +56 -2
  111. package/pp-core/workflows/discovery-sprint/prefill.md +21 -0
  112. package/pp-core/workflows/discovery-sprint/prototype.md +46 -48
  113. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  114. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  117. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  118. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  122. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  123. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  124. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  125. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  126. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  127. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  128. package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
  129. package/pp-core/workflows/discovery-sprint.md +7 -3
  130. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  131. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  132. package/pp-core/workflows/discovery-to-build.md +9 -1
  133. package/pp-core/workflows/discuss-phase.md +3 -10
  134. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
  135. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  136. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  137. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
  138. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  139. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  140. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  141. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -18
  142. package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
  143. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
  144. package/pp-core/workflows/execute-phase.md +154 -110
  145. package/pp-core/workflows/execute-plan.md +58 -4
  146. package/pp-core/workflows/fast.md +53 -0
  147. package/pp-core/workflows/help/modes/full.md +1 -0
  148. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  149. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  150. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  151. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  152. package/pp-core/workflows/init.md +232 -272
  153. package/pp-core/workflows/new-project.md +31 -23
  154. package/pp-core/workflows/plan-phase.md +99 -43
  155. package/pp-core/workflows/quick.md +120 -2
  156. package/pp-core/workflows/secure-phase.md +1 -1
  157. package/pp-core/workflows/ship.md +57 -24
  158. package/pp-core/workflows/update.md +3 -3
  159. package/pp-core/workflows/validate-phase.md +60 -1
  160. package/pp-core/workflows/vault-review.md +325 -0
  161. package/pp-core/workflows/verify-work.md +13 -12
  162. package/scripts/changeset/cli.cjs +6 -5
  163. package/scripts/changeset/lint.cjs +0 -0
  164. package/scripts/changeset/new.cjs +33 -50
  165. package/scripts/dev-env.cjs +25 -85
  166. package/scripts/gen-capability-registry.cjs +199 -0
  167. package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
  168. package/scripts/lib/allowlist-ratchet.cjs +1 -103
  169. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  170. package/scripts/lint-docs-required.cjs +0 -0
  171. package/scripts/lint-pr-check-project-dir.cjs +1 -3
  172. package/scripts/lint-test-file-count.allowlist.json +13 -2
  173. package/scripts/practice-arithmetic-contract.allow.json +35 -0
  174. package/scripts/prompt-injection-scan.sh +22 -15
  175. package/scripts/record-vault-mode-golden.cjs +89 -0
  176. package/scripts/release-channel.cjs +0 -0
  177. package/scripts/sync-manifest-versions.cjs +5 -4
  178. package/scripts/sync-rulesets.sh +1 -1
  179. package/scripts/update-size-baseline.cjs +30 -274
  180. package/scripts/vault-calibrate.cjs +996 -0
  181. package/scripts/verify-opengsd-sweep.cjs +3 -5
  182. package/scripts/workflow-size.cjs +9 -61
  183. package/pp-core/bin/lib/dispatch-plan.cjs +0 -472
  184. package/pp-core/bin/lib/install-run.cjs +0 -536
  185. package/pp-core/bin/lib/phase-tier.cjs +0 -467
  186. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  187. package/pp-core/references/planner-plan-graph.md +0 -66
  188. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  189. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  190. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  191. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -72
  192. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -34
  193. package/scripts/lib/size-budget.cjs +0 -311
  194. package/scripts/lint-neutrality-names.cjs +0 -694
  195. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  196. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -0,0 +1,412 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Inject the canonical practice-verdict arithmetic contract into every
4
+ * instruction surface that states it.
5
+ *
6
+ * WHY THIS EXISTS. Phase 6 verification took seven passes and found eight gaps.
7
+ * Every one had the same root cause: one contract restated in English on five
8
+ * surfaces, with nothing keeping the copies true. Two of the gaps were defects
9
+ * introduced while repairing another.
10
+ *
11
+ * WHY GENERATION AND NOT AN `@`-INCLUDE — checked, not assumed, because
12
+ * ignoring the delivering surface is exactly what produced three of those
13
+ * misses. `agents/pp-vault-reviewer.md` and
14
+ * `pp-core/references/verifier-practice-verdicts.md` carry no `@`-includes; the
15
+ * text must be physically present for the agent reading them. And
16
+ * `steps/verifier-practice-payload.md` is interpolated VERBATIM into
17
+ * pp-verifier's prompt (execute-phase.md:1358 -> :1382), so an `@` line there
18
+ * would reach the agent as literal text. Physical copies are required, so this
19
+ * follows the repo's existing answer for required-but-must-not-drift copies:
20
+ * `gen-capability-registry`, `gen-platform-bindings-schema --check`.
21
+ *
22
+ * NOT A TARGET: `pp-core/templates/VAULT-REVIEW.md`. Its prose is a worked
23
+ * sample the agent ADAPTS, not normative text — verified by diffing the
24
+ * template against an artifact a reviewer actually wrote, where the wording
25
+ * legitimately differs. It stays guarded mechanically instead, by the
26
+ * arithmetic sweep in `tests/vault-review.test.cjs`.
27
+ *
28
+ * THE UNIT OF GOVERNANCE IS THE FILE, NOT THE REGION (06-16, gap G10).
29
+ * Verification pass 8 reproduced the hole this originally left: it replaced
30
+ * `verifier-practice-verdicts.md:98-100` — thirty lines ABOVE that file's own
31
+ * generated block, outside every marker — with the contract's exact inverse,
32
+ * and `--check`, `lint:ci` and the suite all stayed green. Marker-region
33
+ * equality says nothing about the rest of the file, and an agent reads a file
34
+ * linearly, so the hand-written copy is the one it meets first.
35
+ *
36
+ * So `--check` also runs a RESIDUE SCAN: for each target, the text outside its
37
+ * region is scanned for the contract's load-bearing needles. The needle list is
38
+ * NOT hardcoded here — it lives in a second marker region in the same single
39
+ * source, so the guard cannot drift from the contract it guards. One file owns
40
+ * both the canonical text and what counts as restating it.
41
+ *
42
+ * THE SCAN GOVERNS TEXT, NOT LINE BREAKS (06-17, W13). It runs the needles
43
+ * twice: once per raw line, and once over joined normalised windows, reporting
44
+ * the second as its own class only where no single line in the window matches.
45
+ * Verification pass 9 measured why: the per-line pass alone was defeated by an
46
+ * ordinary markdown wrap, using nothing but the needles' own phrases. See
47
+ * `scanResidue`.
48
+ *
49
+ * `--write` deliberately does NOT scan. Generation injects; it must never
50
+ * delete a surface's prose on the strength of a regex.
51
+ *
52
+ * Usage: --write (inject) | --check (exit 1 on drift OR residue, for lint:ci)
53
+ *
54
+ * Exit codes use `process.exitCode` + `return`, never `process.exit()`: the
55
+ * repo's `n/no-process-exit` rule forbids the latter, and the lint scripts here
56
+ * already use this form.
57
+ */
58
+ const fs = require('fs');
59
+ const path = require('path');
60
+ const crypto = require('crypto');
61
+
62
+ const ROOT = path.join(__dirname, '..');
63
+ const SOURCE = path.join(ROOT, 'pp-core', 'references', 'practice-arithmetic-contract.md');
64
+ const ALLOWLIST = path.join(ROOT, 'scripts', 'practice-arithmetic-contract.allow.json');
65
+ const S_START = '<!-- gen:practice-arithmetic-contract:source:start -->';
66
+ const S_END = '<!-- gen:practice-arithmetic-contract:source:end -->';
67
+ const N_START = '<!-- gen:practice-arithmetic-contract:needles:start -->';
68
+ const N_END = '<!-- gen:practice-arithmetic-contract:needles:end -->';
69
+ const START = '<!-- gen:practice-arithmetic-contract:start -->';
70
+ const END = '<!-- gen:practice-arithmetic-contract:end -->';
71
+
72
+ /** The four INSTRUCTION surfaces. The template is deliberately absent (see above). */
73
+ const TARGETS = [
74
+ 'pp-core/workflows/vault-review.md',
75
+ 'agents/pp-vault-reviewer.md',
76
+ 'pp-core/references/verifier-practice-verdicts.md',
77
+ 'pp-core/workflows/execute-phase/steps/verifier-practice-payload.md',
78
+ ];
79
+
80
+ function readCanonical() {
81
+ const text = fs.readFileSync(SOURCE, 'utf-8');
82
+ const a = text.indexOf(S_START);
83
+ const b = text.indexOf(S_END);
84
+ if (a === -1 || b === -1 || b < a) {
85
+ throw new Error(`${SOURCE}: source markers missing or inverted — cannot generate`);
86
+ }
87
+ const body = text.slice(a + S_START.length, b).trim();
88
+ if (!body) throw new Error(`${SOURCE}: canonical block is empty — refusing to blank every target`);
89
+ return body;
90
+ }
91
+
92
+ /**
93
+ * The residue needles, read from the SAME source file as the canonical text and
94
+ * with the SAME empty-block refusal. A needle line is `- ` + a backtick-quoted
95
+ * regex source + ` — ` + why it is load-bearing; the description may itself
96
+ * contain backticks, so only the FIRST backtick span is the pattern.
97
+ *
98
+ * A non-blank line that does not parse is a hard error. A silently skipped
99
+ * needle is a guard that has quietly stopped guarding, which is the whole class
100
+ * this file exists to end.
101
+ */
102
+ function readNeedles() {
103
+ const text = fs.readFileSync(SOURCE, 'utf-8');
104
+ const a = text.indexOf(N_START);
105
+ const b = text.indexOf(N_END);
106
+ if (a === -1 || b === -1 || b < a) {
107
+ throw new Error(`${SOURCE}: needle markers missing or inverted — cannot scan for residue`);
108
+ }
109
+ const body = text.slice(a + N_START.length, b).trim();
110
+ if (!body) throw new Error(`${SOURCE}: the needle list is empty — refusing to scan for nothing`);
111
+
112
+ const needles = [];
113
+ for (const raw of body.split(/\r?\n/)) {
114
+ const line = raw.trim();
115
+ if (!line) continue;
116
+ const m = /^-\s+`([^`]+)`\s+—\s+(\S.*)$/.exec(line);
117
+ if (!m) {
118
+ throw new Error(
119
+ `${SOURCE}: unparseable needle line — expected "- \`<regex>\` — <why>", got:\n ${line}`,
120
+ );
121
+ }
122
+ let re;
123
+ try {
124
+ re = new RegExp(m[1], 'i');
125
+ } catch (err) {
126
+ throw new Error(`${SOURCE}: needle \`${m[1]}\` is not a valid regex — ${err.message}`);
127
+ }
128
+ needles.push({ source: m[1], re, why: m[2] });
129
+ }
130
+ if (!needles.length) throw new Error(`${SOURCE}: the needle list parsed to zero needles`);
131
+ return needles;
132
+ }
133
+
134
+ const sha256 = (s) => crypto.createHash('sha256').update(s, 'utf-8').digest('hex');
135
+
136
+ /**
137
+ * Allowlist entries are keyed by target path and hash the EXACT allowed line.
138
+ * Content-hashing is `06-02`'s mechanism for the protected `pp-vault-audit`
139
+ * sites: editing an allowed line breaks its entry, so an allowance can never
140
+ * quietly widen to cover text nobody justified. A `reason` is mandatory.
141
+ *
142
+ * An ABSENT allowlist means nothing is allowed — the fail-closed direction.
143
+ */
144
+ function readAllowlist() {
145
+ if (!fs.existsSync(ALLOWLIST)) return {};
146
+ const raw = JSON.parse(fs.readFileSync(ALLOWLIST, 'utf-8'));
147
+ const out = {};
148
+ for (const [file, entries] of Object.entries(raw)) {
149
+ if (file.startsWith('//')) continue; // a comment key, never a path
150
+ if (!TARGETS.includes(file)) {
151
+ throw new Error(
152
+ `${ALLOWLIST}: "${file}" is not one of the generated targets. ` +
153
+ 'A stale key means the allowlist outlived the file it excused — re-justify or delete it.',
154
+ );
155
+ }
156
+ if (!Array.isArray(entries)) throw new Error(`${ALLOWLIST}: "${file}" must map to an array`);
157
+ for (const e of entries) {
158
+ if (!/^[0-9a-f]{64}$/.test(String(e && e.sha256))) {
159
+ throw new Error(`${ALLOWLIST}: "${file}" has an entry without a 64-hex sha256`);
160
+ }
161
+ if (!String((e && e.reason) || '').trim()) {
162
+ throw new Error(
163
+ `${ALLOWLIST}: "${file}" entry ${e.sha256.slice(0, 12)} has no reason. ` +
164
+ 'An unjustified allowance is a rubber stamp; state why the line is not a restatement.',
165
+ );
166
+ }
167
+ }
168
+ out[file] = entries;
169
+ }
170
+ return out;
171
+ }
172
+
173
+ /**
174
+ * Adjacent non-blank lines outside the region, joined with a single space and
175
+ * whitespace-collapsed. A markdown paragraph is one window.
176
+ *
177
+ * Joining a whole run rather than sliding a 2-line window is not a wider net:
178
+ * the needles are contiguous phrases, so the only adjacencies a join creates
179
+ * are the ones at line boundaries — exactly the adjacencies the wrap destroyed.
180
+ * A run simply also covers a phrase a reflow spread over three lines.
181
+ */
182
+ function normalisedWindows(lines, inRegion) {
183
+ const windows = [];
184
+ let run = [];
185
+ const flush = () => {
186
+ // A one-line run says nothing the per-line pass did not already say.
187
+ if (run.length > 1) windows.push(run);
188
+ run = [];
189
+ };
190
+ lines.forEach((line, i) => {
191
+ if (inRegion(i) || !line.trim()) {
192
+ flush();
193
+ return;
194
+ }
195
+ run.push({ line, i });
196
+ });
197
+ flush();
198
+ return windows;
199
+ }
200
+
201
+ /**
202
+ * Scan one target's text OUTSIDE its generated region. Returns findings and the
203
+ * set of allowlist hashes this file actually used, so an entry that matches no
204
+ * line can be reported as rotted rather than sitting there looking like cover.
205
+ *
206
+ * TWO PASSES, and the second is additive (06-17, W13).
207
+ *
208
+ * The first tests each raw LINE, which is what shipped and what the three
209
+ * allowlist entries hash. Verification pass 9 measured its ceiling: an ordinary
210
+ * markdown wrap defeats every multi-word needle, including the needles' OWN
211
+ * literal phrases — `two count` ending a line and `populations` opening the
212
+ * next scanned clean, while the same words on one line failed naming all four.
213
+ * The guard governed line breaks where it must govern text. Pass 9 probed all
214
+ * four targets and found 0 live instances, so this was latent coverage rather
215
+ * than residue at HEAD; the trigger, though, is an accidental prose reflow and
216
+ * the direction of failure is silent.
217
+ *
218
+ * So the second pass runs the same needles over joined normalised WINDOWS, and
219
+ * reports a hit only when the needle matches the window and matches NO single
220
+ * line within it. That makes a wrap-only match its own named class instead of
221
+ * folding it into the per-line one, and it leaves per-line semantics — and
222
+ * therefore the existing allowlist hashes — exactly as they were.
223
+ *
224
+ * A wrap-only finding is allowlistable like any other, but its hash is of the
225
+ * JOINED NORMALISED WINDOW, not of a raw line, and `--check` prints that exact
226
+ * text beside the hash so nobody has to reconstruct it. That digest is stable
227
+ * under a reflow of the same words, which is the right behaviour: what somebody
228
+ * justified is the sentence, not where its line breaks happened to fall.
229
+ */
230
+ function scanResidue(file, needles, allowed) {
231
+ const abs = path.join(ROOT, file);
232
+ // Split on /\r?\n/, not '\n': the allowlist hashes the EXACT line, so a
233
+ // CRLF checkout must not produce a different digest for the same text.
234
+ const lines = fs.readFileSync(abs, 'utf-8').split(/\r?\n/);
235
+ const a = lines.findIndex((l) => l.includes(START));
236
+ const b = lines.findIndex((l) => l.includes(END));
237
+ const inRegion = (i) => a !== -1 && b >= a && i >= a && i <= b;
238
+
239
+ const allowedHashes = new Map((allowed[file] || []).map((e) => [e.sha256, e.reason]));
240
+ const used = new Set();
241
+ const findings = [];
242
+ const wrapped = [];
243
+
244
+ lines.forEach((line, i) => {
245
+ if (inRegion(i)) return;
246
+ const hits = needles.filter((n) => n.re.test(line));
247
+ if (!hits.length) return;
248
+ const h = sha256(line);
249
+ if (allowedHashes.has(h)) {
250
+ used.add(h);
251
+ return;
252
+ }
253
+ findings.push({ file, line: i + 1, text: line, hits: hits.map((n) => n.source), sha256: h });
254
+ });
255
+
256
+ for (const run of normalisedWindows(lines, inRegion)) {
257
+ const text = run
258
+ .map((r) => r.line.trim())
259
+ .join(' ')
260
+ .replace(/\s+/g, ' ');
261
+ // `n.re.test(r.line)` against the RAW line, exactly as the first pass tests
262
+ // it, so "wrap-only" means precisely "the first pass could not have seen
263
+ // this" — including when the first pass saw it and the allowlist excused it.
264
+ const hits = needles.filter((n) => n.re.test(text) && !run.some((r) => n.re.test(r.line)));
265
+ if (!hits.length) continue;
266
+ const h = sha256(text);
267
+ if (allowedHashes.has(h)) {
268
+ used.add(h);
269
+ continue;
270
+ }
271
+ wrapped.push({
272
+ file,
273
+ line: run[0].i + 1,
274
+ endLine: run[run.length - 1].i + 1,
275
+ text,
276
+ hits: hits.map((n) => n.source),
277
+ sha256: h,
278
+ });
279
+ }
280
+
281
+ const unused = [...allowedHashes.keys()].filter((h) => !used.has(h));
282
+ return { findings, wrapped, unused };
283
+ }
284
+
285
+ function apply(file, canonical, { write }) {
286
+ const abs = path.join(ROOT, file);
287
+ const text = fs.readFileSync(abs, 'utf-8');
288
+ const a = text.indexOf(START);
289
+ const b = text.indexOf(END);
290
+ if (a === -1 || b === -1) {
291
+ return { file, status: 'missing-markers' };
292
+ }
293
+ if (b < a) return { file, status: 'inverted-markers' };
294
+
295
+ const desired = `${START}\n${canonical}\n${END}`;
296
+ const current = text.slice(a, b + END.length);
297
+ if (current === desired) return { file, status: 'unchanged' };
298
+ if (write) {
299
+ fs.writeFileSync(abs, text.slice(0, a) + desired + text.slice(b + END.length), 'utf-8');
300
+ return { file, status: 'transformed' };
301
+ }
302
+ return { file, status: 'drifted' };
303
+ }
304
+
305
+ function main() {
306
+ const write = process.argv.includes('--write');
307
+ const check = process.argv.includes('--check');
308
+ if (write === check) {
309
+ console.error('usage: gen-practice-arithmetic-contract.cjs (--write | --check)');
310
+ process.exitCode = 2;
311
+ return;
312
+ }
313
+
314
+ const canonical = readCanonical();
315
+ const results = TARGETS.map((f) => apply(f, canonical, { write }));
316
+
317
+ const bad = results.filter((r) => r.status !== 'unchanged' && r.status !== 'transformed');
318
+ for (const r of bad) console.error(` ${r.status}: ${r.file}`);
319
+
320
+ if (check) {
321
+ if (bad.length) {
322
+ console.error(
323
+ `\nFAIL: ${bad.length} surface(s) drifted from ${path.relative(ROOT, SOURCE)}.\n` +
324
+ 'Edit that file, then run: npm run gen:practice-arithmetic-contract',
325
+ );
326
+ process.exitCode = 1;
327
+ return;
328
+ }
329
+
330
+ // The residue scan. Region equality is necessary and not sufficient: a
331
+ // surface can restate the contract by hand ANYWHERE ELSE in the same file,
332
+ // where the region comparison never looks. That is G10.
333
+ const needles = readNeedles();
334
+ const allowed = readAllowlist();
335
+ const findings = [];
336
+ const wrapped = [];
337
+ const rotted = [];
338
+ for (const file of TARGETS) {
339
+ const r = scanResidue(file, needles, allowed);
340
+ findings.push(...r.findings);
341
+ wrapped.push(...r.wrapped);
342
+ for (const h of r.unused) rotted.push({ file, sha256: h });
343
+ }
344
+
345
+ if (findings.length || wrapped.length) {
346
+ console.error(
347
+ `\nFAIL: ${findings.length + wrapped.length} hand-written restatement(s) of the contract, OUTSIDE the generated region:\n`,
348
+ );
349
+ for (const f of findings) {
350
+ console.error(` residue: ${f.file}:${f.line} [${f.hits.join(', ')}]`);
351
+ console.error(` ${f.text.trim()}`);
352
+ }
353
+ // Named as its own class: the phrase is only visible once the lines are
354
+ // joined, so the line number is a RANGE and the hash below is of the
355
+ // joined text, not of any line you can see in the file (06-17, W13).
356
+ for (const f of wrapped) {
357
+ console.error(` wrapped residue: ${f.file}:${f.line}-${f.endLine} [${f.hits.join(', ')}]`);
358
+ console.error(` ${f.text}`);
359
+ }
360
+ console.error(
361
+ '\nREMEDY: replace the sentence with a POINTER to the generated block in the same file\n' +
362
+ '("§7 states the rule; it is not restated here"), keeping only what the canonical block\n' +
363
+ 'does not say — a per-tier destination, for instance. The facts stay physically present\n' +
364
+ 'in the file, in the generated block the agent also reads.\n' +
365
+ `If the line genuinely restates nothing, allowlist it in ${path.relative(ROOT, ALLOWLIST)}\n` +
366
+ 'with its sha256 and a stated reason:\n',
367
+ );
368
+ for (const f of findings) console.error(` "${f.file}": [{ "sha256": "${f.sha256}", "reason": "…" }]`);
369
+ if (wrapped.length) {
370
+ console.error(
371
+ '\nA WRAPPED finding is hashed over the JOINED, whitespace-collapsed text printed above —\n' +
372
+ 'not over any single line of the file. The digest therefore survives a reflow of the same\n' +
373
+ 'words, because what an allowance justifies is the sentence, not its line breaks:\n',
374
+ );
375
+ for (const f of wrapped) console.error(` "${f.file}": [{ "sha256": "${f.sha256}", "reason": "…" }]`);
376
+ }
377
+ process.exitCode = 1;
378
+ return;
379
+ }
380
+
381
+ if (rotted.length) {
382
+ console.error(
383
+ `\nFAIL: ${rotted.length} allowlist entr(ies) match no line or window in their file:\n`,
384
+ );
385
+ for (const r of rotted) console.error(` rotted: ${r.file} ${r.sha256}`);
386
+ console.error(
387
+ '\nThe allowed line was edited or removed. Re-justify the new line or delete the entry —\n' +
388
+ 'an allowance nobody can point at is exactly the rubber stamp content-hashing prevents.',
389
+ );
390
+ process.exitCode = 1;
391
+ return;
392
+ }
393
+
394
+ const allowCount = Object.values(allowed).reduce((n, e) => n + e.length, 0);
395
+ console.log(
396
+ `ok practice-arithmetic-contract: ${results.length} surface(s) match the source; ` +
397
+ `no residue outside the regions, per line or across a wrap (${needles.length} needles, ` +
398
+ `${allowCount} allowlisted).`,
399
+ );
400
+ return;
401
+ }
402
+
403
+ const t = results.filter((r) => r.status === 'transformed').length;
404
+ const u = results.filter((r) => r.status === 'unchanged').length;
405
+ if (bad.length) {
406
+ process.exitCode = 1;
407
+ return;
408
+ }
409
+ console.log(`Done. ${t} transformed, ${u} unchanged.`);
410
+ }
411
+
412
+ main();
@@ -233,106 +233,4 @@ function assertFileBaseline({ label, current, baseline, fail, updateHint }) {
233
233
  return { grown, shrunk, added, removed };
234
234
  }
235
235
 
236
- /**
237
- * Assert that each artifact's measured size stays within a per-change DELTA
238
- * BUDGET of its committed baseline (workflow size budget policy, 2026-10-11).
239
- *
240
- * Unlike `assertFileBaseline` (exact-byte pins), small growth is absorbed by a
241
- * slack so routine edits need no baseline commit, while large or cumulative
242
- * growth still fails by name:
243
- *
244
- * - A file may exceed its baseline by at most `perFileSlack` bytes.
245
- * - The SUM of all positive overages may not exceed `totalSlack` bytes, so many
246
- * small growths cannot add up to a large one unseen.
247
- * - Shrinking always passes and never needs a baseline commit.
248
- * - A file with no baseline entry (ADDED) or a baseline entry with no file
249
- * (REMOVED) fails: the baseline must name exactly the files on disk.
250
- * Recording either needs no reason.
251
- * - An EMPTY baseline fails: with no entries every file is "added", but an
252
- * explicit message makes the vacuous-pass case impossible to misread.
253
- *
254
- * Overage is measured against the committed baseline, not the previous commit,
255
- * so the slack is a budget that a re-baseline resets — not a per-commit
256
- * allowance that can be spent again on every commit.
257
- *
258
- * @param {object} opts
259
- * @param {string} opts.label - Guard name (used in messages).
260
- * @param {Object<string, number>} opts.current - Measured sizes by name.
261
- * @param {Object<string, number>} opts.baseline - Committed sizes by name
262
- * (numbers only; strip metadata keys first).
263
- * @param {number} opts.perFileSlack - Max bytes one file may exceed its baseline.
264
- * @param {number} opts.totalSlack - Max sum of all positive overages.
265
- * @param {function(string): void} opts.fail - Invoked once per violation category.
266
- * @param {string} [opts.updateHint] - Remediation hint appended to growth failures.
267
- * @returns {{ overages: Array<{name:string,from:number,to:number,delta:number}>,
268
- * totalOverage: number,
269
- * shrunk: Array<{name:string,from:number,to:number,delta:number}>,
270
- * added: string[], removed: string[] }}
271
- */
272
- function assertDeltaBudget({ label, current, baseline, perFileSlack, totalSlack, fail, updateHint }) {
273
- const currentNames = new Set(Object.keys(current));
274
- const baselineNames = new Set(Object.keys(baseline));
275
- const hint = updateHint ? `\n${updateHint}` : '';
276
-
277
- if (baselineNames.size === 0) {
278
- fail(
279
- `[${label}] the committed baseline has no size entries — an empty baseline would make ` +
280
- `every growth check pass vacuously. Regenerate it with \`npm run size:baseline\`.`
281
- );
282
- }
283
-
284
- const added = [...currentNames].filter((n) => !baselineNames.has(n)).sort();
285
- const removed = [...baselineNames].filter((n) => !currentNames.has(n)).sort();
286
-
287
- const overages = [];
288
- const shrunk = [];
289
- for (const name of [...currentNames].filter((n) => baselineNames.has(n)).sort()) {
290
- const from = baseline[name];
291
- const to = current[name];
292
- if (!Number.isInteger(from) || from < 0) {
293
- fail(`[${label}] baseline entry ${name} is not a non-negative integer byte size: ${JSON.stringify(from)}`);
294
- continue;
295
- }
296
- if (to > from) overages.push({ name, from, to, delta: to - from });
297
- else if (to < from) shrunk.push({ name, from, to, delta: from - to });
298
- }
299
- const totalOverage = overages.reduce((sum, o) => sum + o.delta, 0);
300
- const fmt = (o) => ` - ${o.name}: ${o.from} → ${o.to} (+${o.delta})`;
301
-
302
- const overFile = overages.filter((o) => o.delta > perFileSlack);
303
- if (overFile.length > 0) {
304
- fail(
305
- `[${label}] ${overFile.length} file(s) grew more than the per-file slack of ${perFileSlack} bytes ` +
306
- `past the committed baseline. Shrink them (extract content lazily), or re-baseline with a ` +
307
- `reason: npm run size:baseline -- --reason "<why this growth is needed>"\n` +
308
- `${overFile.map(fmt).join('\n')}${hint}`
309
- );
310
- }
311
-
312
- if (totalOverage > totalSlack) {
313
- fail(
314
- `[${label}] total growth past the committed baseline is ${totalOverage} bytes, over the total ` +
315
- `slack of ${totalSlack} bytes (each file may be under its own slack — the sum is what failed). ` +
316
- `Shrink, or re-baseline with a reason: npm run size:baseline -- --reason "<why>"\n` +
317
- `${overages.map(fmt).join('\n')}${hint}`
318
- );
319
- }
320
-
321
- if (added.length > 0) {
322
- fail(
323
- `[${label}] ${added.length} file(s) are not in the baseline — run \`npm run size:baseline\` ` +
324
- `to record them (no --reason needed for new files).\n${added.map((n) => ` - ${n}`).join('\n')}`
325
- );
326
- }
327
-
328
- if (removed.length > 0) {
329
- fail(
330
- `[${label}] ${removed.length} baseline entry(ies) no longer exist — run \`npm run size:baseline\` ` +
331
- `to drop them (no --reason needed).\n${removed.map((n) => ` - ${n}`).join('\n')}`
332
- );
333
- }
334
-
335
- return { overages, totalOverage, shrunk, added, removed };
336
- }
337
-
338
- module.exports = { assertWithinAllowlist, assertTightCeiling, assertFileBaseline, assertDeltaBudget };
236
+ module.exports = { assertWithinAllowlist, assertTightCeiling, assertFileBaseline };
@@ -288,7 +288,6 @@
288
288
  "tests/read-guard.test.cjs :: source-text-is-the-product",
289
289
  "tests/reapply-patches.test.cjs :: source-text-is-the-product",
290
290
  "tests/reapply-verify-hunks.test.cjs :: source-text-is-the-product",
291
- "tests/release-coverage-scope.test.cjs :: source-text-is-the-product",
292
291
  "tests/release-tarball-smoke-workflow.test.cjs :: source-text-is-the-product",
293
292
  "tests/release-tarball-smoke.install.test.cjs :: integration-test-input",
294
293
  "tests/research-agent-profiles.test.cjs :: <runtime-contract-is-the-product> research agent .md content is the governed surface",
@@ -321,7 +320,6 @@
321
320
  "tests/windows-test-parity-guard.test.cjs :: structural-regression-guard",
322
321
  "tests/workflow-compat.test.cjs :: source-text-is-the-product",
323
322
  "tests/workflow-guard-registration.test.cjs :: structural-regression-guard",
324
- "tests/workflow-maintainer-skip.test.cjs :: source-text-is-the-product",
325
323
  "tests/workflow-shell-pinning.test.cjs :: file-scope prefilter, not a test assertion — we need to",
326
324
  "tests/workflow-size-budget.test.cjs :: source-text-is-the-product",
327
325
  "tests/workspace.test.cjs :: source-text-is-the-product",
File without changes
@@ -9,9 +9,7 @@ const { runMain } = require('./lib/cli-exit.cjs');
9
9
  const ROOT = path.join(__dirname, '..');
10
10
 
11
11
  const DEFAULT_RELATIVE_FILES = [
12
- '.github/workflows/test.yml',
13
- '.github/workflows/pr-template-format.yml',
14
- '.github/workflows/changeset-required.yml',
12
+ '.github/workflows/ci.yml',
15
13
  'scripts/lint-command-contract.cjs',
16
14
  'scripts/lint-skill-deps.cjs',
17
15
  'scripts/lint-descriptions.cjs',
@@ -114,6 +114,14 @@
114
114
  ],
115
115
  "issue": "180"
116
116
  },
117
+ "vault-review": {
118
+ "files": [
119
+ "vault-review.integration.test.cjs",
120
+ "vault-review.security.test.cjs",
121
+ "vault-review.test.cjs"
122
+ ],
123
+ "issue": "71924 — Vault Assurance phase 06 (06-03/06-04), three distinct concerns for pp-core/bin/lib/vault-review.cjs: vault-review.test (06-03 unit tests over every pure derivation — the verdict vocabularies, both derived-N-A arms, the counts arithmetic, the range resolver and the clean-render refusal predicate), vault-review.integration.test (06-04 SEAM tests — the deterministic validator of the agent-written artifact, plus the `check vault-review` arm driven through the real pp-core/bin/pp-tools.cjs, where the exit-0/no-block-key contract of D-06-01 and VERIFY-03 is proved at the boundary a gate enforcer actually reads, with RED control 2 proving that guard non-vacuous), and vault-review.security.test (06-04 ADVERSARIAL tests — a spoofed external-content fence-end marker, directive-shaped text and a forged tool-call block planted in real practice BODIES, proving 05-REVIEW's open Critical CR-02 is not repeated on this surface, plus the CR-03 class: no absolute filesystem path in any emitted byte). The split mirrors the `vault` cluster's own reasoning: the unit lane, the CLI seam and the security marker are independently load-bearing verification gates, and the `.security.test.cjs` marker is what makes the adversarial lane identifiable to a reader and to scripts/run-tests.cjs. Consolidating would blur three different questions into one file."
124
+ },
117
125
  "verify": {
118
126
  "files": [
119
127
  "bug-2969-verify-reapply-patches.test.cjs",
@@ -179,21 +187,24 @@
179
187
  },
180
188
  "vault": {
181
189
  "files": [
190
+ "feat-71924-vault-required-gate.test.cjs",
191
+ "vault-doors.integration.test.cjs",
182
192
  "vault-practices.property.test.cjs",
183
193
  "vault-read-path.test.cjs",
184
194
  "vault-security.test.cjs",
185
195
  "vault-status-check.test.cjs",
186
196
  "vault.test.cjs"
187
197
  ],
188
- "issue": "v1.7.0 Client Vault Load — four distinct concerns: vault.test (fetch/status/summary unit), vault-security (VSEC-02/03 write-guard + secret/fence), vault-read-path (VGATE-03 SUMMARY pointer), vault-status-check (VGATE-02 gate-query handler). Phase 24-02 added vault-practices.property.test.cjs (fast-check fuzz of the practices glob-fallback compiler + a practices-frontmatter round-trip, PRAC-02) — it is grouped under this bucket rather than \"vault-practices\" because the test-file-count lint's hyphen-prefix matcher treats \"vault-practices.property\" (period before \"property\") as starting with \"vault-\" before it can match the more specific \"vault-practices-\" prefix; the module it actually tests is pp-core/bin/lib/vault-practices.cjs, not vault.cjs."
198
+ "issue": "v1.7.0 Client Vault Load — four distinct concerns: vault.test (fetch/status/summary unit), vault-security (VSEC-02/03 write-guard + secret/fence), vault-read-path (VGATE-03 SUMMARY pointer), vault-status-check (VGATE-02 gate-query handler). Phase 24-02 added vault-practices.property.test.cjs (fast-check fuzz of the practices glob-fallback compiler + a practices-frontmatter round-trip, PRAC-02) — it is grouped under this bucket rather than \"vault-practices\" because the test-file-count lint's hyphen-prefix matcher treats \"vault-practices.property\" (period before \"property\") as starting with \"vault-\" before it can match the more specific \"vault-practices-\" prefix; the module it actually tests is pp-core/bin/lib/vault-practices.cjs, not vault.cjs. Vault Assurance phase 02 (#71924) added feat-71924-vault-required-gate.test.cjs as the phase's ONE consolidated source-grep feature file — it owns the source-text assertions for GATE-02, GATE-03, GATE-06, GATE-08, GATE-09 and GATE-10 together, deliberately in a single file so the phase's six requirements do not each spawn a sibling; later plans in the phase extend it rather than adding files. Vault Assurance phase 16 plan 10 added vault-doors.integration.test.cjs as the phase's ONE door fence-proof owner (DOOR-01..05, D-09): every door's shipped fence (execute-plan, quick, fast, debug, the secondary doors) composed by tests/helpers/gate-fence-composer.cjs and run in bash and zsh, one describe per door plan, so 16-12..16-15 extend it rather than each adding a sibling. The hyphen-prefix matcher attributes it here because no vault-doors module exists; it tests workflow fences, not vault.cjs."
189
199
  },
190
200
  "vault-practices": {
191
201
  "files": [
192
202
  "vault-practices-examples.test.cjs",
193
203
  "vault-practices-matcher.test.cjs",
204
+ "vault-practices.slow.test.cjs",
194
205
  "vault-practices.test.cjs"
195
206
  ],
196
- "issue": "Phase 24 (24-01/24-02/24-04) — three distinct concerns for pp-core/bin/lib/vault-practices.cjs: vault-practices.test (24-01 parser/validation/discovery unit tests), vault-practices-matcher.test (24-02 glob matcher + two-bucket result + report unit tests), vault-practices-examples.test (24-04 shipped-example drift guard + minimal-comments global-bucket smoke case, PRAC-03 Success Criterion 3). All three test the same production module and are each independently load-bearing; consolidating would blur distinct plan-level verification gates."
207
+ "issue": "Phase 24 (24-01/24-02/24-04) — three distinct concerns for pp-core/bin/lib/vault-practices.cjs: vault-practices.test (24-01 parser/validation/discovery unit tests), vault-practices-matcher.test (24-02 glob matcher + two-bucket result + report unit tests), vault-practices-examples.test (24-04 shipped-example drift guard + minimal-comments global-bucket smoke case, PRAC-03 Success Criterion 3). All three test the same production module and are each independently load-bearing; consolidating would blur distinct plan-level verification gates. Vault Assurance phase 05 (#71924) added vault-practices.slow.test.cjs — the >5 s six-axis worst-case matcher corpus for the same production module (pp-core/bin/lib/vault-practices.cjs), split out per the .slow suffix convention so the default suite stays fast; attributed here once the count lint learned to strip the .slow suffix."
197
208
  }
198
209
  }
199
210
  }
@@ -0,0 +1,35 @@
1
+ {
2
+ "//": [
3
+ "Lines that carry a residue needle but restate NO fact the canonical block states.",
4
+ "Read by scripts/gen-practice-arithmetic-contract.cjs --check. Keys starting with // are comments.",
5
+ "sha256 is of the EXACT allowed line. Editing that line breaks its entry and forces re-justification;",
6
+ "an entry matching no line in its file fails as rotted. A reason is mandatory.",
7
+ "A WRAPPED finding — one visible only once adjacent lines are joined — is hashed over the joined,",
8
+ "whitespace-collapsed window text that --check prints beside it, not over any single line of the",
9
+ "file. That digest survives a reflow of the same words, because what an allowance justifies is the",
10
+ "sentence and not where its line breaks fell. All three entries below are per-LINE. 06-17, W13.",
11
+ "Every entry below is the same shape: a payload-KEY INVENTORY, where `derivedCounts` /",
12
+ "`derivedRateLine` are named as things the CLI hands the agent, and the very next clause points at",
13
+ "the generated contract block rather than saying what they mean. Naming a key is not restating a",
14
+ "contract. Keep this file as close to empty as the prose allows.",
15
+ "06-16, gap G10."
16
+ ],
17
+ "pp-core/workflows/vault-review.md": [
18
+ {
19
+ "sha256": "1766709dca03a214fb8fa5496fe6b2aef06fc1b02e67832a6048eeac78e29d6a",
20
+ "reason": "Section 4 is the inventory of what REVIEW_PAYLOAD carries; this line names the two keys and its own continuation says what they describe is stated once in the contract block carried into the spawn prompt below. It states nothing itself."
21
+ }
22
+ ],
23
+ "agents/pp-vault-reviewer.md": [
24
+ {
25
+ "sha256": "1fac53fb5988ab5278f2a262f1564b5dbef830531ec0a82f7200c9b940b45b8d",
26
+ "reason": "The left cell of the supplied-fact table, where the key name IS the row's subject. The right cell is a pointer to the contract block twelve lines below, in the same <derived_facts> element."
27
+ }
28
+ ],
29
+ "pp-core/references/verifier-practice-verdicts.md": [
30
+ {
31
+ "sha256": "52872e6f4b821944e026667c5bd48a3c2319f6876e805e8721865cbf6eb27816",
32
+ "reason": "Section 6's inventory of what the check vault-review arm hands the verifier. The sentence that follows it points at section 7 and says explicitly that the rule is not restated here."
33
+ }
34
+ ]
35
+ }