amicus 4.7.1 → 4.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +988 -1
- package/README.md +5 -4
- package/docs/CITATIONS.md +122 -0
- package/docs/ROADMAP.md +33 -5
- package/docs/SHIMS.md +1 -1
- package/docs/configuration.md +3 -3
- package/docs/council.md +179 -32
- package/docs/doc-system.md +1 -1
- package/docs/publishing.md +2 -0
- package/docs/troubleshooting.md +3 -3
- package/docs/usage.md +2 -2
- package/electron/ipc-setup.js +18 -2
- package/electron/main.js +46 -3
- package/electron/setup-ui-aliases.js +2 -2
- package/electron/setup-ui-model.js +99 -9
- package/electron/setup-ui-styles.js +22 -0
- package/electron/setup-ui.js +231 -29
- 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 +4 -1
- 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/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/src/cli-handlers-doctor.js +18 -14
- 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/parse-stage2.js +1 -1
- 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 +45 -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 +166 -161
- 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 +20 -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/fanout-leg.js +22 -1
- 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 +1 -1
- package/src/sidecar/setup.js +137 -2
- package/src/utils/alias-audit.js +81 -3
- 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-alias-check.js +152 -0
- package/src/utils/model-canonicalization.js +64 -0
- package/src/utils/model-shortlist.js +100 -0
- package/src/utils/no-output-backstop.js +1 -1
- package/src/utils/provider-default-picker.js +93 -45
- package/src/utils/provider-default-prompt.js +1 -1
- package/src/utils/quick-picks.js +2 -2
- package/src/utils/remediation-hints.js +37 -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 +30 -4
- package/src/workspace/seat-space.js +143 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// v4.8 PR5c: deadSeats, split out of live-seats.js for the 300-line gate — the FOURTH
|
|
3
|
+
// extraction this renderer's load-order list has absorbed. Moved verbatim from
|
|
4
|
+
// live-seats.js (which was itself split out of live-model.js in PR0), then seat-keyed.
|
|
5
|
+
// Self-contained: it reads only its four arguments and owns its own isReviewing.
|
|
6
|
+
(function () {
|
|
7
|
+
/**
|
|
8
|
+
* D6 (v4.6.2 PR4 Task 2, "dead-seat rows"): announced-dead seats, unioned
|
|
9
|
+
* from the run's own `degrades[]` (dead-leg/dead-wave channels — the live
|
|
10
|
+
* announcement; shapes verified against src/council/run-retry-notes.js and
|
|
11
|
+
* the skipped-path notes at src/council/run-stages.js:155-174) and the
|
|
12
|
+
* verdict's derived `seatLoss` (verdict.js summarizeSeatLoss/deriveSeatLoss
|
|
13
|
+
* — the critic-loss backstop, kept for verdicts written before `degrades[]`
|
|
14
|
+
* existed, v4.5.2 precedent). De-duped by seat model: `degrades` is scanned
|
|
15
|
+
* FIRST, so a real record's retry marker always beats the backstop's
|
|
16
|
+
* no-data guess for the same seat.
|
|
17
|
+
*
|
|
18
|
+
* `retried` reads `data.retryWaveId` / `data.firstFailure`: every
|
|
19
|
+
* still-dead-after-retry note (run-retry-notes.js's four builders) carries
|
|
20
|
+
* `retryWaveId`; the two run-stages.js skipped-path notes (fired when the
|
|
21
|
+
* once-only retry pass never even attempted the seat — an unmappable lens
|
|
22
|
+
* loss, an out-of-range index, or a zero-model unit) carry neither, so they
|
|
23
|
+
* correctly fall back to the plain phrasing.
|
|
24
|
+
*
|
|
25
|
+
* D6 filter (zero usable legs ONLY, "no ghost when a live row already
|
|
26
|
+
* exists"): a candidate already present in `liveSeats` is dropped. Two
|
|
27
|
+
* different things can put it there, and both are reasons to suppress:
|
|
28
|
+
* an SL-2 retry actually healed it (a real recovered review), OR
|
|
29
|
+
* (owner-ruled, v4.7 CA-4 dead-seat convergence) the seat never recovered
|
|
30
|
+
* at all but the row-per-launch machinery still gives its dead leg an
|
|
31
|
+
* honest primary ERROR row (every billed leg gets a row now, including
|
|
32
|
+
* failures — run-stages.js/run-assemble.js) — so "it has a cost row" no
|
|
33
|
+
* longer implies "it healed". Either way that live row IS the seat's
|
|
34
|
+
* record; rendering a second "did not review" ghost row beside it would
|
|
35
|
+
* be a duplicate, not new information, so suppressing it here is the
|
|
36
|
+
* ACCEPTED terminal-path behavior: exactly one row per seat, whatever its
|
|
37
|
+
* status. This is the one thing standing between a recovered (or
|
|
38
|
+
* honestly-erred) seat and a duplicate/ghost row — same failure family as
|
|
39
|
+
* the F37 debate-role collision and the RN-11 keyed-row lessons just above
|
|
40
|
+
* (seatsFromRunStats, seatCells): an identity that is not carefully
|
|
41
|
+
* matched silently duplicates or overwrites instead of failing loud.
|
|
42
|
+
*
|
|
43
|
+
* Role-aware D6 (v4.6.3 PR2, spec D3): a bare model match used to suppress
|
|
44
|
+
* regardless of what the LIVE row's role was — so a model that died as
|
|
45
|
+
* critic but whose chair-fallback walk happened to land on that same alias
|
|
46
|
+
* (and succeeded, producing a live `role: 'chair'` cost row) silently
|
|
47
|
+
* erased the dead-critic row it was never a replacement for (spec §5, the
|
|
48
|
+
* PR 102 rider). Candidates now carry a `role` (`'critic'` via alias equality
|
|
49
|
+
* with `runMeta.critic` — mirroring `deriveSeatLoss`, verdict.js:72 — or
|
|
50
|
+
* `null`), and only REVIEWING-role live legs (`seat`/`critic`/`lens:*`)
|
|
51
|
+
* suppress at all; a `'critic'` candidate is cleared only by a live
|
|
52
|
+
* CRITIC-role leg for that alias, never by a chair/judge/rebuttal/revote
|
|
53
|
+
* row landing on the same model. Hidden dependency: the recovered-critic
|
|
54
|
+
* suppression below (`byRole[alias + '|critic']`) relies on `roleFor`'s
|
|
55
|
+
* critic branch (src/council/run-stages.js), which only fires when lenses
|
|
56
|
+
* are absent — safe today only because --critic and --lenses are mutually
|
|
57
|
+
* exclusive (src/cli-handlers-council-run.js's `critic && lenses` check); if
|
|
58
|
+
* that exclusion ever loosens, a healed critic on a lens run would carry
|
|
59
|
+
* role 'seat' and this suppression would render a ghost dead row for it.
|
|
60
|
+
*
|
|
61
|
+
* Old-run resilience (v4.6.3 PR2, spec D4): pre-`degrades[]` runs (v4.5.2)
|
|
62
|
+
* carry the BENCH half of a seat loss only in `seatLoss.deadBenchSeats`
|
|
63
|
+
* (string[] of aliases, verdict.js deriveSeatLoss) — `degrades[]` never
|
|
64
|
+
* existed on either doc for these runs. Consumed after the critic backstop,
|
|
65
|
+
* candidates get `role: null` (deadBenchSeats carries no critic/bench
|
|
66
|
+
* distinction beyond what `criticRequested` already covers above) and flow
|
|
67
|
+
* through the same dedup and role-aware suppression as every other candidate.
|
|
68
|
+
*
|
|
69
|
+
* ⚠️ v4.8 PR5c corrected HOW that dedup works, and the old description here was
|
|
70
|
+
* wrong once seats were keyed. Candidates arrive in FOUR identity flavours, and the
|
|
71
|
+
* flavour decides both the dedup key and whether the candidate may be absorbed:
|
|
72
|
+
* 'keyed' a real seat id -> dedup on it; two dead twins are two rows.
|
|
73
|
+
* 'unid' a wave slot the producer could not name (`seats[i] === null`). A
|
|
74
|
+
* DISTINCT seat we cannot spell, so it takes a private token; keying
|
|
75
|
+
* it on the alias is what collapsed two dead twins into one row.
|
|
76
|
+
* 'legacy' a pre-PR5c record with no seat id anywhere -> dedup on the alias,
|
|
77
|
+
* which still collapses twins. Disclosed residual R2.
|
|
78
|
+
* 'derivative' seatLoss-sourced. verdict.js:86 builds deadBenchSeats FROM the same
|
|
79
|
+
* dead legs that emit degrades[], so it is always a duplicate and is
|
|
80
|
+
* the ONLY flavour that may be absorbed by an alias already covered.
|
|
81
|
+
* Absorbing a real degrade instead would erase an unidentified twin.
|
|
82
|
+
*
|
|
83
|
+
* @param {Array<object>} degrades run.json's `degrades[]` (may be absent)
|
|
84
|
+
* @param {?object} seatLoss verdict.json's `seatLoss` (may be absent) —
|
|
85
|
+
* `criticRequested`/`criticSeated` back the critic candidate above,
|
|
86
|
+
* `deadBenchSeats` (string[] of aliases) feeds the bench candidates below
|
|
87
|
+
* @param {Array<{model: string}>} liveSeats seatsFromRunStats(...)'s output
|
|
88
|
+
* (or any seat list keyed the same way — the live seat map)
|
|
89
|
+
* @param {?{critic: ?string}} runMeta run.critic (alias, or null/absent
|
|
90
|
+
* when no critic was requested) — degrade records carry no role field, so
|
|
91
|
+
* this is the ONLY way a degrade-sourced candidate is identified as critic
|
|
92
|
+
* @returns {Array<{model: string, seat: ?string, statusText: string, role: ?string}>}
|
|
93
|
+
* `model` is the ALIAS (what the row displays, what labelOf keys on); `seat` is
|
|
94
|
+
* the seat id when the record named one, else null.
|
|
95
|
+
*/
|
|
96
|
+
function deadSeats(degrades, seatLoss, liveSeats, runMeta) {
|
|
97
|
+
var critic = runMeta && runMeta.critic ? runMeta.critic : null;
|
|
98
|
+
// ⚠️ Object.create(null) throughout this family (also workspace-render.js's
|
|
99
|
+
// `existing`/`seen` and workspace-app.js's `labelByModel`): a model literally
|
|
100
|
+
// named `toString` is truthy off a bare object, so it was dropped here and —
|
|
101
|
+
// worse — crashed workspace-render.js:212 reading `.children` off an inherited
|
|
102
|
+
// function, killing the seats repaint and every tick after it.
|
|
103
|
+
var seen = Object.create(null);
|
|
104
|
+
var covered = Object.create(null);
|
|
105
|
+
var seenUnid = Object.create(null);
|
|
106
|
+
var order = [];
|
|
107
|
+
// `src` is the candidate's identity flavour — see the four-flavour table in the
|
|
108
|
+
// docblock above, which is what decides the key and the absorb rule.
|
|
109
|
+
function add(key, alias, retried, role, src, slotOf) {
|
|
110
|
+
if (!alias) { return; }
|
|
111
|
+
// ⛔ An 'unid' candidate was originally given NO dedup at all, on the grounds that it is
|
|
112
|
+
// one wave slot and distinct by construction. That holds WITHIN a record's seats[]; it
|
|
113
|
+
// does NOT hold across records, and the same dead-wave record appearing twice rendered
|
|
114
|
+
// four rows for two seats. Deduped on (waveId, slot) instead — its own namespace, never
|
|
115
|
+
// `seen`, so an invented key can never collide with a real alias. (The seat literally
|
|
116
|
+
// named `toString` is why this family refuses keys that share the alias keyspace.)
|
|
117
|
+
if (src === 'unid') {
|
|
118
|
+
var uk = ((slotOf && slotOf.waveId) || '') + '#' + (slotOf ? slotOf.i : 0);
|
|
119
|
+
if (seenUnid[uk]) { return; }
|
|
120
|
+
seenUnid[uk] = true;
|
|
121
|
+
}
|
|
122
|
+
if (src !== 'unid') {
|
|
123
|
+
var k = src === 'keyed' ? key : alias;
|
|
124
|
+
if (seen[k]) { return; }
|
|
125
|
+
if (src === 'derivative' && covered[alias]) { return; }
|
|
126
|
+
seen[k] = true;
|
|
127
|
+
}
|
|
128
|
+
// Covered either way: this alias HAS been announced, so a later seatLoss-derived
|
|
129
|
+
// duplicate naming it must still be absorbed.
|
|
130
|
+
covered[alias] = true;
|
|
131
|
+
var row = {
|
|
132
|
+
model: alias,
|
|
133
|
+
seat: src === 'keyed' ? key : null,
|
|
134
|
+
role: role || null,
|
|
135
|
+
statusText: retried ? 'did not review — retried once' : 'did not review',
|
|
136
|
+
};
|
|
137
|
+
// Set only when true, so every other row's shape is byte-identical to before —
|
|
138
|
+
// several suites assert these rows with an exact toEqual.
|
|
139
|
+
if (src === 'unid') { row.unnamed = true; }
|
|
140
|
+
order.push(row);
|
|
141
|
+
}
|
|
142
|
+
(degrades || []).forEach(function (d) {
|
|
143
|
+
if (!d || d.kind !== 'degrade') { return; }
|
|
144
|
+
if (d.channel !== 'dead-leg' && d.channel !== 'dead-wave') { return; }
|
|
145
|
+
var data = d.data || {};
|
|
146
|
+
var retried = !!(data.retryWaveId || data.firstFailure);
|
|
147
|
+
// Critic identification mirrors deriveSeatLoss (verdict.js): alias
|
|
148
|
+
// equality with run.critic — degrade records carry no role field.
|
|
149
|
+
if (d.channel === 'dead-leg') {
|
|
150
|
+
var lk = (data.firstFailure && data.firstFailure.seatId) || data.seatId || null;
|
|
151
|
+
add(lk, data.seat, retried, critic && data.seat === critic ? 'critic' : null,
|
|
152
|
+
lk ? 'keyed' : 'legacy');
|
|
153
|
+
} else {
|
|
154
|
+
// `seats[]` is index-parallel with `models[]` (PR5c Task 1). Its ABSENCE means a
|
|
155
|
+
// legacy record; a null ELEMENT means an unidentified seat. Different statements.
|
|
156
|
+
var hasSeats = Array.isArray(data.seats);
|
|
157
|
+
(data.models || []).forEach(function (m, i) {
|
|
158
|
+
var wk = hasSeats ? data.seats[i] : null;
|
|
159
|
+
add(wk, m, retried, critic && m === critic ? 'critic' : null,
|
|
160
|
+
wk ? 'keyed' : (hasSeats ? 'unid' : 'legacy'), { waveId: data.waveId, i: i });
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
if (seatLoss && seatLoss.criticRequested && !seatLoss.criticSeated) {
|
|
165
|
+
add(null, seatLoss.criticRequested, false, 'critic', 'derivative');
|
|
166
|
+
}
|
|
167
|
+
if (seatLoss) {
|
|
168
|
+
// Pre-degrades[] era (v4.5.2): the bench half of a seat loss lives
|
|
169
|
+
// only here. Alias strings; deriveSeatLoss does not dedup — `seen`
|
|
170
|
+
// absorbs repeats and degrade-sourced duplicates.
|
|
171
|
+
(seatLoss.deadBenchSeats || []).forEach(function (m) {
|
|
172
|
+
add(null, m, false, null, 'derivative');
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// Role-aware D6 (v4.6.3 PR2): only REVIEWING-role live legs suppress —
|
|
176
|
+
// a chair/judge/rebuttal/revote row must not hide a dead reviewer, and
|
|
177
|
+
// a dead-critic candidate is cleared only by a live CRITIC leg. A null
|
|
178
|
+
// role is NOT reviewing: counting it would suppress silently, the exact
|
|
179
|
+
// class the announcement invariant forbids.
|
|
180
|
+
// Role 'claude' is deliberately absent: it is emitted only by claudeRunStatsRow
|
|
181
|
+
// (src/council/run-assemble.js:129-132) for a seat that never launches a leg, and
|
|
182
|
+
// preflightClaudeReview (run-assemble.js:86-102) rejects 'claude' as chair/critic/
|
|
183
|
+
// bench — so no 'claude' leg can die. If that reservation ever loosens, this
|
|
184
|
+
// allowlist is the single place to extend.
|
|
185
|
+
function isReviewing(role) {
|
|
186
|
+
return role === 'seat' || role === 'critic' ||
|
|
187
|
+
(typeof role === 'string' && role.indexOf('lens:') === 0);
|
|
188
|
+
}
|
|
189
|
+
var reviewing = Object.create(null);
|
|
190
|
+
var byRole = Object.create(null);
|
|
191
|
+
// ⚠️ Fable review (PR4b fix wave): same F34/F36 alias-selection seatCells already uses
|
|
192
|
+
// (`seat.modelInput || seat.model`, above) — a LIVE payload seat's `model` is the RESOLVED
|
|
193
|
+
// executable id, not the alias a degrade record names; `modelInput` carries the alias.
|
|
194
|
+
// Keying this map on `s.model` alone meant a dead-leg seat whose errored roster row was
|
|
195
|
+
// still in the active stage's `liveSeats` never matched its alias-keyed degrade candidate,
|
|
196
|
+
// so D6 failed to suppress it — both rows rendered until the stage boundary dropped the
|
|
197
|
+
// errored row. Terminal-path cost rows (seatsFromRunStats) carry no `modelInput` at all and
|
|
198
|
+
// are already alias-only, so `|| s.model` leaves that path unchanged.
|
|
199
|
+
(liveSeats || []).forEach(function (s) {
|
|
200
|
+
if (!isReviewing(s.role)) { return; }
|
|
201
|
+
var alias = s.modelInput || s.model; // F36: alias space, never resolved ids
|
|
202
|
+
reviewing[alias] = true;
|
|
203
|
+
// Both spaces. The alias arm keeps legacy/alias-only candidates suppressible; the seat
|
|
204
|
+
// arm is what lets a dead twin survive beside a live one. Guarded: `s.seat` is null on a
|
|
205
|
+
// unique-alias bench on BOTH paths — the terminal one (run-stats-entry.js ::
|
|
206
|
+
// buildRunStatsEntry) and, since v4.8 R5, the live tick (live-normalize.js :: seatOf).
|
|
207
|
+
// Both spell the same emit-when-DIFFERENT predicate, `seat.id !== seat.alias`, so the two
|
|
208
|
+
// producers cannot disagree. A bare insert would write a STRING key for every such seat.
|
|
209
|
+
if (s.seat) { reviewing[s.seat] = true; }
|
|
210
|
+
byRole[alias + '|' + s.role] = true;
|
|
211
|
+
});
|
|
212
|
+
return order.filter(function (s) {
|
|
213
|
+
if (s.role === 'critic') { return !byRole[s.model + '|critic']; }
|
|
214
|
+
// An UNNAMED dead seat is never suppressed by alias evidence. The producer emitted
|
|
215
|
+
// `null` rather than the alias precisely because "unidentified" and "the alias" are
|
|
216
|
+
// different statements; falling back to the alias here would re-assert the equivalence
|
|
217
|
+
// that emission removed. A live d#1 does not prove the dead seat was d#1 — and a degrade
|
|
218
|
+
// record means it stayed dead after its retry, so any live seat sharing the alias is a
|
|
219
|
+
// DIFFERENT seat. Suppressing would be a silent loss on no evidence.
|
|
220
|
+
if (s.unnamed) { return true; }
|
|
221
|
+
return !reviewing[s.seat || s.model];
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
var api = { deadSeats: deadSeats };
|
|
226
|
+
if (typeof module !== 'undefined' && module.exports) { module.exports = api; }
|
|
227
|
+
if (typeof window !== 'undefined') { window.AmicusDeadSeats = api; }
|
|
228
|
+
})();
|
|
@@ -50,244 +50,18 @@
|
|
|
50
50
|
return !isTerminal(status);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*/
|
|
61
|
-
function seatCells(seat, blindOn, labelOf) {
|
|
62
|
-
// ⚠️ DE-ROT (F36): key the lookup on the council ALIAS, never on `seat.model`. labelMap's
|
|
63
|
-
// values are aliases; a LIVE leg's `model` is the resolved executable id, so labelOf(model)
|
|
64
|
-
// misses and Blind ON renders the real model. `modelInput` is the alias Task 0.5 stamps per
|
|
65
|
-
// leg (terminal rows from runStats are already alias-only, so the fallback is exact there).
|
|
66
|
-
var alias = seat.modelInput || seat.model;
|
|
67
|
-
var label = labelOf ? labelOf(alias) : null;
|
|
68
|
-
// ⚠️ v4.4.1 RN-9: this hand-rolled flip is LOAD-BEARING — do NOT "fix" it into
|
|
69
|
-
// AmicusRender.display({model: seat.model, label: label}, blindOn). The other two copies
|
|
70
|
-
// (workspace-panels.js's review/judge titles) were routed through display() because their
|
|
71
|
-
// pair's `model` IS the identity they must print. Here it is not: the seat's printable
|
|
72
|
-
// identity is the council ALIAS resolved on the line above (F36 — a live leg's `seat.model`
|
|
73
|
-
// is the RESOLVED executable id, e.g. `google/gemini-2.5-pro`, which labelMap never keys on),
|
|
74
|
-
// so display()'s blind-OFF arm would print the resolved id and undo F36. This module is also
|
|
75
|
-
// node-tested with NO DOM and no window.AmicusRender to call. Deliberate third copy.
|
|
76
|
-
var name = blindOn && label ? label : alias;
|
|
77
|
-
var tokens = (seat.tokensIn === null || seat.tokensIn === undefined) &&
|
|
78
|
-
(seat.tokensOut === null || seat.tokensOut === undefined)
|
|
79
|
-
? '—'
|
|
80
|
-
: dash(seat.tokensIn) + '/' + dash(seat.tokensOut);
|
|
81
|
-
return [
|
|
82
|
-
dash(name),
|
|
83
|
-
dash(seat.role),
|
|
84
|
-
dash(seat.status),
|
|
85
|
-
dash(seat.stage),
|
|
86
|
-
dash(seat.messages),
|
|
87
|
-
tokens,
|
|
88
|
-
dash(seat.costDisplay),
|
|
89
|
-
// ⚠️ DE-ROT (F35): pass-through of an ISO timestamp, NOT a relative string. renderSeats
|
|
90
|
-
// formats it with relTime() before calling in — live-model.js is node-tested and loads
|
|
91
|
-
// before workspace-render.js, so it cannot reach relTime itself.
|
|
92
|
-
dash(seat.lastActivity),
|
|
93
|
-
seat.stalled ? '⏳ stalled' : '',
|
|
94
|
-
];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// v4.7 D6/E1: three row-per-launch producer roles added alongside the
|
|
98
|
-
// existing chair-attempt (run-chair.js), repair (run-stages.js/
|
|
99
|
-
// run-stage2.js/run-chair.js) and superseded (run-stages.js + debate.js)
|
|
100
|
-
// rows — launch-accounting extras, not seats, and unlike rebuttal/revote
|
|
101
|
-
// (F37, kept rendering on purpose below) they have no seats-panel meaning
|
|
102
|
-
// of their own. There is no pre-existing allowlist in this function (E1) —
|
|
103
|
-
// this is a plain exclusion added on top of the untouched id/shape logic.
|
|
104
|
-
// Object.create(null): a plain `{...}` literal inherits Object.prototype, so a role
|
|
105
|
-
// literally named 'constructor'/'toString'/etc would resolve to an inherited (truthy)
|
|
106
|
-
// function via bracket lookup instead of `undefined` — silently mis-excluding (or
|
|
107
|
-
// mis-including) that seat. A null-prototype object has no inherited keys to collide with.
|
|
108
|
-
var SEATS_PANEL_EXCLUDED_ROLES = Object.create(null);
|
|
109
|
-
SEATS_PANEL_EXCLUDED_ROLES['chair-attempt'] = true;
|
|
110
|
-
SEATS_PANEL_EXCLUDED_ROLES.repair = true;
|
|
111
|
-
SEATS_PANEL_EXCLUDED_ROLES.superseded = true;
|
|
112
|
-
|
|
113
|
-
/** Terminal fallback: derive seat-shaped rows from tally runStats cost rows. */
|
|
114
|
-
function seatsFromRunStats(costRows) {
|
|
115
|
-
return (costRows || []).filter(function (r) {
|
|
116
|
-
return !SEATS_PANEL_EXCLUDED_ROLES[r.role];
|
|
117
|
-
}).map(function (r) {
|
|
118
|
-
return {
|
|
119
|
-
// ⚠️ DE-ROT (F37): composite id — a v4.1 `--debate` run emits extra runStats rows for
|
|
120
|
-
// the SAME bench alias (role 'rebuttal'/'revote', src/council/debate.js:88-96). With no
|
|
121
|
-
// id, renderSeats keys on model and the re-vote row silently overwrites the seat row.
|
|
122
|
-
// (If the panel is meant to be bench-only, filter to seat/critic/lens:* instead and
|
|
123
|
-
// leave rebuttal/revote spend to the cost table — but say which; do not leave it implied.)
|
|
124
|
-
id: r.model + ':' + (r.role || 'seat'),
|
|
125
|
-
model: r.model, role: r.role || null, status: r.status || null,
|
|
126
|
-
stage: null, messages: null, tokensIn: null, tokensOut: null,
|
|
127
|
-
costDisplay: r.costDisplay || null, lastActivity: null, latestPreview: null,
|
|
128
|
-
stalled: false,
|
|
129
|
-
};
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* D6 (v4.6.2 PR4 Task 2, "dead-seat rows"): announced-dead seats, unioned
|
|
135
|
-
* from the run's own `degrades[]` (dead-leg/dead-wave channels — the live
|
|
136
|
-
* announcement; shapes verified against src/council/run-retry-notes.js and
|
|
137
|
-
* the skipped-path notes at src/council/run-stages.js:155-174) and the
|
|
138
|
-
* verdict's derived `seatLoss` (verdict.js summarizeSeatLoss/deriveSeatLoss
|
|
139
|
-
* — the critic-loss backstop, kept for verdicts written before `degrades[]`
|
|
140
|
-
* existed, v4.5.2 precedent). De-duped by seat model: `degrades` is scanned
|
|
141
|
-
* FIRST, so a real record's retry marker always beats the backstop's
|
|
142
|
-
* no-data guess for the same seat.
|
|
143
|
-
*
|
|
144
|
-
* `retried` reads `data.retryWaveId` / `data.firstFailure`: every
|
|
145
|
-
* still-dead-after-retry note (run-retry-notes.js's four builders) carries
|
|
146
|
-
* `retryWaveId`; the two run-stages.js skipped-path notes (fired when the
|
|
147
|
-
* once-only retry pass never even attempted the seat — an unmappable lens
|
|
148
|
-
* loss, an out-of-range index, or a zero-model unit) carry neither, so they
|
|
149
|
-
* correctly fall back to the plain phrasing.
|
|
150
|
-
*
|
|
151
|
-
* D6 filter (zero usable legs ONLY, "no ghost when a live row already
|
|
152
|
-
* exists"): a candidate already present in `liveSeats` is dropped. Two
|
|
153
|
-
* different things can put it there, and both are reasons to suppress:
|
|
154
|
-
* an SL-2 retry actually healed it (a real recovered review), OR
|
|
155
|
-
* (owner-ruled, v4.7 CA-4 dead-seat convergence) the seat never recovered
|
|
156
|
-
* at all but the row-per-launch machinery still gives its dead leg an
|
|
157
|
-
* honest primary ERROR row (every billed leg gets a row now, including
|
|
158
|
-
* failures — run-stages.js/run-assemble.js) — so "it has a cost row" no
|
|
159
|
-
* longer implies "it healed". Either way that live row IS the seat's
|
|
160
|
-
* record; rendering a second "did not review" ghost row beside it would
|
|
161
|
-
* be a duplicate, not new information, so suppressing it here is the
|
|
162
|
-
* ACCEPTED terminal-path behavior: exactly one row per seat, whatever its
|
|
163
|
-
* status. This is the one thing standing between a recovered (or
|
|
164
|
-
* honestly-erred) seat and a duplicate/ghost row — same failure family as
|
|
165
|
-
* the F37 debate-role collision and the RN-11 keyed-row lessons just above
|
|
166
|
-
* (seatsFromRunStats, seatCells): an identity that is not carefully
|
|
167
|
-
* matched silently duplicates or overwrites instead of failing loud.
|
|
168
|
-
*
|
|
169
|
-
* Role-aware D6 (v4.6.3 PR2, spec D3): a bare model match used to suppress
|
|
170
|
-
* regardless of what the LIVE row's role was — so a model that died as
|
|
171
|
-
* critic but whose chair-fallback walk happened to land on that same alias
|
|
172
|
-
* (and succeeded, producing a live `role: 'chair'` cost row) silently
|
|
173
|
-
* erased the dead-critic row it was never a replacement for (spec §5, the
|
|
174
|
-
* PR 102 rider). Candidates now carry a `role` (`'critic'` via alias equality
|
|
175
|
-
* with `runMeta.critic` — mirroring `deriveSeatLoss`, verdict.js:72 — or
|
|
176
|
-
* `null`), and only REVIEWING-role live legs (`seat`/`critic`/`lens:*`)
|
|
177
|
-
* suppress at all; a `'critic'` candidate is cleared only by a live
|
|
178
|
-
* CRITIC-role leg for that alias, never by a chair/judge/rebuttal/revote
|
|
179
|
-
* row landing on the same model. Hidden dependency: the recovered-critic
|
|
180
|
-
* suppression below (`byRole[alias + '|critic']`) relies on `roleFor`'s
|
|
181
|
-
* critic branch (src/council/run-stages.js), which only fires when lenses
|
|
182
|
-
* are absent — safe today only because --critic and --lenses are mutually
|
|
183
|
-
* exclusive (src/cli-handlers-council-run.js's `critic && lenses` check); if
|
|
184
|
-
* that exclusion ever loosens, a healed critic on a lens run would carry
|
|
185
|
-
* role 'seat' and this suppression would render a ghost dead row for it.
|
|
186
|
-
*
|
|
187
|
-
* Old-run resilience (v4.6.3 PR2, spec D4): pre-`degrades[]` runs (v4.5.2)
|
|
188
|
-
* carry the BENCH half of a seat loss only in `seatLoss.deadBenchSeats`
|
|
189
|
-
* (string[] of aliases, verdict.js deriveSeatLoss) — `degrades[]` never
|
|
190
|
-
* existed on either doc for these runs. Consumed after the critic backstop,
|
|
191
|
-
* candidates get `role: null` (deadBenchSeats carries no critic/bench
|
|
192
|
-
* distinction beyond what `criticRequested` already covers above) and flow
|
|
193
|
-
* through the same `seen`-keyed dedup and role-aware suppression as every
|
|
194
|
-
* other candidate — deriveSeatLoss does not dedup its own array, so `seen`
|
|
195
|
-
* is what keeps a repeated alias (or one also named by a real degrade
|
|
196
|
-
* record) from rendering twice.
|
|
197
|
-
*
|
|
198
|
-
* @param {Array<object>} degrades run.json's `degrades[]` (may be absent)
|
|
199
|
-
* @param {?object} seatLoss verdict.json's `seatLoss` (may be absent) —
|
|
200
|
-
* `criticRequested`/`criticSeated` back the critic candidate above,
|
|
201
|
-
* `deadBenchSeats` (string[] of aliases) feeds the bench candidates below
|
|
202
|
-
* @param {Array<{model: string}>} liveSeats seatsFromRunStats(...)'s output
|
|
203
|
-
* (or any seat list keyed the same way — the live seat map)
|
|
204
|
-
* @param {?{critic: ?string}} runMeta run.critic (alias, or null/absent
|
|
205
|
-
* when no critic was requested) — degrade records carry no role field, so
|
|
206
|
-
* this is the ONLY way a degrade-sourced candidate is identified as critic
|
|
207
|
-
* @returns {Array<{model: string, statusText: string, role: ?string}>}
|
|
208
|
-
*/
|
|
209
|
-
function deadSeats(degrades, seatLoss, liveSeats, runMeta) {
|
|
210
|
-
var critic = runMeta && runMeta.critic ? runMeta.critic : null;
|
|
211
|
-
// ⚠️ Object.create(null) throughout this family (also workspace-render.js's
|
|
212
|
-
// `existing`/`seen` and workspace-app.js's `labelByModel`): a model literally
|
|
213
|
-
// named `toString` is truthy off a bare object, so it was dropped here and —
|
|
214
|
-
// worse — crashed workspace-render.js:212 reading `.children` off an inherited
|
|
215
|
-
// function, killing the seats repaint and every tick after it.
|
|
216
|
-
var seen = Object.create(null);
|
|
217
|
-
var order = [];
|
|
218
|
-
function add(model, retried, role) {
|
|
219
|
-
if (!model || seen[model]) { return; }
|
|
220
|
-
seen[model] = true;
|
|
221
|
-
order.push({
|
|
222
|
-
model: model,
|
|
223
|
-
role: role || null,
|
|
224
|
-
statusText: retried ? 'did not review — retried once' : 'did not review',
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
(degrades || []).forEach(function (d) {
|
|
228
|
-
if (!d || d.kind !== 'degrade') { return; }
|
|
229
|
-
if (d.channel !== 'dead-leg' && d.channel !== 'dead-wave') { return; }
|
|
230
|
-
var data = d.data || {};
|
|
231
|
-
var retried = !!(data.retryWaveId || data.firstFailure);
|
|
232
|
-
// Critic identification mirrors deriveSeatLoss (verdict.js): alias
|
|
233
|
-
// equality with run.critic — degrade records carry no role field.
|
|
234
|
-
if (d.channel === 'dead-leg') {
|
|
235
|
-
add(data.seat, retried, critic && data.seat === critic ? 'critic' : null);
|
|
236
|
-
} else {
|
|
237
|
-
(data.models || []).forEach(function (m) {
|
|
238
|
-
add(m, retried, critic && m === critic ? 'critic' : null);
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
if (seatLoss && seatLoss.criticRequested && !seatLoss.criticSeated) {
|
|
243
|
-
add(seatLoss.criticRequested, false, 'critic');
|
|
244
|
-
}
|
|
245
|
-
if (seatLoss) {
|
|
246
|
-
// Pre-degrades[] era (v4.5.2): the bench half of a seat loss lives
|
|
247
|
-
// only here. Alias strings; deriveSeatLoss does not dedup — `seen`
|
|
248
|
-
// absorbs repeats and degrade-sourced duplicates.
|
|
249
|
-
(seatLoss.deadBenchSeats || []).forEach(function (m) { add(m, false, null); });
|
|
250
|
-
}
|
|
251
|
-
// Role-aware D6 (v4.6.3 PR2): only REVIEWING-role live legs suppress —
|
|
252
|
-
// a chair/judge/rebuttal/revote row must not hide a dead reviewer, and
|
|
253
|
-
// a dead-critic candidate is cleared only by a live CRITIC leg. A null
|
|
254
|
-
// role is NOT reviewing: counting it would suppress silently, the exact
|
|
255
|
-
// class the announcement invariant forbids.
|
|
256
|
-
// Role 'claude' is deliberately absent: it is emitted only by claudeRunStatsRow
|
|
257
|
-
// (src/council/run-assemble.js:129-132) for a seat that never launches a leg, and
|
|
258
|
-
// preflightClaudeReview (run-assemble.js:86-102) rejects 'claude' as chair/critic/
|
|
259
|
-
// bench — so no 'claude' leg can die. If that reservation ever loosens, this
|
|
260
|
-
// allowlist is the single place to extend.
|
|
261
|
-
function isReviewing(role) {
|
|
262
|
-
return role === 'seat' || role === 'critic' ||
|
|
263
|
-
(typeof role === 'string' && role.indexOf('lens:') === 0);
|
|
264
|
-
}
|
|
265
|
-
var reviewing = Object.create(null);
|
|
266
|
-
var byRole = Object.create(null);
|
|
267
|
-
// ⚠️ Fable review (PR4b fix wave): same F34/F36 alias-selection seatCells already uses
|
|
268
|
-
// (`seat.modelInput || seat.model`, above) — a LIVE payload seat's `model` is the RESOLVED
|
|
269
|
-
// executable id, not the alias a degrade record names; `modelInput` carries the alias.
|
|
270
|
-
// Keying this map on `s.model` alone meant a dead-leg seat whose errored roster row was
|
|
271
|
-
// still in the active stage's `liveSeats` never matched its alias-keyed degrade candidate,
|
|
272
|
-
// so D6 failed to suppress it — both rows rendered until the stage boundary dropped the
|
|
273
|
-
// errored row. Terminal-path cost rows (seatsFromRunStats) carry no `modelInput` at all and
|
|
274
|
-
// are already alias-only, so `|| s.model` leaves that path unchanged.
|
|
275
|
-
(liveSeats || []).forEach(function (s) {
|
|
276
|
-
if (!isReviewing(s.role)) { return; }
|
|
277
|
-
var alias = s.modelInput || s.model; // F36: alias space, never resolved ids
|
|
278
|
-
reviewing[alias] = true;
|
|
279
|
-
byRole[alias + '|' + s.role] = true;
|
|
280
|
-
});
|
|
281
|
-
return order.filter(function (s) {
|
|
282
|
-
if (s.role === 'critic') { return !byRole[s.model + '|critic']; }
|
|
283
|
-
return !reviewing[s.model];
|
|
284
|
-
});
|
|
285
|
-
}
|
|
53
|
+
// Seats surface lives in ./live-seats (v4.8 PR0 size-gate split), which
|
|
54
|
+
// loads first; re-exported on window.AmicusLive below so consumers are
|
|
55
|
+
// unchanged. In jest, require resolves it; in the renderer, the script
|
|
56
|
+
// load order guarantees window.AmicusLiveSeats exists.
|
|
57
|
+
var seats = (typeof module !== 'undefined' && module.exports)
|
|
58
|
+
? require('./live-seats')
|
|
59
|
+
: window.AmicusLiveSeats;
|
|
286
60
|
|
|
287
61
|
// ⚠️ DE-ROT (F41): STAGE_LABELS is exported so applyLive() can label post-open stages.
|
|
288
|
-
var api = { pollDelay: pollDelay, seatCells: seatCells, seatsFromRunStats: seatsFromRunStats,
|
|
289
|
-
deadSeats: deadSeats,
|
|
290
|
-
defaultBlind: defaultBlind, isTerminal: isTerminal, dash: dash,
|
|
62
|
+
var api = { pollDelay: pollDelay, seatCells: seats.seatCells, seatsFromRunStats: seats.seatsFromRunStats,
|
|
63
|
+
deadSeats: seats.deadSeats,
|
|
64
|
+
defaultBlind: defaultBlind, isTerminal: isTerminal, dash: seats.dash,
|
|
291
65
|
TERMINAL_STATUSES: TERMINAL_STATUSES, STAGE_LABELS: STAGE_LABELS };
|
|
292
66
|
if (typeof module !== 'undefined' && module.exports) { module.exports = api; }
|
|
293
67
|
if (typeof window !== 'undefined') { window.AmicusLive = api; }
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// electron/workspace-ui/live-seats.js
|
|
2
|
+
// Seats surface: dash, seatCells, SEATS_PANEL_EXCLUDED_ROLES, seatsFromRunStats. Moved
|
|
3
|
+
// verbatim from live-model.js@ac7e7e12:53-285 (v4.8 PR0 size-gate split, zero behavior).
|
|
4
|
+
// `deadSeats` moved too, then PR5c split it to live-dead-seats.js :: deadSeats; it is
|
|
5
|
+
// only re-exported here. Loads BEFORE live-model.js, which re-exports on window.AmicusLive.
|
|
6
|
+
// ES5 IIFE, dual export, strict-CSP <script> loading — same shape as
|
|
7
|
+
// every renderer module. Comments write "PR 102", never the hash-number
|
|
8
|
+
// form (electron-token-drift HEX_RE trips on it — this file is scanned).
|
|
9
|
+
(function () {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
function dash(v) {
|
|
13
|
+
return (v === null || v === undefined || v === '') ? '—' : String(v);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Display cells for one seat row, in the seats-table column order:
|
|
18
|
+
* [name, role, status, stage, msgs, tokens in/out, cost, last activity, stalled]
|
|
19
|
+
*/
|
|
20
|
+
function seatCells(seat, blindOn, labelOf) {
|
|
21
|
+
// ⚠️ DE-ROT (F36): key the lookup on the council ALIAS, never on `seat.model`. labelMap's
|
|
22
|
+
// values are aliases; a LIVE leg's `model` is the resolved executable id, so labelOf(model)
|
|
23
|
+
// misses and Blind ON renders the real model. `modelInput` is the alias Task 0.5 stamps per
|
|
24
|
+
// leg (terminal rows from runStats are already alias-only, so the fallback is exact there).
|
|
25
|
+
var alias = seat.modelInput || seat.model;
|
|
26
|
+
var label = labelOf ? labelOf(alias) : null;
|
|
27
|
+
// ⚠️ v4.4.1 RN-9: this hand-rolled flip is LOAD-BEARING — do NOT "fix" it into
|
|
28
|
+
// AmicusRender.display({model: seat.model, label: label}, blindOn). The other two copies
|
|
29
|
+
// (workspace-panels.js's review/judge titles) were routed through display() because their
|
|
30
|
+
// pair's `model` IS the identity they must print. Here it is not: the seat's printable
|
|
31
|
+
// identity is the council ALIAS resolved on the line above (F36 — a live leg's `seat.model`
|
|
32
|
+
// is the RESOLVED executable id, e.g. `google/gemini-2.5-pro`, which labelMap never keys on),
|
|
33
|
+
// so display()'s blind-OFF arm would print the resolved id and undo F36. This module is also
|
|
34
|
+
// node-tested with NO DOM and no window.AmicusRender to call. Deliberate third copy.
|
|
35
|
+
var name = blindOn && label ? label : alias;
|
|
36
|
+
var tokens = (seat.tokensIn === null || seat.tokensIn === undefined) &&
|
|
37
|
+
(seat.tokensOut === null || seat.tokensOut === undefined)
|
|
38
|
+
? '—'
|
|
39
|
+
: dash(seat.tokensIn) + '/' + dash(seat.tokensOut);
|
|
40
|
+
return [
|
|
41
|
+
dash(name),
|
|
42
|
+
dash(seat.role),
|
|
43
|
+
dash(seat.status),
|
|
44
|
+
dash(seat.stage),
|
|
45
|
+
dash(seat.messages),
|
|
46
|
+
tokens,
|
|
47
|
+
dash(seat.costDisplay),
|
|
48
|
+
// ⚠️ DE-ROT (F35): pass-through of an ISO timestamp, NOT a relative string. renderSeats
|
|
49
|
+
// formats it with relTime() before calling in — live-model.js is node-tested and loads
|
|
50
|
+
// before workspace-render.js, so it cannot reach relTime itself.
|
|
51
|
+
dash(seat.lastActivity),
|
|
52
|
+
seat.stalled ? '⏳ stalled' : '',
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// v4.7 D6/E1: three row-per-launch producer roles added alongside the
|
|
57
|
+
// existing chair-attempt (run-chair.js), repair (run-stages.js/
|
|
58
|
+
// run-stage2.js/run-chair.js) and superseded (run-stages.js + debate.js)
|
|
59
|
+
// rows — launch-accounting extras, not seats, and unlike rebuttal/revote
|
|
60
|
+
// (F37, kept rendering on purpose below) they have no seats-panel meaning
|
|
61
|
+
// of their own. There is no pre-existing allowlist in this function (E1) —
|
|
62
|
+
// this is a plain exclusion added on top of the untouched id/shape logic.
|
|
63
|
+
// Object.create(null): a plain `{...}` literal inherits Object.prototype, so a role
|
|
64
|
+
// literally named 'constructor'/'toString'/etc would resolve to an inherited (truthy)
|
|
65
|
+
// function via bracket lookup instead of `undefined` — silently mis-excluding (or
|
|
66
|
+
// mis-including) that seat. A null-prototype object has no inherited keys to collide with.
|
|
67
|
+
var SEATS_PANEL_EXCLUDED_ROLES = Object.create(null);
|
|
68
|
+
SEATS_PANEL_EXCLUDED_ROLES['chair-attempt'] = true;
|
|
69
|
+
SEATS_PANEL_EXCLUDED_ROLES.repair = true;
|
|
70
|
+
SEATS_PANEL_EXCLUDED_ROLES.superseded = true;
|
|
71
|
+
|
|
72
|
+
/** Terminal fallback: derive seat-shaped rows from tally runStats cost rows. */
|
|
73
|
+
function seatsFromRunStats(costRows) {
|
|
74
|
+
return (costRows || []).filter(function (r) {
|
|
75
|
+
return !SEATS_PANEL_EXCLUDED_ROLES[r.role];
|
|
76
|
+
}).map(function (r) {
|
|
77
|
+
return {
|
|
78
|
+
// ⚠️ DE-ROT (F37) + v4.8 PR5b: composite id over the SEAT, not the alias.
|
|
79
|
+
// ROLE half: a v4.1 `--debate` run emits extra runStats rows for the SAME bench alias
|
|
80
|
+
// (role 'rebuttal'/'revote', src/council/debate.js :: debateRunStatsRows, merged at
|
|
81
|
+
// run-finish.js:43-48). Without it, renderSeats keys on model and the re-vote row
|
|
82
|
+
// silently overwrites the seat row.
|
|
83
|
+
// SEAT half: PR1 made a bench that repeats an alias produce distinct seats, and two of
|
|
84
|
+
// them collided on the alias. Measured at HEAD across two ticks: renderSeats appended
|
|
85
|
+
// BOTH rows on tick one, then froze the first forever — `existing` is snapshotted
|
|
86
|
+
// before the loop, so both seats resolved to the LAST row and the first was never
|
|
87
|
+
// re-matched, while `seen[key]` kept it from being removed.
|
|
88
|
+
// `r.seat` is emit-when-set upstream (src/council/run-stats-entry.js :: buildRunStatsEntry
|
|
89
|
+
// stamps it only when seat.id differs from seat.alias, which seats.js:67 makes true
|
|
90
|
+
// exactly for a repeated alias), so `|| r.model` is load-bearing — it keeps a unique
|
|
91
|
+
// bench byte-identical.
|
|
92
|
+
// JSON.stringify, not concatenation: an alias may contain ':' and roles include
|
|
93
|
+
// 'lens:*', so a concatenated key is NOT injective — ('a','lens:x') and ('a:lens','x')
|
|
94
|
+
// both spell 'a:lens:x'. Never displayed; it is only ever a dataset.key, and
|
|
95
|
+
// workspace-render.js:186 uses a plain object lookup so quotes are already safe.
|
|
96
|
+
id: JSON.stringify([r.seat || r.model, r.role || 'seat']),
|
|
97
|
+
// Carried for workspace-seats.js's retry-badge join (PR5b Task 2). Null on a unique
|
|
98
|
+
// bench, matching the upstream payload exactly.
|
|
99
|
+
seat: r.seat || null,
|
|
100
|
+
model: r.model, role: r.role || null, status: r.status || null,
|
|
101
|
+
stage: null, messages: null, tokensIn: null, tokensOut: null,
|
|
102
|
+
costDisplay: r.costDisplay || null, lastActivity: null, latestPreview: null,
|
|
103
|
+
stalled: false,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
// deadSeats lives in ./live-dead-seats (v4.8 PR5c size-gate split), which loads first;
|
|
110
|
+
// re-exported here so every consumer — including window.AmicusLive's own bridge in
|
|
111
|
+
// live-model.js — is unchanged. In jest require resolves it; in the renderer the script
|
|
112
|
+
// load order guarantees window.AmicusDeadSeats exists.
|
|
113
|
+
var dead = (typeof module !== 'undefined' && module.exports)
|
|
114
|
+
? require('./live-dead-seats')
|
|
115
|
+
: window.AmicusDeadSeats;
|
|
116
|
+
|
|
117
|
+
var api = {
|
|
118
|
+
dash: dash,
|
|
119
|
+
seatCells: seatCells,
|
|
120
|
+
SEATS_PANEL_EXCLUDED_ROLES: SEATS_PANEL_EXCLUDED_ROLES,
|
|
121
|
+
seatsFromRunStats: seatsFromRunStats,
|
|
122
|
+
deadSeats: dead.deadSeats,
|
|
123
|
+
};
|
|
124
|
+
if (typeof module !== 'undefined' && module.exports) { module.exports = api; }
|
|
125
|
+
if (typeof window !== 'undefined') { window.AmicusLiveSeats = api; }
|
|
126
|
+
})();
|