@lannguyensi/harness 0.44.0 → 0.46.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 (234) hide show
  1. package/CHANGELOG.md +1245 -0
  2. package/dist/cli/adopt/derive.d.ts +16 -7
  3. package/dist/cli/adopt/derive.js +19 -2
  4. package/dist/cli/adopt/derive.js.map +1 -1
  5. package/dist/cli/adopt/index.js +18 -3
  6. package/dist/cli/adopt/index.js.map +1 -1
  7. package/dist/cli/apply/apply.d.ts +1 -0
  8. package/dist/cli/apply/apply.js +62 -6
  9. package/dist/cli/apply/apply.js.map +1 -1
  10. package/dist/cli/apply/generate-codex-config.js +5 -6
  11. package/dist/cli/apply/generate-codex-config.js.map +1 -1
  12. package/dist/cli/apply/generate-opencode-config.d.ts +58 -0
  13. package/dist/cli/apply/generate-opencode-config.js +210 -0
  14. package/dist/cli/apply/generate-opencode-config.js.map +1 -0
  15. package/dist/cli/apply/generate-settings.d.ts +90 -32
  16. package/dist/cli/apply/generate-settings.js +133 -10
  17. package/dist/cli/apply/generate-settings.js.map +1 -1
  18. package/dist/cli/apply/index.d.ts +2 -1
  19. package/dist/cli/apply/index.js +2 -1
  20. package/dist/cli/apply/index.js.map +1 -1
  21. package/dist/cli/apply/next-steps.d.ts +20 -0
  22. package/dist/cli/apply/next-steps.js +60 -0
  23. package/dist/cli/apply/next-steps.js.map +1 -1
  24. package/dist/cli/approve/stdin-report.d.ts +15 -0
  25. package/dist/cli/approve/stdin-report.js +1 -1
  26. package/dist/cli/approve/stdin-report.js.map +1 -1
  27. package/dist/cli/approve/understanding.d.ts +75 -0
  28. package/dist/cli/approve/understanding.js +150 -2
  29. package/dist/cli/approve/understanding.js.map +1 -1
  30. package/dist/cli/audit.d.ts +1 -1
  31. package/dist/cli/audit.js +3 -2
  32. package/dist/cli/audit.js.map +1 -1
  33. package/dist/cli/doctor/claude-mcp.d.ts +19 -1
  34. package/dist/cli/doctor/claude-mcp.js +36 -10
  35. package/dist/cli/doctor/claude-mcp.js.map +1 -1
  36. package/dist/cli/doctor/codex.d.ts +3 -1
  37. package/dist/cli/doctor/codex.js +6 -9
  38. package/dist/cli/doctor/codex.js.map +1 -1
  39. package/dist/cli/doctor/format.js +94 -8
  40. package/dist/cli/doctor/format.js.map +1 -1
  41. package/dist/cli/doctor/index.d.ts +23 -0
  42. package/dist/cli/doctor/index.js +106 -2
  43. package/dist/cli/doctor/index.js.map +1 -1
  44. package/dist/cli/doctor/opencode.d.ts +24 -0
  45. package/dist/cli/doctor/opencode.js +107 -0
  46. package/dist/cli/doctor/opencode.js.map +1 -0
  47. package/dist/cli/doctor/target-checks.d.ts +11 -0
  48. package/dist/cli/doctor/target-checks.js +25 -0
  49. package/dist/cli/doctor/target-checks.js.map +1 -0
  50. package/dist/cli/doctor/toolchain-parity.d.ts +63 -0
  51. package/dist/cli/doctor/toolchain-parity.js +154 -0
  52. package/dist/cli/doctor/toolchain-parity.js.map +1 -0
  53. package/dist/cli/doctor/types.d.ts +85 -1
  54. package/dist/cli/doctor/types.js +1 -1
  55. package/dist/cli/doctor/types.js.map +1 -1
  56. package/dist/cli/doctor/understanding-mode-env.d.ts +39 -0
  57. package/dist/cli/doctor/understanding-mode-env.js +83 -0
  58. package/dist/cli/doctor/understanding-mode-env.js.map +1 -0
  59. package/dist/cli/dry-run.js +32 -17
  60. package/dist/cli/dry-run.js.map +1 -1
  61. package/dist/cli/explain.d.ts +1 -1
  62. package/dist/cli/explain.js +1 -1
  63. package/dist/cli/explain.js.map +1 -1
  64. package/dist/cli/index.js +64 -5
  65. package/dist/cli/index.js.map +1 -1
  66. package/dist/cli/init/composer.js +36 -13
  67. package/dist/cli/init/composer.js.map +1 -1
  68. package/dist/cli/init/interactive.d.ts +7 -4
  69. package/dist/cli/init/interactive.js +79 -27
  70. package/dist/cli/init/interactive.js.map +1 -1
  71. package/dist/cli/init/profiles.d.ts +2 -2
  72. package/dist/cli/init/profiles.js +32 -1
  73. package/dist/cli/init/profiles.js.map +1 -1
  74. package/dist/cli/init/templates.d.ts +2 -1
  75. package/dist/cli/init/templates.js +130 -20
  76. package/dist/cli/init/templates.js.map +1 -1
  77. package/dist/cli/pack/approve-escape.js +69 -8
  78. package/dist/cli/pack/approve-escape.js.map +1 -1
  79. package/dist/cli/pack/hook-branch-protection.js +1 -1
  80. package/dist/cli/pack/hook-branch-protection.js.map +1 -1
  81. package/dist/cli/pack/hook-codex-pre-tool-use.js +27 -1
  82. package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
  83. package/dist/cli/pack/hook-post-merge-gate.js +1 -1
  84. package/dist/cli/pack/hook-post-merge-gate.js.map +1 -1
  85. package/dist/cli/pack/hook-pre-tool-use.js +91 -4
  86. package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
  87. package/dist/cli/pack/hook-solution-acceptance.js +19 -2
  88. package/dist/cli/pack/hook-solution-acceptance.js.map +1 -1
  89. package/dist/cli/pack/understanding-report-schema-hint.d.ts +13 -0
  90. package/dist/cli/pack/understanding-report-schema-hint.js +52 -3
  91. package/dist/cli/pack/understanding-report-schema-hint.js.map +1 -1
  92. package/dist/cli/pause/index.js +1 -1
  93. package/dist/cli/pause/index.js.map +1 -1
  94. package/dist/cli/policy/intercept.d.ts +148 -0
  95. package/dist/cli/policy/intercept.js +379 -48
  96. package/dist/cli/policy/intercept.js.map +1 -1
  97. package/dist/cli/session-start/index.d.ts +12 -3
  98. package/dist/cli/session-start/index.js +67 -11
  99. package/dist/cli/session-start/index.js.map +1 -1
  100. package/dist/cli/session-start/stale-base-check.d.ts +71 -0
  101. package/dist/cli/session-start/stale-base-check.js +379 -0
  102. package/dist/cli/session-start/stale-base-check.js.map +1 -0
  103. package/dist/cli/session-start/toolchain-parity.d.ts +142 -8
  104. package/dist/cli/session-start/toolchain-parity.js +244 -36
  105. package/dist/cli/session-start/toolchain-parity.js.map +1 -1
  106. package/dist/cli/smoke/assertions.d.ts +9 -2
  107. package/dist/cli/smoke/assertions.js +9 -2
  108. package/dist/cli/smoke/assertions.js.map +1 -1
  109. package/dist/cli/uninstall/index.js +18 -2
  110. package/dist/cli/uninstall/index.js.map +1 -1
  111. package/dist/cli/validate/checks.d.ts +3 -1
  112. package/dist/cli/validate/checks.js +298 -3
  113. package/dist/cli/validate/checks.js.map +1 -1
  114. package/dist/io/claude-mcp.d.ts +122 -1
  115. package/dist/io/claude-mcp.js +214 -12
  116. package/dist/io/claude-mcp.js.map +1 -1
  117. package/dist/io/duration.js.map +1 -0
  118. package/dist/io/expand-home.js.map +1 -0
  119. package/dist/io/extract.js.map +1 -0
  120. package/dist/io/harness-lock.js +1 -1
  121. package/dist/io/harness-lock.js.map +1 -1
  122. package/dist/{runtime → io}/ledger-record.d.ts +1 -1
  123. package/dist/io/ledger-record.js.map +1 -0
  124. package/dist/policies/index.d.ts +2 -2
  125. package/dist/policies/index.js +2 -2
  126. package/dist/policies/index.js.map +1 -1
  127. package/dist/policies/ledger-client.js +10 -6
  128. package/dist/policies/ledger-client.js.map +1 -1
  129. package/dist/policies/producers.js +1 -1
  130. package/dist/policies/producers.js.map +1 -1
  131. package/dist/policies/requires.js +2 -2
  132. package/dist/policies/requires.js.map +1 -1
  133. package/dist/policy-packs/builtin/branch-protection.js +28 -3
  134. package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
  135. package/dist/policy-packs/builtin/post-merge-gate.js +45 -9
  136. package/dist/policy-packs/builtin/post-merge-gate.js.map +1 -1
  137. package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +139 -8
  138. package/dist/policy-packs/builtin/solution-acceptance-runtime.js +246 -12
  139. package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
  140. package/dist/policy-packs/builtin/solution-acceptance.js +17 -3
  141. package/dist/policy-packs/builtin/solution-acceptance.js.map +1 -1
  142. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.d.ts +25 -0
  143. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js +89 -0
  144. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js.map +1 -0
  145. package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +13 -0
  146. package/dist/policy-packs/builtin/understanding-before-execution/index.js +39 -0
  147. package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +1 -0
  148. package/dist/policy-packs/builtin/understanding-before-execution/ledger.d.ts +25 -0
  149. package/dist/policy-packs/builtin/understanding-before-execution/ledger.js +56 -0
  150. package/dist/policy-packs/builtin/understanding-before-execution/ledger.js.map +1 -0
  151. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.d.ts +20 -0
  152. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js +79 -0
  153. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js.map +1 -0
  154. package/dist/policy-packs/builtin/understanding-before-execution/markers.d.ts +103 -0
  155. package/dist/policy-packs/builtin/understanding-before-execution/markers.js +203 -0
  156. package/dist/policy-packs/builtin/understanding-before-execution/markers.js.map +1 -0
  157. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.d.ts +177 -0
  158. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js +309 -0
  159. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js.map +1 -0
  160. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.d.ts +106 -0
  161. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js +209 -0
  162. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js.map +1 -0
  163. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.d.ts +78 -0
  164. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js +185 -0
  165. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js.map +1 -0
  166. package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +1 -536
  167. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +9 -1095
  168. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +1 -1
  169. package/dist/policy-packs/builtin/understanding-before-execution.d.ts +109 -1
  170. package/dist/policy-packs/builtin/understanding-before-execution.js +183 -15
  171. package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
  172. package/dist/policy-packs/registry.d.ts +11 -0
  173. package/dist/policy-packs/registry.js +1 -1
  174. package/dist/policy-packs/registry.js.map +1 -1
  175. package/dist/policy-packs/runtime.d.ts +1 -1
  176. package/dist/policy-packs/runtime.js +10 -1
  177. package/dist/policy-packs/runtime.js.map +1 -1
  178. package/dist/runtime/agent-facing.js +1 -1
  179. package/dist/runtime/agent-facing.js.map +1 -1
  180. package/dist/runtime/bash-match-facts.d.ts +9 -7
  181. package/dist/runtime/bash-match-facts.js +9 -7
  182. package/dist/runtime/bash-match-facts.js.map +1 -1
  183. package/dist/runtime/bash-match-registry.d.ts +75 -38
  184. package/dist/runtime/bash-match-registry.js +279 -232
  185. package/dist/runtime/bash-match-registry.js.map +1 -1
  186. package/dist/runtime/bash-prefix-parse.d.ts +7 -0
  187. package/dist/runtime/bash-prefix-parse.js +205 -17
  188. package/dist/runtime/bash-prefix-parse.js.map +1 -1
  189. package/dist/runtime/command-normalize.d.ts +112 -0
  190. package/dist/runtime/command-normalize.js +411 -38
  191. package/dist/runtime/command-normalize.js.map +1 -1
  192. package/dist/runtime/hermetic-spawn-guard.js +6 -0
  193. package/dist/runtime/hermetic-spawn-guard.js.map +1 -1
  194. package/dist/runtime/index.d.ts +2 -2
  195. package/dist/runtime/index.js +2 -2
  196. package/dist/runtime/index.js.map +1 -1
  197. package/dist/runtime/intercept.d.ts +78 -3
  198. package/dist/runtime/intercept.js +151 -39
  199. package/dist/runtime/intercept.js.map +1 -1
  200. package/dist/runtime/ledger-add.js +1 -1
  201. package/dist/runtime/ledger-add.js.map +1 -1
  202. package/dist/runtime/read-only-bash.js +303 -24
  203. package/dist/runtime/read-only-bash.js.map +1 -1
  204. package/dist/schema/doctor.d.ts +25 -0
  205. package/dist/schema/doctor.js +23 -0
  206. package/dist/schema/doctor.js.map +1 -0
  207. package/dist/schema/extract.js +1 -1
  208. package/dist/schema/extract.js.map +1 -1
  209. package/dist/schema/index.d.ts +76 -0
  210. package/dist/schema/index.js +9 -0
  211. package/dist/schema/index.js.map +1 -1
  212. package/dist/schema/requires.js +2 -2
  213. package/dist/schema/requires.js.map +1 -1
  214. package/dist/schema/risk.d.ts +7 -0
  215. package/dist/schema/risk.js +23 -0
  216. package/dist/schema/risk.js.map +1 -1
  217. package/dist/schema/stale-base-check.d.ts +18 -0
  218. package/dist/schema/stale-base-check.js +37 -0
  219. package/dist/schema/stale-base-check.js.map +1 -0
  220. package/dist/schema/toolchain-parity.d.ts +3 -0
  221. package/dist/schema/toolchain-parity.js +13 -0
  222. package/dist/schema/toolchain-parity.js.map +1 -1
  223. package/package.json +3 -2
  224. package/dist/policies/duration.js.map +0 -1
  225. package/dist/policies/extract.js.map +0 -1
  226. package/dist/runtime/expand-home.js.map +0 -1
  227. package/dist/runtime/ledger-record.js.map +0 -1
  228. /package/dist/{policies → io}/duration.d.ts +0 -0
  229. /package/dist/{policies → io}/duration.js +0 -0
  230. /package/dist/{runtime → io}/expand-home.d.ts +0 -0
  231. /package/dist/{runtime → io}/expand-home.js +0 -0
  232. /package/dist/{policies → io}/extract.d.ts +0 -0
  233. /package/dist/{policies → io}/extract.js +0 -0
  234. /package/dist/{runtime → io}/ledger-record.js +0 -0
