@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
@@ -21,14 +21,16 @@
21
21
  * (`claim:web-launch-readiness-finds-more`, status `unbacked`). Absent the
22
22
  * opt-in, this exits 0 and says why rather than auditing.
23
23
  *
24
- * Exit codes: 0 = no critical/high finding (or not enabled) · 1 = a blocking
25
- * finding · 2 = usage or a dead scan scope.
24
+ * Exit codes: 0 = no critical/high finding and nothing left undecided (or not
25
+ * enabled) · 1 = a blocking finding, OR an applicable check the instrument could
26
+ * not decide · 2 = usage or a dead scan scope.
26
27
  */
27
28
  import * as fs from 'node:fs';
28
29
  import * as path from 'node:path';
29
30
  import process from 'node:process';
30
31
  import { fileURLToPath } from 'node:url';
31
32
 
33
+ import { asOf } from './_lib/as_of.js';
32
34
  import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
33
35
 
34
36
  const HERE = fileURLToPath(import.meta.url);
@@ -42,7 +44,12 @@ export type Tier = 'critical' | 'high' | 'medium' | 'situational';
42
44
  *
43
45
  * `applies_to` decides WHETHER a check applies; a region decides HOW SEVERELY.
44
46
  * A legal page is not more or less *applicable* in Germany — it is more or less
45
- * *optional*, because TMG 5 and DSGVO Art. 13 make it owed rather than advisable.
47
+ * *optional*, because German and EU law make it owed rather than advisable. The
48
+ * two citations behind that sentence are NOT restated here: they live in the
49
+ * escalation row's `authority` field in `web-launch-readiness.json`, where a
50
+ * `review_by` date can lapse them. That is the whole point of the
51
+ * `ddg-citation-authority` decision — a statute in prose cited law already dead
52
+ * for twenty-seven months and nothing could see it.
46
53
  * Modelling it as a tier escalation keeps one check with one implementation and
47
54
  * puts the jurisdiction where it belongs: on the consequence.
48
55
  */
@@ -56,6 +63,10 @@ export interface CheckDef {
56
63
  why: string;
57
64
  remediation: string;
58
65
  verification: string;
66
+ /** Statute + where it can be read. Required once the row asserts a legal basis. */
67
+ authority?: string;
68
+ /** ISO date the citation must be re-read by. A lapsed date FAILS the loader. */
69
+ review_by?: string;
59
70
  }
60
71
 
