amicus 4.7.0 → 4.8.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +948 -1
- package/README.md +3 -2
- package/docs/CITATIONS.md +114 -0
- package/docs/ROADMAP.md +33 -5
- package/docs/SHIMS.md +1 -1
- package/docs/configuration.md +3 -3
- package/docs/council.md +181 -34
- package/docs/doc-system.md +1 -1
- package/docs/publishing.md +2 -0
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +6 -4
- package/docs/usage.md +15 -11
- package/electron/setup-ui-aliases.js +2 -2
- package/electron/workspace-ui/index.html +9 -0
- package/electron/workspace-ui/live-dead-seats.js +228 -0
- package/electron/workspace-ui/live-model.js +10 -236
- package/electron/workspace-ui/live-seats.js +126 -0
- package/electron/workspace-ui/workspace-app.js +6 -41
- package/electron/workspace-ui/workspace-banners.js +95 -0
- package/electron/workspace-ui/workspace-lazy.js +55 -12
- package/electron/workspace-ui/workspace-matrix.js +2 -2
- package/electron/workspace-ui/workspace-panels.js +42 -10
- package/electron/workspace-ui/workspace-render.js +2 -2
- package/electron/workspace-ui/workspace-seats.js +101 -17
- package/package.json +6 -3
- package/schemas/council-run-live.schema.json +1 -0
- package/schemas/council-run.schema.json +19 -0
- package/schemas/council-tally.schema.json +34 -2
- package/schemas/council-verdict.schema.json +15 -0
- package/scripts/postinstall.js +6 -3
- package/scripts/setup-hooks.js +49 -3
- package/skills/second-opinion/COUNCIL-DESIGN.md +9 -4
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
- package/skills/second-opinion/SKILL.md +25 -11
- package/skills/sidecar/SKILL.md +3 -1
- package/src/cli-handlers-doctor.js +7 -0
- package/src/cli-handlers-resume-continue.js +20 -0
- package/src/council/anonymize.js +80 -11
- package/src/council/briefings-chair.js +272 -0
- package/src/council/briefings-stage2.js +12 -140
- package/src/council/debate.js +120 -15
- package/src/council/ledger-join.js +284 -0
- package/src/council/ledger-stats.js +100 -0
- package/src/council/ledger.js +177 -88
- package/src/council/peer-split.js +196 -0
- package/src/council/report-html.js +12 -5
- package/src/council/report-md.js +146 -0
- package/src/council/report.js +188 -112
- package/src/council/run-assemble.js +100 -79
- package/src/council/run-chair.js +17 -1
- package/src/council/run-debate-revote.js +268 -0
- package/src/council/run-debate.js +92 -102
- package/src/council/run-finish.js +70 -0
- package/src/council/run-launch.js +56 -18
- package/src/council/run-retry-group.js +266 -0
- package/src/council/run-retry-keys.js +74 -0
- package/src/council/run-retry-launch.js +55 -0
- package/src/council/run-retry-notes.js +65 -13
- package/src/council/run-retry.js +179 -162
- package/src/council/run-stage1-launch.js +27 -10
- package/src/council/run-stage1-rows.js +220 -0
- package/src/council/run-stage1-superseded.js +156 -0
- package/src/council/run-stage2.js +65 -5
- package/src/council/run-stages.js +72 -69
- package/src/council/run-state.js +1 -1
- package/src/council/run-stats-entry.js +71 -0
- package/src/council/run-verdict-files.js +52 -0
- package/src/council/run.js +42 -52
- package/src/council/seats.js +262 -0
- package/src/council/stage1-bind.js +142 -0
- package/src/council/street-cred.js +258 -0
- package/src/council/tally.js +100 -49
- package/src/council/verdict.js +49 -1
- package/src/headless.js +83 -6
- package/src/mcp-tools.js +71 -1
- package/src/observe/council-legs.js +7 -1
- package/src/observe/live-doc.js +3 -3
- package/src/sidecar/continue.js +8 -23
- package/src/sidecar/fanout-leg.js +22 -1
- package/src/sidecar/fanout-retry.js +15 -3
- package/src/sidecar/fanout-wave-io.js +26 -1
- package/src/sidecar/fanout.js +4 -10
- package/src/sidecar/leg-ids.js +19 -0
- package/src/sidecar/models-probe.js +7 -4
- package/src/sidecar/reopen-spend.js +32 -0
- package/src/sidecar/resume.js +1 -1
- package/src/sidecar/setup.js +13 -2
- package/src/utils/config.js +109 -11
- package/src/utils/curated-models.js +17 -2
- package/src/utils/degrade.js +5 -0
- package/src/utils/doctor-engine-check.js +32 -0
- package/src/utils/engine-install-scan.js +98 -15
- package/src/utils/engine-repair.js +96 -2
- package/src/utils/no-output-backstop.js +1 -1
- package/src/utils/remediation-hints.js +42 -0
- package/src/utils/session-index-prune.js +297 -0
- package/src/utils/session-metadata-tmp-sweep.js +1 -1
- package/src/workspace/artifact-guard.js +8 -114
- package/src/workspace/artifact-names.js +222 -0
- package/src/workspace/fold-format.js +9 -6
- package/src/workspace/live-normalize.js +6 -2
- package/src/workspace/matrix-model.js +141 -19
- package/src/workspace/run-detail.js +27 -1
- package/src/workspace/seat-space.js +143 -0
|
@@ -7,22 +7,35 @@
|
|
|
7
7
|
* the `--claude-review` pre-flight it now also owns: the five-keys tally input
|
|
8
8
|
* (meta pins: claudeInCouncil false, models = bench seats exactly — critic
|
|
9
9
|
* included, chair excluded — runType 'headless'), runStats rows copied
|
|
10
|
-
* verbatim from leg docs, the run-dir artifact set
|
|
11
|
-
* tally.json,
|
|
10
|
+
* verbatim from leg docs, the tally half of the run-dir artifact set
|
|
11
|
+
* (tally-input.json, tally.json — the verdict half moved to
|
|
12
|
+
* ./run-verdict-files and is re-exported below), the chair packet,
|
|
12
13
|
* v4.1 §4.4 pre-flight validation of the file-sourced Claude review
|
|
13
14
|
* (preflightClaudeReview — the reserved-seat/chair/critic guards), its review-N+1
|
|
14
15
|
* labelling (labelClaudeReview), and its synthesized null-usage runStats row
|
|
15
16
|
* (claudeRunStatsRow). Raiser self-votes are INCLUDED in adjudications —
|
|
16
|
-
* exclusion is tally's job (tally.js
|
|
17
|
+
* exclusion is tally's job (tally.js :: tally — its `peers` filter, and `judged`).
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
const fs = require('fs');
|
|
20
21
|
const path = require('path');
|
|
21
22
|
const { writeFileAtomic } = require('../utils/atomic-write');
|
|
22
|
-
const { buildVerdict, summarizeSeatLoss, deriveSeatLoss, writeVerdictAtomic } = require('./verdict');
|
|
23
|
-
const { buildReport } = require('./report');
|
|
24
23
|
const { validateFindings } = require('./findings');
|
|
25
24
|
const { toGlobalFindings } = require('./anonymize');
|
|
25
|
+
// Seat identity lives in ./seats (v4.8 PR1) — that module is require-free by
|
|
26
|
+
// design, so preflightSeats' body lives there and is re-exported here to keep
|
|
27
|
+
// the asm.preflightSeats(o) call spelling and this file under the size gate.
|
|
28
|
+
const { preflightSeats } = require('./seats');
|
|
29
|
+
// Same precedent (v4.8 PR4c): writeVerdictFiles' body lives in
|
|
30
|
+
// ./run-verdict-files — with the ./verdict and ./report requires it was the
|
|
31
|
+
// sole consumer of — and is re-exported here so the asm.writeVerdictFiles(...)
|
|
32
|
+
// call spelling and every existing test survive the move untouched.
|
|
33
|
+
const { writeVerdictFiles } = require('./run-verdict-files');
|
|
34
|
+
// Same precedent (v4.8 Phase 1 T1.1): buildRunStatsEntry's body lives in
|
|
35
|
+
// ./run-stats-entry — which is require-free so consumers outside this file's
|
|
36
|
+
// graph can use it — and is re-exported here so every existing call spelling
|
|
37
|
+
// survives the move untouched.
|
|
38
|
+
const { buildRunStatsEntry } = require('./run-stats-entry');
|
|
26
39
|
|
|
27
40
|
const CONFORMANCE_RANK = { clean: 0, repaired: 1, unstructured: 2 };
|
|
28
41
|
|
|
@@ -35,42 +48,6 @@ function worseConformance(a, b) {
|
|
|
35
48
|
return (CONFORMANCE_RANK[a] || 0) >= (CONFORMANCE_RANK[b] || 0) ? a : b;
|
|
36
49
|
}
|
|
37
50
|
|
|
38
|
-
/**
|
|
39
|
-
* One runStats row from a leg run document. Verbatim copies only — a missing
|
|
40
|
-
* leg doc yields durationMs/usage null (never invent a value). `model` (the
|
|
41
|
-
* council alias) overrides leg.model (the resolved executable id) so ledger
|
|
42
|
-
* rows join meta.models by exact string (ledger.js:20-24).
|
|
43
|
-
* `resolvedModel` (v4.7 GOA-7) preserves leg.model — the executable id that
|
|
44
|
-
* actually served, post-fallback-substitution — emit-only-when-set and never
|
|
45
|
-
* sourced from modelInput (an alias must never masquerade as a resolved id).
|
|
46
|
-
*
|
|
47
|
-
* ⚠️ LC-11 / review F1: `findingsUnverified` and `repairRefused` are the same
|
|
48
|
-
* class of fact as `conformance` and ride the same row. They are the two halves
|
|
49
|
-
* of the repair contract's outcome: `findingsUnverified` marks a 'repaired' seat
|
|
50
|
-
* whose contract could NOT be checked (the original block was absent or
|
|
51
|
-
* unparseable, so there was no finding count to compare), and `repairRefused`
|
|
52
|
-
* ({code, detail}) marks the stronger case — the contract WAS checked and broken,
|
|
53
|
-
* which is otherwise indistinguishable from a seat that never emitted JSON at
|
|
54
|
-
* all. Both are additive and present only when set, so a run without either is
|
|
55
|
-
* byte-for-byte unchanged.
|
|
56
|
-
*/
|
|
57
|
-
function buildRunStatsEntry({ leg, model, role, wasChair, conformance, findingsUnverified,
|
|
58
|
-
repairRefused }) {
|
|
59
|
-
return {
|
|
60
|
-
model: model !== undefined ? model : (leg ? leg.model : null),
|
|
61
|
-
role,
|
|
62
|
-
wasChair: !!wasChair,
|
|
63
|
-
conformance: conformance || 'clean',
|
|
64
|
-
...(findingsUnverified ? { findingsUnverified: true } : {}),
|
|
65
|
-
...(repairRefused ? { repairRefused } : {}),
|
|
66
|
-
...(leg && leg.waveId ? { waveId: leg.waveId } : {}),
|
|
67
|
-
...(leg && leg.model ? { resolvedModel: leg.model } : {}),
|
|
68
|
-
status: leg ? leg.status : 'error',
|
|
69
|
-
durationMs: leg && typeof leg.durationMs === 'number' ? leg.durationMs : null,
|
|
70
|
-
usage: (leg && leg.usage) || null,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
51
|
/**
|
|
75
52
|
* Pre-flight for `--claude-review <path>` (v4.1 §4.4). Runs AFTER initRun (so the
|
|
76
53
|
* error doc lands in a run dir that exists) and BEFORE any launch, so an invalid
|
|
@@ -135,8 +112,9 @@ function claudeRunStatsRow() {
|
|
|
135
112
|
* Assemble the five-keys tally input (spec §5 / SKILL.md Stage-2 recipe).
|
|
136
113
|
* @param {{runId: string, date: string, bench: string[], chair: string,
|
|
137
114
|
* reviews: Array<{model, role, conformance, leg, globalFindings}>,
|
|
138
|
-
* judgeResults: Array<{judge, ok, order, adjudications}>,
|
|
139
|
-
* chairStats: object|null, claudeReview?: object|null, extraRows?: Array<object
|
|
115
|
+
* judgeResults: Array<{judge, seat, ok, order, orderSeats, adjudications, conformance, leg}>,
|
|
116
|
+
* chairStats: object|null, claudeReview?: object|null, extraRows?: Array<object>,
|
|
117
|
+
* seats?: Array<object>}} args
|
|
140
118
|
* `claudeReview` (v4.1 §4.4) amends the v4.0 meta pin: present ⇒ claudeInCouncil
|
|
141
119
|
* true, 'claude' joins meta.models (the street-cred universe), its findings join
|
|
142
120
|
* the pool and it gets the synthesized null-usage runStats row. Absent ⇒ v4.0
|
|
@@ -144,23 +122,74 @@ function claudeRunStatsRow() {
|
|
|
144
122
|
* (repair/superseded/dead-seat-error, from runStage1 today) appended right
|
|
145
123
|
* after the primary review rows, before judge/chair accounting — absent or
|
|
146
124
|
* empty ⇒ byte-for-byte unchanged, so the pre-v4.7 length-7 pins stay green.
|
|
125
|
+
* `seats` (v4.8 PR4c §3.2) is run.json's seat table; see meta.seats below.
|
|
147
126
|
*/
|
|
148
127
|
function buildTallyInput({ runId, date, bench, chair, reviews, judgeResults, chairStats,
|
|
149
|
-
claudeReview, extraRows }) {
|
|
128
|
+
claudeReview, extraRows, seats }) {
|
|
150
129
|
const meta = {
|
|
151
130
|
runId, date, runType: 'headless',
|
|
152
131
|
models: bench.slice(), // bench seats exactly: critic included, chair excluded
|
|
153
132
|
chair,
|
|
154
133
|
claudeInCouncil: false, // pinned for headless runs
|
|
134
|
+
// v4.8 PR4c §3.2 (R4c-1 as amended by R4c-7): the seat table, a pure TAIL so
|
|
135
|
+
// the shipped six-key order above is byte-identical in every case. Emitted
|
|
136
|
+
// ONLY when the bench repeats an alias — the one case where the `alias#N`
|
|
137
|
+
// seat ids on findings[].raiserSeat, adjudications[].seat and runStats[].seat
|
|
138
|
+
// resolve to nothing else in the document (meta.models is the ALIAS list).
|
|
139
|
+
// ⚠️ `seats ?` alone would be VACUOUS: run.js:133 sets o.seats unconditionally
|
|
140
|
+
// past the preflight and buildSeats always returns an ARRAY ([] is still
|
|
141
|
+
// truthy), so that spelling writes a full table into tally-input.json,
|
|
142
|
+
// tally.json AND verdict.json on every unique-alias bench.
|
|
143
|
+
// ⚠️ NARROW BY RULING, not by oversight. The guard asks "does the bench
|
|
144
|
+
// repeat an alias?", which is a DIFFERENT question from "is anything in
|
|
145
|
+
// seats[] unrecoverable?" — they part on a UNIQUE-alias lens/critic bench.
|
|
146
|
+
// `position` and the raw lens text (runStats[].role has only the slug) ride
|
|
147
|
+
// in meta.seats EXACTLY when it ships, i.e. on a repeated-alias bench, and
|
|
148
|
+
// are absent BY DESIGN otherwise (measured 2026-08-16; BACKLOG SI-21, HOLD).
|
|
149
|
+
// ⚠️ Consumers: absence means "no seat table available", NEVER "the bench was
|
|
150
|
+
// unique" — two of appendRun's three call sites feed hand-assembled input no
|
|
151
|
+
// seat machinery touches. And seats[] is BENCH-ONLY, so it must never be
|
|
152
|
+
// joined positionally to meta.models (`claude` is pushed onto that inside
|
|
153
|
+
// run-assemble.js :: buildTallyInput) or to streetCred[].
|
|
154
|
+
// .slice() is defence-in-depth only: the array is shared with
|
|
155
|
+
// runState.checkpoint (run.js:135) and with both tally inputs. Nothing
|
|
156
|
+
// mutates meta.seats — unlike models, which meta.models.push mutates below —
|
|
157
|
+
// so no test can distinguish the copy from the reference.
|
|
158
|
+
...(Array.isArray(seats) && seats.some(s => s.id !== s.alias) ? { seats: seats.slice() } : {}),
|
|
155
159
|
};
|
|
156
160
|
const findings = reviews.flatMap(r => r.globalFindings);
|
|
157
161
|
const okJudges = judgeResults.filter(j => j.ok);
|
|
158
162
|
const adjudications = okJudges.flatMap(j =>
|
|
159
|
-
j.adjudications.map(a => ({ findingId: a.id, judge: j.judge, verdict: a.verdict
|
|
160
|
-
|
|
163
|
+
j.adjudications.map(a => ({ findingId: a.id, judge: j.judge, verdict: a.verdict,
|
|
164
|
+
// v4.8 PR3 Task 5, re-based by PR4c R4c-9: emit-when-DIFFERENT against
|
|
165
|
+
// the seat's OWN alias, matching buildRunStatsEntry and run.js's
|
|
166
|
+
// raiserSeat call site. It was `!== j.judge` — the alias as the LEG
|
|
167
|
+
// reported it — which emitted a seat id equal to its own alias whenever
|
|
168
|
+
// the two strings drifted (a padded --council member; a leg with no
|
|
169
|
+
// modelInput), i.e. exactly where the field carries no information.
|
|
170
|
+
...(j.seat && j.seat.id !== j.seat.alias ? { seat: j.seat.id } : {}) })));
|
|
171
|
+
// v4.8 T3.2: the SAME predicate as the adjudication map immediately above —
|
|
172
|
+
// the judge's own seat, emit-when-DIFFERENT. `order` itself is untouched
|
|
173
|
+
// (still alias-valued).
|
|
174
|
+
// v4.8 T3.3 carries `orderSeats` the one further hop T3.2 deliberately
|
|
175
|
+
// stopped short of: it reached judgeResults[] in run-stage2.js, but
|
|
176
|
+
// `computeStreetCred` reads `rankings`, so without this the seat channel
|
|
177
|
+
// never arrives where SI-20's collapse happens.
|
|
178
|
+
// ⚠️ EMIT ONLY WHEN IT CARRIES AT LEAST ONE NON-NULL. rankingToOrder returns
|
|
179
|
+
// a PARITY SHAPE, not an absence: on a unique-alias bench `orderSeats` is
|
|
180
|
+
// `[null, null, null]`, and emitting that would add a key to rankings[] in
|
|
181
|
+
// tally-input.json, tally.json and tally-provisional.json on every run that
|
|
182
|
+
// has ever happened. `.flat()` is depth-1 because a tie group is the only
|
|
183
|
+
// nesting rankingToOrder can produce (anonymize.js :: rankingToOrder maps
|
|
184
|
+
// one level of `Array.isArray(slot)`). The named mutant that guards this
|
|
185
|
+
// predicate is tests/council/street-cred-mutants.js :: EMITSET.
|
|
186
|
+
const rankings = okJudges.map(j => ({ judge: j.judge, order: j.order,
|
|
187
|
+
...(Array.isArray(j.orderSeats) && j.orderSeats.flat().some(Boolean)
|
|
188
|
+
? { orderSeats: j.orderSeats } : {}),
|
|
189
|
+
...(j.seat && j.seat.id !== j.seat.alias ? { seat: j.seat.id } : {}) }));
|
|
161
190
|
const runStats = reviews.map(r => buildRunStatsEntry({
|
|
162
191
|
leg: r.leg, model: r.model, role: r.role, wasChair: false, conformance: r.conformance,
|
|
163
|
-
findingsUnverified: r.findingsUnverified, repairRefused: r.repairRefused,
|
|
192
|
+
findingsUnverified: r.findingsUnverified, repairRefused: r.repairRefused, seat: r.seat,
|
|
164
193
|
}));
|
|
165
194
|
// v4.7 D2/E4: pre-built rows (repair/superseded/dead-seat-error) ride right
|
|
166
195
|
// after the primary review rows — same "primary-adjacent" shape, just not
|
|
@@ -177,9 +206,15 @@ function buildTallyInput({ runId, date, bench, chair, reviews, judgeResults, cha
|
|
|
177
206
|
// One row per judge, attributing the judge's ORIGINAL Stage-2 wave leg (never
|
|
178
207
|
// a repair solo's — run-stage2.js mirrors Stage-1's convention there); a judge
|
|
179
208
|
// whose wave leg died still gets an honest error row.
|
|
209
|
+
// v4.8 PR5a T4 (R5-8): the judge row carries its SEAT. PR4c withheld it because
|
|
210
|
+
// `joinsLedger` has no 'judge' member, so nothing consumed it; report.js's cost
|
|
211
|
+
// table does now, and without it a twin bench's two judge rows are identical.
|
|
212
|
+
// `buildRunStatsEntry` (run-stats-entry.js :: buildRunStatsEntry) applies the
|
|
213
|
+
// shared emit-when-DIFFERENT predicate, so a unique bench stays byte-identical
|
|
214
|
+
// and no new predicate enters the tree.
|
|
180
215
|
for (const j of (judgeResults || [])) {
|
|
181
216
|
runStats.push(buildRunStatsEntry({
|
|
182
|
-
leg: j.leg, model: j.judge, role: 'judge', conformance: j.conformance,
|
|
217
|
+
leg: j.leg, model: j.judge, role: 'judge', conformance: j.conformance, seat: j.seat,
|
|
183
218
|
}));
|
|
184
219
|
}
|
|
185
220
|
if (chairStats) { runStats.push(chairStats); }
|
|
@@ -194,36 +229,6 @@ function writeTallyFiles({ runDir, tallyInput, record }) {
|
|
|
194
229
|
JSON.stringify(record, null, 2), { mode: 0o600 });
|
|
195
230
|
}
|
|
196
231
|
|
|
197
|
-
/**
|
|
198
|
-
* Undecided verdict + deterministic report. Sets the nullable overallVerdict
|
|
199
|
-
* (council family v2, Plan A) on buildVerdict's output — independent of
|
|
200
|
-
* buildVerdict's own signature.
|
|
201
|
-
* @param {{runDir: string, record: object, overallVerdict?: (string|null),
|
|
202
|
-
* chairText?: string, critic?: string, deadWaves?: Array<object>,
|
|
203
|
-
* degrades?: Array<object>}} o `degrades` (v4.6 Plan 2), when present, is
|
|
204
|
-
* both carried onto the verdict and used to DERIVE `seatLoss` (deriveSeatLoss)
|
|
205
|
-
* in preference to summarizing it from `deadWaves` (summarizeSeatLoss).
|
|
206
|
-
* @returns {object} the verdict written to disk
|
|
207
|
-
*/
|
|
208
|
-
function writeVerdictFiles({ runDir, record, overallVerdict, chairText, critic, deadWaves, degrades }) {
|
|
209
|
-
// v4.6 Plan 2 (spec D3): when the sink's records are available they are the
|
|
210
|
-
// single source of truth — seatLoss derives from them so it can never
|
|
211
|
-
// disagree with degrades[]. deadWaves remains the fallback for direct
|
|
212
|
-
// callers that predate the sink (their tests pass unedited).
|
|
213
|
-
const seatLoss = degrades
|
|
214
|
-
? deriveSeatLoss({ runId: record.meta.runId, critic, degrades })
|
|
215
|
-
: summarizeSeatLoss({ runId: record.meta.runId, critic, deadWaves });
|
|
216
|
-
const verdict = buildVerdict(record, [], { seatLoss, degrades });
|
|
217
|
-
verdict.overallVerdict = (overallVerdict === undefined) ? null : overallVerdict;
|
|
218
|
-
writeVerdictAtomic(path.join(runDir, 'verdict.json'), verdict);
|
|
219
|
-
const html = buildReport({ verdict }, { format: 'html' });
|
|
220
|
-
fs.writeFileSync(path.join(runDir, 'report.html'), html, { mode: 0o600 });
|
|
221
|
-
if (chairText) {
|
|
222
|
-
fs.writeFileSync(path.join(runDir, 'chair-output.md'), chairText, { mode: 0o600 });
|
|
223
|
-
}
|
|
224
|
-
return verdict;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
232
|
/**
|
|
228
233
|
* Build the chair packet and persist it as `chair-packet.md`. Lifted verbatim
|
|
229
234
|
* out of run.js for the 300-line gate (v4.4.1 Task 0.5) — same composition,
|
|
@@ -240,10 +245,21 @@ function buildChairPacketFile({ runDir, reviews, claudeReview, tallyInput, recor
|
|
|
240
245
|
const packet = buildChairPacket({
|
|
241
246
|
// §4.4: the chair sees Claude's de-anonymized review like any other; it casts
|
|
242
247
|
// no rankings/adjudications, so it appears ONLY as one more review block.
|
|
243
|
-
|
|
248
|
+
// The projection is DELIBERATE — it drops findings/conformance/role/leg; v4.8
|
|
249
|
+
// SI-25 adds `seat`, ⚠️ EMIT-WHEN-DIFFERENT like rankings/adjudications above:
|
|
250
|
+
// `r.model` is the leg's `modelInput || model`, which falls back to the RESOLVED
|
|
251
|
+
// id, so an unconditional forward breaks §4.2 byte identity on a NO-TWIN bench.
|
|
252
|
+
// Mutant: tests/council/chair-packet-seat-mutants.js :: HDRSEATFWD.
|
|
253
|
+
// ⚠️ The Claude review keeps NO seat and renders `claude` via the fallback.
|
|
254
|
+
reviews: reviews.map(r => ({ model: r.model, text: r.text,
|
|
255
|
+
...(r.seat && r.seat.id !== r.seat.alias ? { seat: r.seat } : {}) }))
|
|
244
256
|
.concat(claudeReview ? [{ model: 'claude', text: claudeReview.text }] : []),
|
|
245
257
|
rankings: tallyInput.rankings,
|
|
246
258
|
adjudications: tallyInput.adjudications,
|
|
259
|
+
// v4.8 PR5a T7: `record` is the TALLIED record, so its findings already carry
|
|
260
|
+
// tally.js's R8 stamp. Passing the array the packet already has access to costs one
|
|
261
|
+
// line here and keeps every rendering decision in briefings-chair.js.
|
|
262
|
+
findings: record.findings,
|
|
247
263
|
tierCounts: record.tierCounts, date,
|
|
248
264
|
}) + (debateOutcomes ? '\n\n' + buildDebateAddendum({ outcomes: debateOutcomes }) : '');
|
|
249
265
|
fs.writeFileSync(path.join(runDir, 'chair-packet.md'), packet, { mode: 0o600 });
|
|
@@ -254,4 +270,9 @@ module.exports = {
|
|
|
254
270
|
buildRunStatsEntry, worseConformance, buildTallyInput, writeTallyFiles, writeVerdictFiles,
|
|
255
271
|
buildChairPacketFile,
|
|
256
272
|
preflightClaudeReview, labelClaudeReview, claudeRunStatsRow, CLAUDE_SEAT,
|
|
273
|
+
preflightSeats,
|
|
274
|
+
// Exported for ledger.js's drift guard only (v4.8 PR4b council A1): ledger.js
|
|
275
|
+
// keeps a LOCAL copy of this table, and the guard deep-compares the two so a
|
|
276
|
+
// level added here can never silently go missing there.
|
|
277
|
+
CONFORMANCE_RANK,
|
|
257
278
|
};
|
package/src/council/run-chair.js
CHANGED
|
@@ -53,9 +53,25 @@ function pickFallbackChair(statsRows, bench, failedChair) {
|
|
|
53
53
|
// aggregate shapes only.
|
|
54
54
|
const names = (r) => [r.model, ...(Array.isArray(r.aliases) ? r.aliases : [])];
|
|
55
55
|
const excluded = (r) => names(r).some(n => n === 'claude' || benchSet.has(n) || n === failedChair);
|
|
56
|
+
// v4.8 PR4a: every sort term is read off the row, so CANDIDATE SELECTION is
|
|
57
|
+
// independent of the order `statsRows` arrives in — previously that order
|
|
58
|
+
// (deriveReliability's Map insertion order = council-ledger.jsonl row order)
|
|
59
|
+
// silently decided every exact street-cred tie, and such ties are an ordinary
|
|
60
|
+
// arithmetic outcome. Terms: street cred (lower mean rank = better), then
|
|
61
|
+
// council appearances, then model id for a guaranteed total order. ⚠️ `runs`
|
|
62
|
+
// is the count of DISTINCT runIds across the group's ledger rows (v4.8 PR4b
|
|
63
|
+
// R4b-1, ledger-stats.js's countRuns) — one council run contributes 1 however many
|
|
64
|
+
// seats that executable filled, and rows from `judged:false` runs that
|
|
65
|
+
// contributed no street cred still count. It is a tie-break, never a ranking
|
|
66
|
+
// signal. Always present on deriveReliability output; the default serves
|
|
67
|
+
// fixtures.
|
|
68
|
+
// Full rationale + the tie arithmetic: tests/council/run-chair.test.js.
|
|
69
|
+
const runsOf = (r) => (typeof r.runs === 'number' ? r.runs : 0);
|
|
56
70
|
const candidates = (statsRows || [])
|
|
57
71
|
.filter(r => !excluded(r) && typeof r.avgStreetCredPeersOnly === 'number')
|
|
58
|
-
.sort((a, b) => a.avgStreetCredPeersOnly - b.avgStreetCredPeersOnly)
|
|
72
|
+
.sort((a, b) => (a.avgStreetCredPeersOnly - b.avgStreetCredPeersOnly)
|
|
73
|
+
|| (runsOf(b) - runsOf(a))
|
|
74
|
+
|| (a.model < b.model ? -1 : a.model > b.model ? 1 : 0));
|
|
59
75
|
if (!candidates.length) { return null; }
|
|
60
76
|
const top = candidates[0];
|
|
61
77
|
return (Array.isArray(top.aliases) && top.aliases.length) ? top.aliases[0] : top.model;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// src/council/run-debate-revote.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module council/run-debate-revote
|
|
6
|
+
* v4.8 PR3 Task 1: `legOpts`, `legRow`, and `runRevoteWave` extracted verbatim
|
|
7
|
+
* out of run-debate.js (283/300 — the 300-line-per-file gate) with NO
|
|
8
|
+
* behaviour change. run-debate.js requires all three back; runDebate still
|
|
9
|
+
* calls runRevoteWave for the re-vote mini-wave (spec §5.1).
|
|
10
|
+
*
|
|
11
|
+
* That "verbatim" claim held for all three only through Task 1. `legOpts` and
|
|
12
|
+
* `legRow` are still byte-identical to the extraction. `runRevoteWave` is NOT:
|
|
13
|
+
* PR3 Task 6 gave it real seat-binding behaviour — the padded roster + bind
|
|
14
|
+
* (`stage1-bind.js :: bindPaddedWave` since v4.8 SI-27; an inline `bindSeats`
|
|
15
|
+
* call before it), the seat-keyed `byJudge`, the `sanitizeName`'d per-seat
|
|
16
|
+
* repair id, and the `seat` field on the pushed legs (see the function's own
|
|
17
|
+
* docblock below). Do not treat `runRevoteWave` as a behaviour-neutral mirror
|
|
18
|
+
* of the old run-debate.js code — only `legOpts`/`legRow` still are.
|
|
19
|
+
*
|
|
20
|
+
* `isAbortExit` comes from ./run-launch, NEVER from ./run-stages: run-stage2.js:12
|
|
21
|
+
* records that taking it from run-launch.js "is what dissolved the old cycle
|
|
22
|
+
* (v4.4.1 review F5)". Requiring ./run-stages from this new leaf would drag in
|
|
23
|
+
* run-retry → run-retry-notes → briefings and re-open that cycle class.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const dbrief = require('./briefings-debate');
|
|
29
|
+
const { parseRevote } = require('./parse-stage2');
|
|
30
|
+
const runState = require('./run-state');
|
|
31
|
+
const { emitStageStarted } = require('../observe/events');
|
|
32
|
+
const { isAbortExit } = require('./run-launch');
|
|
33
|
+
// v4.8 PR3 Task 6: seat binding. ./seats requires NOTHING, so taking
|
|
34
|
+
// sanitizeName straight from it (rather than run-launch's re-export) adds
|
|
35
|
+
// zero cycle risk to this leaf — the same call run-stage2.js:30 makes.
|
|
36
|
+
const { sanitizeName } = require('./seats');
|
|
37
|
+
// v4.8 SI-27: the shared roster-padding core. ./stage1-bind requires only
|
|
38
|
+
// ./seats, so this leaf stays cycle-free (see the module docblock's cycle-class
|
|
39
|
+
// paragraph above — named, not line-numbered, so it cannot rot).
|
|
40
|
+
const { bindPaddedWave } = require('./stage1-bind');
|
|
41
|
+
|
|
42
|
+
/** Common launch options for every debate leg (judge-isolated `_scratch` cwd). */
|
|
43
|
+
function legOpts(ctx, waveId) {
|
|
44
|
+
return { project: ctx.scratchDir, waveId, timeout: ctx.o.timeout, gateway: ctx.o.gateway,
|
|
45
|
+
noValidateModel: ctx.o.noValidateModel, noCostGate: ctx.o.noCostGate,
|
|
46
|
+
// v4.3 Task 3 (spec §7.2): attribution ids for every defense/re-vote leg.
|
|
47
|
+
councilRunId: ctx.o.runId, councilName: ctx.o.councilName,
|
|
48
|
+
tag: ctx.o.tag }; // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* v4.7 D2/E4: normalize a raw (possibly leg-absent) leg into the shape
|
|
53
|
+
* debateRunStatsRows' superseded/repair lists expect. Same never-invent-a-waveId
|
|
54
|
+
* discipline as buildRunStatsEntry (run-assemble.js) — only spread `waveId` when
|
|
55
|
+
* the leg genuinely carries one — but keyed on an explicit `model` (the raiser or
|
|
56
|
+
* judge identity), since a leg-absent attempt has no `.model` of its own to read.
|
|
57
|
+
* Threads resolvedModel (the raw leg's .model, the executable id) emit-only-when-set — v4.7 GOA-7 D8.
|
|
58
|
+
*/
|
|
59
|
+
function legRow(model, leg, conformance) {
|
|
60
|
+
return leg
|
|
61
|
+
? { model, status: leg.status, durationMs: typeof leg.durationMs === 'number' ? leg.durationMs : null,
|
|
62
|
+
usage: leg.usage || null, conformance, summary: leg.summary || '',
|
|
63
|
+
...(leg.waveId ? { waveId: leg.waveId } : {}),
|
|
64
|
+
...(leg.model ? { resolvedModel: leg.model } : {}) }
|
|
65
|
+
: { model, status: 'error', durationMs: null, usage: null, conformance, summary: '' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The wave's join key for one leg: the bound seat's id, else the bare alias. */
|
|
69
|
+
function seatKey(seat, alias) { return seat ? seat.id : alias; }
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* T5.1 (owner ruling R8), narrowed by T5.5: announce a re-vote leg whose key
|
|
73
|
+
* this wave cannot account for — its key names none of the judges this wave
|
|
74
|
+
* actually launched. ⚠️ This read "it NEITHER bound to any roster slot (a real
|
|
75
|
+
* seat or a §3.4 placeholder) NOR names one of the judges…" until T5.5 deleted
|
|
76
|
+
* the `boundLegs` arm of the guard below. Binding is no longer part of the
|
|
77
|
+
* condition, and must not be re-added: seats.js :: bindSeats binds on
|
|
78
|
+
* `leg.legId || leg.taskId` with NO alias check, so a leg bound that way can
|
|
79
|
+
* still carry a name this wave never asked for — which is exactly the shape
|
|
80
|
+
* that kept inventing a phantom adjudication row. The
|
|
81
|
+
* leg itself is unaffected — it still gets its runStats row, its
|
|
82
|
+
* revote-<name>.md and its conformance — only its parsed votes are
|
|
83
|
+
* withheld, so `revoteByJudge` never carries this key at all and
|
|
84
|
+
* applyDebate never has to decide whether it belongs to an existing row or
|
|
85
|
+
* is new (that fail-open push in debate.js :: applyDebate is untouched for a
|
|
86
|
+
* key this wave DOES account for).
|
|
87
|
+
*
|
|
88
|
+
* Field shape follows stage1-bind.js:53 :: orphanLegNote's
|
|
89
|
+
* channel/what/why/effect/data — with ONE deliberate divergence: `data`
|
|
90
|
+
* carries no `seat` field. orphanLegNote's `data.seat` (confusingly named —
|
|
91
|
+
* it holds the ALIAS, not a seat object) is exactly what
|
|
92
|
+
* seat-space.js :: orphanExonerations reads to attribute a note to an
|
|
93
|
+
* alias. This note's `waveId` is always `<runId>-rv`, never `<runId>-s2`, so
|
|
94
|
+
* if it carried that field too it would enter that function's alias map and
|
|
95
|
+
* CLEAR — not extend — that alias's already-proven Stage-2 exonerations,
|
|
96
|
+
* via the non-`-s2` branch of the intersection there: a re-vote-stage
|
|
97
|
+
* anomaly wrongly invalidating a Stage-2 review-authorship proof. Do not add
|
|
98
|
+
* `seat` here to "complete" the parity with orphanLegNote; it is withheld
|
|
99
|
+
* on purpose.
|
|
100
|
+
*/
|
|
101
|
+
function reVoteUnboundNote(waveId, judge, key, leg) {
|
|
102
|
+
const legId = (leg && (leg.legId || leg.taskId)) || 'unidentified';
|
|
103
|
+
// `|| 'unknown'` mirrors stage1-bind.js:55's alias fallback, and for the same reason: the
|
|
104
|
+
// caller derives `judge` as `leg.modelInput || leg.model`, so a leg carrying NEITHER makes the
|
|
105
|
+
// record read "… 'undefined'" — a bug in the announcer rather than a fact about the leg.
|
|
106
|
+
// ⚠️ BOTH need it. `key` is `seatKey(seat, judge)`, which RETURNS that same `judge` whenever the
|
|
107
|
+
// leg bound to no real seat — i.e. in every refusal reachable through runDebate — so an undefined
|
|
108
|
+
// `judge` takes `key` with it. T5.5 interpolated `key` raw for one commit and measurably rendered
|
|
109
|
+
// "its join key 'undefined'" AND dropped `data.key` from the JSON. Pinned by
|
|
110
|
+
// run-debate.test.js's "NEITHER modelInput NOR model" test; named mutant KEYRAW.
|
|
111
|
+
const alias = judge || 'unknown';
|
|
112
|
+
const joinKey = key || 'unknown';
|
|
113
|
+
return {
|
|
114
|
+
channel: 'seat-unbound',
|
|
115
|
+
// ⚠️ All three strings below now say the ONE thing the guard tests: the key names no judge
|
|
116
|
+
// this wave launched. Binding is irrelevant — this function's docblock says why.
|
|
117
|
+
// ⚠️ `what` said "matches no seat on that wave's roster" for three rounds AFTER that stopped
|
|
118
|
+
// being the condition: a leg taskId-bound to a §3.4 placeholder DOES match a roster slot and is
|
|
119
|
+
// exactly the leg now refused, so `what` contradicted the `why` three lines under it. A paid
|
|
120
|
+
// council caught it (round 2). ⚠️ stage1-bind.js :: orphanLegNote KEEPS that wording and MUST:
|
|
121
|
+
// a Stage-1 orphan really does match no roster slot. Same sentence, true there, false here.
|
|
122
|
+
// ⚠️ `effect` said "the JUDGE's provisional verdict stands" until round 2 — the very
|
|
123
|
+
// presumption a refusal denies, since a refused key names no judge of this wave.
|
|
124
|
+
// ⚠️ `why` carried "(judge alias '${alias}')" for one commit; dropped, measured — `key ===
|
|
125
|
+
// judge` in every refusal runDebate can produce, so it printed the same string twice
|
|
126
|
+
// (BACKLOG.md holds the measurement). `data` keeps orphanLegNote's field names, `judge`
|
|
127
|
+
// included: it is the leg's own CLAIM, and renaming a machine-readable field is a compat break.
|
|
128
|
+
what: `re-vote leg ${legId} in wave ${waveId} could not be attributed to a judge on that wave`,
|
|
129
|
+
why: `its join key '${joinKey}' names none of the judges this wave launched`,
|
|
130
|
+
effect: 'the re-vote was NOT applied; the provisional verdict stands',
|
|
131
|
+
data: { waveId, legId, judge: alias, key: joinKey },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The re-vote mini-wave (spec §5.1).
|
|
137
|
+
*
|
|
138
|
+
* v4.8 PR3 Task 6 — the parallel-array discipline this function now runs on:
|
|
139
|
+
* `judgeKeys` are SEAT ids (disputingJudges' output) and `judgeSeats` the
|
|
140
|
+
* matching seat objects, both in launch order; `aliasOf` projects a key back to
|
|
141
|
+
* the routable bench alias. On every iteration of the leg loop the seat key and
|
|
142
|
+
* the alias are **two different values**: the key is what `byJudge` is keyed on
|
|
143
|
+
* (so `applyDebate` can join it against `(a.seat || a.judge)`), while the alias
|
|
144
|
+
* is what every launcher argument and every runStats `model` carries. That is
|
|
145
|
+
* not an inconsistency — a seat id is not a routable model name.
|
|
146
|
+
*
|
|
147
|
+
* @param {object} ctx run.js's {o, launchers, addWave, overBudget, degrade, scratchDir}
|
|
148
|
+
* @param {Array<string>} judgeKeys seat ids, in launch order
|
|
149
|
+
* @param {Array<object>} bundleFindings defended/amended findings
|
|
150
|
+
* @param {Array<?object>} judgeSeats seat objects positionally bound to judgeKeys
|
|
151
|
+
* @param {function(string): string} aliasOf seat id → bench alias
|
|
152
|
+
*/
|
|
153
|
+
async function runRevoteWave(ctx, judgeKeys, bundleFindings, judgeSeats, aliasOf) {
|
|
154
|
+
const bundle = dbrief.buildRevoteBundle({ findings: bundleFindings, date: ctx.o.date });
|
|
155
|
+
// spec §5.1 names `revote-bundle.md` a run-dir artifact: the shared re-vote prompt goes to
|
|
156
|
+
// disk exactly like Stage 2's bundle-stage2.md, so the round's model-facing input is
|
|
157
|
+
// auditable alongside briefing-stage1.md and chair-packet.md.
|
|
158
|
+
fs.writeFileSync(path.join(ctx.o.runDir, 'revote-bundle.md'), bundle, { mode: 0o600 });
|
|
159
|
+
const waveId = `${ctx.o.runId}-rv`;
|
|
160
|
+
const expectedIds = bundleFindings.map(f => f.id);
|
|
161
|
+
// run-debate — not run.js — owns this stage's `running` checkpoint AND its abort-cascade
|
|
162
|
+
// id: only this function knows whether the wave actually launched (it is skipped when
|
|
163
|
+
// nothing was defended/amended, or the cost ceiling hit).
|
|
164
|
+
runState.updateStage(ctx.o.runDir, 'debate-revote',
|
|
165
|
+
{ status: 'running', startedAt: new Date().toISOString(), project: ctx.scratchDir, waveId });
|
|
166
|
+
emitStageStarted(ctx.o.runDir, ctx.o.runId, 'debate-revote', waveId, ctx.o.follow);
|
|
167
|
+
runState.appendStageWave(ctx.o.runDir, 'debate-revote', waveId);
|
|
168
|
+
// ⚠️ The launcher takes ALIASES. A seat id here is a non-routable model name
|
|
169
|
+
// on a real paid wave, not a test failure.
|
|
170
|
+
const res = await ctx.launchers.launchWave({
|
|
171
|
+
...legOpts(ctx, waveId), models: judgeKeys.map(aliasOf), prompt: bundle });
|
|
172
|
+
ctx.addWave(res.wave);
|
|
173
|
+
if (isAbortExit(res.exitCode)) { return { aborted: res.exitCode }; }
|
|
174
|
+
const byJudge = {}, legs = [];
|
|
175
|
+
// v4.7 D2/E4: mirrors runDefenseSolo's supersededLeg/repairLeg — one list each,
|
|
176
|
+
// accumulated across every judge in this wave (most judges contribute neither).
|
|
177
|
+
const supersededLegs = [], repairLegs = [];
|
|
178
|
+
const rawLegs = (res.wave && res.wave.legs) || [];
|
|
179
|
+
// §3.4's roster-padding pattern now lives in `stage1-bind.js ::
|
|
180
|
+
// bindPaddedWave` (v4.8 SI-27) — why the roster is padded rather than
|
|
181
|
+
// filtered, and why placeholders are tracked by IDENTITY rather than an
|
|
182
|
+
// id-name prefix test, are in that function's docblock. This site has NO
|
|
183
|
+
// tail: no orphan push, no degrade note, so it destructures `seatOf` alone.
|
|
184
|
+
const { seatOf } = bindPaddedWave(waveId, judgeSeats || [], i => aliasOf(judgeKeys[i]), rawLegs);
|
|
185
|
+
for (const leg of rawLegs) {
|
|
186
|
+
// The council ALIAS, not the resolved executable id — runStats rows join
|
|
187
|
+
// meta.models by exact string (run-assemble.js's buildRunStatsEntry).
|
|
188
|
+
const judge = leg.modelInput || leg.model;
|
|
189
|
+
const seat = seatOf.get(leg) || null;
|
|
190
|
+
const key = seatKey(seat, judge);
|
|
191
|
+
const alive = leg.status === 'complete' && leg.summary;
|
|
192
|
+
let outLeg = leg; // the leg actually recorded (post-repair when there is one)
|
|
193
|
+
let parsed = alive ? parseRevote(leg.summary, expectedIds)
|
|
194
|
+
: { ok: false, byId: {}, errors: [{ code: 'DEAD_LEG', detail: 'no summary' }] };
|
|
195
|
+
let conformance = alive ? 'clean' : 'unstructured';
|
|
196
|
+
if (alive && !parsed.ok) {
|
|
197
|
+
// One repair, solo, to that judge. The id is built from the SEAT key so
|
|
198
|
+
// two twins never share one repair id (and one never overwrites the
|
|
199
|
+
// other's run-state entry). ⚠️ The trailing `r` is load-bearing: it is what
|
|
200
|
+
// stops bindSeats' `/^(.*)-(\d+)$/` matching a repair id whose judge alias
|
|
201
|
+
// is a bare number (`r1-rv-2r` does not match; `r1-rv-2` would). Dropping
|
|
202
|
+
// it re-arms a collision. sanitizeName also fixes the pre-existing slash
|
|
203
|
+
// bug (D4) — `r1-rv-openrouter/deepseek/deepseek-chatr` stops nesting
|
|
204
|
+
// three directory levels — and is a no-op for every plain alias.
|
|
205
|
+
const repairId = `${waveId}-${sanitizeName(key)}r`;
|
|
206
|
+
runState.appendStageWave(ctx.o.runDir, 'debate-revote', repairId);
|
|
207
|
+
const r2 = await ctx.launchers.launchSolo({ ...legOpts(ctx, repairId), model: judge,
|
|
208
|
+
// ⚠️ LC-12: ditto — the re-vote output being repaired rides with its errors.
|
|
209
|
+
prompt: dbrief.buildRevoteRepairPrompt({ errors: parsed.errors, revote: leg.summary }) });
|
|
210
|
+
ctx.addWave(r2.wave);
|
|
211
|
+
if (isAbortExit(r2.exitCode)) { return { aborted: r2.exitCode }; }
|
|
212
|
+
const leg2 = r2.leg && r2.leg.status === 'complete' ? r2.leg : null;
|
|
213
|
+
parsed = leg2 ? parseRevote(leg2.summary, expectedIds) : parsed;
|
|
214
|
+
conformance = parsed.ok ? 'repaired' : 'unstructured';
|
|
215
|
+
// Symmetric with runDefenseSolo's `if (leg2) { leg = leg2; }` — otherwise
|
|
216
|
+
// revote-<model>.md and the runStats row keep the PRE-repair output.
|
|
217
|
+
if (leg2) { supersededLegs.push(legRow(judge, leg, 'unstructured')); outLeg = leg2; }
|
|
218
|
+
else { repairLegs.push(legRow(judge, r2.leg, 'unstructured')); }
|
|
219
|
+
}
|
|
220
|
+
// ⚠️ Two DIFFERENT values on the same iteration: `byJudge`'s key is the SEAT
|
|
221
|
+
// (applyDebate joins it against `(a.seat || a.judge)`), while the leg's
|
|
222
|
+
// `model` is the ALIAS — this literal becomes a debateRunStatsRows row, and
|
|
223
|
+
// R3-1 promises every runStats `model` is alias-valued on every bench.
|
|
224
|
+
// `seat` rides along for materializeDebate's filename only; debateRunStatsRows'
|
|
225
|
+
// `mk` copies an explicit field list and never picks it up.
|
|
226
|
+
//
|
|
227
|
+
// T5.1 (owner ruling R8), narrowed by T5.5: publish IFF the key names one of
|
|
228
|
+
// the judges THIS WAVE actually launched. `judgeSeats` is positionally bound
|
|
229
|
+
// to `judgeKeys` (this function's own docblock) and run-debate.js builds it
|
|
230
|
+
// from an id-keyed table — `judgeKeys.map(k => seatById.get(k) || null)` — so
|
|
231
|
+
// every REAL seat id that can reach `key` here is already a `judgeKeys`
|
|
232
|
+
// entry. Two further shapes are deliberately admitted: a
|
|
233
|
+
// unique-alias bench, where `seatKey(null, 'qwen') === 'qwen'` IS that seat's
|
|
234
|
+
// own judgeKey; and a §3.4 roster hole (a Stage-2-orphaned judge, padded with
|
|
235
|
+
// a placeholder here) whose -rv leg is ALSO unbindable — its bare-alias key is
|
|
236
|
+
// still in `judgeKeys`, and the provisional row for that same orphaned judge
|
|
237
|
+
// is ALSO keyed on the bare alias, so refusing it would discard a re-vote the
|
|
238
|
+
// join already lands correctly (measured: 2 adjudications in, 2 out, the
|
|
239
|
+
// seat-less row's verdict replaced, no phantom row). NOT `seat === null`
|
|
240
|
+
// (§0.5) — a real-seat bind is unaffected either way, but that predicate
|
|
241
|
+
// wrongly refuses both shapes just described. Only a leg whose key names no
|
|
242
|
+
// judge this wave launched is the unnameable case R8 asks to refuse.
|
|
243
|
+
//
|
|
244
|
+
// ⚠️ T5.5 DELETED a second arm, `boundLegs.has(leg) ||`. Binding is not
|
|
245
|
+
// enough: seats.js :: bindSeats matches `leg.legId || leg.taskId` to a roster
|
|
246
|
+
// SLOT with NO alias check, so a leg stamped into a §3.4 placeholder's slot
|
|
247
|
+
// while carrying a foreign alias BOUND (arm 1 true) and still keyed on that
|
|
248
|
+
// foreign alias (arm 2 false) — the key was published, no note was emitted,
|
|
249
|
+
// and applyDebate's fail-open push invented a phantom adjudication row while
|
|
250
|
+
// the hole's own seat-less row kept its stale dispute. That is the SI-10 shape
|
|
251
|
+
// this guard exists to close, surviving through that arm. Measured across the
|
|
252
|
+
// deletion in run-debate.test.js's T5.5 block: 3 A1 rows out of 2 in and zero
|
|
253
|
+
// notes before, 2 out and one `seat-unbound` note after. Do not re-add the arm
|
|
254
|
+
// as "defensive redundancy" — named mutant BOUNDREADD in that block is exactly
|
|
255
|
+
// that re-addition, and it reds.
|
|
256
|
+
if (judgeKeys.includes(key)) {
|
|
257
|
+
byJudge[key] = parsed.byId;
|
|
258
|
+
} else {
|
|
259
|
+
ctx.degrade.note(reVoteUnboundNote(waveId, judge, key, leg));
|
|
260
|
+
}
|
|
261
|
+
legs.push({ model: judge, status: outLeg.status, durationMs: outLeg.durationMs, usage: outLeg.usage,
|
|
262
|
+
conformance, summary: outLeg.summary || '', waveId: outLeg.waveId, seat,
|
|
263
|
+
...(outLeg.model ? { resolvedModel: outLeg.model } : {}) });
|
|
264
|
+
}
|
|
265
|
+
return { byJudge, legs, supersededLegs, repairLegs };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = { legOpts, legRow, runRevoteWave };
|