@@ -1,6 +1,6 @@
1
1
  // Registration point for every engine-side constant set that mirrors
2
2
  // FULL_TEMPLATE's `bash_match` facts (task `074acf5d`, run
3
- // `2026-07-28-manifest-facts-drift-guard`).
3
+ // `2026-07-28-manifest-facts-drift-guard`; redesigned by task `209e6dc4`).
4
4
  //
5
5
  // WHY A REGISTRY, NOT ONE HARD-CODED COMPARISON: the guard this replaces
6
6
  // (`tests/runtime/bash-match-head-token-drift.test.ts`, pre-migration)
@@ -16,7 +16,7 @@
16
16
  // NOT registered is itself a finding.
17
17
  //
18
18
  // TWO INTENTS (minimum required — see the two historical incidents this
19
- // run exists to close):
19
+ // registry exists to close):
20
20
  // - "covers-gated-head-tokens": the set exists so the engine recognises
21
21
  // these head tokens (`NON_GIT_HEAD_TOKENS`'s shape). Checked for
22
22
  // COMPLETENESS: every head token the manifest actually gates (barring
@@ -29,178 +29,227 @@
29
29
  // gates — the run `dbc6d303` CRITICAL's shape (an "inert consumer"
30
30
  // allowlist that accidentally absorbed the kill-switch's own gated