61
72
  export interface Finding {
@@ -75,6 +86,22 @@ export interface Skipped {
75
86
  reason: string;
76
87
  }
77
88
 
89
+ /**
90
+ * An applicable check the instrument could not decide.
91
+ *
92
+ * NOT a finding (nothing was established) and NOT a pass (nothing was cleared).
93
+ * The third state exists because a detector that guesses on an undecidable input
94
+ * is worse than one that abstains — and because a pass this checker did not earn
95
+ * is exactly the silent-green defect its own header names.
96
+ */
97
+ export interface Undecided {
98
+ check: string;
99
+ tier: Tier;
100
+ /** `file:line` the instrument stopped at, so the reader can look at it. */
101
+ location: string;
102
+ reason: string;
103
+ }
104
+
78
105
  /** A tier the region axis moved, and why. Reported so the axis is visible. */
79
106
  export interface Escalated {
80
107
  check: string;
@@ -93,6 +120,8 @@ export interface Report {
93
120
  passed: string[];
94
121
  /** Applicable checks with no implementation yet — never counted as passed. */
95
122
  unimplemented: string[];
123
+ /** Applicable, implemented, and undecidable on THIS input. Never a pass. */
124
+ unknown: Undecided[];
96
125
  /** Tiers the region axis raised. Empty on `unspecified`, by construction. */
97
126
  escalated: Escalated[];
98
127
  scanned_files: number;
@@ -103,6 +132,141 @@ export interface Escalation {
103
132
  region: Region;
104
133
  to: Tier;
105
134
  why: string;
135
+ /** Same contract as `CheckDef` — an escalation row is a legal claim too. */
136
+ authority?: string;
137
+ review_by?: string;
138
+ }
139
+
140
+ /* -- 2.1/2.2: a legal claim carries its own authority and expiry ------------ */
141
+
142
+ /**
143
+ * A statute reference: a named instrument, or a numbered article/section.
144
+ *
145
+ * The list is deliberately short and abbreviation-shaped. A broader "any legal
146
+ * word" match would fire on `regulatory exposure` and turn every jurisdictional
147
+ * sentence into a citation the gate then demands a source for — which is how a
148
+ * scoped gate becomes a blanket one nobody can satisfy. The numbered-provision
149
+ * arm (`Art. 13`, `§ 5`) catches an instrument the list does not name yet.
150
+ *
151
+ * `TMG` is retained as a token on purpose, and this is a HISTORICAL NOTE rather
152
+ * than a citation: the statute was superseded by the DDG in May 2024, so a row
153
+ * that names it again is exactly what this detector must still catch.
154
+ */
155
+ export const STATUTE_RE =
156
+ /\b(TMG|DDG|TTDSG|TDDDG|DSGVO|GDPR|BDSG|RDG|UWG|BGB|HGB|StGB|CCPA|CPRA|HIPAA|ePrivacy|DSA|DMA|BFSG|EAA)\b|(?:\bArt\.|\bArticle\b|§)\s*\d/;
157
+
158
+ /** A config row that may assert a legal basis: a check, or a region escalation. */
159
+ export interface LegalRow {
160
+ kind: 'check' | 'escalation';
161
+ id: string;
162
+ why: string;
163
+ authority?: string;
164
+ review_by?: string;
165
+ }
166
+
167
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
168
+
169
+ /**
170
+ * Today, as an ISO date, through the repository's one sanctioned clock seam.
171
+ *
172
+ * NOT `new Date()`: a lapsed-date verdict read off the wall clock is a verdict
173
+ * that greens on Monday and reds on Friday from the same tree, which is the
174
+ * defect `lint_deterministic_time` exists to keep out of a gate. `asOf()`
175
+ * resolves `--as-of`, `AC_AS_OF`, the commit date under CI, and only then the
176
+ * wall clock — with its own non-reproducibility warning attached.
177
+ */
178
+ export function asOfToday(): string {
179
+ return asOf().toISOString().slice(0, 10);
180
+ }
181
+
182
+ /** Every row of the config that could carry a legal claim, in report order. */
183
+ export function legalRowsOf(doc: {
184
+ checks?: CheckDef[];
185
+ regions?: { escalations?: Escalation[] };
186
+ }): LegalRow[] {
187
+ const rows: LegalRow[] = [];
188
+ for (const c of doc.checks ?? []) {
189
+ rows.push({
190
+ kind: 'check',
191
+ id: c.id,
192
+ why: c.why ?? '',
193
+ ...(c.authority === undefined ? {} : { authority: c.authority }),
194
+ ...(c.review_by === undefined ? {} : { review_by: c.review_by }),
195
+ });
196
+ }
197
+ for (const e of doc.regions?.escalations ?? []) {
198
+ rows.push({
199
+ kind: 'escalation',
200
+ id: `${e.check}@${e.region}`,
201
+ why: e.why ?? '',
202
+ ...(e.authority === undefined ? {} : { authority: e.authority }),
203
+ ...(e.review_by === undefined ? {} : { review_by: e.review_by }),
204
+ });
205
+ }
206
+ return rows;
207
+ }
208
+
209
+ /**
210
+ * The legal-row contract, as a list of violations (empty = clean).
211
+ *
212
+ * Three obligations, and the third is the one that makes the other two more
213
+ * than decoration:
214
+ *
215
+ * 1. a row whose `why` NAMES a statute must carry `authority` and `review_by`;
216
+ * 2. `authority` and `review_by` travel together — a citation with no expiry
217
+ * is the comment this contract exists to replace, and an expiry with no
218
+ * citation dates nothing. This half is what keeps the contract intact when
219
+ * the statute moves OUT of the prose and INTO `authority`;
220
+ * 3. a `review_by` on or after `asOf` is live; anything earlier is LAPSED and
221
+ * is a violation rather than a warning. A date nothing reads is a comment,
222
+ * and this repository already documents a warning ignored eighteen times.
223
+ *
224
+ * A row that asserts no legal basis is not policed. The gate is scoped, not
225
+ * blanket: demanding a citation from `A framework default error page loses the
226
+ * visitor` would make the contract unsatisfiable and therefore ignorable.
227
+ */
228
+ export function legalRowViolations(rows: readonly LegalRow[], asOf: string): string[] {
229
+ const out: string[] = [];
230
+ for (const r of rows) {
231
+ const namesStatute = STATUTE_RE.test(r.why);
232
+ const hasAuthority = (r.authority ?? '').trim().length > 0;
233
+ const hasDate = (r.review_by ?? '').trim().length > 0;
234
+ const where = `${r.kind} "${r.id}"`;
235
+ // One violation per MISSING FIELD, never one per rule that noticed it.
236
+ // Dropping `authority` from a statute row trips both obligation 1 and
237
+ // obligation 2, and reporting it twice would make the count a property
238
+ // of the rule set rather than of the config.
239
+ const isLegal = namesStatute || hasAuthority || hasDate;
240
+ if (isLegal && !hasAuthority) {
241
+ out.push(
242
+ `${where}: carries no \`authority\` — ` +
243
+ (namesStatute
244
+ ? 'its `why` names a statute, and a citation with no source is a comment rather than a claim anyone can re-check.'
245
+ : 'a `review_by` with no citation dates nothing.'),
246
+ );
247
+ }
248
+ if (isLegal && !hasDate) {
249
+ out.push(
250
+ `${where}: carries no \`review_by\` — ` +
251
+ (namesStatute
252
+ ? 'its `why` names a statute, and the citation has no date by which it must be re-read.'
253
+ : 'an undated citation cannot go stale visibly.'),
254
+ );
255
+ }
256
+ if (!hasDate) continue;
257
+ const date = (r.review_by ?? '').trim();
258
+ if (!ISO_DATE_RE.test(date) || Number.isNaN(Date.parse(date))) {
259
+ out.push(`${where}: \`review_by\` is "${date}", which is not an ISO YYYY-MM-DD date.`);
260
+ continue;
261
+ }
262
+ if (date < asOf) {
263
+ out.push(
264
+ `${where}: \`review_by\` lapsed on ${date} (as of ${asOf}) — re-read the citation, ` +
265
+ 'then move the date. Overdue is a finding, not a silent pass.',
266
+ );
267
+ }
268
+ }
269
+ return out;
106
270
  }
107
271
 
108
272
  export function loadConfig(root = REPO_ROOT): {
@@ -144,6 +308,15 @@ export function loadConfig(root = REPO_ROOT): {
144
308
  'situational tier for a legally owed page.',
145
309
  );
146
310
  }
311
+ const legal = legalRowViolations(legalRowsOf(doc), asOfToday());
312
+ if (legal.length > 0) {
313
+ // A gate that audited a site while shipping an unbacked legal claim of
314
+ // its own would be auditing from a position it has not earned.
315
+ throw new DeadScopeError(
316
+ 'check_web_launch_readiness',
317
+ `${CONFIG_REL} ships a legal claim the row cannot back:\n - ${legal.join('\n - ')}`,
318
+ );
319
+ }
147
320
  return { checks, siteTypes, regions, escalations };
148
321
  }
149
322
 
@@ -479,17 +652,44 @@ const requiredLegalPages: Impl = (files) => {
479
652
  };
480
653
 
481
654
  /**
482
- * Analytics loads only behind consent.
655
+ * Analytics loads only behind consent — INCLUDING the load order (3.1).
656
+ *
657
+ * The row's own `verification` field asks for the runtime assertion: *"Load the
658
+ * page with consent declined and assert no request to the analytics origin."*
659
+ * A static reader cannot load a page. The strongest assertion it CAN make is
660
+ * source order within one document, so that is what it makes — and where even
661
+ * that is undecidable it returns `unknown` (3.2) rather than a pass.
483
662
  *
484
- * The finding is analytics present with NO consent mechanism anywhere in the
485
- * build — never "analytics present", which would flag every site that does
486
- * consent correctly. The gate cannot see the wiring order, and says so in the
487
- * evidence rather than implying it checked.
663
+ * What the ordering pass covers, stated so the message can say it: the position
664
+ * of the measurement tag relative to the gate script INSIDE one file. What it
665
+ * does not cover: runtime order, deferred or dynamic imports, order established
666
+ * by a bundler across files, and anything a minifier put on one line — each of
667
+ * which produces an `unknown`, never a silent pass.
488
668
  */
489
669
  const ANALYTICS_RE = /(googletagmanager\.com|google-analytics\.com|gtag\(|plausible\.io|matomo|segment\.com|hotjar)/i;
490
670
  const CONSENT_RE = /(consent|cookiebanner|cookie-banner|cookieconsent|klaro|usercentrics|borlabs|osano)/i;
491
- const analyticsAndConsent: Impl = (files) => {
671
+
672
+ /**
673
+ * A line that can actually LOAD something, as opposed to prose about loading.
674
+ *
675
+ * `consent-order-bad` says the word *consent* in its `<meta description>` and
676
+ * still fires: a sentence describing a gate is not a gate, and a positional
677
+ * detector that counted it would report a correctly ordered page while the tag
678
+ * fires first. Applied to markup only — every line of a `.js` file is code.
679
+ */
680
+ const EXECUTABLE_LINE_RE = /<script\b|\bsrc\s*=|\bimport\b|\brequire\s*\(|\.js\b/i;
681
+
682
+ /** Past this, a line is a bundler artefact and its internal order is not source order. */
683
+ const MINIFIED_LINE_CHARS = 400;
684
+
685
+ interface ConsentOrder {
686
+ hits: Located[];
687
+ undecided: Located[];
688
+ }
689
+
690
+ function analyseConsentOrder(files: readonly SourceFile[]): ConsentOrder {
492
691
  const hits: Located[] = [];
692
+ const undecided: Located[] = [];
493
693
  // Prose is excluded from BOTH sides, and the fixture is why: a `.md` file
494
694
  // in the build tree describing the consent banner matched CONSENT_RE, so
495
695
  // the check reported a pass on a fixture seeded to fire it. Documentation
@@ -497,21 +697,92 @@ const analyticsAndConsent: Impl = (files) => {
497
697
  // runs the other way, so an analytics name mentioned in a README must not
498
698
  // count as analytics either.
499
699
  const code = files.filter((f) => !/\.md$/i.test(f.base));
500
- const anyConsent = code.some((f) => CONSENT_RE.test(f.text));
501
- if (anyConsent) return hits;
700
+ const loadable = (f: SourceFile, line: string): boolean => !isPage(f) || EXECUTABLE_LINE_RE.test(line);
701
+ const firstLoad = (f: SourceFile, re: RegExp): number =>
702
+ f.lines.findIndex((l) => re.test(l) && loadable(f, l));
703
+ const anyConsent = code.some((f) => firstLoad(f, CONSENT_RE) >= 0);
704
+
502
705
  for (const f of code) {
503
- f.lines.forEach((line, i) => {
504
- if (!ANALYTICS_RE.test(line)) return;
706
+ const a = firstLoad(f, ANALYTICS_RE);
707
+ if (a < 0) continue;
708
+ if (!anyConsent) {
709
+ // Unchanged from the pre-3.1 behaviour, and still the sharpest
710
+ // finding available: nothing in the build can gate the tag.
711
+ f.lines.forEach((line, i) => {
712
+ if (!ANALYTICS_RE.test(line) || !loadable(f, line)) return;
713
+ hits.push({
714
+ file: f.rel,
715
+ line: i + 1,
716
+ text:
717
+ `${line.trim().slice(0, 120)} — analytics is loaded here and no consent mechanism ` +
718
+ 'was found anywhere in the build, so nothing can gate it. Source order was not the ' +
719
+ 'question: there is no gate to be ordered against.',
720
+ });
721
+ });
722
+ continue;
723
+ }
724
+ const c = firstLoad(f, CONSENT_RE);
725
+ const aLine = f.lines[a] ?? '';
726
+ if (c < 0) {
727
+ undecided.push({
728
+ file: f.rel,
729
+ line: a + 1,
730
+ text:
731
+ 'analytics is loaded here and the consent mechanism lives in another file. Which of ' +
732
+ 'the two the browser runs first is decided by the bundler and the page, not by source ' +
733
+ 'order across files, and a static read cannot establish it.',
734
+ });
735
+ continue;
736
+ }
737
+ if (c === a) {
738
+ undecided.push({
739
+ file: f.rel,
740
+ line: a + 1,
741
+ text:
742
+ 'the analytics tag and the consent gate are on the SAME line, so line order cannot ' +
743
+ 'separate them. Ordering is undecidable on this input.',
744
+ });
745
+ continue;
746
+ }
747
+ if (aLine.length > MINIFIED_LINE_CHARS) {
748
+ undecided.push({
749
+ file: f.rel,
750
+ line: a + 1,
751
+ text:
752
+ `this line is ${String(aLine.length)} characters, i.e. minified — the order inside a ` +
753
+ 'bundler artefact is not the source order this pass reads.',
754
+ });
755
+ continue;
756
+ }
757
+ if (a < c) {
505
758
  hits.push({
506
759
  file: f.rel,
507
- line: i + 1,
760
+ line: a + 1,
508
761
  text:
509
- `${line.trim().slice(0, 120)} — analytics present and no consent mechanism ` +
510
- 'found anywhere in the build. Load order is NOT checked here.',
762
+ `${aLine.trim().slice(0, 120)} — the analytics tag is at line ${String(a + 1)}, ` +
763
+ `ABOVE the consent gate at line ${String(c + 1)} of the same file, so it is reached ` +
764
+ 'before consent can be recorded. Source order inside this file is what was checked; ' +
765
+ 'runtime order, dynamic imports and cross-file bundler order were not.',
511
766
  });
512
- });
767
+ }
513
768
  }
514
- return hits;
769
+ return { hits, undecided };
770
+ }
771
+
772
+ const analyticsAndConsent: Impl = (files) => analyseConsentOrder(files).hits;
773
+
774
+ /**
775
+ * Checks that can report `unknown`, and the probe that decides when they do.
776
+ *
777
+ * A separate table rather than a widened `Impl` signature: the invariant that an
778
+ * EMPTY `Located[]` means "applied and found nothing" is what keeps the
779
+ * silent-green defect out of `IMPLS`, and folding a third state into that return
780
+ * type would put the invariant back in play for all eight implementations.
781
+ */
782
+ type Undecide = (files: readonly SourceFile[]) => Located[];
783
+
784
+ export const UNDECIDABLE: Readonly<Record<string, Undecide>> = {
785
+ 'analytics-and-consent-wiring': (files) => analyseConsentOrder(files).undecided,
515
786
  };
516
787
 
517
788
  /** First line containing `needle`, 1-indexed; 1 when absent (an absence has no line). */
@@ -560,6 +831,7 @@ export function audit(
560
831
  const skipped: Skipped[] = [];
561
832
  const passed: string[] = [];
562
833
  const unimplemented: string[] = [];
834
+ const unknown: Undecided[] = [];
563
835
  const escalated: Escalated[] = [];
564
836
  // ONE walk for every check. Eight walkers over the same bytes is eight
565
837
  // times the IO and eight chances to disagree about what counts as a file.
@@ -586,7 +858,11 @@ export function audit(
586
858
  impl === undefined
587
859
  ? scanIndexability(buildDir).hits
588
860
  : impl(files);
589
- if (hits.length === 0) {
861
+ const undecided = UNDECIDABLE[c.id]?.(files) ?? [];
862
+ for (const u of undecided) {
863
+ unknown.push({ check: c.id, tier, location: `${u.file}:${String(u.line)}`, reason: u.text });
864
+ }
865
+ if (hits.length === 0 && undecided.length === 0) {
590
866
  passed.push(c.id);
591
867
  continue;
592
868
  }
@@ -609,6 +885,7 @@ export function audit(
609
885
  skipped,
610
886
  passed,
611
887
  unimplemented,
888
+ unknown,
612
889
  escalated,
613
890
  scanned_files: files.length,
614
891
  };
@@ -637,6 +914,15 @@ export function render(r: Report): string {
637
914
  out.push(` verification: ${x.verification}`);
638
915
  }
639
916
  }
917
+ if (r.unknown.length > 0) {
918
+ // Printed with the findings rather than with the passes, because an
919
+ // undecided check is an open question about the site — not a clearance.
920
+ out.push('', `UNDECIDED — applicable, audited, and not decidable from this build (${String(r.unknown.length)})`);
921
+ for (const u of r.unknown) {
922
+ out.push(` ${u.location} ${u.check}`);
923
+ out.push(` unknown: ${u.reason}`);
924
+ }
925
+ }
640
926
  if (r.passed.length > 0) out.push('', `PASSED: ${r.passed.join(', ')}`);
641
927
  if (r.unimplemented.length > 0) {
642
928
  out.push('', `NOT YET IMPLEMENTED (applicable, not audited): ${r.unimplemented.join(', ')}`);
@@ -697,7 +983,11 @@ export function main(argv: string[] = process.argv.slice(2), root = REPO_ROOT):
697
983
  process.stdout.write(`${render(r)}\n`);
698
984
  }
699
985
  const blocking = r.findings.filter((f) => f.tier === 'critical' || f.tier === 'high');
700
- return blocking.length > 0 ? 1 : 0;
986
+ // An UNDECIDED applicable check exits non-zero whatever its tier. Exiting 0
987
+ // would report the run as clean on a question the instrument never answered,
988
+ // which is the same silent-green failure as counting an unimplemented check
989
+ // as passed — the tier axis grades findings, and this is not one.
990
+ return blocking.length > 0 || r.unknown.length > 0 ? 1 : 0;
701
991
  }
702
992
 
703
993
  if (path.resolve(process.argv[1] ?? '') === path.resolve(HERE)) {
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * `corpus_manifest` — pin the metered-proposer experiment's subject, and check
4
+ * that a fresh checkout reproduces it.
5
+ *
6
+ * Two verbs and no third. `capture` writes the pin from the tree as it stands;
7
+ * `verify` re-captures and reports every difference against a pin on disk.
8
+ * There is deliberately no `update` verb: a pin that can be refreshed in place
9
+ * is a pin that silently follows the tree, which is the property it exists not
10
+ * to have.
11
+ *
12
+ * `verify` exits 3 when the SUBJECT differs and 0 when only the explanatory
13
+ * fields do. That split is the whole point of the exit code — a different
14
+ * runtime version is a fact worth printing and is not a reason to refuse a
15
+ * comparison, while a different subject means the two runs measured different
16
+ * things and no amount of green elsewhere repairs it.
17
+ *
18
+ * Nothing here spends: it reads files and shells out to `git` for the commit.
19
+ */
20
+ import * as fs from 'node:fs';
21
+ import * as os from 'node:os';
22
+ import * as path from 'node:path';
23
+ import process from 'node:process';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ import { partitionActive } from '../install/partitionEligibility.js';
27
+ import {
28
+ type CorpusManifest,
29
+ captureManifest,
30
+ diffManifests,
31
+ parseManifest,
32
+ serialiseManifest,
33
+ subjectsEquivalent,
34
+ } from './_lib/corpus_manifest.js';
35
+
36
+ const _HERE = fileURLToPath(import.meta.url);
37
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
38
+
39
+ export const EXIT_OK = 0;
40
+ export const EXIT_ERROR = 1;
41
+ export const EXIT_USAGE = 2;
42
+ /** The subject differs. Distinct from a plain error so a runner can branch. */
43
+ export const EXIT_SUBJECT_DIFFERS = 3;
44
+
45
+ /**
46
+ * The enumeration rule this manifest pins under, as an ID rather than prose.
47
+ *
48
+ * It is part of the digest, so changing the rule changes the subject even when
49
+ * every file hash is unchanged. That is correct: a corpus of the same five
50
+ * files selected by a different rule is a different experiment, and a digest
51
+ * that could not see the difference would let the selection semantics be
52
+ * amended without the pin noticing.
53
+ */
54
+ export const ENUMERATION_RULE_ID = 'claude-rules-md-bytesorted-first-N/v1';
55
+
56
+ const USAGE = `usage: corpus_manifest capture --out FILE [--limit N]
57
+ corpus_manifest verify --manifest FILE
58
+
59
+ capture write the subject pin from the tree as it stands
60
+ verify re-capture and report every difference against FILE
61
+
62
+ --limit N corpus size, on CAPTURE only; defaults to max_candidates from the
63
+ pre-registered budget, which is the ceiling the run guard aborts
64
+ on. verify re-captures at the size its own manifest pinned, so it
65
+ REFUSES --limit rather than accepting a flag it cannot honour
66
+
67
+ Exit: 0 ok · 1 error · 2 usage · 3 the SUBJECT differs
68
+ `;
69
+
70
+ /**
71
+ * The corpus size, read from the pre-registered budget rather than defaulted.
72
+ *
73
+ * Hardcoding five here would put a second copy of a pre-registered number in a
74
+ * file nobody would think to check when the budget changed, and the two would
75
+ * disagree in the direction that matters: a manifest pinning more subjects than
76
+ * the run guard admits describes an experiment that cannot be run.
77
+ */
78
+ export function defaultLimit(repoRoot: string = REPO_ROOT): number {
79
+ const raw = JSON.parse(
80
+ fs.readFileSync(path.join(repoRoot, 'src', 'config', 'harness-evolution-budget.json'), 'utf-8'),
81
+ ) as { budget?: { max_candidates?: unknown } };
82
+ const n = raw.budget?.max_candidates;
83
+ if (typeof n !== 'number' || !Number.isInteger(n) || n <= 0) {
84
+ throw new Error('max_candidates missing or not a positive integer in the pre-registered budget');
85
+ }
86
+ return n;
87
+ }
88
+
89
+ interface Parsed {
90
+ verb?: string;
91
+ out?: string;
92
+ manifest?: string;
93
+ limit?: number;
94
+ }
95
+
96
+ export function parseArgs(argv: readonly string[]): Parsed | string {
97
+ const p: Parsed = {};
98
+ for (let i = 0; i < argv.length; i += 1) {
99
+ const a = argv[i] as string;
100
+ if (!a.startsWith('--') && p.verb === undefined) {
101
+ p.verb = a;
102
+ continue;
103
+ }
104
+ const v = argv[i + 1];
105
+ if (v === undefined || v.startsWith('--')) return `${a} requires a value`;
106
+ i += 1;
107
+ if (a === '--out') p.out = v;
108
+ else if (a === '--manifest') p.manifest = v;
109
+ else if (a === '--limit') {
110
+ const n = Number(v);
111
+ if (!Number.isInteger(n) || n <= 0) return '--limit must be a positive integer';
112
+ p.limit = n;
113
+ } else return `unknown option '${a}'`;
114
+ }
115
+ if (p.verb === 'verify' && p.limit !== undefined) {
116
+ return (
117
+ 'verify does not take --limit: it re-captures at the size its own manifest pinned, ' +
118
+ 'because a re-capture at another corpus size is a comparison against another subject. ' +
119
+ 'Accepting the flag and then using the pinned size reported green on a run that had ' +
120
+ 'not honoured it'
121
+ );
122
+ }
123
+ return p;
124
+ }
125
+
126
+ /**
127
+ * One real capture over the real tree.
128
+ *
129
+ * This is the one caller for which the process globals `partitionActive` reads
130
+ * — the host layer under the running user's home, and the install lockfile —
131
+ * ARE this run's own host, so it is the one caller that passes the predicate.
132
+ * Everywhere else the field stays `null` rather than recording some other
133
+ * machine's state under this tree's name.
134
+ */
135
+ function capture(limit: number): CorpusManifest {
136
+ return captureManifest({
137
+ repoRoot: REPO_ROOT,
138
+ userHome: process.env['HOME'] ?? os.homedir(),
139
+ limit,
140
+ enumerationRule: ENUMERATION_RULE_ID,
141
+ partitionActive,
142
+ });
143
+ }
144
+
145
+ export function main(argv?: string[]): number {
146
+ const args = argv ?? process.argv.slice(2);
147
+ if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
148
+ process.stdout.write(USAGE);
149
+ return args.length === 0 ? EXIT_USAGE : EXIT_OK;
150
+ }
151
+ const parsed = parseArgs(args);
152
+ if (typeof parsed === 'string') {
153
+ process.stderr.write(`corpus_manifest: error: ${parsed}\n${USAGE}`);
154
+ return EXIT_USAGE;
155
+ }
156
+
157
+ if (parsed.verb === 'capture') {
158
+ if (parsed.out === undefined) {
159
+ process.stderr.write(`corpus_manifest: error: capture requires --out FILE\n${USAGE}`);
160
+ return EXIT_USAGE;
161
+ }
162
+ let m: CorpusManifest;
163
+ try {
164
+ m = capture(parsed.limit ?? defaultLimit());
165
+ } catch (e) {
166
+ process.stderr.write(`corpus_manifest: capture refused — ${(e as Error).message}\n`);
167
+ return EXIT_ERROR;
168
+ }
169
+ fs.mkdirSync(path.dirname(path.resolve(parsed.out)), { recursive: true });
170
+ fs.writeFileSync(parsed.out, serialiseManifest(m), 'utf-8');
171
+ process.stdout.write(
172
+ `corpus_manifest: wrote ${parsed.out} · subject_digest=${m.subject_digest.slice(0, 16)} · ` +
173
+ `${String(m.included.length)} subject(s) · ${String(m.produced.length)} produced · ` +
174
+ `${String(m.user_scope.filter((t) => t.byte_identical).length)} byte-identical ` +
175
+ `twin(s), ${String(m.user_scope.filter((t) => t.causes_skip).length)} of them ` +
176
+ `causing a skip under ${String(m.generator_config['projection.scope_dedup'])}\n`,
177
+ );
178
+ if (m.tree_dirty) {
179
+ process.stdout.write(
180
+ 'corpus_manifest: WARNING the working tree is dirty, so the recorded commit does not ' +
181
+ 'describe the bytes that were hashed\n',
182
+ );
183
+ }
184
+ return EXIT_OK;
185
+ }
186
+
187
+ if (parsed.verb === 'verify') {
188
+ if (parsed.manifest === undefined) {
189
+ process.stderr.write(`corpus_manifest: error: verify requires --manifest FILE\n${USAGE}`);
190
+ return EXIT_USAGE;
191
+ }
192
+ let expected: CorpusManifest;
193
+ let actual: CorpusManifest;
194
+ try {
195
+ expected = parseManifest(JSON.parse(fs.readFileSync(parsed.manifest, 'utf-8')));
196
+ actual = capture(expected.included.length);
197
+ } catch (e) {
198
+ process.stderr.write(`corpus_manifest: verify refused — ${(e as Error).message}\n`);
199
+ return EXIT_ERROR;
200
+ }
201
+ const diffs = diffManifests(expected, actual);
202
+ for (const d of diffs) {
203
+ process.stdout.write(`corpus_manifest: DIFF ${d.field}: expected ${d.expected}, got ${d.actual}\n`);
204
+ }
205
+ if (subjectsEquivalent(expected, actual)) {
206
+ process.stdout.write(
207
+ `corpus_manifest: SUBJECT EQUIVALENT · ${String(actual.included.length)} subject(s) · ` +
208
+ `digest=${actual.subject_digest.slice(0, 16)}` +
209
+ (diffs.length > 0 ? ` · ${String(diffs.length)} explanatory difference(s) above\n` : '\n'),
210
+ );
211
+ return EXIT_OK;
212
+ }
213
+ process.stdout.write(
214
+ 'corpus_manifest: SUBJECT DIFFERS — a comparison captured against this manifest is not ' +
215
+ 'comparable to one captured here\n',
216
+ );
217
+ return EXIT_SUBJECT_DIFFERS;
218
+ }
219
+
220
+ process.stderr.write(`corpus_manifest: error: unknown verb ${JSON.stringify(parsed.verb)}\n${USAGE}`);
221
+ return EXIT_USAGE;
222
+ }
223
+
224
+ function _isCliEntry(): boolean {
225
+ try {
226
+ if (process.argv[1] === undefined) return false;
227
+ return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+
233
+ if (_isCliEntry()) {
234
+ process.exit(main());
235
+ }