@event4u/agent-config 16.1.0 → 16.2.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 (217) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +237 -248
  5. package/dist/agent-src/commands/roadmap/process-full.md +1 -1
  6. package/dist/agent-src/commands/work.md +4 -0
  7. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
  8. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  9. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
  10. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
  11. package/dist/agent-src/skills/design-review/SKILL.md +30 -4
  12. package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
  13. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
  14. package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
  15. package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
  16. package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
  17. package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
  18. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  19. package/dist/agent-src/templates/roadmaps.md +30 -0
  20. package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  21. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  22. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  23. package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  24. package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
  25. package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  26. package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  27. package/dist/cli/python/knowledge_ingest.js +1 -1
  28. package/dist/cli/python/workspace_drive.js +1 -1
  29. package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
  30. package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
  31. package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
  32. package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
  33. package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
  34. package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
  35. package/dist/cli-delegate/cmd_conformance.js +902 -782
  36. package/dist/cli-delegate/cmd_doctor.js +2 -2
  37. package/dist/cli-delegate/cmd_export.js +2 -2
  38. package/dist/cli-delegate/cmd_handoff.js +4 -4
  39. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  40. package/dist/cli-delegate/cmd_prune.js +2 -2
  41. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  42. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  43. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  44. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  45. package/dist/cli-delegate/cmd_sync.js +2 -2
  46. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  47. package/dist/cli-delegate/cmd_validate.js +2 -2
  48. package/dist/discovery/deprecation-report.md +1 -1
  49. package/dist/discovery/discovery-manifest.json +10 -10
  50. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  51. package/dist/discovery/discovery-manifest.summary.md +1 -1
  52. package/dist/discovery/dormancy-report.md +1 -1
  53. package/dist/discovery/orphan-report.md +1 -1
  54. package/dist/discovery/packs.json +2 -2
  55. package/dist/discovery/trust-report.md +1 -1
  56. package/dist/discovery/workspaces.json +2 -2
  57. package/dist/hooks/dispatch.js +6666 -2581
  58. package/dist/hooks/dispatch.sha256 +1 -0
  59. package/dist/install/install.mjs +848 -733
  60. package/dist/mcp/registry-manifest.json +2 -2
  61. package/dist/mcp/server.json +2 -2
  62. package/dist/mcp/server.mjs +2747 -1510
  63. package/dist/server/schemas/settings.js +23 -0
  64. package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
  65. package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
  66. package/dist/ui/index.html +2 -2
  67. package/docs/contracts/settings-classes.md +6 -4
  68. package/package.json +4 -4
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  73. package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
  74. package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  75. package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  76. package/src/config/agent-settings.template.yml +29 -0
  77. package/src/config/ci-local-parity.yml +18 -0
  78. package/src/config/continuity-surface.json +2 -2
  79. package/src/config/gate-coverage.yml +234 -3
  80. package/src/config/gate-violation-baselines.json +7 -3
  81. package/src/config/hook-latency-budget.json +21 -0
  82. package/src/config/hook-token-budget.json +2 -2
  83. package/src/config/metric-registry.yml +114 -0
  84. package/src/config/placeholder-drift-budget.json +10 -0
  85. package/src/config/routing-coverage-seed.json +18 -4
  86. package/src/scripts/_cli/cmd_doctor.ts +5 -19
  87. package/src/scripts/_cli/doctor_execution.ts +100 -17
  88. package/src/scripts/_cli/doctor_network_posture.ts +226 -0
  89. package/src/scripts/_lib/body_portable.ts +102 -0
  90. package/src/scripts/_lib/cc_transcript.ts +132 -1
  91. package/src/scripts/_lib/concern_sla_window.ts +200 -0
  92. package/src/scripts/_lib/forge_protection.ts +114 -0
  93. package/src/scripts/_lib/forge_reader.ts +558 -0
  94. package/src/scripts/_lib/host_env_write.ts +111 -0
  95. package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
  96. package/src/scripts/_lib/obligations.ts +119 -8
  97. package/src/scripts/_lib/probe_inputs.ts +271 -0
  98. package/src/scripts/_lib/read_surface_scan.ts +322 -0
  99. package/src/scripts/_lib/release_highlights.ts +2 -1
  100. package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
  101. package/src/scripts/_lib/shell_write_shapes.ts +108 -0
  102. package/src/scripts/_lib/structural_hiding.ts +622 -0
  103. package/src/scripts/_lib/turn_end_refusals.ts +525 -4
  104. package/src/scripts/_lib/turn_end_transcript.ts +216 -0
  105. package/src/scripts/_lib/verification_command.ts +324 -0
  106. package/src/scripts/_lib/verification_evidence.ts +647 -0
  107. package/src/scripts/_lib/verify_clause.ts +280 -0
  108. package/src/scripts/ai_team/team_dispatch.ts +4 -0
  109. package/src/scripts/before_complete_hook.ts +340 -23
  110. package/src/scripts/bench_hook_latency.ts +412 -5
  111. package/src/scripts/cache_realization_report.ts +61 -1
  112. package/src/scripts/check_argument_hint.ts +287 -0
  113. package/src/scripts/check_claims.ts +125 -1
  114. package/src/scripts/check_enforcement_coverage.ts +38 -9
  115. package/src/scripts/check_enforcement_matrix.ts +245 -3
  116. package/src/scripts/check_finding_dispositions.ts +22 -3
  117. package/src/scripts/check_generator_sync.ts +71 -0
  118. package/src/scripts/check_host_docs_digest.ts +786 -0
  119. package/src/scripts/check_host_format_column.ts +559 -0
  120. package/src/scripts/check_input_references.ts +228 -0
  121. package/src/scripts/check_inputs_parity.ts +235 -0
  122. package/src/scripts/check_kernel_edit_ratified.ts +47 -5
  123. package/src/scripts/check_kill_switch_table.ts +294 -0
  124. package/src/scripts/check_placeholder_drift.ts +221 -0
  125. package/src/scripts/check_read_surface_coverage.ts +515 -0
  126. package/src/scripts/check_release_holds.ts +2 -1
  127. package/src/scripts/check_routing_coverage.ts +378 -11
  128. package/src/scripts/check_static_parity.ts +17 -0
  129. package/src/scripts/ci_settle.ts +145 -19
  130. package/src/scripts/closure_scan.ts +115 -1
  131. package/src/scripts/compile_hook_manifest.ts +60 -16
  132. package/src/scripts/external_sources_denylist.json +29 -4
  133. package/src/scripts/hook_manifest.json +1 -1
  134. package/src/scripts/hook_manifest.yaml +42 -5
  135. package/src/scripts/hooks/block_config_weakening.ts +229 -4
  136. package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
  137. package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
  138. package/src/scripts/hooks/bundle_integrity.ts +291 -0
  139. package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
  140. package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
  141. package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
  142. package/src/scripts/hooks/concern_registry.ts +2 -0
  143. package/src/scripts/hooks/concern_timings.ts +94 -0
  144. package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
  145. package/src/scripts/hooks/design_pass_hook.ts +100 -9
  146. package/src/scripts/hooks/design_slop_hook.ts +1 -2
  147. package/src/scripts/hooks/dispatch_hook.ts +25 -19
  148. package/src/scripts/hooks/edit_shape_hook.ts +1 -2
  149. package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
  150. package/src/scripts/hooks/evidence_independence.ts +1 -2
  151. package/src/scripts/hooks/exit_codes.ts +128 -0
  152. package/src/scripts/hooks/host_lowering.json +1 -0
  153. package/src/scripts/hooks/host_lowering.ts +128 -3
  154. package/src/scripts/hooks/host_lowering.yaml +262 -46
  155. package/src/scripts/hooks/host_semantics.ts +30 -2
  156. package/src/scripts/hooks/injection_budget.ts +9 -2
  157. package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
  158. package/src/scripts/hooks/journal_record_hook.ts +1 -2
  159. package/src/scripts/hooks/memory_recall_hook.ts +1 -2
  160. package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
  161. package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
  162. package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
  163. package/src/scripts/hooks/permission_gate.ts +1 -2
  164. package/src/scripts/hooks/push_settle_hook.ts +1 -3
  165. package/src/scripts/hooks/replay_hook.ts +2 -1
  166. package/src/scripts/hooks/reread_guard_hook.ts +1 -2
  167. package/src/scripts/hooks/review_baseline_hook.ts +1 -2
  168. package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
  169. package/src/scripts/hooks/rule_inject_hook.ts +8 -5
  170. package/src/scripts/hooks/run_continuation_hook.ts +13 -15
  171. package/src/scripts/hooks/skill_route_hook.ts +1 -2
  172. package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
  173. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
  174. package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
  175. package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
  176. package/src/scripts/hooks/table_fingerprint.ts +49 -0
  177. package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
  178. package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
  179. package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
  180. package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
  181. package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
  182. package/src/scripts/install.ts +3 -4
  183. package/src/scripts/lint_deny_text.ts +419 -0
  184. package/src/scripts/lint_exit_codes.ts +262 -0
  185. package/src/scripts/lint_hook_manifest.ts +55 -0
  186. package/src/scripts/lint_roadmap_blockers.ts +78 -12
  187. package/src/scripts/lint_skill_descriptions.ts +1 -1
  188. package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
  189. package/src/scripts/mcp_server/prompts.ts +142 -1
  190. package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
  191. package/src/scripts/measure_turn_end_gate.ts +193 -0
  192. package/src/scripts/memory_lookup.ts +1 -1
  193. package/src/scripts/prepack-check.mjs +53 -0
  194. package/src/scripts/release_publication.ts +13 -1
  195. package/src/scripts/report_invocation_surface.ts +383 -0
  196. package/src/scripts/report_obligation_carriers.ts +699 -0
  197. package/src/scripts/report_obligation_writer_split.ts +236 -0
  198. package/src/scripts/report_skill_menu_census.ts +208 -0
  199. package/src/scripts/report_standing_payload_by_host.ts +1 -1
  200. package/src/scripts/roadmap_verify_share.ts +213 -0
  201. package/src/scripts/schemas/command.schema.json +44 -0
  202. package/src/scripts/schemas/review-findings.schema.json +4 -0
  203. package/src/scripts/schemas/skill.schema.json +44 -0
  204. package/src/scripts/self_review_gate.ts +359 -10
  205. package/src/scripts/semantic_parity_census.ts +314 -0
  206. package/src/scripts/session_eol_report.ts +54 -1
  207. package/src/scripts/sweep_dead_scan_roots.ts +1 -1
  208. package/src/scripts/trigger_eval_grandfather.json +0 -2
  209. package/src/scripts/trigger_eval_rotation.ts +122 -27
  210. package/src/scripts/ui_conformance_probe.ts +156 -4
  211. package/src/scripts/update_prices.ts +85 -3
  212. package/src/scripts/write_bundle_digest.mjs +74 -0
  213. package/src/server/schemas/settings.ts +31 -0
  214. package/src/templates/consumer-settings/ONBOARDING.md +1 -0
  215. package/dist/ui/assets/index-DH1xc62v.js +0 -29
  216. package/dist/ui/assets/index-jSfMW8_t.css +0 -1
  217. /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
