@event4u/agent-config 14.7.0 → 14.9.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 (230) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +175 -390
  3. package/README.md +1 -1
  4. package/agents/templates/.ai-council.yml.example +34 -4
  5. package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
  6. package/dist/agent-src/commands/evals/roadmap.json +17 -1
  7. package/dist/agent-src/commands/pr/create.md +14 -2
  8. package/dist/agent-src/commands/pr/merge.md +383 -0
  9. package/dist/agent-src/commands/review/changes.md +95 -14
  10. package/dist/agent-src/commands/roadmap/process-full.md +157 -3
  11. package/dist/agent-src/commands/roadmap.md +1 -1
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
  14. package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
  15. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +100 -2
  16. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
  17. package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
  18. package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
  19. package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
  20. package/dist/agent-src/rules/recurring-criticism.md +1 -1
  21. package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
  22. package/dist/agent-src/rules/source-confidentiality.md +1 -1
  23. package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
  24. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
  25. package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
  26. package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
  27. package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
  28. package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
  29. package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
  30. package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
  31. package/dist/agent-src/skills/humanizer/SKILL.md +39 -0
  32. package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +57 -0
  33. package/dist/agent-src/skills/humanizer/references/fixtures.md +54 -0
  34. package/dist/agent-src/skills/judge-spec-compliance/SKILL.md +154 -0
  35. package/dist/agent-src/skills/judge-synthesis/SKILL.md +45 -0
  36. package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
  37. package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
  38. package/dist/agent-src/skills/security/SKILL.md +8 -0
  39. package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
  40. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  41. package/dist/agent-src/templates/roadmaps.md +28 -2
  42. package/dist/cli/registry.js +3 -0
  43. package/dist/cli/registry.js.map +1 -1
  44. package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-77GR3VD4.js} +68 -43
  45. package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
  46. package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-CYH5SP6M.js} +4 -23
  47. package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
  48. package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
  49. package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
  50. package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
  51. package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-PLSG6UL5.js} +14 -14
  52. package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
  53. package/dist/cli-delegate/cmd_conformance.js +101 -25
  54. package/dist/cli-delegate/cmd_converge.js +3 -3
  55. package/dist/cli-delegate/cmd_doctor.js +7 -7
  56. package/dist/cli-delegate/cmd_export.js +6 -6
  57. package/dist/cli-delegate/cmd_handoff.js +7 -6
  58. package/dist/cli-delegate/cmd_memory_get.js +2 -1
  59. package/dist/cli-delegate/cmd_migrate.js +2 -2
  60. package/dist/cli-delegate/cmd_preflight.js +5 -5
  61. package/dist/cli-delegate/cmd_prune.js +6 -6
  62. package/dist/cli-delegate/cmd_route_audit.js +6 -5
  63. package/dist/cli-delegate/cmd_route_explain.js +3 -3
  64. package/dist/cli-delegate/cmd_session_recycle.js +3 -3
  65. package/dist/cli-delegate/cmd_settings_get.js +4 -4
  66. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  67. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  68. package/dist/cli-delegate/cmd_sync.js +6 -6
  69. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  70. package/dist/cli-delegate/cmd_update.js +3 -3
  71. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  72. package/dist/cli-delegate/cmd_validate.js +6 -6
  73. package/dist/cli-delegate/cmd_versions.js +3 -3
  74. package/dist/discovery/deprecation-report.md +1 -1
  75. package/dist/discovery/discovery-manifest.json +83 -30
  76. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  77. package/dist/discovery/discovery-manifest.summary.md +4 -4
  78. package/dist/discovery/dormancy-report.md +1 -1
  79. package/dist/discovery/orphan-report.md +1 -1
  80. package/dist/discovery/packs.json +11 -9
  81. package/dist/discovery/trust-report.md +2 -2
  82. package/dist/discovery/workspaces.json +9 -7
  83. package/dist/hooks/dispatch.js +1295 -812
  84. package/dist/install/globalRuleLayers.js +169 -0
  85. package/dist/install/globalRuleLayers.js.map +1 -0
  86. package/dist/install/partitionEligibility.js +156 -0
  87. package/dist/install/partitionEligibility.js.map +1 -1
  88. package/dist/install/ruleLayerPartition.js +157 -0
  89. package/dist/install/ruleLayerPartition.js.map +1 -0
  90. package/dist/mcp/registry-manifest.json +3 -3
  91. package/dist/mcp/server.json +2 -2
  92. package/dist/mcp/server.mjs +1358 -805
  93. package/dist/scripts/_lib/claude_plugin.js +162 -0
  94. package/dist/scripts/_lib/claude_plugin.js.map +1 -0
  95. package/dist/scripts/_lib/environment_detector.js +397 -0
  96. package/dist/scripts/_lib/environment_detector.js.map +1 -0
  97. package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
  98. package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
  99. package/dist/scripts/_lib/spawn_env.js +129 -0
  100. package/dist/scripts/_lib/spawn_env.js.map +1 -0
  101. package/dist/scripts/ai_council/modes.js +199 -0
  102. package/dist/scripts/ai_council/modes.js.map +1 -0
  103. package/dist/scripts/ai_council/transport_resolver.js +418 -0
  104. package/dist/scripts/ai_council/transport_resolver.js.map +1 -0
  105. package/dist/scripts/condense.js +52 -89
  106. package/dist/scripts/condense.js.map +1 -1
  107. package/dist/server/routes/wizard.js +3 -2
  108. package/dist/server/routes/wizard.js.map +1 -1
  109. package/dist/ui/assets/index-Cxgy63kz.js +26 -0
  110. package/dist/ui/assets/index-Cxgy63kz.js.map +1 -0
  111. package/dist/ui/index.html +1 -1
  112. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
  113. package/docs/guidelines/agent-infra/false-green.md +47 -1
  114. package/docs/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
  115. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
  116. package/package.json +7 -3
  117. package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
  118. package/src/agent-src/scripts/dashboard_mode.ts +212 -0
  119. package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
  120. package/src/config/ci-local-parity.yml +21 -0
  121. package/src/config/estate-count-budget.json +13 -471
  122. package/src/config/evaluator-budgets.json +4 -4
  123. package/src/config/gate-coverage.yml +458 -12
  124. package/src/config/gate-violation-baselines.json +18 -8
  125. package/src/config/gitignore-block.txt +15 -2
  126. package/src/config/hook-latency-budget.json +5 -0
  127. package/src/config/publish-surface.json +60 -0
  128. package/src/config/requirements-trace-budget.json +32 -0
  129. package/src/config/rule-stub-ceilings.json +25 -4
  130. package/src/install/globalRuleLayers.ts +197 -0
  131. package/src/install/partitionEligibility.ts +177 -0
  132. package/src/install/ruleLayerPartition.ts +212 -0
  133. package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
  134. package/src/scripts/_dispatch.bash +18 -0
  135. package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
  136. package/src/scripts/_lib/adr_frontmatter.ts +459 -0
  137. package/src/scripts/_lib/base_tree.ts +175 -0
  138. package/src/scripts/_lib/billing_grant.ts +165 -0
  139. package/src/scripts/_lib/billing_grant_cli.ts +123 -0
  140. package/src/scripts/_lib/council_fallback_posture.ts +53 -6
  141. package/src/scripts/_lib/council_fallback_wiring.ts +61 -1
  142. package/src/scripts/_lib/envelope_grounding.ts +11 -1
  143. package/src/scripts/_lib/host_capability.ts +59 -5
  144. package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
  145. package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
  146. package/src/scripts/_lib/quota_parked.ts +133 -0
  147. package/src/scripts/_lib/retrieval_sanitize.ts +62 -0
  148. package/src/scripts/_lib/review_telemetry.ts +196 -0
  149. package/src/scripts/_lib/subagent_response.ts +92 -0
  150. package/src/scripts/adr/evidence_census.ts +899 -0
  151. package/src/scripts/adr/regenerate_index.ts +25 -38
  152. package/src/scripts/adr_cite_check.ts +540 -39
  153. package/src/scripts/ai_council/clients.ts +12 -1
  154. package/src/scripts/ai_council/config.ts +72 -115
  155. package/src/scripts/ai_council/fallback_config.ts +19 -7
  156. package/src/scripts/ai_council/mid_flight_fallback.ts +88 -7
  157. package/src/scripts/ai_council/orchestrator.ts +2 -2
  158. package/src/scripts/ai_council/py_format.ts +124 -0
  159. package/src/scripts/ai_council/transport_resolver.ts +65 -2
  160. package/src/scripts/audit_adr_coverage.ts +134 -21
  161. package/src/scripts/bench_hook_latency.ts +204 -0
  162. package/src/scripts/build_archive_index.ts +34 -0
  163. package/src/scripts/check_adr_frontmatter.ts +574 -39
  164. package/src/scripts/check_council_pin_staleness.ts +300 -0
  165. package/src/scripts/check_estate_count.ts +420 -293
  166. package/src/scripts/check_gate_coverage.ts +91 -0
  167. package/src/scripts/check_hook_bundle_content.ts +219 -0
  168. package/src/scripts/check_new_adr_evidence.ts +529 -0
  169. package/src/scripts/check_no_stub_inventory_table.ts +292 -0
  170. package/src/scripts/check_pack_size.ts +234 -1
  171. package/src/scripts/check_publish_surface.ts +297 -0
  172. package/src/scripts/check_references.ts +18 -0
  173. package/src/scripts/check_requirements_trace.ts +378 -0
  174. package/src/scripts/check_rule_layer_partition.ts +531 -0
  175. package/src/scripts/check_secret_leak.ts +166 -12
  176. package/src/scripts/check_single_delivery.ts +137 -4
  177. package/src/scripts/check_standing_rule_delivery.ts +8 -2
  178. package/src/scripts/code_graph/cli.ts +44 -5
  179. package/src/scripts/code_graph/extract.ts +87 -0
  180. package/src/scripts/condense.ts +61 -91
  181. package/src/scripts/council_cli.ts +12 -16
  182. package/src/scripts/detect_ai_tells.ts +87 -0
  183. package/src/scripts/dispatch_r2_reviewer.ts +42 -0
  184. package/src/scripts/external_sources_denylist.json +4 -1
  185. package/src/scripts/generate_capabilities_index.ts +41 -0
  186. package/src/scripts/git_authorization_hook.ts +54 -4
  187. package/src/scripts/hooks/block_unauthorized_git.ts +41 -3
  188. package/src/scripts/hooks/dispatch_hook.ts +154 -187
  189. package/src/scripts/hooks/dispatch_issues.ts +38 -19
  190. package/src/scripts/hooks/fallback_yaml.ts +66 -0
  191. package/src/scripts/hooks/host_semantics.ts +21 -0
  192. package/src/scripts/hooks/injection_budget.ts +13 -3
  193. package/src/scripts/hooks/py_json_dumps.ts +103 -0
  194. package/src/scripts/hooks/run_continuation_hook.ts +3 -1
  195. package/src/scripts/hooks/state_io.ts +88 -1
  196. package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
  197. package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
  198. package/src/scripts/injection_scan_hook.ts +257 -15
  199. package/src/scripts/lint_adr_sweep_routing.ts +365 -0
  200. package/src/scripts/lint_hook_manifest.ts +49 -0
  201. package/src/scripts/lint_provenance_vocabulary.ts +472 -4
  202. package/src/scripts/lint_workflow_security.ts +53 -10
  203. package/src/scripts/lint_workflow_security_allowlist.json +2 -18
  204. package/src/scripts/report_envelope_rate.ts +166 -0
  205. package/src/scripts/review_axis_report.ts +218 -0
  206. package/src/scripts/routing_doctor.ts +5 -1
  207. package/src/scripts/run_supervise.ts +46 -1
  208. package/src/scripts/schemas/evals.schema.json +5 -0
  209. package/src/scripts/sync_pr_branch.ts +170 -20
  210. package/dist/cli/commands/commands.candidates.test.js +0 -257
  211. package/dist/cli/commands/commands.candidates.test.js.map +0 -1
  212. package/dist/cli/commands/profiles.test.js +0 -88
  213. package/dist/cli/commands/profiles.test.js.map +0 -1
  214. package/dist/cli/commands/recordTriggerEval.test.js +0 -113
  215. package/dist/cli/commands/recordTriggerEval.test.js.map +0 -1
  216. package/dist/cli/configRoot.test.js +0 -97
  217. package/dist/cli/configRoot.test.js.map +0 -1
  218. package/dist/cli/mcp/content.test.js +0 -54
  219. package/dist/cli/mcp/content.test.js.map +0 -1
  220. package/dist/cli/mcp/dispatch.test.js +0 -108
  221. package/dist/cli/mcp/dispatch.test.js.map +0 -1
  222. package/dist/server/writeRoot.test.js +0 -91
  223. package/dist/server/writeRoot.test.js.map +0 -1
  224. package/dist/shared/capabilities.test.js +0 -39
  225. package/dist/shared/capabilities.test.js.map +0 -1
  226. package/dist/ui/assets/index-C1pBRm1f.js +0 -26
  227. package/dist/ui/assets/index-C1pBRm1f.js.map +0 -1
  228. package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
  229. package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
  230. package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Shared ADR frontmatter reader — coverage.
