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,136 +1,329 @@
1
- // S9 — judge synthesis (§9, §13). The judge (claude) adjudicates DISPUTED items only; consensus is
2
- // already settled (S7 grouping) and passes through untouched. Two guards beyond the schema:
3
- // 1. Anti-blending (§260, the §602 acceptance test): the judge may reference ONLY disputed ids in
4
- // `adjudications` never a consensus id. `adjudicationScopeViolations` (pure) detects this; the
5
- // stage re-asks once, then filters any still-invalid adjudications out and flags the run.
6
- // 2. Mandatory dissent (§260): empty dissent → re-ask once → else flag `synthesis_suspect` and
7
- // inject a placeholder so the report still renders (continue, not abort).
8
- // Confidence is NOT taken from the judge here — the held/failed/unverified audit + HIGH/MED/LOW labels
9
- // are derived deterministically at S10 (§624: confidence from cross-model confirmation, not self-report).
10
- import { JudgeReportModel } from '../../schemas/index.js';
11
- import { isFatal } from '../context.js';
1
+ // S9 — chair adjudication over graph-selected escalations. Consensus and shared concerns are
2
+ // read-only context; anonymous position text and the verifier record cross the boundary unchanged.
3
+ import { selectEscalations } from '../decision-graph.js';
4
+ import { IdeaChairReportModel } from '../../schemas/index.js';
5
+ import { isFatal, StageError } from '../context.js';
12
6
  import { jsonCall } from '../jsonStage.js';
13
- const S9_PROMPT = `ROLE: Judge. You adjudicate ONLY the disputed items below. Consensus items are already
14
- settled; do not restate, edit, re-litigate, or reference them in your adjudications.
15
- Apply this rubric strictly: {{RUBRIC_JSON}}
16
-
17
- You are the CHAIRMAN of the panel. Write for a decision-maker who did not see the debate — be clear,
18
- specific, and professional. No hedging mush, no restating the question back.
7
+ import { claimVerificationRefIssues } from './s8-verify.js';
8
+ const EMPTY_REBUTTALS = {
9
+ round: 1,
10
+ selected_claim_ids: [],
11
+ events: [],
12
+ stop_reason: 'NO_ESCALATIONS',
13
+ };
14
+ const S9_PROMPT = `ROLE: Judge. Adjudicate ONLY the anonymous escalated claim IDs below. Settled claims
15
+ and shared concerns are read-only context; do not re-litigate them. A context node tagged
16
+ UNRESOLVED_SELF_AUTHORED may not be adjudicated or carry the recommendation. Apply this rubric:
17
+ {{RUBRIC_JSON}}
19
18
 
20
19
  Output ONLY JSON matching the judge schema:
