the-high-council 0.6.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/.env.example +19 -0
- package/CONTRIBUTING.md +41 -0
- package/LICENSE +21 -0
- package/PERSONAS.md +51 -0
- package/README.md +499 -0
- package/TROUBLESHOOTING.md +45 -0
- package/chains/cheap.json +35 -0
- package/chains/gp-judge-v1.json +80 -0
- package/chains/idea-open-c2.json +106 -0
- package/chains/mock-budget.json +15 -0
- package/chains/mock-debate.json +59 -0
- package/chains/mock-external.json +59 -0
- package/chains/mock-open.json +60 -0
- package/chains/mock-proposals.json +57 -0
- package/chains/mock-questions-wait.json +63 -0
- package/chains/mock-questions.json +63 -0
- package/chains/mock-relay.json +42 -0
- package/chains/mock-unanimous.json +42 -0
- package/chains/mock.json +17 -0
- package/chains/plan-auto-mistral.json +101 -0
- package/chains/plan-auto.json +131 -0
- package/chains/plan-cheap.json +82 -0
- package/chains/plan-debate-c2.json +86 -0
- package/chains/plan-debate-open-c2.json +98 -0
- package/chains/plan-debate-roles-c1.json +149 -0
- package/chains/plan-debate.json +116 -0
- package/chains/plan-fast.json +66 -0
- package/chains/plan-proposals-best-of-5.json +98 -0
- package/chains/plan-proposals.json +96 -0
- package/chains/plan-relay.json +83 -0
- package/chains/plan-thorough.json +82 -0
- package/chains/plan-unanimous.json +82 -0
- package/chains/seven-cheap.json +67 -0
- package/chains/seven.json +60 -0
- package/chains/smo-v2-cheap.json +67 -0
- package/chains/smo.json +50 -0
- package/chains/verify.json +40 -0
- package/package.json +55 -0
- package/src/board-export.js +77 -0
- package/src/budget.js +126 -0
- package/src/buys.js +44 -0
- package/src/cache-integrity.js +38 -0
- package/src/chain-lint.js +107 -0
- package/src/chain.js +880 -0
- package/src/cli.js +1031 -0
- package/src/cost-forecast.js +70 -0
- package/src/cost.js +139 -0
- package/src/errors.js +82 -0
- package/src/http-server.js +349 -0
- package/src/integrity.js +48 -0
- package/src/key-redaction.js +97 -0
- package/src/mcp/server.js +319 -0
- package/src/name-lint.js +186 -0
- package/src/partial-deliverable.js +38 -0
- package/src/peer-claim.js +98 -0
- package/src/personas.js +54 -0
- package/src/preflight.js +84 -0
- package/src/pricing.json +120 -0
- package/src/providers.js +268 -0
- package/src/replay.js +40 -0
- package/src/resume-brief.js +114 -0
- package/src/role-diagnostics.js +181 -0
- package/src/roles.js +520 -0
- package/src/schema-version.js +39 -0
- package/src/scope-freeze.js +35 -0
- package/src/seat-role.js +96 -0
- package/src/shape-rounds.js +58 -0
- package/src/spend.js +193 -0
- package/src/stage-contract.js +223 -0
- package/src/stage-submission.js +53 -0
- package/src/tie-break.js +94 -0
- package/src/ui/app.js +139 -0
- package/src/ui/index.html +43 -0
- package/src/ui/parse.js +101 -0
- package/src/ui/server.js +117 -0
- package/src/ui/style.css +37 -0
- package/src/verdict-stats.js +267 -0
- package/src/withdrawal-ledger.js +47 -0
package/src/replay.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// v5 §1 candidate 9: a numbered, indented, step-by-step transcript of a
|
|
2
|
+
// run's reasoning - a CLI-native complement to the HTML export (candidate
|
|
3
|
+
// 8): CLI-stepped replay for a maintainer debugging a run, vs. a
|
|
4
|
+
// shareable artifact for a stranger. Read-only, derived from report.json.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An ordered list of transcript steps from a run's report.json: proposals,
|
|
8
|
+
* then each recorded stage, then the final verdict. Each step is
|
|
9
|
+
* `{ n, kind, indent, text }` so a renderer can number and indent without
|
|
10
|
+
* re-deriving structure.
|
|
11
|
+
*/
|
|
12
|
+
export function buildTranscript(report) {
|
|
13
|
+
const steps = [];
|
|
14
|
+
let n = 1;
|
|
15
|
+
const push = (kind, indent, text) => steps.push({ n: n++, kind, indent, text });
|
|
16
|
+
|
|
17
|
+
push('meta', 0, `Chain: ${report.chain || 'unknown'}${report.runId ? `, run: ${report.runId}` : ''}`);
|
|
18
|
+
|
|
19
|
+
for (const p of report.proposals || []) {
|
|
20
|
+
push('proposal', 0, `Proposal ${p.id} (${p.lab}): ${p.title}`);
|
|
21
|
+
if (p.what) push('proposal-detail', 1, `What: ${p.what}`);
|
|
22
|
+
if (p.withdrawn) push('proposal-detail', 1, `WITHDRAWN${p.replaced_by ? ` in favour of ${p.replaced_by}` : ''}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const s of report.stages || []) {
|
|
26
|
+
push('stage', 0, `Stage ${s.label} (${s.provider}/${s.model})`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const s of report.signoff || []) {
|
|
30
|
+
push('signoff', 1, `${s.provider}: ${s.signedOff === null ? 'abstained' : s.signedOff ? 'signed off' : 'objected'}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
push('verdict', 0, report.passed ? 'Verdict: PASSED - every lab signed off' : 'Verdict: OPEN OBJECTIONS');
|
|
34
|
+
|
|
35
|
+
return steps;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderTranscriptText(steps) {
|
|
39
|
+
return steps.map(s => `${String(s.n).padStart(4)}. ${' '.repeat(s.indent)}${s.text}`).join('\n');
|
|
40
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Resume-brief mechanism (v2 plan §5, DEEPSEEK-4 / GLM-4 / MISTRAL-4 merged design).
|
|
2
|
+
//
|
|
3
|
+
// A short, deterministic, regenerable summary of one run's state, so a driving session
|
|
4
|
+
// returning after time away - possibly with fully fresh context - can re-enter the run
|
|
5
|
+
// by reading one short file instead of reconstructing state from raw board files.
|
|
6
|
+
//
|
|
7
|
+
// Load-bearing property: RESUME.md is never itself authoritative. It is always
|
|
8
|
+
// regenerable from the run's actual state on disk (run.json + which stage files exist),
|
|
9
|
+
// so a deleted, stale, or hand-edited copy can never become a lying source of truth -
|
|
10
|
+
// the next call just rebuilds it identically. No LLM call: an LLM-generated brief could
|
|
11
|
+
// itself hallucinate state and become a new silent-failure vector, which this project has
|
|
12
|
+
// already been burned by once (the lab-dropout incident) and does not want a second version of.
|
|
13
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { stageKindOf, stageKindsFor } from './stage-contract.js';
|
|
16
|
+
import { checkClaimStaleness } from './peer-claim.js';
|
|
17
|
+
|
|
18
|
+
// Mirrors src/mcp/server.js's waiting(dir), duplicated rather than imported: server.js is
|
|
19
|
+
// the MCP entrypoint (constructs and owns the server instance as a side effect of import),
|
|
20
|
+
// not a library other modules should pull in just to read run-folder state.
|
|
21
|
+
function pendingStage(dir) {
|
|
22
|
+
if (!existsSync(dir)) return null;
|
|
23
|
+
const w = readdirSync(dir).filter(f => f.startsWith('NEEDS-')).map(f => f.slice(6, -3)).filter(l => !existsSync(join(dir, `${l}.md`)));
|
|
24
|
+
return w[0] || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function oneLineTaskSummary(taskPath) {
|
|
28
|
+
if (!taskPath || !existsSync(taskPath)) return '(task file not found on disk)';
|
|
29
|
+
const text = readFileSync(taskPath, 'utf8').trim();
|
|
30
|
+
const firstLine = text.split('\n').find(l => l.trim().length > 0) || text;
|
|
31
|
+
const clean = firstLine.replace(/^#+\s*/, '').trim();
|
|
32
|
+
return clean.length > 140 ? `${clean.slice(0, 137)}...` : clean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Which stage-kind labels have at least one completed stage file on disk, in the order
|
|
37
|
+
* chain.js would have run them (per stageKindsFor). A "completed" label is any file
|
|
38
|
+
* `<label>.md` in the run folder whose label maps to a known stage kind - dedup by kind
|
|
39
|
+
* since chain.js emits one label per lab/round for several kinds.
|
|
40
|
+
*/
|
|
41
|
+
function completedKinds(dir, chainConfig) {
|
|
42
|
+
if (!existsSync(dir)) return [];
|
|
43
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.md') && !f.startsWith('NEEDS-') && f !== 'RESUME.md' && f !== 'BOARD.md' && f !== 'HANDOFF.md' && f !== 'deliverable.md' && f !== 'WARNINGS.md' && f !== 'proposals.md' && f !== 'proposals-pool.md');
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
for (const f of files) {
|
|
46
|
+
const kind = stageKindOf(f.replace(/\.md$/, ''));
|
|
47
|
+
if (kind) seen.add(kind);
|
|
48
|
+
}
|
|
49
|
+
// Preserve chain-run order rather than filesystem order, so the brief reads as a timeline.
|
|
50
|
+
return stageKindsFor(chainConfig).filter(k => seen.has(k));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build the resume brief for one run. Pure given its inputs: same run.json + same files on
|
|
55
|
+
* disk always produce the same text (aside from being able to note new files appearing).
|
|
56
|
+
*/
|
|
57
|
+
export function generateResumeBrief({ runId, dir, runMeta, chainConfig }) {
|
|
58
|
+
const kindsInOrder = chainConfig ? stageKindsFor(chainConfig) : [];
|
|
59
|
+
const done = chainConfig ? completedKinds(dir, chainConfig) : [];
|
|
60
|
+
const pending = pendingStage(dir);
|
|
61
|
+
const deliverableDone = existsSync(join(dir, 'deliverable.md'));
|
|
62
|
+
|
|
63
|
+
const lines = [];
|
|
64
|
+
lines.push(`# RESUME BRIEF — run ${runId}`);
|
|
65
|
+
lines.push(`Task: ${oneLineTaskSummary(runMeta?.task)}`);
|
|
66
|
+
lines.push(`Status: ${deliverableDone ? kindsInOrder.length : done.length} of ${kindsInOrder.length || '?'} stages complete`);
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push('## Decided so far');
|
|
69
|
+
if (done.length === 0) lines.push('- (nothing completed yet)');
|
|
70
|
+
else for (const kind of done) lines.push(`- ${kind}: complete`);
|
|
71
|
+
// v3 §2: a declared scope amendment goes on the record here - the one block every
|
|
72
|
+
// subsequent stage's prompt already reads - so a critic sees it instead of an
|
|
73
|
+
// unexplained late requirement.
|
|
74
|
+
const amendmentsPath = join(dir, 'AMENDMENTS.md');
|
|
75
|
+
if (existsSync(amendmentsPath)) {
|
|
76
|
+
lines.push('- Scope amendment(s) recorded, see below:');
|
|
77
|
+
lines.push(readFileSync(amendmentsPath, 'utf8').trim());
|
|
78
|
+
}
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push('## Currently pending');
|
|
81
|
+
if (deliverableDone) {
|
|
82
|
+
lines.push('Stage: none - run is complete');
|
|
83
|
+
} else if (pending) {
|
|
84
|
+
lines.push(`Stage: ${pending}`);
|
|
85
|
+
lines.push('Waiting on: an external seat\'s reply (see NEEDS-<stage>.md, or dispatch it with prepare_stage_prompt)');
|
|
86
|
+
// v3 plan §1: surfaced here too, not only from external_prompt, so a returning session
|
|
87
|
+
// sees a stalled or contested claim without an extra tool call.
|
|
88
|
+
const claim = checkClaimStaleness(dir, pending);
|
|
89
|
+
lines.push(`Claim: ${claim ? claim.type : 'none'}`);
|
|
90
|
+
} else {
|
|
91
|
+
lines.push('Stage: in progress or between stages');
|
|
92
|
+
lines.push('Waiting on: nothing external right now');
|
|
93
|
+
}
|
|
94
|
+
lines.push(`Warnings: ${existsSync(join(dir, 'WARNINGS.md')) ? 'see WARNINGS.md' : 'none recorded'}`);
|
|
95
|
+
// v5 §1 candidate 3: read back from report.json rather than recomputing -
|
|
96
|
+
// by the time this runs (post-deliverable), the ledger is already settled
|
|
97
|
+
// and recorded there; a resume brief has no reason to duplicate the walk.
|
|
98
|
+
const reportPath = join(dir, 'report.json');
|
|
99
|
+
if (existsSync(reportPath)) {
|
|
100
|
+
try {
|
|
101
|
+
const report = JSON.parse(readFileSync(reportPath, 'utf8'));
|
|
102
|
+
if (report.orphanSections?.length) {
|
|
103
|
+
lines.push(`Orphaned withdrawals: ${report.withdrawalCycles} cycle(s), no surviving owner for ${report.orphanSections.join(', ')} - see BOARD.md`);
|
|
104
|
+
}
|
|
105
|
+
} catch { /* malformed report.json: say nothing rather than guess */ }
|
|
106
|
+
}
|
|
107
|
+
lines.push('');
|
|
108
|
+
lines.push('## Next action for the driving session');
|
|
109
|
+
if (deliverableDone) lines.push(`Run is complete. Deliverable at ${join(dir, 'deliverable.md')}.`);
|
|
110
|
+
else if (pending) lines.push(`Call prepare_stage_prompt(${JSON.stringify(runId)}, ${JSON.stringify(pending)}), or answer it directly and call submit_stage.`);
|
|
111
|
+
else lines.push(`Call resume_run(${JSON.stringify(runId)}) or run_status(${JSON.stringify(runId)}) to check current progress.`);
|
|
112
|
+
|
|
113
|
+
return `${lines.join('\n')}\n`;
|
|
114
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// v6 §7: four failure-mode detectors, reconsidered against the corrected
|
|
2
|
+
// framing (Muad, 2026-09-13): the feature is not "catch more planted
|
|
3
|
+
// defects" - it is debate diversity among real model seats reasoning
|
|
4
|
+
// differently about a real infrastructure decision. A deterministic
|
|
5
|
+
// heuristic probe (v6 phase 4) cannot have a philosophy, so it cannot
|
|
6
|
+
// see the thing this feature is for. See docs/v6-decisions.md for the
|
|
7
|
+
// full argument: two of the plan's four detectors originally read
|
|
8
|
+
// per-seat catch-rate data from the phase 4 probe; both are redesigned
|
|
9
|
+
// here to read real debate-stage output (a run's own report.json)
|
|
10
|
+
// instead, so all four detectors measure something the feature's actual
|
|
11
|
+
// purpose can be judged by, not a proxy for a question the feature was
|
|
12
|
+
// never trying to answer.
|
|
13
|
+
//
|
|
14
|
+
// Every function here is pure and read-only: no ledger, nothing
|
|
15
|
+
// transmitted, computed from `report.debate.posts` on disk exactly the
|
|
16
|
+
// same way verdict-stats.js and cost-forecast.js already derive their
|
|
17
|
+
// numbers from run folders.
|
|
18
|
+
|
|
19
|
+
const QUOTE_RE = /"([^"]{3,})"/g;
|
|
20
|
+
|
|
21
|
+
function quotedSpans(text) {
|
|
22
|
+
return [...String(text || '').matchAll(QUOTE_RE)].map(m => m[1]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function wordSet(text) {
|
|
26
|
+
return new Set(String(text || '').toLowerCase().match(/[a-z0-9]{4,}/g) || []);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function jaccard(a, b) {
|
|
30
|
+
if (!a.size && !b.size) return null;
|
|
31
|
+
const inter = [...a].filter(x => b.has(x)).length;
|
|
32
|
+
const union = new Set([...a, ...b]).size;
|
|
33
|
+
return union ? inter / union : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Per-seat (by `by`, the anonymised or real lab tag on each post)
|
|
38
|
+
* substance and evidence-share metrics, computed only from a run's own
|
|
39
|
+
* debate.posts. Returns `null` for a lab with no posts requiring a
|
|
40
|
+
* quote (support posts don't - DEBATE_SYSTEM only asks for one on
|
|
41
|
+
* object/merge), rather than a misleading 0.
|
|
42
|
+
*
|
|
43
|
+
* substanceRatio: v6 §7 failure mode 1, character-performed-not-
|
|
44
|
+
* reviewed. Fraction of a seat's object/merge posts that contain at
|
|
45
|
+
* least one quoted span, since DEBATE_SYSTEM's own instructions
|
|
46
|
+
* require quoting the phrase being objected to or merged - a seat that
|
|
47
|
+
* never quotes despite the prompt asking for it is arguing without
|
|
48
|
+
* engaging the text in front of it, whatever its role.
|
|
49
|
+
*
|
|
50
|
+
* evidenceTokenShare: v6 §7 failure mode 4, token spend shifting from
|
|
51
|
+
* substance to voice. Fraction of a seat's total post-text words that
|
|
52
|
+
* fall inside a quoted span, across every post regardless of stance.
|
|
53
|
+
*/
|
|
54
|
+
export function perSeatMetrics(posts) {
|
|
55
|
+
const byLab = new Map();
|
|
56
|
+
for (const p of posts || []) {
|
|
57
|
+
if (!p.by) continue;
|
|
58
|
+
if (!byLab.has(p.by)) byLab.set(p.by, { evidencePosts: 0, quotedPosts: 0, totalWords: 0, quotedWords: 0 });
|
|
59
|
+
const m = byLab.get(p.by);
|
|
60
|
+
const text = String(p.text || '');
|
|
61
|
+
const words = (text.match(/\S+/g) || []).length;
|
|
62
|
+
m.totalWords += words;
|
|
63
|
+
const spans = quotedSpans(text);
|
|
64
|
+
const quotedWords = spans.reduce((s, q) => s + (q.match(/\S+/g) || []).length, 0);
|
|
65
|
+
m.quotedWords += quotedWords;
|
|
66
|
+
if (p.stance === 'object' || p.stance === 'merge') {
|
|
67
|
+
m.evidencePosts += 1;
|
|
68
|
+
if (spans.length) m.quotedPosts += 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const out = {};
|
|
72
|
+
for (const [lab, m] of byLab) {
|
|
73
|
+
out[lab] = {
|
|
74
|
+
substanceRatio: m.evidencePosts ? m.quotedPosts / m.evidencePosts : null,
|
|
75
|
+
evidenceTokenShare: m.totalWords ? m.quotedWords / m.totalWords : null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* v6 §7 failure mode 2, role-correlation collapse - reconsidered.
|
|
83
|
+
* Originally specified as a comparison against the phase 4 probe's
|
|
84
|
+
* plain-arm baseline; that baseline is a deterministic heuristic mock,
|
|
85
|
+
* not a real model, and cannot speak to real debate diversity. This
|
|
86
|
+
* instead measures the mean pairwise textual overlap (Jaccard over
|
|
87
|
+
* 4+ letter words) between different labs' posts on the SAME proposal,
|
|
88
|
+
* within one real debate stage - a lens meant to diversify argument
|
|
89
|
+
* that instead makes every seat's objection read the same is a real,
|
|
90
|
+
* checkable signal computed from what seats actually wrote. Returns
|
|
91
|
+
* `null` when there are fewer than two comparable posts (nothing to
|
|
92
|
+
* compare, not evidence of agreement).
|
|
93
|
+
*/
|
|
94
|
+
export function pairwiseAgreement(posts) {
|
|
95
|
+
const byTarget = new Map();
|
|
96
|
+
for (const p of posts || []) {
|
|
97
|
+
if (!p.on || !p.by) continue;
|
|
98
|
+
if (!byTarget.has(p.on)) byTarget.set(p.on, []);
|
|
99
|
+
byTarget.get(p.on).push(p);
|
|
100
|
+
}
|
|
101
|
+
const scores = [];
|
|
102
|
+
for (const group of byTarget.values()) {
|
|
103
|
+
for (let i = 0; i < group.length; i++) {
|
|
104
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
105
|
+
if (group[i].by === group[j].by) continue;
|
|
106
|
+
const score = jaccard(wordSet(group[i].text), wordSet(group[j].text));
|
|
107
|
+
if (score !== null) scores.push(score);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return scores.length ? scores.reduce((a, b) => a + b, 0) / scores.length : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* v6 §7 failure mode 3, a role degrading a weaker model - reconsidered.
|
|
116
|
+
* Originally specified as a per-seat catch-rate delta read from the
|
|
117
|
+
* phase 4 probe; catch rate against planted synthetic defects is not
|
|
118
|
+
* what this feature is for (see module header). This instead compares,
|
|
119
|
+
* within the SAME real run, the mean substanceRatio (above) of seats
|
|
120
|
+
* carrying a role against seats that do not - a same-run, cross-
|
|
121
|
+
* sectional read, not a longitudinal one (it cannot tell you whether
|
|
122
|
+
* role-bearing seat X got worse than seat X used to be without a role,
|
|
123
|
+
* only whether role-bearing seats collectively argued less
|
|
124
|
+
* substantively than role-less seats in this one run). Returns `null`
|
|
125
|
+
* when a run has no mix of role-bearing and role-less seats to compare
|
|
126
|
+
* - a chain where every seat has the same role status has nothing this
|
|
127
|
+
* detector can say.
|
|
128
|
+
*/
|
|
129
|
+
export function roleVsPlainSubstanceGap(perSeat, roleLabs) {
|
|
130
|
+
const roleSeats = [];
|
|
131
|
+
const plainSeats = [];
|
|
132
|
+
for (const [lab, m] of Object.entries(perSeat)) {
|
|
133
|
+
if (m.substanceRatio === null) continue;
|
|
134
|
+
(roleLabs.has(lab) ? roleSeats : plainSeats).push(m.substanceRatio);
|
|
135
|
+
}
|
|
136
|
+
if (!roleSeats.length || !plainSeats.length) return null;
|
|
137
|
+
const mean = xs => xs.reduce((a, b) => a + b, 0) / xs.length;
|
|
138
|
+
return mean(roleSeats) - mean(plainSeats);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Placeholder thresholds, author to tighten (v6 Decisions left to the
|
|
142
|
+
// author already names this class of number as open) - not derived
|
|
143
|
+
// from any measurement, only from "clearly bad" being obviously below/
|
|
144
|
+
// above a midpoint.
|
|
145
|
+
const SUBSTANCE_FLOOR = 0.5;
|
|
146
|
+
const EVIDENCE_SHARE_FLOOR = 0.15;
|
|
147
|
+
const AGREEMENT_CEILING = 0.6;
|
|
148
|
+
const SUBSTANCE_GAP_FLOOR = -0.2;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The full diagnostics object for one run's `report.debate`, meant to
|
|
152
|
+
* be attached at `report.json`'s `debate.diagnostics` per v6 §7. Never
|
|
153
|
+
* throws on a run with no debate stage or no posts - returns nulls and
|
|
154
|
+
* empty flags, not an error, matching every other read-only reporting
|
|
155
|
+
* module in this project.
|
|
156
|
+
*/
|
|
157
|
+
export function computeRoleDiagnostics(debate, roleLabs = new Set()) {
|
|
158
|
+
const posts = debate?.posts || [];
|
|
159
|
+
const perSeat = perSeatMetrics(posts);
|
|
160
|
+
const agreement = pairwiseAgreement(posts);
|
|
161
|
+
const substanceGap = roleVsPlainSubstanceGap(perSeat, roleLabs);
|
|
162
|
+
|
|
163
|
+
const characterPerformedNotReviewed = Object.entries(perSeat)
|
|
164
|
+
.filter(([, m]) => m.substanceRatio !== null && m.substanceRatio < SUBSTANCE_FLOOR)
|
|
165
|
+
.map(([lab]) => lab);
|
|
166
|
+
const tokenShiftToVoice = Object.entries(perSeat)
|
|
167
|
+
.filter(([, m]) => m.evidenceTokenShare !== null && m.evidenceTokenShare < EVIDENCE_SHARE_FLOOR)
|
|
168
|
+
.map(([lab]) => lab);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
perSeat,
|
|
172
|
+
pairwiseAgreement: agreement,
|
|
173
|
+
roleVsPlainSubstanceGap: substanceGap,
|
|
174
|
+
flags: {
|
|
175
|
+
characterPerformedNotReviewed,
|
|
176
|
+
tokenShiftToVoice,
|
|
177
|
+
roleCorrelationCollapse: agreement !== null && agreement > AGREEMENT_CEILING,
|
|
178
|
+
roleDegradesOutput: substanceGap !== null && substanceGap < SUBSTANCE_GAP_FLOOR,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|