@@ -0,0 +1,515 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * check_read_surface_coverage.ts — generate the sanitize floor's read-surface
4
+ * table in `docs/contracts/retrieval-read-surfaces.md` from the tree, and fail
5
+ * when the committed table, the module header, or the structural detector
6
+ * disagree with what the tree actually says
7
+ * (road-to-a-sanitize-list-that-is-generated, Phase 1.2 + Phase 3.3).
8
+ *
9
+ * THREE CHECKS, ONE REASON
10
+ * ------------------------
11
+ * 1. DRIFT — the committed table equals the regenerated one. A new module that
12
+ * brings outside bytes in, or a sanitize import added or removed, changes the
13
+ * table; leaving the committed copy behind is the failure this gate exists
14
+ * for.
15
+ * 2. SECOND COPY — `retrieval_sanitize.ts`'s header points at the table instead
16
+ * of restating it. Two copies of a list is exactly how the first one went
17
+ * stale, twice, so a header that names a module path the table also names is
18
+ * a finding rather than a nicety.
19
+ * 3. NO PERCENTAGE — the structural-hiding detector publishes no recall or
20
+ * coverage rate. A rate needs a frozen corpus of hiding techniques and none
21
+ * exists; a number would be an invented denominator that a later reader
22
+ * quotes as measured coverage. Enforced rather than asserted, because a
23
+ * prose promise not to publish a number is the weakest control available.
24
+ *
25
+ * WHAT A GREEN RUN PROVES, AND WHAT IT DOES NOT
26
+ * ---------------------------------------------
27
+ * Green means the published table agrees with the tree's import graph. It
28
+ * proves nothing about whether any module applies the floor to the right
29
+ * string. `covered` is "the floor is in scope in this module"; establishing
30
+ * that it is actually reached needs a read of the module, and this gate does
31
+ * not pretend otherwise — the table header says so in its own first lines.
32
+ */
33
+ import * as fs from 'node:fs';
34
+ import * as os from 'node:os';
35
+ import * as path from 'node:path';
36
+ import process from 'node:process';
37
+ import { fileURLToPath, pathToFileURL } from 'node:url';
38
+
39
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
40
+ import {
41
+ LOCAL_VCS_EXCLUSION,
42
+ type ReadSurfaceRow,
43
+ SCAN_ROOT,
44
+ renderTable,
45
+ scanReadSurfaces,
46
+ tally,
47
+ } from './_lib/read_surface_scan.js';
48
+ import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
49
+
50
+ const _HERE = fileURLToPath(import.meta.url);
51
+ const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
52
+
53
+ const GATE = 'check_read_surface_coverage';
54
+ export const DOC_REL = 'docs/contracts/retrieval-read-surfaces.md';
55
+ export const FLOOR_REL = 'src/scripts/_lib/retrieval_sanitize.ts';
56
+ export const DETECTOR_REL = 'src/scripts/_lib/structural_hiding.ts';
57
+
58
+ export const BEGIN_MARKER = '<!-- BEGIN read-surface-table (generated) -->';
59
+ export const END_MARKER = '<!-- END read-surface-table -->';
60
+
61
+ const REGEN_COMMAND = `./scripts-run src/scripts/${GATE} --write`;
62
+
63
+ function _read(root: string, rel: string): string | null {
64
+ try {
65
+ return fs.readFileSync(path.join(root, rel), 'utf-8');
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /** The leading block comment of a module — its header, and nothing after it. */
72
+ export function leadingBlockComment(src: string): string {
73
+ const m = /^\s*\/\*\*[\s\S]*?\*\//.exec(src);
74
+ return m === null ? '' : m[0];
75
+ }
76
+
77
+ /** Render the full generated region, header prose included. */
78
+ export function renderRegion(rows: readonly ReadSurfaceRow[]): string {
79
+ return [
80
+ '> Generated by `' + GATE + '`. Do not edit between the markers — a cell',
81
+ '> edited by hand is overwritten by the next regeneration and reported by the',
82
+ '> gate in between. Corrections belong in the tree or in the scanner.',
83
+ '',
84
+ '**A row is a claim about imports, not about safety.** `covered` means the',
85
+ 'module imports the sanitize floor, so the floor is in scope there. It does',
86
+ 'not say the floor is applied to the right string or at the right point —',
87
+ 'that needs a read of the module, and no import graph can supply it.',
88
+ '',
89
+ '**What is out of scope, said here rather than left to be discovered.** ' +
90
+ LOCAL_VCS_EXCLUSION,
91
+ '',
92
+ 'A module reaching the network through a shape in neither the strong nor the',
93
+ 'weak detector set is absent from this table entirely. That residual is the',
94
+ 'reason `unclassified` exists and the reason this paragraph does.',
95
+ '',
96
+ renderTable(rows),
97
+ ].join('\n');
98
+ }
99
+
100
+ /** Replace the marked region in `doc`, or append one when absent. */
101
+ export function spliceRegion(doc: string, region: string): string {
102
+ const begin = doc.indexOf(BEGIN_MARKER);
103
+ const end = doc.indexOf(END_MARKER);
104
+ if (begin === -1 || end === -1 || end < begin) {
105
+ return `${doc.trimEnd()}\n\n${BEGIN_MARKER}\n${region}\n${END_MARKER}\n`;
106
+ }
107
+ return `${doc.slice(0, begin)}${BEGIN_MARKER}\n${region}\n${doc.slice(end)}`;
108
+ }
109
+
110
+ /** Extract the marked region's body, or `null` when the markers are missing. */
111
+ export function extractRegion(doc: string): string | null {
112
+ const begin = doc.indexOf(BEGIN_MARKER);
113
+ const end = doc.indexOf(END_MARKER);
114
+ if (begin === -1 || end === -1 || end < begin) return null;
115
+ return doc.slice(begin + BEGIN_MARKER.length, end).trim();
116
+ }
117
+
118
+ /**
119
+ * Module paths the floor's header restates instead of pointing at.
120
+ *
121
+ * Matching on the basename keeps the check honest against a header that names
122
+ * `memory_lookup.ts` without its directory — the shape the previous header
123
+ * actually used.
124
+ */
125
+ export function headerSecondCopy(header: string, rows: readonly ReadSurfaceRow[]): string[] {
126
+ const hits = new Set<string>();
127
+ for (const r of rows) {
128
+ const base = r.module.split('/').pop() ?? r.module;
129
+ if (header.includes(base)) hits.add(base);
130
+ }
131
+ return [...hits].sort();
132
+ }
133
+
134
+ interface Options {
135
+ root: string;
136
+ write: boolean;
137
+ quiet: boolean;
138
+ }
139
+
140
+ /**
141
+ * A published rate — `99 %`, `72.33%`, `recall: 0.98`.
142
+ *
143
+ * Scoped to the DETECTOR FILE and nothing else, deliberately. The codepoint
144
+ * floor next door publishes two real, corpus-backed percentages in its own
145
+ * docblock; a check that swept every citing surface would red on those and be
146
+ * switched off within a week. The claim this rule defends is narrow — the
147
+ * STRUCTURAL layer has no frozen corpus, so it has no denominator — and the
148
+ * check is scoped to exactly that claim.
149
+ */
150
+ const PERCENTAGE_RE = /\b\d+(?:\.\d+)?\s*%|\brecall\s*[:=]\s*\d/i;
151
+
152
+ /** The register's exported name, cited by every surface claiming the coverage. */
153
+ export const GAP_REGISTER_SYMBOL = 'STRUCTURAL_HIDING_GAPS';
154
+
155
+ /**
156
+ * True when a percentage or recall figure is PUBLISHED in the detector.
157
+ *
158
+ * Inline code spans are removed before the test, and that exclusion is the
159
+ * difference between a rule and a nuisance. The gap register names
160
+ * `transform: translate(-100%, 0)` as an off-screen hiding technique — a CSS
161
+ * literal inside backticks, not a claim about this detector's coverage. The
162
+ * first version of this check red on exactly that and the fix belongs here,
163
+ * in the predicate, rather than in the register's wording: a gate that makes
164
+ * honest prose contort around it gets switched off. Both directions are pinned
165
+ * by self-test cases, because a widened matcher that no longer catches the
166
+ * thing it was written for fails silently.
167
+ */
168
+ export function publishesRate(detectorSrc: string): boolean {
169
+ for (const line of detectorSrc.split('\n')) {
170
+ // The rule states the prohibition in prose; the prose is not a breach.
171
+ if (line.includes('PERCENTAGE_RE') || line.includes('no recall or coverage')) continue;
172
+ const prose = line.replace(/`[^`]*`/g, '');
173
+ if (PERCENTAGE_RE.test(prose)) return true;
174
+ }
175
+ return false;
176
+ }
177
+
178
+ function parseArgs(argv: readonly string[]): Options {
179
+ const out: Options = { root: ROOT, write: false, quiet: false };
180
+ for (let i = 0; i < argv.length; i += 1) {
181
+ const a = argv[i];
182
+ if (a === '--root') {
183
+ out.root = path.resolve(argv[i + 1] ?? '.');
184
+ i += 1;
185
+ } else if (a === '--write') out.write = true;
186
+ else if (a === '--quiet') out.quiet = true;
187
+ }
188
+ return out;
189
+ }
190
+
191
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
192
+ if (argv.includes('--self-test')) return selfTest();
193
+ const opts = parseArgs(argv);
194
+ const say = (s: string): void => {
195
+ if (!opts.quiet) process.stdout.write(s);
196
+ };
197
+
198
+ const rows = scanReadSurfaces(opts.root);
199
+ const region = renderRegion(rows);
200
+
201
+ if (opts.write) {
202
+ const existing = _read(opts.root, DOC_REL) ?? `# Retrieval read surfaces\n`;
203
+ fs.mkdirSync(path.join(opts.root, path.dirname(DOC_REL)), { recursive: true });
204
+ fs.writeFileSync(path.join(opts.root, DOC_REL), spliceRegion(existing, region), 'utf-8');
205
+ process.stdout.write(`✍️ ${GATE}: wrote ${String(rows.length)} row(s) to ${DOC_REL}\n`);
206
+ return 0;
207
+ }
208
+
209
+ const findings: string[] = [];
210
+
211
+ // 1. Drift between the tree and the committed table.
212
+ const doc = _read(opts.root, DOC_REL);
213
+ if (doc === null) {
214
+ findings.push(`${DOC_REL} is missing — the generated table has no home.`);
215
+ } else {
216
+ const committed = extractRegion(doc);
217
+ if (committed === null) {
218
+ findings.push(`${DOC_REL} carries no \`${BEGIN_MARKER}\` … \`${END_MARKER}\` region.`);
219
+ } else if (committed !== region.trim()) {
220
+ findings.push(
221
+ `${DOC_REL} disagrees with the tree. The table is generated; regenerate it.`,
222
+ );
223
+ }
224
+ }
225
+
226
+ // 2. The floor's header points at the table rather than restating it.
227
+ const floor = _read(opts.root, FLOOR_REL);
228
+ if (floor === null) {
229
+ findings.push(`${FLOOR_REL} is missing — the floor this table describes is gone.`);
230
+ } else {
231
+ const header = leadingBlockComment(floor);
232
+ if (!header.includes(DOC_REL)) {
233
+ findings.push(
234
+ `${FLOOR_REL}'s header does not point at ${DOC_REL}. The coverage list is ` +
235
+ 'generated; the header carries the pointer, never a second copy.',
236
+ );
237
+ }
238
+ const copies = headerSecondCopy(header, rows);
239
+ if (copies.length > 0) {
240
+ findings.push(
241
+ `${FLOOR_REL}'s header names ${String(copies.length)} module(s) the table also ` +
242
+ `names (${copies.join(', ')}). Two copies of this list is how it went stale ` +
243
+ 'twice; the header points, the table lists.',
244
+ );
245
+ }
246
+ }
247
+
248
+ // 3. NO PERCENTAGE, and the gap register travels with the claim.
249
+ //
250
+ // This rule was REMOVED once, correctly: it watched a detector file that
251
+ // did not exist, and both review seats called it dead policy. It returns
252
+ // here with the detector, written against a file that exists, with its own
253
+ // rejecting self-test cases. If the detector is absent the rule is silent
254
+ // rather than red — a missing detector is Phase 3 being open, not a breach.
255
+ const detector = _read(opts.root, DETECTOR_REL);
256
+ if (detector !== null) {
257
+ if (publishesRate(detector)) {
258
+ findings.push(
259
+ `${DETECTOR_REL} publishes a recall or coverage figure. A rate needs a frozen ` +
260
+ 'corpus of hiding techniques and none exists in this tree, so the number ' +
261
+ 'would be an invented denominator a later reader quotes as measured coverage. ' +
262
+ 'State the covered-channel list and the gap register instead.',
263
+ );
264
+ }
265
+ if (!detector.includes(GAP_REGISTER_SYMBOL)) {
266
+ findings.push(
267
+ `${DETECTOR_REL} does not define ${GAP_REGISTER_SYMBOL}. The register of what the ` +
268
+ "detector does NOT catch belongs in the detector's own file — a detector " +
269
+ 'documented without its gaps is how the coverage claim becomes a false green.',
270
+ );
271
+ }
272
+ for (const [rel, src] of [
273
+ [FLOOR_REL, floor],
274
+ [DOC_REL, doc],
275
+ ] as const) {
276
+ if (src === null) continue;
277
+ if (!src.includes('structural_hiding')) continue; // claims nothing
278
+ if (src.includes(GAP_REGISTER_SYMBOL)) continue;
279
+ findings.push(
280
+ `${rel} claims the structural coverage without citing ${GAP_REGISTER_SYMBOL}. ` +
281
+ 'Every surface that claims this coverage cites the register alongside the ' +
282
+ 'claim, so the caveat travels with the claim rather than staying in one file.',
283
+ );
284
+ }
285
+ }
286
+
287
+ try {
288
+ reportScanned({
289
+ gate: GATE,
290
+ scanned: rows.length,
291
+ units: 'read surface(s)',
292
+ roots: [SCAN_ROOT],
293
+ });
294
+ } catch (e) {
295
+ if (e instanceof DeadScopeError) {
296
+ process.stdout.write(`❌ ${e.message}\n`);
297
+ return 1;
298
+ }
299
+ throw e;
300
+ }
301
+
302
+ if (findings.length > 0) {
303
+ for (const f of findings) process.stdout.write(`❌ ${GATE}: ${f}\n`);
304
+ process.stdout.write(`\n${String(findings.length)} finding(s). Regenerate with:\n ${REGEN_COMMAND}\n`);
305
+ return 1;
306
+ }
307
+ const t = tally(rows);
308
+ say(
309
+ `✅ ${GATE}: ${String(rows.length)} read surface(s) in ${DOC_REL} match the tree ` +
310
+ `(${String(t.covered)} covered, ${String(t.uncovered)} uncovered, ` +
311
+ `${String(t.unclassified)} unclassified).\n`,
312
+ );
313
+ return 0;
314
+ }
315
+
316
+ /**
317
+ * One rejecting case per way the three checks can be defeated.
318
+ *
319
+ * The empty-root case is the one that matters most here: a gate whose scan root
320
+ * moved reads zero surfaces and would otherwise certify a table it never
321
+ * compared. `assertScanned` has to turn that into a refusal, and only a fixture
322
+ * with no `src/scripts/` at all proves it does.
323
+ */
324
+ function selfTest(): number {
325
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'read-surface-'));
326
+ const floor = _read(ROOT, FLOOR_REL) ?? '';
327
+
328
+ /** Plant a fixture root with a one-module scan tree plus the three inputs. */
329
+ const plant = (
330
+ name: string,
331
+ opts: {
332
+ module?: string;
333
+ mutateRegion?: (r: string) => string;
334
+ floorSrc?: string;
335
+ detectorSrc?: string;
336
+ noScanRoot?: boolean;
337
+ },
338
+ ): string => {
339
+ const dir = path.join(tmp, name);
340
+ if (opts.noScanRoot !== true) {
341
+ fs.mkdirSync(path.join(dir, SCAN_ROOT, '_lib'), { recursive: true });
342
+ fs.writeFileSync(
343
+ path.join(dir, SCAN_ROOT, 'seed_surface.ts'),
344
+ opts.module ??
345
+ 'export async function pull(u: string): Promise<string> {\n' +
346
+ ' const r = await fetch(u);\n' +
347
+ ' return await r.text();\n}\n',
348
+ 'utf-8',
349
+ );
350
+ } else {
351
+ fs.mkdirSync(dir, { recursive: true });
352
+ }
353
+ fs.mkdirSync(path.join(dir, path.dirname(FLOOR_REL)), { recursive: true });
354
+ fs.mkdirSync(path.join(dir, path.dirname(DOC_REL)), { recursive: true });
355
+ fs.writeFileSync(path.join(dir, FLOOR_REL), opts.floorSrc ?? floor, 'utf-8');
356
+ // The detector is planted ONLY where a case needs it. Absent, rule 3 is
357
+ // silent — which is itself the behaviour the last case below pins.
358
+ if (opts.detectorSrc !== undefined) {
359
+ fs.mkdirSync(path.join(dir, path.dirname(DETECTOR_REL)), { recursive: true });
360
+ fs.writeFileSync(path.join(dir, DETECTOR_REL), opts.detectorSrc, 'utf-8');
361
+ }
362
+ const region = renderRegion(scanReadSurfaces(dir));
363
+ fs.writeFileSync(
364
+ path.join(dir, DOC_REL),
365
+ `# fixture\n\n${BEGIN_MARKER}\n${(opts.mutateRegion ?? ((r) => r))(region)}\n${END_MARKER}\n`,
366
+ 'utf-8',
367
+ );
368
+ return dir;
369
+ };
370
+
371
+ const run = (root: string): number =>
372
+ runGateCli(ROOT, `src/scripts/${GATE}.ts`, ['--root', root], root);
373
+
374
+ try {
375
+ return runSelfTest({
376
+ gate: GATE,
377
+ minCases: 10,
378
+ minRejectCases: 7,
379
+ cases: [
380
+ {
381
+ name: 'an unsanitized fetch-derived emit the committed table does not carry is rejected',
382
+ expect: 'reject',
383
+ run: () => {
384
+ const dir = plant('seeded', {});
385
+ fs.writeFileSync(
386
+ path.join(dir, SCAN_ROOT, 'seeded_leak.ts'),
387
+ 'export async function grab(u: string): Promise<void> {\n' +
388
+ ' const r = await fetch(u);\n' +
389
+ ' writeFileSync("out.md", await r.text());\n}\n',
390
+ 'utf-8',
391
+ );
392
+ return run(dir);
393
+ },
394
+ },
395
+ {
396
+ name: 'a hand-flipped coverage cell is rejected',
397
+ expect: 'reject',
398
+ run: () =>
399
+ run(
400
+ plant('flipped', {
401
+ mutateRegion: (r) => r.replace('**uncovered**', '**covered**'),
402
+ }),
403
+ ),
404
+ },
405
+ {
406
+ name: "a header restating a module path the table lists is rejected",
407
+ expect: 'reject',
408
+ run: () =>
409
+ run(
410
+ plant('second-copy', {
411
+ floorSrc: `/**\n * Floor.\n * See ${DOC_REL}.\n * - seed_surface.ts — yes\n */\nexport const x = 1;\n`,
412
+ }),
413
+ ),
414
+ },
415
+ {
416
+ name: 'a header with no pointer to the generated table is rejected',
417
+ expect: 'reject',
418
+ run: () => run(plant('no-pointer', { floorSrc: '/**\n * Floor.\n */\nexport const x = 1;\n' })),
419
+ },
420
+ {
421
+ name: 'an empty scan root is refused rather than certified',
422
+ expect: 'reject',
423
+ run: () => run(plant('empty-root', { noScanRoot: true })),
424
+ },
425
+ {
426
+ name: 'a detector publishing a coverage percentage is rejected',
427
+ expect: 'reject',
428
+ run: () =>
429
+ run(
430
+ plant('rate-published', {
431
+ floorSrc: `/**\n * Floor. Coverage list: ${DOC_REL}.\n * Gaps: ${GAP_REGISTER_SYMBOL}.\n */\nexport const x = 1;\n`,
432
+ detectorSrc:
433
+ `export const ${GAP_REGISTER_SYMBOL} = ['stylesheet-driven hiding'];\n` +
434
+ '/** Measured recall over the channel corpus: 94.2 %. */\n' +
435
+ 'export const y = 1;\n',
436
+ }),
437
+ ),
438
+ },
439
+ {
440
+ name: 'a detector with no gap register is rejected',
441
+ expect: 'reject',
442
+ run: () =>
443
+ run(
444
+ plant('no-register', {
445
+ floorSrc: `/**\n * Floor. Coverage list: ${DOC_REL}.\n * Gaps: ${GAP_REGISTER_SYMBOL}.\n */\nexport const x = 1;\n`,
446
+ detectorSrc: 'export const y = 1;\n',
447
+ }),
448
+ ),
449
+ },
450
+ {
451
+ name: 'a floor claiming the coverage without citing the gap register is rejected',
452
+ expect: 'reject',
453
+ run: () =>
454
+ run(
455
+ plant('register-uncited', {
456
+ floorSrc: `/**\n * Floor. Coverage list: ${DOC_REL}. Structural: structural_hiding.\n */\nexport const x = 1;\n`,
457
+ detectorSrc: `export const ${GAP_REGISTER_SYMBOL} = ['stylesheet-driven hiding'];\n`,
458
+ }),
459
+ ),
460
+ },
461
+ {
462
+ name: 'a CSS percentage inside a code span is NOT read as a published rate',
463
+ expect: 'accept',
464
+ run: () =>
465
+ run(
466
+ plant('rate-in-code-span', {
467
+ floorSrc: `/**\n * Floor. Coverage list: ${DOC_REL}.\n * Gaps: ${GAP_REGISTER_SYMBOL}.\n */\nexport const x = 1;\n`,
468
+ detectorSrc:
469
+ `export const ${GAP_REGISTER_SYMBOL} = [\n` +
470
+ " 'off-screen positioning — `transform: translate(-100%, 0)`',\n" +
471
+ '];\n',
472
+ }),
473
+ ),
474
+ },
475
+ {
476
+ name: 'a detector that exists, publishes no rate and carries its register passes',
477
+ expect: 'accept',
478
+ run: () =>
479
+ run(
480
+ plant('detector-clean', {
481
+ floorSrc: `/**\n * Floor. Coverage list: ${DOC_REL}.\n * Gaps: ${GAP_REGISTER_SYMBOL}.\n */\nexport const x = 1;\n`,
482
+ detectorSrc: `export const ${GAP_REGISTER_SYMBOL} = ['stylesheet-driven hiding'];\n`,
483
+ }),
484
+ ),
485
+ },
486
+ {
487
+ name: 'a regenerated table over an intact tree passes',
488
+ expect: 'accept',
489
+ run: () =>
490
+ run(
491
+ plant('good', {
492
+ floorSrc: `/**\n * Floor. Coverage list: ${DOC_REL}.\n */\nexport const x = 1;\n`,
493
+ }),
494
+ ),
495
+ },
496
+ ],
497
+ });
498
+ } finally {
499
+ fs.rmSync(tmp, { recursive: true, force: true });
500
+ }
501
+ }
502
+
503
+ function _isCliEntry(): boolean {
504
+ if (process.argv[1] === undefined) return false;
505
+ if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
506
+ try {
507
+ return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
508
+ } catch {
509
+ return false;
510
+ }
511
+ }
512
+
513
+ if (_isCliEntry()) {
514
+ process.exit(main());
515
+ }
@@ -45,7 +45,8 @@ const _HERE = fileURLToPath(import.meta.url);
45
45
  /**
46
46
  * The folders a release question must read.
47
47
  *
48
- * Wider than `lint_roadmap_blockers`' active-only glob on purpose: rule 28's
48
+ * Wider than `lint_roadmap_blockers`' glob on purpose — that gate reads the
49
+ * active tree plus `stubs/` and stops there (2026-09-28), while rule 28's
49
50
  * per-folder lifecycle says a window does not disappear by moving the file, and
50
51
  * a `later/` roadmap still refuses. `archive/` and `skipped/` are included
51
52
  * because the move INTO them is refused while a window is open — a file that