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
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
"date": { "type": "string" },
|
|
18
18
|
"models": { "type": "array", "items": { "type": "string" } },
|
|
19
19
|
"chair": { "type": ["string", "null"] },
|
|
20
|
-
"claudeInCouncil": { "type": "boolean" }
|
|
20
|
+
"claudeInCouncil": { "type": "boolean" },
|
|
21
|
+
"seats": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"description": "v4.8, optional. The run's seat table, one {id, alias, role, lens, position} entry per BENCH seat in bench order, echoed verbatim from the tally input. The engine emits it only when the bench repeats an alias — the one case where the `alias#N` ids on findings[].raiserSeat, adjudications[].seat and runStats[].seat resolve to nothing else in the document, since meta.models is the ALIAS list. Bench-only: 'claude' is never a seat, so meta.models.length need not equal seats.length and the two must never be joined positionally. ABSENCE DOES NOT MEAN the bench had no repeated alias — hand-assembled and MCP-assembled input reaches tally() with no seat machinery behind it; it means only that no seat table is available. `items` is deliberately unconstrained: consumers must tolerate a table that is not an array of seat objects (the report and Workspace renderers fall back to alias space whole rather than failing), and the MCP input schema accepts the same latitude."
|
|
24
|
+
}
|
|
21
25
|
}
|
|
22
26
|
},
|
|
23
27
|
"judged": { "type": "boolean" },
|
|
@@ -30,7 +34,14 @@
|
|
|
30
34
|
"model": { "type": "string" },
|
|
31
35
|
"withSelf": { "type": ["number", "null"] },
|
|
32
36
|
"peersOnly": { "type": ["number", "null"] },
|
|
33
|
-
"perJudgeRank": {
|
|
37
|
+
"perJudgeRank": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"description": "This row's rank position under each judge that ranked it. KEYED BY THE JUDGE'S SEAT ID where the judge has one that differs from its alias, and by the judge's alias otherwise (v4.8 T3.3) — the same emit-when-DIFFERENT rule as every other seat field in this document, so a bench with no repeated alias is keyed exactly as before. Before T3.3 the key was always the alias, which last-wins-collapsed two twin judges into one entry while withSelf/peersOnly averaged over both, so the map and the averages could disagree about the same row. ⚠️ They can still disagree on hand-assembled input that repeats a judge alias and carries no seat, because nothing in such a document can tell those judges apart."
|
|
40
|
+
},
|
|
41
|
+
"seat": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "v4.8, optional. One row per SEAT rather than per alias, and this row's seat id, when it differs from `model` (its alias). SHIPPED at v4.8 T3.3 — street-cred.js :: computeStreetCred is the producer; this property was DECLARED one task earlier, at T3.2, and read 'documentation-only ahead of its producer' until T3.3 landed. The engine emits it only when the bench repeats an alias, so a unique-alias run carries no seat on any row and its document is byte-identical to a pre-v4.8 one. verdict.js :: buildVerdict carries it through its own closed streetCred projection, so the field is never silently stripped. No `additionalProperties: false` above means an additive field was always validated here even before this declaration; this documents the shape rather than changing what is accepted."
|
|
44
|
+
}
|
|
34
45
|
}
|
|
35
46
|
}
|
|
36
47
|
},
|
|
@@ -52,6 +63,27 @@
|
|
|
52
63
|
"confidence": { "enum": ["thin", "solid"] },
|
|
53
64
|
"tierOverride": { "type": ["object", "null"] },
|
|
54
65
|
"adjudications": { "type": "array", "items": { "type": "object" } },
|
|
66
|
+
"raiserSeat": {
|
|
67
|
+
"type": ["string", "null"],
|
|
68
|
+
"description": "v4.8, optional. The raising SEAT's id (e.g. 'deepseek#1'), echoed from the input finding. Emitted only when it differs from the raiser's alias, so a bench with no repeated alias never carries it; `raiser` stays the alias in every case. Resolve it against meta.seats."
|
|
69
|
+
},
|
|
70
|
+
"location": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "v4.8, optional (SI-23). Echoed verbatim from the input finding when present; absent when the input finding carried none, so a pre-SI-23 document is byte-identical. tally() does not read it for tiering — it exists here only because the input schema (mcp-tools.js's amicus_council_tally) now preserves it instead of silently dropping it, matching what anonymize.js :: toGlobalFindings already carries on the engine's own path."
|
|
73
|
+
},
|
|
74
|
+
"claim": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"description": "v4.8, optional (SI-23 fix round 1, PR #183 council A1/B1). Echoed verbatim from the input finding when present, the same emit-when-present rule as `location` immediately above; absent when the input finding carried none. `claim` was already declared on the amicus_council_tally input schema before SI-23 R10 — R10 fixed `location`'s identical drop in this same outFindings map and left this sibling gap for a later round, closed here."
|
|
77
|
+
},
|
|
78
|
+
"sameModelCorroboration": {
|
|
79
|
+
"const": true,
|
|
80
|
+
"description": "v4.8, optional, TRUE ONLY (never emitted as false, so a document without it is byte-identical to a pre-v4.8 one). Set when, after the seat-aware raiser exclusion, at least one AGREEING peer of this finding shares the raiser's alias — corroboration that is real but came from another seat of the same model, so it is not independent. WRONG IN BOTH DIRECTIONS, by construction: the comparison is on the ALIAS only. It MISSES one model reached under two aliases (e.g. --models gpt-5,openai/gpt-5), because votes carry no resolved-executable id. And it FIRES FALSELY on a SPLIT alias — one alias whose two seats resolved to different executables — telling a reader to discount a genuinely independent cross-executable corroboration. The reliability ledger keys identity on (alias, resolvedModel) for the same run, so the two documents can disagree about what 'the same model' means. Treat this as 'worth a second look', never as proof."
|
|
81
|
+
},
|
|
82
|
+
"unattributedPeerDrops": {
|
|
83
|
+
"type": "integer",
|
|
84
|
+
"minimum": 1,
|
|
85
|
+
"description": "v4.8, optional, EMITTED ONLY WHEN NON-ZERO (never emitted as 0, so a run that does not orphan exactly one side of a twin pair produces a document byte-identical to a pre-T-B2 one; the minimum states that emit rule rather than merely describing it). How many of this finding adjudications the peer split excluded WITHOUT being able to attribute them. TWO SHAPES produce it. (1) RAISER NAMED: excluded on the ALIAS branch while exactly ONE side of the pair carried a seat id - the finding has a raiserSeat and the vote has none, or the reverse; a seat-less vote bearing the raiser own alias cannot be told apart from the raiser own vote, so it is dropped. (2) RAISER NOT NAMED (v4.8 T-B4): the finding raiser is empty or missing and so is the vote judge, so the vote may be the unnamed raiser own and is not counted as peer signal. NEITHER SHAPE includes a vote the SEAT IDS settled: when the vote and the finding both carry a seat id the engine knows whose vote it is, so that exclusion is attributed rather than ambiguous and is never counted here. WARNING - on shape (1) this is an ANNOUNCEMENT, not a repair: that vote stays excluded, basis does not count it, and tier/confidence are computed without it, by owner ruling (mark explicitly, attribute nothing). On shape (2) the exclusion IS the change - those votes used to be counted, which let an unnamed raiser corroborate its own finding, so basis and tier can both move. Read it as: N votes on this finding were excluded without anyone being able to attribute them - each is EITHER a real peer signal being discarded OR the raiser own vote being correctly excluded, and the document does not say which, because nothing in it can. Treat it as a POSSIBLE undercount of up to N, never as an established one; not being able to tell is why the drop is announced at all. The defense brief computes the same number from the same function, so the two documents cannot disagree."
|
|
86
|
+
},
|
|
55
87
|
"debate": {
|
|
56
88
|
"type": "object",
|
|
57
89
|
"properties": {
|
|
@@ -49,6 +49,10 @@
|
|
|
49
49
|
"type": "string"
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
|
+
"seats": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"description": "v4.8, optional. The tally record's meta.seats promoted to the top level beside seatLoss — one {id, alias, role, lens, position} entry per BENCH seat in bench order. Present only when the tally record carried one, i.e. only when the bench repeated an alias. It is what makes the `alias#N` ids on findings[].raiserSeat, adjudications[].seat and runStats[].seat resolvable from the verdict ALONE. Bench-only: 'claude' is never a seat, so this must never be joined positionally to `council`. ABSENCE DOES NOT MEAN the bench had no repeated alias — it means no seat table is available; a verdict written before v4.8, or assembled by hand, has none. `items` is deliberately unconstrained: `council report` accepts any array here and falls back to alias space WHOLE unless every element is an object carrying a string `id`, rather than failing to render."
|
|
55
|
+
},
|
|
52
56
|
"claudeInCouncil": {
|
|
53
57
|
"type": "boolean"
|
|
54
58
|
},
|
|
@@ -124,6 +128,17 @@
|
|
|
124
128
|
"adjudications": {
|
|
125
129
|
"type": "array"
|
|
126
130
|
},
|
|
131
|
+
"raiserSeat": {
|
|
132
|
+
"type": [
|
|
133
|
+
"string",
|
|
134
|
+
"null"
|
|
135
|
+
],
|
|
136
|
+
"description": "v4.8, optional. The raising SEAT's id (e.g. 'deepseek#1'), carried through from the tally record. Emitted only when it differs from the raiser's alias, so a bench with no repeated alias never carries it; `raiser` stays the alias in every case. Resolve it against the top-level `seats` table. Before v4.8 the verdict named seats it could not resolve — a reader holding only the verdict could not tell which of two same-alias seats raised a finding."
|
|
137
|
+
},
|
|
138
|
+
"sameModelCorroboration": {
|
|
139
|
+
"const": true,
|
|
140
|
+
"description": "v4.8, optional, TRUE ONLY (never emitted as false, so a document without it is byte-identical to a pre-v4.8 one). Carried through from the tally record: after the seat-aware raiser exclusion, at least one AGREEING peer of this finding shares the raiser's alias — corroboration that is real but came from another seat of the same model, so it is not independent. WRONG IN BOTH DIRECTIONS, by construction: the comparison is on the ALIAS only. It MISSES one model reached under two aliases (e.g. --models gpt-5,openai/gpt-5), because votes carry no resolved-executable id. And it FIRES FALSELY on a SPLIT alias — one alias whose two seats resolved to different executables — telling a reader to discount a genuinely independent cross-executable corroboration. The reliability ledger keys identity on (alias, resolvedModel) for the same run, so the two documents can disagree about what 'the same model' means. Treat this as 'worth a second look', never as proof."
|
|
141
|
+
},
|
|
127
142
|
"decision": {
|
|
128
143
|
"type": [
|
|
129
144
|
"string",
|
|
@@ -121,7 +121,7 @@ Run as ordered phases; track as todos. **Three sequential waves of model calls**
|
|
|
121
121
|
|
|
122
122
|
### Stage 6 — Capture lessons (compounding)
|
|
123
123
|
- Reflect on failures/mitigations and briefing wording, as today.
|
|
124
|
-
- **Ledger auto-appends** — `ledger.appendRun(record)` writes one row per (run × model) to `council-ledger.jsonl` automatically at finalize (shown in the run summary). No manual reliability-table update needed.
|
|
124
|
+
- **Ledger auto-appends** — `ledger.appendRun(record)` writes one row per (run × model × resolved executable) to `council-ledger.jsonl` automatically at finalize (shown in the run summary) — one row per model on an ordinary bench, and since v4.8 one row per executable when an alias's seats resolved differently. No manual reliability-table update needed.
|
|
125
125
|
- **Qualitative MODEL-NOTES update (approval-gated):** draft per-model quirk/conformance notes; write the proposed diff to `_tmp-proposed-model-notes-update.md`; present its path in the approval prompt; do not write until approved.
|
|
126
126
|
The approval prompt carries the diff file's path; chat text alone is not sufficient (an approval
|
|
127
127
|
dialog can hide the chat transcript). Keep it tight.
|
|
@@ -142,7 +142,10 @@ Run as ordered phases; track as todos. **Three sequential waves of model calls**
|
|
|
142
142
|
|
|
143
143
|
**Street cred** — computed two ways by `amicus council tally`:
|
|
144
144
|
- **withSelf** = each model's mean rank position across **all** judges' `FINAL RANKING:` blocks (lower = better).
|
|
145
|
-
- **peersOnly** = mean rank across judges **other than** that
|
|
145
|
+
- **peersOnly** = mean rank across judges **other than** that row's own vote on itself — **by seat,
|
|
146
|
+
not by model, whenever seat ids are present** (v4.8, see the ⚠️ note below): a twin's OTHER seat
|
|
147
|
+
is a real peer and counts, so only that row's own seat is excluded. Falls back to excluding by
|
|
148
|
+
alias when seat ids are unavailable.
|
|
146
149
|
The cross-review matrix shows both; the ledger and Stage-0 bench recommendations consume **peersOnly** only.
|
|
147
150
|
|
|
148
151
|
**Per-finding peer-confidence tier** — determined by a **peers-only** cascade: for a finding raised by model R, peers are all judges except R (the raiser's own adjudication is excluded — consistent with the peers-only street-cred rule). Let `a` = peer agrees, `d` = peer disputes. The cascade is exhaustive and mutually exclusive:
|
|
@@ -150,12 +153,14 @@ The cross-review matrix shows both; the ledger and Stage-0 bench recommendations
|
|
|
150
153
|
| Priority | Tier | Rule | Meaning |
|
|
151
154
|
|---|---|---|---|
|
|
152
155
|
| 1 | **Disputed** | `d ≥ 2` and `d > a` | Strong peer pushback — the finding itself is likely wrong |
|
|
153
|
-
| 2 | **Confirmed** | `a ≥ 2` and `a > d` | ≥ 2 independent corroborations, agrees dominate |
|
|
156
|
+
| 2 | **Confirmed** | `a ≥ 2` and `a > d`, or `a = 1` and `d = 0` | ≥ 2 independent corroborations, agrees dominate — or a lone corroborating peer (thin) |
|
|
154
157
|
| 3 | **Contested** | `d ≥ 1` (whatever remains) | At least one live dispute — in question |
|
|
155
|
-
| 4 | **Singleton** | else (`
|
|
158
|
+
| 4 | **Singleton** | else (`a = 0` and `d = 0`) | No peer engaged at all — thin |
|
|
156
159
|
|
|
157
160
|
`confidence` is `thin` when total engaged peers `a + d ≤ 1` — cells `(0,0)`, `(1,0)`, and `(0,1)`. **Claude may override the tier at `thin` margins** (recorded as `tierOverride: {from, to, reason}` and surfaced in the matrix and `verdict.json`). These four tiers drive Stage 4. `amicus council tally` assigns them deterministically; judgment at the margins remains Claude's.
|
|
158
161
|
|
|
162
|
+
⚠️ **v4.8 — "except R" is a SEAT rule, not a model rule, whenever seat ids are available.** A bench may seat the same model twice (`--models deepseek,deepseek,gpt`), and those two seats are genuinely independent reviewers: each gets its own review, its own anonymized label, and its own vote. Excluding by *model* threw away the twin's real vote along with the raiser's own, which is why a finding with one true corroborating peer could report as `Singleton {a:0,d:0}` — and why a bench of three identical aliases discarded its entire cross-review. The engine now excludes by **seat** (`adjudications[].seat` vs `findings[].raiserSeat`) whenever the vote *and* the finding both carry one, and falls back to excluding by **alias** when either does not — a pre-v4.8 document, a hand-assembled one, or a real run whose leg never bound to its seat. Two consequences to expect on a repeated-alias bench: tiers move in **both** directions (a twin's corroboration promotes, a twin's dispute demotes), and a finding whose only agreeing peer shares the raiser's alias is stamped `sameModelCorroboration: true` — corroboration that exists, but is not independent.
|
|
163
|
+
|
|
159
164
|
### 5.3 Chair selection & fallback
|
|
160
165
|
- Default: Claude **recommends a non-Claude chair** from the council each run (often the
|
|
161
166
|
strongest reasoner / best reviewer-reliability) and the user confirms at launch.
|
|
@@ -142,7 +142,7 @@ As each judge's ranking + adjudication response returns, collect it (the raw per
|
|
|
142
142
|
- `meta` = `{ "runId": "<run-folder stem>", "models": [<every reviewed model id, including "claude" when the toggle is on — this is the street-cred universe>], "chair": "<confirmed chair model id>", "claudeInCouncil": <Stage-0 toggle> }`. Optional extras: `runType`, `date`.
|
|
143
143
|
- `findings[]` = one entry per finding across ALL reviews: `{ "id": "<run-global label id from step 1, e.g. A1>", "raiser": "<de-anonymized model that raised it>", "severity": "<from the review JSON>" }` (`claim` may be carried along but is not required).
|
|
144
144
|
1. **Rewrite finding ids to run-global label ids.** Each Stage-1 review's local integer ids (`1`, `2`, `3`…) become `A1`, `A2`, `A3`… (where `A` is that review's anonymized label). The label↔model map (`Review A → deepseek`, etc.) is the key.
|
|
145
|
-
2. **Build `adjudications`** — for every judge across all findings: `findingId` = run-global label id; `judge` = the model id (de-anonymized via the map); `verdict ∈ {agree, dispute, neutral}`. Include every judge's verdict on every finding. The raiser's own adjudication of its own finding is **included in the input** (the tally engine excludes it when computing peers-only tiers — do not pre-filter it).
|
|
145
|
+
2. **Build `adjudications`** — for every judge across all findings: `findingId` = run-global label id; `judge` = the model id (de-anonymized via the map); `verdict ∈ {agree, dispute, neutral}`. Include every judge's verdict on every finding. The raiser's own adjudication of its own finding is **included in the input** (the tally engine excludes it when computing peers-only tiers — do not pre-filter it). ⚠️ **v4.8 — that exclusion is by SEAT when seat ids are present.** If your bench seats one model twice, add `seat` to each adjudication and `raiserSeat` to each finding (both optional, both string seat ids like `deepseek#1`), and the engine will drop only the raising *seat's* own vote instead of both twins'. Set them from what you actually observed — never invent a seat id to unlock the seat compare, and never set only one side: with a seat on the vote but not the finding (or vice versa) the engine deliberately falls back to the alias compare rather than guessing.
|
|
146
146
|
3. **Translate each judge's `FINAL RANKING:` block** — convert the label order (`1. Review C / 2. Review A / 3. Review B`) into a model `order` array via the same map (e.g. `{C→mistral, A→deepseek, B→gpt}` ⇒ `order: ["mistral","deepseek","gpt"]`). This is each entry in `rankings[]`.
|
|
147
147
|
4. **Populate `runStats`** from the per-leg run documents emitted by `fanout --json` (and any solo red-team/chair `start --json` docs): copy `model`, `status`, `durationMs`, `usage` verbatim. Any leg with no run doc gets `durationMs: null` and `usage: null` — never invent a value. Attach `role` (`council` | `redteam` | `claude`), `wasChair`, and `conformance` (`clean` | `repaired` | `unstructured`) as council-domain labels.
|
|
148
148
|
|
|
@@ -268,8 +268,9 @@ below needs both sides of the join.
|
|
|
268
268
|
Every finding already carries the **peer-confidence tier** the
|
|
269
269
|
engine's tally computed (see *Key mechanics → §5.2 Scoring*, and COUNCIL-DESIGN.md for the full
|
|
270
270
|
cascade): **Disputed** (strong peer pushback — `d ≥ 2` and `d > a`), **Confirmed** (≥ 2 peer
|
|
271
|
-
agreements, agrees dominate
|
|
272
|
-
|
|
271
|
+
agreements, agrees dominate — or a lone corroborating peer with zero disputes), **Contested** (at
|
|
272
|
+
least one live dispute), **Singleton** (no peer engagement at all — zero agreements, zero
|
|
273
|
+
disputes). `confidence: thin` cells `(0,0)/(1,0)/(0,1)` are override-eligible —
|
|
273
274
|
record any override in `tierOverride: {from, to, reason}` on that finding's decision entry. Present
|
|
274
275
|
the tiers in this order: Confirmed first (bulk decision), then Disputed and Contested and Singleton
|
|
275
276
|
individually in the judgment tier.
|
|
@@ -296,7 +297,7 @@ This is one tier with three sub-types presented separately. Present each finding
|
|
|
296
297
|
|
|
297
298
|
- **Disputed** (`d ≥ 2` and `d > a` — strong peer pushback, the finding itself may be wrong): For each finding show the claim, severity, which model raised it, which peers dispute it and why. Ask for a decision before proceeding to the next: **accept / deny / modify**.
|
|
298
299
|
- **Contested** (`d ≥ 1` with a meaningful split): For each finding show the claim and severity, which model raised it, who agreed, who disputed, and the one-line reasons from the adjudications. Ask for a decision before proceeding to the next: **accept / deny / modify**.
|
|
299
|
-
- **Singleton** (only the original raiser; all other judges were neutral or silent — `
|
|
300
|
+
- **Singleton** (only the original raiser; all other judges were neutral or silent — `a === 0` and `d === 0`): For each finding show the claim and severity and that no other judge engaged with it. Name the sole raiser. Ask for a decision before proceeding to the next: **accept / deny / modify**.
|
|
300
301
|
|
|
301
302
|
**Recording decisions.** Keep a running decision log throughout this stage — every finding's
|
|
302
303
|
outcome (accepted / denied / modified, with any modification noted) — and write it to
|
|
@@ -402,10 +403,17 @@ The `MODEL-NOTES.md` **next to this file** is your machine-local run ledger: npm
|
|
|
402
403
|
Draft new or updated entries for the per-model sections of `MODEL-NOTES.md` that capture what was learned.
|
|
403
404
|
|
|
404
405
|
**Ledger — already appended; do not touch it.** The engine's finalize tally appended one row per
|
|
405
|
-
(run × model) to the append-only `council-ledger.jsonl` under `getConfigDir()`
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
406
|
+
(run × model × resolved executable) to the append-only `council-ledger.jsonl` under `getConfigDir()`
|
|
407
|
+
as part of the run — one row per model on an ordinary bench, and since v4.8 one row per executable
|
|
408
|
+
when an alias's seats resolved differently (expert-lens runs are deliberately excluded). **In the
|
|
409
|
+
fast path, never run `council tally` yourself.** The ledger is append-only, so a second tally over
|
|
410
|
+
the same run double-appends: that permanently doubles every affected model's `conformance` histogram
|
|
411
|
+
in `amicus council stats` and cannot be undone. (It does *not* skew the lifetime averages — a
|
|
412
|
+
duplicated set of rows has the same mean — and since v4.8 it no longer inflates `runs`/`low-N`
|
|
413
|
+
either, because a re-tally of the same input carries the same `meta.runId`. That last part needs
|
|
414
|
+
`meta.runId` to be a non-empty string: an empty or numeric one is not an identity, so each row
|
|
415
|
+
counts individually and a re-tally of that input still doubles `runs`. Engine-produced runs always
|
|
416
|
+
mint a real id; hand-assembled tally input may not.)
|
|
409
417
|
Everything you would have wanted from it (tiers, both street-cred numbers, `runStats`,
|
|
410
418
|
`tierCounts`) is already in `<run-folder>/tally.json`. The quantitative reviewer-reliability data
|
|
411
419
|
in `MODEL-NOTES.md` is sourced entirely from `amicus council stats` (which aggregates the ledger) —
|
|
@@ -451,11 +459,14 @@ The engine's tally stage computes the two scoring signals and writes them to
|
|
|
451
459
|
|
|
452
460
|
**Street-cred** — computed two ways:
|
|
453
461
|
- **withSelf** = each model's mean rank position across **all** judges' `FINAL RANKING:` blocks (lower is better).
|
|
454
|
-
- **peersOnly** = mean rank excluding
|
|
462
|
+
- **peersOnly** = mean rank excluding this row's own ranking of itself — **by seat, not by model,
|
|
463
|
+
whenever seat ids are present** (v4.8): on a bench that seats one model twice, the twin's OTHER
|
|
464
|
+
seat is a real peer and its ranking counts, so only the row's own seat is excluded. Without seat
|
|
465
|
+
ids the engine falls back to excluding by alias, the pre-v4.8 behaviour.
|
|
455
466
|
|
|
456
467
|
Both are surfaced in `report.html` and `report.md`. The ledger and Stage-0 bench recommendations use **peersOnly** only.
|
|
457
468
|
|
|
458
|
-
**Per-finding peer-confidence tier** — assigned by the peers-only cascade (see COUNCIL-DESIGN.md §5.2 for the full table): **Disputed** → **Confirmed** → **Contested** → **Singleton**. The raiser's own adjudication is excluded from the cascade. `confidence: thin` when total engaged peers `a + d ≤ 1` — cells `(0,0)`, `(1,0)`, `(0,1)`. **Claude may override a `thin` tier at the margins** before presenting Stage 4 — the override is recorded in `tierOverride: {from, to, reason}` and surfaced in `verdict.json`.
|
|
469
|
+
**Per-finding peer-confidence tier** — assigned by the peers-only cascade (see COUNCIL-DESIGN.md §5.2 for the full table): **Disputed** → **Confirmed** → **Contested** → **Singleton**. The raiser's own adjudication is excluded from the cascade — **by seat, not by model, whenever seat ids are present** (v4.8): on a bench that seats one model twice, the twin's vote is a real peer vote and is now counted, and only the raising *seat's* own vote is dropped. Without seat ids on both the vote and the finding the engine falls back to excluding by alias, which is the pre-v4.8 behaviour. `confidence: thin` when total engaged peers `a + d ≤ 1` — cells `(0,0)`, `(1,0)`, `(0,1)`. **Claude may override a `thin` tier at the margins** before presenting Stage 4 — the override is recorded in `tierOverride: {from, to, reason}` and surfaced in `verdict.json`.
|
|
459
470
|
|
|
460
471
|
---
|
|
461
472
|
|
|
@@ -528,10 +539,13 @@ Always **rank recommendations by fit**, state the trade-off for each option, and
|
|
|
528
539
|
- `briefing.md` — the Stage-0 review request Claude authored (run provenance, not a temp file)
|
|
529
540
|
- `review-claude.md` — Claude's own fresh review, only when "Claude in the council" is on
|
|
530
541
|
- `run.json` — the engine's run manifest: stage log, wave ids, degradation, `runStats`, cost
|
|
531
|
-
- `review-<
|
|
542
|
+
- `review-<seat>.md` ×N and `judge-<seat>.md` ×N — the raw engine legs. `<seat>` is the bench
|
|
543
|
+
seat's id, which **is** the model alias for any bench that names each alias once; seat one and
|
|
544
|
+
seat two of a repeated alias write `…-<alias>-1.md` and `…-<alias>-2.md` instead of sharing
|
|
545
|
+
one file
|
|
532
546
|
- `briefing-stage1.md`, `bundle-stage2.md`, `chair-packet.md` — the model-facing briefings the engine composed
|
|
533
547
|
- `tally-input.json` and `tally.json` — the assembled input and the tiered record (plus `tally-provisional.json` and `debate.json` when `--debate` was on)
|
|
534
|
-
- `rebuttal-<
|
|
548
|
+
- `rebuttal-<seat>.md` ×(raising seats) and `revote-bundle.md` + `revote-<seat>.md` ×(disputing seats) — the debate round's raw defense and re-vote leg outputs plus the shared re-vote prompt, only when `--debate` was on. Both waves are sized in seats, so a bench that repeats an alias buys up to two extra billed legs per duplicated pair per round (one defense solo, one re-vote leg), each of which may draw its own bounded repair solo
|
|
535
549
|
- `chair-output.md` — the chair's synthesis prose, verbatim from the chair model
|
|
536
550
|
- `decisions.json` — the Stage-4 decision array Claude writes
|
|
537
551
|
- `verdict.json` — schema-stamped machine-readable record: tally output + Stage-4 decisions, written via `amicus council verdict` at Stage 5 (replacing the engine's undecided version)
|
|
@@ -14,6 +14,9 @@ const electronMcpCheck = require('./utils/doctor-electron-mcp-check');
|
|
|
14
14
|
const localProvidersCheck = require('./utils/doctor-local-providers-check');
|
|
15
15
|
// v4.6.2 PR1 (spec §4) — the 'anthropic-base-url' check body.
|
|
16
16
|
const baseUrlCheck = require('./utils/doctor-base-url-check');
|
|
17
|
+
// B3 (council review of PR 198, issue 195) — the 'aliases' check body,
|
|
18
|
+
// including its --fix repair of fabricated bare ids. Same split rationale.
|
|
19
|
+
const aliasCheck = require('./utils/doctor-alias-check');
|
|
17
20
|
|
|
18
21
|
const MAX_CATALOG_AGE_MS = 24 * 60 * 60 * 1000; // 24h (mirrors model-catalog DEFAULT_MAX_AGE_MS)
|
|
19
22
|
|
|
@@ -41,6 +44,10 @@ function realDeps() {
|
|
|
41
44
|
collectAliasSources: () => require('./utils/alias-audit').collectAliasSources(),
|
|
42
45
|
findStaleAliases: (s, c) => require('./utils/alias-audit').findStaleAliases(s, c),
|
|
43
46
|
findDriftedStoredAliases: (s, c) => require('./utils/alias-audit').findDriftedStoredAliases(s, c),
|
|
47
|
+
// B3: the narrow fabricated-bare-id repair class (pure detection) + the
|
|
48
|
+
// impure rewrite primitive `doctor --fix` calls when repairing one.
|
|
49
|
+
findFabricatedAliasRepairs: (s, c) => require('./utils/alias-audit').findFabricatedAliasRepairs(s, c),
|
|
50
|
+
repairAlias: (alias, newId) => aliasCheck.repairAlias(alias, newId),
|
|
44
51
|
hasOpencodeBinary: () => {
|
|
45
52
|
// Single source of truth shared with the runtime server-start guard.
|
|
46
53
|
const { ensureNodeModulesBinInPath, hasOpencodeBinary } = require('./utils/path-setup');
|
|
@@ -82,12 +89,16 @@ function realDeps() {
|
|
|
82
89
|
unlinkSessionIndexTmp: (n) => tmpSweep.unlinkSessionIndexTmp(n),
|
|
83
90
|
listSessionMetadataTmpFiles: () => metaSweep.listSessionMetadataTmpFiles(), // D8
|
|
84
91
|
unlinkSessionMetadataTmp: (n) => metaSweep.unlinkSessionMetadataTmp(n),
|
|
92
|
+
listStaleSessionIndexEntries: () => indexPrune.listStaleSessionIndexEntries(), // R16
|
|
93
|
+
pruneStaleSessionIndexEntries: (ids) => indexPrune.pruneStaleSessionIndexEntries(ids),
|
|
85
94
|
};
|
|
86
95
|
}
|
|
87
96
|
// B15: sweep logic in utils/session-index-tmp-sweep.js (mirrors mcp-legacy's split).
|
|
88
97
|
const tmpSweep = require('./utils/session-index-tmp-sweep');
|
|
89
98
|
// D8: per-session metadata.json sibling sweep — utils/session-metadata-tmp-sweep.js.
|
|
90
99
|
const metaSweep = require('./utils/session-metadata-tmp-sweep');
|
|
100
|
+
// R16: stale sessions-index.json entry prune — utils/session-index-prune.js (mirrors B15 above).
|
|
101
|
+
const indexPrune = require('./utils/session-index-prune');
|
|
91
102
|
|
|
92
103
|
/** Run one guarded check; a thrown fn becomes an error line. */
|
|
93
104
|
function guard(id, name, fn) {
|
|
@@ -152,20 +163,10 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
152
163
|
: { id: 'catalog', name: 'Model catalog', status: 'warn', message: `stale (${hrs}h old)`, hint: 'amicus models --refresh' };
|
|
153
164
|
}));
|
|
154
165
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const stale = d.findStaleAliases(sources, catalog);
|
|
160
|
-
const drifted = d.findDriftedStoredAliases(sources, catalog);
|
|
161
|
-
if (stale.length === 0 && drifted.length === 0) {
|
|
162
|
-
return { id: 'aliases', name: 'Model aliases', status: 'ok', message: catalog.length ? 'all resolve' : 'catalog empty — not checked', hint: null };
|
|
163
|
-
}
|
|
164
|
-
const parts = [];
|
|
165
|
-
if (stale.length) { parts.push(`${stale.length} stale: ${stale.map(s => s.alias).join(', ')}`); }
|
|
166
|
-
if (drifted.length) { parts.push(`${drifted.length} drifted: ${drifted.map(s => s.alias).join(', ')}`); }
|
|
167
|
-
return { id: 'aliases', name: 'Model aliases', status: 'warn', message: parts.join('; '), hint: 'amicus models --check' };
|
|
168
|
-
}));
|
|
166
|
+
// B3: self-heals in place under --fix (fabricated bare ids only) — see
|
|
167
|
+
// utils/doctor-alias-check.js for the check body and utils/alias-audit.js's
|
|
168
|
+
// findFabricatedAliasRepairs for the detection rule.
|
|
169
|
+
checks.push(guard('aliases', 'Model aliases', () => aliasCheck.evaluateAliasesCheck(d)));
|
|
169
170
|
|
|
170
171
|
checks.push(guard('anthropic-base-url', 'ANTHROPIC_BASE_URL',
|
|
171
172
|
() => baseUrlCheck.evaluateAnthropicBaseUrl(d)));
|
|
@@ -205,6 +206,9 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
205
206
|
|
|
206
207
|
checks.push(guard('sessions-index-tmp', 'Session index tmp files', () => tmpSweep.evaluateSessionIndexTmpSweep(d)));
|
|
207
208
|
|
|
209
|
+
// R16: beside its sibling above — same file, a different growth gap.
|
|
210
|
+
checks.push(guard('sessions-index-prune', 'Session index stale entries', () => indexPrune.evaluateSessionIndexPrune(d)));
|
|
211
|
+
|
|
208
212
|
checks.push(guard('session-metadata-tmp', 'Session metadata tmp files', () => metaSweep.evaluateSessionMetadataTmpSweep(d)));
|
|
209
213
|
|
|
210
214
|
// #43: OpenRouter credit/free-tier — warns (never errors); skipped when no key.
|
package/src/council/anonymize.js
CHANGED
|
@@ -14,10 +14,36 @@ const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
|
14
14
|
/**
|
|
15
15
|
* Assign stable labels in the given model order.
|
|
16
16
|
* @param {string[]} models reviewed model ids (bench order)
|
|
17
|
+
* @param {?Array<?{id: string, alias: string}>} [seats] (v4.8 T3.2) the bound
|
|
18
|
+
* seat (or null/undefined) for each entry, SAME length/order as `models`.
|
|
19
|
+
* Optional — every pre-T3.2 caller omits it. This is NOT the positional join
|
|
20
|
+
* run-assemble.js's buildTallyInput docblock forbids for meta.seats (that one
|
|
21
|
+
* joins meta.seats to the INDEPENDENTLY-sourced/filtered meta.models or
|
|
22
|
+
* streetCred[]): here both arrays are built by the ONE caller from the SAME
|
|
23
|
+
* source list in the SAME pass (run.js's `s1.reviews.map(...)`, twice), the
|
|
24
|
+
* same positional pairing run.js already relies on one block below
|
|
25
|
+
* (`labels.entries[i]` against `s1.reviews[i]`).
|
|
17
26
|
* @returns {{entries: Array<{label: string, letter: string, model: string}>,
|
|
18
|
-
* labelMap: Object<string, string>,
|
|
27
|
+
* labelMap: Object<string, string>, seatMap: Object<string, string>}}
|
|
28
|
+
* `labelMap` is UNCHANGED by this task — label -> alias, byte-identical.
|
|
29
|
+
* This is a controller ruling (P-3): `labelMap` is persisted to run.json and
|
|
30
|
+
* schemas/council-run.schema.json:45 declares its shape, and
|
|
31
|
+
* src/workspace/blind-mode.js :: labelFor does an EXACT-STRING match against
|
|
32
|
+
* its VALUE — measured 2026-08-20, `labelFor("gemini", {'Review A':'gemini#1'})`
|
|
33
|
+
* -> `null`, the label silently lost. Seat identity rides the separate,
|
|
34
|
+
* additive `seatMap` below instead.
|
|
35
|
+
* `seatMap` (label -> seat id) is sparse by construction: a label lands in
|
|
36
|
+
* it only when `seats[i]` is a real bound seat that differs from its own
|
|
37
|
+
* alias — the shared emit-when-DIFFERENT predicate every other seat-emit
|
|
38
|
+
* producer uses (run-stats-entry.js:64: `seat.id !== seat.alias`). On a
|
|
39
|
+
* unique-alias bench, or when `seats` is omitted entirely, `seatMap` is
|
|
40
|
+
* `{}` — so no pre-T3.2 caller (or consumer of just `{entries, labelMap}`)
|
|
41
|
+
* can observe a change. T3.2 wired `seatMap` only as far as
|
|
42
|
+
* `rankingToOrder`'s `orderSeats` below; consuming it into street cred was
|
|
43
|
+
* T3.3's, and T3.3 SHIPPED — street-cred.js :: computeStreetCred now keys on
|
|
44
|
+
* it, so this channel has a live consumer rather than a forecast one.
|
|
19
45
|
*/
|
|
20
|
-
function assignLabels(models) {
|
|
46
|
+
function assignLabels(models, seats) {
|
|
21
47
|
if (!Array.isArray(models) || models.length === 0 || models.length > LETTERS.length) {
|
|
22
48
|
throw new Error(`assignLabels needs 1-${LETTERS.length} models`);
|
|
23
49
|
}
|
|
@@ -25,12 +51,17 @@ function assignLabels(models) {
|
|
|
25
51
|
label: `Review ${LETTERS[i]}`, letter: LETTERS[i], model,
|
|
26
52
|
}));
|
|
27
53
|
const labelMap = {};
|
|
28
|
-
const letterByModel = {};
|
|
29
54
|
for (const e of entries) {
|
|
30
55
|
labelMap[e.label] = e.model;
|
|
31
|
-
letterByModel[e.model] = e.letter;
|
|
32
56
|
}
|
|
33
|
-
|
|
57
|
+
const seatMap = {};
|
|
58
|
+
if (Array.isArray(seats)) {
|
|
59
|
+
entries.forEach((e, i) => {
|
|
60
|
+
const seat = seats[i];
|
|
61
|
+
if (seat && seat.id !== seat.alias) { seatMap[e.label] = seat.id; }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return { entries, labelMap, seatMap };
|
|
34
65
|
}
|
|
35
66
|
|
|
36
67
|
/** Rewrite a review's local integer finding id to its run-global label id. */
|
|
@@ -43,14 +74,29 @@ function toGlobalId(letter, localId) {
|
|
|
43
74
|
* @param {string} letter review letter (e.g. 'A')
|
|
44
75
|
* @param {string} raiser de-anonymized model id
|
|
45
76
|
* @param {Array<{id: number, severity: string, claim: string, location: string}>} findings
|
|
77
|
+
* @param {?string} raiserSeat (v4.8 PR3 Task 5) the raiser's bound seat id —
|
|
78
|
+
* emitted ONLY when truthy AND different from `raiser` (emit-when-DIFFERENT,
|
|
79
|
+
* not emit-when-set). On a unique-alias bench seat.id === alias, so the
|
|
80
|
+
* field stays absent and tally-input.json/tally.json/verdict.json stay
|
|
81
|
+
* byte-identical artifacts. Absent entirely on the Claude review call site
|
|
82
|
+
* (3 args) — Claude is never a seat.
|
|
83
|
+
* ⚠️ v4.8 PR4c R4c-9: the `raiserSeat !== raiser` half is now provably DEAD on
|
|
84
|
+
* the engine path. run.js's call site gates on `seat.id !== seat.alias`, and
|
|
85
|
+
* for every bench preflightSeats admits that implies `seat.id !== r.model`.
|
|
86
|
+
* It is KEPT as defense in depth for the hand-assembled callers (this module
|
|
87
|
+
* is pure and has no seat table to consult), and tests/council/
|
|
88
|
+
* run-raiserseat-call.test.js exists so that removing EITHER guard fails a
|
|
89
|
+
* test — but do not read it as a second, independent decision about an
|
|
90
|
+
* engine-produced run.
|
|
46
91
|
* @returns {Array<{id: string, raiser: string, severity: string, claim: string, location: string}>}
|
|
47
92
|
*/
|
|
48
|
-
function toGlobalFindings(letter, raiser, findings) {
|
|
93
|
+
function toGlobalFindings(letter, raiser, findings, raiserSeat) {
|
|
49
94
|
// claim + location must reach tally-input.json — Action v2 (Plan C) joins on
|
|
50
95
|
// them for file:line annotations; tally() reads only id/raiser/severity.
|
|
51
96
|
return findings.map(f => ({
|
|
52
97
|
id: toGlobalId(letter, f.id), raiser, severity: f.severity, claim: f.claim,
|
|
53
98
|
location: f.location,
|
|
99
|
+
...(raiserSeat && raiserSeat !== raiser ? { raiserSeat } : {}),
|
|
54
100
|
}));
|
|
55
101
|
}
|
|
56
102
|
|
|
@@ -59,18 +105,41 @@ function toGlobalFindings(letter, raiser, findings) {
|
|
|
59
105
|
* a tally rankings[].order array of model ids via the private label map.
|
|
60
106
|
* @param {Array<string|string[]>} ranking
|
|
61
107
|
* @param {Object<string, string>} labelMap
|
|
62
|
-
* @
|
|
108
|
+
* @param {Object<string, string>} [seatMap] (v4.8 T3.2) assignLabels' additive
|
|
109
|
+
* label -> seat-id channel. Optional; every pre-T3.2 caller omits it.
|
|
110
|
+
* @returns {{order: Array<string|string[]>,
|
|
111
|
+
* orderSeats: Array<?string|Array<?string>>, errors: string[]}}
|
|
112
|
+
* `order` and `errors` are UNCHANGED — same expression, same call to
|
|
113
|
+
* `mapOne`, so briefings-chair.js and the tally schema stay unmoved
|
|
114
|
+
* (SI-25 site (3) becomes *possible*, not *done*: T3.2 does not touch that
|
|
115
|
+
* call site). ⚠️ THAT PARENTHESIS IS T3.2's OWN HISTORY, NOT THE PRESENT
|
|
116
|
+
* STATE — kept because it says what T3.2 did and did not do. SI-25 has since
|
|
117
|
+
* DONE site (3): `briefings-chair.js :: seatKeyedOrder` consumes `orderSeats`
|
|
118
|
+
* to render the chair packet's rankings block. Only the tally schema still
|
|
119
|
+
* stays unmoved. `orderSeats` is the NEW, separate, parallel channel: the SAME
|
|
120
|
+
* slots resolved through `seatMap` instead of `labelMap` — a by-VALUE
|
|
121
|
+
* (label) lookup mirroring `order`'s own resolution, never a positional
|
|
122
|
+
* join across two independently-sourced arrays. `null` marks a slot whose
|
|
123
|
+
* label has no distinguishing seat (unique-alias bench, or no `seatMap`
|
|
124
|
+
* given at all — every pre-T3.2 call site). T3.2 wired `orderSeats` no
|
|
125
|
+
* further than its `judgeResults[]` call site in run-stage2.js. T3.3 carried
|
|
126
|
+
* it one hop on, to `rankings[]` in run-assemble.js :: buildTallyInput, and
|
|
127
|
+
* seat-keyed street-cred.js :: rankPositions with it — emitting the field
|
|
128
|
+
* only when it holds a non-null, because THIS function returns an all-null
|
|
129
|
+
* parity shape rather than an absence.
|
|
63
130
|
*/
|
|
64
|
-
function rankingToOrder(ranking, labelMap) {
|
|
131
|
+
function rankingToOrder(ranking, labelMap, seatMap) {
|
|
65
132
|
const errors = [];
|
|
66
133
|
const mapOne = (label) => {
|
|
67
134
|
const model = labelMap[label];
|
|
68
135
|
if (!model) { errors.push(`unknown label '${label}'`); }
|
|
69
136
|
return model;
|
|
70
137
|
};
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
138
|
+
const seatOne = (label) => (seatMap && seatMap[label]) || null;
|
|
139
|
+
const slots = Array.isArray(ranking) ? ranking : [];
|
|
140
|
+
const order = slots.map(slot => (Array.isArray(slot) ? slot.map(mapOne) : mapOne(slot)));
|
|
141
|
+
const orderSeats = slots.map(slot => (Array.isArray(slot) ? slot.map(seatOne) : seatOne(slot)));
|
|
142
|
+
return { order, orderSeats, errors };
|
|
74
143
|
}
|
|
75
144
|
|
|
76
145
|
module.exports = { assignLabels, toGlobalId, toGlobalFindings, rankingToOrder, LETTERS };
|