@event4u/agent-config 14.13.0 → 14.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) 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 +138 -376
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/analyze/inbox.md +36 -0
  7. package/dist/agent-src/commands/pr/create.md +16 -1
  8. package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
  9. package/dist/agent-src/rules/code-comment-discipline.md +6 -0
  10. package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
  11. package/dist/agent-src/rules/token-budget-discipline.md +2 -2
  12. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
  13. package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
  14. package/dist/agent-src/scripts/stubs_due.ts +118 -2
  15. package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
  16. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  17. package/dist/cli/registry.js +1 -0
  18. package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
  19. package/dist/cli-delegate/cmd_conformance.js +39 -2
  20. package/dist/cli-delegate/cmd_doctor.js +1 -1
  21. package/dist/discovery/deprecation-report.md +1 -1
  22. package/dist/discovery/discovery-manifest.json +7 -7
  23. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  24. package/dist/discovery/discovery-manifest.summary.md +1 -1
  25. package/dist/discovery/dormancy-report.md +1 -1
  26. package/dist/discovery/orphan-report.md +1 -1
  27. package/dist/discovery/packs.json +2 -2
  28. package/dist/discovery/trust-report.md +1 -1
  29. package/dist/discovery/workspaces.json +2 -2
  30. package/dist/hooks/dispatch.js +1696 -812
  31. package/dist/mcp/registry-manifest.json +2 -2
  32. package/dist/mcp/server.json +2 -2
  33. package/dist/mcp/server.mjs +297 -115
  34. package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
  35. package/docs/guidelines/code-clarity.md +18 -0
  36. package/package.json +1 -1
  37. package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
  38. package/src/agent-src/scripts/guarded_baseline.ts +22 -7
  39. package/src/agent-src/scripts/stubs_due.ts +118 -2
  40. package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
  41. package/src/config/assurance-capability-registry.json +1 -1
  42. package/src/config/estate-count-budget.json +20 -1
  43. package/src/config/evaluator-budgets.json +4 -4
  44. package/src/config/gate-coverage.yml +211 -1
  45. package/src/config/gate-reachability-exemptions.json +1 -0
  46. package/src/config/gate-violation-baselines.json +9 -3
  47. package/src/config/preamble-payload-budget.json +19 -11
  48. package/src/config/rule-stub-ceilings.json +14 -7
  49. package/src/config/web-launch-readiness.json +4 -2
  50. package/src/scripts/_cli/_doctor_emit.ts +71 -0
  51. package/src/scripts/_cli/_doctor_strict.ts +163 -0
  52. package/src/scripts/_cli/cmd_doctor.ts +16 -37
  53. package/src/scripts/_dispatch.bash +11 -4
  54. package/src/scripts/_lib/activation_ladder.ts +47 -3
  55. package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
  56. package/src/scripts/_lib/adr_frontmatter.ts +26 -0
  57. package/src/scripts/_lib/branch_convergence.ts +186 -0
  58. package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
  59. package/src/scripts/_lib/carrier_status.ts +118 -0
  60. package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
  61. package/src/scripts/_lib/context_observation.ts +132 -0
  62. package/src/scripts/_lib/continuation_ladder.ts +188 -0
  63. package/src/scripts/_lib/corpus_manifest.ts +726 -0
  64. package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
  65. package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
  66. package/src/scripts/_lib/evaluation_cascade.ts +452 -0
  67. package/src/scripts/_lib/evolution_roi.ts +562 -0
  68. package/src/scripts/_lib/experiment_binding.ts +95 -0
  69. package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
  70. package/src/scripts/_lib/ignored_blocker.ts +8 -1
  71. package/src/scripts/_lib/lexical_shortlist.ts +215 -0
  72. package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
  73. package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
  74. package/src/scripts/_lib/outcome_envelope.ts +6 -1
  75. package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
  76. package/src/scripts/_lib/pathology_archive.ts +393 -0
  77. package/src/scripts/_lib/promotion_capability.ts +315 -0
  78. package/src/scripts/_lib/promotion_evidence.ts +414 -0
  79. package/src/scripts/_lib/promotion_review.ts +294 -0
  80. package/src/scripts/_lib/prompt_shape.ts +92 -0
  81. package/src/scripts/_lib/release_highlights.ts +79 -1
  82. package/src/scripts/_lib/release_material.ts +125 -2
  83. package/src/scripts/_lib/repeated_failure.ts +19 -2
  84. package/src/scripts/_lib/routing_corpus.ts +307 -0
  85. package/src/scripts/_lib/routing_index_input.ts +104 -0
  86. package/src/scripts/_lib/rule_injection.ts +22 -1
  87. package/src/scripts/_lib/runtime_journal.ts +11 -4
  88. package/src/scripts/_lib/semantic_noop.ts +167 -0
  89. package/src/scripts/activation_receipt.ts +258 -0
  90. package/src/scripts/ai_council/cross_exam.ts +128 -0
  91. package/src/scripts/ai_council/explain_route.ts +157 -0
  92. package/src/scripts/ai_council/information_gain.ts +293 -0
  93. package/src/scripts/ai_council/leakage_corpus.ts +385 -0
  94. package/src/scripts/ai_council/leakage_patterns.ts +453 -0
  95. package/src/scripts/ai_council/minority_retention.ts +171 -0
  96. package/src/scripts/ai_council/recouncil_savings.ts +299 -0
  97. package/src/scripts/ai_council/replay_route.ts +223 -0
  98. package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
  99. package/src/scripts/ai_council/routing_training_row.ts +197 -0
  100. package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
  101. package/src/scripts/check_adr_frontmatter.ts +4 -9
  102. package/src/scripts/check_agent_artifact_location.ts +337 -0
  103. package/src/scripts/check_artefact_count_messaging.ts +74 -8
  104. package/src/scripts/check_enforcement_coverage.ts +40 -1
  105. package/src/scripts/check_estate_count.ts +205 -24
  106. package/src/scripts/check_release_highlights.ts +113 -14
  107. package/src/scripts/check_roadmap_trackable.ts +17 -2
  108. package/src/scripts/check_web_launch_readiness.ts +310 -20
  109. package/src/scripts/corpus_manifest.ts +235 -0
  110. package/src/scripts/evolution_lab.ts +234 -9
  111. package/src/scripts/generate_capability_matrix.ts +16 -9
  112. package/src/scripts/grade_target_readiness.ts +221 -2
  113. package/src/scripts/hook_effect_doctor.ts +47 -4
  114. package/src/scripts/hook_manifest.json +1 -1
  115. package/src/scripts/hook_manifest.yaml +28 -6
  116. package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
  117. package/src/scripts/hooks/concern_registry.ts +2 -0
  118. package/src/scripts/hooks/run_continuation_hook.ts +78 -88
  119. package/src/scripts/hooks/session_eol_hook.ts +12 -1
  120. package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
  121. package/src/scripts/install-hooks.sh +18 -0
  122. package/src/scripts/language_mirror_hook.ts +14 -2
  123. package/src/scripts/lint_carrier_integrity.ts +820 -0
  124. package/src/scripts/lint_code_comments.ts +748 -0
  125. package/src/scripts/lint_plan_risk_register.ts +39 -19
  126. package/src/scripts/lint_promotion_paths.ts +779 -0
  127. package/src/scripts/llm_propose.ts +256 -0
  128. package/src/scripts/measure_delivery_sets.ts +361 -0
  129. package/src/scripts/measure_routing_signal.ts +307 -0
  130. package/src/scripts/model_rule_injection.ts +19 -3
  131. package/src/scripts/probe_council_retention.ts +495 -0
  132. package/src/scripts/release.ts +21 -37
  133. package/src/scripts/release_drill.ts +199 -0
  134. package/src/scripts/release_env.ts +35 -0
  135. package/src/scripts/release_publication.ts +142 -0
  136. package/src/scripts/roadmap_context.ts +15 -0
  137. package/src/scripts/skill_scout.ts +633 -0
  138. package/src/scripts/sync_pr_branch.ts +521 -42
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Agent-artifact location guard — a roadmap-shaped file outside the roadmap root.
4
+ *
5
+ * WHAT IT PROTECTS, AND WHAT IT DOES NOT.
6
+ * ---------------------------------------
7
+ * This gate protects THIS repository, and only this one. A consumer repository
8
+ * never runs this repository's CI, so a gate scoped here protects the tree
9
+ * where the error is LEAST likely to occur — the observed failure was in a
10
+ * consumer project. That is not a reason to skip it, since the same mistake is
11
+ * possible here, but it IS a reason to write the limit down: a gate whose reach
12
+ * is not stated gets cited as broader than it is.
13
+ *
14
+ * The outward-facing half is `agent-config doctor --strict`, which already
15
+ * knows the consumer shape and can now return a non-zero exit.
16
+ *
17
+ * The constraint the detection rests on: every other mechanism that governs a
18
+ * roadmap is keyed on the roadmap root, so a file placed outside it is governed
19
+ * by nothing. This gate is the one that is keyed on the complement.
20
+ *
21
+ * WHY THE NAME TEST ALONE IS NOT THE DETECTOR.
22
+ * -------------------------------------------
23
+ * `is_roadmap_candidate` is IMPORTED from `update_roadmap_progress` rather than
24
+ * mirrored locally, the way `check_estate_count.ts` does it — one definition of
25
+ * what counts, so the roadmap corpus and this guard cannot drift apart.
26
+ *
27
+ * But that function answers "is this filename eligible to be a roadmap inside
28
+ * the roadmap root", and its answer over an arbitrary tree is YES for nearly
29
+ * every `.md` file: it excludes four names and one prefix and otherwise accepts
30
+ * anything. Run bare over this repository it would report thousands of
31
+ * legitimate documents. So the name test is a PRE-FILTER, and the decision is
32
+ * made by {@link roadmapShape}, which reads the file: roadmap frontmatter plus a
33
+ * `## Phase` heading plus at least one checkbox step. All three, because each
34
+ * alone is common — plenty of documents carry frontmatter, and a design doc can
35
+ * carry checkboxes.
36
+ *
37
+ * The finding NAMES the signals that fired, so a false positive is arguable
38
+ * rather than mysterious. A document that trips this gate and should not is a
39
+ * conversation about the signals, which is the conversation worth having.
40
+ *
41
+ * Exit codes: 0 = clean · 1 = a roadmap-shaped file sits outside the root · 3 = internal error.
42
+ */
43
+
44
+ import * as fs from 'node:fs';
45
+ import * as os from 'node:os';
46
+ import * as path from 'node:path';
47
+ import { fileURLToPath, pathToFileURL } from 'node:url';
48
+
49
+ import { is_roadmap_candidate as isRoadmapCandidate } from '../agent-src/scripts/update_roadmap_progress.js';
50
+ import { reportScanned, DeadScopeError } from './_lib/scan_scope.js';
51
+ import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
52
+
53
+ const _HERE = fileURLToPath(import.meta.url);
54
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
55
+
56
+ /** The one legitimate home. Everything else is a finding. */
57
+ export const ROADMAP_ROOT_REL = 'agents/roadmaps';
58
+
59
+ /**
60
+ * Directories the walk never enters.
61
+ *
62
+ * `dist/` and the per-tool projection trees are generated copies — a finding
63
+ * there is a finding about `src/`, reported twice. `node_modules` and `.git`
64
+ * are not this repository's prose.
65
+ */
66
+ const SKIP_DIRS: ReadonlySet<string> = new Set([
67
+ '.git',
68
+ 'node_modules',
69
+ 'dist',
70
+ '.augment',
71
+ '.claude',
72
+ '.cursor',
73
+ '.clinerules',
74
+ '.agents',
75
+ 'coverage',
76
+ '.venv',
77
+ ]);
78
+
79
+ /** Frontmatter keys that only a roadmap carries in this tree. */
80
+ const FM_ROADMAP_RE = /^complexity:[ \t]*(lightweight|structural)[ \t]*$/m;
81
+ const FM_RE = /^---\n([\s\S]*?)\n---/;
82
+ const PHASE_RE = /^##[ \t]+Phase[ \t]/m;
83
+ const CHECKBOX_RE = /^[ \t]*- \[[ x~-]\][ \t]/m;
84
+
85
+ export interface Shape {
86
+ /** Which signals fired. Empty means not roadmap-shaped. */
87
+ readonly signals: readonly string[];
88
+ readonly isRoadmap: boolean;
89
+ }
90
+
91
+ /**
92
+ * Decide whether a file's CONTENT is a roadmap.
93
+ *
94
+ * All three signals are required. Each is individually common; together they are
95
+ * this repository's roadmap format and nothing else's.
96
+ */
97
+ export function roadmapShape(text: string): Shape {
98
+ const signals: string[] = [];
99
+ const fm = FM_RE.exec(text);
100
+ if (fm !== null && FM_ROADMAP_RE.test(fm[1] as string)) {
101
+ signals.push('roadmap frontmatter (`complexity:` lightweight|structural)');
102
+ }
103
+ if (PHASE_RE.test(text)) {
104
+ signals.push('a `## Phase` heading');
105
+ }
106
+ if (CHECKBOX_RE.test(text)) {
107
+ signals.push('at least one checkbox step');
108
+ }
109
+ return { signals, isRoadmap: signals.length === 3 };
110
+ }
111
+
112
+ export interface Finding {
113
+ readonly rel: string;
114
+ readonly signals: readonly string[];
115
+ }
116
+
117
+ export interface ScanResult {
118
+ readonly findings: readonly Finding[];
119
+ /** Every `.md` file the walk visited — the scope, not the matches. */
120
+ readonly scanned: number;
121
+ }
122
+
123
+ function walkMarkdown(root: string): string[] {
124
+ const out: string[] = [];
125
+ const stack = [root];
126
+ while (stack.length > 0) {
127
+ const cur = stack.pop() as string;
128
+ let entries: fs.Dirent[];
129
+ try {
130
+ entries = fs.readdirSync(cur, { withFileTypes: true });
131
+ } catch {
132
+ continue;
133
+ }
134
+ for (const e of entries) {
135
+ if (e.isSymbolicLink()) continue;
136
+ const full = path.join(cur, e.name);
137
+ if (e.isDirectory()) {
138
+ if (!SKIP_DIRS.has(e.name)) stack.push(full);
139
+ } else if (e.name.toLowerCase().endsWith('.md')) {
140
+ out.push(full);
141
+ }
142
+ }
143
+ }
144
+ return out.sort();
145
+ }
146
+
147
+ /** `true` when `child` is at or below `root`. */
148
+ function isUnder(child: string, root: string): boolean {
149
+ const rel = path.relative(root, child);
150
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
151
+ }
152
+
153
+ export function scan(root: string): ScanResult {
154
+ const roadmapRoot = path.join(root, ...ROADMAP_ROOT_REL.split('/'));
155
+ const files = walkMarkdown(root);
156
+ const findings: Finding[] = [];
157
+ for (const f of files) {
158
+ if (isUnder(f, roadmapRoot)) continue;
159
+ // Cheap name pre-filter first — the SHARED definition, imported, so the
160
+ // roadmap corpus and this guard cannot drift apart.
161
+ if (!isRoadmapCandidate(f)) continue;
162
+ let text: string;
163
+ try {
164
+ text = fs.readFileSync(f, 'utf-8');
165
+ } catch {
166
+ continue;
167
+ }
168
+ const shape = roadmapShape(text);
169
+ if (!shape.isRoadmap) continue;
170
+ findings.push({
171
+ rel: path.relative(root, f).split(path.sep).join('/'),
172
+ signals: shape.signals,
173
+ });
174
+ }
175
+ return { findings, scanned: files.length };
176
+ }
177
+
178
+ interface Args {
179
+ quiet: boolean;
180
+ selfTest: boolean;
181
+ root: string;
182
+ }
183
+
184
+ function parseArgs(argv: readonly string[]): Args {
185
+ const args: Args = { quiet: false, selfTest: false, root: REPO_ROOT };
186
+ for (let i = 0; i < argv.length; i++) {
187
+ const a = argv[i] as string;
188
+ if (a === '--quiet') args.quiet = true;
189
+ else if (a === '--self-test') args.selfTest = true;
190
+ else if (a === '--root') args.root = path.resolve(argv[++i] ?? '.');
191
+ else if (a === '-h' || a === '--help') {
192
+ process.stdout.write(
193
+ 'usage: check_agent_artifact_location [-h] [--quiet] [--root DIR] [--self-test]\n',
194
+ );
195
+ process.exit(0);
196
+ } else {
197
+ process.stderr.write(
198
+ `check_agent_artifact_location: error: unrecognized arguments: ${a}\n`,
199
+ );
200
+ process.exit(2);
201
+ }
202
+ }
203
+ return args;
204
+ }
205
+
206
+ /** A minimal file with all three shape signals. */
207
+ const ROADMAP_FIXTURE =
208
+ '---\ncomplexity: lightweight\nstatus: ready\n---\n' +
209
+ '# Road to a fixture\n\n## Phase 1 - a phase\n\n- [ ] **1.1 A step.**\n';
210
+
211
+ /** Same content, but not roadmap-shaped — frontmatter only. */
212
+ const PROSE_FIXTURE = '---\ntitle: a design note\n---\n\n# A design note\n\n- [ ] a todo\n';
213
+
214
+ function selfTestCases(): SelfTestCase[] {
215
+ const mk = (rel: string, body: string): string => {
216
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'artloc-'));
217
+ const full = path.join(dir, ...rel.split('/'));
218
+ fs.mkdirSync(path.dirname(full), { recursive: true });
219
+ fs.writeFileSync(full, body);
220
+ return dir;
221
+ };
222
+ const run = (dir: string): number =>
223
+ runGateCli(
224
+ REPO_ROOT,
225
+ 'src/scripts/check_agent_artifact_location.ts',
226
+ ['--root', dir, '--quiet'],
227
+ REPO_ROOT,
228
+ );
229
+ return [
230
+ {
231
+ name: 'a roadmap-shaped file under docs/roadmaps/ is reported',
232
+ expect: 'reject',
233
+ run: () => run(mk('docs/roadmaps/road-to-a-fixture.md', ROADMAP_FIXTURE)),
234
+ },
235
+ {
236
+ name: 'a roadmap-shaped file at the tree root is reported',
237
+ expect: 'reject',
238
+ run: () => run(mk('road-to-a-fixture.md', ROADMAP_FIXTURE)),
239
+ },
240
+ {
241
+ name: 'the SAME file under agents/roadmaps/ is not reported',
242
+ expect: 'accept',
243
+ run: () => run(mk('agents/roadmaps/road-to-a-fixture.md', ROADMAP_FIXTURE)),
244
+ },
245
+ {
246
+ name: 'a file under agents/roadmaps/later/ is not reported',
247
+ expect: 'accept',
248
+ run: () => run(mk('agents/roadmaps/later/road-to-a-fixture.md', ROADMAP_FIXTURE)),
249
+ },
250
+ {
251
+ name: 'prose carrying frontmatter and a checkbox but no phase is not reported',
252
+ expect: 'accept',
253
+ run: () => run(mk('docs/a-design-note.md', PROSE_FIXTURE)),
254
+ },
255
+ ];
256
+ }
257
+
258
+ export function main(argv?: readonly string[]): number {
259
+ const args = parseArgs(argv ?? process.argv.slice(2));
260
+
261
+ if (args.selfTest) {
262
+ return runSelfTest({
263
+ gate: 'check_agent_artifact_location',
264
+ cases: selfTestCases(),
265
+ minCases: 5,
266
+ minRejectCases: 2,
267
+ });
268
+ }
269
+
270
+ let result: ScanResult;
271
+ try {
272
+ result = scan(args.root);
273
+ } catch (e) {
274
+ const msg = e instanceof Error ? e.message : String(e);
275
+ process.stderr.write(`❌ internal error: ${msg}\n`);
276
+ return 3;
277
+ }
278
+
279
+ // The unit is every `.md` the walk VISITED, not the matches: zero matches is
280
+ // the normal clean state, so only the unfiltered walk can tell "nothing
281
+ // misplaced" from "nothing read".
282
+ try {
283
+ reportScanned({
284
+ gate: 'check_agent_artifact_location',
285
+ scanned: result.scanned,
286
+ units: 'markdown file(s)',
287
+ roots: [path.relative(REPO_ROOT, args.root) || '.'],
288
+ });
289
+ } catch (exc) {
290
+ if (exc instanceof DeadScopeError) {
291
+ process.stderr.write(`❌ ${exc.message}\n`);
292
+ return 1;
293
+ }
294
+ throw exc;
295
+ }
296
+
297
+ if (result.findings.length === 0) {
298
+ if (!args.quiet) {
299
+ process.stdout.write(
300
+ `✅ check_agent_artifact_location: no roadmap-shaped file outside ` +
301
+ `${ROADMAP_ROOT_REL}/ (${String(result.scanned)} markdown file(s) read).\n`,
302
+ );
303
+ }
304
+ return 0;
305
+ }
306
+
307
+ process.stderr.write(
308
+ `❌ check_agent_artifact_location: ${String(result.findings.length)} ` +
309
+ `roadmap-shaped file(s) outside ${ROADMAP_ROOT_REL}/:\n`,
310
+ );
311
+ for (const f of result.findings) {
312
+ process.stderr.write(` ${f.rel}\n`);
313
+ process.stderr.write(` reads as a roadmap because it carries ${f.signals.join(', ')}\n`);
314
+ }
315
+ process.stderr.write(
316
+ ` Move it to ${ROADMAP_ROOT_REL}/, or — if it is genuinely not a roadmap — ` +
317
+ 'say which of the three signals it should not be carrying.\n' +
318
+ ' NOTE: this gate scans THIS repository only. A consumer project never runs\n' +
319
+ ' it; `agent-config doctor` is the surface that carries a finding outward.\n',
320
+ );
321
+ return 1;
322
+ }
323
+
324
+ function _isCliEntry(): boolean {
325
+ if (process.argv[1] === undefined) return false;
326
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
327
+ if (import.meta.url === argvUrl) return true;
328
+ try {
329
+ return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
330
+ } catch {
331
+ return false;
332
+ }
333
+ }
334
+
335
+ if (_isCliEntry()) {
336
+ process.exit(main());
337
+ }
@@ -20,10 +20,14 @@
20
20
  * - `check_command_count_messaging.ts` keeps the command-specific