31
31
  // head token). Checked by testing every LIVE gated head token against
32
- // every such registered set's `has()`.
32
+ // every such registered set's coverage.
33
33
  //
34
- // FIX ROUND 2 (task `074acf5d`, second fix round, findings S1/S2/S4 —
35
- // see `.ai/runs/2026-07-28-manifest-facts-drift-guard/03-decisions.md`
36
- // D-002/D-004): review round 2 MEASURED that the round-1 guard's central
37
- // defence — a registration's self-declared `intent` — was still
38
- // escapable two ways, and that the guard reproduced its own defect class
39
- // inside itself. Three changes close what can be made STRUCTURALLY
40
- // impossible (D-002/D-003 deliberately draws the line here, not at every
41
- // enumerable escape — see the decisions file):
42
- // (S1) `RegisteredHeadTokenSet` is now a DISCRIMINATED UNION on `intent`:
43
- // the "covers-gated-head-tokens" arm's `members` is NON-OPTIONAL.
44
- // Omitting `members` on a covers registration — the round-1 escape,
45
- // "declare no members and check (c) below never runs" — is now a
46
- // COMPILE ERROR, not a test failure. The "must-not-contain" arm keeps
47
- // `members` optional (no purity semantics apply to it).
48
- // (S2) check (d), COVERS-REDUNDANCY, below: NARROWS — does not close —
49
- // the escape where a laundered set's declared `members` are all
50
- // HONESTLY gated (so check (c) is satisfied) but the set's true
51
- // purpose is something else. It works only for a gated token that some
52
- // covers-set legitimately covers, and is STRUCTURALLY SILENT for the
53
- // `documented-uncovered` subset. MEASURED (review round 3): a
54
- // registration declaring `members: ["tee"]` — or any subset of
55
- // `env`/`unset`/`tee`/`cp`, four of the eight live gated head tokens —
56
- // passes checks (a) through (e) with an entirely honest, live-coupled
57
- // `has()` and `members`, end to end on the real tree. That is the
58
- // `dbc6d303` shape (an inert allowlist absorbing a gated head token)
59
- // and this registry does not see it. Redesign filed as a follow-up;
60
- // see the LIMITS paragraph at the end of this header.
61
- // (S4) check (e), COVERS-PREDICATE-PURITY, below: closes the escape
62
- // where a registration's `has()` PREDICATE accepts more than its
63
- // declared `members` claim (measured: widening `GIT_TOKEN_RE` to also
64
- // match "docker" while leaving `members: ["git"]` untouched left the
65
- // full suite green) — this guard reproducing, inside itself, the exact
66
- // "engine-side constant mirrors manifest content with no coupling"
67
- // defect class it exists to eliminate elsewhere.
68
- // Each violation now carries a `check` field (see `RegistrationViolation`)
69
- // naming exactly which of the five checks fired, so a fixture test can
70
- // assert on ONE check's output without an unrelated check's finding on the
71
- // same (setId, token) pair silently changing what the array contains.
34
+ // REDESIGN (task `209e6dc4`) — WHY: the ORIGINAL registration surface (fix
35
+ // rounds 1 and 2 of task `074acf5d`, both superseded by this redesign) let a
36
+ // registration hand the guard a freely-chosen `has: (token) => boolean`
37
+ // closure plus an independently hand-typed `members: readonly string[]`
38
+ // array — TWO self-descriptions of the SAME underlying constant, asserted
39
+ // by the registrant, never derived from it. Three adversarial review
40
+ // passes attacked that gap; two fix rounds narrowed the escape family by
41
+ // adding checks c/d/e over the self-descriptions (covers-purity,
42
+ // covers-redundancy, covers-predicate-purity) without closing it — every
43
+ // added check produced the next bypass. Review round 3 measured the
44
+ // residual: a `covers-gated-head-tokens` registration declaring
45
+ // `members: ["tee"]` (or any subset of the four `documented-uncovered`
46
+ // tokens `env`/`unset`/`tee`/`cp`) passed every one of the five checks with
47
+ // an entirely honest, live-coupled `has()` and `members` — because nothing
48
+ // in the old design distinguished "genuinely not gated" from "gated but
49
+ // consciously uncovered", and `members` could say anything a NEW closure
50
+ // also happened to agree with.
72
51
  //
