@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
@@ -55,6 +55,8 @@
55
55
  import * as fs from 'node:fs';
56
56
  import * as os from 'node:os';
57
57
  import * as path from 'node:path';
58
+
59
+ import * as yaml from 'js-yaml';
58
60
  import process from 'node:process';
59
61
  import { fileURLToPath, pathToFileURL } from 'node:url';
60
62
 
@@ -88,12 +90,28 @@ export const CONFIGURED_OUTCOMES = ['refusal', 'halt-by-state', 'warning', 'unen
88
90
 
89
91
  export type ConfiguredOutcome = (typeof CONFIGURED_OUTCOMES)[number];
90
92
 
93
+ /**
94
+ * What the `Answered` column prints for a pair the configuration never dated.
95
+ *
96
+ * A word rather than an empty cell, because an empty cell in a rendered table
97
+ * is indistinguishable from a rendering bug, and this one is the finding.
98
+ * `lint_hook_manifest._check_slot_answers` is what actually refuses it; this
99
+ * gate's job is to make sure the published document cannot hide it.
100
+ */
101
+ export const UNDATED = '`undated`';
102
+
91
103
  export interface MatrixRow {
92
104
  host: string;
93
105
  slot: string;
94
106
  outcome: ConfiguredOutcome;
95
107
  /** The configuration values the outcome was read off, quoted for the table. */
96
108
  backing: string;
109
+ /**
110
+ * `answered_at` for this pair, or `undated` when the configuration carries
111
+ * none. Printed LAST so the outcome stays in column three and
112
+ * {@link committedCells} keeps reading the same index it always did.
113
+ */
114
+ answered: string;
97
115
  }
98
116
 
99
117
  export interface BuildResult {
@@ -192,6 +210,7 @@ export function buildRows(lowering: HostLowering): BuildResult {
192
210
  slot: reading.slot,
193
211
  outcome,
194
212
  backing: backingFor(reading, surface.fail_policy),
213
+ answered: surface.slots.get(reading.slot)?.answered_at ?? UNDATED,
195
214
  });
196
215
  }
197
216
  }