21
21
  * active-vs-shim split checks.
22
22
  *
23
- * Scope: flagship surfaces only (SURFACES below). Dated census / analysis
24
- * snapshots (SKILL_CENSUS, skills-taxonomy, positioning-evidence, …) carry
25
- * point-in-time counts by design and are deliberately out of scope — the
26
- * gate's charter is the surfaces a fresh reader treats as current.
23
+ * Scope: flagship surfaces (SURFACES below) PLUS every governed rule under
24
+ * `src/rules/` (RULE_SURFACE_DIR). Rules were the largest hole this gate had:
25
+ * they are the most-delivered surface this package ships — a wrong self-count
26
+ * there reaches every consumer session — and the list below named sixteen doc
27
+ * paths and no rule path. Dated census / analysis snapshots (SKILL_CENSUS,
28
+ * skills-taxonomy, positioning-evidence, …) carry point-in-time counts by
29
+ * design and are deliberately out of scope — the gate's charter is the surfaces
30
+ * a fresh reader treats as current.
27
31
  *
28
32
  * Exit codes: 0 clean · 1 drift or internal inconsistency.
29
33
  */
@@ -66,6 +70,41 @@ export const SURFACES: readonly string[] = [
66
70
  // absent there. A gate must judge repo content, not untracked local state.
67
71
  ];
