@tangle-network/agent-bench 0.8.15 → 0.8.17

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.
@@ -1,523 +0,0 @@
1
- /**
2
- * Generation v2 of the agent-graphs skill improvement loop — the GATED half of
3
- * skills/agent-graphs/IMPROVE.md, composed on agent-eval's `runImprovementLoop`.
4
- *
5
- * What upstream owns here: the baseline campaign (v1 on TRAIN, reps=3), candidate
6
- * measurement (v2 on TRAIN, reps=3), the enforced-disjoint holdout scoring of both
7
- * arms, winner selection, and the gate invocation. What this file owns: the same two
8
- * closures the baseline run owned (author dispatch + deterministic scorer, imported
9
- * from agent-graphs-improve.mts), the reviser proposer (glm-5.2, temp 0.7, TRAIN
10
- * failures only), and the protocol gate:
11
- *
12
- * ship iff v2 holdout mean > v1 holdout mean
13
- * and v2 train mean >= v1 train mean - 0.05
14
- *
15
- * Split (declared here, enforced by runImprovementLoop's overlap check):
16
- * TRAIN = review-pipeline, single-agent-suffices, cap-as-stop-mistake,
17
- * runtime-discovered-fanout
18
- * HOLDOUT = mission-in-deliverable, steer-heavy-drafting, unmeasured-harness
19
- *
20
- * Holdout hygiene: the revision prompt is built ONLY from TRAIN-case records and the
21
- * run asserts the holdout ids and briefs are absent from the final prompt string.
22
- *
23
- * Run: pnpm tsx src/agent-graphs-gen2.mts (from bench/)
24
- * Smoke: GEN2_SMOKE=1 pnpm tsx src/agent-graphs-gen2.mts — stubs both LLM calls,
25
- * exercises the full loop wiring + gate + report at zero cost.
26
- *
27
- * Writes skills/agent-graphs/generations/gen2.json; on ship, replaces SKILL.md with v2.
28
- */
29
-
30
- import { createHash } from 'node:crypto'
31
- import { mkdirSync, writeFileSync } from 'node:fs'
32
- import { join, dirname } from 'node:path'
33
- import { fileURLToPath } from 'node:url'
34
- import {
35
- runEval,
36
- runImprovementLoop,
37
- type CampaignResult,
38
- type DispatchContext,
39
- type Gate,
40
- type GateContext,
41
- type JudgeConfig,
42
- type MutableSurface,
43
- type ProposeContext,
44
- type ProposedCandidate,
45
- type SurfaceProposer,
46
- } from '@tangle-network/agent-eval/campaign'
47
- import {
48
- type AuthoredArtifact,
49
- type CaseSpec,
50
- buildAgentGraphsAuthorProfile,
51
- callAuthor,
52
- dispatchWithSurface,
53
- judgeArtifact,
54
- loadInputs,
55
- } from './agent-graphs-improve.mts'
56
-
57
- const HERE = dirname(fileURLToPath(import.meta.url))
58
- const REPO = join(HERE, '..', '..')
59
- const SKILL_PATH = join(REPO, 'skills', 'agent-graphs', 'SKILL.md')
60
- const OUT_PATH = join(REPO, 'skills', 'agent-graphs', 'generations', 'gen2.json')
61
- const RUNS_ROOT = join(REPO, '.gen2-runs')
62
- const SMOKE = process.env.GEN2_SMOKE === '1'
63
-
64
- const K = 3
65
- const SEED = 42
66
- const TRAIN_IDS = [
67
- 'review-pipeline',
68
- 'single-agent-suffices',
69
- 'cap-as-stop-mistake',
70
- 'runtime-discovered-fanout',
71
- ] as const
72
- const HOLDOUT_IDS = ['mission-in-deliverable', 'steer-heavy-drafting', 'unmeasured-harness'] as const
73
- // The anti-over-graphing cases an "always graph" hack would regress on.
74
- const DEGENERATE_IDS = ['single-agent-suffices', 'runtime-discovered-fanout'] as const
75
-
76
- type GraphScenario = CaseSpec & { kind: 'agent-graph-case' }
77
- /** The dispatch artifact: closure A's output plus the cell coordinates the judge
78
- * needs to build per-rep failure records for the proposer. */
79
- type CellArtifact = AuthoredArtifact & { repIndex: number; surfaceSha: string }
80
-
81
- function sha256(text: string): string {
82
- return createHash('sha256').update(text).digest('hex')
83
- }
84
-
85
- // ── Captured evidence (fed to the proposer; TRAIN-filtered at prompt build) ────
86
-
87
- interface JudgedRecord {
88
- surfaceSha: string
89
- scenarioId: string
90
- rep: number
91
- decision: string
92
- score: number
93
- failures: string[]
94
- validationError?: string
95
- }
96
-
97
- const judged: JudgedRecord[] = []
98
-
99
- function makeJudge(): JudgeConfig<CellArtifact, GraphScenario> {
100
- return {
101
- name: 'deterministic-expect',
102
- judgeVersion: 'gen2-1',
103
- dimensions: [{ key: 'expect', description: 'fraction of case expectations satisfied' }],
104
- score({ artifact, scenario }) {
105
- const { score, reasons } = judgeArtifact(artifact, scenario)
106
- judged.push({
107
- surfaceSha: artifact.surfaceSha,
108
- scenarioId: scenario.id,
109
- rep: artifact.repIndex,
110
- decision: artifact.decision,
111
- score,
112
- failures: reasons.filter((r) => !r.startsWith('PASS')),
113
- ...(artifact.validationError !== undefined ? { validationError: artifact.validationError } : {}),
114
- })
115
- return { dimensions: { expect: score }, composite: score, notes: reasons.join('\n') }
116
- },
117
- }
118
- }
119
-
120
- // ── Dispatch (closure A behind the loop's surface-aware seam) ──────────────────
121
-
122
- function smokeArtifact(scenario: GraphScenario): AuthoredArtifact {
123
- // Deterministic offline stand-in: always "single-agent" — wrong on graph cases,
124
- // right on the no-graph case; enough to exercise scoring + gate arithmetic.
125
- return { decision: 'single-agent', reason: `smoke stub for ${scenario.id}`, raw: '{}' }
126
- }
127
-
128
- async function dispatchCell(
129
- surface: MutableSurface,
130
- scenario: GraphScenario,
131
- ctx: DispatchContext,
132
- ): Promise<CellArtifact> {
133
- if (typeof surface !== 'string') throw new Error('gen2 surfaces are strings')
134
- const artifact = SMOKE ? smokeArtifact(scenario) : await dispatchWithSurface(surface, scenario)
135
- return { ...artifact, repIndex: ctx.rep, surfaceSha: sha256(surface) }
136
- }
137
-
138
- // ── The reviser proposer ───────────────────────────────────────────────────────
139
-
140
- let revisionPrompt = ''
141
- let revisionPromptSha256 = ''
142
-
143
- function buildRevisionPrompt(v1Surface: string, trainCases: GraphScenario[]): string {
144
- const v1Sha = sha256(v1Surface)
145
- const caseBlocks = trainCases.map((kase) => {
146
- const rows = judged
147
- .filter((r) => r.surfaceSha === v1Sha && r.scenarioId === kase.id)
148
- .sort((a, b) => a.rep - b.rep)
149
- .map((r) => {
150
- const fails = r.failures.length > 0 ? r.failures.join('\n ') : '(all checks passed)'
151
- return ` rep ${r.rep}: decision=${r.decision} score=${r.score.toFixed(2)}\n ${fails}`
152
- })
153
- return [`<case id="${kase.id}">`, `brief: ${kase.brief}`, `measured (k=${K}):`, ...rows, '</case>'].join('\n')
154
- })
155
- return [
156
- 'You are revising an agent-skill document. The skill below ("v1") instructs a model to author',
157
- 'agent graphs (or decline to) from loose case briefs. It was measured k=3 per case against a',
158
- 'deterministic scorer; the per-rep failures for the training cases are listed after the text.',
159
- '',
160
- '<v1-skill>',
161
- v1Surface,
162
- '</v1-skill>',
163
- '',
164
- 'Measured training failures:',
165
- '',
166
- ...caseBlocks,
167
- '',
168
- 'The two failure clusters your revision must target:',
169
- '1. Analysts never authored when warranted: when independent post-settle findings are required,',
170
- ' the author omits analyzes edges entirely and merges review into the root.',
171
- '2. Identical-role parallelism collapsed: when the work is N parallel instances of the same',
172
- ' role, the author collapses them into one worker node instead of N nodes (one delegation',
173
- ' edge each), losing the parallelism the brief asked for.',
174
- '',
175
- 'Rewrite the skill into v2. Hard constraints:',
176
- '- Keep the YAML frontmatter: `name: agent-graphs` unchanged; `description:` must be a single',
177
- ' line of at most 96 characters.',
178
- '- Total file must stay under 20000 bytes.',
179
- '- Keep the decision honest: "single-agent" and "dynamic-workflow" remain the CORRECT answers',
180
- ' when one profile suffices or when topology is discovered mid-run. Do not teach "always',
181
- ' graph" — fixing under-graphing must not create over-graphing.',
182
- '- Keep the existing correct doctrine (traversal caps, analyzes-cap-is-not-a-stop,',
183
- ' deliverable-carries-mission, offline proving) — sharpen it, do not delete it.',
184
- '- The skill is consumed by a model that must output a strict JSON graph spec; keep the text',
185
- ' operational, not narrative.',
186
- '',
187
- 'Reply with the COMPLETE revised SKILL.md between the markers, nothing else:',
188
- '<<<SKILL',
189
- '(full file here)',
190
- 'SKILL>>>',
191
- ].join('\n')
192
- }
193
-
194
- function extractSkill(reply: string): string {
195
- const m = reply.match(/<<<SKILL\n([\s\S]*?)\nSKILL>>>/)
196
- if (!m?.[1]) throw new Error('proposer reply carries no <<<SKILL ... SKILL>>> block')
197
- return `${m[1].trim()}\n`
198
- }
199
-
200
- function validateSkillGate(text: string): string[] {
201
- const problems: string[] = []
202
- const fm = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1]
203
- if (!fm) problems.push('missing YAML frontmatter')
204
- const name = fm?.match(/^name:\s*(.+)$/m)?.[1]?.trim()
205
- if (name !== 'agent-graphs') problems.push(`frontmatter name is ${JSON.stringify(name)}, expected agent-graphs`)
206
- const description = fm?.match(/^description:\s*(.+)$/m)?.[1]?.trim().replace(/^["']|["']$/g, '')
207
- if (!description) problems.push('frontmatter description missing')
208
- else if (description.length > 96) problems.push(`description is ${description.length} chars (max 96)`)
209
- if (Buffer.byteLength(text) > 20_000) problems.push(`file is ${Buffer.byteLength(text)} bytes (max 20000)`)
210
- return problems
211
- }
212
-
213
- function makeProposer(v1Surface: string, trainCases: GraphScenario[]): SurfaceProposer {
214
- const proposerProfile = buildAgentGraphsAuthorProfile(v1Surface, {
215
- ...process.env,
216
- AGENT_GRAPHS_AUTHOR_PROFILE_NAME: 'agent-graphs-skill-reviser',
217
- AGENT_GRAPHS_AUTHOR_SYSTEM_PROMPT:
218
- 'Revise an agent skill from measured development-case failures. Return only the requested artifact.',
219
- })
220
- const attemptLimit = Number(process.env.AGENT_GRAPHS_GEN2_PROPOSER_ATTEMPTS ?? 2)
221
- if (!Number.isSafeInteger(attemptLimit) || attemptLimit <= 0) {
222
- throw new Error('AGENT_GRAPHS_GEN2_PROPOSER_ATTEMPTS must be a positive integer')
223
- }
224
- return {
225
- kind: 'agent-graphs-skill-reviser',
226
- async propose(_ctx: ProposeContext): Promise<ProposedCandidate[]> {
227
- revisionPrompt = buildRevisionPrompt(v1Surface, trainCases)
228
- // Holdout hygiene is asserted mechanically on the final prompt string.
229
- for (const id of HOLDOUT_IDS) {
230
- if (revisionPrompt.includes(id)) throw new Error(`holdout id '${id}' leaked into the revision prompt`)
231
- }
232
- const allCases = loadInputs().cases
233
- for (const id of HOLDOUT_IDS) {
234
- const brief = allCases.find((c) => c.id === id)?.brief
235
- if (brief && revisionPrompt.includes(brief)) {
236
- throw new Error(`holdout brief for '${id}' leaked into the revision prompt`)
237
- }
238
- }
239
- revisionPromptSha256 = sha256(revisionPrompt)
240
- if (SMOKE) {
241
- return [
242
- {
243
- surface: v1Surface.replace(
244
- '# Agent graphs',
245
- '# Agent graphs\n\n(smoke marker: candidate differs from baseline)',
246
- ),
247
- label: 'smoke-candidate',
248
- rationale: 'zero-cost wiring check',
249
- },
250
- ]
251
- }
252
- let prompt = revisionPrompt
253
- let lastProblems: string[] = []
254
- for (let attempt = 0; attempt < attemptLimit; attempt += 1) {
255
- const reply = await callAuthor(proposerProfile, prompt)
256
- const skill = extractSkill(reply)
257
- lastProblems = validateSkillGate(skill)
258
- if (lastProblems.length === 0) {
259
- return [
260
- {
261
- surface: skill,
262
- label: 'gen2-revision',
263
- rationale:
264
- 'glm-5.2 rewrite targeting under-graphing on cheap briefs, missing analyzes edges, and collapsed identical-role parallelism',
265
- },
266
- ]
267
- }
268
- prompt = `${revisionPrompt}\n\nYour previous attempt violated: ${lastProblems.join('; ')}. Fix these and reply again with the full file between the markers.`
269
- }
270
- throw new Error(`proposer surface failed the skills gate after retry: ${lastProblems.join('; ')}`)
271
- },
272
- }
273
- }
274
-
275
- // ── The protocol gate ──────────────────────────────────────────────────────────
276
-
277
- /** Train-side campaigns captured from `analyzeGeneration` so the gate can apply the
278
- * train-mean condition (the gate ctx itself only carries the holdout arms). */
279
- const trainMeanBySurfaceSha = new Map<string, number>()
280
-
281
- function campaignPerRep(campaign: CampaignResult<CellArtifact, GraphScenario>) {
282
- const perCase = new Map<string, Array<{ rep: number; score: number; decision: string }>>()
283
- for (const cell of campaign.cells) {
284
- const s = cell.judgeScores['deterministic-expect']
285
- if (!s || s.failed) continue
286
- const rows = perCase.get(cell.scenarioId) ?? []
287
- rows.push({ rep: cell.rep, score: s.composite, decision: cell.artifact?.decision ?? 'unknown' })
288
- perCase.set(cell.scenarioId, rows)
289
- }
290
- for (const rows of perCase.values()) rows.sort((a, b) => a.rep - b.rep)
291
- return perCase
292
- }
293
-
294
- /** Split mean per protocol: mean over cases of the per-case rep means. */
295
- function splitMean(perCase: Map<string, Array<{ score: number }>>, ids: readonly string[]): number {
296
- const caseMeans = ids.map((id) => {
297
- const rows = perCase.get(id) ?? []
298
- return rows.length === 0 ? 0 : rows.reduce((s, r) => s + r.score, 0) / rows.length
299
- })
300
- return caseMeans.reduce((s, x) => s + x, 0) / Math.max(caseMeans.length, 1)
301
- }
302
-
303
- function holdoutMeanFromScores(scores: Map<string, Record<string, import('@tangle-network/agent-eval/campaign').JudgeScore>>): number {
304
- const values: number[] = []
305
- for (const byJudge of scores.values()) {
306
- const s = byJudge['deterministic-expect']
307
- if (s && !s.failed) values.push(s.composite)
308
- }
309
- return values.length === 0 ? 0 : values.reduce((a, b) => a + b, 0) / values.length
310
- }
311
-
312
- function makeGate(v1Sha: string): Gate<CellArtifact, GraphScenario> {
313
- return {
314
- name: 'gen2-protocol-gate',
315
- async decide(ctx: GateContext<CellArtifact, GraphScenario>) {
316
- const winnerHoldout = holdoutMeanFromScores(ctx.judgeScores)
317
- const baselineHoldout = ctx.baselineJudgeScores ? holdoutMeanFromScores(ctx.baselineJudgeScores) : 0
318
- const v1Train = trainMeanBySurfaceSha.get(v1Sha)
319
- const candidateShas = [...trainMeanBySurfaceSha.keys()].filter((k) => k !== v1Sha)
320
- const v2Train = candidateShas.length === 1 ? trainMeanBySurfaceSha.get(candidateShas[0] ?? '') : undefined
321
- const holdoutOk = winnerHoldout > baselineHoldout
322
- const trainOk = v1Train !== undefined && v2Train !== undefined && v2Train >= v1Train - 0.05
323
- const ship = holdoutOk && trainOk
324
- return {
325
- decision: ship ? ('ship' as const) : ('hold' as const),
326
- delta: winnerHoldout - baselineHoldout,
327
- reasons: [
328
- `holdout: winner ${winnerHoldout.toFixed(3)} vs baseline ${baselineHoldout.toFixed(3)} → ${holdoutOk ? 'pass' : 'fail'}`,
329
- `train: v2 ${v2Train?.toFixed(3) ?? 'unmeasured'} vs v1 ${v1Train?.toFixed(3) ?? 'unmeasured'} - 0.05 → ${trainOk ? 'pass' : 'fail'}`,
330
- ],
331
- contributingGates: [
332
- { name: 'holdout-mean-strictly-better', status: holdoutOk ? 'pass' : 'fail', detail: { winnerHoldout, baselineHoldout } },
333
- { name: 'train-mean-within-0.05', status: trainOk ? 'pass' : 'fail', detail: { v2Train, v1Train } },
334
- ],
335
- }
336
- },
337
- }
338
- }
339
-
340
- // ── The run ────────────────────────────────────────────────────────────────────
341
-
342
- interface RepRow {
343
- rep: number
344
- score: number
345
- decision: string
346
- }
347
-
348
- function tableFor(perCase: Map<string, RepRow[]>, ids: readonly string[]): Record<string, RepRow[]> {
349
- return Object.fromEntries(ids.map((id) => [id, perCase.get(id) ?? []]))
350
- }
351
-
352
- function printSplit(label: string, perCase: Map<string, RepRow[]>, ids: readonly string[]): void {
353
- console.log(` ${label}:`)
354
- for (const id of ids) {
355
- const rows = perCase.get(id) ?? []
356
- const reps = rows.map((r) => r.score.toFixed(2)).join(' ')
357
- const mean = rows.length === 0 ? 0 : rows.reduce((s, r) => s + r.score, 0) / rows.length
358
- console.log(` ${id.padEnd(28)} reps=[${reps}] mean=${mean.toFixed(3)}`)
359
- }
360
- console.log(` split mean = ${splitMean(perCase, ids).toFixed(4)}`)
361
- }
362
-
363
- async function main(): Promise<void> {
364
- const inputs = loadInputs()
365
- const v1Surface = inputs.surface
366
- const v1Sha = sha256(v1Surface)
367
- const byId = new Map(inputs.cases.map((c) => [c.id, c]))
368
- const missing = [...TRAIN_IDS, ...HOLDOUT_IDS].filter((id) => !byId.has(id))
369
- if (missing.length > 0) throw new Error(`cases missing from skills/agent-graphs/cases: ${missing.join(', ')}`)
370
- if (TRAIN_IDS.length + HOLDOUT_IDS.length !== inputs.cases.length) {
371
- throw new Error(`split covers ${TRAIN_IDS.length + HOLDOUT_IDS.length} of ${inputs.cases.length} cases`)
372
- }
373
- const toScenario = (id: string): GraphScenario => ({ ...(byId.get(id) as CaseSpec), kind: 'agent-graph-case' })
374
- const trainScenarios = TRAIN_IDS.map(toScenario)
375
- const holdoutScenarios = HOLDOUT_IDS.map(toScenario)
376
-
377
- console.log(
378
- `gen2 ${SMOKE ? '(SMOKE) ' : ''}v1=${v1Sha.slice(0, 12)} (${v1Surface.length} chars, ${inputs.source}); train=${TRAIN_IDS.length} holdout=${HOLDOUT_IDS.length} k=${K}`,
379
- )
380
-
381
- const runDir = join(RUNS_ROOT, SMOKE ? 'smoke-loop' : 'loop')
382
- mkdirSync(runDir, { recursive: true })
383
-
384
- const result = await runImprovementLoop<GraphScenario, CellArtifact>({
385
- scenarios: trainScenarios,
386
- holdoutScenarios,
387
- reps: K,
388
- seed: SEED,
389
- maxConcurrency: 1,
390
- candidateConcurrency: 1,
391
- populationSize: 1,
392
- maxGenerations: 1,
393
- baselineSurface: v1Surface,
394
- dispatchRef: SMOKE ? 'gen2-smoke-stub' : 'agent-graphs-author/glm-5.2/temp-0.2',
395
- dispatchWithSurface: dispatchCell,
396
- dispatchTimeoutMs: 600_000,
397
- expectUsage: 'off',
398
- judges: [makeJudge()],
399
- proposer: makeProposer(v1Surface, trainScenarios),
400
- analyzeGeneration: async ({ candidates }) => {
401
- // Capture every train-side mean by surface so the gate can apply the
402
- // protocol's train condition; findings stay untouched.
403
- for (const c of candidates) {
404
- const perCase = campaignPerRep(c.campaign)
405
- const sha = firstCellSurfaceSha(c.campaign)
406
- if (sha !== undefined) trainMeanBySurfaceSha.set(sha, splitMean(perCase, TRAIN_IDS))
407
- }
408
- return []
409
- },
410
- gate: makeGate(v1Sha),
411
- autoOnPromote: 'none',
412
- runDir,
413
- })
414
-
415
- // ── Assemble the four arms ──
416
- const v1Train = campaignPerRep(result.baselineCampaign)
417
- const candidateGen = result.generations[0]?.surfaces[0]
418
- if (!candidateGen) throw new Error('loop produced no generation-0 candidate campaign')
419
- const v2Surface = candidateGen.surface
420
- if (typeof v2Surface !== 'string') throw new Error('candidate surface is not a string')
421
- const v2Sha = sha256(v2Surface)
422
- const v2Train = campaignPerRep(candidateGen.campaign)
423
- const v1Holdout = campaignPerRep(result.baselineOnHoldout)
424
-
425
- // When the upstream winner-selection kept the baseline (candidate did not strictly
426
- // beat v1 on train mean), `winnerOnHoldout` is the baseline arm — the protocol still
427
- // requires v2 measured on holdout, so score it with the same judge/reps/seed.
428
- const winnerIsCandidate = result.winnerSurfaceHash !== undefined && result.winnerSurface === v2Surface
429
- let v2HoldoutCampaign: CampaignResult<CellArtifact, GraphScenario>
430
- if (winnerIsCandidate) {
431
- v2HoldoutCampaign = result.winnerOnHoldout
432
- } else {
433
- console.log('upstream winner = baseline; measuring v2 on holdout via runEval for the protocol gate')
434
- v2HoldoutCampaign = await runEval<GraphScenario, CellArtifact>({
435
- scenarios: holdoutScenarios,
436
- dispatch: (scenario, ctx) => dispatchCell(v2Surface, scenario, ctx),
437
- dispatchRef: SMOKE ? 'gen2-smoke-stub-v2' : 'agent-graphs-author/glm-5.2/temp-0.2/v2',
438
- judges: [makeJudge()],
439
- reps: K,
440
- seed: SEED,
441
- maxConcurrency: 1,
442
- dispatchTimeoutMs: 600_000,
443
- expectUsage: 'off',
444
- runDir: join(RUNS_ROOT, SMOKE ? 'smoke-v2-holdout' : 'v2-holdout'),
445
- })
446
- }
447
- const v2Holdout = campaignPerRep(v2HoldoutCampaign)
448
-
449
- // ── Protocol gate, applied to the assembled arms ──
450
- const v1TrainMean = splitMean(v1Train, TRAIN_IDS)
451
- const v2TrainMean = splitMean(v2Train, TRAIN_IDS)
452
- const v1HoldoutMean = splitMean(v1Holdout, HOLDOUT_IDS)
453
- const v2HoldoutMean = splitMean(v2Holdout, HOLDOUT_IDS)
454
- const promoted = v2HoldoutMean > v1HoldoutMean && v2TrainMean >= v1TrainMean - 0.05
455
- const gateVerdict = promoted ? 'ship' : 'hold'
456
-
457
- const caseMean = (perCase: Map<string, RepRow[]>, id: string): number => {
458
- const rows = perCase.get(id) ?? []
459
- return rows.length === 0 ? 0 : rows.reduce((s, r) => s + r.score, 0) / rows.length
460
- }
461
- const degenerate = Object.fromEntries(
462
- DEGENERATE_IDS.map((id) => [id, { v1: caseMean(v1Train, id), v2: caseMean(v2Train, id) }]),
463
- )
464
-
465
- console.log('\nv1 (baseline surface):')
466
- printSplit('train', v1Train, TRAIN_IDS)
467
- printSplit('holdout', v1Holdout, HOLDOUT_IDS)
468
- console.log('v2 (revised surface):')
469
- printSplit('train', v2Train, TRAIN_IDS)
470
- printSplit('holdout', v2Holdout, HOLDOUT_IDS)
471
- console.log(`\ndegenerate check (anti-over-graphing cases, v1 → v2): ${JSON.stringify(degenerate)}`)
472
- console.log(`upstream gate: ${result.gateResult.decision} [${result.gateResult.reasons.join(' | ')}]`)
473
- console.log(`protocol gate: ${gateVerdict} (holdout ${v1HoldoutMean.toFixed(3)} → ${v2HoldoutMean.toFixed(3)}, train ${v1TrainMean.toFixed(3)} → ${v2TrainMean.toFixed(3)})`)
474
-
475
- const out = {
476
- generation: 2,
477
- date: new Date().toISOString(),
478
- smoke: SMOKE,
479
- authorModel: 'glm-5.2',
480
- authorTemperature: 0.2,
481
- proposerModel: 'glm-5.2',
482
- proposerTemperature: 0.7,
483
- split: { train: TRAIN_IDS, holdout: HOLDOUT_IDS },
484
- k: K,
485
- seed: SEED,
486
- surfaces: { v1Sha256: v1Sha, v2Sha256: v2Sha, v2Label: candidateGen.campaign ? result.generations[0]?.record.candidates[0]?.label : undefined },
487
- perCase: {
488
- v1: { train: tableFor(v1Train, TRAIN_IDS), holdout: tableFor(v1Holdout, HOLDOUT_IDS) },
489
- v2: { train: tableFor(v2Train, TRAIN_IDS), holdout: tableFor(v2Holdout, HOLDOUT_IDS) },
490
- },
491
- aggregates: {
492
- v1: { trainMean: v1TrainMean, holdoutMean: v1HoldoutMean },
493
- v2: { trainMean: v2TrainMean, holdoutMean: v2HoldoutMean },
494
- },
495
- degenerateCheck: degenerate,
496
- upstreamGate: result.gateResult,
497
- upstreamWinnerWasCandidate: winnerIsCandidate,
498
- gateVerdict,
499
- promoted,
500
- revisionPromptSha256,
501
- v2Surface,
502
- }
503
- writeFileSync(OUT_PATH, `${JSON.stringify(out, null, 2)}\n`)
504
- console.log(`written: ${OUT_PATH}`)
505
-
506
- if (promoted && !SMOKE) {
507
- writeFileSync(SKILL_PATH, v2Surface)
508
- console.log(`promoted: ${SKILL_PATH} replaced with v2 (${v2Sha.slice(0, 12)})`)
509
- }
510
- }
511
-
512
- function firstCellSurfaceSha(campaign: CampaignResult<CellArtifact, GraphScenario>): string | undefined {
513
- for (const cell of campaign.cells) {
514
- const sha = cell.artifact?.surfaceSha
515
- if (typeof sha === 'string') return sha
516
- }
517
- return undefined
518
- }
519
-
520
- main().catch((err) => {
521
- console.error(err instanceof Error ? (err.stack ?? err.message) : String(err))
522
- process.exit(1)
523
- })