@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
@@ -13,7 +13,7 @@
13
13
  * |---|---|---|---|
14
14
  * | phase | one audit-log line — a `/work` PHASE | here, consumed by `orchestration_record` | 4 |
15
15
  * | step | one work-engine STEP | `work_engine/delivery_state.ts` (template tree) | 3 |
16
- * | run | one command or gate RUN | here, consumed by `outcome_envelope` | 6 |
16
+ * | run | one command or gate RUN | here, consumed by `outcome_envelope` | 7 | code-comment-allow report-comment -- one row of a pre-existing three-row table documenting the three vocabularies; the digit changed from 6 to 7 with the vocabulary, and the table is the registry's whole point
17
17
  *
18
18
  * All three are declared in code AND emitted today. `skipped` and `error` are
19
19
  * not aspirational: `envelopeOutcome` in `orchestration_record.ts` returns
@@ -33,16 +33,42 @@
33
33
  * `partial`. So: separate vocabularies, one registry, mappings only where
34
34
  * execution actually crosses.
35
35
  *
36
- * **Mappings are not written for symmetry.** Exactly one crossing exists
37
- * (`DispatchOutcome` -> phase, in `orchestration_record.ts`). A total
38
- * phase<->step<->run mapping would be nine relations of which one is real, and
39
- * the eight invented ones would read as contract.
36
+ * **Mappings are not written for symmetry.** Two crossings exist, and both are
37
+ * places where execution actually translates: `DispatchOutcome` -> phase in
38
+ * `orchestration_record.ts`, and `LadderAction` -> run in
39
+ * `continuation_ladder.ts` (added 2026-09-03 with the seventh run state, so the
40
+ * state has a producer rather than being a word nothing emits). A total
41
+ * phase<->step<->run mapping would be nine relations of which two are real, and
42
+ * the seven invented ones would read as contract.
40
43
  *
41
44
  * `revisit-if`: a producer needs a value its vocabulary cannot express, or a
42
45
  * second real crossing appears, or producer analysis shows two of the three
43
46
  * subjects share identical terminal semantics with a lossless mapping — at
44
47
  * which point unifying those two becomes the cheaper answer.
45
48
  *
49
+ * **The run vocabulary grew to seven on 2026-09-03** — exactly the first branch
50
+ * of the reopening condition above: the continuation ladder needed a word for a
51
+ * run whose plan premise moved, and had none. AI council (anthropic/claude-sonnet-4-5 +
52
+ * openai/codex-default, three rounds, blind chairman) resolved it unanimously
53
+ * as option (a), extend rather than overload `blocked` with a reason field. The
54
+ * seats attached the same prerequisites, discharged here: a versioned value
55
+ * domain ({@link RUN_TERMINAL_VOCABULARY_VERSION}), a tolerant read
56
+ * ({@link readRunTerminalState}), a declared downgrade
57
+ * ({@link RUN_TERMINAL_STATE_DOWNGRADE}) and the rollback trigger below.
58
+ *
59
+ * **Rollback trigger, stated rather than instrumented.** Withdraw
60
+ * `premise-invalidated` — remove it from {@link RUN_TERMINAL_STATES} and let the
61
+ * ladder rung report its declared downgrade `blocked` instead — on EITHER of:
62
+ * (1) any consumer is observed failing (a throw, a refused write, a dropped
63
+ * record) on encountering the value, or (2) `readRunTerminalState` returns
64
+ * `null` for a persisted value in normal operation, which would mean a writer
65
+ * emitted something this registry does not know. Both are single-occurrence
66
+ * triggers, not rates: the downgrade mapping already exists, so withdrawal
67
+ * costs one commit and no migration, and there is no reason to tolerate a
68
+ * budget of failures before paying that. No telemetry is built for this and
69
+ * none is claimed — condition (1) surfaces as a failing run and (2) as a null
70
+ * where a value was written.
71
+ *
46
72
  * Type-only + literal arrays. No `node:` import, so any surface may read it.
47
73
  */
48
74
 