73
- // COVERS-PURITY (fix round, F1+F2): a THIRD check, over a "covers"
74
- // registration's OWN declared `members` (see `RegisteredHeadTokenSet.members`
75
- // below), closes two escapes with one rule — every declared member must
76
- // itself be a live gated head token:
77
- // (F1) the REVERSE direction, ENGINE->MANIFEST: the pre-migration guard
78
- // asserted this (every member of `NON_GIT_HEAD_TOKENS` must be a token
79
- // some shipped policy gates) and the migration to this registry
80
- // silently dropped it — completeness-checking (b) only ever walks
81
- // `facts.gatedHeadTokens` outward to registered sets, never a
82
- // registered set's own members inward to the facts. An engine set
83
- // gaining an ungated member (e.g. "docker" added to
84
- // `NON_GIT_HEAD_TOKENS`) is real DEAD CODE at best and a sign the
85
- // manifest and the engine have silently diverged at worst; either way
86
- // it is a regression this registry exists to catch.
87
- // (F2) MIS-DECLARED INTENT: a `must-not-contain-gated-head-token` set
88
- // relabeled `covers-gated-head-tokens` to dodge check (a) — cheap,
89
- // because `intent` is self-declared and was, before this check,
90
- // cross-checked against nothing. A set that exists for any purpose
91
- // OTHER than recognising gated head tokens (an inert-consumer
92
- // allowlist, say) necessarily contains members that are not
93
- // themselves gated head tokens, so it fails THIS check the moment it
94
- // declares `members` — the decision rule is: pick the intent that
95
- // describes the set's PURPOSE; a set whose purpose is anything other
96
- // than recognising gated head tokens is
97
- // "must-not-contain-gated-head-token", never "covers-gated-head-tokens".
52
+ // HALT CRITERION (recorded before this redesign's first line, task
53
+ // `209e6dc4` brief, quoted verbatim): "Findet ein spaeterer Review eine
54
+ // weitere Registrierung, die akzeptiert wird und nicht sollte, wird NICHT
55
+ // nachgebessert (keine fuenfte/sechste Pruefung), sondern die Oberflaeche
56
+ // erneut vorgelegt. Drei Passes haben gezeigt: jede zusaetzliche Pruefung
57
+ // produziert die naechste Umgehung. Der Umbau muss die Formen STRUKTURELL
58
+ // unausdrueckbar machen, nicht einzeln fangen." This redesign follows that
59
+ // instruction literally: it does not add a sixth check on top of the old
60
+ // five. It removes the SURFACE the old checks were inspecting.
98
61
  //
99
- // AXIS SCOPE (F6, fix round): this registry models the HEAD-TOKEN axis
100
- // only — which literal head token a `bash_match` anchors on. A shipped
101
- // policy's `bash_match` can equally anchor on a VERB with no head-token
102
- // significance of its own (`pause` in `deny-kill-switch-bypass`), checked
103
- // today only by `HARNESS_READ_ONLY_SUBS` in `read-only-bash.ts`, a set this
104
- // registry does not see and does not check. The VERB axis is explicitly
105
- // OUT OF SCOPE for this run: the exact same defect shape (an engine-side
106
- // allowlist silently absorbing a gated token) is unmodellable here today,
107
- // one axis over. Do not assume verb coverage exists because head-token
108
- // coverage does.
62
+ // THE STRUCTURAL FIX: `has()` as a freely-chosen closure, and `members` as
63
+ // an independently hand-typed array, are BOTH GONE. A registration now
64
+ // supplies a single `source: HeadTokenSource` — a discriminated union that
65
+ // holds the ACTUAL constant (`{ kind: "set"; set: NON_GIT_HEAD_TOKENS }` or
66
+ // `{ kind: "regex"; re: GIT_TOKEN_RE }`), imported and passed by reference,
67
+ // not re-described. `sourceHas` (below) derives membership testing FROM
68
+ // `source` — there is no code path left where a registration's declared
69
+ // coverage and its actual behaviour can diverge, because there is only one
70
+ // value now, not two. See the six-point closure list below for exactly
71
+ // which of the three passes' findings this makes unexpressible vs. which
72
+ // residual boundary (documented, not hidden) remains.
109
73
  //
110
- // WHY src/runtime/, NOT tests/ (F8, fix round): see `bash-match-facts.ts`'s
111
- // module header — the same reasoning applies here.
74
+ // SIX FORMS MEASURED OPEN BEFORE THIS REDESIGN (task `209e6dc4` brief,
75
+ // reconstructed from review round 3 since the run's own `.ai/runs/` docs
76
+ // are gitignored and not present in this checkout) AND THEIR DISPOSITION:
112
77
  //
