@event4u/agent-config 14.21.0 → 14.23.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 (235) 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 +164 -187
  5. package/CREDITS.md +37 -0
  6. package/README.md +1 -1
  7. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  8. package/dist/agent-src/commands/analyze/repo.md +4 -2
  9. package/dist/agent-src/commands/pr/merge.md +47 -38
  10. package/dist/agent-src/commands/roadmap/next.md +8 -0
  11. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
  14. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  15. package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
  16. package/dist/agent-src/rules/token-efficiency.md +26 -3
  17. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  18. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  19. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  20. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  21. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  22. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  23. package/dist/agent-src/skills/code-intelligence/SKILL.md +82 -1
  24. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
  25. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  26. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
  27. package/dist/agent-src/templates/agent-settings.md +0 -1
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/cli/registry.js +1 -1
  30. package/dist/cli-delegate/_cli_entry.js +8 -0
  31. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  32. package/dist/cli-delegate/archival_sweep.js +8 -0
  33. package/dist/cli-delegate/archive_completed_roadmaps.js +542 -0
  34. package/dist/cli-delegate/check_update_banner.js +923 -0
  35. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
  36. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  37. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  38. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  39. package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
  40. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  41. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
  42. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  43. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  44. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  45. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  46. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  47. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  48. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  49. package/dist/cli-delegate/cmd_conformance.js +2 -2
  50. package/dist/cli-delegate/cmd_export.js +2 -2
  51. package/dist/cli-delegate/cmd_handoff.js +45 -13
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_session_recycle.js +4 -14
  54. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  55. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/cli-delegate/dashboard_mode.js +16 -0
  60. package/dist/cli-delegate/gate_budget.js +24 -0
  61. package/dist/cli-delegate/gate_execute.js +25 -0
  62. package/dist/cli-delegate/guarded_baseline.js +18 -0
  63. package/dist/cli-delegate/resume_probe.js +22 -0
  64. package/dist/cli-delegate/roadmap_gates.js +638 -0
  65. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  66. package/dist/cli-delegate/stub_queue.js +47 -0
  67. package/dist/cli-delegate/stubs_due.js +38 -0
  68. package/dist/cli-delegate/ticket_bundles.js +59 -0
  69. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  70. package/dist/discovery/deprecation-report.md +1 -1
  71. package/dist/discovery/discovery-manifest.json +13 -13
  72. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  73. package/dist/discovery/discovery-manifest.summary.md +1 -1
  74. package/dist/discovery/dormancy-report.md +1 -1
  75. package/dist/discovery/orphan-report.md +1 -1
  76. package/dist/discovery/packs.json +2 -2
  77. package/dist/discovery/trust-report.md +1 -1
  78. package/dist/discovery/workspaces.json +2 -2
  79. package/dist/hooks/dispatch.js +1813 -946
  80. package/dist/install/claudeLayerCarriage.js +237 -0
  81. package/dist/install/claudePathsPlan.js +79 -0
  82. package/dist/install/hostLayerFingerprint.js +1 -1
  83. package/dist/install/install.mjs +21 -3
  84. package/dist/install/partitionEligibility.js +231 -117
  85. package/dist/install/ruleLayerPartition.js +10 -7
  86. package/dist/mcp/registry-manifest.json +2 -2
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +10870 -7641
  89. package/dist/scripts/_lib/lean_projection_mode.js +108 -0
  90. package/dist/scripts/_lib/router_match.js +243 -0
  91. package/dist/scripts/_lib/rule_injection.js +216 -0
  92. package/dist/scripts/condense.js +52 -45
  93. package/dist/scripts/project_thin_rules.js +139 -19
  94. package/dist/server/schemas/settings.js +16 -3
  95. package/docs/MIGRATION.md +105 -1
  96. package/docs/contracts/settings-classes.md +40 -8
  97. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  98. package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
  99. package/llms.txt +1 -1
  100. package/package.json +7 -4
  101. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  102. package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  103. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  104. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  105. package/src/agent-src/scripts/stubs_due.ts +4 -18
  106. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  107. package/src/config/agent-settings.template.yml +47 -1
  108. package/src/config/continuity-surface.json +516 -0
  109. package/src/config/estate-count-budget.json +1 -1
  110. package/src/config/evaluator-budgets.json +4 -4
  111. package/src/config/gate-coverage.yml +29 -8
  112. package/src/config/gate-violation-baselines.json +8 -8
  113. package/src/config/gitignore-block.txt +19 -0
  114. package/src/config/hook-token-budget.json +7 -6
  115. package/src/config/host-injection-effect.json +73 -0
  116. package/src/config/packed-binary-manifest.json +33 -0
  117. package/src/config/preamble-payload-budget.json +112 -105
  118. package/src/config/publish-surface.json +4 -2
  119. package/src/config/routing-coverage-seed.json +9 -2
  120. package/src/install/claudeLayerCarriage.ts +273 -0
  121. package/src/install/claudePathsPlan.ts +76 -0
  122. package/src/install/hostLayerFingerprint.ts +1 -1
  123. package/src/install/partitionEligibility.ts +261 -127
  124. package/src/install/ruleLayerPartition.ts +19 -23
  125. package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
  126. package/src/scripts/_cli/handoff_sessions.ts +14 -44
  127. package/src/scripts/_dispatch.bash +77 -27
  128. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  129. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  130. package/src/scripts/_lib/continuity_slot.ts +295 -0
  131. package/src/scripts/_lib/continuity_surface.ts +296 -0
  132. package/src/scripts/_lib/continuity_writer.ts +253 -0
  133. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  134. package/src/scripts/_lib/hook_settings.ts +86 -1
  135. package/src/scripts/_lib/injection_effect.ts +151 -0
  136. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  137. package/src/scripts/_lib/lean_projection_mode.ts +127 -0
  138. package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
  139. package/src/scripts/_lib/pin_resolver.ts +13 -0
  140. package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
  141. package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
  142. package/src/scripts/_lib/retired_status.ts +74 -0
  143. package/src/scripts/_lib/session_eol.ts +44 -0
  144. package/src/scripts/_lib/session_index_trust.ts +346 -0
  145. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  146. package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
  147. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  148. package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
  149. package/src/scripts/ai_council/api_transport.ts +5 -0
  150. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  151. package/src/scripts/ai_council/prompts.ts +9 -0
  152. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  153. package/src/scripts/audit_mcp_tools.ts +69 -25
  154. package/src/scripts/build_mcp_catalog.ts +24 -1
  155. package/src/scripts/capture_skill_catalogue.ts +24 -11
  156. package/src/scripts/check_candidate_lines.ts +499 -0
  157. package/src/scripts/check_continuity_surface.ts +475 -0
  158. package/src/scripts/check_dependency_floors.ts +13 -6
  159. package/src/scripts/check_estate_count.ts +40 -79
  160. package/src/scripts/check_host_tree_parity.ts +291 -0
  161. package/src/scripts/check_pack_size.ts +262 -23
  162. package/src/scripts/check_preamble_payload_budget.ts +136 -7
  163. package/src/scripts/check_roadmap_trackable.ts +19 -16
  164. package/src/scripts/check_rule_layer_partition.ts +54 -34
  165. package/src/scripts/check_rule_projection_integrity.ts +102 -2
  166. package/src/scripts/check_single_delivery.ts +97 -31
  167. package/src/scripts/check_update_banner.ts +13 -0
  168. package/src/scripts/ci_settle.ts +205 -5
  169. package/src/scripts/code_graph/build.ts +217 -15
  170. package/src/scripts/code_graph/cli.ts +334 -15
  171. package/src/scripts/code_graph/detect.ts +38 -0
  172. package/src/scripts/code_graph/extract.ts +11 -0
  173. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  174. package/src/scripts/code_graph/loader.ts +88 -14
  175. package/src/scripts/code_graph/query.ts +144 -11
  176. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  177. package/src/scripts/code_graph/sqlite_store.ts +262 -6
  178. package/src/scripts/code_graph/types.ts +117 -3
  179. package/src/scripts/code_graph/validate.ts +26 -1
  180. package/src/scripts/code_graph/verbs.ts +590 -0
  181. package/src/scripts/condense.ts +54 -42
  182. package/src/scripts/corpus_manifest.ts +5 -5
  183. package/src/scripts/council_cli.ts +11 -12
  184. package/src/scripts/count_intent_disagreement.ts +426 -0
  185. package/src/scripts/handoff_context_hook.ts +118 -1
  186. package/src/scripts/hook_effect_doctor.ts +82 -1
  187. package/src/scripts/hook_manifest.json +1 -1
  188. package/src/scripts/hook_manifest.yaml +87 -12
  189. package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
  190. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  191. package/src/scripts/hooks/code_graph_context_hook.ts +201 -0
  192. package/src/scripts/hooks/concern_registry.ts +4 -2
  193. package/src/scripts/hooks/host_semantics.ts +1 -1
  194. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  195. package/src/scripts/hooks/push_settle_hook.ts +3 -2
  196. package/src/scripts/hooks/rule_inject_hook.ts +108 -21
  197. package/src/scripts/hooks/session_eol_hook.ts +120 -2
  198. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  199. package/src/scripts/hot_context_hook.ts +62 -7
  200. package/src/scripts/inbox_source_census.ts +322 -0
  201. package/src/scripts/install-hooks.sh +82 -0
  202. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  203. package/src/scripts/lint_plan_risk_register.ts +13 -17
  204. package/src/scripts/lint_roadmap_blockers.ts +164 -1
  205. package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
  206. package/src/scripts/mcp_server/graph_tools.ts +294 -0
  207. package/src/scripts/mcp_server/path_util.ts +31 -0
  208. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  209. package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
  210. package/src/scripts/mcp_server/tools.ts +5 -69
  211. package/src/scripts/media/lib/adapter-contract.md +7 -2
  212. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  213. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  214. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  215. package/src/scripts/model_rule_injection.ts +61 -4
  216. package/src/scripts/project_thin_rules.ts +149 -16
  217. package/src/scripts/rdp_candidates_delta.ts +276 -0
  218. package/src/scripts/rdp_quality_eval.ts +217 -19
  219. package/src/scripts/release_mix_taxonomy.json +3 -2
  220. package/src/scripts/report_host_injection_effect.ts +189 -0
  221. package/src/scripts/report_layer_overlap.ts +41 -0
  222. package/src/scripts/report_skill_menu_census.ts +341 -0
  223. package/src/scripts/report_standing_payload_by_host.ts +650 -0
  224. package/src/scripts/schemas/agent-settings.schema.json +7 -1
  225. package/src/scripts/session_memory_index.ts +81 -8
  226. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  227. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  228. package/src/server/schemas/settings.ts +23 -4
  229. package/src/vendor/grammars/README.md +86 -0
  230. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  231. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  232. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  233. package/src/scripts/_lib/carrier_status.ts +0 -118
  234. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  235. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -0,0 +1,499 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * check_candidate_lines.ts — the SHAPE of the candidate-forms line, and the
