amicus 4.8.0 → 4.9.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 +355 -0
- package/README.md +10 -5
- package/docs/CITATIONS.md +13 -5
- package/docs/ROADMAP.md +101 -10
- package/docs/configuration.md +55 -5
- package/docs/council.md +102 -14
- package/docs/troubleshooting.md +9 -2
- package/docs/usage.md +128 -12
- package/electron/ipc-setup.js +39 -2
- package/electron/main.js +46 -3
- package/electron/offer-session.js +51 -0
- package/electron/setup-ui-model.js +99 -9
- package/electron/setup-ui-styles.js +22 -0
- package/electron/setup-ui.js +244 -32
- package/electron/workspace-ui/live-dead-seats.js +163 -91
- package/electron/workspace-ui/live-seats.js +4 -4
- package/electron/workspace-ui/workspace-banners.js +30 -7
- package/electron/workspace-ui/workspace-matrix.js +23 -3
- package/electron/workspace-ui/workspace-seats.js +95 -79
- package/package.json +2 -1
- package/schemas/council-run.schema.json +2 -2
- package/schemas/council-tally.schema.json +17 -1
- package/schemas/council-verdict.schema.json +12 -4
- package/schemas/run.schema.json +6 -1
- package/skills/second-opinion/COUNCIL-DESIGN.md +1 -1
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
- package/skills/second-opinion/MODEL-NOTES.md +88 -9
- package/skills/second-opinion/SEAT-BRIEFS.md +36 -4
- package/skills/second-opinion/SKILL.md +151 -36
- package/src/cli-council-run-bench.js +98 -6
- package/src/cli-handlers-council-run.js +18 -6
- package/src/cli-handlers-council.js +57 -7
- package/src/cli-handlers-doctor.js +12 -15
- package/src/cli.js +3 -1
- package/src/council/anonymize.js +2 -1
- package/src/council/briefings-chair-task.js +161 -0
- package/src/council/briefings-chair.js +33 -8
- package/src/council/briefings-debate.js +79 -13
- package/src/council/briefings-stage2-task.js +236 -0
- package/src/council/briefings-stage2.js +103 -26
- package/src/council/briefings-task.js +167 -0
- package/src/council/briefings.js +41 -4
- package/src/council/chair-fallback.js +95 -0
- package/src/council/debate.js +38 -21
- package/src/council/findings.js +3 -2
- package/src/council/ledger.js +2 -2
- package/src/council/parse-stage2.js +64 -16
- package/src/council/report-cost.js +61 -0
- package/src/council/report-html.js +26 -4
- package/src/council/report-md.js +30 -2
- package/src/council/report.js +40 -37
- package/src/council/run-assemble.js +21 -6
- package/src/council/run-chair.js +44 -95
- package/src/council/run-debate-revote.js +81 -49
- package/src/council/run-debate.js +51 -34
- package/src/council/run-finish.js +5 -3
- package/src/council/run-retry-keys.js +4 -4
- package/src/council/run-retry-launch.js +4 -4
- package/src/council/run-retry-notes.js +72 -15
- package/src/council/run-stage1-launch.js +4 -4
- package/src/council/run-stage1-rows.js +9 -6
- package/src/council/run-stage2.js +81 -47
- package/src/council/run-stages.js +9 -21
- package/src/council/run-stats-entry.js +46 -1
- package/src/council/run.js +28 -13
- package/src/council/seats.js +2 -2
- package/src/council/stage1-bind.js +3 -2
- package/src/council/verdict-seat-loss.js +124 -0
- package/src/council/verdict.js +108 -99
- package/src/headless.js +256 -49
- package/src/mcp-council-bench.js +64 -3
- package/src/mcp-council-run.js +10 -3
- package/src/mcp-server.js +52 -12
- package/src/mcp-tools.js +41 -5
- package/src/observe/council-legs.js +2 -2
- package/src/opencode-client.js +19 -1
- package/src/pack/pack-forward.js +15 -12
- package/src/pack/pack-resolve.js +1 -1
- package/src/prompt-builder.js +17 -1
- package/src/sidecar/fanout-leg.js +26 -0
- package/src/sidecar/fanout.js +1 -1
- package/src/sidecar/list-council.js +178 -0
- package/src/sidecar/list-limit.js +3 -1
- package/src/sidecar/list-search.js +2 -1
- package/src/sidecar/models.js +8 -1
- package/src/sidecar/read.js +34 -10
- package/src/sidecar/setup.js +124 -0
- package/src/template/render.js +16 -7
- package/src/utils/alias-audit.js +81 -3
- package/src/utils/alias-shadow-writer.js +220 -0
- package/src/utils/alias-shadow.js +294 -0
- package/src/utils/config.js +1 -1
- package/src/utils/curated-models.js +16 -8
- package/src/utils/degrade.js +12 -5
- package/src/utils/doctor-alias-check.js +149 -0
- package/src/utils/engine-log-parse.js +289 -0
- package/src/utils/engine-log-tail.js +114 -0
- package/src/utils/engine-log.js +250 -0
- package/src/utils/engine-skew-records.js +146 -0
- package/src/utils/engine-skew.js +300 -0
- package/src/utils/gateway-router.js +10 -2
- package/src/utils/model-canonicalization.js +64 -0
- package/src/utils/model-catalog.js +1 -1
- package/src/utils/model-shortlist.js +100 -0
- 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 +24 -0
- package/src/utils/result-schema.js +10 -0
- package/src/utils/text-sanitize.js +81 -0
- package/src/utils/ttft.js +57 -0
- package/src/utils/untrusted-fence.js +111 -1
- package/src/workspace/fold-format.js +28 -7
- package/src/workspace/live-normalize.js +2 -1
- package/src/workspace/matrix-model.js +6 -2
- package/src/workspace/run-detail.js +35 -9
- package/src/workspace/seat-space.js +10 -6
|
@@ -5,66 +5,105 @@
|
|
|
5
5
|
// Self-contained: it reads only its four arguments and owns its own isReviewing.
|
|
6
6
|
(function () {
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
8
|
+
* The kind/channel admission rule, v4.9 W9 (SI-02). ⚠️ MIRROR — this rule is spelled a
|
|
9
|
+
* second time at `workspace-seats.js :: retriedSeats`, and the two MUST move together
|
|
10
|
+
* (two spellings of one rule is PR5a council finding B1; the mirror is declared in that
|
|
11
|
+
* function's docblock). A renderer module cannot `require` src/ and this file cannot
|
|
12
|
+
* `require` its sibling, so the mirror is enforced behaviourally instead: the drift pin
|
|
13
|
+
* in `tests/workspace/workspace-seats.test.js` drives BOTH consumers with one fixture set.
|
|
17
14
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* correctly fall back to the plain phrasing.
|
|
15
|
+
* ⚠️ A kind-LESS record IS a degrade (W9 fix round, council C4). Both renderers read
|
|
16
|
+
* `run.json`/`verdict.json` off DISK — `workspace-seats.js :: renderSeatsPanel` even falls back
|
|
17
|
+
* to `verdict.degrades` — i.e. exactly the documents `report.js`'s LEGACYDROP note is about:
|
|
18
|
+
* they can PREDATE kinds, and `utils/degrade.js :: formatDegrade` still serves such a record as
|
|
19
|
+
* 'Notice'. A positive `kind === 'degrade'` silently dropped every one of them.
|
|
24
20
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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.
|
|
21
|
+
* `dead-leg`/`dead-wave` are admitted unconditionally — the skipped-LEG note (`run-stages.js`,
|
|
22
|
+
* fired when the once-only retry never attempted the seat) carries no retry-family field and
|
|
23
|
+
* must still render, with the plain phrasing.
|
|
42
24
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `
|
|
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.
|
|
25
|
+
* `seat-unbound` is admitted GATED, never raw: it is a SHARED channel with SEVEN emit sites
|
|
26
|
+
* (re-counted 2026-08-26 — FOUR arms of `run-retry-notes.js`, one in each of `stage1-bind.js`,
|
|
27
|
+
* `run-debate-revote.js` and `run-stage2.js`) that do NOT mean the same thing. `orphanLegNote`
|
|
28
|
+
* and `reVoteUnboundNote` mean "a leg LANDED but names no seat/judge" — a review that was paid
|
|
29
|
+
* for and rendered, not a lost seat; `run-stage2.js`'s judge-side note names a seat that DID
|
|
30
|
+
* review and merely failed to judge. None of the three carries `retryWaveId` or `firstFailure`
|
|
31
|
+
* (measured), so the retry-family test excludes all three and the BACKLOG's proposed
|
|
32
|
+
* `data.legId` discriminator is subsumed rather than duplicated.
|
|
60
33
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
34
|
+
* ⚠️ The seat conjunct is HONESTLY belt-and-braces HERE and load-bearing in the third twin.
|
|
35
|
+
* Measured: dropping it from this function reds nothing, because `add()` already refuses a
|
|
36
|
+
* candidate with no alias; dropping it from `verdict-seat-loss.js :: deriveSeatLoss` puts an
|
|
37
|
+
* `undefined` into `deadBenchSeats`, which every reader of that list treats as a seat. It is
|
|
38
|
+
* kept in all three so one rule has one spelling — pinned on the verdict side.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ R-W9a is CLOSED at the PRODUCER (W9 fix round, council A1/C1): `run-retry-notes.js ::
|
|
41
|
+
* skippedWaveNote` emits the `firstFailure` fact its record already carried, so this gate
|
|
42
|
+
* admits that real loss UNCHANGED and the three controls above still fail it.
|
|
43
|
+
*/
|
|
44
|
+
function isSeatLoss(d) {
|
|
45
|
+
if (!d || (d.kind !== undefined && d.kind !== 'degrade')) { return false; }
|
|
46
|
+
if (d.channel === 'dead-leg' || d.channel === 'dead-wave') { return true; }
|
|
47
|
+
if (d.channel !== 'seat-unbound') { return false; }
|
|
48
|
+
var data = d.data || {};
|
|
49
|
+
return !!((data.retryWaveId || data.firstFailure) && (data.seatId || data.seat));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* D6 (v4.6.2 PR4 Task 2, "dead-seat rows"): announced-dead seats, unioned from the run's own
|
|
54
|
+
* `degrades[]` (the loss channels `isSeatLoss` above admits — the live announcement; shapes
|
|
55
|
+
* verified against src/council/run-retry-notes.js, which since the W9 fix round owns the
|
|
56
|
+
* skipped-path WAVE note too, plus the `retry.skippedDeadLegs` loop still in run-stages.js —
|
|
57
|
+
* anchored BY SYMBOL at v4.9 W9, the old `:155-174` having rotted onto the materialize/repair
|
|
58
|
+
* block) and the verdict's derived `seatLoss` (verdict-seat-loss.js's summarizeSeatLoss /
|
|
59
|
+
* deriveSeatLoss — the critic-loss backstop, kept for verdicts written before `degrades[]`
|
|
60
|
+
* existed, v4.5.2 precedent). De-duped by seat model: `degrades` is scanned FIRST, so a real
|
|
61
|
+
* record's retry marker always beats the backstop's no-data guess for the same seat.
|
|
62
|
+
*
|
|
63
|
+
* `retried` reads `data.retryWaveId` ALONE — narrowed in the W9 fix round, named mutant
|
|
64
|
+
* SKIPRETRIED. That field exists if and only if a retry wave was actually LAUNCHED for the
|
|
65
|
+
* seat: every still-dead-after-retry builder emits it, and both skipped-path notes (the retry
|
|
66
|
+
* never attempted the seat — an unmappable lens loss, an out-of-range index, a zero-model
|
|
67
|
+
* unit, or `overBudget()`) emit none, so they keep the plain phrasing. ⚠️ `firstFailure` is
|
|
68
|
+
* NOT a retry marker: `skippedWaveNote` (the sole `firstFailure`-without-`retryWaveId`
|
|
69
|
+
* producer) describes a seat never retried; every RETRIED seat's builder emits the former.
|
|
70
|
+
*
|
|
71
|
+
* D6 filter (zero usable legs ONLY, "no ghost when a live row already exists"): a candidate
|
|
72
|
+
* already present in `liveSeats` is dropped. Two different things can put it there and both
|
|
73
|
+
* are reasons to suppress: an SL-2 retry actually healed it (a real recovered review), OR
|
|
74
|
+
* (owner-ruled, v4.7 CA-4 dead-seat convergence) the seat never recovered at all but the
|
|
75
|
+
* row-per-launch machinery still gives its dead leg an honest primary ERROR row (every billed
|
|
76
|
+
* leg gets a row now, including failures — run-stages.js/run-assemble.js), so "it has a cost
|
|
77
|
+
* row" no longer implies "it healed". Either way that live row IS the seat's record, and a
|
|
78
|
+
* second "did not review" ghost beside it is a duplicate, not new information — so suppressing
|
|
79
|
+
* here is the ACCEPTED terminal-path behavior: exactly one row per seat, whatever its status.
|
|
80
|
+
* It is the one thing standing between a recovered (or honestly-erred) seat and a duplicate
|
|
81
|
+
* row — the F37 debate-role and RN-11 keyed-row family (seatsFromRunStats, seatCells): an
|
|
82
|
+
* identity that is not carefully matched silently duplicates or overwrites instead of failing.
|
|
83
|
+
*
|
|
84
|
+
* Role-aware D6 (v4.6.3 PR2, spec D3): a bare model match used to suppress regardless of what
|
|
85
|
+
* the LIVE row's role was — so a model that died as critic but whose chair-fallback walk
|
|
86
|
+
* happened to land on that same alias (and succeeded, producing a live `role: 'chair'` cost
|
|
87
|
+
* row) silently erased the dead-critic row it was never a replacement for (spec §5, the PR 102
|
|
88
|
+
* rider). Candidates now carry a `role` — `'critic'` or `null`, decided by `roleOf` below,
|
|
89
|
+
* which since v4.9 W9 (R4) keys on SEAT identity whenever the record and the run can both
|
|
90
|
+
* spell it and falls back to alias equality otherwise (so it no longer mirrors
|
|
91
|
+
* verdict-seat-loss.js :: deriveSeatLoss, which stays alias-keyed on purpose) — and only
|
|
92
|
+
* REVIEWING-role live legs (`seat`/`critic`/`lens:*`) suppress at all; a `'critic'` candidate
|
|
93
|
+
* is cleared only by a live CRITIC-role leg for that seat (alias where no seat id exists),
|
|
94
|
+
* never by a chair/judge/rebuttal/revote row landing on the same model. Hidden dependency: the
|
|
95
|
+
* recovered-critic suppression below (`byRole[key + '|critic']`) relies on `roleFor`'s critic
|
|
96
|
+
* branch (src/council/run-stages.js), which only fires when lenses are absent — safe today
|
|
97
|
+
* only because --critic and --lenses are mutually exclusive (src/cli-handlers-council-run.js's
|
|
98
|
+
* `critic && lenses` check); if that exclusion ever loosens, a healed critic on a lens run
|
|
99
|
+
* would carry role 'seat' and this suppression would render a ghost dead row for it.
|
|
100
|
+
*
|
|
101
|
+
* Old-run resilience (v4.6.3 PR2, spec D4): pre-`degrades[]` runs (v4.5.2) carry the BENCH
|
|
102
|
+
* half of a seat loss only in `seatLoss.deadBenchSeats` (string[] of aliases, deriveSeatLoss)
|
|
103
|
+
* — `degrades[]` never existed on either doc for these runs. Consumed after the critic
|
|
104
|
+
* backstop, candidates get `role: null` (deadBenchSeats carries no critic/bench distinction
|
|
105
|
+
* beyond what `criticRequested` already covers above) and flow through the same dedup and
|
|
106
|
+
* role-aware suppression as every other candidate.
|
|
68
107
|
*
|
|
69
108
|
* ⚠️ v4.8 PR5c corrected HOW that dedup works, and the old description here was
|
|
70
109
|
* wrong once seats were keyed. Candidates arrive in FOUR identity flavours, and the
|
|
@@ -75,7 +114,9 @@
|
|
|
75
114
|
* it on the alias is what collapsed two dead twins into one row.
|
|
76
115
|
* 'legacy' a pre-PR5c record with no seat id anywhere -> dedup on the alias,
|
|
77
116
|
* which still collapses twins. Disclosed residual R2.
|
|
78
|
-
* 'derivative' seatLoss-sourced. verdict.js
|
|
117
|
+
* 'derivative' seatLoss-sourced. `verdict-seat-loss.js :: deriveSeatLoss` (by SYMBOL — the
|
|
118
|
+
* old `verdict.js:86` rotted in v4.9 W9, and the function has since left
|
|
119
|
+
* verdict.js entirely) builds deadBenchSeats FROM the same
|
|
79
120
|
* dead legs that emit degrades[], so it is always a duplicate and is
|
|
80
121
|
* the ONLY flavour that may be absorbed by an alias already covered.
|
|
81
122
|
* Absorbing a real degrade instead would erase an unidentified twin.
|
|
@@ -86,8 +127,9 @@
|
|
|
86
127
|
* `deadBenchSeats` (string[] of aliases) feeds the bench candidates below
|
|
87
128
|
* @param {Array<{model: string}>} liveSeats seatsFromRunStats(...)'s output
|
|
88
129
|
* (or any seat list keyed the same way — the live seat map)
|
|
89
|
-
* @param {?{critic: ?string}} runMeta run.critic (alias
|
|
90
|
-
*
|
|
130
|
+
* @param {?{critic: ?string, criticSeat: ?string}} runMeta run.critic (alias) and
|
|
131
|
+
* run.criticSeat (the seat id `seats.js :: preflightSeats` resolved for it), each
|
|
132
|
+
* null/absent when no critic was requested — degrade records carry no role field, so
|
|
91
133
|
* this is the ONLY way a degrade-sourced candidate is identified as critic
|
|
92
134
|
* @returns {Array<{model: string, seat: ?string, statusText: string, role: ?string}>}
|
|
93
135
|
* `model` is the ALIAS (what the row displays, what labelOf keys on); `seat` is
|
|
@@ -95,11 +137,28 @@
|
|
|
95
137
|
*/
|
|
96
138
|
function deadSeats(degrades, seatLoss, liveSeats, runMeta) {
|
|
97
139
|
var critic = runMeta && runMeta.critic ? runMeta.critic : null;
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
140
|
+
var criticSeat = runMeta && runMeta.criticSeat ? runMeta.criticSeat : null;
|
|
141
|
+
// R4 (v4.9 W9). The old rule was `alias === critic`, which on a bench where one alias holds
|
|
142
|
+
// a critic seat AND a bench seat tagged the WRONG candidate 'critic' — and critic candidates
|
|
143
|
+
// suppress through `byRole`, a map PR5c's seat-keying never reached, so a dead bench twin
|
|
144
|
+
// beside a live critic twin rendered NOTHING. Seat identity decides whenever BOTH sides can
|
|
145
|
+
// spell it; a record with no seat id, or a run with no resolved critic seat (pre-`criticSeat`
|
|
146
|
+
// run.json), keeps the legacy alias rule rather than guessing.
|
|
147
|
+
// ⚠️ W9 fix round (council C3): a truthy key is NOT necessarily seat-space. On the INEXACT
|
|
148
|
+
// twin branch `run-retry-group.js :: recordFailure` keys `firstFailure.seatId` by the ALIAS
|
|
149
|
+
// (residual R3), so the critic's OWN record can arrive keyed 'gpt' against a criticSeat of
|
|
150
|
+
// 'gpt#1' and came back role null — while `verdict-seat-loss.js :: deriveSeatLoss`, comparing
|
|
151
|
+
// `data.seat` to the alias, called that same record a critic loss. The alias arm of the
|
|
152
|
+
// disjunct IS that legacy rule, so the two surfaces agree; it cannot re-open R4, since a real
|
|
153
|
+
// bench-twin id ('gpt#2') equals neither operand.
|
|
154
|
+
function roleOf(key, alias) {
|
|
155
|
+
if (key && criticSeat) { return (key === criticSeat || key === critic) ? 'critic' : null; }
|
|
156
|
+
return critic && alias === critic ? 'critic' : null;
|
|
157
|
+
}
|
|
158
|
+
// ⚠️ Object.create(null) throughout this family (also workspace-render.js's `existing`/`seen`
|
|
159
|
+
// and workspace-app.js's `labelByModel`): a model literally named `toString` is truthy off a
|
|
160
|
+
// bare object, so it was dropped here and — worse — crashed workspace-render.js:212 reading
|
|
161
|
+
// `.children` off an inherited function, killing the seats repaint and every tick after it.
|
|
103
162
|
var seen = Object.create(null);
|
|
104
163
|
var covered = Object.create(null);
|
|
105
164
|
var seenUnid = Object.create(null);
|
|
@@ -108,12 +167,12 @@
|
|
|
108
167
|
// docblock above, which is what decides the key and the absorb rule.
|
|
109
168
|
function add(key, alias, retried, role, src, slotOf) {
|
|
110
169
|
if (!alias) { return; }
|
|
111
|
-
// ⛔ An 'unid' candidate was originally given NO dedup at all, on the grounds that it is
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
170
|
+
// ⛔ An 'unid' candidate was originally given NO dedup at all, on the grounds that it is one
|
|
171
|
+
// wave slot and distinct by construction. That holds WITHIN a record's seats[]; it does NOT
|
|
172
|
+
// hold across records, and the same dead-wave record appearing twice rendered four rows for
|
|
173
|
+
// two seats. Deduped on (waveId, slot) instead — its own namespace, never `seen`, so an
|
|
174
|
+
// invented key can never collide with a real alias. (The seat literally named `toString` is
|
|
175
|
+
// why this family refuses keys that share the alias keyspace.)
|
|
117
176
|
if (src === 'unid') {
|
|
118
177
|
var uk = ((slotOf && slotOf.waveId) || '') + '#' + (slotOf ? slotOf.i : 0);
|
|
119
178
|
if (seenUnid[uk]) { return; }
|
|
@@ -140,23 +199,23 @@
|
|
|
140
199
|
order.push(row);
|
|
141
200
|
}
|
|
142
201
|
(degrades || []).forEach(function (d) {
|
|
143
|
-
if (!d
|
|
144
|
-
if (d.channel !== 'dead-leg' && d.channel !== 'dead-wave') { return; }
|
|
202
|
+
if (!isSeatLoss(d)) { return; }
|
|
145
203
|
var data = d.data || {};
|
|
146
|
-
var retried = !!
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
204
|
+
var retried = !!data.retryWaveId; // NOT firstFailure — docblock above, mutant SKIPRETRIED
|
|
205
|
+
// ONE shape, TWO channels: `dead-leg` and every admitted `seat-unbound` arm name a
|
|
206
|
+
// single seat through `data.seat` (alias) plus an optional seat id — the partial arm's
|
|
207
|
+
// `data.seatId` (v4.9 W9 P1) or the two missing-leg arms' `firstFailure.seatId`. Only
|
|
208
|
+
// `dead-wave` is a LIST, which is why the branch tests for it rather than for dead-leg.
|
|
209
|
+
if (d.channel !== 'dead-wave') {
|
|
150
210
|
var lk = (data.firstFailure && data.firstFailure.seatId) || data.seatId || null;
|
|
151
|
-
add(lk, data.seat, retried,
|
|
152
|
-
lk ? 'keyed' : 'legacy');
|
|
211
|
+
add(lk, data.seat, retried, roleOf(lk, data.seat), lk ? 'keyed' : 'legacy');
|
|
153
212
|
} else {
|
|
154
213
|
// `seats[]` is index-parallel with `models[]` (PR5c Task 1). Its ABSENCE means a
|
|
155
214
|
// legacy record; a null ELEMENT means an unidentified seat. Different statements.
|
|
156
215
|
var hasSeats = Array.isArray(data.seats);
|
|
157
216
|
(data.models || []).forEach(function (m, i) {
|
|
158
217
|
var wk = hasSeats ? data.seats[i] : null;
|
|
159
|
-
add(wk, m, retried,
|
|
218
|
+
add(wk, m, retried, roleOf(wk, m),
|
|
160
219
|
wk ? 'keyed' : (hasSeats ? 'unid' : 'legacy'), { waveId: data.waveId, i: i });
|
|
161
220
|
});
|
|
162
221
|
}
|
|
@@ -165,18 +224,15 @@
|
|
|
165
224
|
add(null, seatLoss.criticRequested, false, 'critic', 'derivative');
|
|
166
225
|
}
|
|
167
226
|
if (seatLoss) {
|
|
168
|
-
// Pre-degrades[] era (v4.5.2): the bench half of a seat loss lives
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
(seatLoss.deadBenchSeats || []).forEach(function (m) {
|
|
172
|
-
add(null, m, false, null, 'derivative');
|
|
173
|
-
});
|
|
227
|
+
// Pre-degrades[] era (v4.5.2): the bench half of a seat loss lives only here. Alias
|
|
228
|
+
// strings; deriveSeatLoss does not dedup — `seen` absorbs repeats and degrade-sourced
|
|
229
|
+
// duplicates.
|
|
230
|
+
(seatLoss.deadBenchSeats || []).forEach(function (m) { add(null, m, false, null, 'derivative'); });
|
|
174
231
|
}
|
|
175
|
-
// Role-aware D6 (v4.6.3 PR2): only REVIEWING-role live legs suppress —
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
// class the announcement invariant forbids.
|
|
232
|
+
// Role-aware D6 (v4.6.3 PR2): only REVIEWING-role live legs suppress — a chair/judge/
|
|
233
|
+
// rebuttal/revote row must not hide a dead reviewer, and a dead-critic candidate is cleared
|
|
234
|
+
// only by a live CRITIC leg. A null role is NOT reviewing: counting it would suppress
|
|
235
|
+
// silently, the exact class the announcement invariant forbids.
|
|
180
236
|
// Role 'claude' is deliberately absent: it is emitted only by claudeRunStatsRow
|
|
181
237
|
// (src/council/run-assemble.js:129-132) for a seat that never launches a leg, and
|
|
182
238
|
// preflightClaudeReview (run-assemble.js:86-102) rejects 'claude' as chair/critic/
|
|
@@ -188,6 +244,7 @@
|
|
|
188
244
|
}
|
|
189
245
|
var reviewing = Object.create(null);
|
|
190
246
|
var byRole = Object.create(null);
|
|
247
|
+
var byRoleUnseated = Object.create(null); // alias|role, live legs that named NO seat id
|
|
191
248
|
// ⚠️ Fable review (PR4b fix wave): same F34/F36 alias-selection seatCells already uses
|
|
192
249
|
// (`seat.modelInput || seat.model`, above) — a LIVE payload seat's `model` is the RESOLVED
|
|
193
250
|
// executable id, not the alias a degrade record names; `modelInput` carries the alias.
|
|
@@ -208,9 +265,24 @@
|
|
|
208
265
|
// producers cannot disagree. A bare insert would write a STRING key for every such seat.
|
|
209
266
|
if (s.seat) { reviewing[s.seat] = true; }
|
|
210
267
|
byRole[alias + '|' + s.role] = true;
|
|
268
|
+
// R4 (v4.9 W9): the seat arm, exactly mirroring `reviewing` two lines up. Without it a
|
|
269
|
+
// critic candidate carrying a seat id has no seat-keyed entry to match and the lookup
|
|
270
|
+
// below would fall through to a ghost row on every twin bench.
|
|
271
|
+
// ⚠️ The ELSE arm is the W9 fix round's (council C2): a live leg that named NO seat id can
|
|
272
|
+
// only write the alias key, so a seat-KEYED candidate had nothing to match and the read
|
|
273
|
+
// below rendered a ghost 'critic did not review' row the pre-W9 alias read suppressed.
|
|
274
|
+
// Kept OUT of `byRole` on purpose — an alias entry written by a SEATED leg must not clear
|
|
275
|
+
// a different seat's candidate, which is the erasure R4 exists to have fixed.
|
|
276
|
+
if (s.seat) { byRole[s.seat + '|' + s.role] = true; } else { byRoleUnseated[alias + '|' + s.role] = true; }
|
|
211
277
|
});
|
|
212
278
|
return order.filter(function (s) {
|
|
213
|
-
|
|
279
|
+
// Seat where the candidate has one, alias otherwise — the same key `reviewing` uses
|
|
280
|
+
// below, so the two suppression maps cannot disagree about what a candidate IS. The
|
|
281
|
+
// second disjunct reads the alias keyspace the WRITE also lands in (C2, above): it is
|
|
282
|
+
// consulted only for live legs that named no seat, so it cannot re-open R4's erasure.
|
|
283
|
+
if (s.role === 'critic') {
|
|
284
|
+
return !(byRole[(s.seat || s.model) + '|critic'] || byRoleUnseated[s.model + '|critic']);
|
|
285
|
+
}
|
|
214
286
|
// An UNNAMED dead seat is never suppressed by alias evidence. The producer emitted
|
|
215
287
|
// `null` rather than the alias precisely because "unidentified" and "the alias" are
|
|
216
288
|
// different statements; falling back to the alias here would re-assert the equivalence
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// electron/workspace-ui/live-seats.js
|
|
2
|
-
// Seats surface: dash, seatCells, SEATS_PANEL_EXCLUDED_ROLES,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
6
|
// ES5 IIFE, dual export, strict-CSP <script> loading — same shape as
|
|
7
7
|
// every renderer module. Comments write "PR 102", never the hash-number
|
|
8
8
|
// form (electron-token-drift HEX_RE trips on it — this file is scanned).
|
|
@@ -60,15 +60,38 @@
|
|
|
60
60
|
'. This run directory cannot distinguish those artifacts, so prose below may be misattributed.', '');
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
|
-
// ⚠️ Fix-wave 3 (council-3 C2): seats[] was present but malformed, so the
|
|
64
|
-
// back to alias space — two seats running one model are indistinguishable in
|
|
65
|
-
//
|
|
66
|
-
// a crash. Ranked AFTER the collision banner (that one says prose may be
|
|
67
|
-
// strictly worse) and BEFORE run.error, since it qualifies how to read
|
|
63
|
+
// ⚠️ Fix-wave 3 (council-3 C2): seats[] was present but malformed, so the run's ARTIFACT
|
|
64
|
+
// NAMING fell back to alias space — two seats running one model are indistinguishable in
|
|
65
|
+
// the prose panels. Fail-safe, but SILENT until now, which the product principle rejects
|
|
66
|
+
// as hard as a crash. Ranked AFTER the collision banner (that one says prose may be
|
|
67
|
+
// misattributed, strictly worse) and BEFORE run.error, since it qualifies how to read
|
|
68
|
+
// those panels.
|
|
69
|
+
// ⚠️ This comment used to say "the WHOLE run fell back to alias space … in every panel
|
|
70
|
+
// BELOW", and W9 measured that as FALSE (it was already false when written, not falsified
|
|
71
|
+
// by this change — the same reading the banner string itself shipped). The correction and
|
|
72
|
+
// its evidence are below, where the sentence the user actually reads is built.
|
|
73
|
+
// ⚠️ v4.9 W9 PR5b-1 (ruling V15, disclosure over uniformity): this used to end "cannot be
|
|
74
|
+
// told apart BELOW" — a blanket claim over the whole detail view, and measurably too wide.
|
|
75
|
+
// `seatTableRejected` reaches exactly one surface: derived.seatSpace goes false, so
|
|
76
|
+
// workspace-lazy.js :: roster falls back to `bench` and the reviews/judges/re-vote panels
|
|
77
|
+
// name their artifacts by alias. Everything sourced from tally.json is untouched — the
|
|
78
|
+
// seats panel's rows are live-seats.js :: seatsFromRunStats over run-detail.js ::
|
|
79
|
+
// costPanel's `tally.runStats` projection, keyed on `r.seat || r.model`, and `r.seat` is
|
|
80
|
+
// stamped by run-stats-entry.js :: buildRunStatsEntry from the IN-MEMORY seat at assembly
|
|
81
|
+
// (run-assemble.js :: buildTallyInput), not from the run.json table that failed to parse.
|
|
82
|
+
// A twin bench therefore still gets two seat rows carrying their own numbers.
|
|
83
|
+
// V15's ruling is to DISCLOSE that split rather than force the seats panel down to
|
|
84
|
+
// derived.seatSpace for uniformity, which would throw away data that is truthful.
|
|
85
|
+
// ⚠️ The ROWS are per-seat; the NAMES are not. live-seats.js :: seatCells prints
|
|
86
|
+
// `seat.modelInput || seat.model` — the alias — so the sentence claims separate NUMBERS,
|
|
87
|
+
// never distinguishable names. Wording pinned full-string in workspace-app-boundary.test.js.
|
|
68
88
|
if (d.derived.seatTableRejected) {
|
|
69
89
|
R.renderBanner(el, 'This run\'s seat table could not be read (a malformed entry), so ' +
|
|
70
|
-
'
|
|
71
|
-
'model cannot be told apart
|
|
90
|
+
'the reviews, judges and re-vote panels name their artifacts by model alias instead ' +
|
|
91
|
+
'of per seat — two seats running the same model cannot be told apart there. The seats ' +
|
|
92
|
+
'panel is NOT affected: its rows come from tally.json\'s runStats, one per seat, so ' +
|
|
93
|
+
'both seats\' numbers are still shown separately, sharing the alias as a name. ' +
|
|
94
|
+
'Artifacts: ' + d.runDir, 'warn');
|
|
72
95
|
return;
|
|
73
96
|
}
|
|
74
97
|
// ⚠️ PRE-FLIGHT (P3), caught live by the CDP e2e (Task 18): gating on `d.run.error` ALONE
|
|
@@ -126,9 +126,15 @@
|
|
|
126
126
|
container.textContent = '';
|
|
127
127
|
var head = R.el('div', { className: 'chips' }, []);
|
|
128
128
|
if (vp.overallVerdict) {
|
|
129
|
-
|
|
129
|
+
// v4.9 W8: the chip names the SCALE the phrase belongs to — a task chair's
|
|
130
|
+
// phrase comes from CHAIR_ANSWERS and is never a verdict. Payload-driven:
|
|
131
|
+
// this file cannot require() src/, so run-detail.js :: verdictPanel mints
|
|
132
|
+
// `intent`. `=== 'task'` (not `!== 'review'`) so a pre-v4.9 payload with no
|
|
133
|
+
// intent key reads as review instead of relabelling every legacy run.
|
|
134
|
+
head.appendChild(R.el('span', { className: 'chip complete' },
|
|
135
|
+
[(vp.intent === 'task' ? 'ANSWER: ' : 'VERDICT: ') + vp.overallVerdict]));
|
|
130
136
|
} else {
|
|
131
|
-
head.appendChild(R.el('span', { className: 'chip error' }, ['no chair verdict']));
|
|
137
|
+
head.appendChild(R.el('span', { className: 'chip error' }, [vp.intent === 'task' ? 'no chair answer' : 'no chair verdict']));
|
|
132
138
|
if (vp.reason) { head.appendChild(R.el('span', { className: 'empty-note' }, [vp.reason])); }
|
|
133
139
|
}
|
|
134
140
|
container.appendChild(head);
|
|
@@ -145,8 +151,22 @@
|
|
|
145
151
|
|
|
146
152
|
if (vp.streetCred && vp.streetCred.length) {
|
|
147
153
|
var rows = vp.streetCred.map(function (s) {
|
|
154
|
+
// v4.9 W9 — SI-22.4 rider (2): the THIRD street-cred renderer, joining
|
|
155
|
+
// report-md.js :: renderMd and report-html.js :: renderHtml (v4.8 R22.4-6), which
|
|
156
|
+
// both label the row `s.seat || s.model`. Until now a twin bench read `gemini#1`/
|
|
157
|
+
// `gemini#2` in the report and `gemini` TWICE here, different numbers under one
|
|
158
|
+
// identical name. `seat` is emit-when-DIFFERENT (street-cred.js :: computeStreetCred),
|
|
159
|
+
// so a unique-alias bench has no seat key and this renders exactly what it did before.
|
|
160
|
+
// ⚠️ BLIND KEEPS THE ALIAS SPACE, both branches. `opts.labelOf` is ALIAS-keyed
|
|
161
|
+
// (workspace-app.js's state.labelByModel, from run.json's labelMap — label -> alias,
|
|
162
|
+
// which the seat work deliberately left alone: anonymize.js :: assignLabels puts seat
|
|
163
|
+
// identity in the separate seatMap), so the lookup key stays `s.model` and the
|
|
164
|
+
// signature is unchanged. The fallback stays `s.model` too: a seat id CONTAINS its
|
|
165
|
+
// alias, so printing one with blind mode on defeats blind mode — the same rule
|
|
166
|
+
// seat-space.js :: isSeatTable and workspace-lazy.js :: roster were both fixed to.
|
|
167
|
+
// Named mutant RIDERALIAS (workspace-matrix.test.js).
|
|
148
168
|
var label = opts.labelOf(s.model);
|
|
149
|
-
var name = opts.isBlind()
|
|
169
|
+
var name = opts.isBlind() ? (label || s.model) : (s.seat || s.model);
|
|
150
170
|
var fmt = function (v) { return (v === null || v === undefined) ? '—' : Number(v).toFixed(2); };
|
|
151
171
|
return R.el('tr', {}, [
|
|
152
172
|
R.el('td', {}, [name]),
|