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,220 @@
|
|
|
1
|
+
// src/council/run-stage1-rows.js
|
|
2
|
+
'use strict';
|
|
3
|
+
// Superseded-seat rows + primary-error dead-seat rows (v4.7 D2/E4), moved
|
|
4
|
+
// verbatim from run-stages.js:230-279 (v4.8 PR0 size-gate split, zero
|
|
5
|
+
// behavior). The code ran inline in runStage1; it is now a function.
|
|
6
|
+
// roleFor and seatOf are PARAMETERS, not requires — requiring them back from
|
|
7
|
+
// run-stages would recreate the parent-child cycle that file's tail comment
|
|
8
|
+
// documents eliminating (v4.4.1 F5).
|
|
9
|
+
const { buildRunStatsEntry } = require('./run-assemble');
|
|
10
|
+
// The one keyspace. It lives in ./run-retry-keys (v4.8 Phase 2 T-A1), which IS require-free;
|
|
11
|
+
// run-retry-group.js re-exports it and requires nothing else, so this import's closure
|
|
12
|
+
// terminates at a leaf and cannot re-create the parent-child cycle the header above
|
|
13
|
+
// documents eliminating.
|
|
14
|
+
const { twinAliases, legLossKey } = require('./run-retry-group');
|
|
15
|
+
// v4.8 T-A6 size-gate split: the SUPERSEDED half of these rows moved to
|
|
16
|
+
// ./run-stage1-superseded, taking `../utils/degrade` and the default stderr sink with it.
|
|
17
|
+
// `degrade` is still a parameter HERE, forwarded unchanged, so a caller that omits it still
|
|
18
|
+
// gets that stderr sink — and since council C1 so does one that passes `null` or a `.note`-less
|
|
19
|
+
// object, because that module now resolves the sink by testing the method rather than by a
|
|
20
|
+
// destructuring default. That module requires exactly the two modules the moved block already
|
|
21
|
+
// required from this file — ./run-assemble and ../utils/degrade — so the split adds no import
|
|
22
|
+
// edge the tree did not already carry, and the parent-child cycle the header above documents
|
|
23
|
+
// eliminating stays eliminated. Re-exported at the foot of this file, which is what gives the
|
|
24
|
+
// extraction a second import path to pin function identity across.
|
|
25
|
+
const { supersededRows } = require('./run-stage1-superseded');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Push superseded-seat and primary-error dead-seat rows onto extraRows.
|
|
29
|
+
*
|
|
30
|
+
* v4.8 PR2b Task 8: every alias->leg join below is SEAT-keyed. An alias is not
|
|
31
|
+
* a seat identity: two dead twins used to collapse into ONE dead-seat row, and
|
|
32
|
+
* one twin's first leg could be handed to the other twin as its "final" leg.
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ `seatOf` MUST be the Stage-1 ∪ retry union (run-stages.js's `allSeatOf`).
|
|
35
|
+
* `retry.recoveredLegs` and `retry.stillDeadRetryLegs` are RETRY-wave leg
|
|
36
|
+
* objects that Stage-1's own object-keyed map has never seen, so a Stage-1-only
|
|
37
|
+
* map makes every lookup here return undefined and the whole re-key inert.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ The row's `model` stays the ALIAS (spec §4.7): a seat id must never appear
|
|
40
|
+
* in a ledger row, because pickFallbackChair launches `top.aliases[0]` and
|
|
41
|
+
* 'deepseek#2' is not routable. Two RUNSTATS rows both reading
|
|
42
|
+
* `model: 'deepseek'` remains the CORRECT outcome for two dead twins — runStats
|
|
43
|
+
* is row-per-launch and both seats were paid for. What changed in v4.8 PR4b is
|
|
44
|
+
* downstream: the ledger now groups by (model, resolvedModel), so the LEDGER row
|
|
45
|
+
* count for two dead twins depends on whether their seats produced a leg at all.
|
|
46
|
+
* Two dead twins whose seats produced NO leg (`finalLeg` null — the srcLegStillDead
|
|
47
|
+
* and missingLegStillDead classes below) share the empty resolved key and collapse
|
|
48
|
+
* into ONE ledger row. Two whose still-dead RETRY legs came back resolved
|
|
49
|
+
* DIFFERENTLY still produce TWO, keyed by executable: retryLegBySeat (below)
|
|
50
|
+
* surfaces a real leg, and buildRunStatsEntry stamps `resolvedModel` from
|
|
51
|
+
* `leg.model` whether that leg succeeded or not. Either way runStats keeps two rows.
|
|
52
|
+
* ⚠️ v4.8 council A1 NARROWED that second sentence: it holds for a twin whose retry leg
|
|
53
|
+
* was IDENTIFIED (the retryLegBySeat path). An UNATTRIBUTED twin's row now takes only a
|
|
54
|
+
* borrowed leg's `usage` and NO `resolvedModel`, so two of those share the empty resolved
|
|
55
|
+
* key and collapse into ONE ledger row — the same outcome as the no-leg classes above.
|
|
56
|
+
* MEASURED against buildLedgerRows, not reasoned: two such rows whose borrowed legs
|
|
57
|
+
* resolved DIFFERENTLY gave 2 ledger rows before and give 1 now; two that resolved the
|
|
58
|
+
* SAME gave 1 either way. Nothing about spend moves — a ledger row carries no cost field
|
|
59
|
+
* at all (`buildLedgerRows`' keys are stats, not usage), and runStats, which IS what the
|
|
60
|
+
* run total is summed from, still keeps two rows carrying both legs' `usage`. What the
|
|
61
|
+
* ledger loses is an executable split that `shift()` assigned by arrival order.
|
|
62
|
+
*/
|
|
63
|
+
function pushDeadSeatRows({ o, retry, deadLegs0, stillDeadLegs, stillDeadWaves, extraRows,
|
|
64
|
+
roleFor, seatOf, degrade, twins = twinAliases(o.seats) }) {
|
|
65
|
+
// A leg's join key: its bound seat's id, else its alias — the same fallback
|
|
66
|
+
// `run-retry-keys.js :: seatKey` (imported by run-retry.js since v4.8 T-A1) uses
|
|
67
|
+
// for a roster slot it could not identify, so both sides of every lookup agree.
|
|
68
|
+
const keyOf = (leg) => { const s = seatOf.get(leg); return s ? s.id : (leg.modelInput || leg.model); };
|
|
69
|
+
// v4.8 T2.2 ruling R2, the LEG arms' MINT branch: on an alias the roster repeats, an
|
|
70
|
+
// unbound leg's `keyOf` names BOTH twins, so N dead legs collapsed into ONE row.
|
|
71
|
+
// `legLossKey` falls back to the leg's own taskId there — a real per-leg id, not an
|
|
72
|
+
// inferred seat. It is identical to `keyOf` everywhere else, so only the twin case
|
|
73
|
+
// moves. Deliberately NOT used for `supersededKeys` — which since v4.8 T-A6 lives in
|
|
74
|
+
// `run-stage1-superseded.js :: supersededRows` — nor for `retryLegBySeat` below:
|
|
75
|
+
// those join FIRST-attempt legs against RETRY legs, two different leg populations
|
|
76
|
+
// whose taskIds can never match, and minting there would silently drop the
|
|
77
|
+
// superseded row of every twin whose retry healed.
|
|
78
|
+
// v4.8 T-A6 (SI-TWINS): `twins` is the run's ONE `twinAliases`, threaded in — run-stages.js
|
|
79
|
+
// passes `retry.twins`, the very collection `run-retry.js :: retryStage1Losses` minted every
|
|
80
|
+
// `attemptedSeats` key with. The default is for direct callers (several suites hand this
|
|
81
|
+
// function a fixture `retry` with no `twins` at all), and it re-derives from the SAME roster,
|
|
82
|
+
// so it agrees by construction — but only the threaded value agrees by IDENTITY.
|
|
83
|
+
const rowKeyOf = (l) => legLossKey(seatOf.get(l) || null, l.modelInput || l.model, l, twins);
|
|
84
|
+
|
|
85
|
+
// v4.7 D2/E4 superseded rows + the T-A5 invariant guard: ./run-stage1-superseded (T-A6 split).
|
|
86
|
+
// Appended HERE, before the dead-seat loop, exactly where the moved loop stood — and handed the
|
|
87
|
+
// SAME `keyOf`/`rowKeyOf` this function built, never a second derivation of either.
|
|
88
|
+
extraRows.push(...supersededRows({ retry, deadLegs0, keyOf, rowKeyOf, degrade }));
|
|
89
|
+
|
|
90
|
+
// v4.7 D2/E4 — primary error rows: one per SEAT with NO surviving review
|
|
91
|
+
// (every seat still in stillDeadLegs/stillDeadWaves after retry). E5 amended
|
|
92
|
+
// (Task-4 review, owner-ruled): run-retry.js surfaces the real retry leg
|
|
93
|
+
// (stillDeadRetryLegs), from the ONE branch it exists in —
|
|
94
|
+
// retryLegStillDeadNote, a retry leg that came back unusable. Prefer that
|
|
95
|
+
// REAL leg: status/waveId/usage/duration all real, all from the SAME attempt
|
|
96
|
+
// (no more pairing a retry's waveId with a different attempt's status). The
|
|
97
|
+
// other two dead classes — the retry wave died wholesale (srcLegStillDead),
|
|
98
|
+
// and a partial return that never named this seat (missingLegStillDead) —
|
|
99
|
+
// never get a real leg, so `leg: null` (no phantom waveId: one must never
|
|
100
|
+
// appear without a real billed leg).
|
|
101
|
+
// v4.8 T2.2: a still-dead RETRY leg that named no seat on a twin alias cannot be
|
|
102
|
+
// MATCHED to a row — but it was billed, and dropping it hides the spend that this
|
|
103
|
+
// whole change exists to record. Every unattributed row on that alias is equally
|
|
104
|
+
// dead, so they are handed out one apiece below: the SET of billed legs on the
|
|
105
|
+
// record is exact, and no row claims a seat identity (all carry seat: null).
|
|
106
|
+
// ⚠️ Which row gets which spare is arbitrary in BOTH directions, and stays arbitrary:
|
|
107
|
+
// `shift()` takes the next spare in arrival order, and the rows it feeds are the `!exact`
|
|
108
|
+
// rows of BOTH arms — so a LEG-origin retry leg can be claimed by a WAVE-origin row (a
|
|
109
|
+
// `Symbol('unattributed-seat')` slot below) whose seat produced no leg at all, and two
|
|
110
|
+
// leg-origin rows can swap. v4.8 council A1 bounds what that arbitrariness can SAY: a
|
|
111
|
+
// claimed spare is BILLING ONLY. Its `usage` rides the row, because the run paid for it
|
|
112
|
+
// and dropping it re-opens the spend hole this change exists to close; its `waveId`,
|
|
113
|
+
// `resolvedModel`, `status` and `durationMs` do NOT, because those are one seat's
|
|
114
|
+
// execution and the row is another's — a row that produced no leg would otherwise be
|
|
115
|
+
// emitted with a real attempt's duration and outcome. What remains, disclosed: the SPLIT
|
|
116
|
+
// of a known alias total across its anonymous rows is still arbitrary (row order, not
|
|
117
|
+
// identity). The SET, COUNT and SUM of billed legs are exact, and no row now asserts an
|
|
118
|
+
// execution it cannot own. Pinned by BORROWALL (a row must never differ from the leg-less
|
|
119
|
+
// row by anything but `usage`) — deleting the `usage` line reds the spend half instead.
|
|
120
|
+
const retryLegBySeat = new Map();
|
|
121
|
+
const spareRetryLegs = new Map(); // alias -> still-dead retry legs naming no seat
|
|
122
|
+
for (const leg of retry.stillDeadRetryLegs) {
|
|
123
|
+
const alias = leg.modelInput || leg.model;
|
|
124
|
+
if (seatOf.get(leg) || !twins.has(alias)) { retryLegBySeat.set(keyOf(leg), leg); continue; }
|
|
125
|
+
if (!spareRetryLegs.has(alias)) { spareRetryLegs.set(alias, []); }
|
|
126
|
+
spareRetryLegs.get(alias).push(leg);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// `exact` says the key names ONE seat; `join` is what the lookups below ask with.
|
|
130
|
+
const deadSeats = new Map(); // row key -> { seat, alias, exact, join }
|
|
131
|
+
for (const l of stillDeadLegs) {
|
|
132
|
+
const seat = seatOf.get(l) || null;
|
|
133
|
+
const alias = l.modelInput || l.model;
|
|
134
|
+
const key = rowKeyOf(l);
|
|
135
|
+
deadSeats.set(key, { seat, alias, exact: !!seat || !twins.has(alias), join: key });
|
|
136
|
+
}
|
|
137
|
+
for (const w of stillDeadWaves) {
|
|
138
|
+
// `models` and `seats` are narrowed in LOCKSTEP by run-retry.js's
|
|
139
|
+
// reconcile, so index-zipping them is safe here and nowhere else.
|
|
140
|
+
(w.models || []).forEach((alias, i) => {
|
|
141
|
+
const s = (w.seats || [])[i] || null;
|
|
142
|
+
const join = s ? s.id : alias;
|
|
143
|
+
// R2's MARK branch. This arm has no leg to mint from, and `(waveId, i)` is
|
|
144
|
+
// measurably NOT unique — missingSeatDeadWave emits several records under one
|
|
145
|
+
// waveId each with i === 0, and run-retry.js re-indexes when it narrows a
|
|
146
|
+
// partially healed wave — so inventing an id here would silently merge two
|
|
147
|
+
// distinct seats. A Symbol marks the slot instead: it cannot be joined,
|
|
148
|
+
// compared to a seat id, or mistaken for one, and the row it makes attributes
|
|
149
|
+
// nothing. The ANNOUNCEMENT is the wave's own dead-wave note, whose
|
|
150
|
+
// `data.models`/`data.seats` already carry one entry per slot (PR5c), null for
|
|
151
|
+
// the unnamed ones — which is what a reader sees instead of an id.
|
|
152
|
+
const exact = !!s || !twins.has(alias);
|
|
153
|
+
deadSeats.set(exact ? join : Symbol('unattributed-seat'), { seat: s, alias, exact, join });
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const [, { seat, alias, exact, join }] of deadSeats) {
|
|
158
|
+
// ⚠️ Safe only because `stage1-bind.js :: bindPaddedWave` — which
|
|
159
|
+
// `run-retry-launch.js :: bindRetryWave` calls — DROPS every placeholder bind and keeps
|
|
160
|
+
// placeholder ids unique. Together they guarantee a BOUND still-dead retry leg always
|
|
161
|
+
// resolves `exact` here, never the branch below. Break that conjunction and a bound retry
|
|
162
|
+
// leg's usage is lost silently. Measured unreachable today — see BACKLOG.md's PR #170
|
|
163
|
+
// T2.2 section (the retry-leg-drop finding).
|
|
164
|
+
// ⚠️ Symbol anchors, never bare line numbers. This comment used to name those two
|
|
165
|
+
// guarantees as `(:59)`/`(:53)` into run-retry-launch.js; v4.8 SI-27 moved the block and
|
|
166
|
+
// both went out of range, and `check:citations` stayed GREEN — its CITATION regex
|
|
167
|
+
// requires a `.js` path immediately before `:NNN`, so a bare-paren line ref is invisible
|
|
168
|
+
// to the gate. A green citation gate proves nothing about that form.
|
|
169
|
+
let finalLeg = exact ? retryLegBySeat.get(join) : undefined;
|
|
170
|
+
// v4.8 council A1: claim the spare HERE, and never as `finalLeg`. It is consumed exactly
|
|
171
|
+
// as before — same pool, same `shift()`, same one-apiece hand-out, so the SET of billed
|
|
172
|
+
// legs on the record is unchanged — but it reaches the row through `usage` alone below.
|
|
173
|
+
// Mutually exclusive with the `deadLegs0` fallback: that branch runs only when
|
|
174
|
+
// `attemptedSeats` does NOT hold `join`, which is the condition this one requires.
|
|
175
|
+
const borrowed = !finalLeg && !exact && retry.attemptedSeats.has(join)
|
|
176
|
+
? (spareRetryLegs.get(alias) || []).shift() || null
|
|
177
|
+
: null;
|
|
178
|
+
if (!finalLeg) {
|
|
179
|
+
// `retry.attemptedSeats` — never a scan of stillDeadNotes. Those notes'
|
|
180
|
+
// `data.seat` is ALIAS-valued by contract, so no twin's seat id could
|
|
181
|
+
// ever match one, and this fallback would re-attach a first-attempt leg
|
|
182
|
+
// to a seat that WAS retried. It is also channel-blind on purpose: a
|
|
183
|
+
// still-dead seat rides `dead-leg`, `dead-wave` OR `seat-unbound`
|
|
184
|
+
// depending on how it was lost, and all three mean "we retried it".
|
|
185
|
+
// ⚠️ v4.8 T2.2: `join` and `attemptedSeats` MUST move in lockstep. run-retry.js
|
|
186
|
+
// adds BOTH spellings (the seat key AND legLossKey's), so a minted row asks a
|
|
187
|
+
// question this Set can answer. Mint here alone and the predicate flips: every
|
|
188
|
+
// retried twin re-acquires its own first-attempt leg — which already has its
|
|
189
|
+
// own `superseded` row above, so that leg's cost lands in runStats twice.
|
|
190
|
+
finalLeg = retry.attemptedSeats.has(join)
|
|
191
|
+
? null
|
|
192
|
+
: (deadLegs0.find(l => rowKeyOf(l) === join) || null); // never retried
|
|
193
|
+
}
|
|
194
|
+
// Seat-space role (spec §4.5), matching the review push in run-stages.js:
|
|
195
|
+
// the SEAT's own role, NOT roleAt(o.seats, seat.id) — o.seats is absent on
|
|
196
|
+
// the buildSeats fallback path while the seat is not, so roleAt's unknown-id
|
|
197
|
+
// 'seat' collapses every critic/lens role. Unidentified seats keep the shim.
|
|
198
|
+
// v4.8 PR4c §3.1: `seat` is the seat OBJECT (buildRunStatsEntry compares
|
|
199
|
+
// its id to its own alias) — never `seat.id`, which would make both sides
|
|
200
|
+
// of that comparison undefined and the stamp silently inert. Null here is an
|
|
201
|
+
// orphaned seat: since v4.8 T2.2 two orphaned twins that BOTH reach this loop get
|
|
202
|
+
// TWO rows (they are two seats the run paid for), each carrying no seat at all.
|
|
203
|
+
// ⚠️ Scoped on purpose — this function emits one row per still-dead input it is GIVEN, and
|
|
204
|
+
// cannot emit a row for a seat that never arrives. When a retry wave returned FEWER legs than
|
|
205
|
+
// it launched, `run-retry.js`'s alias-granular `launched` reconcile passed only ONE of two
|
|
206
|
+
// unattributable twins, so the run showed one row. v4.8 T-A4 closed that half in the PRODUCER:
|
|
207
|
+
// it emits `max(slots, 1) - seen` notes now, so both twins arrive here. Nothing here changed.
|
|
208
|
+
const row = buildRunStatsEntry({ leg: finalLeg, model: alias, seat,
|
|
209
|
+
role: seat ? seat.role : roleFor(o, alias), wasChair: false });
|
|
210
|
+
// Overwrite rather than synthesize a leg: `buildRunStatsEntry({leg: null})` is the ONE
|
|
211
|
+
// definition of "a dead seat with nothing to report", so a borrowed row is that row plus
|
|
212
|
+
// one field, and it stays that way if the leg-less defaults ever change. A synthetic
|
|
213
|
+
// `{usage, status}` stub would fork them silently — and a bare `{usage}` stub is worse,
|
|
214
|
+
// because `leg ? leg.status : 'error'` would then stamp `status: undefined`.
|
|
215
|
+
if (borrowed) { row.usage = borrowed.usage || null; }
|
|
216
|
+
extraRows.push(row);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { pushDeadSeatRows, supersededRows };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// src/council/run-stage1-superseded.js
|
|
2
|
+
'use strict';
|
|
3
|
+
// The SUPERSEDED half of the Stage-1 extra rows, lifted out of
|
|
4
|
+
// `run-stage1-rows.js :: pushDeadSeatRows` (v4.8 T-A6 size-gate split, zero behaviour): which
|
|
5
|
+
// FIRST-attempt legs stop being primary, and the one invariant that join can check for itself.
|
|
6
|
+
// A CLOSURE LIFT, not a byte-for-byte move — `keyOf` and `rowKeyOf` stay with the caller,
|
|
7
|
+
// because the dead-seat half left behind needs both, so they ride in as parameters. And it
|
|
8
|
+
// RETURNS its rows rather than pushing onto the caller's `extraRows`: an extracted helper that
|
|
9
|
+
// mutates its caller's accumulator is the shape this release ruled out.
|
|
10
|
+
// Its two requires are the two the moved block already had — ./run-assemble and
|
|
11
|
+
// ../utils/degrade — so the split added no import edge the tree did not already carry.
|
|
12
|
+
// MEASURED, not argued: their eager require closure is 13 first-party modules and contains
|
|
13
|
+
// neither run-stages.js nor run-stage1-rows.js nor this file, so the parent-child cycle
|
|
14
|
+
// run-stage1-rows.js's own header documents eliminating stays eliminated on this path too.
|
|
15
|
+
const { buildRunStatsEntry } = require('./run-assemble');
|
|
16
|
+
// v4.8 T-A5: the ONE voice, for the one thing this file can now detect and refuse (below).
|
|
17
|
+
// ../utils/degrade requires nothing at all, so this is a leaf import and the cycle the
|
|
18
|
+
// header above documents eliminating stays eliminated.
|
|
19
|
+
const { formatDegrade } = require('../utils/degrade');
|
|
20
|
+
// The announcement must not be defeatable by the caller's sink, so anything that cannot carry it
|
|
21
|
+
// falls back to the same sentence on stderr — resolved inside `supersededRows` (see `sink` there),
|
|
22
|
+
// never by a parameter default, which substitutes only for `undefined`.
|
|
23
|
+
// Nothing on this path may throw — it runs after a whole council has
|
|
24
|
+
// already been paid for — so BOTH halves are covered: `formatDegrade` is pure interpolation, and
|
|
25
|
+
// the sink's validating `makeDegrade` is deliberately not called; and the write itself is wrapped,
|
|
26
|
+
// because a stream can fail even when the string cannot. Same guard, same reason, as the real sink
|
|
27
|
+
// (`run-degrade.js :: createDegradeSink`'s `safeEmit` — anchored by symbol; it is the only one).
|
|
28
|
+
const STDERR_NOTICE = { note: (r) => {
|
|
29
|
+
try { process.stderr.write(formatDegrade({ ...r, kind: 'degrade' })); } catch { /* EPIPE etc */ }
|
|
30
|
+
} };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The `superseded` rows for one Stage-1 pass, in `deadLegs0` order.
|
|
34
|
+
*
|
|
35
|
+
* `keyOf` and `rowKeyOf` are the CALLER's — passed in, never re-derived here, and that is the
|
|
36
|
+
* point: this join and the dead-seat rows must ask the SAME keyspace the SAME question. A second
|
|
37
|
+
* spelling of either, or a second `twinAliases` behind `rowKeyOf`, is the desync the mutants
|
|
38
|
+
* DESYNCLEG and DESYNCPLAN pin (see `run-retry-keys.js :: legLossKey` for what the mint buys).
|
|
39
|
+
*
|
|
40
|
+
* @param {object} a.retry the `retryStage1Losses` return value
|
|
41
|
+
* @param {Array} a.deadLegs0 Stage-1's first-attempt dead legs, in their original order
|
|
42
|
+
* @param {Function} a.keyOf leg -> its bound seat's id, else its alias (the caller's)
|
|
43
|
+
* @param {Function} a.rowKeyOf leg -> its ROW key, `legLossKey`-minted (the caller's)
|
|
44
|
+
* @param {object} [a.degrade] the one voice; anything without a callable `.note` — omitted,
|
|
45
|
+
* null, or malformed — falls back to the stderr notice above
|
|
46
|
+
* @returns {Array<object>} rows for the caller to append — this function appends nothing.
|
|
47
|
+
*/
|
|
48
|
+
function supersededRows({ retry, deadLegs0, keyOf, rowKeyOf, degrade }) {
|
|
49
|
+
// ⚠️ v4.8 council C1 — resolved HERE, by testing the METHOD, not by a destructuring default.
|
|
50
|
+
// A parameter default substitutes only for `undefined`, so `degrade: null` and `degrade: {}`
|
|
51
|
+
// both reached `degrade.note(...)` and threw a TypeError. MEASURED through this function in
|
|
52
|
+
// the refusal shape below before this line existed: omitted and a real sink returned 1 row,
|
|
53
|
+
// null and a `.note`-less object threw. That was the SECOND throw path in this one guard
|
|
54
|
+
// (round 1 closed the first, an EPIPE from the sink's own write) and it is the one the
|
|
55
|
+
// `Channel internal` paragraph below already rules out in words: a throw here aborts a council
|
|
56
|
+
// that has already been paid for, over a row miscount. Not reachable in production —
|
|
57
|
+
// run-stages.js hands `ctx.degrade` to `pushDeadSeatRows`, which forwards it here, and `run.js`
|
|
58
|
+
// builds that sink with `createDegradeSink`, PROBED to return a `{note, all}` object whose
|
|
59
|
+
// `note` is callable — but reachable from any fixture or future caller, which is the
|
|
60
|
+
// population a fallback sink exists for at all.
|
|
61
|
+
const sink = degrade && typeof degrade.note === 'function' ? degrade : STDERR_NOTICE;
|
|
62
|
+
const rows = [];
|
|
63
|
+
// v4.7 D2/E4 — superseded rows: a leg-origin seat's FIRST leg stops being
|
|
64
|
+
// primary the moment a retry was actually attempted for it, healed or not
|
|
65
|
+
// (deadLegs0 × recovered-or-still-dead seats — mirrors the healed-set idiom
|
|
66
|
+
// above, extended to the still-dead half E4 also requires). A skipped seat
|
|
67
|
+
// (cost ceiling / unmappable — never got a second leg) keeps NO superseded
|
|
68
|
+
// row: nothing replaced it. Wave-origin seats never had a first leg at all,
|
|
69
|
+
// so they can never appear here regardless of healed/dead outcome (E4).
|
|
70
|
+
// ⚠️ v4.8 T2.2 — this is the ONE join in this function still in the ALIAS-granular
|
|
71
|
+
// keyspace, and it stays safe only while a twin alias cannot produce one RETRIED and one
|
|
72
|
+
// SKIPPED leg. Both sides of the join are LEG-origin (`deadLegs0` below, and retry's
|
|
73
|
+
// recovered/still-dead LEG arrays above — a wave-origin seat has no first leg at all and
|
|
74
|
+
// reaches neither), so the shape to rule out is two UNBOUND LEG-origin twins in different
|
|
75
|
+
// retry units. Two facts, read off run-retry.js and run-retry-group.js rather than
|
|
76
|
+
// inferred: (1) skipping is all-or-nothing per UNIT — every skip branch pushes
|
|
77
|
+
// `...unit.srcWaves`/`...unit.srcLegs` wholesale and `continue`s; (2) such twins always
|
|
78
|
+
// share a unit — bench and critic are one unit each, and the deadLegs loop calls
|
|
79
|
+
// `lensIndexOf(o, null, alias, seatObj)` with `seatObj` null when unbound, falling through
|
|
80
|
+
// to `o.models.indexOf(alias)`, first-match, so both resolve to the SAME lens index.
|
|
81
|
+
// (BOUND twins never needed this: their `keyOf` values already differ. And a wave-origin
|
|
82
|
+
// twin CAN land in a different lens unit than its leg-origin sibling — that split is real,
|
|
83
|
+
// it just cannot reach this join.) Break either fact and the skipped twin takes its own
|
|
84
|
+
// first leg as a primary row AND gets a superseded row for it: one billed leg counted
|
|
85
|
+
// twice.
|
|
86
|
+
// ⚠️ v4.8 T-A5 — and the paragraph above is now the DERIVATION, not the safety. Both facts
|
|
87
|
+
// exist to make ONE statement true: no first leg is SKIPPED while its alias key is superseded.
|
|
88
|
+
// `retry.skippedDeadLegs` states that directly, in leg OBJECTS — the very members of
|
|
89
|
+
// `deadLegs0` the retry declined to attempt — so the test below asks IDENTITY, which no
|
|
90
|
+
// keyspace can blur, at the one place the alias key is relied on. Unreachable while either fact
|
|
91
|
+
// holds (measured over 4000 fuzzed retry runs, 993 of them with skips: 0 hits on the whole
|
|
92
|
+
// shipped condition, all three conjuncts, and 0 on the SKIPPED-and-superseded test alone), so
|
|
93
|
+
// every correct input is byte-identical — and the double count the paragraph above ends on no
|
|
94
|
+
// longer FOLLOWS from breaking a fact: it is refused and announced. Under mutant GUESSPOS the
|
|
95
|
+
// same fuzz hits 85 on both counts; its legs carry taskIds, so row keys are distinct there.
|
|
96
|
+
const skippedLegs = new Set(retry.skippedDeadLegs || []);
|
|
97
|
+
const supersededKeys = new Set([
|
|
98
|
+
...retry.recoveredLegs.map(keyOf),
|
|
99
|
+
...retry.stillDeadLegs.map(keyOf),
|
|
100
|
+
]);
|
|
101
|
+
// Refusing the row is the repair — but ONLY for a leg the dead-seat loop would hand back as its
|
|
102
|
+
// own primary row, which is what `willTakeItsOwnLeg` decides. Since v4.8 T-A6 that loop is the
|
|
103
|
+
// CALLER's (`run-stage1-rows.js :: pushDeadSeatRows`), not this file's — so any "below" naming
|
|
104
|
+
// THAT LOOP points across the boundary (the other two still point inside). run-stages.js merges `skippedDeadLegs`
|
|
105
|
+
// into the `stillDeadLegs` it hands that caller, so a skipped leg IS a still-dead seat there,
|
|
106
|
+
// and that loop's `deadLegs0.find` fallback runs for exactly the keys `attemptedSeats` does NOT
|
|
107
|
+
// hold — and returns ONE leg per key.
|
|
108
|
+
// ⚠️ BOTH conjuncts are load-bearing and BOTH were learned by MEASUREMENT, not argument. The
|
|
109
|
+
// first version of this comment argued the second was unreachable; it was wrong, and the guard
|
|
110
|
+
// built on it lost billed spend (T-A5 rounds 1-3).
|
|
111
|
+
// `attemptedSeats` half: where the key IS held, R2's taskId-less floor has collapsed the twins
|
|
112
|
+
// onto one LEG-LESS row, so the superseded row is the only place that leg's `usage` survives.
|
|
113
|
+
// `=== dead` half: where the key is free but `find` would hand the row a DIFFERENT leg, this
|
|
114
|
+
// leg gets no row of its own and refusing drops its `usage` entirely. Reachable by breaking
|
|
115
|
+
// invariant 2 ALONE — `supersededKeys` also draws from `retry.recoveredLegs` just above, and NO
|
|
116
|
+
// writer of `attemptedSeats` sits on run-retry.js's HEAL branch (they are all on still-dead
|
|
117
|
+
// paths), so a healed twin supersedes the alias while `attemptedSeats` stays empty.
|
|
118
|
+
// Measured end to end, 3 unbound twins in one lens with invariant 2 broken: row keys COLLIDING —
|
|
119
|
+
// billed 0.60, recorded 0.70 both with this guard and without any guard, nothing lost; row keys
|
|
120
|
+
// DISTINCT — 1.10 without the guard against 0.60 with it, the whole 0.50 double count removed
|
|
121
|
+
// and nothing lost. Dropping either conjunct is a named mutant: WIDEGUARD and KEYNOTLEG.
|
|
122
|
+
// ⚠️ The 0.10 surviving on the COLLIDING shape is the R2 collapse floor, NOT a borrow —
|
|
123
|
+
// INSTRUMENTED, because two earlier versions of this sentence named a path that cannot execute
|
|
124
|
+
// here. The twins share ONE `deadSeats` entry and `attemptedSeats` is empty, so `deadLegs0.find`
|
|
125
|
+
// hands that row the HEALED twin's first leg, which already carries a superseded row: one leg,
|
|
126
|
+
// two rows. Probed at the push site — `finalLeg` is that leg (not null), `borrowed` null, spare
|
|
127
|
+
// pool empty. A borrow needs `attemptedSeats.has(join)` TRUE: the negation of this shape.
|
|
128
|
+
// It is announced either way, because a silently corrected number is the failure mode this join
|
|
129
|
+
// is watched for; a THROW would be wrong here, aborting a paid-for council over a row miscount.
|
|
130
|
+
// Channel `internal` — the runtime disagreed with itself, which is not a seat loss. All FOUR
|
|
131
|
+
// readers of a note's `data.seat` (verdict.js, workspace-seats.js, live-dead-seats.js,
|
|
132
|
+
// workspace/seat-space.js) gate on dead-leg/dead-wave/seat-unbound first, so it reaches none, and it cannot
|
|
133
|
+
// move the exit code either: run-stages.js notes a `dead-leg` degrade for every skipped leg
|
|
134
|
+
// before this function is called, so the run is already degraded whenever this can fire.
|
|
135
|
+
const willTakeItsOwnLeg = (dead) => !retry.attemptedSeats.has(rowKeyOf(dead))
|
|
136
|
+
&& deadLegs0.find(l => rowKeyOf(l) === rowKeyOf(dead)) === dead;
|
|
137
|
+
const refuseSupersede = (dead) => {
|
|
138
|
+
const alias = dead.modelInput || dead.model;
|
|
139
|
+
sink.note({ channel: 'internal',
|
|
140
|
+
what: `a superseded row for seat ${alias} was refused`,
|
|
141
|
+
why: 'the retry both SKIPPED this first-attempt leg and superseded its alias key, which the '
|
|
142
|
+
+ 'two invariants above forbid — so that key no longer names one outcome',
|
|
143
|
+
effect: 'the leg keeps its primary row and is counted once; the run continues, but the '
|
|
144
|
+
+ `superseded/primary split for '${alias}' is no longer trustworthy`,
|
|
145
|
+
data: { seat: alias, taskId: dead.taskId || null } });
|
|
146
|
+
};
|
|
147
|
+
for (const dead of deadLegs0) {
|
|
148
|
+
if (!supersededKeys.has(keyOf(dead))) { continue; }
|
|
149
|
+
if (skippedLegs.has(dead) && willTakeItsOwnLeg(dead)) { refuseSupersede(dead); continue; }
|
|
150
|
+
rows.push(buildRunStatsEntry({ leg: dead, model: dead.modelInput || dead.model,
|
|
151
|
+
role: 'superseded', wasChair: false }));
|
|
152
|
+
}
|
|
153
|
+
return rows;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { supersededRows };
|
|
@@ -24,6 +24,11 @@ const { parseJudgeOutput } = require('./parse-stage2');
|
|
|
24
24
|
const { sanitizeName, isAbortExit } = require('./run-launch');
|
|
25
25
|
const runState = require('./run-state');
|
|
26
26
|
const { buildRunStatsEntry } = require('./run-assemble');
|
|
27
|
+
// v4.8 PR3 Task 4: seat binding. artifactName is NOT re-exported from
|
|
28
|
+
// run-launch.js (its exports stop at sanitizeName/isAbortExit), so it comes
|
|
29
|
+
// straight from ./seats — that module requires nothing, zero cycle risk.
|
|
30
|
+
const { artifactName } = require('./seats');
|
|
31
|
+
const { orphanLegNote, bindPaddedWave } = require('./stage1-bind');
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* Stage 2: shared anonymized bundle → judge wave in _scratch → parse + repair.
|
|
@@ -71,6 +76,53 @@ async function runStage2(ctx, { reviews, labels, globalFindings, extraLabeled =
|
|
|
71
76
|
ctx.addWave(wave);
|
|
72
77
|
if (isAbortExit(exitCode)) { return { aborted: exitCode, judgeResults: [], extraRows: [] }; }
|
|
73
78
|
|
|
79
|
+
// v4.8 PR3 Task 4: bind the -s2 wave's legs to the SAME seats reviews[] holds,
|
|
80
|
+
// in `reviews` order — judges is built from that same array by the same
|
|
81
|
+
// `.map`, and real fanout stamps `${waveId}-${i+1}` off that same index
|
|
82
|
+
// (sidecar/leg-ids.js), so this holds even after an SL-2 heal (recovered legs
|
|
83
|
+
// are appended, run-stages.js:153, so `reviews` order is no longer seat order
|
|
84
|
+
// — both arrays still derive from the same `reviews`).
|
|
85
|
+
// §3.4's padding pattern now lives in `stage1-bind.js :: bindPaddedWave`
|
|
86
|
+
// (v4.8 SI-27) — why the roster is padded rather than filtered, and why
|
|
87
|
+
// placeholders are tracked by IDENTITY rather than an id-name prefix test,
|
|
88
|
+
// are in that function's docblock. Only this site's TAIL stays here.
|
|
89
|
+
const s2WaveId = `${o.runId}-s2`;
|
|
90
|
+
// review F(critical): `wave` is legitimately null — a budget/args refusal
|
|
91
|
+
// (run-budget.js failPre) returns `{wave: null, exitCode: 1}`, which
|
|
92
|
+
// isAbortExit does NOT catch (only 130/143), so execution reaches here.
|
|
93
|
+
// Guarded exactly like the leg loop below (`(wave && wave.legs) || []`) —
|
|
94
|
+
// this is the only OTHER dereference of wave.legs in the file.
|
|
95
|
+
const s2Legs = (wave && wave.legs) || [];
|
|
96
|
+
const { seatOf: judgeSeatOf, bindRes, placeholders } =
|
|
97
|
+
bindPaddedWave(s2WaveId, reviews.map(r => r.seat), i => judges[i], s2Legs);
|
|
98
|
+
// An orphan leg (a judge DID land, but no roster slot claims it) gets the
|
|
99
|
+
// same shape as Stage 1's orphanLegNote — `data.legId` present.
|
|
100
|
+
for (const leg of bindRes.orphanLegs) { ctx.degrade.note(orphanLegNote(s2WaveId, leg)); }
|
|
101
|
+
// review F(important): mirrors stage1-bind.js:40's suppression rule
|
|
102
|
+
// verbatim. A wave that returned ZERO legs is already announced on a louder
|
|
103
|
+
// channel (thin-cross-review, or this refusal itself) — there is no
|
|
104
|
+
// "missing seat" fact this adds. An orphan leg means a judge DID land for
|
|
105
|
+
// SOME seat we could not name — reporting the roster's other unbound seats
|
|
106
|
+
// as "missing" would double-count that one failure as two, on a channel
|
|
107
|
+
// whose whole contract is "nothing was guessed": the stray leg might BE the
|
|
108
|
+
// seat this loop would otherwise call missing.
|
|
109
|
+
if (s2Legs.length > 0 && bindRes.orphanLegs.length === 0) {
|
|
110
|
+
for (const seat of bindRes.unbound) {
|
|
111
|
+
if (placeholders.has(seat)) { continue; }
|
|
112
|
+
ctx.degrade.note({
|
|
113
|
+
channel: 'seat-unbound',
|
|
114
|
+
what: `leg for seat ${seat.alias} in wave ${s2WaveId} never returned`,
|
|
115
|
+
// R27-5: `reviews.length` IS the padded roster's length — bindPaddedWave
|
|
116
|
+
// maps 1:1 over the source array, and `Array.prototype.map` preserves
|
|
117
|
+
// length. Pinned (`ROSTERLEN`) in run-stages.test.js, because a
|
|
118
|
+
// substitution into unpinned prose is how a true sentence goes quietly false.
|
|
119
|
+
why: `the wave returned fewer judge legs than its roster of ${reviews.length}`,
|
|
120
|
+
effect: 'That seat did not judge; nothing was guessed and nothing was dropped',
|
|
121
|
+
data: { waveId: s2WaveId, seat: seat.alias },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
74
126
|
const judgeResults = [];
|
|
75
127
|
// v4.7 D2: every judge-repair launch is a billed leg of its own, distinct from
|
|
76
128
|
// the judge's original Stage-2 wave leg it is trying to fix — it gets its own
|
|
@@ -80,8 +132,12 @@ async function runStage2(ctx, { reviews, labels, globalFindings, extraLabeled =
|
|
|
80
132
|
let repairSeq = 0;
|
|
81
133
|
for (const leg of (wave && wave.legs) || []) {
|
|
82
134
|
const judge = leg.modelInput || leg.model;
|
|
135
|
+
const seat = judgeSeatOf.get(leg) || null;
|
|
83
136
|
if (leg.status === 'complete' && leg.summary) {
|
|
84
|
-
|
|
137
|
+
// Mirrors the shape run-launch.js :: materializeReviews ships exactly:
|
|
138
|
+
// seat filename when bound, alias filename (today's behaviour) otherwise.
|
|
139
|
+
const name = seat ? artifactName(seat, 'judge') : `judge-${sanitizeName(judge)}.md`;
|
|
140
|
+
fs.writeFileSync(path.join(o.runDir, name), leg.summary, { mode: 0o600 });
|
|
85
141
|
}
|
|
86
142
|
let conformance = 'clean';
|
|
87
143
|
let parsed = (leg.status === 'complete' && leg.summary)
|
|
@@ -126,7 +182,7 @@ async function runStage2(ctx, { reviews, labels, globalFindings, extraLabeled =
|
|
|
126
182
|
if (parsed.ok) { conformance = 'repaired'; }
|
|
127
183
|
}
|
|
128
184
|
if (!parsed.ok) {
|
|
129
|
-
judgeResults.push({ judge, ok: false, order: null, adjudications: null,
|
|
185
|
+
judgeResults.push({ judge, seat, ok: false, order: null, orderSeats: null, adjudications: null,
|
|
130
186
|
conformance: leg.status === 'complete' ? 'unstructured' : 'clean',
|
|
131
187
|
// #83 (v4.6 Plan 2): the judge's ORIGINAL Stage-2 wave leg, mirroring
|
|
132
188
|
// Stage-1's convention (reviews carry the original wave leg even when a
|
|
@@ -137,9 +193,13 @@ async function runStage2(ctx, { reviews, labels, globalFindings, extraLabeled =
|
|
|
137
193
|
leg: leg || null });
|
|
138
194
|
continue;
|
|
139
195
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
196
|
+
// v4.8 T3.2: labels.seatMap (anonymize.js :: assignLabels) threads through
|
|
197
|
+
// so orderSeats can disambiguate a twin bench's `order`, which stays
|
|
198
|
+
// alias-only. T3.3 wired it into street-cred.js :: rankPositions, via
|
|
199
|
+
// rankings[] in run-assemble.js :: buildTallyInput; `order` never moved.
|
|
200
|
+
const { order, orderSeats } = rankingToOrder(parsed.ranking, labels.labelMap, labels.seatMap);
|
|
201
|
+
judgeResults.push({ judge, seat, ok: true, order, orderSeats, adjudications: parsed.adjudications,
|
|
202
|
+
conformance, leg: leg || null });
|
|
143
203
|
}
|
|
144
204
|
return { aborted: null, judgeResults, extraRows };
|
|
145
205
|
}
|