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.
- package/CHANGELOG.md +98 -7
- package/README.md +109 -30
- package/dist/bench/arms.js +10 -9
- package/dist/bench/harness.js +13 -10
- package/dist/bench/idea-lane-rotation.js +237 -0
- package/dist/bench/idea-v3-bench.js +506 -0
- package/dist/bench/idea-v3-rating.js +582 -0
- package/dist/bench/results.js +10 -3
- package/dist/bench/scoring/decision-insights.js +112 -0
- package/dist/bench/scoring/seeded-bugs.js +4 -0
- package/dist/cli/bench.js +180 -3
- package/dist/cli/doctor.js +56 -24
- package/dist/cli/index.js +31 -6
- package/dist/cli/resolve.js +7 -6
- package/dist/cli/resume.js +18 -0
- package/dist/cli/run.js +63 -8
- package/dist/council/view.js +378 -109
- package/dist/orchestration/calculations.js +97 -0
- package/dist/orchestration/context.js +37 -9
- package/dist/orchestration/decision-dossier.js +262 -0
- package/dist/orchestration/decision-graph.js +256 -0
- package/dist/orchestration/engine.js +5 -2
- package/dist/orchestration/evidence-pack.js +46 -0
- package/dist/orchestration/idea-lanes.js +20 -0
- package/dist/orchestration/jsonStage.js +72 -0
- package/dist/orchestration/legacy-idea-adapter.js +102 -0
- package/dist/orchestration/modes.js +33 -0
- package/dist/orchestration/preflight.js +183 -0
- package/dist/orchestration/quick-analysis.js +81 -0
- package/dist/orchestration/stages/cr-ladder.js +80 -0
- package/dist/orchestration/stages/s10-render.js +562 -150
- package/dist/orchestration/stages/s4-analyze.js +12 -7
- package/dist/orchestration/stages/s5-drift.js +9 -9
- package/dist/orchestration/stages/s6-positions.js +10 -0
- package/dist/orchestration/stages/s7-decision-graph.js +76 -0
- package/dist/orchestration/stages/s8-verify.js +153 -46
- package/dist/orchestration/stages/s8b-rebuttal.js +208 -0
- package/dist/orchestration/stages/s9-judge.js +329 -108
- package/dist/orchestration/stages/s9b-plan.js +85 -75
- package/dist/providers/codex.js +2 -1
- package/dist/providers/spawn.js +5 -0
- package/dist/schemas/index.js +572 -13
- package/dist/skills/idea-refinement/analyst.md +18 -14
- package/dist/skills/idea-refinement/economics-delivery.md +7 -0
- package/dist/skills/idea-refinement/market-adoption.md +7 -0
- package/dist/storage/runs.js +11 -4
- package/dist/tui/app.js +37 -13
- package/dist/tui/format.js +4 -5
- package/dist/tui/smart-entry.js +17 -1
- package/dist/tui/timeline.js +2 -4
- package/dist/workflows/code-review.js +4 -2
- package/dist/workflows/idea-refinement.js +110 -46
- package/package.json +12 -4
- package/dist/orchestration/stages/s0-grill.js +0 -79
- package/dist/orchestration/stages/s1-intent.js +0 -25
- package/dist/orchestration/stages/s2-misread.js +0 -76
- package/dist/orchestration/stages/s3-prompts.js +0 -55
- package/dist/orchestration/stages/s6-claims.js +0 -56
- package/dist/orchestration/stages/s7-disagreement.js +0 -134
|
@@ -1,136 +1,329 @@
|
|
|
1
|
-
// S9 —
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
for (const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
const claimById = new Map(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
.replace('{{
|
|
104
|
-
.replace('{{
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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',
|
|
120
|
-
|
|
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 (
|
|
124
|
-
if (isFatal(
|
|
125
|
-
throw
|
|
314
|
+
catch (error) {
|
|
315
|
+
if (isFatal(error))
|
|
316
|
+
throw error;
|
|
126
317
|
}
|
|
127
318
|
}
|
|
128
|
-
|
|
129
|
-
const inScope = report.adjudications.filter((
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
}
|