4
+ * eval instrument that measures it. Not a shipped obligation on anyone.
5
+ *
6
+ * Steps 2.3, 2.5 and 2.6 of `road-to-candidate-moves-floor` define what a
7
+ * `Candidates:` line must look like; step 3.1 needs the three deterministic
8
+ * counts (line present at owed slots, `K0` present, axes pairwise distinct)
9
+ * before any rubric scoring runs. This file is both.
10
+ *
11
+ * WHY IT IS A SEPARATE SCRIPT, and why `lint_mandated_lines.ts` is untouched.
12
+ * The split AI council of 2026-09-07 blocked one specific thing: shipping a
13
+ * sixth line into `mandated-lines.md` § The five lines and giving
14
+ * `lint_mandated_lines` a third obligation. That is the "globally mandatory"
15
+ * step. It explicitly authorised the other half — `Candidates:` may be
16
+ * instructed to the model under test in the eval treatment arm, "where it can
17
+ * be measured without being a shipped obligation on anyone". A measurement
18
+ * instrument is that half. Adding the obligation to the existing linter would
19
+ * be the blocked half, so this checker lives beside it and nothing imports it
20
+ * into the shipped gate.
21
+ *
22
+ * THE GRAMMAR, one line, candidates separated by the house middle dot:
23
+ *
24
+ * Candidates: K0 keep the current shape · A extract a Notifier service
25
+ * [ownership boundary] · B pass a callback [call-site coupling] → A; three
26
+ * callers already build the payload, so the boundary exists already.
27
+ *
28
+ * `K0` is `keep the current form / change nothing` and is owed first. A forced
29
+ * form — exactly one admissible shape — carries the constraint instead of an
30
+ * invented alternative:
31
+ *
32
+ * Candidates: K0 keep the registration in `register()` [forced: the base
33
+ * class fixes the extension point] → K0; no other location is admissible.
34
+ *
35
+ * WHAT IT CANNOT CHECK. Not whether the candidates were generated before the
36
+ * choice. A line with an axis and a discriminator is reconstructable in ten
37
+ * seconds after deciding, and no textual check sees the order — the roadmap
38
+ * lists that as its own first risk, and `mandated-lines.md` § Honest scope
39
+ * says the same about the intent line. What IS decidable is that the axes are
40
+ * pairwise distinct, which kills the cosmetic set where three candidates
41
+ * differ only in where a helper lives.
42
+ *
43
+ * Exit: 0 clean · 1 usage/IO error · 2 findings.
44
+ */
45
+
46
+ import * as fs from 'node:fs';
47
+ import * as path from 'node:path';
48
+ import { fileURLToPath, pathToFileURL } from 'node:url';
49
+
50
+ import { contentWords, overlap } from './count_intent_disagreement.js';
51
+ import { stripFences, unwrapLines } from './lint_mandated_lines.js';
52
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
53
+
54
+ const _HERE = fileURLToPath(import.meta.url);
55
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
56
+
57
+ const SLOT_SEPARATOR = '·';
58
+
59
+ /**
60
+ * `Candidates: …` — one physical line, EMPHASIS-TOLERANT.
61
+ *
62
+ * The optional `**`/`__`/`*`/`_` runs are not cosmetic tolerance; they are the
63
+ * difference between a reading and an artefact. The first version copied the
64
+ * shipped `INTENT_RE` shape, which anchors the label at line start with no
65
+ * emphasis allowed. The 2026-09-08 treatment run then emitted its ONE
66
+ * compliant line as `**Candidates:** K0 …` — markdown bold, which every
67
+ * assistant reaches for on a labelled line — and the checker reported
68
+ * `Candidates line present: 0` over 32 transcripts. A detector that cannot see
69
+ * the thing it counts reproduces the defect it is measuring, and it does so
70
+ * silently, in the direction that confirms the null.
71
+ *
72
+ * The same gap exists in `lint_mandated_lines.ts`'s `INTENT_RE` and in the four
73
+ * other shipped labels. It is recorded as a finding rather than fixed here:
74
+ * that file is a shipped gate with 19 tests of its own, adjacent to the
75
+ * contract the 2026-09-07 council blocked, and widening its discrimination is
76
+ * its own change.
77
+ *
78
+ * CASE-SENSITIVE on the label. Widening with an `i` flag was tried on the
79
+ * sibling counter and took its population from 0 to 172, almost all of them
80
+ * lowercase `intent:` YAML keys in config — a line anchor turned into a prose
81
+ * detector. The instructed label is `Candidates:` and that is what the one
82
+ * real emission carried.
83
+ */
84
+ export const CANDIDATES_RE =
85
+ /^[ \t]*(?:>[ \t]*)?(?:[-*+][ \t]+)?(?:\*\*|__|\*|_)?Candidates(?:\*\*|__|\*|_)?(?:[ \t]*\([^)]*\))?(?:\*\*|__|\*|_)?:(?:\*\*|__|\*|_)?[ \t]*(.+)$/gm;
86
+
87
+ /**
88
+ * Decision classes where a form choice actually exists — step 2.3.
89
+ *
90
+ * A FILE COUNT IS THE WRONG TRIGGER and that is the point of the list: a
91
+ * controller plus its test is two files and no architectural choice, while a
92
+ * rename touches four and offers none. These match the semantics instead.
93
+ */
94
+ export const OWED_CLASS_TRIGGERS: readonly (readonly [string, RegExp])[] = [
95
+ // The `(?:\w+ ){0,3}` gaps are load-bearing: the first version required the
96
+ // noun to follow the article immediately, so "extracted a Notifier service"
97
+ // — the canonical case — did not fire at all.
98
+ ['ownership-boundary', /\b(ownership boundary|new (?:\w+ ){0,2}(?:service|module|package|bounded context)|extract(?:ed|ing|s)? (?:a |an |the )?(?:\w+ ){0,3}(?:service|module|class|package))\b/i],
99
+ ['new-abstraction', /\b(new (?:\w+ ){0,2}abstraction|introduce[sd]? (?:an |a |the )?(?:\w+ ){0,3}(?:interface|abstraction|base class|strategy|candidate)|abstract(?:ed|ing)? (?:out|over))\b/i],
100
+ ['contract-change', /\b((?:contract|schema|DTO|signature|API response|payload)s?\b[^.]{0,40}\bchang|chang[^.]{0,40}\b(?:contract|schema|DTO|signature|payload)s?\b|migrat(?:e|ed|ion)\b[^.]{0,30}\bcolumn)\b/i],
101
+ ['state-transition', /\b(state (?:transition|machine)|status transition|lifecycle state)\b/i],
102
+ ['migration-rollout', /\b(migration|rollout|roll-out|backfill|expand-contract)\b/i],
103
+ ['failure-semantics', /\b(failure semantics|error semantics|retry (?:policy|semantics)|timeout (?:policy|semantics)|new (?:exception|error) (?:type|class))\b/i],
104
+ ['irreversible-action', /\b(irreversible|destructive|force[- ]push|drop (?:table|column)|truncate)\b/i],
105
+ ['unresolved-root-cause', /\b(root cause (?:is |remains )?(?:unknown|unresolved|not (?:yet )?(?:found|established))|cause (?:is|remains) unclear)\b/i],
106
+ ['maintainer-decision', /\b((?:roadmap|budget|ratchet|projection) (?:shape|scope|baseline)|re-?scope[sd]? the roadmap|lower(?:ed|ing)? the (?:baseline|ratchet))\b/i],
107
+ ];
108
+
109
+ /**
110
+ * Classes where the line is NOT owed — step 2.3's other half.
111
+ *
112
+ * These suppress ONLY when no owed trigger fired. The asymmetry is deliberate
113
+ * and is the safe direction: a rename that also changes a contract is a
114
+ * contract change, so an exemption may never overrule a positive owed match.
115
+ * Residual, stated rather than discovered later: a report that describes an
116
+ * abstraction change using only rename vocabulary owes nothing, and no lexical
117
+ * check fixes that.
118
+ */
119
+ export const NOT_OWED_TRIGGERS: readonly (readonly [string, RegExp])[] = [
120
+ ['typo', /\b(typo|misspell|spelling fix)\b/i],
121
+ ['format', /\b(reformat|formatting|whitespace|prettier|lint fix|style-only)\b/i],
122
+ ['pure-rename', /\b(pure(?:ly)? rename|rename[sd]? (?:the )?(?:variable|file|symbol|method|function)\b)/i],
123
+ ['user-prescribed', /\b(the user (?:prescribed|specified|asked for) (?:the )?(?:form|shape|approach)|as instructed)\b/i],
124
+ ['mechanical-test-update', /\b(mechanical(?:ly)? (?:test )?update|updated the (?:test )?(?:fixtures?|snapshots?)|snapshot refresh)\b/i],
125
+ ['generated-refresh', /\b(regenerated?|generated-code refresh|re-?ran the generator|task (?:sync|generate-tools))\b/i],
126
+ ['deterministic-config', /\b(one-line config|bumped the (?:version|constant)|flipped the (?:flag|setting))\b/i],
127
+ ];
128
+
129
+ export interface Candidate {
130
+ id: string;
131
+ description: string;
132
+ /** Bracketed `[...]` content, or null. */
133
+ bracket: string | null;
134
+ /** The axis, when the bracket is an axis rather than a `forced:` constraint. */
135
+ axis: string | null;
136
+ /** The constraint, when the bracket declares a forced form. */
137
+ forced: string | null;
138
+ }
139
+
140
+ export interface ParsedLine {
141
+ raw: string;
142
+ candidates: Candidate[];
143
+ chosen: string | null;
144
+ observation: string | null;
145
+ }
146
+
147
+ export type FindingCode =
148
+ | 'missing-candidates'
149
+ | 'candidates-no-decision'
150
+ | 'missing-k0'
151
+ | 'k0-not-first'
152
+ | 'missing-axis'
153
+ | 'shared-axis'
154
+ | 'single-candidate-unforced'
155
+ | 'unknown-choice'
156
+ | 'missing-observation';
157
+
158
+ export interface Finding {
159
+ code: FindingCode;
160
+ message: string;
161
+ }
162
+
163
+ export interface Verdict {
164
+ findings: Finding[];
165
+ lines: ParsedLine[];
166
+ /** Which owed classes fired, for the report. */
167
+ owedClasses: string[];
168
+ /** Which exemptions fired. */
169
+ exemptClasses: string[];
170
+ owed: boolean;
171
+ }
172
+
173
+ /** A candidate id: `K0`, or a single letter optionally followed by digits. */
174
+ const ID_RE = /^(K\d+|[A-Z]\d*)\b/;
175
+
176
+ /** Sentences that DENY rather than claim — mirrors the sibling linter. */
177
+ const DENIAL_RE = /\b(no|nothing|not|never|without)\b[^.]{0,40}$/i;
178
+
179
+ function fires(text: string, triggers: readonly (readonly [string, RegExp])[]): string[] {
180
+ const hit: string[] = [];
181
+ for (const sentence of text.split(/(?<=[.!?\n])/)) {
182
+ for (const [name, re] of triggers) {
183
+ if (!re.test(sentence)) continue;
184
+ const before = sentence.slice(0, sentence.search(re));
185
+ if (DENIAL_RE.test(before)) continue;
186
+ if (!hit.includes(name)) hit.push(name);
187
+ }
188
+ }
189
+ return hit;
190
+ }
191
+
192
+ export function parseLine(rawBody: string): ParsedLine {
193
+ // A line the model wrapped in emphasis leaves its closing run at the end,
194
+ // where it would otherwise be read as part of the deciding observation.
195
+ const body = rawBody.replace(/(?:\*\*|__|\*|_)+[ \t]*$/, '').trim();
196
+ const arrowIdx = Math.max(body.indexOf('→'), body.indexOf('->'));
197
+ const listPart = arrowIdx === -1 ? body : body.slice(0, arrowIdx);
198
+ const decisionPart = arrowIdx === -1 ? '' : body.slice(arrowIdx).replace(/^(→|->)\s*/, '');
199
+
200
+ const candidates: Candidate[] = [];
201
+ for (const chunk of listPart.split(SLOT_SEPARATOR)) {
202
+ const t = chunk.trim();
203
+ if (t === '') continue;
204
+ const m = ID_RE.exec(t);
205
+ const id = m ? (m[1] as string) : '';
206
+ let rest = m ? t.slice((m[1] as string).length).trim() : t;
207
+ let bracket: string | null = null;
208
+ const b = /\[([^\]]*)\]\s*$/.exec(rest);
209
+ if (b) {
210
+ bracket = (b[1] ?? '').trim();
211
+ rest = rest.slice(0, b.index).trim();
212
+ }
213
+ const isForced = bracket !== null && /^forced\s*:/i.test(bracket);
214
+ candidates.push({
215
+ id,
216
+ description: rest,
217
+ bracket,
218
+ axis: bracket !== null && !isForced ? bracket : null,
219
+ forced: isForced ? (bracket as string).replace(/^forced\s*:\s*/i, '').trim() : null,
220
+ });
221
+ }
222
+
223
+ let chosen: string | null = null;
224
+ let observation: string | null = null;
225
+ if (decisionPart !== '') {
226
+ const cm = ID_RE.exec(decisionPart.trim());
227
+ chosen = cm ? (cm[1] as string) : null;
228
+ const sep = decisionPart.search(/[;—]|\s-\s/);
229
+ observation = sep === -1 ? null : decisionPart.slice(sep + 1).replace(/^[—;\s-]+/, '').trim() || null;
230
+ }
231
+
232
+ return { raw: body, candidates, chosen, observation };
233
+ }
234
+
235
+ /**
236
+ * The axis-distinctness threshold.
237
+ *
238
+ * A STATED DEFAULT, not a measured optimum. Two axes whose content words
239
+ * overlap at or above this read as one axis — step 2.5's "three candidates
240
+ * that differ only in where a helper lives are one candidate".
241
+ */
242
+ export const AXIS_OVERLAP = 0.6;
243
+
244
+ export function checkReport(text: string): Verdict {
245
+ const findings: Finding[] = [];
246
+ const prepared = unwrapLines(stripFences(text));
247
+
248
+ const owedClasses = fires(prepared, OWED_CLASS_TRIGGERS);
249
+ const exemptClasses = fires(prepared, NOT_OWED_TRIGGERS);
250
+ // An exemption suppresses only when nothing owed fired. See NOT_OWED_TRIGGERS.
251
+ const owed = owedClasses.length > 0;
252
+
253
+ CANDIDATES_RE.lastIndex = 0;
254
+ const lines = [...prepared.matchAll(CANDIDATES_RE)].map((m) => parseLine(m[1] ?? ''));
255
+
256
+ if (owed && lines.length === 0) {
257
+ findings.push({
258
+ code: 'missing-candidates',
259
+ message:
260
+ `the report is in ${owedClasses.length} owed decision class(es) ` +
261
+ `(${owedClasses.join(', ')}) and carries no Candidates line. ` +
262
+ 'K0 first, each non-K0 candidate with the axis it differs on, then the choice ' +
263
+ 'and the observation that decided it.',
264
+ });
265
+ }
266
+
267
+ for (const [i, l] of lines.entries()) {
268
+ const where = lines.length > 1 ? `line ${String(i + 1)} of ${String(lines.length)}: ` : '';
269
+ const ids = l.candidates.map((c) => c.id);
270
+
271
+ if (!ids.includes('K0')) {
272
+ findings.push({
273
+ code: 'missing-k0',
274
+ message:
275
+ `${where}no K0 candidate. K0 is "keep the current form / change nothing" and is ` +
276
+ 'owed wherever the line is owed — without it the set assumes some edit is correct ' +
277
+ 'before any reasoning starts.',
278
+ });
279
+ } else if (ids[0] !== 'K0') {
280
+ findings.push({ code: 'k0-not-first', message: `${where}K0 is present but not first (order: ${ids.join(', ')}).` });
281
+ }
282
+
283
+ const nonK0 = l.candidates.filter((c) => c.id !== 'K0');
284
+ const forcedDecl = l.candidates.find((c) => c.forced !== null);
285
+
286
+ if (nonK0.length === 0 && forcedDecl === undefined) {
287
+ findings.push({
288
+ code: 'single-candidate-unforced',
289
+ message:
290
+ `${where}only K0, and no forced-form constraint. Where exactly one form is ` +
291
+ 'admissible, name the constraint that forecloses the alternatives — ' +
292
+ '`[forced: the base class fixes the extension point]`. A bare single candidate ' +
293
+ 'is indistinguishable from not having looked.',
294
+ });
295
+ }
296
+
297
+ for (const c of nonK0) {
298
+ if (c.axis === null && c.forced === null) {
299
+ findings.push({
300
+ code: 'missing-axis',
301
+ message:
302
+ `${where}candidate ${c.id} carries no axis. Three candidates that differ only ` +
303
+ 'in where a helper lives are one candidate.',
304
+ });
305
+ }
306
+ }
307
+
308
+ const axes = nonK0.filter((c) => c.axis !== null).map((c) => ({ id: c.id, set: contentWords(c.axis as string) }));
309
+ for (let a = 0; a < axes.length; a++) {
310
+ for (let b = a + 1; b < axes.length; b++) {
311
+ const ov = overlap((axes[a] as { set: Set<string> }).set, (axes[b] as { set: Set<string> }).set);
312
+ if (ov >= AXIS_OVERLAP) {
313
+ findings.push({
314
+ code: 'shared-axis',
315
+ message:
316
+ `${where}candidates ${(axes[a] as { id: string }).id} and ${(axes[b] as { id: string }).id} ` +
317
+ `share an axis (overlap ${String(Math.round(ov * 1000) / 1000)}) — they count as ONE material candidate.`,
318
+ });
319
+ }
320
+ }
321
+ }
322
+
323
+ if (l.chosen === null) {
324
+ findings.push({
325
+ code: 'candidates-no-decision',
326
+ message: `${where}no choice. The line ends with "→ <id>; <the observation that decided it>".`,
327
+ });
328
+ } else {
329
+ if (!ids.includes(l.chosen)) {
330
+ findings.push({
331
+ code: 'unknown-choice',
332
+ message: `${where}the choice ${l.chosen} is not one of the listed candidates (${ids.join(', ')}).`,
333
+ });
334
+ }
335
+ if (l.observation === null || l.observation === '') {
336
+ findings.push({
337
+ code: 'missing-observation',
338
+ message:
339
+ `${where}the choice ${l.chosen} states no deciding observation. Without one the ` +
340
+ 'choice reads as preference, which the rubric scores 2 rather than 3.',
341
+ });
342
+ }
343
+ }
344
+ }
345
+
346
+ return { findings, lines, owedClasses, exemptClasses, owed };
347
+ }
348
+
349
+ /** The three deterministic counts step 3.1 runs before any scoring. */
350
+ export interface CorpusCounts {
351
+ transcripts: number;
352
+ withLine: number;
353
+ withK0: number;
354
+ axesPairwiseDistinct: number;
355
+ k0Drawn: number;
356
+ findings: number;
357
+ }
358
+
359
+ /**
360
+ * Count a stored eval results file.
361
+ *
362
+ * Reads the same envelope `rdp_quality_eval --score-only` reads, so the
363
+ * treatment run needs no bespoke format. `k0Drawn` is the counter step 2.1
364
+ * actually wants: K0 present but never chosen across a whole corpus is K0 as
365
+ * an unchosen first row, which is the failure that step names.
366
+ */
367
+ export function countCorpus(runPath: string): CorpusCounts | { error: string } {
368
+ let run: { results?: Array<{ variants?: Record<string, { text?: string }> }> };
369
+ try {
370
+ run = JSON.parse(fs.readFileSync(runPath, 'utf-8')) as typeof run;
371
+ } catch (e) {
372
+ return { error: `cannot read ${runPath}: ${String(e)}` };
373
+ }
374
+ const counts: CorpusCounts = {
375
+ transcripts: 0, withLine: 0, withK0: 0, axesPairwiseDistinct: 0, k0Drawn: 0, findings: 0,
376
+ };
377
+ for (const r of run.results ?? []) {
378
+ for (const v of Object.values(r.variants ?? {})) {
379
+ if (typeof v.text !== 'string' || v.text === '') continue;
380
+ counts.transcripts += 1;
381
+ const verdict = checkReport(v.text);
382
+ if (verdict.lines.length === 0) continue;
383
+ counts.withLine += 1;
384
+ counts.findings += verdict.findings.length;
385
+ const l = verdict.lines[0] as ParsedLine;
386
+ if (l.candidates.some((c) => c.id === 'K0')) counts.withK0 += 1;
387
+ if (!verdict.findings.some((f) => f.code === 'shared-axis')) counts.axesPairwiseDistinct += 1;
388
+ if (l.chosen === 'K0') counts.k0Drawn += 1;
389
+ }
390
+ }
391
+ return counts;
392
+ }
393
+
394
+ function readInput(argv: readonly string[]): { text: string } | { error: string } {
395
+ const fileIdx = argv.indexOf('--file');
396
+ if (fileIdx !== -1) {
397
+ const p = argv[fileIdx + 1];
398
+ if (p === undefined) return { error: '--file needs a path' };
399
+ try {
400
+ return { text: fs.readFileSync(path.resolve(p), 'utf-8') };
401
+ } catch (e) {
402
+ return { error: `cannot read ${p}: ${String(e)}` };
403
+ }
404
+ }
405
+ if (argv.includes('--stdin')) {
406
+ try {
407
+ return { text: fs.readFileSync(0, 'utf-8') };
408
+ } catch (e) {
409
+ return { error: `cannot read stdin: ${String(e)}` };
410
+ }
411
+ }
412
+ return { error: 'usage: check_candidate_lines --stdin | --file <report.md> | --corpus <results.json>' };
413
+ }
414
+
415
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
416
+ const corpusIdx = argv.indexOf('--corpus');
417
+ if (corpusIdx !== -1) {
418
+ const p = argv[corpusIdx + 1];
419
+ if (p === undefined) {
420
+ process.stderr.write('--corpus needs a results.json path\n');
421
+ return 1;
422
+ }
423
+ const c = countCorpus(path.resolve(p));
424
+ if ('error' in c) {
425
+ process.stderr.write(`${c.error}\n`);
426
+ return 1;
427
+ }
428
+ try {
429
+ assertScanned({
430
+ gate: 'check_candidate_lines',
431
+ scanned: c.transcripts,
432
+ units: 'transcript(s)',
433
+ roots: [p],
434
+ });
435
+ } catch (e) {
436
+ if (e instanceof DeadScopeError) {
437
+ process.stderr.write(`❌ ${e.message}\n`);
438
+ return 1;
439
+ }
440
+ throw e;
441
+ }
442
+ process.stdout.write(
443
+ `transcripts: ${String(c.transcripts)}\n` +
444
+ `Candidates line present: ${String(c.withLine)}\n` +
445
+ `K0 present: ${String(c.withK0)}\n` +
446
+ `axes pairwise distinct: ${String(c.axesPairwiseDistinct)}\n` +
447
+ `K0 actually drawn: ${String(c.k0Drawn)}\n` +
448
+ `shape findings: ${String(c.findings)}\n` +
449
+ `scanned: ${String(c.transcripts)}\n`,
450
+ );
451
+ return 0;
452
+ }
453
+
454
+ const quiet = argv.includes('--quiet');
455
+ const input = readInput(argv);
456
+ if ('error' in input) {
457
+ process.stderr.write(`${input.error}\n`);
458
+ return 1;
459
+ }
460
+ const contentLines = input.text.split('\n').filter((l) => l.trim() !== '').length;
461
+ try {
462
+ assertScanned({ gate: 'check_candidate_lines', scanned: contentLines, units: 'report line(s)', roots: ['<stdin>'] });
463
+ } catch (e) {
464
+ if (e instanceof DeadScopeError) {
465
+ process.stderr.write(`❌ ${e.message}\n`);
466
+ return 1;
467
+ }
468
+ throw e;
469
+ }
470
+
471
+ const verdict = checkReport(input.text);
472
+ for (const f of verdict.findings) process.stderr.write(`❌ ${f.code} ${f.message}\n`);
473
+ if (verdict.findings.length > 0) {
474
+ process.stderr.write(`\n❌ check_candidate_lines: ${String(verdict.findings.length)} finding(s)\n`);
475
+ return 2;
476
+ }
477
+ if (!quiet) {
478
+ if (!verdict.owed && verdict.lines.length === 0) {
479
+ process.stdout.write(
480
+ '✅ check_candidate_lines: no owed decision class detected in this report' +
481
+ (verdict.exemptClasses.length > 0 ? ` (exempt: ${verdict.exemptClasses.join(', ')})` : '') +
482
+ '\n',
483
+ );
484
+ } else {
485
+ process.stdout.write(
486
+ `✅ check_candidate_lines: ${String(verdict.lines.length)} well-formed line(s)` +
487
+ (verdict.owedClasses.length > 0 ? ` for owed class(es) ${verdict.owedClasses.join(', ')}` : '') +
488
+ '\n',
489
+ );
490
+ }
491
+ }
492
+ return 0;
493
+ }
494
+
495
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
496
+ process.exit(main());
497
+ }
498
+
499
+ export { REPO_ROOT };