68
72
 
73
+ /**
74
+ * Governed rules, enumerated rather than listed.
75
+ *
76
+ * A hand-maintained list of 120 rule paths would go stale on the first rule
77
+ * added, which is the drift class this gate exists to catch — so the rule
78
+ * surface is a directory, walked at run time.
79
+ */
80
+ export const RULE_SURFACE_DIR = 'src/rules';
81
+
82
+ export function rule_surfaces(root: string = ROOT): string[] {
83
+ const dir = path.join(root, RULE_SURFACE_DIR);
84
+ if (!fs.existsSync(dir)) {
85
+ return [];
86
+ }
87
+ return fs
88
+ .readdirSync(dir)
89
+ .filter((f) => f.endsWith('.md'))
90
+ .sort()
91
+ .map((f) => `${RULE_SURFACE_DIR}/${f}`);
92
+ }
93
+
94
+ /**
95
+ * A count that belongs to a dated measurement, not to this package's live
96
+ * totals.
97
+ *
98
+ * The gate's charter already excludes point-in-time snapshots; until now it
99
+ * could only express that by leaving a whole FILE out. A rule may carry a dated
100
+ * host measurement inside a paragraph of live prose, and neither excluding the
101
+ * rule nor rewriting the measurement to today's total is correct — the second
102
+ * falsifies recorded evidence. The marker is per line, and it is a claim a
103
+ * reviewer can falsify: the line must name the date or the artefact the figure
104
+ * was measured against.
105
+ */
106
+ export const DATED_MEASUREMENT_MARKER = '<!-- count: dated -->';
107
+
69
108
  /** kind → canonical-count resolver. "governed rules" is the canonical total
70
109
  * phrasing for rules; bare "N rules" is NOT matched (too many legitimate
71
110
  * subset scopes: kernel rules, router rules, maintainer-only rules …). */