3
+ *
4
+ * The load-bearing test is `corpus equivalence`: the shared reader is run over
5
+ * every real ADR alongside a verbatim copy of each parser it replaces, and the
6
+ * scalar views must match key-for-key. A refactor of a parser three call sites
7
+ * depend on is only safe if it is provably the same function on the input that
8
+ * exists, and a hand-written fixture cannot establish that — the corpus can.
9
+ *
10
+ * The nested cases then cover what the old parsers could NOT do, and the
11
+ * negative cases pin the discriminations that make the new behaviour a
12
+ * decision rather than an accident.
13
+ */
14
+ import { readdirSync, readFileSync } from 'node:fs';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ import { describe, expect, it } from 'vitest';
19
+
20
+ import {
21
+ evidenceOf,
22
+ isLowEvidenceAccepted,
23
+ provenanceOf,
24
+ readAdrFrontmatter,
25
+ readAdrFrontmatterScalars,
26
+ } from './adr_frontmatter.js';
27
+
28
+ /**
29
+ * Keys the shared reader parses as NESTED, where equivalence with the legacy
30
+ * parsers was never the promise.
31
+ *
32
+ * The legacy fold produced a meaningless string for these — ADR-239's evidence
33
+ * block came back as `"strength: E3 discovery: complete basis: - url1 - url2"`,
34
+ * a value nothing read and nothing could use. The equivalence contract is about
35
+ * SCALAR keys, which is where the three parsers actually diverged in ways a
36
+ * caller could observe. Naming the exception here rather than filtering it
37
+ * silently, and asserting below that the legacy value really was garbage — a
38
+ * skip nobody checks is how a real divergence hides behind an intended one.
39
+ */
40
+ const NESTED_KEYS = ['provenance', 'evidence'];
41
+
42
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
43
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
44
+ const DECISIONS = path.join(REPO_ROOT, 'docs', 'decisions');
45
+
46
+ /** Verbatim copy of the parser `check_adr_frontmatter.ts` carried. */
47
+ function legacyStrictParser(text: string): Record<string, string> | null {
48
+ if (!text.startsWith('---\n')) return null;
49
+ const end = text.indexOf('\n---\n', 4);
50
+ if (end === -1) return null;
51
+ const out: Record<string, string> = {};
52
+ let key: string | null = null;
53
+ for (const raw of text.slice(4, end).split('\n')) {
54
+ const line = raw.replace(/\s+$/, '');
55
+ if (!line || line.trimStart().startsWith('#')) continue;
56
+ if (/^\s/.test(line) && key) {
57
+ out[key] = `${out[key]} ${line.trim()}`.trim();
58
+ continue;
59
+ }
60
+ const idx = line.indexOf(':');
61
+ if (idx === -1) continue;
62
+ key = line.slice(0, idx).trim();
63
+ out[key] = line
64
+ .slice(idx + 1)
65
+ .trim()
66
+ .replace(/^["'](.*)["']$/, '$1');
67
+ }
68
+ return out;
69
+ }
70
+
71
+ /** Verbatim copy of the parser `adr_cite_check.ts` carried (adds the `>-` strip). */
72
+ function legacyCiteParser(text: string): Record<string, string> | null {
73
+ if (!text.startsWith('---\n')) return null;
74
+ const end = text.indexOf('\n---\n', 4);
75
+ if (end === -1) return null;
76
+ const out: Record<string, string> = {};
77
+ let key: string | null = null;
78
+ for (const raw of text.slice(4, end).split('\n')) {
79
+ const line = raw.replace(/\s+$/, '');
80
+ if (!line || line.trimStart().startsWith('#')) continue;
81
+ if (/^\s/.test(line) && key !== null) {
82
+ out[key] = `${out[key]} ${line.trim()}`.trim();
83
+ continue;
84
+ }
85
+ const idx = line.indexOf(':');
86
+ if (idx === -1) continue;
87
+ key = line.slice(0, idx).trim();
88
+ out[key] = line
89
+ .slice(idx + 1)
90
+ .trim()
91
+ .replace(/^["'](.*)["']$/, '$1')
92
+ .replace(/^>-?$/, '');
93
+ }
94
+ return out;
95
+ }
96
+
97
+ function adrFiles(): string[] {
98
+ return readdirSync(DECISIONS)
99
+ .filter((name) => /^ADR-.*\.md$/.test(name))
100
+ .sort()
101
+ .map((name) => path.join(DECISIONS, name));
102
+ }
103
+
104
+ describe('corpus equivalence with the parsers it replaces', () => {
105
+ const files = adrFiles();
106
+
107
+ it('finds the corpus (guards against a silently empty sweep)', () => {
108
+ expect(files.length).toBeGreaterThan(150);
109
+ });
110
+
111
+ it('matches the cite parser key-for-key on every real ADR', () => {
112
+ const divergent: string[] = [];
113
+ for (const file of files) {
114
+ const text = readFileSync(file, 'utf8');
115
+ const legacy = legacyCiteParser(text);
116
+ const shared = readAdrFrontmatterScalars(text);
117
+ if (legacy === null || shared === null) {
118
+ if (legacy !== shared) divergent.push(`${path.basename(file)}: null-ness differs`);
119
+ continue;
120
+ }
121
+ for (const [key, value] of Object.entries(legacy)) {
122
+ if (NESTED_KEYS.includes(key)) continue;
123
+ if (shared[key] !== value) {
124
+ divergent.push(`${path.basename(file)}#${key}: ${JSON.stringify(value)} vs ${JSON.stringify(shared[key])}`);
125
+ }
126
+ }
127
+ }
128
+ expect(divergent).toEqual([]);
129
+ });
130
+
131
+ it('the legacy value for a nested key really was garbage — so the exception is earned', () => {
132
+ // If a legacy parser ever produced something usable for a nested key,
133
+ // excluding it above would be hiding a real divergence. Assert the
134
+ // premise instead of trusting it.
135
+ let checked = 0;
136
+ for (const file of files) {
137
+ const text = readFileSync(file, 'utf8');
138
+ const legacy = legacyCiteParser(text);
139
+ if (legacy === null) continue;
140
+ for (const key of NESTED_KEYS) {
141
+ const value = legacy[key];
142
+ if (value === undefined) continue;
143
+ checked += 1;
144
+ // A usable scalar would not carry an inner `key: value` pair or
145
+ // a bare list marker. The legacy fold always does.
146
+ expect(value).toMatch(/(\w+:\s)|(^|\s)- /);
147
+ }
148
+ }
149
+ // At least one record must exercise this, or the assertion is vacuous.
150
+ expect(checked).toBeGreaterThan(0);
151
+ });
152
+
153
+ it('matches the strict parser on every real ADR, modulo the documented `>-` strip', () => {
154
+ const divergent: string[] = [];
155
+ for (const file of files) {
156
+ const text = readFileSync(file, 'utf8');
157
+ const legacy = legacyStrictParser(text);
158
+ const shared = readAdrFrontmatterScalars(text);
159
+ if (legacy === null || shared === null) continue;
160
+ for (const [key, value] of Object.entries(legacy)) {
161
+ // The strict parser kept the fold marker and then folded the
162
+ // block onto it (`>- Reopen when …`); the cite parser stripped
163
+ // it, and the shared reader keeps the cite behaviour. That is
164
+ // the one intended divergence, so it is normalised here and
165
+ // named rather than smoothed away.
166
+ if (NESTED_KEYS.includes(key)) continue;
167
+ const normalised = value.replace(/^\|[-+]?\s*|^>[-+]?\s*/, '');
168
+ if (shared[key] !== normalised) {
169
+ divergent.push(`${path.basename(file)}#${key}`);
170
+ }
171
+ }
172
+ }
173
+ expect(divergent).toEqual([]);
174
+ });
175
+ });
176
+
177
+ describe('scalar behaviour preserved', () => {
178
+ it('reads plain scalars and strips quotes', () => {
179
+ const fm = readAdrFrontmatter('---\nadr: 238\nstatus: "accepted"\n---\nbody\n');
180
+ expect(fm?.scalars.adr).toBe('238');
181
+ expect(fm?.scalars.status).toBe('accepted');
182
+ });
183
+
184
+ it('folds a >- block into one scalar', () => {
185
+ const fm = readAdrFrontmatter(
186
+ '---\nreview_trigger: >-\n Reopen when the standard is withdrawn\n or the contract is retired.\n---\n',
187
+ );
188
+ expect(fm?.scalars.review_trigger).toBe('Reopen when the standard is withdrawn or the contract is retired.');
189
+ });
190
+
191
+ it('folds an unmarked indented continuation, as the old readers did', () => {
192
+ const fm = readAdrFrontmatter('---\ndecision: a-long\n slug-continued\n---\n');
193
+ expect(fm?.scalars.decision).toBe('a-long slug-continued');
194
+ });
195
+
196
+ it('returns null when there is no frontmatter at all', () => {
197
+ expect(readAdrFrontmatter('# ADR 0001 — per-area\n\n> Area: `router`\n')).toBeNull();
198
+ });
199
+
200
+ it('skips comment lines', () => {
201
+ const fm = readAdrFrontmatter('---\n# a comment\nadr: 1\n---\n');
202
+ expect(fm?.scalars.adr).toBe('1');
203
+ expect(fm?.keyOrder).toEqual(['adr']);
204
+ });
205
+ });
206
+
207
+ describe('nested axes — what three parsers could not read', () => {
208
+ const doc = [
209
+ '---',
210
+ 'adr: 238',
211
+ 'status: accepted',
212
+ 'provenance:',
213
+ ' kind: agentic',
214
+ ' decision_makers: [claude-sonnet-4-5, gpt-4o]',
215
+ ' human_directed: false',
216
+ ' agentic_mode: council',
217
+ 'evidence:',
218
+ ' strength: E1',
219
+ ' discovery: incomplete',
220
+ ' basis:',
221
+ ' - docs/CLAIMS.md#code-graph-retrieval-null',
222
+ ' - ADR-229:52',
223
+ 'authority_basis: evidence',
224
+ '---',
225
+ 'body',
226
+ '',
227
+ ].join('\n');
228
+
229
+ it('parses a nested map', () => {
230
+ const p = provenanceOf(readAdrFrontmatter(doc)!);
231
+ expect(p).toEqual({
232
+ kind: 'agentic',
233
+ decisionMakers: ['claude-sonnet-4-5', 'gpt-4o'],
234
+ humanDirected: 'false',
235
+ agenticMode: 'council',
236
+ });
237
+ });
238
+
239
+ it('parses an inline list and a block list in the same record', () => {
240
+ const e = evidenceOf(readAdrFrontmatter(doc)!);
241
+ expect(e?.strength).toBe('E1');
242
+ expect(e?.discovery).toBe('incomplete');
243
+ expect(e?.basis).toEqual(['docs/CLAIMS.md#code-graph-retrieval-null', 'ADR-229:52']);
244
+ });
245
+
246
+ it('keeps nested keys out of the scalar map', () => {
247
+ const fm = readAdrFrontmatter(doc)!;
248
+ expect(fm.scalars.provenance).toBeUndefined();
249
+ expect(fm.scalars.evidence).toBeUndefined();
250
+ expect(fm.scalars.status).toBe('accepted');
251
+ expect(fm.scalars.authority_basis).toBe('evidence');
252
+ });
253
+
254
+ it('round-trips a nested record through the scalar view without losing the key', () => {
255
+ // The index reader only knows scalars; an absent key and an empty key
256
+ // print differently there, so the folded form has to be non-empty.
257
+ const flat = readAdrFrontmatterScalars(doc)!;
258
+ expect(flat.provenance).toContain('kind: agentic');
259
+ expect(flat.evidence).toContain('strength: E1');
260
+ });
261
+
262
+ it('reads an empty inline list as an empty list, not as absent', () => {
263
+ const fm = readAdrFrontmatter('---\nevidence:\n strength: E0\n basis: []\n---\n')!;
264
+ expect(evidenceOf(fm)?.basis).toEqual([]);
265
+ expect(evidenceOf(fm)?.strength).toBe('E0');
266
+ });
267
+ });
268
+
269
+ describe('negative cases — the discriminations are decisions, not accidents', () => {
270
+ it('does not turn a folded scalar into a map even when its prose contains a colon', () => {
271
+ const fm = readAdrFrontmatter(
272
+ '---\nreview_trigger: >-\n Reopen when: the standard is withdrawn.\n---\n',
273
+ )!;
274
+ expect(fm.nested.review_trigger).toBeUndefined();
275
+ expect(fm.scalars.review_trigger).toBe('Reopen when: the standard is withdrawn.');
276
+ });
277
+
278
+ it('reports a missing axis as null rather than inventing a default', () => {
279
+ const fm = readAdrFrontmatter('---\nadr: 1\nstatus: accepted\n---\n')!;
280
+ expect(provenanceOf(fm)).toBeNull();
281
+ expect(evidenceOf(fm)).toBeNull();
282
+ });
283
+
284
+ it('reports a malformed axis as null rather than half-reading it', () => {
285
+ const fm = readAdrFrontmatter('---\nprovenance: agentic\n---\n')!;
286
+ // A scalar where a map belongs: the accessor refuses it, and the raw
287
+ // value stays visible in `scalars` so a linter can report the shape.
288
+ expect(provenanceOf(fm)).toBeNull();
289
+ expect(fm.scalars.provenance).toBe('agentic');
290
+ });
291
+
292
+ it('drops an orphan indented line instead of attaching it to nothing', () => {
293
+ const fm = readAdrFrontmatter('---\n orphan: value\nadr: 1\n---\n')!;
294
+ expect(fm.scalars.orphan).toBeUndefined();
295
+ expect(fm.scalars.adr).toBe('1');
296
+ });
297
+ });
298
+
299
+ describe('low-evidence notice — provenance-independent by design', () => {
300
+ function build(status: string, kind: string, strength: string): string {
301
+ return [
302
+ '---',
303
+ `status: ${status}`,
304
+ 'provenance:',
305
+ ` kind: ${kind}`,
306
+ 'evidence:',
307
+ ` strength: ${strength}`,
308
+ ' discovery: incomplete',
309
+ '---',
310
+ '',
311
+ ].join('\n');
312
+ }
313
+
314
+ it('fires on an accepted agentic E0 record', () => {
315
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'agentic', 'E0'))!)).toBe(true);
316
+ });
317
+
318
+ it('fires on an accepted agentic E1 record', () => {
319
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'agentic', 'E1'))!)).toBe(true);
320
+ });
321
+
322
+ // The regression this rename exists for. The predicate used to require
323
+ // `kind === 'agentic'`, so an identical human E0 record printed nothing —
324
+ // while the notice is a statement about EVIDENCE strength, which a human
325
+ // snapshot has exactly as little of.
326
+ it('fires on an accepted HUMAN E0 record — the notice is about evidence, not authorship', () => {
327
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'human', 'E0'))!)).toBe(true);
328
+ });
329
+
330
+ it('fires on mixed and unknown provenance too', () => {
331
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'mixed', 'E1'))!)).toBe(true);
332
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'unknown', 'E0'))!)).toBe(true);
333
+ });
334
+
335
+ it('does NOT fire on E2 — the grade boundary is the whole point', () => {
336
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'agentic', 'E2'))!)).toBe(false);
337
+ });
338
+
339
+ it('does NOT fire on E3 or E4', () => {
340
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'agentic', 'E3'))!)).toBe(false);
341
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'human', 'E4'))!)).toBe(false);
342
+ });
343
+
344
+ it('does NOT fire on a superseded record', () => {
345
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('superseded', 'agentic', 'E0'))!)).toBe(false);
346
+ });
347
+
348
+ it('does NOT fire when the axes are absent — silence is not a weak grade', () => {
349
+ expect(isLowEvidenceAccepted(readAdrFrontmatter('---\nstatus: accepted\n---\n')!)).toBe(false);
350
+ });
351
+
352
+ // The exemption, and the reason provenance was the wrong gate: an owner
353
+ // purpose statement is legitimately E0 and its alternatives ARE foreclosed —
354
+ // by ownership, not by evidence. `authority_basis` is the declared field for
355
+ // that, so it is the field that decides.
356
+ it('does NOT fire on an owner_intent record, at E0', () => {
357
+ const doc = '---\nstatus: accepted\nauthority_basis: owner_intent\nevidence:\n strength: E0\n discovery: complete\n---\n';
358
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(doc)!)).toBe(false);
359
+ });
360
+
361
+ it('DOES fire on a thin human record that carries no owner claim', () => {
362
+ const doc = '---\nstatus: accepted\nprovenance:\n kind: human\nevidence:\n strength: E0\n discovery: incomplete\n---\n';
363
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(doc)!)).toBe(true);
364
+ });
365
+ });