@event4u/agent-config 14.13.0 → 14.14.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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +116 -387
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- 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-delegate/cmd_conformance.js +38 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +4 -4
- 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 +1438 -711
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +112 -17
- 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/estate-count-budget.json +20 -1
- package/src/config/gate-coverage.yml +150 -0
- package/src/config/gate-violation-baselines.json +8 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/scripts/_dispatch.bash +0 -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/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/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/harness_evolution_guards.ts +10 -0
- 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/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 +70 -1
- package/src/scripts/_lib/release_material.ts +125 -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/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_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +96 -13
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +181 -8
- 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/turn_end_gate_hook.ts +9 -1
- 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 +643 -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 +143 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +84 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* `llm_propose` — the metered arm's entry point, dry by default.
|
|
4
|
+
*
|
|
5
|
+
* `road-to-governed-evidence-production` step 2.1's executable surface. The
|
|
6
|
+
* deterministic twin is `evolution_lab propose`; this writes records with the
|
|
7
|
+
* SAME serialiser and the SAME filename function, so its `--out DIR` is drop-in
|
|
8
|
+
* comparable with that verb's.
|
|
9
|
+
*
|
|
10
|
+
* ## Why a separate CLI and not a verb on `evolution_lab`
|
|
11
|
+
*
|
|
12
|
+
* `verbPropose` (`src/scripts/evolution_lab.ts:645`) is synchronous, and so is
|
|
13
|
+
* `main`'s whole dispatch. A metered arm is asynchronous by nature, so adding it
|
|
14
|
+
* as a verb means making a shared entry point async — a refactor of every verb
|
|
15
|
+
* to ship one. A separate file leaves the deterministic path byte-identical,
|
|
16
|
+
* which also keeps it a clean control arm.
|
|
17
|
+
*
|
|
18
|
+
* ## Dry by default, and the default is the point
|
|
19
|
+
*
|
|
20
|
+
* With no `--confirm` this sends nothing: it prints the ladder plan, the pinned
|
|
21
|
+
* model per tier, the exact request body that WOULD go out for the first
|
|
22
|
+
* observation, and a cost estimate. `--confirm` is the only path that spends.
|
|
23
|
+
* The shape mirrors `src/scripts/rdp_gate_classify.ts`, which is this
|
|
24
|
+
* repository's own precedent for a metered script.
|
|
25
|
+
*
|
|
26
|
+
* **No live call has been made through this file.** The commit that adds it
|
|
27
|
+
* exercised the dry path only; the park's un-park procedure requires an
|
|
28
|
+
* independent session to freeze the protocol before any capture, and freezing
|
|
29
|
+
* plus capturing in one session is not a discharge.
|
|
30
|
+
*
|
|
31
|
+
* ## The guards it inherits rather than reimplements
|
|
32
|
+
*
|
|
33
|
+
* GUARD 0.5 (`assertWithinBudget`) and GUARD 0.4 (`discloseObservations`, the
|
|
34
|
+
* holdout-disclosure abort) run here exactly as they do on the deterministic
|
|
35
|
+
* verb, and against the same observations document. An arm that skipped them
|
|
36
|
+
* would not be the same experiment.
|
|
37
|
+
*/
|
|
38
|
+
import * as fs from 'node:fs';
|
|
39
|
+
import * as path from 'node:path';
|
|
40
|
+
import { fileURLToPath } from 'node:url';
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
discloseObservations,
|
|
44
|
+
loadRunBudget,
|
|
45
|
+
parseObservationDocument,
|
|
46
|
+
} from './evolution_lab.js';
|
|
47
|
+
import {
|
|
48
|
+
candidateRecordFilename,
|
|
49
|
+
parseObservations,
|
|
50
|
+
serialiseCandidateRecord,
|
|
51
|
+
type DefectObservation,
|
|
52
|
+
} from './_lib/candidate_proposer.js';
|
|
53
|
+
import {
|
|
54
|
+
SYSTEM_PROMPT,
|
|
55
|
+
buildPrompt,
|
|
56
|
+
plannedAttempts,
|
|
57
|
+
proposeCandidatesWithModel,
|
|
58
|
+
} from './_lib/llm_candidate_proposer.js';
|
|
59
|
+
import {
|
|
60
|
+
TIER_MODEL,
|
|
61
|
+
anthropicGenerator,
|
|
62
|
+
describeRequest,
|
|
63
|
+
estimateTokens,
|
|
64
|
+
} from './_lib/llm_proposer_transport.js';
|
|
65
|
+
import { assertWithinBudget, type RunPlan } from './_lib/harness_evolution_guards.js';
|
|
66
|
+
import type { DisclosureRecord } from './_lib/harness_evolution_guards.js';
|
|
67
|
+
|
|
68
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
69
|
+
/**
|
|
70
|
+
* Resolved here rather than imported from `evolution_lab.ts`, whose `REPO_ROOT`
|
|
71
|
+
* is module-private. Exporting it would widen a shared module's surface to save
|
|
72
|
+
* one line in this one.
|
|
73
|
+
*/
|
|
74
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
75
|
+
|
|
76
|
+
export const EXIT_OK = 0;
|
|
77
|
+
export const EXIT_ERROR = 1;
|
|
78
|
+
export const EXIT_USAGE = 2;
|
|
79
|
+
|
|
80
|
+
const USAGE = `usage: llm_propose --observations FILE --out DIR [--confirm] [--force]
|
|
81
|
+
|
|
82
|
+
--observations FILE the same document evolution_lab propose consumes
|
|
83
|
+
--out DIR where candidate records are written
|
|
84
|
+
--confirm ACTUALLY SPEND. Without it nothing is sent.
|
|
85
|
+
--force overwrite an existing record with different bytes
|
|
86
|
+
|
|
87
|
+
Dry by default. The dry path prints the ladder plan, the pinned model per
|
|
88
|
+
tier, the exact request body for the first observation, and a cost estimate.
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
interface Parsed {
|
|
92
|
+
observations?: string;
|
|
93
|
+
out?: string;
|
|
94
|
+
confirm: boolean;
|
|
95
|
+
force: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function parseArgs(argv: readonly string[]): Parsed | string {
|
|
99
|
+
const p: Parsed = { confirm: false, force: false };
|
|
100
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
101
|
+
const a = argv[i] as string;
|
|
102
|
+
if (a === '--confirm') {
|
|
103
|
+
p.confirm = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (a === '--force') {
|
|
107
|
+
p.force = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const v = argv[i + 1];
|
|
111
|
+
if (v === undefined || v.startsWith('--')) return `${a} requires a value`;
|
|
112
|
+
i += 1;
|
|
113
|
+
if (a === '--observations') p.observations = v;
|
|
114
|
+
else if (a === '--out') p.out = v;
|
|
115
|
+
else return `unknown option '${a}'`;
|
|
116
|
+
}
|
|
117
|
+
return p;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Load, disclose and validate the observations. Same path as the deterministic verb. */
|
|
121
|
+
export function loadObservations(file: string, emit: (s: string) => void): DefectObservation[] {
|
|
122
|
+
const doc = parseObservationDocument(JSON.parse(fs.readFileSync(file, 'utf-8')));
|
|
123
|
+
const plan: RunPlan = {
|
|
124
|
+
candidates: doc.observations.length,
|
|
125
|
+
trialsPerCandidate: 1,
|
|
126
|
+
estimatedSpendCents: 0,
|
|
127
|
+
};
|
|
128
|
+
assertWithinBudget(plan, loadRunBudget());
|
|
129
|
+
const log: DisclosureRecord[] = [];
|
|
130
|
+
return parseObservations(discloseObservations(doc, log, emit));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function dryRun(observations: readonly DefectObservation[], read: (s: string) => string): number {
|
|
134
|
+
// The ladder plan, validated by the SAME `assertCheapestFirst` call the live
|
|
135
|
+
// walk makes. A dry run is not a run, but this population is real: it is
|
|
136
|
+
// derived from the real observations by the real ladder code.
|
|
137
|
+
const attempts = plannedAttempts(observations);
|
|
138
|
+
process.stdout.write(`llm_propose: DRY RUN — nothing sent, nothing spent.\n`);
|
|
139
|
+
for (const [tier, model] of Object.entries(TIER_MODEL)) {
|
|
140
|
+
process.stdout.write(
|
|
141
|
+
`llm_propose: tier ${tier} -> ${model ?? 'UNPINNED (refused until a dated id is pinned)'}\n`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
for (const a of attempts) {
|
|
145
|
+
process.stdout.write(
|
|
146
|
+
`llm_propose: planned attempt ${String(a.sequence)} · class=${a.defect_class} · tier=${a.tier}\n`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const first = observations[0];
|
|
150
|
+
if (first !== undefined) {
|
|
151
|
+
const req = describeRequest({
|
|
152
|
+
tier: attempts[0]?.tier ?? 'lite',
|
|
153
|
+
system: SYSTEM_PROMPT,
|
|
154
|
+
prompt: buildPrompt(first, read(first.subject)),
|
|
155
|
+
});
|
|
156
|
+
process.stdout.write(`llm_propose: request body for ${first.subject}:\n`);
|
|
157
|
+
process.stdout.write(`${JSON.stringify(req.body, null, 2)}\n`);
|
|
158
|
+
const inTok = estimateTokens(`${String(req.body['system'])}${JSON.stringify(req.body['messages'])}`);
|
|
159
|
+
// Tokens, and DELIBERATELY no dollar figure. `ai_council/pricing.ts`'s
|
|
160
|
+
// `estimate_cost` needs a loaded price table from a runtime file that a
|
|
161
|
+
// fresh checkout does not have, and a price invented here would read as
|
|
162
|
+
// authoritative. The spend ceiling belongs to the protocol document,
|
|
163
|
+
// which states it once for the whole run.
|
|
164
|
+
process.stdout.write(
|
|
165
|
+
`llm_propose: ~${String(inTok)} input tokens for this one call ` +
|
|
166
|
+
`(ESTIMATE: characters over four, not a tokenizer; no price is computed here — ` +
|
|
167
|
+
`the run budget is in docs/contracts/metered-proposer-protocol.md)\n`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
process.stdout.write('llm_propose: re-run with --confirm to spend.\n');
|
|
171
|
+
return EXIT_OK;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function main(argv?: string[]): Promise<number> {
|
|
175
|
+
const args = argv ?? process.argv.slice(2);
|
|
176
|
+
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
|
|
177
|
+
process.stdout.write(USAGE);
|
|
178
|
+
return args.length === 0 ? EXIT_USAGE : EXIT_OK;
|
|
179
|
+
}
|
|
180
|
+
const parsed = parseArgs(args);
|
|
181
|
+
if (typeof parsed === 'string') {
|
|
182
|
+
process.stderr.write(`llm_propose: error: ${parsed}\n${USAGE}`);
|
|
183
|
+
return EXIT_USAGE;
|
|
184
|
+
}
|
|
185
|
+
if (parsed.observations === undefined || parsed.out === undefined) {
|
|
186
|
+
process.stderr.write(`llm_propose: error: --observations FILE and --out DIR are required\n${USAGE}`);
|
|
187
|
+
return EXIT_USAGE;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const read = (subject: string): string =>
|
|
191
|
+
fs.readFileSync(path.join(REPO_ROOT, subject), 'utf-8');
|
|
192
|
+
|
|
193
|
+
let observations: DefectObservation[];
|
|
194
|
+
try {
|
|
195
|
+
observations = loadObservations(parsed.observations, (line) => process.stderr.write(`${line}\n`));
|
|
196
|
+
} catch (e) {
|
|
197
|
+
process.stderr.write(`llm_propose: observations rejected — ${(e as Error).message}\n`);
|
|
198
|
+
return EXIT_ERROR;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!parsed.confirm) {
|
|
202
|
+
try {
|
|
203
|
+
return dryRun(observations, read);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
process.stderr.write(`llm_propose: dry run refused — ${(e as Error).message}\n`);
|
|
206
|
+
return EXIT_ERROR;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let proposal;
|
|
211
|
+
try {
|
|
212
|
+
proposal = await proposeCandidatesWithModel(observations, read, anthropicGenerator());
|
|
213
|
+
} catch (e) {
|
|
214
|
+
process.stderr.write(`llm_propose: proposal failed — ${(e as Error).message}\n`);
|
|
215
|
+
return EXIT_ERROR;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fs.mkdirSync(parsed.out, { recursive: true });
|
|
219
|
+
for (const r of proposal.records) {
|
|
220
|
+
const dest = path.join(parsed.out, candidateRecordFilename(r));
|
|
221
|
+
const bytes = serialiseCandidateRecord(r);
|
|
222
|
+
if (fs.existsSync(dest) && !parsed.force) {
|
|
223
|
+
if (fs.readFileSync(dest, 'utf-8') === bytes) {
|
|
224
|
+
process.stdout.write(`llm_propose: ${dest} already identical\n`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
process.stderr.write(`llm_propose: ${dest} exists with different bytes — pass --force\n`);
|
|
228
|
+
return EXIT_ERROR;
|
|
229
|
+
}
|
|
230
|
+
fs.writeFileSync(dest, bytes, 'utf-8');
|
|
231
|
+
process.stdout.write(`llm_propose: wrote ${dest} (model ${proposal.models[r.id] ?? 'unknown'})\n`);
|
|
232
|
+
}
|
|
233
|
+
for (const a of proposal.attempts) {
|
|
234
|
+
process.stdout.write(
|
|
235
|
+
`llm_propose: attempt ${String(a.sequence)} · class=${a.defect_class} · tier=${a.tier}\n`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
process.stdout.write(
|
|
239
|
+
`llm_propose: ${String(proposal.records.length)} candidate(s) from ` +
|
|
240
|
+
`${String(proposal.attempts.length)} metered attempt(s)\n`,
|
|
241
|
+
);
|
|
242
|
+
return EXIT_OK;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function _isCliEntry(): boolean {
|
|
246
|
+
try {
|
|
247
|
+
if (process.argv[1] === undefined) return false;
|
|
248
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (_isCliEntry()) {
|
|
255
|
+
void main().then((code) => process.exit(code));
|
|
256
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* measure_delivery_sets — what a narrowed delivery costs, and which SETS are
|
|
4
|
+
* wrong together (`road-to-governed-harness-evolution` step 6.4).
|
|
5
|
+
*
|
|
6
|
+
* THE CEILING WAS FIXED BEFORE THIS FILE EXISTED. Recall-loss ceiling 20.0 pp,
|
|
7
|
+
* token target 500, k = 5, and the definition of a jointly-wrong pair all live
|
|
8
|
+
* in `agents/evidence/analysis/delivery-set-preregistration-2026-08-31.md`,
|
|
9
|
+
* committed in `fe8749458` — a commit that adds no measurement module and runs
|
|
10
|
+
* nothing. The constants below are transcribed from it.
|
|
11
|
+
*
|
|
12
|
+
* WHY A PAIR METRIC AT ALL. Precision, recall and false activation each score
|
|
13
|
+
* ONE delivery in isolation, so none of them can see the failure where two
|
|
14
|
+
* individually plausible skills are wrong TOGETHER. The right question is which
|
|
15
|
+
* set to deliver, and a per-artefact metric cannot ask it.
|
|
16
|
+
*
|
|
17
|
+
* ITS BOUND, STATED RATHER THAN IMPLIED. A pair is only observable when the
|
|
18
|
+
* SAME prompt is adjudicated by BOTH corpora, because the corpus labels one
|
|
19
|
+
* skill per prompt. So the count is a LOWER bound on joint wrongness and never
|
|
20
|
+
* an estimate of it: a wrong pair whose members share no prompt cannot be seen
|
|
21
|
+
* from this corpus at all.
|
|
22
|
+
*
|
|
23
|
+
* THE RANKING ARM IS NOT CHOSEN HERE. It is resolved from the 5.1 verdict file
|
|
24
|
+
* by `_lib/routing_index_input.ts` (step 6.5): `signal` widens the index to the
|
|
25
|
+
* body, anything else — including a missing or provenance-stripped record —
|
|
26
|
+
* resolves to description-only. Today`s verdict is `harmful`, so this run
|
|
27
|
+
* measures the description index; flipping the verdict file flips what this run
|
|
28
|
+
* indexes, which is the property step 6.5`s verify asks for.
|
|
29
|
+
*
|
|
30
|
+
* ZERO MODEL CALLS AND ZERO SPEND: the imports are a file reader and a pure
|
|
31
|
+
* scorer.
|
|
32
|
+
*
|
|
33
|
+
* Usage:
|
|
34
|
+
* ./scripts-run src/scripts/measure_delivery_sets human table
|
|
35
|
+
* ./scripts-run src/scripts/measure_delivery_sets --json machine record
|
|
36
|
+
*
|
|
37
|
+
* Exit 0 = the measurement ran; 2 = it could not run.
|
|
38
|
+
*/
|
|
39
|
+
import * as fs from 'node:fs';
|
|
40
|
+
import * as path from 'node:path';
|
|
41
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
type CatalogueEntry,
|
|
45
|
+
type CorpusCase,
|
|
46
|
+
loadCatalogue,
|
|
47
|
+
loadTrainCases,
|
|
48
|
+
termIndex,
|
|
49
|
+
topK,
|
|
50
|
+
} from './_lib/routing_corpus.js';
|
|
51
|
+
import { resolveIndexInput } from './_lib/routing_index_input.js';
|
|
52
|
+
|
|
53
|
+
export const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
54
|
+
export const PREREG_REL = 'agents/evidence/analysis/delivery-set-preregistration-2026-08-31.md';
|
|
55
|
+
export const RECORD_REL = 'agents/evidence/analysis/delivery-set-measurement-2026-08-31.json';
|
|
56
|
+
|
|
57
|
+
export const K = 5;
|
|
58
|
+
export const RECALL_LOSS_CEILING_PP = 20.0;
|
|
59
|
+
export const TOKEN_TARGET = 500;
|
|
60
|
+
/** The recall curve reported beside k, so a breach says at which k it would clear. */
|
|
61
|
+
export const CURVE_KS = [1, 3, 5, 10, 20] as const;
|
|
62
|
+
|
|
63
|
+
export interface JointlyWrongPair {
|
|
64
|
+
readonly prompt: string;
|
|
65
|
+
readonly pair: readonly [string, string];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A pair the CORPUS adjudicates wrong together, whether or not delivery
|
|
70
|
+
* currently hands them over together.
|
|
71
|
+
*
|
|
72
|
+
* The step`s verify asks whether *the corpus* contains a jointly-wrong pair.
|
|
73
|
+
* The pre-registration`s own definition added a third condition — both members
|
|
74
|
+
* delivered in the top-k — which turns a corpus property into a delivery
|
|
75
|
+
* property. That is a defect in the pre-registration, found by implementing it,
|
|
76
|
+
* and it is fixed by reporting BOTH rather than by rewriting the definition
|
|
77
|
+
* after seeing the count. `jointlyDeliveredAtK` is the smallest k at which the
|
|
78
|
+
* pair is actually handed over together, or `null` inside the reported curve.
|
|
79
|
+
*/
|
|
80
|
+
export interface JointlyWrongCandidate {
|
|
81
|
+
readonly prompt: string;
|
|
82
|
+
readonly pair: readonly [string, string];
|
|
83
|
+
readonly jointlyDeliveredAtK: number | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface DeliveryMeasurement {
|
|
87
|
+
readonly k: number;
|
|
88
|
+
readonly catalogueSize: number;
|
|
89
|
+
readonly cases: number;
|
|
90
|
+
readonly prompts: number;
|
|
91
|
+
readonly positives: number;
|
|
92
|
+
readonly negatives: number;
|
|
93
|
+
readonly recallPp: number;
|
|
94
|
+
readonly recallLossPp: number;
|
|
95
|
+
readonly ceilingMet: boolean;
|
|
96
|
+
readonly recallCurvePp: Record<number, number>;
|
|
97
|
+
readonly falseActivationPp: number;
|
|
98
|
+
readonly precisionPp: number;
|
|
99
|
+
readonly adjudicatedDeliveries: number;
|
|
100
|
+
readonly unadjudicatedDeliveries: number;
|
|
101
|
+
readonly contextCostTokens: number;
|
|
102
|
+
readonly tokenTargetMet: boolean;
|
|
103
|
+
readonly benefitUnconditionalPp: number;
|
|
104
|
+
readonly benefitConditionalPp: number;
|
|
105
|
+
/**
|
|
106
|
+
* Prompts where the top-k CUT was decided by the alphabetical tie-break
|
|
107
|
+
* rather than by score — `score(k) === score(k+1)`. Reported, not barred:
|
|
108
|
+
* it is the "recalls but does not rank" pathology, and it bounds how much
|
|
109
|
+
* any of the figures above are about discrimination at all.
|
|
110
|
+
*/
|
|
111
|
+
readonly tieDecidedCutPp: number;
|
|
112
|
+
readonly indexInput: ReturnType<typeof resolveIndexInput>;
|
|
113
|
+
readonly sharedPrompts: number;
|
|
114
|
+
readonly jointlyWrongPairs: readonly JointlyWrongPair[];
|
|
115
|
+
readonly jointlyWrongCandidates: readonly JointlyWrongCandidate[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The corpus verdict for a `(skill, prompt)` key, absent when unadjudicated. */
|
|
119
|
+
export function adjudicationIndex(cases: readonly CorpusCase[]): Map<string, boolean> {
|
|
120
|
+
const index = new Map<string, boolean>();
|
|
121
|
+
for (const c of cases) index.set(`${c.skill} ${c.prompt.trim().toLowerCase()}`, c.expect);
|
|
122
|
+
return index;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const pp = (hits: number, total: number): number => (total === 0 ? 0 : (hits / total) * 100);
|
|
126
|
+
|
|
127
|
+
export function measureDelivery(repo = REPO, k = K): DeliveryMeasurement {
|
|
128
|
+
const catalogue: CatalogueEntry[] = loadCatalogue(repo);
|
|
129
|
+
const cases: CorpusCase[] = loadTrainCases(repo);
|
|
130
|
+
if (catalogue.length === 0 || cases.length === 0) {
|
|
131
|
+
throw new Error('measure_delivery_sets: empty catalogue or corpus — a run over nothing');
|
|
132
|
+
}
|
|
133
|
+
const indexInput = resolveIndexInput(repo);
|
|
134
|
+
const index = termIndex(catalogue, indexInput.indexesBody ? 'description+body' : 'description');
|
|
135
|
+
const adjudicated = adjudicationIndex(cases);
|
|
136
|
+
const cost = new Map<string, number>();
|
|
137
|
+
for (const e of catalogue) cost.set(e.name, `${e.name} ${e.description}`.length);
|
|
138
|
+
|
|
139
|
+
// One delivery per distinct prompt: a prompt carried by two corpora is one
|
|
140
|
+
// delivery event, adjudicated twice.
|
|
141
|
+
const byPrompt = new Map<string, CorpusCase[]>();
|
|
142
|
+
for (const c of cases) {
|
|
143
|
+
const key = c.prompt.trim().toLowerCase();
|
|
144
|
+
const bucket = byPrompt.get(key);
|
|
145
|
+
if (bucket === undefined) byPrompt.set(key, [c]);
|
|
146
|
+
else bucket.push(c);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const curveHits: Record<number, number> = {};
|
|
150
|
+
for (const ck of CURVE_KS) curveHits[ck] = 0;
|
|
151
|
+
const maxK = Math.max(k, ...CURVE_KS);
|
|
152
|
+
|
|
153
|
+
let positives = 0;
|
|
154
|
+
let negatives = 0;
|
|
155
|
+
let recallHits = 0;
|
|
156
|
+
let falseHits = 0;
|
|
157
|
+
let adjudicatedDeliveries = 0;
|
|
158
|
+
let correctDeliveries = 0;
|
|
159
|
+
let unadjudicatedDeliveries = 0;
|
|
160
|
+
let totalChars = 0;
|
|
161
|
+
let tieDecidedCuts = 0;
|
|
162
|
+
let sharedPrompts = 0;
|
|
163
|
+
const jointly: JointlyWrongPair[] = [];
|
|
164
|
+
const candidates: JointlyWrongCandidate[] = [];
|
|
165
|
+
|
|
166
|
+
for (const [key, group] of [...byPrompt.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
|
|
167
|
+
const prompt = (group[0] as CorpusCase).prompt;
|
|
168
|
+
const rankedFull = topK(prompt, catalogue, index, maxK);
|
|
169
|
+
const ranked = rankedFull.map((r) => r.name);
|
|
170
|
+
const delivered = ranked.slice(0, k);
|
|
171
|
+
if (
|
|
172
|
+
rankedFull.length > k &&
|
|
173
|
+
(rankedFull[k - 1] as { score: number }).score ===
|
|
174
|
+
(rankedFull[k] as { score: number }).score
|
|
175
|
+
) {
|
|
176
|
+
tieDecidedCuts += 1;
|
|
177
|
+
}
|
|
178
|
+
const deliveredSet = new Set(delivered);
|
|
179
|
+
|
|
180
|
+
for (const c of group) {
|
|
181
|
+
if (c.expect) {
|
|
182
|
+
positives += 1;
|
|
183
|
+
if (deliveredSet.has(c.skill)) recallHits += 1;
|
|
184
|
+
for (const ck of CURVE_KS) {
|
|
185
|
+
if (ranked.slice(0, ck).includes(c.skill)) curveHits[ck] = (curveHits[ck] ?? 0) + 1;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
negatives += 1;
|
|
189
|
+
if (deliveredSet.has(c.skill)) falseHits += 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const name of delivered) {
|
|
194
|
+
totalChars += cost.get(name) ?? 0;
|
|
195
|
+
const verdict = adjudicated.get(`${name} ${key}`);
|
|
196
|
+
if (verdict === undefined) unadjudicatedDeliveries += 1;
|
|
197
|
+
else {
|
|
198
|
+
adjudicatedDeliveries += 1;
|
|
199
|
+
if (verdict) correctDeliveries += 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Set compatibility. Observable only where two corpora adjudicate the
|
|
204
|
+
// same prompt; both members must be delivered AND both recorded false.
|
|
205
|
+
if (group.length < 2) continue;
|
|
206
|
+
sharedPrompts += 1;
|
|
207
|
+
|
|
208
|
+
// The corpus-level property the step`s verify names: two corpora
|
|
209
|
+
// adjudicating the same prompt `false`. Independent of delivery.
|
|
210
|
+
const wrongInCorpus = group
|
|
211
|
+
.filter((c) => !c.expect)
|
|
212
|
+
.map((c) => c.skill)
|
|
213
|
+
.sort();
|
|
214
|
+
for (let i = 0; i < wrongInCorpus.length; i++) {
|
|
215
|
+
for (let j = i + 1; j < wrongInCorpus.length; j++) {
|
|
216
|
+
const a = wrongInCorpus[i] as string;
|
|
217
|
+
const b = wrongInCorpus[j] as string;
|
|
218
|
+
let at: number | null = null;
|
|
219
|
+
for (const ck of CURVE_KS) {
|
|
220
|
+
const head = ranked.slice(0, ck);
|
|
221
|
+
if (head.includes(a) && head.includes(b)) {
|
|
222
|
+
at = ck;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
candidates.push({ prompt, pair: [a, b], jointlyDeliveredAtK: at });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const wrongHere = group
|
|
231
|
+
.filter((c) => !c.expect && deliveredSet.has(c.skill))
|
|
232
|
+
.map((c) => c.skill)
|
|
233
|
+
.sort();
|
|
234
|
+
for (let i = 0; i < wrongHere.length; i++) {
|
|
235
|
+
for (let j = i + 1; j < wrongHere.length; j++) {
|
|
236
|
+
jointly.push({ prompt, pair: [wrongHere[i] as string, wrongHere[j] as string] });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const recall = pp(recallHits, positives);
|
|
242
|
+
const loss = 100 - recall;
|
|
243
|
+
const curve: Record<number, number> = {};
|
|
244
|
+
for (const ck of CURVE_KS) curve[ck] = pp(curveHits[ck] ?? 0, positives);
|
|
245
|
+
const tokens = byPrompt.size === 0 ? 0 : totalChars / 4 / byPrompt.size;
|
|
246
|
+
const precision = pp(correctDeliveries, adjudicatedDeliveries);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
k,
|
|
250
|
+
catalogueSize: catalogue.length,
|
|
251
|
+
cases: cases.length,
|
|
252
|
+
prompts: byPrompt.size,
|
|
253
|
+
positives,
|
|
254
|
+
negatives,
|
|
255
|
+
recallPp: recall,
|
|
256
|
+
recallLossPp: loss,
|
|
257
|
+
ceilingMet: loss <= RECALL_LOSS_CEILING_PP,
|
|
258
|
+
recallCurvePp: curve,
|
|
259
|
+
falseActivationPp: pp(falseHits, negatives),
|
|
260
|
+
precisionPp: precision,
|
|
261
|
+
adjudicatedDeliveries,
|
|
262
|
+
unadjudicatedDeliveries,
|
|
263
|
+
contextCostTokens: tokens,
|
|
264
|
+
tokenTargetMet: tokens <= TOKEN_TARGET,
|
|
265
|
+
benefitUnconditionalPp: recall,
|
|
266
|
+
benefitConditionalPp: precision,
|
|
267
|
+
indexInput,
|
|
268
|
+
tieDecidedCutPp: pp(tieDecidedCuts, byPrompt.size),
|
|
269
|
+
sharedPrompts,
|
|
270
|
+
jointlyWrongPairs: jointly,
|
|
271
|
+
jointlyWrongCandidates: candidates,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function record(m: DeliveryMeasurement): Record<string, unknown> {
|
|
276
|
+
return {
|
|
277
|
+
schema: 1,
|
|
278
|
+
step: '6.4',
|
|
279
|
+
roadmap: 'road-to-governed-harness-evolution',
|
|
280
|
+
preregistration: PREREG_REL,
|
|
281
|
+
k: m.k,
|
|
282
|
+
index_input: {
|
|
283
|
+
fields: m.indexInput.fields,
|
|
284
|
+
derived_from: '5.1 verdict file, via _lib/routing_index_input.ts',
|
|
285
|
+
verdict: m.indexInput.verdict,
|
|
286
|
+
reason: m.indexInput.reason,
|
|
287
|
+
},
|
|
288
|
+
bars: { recall_loss_ceiling_pp: RECALL_LOSS_CEILING_PP, token_target: TOKEN_TARGET },
|
|
289
|
+
metrics: {
|
|
290
|
+
precision_at_k: Number(m.precisionPp.toFixed(3)),
|
|
291
|
+
recall_at_k: Number(m.recallPp.toFixed(3)),
|
|
292
|
+
recall_loss_pp: Number(m.recallLossPp.toFixed(3)),
|
|
293
|
+
ceiling_met: m.ceilingMet,
|
|
294
|
+
recall_curve_pp: Object.fromEntries(
|
|
295
|
+
Object.entries(m.recallCurvePp).map(([kk, v]) => [kk, Number(v.toFixed(3))]),
|
|
296
|
+
),
|
|
297
|
+
false_activation_at_k: Number(m.falseActivationPp.toFixed(3)),
|
|
298
|
+
context_cost_tokens: Number(m.contextCostTokens.toFixed(1)),
|
|
299
|
+
token_target_met: m.tokenTargetMet,
|
|
300
|
+
benefit_unconditional: Number(m.benefitUnconditionalPp.toFixed(3)),
|
|
301
|
+
benefit_conditional_on_activation: Number(m.benefitConditionalPp.toFixed(3)),
|
|
302
|
+
adjudicated_deliveries: m.adjudicatedDeliveries,
|
|
303
|
+
unadjudicated_deliveries: m.unadjudicatedDeliveries,
|
|
304
|
+
tie_decided_cut_pp: Number(m.tieDecidedCutPp.toFixed(3)),
|
|
305
|
+
},
|
|
306
|
+
set_compatibility: {
|
|
307
|
+
shared_prompts: m.sharedPrompts,
|
|
308
|
+
jointly_wrong_pairs_delivered_at_k: m.jointlyWrongPairs.length,
|
|
309
|
+
pairs_delivered_at_k: m.jointlyWrongPairs,
|
|
310
|
+
jointly_wrong_pairs_in_corpus: m.jointlyWrongCandidates.length,
|
|
311
|
+
candidates: m.jointlyWrongCandidates,
|
|
312
|
+
},
|
|
313
|
+
corpus: { partition: 'train', holdout_sealed: true, cases: m.cases, prompts: m.prompts },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function render(m: DeliveryMeasurement): string {
|
|
318
|
+
const curve = CURVE_KS.map((ck) => `@${ck} ${(m.recallCurvePp[ck] ?? 0).toFixed(1)}%`).join(' ');
|
|
319
|
+
return [
|
|
320
|
+
`measure_delivery_sets — step 6.4, k=${m.k}`,
|
|
321
|
+
` index input ${m.indexInput.fields.join(' + ')} (${m.indexInput.reason})`,
|
|
322
|
+
` catalogue ${m.catalogueSize} · prompts ${m.prompts} · positives ${m.positives} · negatives ${m.negatives}`,
|
|
323
|
+
` precision@${m.k} ${m.precisionPp.toFixed(2)} % over ${m.adjudicatedDeliveries} adjudicated deliveries`,
|
|
324
|
+
` recall@${m.k} ${m.recallPp.toFixed(2)} %`,
|
|
325
|
+
` recall loss ${m.recallLossPp.toFixed(2)} pp (ceiling ${RECALL_LOSS_CEILING_PP.toFixed(1)} pp: ${m.ceilingMet ? 'MET' : 'BREACHED'})`,
|
|
326
|
+
` recall curve ${curve}`,
|
|
327
|
+
` false activation@${m.k} ${m.falseActivationPp.toFixed(2)} %`,
|
|
328
|
+
` context cost ${m.contextCostTokens.toFixed(1)} tokens/prompt (target ${TOKEN_TARGET}: ${m.tokenTargetMet ? 'MET' : 'BREACHED'})`,
|
|
329
|
+
` benefit unconditional ${m.benefitUnconditionalPp.toFixed(2)} %`,
|
|
330
|
+
` benefit given activation ${m.benefitConditionalPp.toFixed(2)} %`,
|
|
331
|
+
` unadjudicated deliveries ${m.unadjudicatedDeliveries}`,
|
|
332
|
+
` tie-decided top-${m.k} cut ${m.tieDecidedCutPp.toFixed(2)} % of prompts`,
|
|
333
|
+
` set compatibility corpus ${m.jointlyWrongCandidates.length} jointly-wrong pair(s)` +
|
|
334
|
+
` over ${m.sharedPrompts} shared prompt(s); delivered together at k=${m.k}: ${m.jointlyWrongPairs.length}`,
|
|
335
|
+
...m.jointlyWrongCandidates.map(
|
|
336
|
+
(p) =>
|
|
337
|
+
` - {${p.pair[0]}, ${p.pair[1]}} jointly delivered at k=` +
|
|
338
|
+
`${p.jointlyDeliveredAtK ?? 'never (k<=20)'} :: ${p.prompt}`,
|
|
339
|
+
),
|
|
340
|
+
].join('\n');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function main(argv: readonly string[]): number {
|
|
344
|
+
const m = measureDelivery();
|
|
345
|
+
if (argv.includes('--write')) {
|
|
346
|
+
fs.writeFileSync(path.join(REPO, RECORD_REL), `${JSON.stringify(record(m), null, 2)}\n`);
|
|
347
|
+
}
|
|
348
|
+
process.stdout.write(
|
|
349
|
+
argv.includes('--json') ? `${JSON.stringify(record(m), null, 2)}\n` : `${render(m)}\n`,
|
|
350
|
+
);
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
355
|
+
try {
|
|
356
|
+
process.exit(main(process.argv.slice(2)));
|
|
357
|
+
} catch (err) {
|
|
358
|
+
process.stderr.write(`measure_delivery_sets: ${(err as Error).message}\n`);
|
|
359
|
+
process.exit(2);
|
|
360
|
+
}
|
|
361
|
+
}
|