@@ -108,6 +147,9 @@ export function scan_text(
108
147
  for (const [kind, pattern] of KIND_PATTERNS) {
109
148
  for (let i = 0; i < lines.length; i++) {
110
149
  const re = new RegExp(pattern.source, pattern.flags);
150
+ if (lines[i]!.includes(DATED_MEASUREMENT_MARKER)) {
151
+ continue;
152
+ }
111
153
  for (const m of lines[i]!.matchAll(re)) {
112
154
  const found = Number.parseInt(m[2]!, 10);
113
155
  (seen[kind] ??= new Set()).add(found);
@@ -219,6 +261,15 @@ export interface CoverageGap {
219
261
  *
220
262
  * A position counts as anchored when some `TARGETS` pattern for the same
221
263
  * file, bound to a compatible kind, matches the same line.
264
+ *
265
+ * `src/rules/**` is scanned for VALUE drift but is deliberately excluded from
266
+ * this pass, and the reason is mechanical rather than stylistic: satisfying it
267
+ * would require `update_counts` to become a writer into `src/rules/`, where the
268
+ * kernel rules live behind `block_kernel_rule_writes`. A generator that must
269
+ * never touch part of its own target directory is a generator waiting to be
270
+ * disarmed by a path move. So a stale count in a rule fails on value and a human
271
+ * fixes it — the coverage claim this pass makes stays true of the surfaces it
272
+ * actually covers, instead of being widened into a claim it cannot keep.
222
273
  */
223
274
  export function anchor_coverage_gaps(root: string = ROOT): CoverageGap[] {
224
275
  const anchorsByFile = new Map<string, ReadonlyArray<[string, string]>>();
@@ -254,6 +305,16 @@ export function anchor_coverage_gaps(root: string = ROOT): CoverageGap[] {
254
305
 
255
306
  export function main(argv: readonly string[] = process.argv.slice(2)): number {
256
307
  const QUIET = argv.includes('--quiet');
308
+ // Enumerated, not listed. An empty walk means the rule tree moved and this
309
+ // gate is watching nothing — a dead scope, never a clean run.
310
+ const rules = rule_surfaces();
311
+ if (rules.length === 0) {
312
+ process.stderr.write(
313
+ `❌ check_artefact_count_messaging: ${RULE_SURFACE_DIR}/ resolved to no .md file — ` +
314
+ 'the rule surface moved and this gate would pass by blindness.\n',
315
+ );
316
+ return 1;
317
+ }
257
318
  // This gate walks no tree — it guards a hand-maintained surface list, and
258
319
  // every missing entry is skipped with a warning so nothing hard-breaks on a
259
320
  // doc rename. That tolerance is also the failure mode: a repo-wide docs
@@ -262,7 +323,7 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
262
323
  try {
263
324
  assertWatchlistResolves({
264
325
  gate: 'check_artefact_count_messaging',
265
- candidates: [...SURFACES, ...STRUCTURED_SURFACES.map((s) => s.file)],
326
+ candidates: [...SURFACES, ...STRUCTURED_SURFACES.map((s) => s.file), ...rules],
266
327
  repoRoot: ROOT,
267
328
  });
268
329
  } catch (exc) {
@@ -285,7 +346,7 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
285
346
 
286
347
  const allFindings: Finding[] = [];
287
348
  const global_seen: Record<string, Set<number>> = {};
288
- for (const rel of SURFACES) {
349
+ for (const rel of [...SURFACES, ...rules]) {
289
350
  const p = path.join(ROOT, rel);
290
351
  if (!fs.existsSync(p)) {
291
352
  process.stderr.write(` ⚠️ surface missing (skipped): ${rel}\n`);
@@ -347,8 +408,13 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
347
408
  process.stdout.write(` internal inconsistency — ${line}\n`);
348
409
  }
349
410
  process.stdout.write(
350
- '\nFix: run `./scripts-run src/scripts/update_counts` — every checked position\n' +
351
- 'is anchored, so the generator can write it. Never hand-type a count.\n',
411
+ '\nFix: on a flagship surface, run `./scripts-run src/scripts/update_counts` —\n' +
412
+ 'every anchored position is generator-written, so never hand-type one there.\n' +
413
+ `Under ${RULE_SURFACE_DIR}/ the generator does not write (the kernel rules live\n` +
414
+ 'there behind block_kernel_rule_writes), so a rule count is a hand edit: correct\n' +
415
+ 'the number, or — if the figure is a dated measurement rather than a live\n' +
416
+ `self-count — mark that line \`${DATED_MEASUREMENT_MARKER}\` and say what it was\n` +
417
+ 'measured against.\n',
352
418
  );
353
419
  }
354
420
 
@@ -628,6 +628,26 @@ export interface Summary {
628
628
  frequency_gap: number;
629
629
  /** Rules with no `obligation_frequency` to join — the nine kernel rules. */
630
630
  frequency_unclassified: number;
631
+ /**
632
+ * The three classes that make `undeclared` readable.
633
+ *
634
+ * WHY THEY SHIP IN THE ARTEFACT. `undeclared` is the number reviewers quote,
635
+ * and quoted bare it reads as a backlog of rules somebody forgot to wire.
636
+ * Most of it is nothing of the kind. `kernel_denied` rules cannot carry an
637
+ * `enforced_by` field at all — `block_kernel_rule_writes` refuses the write
638
+ * with no agent-accessible override, so their absence is structural.
639
+ * `observer` rules have a carrier that fires and cannot block by design.
640
+ * `carrier_less` rules have no machine carrier of any kind.
641
+ *
642
+ * A reader subtracting these from `undeclared` gets the population where
643
+ * wiring is actually the missing thing — and, more usefully, sees at a glance
644
+ * that the observer set (10) is the reachable one: a floor set against the
645
+ * full `undeclared` would stall in its second release and be ignored, which
646
+ * is the failure `check_release_highlights.ts` already documents for its own
647
+ * warning.
648
+ */
649
+ kernel_denied: number;
650
+ carrier_less: number;
631
651
  /**
632
652
  * The denominator every figure above is taken over, WITH its frame. See
633
653
  * {@link denominator_frames} for why the frame ships even when the two
@@ -652,6 +672,14 @@ export function summarise(rows: RuleCoverage[]): Summary {
652
672
  blocking_pct: rows.length === 0 ? 0 : Math.round((blocking / rows.length) * 1000) / 10,
653
673
  frequency_gap: count((r) => r.frequency_verdict === 'gap'),
654
674
  frequency_unclassified: count((r) => r.frequency_verdict === 'unclassified'),
675
+ kernel_denied: count(
676
+ (r) => KERNEL_RULE_ID_SET.has(r.id) && r.frequency_verdict === 'unclassified',
677
+ ),
678
+ carrier_less: count(
679
+ (r) =>
680
+ r.declared.length > 0 &&
681
+ r.declared.every((d) => d.startsWith('observer:maintainer-review')),
682
+ ),
655
683
  frames: denominator_frames(rows.length),
656
684
  };
657
685
  }
@@ -779,7 +807,13 @@ function main(argv: string[]): number {
779
807
  'blocking and in that bucket at once. Coverage is a ratchet: --check fails ' +
780
808
  'when blocking falls, or unwired / local_only / missing / frequency_gap ' +
781
809
  'rises. Regenerate intentionally with --write-baseline when the change is ' +
782
- 'the point.',
810
+ 'the point. READ `undeclared` WITH ITS THREE CLASSES, never bare: ' +
811
+ '`kernel_denied` cannot carry an enforced_by field at all ' +
812
+ '(block_kernel_rule_writes refuses the write, no agent-accessible ' +
813
+ 'override), `observer` has a carrier that fires and cannot block by ' +
814
+ 'design, and `carrier_less` has no machine carrier of any kind. The ' +
815
+ 'reachable population is the observer set, not `undeclared` — a floor ' +
816
+ 'set against the full number would stall in its second release.',
783
817
  summary,
784
818
  },
785
819
  null,
@@ -811,6 +845,11 @@ function main(argv: string[]): number {
811
845
  ` frequency: ${summary.frequency_gap} gap · ${summary.frequency_unclassified} unclassified ` +
812
846
  `(kernel — block_kernel_rule_writes denies the field)`,
813
847
  );
848
+ lines.push(
849
+ ` undeclared ${summary.undeclared} splits: ${summary.kernel_denied} kernel-denied · ` +
850
+ `${summary.observer} observer · ${summary.carrier_less} carrier-less — ` +
851
+ `the observer set is the reachable population`,
852
+ );
814
853
  // The denominator is published WITH its frame, and it is the only sanctioned
815
854
  // source for that number: `check_enforcement_denominator` reds when a
816
855
  // tracked doc carries an enforcement count this resolver did not produce.