aiki-cli 0.2.2 → 0.3.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +98 -7
  2. package/README.md +109 -30
  3. package/dist/bench/arms.js +10 -9
  4. package/dist/bench/harness.js +13 -10
  5. package/dist/bench/idea-lane-rotation.js +237 -0
  6. package/dist/bench/idea-v3-bench.js +506 -0
  7. package/dist/bench/idea-v3-rating.js +582 -0
  8. package/dist/bench/results.js +10 -3
  9. package/dist/bench/scoring/decision-insights.js +112 -0
  10. package/dist/bench/scoring/seeded-bugs.js +4 -0
  11. package/dist/cli/bench.js +180 -3
  12. package/dist/cli/doctor.js +56 -24
  13. package/dist/cli/index.js +31 -6
  14. package/dist/cli/resolve.js +7 -6
  15. package/dist/cli/resume.js +18 -0
  16. package/dist/cli/run.js +63 -8
  17. package/dist/council/view.js +378 -109
  18. package/dist/orchestration/calculations.js +97 -0
  19. package/dist/orchestration/context.js +37 -9
  20. package/dist/orchestration/decision-dossier.js +262 -0
  21. package/dist/orchestration/decision-graph.js +256 -0
  22. package/dist/orchestration/engine.js +5 -2
  23. package/dist/orchestration/evidence-pack.js +46 -0
  24. package/dist/orchestration/idea-lanes.js +20 -0
  25. package/dist/orchestration/jsonStage.js +72 -0
  26. package/dist/orchestration/legacy-idea-adapter.js +102 -0
  27. package/dist/orchestration/modes.js +33 -0
  28. package/dist/orchestration/preflight.js +183 -0
  29. package/dist/orchestration/quick-analysis.js +81 -0
  30. package/dist/orchestration/stages/cr-ladder.js +80 -0
  31. package/dist/orchestration/stages/s10-render.js +562 -150
  32. package/dist/orchestration/stages/s4-analyze.js +12 -7
  33. package/dist/orchestration/stages/s5-drift.js +9 -9
  34. package/dist/orchestration/stages/s6-positions.js +10 -0
  35. package/dist/orchestration/stages/s7-decision-graph.js +76 -0
  36. package/dist/orchestration/stages/s8-verify.js +153 -46
  37. package/dist/orchestration/stages/s8b-rebuttal.js +208 -0
  38. package/dist/orchestration/stages/s9-judge.js +329 -108
  39. package/dist/orchestration/stages/s9b-plan.js +85 -75
  40. package/dist/providers/codex.js +2 -1
  41. package/dist/providers/spawn.js +5 -0
  42. package/dist/schemas/index.js +572 -13
  43. package/dist/skills/idea-refinement/analyst.md +18 -14
  44. package/dist/skills/idea-refinement/economics-delivery.md +7 -0
  45. package/dist/skills/idea-refinement/market-adoption.md +7 -0
  46. package/dist/storage/runs.js +11 -4
  47. package/dist/tui/app.js +37 -13
  48. package/dist/tui/format.js +4 -5
  49. package/dist/tui/smart-entry.js +17 -1
  50. package/dist/tui/timeline.js +2 -4
  51. package/dist/workflows/code-review.js +4 -2
  52. package/dist/workflows/idea-refinement.js +110 -46
  53. package/package.json +12 -4
  54. package/dist/orchestration/stages/s0-grill.js +0 -79
  55. package/dist/orchestration/stages/s1-intent.js +0 -25
  56. package/dist/orchestration/stages/s2-misread.js +0 -76
  57. package/dist/orchestration/stages/s3-prompts.js +0 -55
  58. package/dist/orchestration/stages/s6-claims.js +0 -56
  59. package/dist/orchestration/stages/s7-disagreement.js +0 -134
@@ -1,46 +1,48 @@
1
1
  // S10 — artifact rendering (§9, §12.1, §307). Pure code → `final-report.md`, a DECISION BRIEF, not a
2
- // smoothed essay (§263). Every section is assembled deterministically from prior artifacts; the only
3
- // computed content is the assumption-audit status/confidence, which is DERIVED here (not taken from
4
- // the judge — §624). A truly missing required field is a template bug (fail loudly); degraded-but-valid
2
+ // smoothed essay (§263). Every section is assembled deterministically from prior artifacts; audit,
3
+ // sensitivity, coverage, contribution, and confidence are DERIVED here rather than invented by the judge.
4
+ // A truly missing required field is a template bug (fail loudly); degraded-but-valid
5
5
  // states (S8 skipped, items UNVERIFIED, empty consensus) render normally. User-facing → DISPLAY_NAME.
6
6
  import { DISPLAY_NAME } from '../../providers/types.js';
7
7
  import { overlap, tokenize } from '../cluster.js';