21
- - adjudications: for EACH disputed id → {id, ruling: UPHOLD|REJECT|UNRESOLVED, reasoning ≤3 sentences, evidence_cited}.
22
- UPHOLD = the argument defeats the claim; REJECT = the claim survives; UNRESOLVED = genuinely undecided.
23
- - verdict: 2-5 sentences — your clear recommendation (proceed / proceed-if / don't) and the single most
24
- important reason. Grounded ONLY in adjudicated + consensus claims.
25
- - recommendation: one of PROCEED, PROCEED_WITH_CONDITIONS, PIVOT, STOP.
26
- PIVOT = the core idea is unsound but an adjacent version may work. STOP = load-bearing assumptions failed
27
- with no credible repair. PROCEED_WITH_CONDITIONS means the idea is viable only if specific checks pass.
28
- - conditions: required ONLY for PROCEED_WITH_CONDITIONS; each condition must be checkable, not a vibe.
29
- - key_points: 4-8 bullets the reasoning a decision-maker needs, in plain language. Cover: what decided
30
- it, the decisive trade-offs, where the analysts DISAGREED and whose side you took and why, and the one
31
- thing that would most change the verdict. Each bullet a full standalone point, not a fragment.
32
- - dissent: 1 item the strongest argument AGAINST your own verdict. Empty dissent is invalid.
33
- - confidence_notes: which conclusions are HIGH/MEDIUM/LOW and why.
34
- DISPUTED ITEMS + VERIFICATION: {{DISPUTES_JSON}}
35
- CONSENSUS (context only, read-only): {{CONSENSUS_JSON}}`;
36
- /** Pure anti-blending validator: adjudication ids that are NOT disputed items (the judge trying to
37
- * touch consensus). Empty = clean. This is the heart of the §602 "rejects consensus edits" test. */
38
- export function adjudicationScopeViolations(report, disputeIds) {
39
- const allowed = new Set(disputeIds);
40
- return report.adjudications.map((a) => a.id).filter((id) => !allowed.has(id));
20
+ - adjudications: each escalated id with valid verification evidence → {claim_id, ruling:
21
+ HOLDS|FAILS|UNRESOLVED, reasoning ≤3 sentences, evidence_ids: [IDs from that claim's verification],
22
+ effect_on_decision, and what_would_change_it when UNRESOLVED}.
23
+ Omit claims with no evidence IDs; they remain unresolved. Never emit evidence_cited prose.
24
+ - verdict: a clear 2-5 sentence recommendation grounded in adjudicated and settled claims.
25
+ - recommendation: PROCEED, PROCEED_WITH_CONDITIONS, PIVOT, or STOP.
26
+ - conditions: required only for PROCEED_WITH_CONDITIONS; a JSON array of AT MOST 6 checkable strings
27
+ merge related checks rather than exceeding 6.
28
+ - recommendation_claim_ids: 1-8 graph claim IDs carrying the verdict's load-bearing reasons.
29
+ - condition_claim_ids: required only for PROCEED_WITH_CONDITIONS; 1-8 graph IDs anchoring its conditions.
30
+ - pivot: required only for PIVOT; {changed_claim_id, new_risk_claim_id}, both existing graph IDs.
31
+ - strongest_counter_case: {claim_ids (1-4), reasoning}; it must argue against the verdict from the same graph.
32
+ - decision_snapshot: include this for decisions involving money, rates, quantities, or numeric thresholds:
33
+ {decisive_numbers:[{label,value,meaning,claim_ids}], payback?:{status:ACHIEVED|NOT_ACHIEVED|NOT_COMPUTABLE,
34
+ result,basis,claim_ids}, options:[{label,commitment,commitment_kind:KNOWN|TARGET_CAP|UNKNOWN,
35
+ tradeoff,claim_ids}], tripwire?:{metric,threshold,decision_rule,claim_ids}}.
36
+ Every numeric statement must already exist in the graph's evidence or calculation records and cite its graph claim IDs.
37
+ Never invent arithmetic. Keep operating break-even distinct from capital-payback targets. A proposed budget or cap is
38
+ TARGET_CAP, not a known cost; use UNKNOWN when an option's commitment was not established.
39
+ - key_points: 4-8 standalone decision-relevant bullets.
40
+ - dissent: a JSON array of strings (an array even when there is only one) — the strongest arguments
41
+ against your verdict.
42
+ - confidence_notes: explain calibrated confidence.
43
+ ESCALATED CLAIMS + VERIFICATION: {{ESCALATIONS_JSON}}
44
+ APPEND-ONLY REBUTTAL EVENTS: {{REBUTTALS_JSON}}
45
+ SETTLED/UNRESOLVED CONTEXT: {{CONTEXT_JSON}}`;
46
+ export function adjudicationScopeViolations(report, ids) {
47
+ const allowed = new Set(ids);
48
+ return report.adjudications.map((item) => item.id).filter((id) => !allowed.has(id));
41
49
  }
42
- /** Idea S9 semantic guard: the model-facing schema permits missing recommendation fields so S9 can
43
- * re-ask once instead of losing a usable judge report to one absent enum. */
44
50
  export function recommendationIssues(report) {
45
51
  const issues = [];
46
52
  const hasConditions = (report.conditions?.length ?? 0) > 0;
47
53
  if (!report.recommendation)
48
54
  issues.push('recommendation is required');
49
- if (report.recommendation === 'PROCEED_WITH_CONDITIONS' && !hasConditions) {
55
+ if (report.recommendation === 'PROCEED_WITH_CONDITIONS' && !hasConditions)
50
56
  issues.push('conditions are required for PROCEED_WITH_CONDITIONS');
51
- }
52
- if (hasConditions && report.recommendation !== 'PROCEED_WITH_CONDITIONS') {
57
+ if (hasConditions && report.recommendation !== 'PROCEED_WITH_CONDITIONS')
53
58
  issues.push('conditions are only valid with PROCEED_WITH_CONDITIONS');
54
- }
55
59
  return issues;
56
60
  }
57
- /** §272 2-provider demotion (pure). When the judge is also an S4 author (only happens with 2
58
- * providers), any adjudication on a dispute whose contested claim was authored SOLELY by the judge's
59
- * provider is forced to `UNRESOLVED` the judge may not confirm its own claim; it stays for the
60
- * human. A no-op with 3 providers, where the judge (claude) authored no S4 claim. */
61
- export function demoteSelfAuthored(adjudications, map, judgeProvider) {
62
- const claimProviders = new Map();
63
- for (const c of [...map.consensus, ...map.unique])
64
- claimProviders.set(c.id, c.providers);
65
- const disputeClaims = new Map(map.contradictions.map((d) => [d.id, d.claim_ids]));
66
- return adjudications.map((a) => {
67
- const cids = disputeClaims.get(a.id) ?? [];
68
- const soleJudge = cids.length > 0 && cids.every((cid) => {
69
- const ps = claimProviders.get(cid) ?? [];
70
- return ps.length === 1 && ps[0] === judgeProvider;
61
+ export function chairRecommendationIssues(report, graph, verifications) {
62
+ const issues = [];
63
+ const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
64
+ const recommendationIds = report.recommendation_claim_ids ?? [];
65
+ if (recommendationIds.length === 0)
66
+ issues.push('recommendation_claim_ids are required');
67
+ for (const id of recommendationIds)
68
+ if (!claimById.has(id))
69
+ issues.push(`unknown recommendation claim id: ${id}`);
70
+ if (recommendationIds.length > 0 && !recommendationIds.some((id) => claimById.get(id)?.load_bearing)) {
71
+ issues.push('recommendation must reference a load-bearing claim');
72
+ }
73
+ const counter = report.strongest_counter_case;
74
+ if (!counter)
75
+ issues.push('strongest counter-case is required');
76
+ for (const id of counter?.claim_ids ?? [])
77
+ if (!claimById.has(id))
78
+ issues.push(`unknown counter-case claim id: ${id}`);
79
+ const snapshot = report.decision_snapshot;
80
+ const snapshotIds = [
81
+ ...(snapshot?.decisive_numbers.flatMap((item) => item.claim_ids) ?? []),
82
+ ...(snapshot?.payback?.claim_ids ?? []),
83
+ ...(snapshot?.options.flatMap((item) => item.claim_ids) ?? []),
84
+ ...(snapshot?.tripwire?.claim_ids ?? []),
85
+ ];
86
+ for (const id of snapshotIds)
87
+ if (!claimById.has(id))
88
+ issues.push(`unknown decision snapshot claim id: ${id}`);
89
+ if (report.recommendation === 'PIVOT') {
90
+ if (!report.pivot)
91
+ issues.push('pivot claim links are required for PIVOT');
92
+ else {
93
+ if (!claimById.has(report.pivot.changed_claim_id))
94
+ issues.push(`unknown pivot changed claim id: ${report.pivot.changed_claim_id}`);
95
+ if (!claimById.has(report.pivot.new_risk_claim_id))
96
+ issues.push(`unknown pivot risk claim id: ${report.pivot.new_risk_claim_id}`);
97
+ if (report.pivot.changed_claim_id === report.pivot.new_risk_claim_id)
98
+ issues.push('pivot must name a distinct new risk claim');
99
+ }
100
+ }
101
+ else if (report.pivot) {
102
+ issues.push('pivot links are only valid for PIVOT');
103
+ }
104
+ const conditionIds = report.condition_claim_ids ?? [];
105
+ if (report.recommendation === 'PROCEED_WITH_CONDITIONS' && conditionIds.length === 0) {
106
+ issues.push('condition_claim_ids are required for PROCEED_WITH_CONDITIONS');
107
+ }
108
+ if (report.recommendation !== 'PROCEED_WITH_CONDITIONS' && conditionIds.length > 0) {
109
+ issues.push('condition_claim_ids are only valid for PROCEED_WITH_CONDITIONS');
110
+ }
111
+ for (const id of conditionIds)
112
+ if (!claimById.has(id))
113
+ issues.push(`unknown condition claim id: ${id}`);
114
+ if (report.recommendation === 'STOP') {
115
+ const verificationById = new Map(verifications.verifications.map((item) => [item.claim_id, item]));
116
+ const rulingById = new Map(report.adjudications.map((item) => [item.id, item.ruling]));
117
+ const positionById = new Map(graph.positions.map((position) => [position.id, position]));
118
+ const failed = recommendationIds.some((id) => {
119
+ const claim = claimById.get(id);
120
+ if (!claim?.load_bearing)
121
+ return false;
122
+ if (verificationById.get(id)?.status === 'CONTRADICTED')
123
+ return true;
124
+ if (claim.state === 'DISAGREEMENT')
125
+ return rulingById.get(id) === 'UPHOLD';
126
+ const stances = claim.position_ids.map((positionId) => positionById.get(positionId)?.stance);
127
+ return claim.state === 'SHARED_CONCERN' || (claim.state === 'UNIQUE' && stances.includes('OPPOSE'));
71
128
  });
72
- return soleJudge && a.ruling !== 'UNRESOLVED' ? { ...a, ruling: 'UNRESOLVED' } : a;
129
+ if (!failed)
130
+ issues.push('STOP requires a failed load-bearing claim');
131
+ }
132
+ return issues;
133
+ }
134
+ function adjudicationDetailViolations(report) {
135
+ return report.adjudications.flatMap((item) => [
136
+ ...(!item.effect_on_decision ? [`${item.id}: missing effect_on_decision`] : []),
137
+ ...(item.ruling === 'UNRESOLVED' && !item.what_would_change_it ? [`${item.id}: missing what_would_change_it`] : []),
138
+ ]);
139
+ }
140
+ export function adjudicationEvidenceViolations(report, verifications) {
141
+ const verificationById = new Map(verifications.verifications.map((item) => [item.claim_id, item]));
142
+ return report.adjudications.flatMap((item) => {
143
+ const verification = verificationById.get(item.id);
144
+ if (!verification)
145
+ return [`${item.id}: missing claim verification`];
146
+ if (!item.evidence_ids?.length)
147
+ return [`${item.id}: missing evidence_ids`];
148
+ const allowed = new Set(verification.evidence_ids);
149
+ const bad = item.evidence_ids.filter((id) => !allowed.has(id));
150
+ const unsettled = (verification.status === 'PARTIAL' || verification.status === 'UNVERIFIABLE') && item.ruling !== 'UNRESOLVED';
151
+ return [
152
+ ...(bad.length ? [`${item.id}: invalid evidence ids ${bad.join(', ')}`] : []),
153
+ ...(unsettled ? [`${item.id}: ${verification.status} verification requires UNRESOLVED`] : []),
154
+ ];
73
155
  });
74
156
  }
75
- const sevRank = (s) => (s === 'HIGH' ? 0 : s === 'MED' ? 1 : 2);
76
- function fallbackConditions(map, adjudications) {
77
- const claimById = new Map([...map.consensus, ...map.unique].map((c) => [c.id, c.statement]));
78
- const rulingById = new Map(adjudications.map((a) => [a.id, a.ruling]));
79
- const riskConditions = map.contradictions
80
- .filter((d) => rulingById.get(d.id) === 'UPHOLD')
81
- .sort((a, b) => Math.min(...a.attacks.map((x) => sevRank(x.severity))) - Math.min(...b.attacks.map((x) => sevRank(x.severity))))
82
- .map((d) => {
83
- const claim = d.claim_ids.map((id) => claimById.get(id) ?? id).join(' / ');
84
- return `Proceed only if you can validate that ${claim}.`;
157
+ export function demoteSelfAuthored(adjudications, graph, judgeProvider) {
158
+ const positionById = new Map(graph.positions.map((position) => [position.id, position]));
159
+ const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
160
+ return adjudications.map((adjudication) => {
161
+ const claim = claimById.get(adjudication.id);
162
+ const providers = new Set(claim?.position_ids.map((id) => positionById.get(id)?.provider).filter((provider) => provider !== undefined));
163
+ const judgeAuthored = providers.has(judgeProvider);
164
+ return judgeAuthored && adjudication.ruling !== 'UNRESOLVED'
165
+ ? { ...adjudication, ruling: 'UNRESOLVED' }
166
+ : adjudication;
85
167
  });
86
- const blindSpotConditions = map.blind_spots.map((b) => `Proceed only after examining the ${b} gap.`);
87
- return [...riskConditions, ...blindSpotConditions, 'Proceed only after one cheap test confirms the core user need.'].slice(0, 6);
88
168
  }
89
- export async function s9Judge(ctx, contract, map, verifications, rubric) {
90
- const claimById = new Map();
91
- for (const c of [...map.consensus, ...map.unique])
92
- claimById.set(c.id, c.statement);
93
- const verdictById = new Map(verifications.verifications.map((v) => [v.target_id, v.verdict]));
94
- const disputes = map.contradictions.map((d) => ({
95
- id: d.id,
96
- claim: d.claim_ids.map((cid) => claimById.get(cid) ?? cid).join(' / '),
97
- arguments_against: d.attacks.map((a) => a.argument),
98
- verifier_verdict: verdictById.get(d.id) ?? 'UNVERIFIED',
169
+ /** Exclude judge-authored nodes before prompt construction, not merely after a ruling is returned. */
170
+ export function adjudicableClaimIds(graph, ids, judgeProvider) {
171
+ const positionById = new Map(graph.positions.map((position) => [position.id, position]));
172
+ const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
173
+ return [...ids].filter((id) => !claimById.get(id)?.position_ids
174
+ .some((positionId) => positionById.get(positionId)?.provider === judgeProvider));
175
+ }
176
+ function fallbackConditions(graph, adjudications) {
177
+ const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
178
+ const upheld = adjudications
179
+ .filter((item) => item.ruling === 'UPHOLD')
180
+ .map((item) => `Proceed only if you can resolve: ${claimById.get(item.id)?.proposition ?? item.id}.`);
181
+ const holes = graph.holes.coverage.map((hole) => `Proceed only after examining the ${hole.label} gap.`);
182
+ const evidenceHoles = graph.holes.evidence.map((hole) => `Proceed only after resolving evidence gap ${hole.claim_id}: ${hole.reason}.`);
183
+ return [...upheld, ...holes, ...evidenceHoles, 'Proceed only after one cheap test confirms the core user need.'].slice(0, 6);
184
+ }
185
+ export function buildJudgePrompt(contract, graph, verifications, rubric, rebuttals = EMPTY_REBUTTALS, judgeProvider) {
186
+ return judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProvider).prompt;
187
+ }
188
+ function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProvider) {
189
+ const positionById = new Map(graph.positions.map((position) => [position.id, position]));
190
+ const verificationById = new Map(verifications.verifications.map((item) => [item.claim_id, item]));
191
+ const citedEvidence = [...new Set([
192
+ ...verifications.verifications.flatMap((item) => item.evidence_ids),
193
+ ...rebuttals.events.flatMap((item) => item.evidence_ids),
194
+ ])];
195
+ const evidenceRefs = new Map(citedEvidence.map((id, index) => [`E${index + 1}`, id]));
196
+ const aliasByEvidence = new Map([...evidenceRefs].map(([alias, id]) => [id, alias]));
197
+ const selectedIds = selectEscalations(graph, { max: 8 }).map((item) => item.claim_id);
198
+ const eligibleIds = judgeProvider ? adjudicableClaimIds(graph, selectedIds, judgeProvider) : selectedIds;
199
+ const escalationIds = new Set(eligibleIds);
200
+ const escalations = graph.claims.filter((claim) => escalationIds.has(claim.id)).map((claim) => {
201
+ const verification = verificationById.get(claim.id);
202
+ return {
203
+ id: claim.id,
204
+ proposition: claim.proposition,
205
+ positions: claim.position_ids.map((id) => {
206
+ const position = positionById.get(id);
207
+ return { stance: position.stance, reasoning: position.reasoning };
208
+ }),
209
+ verification: verification ? {
210
+ ...verification,
211
+ evidence_ids: verification.evidence_ids.map((id) => aliasByEvidence.get(id) ?? id),
212
+ } : {
213
+ claim_id: claim.id,
214
+ status: 'UNVERIFIABLE',
215
+ reasoning: 'No verifier record.',
216
+ evidence_ids: [],
217
+ missing_evidence: ['independent verification'],
218
+ },
219
+ };
220
+ });
221
+ const context = graph.claims.filter((claim) => !escalationIds.has(claim.id)).map((claim) => ({
222
+ id: claim.id,
223
+ proposition: claim.proposition,
224
+ state: claim.state,
225
+ evidence_state: claim.evidence_state,
226
+ evidence_hole: graph.holes.evidence.find((hole) => hole.claim_id === claim.id)?.reason,
227
+ ...(selectedIds.includes(claim.id) ? { adjudication: 'UNRESOLVED_SELF_AUTHORED' } : {}),
228
+ }));
229
+ const rebuttalContext = rebuttals.events.filter((event) => escalationIds.has(event.claim_id)).map((event) => ({
230
+ claim_id: event.claim_id,
231
+ response: event.response,
232
+ reasoning: event.reasoning,
233
+ evidence_ids: event.evidence_ids.map((id) => aliasByEvidence.get(id) ?? id),
234
+ ...(event.narrowed_proposition ? { narrowed_proposition: event.narrowed_proposition } : {}),
99
235
  }));
100
- const consensus = map.consensus.map((c) => ({ id: c.id, statement: c.statement }));
101
- const disputeIds = map.contradictions.map((d) => d.id);
102
- const basePrompt = S9_PROMPT.replace('{{RUBRIC_JSON}}', JSON.stringify(rubric.map((r) => r.label)))
103
- .replace('{{DISPUTES_JSON}}', JSON.stringify(disputes, null, 2))
104
- .replace('{{CONSENSUS_JSON}}', JSON.stringify(consensus, null, 2))
105
- // reference the contract task so the verdict stays anchored to what the user asked
236
+ const prompt = S9_PROMPT
237
+ .replace('{{RUBRIC_JSON}}', JSON.stringify(rubric.map((item) => item.label)))
238
+ .replace('{{ESCALATIONS_JSON}}', JSON.stringify(escalations, null, 2))
239
+ .replace('{{REBUTTALS_JSON}}', JSON.stringify(rebuttalContext, null, 2))
240
+ .replace('{{CONTEXT_JSON}}', JSON.stringify(context, null, 2))
106
241
  .concat(`\nTASK: ${contract.task}`);
242
+ return { prompt, evidenceRefs };
243
+ }
244
+ export async function s9Judge(ctx, contract, graph, verifications, rubric, rebuttals = EMPTY_REBUTTALS) {
245
+ const selectedIds = selectEscalations(graph, { max: 8 }).map((item) => item.claim_id);
246
+ const verificationIssues = claimVerificationRefIssues(graph, verifications, selectedIds);
247
+ if (verificationIssues.length)
248
+ throw new StageError('S9', 'BAD_OUTPUT', `invalid verification references: ${verificationIssues.join('; ')}`);
249
+ const ids = adjudicableClaimIds(graph, selectedIds, ctx.roles.judge);
250
+ const selfAuthored = new Set(selectedIds.filter((id) => !ids.includes(id)));
251
+ const input = judgeInput(contract, graph, verifications, rubric, rebuttals, ctx.roles.judge);
252
+ const basePrompt = input.prompt;
107
253
  const judge = ctx.handle(ctx.roles.judge);
108
- let report = await jsonCall(ctx, judge, 'S9', basePrompt, JudgeReportModel);
109
- // Semantic guards beyond the schema one targeted re-ask.
110
- let violations = adjudicationScopeViolations(report, disputeIds);
254
+ const positionById = new Map(graph.positions.map((position) => [position.id, position]));
255
+ const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
256
+ const legacyRuling = (claimId, ruling) => {
257
+ if (ruling === 'UNRESOLVED')
258
+ return 'UNRESOLVED';
259
+ const firstPosition = positionById.get(claimById.get(claimId)?.position_ids[0] ?? '');
260
+ const propositionIsObjection = firstPosition?.stance === 'OPPOSE';
261
+ if (ruling === 'HOLDS')
262
+ return propositionIsObjection ? 'UPHOLD' : 'REJECT';
263
+ return propositionIsObjection ? 'REJECT' : 'UPHOLD';
264
+ };
265
+ const translateChairReport = (value) => {
266
+ const { adjudications, ...report } = value;
267
+ return {
268
+ ...report,
269
+ adjudications: adjudications.map((item) => ({
270
+ id: item.claim_id,
271
+ // Legacy reports rule on the objection; translate relative to the preserved proposition stance.
272
+ ruling: legacyRuling(item.claim_id, item.ruling),
273
+ reasoning: item.reasoning,
274
+ evidence_ids: item.evidence_ids.map((id) => input.evidenceRefs.get(id) ?? id),
275
+ effect_on_decision: item.effect_on_decision,
276
+ what_would_change_it: item.what_would_change_it,
277
+ })),
278
+ };
279
+ };
280
+ // Preserve one call for the planner. A chair repair is allowed only when a third call remains.
281
+ let report = translateChairReport(await jsonCall(ctx, judge, 'S9', basePrompt, IdeaChairReportModel, {
282
+ repair: ctx.budget.limit - ctx.budget.used >= 3,
283
+ }));
284
+ let violations = adjudicationScopeViolations(report, ids);
285
+ let evidenceViolations = adjudicationEvidenceViolations(report, verifications);
111
286
  let recIssues = recommendationIssues(report);
112
- if (violations.length || report.dissent.length === 0 || recIssues.length) {
113
- const fix = `${basePrompt}\n\n---\nYour previous output had problems:\n` +
114
- (violations.length ? `- adjudications must reference ONLY these disputed ids [${disputeIds.join(', ')}]; not: ${violations.join(', ')}\n` : '') +
115
- (report.dissent.length === 0 ? `- dissent must contain at least one item.\n` : '') +
116
- (recIssues.length ? `- recommendation/conditions problem: ${recIssues.join('; ')}.\n` : '') +
117
- `Output ONLY the corrected JSON.`;
287
+ let chairIssues = [
288
+ ...chairRecommendationIssues(report, graph, verifications),
289
+ ...adjudicationDetailViolations(report),
290
+ ...(report.recommendation_claim_ids?.filter((id) => selfAuthored.has(id)).map((id) => `${id}: judge-authored claim cannot carry the recommendation`) ?? []),
291
+ ];
292
+ if ((violations.length || evidenceViolations.length || report.dissent.length === 0 || recIssues.length || chairIssues.length)
293
+ && ctx.budget.limit - ctx.budget.used > 1) {
294
+ const repair = `${basePrompt}\n\n---\nCorrect these problems:\n`
295
+ + (violations.length ? `- adjudications may reference only [${ids.join(', ')}], not ${violations.join(', ')}\n` : '')
296
+ + (evidenceViolations.length ? `- evidence reference errors: ${evidenceViolations.join('; ')}\n` : '')
297
+ + (report.dissent.length === 0 ? '- dissent must contain at least one item\n' : '')
298
+ + (recIssues.length ? `- ${recIssues.join('; ')}\n` : '')
299
+ + (chairIssues.length ? `- chair contract errors: ${chairIssues.join('; ')}\n` : '')
300
+ + 'Output ONLY corrected JSON.';
118
301
  try {
119
- report = await jsonCall(ctx, judge, 'S9-repair', fix, JudgeReportModel);
120
- violations = adjudicationScopeViolations(report, disputeIds);
302
+ report = translateChairReport(await jsonCall(ctx, judge, 'S9-repair', repair, IdeaChairReportModel, {
303
+ repair: ctx.budget.limit - ctx.budget.used > 2,
304
+ }));
305
+ violations = adjudicationScopeViolations(report, ids);
306
+ evidenceViolations = adjudicationEvidenceViolations(report, verifications);
121
307
  recIssues = recommendationIssues(report);
308
+ chairIssues = [
309
+ ...chairRecommendationIssues(report, graph, verifications),
310
+ ...adjudicationDetailViolations(report),
311
+ ...(report.recommendation_claim_ids?.filter((id) => selfAuthored.has(id)).map((id) => `${id}: judge-authored claim cannot carry the recommendation`) ?? []),
312
+ ];
122
313
  }
123
- catch (e) {
124
- if (isFatal(e))
125
- throw e; // keep the first report on a non-fatal repair failure
314
+ catch (error) {
315
+ if (isFatal(error))
316
+ throw error;
126
317
  }
127
318
  }
128
- // Enforce after the re-ask: drop any still-out-of-scope adjudications; guarantee non-empty dissent.
129
- const inScope = report.adjudications.filter((a) => new Set(disputeIds).has(a.id));
130
- if (inScope.length !== report.adjudications.length)
319
+ const allowed = new Set(ids);
320
+ const inScope = report.adjudications.filter((item) => allowed.has(item.id));
321
+ const evidenceValid = inScope.filter((item) => adjudicationEvidenceViolations({ adjudications: [item] }, verifications).length === 0)
322
+ .filter((item) => adjudicationDetailViolations({ ...report, adjudications: [item] }).length === 0)
323
+ .map(({ evidence_cited: _legacy, ...item }) => item);
324
+ if (evidenceValid.length !== report.adjudications.length)
131
325
  ctx.addFlag('synthesis_suspect');
132
- // §272: if the judge authored a contested claim (2-provider only), it can't confirm it → UNRESOLVED.
133
- const adjudications = demoteSelfAuthored(inScope, map, ctx.roles.judge);
326
+ const adjudications = demoteSelfAuthored(evidenceValid, graph, ctx.roles.judge);
134
327
  let dissent = report.dissent;
135
328
  if (dissent.length === 0) {
136
329
  ctx.addFlag('synthesis_suspect');
@@ -138,15 +331,43 @@ export async function s9Judge(ctx, contract, map, verifications, rubric) {
138
331
  }
139
332
  let recommendation = report.recommendation;
140
333
  let conditions = report.conditions;
141
- if (recIssues.length) {
334
+ let recommendationClaimIds = report.recommendation_claim_ids;
335
+ let conditionClaimIds = report.condition_claim_ids;
336
+ let pivot = report.pivot;
337
+ let strongestCounterCase = report.strongest_counter_case;
338
+ if (recIssues.length || evidenceViolations.length || chairIssues.length) {
142
339
  ctx.addFlag('synthesis_suspect');
143
340
  recommendation = 'PROCEED_WITH_CONDITIONS';
144
- conditions = fallbackConditions(map, adjudications);
341
+ conditions = fallbackConditions(graph, adjudications);
342
+ const anchors = graph.claims.filter((claim) => claim.load_bearing && !selfAuthored.has(claim.id)).slice(0, 6).map((claim) => claim.id);
343
+ recommendationClaimIds = anchors.length ? anchors : undefined;
344
+ conditionClaimIds = recommendationClaimIds;
345
+ pivot = undefined;
346
+ strongestCounterCase = recommendationClaimIds ? {
347
+ claim_ids: recommendationClaimIds.slice(0, 1),
348
+ reasoning: dissent[0] ?? 'The strongest remaining graph-linked objection is unresolved.',
349
+ } : undefined;
145
350
  }
146
351
  else if (recommendation !== 'PROCEED_WITH_CONDITIONS') {
147
352
  conditions = undefined;
353
+ conditionClaimIds = undefined;
148
354
  }
149
- const final = { ...report, adjudications, dissent, recommendation, conditions };
355
+ const snapshotInvalid = chairIssues.some((issue) => issue.startsWith('unknown decision snapshot claim id:'));
356
+ if (snapshotInvalid)
357
+ ctx.addFlag('synthesis_suspect');
358
+ const { decision_snapshot: decisionSnapshot, ...reportWithoutSnapshot } = report;
359
+ const final = {
360
+ ...reportWithoutSnapshot,
361
+ ...(!snapshotInvalid && decisionSnapshot ? { decision_snapshot: decisionSnapshot } : {}),
362
+ adjudications,
363
+ dissent,
364
+ recommendation,
365
+ conditions,
366
+ recommendation_claim_ids: recommendationClaimIds,
367
+ condition_claim_ids: conditionClaimIds,
368
+ pivot,
369
+ strongest_counter_case: strongestCounterCase,
370
+ };
150
371
  await ctx.writer.writeJson('judge-report', final);
151
372
  return final;
152
373
  }