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
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// src/council/run-retry-group.js
|
|
2
|
+
'use strict';
|
|
3
|
+
// Stage-1 loss grouping: lensIndexOf + recordFailure + groupStage1Losses.
|
|
4
|
+
// Moved verbatim from run-retry.js:24-126 (v4.8 PR0 size-gate split, zero
|
|
5
|
+
// behavior). Pure but for ONE leaf require: ./run-retry-keys, itself require-free.
|
|
6
|
+
// run-retry.js re-exports groupStage1Losses so existing import paths
|
|
7
|
+
// (tests/council/run-retry.test.js) stay stable.
|
|
8
|
+
const { seatKey, twinAliases, legLossKey, srcLegClaimer } = require('./run-retry-keys');
|
|
9
|
+
|
|
10
|
+
/** 1-based lens index for a loss: the waveId convention, else the seat's own
|
|
11
|
+
* bench position, else the alias's first bench index. v4.8 PR2b H4: the old
|
|
12
|
+
* `o.models.indexOf(model)` was first-match, so twin aliases both resolved to
|
|
13
|
+
* the FIRST twin's lens and shared one retry unit — and the deadLegs loop
|
|
14
|
+
* passes waveId=null, so that branch was the only one those losses could
|
|
15
|
+
* take. The alias fallback survives for a loss with no identified seat. */
|
|
16
|
+
function lensIndexOf(o, waveId, model, seatObj = null) {
|
|
17
|
+
const m = /-l(\d+)$/.exec(waveId || '');
|
|
18
|
+
if (m) { return Number(m[1]); }
|
|
19
|
+
if (seatObj && Number.isInteger(seatObj.position)) { return seatObj.position; }
|
|
20
|
+
const i = (o.models || []).indexOf(model);
|
|
21
|
+
return i === -1 ? null : i + 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Which of a unit's srcLegs still need their own still-dead note once its srcWaves have
|
|
26
|
+
* emitted theirs, plus every seat key those announcements cover.
|
|
27
|
+
*
|
|
28
|
+
* v4.8 PR5c: an UNIDENTIFIED wave slot keys by ALIAS (seatKey's fallback) while a leg that
|
|
29
|
+
* WAS bound keys by its seat id, so a plain `noted.has(key)` test misses and the same seat
|
|
30
|
+
* is announced TWICE, once per keyspace. HISTORY, not the present: at pre-PR5c HEAD the
|
|
31
|
+
* Workspace's alias-keyed dead-row dedup hid this by collapsing both into one row; PR5c Task 2
|
|
32
|
+
* seat-keyed that consumer, and T2.2 abolished the producer-side collapse — nothing masks it now.
|
|
33
|
+
*
|
|
34
|
+
* ⛔ TWO earlier attempts inferred identity here and both were rated blockers. A per-alias
|
|
35
|
+
* BUDGET assumed the next leg on an alias was the wave's unnamed slot. A roster PIGEONHOLE
|
|
36
|
+
* (`I + U + 1 > K`) replaced the assumption with arithmetic — but silently assumed `I` and `U`
|
|
37
|
+
* were disjoint, so when an unnamed slot and an identified leg were the same seat it overcounted
|
|
38
|
+
* and dropped a genuinely distinct one. Both failures were SILENT, which is the direction this
|
|
39
|
+
* module exists to reject.
|
|
40
|
+
*
|
|
41
|
+
* Owner ruling: stop inferring. Dedup ONLY where identity is exact, and announce otherwise.
|
|
42
|
+
* Exact means: the leg was bound to a seat (a real id), OR its alias holds exactly one seat in
|
|
43
|
+
* the roster, where the alias IS the seat id (`seats.js` mints `alias#N` only for repeats).
|
|
44
|
+
* Everything else is announced, because nothing proves it is a repeat.
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ The accepted cost, disclosed: on a bench that repeats an alias, an unnamed wave slot and a
|
|
47
|
+
* leg for the same seat can each produce a note, so one dead seat may be announced twice. That
|
|
48
|
+
* is a VISIBLE duplicate. The alternative — the two inferences above — hid a dead seat instead,
|
|
49
|
+
* and a duplicate that a reader can see beats a loss they cannot.
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ v4.8 T-A6 (SI-TWINS): `twins` is THREADED IN — one derivation per run, made by
|
|
52
|
+
* `run-retry.js :: retryStage1Losses` and handed to every producer and consumer of a
|
|
53
|
+
* `legLossKey`. It is NOT the same thing as `seatsPerAlias` below, and merging them would be a
|
|
54
|
+
* silent behaviour change: `twins` is `> 1` (an alias the roster PROVES repeated) while
|
|
55
|
+
* `seatsPerAlias.get(alias) === 1` is `=== 1` (an alias the roster proves UNIQUE). They differ
|
|
56
|
+
* on the alias the roster does not mention at all — count 0 — where `!twins.has(alias)` is true
|
|
57
|
+
* but `=== 1` is false. That gap is the whole point of this function's rule: with no proof of
|
|
58
|
+
* identity it ANNOUNCES, because a duplicate note a reader can see beats a dead seat they
|
|
59
|
+
* cannot. `twinAliases`' own docblock states the opposite default for the same reason.
|
|
60
|
+
* The parameter default exists for direct callers only; the production path always passes.
|
|
61
|
+
*
|
|
62
|
+
* @param {Array<{id: string, alias: string}>} roster the run's seat table (`o.seats`)
|
|
63
|
+
* @param {Map<string, number>} [twins] the run's ONE `twinAliases(roster)` (see above)
|
|
64
|
+
* @returns {{attempted: Set<string>, legs: Array<{leg: object, seatId: ?string}>}}
|
|
65
|
+
*/
|
|
66
|
+
function planStillDeadSources(unit, seatOf, roster, twins = twinAliases(roster)) {
|
|
67
|
+
const seatsPerAlias = new Map();
|
|
68
|
+
for (const seat of roster || []) {
|
|
69
|
+
if (seat && seat.alias) { seatsPerAlias.set(seat.alias, (seatsPerAlias.get(seat.alias) || 0) + 1); }
|
|
70
|
+
}
|
|
71
|
+
const noted = new Set();
|
|
72
|
+
const attempted = new Set();
|
|
73
|
+
for (const w of unit.srcWaves) {
|
|
74
|
+
(w.models || []).forEach((m, i) => {
|
|
75
|
+
const k = seatKey((w.seats || [])[i] || null, m);
|
|
76
|
+
noted.add(k); attempted.add(k);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const legs = [];
|
|
80
|
+
for (const l of unit.srcLegs) {
|
|
81
|
+
const bound = seatOf.get(l) || null;
|
|
82
|
+
const alias = l.modelInput || l.model;
|
|
83
|
+
const key = seatKey(bound, alias);
|
|
84
|
+
// BOTH spellings: `key` is what an exact dead-seat row asks with, `legLossKey` is
|
|
85
|
+
// what an unattributable twin's row asks with (run-stage1-rows.js's finalLeg
|
|
86
|
+
// fallback). They are the same string whenever identity is exact, so the Set is
|
|
87
|
+
// unchanged on every bench but a twin one. Missing the second spelling re-attaches
|
|
88
|
+
// a first-attempt leg to a seat that WAS retried — and that leg already has its
|
|
89
|
+
// own `superseded` row, so its cost would be counted twice.
|
|
90
|
+
attempted.add(key);
|
|
91
|
+
attempted.add(legLossKey(bound, alias, l, twins));
|
|
92
|
+
// `key` names a specific seat only when the leg was bound, or when the alias holds exactly
|
|
93
|
+
// one seat (then the alias IS the id). Otherwise it is an alias standing in for "some seat",
|
|
94
|
+
// and matching it against a wave's unnamed slot would be a guess, not a repeat.
|
|
95
|
+
const identityIsExact = !!bound || seatsPerAlias.get(alias) === 1;
|
|
96
|
+
if (identityIsExact && noted.has(key)) { continue; }
|
|
97
|
+
noted.add(key);
|
|
98
|
+
legs.push({ leg: l, seatId: bound ? bound.id : null });
|
|
99
|
+
}
|
|
100
|
+
return { attempted, legs };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Dedup helper (Task-4 review hardening): the same seat can arrive twice in one
|
|
105
|
+
* grouping pass — two dead legs naming it, or a dead wave and a dead leg both naming
|
|
106
|
+
* it. One seat must still mean ONE `firstFailures` entry — first occurrence wins —
|
|
107
|
+
* while every SOURCE record is kept regardless (srcWaves/srcLegs are the audit trail
|
|
108
|
+
* and are never deduped). The critic unit's `.models` is fixed at creation (there is
|
|
109
|
+
* only ever one critic seat), so only bench/lens units grow `.models` here — the
|
|
110
|
+
* critic call sites pass `trackModel: false`.
|
|
111
|
+
*
|
|
112
|
+
* v4.8 PR2b: `seatObj` rides in lockstep, so `unit.seats` stays INDEX-PARALLEL to
|
|
113
|
+
* `unit.models` — same order, same length. `null` means "we could not identify this
|
|
114
|
+
* seat"; it is never back-filled from an alias lookup, which is exactly the guess
|
|
115
|
+
* seat identity exists to forbid.
|
|
116
|
+
*
|
|
117
|
+
* v4.8 T2.2: dedup ONLY where identity is EXACT — the leg was bound, or the roster
|
|
118
|
+
* does not repeat its alias. On a twin alias with no binding `key` names BOTH twins,
|
|
119
|
+
* and this early return then DISCARDED the second one BEFORE `models`/`seats` were
|
|
120
|
+
* ever pushed: one retry slot for two seats the run had already paid for. (PR2b H4
|
|
121
|
+
* claimed that collapse was correct "because nothing distinguishes them". The ROW does
|
|
122
|
+
* have a distinguisher — see `run-retry-keys.js :: legLossKey` — and the SLOT does not need one.)
|
|
123
|
+
*
|
|
124
|
+
* v4.8 T-A3: …and, ON THE INEXACT ARM AND PER UNIT, no further than the roster proves. There
|
|
125
|
+
* the alias's roster COUNT is the only upper bound there is, so mint up to it and stop: without it, N
|
|
126
|
+
* unattributable losses on a 2-seat alias minted N slots, each buying a real, billed retry
|
|
127
|
+
* leg for a seat that may not exist. (Not reachable end-to-end today — a first-pass dead
|
|
128
|
+
* wave carries real seats, and a wave cannot return more legs than it launched — but the
|
|
129
|
+
* safety rested entirely on those two facts, which nothing here stated or checked.)
|
|
130
|
+
* ⚠️ Scope, stated because the code cannot: this bounds SLOTS, i.e. `unit.models`, so a
|
|
131
|
+
* `trackModel: false` unit is unaffected — the critic's `.models` is fixed at creation
|
|
132
|
+
* (first paragraph above) and never grows here.
|
|
133
|
+
*
|
|
134
|
+
* ⚠️ The key is ADDED as `seatId`, and on the inexact branch it stays ALIAS-valued
|
|
135
|
+
* for BOTH entries. `ff.seat`/`ff.seatId` become `data.seat` /
|
|
136
|
+
* `data.firstFailure.seatId` on every emitted note, which verdict.js compares against
|
|
137
|
+
* `o.critic` (an alias) and the Workspace renders as a seat id: seat-keying `ff.seat`
|
|
138
|
+
* silently breaks critic-loss detection, and minting one here would put a fabricated
|
|
139
|
+
* seat identity on screen.
|
|
140
|
+
*/
|
|
141
|
+
function recordFailure(unit, seat, ff, trackModel = true, seatObj = null, twins = null) {
|
|
142
|
+
const key = seatKey(seatObj, seat);
|
|
143
|
+
const identityIsExact = !!seatObj || !(twins && twins.has(seat));
|
|
144
|
+
// Exact identity: one seat is one entry. Inexact: `twins.get(seat)` is defined and >1
|
|
145
|
+
// exactly when this arm is taken, and it is the roster's whole claim about the alias.
|
|
146
|
+
const alreadyCovered = identityIsExact
|
|
147
|
+
? unit.firstFailures.some(f => f.seatId === key)
|
|
148
|
+
: unit.models.filter(m => m === seat).length >= twins.get(seat);
|
|
149
|
+
if (alreadyCovered) { return; }
|
|
150
|
+
unit.firstFailures.push({ ...ff, seatId: key });
|
|
151
|
+
if (trackModel) { unit.models.push(seat); unit.seats.push(seatObj); }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Group Stage-1 losses into retry units. Pure — no I/O.
|
|
156
|
+
* Bench losses (a dead bench wave's models + dead bench legs) collapse into
|
|
157
|
+
* ONE retry wave; the critic and each lens retry as solos (their briefings
|
|
158
|
+
* differ). Stable order: bench, critic, lenses ascending. The critic matches
|
|
159
|
+
* on EITHER carrier — waveId convention or model — mirroring
|
|
160
|
+
* verdict.js summarizeSeatLoss.
|
|
161
|
+
*
|
|
162
|
+
* `seatOf` (v4.8 PR2b) is Stage-1's leg->seat binding, keyed by leg OBJECT
|
|
163
|
+
* identity: it is how a dead LEG contributes the seat it was actually bound to
|
|
164
|
+
* rather than one guessed from its alias. A wave-origin loss carries its own
|
|
165
|
+
* roster on `w.seats`, positionally parallel to `w.models`.
|
|
166
|
+
*
|
|
167
|
+
* `twins` (v4.8 T-A6, SI-TWINS) is THREADED IN rather than derived here — one
|
|
168
|
+
* `twinAliases` per run, shared by every site that mints or asks a `legLossKey`. The
|
|
169
|
+
* default is for direct callers (this function is exported and several suites drive it
|
|
170
|
+
* with four arguments); the production path always passes.
|
|
171
|
+
*/
|
|
172
|
+
function groupStage1Losses(o, deadWaves = [], deadLegs = [], seatOf = new Map(),
|
|
173
|
+
twins = twinAliases(o.seats)) {
|
|
174
|
+
const isCriticWave = (w) =>
|
|
175
|
+
w.waveId === `${o.runId}-c1` || (!!o.critic && (w.models || []).includes(o.critic));
|
|
176
|
+
const bench = { unit: 'bench', waveId: `${o.runId}-s1r1`, retryOfWaveId: `${o.runId}-s1`,
|
|
177
|
+
models: [], seats: [], firstFailures: [], srcWaves: [], srcLegs: [] };
|
|
178
|
+
const lensUnits = new Map(); // lensIndex (number, or null for unmappable) -> unit
|
|
179
|
+
// Seeded exactly like `models`, and gated on the same `o.critic` so the two
|
|
180
|
+
// arrays cannot diverge in LENGTH when the critic's seat cannot be resolved
|
|
181
|
+
// (a caller with no o.seats): `[null]` says "unidentified", `[]` would say
|
|
182
|
+
// "no slot at all" and shift the retry roster.
|
|
183
|
+
const criticSeatObj = (o.seats || []).find(s => s.alias === o.critic) || null;
|
|
184
|
+
const criticUnit = { unit: 'critic', waveId: `${o.runId}-c1r1`, retryOfWaveId: `${o.runId}-c1`,
|
|
185
|
+
models: o.critic ? [o.critic] : [], seats: o.critic ? [criticSeatObj] : [],
|
|
186
|
+
firstFailures: [], srcWaves: [], srcLegs: [] };
|
|
187
|
+
|
|
188
|
+
const lensUnitFor = (i) => {
|
|
189
|
+
if (!lensUnits.has(i)) {
|
|
190
|
+
// Task-4 review hardening: an unmappable loss (lensIndexOf resolved
|
|
191
|
+
// neither the waveId convention nor a model-roster membership) must
|
|
192
|
+
// still be GROUPABLE — dropping it here would let it vanish before the
|
|
193
|
+
// orchestrator ever sees it — but must not manufacture a fake
|
|
194
|
+
// `-lnullr1` waveId. The orchestrator refuses to launch any unit with
|
|
195
|
+
// `lensIndex === null` and routes its sources to skipped instead.
|
|
196
|
+
lensUnits.set(i, i === null
|
|
197
|
+
? { unit: 'lens', lensIndex: null, waveId: null, retryOfWaveId: null,
|
|
198
|
+
models: [], seats: [], firstFailures: [], srcWaves: [], srcLegs: [] }
|
|
199
|
+
: { unit: 'lens', lensIndex: i, waveId: `${o.runId}-l${i}r1`,
|
|
200
|
+
retryOfWaveId: `${o.runId}-l${i}`, models: [], seats: [],
|
|
201
|
+
firstFailures: [], srcWaves: [], srcLegs: [] });
|
|
202
|
+
}
|
|
203
|
+
return lensUnits.get(i);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// v4.8 PR2b Task 7 (R-B): a `partial` record is stage1-bind.js's
|
|
207
|
+
// missingSeatDeadWave — ONE seat of a wave that DID return legs, just not
|
|
208
|
+
// this seat's. It retries exactly like a dead wave, but its loss CLASS is
|
|
209
|
+
// 'missing': every still-dead/heal builder reads ff.class, and a 'wave'
|
|
210
|
+
// record makes them claim the wave "produced no legs", which is false.
|
|
211
|
+
// A partial critic record is stamped `-c1`, so it matches isCriticWave and
|
|
212
|
+
// would otherwise be recorded 'wave' on that branch alone.
|
|
213
|
+
const lossClass = w => (w.partial ? 'missing' : 'wave');
|
|
214
|
+
for (const w of deadWaves) {
|
|
215
|
+
const models = w.models || [];
|
|
216
|
+
if (o.lenses) {
|
|
217
|
+
const u = lensUnitFor(lensIndexOf(o, w.waveId, models[0], (w.seats || [])[0] || null));
|
|
218
|
+
u.srcWaves.push(w);
|
|
219
|
+
models.forEach((seat, idx) => recordFailure(u, seat,
|
|
220
|
+
{ seat, class: lossClass(w), waveId: w.waveId, reason: w.reason }, true, (w.seats || [])[idx] || null, twins));
|
|
221
|
+
} else if (isCriticWave(w)) {
|
|
222
|
+
criticUnit.srcWaves.push(w);
|
|
223
|
+
// criticSeatObj rides even though trackModel is false: it is already
|
|
224
|
+
// criticUnit.seats[0], so keying the firstFailure off it is what keeps
|
|
225
|
+
// the dedup key and the roster slot's key the SAME string.
|
|
226
|
+
recordFailure(criticUnit, o.critic,
|
|
227
|
+
{ seat: o.critic, class: lossClass(w), waveId: w.waveId, reason: w.reason }, false, criticSeatObj, twins);
|
|
228
|
+
} else {
|
|
229
|
+
bench.srcWaves.push(w);
|
|
230
|
+
models.forEach((seat, idx) => recordFailure(bench, seat,
|
|
231
|
+
{ seat, class: lossClass(w), waveId: w.waveId, reason: w.reason }, true, (w.seats || [])[idx] || null, twins));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const leg of deadLegs) {
|
|
235
|
+
const seat = leg.modelInput || leg.model;
|
|
236
|
+
const ff = { seat, class: 'leg', status: leg.status, reason: leg.error || null };
|
|
237
|
+
if (o.lenses) {
|
|
238
|
+
const u = lensUnitFor(lensIndexOf(o, null, seat, seatOf.get(leg) || null));
|
|
239
|
+
u.srcLegs.push(leg);
|
|
240
|
+
recordFailure(u, seat, ff, true, seatOf.get(leg) || null, twins);
|
|
241
|
+
} else if (o.critic && seat === o.critic) {
|
|
242
|
+
criticUnit.srcLegs.push(leg);
|
|
243
|
+
recordFailure(criticUnit, seat, ff, false, criticSeatObj, twins);
|
|
244
|
+
} else {
|
|
245
|
+
bench.srcLegs.push(leg);
|
|
246
|
+
recordFailure(bench, seat, ff, true, seatOf.get(leg) || null, twins);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const out = [];
|
|
251
|
+
// Task-4 review hardening: gate on whether the unit received any SOURCE
|
|
252
|
+
// record, not on firstFailures.length — a zero-model dead wave contributes
|
|
253
|
+
// a srcWaves entry but nothing to firstFailures/models (nothing for the
|
|
254
|
+
// `.forEach` above to iterate), and must still surface here so the
|
|
255
|
+
// orchestrator can route it to skipped instead of it vanishing silently.
|
|
256
|
+
if (bench.srcWaves.length > 0 || bench.srcLegs.length > 0) { out.push(bench); }
|
|
257
|
+
if (criticUnit.srcWaves.length > 0 || criticUnit.srcLegs.length > 0) { out.push(criticUnit); }
|
|
258
|
+
// Coordinator-review MINOR-7a: null sorts LAST (Infinity), not first (0) —
|
|
259
|
+
// an unmappable loss is not "lens index 0"; it should not perturb the
|
|
260
|
+
// ascending order of the real, well-indexed lens retries.
|
|
261
|
+
out.push(...[...lensUnits.values()].sort((a, b) => (a.lensIndex ?? Infinity) - (b.lensIndex ?? Infinity)));
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = { lensIndexOf, recordFailure, groupStage1Losses, planStillDeadSources, seatKey,
|
|
266
|
+
twinAliases, legLossKey, srcLegClaimer };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/council/run-retry-keys.js
|
|
2
|
+
'use strict';
|
|
3
|
+
// The Stage-1 loss KEYSPACE: seatKey + twinAliases + legLossKey + srcLegClaimer.
|
|
4
|
+
// Moved verbatim from run-retry-group.js:23-86 AT 3d8f9d38 (v4.8 Phase 2 T-A1 size-gate
|
|
5
|
+
// split, zero behavior). REQUIRE-FREE by design, like ./seats and ./run-stats-entry — it is
|
|
6
|
+
// the leaf the loss keyspace hangs from, so no consumer can land on an import cycle.
|
|
7
|
+
// run-retry-group.js re-exports all four, so no import path in the tree moved.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The one seat-key rule: a seat's id when it was identified, its alias otherwise.
|
|
11
|
+
* Exported so `run-retry-group.js :: recordFailure` and run-retry.js both consume it
|
|
12
|
+
* rather than re-spelling it — two readers of one rule that drift apart is how the
|
|
13
|
+
* alias/seat-id keyspace splits in the first place.
|
|
14
|
+
*/
|
|
15
|
+
const seatKey = (s, alias) => (s ? s.id : alias);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The aliases this run's roster proves are REPEATED, mapped to the COUNT of seats holding
|
|
19
|
+
* each — the only evidence that two losses on one alias are two seats and not one seat
|
|
20
|
+
* losing twice, and the BOUND on the slots they mint. No roster means no proof: empty Map.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ Deliberately NOT `run-retry-group.js :: planStillDeadSources`' own
|
|
23
|
+
* `seatsPerAlias.get(alias) === 1`, and the difference is load-bearing. That rule gates an
|
|
24
|
+
* ANNOUNCEMENT — being wrong costs a duplicate note a reader can see — so with no roster it
|
|
25
|
+
* errs toward announcing. This one gates a RETRY SLOT and a runStats row — being wrong buys
|
|
26
|
+
* a leg for a seat that may not exist — so with no roster it errs toward collapsing, as HEAD
|
|
27
|
+
* always did. ⚠️ That default is the CONSUMER's, not this filter's — with NO roster BOTH spellings
|
|
28
|
+
* give an empty Map. Pinned: run-retry-roster-bound.test.js :: "control: NO roster at all…".
|
|
29
|
+
*/
|
|
30
|
+
function twinAliases(roster) {
|
|
31
|
+
const n = new Map();
|
|
32
|
+
for (const s of roster || []) { if (s && s.alias) { n.set(s.alias, (n.get(s.alias) || 0) + 1); } }
|
|
33
|
+
return new Map([...n].filter(([, c]) => c > 1));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A dead LEG's key where `seatKey` alone names N seats at once: an alias the roster
|
|
38
|
+
* repeats, on a leg no binding could identify. v4.8 T2.2 ruling R2 — MINT a
|
|
39
|
+
* distinguisher where one exists, and the leg arms have one: the leg's own `taskId`
|
|
40
|
+
* (`${waveId}-${n}`, src/sidecar/leg-ids.js:15), stamped even on a leg that never
|
|
41
|
+
* routed (`fanout-leg.js:61`), surviving the disk round-trip, distinct through three
|
|
42
|
+
* of the four ways a leg is orphaned. The fourth — NO `taskId` at all — has genuinely
|
|
43
|
+
* nothing and keeps the collapsing key: that is the honest floor, and inventing one
|
|
44
|
+
* would be the guess this keyspace exists to reject.
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ INTERNAL, never rendered: it joins the dead-seat rows, `attemptedSeats` and
|
|
47
|
+
* `deadLegs0`, nothing else. `ff.seat`/`ff.seatId` stay ALIAS-valued — they become
|
|
48
|
+
* `data.seat` / `data.firstFailure.seatId`, which verdict.js compares to `o.critic`
|
|
49
|
+
* and the Workspace renders as a seat id.
|
|
50
|
+
*/
|
|
51
|
+
function legLossKey(seatObj, alias, leg, twins) {
|
|
52
|
+
const key = seatKey(seatObj, alias);
|
|
53
|
+
if (seatObj || !twins || !twins.has(alias) || !leg || !leg.taskId) { return key; }
|
|
54
|
+
return `${key}\u0000${leg.taskId}`; // NUL: impossible in an alias, id or taskId
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A STATEFUL claimer over `srcLegs` — build ONE per unit, never reuse it: each call CONSUMES
|
|
59
|
+
* and returns one leg whose key matches, never hands the same leg out twice, and returns null
|
|
60
|
+
* once that key is exhausted (`(key) => leg | null`). It replaces `srcLegs.find(...)`, which
|
|
61
|
+
* returned the FIRST match every time, so two unattributable twins on one alias both recorded
|
|
62
|
+
* the SAME source and the second seat — a leg the run paid for — vanished from stillDeadLegs.
|
|
63
|
+
* Which dead source pairs with which dead outcome is unknowable AND immaterial: every candidate
|
|
64
|
+
* is still dead after its retry, so one apiece yields the exact SET (their rows carry no seat).
|
|
65
|
+
*/
|
|
66
|
+
function srcLegClaimer(srcLegs, keyOfSrc) {
|
|
67
|
+
const pool = new Set(srcLegs);
|
|
68
|
+
return (key) => {
|
|
69
|
+
for (const l of pool) { if (keyOfSrc(l) === key) { pool.delete(l); return l; } } // exits on the delete
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { seatKey, twinAliases, legLossKey, srcLegClaimer };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/council/run-retry-launch.js
|
|
2
|
+
'use strict';
|
|
3
|
+
// The retry pass's LAUNCH side: briefingFor + bindRetryWave. Lifted out of
|
|
4
|
+
// run-retry.js (v4.8 Phase 2 T-A2 size-gate split, zero behavior) — briefingFor
|
|
5
|
+
// byte-for-byte, the roster pad/bind block as a PURE function that returns its
|
|
6
|
+
// bindings rather than mutating the orchestrator's accumulator.
|
|
7
|
+
// run-retry.js requires both back, so no existing import path moved.
|
|
8
|
+
// v4.8 SI-27 (ruling R14) landed: the pad/bind/drop CORE now lives in
|
|
9
|
+
// `stage1-bind.js :: bindPaddedWave`, shared with run-stage2.js and
|
|
10
|
+
// run-debate-revote.js. What stays here is retry-specific — WHICH array is the
|
|
11
|
+
// roster, and the orphan tail. ⚠️ `bindStage1Waves` in that same file is still a
|
|
12
|
+
// different contract (one call per Stage-1 wave, a real roster, no padding, no
|
|
13
|
+
// placeholders); `bindPaddedWave` is the one this file calls.
|
|
14
|
+
|
|
15
|
+
const briefings = require('./briefings');
|
|
16
|
+
const { bindPaddedWave } = require('./stage1-bind');
|
|
17
|
+
|
|
18
|
+
/** The briefing a retry unit re-issues — same builders Stage 1 used. */
|
|
19
|
+
function briefingFor(o, unit) {
|
|
20
|
+
if (unit.unit === 'critic') { return briefings.buildCriticBriefing({ briefing: o.briefing, date: o.date }); }
|
|
21
|
+
if (unit.unit === 'lens') {
|
|
22
|
+
return briefings.buildLensBriefing({ lens: o.lenses[unit.lensIndex - 1], briefing: o.briefing, date: o.date });
|
|
23
|
+
}
|
|
24
|
+
return briefings.buildSeatBriefing({ briefing: o.briefing, date: o.date });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Bind a retry wave's returned legs to the unit that launched them.
|
|
29
|
+
*
|
|
30
|
+
* PURE by design: the caller owns the accumulator. `retrySeatOf` is published into
|
|
31
|
+
* `out.seatOf` and the orphan legs are re-emitted with their waveId by
|
|
32
|
+
* `run-retry.js :: retryStage1Losses`, which also carries the rationale for why an
|
|
33
|
+
* orphan is REPORTED rather than noted as a degrade.
|
|
34
|
+
*
|
|
35
|
+
* @param {{seats: Array<?object>, models: Array<string>, waveId: string}} unit
|
|
36
|
+
* @param {Array<object>} legs the retry wave's returned legs
|
|
37
|
+
* @returns {{retrySeatOf: Map<object, object>, orphanLegs: Array<object>}}
|
|
38
|
+
*/
|
|
39
|
+
function bindRetryWave(unit, legs) {
|
|
40
|
+
// The retry wave's roster IS unit.seats — run-retry-group.js :: recordFailure
|
|
41
|
+
// pushes models and seats in lockstep (it pushes NEITHER for the critic unit,
|
|
42
|
+
// whose pair is seeded off one `o.critic` gate at creation and never grows), and
|
|
43
|
+
// the legId `-N` suffix slot-indexes that same launch plan. `firstFailures` is a
|
|
44
|
+
// WEAKER case and is not read here — see run-retry.js's mint.
|
|
45
|
+
// A null entry means "we could not identify this seat". Why that hole is
|
|
46
|
+
// PADDED rather than filtered, and why the placeholders are tracked by
|
|
47
|
+
// identity rather than an id-name prefix test, are in the docblock of
|
|
48
|
+
// `stage1-bind.js :: bindPaddedWave` — which owns all three steps.
|
|
49
|
+
const { seatOf, bindRes } = bindPaddedWave(unit.waveId, unit.seats, i => unit.models[i], legs);
|
|
50
|
+
// The tail: this site hands its orphans back to the caller (run-retry.js ::
|
|
51
|
+
// retryStage1Losses re-emits them with their waveId).
|
|
52
|
+
return { retrySeatOf: seatOf, orphanLegs: bindRes.orphanLegs };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { briefingFor, bindRetryWave };
|
|
@@ -17,36 +17,85 @@ const legEffect = (counts) =>
|
|
|
17
17
|
`${counts.reviewed} of ${counts.total} seats reviewed; `
|
|
18
18
|
+ 'the run continues with the bench that did and will exit degraded (2)';
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Wave-origin, retry wave died wholesale (D5 wave granularity).
|
|
22
|
+
*
|
|
23
|
+
* v4.8 PR2b Task 7: a `partial` record (stage1-bind.js's missingSeatDeadWave) is
|
|
24
|
+
* ONE seat of a wave that DID return legs, so the dead-wave sentence would be a
|
|
25
|
+
* false statement in a user-facing degrade — it names the seat instead, on the
|
|
26
|
+
* `seat-unbound` channel the other half of that join failure already uses.
|
|
27
|
+
*/
|
|
21
28
|
function waveStillDeadNote(w, unit) {
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
const partial = !!w.partial;
|
|
30
|
+
return { channel: partial ? 'seat-unbound' : 'dead-wave',
|
|
31
|
+
what: partial
|
|
32
|
+
? `seat ${(w.models || [])[0]} did not review`
|
|
33
|
+
: `Stage-1 wave ${w.waveId} (${(w.models || []).join(', ') || 'no models'}) produced NO legs`,
|
|
24
34
|
// Coordinator-review MINOR-7c: a falsy w.reason must not render as the
|
|
25
35
|
// literal string "undefined" in the why text.
|
|
26
36
|
why: `${w.reason || 'no reason recorded'}; the once-only retry wave also produced no legs`,
|
|
27
37
|
effect: 'Those seats are NOT in this council. The run continues with the bench that did '
|
|
28
38
|
+ 'launch and will exit degraded (2)',
|
|
29
|
-
|
|
39
|
+
// `seat` rides ONLY on the partial shape: adding it unconditionally breaks
|
|
40
|
+
// degrade-channels.test.js's exact toEqual on a real dead wave. It stays the
|
|
41
|
+
// ALIAS because verdict.js:72 compares data.seat against o.critic, an alias,
|
|
42
|
+
// and because it is the same key the dead-leg shape uses — one vocabulary
|
|
43
|
+
// for one field. ⚠️ It is NOT read by the Workspace today: the kind/channel filters in
|
|
44
|
+
// live-dead-seats.js (deadSeats) and workspace-seats.js (retriedSeats), and verdict.js's
|
|
45
|
+
// own two, all admit only dead-leg/dead-wave — so nothing consumes `seat-unbound` yet.
|
|
46
|
+
// Cited by SYMBOL, not line: these three references rotted twice during v4.8 PR5c alone.
|
|
47
|
+
data: { waveId: w.waveId, models: w.models, reason: w.reason, retryWaveId: unit.waveId,
|
|
48
|
+
// v4.8 PR5c: seat identity, index-parallel with `models`, on the dead-wave arm only
|
|
49
|
+
// (the partial arm names ONE seat and rides `seat` above; `seat-unbound` has no
|
|
50
|
+
// consumer, so an array there would be unpinned surface for no gain).
|
|
51
|
+
// ⚠️ An unidentified slot emits `null`, NEVER the alias. Collapsing it onto the alias
|
|
52
|
+
// makes it indistinguishable from a second reference to that alias, and no consumer
|
|
53
|
+
// can recover the difference — deadSeats has no per-alias seat count. That collapse
|
|
54
|
+
// is what made two distinct dead twins render as a single row.
|
|
55
|
+
...(partial ? { seat: (w.models || [])[0] } : {
|
|
56
|
+
seats: (w.models || []).map((m, i) => {
|
|
57
|
+
const so = (w.seats || [])[i];
|
|
58
|
+
return so ? so.id : null;
|
|
59
|
+
}),
|
|
60
|
+
}) } };
|
|
30
61
|
}
|
|
31
62
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Leg-origin, retry wave died wholesale (bench-batch case).
|
|
65
|
+
*
|
|
66
|
+
* v4.8 PR5c: `seatId` is the caller's Stage-1 leg->seat binding, or null when the leg was
|
|
67
|
+
* never bound. It is a SEPARATE key from `seat`, which stays the ALIAS — verdict.js:72
|
|
68
|
+
* compares `data.seat` against `o.critic`, an alias, so re-pointing it breaks critic-loss
|
|
69
|
+
* detection. Add a key; never repurpose that one.
|
|
70
|
+
*/
|
|
71
|
+
function srcLegStillDeadNote(leg, unit, counts, seatId = null) {
|
|
34
72
|
const seat = leg.modelInput || leg.model;
|
|
35
73
|
return { channel: 'dead-leg', what: `seat ${seat} did not review`,
|
|
36
74
|
why: `the leg ended '${leg.status}'${leg.error ? `: ${leg.error}` : ''} with no usable output; `
|
|
37
75
|
+ 'its once-only retry wave produced no legs',
|
|
38
76
|
effect: legEffect(counts),
|
|
39
|
-
data: { seat, status: leg.status, reason: leg.error || null,
|
|
77
|
+
data: { seat, seatId: seatId || null, status: leg.status, reason: leg.error || null,
|
|
78
|
+
retryWaveId: unit.waveId } };
|
|
40
79
|
}
|
|
41
80
|
|
|
42
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Either origin, the retry produced legs but THIS seat's retry leg died.
|
|
83
|
+
*
|
|
84
|
+
* A 'missing' first failure has a `reason` and a `waveId` but NEVER a `status` —
|
|
85
|
+
* the leg arm would report that its first leg "ended 'undefined'" for a seat
|
|
86
|
+
* that never had a first leg at all.
|
|
87
|
+
*/
|
|
43
88
|
function retryLegStillDeadNote(seat, ff, retryLeg, unit, counts) {
|
|
89
|
+
const missing = !!(ff && ff.class === 'missing');
|
|
44
90
|
const why = ff && ff.class === 'wave'
|
|
45
91
|
? `its first wave ${ff.waveId} produced no legs (${ff.reason}); `
|
|
46
92
|
+ `its once-only retry leg ended '${retryLeg.status}' with no usable output`
|
|
47
|
-
:
|
|
48
|
-
|
|
49
|
-
|
|
93
|
+
: missing
|
|
94
|
+
? `${ff.reason} in wave ${ff.waveId}; its once-only retry leg ended `
|
|
95
|
+
+ `'${retryLeg.status}' with no usable output`
|
|
96
|
+
: `the leg ended '${ff ? ff.status : 'unknown'}'${ff && ff.reason ? `: ${ff.reason}` : ''} `
|
|
97
|
+
+ `with no usable output; its once-only retry also ended '${retryLeg.status}'`;
|
|
98
|
+
return { channel: missing ? 'seat-unbound' : 'dead-leg', what: `seat ${seat} did not review`, why,
|
|
50
99
|
effect: legEffect(counts),
|
|
51
100
|
data: { seat, status: retryLeg.status, reason: retryLeg.error || null,
|
|
52
101
|
firstFailure: ff, retryWaveId: unit.waveId } };
|
|
@@ -62,10 +111,13 @@ function retryLegStillDeadNote(seat, ff, retryLeg, unit, counts) {
|
|
|
62
111
|
* this time.
|
|
63
112
|
*/
|
|
64
113
|
function missingLegStillDeadNote(seat, ff, unit, counts) {
|
|
114
|
+
const missing = !!(ff && ff.class === 'missing');
|
|
65
115
|
const fact = ff && ff.class === 'wave'
|
|
66
116
|
? `its first wave ${ff.waveId} produced no legs (${ff.reason})`
|
|
67
|
-
:
|
|
68
|
-
|
|
117
|
+
: missing
|
|
118
|
+
? `${ff.reason} in wave ${ff.waveId}`
|
|
119
|
+
: `the leg ended '${ff ? ff.status : 'unknown'}'${ff && ff.reason ? `: ${ff.reason}` : ''} with no usable output`;
|
|
120
|
+
return { channel: missing ? 'seat-unbound' : 'dead-leg', what: `seat ${seat} did not review`,
|
|
69
121
|
why: `${fact}; its once-only retry produced no leg for this seat`,
|
|
70
122
|
effect: legEffect(counts),
|
|
71
123
|
data: { seat, status: null, reason: null, firstFailure: ff, retryWaveId: unit.waveId } };
|