@@ -82,9 +108,99 @@ export const RUN_TERMINAL_STATES = [
82
108
  'approval-required',
83
109
  'exhausted',
84
110
  'stagnated',
111
+ /**
112
+ * The plan premise this run was built on no longer holds — the situational
113
+ * -awareness fingerprint the run engaged under (`origin/main` plus every open
114
+ * PR head) was re-observed and differs.
115
+ *
116
+ * Added by `road-to-wired-instruments` Phase 2 under the AI-council decision
117
+ * of 2026-09-03 (option (a), unanimous). It is NOT `blocked` with a reason
118
+ * field: premise invalidation is operationally distinct from a missing
119
+ * precondition and from an exhausted budget, and folding it into either loses
120
+ * both aggregation by state and type-enforced exhaustive handling. The
121
+ * remedy differs too — re-probe and re-plan, where `blocked` says obtain the
122
+ * missing thing and `exhausted` says the budget was too small.
123
+ */
124
+ 'premise-invalidated',
85
125
  ] as const;
86
126
  export type RunTerminalState = (typeof RUN_TERMINAL_STATES)[number];
87
127
 
128
+ /**
129
+ * Version of the RUN vocabulary's VALUE DOMAIN — bumped when a member is added
130
+ * or removed, never for prose.
131
+ *
132
+ * Distinct from any table version: `runtime_journal.JOURNAL_SCHEMA_VERSION`
133
+ * covers tables and columns and is unaffected by a widening of what one column
134
+ * may contain, so a value-domain change needs its own number or it travels
135
+ * unversioned. A persisted shape carrying a `RunTerminalState` stamps THIS
136
+ * number beside the value, so a reader can tell a value it does not know from a
137
+ * value that is corrupt.
138
+ *
139
+ * v1 — the six original states.
140
+ * v2 — adds `premise-invalidated` (2026-09-03).
141
+ */
142
+ export const RUN_TERMINAL_VOCABULARY_VERSION = 2;
143
+
144
+ /** The vocabulary version each member first appeared in. */
145
+ export const RUN_TERMINAL_STATE_SINCE: Readonly<Record<RunTerminalState, number>> = {
146
+ success: 1,
147
+ 'clean-no-op': 1,
148
+ blocked: 1,
149
+ 'approval-required': 1,
150
+ exhausted: 1,
151
+ stagnated: 1,
152
+ 'premise-invalidated': 2,
153
+ };
154
+
155
+ /**
156
+ * What a consumer pinned to an older vocabulary version reports instead.
157
+ *
158
+ * `premise-invalidated` -> `blocked`: of the six v1 states it is the only one
159
+ * that is honest for a reader that cannot represent it — the run stopped on an
160
+ * external condition it cannot resolve by iterating. Mapping it to `exhausted`
161
+ * would invite the wrong remedy (raise the budget), which is the exact
162
+ * confusion the contract's own `exhausted`/`stagnated` split exists to prevent.
163
+ *
164
+ * Every member introduced after v1 MUST have a row here; `outcome_vocabularies`
165
+ * contract tests assert that, so a future eighth value cannot ship undowngradable.
166
+ */
167
+ export const RUN_TERMINAL_STATE_DOWNGRADE: Readonly<Partial<Record<RunTerminalState, RunTerminalState>>> = {
168
+ 'premise-invalidated': 'blocked',
169
+ };
170
+
171
+ /**
172
+ * The state a consumer pinned at `toVersion` should report for `state`.
173
+ *
174
+ * Returns the state unchanged when it already existed at that version, the
175
+ * declared downgrade when it did not, and `null` for a string this vocabulary
176
+ * does not know at all — never a throw, because this is the function a reader
177
+ * of PERSISTED data calls, and persisted data outlives the code that wrote it.
178
+ */
179
+ export function downgradeRunTerminalState(
180
+ state: string,
181
+ toVersion: number,
182
+ ): RunTerminalState | null {
183
+ if (!isRunTerminalState(state)) return null;
184
+ if ((RUN_TERMINAL_STATE_SINCE[state] ?? 1) <= toVersion) return state;
185
+ return RUN_TERMINAL_STATE_DOWNGRADE[state] ?? null;
186
+ }
187
+
188
+ /**
189
+ * Tolerant read of a persisted terminal state: a member returns itself,
190
+ * anything else returns `null`. Never throws.
191
+ *
192
+ * `null` rather than a fabricated fallback is the load-bearing choice. This
193
+ * vocabulary's own consumers already treat a null terminal state as "not
194
+ * recorded" (`repeated_failure` counts it under `unknown`), and that is exactly
195
+ * what an unrecognised value IS to a reader that cannot name it. Substituting
196
+ * `blocked` here would manufacture a measurement out of an absence — the
197
+ * downgrade above is for a value the reader KNOWS is newer, which is a
198
+ * different fact from a value it cannot place.
199
+ */
200
+ export function readRunTerminalState(v: unknown): RunTerminalState | null {
201
+ return isRunTerminalState(v) ? v : null;
202
+ }
203
+
88
204
  /**
89
205
  * The crossings that exist. One row, because one crossing is real.
90
206
  *
@@ -112,6 +228,20 @@ export const CROSS_DOMAIN_MAPPINGS = [
112
228
  */