113
- // LIMITS — READ THIS BEFORE TRUSTING A REGISTRATION. Three independent
114
- // adversarial review passes each found a way to register a set that this
115
- // registry accepts but should not. Two rounds of added checks narrowed the
116
- // family without closing it, so the checks were frozen here deliberately
117
- // and the registration SURFACE is being redesigned instead (follow-up
118
- // task; the shape under consideration is that a registration hands the
119
- // registry the constant itself rather than a self-description, so a stale
120
- // or lying mirror cannot be expressed at all). What this registry does NOT
121
- // catch today, each measured end to end, not theorised:
122
- // 1. A covers registration whose declared `members` are all
123
- // `documented-uncovered` gated tokens (`env`, `unset`, `tee`, `cp` —
124
- // four of eight). Passes every check with an honest, live-coupled
125
- // registration. This is the `dbc6d303` CRITICAL shape.
126
- // 2. A `has()` predicate that does not reflect the constant its
127
- // `id`/`module` names: a stale hand-copied snapshot, or `() => false`.
128
- // Nothing couples the two. The `must-not-contain` arm has no
129
- // predicate check at all, and that is the arm the CRITICAL shape
130
- // lives in.
131
- // 3. `members` declaring coverage the predicate rejects (the two are
132
- // each walked in one direction, never against each other), and an
133
- // empty `members: []`, which typechecks.
134
- // 4. A registration naming a `module`/`id` that no exported constant
135
- // matches.
136
- // What it DOES catch is the bar it was built for and still meets: both
137
- // historical incidents (`432db3d3`'s covers-completeness gap and
138
- // `dbc6d303`'s `harness` in an inert allowlist), plus an unregistered set
139
- // in the runtime layer. Treat it as a tripwire for the accidental case,
140
- // never as a boundary against a determined one.
78
+ // 1. Documented-uncovered laundering WITHOUT dishonesty: a covers
79
+ // registration whose declared members are all `documented-uncovered`
80
+ // gated tokens (env/unset/tee/cp) passed all five checks with an
81
+ // honest, live-coupled has()/members (the dbc6d303-CRITICAL shape,
82
+ // concretely reproduced with `members: ["tee"]`).
83
+ // CLOSED, structurally + a live probe: `checkRegisteredSets`'s (now
84
+ // merged) covers-purity check probes the registration's actual
85
+ // `source` — not a hand-typed members list, there isn't one any
86
+ // more — against every live gated head token AND treats a
87
+ // `documented-uncovered` token as ILLEGITIMATE to claim, exactly like
88
+ // a token that is not gated at all. A registration cannot "declare"
89
+ // its way around this: the probe reads the real Set/RegExp.
90
+ // Pinned by the `["tee"]`-shaped regression test in
91
+ // `tests/runtime/bash-match-registry.test.ts` (the exact form review
92
+ // round 3 measured).
93
+ // 2. `has()` as an arbitrary closure with no coupling to the constant
94
+ // `id`/`module` name (stale hand-copied predicate; `has: () => false`;
95
+ // no predicate check at all on the must-not-contain arm).
96
+ // STRUCTURALLY UNEXPRESSIBLE: there is no `has()` field any more.
97
+ // `source` holds the constant directly; `sourceHas` is the ONLY way
98
+ // membership is ever tested, for both intents.
99
+ // 3. `members` claiming coverage `has()` rejects (each walked in only one
100
+ // direction), and `members: []`, which type-checked and passed.
101
+ // STRUCTURALLY UNEXPRESSIBLE (the divergence half): there is no
102
+ // `members` field to disagree with `source` — a registration has
103
+ // exactly one thing to say about its coverage. CHECKED, not merely
104
+ // unexpressible (the empty half): an empty/vacuous `source` (e.g.
105
+ // `new Set([])`, or a regex matching none of the live vocabulary) is
106
+ // still a value a registrant CAN write — `checkRegisteredSets`'s new
107
+ // covers-empty check flags a covers registration whose `source`
108
+ // matches none of the legitimately-coverable live gated head tokens.
109
+ // 4. Check (d) COVERS-REDUNDANCY (fix round 2) forbade ALL double
110
+ // coverage, so it would also convict a legitimate future case (two
111
+ // registered sets consciously, by design, both keying the same head
112
+ // token) as if it were laundering.
113
+ // RESOLVED: `CoversHeadTokenSet.intentionalOverlaps` is an explicit,
114
+ // conscious per-token declaration. Covers-redundancy now only clears
115
+ // a multiply-covered token when EVERY registered set currently
116
+ // covering it lists that token in ITS OWN `intentionalOverlaps` — a
117
+ // set that silently overlaps (the round-2 laundering shape: a new
118
+ // set quietly re-covering a token an existing set already owns)
119
+ // still trips the check, because it has declared nothing. Legitimate
120
+ // double coverage requires touching BOTH registrations, not one.
121
+ // 5. `PLAUSIBLE_NON_GATED_HEAD_TOKENS` (the probe vocabulary check (e)
122
+ // used) was itself an uncoupled hand-written constant — 5 of its 8
123
+ // entries were exercised by no test at the time, and a token outside
124
+ // BOTH vocabularies (e.g. `yarn`/`pnpm`) went unprobed.
125
+ // PARTIALLY CLOSED (acceptance's declared "OR" branch — the vocabulary
126
+ // cannot be DERIVED from the manifest, which only enumerates what IS
127
+ // gated, never the negative space of what is not): every entry is now
128
+ // individually pinned by a live regression test in
129
+ // `tests/runtime/bash-match-head-token-drift.test.ts` asserting it is
130
+ // NOT a live gated head token — a future policy gating one of these
131
+ // words turns that test red, forcing a conscious update instead of a
132
+ // silent drift. The residual boundary (a probe token entirely outside
133
+ // both vocabularies stays unprobed) is named here AND at the constant.
134
+ // 6. A registration could name a `module`/`id` to which no exported
135
+ // constant actually corresponds.
136
+ // STRUCTURALLY UNEXPRESSIBLE, by the TypeScript compiler: `id` and
137
+ // `module` are now purely descriptive strings (used only in failure
138
+ // messages) — the CHECKED value is `source.set` / `source.re`, a
139
+ // direct reference to the real imported binding. Writing
140
+ // `source: { kind: "set", set: SOME_NAME_THAT_DOES_NOT_EXIST }`
141
+ // is `TS2304: Cannot find name` — a compile error, before any test
142
+ // runs. Manually verified (task `209e6dc4`): temporarily editing a
143
+ // real registration's `source.set` reference to an undeclared
144
+ // identifier fails `npm run typecheck` immediately; reverted right
145
+ // after. `id`/`module` can still be a MISLEADING label for a real,
146
+ // existing `source` (e.g. call `NON_GIT_HEAD_TOKENS`'s registration
147
+ // `id: "TOTALLY_WRONG_NAME"` while `source.set` still correctly
148
+ // references it) — that is a cosmetic residual, not a behavioural
149
+ // one: every check still evaluates the REAL constant regardless of
150
+ // the label.
151
+ //
152
+ // WHAT IS STILL A RESIDUAL, NAMED HONESTLY, NOT HIDDEN:
153
+ // - The probe vocabulary (point 5) is finite; a token outside both
154
+ // `facts.gatedHeadTokens` and `PLAUSIBLE_NON_GATED_HEAD_TOKENS` is not
155
+ // probed by covers-purity. This is inherent to any predicate-based
156
+ // check over an unbounded domain (a `RegExp`'s match set is not
157
+ // enumerable) and is not something the source-based redesign changes;
158
+ // it narrows the practical exposure (both arms are now checked with
159
+ // the SAME probe set, no separate unguarded closure) without claiming
160
+ // to enumerate "every string that is not gated".
161
+ // - `id`/`module` remain free-text labels, decoupled from `source`'s
162
+ // actual identity (point 6's cosmetic half, above).
163
+ // - `intentionalOverlaps` is itself a hand-declared field. A registrant
164
+ // COULD mark an overlap "intentional" when it is not. The bar this
165
+ // redesign sets is that doing so requires editing BOTH covering
166
+ // registrations (a two-registration diff, visible in review), not
167
+ // silently adding one new registration next to an untouched existing
168
+ // one — a materially higher bar than the round-2 escape, not a
169
+ // mathematical impossibility.
170
+ // - The must-not-contain arm has no purity check of its own (never did):
171
+ // its whole point is that its content is unrelated to gated head
172
+ // tokens, so there is nothing to compare its coverage against.
173
+ //
174
+ // AXIS SCOPE (carried forward, unchanged by this redesign): this registry
175
+ // models the HEAD-TOKEN axis only — which literal head token a
176
+ // `bash_match` anchors on. A shipped policy's `bash_match` can equally
177
+ // anchor on a VERB with no head-token significance of its own (`pause` in
178
+ // `deny-kill-switch-bypass`), checked today only by `HARNESS_READ_ONLY_SUBS`
179
+ // in `read-only-bash.ts`, a set this registry does not see and does not
180
+ // check. The VERB axis is explicitly OUT OF SCOPE here, unchanged from the
181
+ // prior design: the exact same defect shape (an engine-side allowlist
182
+ // silently absorbing a gated token) is unmodellable here today, one axis
183
+ // over. Do not assume verb coverage exists because head-token coverage
184
+ // does.
185
+ //
186
+ // WHY src/runtime/, NOT tests/: see `bash-match-facts.ts`'s module header —
187
+ // the same reasoning applies here.
141
188
  //
142
189
  // TODAY'S REAL REGISTRATIONS are both "covers" sets — `GIT_TOKEN_RE` and
143
190
  // `NON_GIT_HEAD_TOKENS`, both from `command-normalize.ts`, the two sets
