@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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +138 -376
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/commands/pr/create.md +16 -1
- package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
- package/dist/agent-src/scripts/stubs_due.ts +118 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
- package/dist/cli-delegate/cmd_conformance.js +39 -2
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1696 -812
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +297 -115
- package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
- package/docs/guidelines/code-clarity.md +18 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/src/agent-src/scripts/guarded_baseline.ts +22 -7
- package/src/agent-src/scripts/stubs_due.ts +118 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/src/config/assurance-capability-registry.json +1 -1
- package/src/config/estate-count-budget.json +20 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +211 -1
- package/src/config/gate-reachability-exemptions.json +1 -0
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/config/web-launch-readiness.json +4 -2
- package/src/scripts/_cli/_doctor_emit.ts +71 -0
- package/src/scripts/_cli/_doctor_strict.ts +163 -0
- package/src/scripts/_cli/cmd_doctor.ts +16 -37
- package/src/scripts/_dispatch.bash +11 -4
- package/src/scripts/_lib/activation_ladder.ts +47 -3
- package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
- package/src/scripts/_lib/adr_frontmatter.ts +26 -0
- package/src/scripts/_lib/branch_convergence.ts +186 -0
- package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
- package/src/scripts/_lib/carrier_status.ts +118 -0
- package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
- package/src/scripts/_lib/context_observation.ts +132 -0
- package/src/scripts/_lib/continuation_ladder.ts +188 -0
- package/src/scripts/_lib/corpus_manifest.ts +726 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
- package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
- package/src/scripts/_lib/evaluation_cascade.ts +452 -0
- package/src/scripts/_lib/evolution_roi.ts +562 -0
- package/src/scripts/_lib/experiment_binding.ts +95 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
- package/src/scripts/_lib/ignored_blocker.ts +8 -1
- package/src/scripts/_lib/lexical_shortlist.ts +215 -0
- package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
- package/src/scripts/_lib/outcome_envelope.ts +6 -1
- package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
- package/src/scripts/_lib/pathology_archive.ts +393 -0
- package/src/scripts/_lib/promotion_capability.ts +315 -0
- package/src/scripts/_lib/promotion_evidence.ts +414 -0
- package/src/scripts/_lib/promotion_review.ts +294 -0
- package/src/scripts/_lib/prompt_shape.ts +92 -0
- package/src/scripts/_lib/release_highlights.ts +79 -1
- package/src/scripts/_lib/release_material.ts +125 -2
- package/src/scripts/_lib/repeated_failure.ts +19 -2
- package/src/scripts/_lib/routing_corpus.ts +307 -0
- package/src/scripts/_lib/routing_index_input.ts +104 -0
- package/src/scripts/_lib/rule_injection.ts +22 -1
- package/src/scripts/_lib/runtime_journal.ts +11 -4
- package/src/scripts/_lib/semantic_noop.ts +167 -0
- package/src/scripts/activation_receipt.ts +258 -0
- package/src/scripts/ai_council/cross_exam.ts +128 -0
- package/src/scripts/ai_council/explain_route.ts +157 -0
- package/src/scripts/ai_council/information_gain.ts +293 -0
- package/src/scripts/ai_council/leakage_corpus.ts +385 -0
- package/src/scripts/ai_council/leakage_patterns.ts +453 -0
- package/src/scripts/ai_council/minority_retention.ts +171 -0
- package/src/scripts/ai_council/recouncil_savings.ts +299 -0
- package/src/scripts/ai_council/replay_route.ts +223 -0
- package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
- package/src/scripts/ai_council/routing_training_row.ts +197 -0
- package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
- package/src/scripts/check_adr_frontmatter.ts +4 -9
- package/src/scripts/check_agent_artifact_location.ts +337 -0
- package/src/scripts/check_artefact_count_messaging.ts +74 -8
- package/src/scripts/check_enforcement_coverage.ts +40 -1
- package/src/scripts/check_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +113 -14
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/check_web_launch_readiness.ts +310 -20
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +234 -9
- package/src/scripts/generate_capability_matrix.ts +16 -9
- package/src/scripts/grade_target_readiness.ts +221 -2
- package/src/scripts/hook_effect_doctor.ts +47 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +28 -6
- package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/run_continuation_hook.ts +78 -88
- package/src/scripts/hooks/session_eol_hook.ts +12 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
- package/src/scripts/install-hooks.sh +18 -0
- package/src/scripts/language_mirror_hook.ts +14 -2
- package/src/scripts/lint_carrier_integrity.ts +820 -0
- package/src/scripts/lint_code_comments.ts +748 -0
- package/src/scripts/lint_plan_risk_register.ts +39 -19
- package/src/scripts/lint_promotion_paths.ts +779 -0
- package/src/scripts/llm_propose.ts +256 -0
- package/src/scripts/measure_delivery_sets.ts +361 -0
- package/src/scripts/measure_routing_signal.ts +307 -0
- package/src/scripts/model_rule_injection.ts +19 -3
- package/src/scripts/probe_council_retention.ts +495 -0
- package/src/scripts/release.ts +21 -37
- package/src/scripts/release_drill.ts +199 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +142 -0
- package/src/scripts/roadmap_context.ts +15 -0
- package/src/scripts/skill_scout.ts +633 -0
- package/src/scripts/sync_pr_branch.ts +521 -42
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The missing seam: two candidate records over one observation, and one signed
|
|
3
|
+
* delta per trial.
|
|
4
|
+
*
|
|
5
|
+
* A recorded search over this tree established that nothing read two
|
|
6
|
+
* {@link CandidateRecord}s and emitted a delta, a {@link PairedVerdict} or a
|
|
7
|
+
* {@link MetricVector}. The only live `decidePairedVerdict` caller runs over
|
|
8
|
+
* benchmark task pairs, the only paired-row construction is a deserialiser, and
|
|
9
|
+
* the evaluation cascade takes its vector from the caller. So the comparison a
|
|
10
|
+
* two-arm proposer experiment needs had no producer at all: the verdict
|
|
11
|
+
* function existed and nothing could feed it from candidates.
|
|
12
|
+
*
|
|
13
|
+
* This module is that producer, and it is deliberately the only thing it is.
|
|
14
|
+
*
|
|
15
|
+
* Everything about the SHAPE of the comparison is derived from constants
|
|
16
|
+
* committed before either arm existed: the sign convention from
|
|
17
|
+
* {@link PairedInput}'s own contract, the direction handling from
|
|
18
|
+
* {@link MetricDirection}, the tie epsilon from the value the A/B report
|
|
19
|
+
* already uses, the aggregation and the significance bar from
|
|
20
|
+
* {@link decidePairedVerdict}. What is NOT here is the outcome scalar itself.
|
|
21
|
+
* {@link TrialOutcome} is supplied by a caller, because choosing what a trial
|
|
22
|
+
* MEASURES is the one decision in this experiment that could favour an arm, and
|
|
23
|
+
* a module that invented one would be answering it in the wrong place. The
|
|
24
|
+
* artifact-count delta is supplied on the same terms and for a sharper reason:
|
|
25
|
+
* the pairing makes every pair-derived path count differ by exactly 0, so a
|
|
26
|
+
* derivation here would report a constant and call it a measurement.
|
|
27
|
+
*
|
|
28
|
+
* The honest consequence, stated rather than left to be discovered: this
|
|
29
|
+
* producer has no live population today. No shipped evaluator emits a
|
|
30
|
+
* {@link TrialOutcome} for a candidate over the frozen corpus, so every test
|
|
31
|
+
* here supplies its own. A producer with no population is exactly as honest as
|
|
32
|
+
* a guard with no caller, and the same disclosure applies.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { CandidateRecord } from './candidate_record.js';
|
|
36
|
+
import {
|
|
37
|
+
ARTIFACT_COUNT_METRIC,
|
|
38
|
+
buildVector,
|
|
39
|
+
type MetricDirection,
|
|
40
|
+
type MetricRow,
|
|
41
|
+
type MetricVector,
|
|
42
|
+
} from './evaluation_vector.js';
|
|
43
|
+
import { decidePairedVerdict, type PairedVerdict } from './paired_verdict.js';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Deltas within this of zero are ties.
|
|
47
|
+
*
|
|
48
|
+
* NOT chosen here: it is the value the A/B report's own direction counts
|
|
49
|
+
* already use, and its reason is recorded there, that two definitions of a tie
|
|
50
|
+
* in one report is how the numbers start disagreeing. Restating that value
|
|
51
|
+
* rather than picking a fresh one keeps a candidate comparison and a bench
|
|
52
|
+
* comparison answering to the same notion of no difference.
|
|
53
|
+
*/
|
|
54
|
+
export const TIE_EPSILON = 1e-9;
|
|
55
|
+
|
|
56
|
+
export class PairingError extends Error {
|
|
57
|
+
constructor(message: string) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = 'PairingError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The pairing key, derived from the record alone.
|
|
65
|
+
*
|
|
66
|
+
* A record carries no observation field, so the key is reconstructed from what
|
|
67
|
+
* it does carry: the mutation dimension and the sorted mutation paths. That is
|
|
68
|
+
* exactly the observation identity for this experiment, because each fixed
|
|
69
|
+
* recipe owns one dimension and each observation names one subject. Keying on
|
|
70
|
+
* the candidate id would pair nothing, since an id hashes the mutated bytes, so
|
|
71
|
+
* two arms that produced different text have different ids by construction,
|
|
72
|
+
* which is the whole reason there is something to compare.
|
|
73
|
+
*/
|
|
74
|
+
export function pairingKey(record: CandidateRecord): string {
|
|
75
|
+
const paths = record.mutations.map((m) => m.path).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
76
|
+
return `${record.dimension} ${paths.join(' ')}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CandidatePair {
|
|
80
|
+
readonly key: string;
|
|
81
|
+
readonly control: CandidateRecord;
|
|
82
|
+
readonly treatment: CandidateRecord;
|
|
83
|
+
/**
|
|
84
|
+
* The arms produced byte-identical mutations.
|
|
85
|
+
*
|
|
86
|
+
* Not an error and not silently dropped. It means every trial on this pair
|
|
87
|
+
* is a structural tie, so the pair contributes to the trial count and to no
|
|
88
|
+
* discordant count, and a reader who sees `underpowered` without this flag
|
|
89
|
+
* would look for the cause in the measurement rather than in the arms.
|
|
90
|
+
*/
|
|
91
|
+
readonly identical_arms: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function serialiseMutations(r: CandidateRecord): string {
|
|
95
|
+
return [...r.mutations]
|
|
96
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
97
|
+
.map((m) => `${m.path} ${m.content}`)
|
|
98
|
+
.join('');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Pair two arms' record sets, refusing every shape where half a pair exists.
|
|
103
|
+
*
|
|
104
|
+
* An unmatched record on either side is refused rather than dropped. Dropping
|
|
105
|
+
* it would make the trial count depend on which observations each arm happened
|
|
106
|
+
* to satisfy, and a comparison whose denominator is chosen by one of the arms
|
|
107
|
+
* is not a paired comparison.
|
|
108
|
+
*/
|
|
109
|
+
export function pairCandidates(
|
|
110
|
+
control: readonly CandidateRecord[],
|
|
111
|
+
treatment: readonly CandidateRecord[],
|
|
112
|
+
): CandidatePair[] {
|
|
113
|
+
const byKey = new Map<string, CandidateRecord>();
|
|
114
|
+
for (const r of treatment) {
|
|
115
|
+
const k = pairingKey(r);
|
|
116
|
+
if (byKey.has(k)) {
|
|
117
|
+
throw new PairingError(
|
|
118
|
+
`treatment arm emitted two records for '${k}': one observation is one candidate ` +
|
|
119
|
+
'per arm, and a second would make the pairing pick a winner before any trial ran',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
byKey.set(k, r);
|
|
123
|
+
}
|
|
124
|
+
const out: CandidatePair[] = [];
|
|
125
|
+
const seen = new Set<string>();
|
|
126
|
+
for (const c of control) {
|
|
127
|
+
const k = pairingKey(c);
|
|
128
|
+
if (seen.has(k)) {
|
|
129
|
+
throw new PairingError(`control arm emitted two records for '${k}'`);
|
|
130
|
+
}
|
|
131
|
+
seen.add(k);
|
|
132
|
+
const t = byKey.get(k);
|
|
133
|
+
if (t === undefined) {
|
|
134
|
+
throw new PairingError(
|
|
135
|
+
`no treatment record for '${k}': half a pair is no result, not a partial one`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
out.push({
|
|
139
|
+
key: k,
|
|
140
|
+
control: c,
|
|
141
|
+
treatment: t,
|
|
142
|
+
identical_arms: serialiseMutations(c) === serialiseMutations(t),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
for (const k of byKey.keys()) {
|
|
146
|
+
if (!seen.has(k)) {
|
|
147
|
+
throw new PairingError(
|
|
148
|
+
`no control record for '${k}': the treatment arm ran an observation the control did not`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* One measured trial: the same scalar, on both arms, under the same conditions.
|
|
157
|
+
*
|
|
158
|
+
* Both values are required. An absent arm is refused rather than imputed,
|
|
159
|
+
* because imputing it would put a number the run did not observe into a
|
|
160
|
+
* denominator that decides a verdict.
|
|
161
|
+
*/
|
|
162
|
+
export interface TrialOutcome {
|
|
163
|
+
/**
|
|
164
|
+
* Unique across the WHOLE comparison, not merely within its pair.
|
|
165
|
+
*
|
|
166
|
+
* Stated rather than left implicit, because the two readings differ in what
|
|
167
|
+
* they refuse and the ambiguity was the defect. `pairedDeltas` refuses a
|
|
168
|
+
* repeat within one call and cannot see across pairs; {@link compareArms}
|
|
169
|
+
* pools every pair's deltas into ONE sample, and the denominator of that
|
|
170
|
+
* sample is what the sign test adjudicates. A caller that derives the
|
|
171
|
+
* outcomes map wrongly — the realistic shape being the same measurement
|
|
172
|
+
* array reached under two keys — then contributes one set of deltas twice.
|
|
173
|
+
* Under a pair-scoped reading that is undetectable; under this one it is a
|
|
174
|
+
* refusal, and the cost is that a caller names its trials per pair.
|
|
175
|
+
*/
|
|
176
|
+
readonly trial_id: string;
|
|
177
|
+
readonly control: number;
|
|
178
|
+
readonly treatment: number;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The signed deltas for one pair. Positive favours the TREATMENT, always.
|
|
183
|
+
*
|
|
184
|
+
* The convention is not this module's to set: the verdict's own input contract
|
|
185
|
+
* states one signed delta per trial with positive favouring the treatment, so
|
|
186
|
+
* the direction handling here is a translation of {@link MetricDirection} into
|
|
187
|
+
* that fixed convention rather than a second convention beside it. For a
|
|
188
|
+
* lower-better metric the subtraction is reversed, so a caller never has to
|
|
189
|
+
* remember which endpoint it is looking at.
|
|
190
|
+
*/
|
|
191
|
+
export function pairedDeltas(
|
|
192
|
+
outcomes: readonly TrialOutcome[],
|
|
193
|
+
direction: MetricDirection,
|
|
194
|
+
): number[] {
|
|
195
|
+
if (outcomes.length === 0) {
|
|
196
|
+
throw new PairingError(
|
|
197
|
+
'no trials: a verdict over zero trials is a fabricated result, not an empty one',
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const ids = new Set<string>();
|
|
201
|
+
for (const o of outcomes) {
|
|
202
|
+
if (ids.has(o.trial_id)) {
|
|
203
|
+
throw new PairingError(
|
|
204
|
+
`duplicate trial id '${o.trial_id}': one trial counted twice inflates the discordant count`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
ids.add(o.trial_id);
|
|
208
|
+
if (!Number.isFinite(o.control) || !Number.isFinite(o.treatment)) {
|
|
209
|
+
throw new PairingError(
|
|
210
|
+
`trial '${o.trial_id}' carries a non-finite outcome: an unmeasured trial is absent, never zero`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return outcomes.map((o) =>
|
|
215
|
+
direction === 'higher-better' ? o.treatment - o.control : o.control - o.treatment,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface PairComparison {
|
|
220
|
+
readonly pairs: readonly CandidatePair[];
|
|
221
|
+
readonly trials: number;
|
|
222
|
+
readonly verdict: PairedVerdict;
|
|
223
|
+
/** Pairs whose arms produced identical bytes, so every one of their trials tied. */
|
|
224
|
+
readonly identical_pairs: readonly string[];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface ComparisonInput {
|
|
228
|
+
readonly control: readonly CandidateRecord[];
|
|
229
|
+
readonly treatment: readonly CandidateRecord[];
|
|
230
|
+
/** Trials keyed by pairing key. Every pair must carry at least one. */
|
|
231
|
+
readonly outcomes: Readonly<Record<string, readonly TrialOutcome[]>>;
|
|
232
|
+
readonly direction: MetricDirection;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The comparison, pooled across pairs.
|
|
237
|
+
*
|
|
238
|
+
* Pooling is what raises the experiment off the discordant floor: with one
|
|
239
|
+
* delta per pair the trial count equals the corpus size, which sits exactly on
|
|
240
|
+
* the derived floor, so a single dissent makes a pass arithmetically
|
|
241
|
+
* unreachable before the run starts. Pooling per-trial deltas across pairs
|
|
242
|
+
* makes the count the number of TRIALS rather than the number of pairs.
|
|
243
|
+
*
|
|
244
|
+
* The assumption that buys it is stated rather than assumed away: the exact
|
|
245
|
+
* sign test treats each delta as an independent draw, and trials on the same
|
|
246
|
+
* pair share a subject. A caller that cannot defend that independence should
|
|
247
|
+
* pass one aggregate delta per pair and accept the smaller sample, which this
|
|
248
|
+
* function supports by simply being given fewer trials.
|
|
249
|
+
*/
|
|
250
|
+
export function compareArms(input: ComparisonInput): PairComparison {
|
|
251
|
+
const pairs = pairCandidates(input.control, input.treatment);
|
|
252
|
+
const keys = new Set(pairs.map((p) => p.key));
|
|
253
|
+
for (const k of Object.keys(input.outcomes)) {
|
|
254
|
+
if (!keys.has(k)) {
|
|
255
|
+
throw new PairingError(
|
|
256
|
+
`outcomes carry key '${k}', which matches no pair: the key is a formatted string ` +
|
|
257
|
+
'the caller derives rather than receives, so an unmatched one is a mis-derived ' +
|
|
258
|
+
'or stale measurement set, and pooling only the keys that happened to match ' +
|
|
259
|
+
'is the same silently smaller denominator the opposite refusal names',
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const deltas: number[] = [];
|
|
264
|
+
const seenTrials = new Set<string>();
|
|
265
|
+
for (const p of pairs) {
|
|
266
|
+
const trials = input.outcomes[p.key];
|
|
267
|
+
if (trials === undefined || trials.length === 0) {
|
|
268
|
+
throw new PairingError(
|
|
269
|
+
`pair '${p.key}' has no trials: a pair present in the arms and absent from the ` +
|
|
270
|
+
'measurements silently shrinks the denominator',
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
for (const t of trials) {
|
|
274
|
+
if (seenTrials.has(t.trial_id)) {
|
|
275
|
+
throw new PairingError(
|
|
276
|
+
`trial id '${t.trial_id}' appears on more than one pair: the refusal in ` +
|
|
277
|
+
'pairedDeltas is scoped to one pair and cannot see a reuse across pairs, ' +
|
|
278
|
+
'and one trial counted twice inflates the discordant count either way',
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
seenTrials.add(t.trial_id);
|
|
282
|
+
}
|
|
283
|
+
deltas.push(...pairedDeltas(trials, input.direction));
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
pairs,
|
|
287
|
+
trials: deltas.length,
|
|
288
|
+
verdict: decidePairedVerdict({ deltas, tieEpsilon: TIE_EPSILON }),
|
|
289
|
+
identical_pairs: pairs.filter((p) => p.identical_arms).map((p) => p.key),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The comparison as a metric vector the evaluation cascade can consume.
|
|
295
|
+
*
|
|
296
|
+
* The artifact-count row is not optional: {@link buildVector} refuses a vector
|
|
297
|
+
* without it, so a candidate cannot reach the promotion verdict without its
|
|
298
|
+
* artifact delta having been measured. The delta is SUPPLIED here rather than
|
|
299
|
+
* derived, and the reason is the same one that keeps {@link TrialOutcome} out
|
|
300
|
+
* of this module.
|
|
301
|
+
*
|
|
302
|
+
* Why this module cannot derive it. {@link pairingKey} folds the sorted mutation-path list into the key and
|
|
303
|
+
* {@link pairCandidates} forms a pair only on an exact key match, so control
|
|
304
|
+
* and treatment of every pair carry identical path multisets by construction.
|
|
305
|
+
* Any count taken over those paths therefore differs by exactly 0 for every
|
|
306
|
+
* pair and every candidate — not usually, but always. A row filled that way
|
|
307
|
+
* satisfies the builder's SHAPE check while making the ceiling branch in
|
|
308
|
+
* `promotionVerdict` unreachable, which defeats the invariant the shape
|
|
309
|
+
* check exists to carry: the vector would be refused for omitting the row and
|
|
310
|
+
* accepted for carrying a constant.
|
|
311
|
+
*
|
|
312
|
+
* The quantity the gate actually names is baseline-relative growth — how many
|
|
313
|
+
* artifacts the candidate ADDS to the tree it is a variant of. That is a fact
|
|
314
|
+
* about the candidate against an unmutated baseline, and this module sees
|
|
315
|
+
* neither the baseline nor the filesystem. So the caller that knows which
|
|
316
|
+
* mutated paths are new files passes the count, and a caller that has not
|
|
317
|
+
* measured it has nothing to pass.
|
|
318
|
+
*
|
|
319
|
+
* @param artifactDelta signed count of artifacts the candidate adds against
|
|
320
|
+
* its baseline. Must be a finite integer; a fractional or non-finite value is
|
|
321
|
+
* refused rather than rounded, because a rounded ceiling comparison is a
|
|
322
|
+
* verdict on a number nobody measured.
|
|
323
|
+
* @throws {PairingError} when `artifactDelta` is not a finite integer.
|
|
324
|
+
*/
|
|
325
|
+
export function comparisonVector(
|
|
326
|
+
candidate_id: string,
|
|
327
|
+
metric: string,
|
|
328
|
+
direction: MetricDirection,
|
|
329
|
+
comparison: PairComparison,
|
|
330
|
+
artifactDelta: number,
|
|
331
|
+
): MetricVector {
|
|
332
|
+
if (!Number.isInteger(artifactDelta)) {
|
|
333
|
+
throw new PairingError(
|
|
334
|
+
`artifact delta ${String(artifactDelta)} is not a finite integer: the artifact-count ` +
|
|
335
|
+
'row is a count of artifacts, and a gate that compared a fraction against its ' +
|
|
336
|
+
'ceiling would be adjudicating a quantity the run never observed',
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const rows: MetricRow[] = [
|
|
340
|
+
{ kind: 'paired', metric, direction, verdict: comparison.verdict },
|
|
341
|
+
{
|
|
342
|
+
kind: 'counted',
|
|
343
|
+
metric: ARTIFACT_COUNT_METRIC,
|
|
344
|
+
direction: 'lower-better',
|
|
345
|
+
delta: artifactDelta,
|
|
346
|
+
},
|
|
347
|
+
];
|
|
348
|
+
return buildVector(candidate_id, rows);
|
|
349
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `status: carrier` predicate, and the reasoning five readers share.
|
|
3
|
+
*
|
|
4
|
+
* A carrier is a roadmap whose job is to hold obligations deferred out of a
|
|
5
|
+
* parent that is already archived. Every item in it has an unmet resumption
|
|
6
|
+
* trigger, and it has no `## Phase` headings, so it is skipped wherever a draft
|
|
7
|
+
* is: the dashboard, `/roadmap:process-*`, the trackability check and the plan
|
|
8
|
+
* risk register. It differs from a draft in what it costs to remove — a draft
|
|
9
|
+
* may be deleted freely, a carrier may not.
|
|
10
|
+
*
|
|
11
|
+
* This module exists because the predicate had two implementations within one
|
|
12
|
+
* change and five callers within one week. Two implementations of one frontmatter
|
|
13
|
+
* test drift, and the one nobody runs locally drifts first — the same
|
|
14
|
+
* memory-twin failure this repository has already recorded against a fixture
|
|
15
|
+
* corpus. One test, one place, and the reasoning above lives beside it rather
|
|
16
|
+
* than being restated in each caller.
|
|
17
|
+
*
|
|
18
|
+
* The read is frontmatter-scoped on purpose. A fenced `status: carrier` inside a
|
|
19
|
+
* roadmap that documents the syntax is not a declaration, and a body-wide match
|
|
20
|
+
* turned exactly that into a hard failure before a review caught it.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** The frontmatter values that mean "not schedulable work, and not disposable". */
|
|
24
|
+
export const CARRIER_VALUES: ReadonlySet<string> = new Set(['carrier']);
|
|
25
|
+
|
|
26
|
+
/** The leading `---\n…\n---` block, or null when the text carries none. */
|
|
27
|
+
export function frontmatterBlock(text: string): string | null {
|
|
28
|
+
const m = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
29
|
+
return m === null ? null : (m[1] ?? '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** True when `text`'s FRONTMATTER declares one of {@link CARRIER_VALUES}. */
|
|
33
|
+
export function declaresCarrier(text: string | null | undefined): boolean {
|
|
34
|
+
if (text === null || text === undefined) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const fm = frontmatterBlock(text);
|
|
38
|
+
if (fm === null) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const m = /^status:[ \t]*([A-Za-z-]+)[ \t]*$/m.exec(fm);
|
|
42
|
+
return m !== null && CARRIER_VALUES.has((m[1] ?? '').toLowerCase());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Count the top-level roadmaps in `roadmapRoot` that declare themselves carriers.
|
|
47
|
+
*
|
|
48
|
+
* Lives here rather than in the estate gate because it is the same fact as the
|
|
49
|
+
* predicate above, read over a directory instead of a string, and the gate that
|
|
50
|
+
* needs it is already at its source-size ceiling.
|
|
51
|
+
*
|
|
52
|
+
* @param isCandidate the caller's own roadmap-filename test, so this module does
|
|
53
|
+
* not acquire a second opinion about what counts as a roadmap.
|
|
54
|
+
*/
|
|
55
|
+
export function countDeclaredCarriers(
|
|
56
|
+
roadmapRoot: string,
|
|
57
|
+
isCandidate: (name: string) => boolean,
|
|
58
|
+
fsLike: {
|
|
59
|
+
readdirSync: (p: string) => string[];
|
|
60
|
+
statSync: (p: string) => { isFile: () => boolean };
|
|
61
|
+
readFileSync: (p: string, enc: 'utf-8') => string;
|
|
62
|
+
},
|
|
63
|
+
join: (...parts: string[]) => string,
|
|
64
|
+
): number {
|
|
65
|
+
let names: string[];
|
|
66
|
+
try {
|
|
67
|
+
names = fsLike.readdirSync(roadmapRoot);
|
|
68
|
+
} catch {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
let n = 0;
|
|
72
|
+
for (const name of names) {
|
|
73
|
+
if (!name.endsWith('.md') || !isCandidate(name)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const abs = join(roadmapRoot, name);
|
|
77
|
+
try {
|
|
78
|
+
if (!fsLike.statSync(abs).isFile()) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (declaresCarrier(fsLike.readFileSync(abs, 'utf-8'))) {
|
|
82
|
+
n += 1;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return n;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Fixture design — why two of the estate self-test cases are shaped as they are.
|
|
93
|
+
*
|
|
94
|
+
* The reasoning behind two of `check_estate_count`'s self-test cases, kept here
|
|
95
|
+
* because the gate that holds them is at its source-size ceiling and because
|
|
96
|
+
* both facts are about the carrier status rather than about that gate.
|
|
97
|
+
*
|
|
98
|
+
* **The deletion case's addition is a DRAFT, and that is the whole design.**
|
|
99
|
+
* A carrier holds obligations lifted out of an already-archived parent, so
|
|
100
|
+
* paying an offset for removing one rewards the loss: the deletion buys nothing
|
|
101
|
+
* and the addition stays unpaid. With an *ordinary* addition the growth half and
|
|
102
|
+
* the unpaid half coincide — growth iff `n > a`, unpaid iff `n > a` — so the
|
|
103
|
+
* case rejected on two independent grounds and stayed red with the carrier
|
|
104
|
+
* branch reverted, asserting nothing about the feature it named. A draft is
|
|
105
|
+
* invisible to `collect()`, so the count half cannot move and only the offset
|
|
106
|
+
* scoring decides.
|
|
107
|
+
*
|
|
108
|
+
* **The count case asserts the flip BACK, not the flip TO.** A reclassification
|
|
109
|
+
* is not a disposal in either direction, but the flip *to* carrier cannot be
|
|
110
|
+
* asserted by exit code: it lowers the count, a count below the floor is a
|
|
111
|
+
* drawdown, and a drawdown is free whichever way carriers are counted. Its
|
|
112
|
+
* consequence is assertable — with carriers uncounted the flip lowers the floor,
|
|
113
|
+
* so flipping the same file back reads as growth and must be paid for. That is
|
|
114
|
+
* the fixture's state, with the carrier already in the base tree, and it rejects
|
|
115
|
+
* with the carrier term removed from the count. The neutrality of the forward
|
|
116
|
+
* flip is asserted directly in `check_estate_count.test.ts`, where a count is
|
|
117
|
+
* readable.
|
|
118
|
+
*/
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The twelve-stage evaluation cascade, DERIVED rather than enumerated by hand.
|
|
3
|
+
*
|
|
4
|
+
* `road-to-governed-evidence-production` step 1.2. E9 (2026-08-30) decided the
|
|
5
|
+
* ARITY — twelve — and enumerated the stages nowhere. The 2026-08-31 council
|
|
6
|
+
* round that was asked for the enumeration returned `REVISE`, and the reason it
|
|
7
|
+
* gave is the reason this module exists rather than a constant list: running the
|
|
8
|
+
* same seat twice produced two materially different twelves — different names,
|
|
9
|
+
* different order, a different placement of the statistical stage.
|
|
10
|
+
*
|
|
11
|
+
* **So the enumeration is not proposed. It is computed.** Two committed arrays
|
|
12
|
+
* and one stated ordering rule determine it, and re-running the computation over
|
|
13
|
+
* the same tree cannot yield a different answer. Nobody's judgement is in the
|
|
14
|
+
* output, which is what "settled" has to mean after two judgements disagreed.
|
|
15
|
+
*
|
|
16
|
+
* ## The ordering rule
|
|
17
|
+
*
|
|
18
|
+
* Stages are ordered by the EVIDENCE each one needs — the `CascadeInput` field
|
|
19
|
+
* it reads — and within a class by source order. The classes, cheapest evidence
|
|
20
|
+
* first:
|
|
21
|
+
*
|
|
22
|
+
* | Rank | Class | Evidence it needs | `CascadeInput` field |
|
|
23
|
+
* |---|---|---|---|
|
|
24
|
+
* | 0 | `record` | the candidate record alone | `raw` |
|
|
25
|
+
* | 1 | `plan` | the run plan and the budget ceiling | `plan`, `budget` |
|
|
26
|
+
* | 2 | `peers` | the sibling candidates of this run | `peers` |
|
|
27
|
+
* | 3 | `receipt` | an activation receipt | `receipt` |
|
|
28
|
+
* | 4 | `measurement` | measured trials | `rows`, `vector` |
|
|
29
|
+
*
|
|
30
|
+
* The rule is not a preference. It is the cascade's own cheapest-first,
|
|
31
|
+
* abort-on-first-failure discipline restated over evidence rather than over
|
|
32
|
+
* cost: a stage may not run before the evidence it depends on could exist, and a
|
|
33
|
+
* candidate that fails a cheap-evidence stage must never consume the expensive
|
|
34
|
+
* evidence. `docs/contracts/activation-receipt-trust-boundary.md` EC-2 is the
|
|
35
|
+
* receipt row of exactly this table.
|
|
36
|
+
*
|
|
37
|
+
* ## What is NOT decided here
|
|
38
|
+
*
|
|
39
|
+
* The rung SEMANTICS — what `delivered` means, what observes it — belong to
|
|
40
|
+
* `_lib/activation_ladder.ts`, and the `REVISE` verdict's condition on them was
|
|
41
|
+
* discharged by the trust-boundary and evidence-cost contract, not by this file.
|
|
42
|
+
* This module settles the enumeration and its order, and nothing else.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { CASCADE_STAGES, type StageId } from './evaluation_cascade.js';
|
|
46
|
+
import { RECEIPT_STAGES } from './activation_ladder.js';
|
|
47
|
+
|
|
48
|
+
/** What evidence a stage needs. The ordering key. */
|
|
49
|
+
export const EVIDENCE_CLASSES = ['record', 'plan', 'peers', 'receipt', 'measurement'] as const;
|
|
50
|
+
export type EvidenceClass = (typeof EVIDENCE_CLASSES)[number];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The evidence class of each DETERMINISTIC prefix stage.
|
|
54
|
+
*
|
|
55
|
+
* Written out because it is a fact about `runCascade`'s body — which
|
|
56
|
+
* `CascadeInput` field each stage reads — and a fact about code cannot be
|
|
57
|
+
* derived from a list of names. It is not unchecked: the companion test asserts
|
|
58
|
+
* this order against the order in which `runCascade` first touches each input
|
|
59
|
+
* field, so a stage that starts reading a different field reds here rather than
|
|
60
|
+
* quietly reordering the enumeration.
|
|
61
|
+
*/
|
|
62
|
+
export const PREFIX_EVIDENCE_CLASS: Readonly<Record<string, EvidenceClass>> = {
|
|
63
|
+
'schema-validity': 'record',
|
|
64
|
+
'path-ownership': 'record',
|
|
65
|
+
'holdout-disclosure': 'record',
|
|
66
|
+
budget: 'plan',
|
|
67
|
+
'near-duplicate': 'peers',
|
|
68
|
+
'metric-verdict': 'measurement',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** The evidence class of any stage. Receipt stages are uniformly `receipt`. */
|
|
72
|
+
export function evidenceClass(stage: StageId): EvidenceClass {
|
|
73
|
+
if ((RECEIPT_STAGES as readonly string[]).includes(stage)) return 'receipt';
|
|
74
|
+
const c = PREFIX_EVIDENCE_CLASS[stage];
|
|
75
|
+
if (c === undefined) throw new Error(`no evidence class declared for stage '${stage}'`);
|
|
76
|
+
return c;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The committed twelve, in order.
|
|
81
|
+
*
|
|
82
|
+
* Route A of step 1.2's reproduction: the two committed arrays, sorted by
|
|
83
|
+
* evidence class, stable within a class. `Array.prototype.sort` is specified as
|
|
84
|
+
* stable since ES2019, which is what makes "within a class, source order" a
|
|
85
|
+
* property of the language rather than of an implementation.
|
|
86
|
+
*/
|
|
87
|
+
export const TWELVE_STAGES: readonly StageId[] = [...CASCADE_STAGES, ...RECEIPT_STAGES].sort(
|
|
88
|
+
(a, b) => EVIDENCE_CLASSES.indexOf(evidenceClass(a)) - EVIDENCE_CLASSES.indexOf(evidenceClass(b)),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
/** The arity E9 decided. Stated so a drift from twelve is a named failure. */
|
|
92
|
+
export const DECIDED_ARITY = 12;
|