113
229
  semantics_version: 2,
114
230
  },
231
+ {
232
+ source: 'LadderAction',
233
+ target: 'run',
234
+ at: 'src/scripts/_lib/continuation_ladder.ts',
235
+ fn: 'terminalStateFor',
236
+ /**
237
+ * Version 1, and the mapping is TOTAL over `LadderAction` by
238
+ * construction: `TERMINAL_STATE_BY_ACTION` is a `Record<LadderAction,
239
+ * RunTerminalState | null>`, so adding a rung without deciding its
240
+ * terminal state does not compile. `engage` maps to null because it is
241
+ * the one action that is not terminal.
242
+ */
243
+ semantics_version: 1,
244
+ },
115
245
  ] as const;
116
246
 
117
247
  /** Is this string a phase outcome? The guard the audit-log consumer was missing. */
@@ -0,0 +1,393 @@
1
+ /**
2
+ * The pathology archive — best-known intervention per `WHERE x WHY` failure
3
+ * cell, over closed vocabularies, backed by append-only attempt history.
4
+ *
5
+ * `road-to-governed-harness-evolution` step 4.4. A pure Pareto frontier orders
6
+ * candidates by their metric vectors and throws away the reason each candidate
7
+ * exists, so two candidates that fail for completely different reasons compete
8
+ * as if they were interchangeable. This module keeps the reason.
9
+ *
10
+ * ## What this is NOT
11
+ *
12
+ * It is not a second verdict beside `_lib/paired_verdict.ts`. Nothing here
13
+ * decides whether a candidate is better; the archive records WHY attempts
14
+ * failed and which intervention is currently RETAINED per cell. Promotion stays
15
+ * with `_lib/evaluation_vector.ts`'s `promotionVerdict`, which this module never
16
+ * calls and never reimplements.
17
+ *
18
+ * ## "Best" is a retained representative, not a quality claim
19
+ *
20
+ * The AI council of 2026-08-31 was explicit that "archive the best intervention
21
+ * per cell" names no operation until ranking, tie-break and replacement are all
22
+ * total. Two of the three candidate rules it enumerated need evidence systems
23
+ * that do not exist — lowest edit distance to a human gold standard (there is no
24
+ * gold standard) and highest measured failure-class coverage (there is no
25
+ * coverage metric). The third, recency, is trivially deterministic and is what
26
+ * ships. So the retained entry is a REPRESENTATIVE selected by recency, and this
27
+ * module says so in its type names rather than calling it "best" and implying a
28
+ * measurement nobody took.
29
+ *
30
+ * Because recency says nothing about quality, the attempt history is append-only
31
+ * and every ranking decision is stamped with `ranking_rule_version`. When a real
32
+ * quality metric arrives, representatives are RECOMPUTED from history rather
33
+ * than lost.
34
+ *
35
+ * ## Ordering is by sequence, never by timestamp
36
+ *
37
+ * `attempt_sequence` is assigned by the ingester, monotonically. Timestamps come
38
+ * from producer clocks: they collide, they arrive out of order, and they make a
39
+ * replacement rule that is not total. `observed_at` is retained for reporting
40
+ * and is never consulted for ordering.
41
+ */
42
+
43
+ import { LADDER_RUNGS, type LadderRung } from './activation_ladder.js';
44
+
45
+ /** `WHERE` — reused from the activation ladder, never re-invented. */
46
+ export const PATHOLOGY_WHERE = LADDER_RUNGS;
47
+ export type PathologyWhere = LadderRung;
48
+
49
+ /**
50
+ * `WHY` — the closed failure-REASON vocabulary.
51
+ *
52
+ * Closed means: a versioned enum, exactly one value per attempt, and an addition
53
+ * requires a `classification_rule_version` bump plus migration. Free-form
54
+ * explanation belongs in `reason_detail` and never in this axis.
55
+ *
56
+ * The order IS the precedence order. When several reasons could apply, the
57
+ * earliest applicable value wins, so classification is deterministic rather than
58
+ * dependent on evaluation order. `reason_unknown` applies only when none of the
59
+ * first seven can be established — it is a real state, not a default.
60
+ *
61
+ * These are REASONS, deliberately not places. An earlier draft of this
62
+ * vocabulary used names like `projection_failed` and `delivery_failed`, which
63
+ * restate `WHERE` and collapse the very distinction the two axes exist to draw.
64
+ */
65
+ export const PATHOLOGY_WHY = [
66
+ 'precondition_unsatisfied',
67
+ 'policy_blocked',
68
+ 'dependency_unavailable',
69
+ 'execution_failed',
70
+ 'output_contract_violated',
71
+ 'evidence_missing',
72
+ 'human_rejected',
73
+ 'reason_unknown',
74
+ ] as const;
75
+ export type PathologyWhy = (typeof PATHOLOGY_WHY)[number];
76
+
77
+ /** Bumped when the cell/query shape changes. */
78
+ export const ARCHIVE_SCHEMA_VERSION = 1;
79
+ /** Bumped when `PATHOLOGY_WHY` or its precedence changes. */
80
+ export const CLASSIFICATION_RULE_VERSION = 1;
81
+ /** Bumped when the ranking/tie-break/replacement rule changes. */
82
+ export const RANKING_RULE_VERSION = 1;
83
+
84
+ /**
85
+ * Dominance threshold for the search-collapse guard.
86
+ *
87
+ * STATED POLICY DEFAULT, not a measured optimum — and deliberately a NEW
88
+ * constant rather than a reuse.
89
+ *
90
+ * The step's own verify line carries a bare `0.6`, and the prior council warned
91
+ * that reusing it would be "a semantic change wearing a constant's clothes" IF
92
+ * it already meant textual similarity. Checked against the tree on 2026-08-31:
93
+ * it does not, because no such constant exists. `diversityCollapsed`
94
+ * (`_lib/harness_evolution_guards.ts:215`) is a DISTINCT-COUNT check with
95
+ * `minDistinct = 2` and no ratio at all, and the only near-duplicate constant is
96
+ * `NEAR_DUPLICATE_THRESHOLD = 70` in `_lib/curator_ops.ts:106` — an integer
97
+ * percentage on a different scale. So the collision the objection feared is not
98
+ * present, and the number below is free to be given its own meaning.
99
+ *
100
+ * Revisit-if: a run records a collapse the guard did not catch, or a warm-up
101
+ * period in which the floor below suppressed a real signal.
102
+ */
103
+ export const PATHOLOGY_DOMINANCE_THRESHOLD = 0.6;
104
+ /** Observation window: the latest N classifiable failed attempts. */
105
+ export const PATHOLOGY_WINDOW_SIZE = 50;
106
+ /** No verdict below this many classifiable attempts in the window. */
107
+ export const PATHOLOGY_MIN_CLASSIFIABLE_ATTEMPTS = 20;
108
+
109
+ export type ClassificationStatus = 'classified' | 'unclassifiable';
110
+ export type ValidationStatus = 'valid' | 'invalid';
111
+
112
+ /** One append-only attempt observation. Never mutated, never deleted. */
113
+ export interface PathologyAttempt {
114
+ readonly attempt_id: string;
115
+ /** Monotonic, ingester-assigned. The ONLY ordering key. */
116
+ readonly attempt_sequence: number;
117
+ readonly candidate_id: string;
118
+ readonly intervention_ref: string;
119
+ readonly where: PathologyWhere;
120
+ readonly why: PathologyWhy;
121
+ readonly reason_detail: string;
122
+ readonly classification_status: ClassificationStatus;
123
+ readonly validation_status: ValidationStatus;
124
+ /** Reporting only. Never consulted for ordering. */
125
+ readonly observed_at: string;
126
+ readonly archive_schema_version: number;
127
+ readonly classification_rule_version: number;
128
+ readonly ranking_rule_version: number;
129
+ readonly cohort_id: string;
130
+ readonly cohort_version: number;
131
+ }
132
+
133
+ /** A versioned cell query result. The guard consumes THIS, never storage internals. */
134
+ export interface PathologyCell {
135
+ readonly where: PathologyWhere;
136
+ readonly why: PathologyWhy;
137
+ readonly archive_schema_version: number;
138
+ readonly classification_rule_version: number;
139
+ readonly ranking_rule_version: number;
140
+ readonly cohort_id: string;
141
+ readonly cohort_version: number;
142
+ readonly attempt_count: number;
143
+ readonly classifiable_count: number;
144
+ readonly unclassifiable_count: number;
145
+ readonly first_observed_attempt_sequence: number;
146
+ readonly first_observed_at: string;
147
+ readonly last_observed_attempt_sequence: number;
148
+ readonly last_observed_at: string;
149
+ readonly retained_candidate_id: string | null;
150
+ readonly retained_intervention_ref: string | null;
151
+ readonly retained_attempt_id: string | null;
152
+ readonly retained_attempt_sequence: number | null;
153
+ readonly retained_observed_at: string | null;
154
+ readonly retained_ranking_rule: string;
155
+ readonly retained_ranking_key: string | null;
156
+ readonly retained_validation_status: ValidationStatus | null;
157
+ }
158
+
159
+ /** A versioned window query result. The guard's only legal input. */
160
+ export interface PathologyWindowQuery {
161
+ readonly archive_schema_version: number;
162
+ readonly classification_rule_version: number;
163
+ readonly ranking_rule_version: number;
164
+ readonly window_size: number;
165
+ readonly unclassifiable_excluded: number;
166
+ readonly rows: readonly PathologyAttempt[];
167
+ }
168
+
169
+ export interface DominanceOptions {
170
+ readonly threshold?: number;
171
+ readonly windowSize?: number;
172
+ readonly minClassifiable?: number;
173
+ }
174
+
175
+ export const RETAINED_RANKING_RULE = 'recency-v1: attempt_sequence DESC, candidate_id ASC, attempt_id ASC';
176
+
177
+ export function cellKey(where: PathologyWhere, why: PathologyWhy): string {
178
+ return `${where} ${why}`;
179
+ }
180
+
181
+ export class PathologyArchiveError extends Error {}
182
+
183
+ /**
184
+ * Is `incoming` a better representative than `retained`?
185
+ *
186
+ * Total by construction: the tuple `(attempt_sequence DESC, candidate_id ASC,
187
+ * attempt_id ASC)` orders any two distinct attempts, and `attempt_id` is unique,
188
+ * so the comparison never falls through to "equal" for two different attempts.
189
+ * A rule whose tie-break is declared but unreachable is not total, which is the
190
+ * defect the council named in the timestamp-ordered alternative.
191
+ */
192
+ export function replacesRetained(incoming: PathologyAttempt, retained: PathologyAttempt): boolean {
193
+ if (incoming.attempt_sequence !== retained.attempt_sequence) {
194
+ return incoming.attempt_sequence > retained.attempt_sequence;
195
+ }
196
+ if (incoming.candidate_id !== retained.candidate_id) {
197
+ return incoming.candidate_id < retained.candidate_id;
198
+ }
199
+ return incoming.attempt_id < retained.attempt_id;
200
+ }
201
+
202
+ /** Only a validated, classified attempt may become a representative. */
203
+ function eligibleAsRepresentative(a: PathologyAttempt): boolean {
204
+ return a.classification_status === 'classified' && a.validation_status === 'valid';
205
+ }
206
+
207
+ /**
208
+ * Append-only, idempotent attempt store.
209
+ *
210
+ * Re-ingesting an existing `attempt_id` is a no-op: it cannot alter counts and
211
+ * cannot change a representative. Without that, a retry loop silently inflates
212
+ * the very frequency evidence the dominance guard reads.
213
+ */
214
+ export class PathologyArchive {
215
+ private readonly byId = new Map<string, PathologyAttempt>();
216
+ private readonly order: PathologyAttempt[] = [];
217
+
218
+ /** @returns true when the attempt was new, false when it was a duplicate. */
219
+ ingest(a: PathologyAttempt): boolean {
220
+ if (!PATHOLOGY_WHERE.includes(a.where)) {
221
+ throw new PathologyArchiveError(`unknown WHERE '${a.where}'`);
222
+ }
223
+ if (!PATHOLOGY_WHY.includes(a.why)) {
224
+ throw new PathologyArchiveError(`unknown WHY '${a.why}'`);
225
+ }
226
+ if (this.byId.has(a.attempt_id)) return false;
227
+ this.byId.set(a.attempt_id, a);
228
+ this.order.push(a);
229
+ return true;
230
+ }
231
+
232
+ /** Every attempt, in ingest order. Callers must not mutate. */
233
+ attempts(): readonly PathologyAttempt[] {
234
+ return this.order;
235
+ }
236
+
237
+ /**
238
+ * The versioned window query the collapse guard consumes.
239
+ *
240
+ * The guard may not read `attempts()` directly: the council required it to
241
+ * consume "a versioned archive query rather than storage internals", so the
242
+ * version/cohort quad travels WITH the rows and a guard cannot silently mix
243
+ * two classification vocabularies. It returns attempts rather than cells
244
+ * because a last-N window is not reconstructible from aggregates — which is
245
+ * the same reason `cells()` alone is not the guard's input.
246
+ */
247
+ dominanceWindow(windowSize: number = PATHOLOGY_WINDOW_SIZE): PathologyWindowQuery {
248
+ const classifiable = this.order
249
+ .filter((a) => a.classification_status === 'classified')
250
+ .sort((a, b) => a.attempt_sequence - b.attempt_sequence);
251
+ const rows = classifiable.slice(-windowSize);
252
+ const head = rows[0] ?? this.order[0];
253
+ return {
254
+ archive_schema_version: head?.archive_schema_version ?? ARCHIVE_SCHEMA_VERSION,
255
+ classification_rule_version: head?.classification_rule_version ?? CLASSIFICATION_RULE_VERSION,
256
+ ranking_rule_version: head?.ranking_rule_version ?? RANKING_RULE_VERSION,
257
+ window_size: windowSize,
258
+ unclassifiable_excluded: this.order.length - classifiable.length,
259
+ rows,
260
+ };
261
+ }
262
+
263
+ /** The collapse verdict, read through the versioned query above. */
264
+ collapseVerdict(opts: DominanceOptions = {}): DominanceVerdict {
265
+ return dominanceVerdict(this.dominanceWindow(opts.windowSize ?? PATHOLOGY_WINDOW_SIZE), opts);
266
+ }
267
+
268
+ /**
269
+ * The versioned cell query. Cells are keyed on `WHERE x WHY` AND on the
270
+ * version/cohort quad — attempts recorded under a different classification
271
+ * rule describe a different vocabulary and must not be summed with these.
272
+ */
273
+ cells(): readonly PathologyCell[] {
274
+ const groups = new Map<string, PathologyAttempt[]>();
275
+ for (const a of this.order) {
276
+ const k = [
277
+ a.where,
278
+ a.why,
279
+ a.archive_schema_version,
280
+ a.classification_rule_version,
281
+ a.ranking_rule_version,
282
+ a.cohort_id,
283
+ a.cohort_version,
284
+ ].join(' ');
285
+ const bucket = groups.get(k);
286
+ if (bucket) bucket.push(a);
287
+ else groups.set(k, [a]);
288
+ }
289
+
290
+ const out: PathologyCell[] = [];
291
+ for (const bucket of groups.values()) {
292
+ const first = bucket.reduce((m, a) => (a.attempt_sequence < m.attempt_sequence ? a : m));
293
+ const last = bucket.reduce((m, a) => (a.attempt_sequence > m.attempt_sequence ? a : m));
294
+ let rep: PathologyAttempt | null = null;
295
+ for (const a of bucket) {
296
+ if (!eligibleAsRepresentative(a)) continue;
297
+ if (rep === null || replacesRetained(a, rep)) rep = a;
298
+ }
299
+ const head = bucket[0]!;
300
+ out.push({
301
+ where: head.where,
302
+ why: head.why,
303
+ archive_schema_version: head.archive_schema_version,
304
+ classification_rule_version: head.classification_rule_version,
305
+ ranking_rule_version: head.ranking_rule_version,
306
+ cohort_id: head.cohort_id,
307
+ cohort_version: head.cohort_version,
308
+ attempt_count: bucket.length,
309
+ classifiable_count: bucket.filter((a) => a.classification_status === 'classified').length,
310
+ unclassifiable_count: bucket.filter((a) => a.classification_status === 'unclassifiable').length,
311
+ first_observed_attempt_sequence: first.attempt_sequence,
312
+ first_observed_at: first.observed_at,
313
+ last_observed_attempt_sequence: last.attempt_sequence,
314
+ last_observed_at: last.observed_at,
315
+ retained_candidate_id: rep?.candidate_id ?? null,
316
+ retained_intervention_ref: rep?.intervention_ref ?? null,
317
+ retained_attempt_id: rep?.attempt_id ?? null,
318
+ retained_attempt_sequence: rep?.attempt_sequence ?? null,
319
+ retained_observed_at: rep?.observed_at ?? null,
320
+ retained_ranking_rule: RETAINED_RANKING_RULE,
321
+ retained_ranking_key: rep
322
+ ? `${rep.attempt_sequence}|${rep.candidate_id}|${rep.attempt_id}`
323
+ : null,
324
+ retained_validation_status: rep?.validation_status ?? null,
325
+ });
326
+ }
327
+ return out.sort((a, b) => (cellKey(a.where, a.why) < cellKey(b.where, b.why) ? -1 : 1));
328
+ }
329
+ }
330
+
331
+ export type DominanceVerdict =
332
+ | { readonly status: 'warming-up'; readonly classifiable_in_window: number; readonly floor: number }
333
+ | {
334
+ readonly status: 'ok' | 'collapsed';
335
+ readonly dominant_share: number;
336
+ readonly dominant_cell: string;
337
+ readonly classifiable_in_window: number;
338
+ readonly window_size: number;
339
+ readonly threshold: number;
340
+ };
341
+
342
+ /**
343
+ * The search-collapse guard, reading the archive through `cells()`-shaped data.
344
+ *
345
+ * Why a share and not the occupancy the frontier already gives: retaining one
346
+ * winner per cell erases whether attempts were distributed 50/50 or 99.9/0.1 —
347
+ * identical occupancy, radically different evidence of collapse. The denominator
348
+ * is therefore CLASSIFIABLE ATTEMPTS in the window, not distinct cells.
349
+ *
350
+ * `warming-up` is a real state and NOT a pass. A guard that returned "ok" on
351
+ * three observations would be reporting a confidence it does not have; the floor
352
+ * exists so an empty archive cannot look healthy.
353
+ */
354
+ export function dominanceVerdict(
355
+ query: PathologyWindowQuery,
356
+ opts: DominanceOptions = {},
357
+ ): DominanceVerdict {
358
+ const threshold = opts.threshold ?? PATHOLOGY_DOMINANCE_THRESHOLD;
359
+ const windowSize = query.window_size;
360
+ const floor = opts.minClassifiable ?? PATHOLOGY_MIN_CLASSIFIABLE_ATTEMPTS;
361
+
362
+ // The query has already excluded unclassifiable rows and applied the
363
+ // window; re-filtering here would let a caller pass raw storage and get a
364
+ // verdict, which is the coupling this signature exists to prevent.
365
+ const window = query.rows;
366
+
367
+ if (window.length < floor) {
368
+ return { status: 'warming-up', classifiable_in_window: window.length, floor };
369
+ }
370
+
371
+ const counts = new Map<string, number>();
372
+ for (const a of window) {
373
+ const k = cellKey(a.where, a.why);
374
+ counts.set(k, (counts.get(k) ?? 0) + 1);
375
+ }
376
+ let dominantCell = '';
377
+ let dominantCount = 0;
378
+ for (const [k, n] of [...counts.entries()].sort((x, y) => (x[0] < y[0] ? -1 : 1))) {
379
+ if (n > dominantCount) {
380
+ dominantCount = n;
381
+ dominantCell = k;
382
+ }
383
+ }
384
+ const share = dominantCount / window.length;
385
+ return {
386
+ status: share >= threshold ? 'collapsed' : 'ok',
387
+ dominant_share: share,
388
+ dominant_cell: dominantCell.replace(' ', ' x '),
389
+ classifiable_in_window: window.length,
390
+ window_size: windowSize,
391
+ threshold,
392
+ };
393
+ }