144
- // the pre-migration guard already knew about. No real
145
- // "must-not-contain-gated-head-token" consumer exists in this tree today
146
- // (run `dbc6d303`'s own module is explicit non-goal / future work for
147
- // THIS run) — the intent is fully implemented and covered by the fixture
148
- // regression tests in `tests/runtime/bash-match-registry.test.ts`
149
- // (reproducing the CRITICAL's exact shape) and by a live mutation probe
150
- // against this file's own `REGISTERED_HEAD_TOKEN_SETS` array, not against
151
- // a second production consumer: temporarily registering a fixture
152
- // (`intent: "must-not-contain-gated-head-token"`, `has` containing
153
- // "harness") reddens exactly "every registered engine-side set is clean
154
- // against the live facts ..." and the F5 defaults test in
191
+ // the pre-migration guard already knew about, each now registered by
192
+ // handing this module the ACTUAL constant (`source: { kind: "regex", re:
193
+ // GIT_TOKEN_RE }` / `source: { kind: "set", set: NON_GIT_HEAD_TOKENS }`),
194
+ // not a re-description of it. No real "must-not-contain-gated-head-token"
195
+ // consumer exists in this tree today (run `dbc6d303`'s own module is
196
+ // explicit non-goal / future work) — the intent is fully implemented and
197
+ // covered by the fixture regression tests in
198
+ // `tests/runtime/bash-match-registry.test.ts` (reproducing the CRITICAL's
199
+ // exact shape) and by a live mutation probe against this file's own
200
+ // `REGISTERED_HEAD_TOKEN_SETS` array, not against a second production
201
+ // consumer: temporarily registering a fixture
202
+ // (`intent: "must-not-contain-gated-head-token"`,
203
+ // `source: { kind: "set", set: new Set(["harness"]) }`) reddens exactly
204
+ // "every registered engine-side set is clean against the live facts ..."
205
+ // and the defaults test in
155
206
  // `tests/runtime/bash-match-head-token-drift.test.ts`, with the violation
156
207
  // naming the fixture's own `setId` and the offending token — the probe was
157
208
  // reverted immediately after.
158
- //
159
- // `has()` rather than an enumerable `ReadonlySet<string>` uniformly for
160
- // checks (a) and (b): the live gated-head-token vocabulary is small and
161
- // fully known from `BashMatchFacts.gatedHeadTokens` (never more than ~8
162
- // entries), so both checks only ever need to ask "does this registered set
163
- // answer YES for token X", which a `RegExp.test` (`GIT_TOKEN_RE`) and a
164
- // `Set.has` (`NON_GIT_HEAD_TOKENS`) can equally answer. Check (c)
165
- // (COVERS-PURITY, above) is different in kind — it asks "does this
166
- // registered set contain something the facts DON'T know about", which no
167
- // predicate over the ~8 known tokens can answer; that is why `members` is
168
- // a separate, enumerable field rather than folded into `has()` — NON-
169
- // OPTIONAL on the "covers-gated-head-tokens" arm as of fix round 2 (S1;
170
- // see `CoversHeadTokenSet`'s doc), optional on "must-not-contain", which
171
- // this field's checks never apply to.
172
209
  import { GIT_TOKEN_RE, NON_GIT_HEAD_TOKENS } from "./command-normalize.js";
173
210
  import { DOCUMENTED_UNCOVERED_HEAD_TOKENS, isHeadTokenGated } from "./bash-match-facts.js";
211
+ /** Membership test derived FROM `source` — never independently supplied. */
212
+ export function sourceHas(source, token) {
213
+ return source.kind === "set" ? source.set.has(token) : source.re.test(token);
214
+ }
174
215
  export const REGISTERED_HEAD_TOKEN_SETS = [
175
216
  {
176
217
  id: "GIT_TOKEN_RE",
177
218
  module: "src/runtime/command-normalize.ts",
178
219
  intent: "covers-gated-head-tokens",
179
- has: (token) => GIT_TOKEN_RE.test(token),
180
- members: ["git"],
220
+ source: { kind: "regex", re: GIT_TOKEN_RE },
181
221
  description: "canonicalizeSegment's git-basename recognizer (matches `git`, and any `\\S*/git` path-qualified spelling, by basename)",
182
222
  },
183
223
  {
184
224
  id: "NON_GIT_HEAD_TOKENS",
185
225
  module: "src/runtime/command-normalize.ts",
186
226
  intent: "covers-gated-head-tokens",
187
- has: (token) => NON_GIT_HEAD_TOKENS.has(token),
188
- members: [...NON_GIT_HEAD_TOKENS],
227
+ source: { kind: "set", set: NON_GIT_HEAD_TOKENS },
189
228
  description: "canonicalizeSegment's closed non-git head-token set (today: gh, npm, harness)",
190
229
  },
191
230
  ];
192
231
  /**
193
- * Fixed, deliberately small vocabulary of plausible NON-gated head tokens
194
- * (fix round 2, S4/D-004) — commands a reasonable engine-side set might
195
- * plausibly mirror or accidentally absorb, but that no shipped
196
- * `bash_match` policy gates today. Combined with the live
197
- * `facts.gatedHeadTokens` (every token that IS gated, verified fresh —
198
- * see `bash-match-facts.ts`) this gives check (e) below a concrete probe
199
- * domain to test every registered "covers" predicate against, instead of
200
- * trusting a registration's declared `members` to be an honest reflection
201
- * of what its `has()` actually accepts.
232
+ * Fixed, deliberately small, PER-ENTRY-PINNED vocabulary of plausible
233
+ * NON-gated head tokens (originally fix round 2 S4/D-004; each entry
234
+ * individually pinned by a live regression test in
235
+ * `tests/runtime/bash-match-head-token-drift.test.ts` as of task
236
+ * `209e6dc4` — see the module header's point 5): commands a reasonable
237
+ * engine-side set might plausibly mirror or accidentally absorb, but that
238
+ * no shipped `bash_match` policy gates today. Combined with the live
239
+ * `facts.gatedHeadTokens` (every token that IS gated, verified fresh — see
240
+ * `bash-match-facts.ts`) this gives covers-purity a concrete probe domain
241
+ * to test every registered "covers" source against, instead of trusting a
242
+ * registration to honestly describe what it covers.
243
+ *
244
+ * RESIDUAL, named not hidden: this list cannot be exhaustively DERIVED from
245
+ * the manifest — the manifest enumerates what IS gated, never the infinite
246
+ * negative space of what is not. A token entirely outside both this list
247
+ * and `facts.gatedHeadTokens` (e.g. `yarn`, `pnpm`) is not probed by
248
+ * covers-purity. Widening this list narrows that residual; it cannot close
249
+ * it completely for a `RegExp`-backed source, whose true match set is not
250
+ * enumerable at all.
202
251
  */
