amicus 4.7.1 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +882 -1
- package/README.md +3 -2
- package/docs/CITATIONS.md +114 -0
- package/docs/ROADMAP.md +33 -5
- package/docs/SHIMS.md +1 -1
- package/docs/configuration.md +2 -2
- 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/setup-ui-aliases.js +2 -2
- package/electron/workspace-ui/index.html +9 -0
- package/electron/workspace-ui/live-dead-seats.js +228 -0
- package/electron/workspace-ui/live-model.js +10 -236
- package/electron/workspace-ui/live-seats.js +126 -0
- package/electron/workspace-ui/workspace-app.js +6 -41
- package/electron/workspace-ui/workspace-banners.js +95 -0
- package/electron/workspace-ui/workspace-lazy.js +55 -12
- package/electron/workspace-ui/workspace-matrix.js +2 -2
- package/electron/workspace-ui/workspace-panels.js +42 -10
- package/electron/workspace-ui/workspace-render.js +2 -2
- package/electron/workspace-ui/workspace-seats.js +101 -17
- package/package.json +3 -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 +7 -0
- package/src/council/anonymize.js +80 -11
- package/src/council/briefings-chair.js +272 -0
- package/src/council/briefings-stage2.js +12 -140
- package/src/council/debate.js +120 -15
- package/src/council/ledger-join.js +284 -0
- package/src/council/ledger-stats.js +100 -0
- package/src/council/ledger.js +177 -88
- package/src/council/peer-split.js +196 -0
- package/src/council/report-html.js +12 -5
- package/src/council/report-md.js +146 -0
- package/src/council/report.js +188 -112
- package/src/council/run-assemble.js +100 -79
- package/src/council/run-chair.js +17 -1
- package/src/council/run-debate-revote.js +268 -0
- package/src/council/run-debate.js +92 -102
- package/src/council/run-finish.js +70 -0
- package/src/council/run-launch.js +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 +13 -2
- package/src/utils/config.js +109 -11
- package/src/utils/curated-models.js +17 -2
- package/src/utils/degrade.js +5 -0
- package/src/utils/no-output-backstop.js +1 -1
- package/src/utils/remediation-hints.js +13 -0
- package/src/utils/session-index-prune.js +297 -0
- package/src/utils/session-metadata-tmp-sweep.js +1 -1
- package/src/workspace/artifact-guard.js +8 -114
- package/src/workspace/artifact-names.js +222 -0
- package/src/workspace/fold-format.js +9 -6
- package/src/workspace/live-normalize.js +6 -2
- package/src/workspace/matrix-model.js +141 -19
- package/src/workspace/run-detail.js +27 -1
- package/src/workspace/seat-space.js +143 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// src/council/ledger-join.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module council/ledger-join
|
|
6
|
+
* How ONE pair group resolves into ONE ledger row's fields: which of its
|
|
7
|
+
* runStats rows may decide `role`/`conformance` (SI-17's normalise), which
|
|
8
|
+
* street-cred rows are its own (the seat-aware join, T3.3), and — since v4.8
|
|
9
|
+
* SI-18 — which of the alias's raised findings are its own. Split out of
|
|
10
|
+
* ./ledger at v4.8 T3.3, on the same seam T3.0 used for ./ledger-stats: that
|
|
11
|
+
* split took the READ half out, this one takes the JOIN SEMANTICS out, and
|
|
12
|
+
* `buildLedgerRows` keeps only the row assembly. Both dependencies run one way
|
|
13
|
+
* (ledger.js -> ledger-join.js / ledger-stats.js) and neither can cycle.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ REQUIRE-FREE by design — the ./seats · ./run-stats-entry · ./peer-split
|
|
16
|
+
* precedent. Nothing here reads the filesystem or the config dir.
|
|
17
|
+
* ⚠️ NOT re-exported from ./ledger: none of the four is used outside the row
|
|
18
|
+
* build, so adding them to that module's export list would only risk the
|
|
19
|
+
* AUTO:modules truncation its own comment warns about. Tests import them here.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ FIVE named mutants guard these, alongside LEDGERALIAS on ledger.js's
|
|
22
|
+
* join key. Each mutation and its MEASURED red set is recorded with the rest:
|
|
23
|
+
* tests/council/street-cred-mutants.js :: CHAIRWINS guards benchLegs, while
|
|
24
|
+
* tests/council/street-cred-mutants.js :: CREDALIAS guards WHETHER the seat
|
|
25
|
+
* lookup can ever win, tests/council/street-cred-mutants.js :: ANYSEATED
|
|
26
|
+
* (v4.8 follow-up) guards HOW COMPLETE a group's seats must be before it does,
|
|
27
|
+
* tests/council/street-cred-mutants.js :: ALIASLASTWINS guards how the lookup
|
|
28
|
+
* combines what it finds, and tests/council/street-cred-mutants.js ::
|
|
29
|
+
* FINDINGALIAS (v4.8 SI-18) guards splitFindingsBySeat the same wholesale way.
|
|
30
|
+
* RE-RUN them, never renumber them.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* v4.8 T3.3 — SI-17's NORMALISE, per owner ruling R4 ("normalise before the
|
|
35
|
+
* ledger join, inside Phase 3 — works on all paths including the two
|
|
36
|
+
* hand-assembled `appendRun` ones a preflight guard cannot reach"). It lives on
|
|
37
|
+
* the WRITE path — here, called from ledger.js :: buildLedgerRows — for exactly
|
|
38
|
+
* that reason: `cli-handlers-council.js` and `mcp-server.js` copy `meta`
|
|
39
|
+
* verbatim out of user JSON, and the documented `amicus council tally` shape
|
|
40
|
+
* PUTS THE CHAIR ON THE BENCH. Both re-read 2026-08-20: docs/council.md's
|
|
41
|
+
* `## Worked example` meta block is `models ["deepseek","gpt"]` with
|
|
42
|
+
* `chair "deepseek"`, and tests/council/fixtures/av-receiver-input.js is
|
|
43
|
+
* `models ["deepseek","gpt","mistral"]` with the same chair. There it is the
|
|
44
|
+
* normal case rather than an edge — though neither of those two carries a
|
|
45
|
+
* `role: 'chair'` runStats row, so neither is what this function changes.
|
|
46
|
+
*
|
|
47
|
+
* A `role: 'chair'` row is the CHAIR SYNTHESIS leg. That is a different
|
|
48
|
+
* contract from a bench review — prose plus a VERDICT line, not findings JSON —
|
|
49
|
+
* so its `conformance` and `role` describe a different job from the seat row it
|
|
50
|
+
* shares an alias and a resolution with. MEASURED at BASE `b341b273` on
|
|
51
|
+
* `--models ds,gpt --chair ds`, both legs resolving to `v/ds`:
|
|
52
|
+
*
|
|
53
|
+
* bench UNSTRUCTURED + chair clean -> role 'chair', conformance 'unstructured'
|
|
54
|
+
* bench clean + chair UNSTRUCTURED -> role 'chair', conformance 'unstructured'
|
|
55
|
+
* the same bench with the chair OFF it -> role 'council', conformance 'clean'
|
|
56
|
+
*
|
|
57
|
+
* The two directions are INDISTINGUISHABLE in the persisted row, and neither
|
|
58
|
+
* agrees with the off-bench control for the same bench leg — in an append-only
|
|
59
|
+
* file that is never migrated. Normalising means the bench leg decides both
|
|
60
|
+
* fields whenever the group holds one, so a seat's ledger identity no longer
|
|
61
|
+
* depends on whether that model also chaired.
|
|
62
|
+
*
|
|
63
|
+
* `wasChair` is NOT normalised away: it is a fact about the run rather than
|
|
64
|
+
* about a contract, it stays any-wins over the WHOLE group (T15), and it is the
|
|
65
|
+
* one thing the chair row legitimately contributes to a bench seat's row.
|
|
66
|
+
* A group holding ONLY chair rows — the split-resolution chair leg of T16, a
|
|
67
|
+
* give-up row — is unchanged: there is no bench leg to prefer, so the chair row
|
|
68
|
+
* still decides both fields and nothing is lost.
|
|
69
|
+
*
|
|
70
|
+
* ⚠️ ON A MIXED GROUP SOMETHING IS LOST, AND IT IS NOT `wasChair`. The chair
|
|
71
|
+
* leg's `conformance` no longer reaches the ledger AT ALL there, so a BROKEN
|
|
72
|
+
* chair synthesis by a model that also sat on the bench becomes invisible to
|
|
73
|
+
* the lifetime histogram; only `wasChair: true` records that it chaired.
|
|
74
|
+
* MEASURED end to end through appendRun -> ledger-stats.js ::
|
|
75
|
+
* deriveReliability, on `--models ds,gpt --chair ds` with a CLEAN bench leg and
|
|
76
|
+
* an UNSTRUCTURED chair leg, both resolving to `v/ds`:
|
|
77
|
+
*
|
|
78
|
+
* b341b273 : conformance { unstructured: 1 }
|
|
79
|
+
* after : conformance { clean: 1 }
|
|
80
|
+
*
|
|
81
|
+
* That is the intended direction — the bench leg's own contract outcome is what
|
|
82
|
+
* a bench seat's row should report, and the old value was the chair's outcome
|
|
83
|
+
* wearing the seat's name — but it is a REAL loss of signal, not a relabelling,
|
|
84
|
+
* and no other surface picks it up. It is disclosed here rather than repaired:
|
|
85
|
+
* recording a chair leg's conformance in its own right would mean giving the
|
|
86
|
+
* chair its own ledger identity, which changes the row SET and is out of this
|
|
87
|
+
* task's scope. FILED for the owner to rule on, not decided here.
|
|
88
|
+
* @param {Array<object>} group one pair group's runStats rows
|
|
89
|
+
* @returns {Array<object>} the rows that may decide `role` and `conformance`
|
|
90
|
+
*/
|
|
91
|
+
function benchLegs(group) {
|
|
92
|
+
const bench = group.filter(r => r.role !== 'chair');
|
|
93
|
+
return bench.length ? bench : group;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Mean of the numeric values of `field` across `rows`; null when none is numeric. */
|
|
97
|
+
function meanCred(rows, field) {
|
|
98
|
+
const nums = rows.map(r => r[field]).filter(v => typeof v === 'number');
|
|
99
|
+
return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* v4.8 T3.3 — the street-cred join, seat-aware. THE HAZARD THIS EXISTS FOR:
|
|
104
|
+
* `new Map(streetCred.map(s => [s.model, s]))` is last-wins by alias. At BASE
|
|
105
|
+
* `b341b273` that was a genuine no-op — measured on bench `['a','a','b']`, the
|
|
106
|
+
* two twin rows were BYTE-IDENTICAL (3 rows in, `Map` size 2: one dropped, but
|
|
107
|
+
* LOSSLESSLY). The moment `street-cred.js :: rankPositions` is seat-keyed those
|
|
108
|
+
* rows DIVERGE, and the same silent drop starts losing a real seat's numbers
|
|
109
|
+
* into an append-only file. That is why the two halves shipped in ONE commit
|
|
110
|
+
* (controller ruling ledger C-1) and why no intermediate tree may hold one
|
|
111
|
+
* without the other.
|
|
112
|
+
*
|
|
113
|
+
* TWO LOOKUPS, TRIED IN THAT ORDER, because the two sides are indexed by
|
|
114
|
+
* INDEPENDENT producers and either can be seated without the other:
|
|
115
|
+
* 1. the group's own seats, named by `runStats[].seat` (emit-when-DIFFERENT),
|
|
116
|
+
* resolved through `sc` — which holds ONLY the seated street-cred rows —
|
|
117
|
+
* and ONLY when EVERY row in the group resolves this way;
|
|
118
|
+
* 2. failing that (including a PARTIAL resolution), every street-cred row
|
|
119
|
+
* carrying this ALIAS.
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ (1) IS ALL-OR-NOTHING. v4.8 follow-up (2026-08-21), council finding 3 on
|
|
122
|
+
* PR #176, reproduced at BASE `13cd0a2d`: this function used to take lookup
|
|
123
|
+
* (1) the moment ANY of the group's seats resolved through `sc`, discarding
|
|
124
|
+
* the numbers of any OTHER row in the SAME group whose seat did not resolve.
|
|
125
|
+
* A MIXED group — one seated row, one not — then read NARROWER than a group
|
|
126
|
+
* with NO seat information at all, which falls straight to (2)'s alias mean.
|
|
127
|
+
* Partial seat information produced a narrower read than no information,
|
|
128
|
+
* which is backwards — the mirror image of fix round 1's defect just above.
|
|
129
|
+
* MEASURED on a two-row `a` group, seat A resolving to peersOnly 1 and seat B
|
|
130
|
+
* — same alias, unresolvable through `sc` — to peersOnly 5:
|
|
131
|
+
*
|
|
132
|
+
* before : MIXED group (A seated, B not) peersOnly 1 (A alone)
|
|
133
|
+
* NEITHER seated peersOnly 3 (mean of A, B)
|
|
134
|
+
* after : MIXED group (A seated, B not) peersOnly 3 (== neither seated)
|
|
135
|
+
*
|
|
136
|
+
* FIX: lookup (1) now requires EVERY row's seat to resolve in `sc` —
|
|
137
|
+
* `ids.length > 0 && ids.every(id => id && sc.has(id))`. Anything short of
|
|
138
|
+
* that — one row unseated, one row seated to an id `sc` does not hold, or an
|
|
139
|
+
* empty group — falls through to (2), the same alias mean a fully-unseated
|
|
140
|
+
* group already read. ⚠️ `ids.length > 0` is load-bearing on its own:
|
|
141
|
+
* `[].every(...)` is vacuously `true` in JavaScript, so without that guard an
|
|
142
|
+
* EMPTY group would count as "fully identified", resolve through an empty
|
|
143
|
+
* `ids` to an empty seated array, and return `{}` (via the `!rows.length`
|
|
144
|
+
* guard below) instead of falling through to the alias mean the empty-group
|
|
145
|
+
* case already relied on. Killed by the named mutant
|
|
146
|
+
* tests/council/street-cred-mutants.js :: ANYSEATED, which reverts the gate
|
|
147
|
+
* to its pre-fix "any resolves" form and reproduces the MIXED regression
|
|
148
|
+
* above; CREDALIAS (below the function) is the narrower mutant forcing the
|
|
149
|
+
* seat lookup to never win AT ALL, fully-seated groups included.
|
|
150
|
+
*
|
|
151
|
+
* ⚠️ WHY (2) IS A FILTER AND NOT AN ALIAS KEY IN `sc`. Fix round 1, Important 1
|
|
152
|
+
* — this function REGRESSED that quadrant and the regression was measured, not
|
|
153
|
+
* argued. When the street-cred rows are seated and the runStats rows are NOT,
|
|
154
|
+
* an alias-keyed map holds no entry for the alias at all, so the old
|
|
155
|
+
* `sc.get(model) || {}` resolved to `{}` and the row persisted NULL street
|
|
156
|
+
* cred. MEASURED on one hand-assembled record driven through the real
|
|
157
|
+
* `tally()` -> `buildLedgerRows()`, bench `['a','a','b']` with `meta.seats` and
|
|
158
|
+
* no `runStats[].seat`:
|
|
159
|
+
*
|
|
160
|
+
* b341b273 : ledger row a withSelf 2.667 peersOnly 3
|
|
161
|
+
* 46719a7f : ledger row a withSelf null peersOnly null
|
|
162
|
+
*
|
|
163
|
+
* That quadrant is not exotic: mcp-tools.js :: amicus_council_tally declares
|
|
164
|
+
* `meta.seats` while its `runStats` is `z.array(z.record(z.any()))`, so nothing
|
|
165
|
+
* asks that caller for a seat on a runStats row, and cli-handlers-council.js
|
|
166
|
+
* passes user JSON through verbatim. Both then `appendRun` into a file that is
|
|
167
|
+
* never migrated, and ledger-stats.js :: deriveReliability filters
|
|
168
|
+
* `typeof v === 'number'`, so a null drops the run out of
|
|
169
|
+
* `avgStreetCredPeersOnly` — the value run-chair.js :: pickFallbackChair ranks
|
|
170
|
+
* on. This is precisely the hand-assembled path owner ruling R4 puts in scope.
|
|
171
|
+
*
|
|
172
|
+
* ⚠️ THE MEAN IS THE FALLBACK, NOT A LAST-WINS ALIAS KEY, and the two are
|
|
173
|
+
* DISTINGUISHABLE only on input BASE could never produce. At `b341b273`
|
|
174
|
+
* `computeStreetCred` was `models.map(m => f(m, judgePos))` — deterministic in
|
|
175
|
+
* `m` — so two rows sharing an alias were always byte-identical, and a mean
|
|
176
|
+
* across them equals the last-wins value exactly. VERIFIED BY BRUTE FORCE at
|
|
177
|
+
* BASE over 4374 cases (every 3-slot bench over three aliases x two judges x
|
|
178
|
+
* nine order shapes each, strict and tied, under two judge-name pairs), which
|
|
179
|
+
* produced 3402 duplicated-alias groups: ZERO had rows that differ. So the mean
|
|
180
|
+
* is byte-identical to BASE on every shape BASE could produce, and is the honest
|
|
181
|
+
* answer on the two shapes it cannot: seated rows that genuinely diverge, and
|
|
182
|
+
* hand-assembled input that hands us two differing rows under one alias — where
|
|
183
|
+
* last-wins would silently discard one, which is the whole defect class this
|
|
184
|
+
* task exists to close. Pinned by ledger.test.js T12b, "the alias fallback
|
|
185
|
+
* MEANS its rows, it does not take the last one", and killed by the named
|
|
186
|
+
* mutant tests/council/street-cred-mutants.js :: ALIASLASTWINS.
|
|
187
|
+
*
|
|
188
|
+
* ⚠️ A group can cover MORE THAN ONE SEAT — two twins that resolved to the same
|
|
189
|
+
* executable are one (alias, resolvedModel) pair — and a ledger row has ONE
|
|
190
|
+
* street-cred slot. The row SET is not this task's to change (`meta.models`
|
|
191
|
+
* stays the driver, and PR4b's emission order is one row per pair group), so
|
|
192
|
+
* with one slot the choice is between dropping seats and combining them. The
|
|
193
|
+
* mean is the only combination that reads every seat and still returns a lone
|
|
194
|
+
* seat's own number unchanged.
|
|
195
|
+
* ⚠️ It is NOT weight-preserving, and saying otherwise would be the easy wrong
|
|
196
|
+
* claim: ledger-stats.js :: deriveReliability averages ROWS, so N seats folded
|
|
197
|
+
* into one row count once where N separate rows would count N times. That
|
|
198
|
+
* matters only when the aggregate mixes this group with other rows or other
|
|
199
|
+
* runs — and it is still strictly better than today, which reads exactly one of
|
|
200
|
+
* the N and discards the others without saying so.
|
|
201
|
+
* @param {Map<string, object>} sc the SEATED street-cred rows, keyed by seat id
|
|
202
|
+
* @param {Array<object>} group one pair group's runStats rows
|
|
203
|
+
* @param {string} model the block's alias
|
|
204
|
+
* @param {Array<object>} streetCred the record's street-cred rows, for (2)
|
|
205
|
+
*/
|
|
206
|
+
function credFor(sc, group, model, streetCred) {
|
|
207
|
+
// Named mutant "ANYSEATED": revert this gate to its pre-fix "any resolves"
|
|
208
|
+
// form —
|
|
209
|
+
// const seated = [...new Set(group.map(r => r.seat).filter(Boolean))]
|
|
210
|
+
// .map(id => sc.get(id)).filter(Boolean);
|
|
211
|
+
// const rows = seated.length ? seated : (streetCred || []).filter(...);
|
|
212
|
+
// tests/council/street-cred-mutants.js :: ANYSEATED.
|
|
213
|
+
const ids = group.map(r => r.seat);
|
|
214
|
+
const fully = ids.length > 0 && ids.every(id => id && sc.has(id));
|
|
215
|
+
// Named mutant "CREDALIAS": force `fully` to never be true, so the seat
|
|
216
|
+
// lookup can never win even on a genuinely fully-seated group —
|
|
217
|
+
// const rows = fully ? [...] : fallback; -> const rows = false ? [...] : fallback;
|
|
218
|
+
// tests/council/street-cred-mutants.js :: CREDALIAS.
|
|
219
|
+
const rows = fully ? [...new Set(ids)].map(id => sc.get(id))
|
|
220
|
+
: (streetCred || []).filter(s => s && s.model === model);
|
|
221
|
+
if (!rows.length) { return {}; }
|
|
222
|
+
return { withSelf: meanCred(rows, 'withSelf'), peersOnly: meanCred(rows, 'peersOnly') };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* v4.8 SI-18 — the FINDINGS attribution, seat-aware. THE HAZARD THIS EXISTS
|
|
227
|
+
* FOR: ledger.js used to compute `findings.filter(f => f.raiser === model)`
|
|
228
|
+
* once per alias and hand the WHOLE result to the block's FIRST pair group
|
|
229
|
+
* (R4b-2's concentration), regardless of how many pair groups PR4b's fan-out
|
|
230
|
+
* gave that alias. On a twin bench whose two seats resolve to DIFFERENT
|
|
231
|
+
* executables — already two separate rows since PR4b — every finding still
|
|
232
|
+
* landed on the first row and the second read 0 and null rates; on a twin
|
|
233
|
+
* bench sharing ONE executable, the single row combined both seats' findings
|
|
234
|
+
* with no way to tell them apart. `findings[].raiserSeat` (tally.js:114-115,
|
|
235
|
+
* emit-when-DIFFERENT from `raiser`) has named the seat that actually raised
|
|
236
|
+
* each finding since v4.8 PR3 Task 5 (anonymize.js :: toGlobalFindings); this
|
|
237
|
+
* join simply never read it. That is the half T3.3's seat-aware `credFor`,
|
|
238
|
+
* just above, did not close.
|
|
239
|
+
*
|
|
240
|
+
* PER-FINDING, NOT PER-GROUP MEAN. Unlike `credFor`'s street-cred numbers, a
|
|
241
|
+
* finding is a discrete item with exactly one true home, not a value to
|
|
242
|
+
* average away. A finding whose `raiserSeat` names a seat exactly one pair
|
|
243
|
+
* group's own runStats rows carry (seat ids are unique within an alias's own
|
|
244
|
+
* table, so at most one group can ever match) is credited to THAT group. The
|
|
245
|
+
* row SET itself never moves — PR4b's (alias, resolvedModel) pairing is
|
|
246
|
+
* unchanged; only which existing row a finding's numbers land on can.
|
|
247
|
+
*
|
|
248
|
+
* THE FALLBACK IS R4b-2, NARROWED, NOT REPLACED. A finding that cannot be
|
|
249
|
+
* resolved to a specific group — `raiserSeat` absent (every pre-seat
|
|
250
|
+
* document, every hand-assembled one), or present but matching no group's own
|
|
251
|
+
* seat (the asymmetric quadrant tally.js's own comment names: both
|
|
252
|
+
* hand-assembled `appendRun` callers can seed `meta.seats` — and so
|
|
253
|
+
* `streetCred` — without ever being asked for a seat on a `runStats` row, so
|
|
254
|
+
* a document can carry `raiserSeat` while no runStats row anywhere carries
|
|
255
|
+
* `.seat`) — still concentrates on the block's FIRST pair group, exactly as
|
|
256
|
+
* R4b-2 always has. So: a document with no seat channel at all leaves every
|
|
257
|
+
* finding unresolved, landing exactly where it always did; a document with a
|
|
258
|
+
* full seat channel splits findings precisely across the rows PR4b's fan-out
|
|
259
|
+
* already produces, and only a genuinely unattributable finding still falls
|
|
260
|
+
* back to the anchor. On the single-row case — twins sharing one executable —
|
|
261
|
+
* every seat's findings resolve to the SAME (only) group, so they combine
|
|
262
|
+
* there exactly as they do today: the row SET, not the attribution rule, is
|
|
263
|
+
* what limits how far apart two twins' numbers can land.
|
|
264
|
+
* @param {Array<Array<object>>} groups this alias's pair groups' runStats
|
|
265
|
+
* rows, in emission order (ledger.js's `groups.map(([, group]) => group)`)
|
|
266
|
+
* @param {Array<object>} raised this alias's raised findings
|
|
267
|
+
* (`findings.filter(f => f.raiser === model)`)
|
|
268
|
+
* @returns {Array<Array<object>>} one findings array per group, same length
|
|
269
|
+
* and order as `groups`; group 0 additionally absorbs every finding no
|
|
270
|
+
* group's own seats could place
|
|
271
|
+
*/
|
|
272
|
+
function splitFindingsBySeat(groups, raised) {
|
|
273
|
+
const seatSets = groups.map(group => new Set(group.map(r => r.seat).filter(Boolean)));
|
|
274
|
+
const mine = groups.map(() => []);
|
|
275
|
+
const leftover = [];
|
|
276
|
+
for (const f of raised) {
|
|
277
|
+
const idx = f.raiserSeat ? seatSets.findIndex(s => s.has(f.raiserSeat)) : -1;
|
|
278
|
+
(idx === -1 ? leftover : mine[idx]).push(f);
|
|
279
|
+
}
|
|
280
|
+
if (mine.length) { mine[0] = mine[0].concat(leftover); }
|
|
281
|
+
return mine;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = { benchLegs, credFor, splitFindingsBySeat, meanCred };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/council/ledger-stats.js
|
|
2
|
+
'use strict';
|
|
3
|
+
// The ledger READ/aggregate half: readRows, avg, countRuns, deriveReliability,
|
|
4
|
+
// buildStatsDoc, and LEDGER_FILE. Moved verbatim from ledger.js@f207538c:192-274
|
|
5
|
+
// (LEDGER_FILE was ledger.js@f207538c:15) — v4.8 Phase 3 T3.0 size-gate split,
|
|
6
|
+
// zero behavior. REQUIRE-FREE of ./ledger by design: this is the leaf the WRITE
|
|
7
|
+
// half (buildLedgerRows/appendRun, which stay in ledger.js) depends on, so the
|
|
8
|
+
// dependency runs one way (ledger.js -> ledger-stats.js) and cannot cycle.
|
|
9
|
+
// ledger.js re-exports all six, so no import path in the tree moved.
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { getConfigDir } = require('../utils/config');
|
|
13
|
+
|
|
14
|
+
const LEDGER_FILE = 'council-ledger.jsonl';
|
|
15
|
+
|
|
16
|
+
function readRows(dir) {
|
|
17
|
+
const file = path.join(dir, LEDGER_FILE);
|
|
18
|
+
if (!fs.existsSync(file)) { return []; }
|
|
19
|
+
return fs.readFileSync(file, 'utf-8').split('\n').map(l => l.trim()).filter(Boolean)
|
|
20
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function avg(nums) { return nums.length ? nums.reduce((s, x) => s + x, 0) / nums.length : null; }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* v4.8 PR4b (R4b-1): `runs` counts distinct council RUNS, not ledger rows. One
|
|
27
|
+
* run on `--models a,a` is one appearance, not two — and that was true of every
|
|
28
|
+
* bench where one executable served more than one seat, pre-PR4b rows included
|
|
29
|
+
* (history self-corrects; the append-only file never blends two counting units).
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ Spell the predicate literally: only a NON-EMPTY STRING runId is an
|
|
32
|
+
* identity. `runId: ''` is persistable and reaches disk verbatim on the
|
|
33
|
+
* hand-assembled tally path, and `runId: 0` is the mirror hazard — under a
|
|
34
|
+
* bare `if (r.runId)` or an `'runId' in r` both would silently collapse a
|
|
35
|
+
* whole history into ONE run, permanently, in a file that is never migrated.
|
|
36
|
+
* Anything that is not a non-empty string counts individually.
|
|
37
|
+
*/
|
|
38
|
+
function countRuns(rows) {
|
|
39
|
+
const ids = new Set();
|
|
40
|
+
let unkeyed = 0;
|
|
41
|
+
for (const r of rows) {
|
|
42
|
+
if (typeof r.runId === 'string' && r.runId) { ids.add(r.runId); } else { unkeyed += 1; }
|
|
43
|
+
}
|
|
44
|
+
return ids.size + unkeyed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Aggregate the ledger per model. peersOnly nulls excluded; lowN flags < 3 runs
|
|
49
|
+
* (v4.8 PR4b R4b-1: `runs` is DISTINCT runIds — see countRuns).
|
|
50
|
+
* v4.7 GOA-7 D10: groups by `row.resolvedModel || row.model` — v2 rows segment
|
|
51
|
+
* by the executable id that actually served; rows without a resolvedModel
|
|
52
|
+
* (pre-v2 history, leg-less rows, hand-assembled tally input) stay alias-keyed
|
|
53
|
+
* with `legacy: true`. `aliases` lists every row-level `model` (alias) observed
|
|
54
|
+
* for the group, most recently observed FIRST — ledger append order is the only
|
|
55
|
+
* recency signal (`date` is day-granular, free-form on the MCP path), so
|
|
56
|
+
* aliases[0] is the launch-preferred name (pickFallbackChair, D11).
|
|
57
|
+
* Version-blind by design: schemaVersion is never read (legacy-read, R2).
|
|
58
|
+
*/
|
|
59
|
+
function deriveReliability(opts = {}) {
|
|
60
|
+
const dir = opts.dir || getConfigDir();
|
|
61
|
+
const byKey = new Map();
|
|
62
|
+
for (const row of readRows(dir)) {
|
|
63
|
+
const key = row.resolvedModel || row.model;
|
|
64
|
+
if (!byKey.has(key)) { byKey.set(key, []); }
|
|
65
|
+
byKey.get(key).push(row);
|
|
66
|
+
}
|
|
67
|
+
return [...byKey.entries()].map(([model, rows]) => {
|
|
68
|
+
const peers = rows.map(r => r.streetCredPeersOnly).filter(v => typeof v === 'number');
|
|
69
|
+
const confirms = rows.map(r => r.confirmRate).filter(v => typeof v === 'number');
|
|
70
|
+
const facts = rows.map(r => r.factErrorRate).filter(v => typeof v === 'number');
|
|
71
|
+
const conformance = rows.reduce((acc, r) => { acc[r.conformance] = (acc[r.conformance] || 0) + 1; return acc; }, {});
|
|
72
|
+
const lastSeen = new Map();
|
|
73
|
+
rows.forEach((r, i) => { lastSeen.set(r.model, i); });
|
|
74
|
+
const aliases = [...lastSeen.entries()].sort((a, b) => b[1] - a[1]).map(([m]) => m);
|
|
75
|
+
const runs = countRuns(rows);
|
|
76
|
+
return {
|
|
77
|
+
model, runs, lowN: runs < 3,
|
|
78
|
+
avgStreetCredPeersOnly: avg(peers),
|
|
79
|
+
lifetimeConfirmRate: avg(confirms),
|
|
80
|
+
lifetimeFactErrorRate: avg(facts),
|
|
81
|
+
conformance,
|
|
82
|
+
aliases,
|
|
83
|
+
...(rows.every(r => !r.resolvedModel) ? { legacy: true } : {}),
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* v4.0 §7: wrap the deriveReliability() rows in the council v2 envelope —
|
|
90
|
+
* THE one sanctioned breaking shape change (`council stats --json` used to
|
|
91
|
+
* emit the bare array). Human rendering keeps consuming the bare rows.
|
|
92
|
+
* @param {Array<object>} models deriveReliability() output
|
|
93
|
+
* @returns {{schemaVersion: number, type: 'council-stats', models: Array<object>}}
|
|
94
|
+
*/
|
|
95
|
+
function buildStatsDoc(models) {
|
|
96
|
+
const { COUNCIL_SCHEMA_VERSION } = require('./tally');
|
|
97
|
+
return { schemaVersion: COUNCIL_SCHEMA_VERSION, type: 'council-stats', models };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { LEDGER_FILE, readRows, avg, countRuns, deriveReliability, buildStatsDoc };
|