@event4u/agent-config 14.17.0 → 14.19.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 (184) 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 +207 -77
  5. package/README.md +72 -150
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +2 -2
  8. package/dist/agent-src/commands/pr/merge.md +35 -44
  9. package/dist/agent-src/commands/review/changes.md +19 -19
  10. package/dist/agent-src/commands/review.md +2 -2
  11. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  12. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  13. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  14. package/dist/agent-src/rules/guidelines.md +1 -1
  15. package/dist/agent-src/rules/session-canary.md +2 -2
  16. package/dist/agent-src/rules/token-efficiency.md +1 -1
  17. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
  18. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  19. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
  20. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  21. package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
  22. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
  23. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  24. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  25. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  26. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  27. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  28. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  29. package/dist/agent-src/skills/design-review/SKILL.md +3 -1
  30. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  31. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  32. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  33. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  34. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  35. package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
  36. package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
  37. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
  38. package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
  39. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  40. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  41. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  42. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  43. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  44. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  45. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
  46. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  47. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  48. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
  49. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
  50. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
  51. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  52. package/dist/cli/commands/recordTriggerEval.js +2 -2
  53. package/dist/cli/main.js +1 -1
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  55. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  56. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  57. package/dist/cli-delegate/cmd_conformance.js +749 -669
  58. package/dist/cli-delegate/cmd_doctor.js +1 -1
  59. package/dist/cli-delegate/cmd_export.js +1 -1
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +1 -1
  62. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  63. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +1 -1
  65. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  66. package/dist/cli-delegate/cmd_validate.js +1 -1
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +39 -38
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +1 -1
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +2 -2
  74. package/dist/discovery/trust-report.md +1 -1
  75. package/dist/discovery/workspaces.json +2 -2
  76. package/dist/hooks/dispatch.js +2764 -2439
  77. package/dist/install/install.mjs +588 -527
  78. package/dist/mcp/registry-manifest.json +2 -2
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +1381 -1138
  81. package/dist/scripts/_lib/model_tier.js +1 -1
  82. package/dist/shared/settingsConsent.js +1 -1
  83. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  84. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  85. package/docs/guidelines/design-antipatterns.md +87 -64
  86. package/package.json +1 -1
  87. package/src/agent-src/README.md +1 -1
  88. package/src/config/ci-local-parity.yml +6 -0
  89. package/src/config/conformance-claim-baseline.json +9 -0
  90. package/src/config/gate-coverage.yml +170 -0
  91. package/src/config/gate-violation-baselines.json +4 -4
  92. package/src/config/release-gate-locality.yml +141 -0
  93. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  94. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  95. package/src/scripts/_cli/cmd_update.ts +1 -1
  96. package/src/scripts/_cli/cmd_versions.ts +1 -1
  97. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  98. package/src/scripts/_lib/consequence_objects.ts +152 -0
  99. package/src/scripts/_lib/exec_evidence.ts +7 -0
  100. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  101. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  102. package/src/scripts/_lib/model_tier.ts +1 -1
  103. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  104. package/src/scripts/_lib/release_highlights.ts +265 -1
  105. package/src/scripts/_lib/release_material.ts +129 -0
  106. package/src/scripts/_lib/repeated_run.ts +77 -0
  107. package/src/scripts/_lib/runtime_journal.ts +26 -3
  108. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  109. package/src/scripts/_lib/session_register.ts +11 -6
  110. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  111. package/src/scripts/_lib/surface.ts +86 -0
  112. package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
  113. package/src/scripts/_lib/ui_authority.ts +116 -0
  114. package/src/scripts/_lib/unified_diff.ts +121 -0
  115. package/src/scripts/adr/regenerate_index.ts +54 -1
  116. package/src/scripts/adr_cite_check.ts +71 -15
  117. package/src/scripts/ai_council/clients.ts +1 -1
  118. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  119. package/src/scripts/build_proof.ts +15 -2
  120. package/src/scripts/check_beta_review_markers.ts +141 -17
  121. package/src/scripts/check_enforcement_coverage.ts +1 -1
  122. package/src/scripts/check_enforcement_denominator.ts +121 -11
  123. package/src/scripts/check_finding_dispositions.ts +239 -6
  124. package/src/scripts/check_gate_coverage.ts +96 -0
  125. package/src/scripts/check_installed_hooks_fresh.ts +381 -0
  126. package/src/scripts/check_release_highlights.ts +99 -14
  127. package/src/scripts/check_skill_activation_claim.ts +179 -0
  128. package/src/scripts/check_suppression_hygiene.ts +11 -0
  129. package/src/scripts/check_trigger_evals.ts +1 -1
  130. package/src/scripts/code_graph/build.ts +230 -33
  131. package/src/scripts/code_graph/extract.ts +168 -33
  132. package/src/scripts/code_graph/types.ts +37 -2
  133. package/src/scripts/code_graph/validate.ts +12 -1
  134. package/src/scripts/conformance_scan.ts +45 -4
  135. package/src/scripts/detect_silent_catch.ts +403 -0
  136. package/src/scripts/detect_verification_tampering.ts +538 -0
  137. package/src/scripts/explain_run.ts +1 -1
  138. package/src/scripts/generate_capability_matrix.ts +1 -1
  139. package/src/scripts/git_authorization_hook.ts +228 -18
  140. package/src/scripts/hook_manifest.json +1 -1
  141. package/src/scripts/hook_manifest.yaml +47 -30
  142. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  143. package/src/scripts/hooks/block_no_verify.ts +3 -3
  144. package/src/scripts/hooks/category_a.ts +291 -0
  145. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  146. package/src/scripts/hooks/concern_registry.ts +2 -2
  147. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  148. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  149. package/src/scripts/hooks/evidence_independence.ts +21 -2
  150. package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
  151. package/src/scripts/hooks/host_lowering.ts +215 -0
  152. package/src/scripts/hooks/host_lowering.yaml +185 -0
  153. package/src/scripts/hooks/host_semantics.ts +105 -37
  154. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  155. package/src/scripts/hooks/permission_gate.ts +117 -0
  156. package/src/scripts/hooks/push_settle_hook.ts +250 -0
  157. package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
  158. package/src/scripts/hooks_status.ts +12 -10
  159. package/src/scripts/install-hooks.sh +134 -1
  160. package/src/scripts/install.ts +53 -82
  161. package/src/scripts/install_anthropic_key.sh +2 -2
  162. package/src/scripts/language_mirror_hook.ts +27 -5
  163. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  164. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  165. package/src/scripts/lint_hook_manifest.ts +87 -3
  166. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  167. package/src/scripts/lint_readme_size.ts +158 -12
  168. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  169. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  170. package/src/scripts/measure_sibling_naming.ts +150 -0
  171. package/src/scripts/redact_hook_capture.ts +1 -1
  172. package/src/scripts/release.ts +31 -143
  173. package/src/scripts/release_drill.ts +71 -1
  174. package/src/scripts/release_publication.ts +243 -3
  175. package/src/scripts/release_verify.ts +220 -0
  176. package/src/scripts/report_skill_activation.ts +319 -7
  177. package/src/scripts/schemas/review-findings.schema.json +6 -1
  178. package/src/scripts/schemas/skill.schema.json +12 -2
  179. package/src/scripts/self_review_gate.ts +10 -1
  180. package/src/scripts/session_canary_hook.ts +1 -1
  181. package/src/scripts/session_register_hook.ts +4 -3
  182. package/src/scripts/trigger_eval_grandfather.json +5 -1
  183. package/src/scripts/validate_agent_settings.ts +1 -1
  184. package/src/shared/settingsConsent.ts +1 -1