203
- const PLAUSIBLE_NON_GATED_HEAD_TOKENS = [
252
+ export const PLAUSIBLE_NON_GATED_HEAD_TOKENS = [
204
253
  "docker",
205
254
  "kubectl",
206
255
  "ls",
@@ -213,42 +262,45 @@ const PLAUSIBLE_NON_GATED_HEAD_TOKENS = [
213
262
  /**
214
263
  * Check every registered set against the live facts. Returns an EMPTY
215
264
  * array when clean. `registrations` and `documentedUncovered` default to
216
- * the real, shipped values (F5 fix-round correction: the
217
- * `documentedUncovered` default previously silently fell back to an empty
218
- * set, which raised 4 FALSE violations — tee, cp, env, unset — when called
219
- * with only `facts`; pinned by
220
- * `tests/runtime/bash-match-head-token-drift.test.ts`'s
221
- * "checkRegisteredSets(liveFacts())" defaults test) but accept fixtures —
222
- * see `tests/runtime/bash-match-registry.test.ts`'s regression tests for
223
- * the historical incidents, which pass deliberately-broken fixtures to
224
- * prove this function catches their exact shape.
265
+ * the real, shipped values but accept fixtures — see
266
+ * `tests/runtime/bash-match-registry.test.ts`'s regression tests for the
267
+ * historical incidents, which pass deliberately-broken fixtures to prove
268
+ * this function catches their exact shape.
225
269
  */
226
270
  export function checkRegisteredSets(facts, registrations = REGISTERED_HEAD_TOKEN_SETS, documentedUncovered = DOCUMENTED_UNCOVERED_HEAD_TOKENS) {
227
271
  const violations = [];
228
272
  // (a) must-not-contain: no registered set with this intent may answer
229
- // `true` for ANY live gated head token — the dbc6d303 CRITICAL shape.
273
+ // `true` (via `sourceHas`, the ONLY membership test that exists now) for
274
+ // ANY live gated head token — the dbc6d303 CRITICAL shape.
230
275
  for (const reg of registrations) {
231
276
  if (reg.intent !== "must-not-contain-gated-head-token")
232
277
  continue;
233
278
  for (const token of facts.gatedHeadTokens) {
234
- if (reg.has(token)) {
279
+ if (sourceHas(reg.source, token)) {
235
280
  violations.push({
236
281
  setId: reg.id,
237
282
  intent: reg.intent,
238
283
  token,
239
284
  check: "must-not-contain",
240
- reason: `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "must-not-contain-gated-head-token" but contains "${token}", which IS gated by a shipped bash_match policy`,
285
+ reason: `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "must-not-contain-gated-head-token" but its source contains "${token}", which IS gated by a shipped bash_match policy`,
241
286
  });
242
287
  }
243
288
  }
244
289
  }
290
+ // Is `token` something a "covers" registration is LEGITIMATELY allowed to
291
+ // claim: gated, AND not consciously carved out as documented-uncovered?
292
+ // Point 1 of the module header's six forms lived exactly in the gap this
293
+ // predicate closes — the old design treated "gated" as the only bar to
294
+ // clear, so a covers set claiming ONLY documented-uncovered tokens
295
+ // passed cleanly.
296
+ const isLegitimatelyCoverable = (token) => isHeadTokenGated(facts, token) && !documentedUncovered.has(token);
245
297
  // (b) covers-completeness: every gated head token NOT consciously
246
298
  // documented as uncovered must be answered `true` by at least one
247
- // registered "covers" set — the 432db3d3 HIGH shape.
299
+ // registered "covers" set's `source` — the 432db3d3 HIGH shape.
248
300
  for (const token of facts.gatedHeadTokens) {
249
301
  if (documentedUncovered.has(token))
250
302
  continue;
251
- const covered = registrations.some((reg) => reg.intent === "covers-gated-head-tokens" && reg.has(token));
303
+ const covered = registrations.some((reg) => reg.intent === "covers-gated-head-tokens" && sourceHas(reg.source, token));
252
304
  if (!covered) {
253
305
  const cls = facts.classify(token);
254
306
  violations.push({
@@ -260,81 +312,45 @@ export function checkRegisteredSets(facts, registrations = REGISTERED_HEAD_TOKEN
260
312
  });
261
313
  }
262
314
  }
263
- // (c) covers-purity (fix round, F1+F2): every member a "covers" set
264
- // DECLARES (see RegisteredHeadTokenSet.members) must itself be a live
265
- // gated head token. Closes two escapes with one check: (1) the reverse
266
- // ENGINE->MANIFEST direction the pre-migration guard checked and the
267
- // migration dropped (a covers set gaining a member — e.g. "docker" —
268
- // that no shipped policy gates at all); (2) the mis-declared-intent
269
- // laundering escape, where a `must-not-contain-gated-head-token` set is
270
- // relabeled `covers-gated-head-tokens` to dodge check (a) — a set that
271
- // exists for any purpose OTHER than recognising gated head tokens
272
- // necessarily contains a member that fails this check. `members` is
273
- // non-optional on this arm as of fix round 2 (S1) — every registration
274
- // that reaches here has one to walk.
315
+ // (c) covers-empty (task `209e6dc4`, module header point 3, empty half):
316
+ // a covers registration whose `source` matches NONE of the legitimately
317
+ // coverable live gated head tokens contributes zero real coverage — the
318
+ // structural echo of the old `members: []` escape, which type-checked
319
+ // and passed silently because nothing walked an empty declaration back
320
+ // to a finding. There is no `members` field to leave empty any more, but
321
+ // an empty backing `Set`, or a regex that matches nothing in the known
322
+ // vocabulary, is still a value a registrant can write; this check makes
323
+ // that visible instead of silently accepted.
275
324
  for (const reg of registrations) {
276
325
  if (reg.intent !== "covers-gated-head-tokens")
277
326
  continue;
278
- for (const member of reg.members) {
279
- if (!isHeadTokenGated(facts, member)) {
280
- violations.push({
281
- setId: reg.id,
282
- intent: reg.intent,
283
- token: member,
284
- check: "covers-purity",
285
- reason: `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "covers-gated-head-tokens" and lists "${member}" among its members, but "${member}" is not a live gated head token (no shipped bash_match policy gates it) — pick the intent that describes the set's PURPOSE: a set that exists for any purpose OTHER than recognising gated head tokens is "must-not-contain-gated-head-token", never "covers-gated-head-tokens"`,
286
- });
287
- }
288
- }
289
- }
290
- // (d) covers-redundancy (fix round 2, S2): a gated head token answered
291
- // `true` by MORE THAN ONE registered "covers-gated-head-tokens" set is a
292
- // violation. Closes the escape check (c) cannot: a laundered set (its
293
- // TRUE purpose is something other than recognising gated head tokens,
294
- // relabeled "covers-gated-head-tokens" to dodge check (a)) whose
295
- // declared `members` are HONESTLY all gated — e.g. `new
296
- // Set(["harness"])` or `new Set(["harness", "gh"])` — passes check (c)
297
- // cleanly (every declared member IS a live gated head token). The tell
298
- // is redundancy: a gated head token legitimately has exactly ONE
299
- // registered "covers" set responsible for it.
300
- //
301
- // SCOPE OF THIS CHECK, measured, not assumed (review round 3): the tell
302
- // only exists for a token some covers-set ALREADY covers. For the four
303
- // `documented-uncovered` tokens (`env`, `unset`, `tee`, `cp`) no
304
- // covers-set covers them by definition, so `coveringSets.length` is 1
305
- // and this check is structurally silent — while check (b) skips them and
306
- // checks (c)/(e) pass because they ARE gated. A laundered set declaring
307
- // only those tokens therefore passes everything. Do not read this check
308
- // as a completeness proof; an earlier version of this comment did, and
309
- // it was false for half the live vocabulary.
310
- for (const token of facts.gatedHeadTokens) {
311
- const coveringSets = registrations.filter((reg) => reg.intent === "covers-gated-head-tokens" && reg.has(token));
312
- if (coveringSets.length > 1) {
313
- for (const reg of coveringSets) {
314
- const others = coveringSets.filter((r) => r.id !== reg.id).map((r) => r.id);
315
- violations.push({
316
- setId: reg.id,
317
- intent: reg.intent,
318
- token,
319
- check: "covers-redundancy",
320
- reason: `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "covers-gated-head-tokens" and answers true for "${token}", but "${token}" is ALSO covered by ${others.length} other registered covers-set(s) (${others.join(", ")}) — a gated head token must be covered by exactly one registered covers-set; redundant coverage is the signature of an intent-laundered set whose declared members happen to already be genuinely gated elsewhere`,
321
- });
322
- }
327
+ const coversSomething = [...facts.gatedHeadTokens]
328
+ .filter(isLegitimatelyCoverable)
329
+ .some((token) => sourceHas(reg.source, token));
330
+ if (!coversSomething) {
331
+ violations.push({
332
+ setId: reg.id,
333
+ intent: reg.intent,
334
+ token: "(none)",
335
+ check: "covers-empty",
336
+ reason: `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "covers-gated-head-tokens" but its source matches NONE of the live, legitimately-coverable gated head tokens — an empty or vacuous registration`,
337
+ });
323
338
  }
324
339
  }
325
- // (e) covers-predicate-purity (fix round 2, S4/D-004): unlike check (c),
326
- // which only inspects a registration's DECLARED `members`, this probes
327
- // the registration's ACTUAL `has()` predicate directly against a fixed
328
- // vocabulary — every live gated head token plus
329
- // `PLAUSIBLE_NON_GATED_HEAD_TOKENS` — and demands `has(token)` answer
330
- // `true` ONLY for tokens that ARE live gated head tokens. Closes the
331
- // exact defect class this run exists to eliminate, reproduced INSIDE
332
- // this guard: a predicate-based registration (`GIT_TOKEN_RE.test`)
333
- // paired with a hand-written `members` list that check (c) verifies but
334
- // the predicate ITSELF is never re-checked against — measured (D-004):
335
- // widening `GIT_TOKEN_RE` to also match "docker" while leaving
336
- // `members: ["git"]` untouched left the full suite green before this
337
- // check existed.
340
+ // (d) covers-purity (task `209e6dc4` redesign, merging the old checks
341
+ // (c) covers-purity, (e) covers-predicate-purity, and closing the
342
+ // previously-unchecked documented-uncovered laundering gap — module
343
+ // header points 1 and 3): probe the registration's ACTUAL `source`
344
+ // directly — never a hand-typed `members` list, there isn't one any
345
+ // more — against every live gated head token PLUS
346
+ // `PLAUSIBLE_NON_GATED_HEAD_TOKENS`. A covers registration must answer
347
+ // `true` ONLY for tokens that are BOTH gated AND not consciously
348
+ // documented-uncovered (i.e. `isLegitimatelyCoverable`). Because coverage
349
+ // is read straight off `source` instead of a separate declared list,
350
+ // "declaring coverage the predicate rejects" and "claiming a
351
+ // documented-uncovered token honestly" collapse into the same probe:
352
+ // there is nothing left to declare independently of what the constant
353
+ // actually does.
338
354
  const probeVocabulary = new Set([
339
355
  ...facts.gatedHeadTokens,
340
356
  ...PLAUSIBLE_NON_GATED_HEAD_TOKENS,
@@ -343,17 +359,48 @@ export function checkRegisteredSets(facts, registrations = REGISTERED_HEAD_TOKEN
343
359
  if (reg.intent !== "covers-gated-head-tokens")
344
360
  continue;
345
361
  for (const token of probeVocabulary) {
346
- if (reg.has(token) && !isHeadTokenGated(facts, token)) {
362
+ if (sourceHas(reg.source, token) && !isLegitimatelyCoverable(token)) {
363
+ const reason = documentedUncovered.has(token)
364
+ ? `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "covers-gated-head-tokens" and its source answers true for "${token}", but "${token}" IS gated while also being consciously documented-uncovered — a covers set may not claim coverage of a token this codebase has deliberately marked as NOT covered by any engine-side set (the dbc6d303-shape laundering this check exists to close)`
365
+ : `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "covers-gated-head-tokens" and its source answers true for "${token}", but "${token}" is not a live gated head token at all — the source accepts more than it should, reproducing this registry's own defect class (an engine-side constant mirroring manifest content with no coupling) inside the guard itself`;
347
366
  violations.push({
348
367
  setId: reg.id,
349
368
  intent: reg.intent,
350
369
  token,
351
- check: "covers-predicate-purity",
352
- reason: `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "covers-gated-head-tokens" and its has() predicate answers true for "${token}", but "${token}" is not a live gated head token — the predicate accepts more than its declared members claim, reproducing this run's own defect class (an engine-side predicate mirroring manifest content with no coupling) inside the guard itself`,
370
+ check: "covers-purity",
371
+ reason,
353
372
  });
354
373
  }
355
374
  }
356
375
  }
376
+ // (e) covers-redundancy: a gated head token answered `true` by MORE THAN
377
+ // ONE registered "covers-gated-head-tokens" set's `source` is a
378
+ // violation UNLESS every one of those covering sets consciously declares
379
+ // that token in its own `intentionalOverlaps` (task `209e6dc4`,
380
+ // resolving module header point 4: a silent new registration re-covering
381
+ // a token an existing set already owns still trips this — only a
382
+ // declaration on BOTH sides clears it).
383
+ const isCoversSet = (reg) => reg.intent === "covers-gated-head-tokens";
384
+ for (const token of facts.gatedHeadTokens) {
385
+ const coveringSets = registrations
386
+ .filter(isCoversSet)
387
+ .filter((reg) => sourceHas(reg.source, token));
388
+ if (coveringSets.length <= 1)
389
+ continue;
390
+ const allAcknowledged = coveringSets.every((reg) => reg.intentionalOverlaps?.has(token));
391
+ if (allAcknowledged)
392
+ continue;
393
+ for (const reg of coveringSets) {
394
+ const others = coveringSets.filter((r) => r.id !== reg.id).map((r) => r.id);
395
+ violations.push({
396
+ setId: reg.id,
397
+ intent: reg.intent,
398
+ token,
399
+ check: "covers-redundancy",
400
+ reason: `"${reg.id}" (${reg.module} — ${reg.description}) declares intent "covers-gated-head-tokens" and answers true for "${token}", but "${token}" is ALSO covered by ${others.length} other registered covers-set(s) (${others.join(", ")}) without every covering set declaring "${token}" in its own intentionalOverlaps — a gated head token must be covered by exactly one registered covers-set unless ALL covering sets consciously declare the overlap`,
401
+ });
402
+ }
403
+ }
357
404
  return violations;
358
405
  }
359
406
  //# sourceMappingURL=bash-match-registry.js.map