8
- /** Pure: derive the assumption-audit (held/failed/unverified + confidence) from the map + the judge's
9
- * rulings on disputes. Consensus+undisputed → held/HIGH; single-provider+undisputed → held/MEDIUM;
10
- * attack REJECTed held/MEDIUM; UPHELD → failed/LOW; UNRESOLVED or unadjudicated → unverified/LOW. */
11
- export function deriveAudit(map, judgeReport) {
12
- const contestedBy = new Map(); // claim id → contradiction id
13
- for (const d of map.contradictions)
14
- for (const cid of d.claim_ids)
15
- contestedBy.set(cid, d.id);
8
+ import { renderDecisionDossierMarkdown } from '../decision-dossier.js';
9
+ import { callCategory } from '../modes.js';
10
+ /** Pure graph-backed decision audit. */
11
+ export function deriveAudit(graph, judgeReport) {
16
12
  const ruling = new Map(judgeReport.adjudications.map((a) => [a.id, a.ruling]));
17
- return [...map.consensus, ...map.unique].map((c) => {
18
- const dispId = contestedBy.get(c.id);
13
+ const positionById = new Map(graph.positions.map((position) => [position.id, position]));
14
+ return graph.claims.map((claim) => {
15
+ const positions = claim.position_ids.map((id) => positionById.get(id));
16
+ const providers = [...new Set(positions.map((position) => position.provider))];
19
17
  let status;
20
18
  let confidence;
21
- if (!dispId) {
22
- status = 'held';
23
- confidence = c.providers.length >= 2 ? 'HIGH' : 'MEDIUM';
19
+ if (claim.state === 'UNCERTAIN' || claim.evidence_state !== 'SUPPORTED') {
20
+ [status, confidence] = ['unverified', 'LOW'];
24
21
  }
25
- else {
26
- const r = ruling.get(dispId);
27
- if (r === 'REJECT')
22
+ else if (claim.state === 'SHARED_CONCERN' || (claim.state === 'UNIQUE' && positions[0]?.stance === 'OPPOSE')) {
23
+ [status, confidence] = ['failed', providers.length >= 2 ? 'HIGH' : 'MEDIUM'];
24
+ }
25
+ else if (claim.state === 'DISAGREEMENT') {
26
+ const result = ruling.get(claim.id);
27
+ if (result === 'UPHOLD')
28
+ [status, confidence] = ['failed', 'MEDIUM'];
29
+ else if (result === 'REJECT')
28
30
  [status, confidence] = ['held', 'MEDIUM'];
29
- else if (r === 'UPHOLD')
30
- [status, confidence] = ['failed', 'LOW'];
31
31
  else
32
- [status, confidence] = ['unverified', 'LOW']; // UNRESOLVED / unadjudicated
32
+ [status, confidence] = ['unverified', 'LOW'];
33
33
  }
34
- return { id: c.id, statement: c.statement, providers: c.providers, status, confidence };
34
+ else {
35
+ [status, confidence] = ['held', providers.length >= 2 ? 'HIGH' : 'MEDIUM'];
36
+ }
37
+ return { id: claim.id, statement: claim.proposition, providers, status, confidence };
35
38
  });
36
39
  }
37
40
  const disp = (id) => DISPLAY_NAME[id];
38
- const attrib = (ps) => ps.map(disp).join(', ');
39
- /** Union of the seats' open questions, deduped by lexical similarity (≥0.85), capped. */
41
+ /** Union of the seats' decision questions, deduped by lexical similarity (≥0.85), capped. */
40
42
  export function mergeOpenQuestions(seats, cap = 10) {
41
43
  const kept = [];
42
44
  for (const seat of seats) {
43
- for (const q of seat.output.open_questions) {
45
+ for (const { question: q } of seat.output.decision_questions) {
44
46
  const tokens = tokenize(q);
45
47
  if (!kept.some((k) => overlap(k.tokens, tokens) >= 0.85))
46
48
  kept.push({ q, tokens });
@@ -48,25 +50,29 @@ export function mergeOpenQuestions(seats, cap = 10) {
48
50
  }
49
51
  return kept.slice(0, cap).map((k) => k.q);
50
52
  }
51
- /** Best-effort rubric coverage for the report. This is intentionally coarse: it mirrors S7's keyword
52
- * coverage, never throws, and labels the result honestly as best-effort in the renderer. */
53
- export function deriveScorecard(rubric, map) {
54
- const claimById = new Map([...map.consensus, ...map.unique].map((c) => [c.id, c.statement]));
55
- const blind = new Set(map.blind_spots.map((b) => b.toLowerCase()));
56
- const contestedTexts = map.contradictions.map((d) => d.claim_ids.map((id) => claimById.get(id) ?? id).join(' '));
53
+ /** Exact rubric coverage derived from graph dimension anchors and holes. */
54
+ export function deriveScorecard(rubric, graph) {
55
+ const blind = new Set(graph.holes.coverage.map((hole) => hole.dimension_id));
56
+ const claimByPosition = new Map(graph.claims.flatMap((claim) => claim.position_ids.map((id) => [id, claim])));
57
57
  return rubric.map((r) => {
58
- if (blind.has(r.label.toLowerCase()))
58
+ if (blind.has(r.id))
59
59
  return { id: r.id, label: r.label, status: 'unexamined' };
60
- const dimensionTokens = new Set([...tokenize(r.label), ...r.keywords.flatMap((kw) => [...tokenize(kw)])]);
61
- const contested = contestedTexts.some((text) => overlap(dimensionTokens, tokenize(text)) > 0);
60
+ const contested = graph.positions
61
+ .filter((position) => position.dimension_id === r.id)
62
+ .some((position) => {
63
+ const state = claimByPosition.get(position.id)?.state;
64
+ return state === 'DISAGREEMENT' || state === 'UNCERTAIN';
65
+ });
62
66
  return { id: r.id, label: r.label, status: contested ? 'contested' : 'examined' };
63
67
  });
64
68
  }
65
- function recommendationLabel(r) {
66
- return r === 'PROCEED_WITH_CONDITIONS' ? 'PROCEED WITH CONDITIONS' : r;
67
- }
68
- function mdCell(s) {
69
- return s.replaceAll('\n', ' ').replaceAll('|', '\\|');
69
+ function receiptCategories(ctx) {
70
+ if (typeof ctx.receipt === 'function')
71
+ return ctx.receipt();
72
+ const categories = { discovery: 0, verification: 0, repair: 0, planning: 0 };
73
+ for (const call of ctx.calls)
74
+ categories[call.category ?? callCategory(call.stage)]++;
75
+ return categories;
70
76
  }
71
77
  function rulingPhrase(ruling) {
72
78
  if (ruling === 'UPHOLD')
@@ -75,124 +81,530 @@ function rulingPhrase(ruling) {
75
81
  return 'the idea holds here';
76
82
  return 'left to you';
77
83
  }
78
- function receiptLines(ctx) {
79
- const byProvider = new Map();
80
- let ms = 0;
81
- for (const c of ctx.calls) {
82
- byProvider.set(c.provider, (byProvider.get(c.provider) ?? 0) + 1);
83
- ms += c.durationMs;
84
+ export function statusFrom(judgeReport) {
85
+ switch (judgeReport.recommendation) {
86
+ case 'PROCEED': return 'ACCEPTED';
87
+ case 'PROCEED_WITH_CONDITIONS': return 'ACCEPTED_WITH_CONDITIONS';
88
+ case 'PIVOT':
89
+ case 'STOP': return 'REJECTED';
90
+ default: return 'INCONCLUSIVE';
84
91
  }
85
- const providerCounts = [...byProvider.entries()].map(([p, n]) => `${disp(p)} ${n}`).join(', ') || 'none';
86
- return [
87
- `- Calls: ${ctx.calls.length}/${ctx.budget.limit}`,
88
- `- By provider: ${providerCounts}`,
89
- `- Recorded model time: ${(ms / 1000).toFixed(1)}s`,
90
- ];
91
92
  }
92
- /** Build the markdown report (pure given the run context's read-only accounting). */
93
- export function renderReport(ctx, args) {
94
- const { seats, map, judgeReport, actionPlan } = args;
95
- const flags = [...ctx.flags];
96
- const audit = deriveAudit(map, judgeReport);
97
- const scorecard = args.rubric ? deriveScorecard(args.rubric, map) : [];
98
- const rulingById = new Map(judgeReport.adjudications.map((a) => [a.id, a]));
99
- const claimById = new Map([...map.consensus, ...map.unique].map((c) => [c.id, c]));
100
- const L = [];
101
- L.push(`# Decision Brief ${ctx.runId}`, '');
102
- L.push(`- Providers: ${ctx.available().map(disp).join(', ')} · calls: ${ctx.calls.length}/${ctx.budget.limit}`);
103
- if (flags.length)
104
- L.push(`- Flags: ${flags.join(', ')}`);
105
- L.push('');
106
- if (judgeReport.recommendation) {
107
- L.push('## Bottom line', '', `**${recommendationLabel(judgeReport.recommendation)}** ${judgeReport.verdict}`, '');
108
- if (judgeReport.conditions?.length) {
109
- L.push('Conditions:');
110
- for (const c of judgeReport.conditions)
111
- L.push(`- ${c}`);
112
- L.push('');
113
- }
114
- }
115
- else {
116
- L.push('## Verdict', '', judgeReport.verdict, '');
117
- }
118
- if (judgeReport.key_points?.length) {
119
- L.push("## Chairman's reasoning", '');
120
- for (const p of judgeReport.key_points)
121
- L.push(`- ${p}`);
122
- L.push('');
93
+ const DEGRADATION_FLAGS = ['low_diversity', 'synthesis_suspect', 'plan_fallback', 'plan_skipped'];
94
+ /** Structural confidence 40% verification coverage + 25% convergence + 20% evidence quality +
95
+ * 15% stability critical-risk penalty. A starting heuristic, NOT a calibrated probability; model
96
+ * self-confidence never enters it, and consensus alone can never reach the High band. */
97
+ export function computeConfidence(graph, flags) {
98
+ const loadBearing = graph.claims.filter((claim) => claim.load_bearing);
99
+ const verificationCoverage = loadBearing.length
100
+ ? loadBearing.filter((claim) => claim.evidence_state === 'SUPPORTED').length / loadBearing.length : 0;
101
+ const independentConvergence = graph.claims.length
102
+ ? graph.claims.filter((claim) => claim.state === 'CONSENSUS' || claim.state === 'SHARED_CONCERN').length / graph.claims.length : 0;
103
+ const evidenceQuality = graph.evidence.length
104
+ ? graph.evidence.filter((card) => card.source_kind !== 'MODEL_KNOWLEDGE').length / graph.evidence.length : 0;
105
+ const stability = Math.max(0, 1 - 0.25 * DEGRADATION_FLAGS.filter((flag) => flags.has(flag)).length);
106
+ const criticalRiskPenalty = Math.min(20, 5 * loadBearing.filter((claim) => claim.if_false === 'STOP' && claim.evidence_state !== 'SUPPORTED').length);
107
+ let score = Math.round(verificationCoverage * 40 + independentConvergence * 25 + evidenceQuality * 20 + stability * 15 - criticalRiskPenalty);
108
+ if (verificationCoverage < 0.5)
109
+ score = Math.min(score, 79); // consensus alone never yields High
110
+ score = Math.max(0, Math.min(100, score));
111
+ const label = score >= 80 ? 'High' : score >= 60 ? 'Medium' : 'Low';
112
+ return { score, label, verificationCoverage, independentConvergence, evidenceQuality, stability, criticalRiskPenalty };
113
+ }
114
+ const STANCE_MAP = { SUPPORT: 'AGREE', OPPOSE: 'DISAGREE', MIXED: 'CONDITIONAL', UNKNOWN: 'UNKNOWN' };
115
+ const VERIFICATION_MAP = { SUPPORTED: 'VERIFIED', CONFLICTED: 'PARTIAL', UNVERIFIED: 'UNVERIFIED' };
116
+ const SEVERITY_MAP = { DECISIVE: 'High', MATERIAL: 'Medium', LOW: 'Low' };
117
+ function claimRuling(claim, adjudication) {
118
+ // UPHOLD/REJECT inverts ONLY on a disagreement (the ruling is on the objection); for every other
119
+ // state the ruling is evidence-based, matching deriveAudit's semantics.
120
+ if (claim.state === 'DISAGREEMENT') {
121
+ if (!adjudication || adjudication.ruling === 'UNRESOLVED')
122
+ return 'UNRESOLVED';
123
+ return adjudication.ruling === 'UPHOLD' ? 'REJECTED' : 'ACCEPTED';
123
124
  }
124
- if (scorecard.length) {
125
- L.push('## Dimension scorecard (best-effort)', '', '| Dimension | Status |', '|---|---|');
126
- for (const r of scorecard)
127
- L.push(`| ${mdCell(r.label)} | ${r.status} |`);
128
- L.push('');
125
+ if (claim.state === 'UNCERTAIN')
126
+ return 'UNRESOLVED';
127
+ return claim.evidence_state === 'SUPPORTED' ? 'ACCEPTED' : 'CONDITIONAL';
128
+ }
129
+ function verificationStatusByClaim(verifications) {
130
+ const statuses = new Map();
131
+ for (const item of verifications.verifications) {
132
+ if ('claim_id' in item)
133
+ statuses.set(item.claim_id, item.status);
134
+ else
135
+ statuses.set(item.target_id, item.verdict === 'CONFIRM' ? 'VERIFIED' : item.verdict === 'REFUTE' ? 'CONTRADICTED' : 'PARTIAL');
129
136
  }
130
- L.push('## Assumption audit', '', '| Claim | Status | Confidence | Analysts |', '|---|---|---|---|');
131
- for (const r of audit)
132
- L.push(`| ${mdCell(r.statement)} | ${r.status} | ${r.confidence} | ${attrib(r.providers)} |`);
133
- L.push('');
134
- const failed = audit.filter((r) => r.status === 'failed');
135
- if (failed.length) {
136
- L.push('## Risks that held up', '');
137
- for (const r of failed)
138
- L.push(`- ${r.statement} _(${r.confidence})_`);
139
- L.push('');
137
+ return statuses;
138
+ }
139
+ function buildDossier(args) {
140
+ const { graph, judgeReport, actionPlan, rebuttals, seats } = args;
141
+ const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
142
+ const reportClaimById = new Map(args.claims.map((claim) => [claim.id, claim]));
143
+ const positionById = new Map(graph.positions.map((position) => [position.id, position]));
144
+ const verificationById = verificationStatusByClaim(args.verifications);
145
+ const verifiedClaimIds = new Set([...verificationById].filter(([, status]) => status === 'VERIFIED').map(([id]) => id));
146
+ const dependencies = new Map();
147
+ for (const edge of graph.edges) {
148
+ if (edge.type !== 'DEPENDS_ON' || !claimById.has(edge.from) || !claimById.has(edge.to))
149
+ continue;
150
+ const ids = dependencies.get(edge.from) ?? [];
151
+ if (!ids.includes(edge.to))
152
+ ids.push(edge.to);
153
+ dependencies.set(edge.from, ids);
140
154
  }
141
- if (map.contradictions.length) {
142
- L.push('## The debate', '');
143
- for (const d of map.contradictions) {
144
- const adj = rulingById.get(d.id);
145
- const claim = d.claim_ids.map((id) => claimById.get(id)?.statement ?? id).join(' / ');
146
- const claimants = [...new Set(d.claim_ids.flatMap((id) => claimById.get(id)?.providers ?? []))];
147
- const attackers = [...new Set(d.attacks.map((a) => a.provider))];
148
- L.push(`- ${attrib(claimants)} claimed ${claim}. ${attrib(attackers)} countered: ${d.attacks.map((a) => a.argument).join('; ')}. ` +
149
- `Chair: ${rulingPhrase(adj?.ruling)}${adj?.reasoning ? ` — ${adj.reasoning}` : ''}.`);
155
+ const recommendationIds = (judgeReport.recommendation_claim_ids ?? [])
156
+ .filter((id) => claimById.has(id));
157
+ const anchors = recommendationIds.length
158
+ ? recommendationIds
159
+ : graph.claims.filter((claim) => claim.load_bearing).slice(0, 8).map((claim) => claim.id);
160
+ const chainIds = [];
161
+ const seen = new Set();
162
+ const visit = (id) => {
163
+ if (seen.has(id))
164
+ return;
165
+ seen.add(id);
166
+ for (const dependency of dependencies.get(id) ?? [])
167
+ visit(dependency);
168
+ chainIds.push(id);
169
+ };
170
+ anchors.forEach(visit);
171
+ const claimChain = chainIds.flatMap((id) => {
172
+ const claim = reportClaimById.get(id);
173
+ return claim ? [{
174
+ claimId: id,
175
+ text: claim.text,
176
+ ruling: claim.ruling,
177
+ evidenceStatus: verificationById.get(id) ?? claim.verification,
178
+ dependsOn: dependencies.get(id) ?? [],
179
+ }] : [];
180
+ });
181
+ const evidence = graph.evidence.map((item) => {
182
+ const claimIds = graph.edges
183
+ .filter((edge) => edge.from === item.id && (edge.type === 'SUPPORTS' || edge.type === 'ATTACKS') && claimById.has(edge.to))
184
+ .map((edge) => edge.to);
185
+ const statuses = claimIds.map((id) => verificationById.get(id)).filter((status) => Boolean(status));
186
+ const verificationStatus = statuses.includes('CONTRADICTED') ? 'CONTRADICTED'
187
+ : statuses.length > 0 && statuses.every((status) => status === 'VERIFIED') ? 'VERIFIED'
188
+ : statuses.includes('UNVERIFIABLE') ? 'UNVERIFIABLE'
189
+ : statuses.length ? 'PARTIAL' : 'NOT_CHECKED';
190
+ return {
191
+ id: item.id,
192
+ source: item.locator ?? (item.source_kind === 'MODEL_KNOWLEDGE' ? `${disp(item.provider)} model knowledge` : item.source_kind),
193
+ sourceKind: item.source_kind,
194
+ date: item.accessed_at ?? 'Not recorded',
195
+ freshness: item.freshness,
196
+ verificationStatus,
197
+ claimIds,
198
+ };
199
+ });
200
+ const positionChanges = (rebuttals?.events ?? []).map((event) => ({
201
+ eventId: event.id,
202
+ claimId: event.claim_id,
203
+ responder: event.responder,
204
+ response: event.response,
205
+ reasoning: event.reasoning,
206
+ evidenceIds: event.evidence_ids,
207
+ ...(event.narrowed_proposition ? { narrowedProposition: event.narrowed_proposition } : {}),
208
+ }));
209
+ const claimProviders = (claimId) => {
210
+ const claim = claimById.get(claimId);
211
+ if (!claim)
212
+ return [];
213
+ return [...new Set(claim.position_ids.map((id) => positionById.get(id).provider))];
214
+ };
215
+ const sharedConcerns = graph.claims.filter((claim) => claim.state === 'SHARED_CONCERN').map((claim) => ({
216
+ claimId: claim.id,
217
+ text: claim.proposition,
218
+ providerIds: claimProviders(claim.id),
219
+ evidenceStatus: verificationById.get(claim.id) ?? VERIFICATION_MAP[claim.evidence_state],
220
+ }));
221
+ const uniqueSupportedInsights = graph.claims.filter((claim) => claim.state === 'UNIQUE' && claim.evidence_state === 'SUPPORTED').flatMap((claim) => {
222
+ const providerId = claimProviders(claim.id)[0];
223
+ return providerId ? [{
224
+ claimId: claim.id,
225
+ text: claim.proposition,
226
+ providerId,
227
+ verificationStatus: verificationById.get(claim.id) ?? 'NOT_CHECKED',
228
+ }] : [];
229
+ });
230
+ const claimByPosition = new Map(graph.claims.flatMap((claim) => claim.position_ids.map((id) => [id, claim.id])));
231
+ const missingCoverage = new Set(graph.holes.coverage.map((hole) => hole.dimension_id));
232
+ const missingEvidence = new Set(graph.holes.evidence.map((hole) => hole.claim_id));
233
+ const coverage = args.rubric.map((dimension) => {
234
+ const claimIds = [...new Set(graph.positions
235
+ .filter((position) => position.dimension_id === dimension.id)
236
+ .map((position) => claimByPosition.get(position.id))
237
+ .filter((id) => Boolean(id)))];
238
+ const notApplicable = seats.some((seat) => seat.output.coverage.find((entry) => entry.dimension_id === dimension.id)?.status === 'NOT_APPLICABLE');
239
+ const status = missingCoverage.has(dimension.id) ? 'MISSING'
240
+ : claimIds.some((id) => missingEvidence.has(id)) ? 'MISSING_EVIDENCE'
241
+ : notApplicable && claimIds.length === 0 ? 'NOT_APPLICABLE' : 'COVERED';
242
+ return { dimensionId: dimension.id, label: dimension.label, status, claimIds };
243
+ });
244
+ const adjudicationById = new Map(judgeReport.adjudications.map((item) => [item.id, item]));
245
+ const sensitivity = graph.claims
246
+ .filter((claim) => claim.load_bearing && claim.sensitivity !== 'LOW')
247
+ .sort((a, b) => (a.sensitivity === 'DECISIVE' ? 0 : 1) - (b.sensitivity === 'DECISIVE' ? 0 : 1))
248
+ .map((claim) => {
249
+ const related = graph.edges
250
+ .filter((edge) => (edge.from === claim.id || edge.to === claim.id) && claimById.has(edge.from) && claimById.has(edge.to))
251
+ .map((edge) => edge.from === claim.id ? edge.to : edge.from);
252
+ const experiment = actionPlan && !('kind' in actionPlan)
253
+ ? actionPlan.actions.find((action) => action.validates === claim.id) : undefined;
254
+ const evidenceHole = graph.holes.evidence.find((hole) => hole.claim_id === claim.id);
255
+ return {
256
+ claimId: claim.id,
257
+ fact: claim.proposition,
258
+ sensitivity: claim.sensitivity,
259
+ impactIfFalse: claim.if_false,
260
+ whatWouldChangeIt: adjudicationById.get(claim.id)?.what_would_change_it
261
+ ?? experiment?.kill_signal
262
+ ?? evidenceHole?.reason
263
+ ?? `Resolve the evidence status for ${claim.id}.`,
264
+ linkedClaimIds: [...new Set(related)],
265
+ };
266
+ });
267
+ const experiments = actionPlan && !('kind' in actionPlan)
268
+ ? {
269
+ status: 'AVAILABLE',
270
+ note: actionPlan.sequencing_note,
271
+ actions: actionPlan.actions.map((action) => ({
272
+ order: action.order,
273
+ action: action.action,
274
+ why: action.why,
275
+ validates: action.validates,
276
+ effort: action.effort,
277
+ killSignal: action.kill_signal,
278
+ })),
150
279
  }
151
- L.push('');
152
- }
153
- if (actionPlan) {
154
- L.push('## Validation plan', '', '| # | Action | Why | Validates | Effort | Kill signal |', '|---|---|---|---|---|---|');
155
- for (const a of actionPlan.actions) {
156
- L.push(`| ${a.order} | ${mdCell(a.action)} | ${mdCell(a.why)} | ${mdCell(a.validates)} | ${a.effort} | ${mdCell(a.kill_signal)} |`);
280
+ : {
281
+ status: 'DEGRADED',
282
+ note: actionPlan && 'kind' in actionPlan
283
+ ? `Planner unavailable: ${actionPlan.reason}. Unresolved: ${actionPlan.unresolved_questions.join('; ')}`
284
+ : 'No planner artifact was recorded.',
285
+ actions: [],
286
+ };
287
+ const counter = judgeReport.strongest_counter_case;
288
+ const contributions = args.models.map((model) => ({
289
+ provider: model.provider,
290
+ name: model.name,
291
+ verifiedUniqueClaimIds: graph.claims
292
+ .filter((claim) => claim.state === 'UNIQUE' && verifiedClaimIds.has(claim.id) && claimProviders(claim.id).includes(model.provider))
293
+ .map((claim) => claim.id),
294
+ }));
295
+ const technicalPositions = graph.positions.map((position) => ({
296
+ id: position.id,
297
+ provider: position.provider,
298
+ stance: position.stance,
299
+ proposition: position.proposition,
300
+ evidenceIds: graph.evidence
301
+ .filter((evidence) => evidence.provider === position.provider
302
+ && evidence.source_id === position.source_id
303
+ && position.evidence_ids.some((id) => id === evidence.id || evidence.id.endsWith(`/${id}`)))
304
+ .map((evidence) => evidence.id),
305
+ }));
306
+ return {
307
+ recommendation: {
308
+ status: args.status,
309
+ summary: args.summary,
310
+ reason: args.reason,
311
+ claimIds: recommendationIds,
312
+ conditions: (judgeReport.conditions ?? []).map((text) => ({
313
+ text,
314
+ claimIds: (judgeReport.condition_claim_ids ?? []).filter((id) => claimById.has(id)),
315
+ })),
316
+ },
317
+ claimChain,
318
+ evidence,
319
+ positionChanges,
320
+ sharedConcerns,
321
+ uniqueSupportedInsights,
322
+ coverage,
323
+ sensitivity,
324
+ experiments,
325
+ counterCase: counter
326
+ ? { available: true, reasoning: counter.reasoning, claimIds: counter.claim_ids.filter((id) => claimById.has(id)) }
327
+ : { available: false, reasoning: 'No graph-anchored counter-case was recorded.', claimIds: [] },
328
+ contributions,
329
+ technical: {
330
+ submissions: seats.map((seat) => ({
331
+ provider: seat.provider,
332
+ name: disp(seat.provider),
333
+ strongestVersion: seat.output.strongest_version,
334
+ positionIds: graph.positions.filter((position) => position.provider === seat.provider).map((position) => position.id),
335
+ })),
336
+ positions: technicalPositions,
337
+ edges: graph.edges,
338
+ events: positionChanges,
339
+ },
340
+ };
341
+ }
342
+ /** Assemble the machine-readable report deterministically from the run's persisted artifacts. */
343
+ export function buildDecisionReport(ctx, args) {
344
+ const { contract, seats, graph, verifications, judgeReport, actionPlan } = args;
345
+ const mode = ctx.mode ?? 'council';
346
+ const flags = new Set(ctx.flags);
347
+ const confidence = computeConfidence(graph, flags);
348
+ const status = statusFrom(judgeReport);
349
+ const rulingById = new Map(judgeReport.adjudications.map((a) => [a.id, a]));
350
+ const positionById = new Map(graph.positions.map((position) => [position.id, position]));
351
+ const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
352
+ const openQuestions = mergeOpenQuestions(seats);
353
+ const claims = graph.claims.map((claim) => {
354
+ const stances = {};
355
+ for (const id of claim.position_ids) {
356
+ const position = positionById.get(id);
357
+ stances[position.provider] ??= STANCE_MAP[position.stance] ?? 'UNKNOWN';
157
358
  }
158
- L.push('', actionPlan.sequencing_note, '');
159
- }
160
- const questions = mergeOpenQuestions(seats);
161
- if (questions.length) {
162
- L.push('## Open questions that flip the verdict', '');
163
- for (const q of questions)
164
- L.push(`- ${q}`);
165
- L.push('');
166
- }
167
- L.push('## Red-team note', '');
168
- for (const d of judgeReport.dissent)
169
- L.push(`- ${d}`);
170
- L.push('', `_Confidence: ${judgeReport.confidence_notes}_`, '');
171
- L.push('## Strongest case (per analyst)', '');
172
- for (const seat of seats)
173
- L.push(`- **${disp(seat.provider)}:** ${seat.output.strongest_version}`);
174
- L.push('');
175
- L.push('## Disagreement map', '');
176
- L.push(`**Consensus (≥2 analysts):** ${map.consensus.length}`);
177
- for (const c of map.consensus)
178
- L.push(`- ${c.statement} _(${attrib(c.providers)})_`);
179
- L.push('', `**Unique (one analyst):** ${map.unique.length}`);
180
- for (const c of map.unique)
181
- L.push(`- ${c.statement} _(${attrib(c.providers)})_`);
182
- L.push('', `**Contradictions:** ${map.contradictions.length}`);
183
- for (const d of map.contradictions) {
184
- const adj = rulingById.get(d.id);
185
- L.push(`- **${d.id}** ${adj ? `→ ${adj.ruling}` : ''}: ${d.attacks.map((a) => a.argument).join('; ')}`);
186
- }
187
- if (map.blind_spots.length) {
188
- L.push('', `**Blind spots (rubric items no analyst addressed):**`);
189
- for (const b of map.blind_spots)
190
- L.push(`- ${b}`);
359
+ return {
360
+ id: claim.id,
361
+ text: claim.proposition,
362
+ stances,
363
+ verification: VERIFICATION_MAP[claim.evidence_state],
364
+ ruling: claimRuling(claim, rulingById.get(claim.id)),
365
+ loadBearing: claim.load_bearing,
366
+ sensitivity: claim.sensitivity,
367
+ };
368
+ });
369
+ const disagreements = graph.claims.filter((claim) => claim.state === 'DISAGREEMENT').map((claim) => {
370
+ const positions = claim.position_ids.map((id) => positionById.get(id));
371
+ const sides = ['SUPPORT', 'OPPOSE', 'MIXED'].flatMap((stance) => {
372
+ const inStance = positions.filter((position) => position.stance === stance);
373
+ return inStance.length
374
+ ? [{ stance, providers: inStance.map((position) => disp(position.provider)), reasoning: inStance.map((position) => position.reasoning) }]
375
+ : [];
376
+ });
377
+ const adjudication = rulingById.get(claim.id);
378
+ return {
379
+ id: claim.id,
380
+ topic: claim.proposition,
381
+ sides,
382
+ ruling: rulingPhrase(adjudication?.ruling),
383
+ reasoning: adjudication?.reasoning ?? null,
384
+ status: (adjudication && adjudication.ruling !== 'UNRESOLVED' ? 'RESOLVED' : 'UNRESOLVED'),
385
+ };
386
+ });
387
+ const uniqueOppositions = graph.claims
388
+ .filter((claim) => claim.state === 'UNIQUE')
389
+ .flatMap((claim) => claim.position_ids.map((id) => positionById.get(id)))
390
+ .filter((position) => position.stance === 'OPPOSE')
391
+ .map((position) => ({ provider: disp(position.provider), proposition: position.proposition }));
392
+ const unresolvedDecisive = disagreements.some((d) => d.status === 'UNRESOLVED' && claimById.get(d.id)?.sensitivity === 'DECISIVE');
393
+ const consensusType = mode === 'quick' ? 'single_analyst' : disagreements.length === 0
394
+ ? (claims.some((claim) => claim.ruling === 'UNRESOLVED') ? 'convergent_with_unresolved_claims' : 'unanimous')
395
+ : disagreements.every((d) => d.status === 'RESOLVED') ? 'majority_with_dissent' : 'contested';
396
+ const risks = graph.claims
397
+ .filter((claim) => claim.load_bearing && claim.evidence_state !== 'SUPPORTED')
398
+ .map((claim) => ({ risk: claim.proposition, severity: SEVERITY_MAP[claim.sensitivity] ?? 'Medium' }));
399
+ // Frame the slot as what it is — a decisive assumption that is NOT proven. Echoing the bare
400
+ // proposition inverts the meaning when the claim is phrased affirmatively (run 20260714-2321:
401
+ // "a cutover can preserve compliance" read as reassurance, the opposite of the verdict).
402
+ const criticalClaim = graph.claims.find((claim) => claim.load_bearing && claim.if_false === 'STOP' && claim.evidence_state !== 'SUPPORTED');
403
+ const criticalWarning = criticalClaim
404
+ ? `Unverified decisive assumption (if false, STOP): ${criticalClaim.proposition} — evidence ${criticalClaim.evidence_state}.`
405
+ : null;
406
+ const verificationResults = verifications.verifications.map((v) => {
407
+ if ('claim_id' in v) {
408
+ return {
409
+ claimId: v.claim_id,
410
+ claim: claimById.get(v.claim_id)?.proposition ?? v.claim_id,
411
+ method: 'independent verifier model',
412
+ verdict: (v.status === 'VERIFIED' ? 'CONFIRMED' : v.status === 'CONTRADICTED' ? 'REFUTED' : 'UNCERTAIN'),
413
+ evidence: v.evidence_ids.length ? v.evidence_ids.join(', ') : v.missing_evidence.join('; '),
414
+ note: v.reasoning,
415
+ };
416
+ }
417
+ return {
418
+ claimId: v.target_id,
419
+ claim: claimById.get(v.target_id)?.proposition ?? v.target_id,
420
+ method: 'independent verifier model',
421
+ verdict: (v.verdict === 'CONFIRM' ? 'CONFIRMED' : v.verdict === 'REFUTE' ? 'REFUTED' : 'UNCERTAIN'),
422
+ evidence: v.evidence,
423
+ note: v.note,
424
+ };
425
+ });
426
+ const byProvider = {};
427
+ let modelTimeMs = 0;
428
+ for (const call of ctx.calls) {
429
+ byProvider[call.provider] = (byProvider[call.provider] ?? 0) + 1;
430
+ modelTimeMs += call.durationMs;
191
431
  }
192
- L.push('');
193
- L.push('## Receipt', '', ...receiptLines(ctx), '');
194
- return L.join('\n');
432
+ const roles = ctx.roles;
433
+ const reportProviders = mode === 'quick' ? [...new Set(seats.map((seat) => seat.provider))] : ctx.available();
434
+ const models = reportProviders.map((provider) => ({
435
+ provider,
436
+ name: disp(provider),
437
+ roles: mode === 'quick' ? ['single analyst'] : [
438
+ ...(roles.analyst === provider ? ['analyst'] : []),
439
+ ...(roles.judge === provider ? ['judge'] : []),
440
+ ...(roles.verifier === provider ? ['verifier'] : []),
441
+ ...(roles.s4.includes(provider) ? ['scout'] : []),
442
+ ],
443
+ }));
444
+ const positions = seats.map((seat) => {
445
+ const seatPositions = seat.output.positions;
446
+ const oppose = seatPositions.filter((position) => position.stance === 'OPPOSE').length;
447
+ const support = seatPositions.filter((position) => position.stance === 'SUPPORT').length;
448
+ return {
449
+ provider: seat.provider,
450
+ name: disp(seat.provider),
451
+ initialConclusion: seat.output.strongest_version,
452
+ mainArgument: seatPositions.find((position) => position.load_bearing)?.reasoning ?? seatPositions[0]?.reasoning ?? '—',
453
+ keyRisk: seatPositions.find((position) => position.stance === 'OPPOSE')?.proposition ?? '—',
454
+ finalPosition: (oppose > support ? 'Oppose' : oppose > 0 || status === 'ACCEPTED_WITH_CONDITIONS' ? 'Conditional' : 'Support'),
455
+ };
456
+ });
457
+ const dossier = buildDossier({
458
+ status,
459
+ summary: judgeReport.verdict,
460
+ reason: judgeReport.key_points?.[0] ?? judgeReport.verdict,
461
+ claims,
462
+ models,
463
+ seats,
464
+ graph,
465
+ verifications,
466
+ judgeReport,
467
+ actionPlan,
468
+ rebuttals: args.rebuttals,
469
+ rubric: args.rubric ?? [],
470
+ });
471
+ const decisionSnapshot = judgeReport.decision_snapshot ? {
472
+ decisiveNumbers: judgeReport.decision_snapshot.decisive_numbers.map((item) => ({
473
+ label: item.label,
474
+ value: item.value,
475
+ meaning: item.meaning,
476
+ claimIds: item.claim_ids,
477
+ })),
478
+ ...(judgeReport.decision_snapshot.payback ? {
479
+ payback: {
480
+ status: judgeReport.decision_snapshot.payback.status,
481
+ result: judgeReport.decision_snapshot.payback.result,
482
+ basis: judgeReport.decision_snapshot.payback.basis,
483
+ claimIds: judgeReport.decision_snapshot.payback.claim_ids,
484
+ },
485
+ } : {}),
486
+ options: judgeReport.decision_snapshot.options.map((option) => ({
487
+ label: option.label,
488
+ commitment: option.commitment,
489
+ commitmentKind: option.commitment_kind,
490
+ tradeoff: option.tradeoff,
491
+ claimIds: option.claim_ids,
492
+ })),
493
+ ...(judgeReport.decision_snapshot.tripwire ? {
494
+ tripwire: {
495
+ metric: judgeReport.decision_snapshot.tripwire.metric,
496
+ threshold: judgeReport.decision_snapshot.tripwire.threshold,
497
+ decisionRule: judgeReport.decision_snapshot.tripwire.decision_rule,
498
+ claimIds: judgeReport.decision_snapshot.tripwire.claim_ids,
499
+ },
500
+ } : {}),
501
+ } : undefined;
502
+ return {
503
+ reportId: ctx.runId,
504
+ generatedAt: new Date().toISOString(),
505
+ mode,
506
+ task: {
507
+ original: args.original ?? contract.task,
508
+ normalized: contract.task,
509
+ type: contract.task_type,
510
+ constraints: contract.constraints,
511
+ successCriteria: contract.success_criteria,
512
+ ...('confirmation' in contract && typeof contract.confirmation === 'string' ? { confirmation: contract.confirmation } : {}),
513
+ },
514
+ verdict: {
515
+ status,
516
+ summary: judgeReport.verdict,
517
+ confidence: confidence.score / 100,
518
+ confidenceLabel: confidence.label,
519
+ consensusType,
520
+ conditions: judgeReport.conditions ?? [],
521
+ primaryReason: judgeReport.key_points?.[0] ?? judgeReport.verdict,
522
+ criticalWarning,
523
+ },
524
+ keyFindings: (judgeReport.key_points?.length ? judgeReport.key_points : [judgeReport.verdict]).slice(0, 4),
525
+ criticalUnknowns: openQuestions.slice(0, 3),
526
+ ...(decisionSnapshot ? { decisionSnapshot } : {}),
527
+ confidenceBreakdown: confidence,
528
+ models,
529
+ positions,
530
+ claims,
531
+ consensusSummary: {
532
+ unanimous: graph.claims.filter((claim) => claim.state === 'CONSENSUS').length,
533
+ accepted: claims.filter((claim) => claim.ruling === 'ACCEPTED').length,
534
+ conditional: claims.filter((claim) => claim.ruling === 'CONDITIONAL').length,
535
+ unresolved: claims.filter((claim) => claim.ruling === 'UNRESOLVED').length,
536
+ rejected: claims.filter((claim) => claim.ruling === 'REJECTED').length,
537
+ },
538
+ disagreements,
539
+ minority: {
540
+ dissent: judgeReport.dissent,
541
+ uniqueOppositions,
542
+ blocksDecision: unresolvedDecisive ? 'YES' : (judgeReport.conditions?.length ? 'ONLY_IF_CONDITION' : 'NO'),
543
+ },
544
+ verification: {
545
+ results: verificationResults,
546
+ confirmed: verificationResults.filter((result) => result.verdict === 'CONFIRMED').length,
547
+ refuted: verificationResults.filter((result) => result.verdict === 'REFUTED').length,
548
+ uncertain: verificationResults.filter((result) => result.verdict === 'UNCERTAIN').length,
549
+ },
550
+ risks,
551
+ recommendedActions: actionPlan && !('kind' in actionPlan)
552
+ ? actionPlan.actions.map((action) => ({ order: action.order, action: action.action, why: action.why, effort: action.effort, killSignal: action.kill_signal }))
553
+ : [],
554
+ openQuestions,
555
+ flags: [...ctx.flags],
556
+ receipt: { calls: ctx.calls.length, budget: ctx.budget.limit, byProvider, modelTimeMs, categories: receiptCategories(ctx) },
557
+ dossier,
558
+ };
559
+ }
560
+ const pct = (n) => `${Math.round(n * 100)}%`;
561
+ export { renderDecisionDossierMarkdown };
562
+ export function renderReport(ctx, args) {
563
+ return renderDecisionDossierMarkdown(buildDecisionReport(ctx, args));
564
+ }
565
+ // ── Terminal summary (level 1) ───────────────────────────────────────────────
566
+ const RULE = '─'.repeat(52);
567
+ export function renderTerminalSummary(report, paths) {
568
+ const summary = report.consensusSummary;
569
+ const c = report.confidenceBreakdown;
570
+ const snapshot = report.decisionSnapshot;
571
+ const decisiveLines = snapshot ? [
572
+ 'Decision numbers:',
573
+ ...snapshot.decisiveNumbers.slice(0, 3).map((item) => `${item.label}: ${item.value} — ${item.meaning}`),
574
+ ...(snapshot.payback ? [`Payback (${snapshot.payback.status.replaceAll('_', ' ').toLowerCase()}): ${snapshot.payback.result}`] : []),
575
+ `Options: ${snapshot.options.map((option) => `${option.label} — ${option.commitment} (${option.commitmentKind.replace('_', ' ').toLowerCase()})`).join(' · ')}`,
576
+ ] : ['Decisive result:', report.verdict.primaryReason];
577
+ const checks = [
578
+ `[${c.verificationCoverage >= 0.5 ? 'PASS' : 'WARN'}] Verification coverage ${pct(c.verificationCoverage)} of load-bearing claims`,
579
+ `[PASS] Minority opinion preserved (${report.minority.dissent.length} dissent item(s))`,
580
+ `[${report.verification.refuted === 0 ? 'PASS' : 'WARN'}] Verifier refutations: ${report.verification.refuted}`,
581
+ ...(report.flags.length ? [`[WARN] Degradation flags: ${report.flags.join(', ')}`] : []),
582
+ `[WARN] Structural score ${c.score}/100 is heuristic — not yet benchmark-calibrated`,
583
+ ];
584
+ return [
585
+ report.mode === 'quick' ? 'SINGLE-MODEL DECISION REPORT' : 'MULTI-MODEL DECISION REPORT',
586
+ RULE,
587
+ `Verdict: ${report.verdict.summary}`,
588
+ `Decision state: ${report.verdict.status}`,
589
+ `Evidence coverage: ${pct(c.verificationCoverage)} of load-bearing claims independently verified`,
590
+ 'Coverage note: unchecked inputs remain; this is not a probability of correctness.',
591
+ `${report.mode === 'quick' ? 'Claims' : 'Consensus'}: ${report.verdict.consensusType.replaceAll('_', ' ')} — ${summary.accepted} accepted · ${summary.rejected} rejected · ${summary.unresolved} unresolved`,
592
+ '',
593
+ ...decisiveLines,
594
+ '',
595
+ ...(report.verdict.criticalWarning ? ['Critical warning:', report.verdict.criticalWarning, ''] : []),
596
+ ...(report.minority.dissent[0] ? ['Critical dissent:', report.minority.dissent[0], ''] : []),
597
+ 'Verification:',
598
+ ...checks,
599
+ '',
600
+ ...(report.recommendedActions[0] ? ['Recommended action:', report.recommendedActions[0].action, ''] : []),
601
+ ...(snapshot?.tripwire ? ['Go/no-go tripwire:', `${snapshot.tripwire.metric}: ${snapshot.tripwire.threshold} — ${snapshot.tripwire.decisionRule}`, ''] : []),
602
+ `Full report: ${paths.markdownPath}`,
603
+ `Audit JSON: ${paths.jsonPath}`,
604
+ ].join('\n');
195
605
  }
196
606
  export async function s10Render(ctx, args) {
197
- await ctx.writer.writeText('final-report', renderReport(ctx, args));
607
+ const report = buildDecisionReport(ctx, args);
608
+ await ctx.writer.writeJson('decision-report', report);
609
+ await ctx.writer.writeText('final-report', renderDecisionDossierMarkdown(report));
198
610
  }