@@ -63,28 +63,34 @@ interface LapsedBaseline {
63
63
  contracts: string[];
64
64
  }
65
65
 
66
- let _baselineCache: Set<string> | null = null;
66
+ // Keyed by root: an unkeyed cache returns the REPO baseline for a fixture root
67
+ // whenever the repo was read first, which silently judges a fixture against
68
+ // production data and is invisible in the result.
69
+ const _baselineCache = new Map<string, Set<string>>();
67
70
 
68
71
  export function loadLapsedBaseline(root: string = ROOT): Set<string> {
69
- if (_baselineCache !== null) {
70
- return _baselineCache;
72
+ const hit = _baselineCache.get(root);
73
+ if (hit !== undefined) {
74
+ return hit;
71
75
  }
72
76
  const p = path.join(root, BASELINE_REL);
73
77
  if (!fs.existsSync(p)) {
74
78
  // The baseline reaching zero is the SUCCESS state: the file is deleted
75
79
  // in the same change that flips the severity. An absent file therefore
76
80
  // means "no inherited debt", i.e. every lapse is fresh and errors.
77
- _baselineCache = new Set<string>();
78
- return _baselineCache;
81
+ const empty = new Set<string>();
82
+ _baselineCache.set(root, empty);
83
+ return empty;
79
84
  }
80
85
  const parsed = JSON.parse(fs.readFileSync(p, 'utf-8')) as LapsedBaseline;
81
- _baselineCache = new Set(parsed.contracts);
82
- return _baselineCache;
86
+ const loaded = new Set(parsed.contracts);
87
+ _baselineCache.set(root, loaded);
88
+ return loaded;
83
89
  }
84
90
 
85
91
  /** Test seam — the module-level cache would otherwise outlive a fixture. */
86
92
  export function _resetLapsedBaseline(): void {
87
- _baselineCache = null;
93
+ _baselineCache.clear();
88
94
  }
89
95
 
90
96
  /**
@@ -115,12 +121,43 @@ const SUPERSEDED_RE = /^superseded-by:\s*\S+\s*$/m;
115
121
 
116
122
  const MAX_REVIEW_WINDOW_DAYS = 90;
117
123
 
124
+ /**
125
+ * Forward horizon for the upcoming-lapse report.
126
+ *
127
+ * The gate reports a contract on the day it lapses, which is the day it is
128
+ * already too late to decide anything: a FRESH lapse errors, and every pull
129
+ * request in the repository reds until someone edits a contract under time
130
+ * pressure. The horizon names the set that is ABOUT to enter that branch.
131
+ *
132
+ * The number is bounded on both sides: too wide and the report names most of
133
+ * the forward-dated corpus on every run, which is the same as naming none; too
134
+ * narrow and it arrives after the decision needed to be made.
135
+ */
136
+ const HORIZON_DAYS = 14;
137
+
118
138
  interface Violation {
119
139
  file: string;
120
140
  reason: string;
121
141
  severity: 'error' | 'warning';
122
142
  }
123
143
 
144
+ /**
145
+ * A contract whose window closes within {@link HORIZON_DAYS} and which is NOT
146
+ * in the frozen baseline — i.e. one that will enter the ERROR branch, not the
147
+ * inherited-warning one.
148
+ *
149
+ * Deliberately NOT a {@link Violation}: it carries no severity and never moves
150
+ * the exit code. A third severity class printed into the same stream as the 84
151
+ * standing warnings is the way this work changes nothing (risk 1 on the
152
+ * roadmap's own register), so the horizon is a separate, clearly-labelled
153
+ * section and the exit code stays a function of real violations alone.
154
+ */
155
+ interface Upcoming {
156
+ file: string;
157
+ date: string;
158
+ daysOut: number;
159
+ }
160
+
124
161
  function _exists(p: string): boolean {
125
162
  try {
126
163
  fs.statSync(p);
@@ -184,7 +221,7 @@ function _ordinalToISO(ordinal: number): string {
184
221
  return `${y}-${m}-${d}`;
185
222
  }
186
223
 
187
- function check_one(p: string, todayOrdinal: number): Violation[] {
224
+ function check_one(p: string, todayOrdinal: number, root: string = ROOT): Violation[] {
188
225
  const fm = read_frontmatter(p);
189
226
  if (fm === null) {
190
227
  return [];
@@ -199,7 +236,7 @@ function check_one(p: string, todayOrdinal: number): Violation[] {
199
236
  ['superseded-by', SUPERSEDED_RE.test(fm)],
200
237
  ];
201
238
  const setMarkers = markers.filter(([, present]) => present).map(([name]) => name);
202
- const rel = _relPosix(p, ROOT);
239
+ const rel = _relPosix(p, root);
203
240
  if (setMarkers.length === 0) {
204
241
  return [
205
242
  {
@@ -231,7 +268,7 @@ function check_one(p: string, todayOrdinal: number): Violation[] {
231
268
  // bound because a lapsed date can never also exceed the forward window,
232
269
  // so the order is a readability choice rather than a precedence one.
233
270
  if (reviewOrdinal < todayOrdinal) {
234
- const inherited = loadLapsedBaseline().has(rel);
271
+ const inherited = loadLapsedBaseline(root).has(rel);
235
272
  const age = todayOrdinal - reviewOrdinal;
236
273
  return [
237
274
  {
@@ -263,6 +300,34 @@ function check_one(p: string, todayOrdinal: number): Violation[] {
263
300
  return [];
264
301
  }
265
302
 
303
+ /**
304
+ * The upcoming-lapse finding for one contract, or `null`.
305
+ *
306
+ * Baselined contracts are excluded EXPLICITLY rather than by relying on the
307
+ * fact that a baselined contract is already lapsed and so cannot also carry a
308
+ * future date. That implication holds today and is not a property anything
309
+ * enforces; a guard that is merely vacuous cannot be distinguished from a
310
+ * guard that is absent, and the difference shows up the first time a baseline
311
+ * entry is re-dated forward.
312
+ */
313
+ export function upcoming_one(
314
+ p: string,
315
+ todayOrdinal: number,
316
+ horizonDays: number = HORIZON_DAYS,
317
+ root: string = ROOT,
318
+ ): Upcoming | null {
319
+ const fm = read_frontmatter(p);
320
+ if (fm === null || !/^stability:\s*beta\s*$/m.test(fm)) return null;
321
+ const km = KEEP_RE.exec(fm);
322
+ if (!km) return null;
323
+ const rel = _relPosix(p, root);
324
+ if (loadLapsedBaseline(root).has(rel)) return null;
325
+ const [ry, rm, rd] = _parseISODate(km[1]!);
326
+ const reviewOrdinal = _dateOrdinal(ry, rm, rd);
327
+ const daysOut = reviewOrdinal - todayOrdinal;
328
+ if (daysOut < 0 || daysOut > horizonDays) return null;
329
+ return { file: rel, date: _ordinalToISO(reviewOrdinal), daysOut };
330
+ }
266
331
 
267
332
  /**
268
333
  * STABILITY.md's own re-audit condition, as a command instead of as prose.
@@ -302,10 +367,16 @@ interface ParsedArgs {
302
367
  trigger: boolean;
303
368
  /** Fixture root, for `--self-test`. A fixture is never judged against the repo baseline. */
304
369
  root: string | null;
370
+ horizon: number;
305
371
  }
306
372
 
307
373
  function parse_args(argv: readonly string[]): ParsedArgs {
308
- const args: ParsedArgs = { json: false, trigger: false, root: null };
374
+ const args: ParsedArgs = {
375
+ json: false,
376
+ trigger: false,
377
+ root: null,
378
+ horizon: HORIZON_DAYS,
379
+ };
309
380
  for (let i = 0; i < argv.length; i += 1) {
310
381
  const arg = argv[i] as string;
311
382
  if (arg === '--json') {
@@ -313,14 +384,27 @@ function parse_args(argv: readonly string[]): ParsedArgs {
313
384
  } else if (arg === '--root') {
314
385
  args.root = argv[i + 1] ?? null;
315
386
  i += 1;
387
+ } else if (arg === '--horizon') {
388
+ const raw = argv[i + 1] ?? '';
389
+ const n = Number.parseInt(raw, 10);
390
+ if (!/^\d+$/.test(raw) || Number.isNaN(n)) {
391
+ process.stderr.write(
392
+ `check_beta_review_markers: error: --horizon needs a non-negative integer, got: ${raw || '(nothing)'}\n`,
393
+ );
394
+ process.exit(2);
395
+ }
396
+ args.horizon = n;
397
+ i += 1;
316
398
  } else if (arg === '--trigger') {
317
399
  args.trigger = true;
318
400
  } else if (arg === '-h' || arg === '--help') {
319
401
  process.stdout.write(
320
- 'usage: check_beta_review_markers [-h] [--json] [--trigger]\n' +
402
+ 'usage: check_beta_review_markers [-h] [--json] [--trigger] [--horizon N]\n' +
321
403
  ' --trigger print the lapsed percentage and whether STABILITY.md\n' +
322
404
  ' re-audit condition (>= 25 %) has fired. Reports; never fails.\n' +
323
- ' --root DIR judge a fixture tree instead of the repo (self-test seam).\n',
405
+ ' --root DIR judge a fixture tree instead of the repo (self-test seam).\n' +
406
+ ` --horizon N days ahead to report upcoming fresh lapses (default ${String(HORIZON_DAYS)}).\n` +
407
+ ' Advisory: never changes the exit code.\n',
324
408
  );
325
409
  process.exit(0);
326
410
  } else {
@@ -400,11 +484,15 @@ function main(): number {
400
484
  const scanRoot = args.root ?? ROOT;
401
485
  const contracts = _globMdSorted(path.join(scanRoot, CONTRACTS_DIR));
402
486
  let betaContracts = 0;
487
+ const upcoming: Upcoming[] = [];
403
488
  for (const p of contracts) {
404
489
  const fm = read_frontmatter(p);
405
490
  if (fm !== null && /^stability:\s*beta\s*$/m.test(fm)) betaContracts += 1;
406
- violations.push(...check_one(p, todayOrdinal));
491
+ violations.push(...check_one(p, todayOrdinal, scanRoot));
492
+ const due = upcoming_one(p, todayOrdinal, args.horizon, scanRoot);
493
+ if (due !== null) upcoming.push(due);
407
494
  }
495
+ upcoming.sort((a, b) => a.daysOut - b.daysOut || a.file.localeCompare(b.file));
408
496
  // Count every contract read, not the `stability: beta` subset: over a moved
409
497
  // `docs/contracts/` "no beta contracts" and "no contracts at all" produce
410
498
  // the same clean line, and only the second is a dead gate. Exit 1 is the
@@ -446,7 +534,9 @@ function main(): number {
446
534
  return 0;
447
535
  }
448
536
  if (args.json) {
449
- process.stdout.write(JSON.stringify({ violations }, null, 2) + '\n');
537
+ process.stdout.write(
538
+ JSON.stringify({ violations, upcoming, horizonDays: args.horizon }, null, 2) + '\n',
539
+ );
450
540
  } else {
451
541
  if (violations.length === 0) {
452
542
  process.stdout.write('✅ All beta contracts carry a valid review marker.\n');
@@ -455,7 +545,39 @@ function main(): number {
455
545
  const icon = v.severity === 'error' ? '❌' : '⚠️ ';
456
546
  process.stdout.write(`${icon} ${v.file}: ${v.reason}\n`);
457
547
  }
458
- process.stdout.write(`\n${violations.length} violation(s).\n`);
548
+ // Both counts, and the relationship between them, because a
549
+ // violation count alone cannot answer "how many contracts is this".
550
+ // Today they are equal — check_one returns at most one finding per
551
+ // contract — and printing only one of them hides the fact that it
552
+ // is the size of the decision, not just the size of the output.
553
+ const distinct = new Set(violations.map((v) => v.file)).size;
554
+ const baselineSize = loadLapsedBaseline(scanRoot).size;
555
+ const reportedInherited = violations.filter((v) =>
556
+ loadLapsedBaseline(scanRoot).has(v.file),
557
+ ).length;
558
+ process.stdout.write(
559
+ `\n${String(violations.length)} violation(s) across ` +
560
+ `${String(distinct)} distinct contract(s) — one line per finding, so the ` +
561
+ 'two differ only where a contract carries more than one.\n',
562
+ );
563
+ process.stdout.write(
564
+ `Frozen baseline: ${String(baselineSize)} entries · ` +
565
+ `${String(reportedInherited)} still reported as lapsed · ` +
566
+ `${String(baselineSize - reportedInherited)} inert ` +
567
+ '(left the lapsed set by promotion, supersession, or re-dating).\n',
568
+ );
569
+ }
570
+ if (upcoming.length > 0) {
571
+ process.stdout.write(
572
+ `\nUpcoming FRESH lapses within ${String(args.horizon)} day(s) — advisory, ` +
573
+ 'exit code unchanged. These are absent from the frozen baseline, so each\n' +
574
+ 'becomes an ERROR on its date rather than an inherited warning:\n',
575
+ );
576
+ for (const u of upcoming) {
577
+ process.stdout.write(
578
+ ` ${u.file}: keep-beta-until=${u.date} in ${String(u.daysOut)} day(s) [fresh]\n`,
579
+ );
580
+ }
459
581
  }
460
582
  }
461
583
  return violations.some((v) => v.severity === 'error') ? 1 : 0;
@@ -495,9 +617,11 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
495
617
 
496
618
  export {
497
619
  type Violation,
620
+ type Upcoming,
498
621
  ROOT,
499
622
  CONTRACTS_DIR,
500
623
  MAX_REVIEW_WINDOW_DAYS,
624
+ HORIZON_DAYS,
501
625
  read_frontmatter,
502
626
  check_one,
503
627
  main,
@@ -118,7 +118,7 @@ export interface RuleCoverage {
118
118
  /**
119
119
  * Carrier period per hook-capable platform — never a single scalar.
120
120
  *
121
- * A scalar is wrong twice over: copilot has no hook surface at all, so a
121
+ * A scalar is wrong twice over: this package binds nothing on copilot, so a
122
122
  * weakest-platform collapse turns one platform property into a finding on
123
123
  * every hook-carried rule; and cline maps `stop` from `TaskCancel`, so the
124
124
  * same slot means per-turn on six platforms and per-interruption there. Both
@@ -39,9 +39,26 @@
39
39
  * Escape hatch: `<!-- enforcement-count-ok: <reason> -->` on the line, reason
40
40
  * mandatory. A bare marker stays a finding.
41
41
  *
42
+ * THE SECOND HALF — the projection owes the split. Exempting the generated file
43
+ * settles WHERE the number may be published; it says nothing about whether the
44
+ * published number is readable. `undeclared` quoted bare reads as a backlog
45
+ * somebody forgot to wire, and for nine of them that is not what it is:
46
+ * `block_kernel_rule_writes` refuses an `enforced_by` write on a kernel rule
47
+ * with no agent-accessible override, so those cannot declare one at all. The
48
+ * resolver has always computed `kernel_denied`; the published line dropped it
49
+ * and appended the word `yet`, promising a future that does not exist for that
50
+ * nine. So a line in the sanctioned projection that states an `undeclared`
51
+ * figure without its kernel-denied split is the SAME class of failure as a
52
+ * hand-written count elsewhere: a figure the reader cannot resolve.
53
+ *
54
+ * This deliberately does NOT check the split's value, for the same reason the
55
+ * first half does not check the count's — the resolver is the only source, and
56
+ * a value comparison here would be a second place the number lives.
57
+ *
42
58
  * Exit codes:
43
- * 0 — no hand-written enforcement count in the published corpus
44
- * 1 — at least one (or a marker with no reason)
59
+ * 0 — no hand-written enforcement count in the published corpus, and every
60
+ * published `undeclared` figure carries its kernel-denied split
61
+ * 1 — at least one (or a marker with no reason, or a split-less figure)
45
62
  * 2 — the gate could not run (dead scan scope, bad args)
46
63
  */
47
64
  import * as fs from 'node:fs';
@@ -57,8 +74,8 @@ const _HERE = fileURLToPath(import.meta.url);
57
74
  const DEFAULT_ROOT = path.join(path.dirname(_HERE), '..', '..');
58
75
  const SELF = 'src/scripts/check_enforcement_denominator.ts';
59
76
 
60
- const SELF_TEST_MIN_CASES = 7;
61
- const SELF_TEST_MIN_REJECT = 3;
77
+ const SELF_TEST_MIN_CASES = 10;
78
+ const SELF_TEST_MIN_REJECT = 5;
62
79
 
63
80
  /** Roots walked, repo-relative. */
64
81
  const SCAN_ROOTS = ['docs', 'README.md'] as const;
@@ -82,6 +99,19 @@ const MARKER = 'enforcement-count-ok:';
82
99
  const ENFORCEMENT_KEYWORDS =
83
100
  /undeclared|enforced_by|enforcement coverage|carry a backstop|backstop that fails/;
84
101
 
102
+ /**
103
+ * Shapes that state the `undeclared` figure itself. Narrower than
104
+ * {@link COUNT_SHAPES} on purpose: this is the ONE figure whose split is
105
+ * load-bearing, and a wider net would demand the split beside `blocking`.
106
+ */
107
+ const UNDECLARED_FIGURE_SHAPES: readonly RegExp[] = [
108
+ /\b\d+\s+undeclared\b/i,
109
+ /\bundeclared\s+rules?\s*\(\s*\d+/i,
110
+ ];
111
+
112
+ /** The split that makes the figure readable. Presence is checked, never value. */
113
+ const SPLIT_TOKEN = /kernel[-_]denied/i;
114
+
85
115
  const COUNT_SHAPES: readonly RegExp[] = [
86
116
  /\b\d+\s+of\s+\d+\s+(?:governed\s+)?rules?\b/,
87
117
  /\b\d+\s+(?:governed\s+)?rules?\b/,
@@ -93,8 +123,12 @@ export interface Finding {
93
123
  file: string;
94
124
  line: number;
95
125
  excerpt: string;
96
- /** `raw` — an unmarked count. `bare-marker` — a marker with no reason. */
97
- kind: 'raw' | 'bare-marker';
126
+ /**
127
+ * `raw` — an unmarked count. `bare-marker` — a marker with no reason.
128
+ * `missing-split` — the projection states `undeclared` without its
129
+ * kernel-denied split.
130
+ */
131
+ kind: 'raw' | 'bare-marker' | 'missing-split';
98
132
  }
99
133
 
100
134
  function markerOn(line: string): 'none' | 'bare' | 'reasoned' {
@@ -123,6 +157,30 @@ export function scanText(relPath: string, text: string): Finding[] {
123
157
  return out;
124
158
  }
125
159
 
160
+ /**
161
+ * The projection's own obligation: every line stating an `undeclared` figure
162
+ * carries the kernel-denied split. Run INSTEAD of {@link scanText} on a
163
+ * generated file — that file is allowed to state the number, and is the only
164
+ * one that owes the split.
165
+ */
166
+ export function scanGenerated(relPath: string, text: string): Finding[] {
167
+ const out: Finding[] = [];
168
+ const lines = text.split('\n');
169
+ for (let i = 0; i < lines.length; i += 1) {
170
+ const line = lines[i] ?? '';
171
+ if (!UNDECLARED_FIGURE_SHAPES.some((re) => re.test(line))) continue;
172
+ if (SPLIT_TOKEN.test(line)) continue;
173
+ if (markerOn(line) === 'reasoned') continue;
174
+ out.push({
175
+ file: relPath,
176
+ line: i + 1,
177
+ excerpt: line.trim().slice(0, 120),
178
+ kind: 'missing-split',
179
+ });
180
+ }
181
+ return out;
182
+ }
183
+
126
184
  /** `true` when the file declares itself generated — i.e. it IS the projection. */
127
185
  export function isGenerated(text: string): boolean {
128
186
  return text.split('\n', GENERATED_HEAD_LINES).join('\n').includes(GENERATED_MARKER);
@@ -177,7 +235,13 @@ export function check(root: string): { code: number; findings: Finding[]; scanne
177
235
  for (const rel of files) {
178
236
  const text = fs.readFileSync(path.join(root, rel), 'utf-8');
179
237
  if (isGenerated(text)) {
180
- ledger.complete(rel);
238
+ const split = scanGenerated(rel, text);
239
+ if (split.length > 0) {
240
+ findings.push(...split);
241
+ ledger.fail(rel, `${String(split.length)} undeclared figure(s) without the split`);
242
+ } else {
243
+ ledger.complete(rel);
244
+ }
181
245
  continue;
182
246
  }
183
247
  const found = scanText(rel, text);
@@ -259,8 +323,21 @@ export function selfTest(): number {
259
323
  }),
260
324
  },
261
325
  {
262
- name: "accepts the resolver's own generated projection",
326
+ name: "accepts the resolver's own generated projection when it carries the split",
263
327
  expect: 'accept',
328
+ run: () =>
329
+ run({
330
+ 'docs/proof.md':
331
+ `<!-- ${GENERATED_MARKER} src/scripts/build_proof.ts -->\n\n` +
332
+ '120 rules · 15 blocking · 86 undeclared — of which 9 kernel-denied.\n',
333
+ }),
334
+ },
335
+ {
336
+ // DENIAL POLARITY for the split half. Same file, same exemption, the
337
+ // split removed: the projection is allowed to state the number and is
338
+ // still a finding when it states it bare.
339
+ name: 'rejects the generated projection when the kernel-denied split is removed',
340
+ expect: 'reject',
264
341
  run: () =>
265
342
  run({
266
343
  'docs/proof.md':
@@ -268,6 +345,26 @@ export function selfTest(): number {
268
345
  '120 rules · 15 blocking · 86 undeclared (no enforced_by yet).\n',
269
346
  }),
270
347
  },
348
+ {
349
+ name: 'rejects a bare "Undeclared rules (N)" restatement in the projection',
350
+ expect: 'reject',
351
+ run: () =>
352
+ run({
353
+ 'docs/proof.md':
354
+ `<!-- ${GENERATED_MARKER} src/scripts/build_proof.ts -->\n\n` +
355
+ 'Undeclared rules (86) carry no row.\n',
356
+ }),
357
+ },
358
+ {
359
+ name: 'accepts a generated line about enforcement that states no undeclared figure',
360
+ expect: 'accept',
361
+ run: () =>
362
+ run({
363
+ 'docs/proof.md':
364
+ `<!-- ${GENERATED_MARKER} src/scripts/build_proof.ts -->\n\n` +
365
+ '120 rules · 15 blocking · the resolver prints its own frame.\n',
366
+ }),
367
+ },
271
368
  {
272
369
  name: 'accepts a dated ADR record (docs/decisions/ is excluded)',
273
370
  expect: 'accept',
@@ -318,12 +415,25 @@ export function main(argv?: readonly string[]): number {
318
415
  if (code === 2) return 2;
319
416
 
320
417
  if (findings.length > 0) {
321
- process.stdout.write('❌ hand-written enforcement count(s) in a published doc:\n\n');
418
+ process.stdout.write('❌ unresolvable enforcement figure(s) in a published doc:\n\n');
322
419
  for (const f of findings) {
323
420
  const why =
324
- f.kind === 'bare-marker' ? `\`${MARKER}\` with no reason` : 'hand-written count';
421
+ f.kind === 'bare-marker'
422
+ ? `\`${MARKER}\` with no reason`
423
+ : f.kind === 'missing-split'
424
+ ? 'published `undeclared` figure with no kernel-denied split'
425
+ : 'hand-written count';
325
426
  process.stdout.write(` ${f.file}:${String(f.line)} — ${why}\n ${f.excerpt}\n`);
326
427
  }
428
+ if (findings.some((f) => f.kind === 'missing-split')) {
429
+ process.stdout.write(
430
+ '\nThe projection may state the `undeclared` figure — it is the only surface\n' +
431
+ 'that may — but not bare. Nine of those rules are kernel-denied:\n' +
432
+ '`block_kernel_rule_writes` refuses an `enforced_by` write with no\n' +
433
+ 'agent-accessible override, so for them there is no *yet*. Print\n' +
434
+ "`summary.kernel_denied` on the same line (`build_proof.ts` § 4b does).\n",
435
+ );
436
+ }
327
437
  process.stdout.write(
328
438
  '\nExactly one enforcement denominator is quotable, and it comes from\n' +
329
439
  '`check_enforcement_coverage` — which resolves rather than counts, and prints\n' +
@@ -362,4 +472,4 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
362
472
  process.exit(main());
363
473
  }
364
474
 
365
- export { SCAN_ROOTS, EXCLUDED_PREFIXES, MARKER };
475
+ export { SCAN_ROOTS, EXCLUDED_PREFIXES, MARKER, SPLIT_TOKEN, UNDECLARED_FIGURE_SHAPES };