@@ -295,10 +314,12 @@ export function renderRegion(lowering: HostLowering): string {
295
314
  out.push('');
296
315
  out.push(summaryLine(lowering, rows));
297
316
  out.push('');
298
- out.push('| Host | Slot | Configured outcome | Backing |');
299
- out.push('|---|---|---|---|');
317
+ out.push('| Host | Slot | Configured outcome | Backing | Answered |');
318
+ out.push('|---|---|---|---|---|');
300
319
  for (const r of rows) {
301
- out.push(`| \`${r.host}\` | \`${r.slot}\` | \`${r.outcome}\` | ${r.backing} |`);
320
+ out.push(
321
+ `| \`${r.host}\` | \`${r.slot}\` | \`${r.outcome}\` | ${r.backing} | ${r.answered} |`,
322
+ );
302
323
  }
303
324
  out.push('');
304
325
  if (slotlessHosts.length === 0) {
@@ -394,6 +415,220 @@ function _read(root: string, rel: string): string | null {
394
415
  /** Flags this gate understands. `--root` is the only one taking a value. */
395
416
  const KNOWN_FLAGS: ReadonlySet<string> = new Set(['--self-test', '--root', '--write', '--quiet']);
396
417
 
418
+ /** Where the concern→slot bindings and per-concern severities live. */
419
+ export const MANIFEST_REL = path.join('src', 'scripts', 'hook_manifest.yaml');
420
+
421
+ /**
422
+ * Why a bound concern's verdict cannot leave its slot.
423
+ *
424
+ * Three states, not two, and the third is the one an earlier version of this
425
+ * audit got wrong by skipping it. They are ordered worst-first:
426
+ *
427
+ * - `unlowerable` — the lowering table carries NO row for this slot on this
428
+ * host, so the installer writes no native binding at all.
429
+ * `cowork` is the worked case: `slots: {}` against sixty-odd
430
+ * declared bindings. Skipping it printed `0/0 · 0 blocking`,
431
+ * byte-identical to a host that binds nothing — a false
432
+ * green on the host with the largest gap.
433
+ * - `null-block` — a row exists and its literal `block_exit` is null. The
434
+ * slot is wired and a refusal has nowhere to go.
435
+ * - `stale-proof` — the literal `block_exit` CAN deny, but the row's
436
+ * `verified` block has expired, so `effective` reads null.
437
+ * This is a statement about this package's provenance, NOT
438
+ * about the host: `host_lowering.yaml` says in its own
439
+ * header that an absent `verified` "does NOT mean the host
440
+ * cannot enforce". Printing "cannot deny" here would
441
+ * assert exactly the host fact that file disclaims, so it
442
+ * is reported as its own class and worded differently.
443
+ */
444
+ export type NoDenyReason = 'unlowerable' | 'null-block' | 'stale-proof';
445
+
446
+ /** One concern bound where its verdict cannot produce a refusal. */
447
+ export interface NullBlockBinding {
448
+ readonly host: string;
449
+ readonly slot: string;
450
+ readonly concern: string;
451
+ /** The concern's declared `severity`, or `(undeclared)`. */
452
+ readonly severity: string;
453
+ readonly reason: NoDenyReason;
454
+ }
455
+
456
+ /** What {@link concernSlotAudit} establishes for one host. */
457
+ export interface HostConcernAudit {
458
+ readonly host: string;
459
+ /** Lowerable slots whose literal `block_exit` can deny. */
460
+ readonly denySlots: number;
461
+ /** Lowerable slots in total. */
462
+ readonly lowerableSlots: number;
463
+ /** Distinct slots this host binds a concern to, whether lowerable or not. */
464
+ readonly boundSlots: number;
465
+ /**
466
+ * Bindings whose verdict cannot become a refusal, worst reason first and
467
+ * `blocking` severity ahead of the rest within a reason.
468
+ *
469
+ * An `advisory` concern here is CONSISTENT, not a defect — most concerns are
470
+ * advisory and belong on a reporting slot. The row that matters is a
471
+ * `blocking` severity: a concern declared to refuse, bound where a refusal
472
+ * has nowhere to go.
473
+ */
474
+ readonly noDeny: readonly NullBlockBinding[];
475
+ }
476
+
477
+ /** Slots that carry a dispatcher verdict. `ask` is a UI affordance, not one. */
478
+ const NON_VERDICT_SLOTS: ReadonlySet<string> = new Set(['ask']);
479
+
480
+ /**
481
+ * Cross-reference the manifest's concern bindings against the lowering table's
482
+ * per-slot `block_exit`.
483
+ *
484
+ * road-to-a-content-scanner-on-a-slot-that-can-refuse 2.1. The gate above
485
+ * compares published host-SLOT rows against the configuration and is green
486
+ * whenever those agree — it never asks whether a CONCERN is bound to a slot
487
+ * that can carry its severity. That question was answered once, by an external
488
+ * reader comparing two files by hand, which is why it drifted back: a hand
489
+ * comparison leaves no number behind. This makes it a printed count.
490
+ *
491
+ * DELIBERATELY NOT A FAILURE. It prints; the exit code is unchanged. Failing
492
+ * would red the tree on bindings that predate this check — the same reason the
493
+ * continuity and estate ratchets report distance-to-target instead of gating on
494
+ * it. What a reader gets is a number that moves when a binding or a slot moves.
495
+ *
496
+ * NOTHING BOUND IS SILENTLY DROPPED. Every bound slot lands in exactly one of
497
+ * {can deny, one of the three {@link NoDenyReason} classes, non-verdict}, and
498
+ * `boundSlots` publishes the denominator so a reader can check the arithmetic
499
+ * rather than trust it. That is the correction the first version needed: it
500
+ * skipped any slot missing from the lowering table, which is precisely the
501
+ * largest gap in the tree.
502
+ */
503
+ export function concernSlotAudit(root: string, lowering: HostLowering): HostConcernAudit[] {
504
+ const manifestText = _read(root, MANIFEST_REL);
505
+ if (manifestText === null) return [];
506
+ let manifest: unknown;
507
+ try {
508
+ manifest = yaml.load(manifestText);
509
+ } catch {
510
+ return [];
511
+ }
512
+ if (manifest === null || typeof manifest !== 'object') return [];
513
+ const m = manifest as Record<string, unknown>;
514
+
515
+ const severities = new Map<string, string>();
516
+ const concerns = m['concerns'];
517
+ if (concerns !== null && typeof concerns === 'object') {
518
+ for (const [name, spec] of Object.entries(concerns as Record<string, unknown>)) {
519
+ const sev =
520
+ spec !== null && typeof spec === 'object'
521
+ ? (spec as Record<string, unknown>)['severity']
522
+ : undefined;
523
+ severities.set(name, typeof sev === 'string' ? sev : '(undeclared)');
524
+ }
525
+ }
526
+
527
+ const platforms = m['platforms'];
528
+ if (platforms === null || typeof platforms !== 'object') return [];
529
+
530
+ const out: HostConcernAudit[] = [];
531
+ for (const [host, blockRaw] of Object.entries(platforms as Record<string, unknown>)) {
532
+ const reading = readSlots(lowering, host);
533
+ const rows = new Map<string, SlotReading>(reading.slots.map((s: SlotReading) => [s.slot, s]));
534
+ const denySlots = reading.slots.filter((s: SlotReading) => s.literal !== null).length;
535
+
536
+ const noDeny: NullBlockBinding[] = [];
537
+ let boundSlots = 0;
538
+ if (blockRaw !== null && typeof blockRaw === 'object') {
539
+ for (const [slot, value] of Object.entries(blockRaw as Record<string, unknown>)) {
540
+ if (!Array.isArray(value)) continue;
541
+ if (NON_VERDICT_SLOTS.has(slot)) continue;
542
+ boundSlots += 1;
543
+ const row = rows.get(slot);
544
+ // The literal decides whether a refusal can leave the slot; the
545
+ // `verified` currency decides only whether this package has
546
+ // cited its proof. Reading `effective` here would turn an
547
+ // expired citation into a claim that the host cannot enforce.
548
+ const reason: NoDenyReason | null =
549
+ row === undefined
550
+ ? 'unlowerable'
551
+ : row.literal === null
552
+ ? 'null-block'
553
+ : row.effective === null
554
+ ? 'stale-proof'
555
+ : null;
556
+ if (reason === null) continue;
557
+ for (const concern of value) {
558
+ if (typeof concern !== 'string') continue;
559
+ noDeny.push({
560
+ host,
561
+ slot,
562
+ concern,
563
+ severity: severities.get(concern) ?? '(unknown concern)',
564
+ reason,
565
+ });
566
+ }
567
+ }
568
+ }
569
+ const reasonRank = (r: NoDenyReason): number =>
570
+ r === 'unlowerable' ? 0 : r === 'null-block' ? 1 : 2;
571
+ const sevRank = (s: string): number => (s === 'blocking' ? 0 : s === 'advisory' ? 2 : 1);
572
+ noDeny.sort(
573
+ (a, b) =>
574
+ reasonRank(a.reason) - reasonRank(b.reason) ||
575
+ sevRank(a.severity) - sevRank(b.severity) ||
576
+ a.slot.localeCompare(b.slot) ||
577
+ a.concern.localeCompare(b.concern),
578
+ );
579
+ out.push({
580
+ host,
581
+ denySlots,
582
+ lowerableSlots: reading.slots.length,
583
+ boundSlots,
584
+ noDeny,
585
+ });
586
+ }
587
+ return out;
588
+ }
589
+
590
+ /** How each {@link NoDenyReason} is worded. `stale-proof` never says "cannot". */
591
+ const REASON_TEXT: Readonly<Record<NoDenyReason, string>> = {
592
+ unlowerable: 'no lowering row for this slot — nothing is bound natively at all',
593
+ 'null-block': 'block_exit is null — a refusal has nowhere to go',
594
+ 'stale-proof': 'block_exit can deny, but this row’s `verified` proof has expired',
595
+ };
596
+
597
+ /** Render the audit as the lines the reporter prints. */
598
+ export function renderConcernAudit(audit: readonly HostConcernAudit[]): string[] {
599
+ const lines: string[] = [];
600
+ lines.push('');
601
+ lines.push(`${GATE} · concerns bound where a verdict cannot become a refusal`);
602
+ lines.push(
603
+ ' A blocking severity here is a concern declared to refuse, bound where a refusal',
604
+ );
605
+ lines.push(' cannot happen. An advisory one is consistent and is counted, not flagged.');
606
+ lines.push(' `stale-proof` is a statement about this package’s citation, never about the host.');
607
+ for (const h of audit) {
608
+ const blocking = h.noDeny.filter((b) => b.severity === 'blocking');
609
+ const others = h.noDeny.length - blocking.length;
610
+ lines.push(
611
+ ` ${h.host.padEnd(10)} ${String(h.denySlots)}/${String(h.lowerableSlots)} lowerable slot(s) can deny · ` +
612
+ `${String(h.boundSlots)} verdict-bearing slot(s) bound · ` +
613
+ `${String(blocking.length)} blocking + ${String(others)} non-blocking binding(s) cannot refuse`,
614
+ );
615
+ // One line per (reason, slot) rather than per concern: a host with an
616
+ // empty `slots:` map has sixty-odd bindings and the reason is the fact,
617
+ // not the roster.
618
+ const seen = new Set<string>();
619
+ for (const b of h.noDeny) {
620
+ if (b.severity !== 'blocking') continue;
621
+ const key = `${b.reason}/${b.slot}`;
622
+ if (!seen.has(key)) {
623
+ seen.add(key);
624
+ lines.push(` ⚠️ ${b.slot} — ${REASON_TEXT[b.reason]}`);
625
+ }
626
+ lines.push(` ${b.concern} (${b.severity})`);
627
+ }
628
+ }
629
+ return lines;
630
+ }
631
+
397
632
  export function main(argv?: readonly string[]): number {
398
633
  const args = argv ?? process.argv.slice(2);
399
634
  // An unrecognised flag is refused rather than ignored. Both ratification
@@ -517,6 +752,13 @@ export function main(argv?: readonly string[]): number {
517
752
  });
518
753
  ledger.report();
519
754
 
755
+ // The concern-vs-slot audit prints on every read-only run, before the
756
+ // verdict, so it is visible whether the row comparison passed or failed.
757
+ // It never changes the exit code — see `concernSlotAudit`.
758
+ for (const line of renderConcernAudit(concernSlotAudit(root, lowering))) {
759
+ process.stdout.write(`${line}\n`);
760
+ }
761
+
520
762
  if (findings.length > 0) {
521
763
  for (const f of findings) process.stdout.write(`❌ ${GATE}: ${f}\n`);
522
764
  process.stdout.write(
@@ -95,6 +95,12 @@ export interface LedgerFinding {
95
95
  rationale?: string;
96
96
  verified_by?: string;
97
97
  date?: string;
98
+ /**
99
+ * The tree's mechanical disproof of the finding's premise, written by
100
+ * `self_review_gate.annotateContradicted`. A finding carrying one is not
101
+ * blocking — see `isBlocking`.
102
+ */
103
+ contradicted?: string;
98
104
  }
99
105
 
100
106
  export interface Ledger {
@@ -220,8 +226,21 @@ function _clean_review_reason(artifact: Record<string, unknown>): string {
220
226
  );
221
227
  }
222
228
 
223
- /** Mirrors self_review_gate.classifyBlocking — security/claim × critical/high. */
224
- export function isBlocking(f: Pick<LedgerFinding, 'kind' | 'severity'>): boolean {
229
+ /**
230
+ * Mirrors self_review_gate.classifyBlocking — security/claim × critical/high,
231
+ * MINUS anything the tree disproved.
232
+ *
233
+ * The `contradicted` clause is not a second policy; it is the same one, and
234
+ * the mirror has to carry it or the two gates disagree: the review would stop
235
+ * counting a disproved deletion claim as blocking while the release kept
236
+ * demanding a disposition for it. A `contradicted` value is written only by
237
+ * `annotateContradicted`, which checks the claim against
238
+ * `git diff --name-status base...HEAD` AND the working tree.
239
+ */
240
+ export function isBlocking(f: Pick<LedgerFinding, 'kind' | 'severity' | 'contradicted'>): boolean {
241
+ if ((f.contradicted ?? '').trim() !== '') {
242
+ return false;
243
+ }
225
244
  return (
226
245
  (f.kind === 'security' || f.kind === 'claim') &&
227
246
  (f.severity === 'critical' || f.severity === 'high')
@@ -413,7 +432,7 @@ export function resolve_release_status(release: string): ReleaseStatus {
413
432
 
414
433
  /** Findings the gate comment reported but the ledger never ingested. */
415
434
  export function unrecorded_findings(
416
- reported: ReadonlyArray<Pick<LedgerFinding, 'finding_id' | 'severity' | 'kind' | 'title'>>,
435
+ reported: ReadonlyArray<Pick<LedgerFinding, 'finding_id' | 'severity' | 'kind' | 'title' | 'contradicted'>>,
417
436
  ledger: readonly LedgerFinding[],
418
437
  ): string[] {
419
438
  const known = new Set(ledger.map((f) => f.finding_id));
@@ -476,10 +476,81 @@ const HOOK_MANIFEST_TRIPLE: Triple = {
476
476
  },
477
477
  };
478
478
 
479
+ const HOST_LOWERING_YAML = 'src/scripts/hooks/host_lowering.yaml';
480
+ const HOST_LOWERING_JSON = 'src/scripts/hooks/host_lowering.json';
481
+
482
+ /**
483
+ * The second compiled plumbing table, and it is here for the reason the first
484
+ * one is: the fingerprint hashes the YAML TEXT, so a comment-only edit to
485
+ * `host_lowering.yaml` invalidates the committed JSON while every parsed field
486
+ * stays byte-identical. That file is mostly comments — 16.8 kB of source
487
+ * compiling to 5.9 kB of data — so the comment-only case is the LIKELY one
488
+ * here, not the corner case.
489
+ *
490
+ * A stale sibling is slow, never wrong: `loadHostLowering` falls through to the
491
+ * YAML source on a fingerprint mismatch. So what this row protects is the
492
+ * measured 9 ms per dispatch, which would otherwise be given back silently and
493
+ * would surface only as the hook-latency gate flapping again.
494
+ */
495
+ const HOST_LOWERING_TRIPLE: Triple = {
496
+ id: 'host-lowering-compiled',
497
+ output: HOST_LOWERING_JSON,
498
+ remedy: './scripts-run src/scripts/compile_hook_manifest --table host-lowering',
499
+ why:
500
+ 'the compiled lowering table carries a fingerprint hashed over the YAML TEXT, so even a ' +
501
+ 'comment-only edit to the source invalidates it — and no `task` target regenerates it',
502
+ sourcesOf() {
503
+ return { ok: true, sources: [{ kind: 'file', value: HOST_LOWERING_YAML }] };
504
+ },
505
+ regenerate(root, workDir) {
506
+ const outFile = path.join(workDir, 'host_lowering.json');
507
+ const tsx = path.join(
508
+ root,
509
+ 'node_modules',
510
+ '.bin',
511
+ process.platform === 'win32' ? 'tsx.cmd' : 'tsx',
512
+ );
513
+ if (!fs.existsSync(tsx)) {
514
+ return {
515
+ ok: false,
516
+ reason: `no tsx at ${path.relative(root, tsx)} — run \`npm ci\` before this gate`,
517
+ };
518
+ }
519
+ const res = spawnSync(
520
+ tsx,
521
+ [
522
+ path.join(root, 'src', 'scripts', 'compile_hook_manifest.ts'),
523
+ '--table',
524
+ 'host-lowering',
525
+ '--out',
526
+ outFile,
527
+ ],
528
+ { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 },
529
+ );
530
+ if (res.status !== 0) {
531
+ return {
532
+ ok: false,
533
+ reason:
534
+ `compile_hook_manifest --table host-lowering exited ${String(res.status)}: ` +
535
+ `${(res.stderr ?? '').trim()}`,
536
+ };
537
+ }
538
+ try {
539
+ return { ok: true, text: fs.readFileSync(outFile, 'utf8') };
540
+ } catch (e) {
541
+ return {
542
+ ok: false,
543
+ reason: `compile_hook_manifest reported success but wrote no readable file (${String(e)})`,
544
+ };
545
+ }
546
+ },
547
+ };
548
+
479
549
  export const REGISTRY: readonly Triple[] = [
480
550
  CENSUS_TRIPLE,
481
551
  INSTALL_BUNDLE_TRIPLE,
482
552
  HOOK_MANIFEST_TRIPLE,
553
+ HOST_LOWERING_TRIPLE,
483
554
  ];
484
555
 
485
556
  // ---------------------------------------------------------------------------