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,272 @@
|
|
|
1
|
+
// src/council/briefings-chair.js
|
|
2
|
+
'use strict';
|
|
3
|
+
// Chair briefing surface: packet, repair prompt, verdict scale, task
|
|
4
|
+
// templates, no-tools preamble. Moved verbatim from briefings-stage2.js
|
|
5
|
+
// (chunks :16-20 and :154-279 at e8406db; v4.8 PR0 size-gate split, zero
|
|
6
|
+
// behavior). dateLine lives HERE (not required back from stage2) because
|
|
7
|
+
// stage2 requires this module — a back-require would be a cycle that
|
|
8
|
+
// resolves dateLine to undefined at load. briefings-stage2.js re-exports
|
|
9
|
+
// every public name below so existing import paths stay stable.
|
|
10
|
+
//
|
|
11
|
+
// ./seats is the ONLY require here, and it is cycle-free by construction: that
|
|
12
|
+
// module deliberately requires nothing (seats.js header), so the load-order
|
|
13
|
+
// hazard above does not apply to it. Do NOT require ./briefings-stage2.
|
|
14
|
+
const { displayName } = require('./seats');
|
|
15
|
+
|
|
16
|
+
const CHAIR_NO_TOOLS_PREAMBLE =
|
|
17
|
+
'Do NOT use any tools or read any files; everything is in this message; ' +
|
|
18
|
+
'begin immediately with the verdict.';
|
|
19
|
+
|
|
20
|
+
const CHAIR_VERDICT_VALUES = ['Ship it', 'Fix these first', 'Fundamental rethink'];
|
|
21
|
+
|
|
22
|
+
/** Shared date line (spec §4.3) — prepended to every model-facing briefing. */
|
|
23
|
+
function dateLine(date) { return `Today's date is ${date}.`; }
|
|
24
|
+
|
|
25
|
+
/** Verdict-scale addendum (SEAT-BRIEFS.md § Chair verdict-scale addendum; always on headless). */
|
|
26
|
+
const VERDICT_SCALE_ADDENDUM = [
|
|
27
|
+
'After your synthesis, add two closing sections:',
|
|
28
|
+
'',
|
|
29
|
+
'1. HARD QUESTIONS — three to five questions the material\'s author has probably not',
|
|
30
|
+
' asked themselves, chosen so that an unanswerable question reveals a structural gap',
|
|
31
|
+
' (not gotchas — questions whose answers should exist).',
|
|
32
|
+
'2. A final line, alone on the last line, containing ONLY the phrase — no rationale, no',
|
|
33
|
+
' dash, no trailing text of any kind — exactly one of:',
|
|
34
|
+
'',
|
|
35
|
+
' VERDICT: Ship it',
|
|
36
|
+
' VERDICT: Fix these first',
|
|
37
|
+
' VERDICT: Fundamental rethink',
|
|
38
|
+
'',
|
|
39
|
+
' Pick one. "Ship it" = solid, nothing blocking. "Fix these first" = specific gaps',
|
|
40
|
+
' must be resolved first. "Fundamental rethink" = structural problems that cannot be',
|
|
41
|
+
' patched. Name the gaps or the structural problems in the synthesis ABOVE, not on the',
|
|
42
|
+
' VERDICT line itself — that line carries the phrase and nothing else.',
|
|
43
|
+
].join('\n');
|
|
44
|
+
|
|
45
|
+
/** The chair's opening instruction when there is something to weigh (the ordinary case). */
|
|
46
|
+
const CHAIR_TASK =
|
|
47
|
+
'You are the council chair. Write the synthesized verdict across the reviews, ' +
|
|
48
|
+
'rankings, and adjudications below. Weigh each reviewer\'s findings by their ' +
|
|
49
|
+
'peer-validated standing (rank position and adjudication pattern), distinguish ' +
|
|
50
|
+
'findings the bench broadly endorsed from contested or singleton claims, and ' +
|
|
51
|
+
'arrive at an overall assessment of the material.';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The chair's opening instruction when the bench raised nothing at all (LC-10
|
|
55
|
+
* fast-follow, review minor M2).
|
|
56
|
+
*
|
|
57
|
+
* Task 3 fixed this by keeping CHAIR_TASK unchanged and appending a correcting note
|
|
58
|
+
* — asymmetric with buildJudgeBundle, which swaps JUDGE_TASK_B for
|
|
59
|
+
* JUDGE_TASK_B_NO_FINDINGS outright. The un-swapped CHAIR_TASK asks the chair to
|
|
60
|
+
* "distinguish findings the bench broadly endorsed from contested or singleton
|
|
61
|
+
* claims" over an empty tier table and two bare section headings — an instruction
|
|
62
|
+
* whose only satisfiable reading is to invent material — and the chair reads that
|
|
63
|
+
* unfollowable directive FIRST, the correction second. This constant replaces
|
|
64
|
+
* CHAIR_TASK entirely on a clean bench instead: it never asks for a distinction
|
|
65
|
+
* that cannot exist, and the clean-bench framing (still stated, not just implied)
|
|
66
|
+
* is now the only instruction the chair sees, not an amendment to a bad one.
|
|
67
|
+
*/
|
|
68
|
+
const CHAIR_TASK_NO_FINDINGS =
|
|
69
|
+
'You are the council chair. Write the synthesized verdict across the reviews and ' +
|
|
70
|
+
'rankings below. Weigh each reviewer\'s standing by rank position, and arrive at ' +
|
|
71
|
+
'an overall assessment of the material. NOTE: this bench raised NO findings. ' +
|
|
72
|
+
'Every reviewer read the material and reported nothing to fix, which is a valid ' +
|
|
73
|
+
'outcome — not a failed run. Synthesize on that basis: say what the reviews ' +
|
|
74
|
+
'actually establish and where the bench\'s agreement is thin, and do not ' +
|
|
75
|
+
'manufacture concerns to fill the sections below.';
|
|
76
|
+
|
|
77
|
+
/** A section body, or an explicit reason it is empty — never a heading over nothing. */
|
|
78
|
+
function orNone(text, none) {
|
|
79
|
+
return (typeof text === 'string' && text.trim()) ? text : none;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Site (3)'s per-slot zip (v4.8 SI-25, ruling R25-3). `order` and `orderSeats`
|
|
84
|
+
* are PARALLEL per-slot arrays minted together by
|
|
85
|
+
* `anonymize.js :: rankingToOrder`, and a slot is either a scalar or an array
|
|
86
|
+
* (a tie group) — hence the two arms.
|
|
87
|
+
*
|
|
88
|
+
* EVERY read is a fallback, because `orderSeats` is a PARITY SHAPE, not a
|
|
89
|
+
* presence: its `seatOne` returns `(seatMap && seatMap[label]) || null`, and
|
|
90
|
+
* `run-assemble.js :: buildTallyInput` ships the array whenever ANY entry is
|
|
91
|
+
* truthy — so a MIXED array of seat ids and `null`s is a normal shipping shape.
|
|
92
|
+
* No `null` may reach the rendered JSON: this is the one artifact a paid chair
|
|
93
|
+
* reads as authoritative. An absent `orderSeats` returns `order` untouched
|
|
94
|
+
* (spec §4.2's byte-identity promise). ⚠️ That promise is UNCONDITIONAL as of
|
|
95
|
+
* the SHAPESWAP fix — it holds even when the two arrays DISAGREE on a slot's
|
|
96
|
+
* structure (scalar vs array), which no producer here can emit but which two
|
|
97
|
+
* independent reviewers reached for. A disagreeing slot falls back to `order`'s
|
|
98
|
+
* own value and NEVER changes that slot's shape.
|
|
99
|
+
*
|
|
100
|
+
* ⚠️ The SHORT-array arm is DEFENSIVE, and this docblock does not claim
|
|
101
|
+
* otherwise: `rankingToOrder` mints both arrays from one `slots.map`, so no
|
|
102
|
+
* producer in this tree can emit an `orderSeats` shorter than its `order`
|
|
103
|
+
* (measured — 0 mismatches over 24 ranking × seatMap shapes). The arm and its
|
|
104
|
+
* pin guard hand-assembled input, which is why they stay.
|
|
105
|
+
*
|
|
106
|
+
* ⚠️ `||`, deliberately, NOT `??`. `anonymize.js :: rankingToOrder`'s `seatOne`
|
|
107
|
+
* returns `(seatMap && seatMap[label]) || null`, so every entry this array can
|
|
108
|
+
* hold is already truthy-or-null and the two operators are behaviourally
|
|
109
|
+
* identical on it. Where they differ — a falsy-but-present id — `||` falls back
|
|
110
|
+
* to the alias and `??` would render the empty string into the packet, so `||`
|
|
111
|
+
* is never worse and sometimes safer. It is also this spine's convention
|
|
112
|
+
* (`seat || alias` at every other site). PR #189's council raised the swap as
|
|
113
|
+
* A1 (solid); declined on that measurement, and recorded here so the next
|
|
114
|
+
* reader does not re-propose it as a tidy-up.
|
|
115
|
+
*
|
|
116
|
+
* Two named mutants guard the two arms — NULLLEAK the scalar fallback, FLATTIE
|
|
117
|
+
* the tie arm — with their measured red sets in
|
|
118
|
+
* tests/council/chair-packet-seat-mutants.js :: NULLLEAK. Re-run them, never
|
|
119
|
+
* renumber, if this expression or its pins change.
|
|
120
|
+
*/
|
|
121
|
+
function seatKeyedOrder(order, orderSeats) {
|
|
122
|
+
if (!Array.isArray(order) || !Array.isArray(orderSeats)) { return order; }
|
|
123
|
+
return order.map((slot, i) => {
|
|
124
|
+
const seats = orderSeats[i];
|
|
125
|
+
return Array.isArray(slot)
|
|
126
|
+
? slot.map((alias, k) => (Array.isArray(seats) ? seats[k] : null) || alias)
|
|
127
|
+
// ⚠️ `Array.isArray(seats)` here, NOT just `seats || slot`: a scalar `order`
|
|
128
|
+
// slot against an ARRAY `orderSeats` slot returned the seats array RAW, which
|
|
129
|
+
// both changed the slot's shape and could render `[null,null]` — breaking the
|
|
130
|
+
// no-null promise above for the one input class this arm exists to defend.
|
|
131
|
+
// Named mutant SHAPESWAP (drop this guard) in
|
|
132
|
+
// tests/council/chair-packet-seat-mutants.js :: SHAPESWAP.
|
|
133
|
+
: (Array.isArray(seats) ? slot : (seats || slot));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* De-anonymized chair packet (spec §5/§6: the chair sees identities).
|
|
139
|
+
*
|
|
140
|
+
* ⚠️ `seat` is an OBJECT on `reviews[]` and a STRING on `rankings[]` and
|
|
141
|
+
* `adjudications[]`. Deliberate, and declared as such in the @param below.
|
|
142
|
+
* Reviews carry the object because `seats.js :: displayName` is the naming seam
|
|
143
|
+
* (v4.8 SI-25 / R25-4); the other two carry `j.seat.id` because
|
|
144
|
+
* `run-assemble.js :: buildTallyInput` has emitted a string there since PR4c and
|
|
145
|
+
* changing it would move a shipped tally schema. These are three arrays each
|
|
146
|
+
* with one consistently-typed field, not one polymorphic field. PR #189's
|
|
147
|
+
* council raised it as D1 (major, CONTESTED a1/d1/n1); declined on that reading.
|
|
148
|
+
* @param {{reviews: Array<{model: string, text: string, seat?: ?object}>,
|
|
149
|
+
* rankings: Array<{judge: string, seat?: ?string, order: Array<string|string[]>,
|
|
150
|
+
* orderSeats?: ?Array<?string|Array<?string>>}>,
|
|
151
|
+
* adjudications: Array<{findingId: string, judge: string, seat?: ?string, verdict: string}>,
|
|
152
|
+
* tierCounts: object}} args
|
|
153
|
+
* `rankings` and `adjudications` may both be empty — an all-clean bench (LC-10)
|
|
154
|
+
* has nothing to adjudicate, and a Stage 2 whose judges all died has nothing to
|
|
155
|
+
* rank. Each empty section says WHICH of those it is rather than rendering blank.
|
|
156
|
+
*/
|
|
157
|
+
function buildChairPacket({ reviews, rankings, adjudications, tierCounts, date, findings }) {
|
|
158
|
+
// ⚠️ v4.8 SI-25: all three rendering sites below are SEAT-KEYED with an ALIAS
|
|
159
|
+
// FALLBACK. Alias-keyed, a twin bench handed the chair "tier counts:
|
|
160
|
+
// {Confirmed: 1}" beside two identical `A1 — deepseek:` lines, with nothing in
|
|
161
|
+
// the packet able to reconcile them; PR4c seat-keyed the report and the
|
|
162
|
+
// Workspace matrix but not this packet, so the human-facing artifact and the
|
|
163
|
+
// model-facing one disagreed.
|
|
164
|
+
// ⚠️ THIS PACKET IS PROSE, NEVER A LAUNCH ARGUMENT — that boundary is what
|
|
165
|
+
// makes seat ids safe here. A seat id in a model-carrying LAUNCH argument is a
|
|
166
|
+
// non-routable model name and a real paid failure, which is why `order`,
|
|
167
|
+
// `orderSeats`, `tallyInput`, verdict.json, the report and every launcher
|
|
168
|
+
// option stay alias-valued and untouched (run-debate.test.js's parity pin
|
|
169
|
+
// guards that boundary). Nothing routes on this artifact.
|
|
170
|
+
// The fallbacks are load-bearing, not defensive — named mutants ALIASBACK and
|
|
171
|
+
// SEATONLY, with their measured red sets, in
|
|
172
|
+
// tests/council/chair-packet-seat-mutants.js :: ALIASBACK. On a unique-alias
|
|
173
|
+
// bench the seat channel is ABSENT at every site (spec §4.2's
|
|
174
|
+
// emit-when-DIFFERENT rule), so the rendering is byte-identical there by
|
|
175
|
+
// construction — and the review projection that feeds site (1) applies that
|
|
176
|
+
// same rule for a reason its own comment gives.
|
|
177
|
+
const reviewBlocks = reviews
|
|
178
|
+
.map(r => `--- Review by ${displayName(r.seat) || r.model} ---\n${r.text}`).join('\n\n');
|
|
179
|
+
const rankingLines = (rankings || [])
|
|
180
|
+
.map(r => `${r.seat || r.judge}: ${JSON.stringify(seatKeyedOrder(r.order, r.orderSeats))}`)
|
|
181
|
+
.join('\n');
|
|
182
|
+
const adjLines = (adjudications || [])
|
|
183
|
+
.map(a => `${a.findingId} — ${a.seat || a.judge}: ${a.verdict}`)
|
|
184
|
+
.join('\n');
|
|
185
|
+
// ⚠️ v4.8 PR5a T7 (R5-3): surface R8. tally.js has stamped sameModelCorroboration since
|
|
186
|
+
// PR4c and it reached verdict.json — and stopped. Spec §4.6 says the chair packet is
|
|
187
|
+
// REQUIRED to surface it; measured, nothing did. A Confirmed reached only via the
|
|
188
|
+
// raiser's own twin read to the chair exactly like independent corroboration, which is
|
|
189
|
+
// the overstatement R8 was chosen over model-exact exclusion to prevent.
|
|
190
|
+
// Emit-when-present, so a bench that raised none is byte-identical.
|
|
191
|
+
const corroborated = (findings || [])
|
|
192
|
+
// ⚠️ `&& f.id` (council-3 C3, as corrected by measurement): the reported symptom —
|
|
193
|
+
// 'undefined' in the prose — does NOT occur, because Array#join renders undefined as ''.
|
|
194
|
+
// What DID render is worse in kind: "…corroborated only by a same-model seat: ." — the R8
|
|
195
|
+
// caveat firing while naming nobody, in the one artifact a paid chair reads as
|
|
196
|
+
// authoritative. Unnameable findings are dropped, and the emit-when-present guard below
|
|
197
|
+
// then suppresses the section entirely rather than showing an empty list.
|
|
198
|
+
.filter(f => f && f.sameModelCorroboration && f.id)
|
|
199
|
+
.map(f => f.id);
|
|
200
|
+
// Every finding lands in exactly one tier (tally.js countTiers), so the tier
|
|
201
|
+
// counts sum to the record's finding count — which is how an all-clean bench is
|
|
202
|
+
// told apart from a bench whose judges simply never voted.
|
|
203
|
+
const raisedCount = Object.values(tierCounts || {})
|
|
204
|
+
.reduce((s, n) => s + (typeof n === 'number' ? n : 0), 0);
|
|
205
|
+
const tiers = JSON.stringify(tierCounts);
|
|
206
|
+
const parts = [CHAIR_NO_TOOLS_PREAMBLE];
|
|
207
|
+
if (date) { parts.push(dateLine(date)); }
|
|
208
|
+
parts.push(raisedCount === 0 ? CHAIR_TASK_NO_FINDINGS : CHAIR_TASK);
|
|
209
|
+
parts.push(
|
|
210
|
+
`Deterministic tier counts (peers-only cascade): ${tiers}`,
|
|
211
|
+
'--- STAGE-1 REVIEWS (de-anonymized) ---',
|
|
212
|
+
reviewBlocks,
|
|
213
|
+
'--- PEER RANKINGS (judge: order, best first) ---',
|
|
214
|
+
orNone(rankingLines, '(none — no judge produced a usable ranking)'),
|
|
215
|
+
'--- PER-FINDING ADJUDICATIONS ---',
|
|
216
|
+
orNone(adjLines, raisedCount === 0
|
|
217
|
+
? '(none — the bench raised no findings, so there was nothing to adjudicate)'
|
|
218
|
+
: '(none — no judge produced a usable adjudication)'),
|
|
219
|
+
);
|
|
220
|
+
// Placed AFTER the adjudications it qualifies and BEFORE the verdict scale, so the chair
|
|
221
|
+
// reads the caveat while the votes are still in view rather than after being told how to
|
|
222
|
+
// score them.
|
|
223
|
+
if (corroborated.length) {
|
|
224
|
+
parts.push(
|
|
225
|
+
'--- SAME-MODEL CORROBORATION (R8) ---',
|
|
226
|
+
`These findings were agreed only by a seat running the SAME model as the raiser: ${corroborated.join(', ')}.`
|
|
227
|
+
+ ' Their tier reflects concurrence between two generations of one model, not independent support.'
|
|
228
|
+
+ ' Weigh them accordingly.',
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
parts.push(VERDICT_SCALE_ADDENDUM);
|
|
232
|
+
return parts.join('\n\n');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* One-shot chair repair: the VERDICT line was missing (spec §5 chair contract).
|
|
237
|
+
*
|
|
238
|
+
* ⚠️ LC-12: this builder took NO arguments at all, so the repair solo — a fresh
|
|
239
|
+
* session — was asked for a verdict on a synthesis it could not see. The chair's
|
|
240
|
+
* synthesis WAS received; it is the verdict line that is missing. Handing back the
|
|
241
|
+
* synthesis lets the chair pick the verdict its own prose supports instead of
|
|
242
|
+
* re-deriving one from nothing.
|
|
243
|
+
* @param {{synthesis?: string}} [args]
|
|
244
|
+
*/
|
|
245
|
+
function buildChairRepairPrompt({ synthesis } = {}) {
|
|
246
|
+
const text = typeof synthesis === 'string' ? synthesis.trim() : '';
|
|
247
|
+
const prior = text
|
|
248
|
+
? ['--- YOUR SYNTHESIS (verbatim — verdict on THIS) ---', text,
|
|
249
|
+
'--- END OF YOUR SYNTHESIS ---'].join('\n')
|
|
250
|
+
: null;
|
|
251
|
+
return [
|
|
252
|
+
'Do NOT use any tools or read any files; everything is in this message; begin '
|
|
253
|
+
+ 'immediately with the VERDICT line.',
|
|
254
|
+
...(prior ? [prior] : []),
|
|
255
|
+
'Your synthesis was received, but the final parseable line was missing. Emit ONLY '
|
|
256
|
+
+ 'one line, exactly one of:',
|
|
257
|
+
'VERDICT: Ship it',
|
|
258
|
+
'VERDICT: Fix these first',
|
|
259
|
+
'VERDICT: Fundamental rethink',
|
|
260
|
+
].join('\n\n');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
dateLine,
|
|
265
|
+
CHAIR_NO_TOOLS_PREAMBLE,
|
|
266
|
+
CHAIR_VERDICT_VALUES,
|
|
267
|
+
VERDICT_SCALE_ADDENDUM,
|
|
268
|
+
CHAIR_TASK,
|
|
269
|
+
CHAIR_TASK_NO_FINDINGS,
|
|
270
|
+
buildChairPacket,
|
|
271
|
+
buildChairRepairPrompt,
|
|
272
|
+
};
|
|
@@ -9,19 +9,16 @@
|
|
|
9
9
|
* seat/lens/critic information or model names (skill §5.1 / Stage-2 rule).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
// Chair surface lives in ./briefings-chair (v4.8 PR0 size-gate split).
|
|
13
|
+
// Every chair name is re-exported below — run-chair.js, run-assemble.js,
|
|
14
|
+
// briefings-debate.js and the briefings tests import them from here.
|
|
15
|
+
const chair = require('./briefings-chair');
|
|
16
|
+
const { dateLine } = chair;
|
|
17
|
+
|
|
12
18
|
const JUDGE_NO_TOOLS_PREAMBLE =
|
|
13
19
|
'Do NOT use any tools or read any files; everything is in this message; ' +
|
|
14
20
|
'begin immediately with A1:';
|
|
15
21
|
|
|
16
|
-
const CHAIR_NO_TOOLS_PREAMBLE =
|
|
17
|
-
'Do NOT use any tools or read any files; everything is in this message; ' +
|
|
18
|
-
'begin immediately with the verdict.';
|
|
19
|
-
|
|
20
|
-
const CHAIR_VERDICT_VALUES = ['Ship it', 'Fix these first', 'Fundamental rethink'];
|
|
21
|
-
|
|
22
|
-
/** Shared date line (spec §4.3) — prepended to every model-facing briefing. */
|
|
23
|
-
function dateLine(date) { return `Today's date is ${date}.`; }
|
|
24
|
-
|
|
25
22
|
/**
|
|
26
23
|
* Stage-2 headless output contract (spec §5, embedded in the judge bundle).
|
|
27
24
|
*
|
|
@@ -151,137 +148,12 @@ function buildJudgeRepairPrompt({ errors, judgement }) {
|
|
|
151
148
|
].join('\n\n');
|
|
152
149
|
}
|
|
153
150
|
|
|
154
|
-
/** Verdict-scale addendum (SEAT-BRIEFS.md § Chair verdict-scale addendum; always on headless). */
|
|
155
|
-
const VERDICT_SCALE_ADDENDUM = [
|
|
156
|
-
'After your synthesis, add two closing sections:',
|
|
157
|
-
'',
|
|
158
|
-
'1. HARD QUESTIONS — three to five questions the material\'s author has probably not',
|
|
159
|
-
' asked themselves, chosen so that an unanswerable question reveals a structural gap',
|
|
160
|
-
' (not gotchas — questions whose answers should exist).',
|
|
161
|
-
'2. A final line, alone on the last line, containing ONLY the phrase — no rationale, no',
|
|
162
|
-
' dash, no trailing text of any kind — exactly one of:',
|
|
163
|
-
'',
|
|
164
|
-
' VERDICT: Ship it',
|
|
165
|
-
' VERDICT: Fix these first',
|
|
166
|
-
' VERDICT: Fundamental rethink',
|
|
167
|
-
'',
|
|
168
|
-
' Pick one. "Ship it" = solid, nothing blocking. "Fix these first" = specific gaps',
|
|
169
|
-
' must be resolved first. "Fundamental rethink" = structural problems that cannot be',
|
|
170
|
-
' patched. Name the gaps or the structural problems in the synthesis ABOVE, not on the',
|
|
171
|
-
' VERDICT line itself — that line carries the phrase and nothing else.',
|
|
172
|
-
].join('\n');
|
|
173
|
-
|
|
174
|
-
/** The chair's opening instruction when there is something to weigh (the ordinary case). */
|
|
175
|
-
const CHAIR_TASK =
|
|
176
|
-
'You are the council chair. Write the synthesized verdict across the reviews, ' +
|
|
177
|
-
'rankings, and adjudications below. Weigh each reviewer\'s findings by their ' +
|
|
178
|
-
'peer-validated standing (rank position and adjudication pattern), distinguish ' +
|
|
179
|
-
'findings the bench broadly endorsed from contested or singleton claims, and ' +
|
|
180
|
-
'arrive at an overall assessment of the material.';
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* The chair's opening instruction when the bench raised nothing at all (LC-10
|
|
184
|
-
* fast-follow, review minor M2).
|
|
185
|
-
*
|
|
186
|
-
* Task 3 fixed this by keeping CHAIR_TASK unchanged and appending a correcting note
|
|
187
|
-
* — asymmetric with buildJudgeBundle, which swaps JUDGE_TASK_B for
|
|
188
|
-
* JUDGE_TASK_B_NO_FINDINGS outright. The un-swapped CHAIR_TASK asks the chair to
|
|
189
|
-
* "distinguish findings the bench broadly endorsed from contested or singleton
|
|
190
|
-
* claims" over an empty tier table and two bare section headings — an instruction
|
|
191
|
-
* whose only satisfiable reading is to invent material — and the chair reads that
|
|
192
|
-
* unfollowable directive FIRST, the correction second. This constant replaces
|
|
193
|
-
* CHAIR_TASK entirely on a clean bench instead: it never asks for a distinction
|
|
194
|
-
* that cannot exist, and the clean-bench framing (still stated, not just implied)
|
|
195
|
-
* is now the only instruction the chair sees, not an amendment to a bad one.
|
|
196
|
-
*/
|
|
197
|
-
const CHAIR_TASK_NO_FINDINGS =
|
|
198
|
-
'You are the council chair. Write the synthesized verdict across the reviews and ' +
|
|
199
|
-
'rankings below. Weigh each reviewer\'s standing by rank position, and arrive at ' +
|
|
200
|
-
'an overall assessment of the material. NOTE: this bench raised NO findings. ' +
|
|
201
|
-
'Every reviewer read the material and reported nothing to fix, which is a valid ' +
|
|
202
|
-
'outcome — not a failed run. Synthesize on that basis: say what the reviews ' +
|
|
203
|
-
'actually establish and where the bench\'s agreement is thin, and do not ' +
|
|
204
|
-
'manufacture concerns to fill the sections below.';
|
|
205
|
-
|
|
206
|
-
/** A section body, or an explicit reason it is empty — never a heading over nothing. */
|
|
207
|
-
function orNone(text, none) {
|
|
208
|
-
return (typeof text === 'string' && text.trim()) ? text : none;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* De-anonymized chair packet (spec §5/§6: the chair sees identities).
|
|
213
|
-
* @param {{reviews: Array<{model: string, text: string}>,
|
|
214
|
-
* rankings: Array<{judge: string, order: Array<string|string[]>}>,
|
|
215
|
-
* adjudications: Array<{findingId: string, judge: string, verdict: string}>,
|
|
216
|
-
* tierCounts: object}} args
|
|
217
|
-
* `rankings` and `adjudications` may both be empty — an all-clean bench (LC-10)
|
|
218
|
-
* has nothing to adjudicate, and a Stage 2 whose judges all died has nothing to
|
|
219
|
-
* rank. Each empty section says WHICH of those it is rather than rendering blank.
|
|
220
|
-
*/
|
|
221
|
-
function buildChairPacket({ reviews, rankings, adjudications, tierCounts, date }) {
|
|
222
|
-
const reviewBlocks = reviews.map(r => `--- Review by ${r.model} ---\n${r.text}`).join('\n\n');
|
|
223
|
-
const rankingLines = (rankings || [])
|
|
224
|
-
.map(r => `${r.judge}: ${JSON.stringify(r.order)}`)
|
|
225
|
-
.join('\n');
|
|
226
|
-
const adjLines = (adjudications || [])
|
|
227
|
-
.map(a => `${a.findingId} — ${a.judge}: ${a.verdict}`)
|
|
228
|
-
.join('\n');
|
|
229
|
-
// Every finding lands in exactly one tier (tally.js countTiers), so the tier
|
|
230
|
-
// counts sum to the record's finding count — which is how an all-clean bench is
|
|
231
|
-
// told apart from a bench whose judges simply never voted.
|
|
232
|
-
const raisedCount = Object.values(tierCounts || {})
|
|
233
|
-
.reduce((s, n) => s + (typeof n === 'number' ? n : 0), 0);
|
|
234
|
-
const tiers = JSON.stringify(tierCounts);
|
|
235
|
-
const parts = [CHAIR_NO_TOOLS_PREAMBLE];
|
|
236
|
-
if (date) { parts.push(dateLine(date)); }
|
|
237
|
-
parts.push(raisedCount === 0 ? CHAIR_TASK_NO_FINDINGS : CHAIR_TASK);
|
|
238
|
-
parts.push(
|
|
239
|
-
`Deterministic tier counts (peers-only cascade): ${tiers}`,
|
|
240
|
-
'--- STAGE-1 REVIEWS (de-anonymized) ---',
|
|
241
|
-
reviewBlocks,
|
|
242
|
-
'--- PEER RANKINGS (judge: order, best first) ---',
|
|
243
|
-
orNone(rankingLines, '(none — no judge produced a usable ranking)'),
|
|
244
|
-
'--- PER-FINDING ADJUDICATIONS ---',
|
|
245
|
-
orNone(adjLines, raisedCount === 0
|
|
246
|
-
? '(none — the bench raised no findings, so there was nothing to adjudicate)'
|
|
247
|
-
: '(none — no judge produced a usable adjudication)'),
|
|
248
|
-
VERDICT_SCALE_ADDENDUM,
|
|
249
|
-
);
|
|
250
|
-
return parts.join('\n\n');
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* One-shot chair repair: the VERDICT line was missing (spec §5 chair contract).
|
|
255
|
-
*
|
|
256
|
-
* ⚠️ LC-12: this builder took NO arguments at all, so the repair solo — a fresh
|
|
257
|
-
* session — was asked for a verdict on a synthesis it could not see. The chair's
|
|
258
|
-
* synthesis WAS received; it is the verdict line that is missing. Handing back the
|
|
259
|
-
* synthesis lets the chair pick the verdict its own prose supports instead of
|
|
260
|
-
* re-deriving one from nothing.
|
|
261
|
-
* @param {{synthesis?: string}} [args]
|
|
262
|
-
*/
|
|
263
|
-
function buildChairRepairPrompt({ synthesis } = {}) {
|
|
264
|
-
const text = typeof synthesis === 'string' ? synthesis.trim() : '';
|
|
265
|
-
const prior = text
|
|
266
|
-
? ['--- YOUR SYNTHESIS (verbatim — verdict on THIS) ---', text,
|
|
267
|
-
'--- END OF YOUR SYNTHESIS ---'].join('\n')
|
|
268
|
-
: null;
|
|
269
|
-
return [
|
|
270
|
-
'Do NOT use any tools or read any files; everything is in this message; begin '
|
|
271
|
-
+ 'immediately with the VERDICT line.',
|
|
272
|
-
...(prior ? [prior] : []),
|
|
273
|
-
'Your synthesis was received, but the final parseable line was missing. Emit ONLY '
|
|
274
|
-
+ 'one line, exactly one of:',
|
|
275
|
-
'VERDICT: Ship it',
|
|
276
|
-
'VERDICT: Fix these first',
|
|
277
|
-
'VERDICT: Fundamental rethink',
|
|
278
|
-
].join('\n\n');
|
|
279
|
-
}
|
|
280
|
-
|
|
281
151
|
module.exports = {
|
|
282
|
-
JUDGE_NO_TOOLS_PREAMBLE, CHAIR_NO_TOOLS_PREAMBLE
|
|
283
|
-
|
|
152
|
+
JUDGE_NO_TOOLS_PREAMBLE, CHAIR_NO_TOOLS_PREAMBLE: chair.CHAIR_NO_TOOLS_PREAMBLE,
|
|
153
|
+
CHAIR_VERDICT_VALUES: chair.CHAIR_VERDICT_VALUES,
|
|
154
|
+
JUDGE_OUTPUT_CONTRACT, VERDICT_SCALE_ADDENDUM: chair.VERDICT_SCALE_ADDENDUM, dateLine,
|
|
284
155
|
JUDGE_TASK_B, JUDGE_TASK_B_NO_FINDINGS, NO_FINDINGS_INDEX,
|
|
285
|
-
CHAIR_TASK, CHAIR_TASK_NO_FINDINGS,
|
|
286
|
-
buildJudgeBundle, buildJudgeRepairPrompt,
|
|
156
|
+
CHAIR_TASK: chair.CHAIR_TASK, CHAIR_TASK_NO_FINDINGS: chair.CHAIR_TASK_NO_FINDINGS,
|
|
157
|
+
buildJudgeBundle, buildJudgeRepairPrompt,
|
|
158
|
+
buildChairPacket: chair.buildChairPacket, buildChairRepairPrompt: chair.buildChairRepairPrompt,
|
|
287
159
|
};
|
package/src/council/debate.js
CHANGED
|
@@ -11,7 +11,17 @@
|
|
|
11
11
|
* Also holds the pure pre-debate helpers (target selection, dispute detection, re-vote bundling) moved from run-debate.js (v4.7 PR0).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const { peersOf, unattributedPeerDrops } = require('./peer-split');
|
|
15
|
+
|
|
16
|
+
// __proto__: null — an inherited/unknown action (e.g. "toString") must fall
|
|
17
|
+
// through the `|| 'no-response'` guards below and in run-debate.js, never
|
|
18
|
+
// resolve off Object.prototype: a function value there survives in memory
|
|
19
|
+
// but JSON.stringify drops it, silently deleting `action` from the finding.
|
|
20
|
+
// Defense-in-depth: `parse-stage2.js :: parseDebateDefense` is an
|
|
21
|
+
// allowlist that normalises every foreign action to `'no-response'`
|
|
22
|
+
// before this table is consulted — no real run reaches it with a
|
|
23
|
+
// foreign key. See `run-debate.test.js :: DOUBLEBREACH`.
|
|
24
|
+
const PAST_TENSE = { __proto__: null, defend: 'defended', amend: 'amended', withdraw: 'withdrawn', 'no-response': 'no-response' };
|
|
15
25
|
|
|
16
26
|
// The debate-role vocabulary: a debate leg is an extra leg by an already-benched model,
|
|
17
27
|
// never an extra ledger row and never that model's ledger identity. Through v4.6 this Set
|
|
@@ -23,12 +33,44 @@ const DEBATE_ROLES = new Set(['rebuttal', 'revote']);
|
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
35
|
* Reassemble the tally input after the debate round.
|
|
26
|
-
*
|
|
36
|
+
*
|
|
37
|
+
* v4.8 PR3 Task 6 (spec §4.5): `defenseByRaiser` and `revoteByJudge` are keyed
|
|
38
|
+
* on the SEAT (`seat.id`), which equals the alias for every bench without a
|
|
39
|
+
* repeated entry. `aliasOf` is the ONE seat→alias projection run-debate.js
|
|
40
|
+
* builds from `o.seats`; it is the identity for any key that is not a known
|
|
41
|
+
* seat id, so a direct-require caller with no seats and the reserved 'claude'
|
|
42
|
+
* key both behave exactly as before.
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ SI-13: when `aliasOf` is omitted, the fail-open push below uses the raw
|
|
45
|
+
* key unchanged (`alias = key`), leaving the SEAT id in the alias-space
|
|
46
|
+
* `judge` field instead of projecting it to its alias. `judge` must stay
|
|
47
|
+
* alias-space: via `peer-split.js :: peersOf`'s `v.judge !== f.raiser`, a
|
|
48
|
+
* seat id there can silently retier a finding. `report.js :: toModel` never
|
|
49
|
+
* retiers — it copies `f.tier`, never recomputes it — but the same seat id
|
|
50
|
+
* still reaches its `columnFor` join as an out-of-contract `judge`.
|
|
51
|
+
* `applyDebate` has no seat roster of its own, so building that projection
|
|
52
|
+
* stays the caller's obligation.
|
|
53
|
+
*
|
|
54
|
+
* The gap needs a caller that both omits `aliasOf` and hits a repeated alias,
|
|
55
|
+
* and none exists today: the sole non-test caller, `run-debate.js`, always
|
|
56
|
+
* supplies `aliasOf` (see the warning comment at its call site), and
|
|
57
|
+
* `package.json`'s `exports` map publishes only `./opencode-client`, blocking
|
|
58
|
+
* a deep import of `amicus/src/council/debate` from outside this package.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ This block used to declare a `provisionalRecord` param the destructure has
|
|
61
|
+
* never taken (pre-existing doc rot). Callers still pass one and it is still
|
|
62
|
+
* ignored — the DOC is what was wrong, so only the doc was fixed. Deliberately
|
|
63
|
+
* not wired up: applyDebate reads `previousTier` off `tallyInput.findings[]`,
|
|
64
|
+
* which run-debate.js stamps, and a second source of that fact is how it
|
|
65
|
+
* silently read null before.
|
|
66
|
+
*
|
|
67
|
+
* @param {{tallyInput: object,
|
|
27
68
|
* defenseByRaiser: Object<string, Object<string, object>>,
|
|
28
|
-
* revoteByJudge: Object<string, Object<string, {verdict, reason?}
|
|
69
|
+
* revoteByJudge: Object<string, Object<string, {verdict, reason?}>>,
|
|
70
|
+
* aliasOf?: function(string): string}} args
|
|
29
71
|
* @returns {{input: object, debateFindings: Array}}
|
|
30
72
|
*/
|
|
31
|
-
function applyDebate({ tallyInput, defenseByRaiser, revoteByJudge }) {
|
|
73
|
+
function applyDebate({ tallyInput, defenseByRaiser, revoteByJudge, aliasOf }) {
|
|
32
74
|
// Deep-ish clone the mutable arrays we touch (findings + adjudications).
|
|
33
75
|
const findings = tallyInput.findings.map(f => ({ ...f }));
|
|
34
76
|
const adjudications = tallyInput.adjudications.map(a => ({ ...a }));
|
|
@@ -54,12 +96,29 @@ function applyDebate({ tallyInput, defenseByRaiser, revoteByJudge }) {
|
|
|
54
96
|
delete f.previousTier; // provisional-only scratch field, never written to tally input
|
|
55
97
|
}
|
|
56
98
|
|
|
57
|
-
// Re-vote replacement: replace the wave
|
|
58
|
-
|
|
99
|
+
// Re-vote replacement: replace the wave SEAT's entry on each bundled id.
|
|
100
|
+
// `(a.seat || a.judge)` is the seat-space read every consumer uses — `seat` is
|
|
101
|
+
// emit-when-different (Task 5), so a bench with no repeated alias reduces this
|
|
102
|
+
// to today's `a.judge === key` exactly. Matching on the alias instead is D5:
|
|
103
|
+
// two twins share one `judge` value, so the first row wins and the second
|
|
104
|
+
// twin's re-vote is silently dropped.
|
|
105
|
+
for (const [key, perId] of Object.entries(revoteByJudge || {})) {
|
|
59
106
|
for (const [id, rv] of Object.entries(perId)) {
|
|
60
|
-
const entry = adjudications.find(a => a.findingId === id && a.judge ===
|
|
107
|
+
const entry = adjudications.find(a => a.findingId === id && (a.seat || a.judge) === key);
|
|
61
108
|
if (entry) { entry.verdict = rv.verdict; }
|
|
62
|
-
else {
|
|
109
|
+
else {
|
|
110
|
+
// Fail-open push (a stateless leg re-voting an id it never adjudicated).
|
|
111
|
+
// `judge` MUST stay alias-space: via peer-split.js :: peersOf's
|
|
112
|
+
// `v.judge !== f.raiser`, a seat id there can silently retier a
|
|
113
|
+
// finding. report.js :: toModel never retiers — it copies `f.tier`,
|
|
114
|
+
// never recomputes it — but the same seat id still reaches its
|
|
115
|
+
// `columnFor` join as an out-of-contract `judge`.
|
|
116
|
+
// The seat rides beside it, emitted only when it differs — so a unique
|
|
117
|
+
// bench pushes today's exact row.
|
|
118
|
+
const alias = aliasOf ? aliasOf(key) : key;
|
|
119
|
+
adjudications.push({ findingId: id, judge: alias, verdict: rv.verdict,
|
|
120
|
+
...(alias !== key ? { seat: key } : {}) });
|
|
121
|
+
}
|
|
63
122
|
}
|
|
64
123
|
}
|
|
65
124
|
|
|
@@ -88,7 +147,10 @@ function decorateRecord(record, debateFindings) {
|
|
|
88
147
|
* 'superseded' for an original leg a successful repair replaced, and 'repair' for
|
|
89
148
|
* a repair attempt that itself never became usable (error status rides naturally
|
|
90
149
|
* off the raw leg). The rebuttal/revote legs never enter meta.models, so the
|
|
91
|
-
* ledger stays one row per (
|
|
150
|
+
* ledger stays one row per (run × model × resolvedModel) pair — a debate round
|
|
151
|
+
* can never ADD a row (v4.8 PR4b: meta.models is still the row driver, and the
|
|
152
|
+
* pair fan-out only splits an alias whose own joinable rows resolved
|
|
153
|
+
* differently). DEBATE_ROLES remains the debate-role
|
|
92
154
|
* vocabulary (rebuttal/revote); the ledger's overwrite protection for ALL FOUR
|
|
93
155
|
* of these row-per-launch roles — rebuttal, revote, superseded AND repair —
|
|
94
156
|
* lives in ledger.js's own LEDGER_JOIN_ROLES allowlist (v4.7 D4, Task 7):
|
|
@@ -129,20 +191,52 @@ function nothingToDebate(provisionalRecord) {
|
|
|
129
191
|
return n === 0;
|
|
130
192
|
}
|
|
131
193
|
|
|
132
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* Seats whose provisional adjudications dispute at least one bundled id.
|
|
196
|
+
* v4.8 PR3 Task 6: returns SEAT ids and dedups by seat (D6) — deduping by alias
|
|
197
|
+
* collapses two disputing bench positions into one re-vote leg, so the twin that
|
|
198
|
+
* never re-voted keeps a verdict the round meant to replace. run-debate.js
|
|
199
|
+
* projects these back to aliases for the launcher.
|
|
200
|
+
*/
|
|
133
201
|
function disputingJudges(provisionalRecord, bundledIds) {
|
|
134
202
|
const ids = new Set(bundledIds);
|
|
135
203
|
const judges = new Set();
|
|
136
204
|
for (const f of provisionalRecord.findings) {
|
|
137
205
|
if (!ids.has(f.id)) { continue; }
|
|
138
206
|
for (const adj of f.adjudications || []) {
|
|
139
|
-
if (adj.verdict === 'dispute') { judges.add(adj.judge); }
|
|
207
|
+
if (adj.verdict === 'dispute') { judges.add(adj.seat || adj.judge); }
|
|
140
208
|
}
|
|
141
209
|
}
|
|
142
210
|
return [...judges];
|
|
143
211
|
}
|
|
144
212
|
|
|
145
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* Group Contested+Disputed findings by raiser SEAT (defense targets).
|
|
215
|
+
* v4.8 PR3 Task 6: keyed on `f.raiserSeat || f.raiser`. Claude's findings carry
|
|
216
|
+
* no `raiserSeat`, so its key stays the literal 'claude' and run-debate.js's
|
|
217
|
+
* `.filter(m => m !== 'claude')` / `byRaiser.claude` keep working unchanged.
|
|
218
|
+
* v4.8 Phase 2 T-B2: `peerVerdicts` below CALLS peer-split.js :: peersOf —
|
|
219
|
+
* the one predicate tally.js also calls — so the defense brief's peer split and
|
|
220
|
+
* the tally the chair reads agree by construction rather than by two spellings
|
|
221
|
+
* kept in step by hand. Until T-B2 this file spelled its own copy with TWO
|
|
222
|
+
* branches while `peersOf` had THREE: the outer `f.raiser ? … : …` arm was
|
|
223
|
+
* missing here, so a finding whose raiser is falsy ('' from the MCP path,
|
|
224
|
+
* `undefined` from the CLI path) was briefed a peer split the tally never
|
|
225
|
+
* computed. `unattributedPeerDrops` rides beside it under the same emit rule as
|
|
226
|
+
* tally.js's, so the two documents also carry the same mark.
|
|
227
|
+
* v4.8 T-B4 then changed what that branch COMPUTES — a falsy raiser no longer
|
|
228
|
+
* corroborates itself, and the seat compare was lifted ABOVE the raiser test so
|
|
229
|
+
* the seats decide first whatever `f.raiser` says (P0). Because both documents
|
|
230
|
+
* read the one function, the brief moved with the tally at no cost here. Two
|
|
231
|
+
* consequences are visible from this file: the briefed peer split of a
|
|
232
|
+
* falsy-raiser finding changes, and some such findings no longer reach a brief
|
|
233
|
+
* at all, because dropping their unattributable votes moves them off
|
|
234
|
+
* Contested/Disputed and `debateTargets` skips every other tier.
|
|
235
|
+
* debate.js :: applyDebate reads `(a.seat || a.judge)` and
|
|
236
|
+
* debate.js :: disputingJudges reads `adj.seat || adj.judge` — both already
|
|
237
|
+
* seat-space, both read at T-B2 — so this was the last hand-rolled peer filter
|
|
238
|
+
* left in this file.
|
|
239
|
+
*/
|
|
146
240
|
function debateTargets(provisionalRecord, tallyInput) {
|
|
147
241
|
const claimById = new Map(tallyInput.findings.map(f => [f.id, f]));
|
|
148
242
|
const byRaiser = {};
|
|
@@ -151,9 +245,20 @@ function debateTargets(provisionalRecord, tallyInput) {
|
|
|
151
245
|
if (f.tier !== 'Contested' && f.tier !== 'Disputed') { continue; }
|
|
152
246
|
previousTier[f.id] = f.tier;
|
|
153
247
|
const src = claimById.get(f.id) || {};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
248
|
+
// ⚠️ The trailing `.map(a => a.verdict)` is load-bearing: briefings-debate's
|
|
249
|
+
// verdictCounts indexes its counter BY THE ELEMENT, so a list of
|
|
250
|
+
// adjudication OBJECTS renders "0 dispute, 0 agree, 0 neutral" — a silent
|
|
251
|
+
// all-zero byte-identical to the no-data case, i.e. a paid brief telling the
|
|
252
|
+
// model nobody disputed it.
|
|
253
|
+
const adjs = f.adjudications || [];
|
|
254
|
+
const peerVerdicts = peersOf(f, adjs).map(a => a.verdict);
|
|
255
|
+
// Emitted only when > 0, exactly as tally.js emits it, so a run that does
|
|
256
|
+
// not orphan one side of a twin pair produces a byte-identical row.
|
|
257
|
+
const drops = unattributedPeerDrops(f, adjs);
|
|
258
|
+
const raiserKey = f.raiserSeat || f.raiser;
|
|
259
|
+
(byRaiser[raiserKey] = byRaiser[raiserKey] || []).push({ id: f.id, claim: src.claim,
|
|
260
|
+
severity: f.severity, location: src.location, peerVerdicts,
|
|
261
|
+
...(drops > 0 ? { unattributedPeerDrops: drops } : {}), disputeReasons: [] });
|
|
157
262
|
}
|
|
158
263
|
return { byRaiser, previousTier };
|
|
159
264
|
}
|