aiki-cli 0.2.2 → 0.3.1
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 +113 -6
- package/README.md +119 -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 +66 -8
- package/dist/council/view.js +446 -117
- package/dist/orchestration/calculations.js +97 -0
- package/dist/orchestration/context.js +37 -9
- package/dist/orchestration/decision-dossier.js +320 -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 +39 -0
- package/dist/orchestration/preflight.js +203 -0
- package/dist/orchestration/quick-analysis.js +93 -0
- package/dist/orchestration/stages/cr-ladder.js +80 -0
- package/dist/orchestration/stages/s10-render.js +574 -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 +118 -85
- package/dist/orchestration/url-sources.js +200 -0
- package/dist/providers/codex.js +2 -1
- package/dist/providers/spawn.js +5 -0
- package/dist/schemas/index.js +638 -15
- 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 +12 -4
- package/dist/tui/app.js +53 -14
- 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 +118 -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
package/dist/schemas/index.js
CHANGED
|
@@ -17,6 +17,43 @@ export const ProviderIdSchema = z.enum(['claude', 'codex', 'agy']);
|
|
|
17
17
|
export const TaskTypeSchema = z.enum(['idea-refinement', 'code-review', 'other']);
|
|
18
18
|
/** The two runnable v1 workflows (§12). Discriminates RoleOutput and tags RunMeta. */
|
|
19
19
|
export const WorkflowIdSchema = z.enum(['idea-refinement', 'code-review']);
|
|
20
|
+
/** Explicit user-selected idea protocol. There is deliberately no learned mode router. */
|
|
21
|
+
export const IdeaModeSchema = z.enum(['quick', 'council', 'research']);
|
|
22
|
+
export const RequestedOutputSchema = z.enum([
|
|
23
|
+
'DECISION',
|
|
24
|
+
'FEATURE_BACKLOG',
|
|
25
|
+
'IMPLEMENTATION_PLAN',
|
|
26
|
+
]);
|
|
27
|
+
export const UrlSourceStatusSchema = z.enum(['FETCHED', 'BLOCKED', 'FAILED']);
|
|
28
|
+
export const UrlSourceSnapshot = z.object({
|
|
29
|
+
id: z.string().regex(/^U[1-5]$/),
|
|
30
|
+
url: z.string().url(),
|
|
31
|
+
final_url: z.string().url().optional(),
|
|
32
|
+
status: UrlSourceStatusSchema,
|
|
33
|
+
title: z.string().min(1).optional(),
|
|
34
|
+
content_type: z.string().min(1).optional(),
|
|
35
|
+
accessed_at: z.string().datetime(),
|
|
36
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
37
|
+
content: z.string().min(1).optional(),
|
|
38
|
+
error: z.string().min(1).optional(),
|
|
39
|
+
}).strict().superRefine((source, ctx) => {
|
|
40
|
+
if (source.status === 'FETCHED' && (!source.content || !source.sha256)) {
|
|
41
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'FETCHED source requires content and sha256' });
|
|
42
|
+
}
|
|
43
|
+
if (source.status !== 'FETCHED' && !source.error) {
|
|
44
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `${source.status} source requires an error` });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
export const UrlSourceSet = z.object({
|
|
48
|
+
sources: z.array(UrlSourceSnapshot).max(5),
|
|
49
|
+
}).strict();
|
|
50
|
+
export const DomainDimension = z
|
|
51
|
+
.object({
|
|
52
|
+
id: z.string().regex(/^D[1-5]$/),
|
|
53
|
+
label: z.string().min(1),
|
|
54
|
+
rationale: z.string().min(1),
|
|
55
|
+
})
|
|
56
|
+
.strict();
|
|
20
57
|
// ── S1: IntentContract (§13) ────────────────────────────────────────────────
|
|
21
58
|
export const IntentContract = z
|
|
22
59
|
.object({
|
|
@@ -25,8 +62,22 @@ export const IntentContract = z
|
|
|
25
62
|
constraints: z.array(z.string()), // explicit constraints the user stated (may be empty)
|
|
26
63
|
unknowns: z.array(z.string()), // things the request leaves unspecified
|
|
27
64
|
success_criteria: z.array(z.string()), // what a good final output must contain
|
|
65
|
+
domain_dimensions: z.array(DomainDimension).min(3).max(5).optional(), // required by the idea preflight; optional for old/code-review contracts
|
|
28
66
|
})
|
|
29
67
|
.strict();
|
|
68
|
+
/** R6 decision contract: the two-view preflight's single downstream boundary. */
|
|
69
|
+
export const DecisionContract = IntentContract.extend({
|
|
70
|
+
alternatives: z.array(z.string().min(1)).max(8),
|
|
71
|
+
success_bar: z.string().min(1),
|
|
72
|
+
evidence_supplied: z.array(z.string().min(1)).max(12),
|
|
73
|
+
missing_evidence: z.array(z.string().min(1)).max(12),
|
|
74
|
+
core_rubric: z.array(z.string().min(1)).min(1),
|
|
75
|
+
user_confirmed: z.boolean(),
|
|
76
|
+
confirmation: z.enum(['user-confirmed', 'headless-defaulted']),
|
|
77
|
+
requested_outputs: z.array(RequestedOutputSchema).min(1).max(4).optional(),
|
|
78
|
+
}).strict();
|
|
79
|
+
/** Code review and old idea runs keep the smaller v1 contract. */
|
|
80
|
+
export const DecisionContractArtifact = z.union([DecisionContract, IntentContract]);
|
|
30
81
|
// ── S2: Interpretation — per provider (§13) ─────────────────────────────────
|
|
31
82
|
export const Interpretation = z
|
|
32
83
|
.object({
|
|
@@ -65,7 +116,8 @@ const RunBriefDraftBase = z
|
|
|
65
116
|
claims_to_test: z.array(z.string().min(1)).max(8),
|
|
66
117
|
evidence_supplied: z.array(z.string().min(1)).max(8),
|
|
67
118
|
missing_axes: z.array(z.string().min(1)).max(8),
|
|
68
|
-
|
|
119
|
+
domain_dimensions: z.array(DomainDimension).min(3).max(5),
|
|
120
|
+
questions: z.array(RunBriefQuestion).max(4),
|
|
69
121
|
})
|
|
70
122
|
.strict();
|
|
71
123
|
function checkQuestionIds(questions, ctx) {
|
|
@@ -77,7 +129,19 @@ function checkQuestionIds(questions, ctx) {
|
|
|
77
129
|
seen.add(q.id);
|
|
78
130
|
}
|
|
79
131
|
}
|
|
80
|
-
|
|
132
|
+
function checkDomainDimensionIds(dimensions, ctx) {
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
for (const dimension of dimensions) {
|
|
135
|
+
if (seen.has(dimension.id)) {
|
|
136
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['domain_dimensions'], message: `duplicate domain dimension id: ${dimension.id}` });
|
|
137
|
+
}
|
|
138
|
+
seen.add(dimension.id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export const RunBriefDraft = RunBriefDraftBase.superRefine((brief, ctx) => {
|
|
142
|
+
checkQuestionIds(brief.questions, ctx);
|
|
143
|
+
checkDomainDimensionIds(brief.domain_dimensions, ctx);
|
|
144
|
+
});
|
|
81
145
|
export const GrillAnswer = z
|
|
82
146
|
.object({
|
|
83
147
|
question_id: z.string().min(1),
|
|
@@ -86,9 +150,10 @@ export const GrillAnswer = z
|
|
|
86
150
|
})
|
|
87
151
|
.strict();
|
|
88
152
|
export const RunBrief = RunBriefDraftBase.extend({
|
|
89
|
-
answers: z.array(GrillAnswer).
|
|
153
|
+
answers: z.array(GrillAnswer).max(4),
|
|
90
154
|
}).superRefine((brief, ctx) => {
|
|
91
155
|
checkQuestionIds(brief.questions, ctx);
|
|
156
|
+
checkDomainDimensionIds(brief.domain_dimensions, ctx);
|
|
92
157
|
const questionIds = new Set(brief.questions.map((q) => q.id));
|
|
93
158
|
const answerIds = new Set();
|
|
94
159
|
for (const answer of brief.answers) {
|
|
@@ -106,6 +171,34 @@ export const RunBrief = RunBriefDraftBase.extend({
|
|
|
106
171
|
}
|
|
107
172
|
}
|
|
108
173
|
});
|
|
174
|
+
/** One of the two independent R6 preflight readings. */
|
|
175
|
+
export const PreflightReading = z.object({
|
|
176
|
+
subject: z.string().min(1),
|
|
177
|
+
interpretation: z.string().min(1),
|
|
178
|
+
normalized_decision: z.string().min(1),
|
|
179
|
+
alternatives: z.array(z.string().min(1)).max(8),
|
|
180
|
+
target_user: z.string().min(1).nullable(),
|
|
181
|
+
constraints: z.array(z.string().min(1)).max(10),
|
|
182
|
+
success_bar: z.string().min(1),
|
|
183
|
+
success_criteria: z.array(z.string().min(1)).max(8),
|
|
184
|
+
claims_to_test: z.array(z.string().min(1)).max(8),
|
|
185
|
+
evidence_supplied: z.array(z.string().min(1)).max(8),
|
|
186
|
+
missing_evidence: z.array(z.string().min(1)).max(8),
|
|
187
|
+
domain_dimensions: z.array(DomainDimension).min(3).max(5),
|
|
188
|
+
questions: z.array(RunBriefQuestion).max(4),
|
|
189
|
+
}).strict().superRefine((reading, ctx) => {
|
|
190
|
+
checkQuestionIds(reading.questions, ctx);
|
|
191
|
+
checkDomainDimensionIds(reading.domain_dimensions, ctx);
|
|
192
|
+
});
|
|
193
|
+
export const PreflightArtifact = z.object({
|
|
194
|
+
readings: z.array(z.object({ provider: ProviderIdSchema, reading: PreflightReading }).strict()).min(1).max(2),
|
|
195
|
+
clusters: z.array(z.object({ members: z.array(z.string()), representative: z.string().min(1) }).strict()),
|
|
196
|
+
chosen: z.object({
|
|
197
|
+
interpretation: z.string().min(1),
|
|
198
|
+
how: z.enum(['single-cluster', 'majority-cluster', 'user-selected', 'user-combined', 'user-typed']),
|
|
199
|
+
}).strict(),
|
|
200
|
+
dropped: z.array(z.object({ provider: ProviderIdSchema, error: z.string().min(1) }).strict()),
|
|
201
|
+
}).strict();
|
|
109
202
|
// ── S4: RoleOutput — workflow-discriminated union (§12, §13) ─────────────────
|
|
110
203
|
//
|
|
111
204
|
// The model output (§13) does NOT carry a `workflow` field; the engine injects the discriminator
|
|
@@ -126,7 +219,7 @@ const Attack = z
|
|
|
126
219
|
severity: z.enum(['HIGH', 'MED', 'LOW']),
|
|
127
220
|
})
|
|
128
221
|
.strict();
|
|
129
|
-
export const
|
|
222
|
+
export const LegacyIdeaRoleOutput = z
|
|
130
223
|
.object({
|
|
131
224
|
workflow: z.literal('idea-refinement'),
|
|
132
225
|
task_echo: z.string().min(1), // ≤2 sentence restatement (drift check, S5)
|
|
@@ -136,6 +229,174 @@ export const IdeaRoleOutput = z
|
|
|
136
229
|
open_questions: z.array(z.string()).max(5),
|
|
137
230
|
})
|
|
138
231
|
.strict();
|
|
232
|
+
export const ClaimPosition = z
|
|
233
|
+
.object({
|
|
234
|
+
local_id: z.string().min(1),
|
|
235
|
+
proposition: z.string().min(1),
|
|
236
|
+
dimension_id: z.string().min(1),
|
|
237
|
+
stance: z.enum(['SUPPORT', 'OPPOSE', 'MIXED', 'UNKNOWN']),
|
|
238
|
+
basis: z.enum(['EVIDENCE', 'INFERENCE', 'ASSUMPTION']),
|
|
239
|
+
load_bearing: z.boolean(),
|
|
240
|
+
if_false: z.enum(['STOP', 'PIVOT', 'CONDITION', 'MINOR']),
|
|
241
|
+
reasoning: z.string().min(1),
|
|
242
|
+
evidence_ids: z.array(z.string().min(1)),
|
|
243
|
+
depends_on: z.array(z.string().min(1)),
|
|
244
|
+
})
|
|
245
|
+
.strict();
|
|
246
|
+
const EvidenceCardBase = z
|
|
247
|
+
.object({
|
|
248
|
+
id: z.string().min(1),
|
|
249
|
+
claim_supported: z.string().min(1),
|
|
250
|
+
source_kind: z.enum(['USER', 'PRIMARY', 'SECONDARY', 'MODEL_KNOWLEDGE']),
|
|
251
|
+
title: z.string().min(1).optional(),
|
|
252
|
+
url: z.string().url().optional(),
|
|
253
|
+
published_at: z.string().min(1).optional(),
|
|
254
|
+
accessed_at: z.string().min(1).optional(),
|
|
255
|
+
locator: z.string().min(1).optional(),
|
|
256
|
+
support: z.enum(['SUPPORTS', 'CONTRADICTS', 'CONTEXT_ONLY']),
|
|
257
|
+
freshness: z.enum(['CURRENT', 'DATED', 'UNKNOWN']),
|
|
258
|
+
})
|
|
259
|
+
.strict();
|
|
260
|
+
function checkEvidenceCard(card, ctx) {
|
|
261
|
+
const external = card.source_kind === 'PRIMARY' || card.source_kind === 'SECONDARY';
|
|
262
|
+
if (external && !card.url && !card.locator) {
|
|
263
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['locator'], message: `${card.source_kind} evidence requires a URL or locator` });
|
|
264
|
+
}
|
|
265
|
+
if (external && card.freshness === 'CURRENT' && !card.accessed_at) {
|
|
266
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['accessed_at'], message: 'current external evidence requires accessed_at' });
|
|
267
|
+
}
|
|
268
|
+
if (card.source_kind === 'MODEL_KNOWLEDGE' && card.freshness === 'CURRENT') {
|
|
269
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['freshness'], message: 'model knowledge cannot claim current freshness' });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
export const EvidenceCard = EvidenceCardBase.superRefine(checkEvidenceCard);
|
|
273
|
+
export const CalculationInput = z.object({
|
|
274
|
+
id: z.string().min(1),
|
|
275
|
+
name: z.string().min(1),
|
|
276
|
+
value: z.number().finite(),
|
|
277
|
+
unit: z.string().min(1),
|
|
278
|
+
evidence_ids: z.array(z.string().min(1)).min(1),
|
|
279
|
+
}).strict();
|
|
280
|
+
export const CalculationStep = z.object({
|
|
281
|
+
id: z.string().min(1),
|
|
282
|
+
operation: z.enum(['ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE']),
|
|
283
|
+
left: z.string().min(1),
|
|
284
|
+
right: z.string().min(1),
|
|
285
|
+
result: z.number().finite(),
|
|
286
|
+
unit: z.string().min(1),
|
|
287
|
+
}).strict();
|
|
288
|
+
export const CalculationLedger = z.object({
|
|
289
|
+
id: z.string().min(1),
|
|
290
|
+
claim_id: z.string().min(1),
|
|
291
|
+
inputs: z.array(CalculationInput).min(1).max(12),
|
|
292
|
+
steps: z.array(CalculationStep).min(1).max(12),
|
|
293
|
+
result_step: z.string().min(1),
|
|
294
|
+
}).strict();
|
|
295
|
+
// Live 20260714-2142 killed both seats on shape ceremony the prompt never promised: rationale on
|
|
296
|
+
// COVERED entries and a question id have no consumer, so the schema now matches the documented
|
|
297
|
+
// either/or shape (NOT_APPLICABLE still requires its rationale).
|
|
298
|
+
const CoverageEntryBase = z
|
|
299
|
+
.object({
|
|
300
|
+
dimension_id: z.string().min(1),
|
|
301
|
+
status: z.enum(['COVERED', 'NOT_APPLICABLE']),
|
|
302
|
+
position_ids: z.array(z.string().min(1)).default([]),
|
|
303
|
+
rationale: z.string().min(1).optional(),
|
|
304
|
+
})
|
|
305
|
+
.strict();
|
|
306
|
+
function checkCoverageEntry(entry, ctx) {
|
|
307
|
+
if (entry.status === 'NOT_APPLICABLE' && !entry.rationale) {
|
|
308
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['rationale'], message: 'NOT_APPLICABLE coverage requires a rationale' });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
export const CoverageEntry = CoverageEntryBase.superRefine(checkCoverageEntry);
|
|
312
|
+
export const DecisionQuestion = z
|
|
313
|
+
.object({
|
|
314
|
+
id: z.string().min(1).optional(),
|
|
315
|
+
question: z.string().min(1),
|
|
316
|
+
claim_ids: z.array(z.string().min(1)),
|
|
317
|
+
})
|
|
318
|
+
.strict();
|
|
319
|
+
const IdeaRoleOutputBase = z
|
|
320
|
+
.object({
|
|
321
|
+
workflow: z.literal('idea-refinement'),
|
|
322
|
+
task_echo: z.string().min(1),
|
|
323
|
+
strongest_version: z.string().min(1),
|
|
324
|
+
positions: z.array(ClaimPosition).max(12),
|
|
325
|
+
evidence: z.array(EvidenceCard).max(20),
|
|
326
|
+
calculations: z.array(CalculationLedger).max(8).default([]),
|
|
327
|
+
coverage: z.array(CoverageEntry).max(18), // 13 core + up to 5 preflight domain dimensions
|
|
328
|
+
decision_questions: z.array(DecisionQuestion).max(8),
|
|
329
|
+
})
|
|
330
|
+
.strict();
|
|
331
|
+
function checkSubmissionRefs(submission, ctx) {
|
|
332
|
+
const positionIds = new Set();
|
|
333
|
+
for (const [index, position] of submission.positions.entries()) {
|
|
334
|
+
if (positionIds.has(position.local_id))
|
|
335
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['positions', index, 'local_id'], message: `duplicate position id: ${position.local_id}` });
|
|
336
|
+
positionIds.add(position.local_id);
|
|
337
|
+
}
|
|
338
|
+
const evidenceIds = new Set();
|
|
339
|
+
for (const [index, evidence] of submission.evidence.entries()) {
|
|
340
|
+
if (evidenceIds.has(evidence.id))
|
|
341
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['evidence', index, 'id'], message: `duplicate evidence id: ${evidence.id}` });
|
|
342
|
+
evidenceIds.add(evidence.id);
|
|
343
|
+
}
|
|
344
|
+
const calculationIds = new Set();
|
|
345
|
+
for (const [index, calculation] of submission.calculations.entries()) {
|
|
346
|
+
if (calculationIds.has(calculation.id))
|
|
347
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'id'], message: `duplicate calculation id: ${calculation.id}` });
|
|
348
|
+
calculationIds.add(calculation.id);
|
|
349
|
+
if (!positionIds.has(calculation.claim_id))
|
|
350
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'claim_id'], message: `unknown position id: ${calculation.claim_id}` });
|
|
351
|
+
const refs = new Set(calculation.inputs.map((input) => input.id));
|
|
352
|
+
const inputIds = new Set();
|
|
353
|
+
const stepIds = new Set();
|
|
354
|
+
for (const [inputIndex, input] of calculation.inputs.entries()) {
|
|
355
|
+
if (inputIds.has(input.id))
|
|
356
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'inputs', inputIndex, 'id'], message: `duplicate calculation input: ${input.id}` });
|
|
357
|
+
inputIds.add(input.id);
|
|
358
|
+
for (const evidenceId of input.evidence_ids) {
|
|
359
|
+
if (!evidenceIds.has(evidenceId))
|
|
360
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'inputs', inputIndex, 'evidence_ids'], message: `unknown evidence id: ${evidenceId}` });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
for (const [stepIndex, step] of calculation.steps.entries()) {
|
|
364
|
+
if (!refs.has(step.left))
|
|
365
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'steps', stepIndex, 'left'], message: `unknown or forward calculation reference: ${step.left}` });
|
|
366
|
+
if (!refs.has(step.right))
|
|
367
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'steps', stepIndex, 'right'], message: `unknown or forward calculation reference: ${step.right}` });
|
|
368
|
+
if (refs.has(step.id))
|
|
369
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'steps', stepIndex, 'id'], message: `duplicate calculation reference: ${step.id}` });
|
|
370
|
+
refs.add(step.id);
|
|
371
|
+
stepIds.add(step.id);
|
|
372
|
+
}
|
|
373
|
+
if (!stepIds.has(calculation.result_step))
|
|
374
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'result_step'], message: `unknown result step: ${calculation.result_step}` });
|
|
375
|
+
}
|
|
376
|
+
for (const [index, position] of submission.positions.entries()) {
|
|
377
|
+
for (const id of position.evidence_ids) {
|
|
378
|
+
if (!evidenceIds.has(id))
|
|
379
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['positions', index, 'evidence_ids'], message: `unknown evidence id: ${id}` });
|
|
380
|
+
}
|
|
381
|
+
for (const id of position.depends_on) {
|
|
382
|
+
if (!positionIds.has(id))
|
|
383
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['positions', index, 'depends_on'], message: `unknown position id: ${id}` });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
for (const [index, entry] of submission.coverage.entries()) {
|
|
387
|
+
for (const id of entry.position_ids) {
|
|
388
|
+
if (!positionIds.has(id))
|
|
389
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['coverage', index, 'position_ids'], message: `unknown position id: ${id}` });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const [index, question] of submission.decision_questions.entries()) {
|
|
393
|
+
for (const id of question.claim_ids) {
|
|
394
|
+
if (!positionIds.has(id))
|
|
395
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['decision_questions', index, 'claim_ids'], message: `unknown position id: ${id}` });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
export const IdeaRoleOutput = IdeaRoleOutputBase.superRefine(checkSubmissionRefs);
|
|
139
400
|
/** Defect categories a finding (and a seeded bug, T11) can carry. BENCHMARK.md's "defect class" match
|
|
140
401
|
* is equality on this enum (off-by-one→CORRECTNESS, race→CONCURRENCY, unhandled-rejection→ERROR_HANDLING,
|
|
141
402
|
* auth-gap→SECURITY, N+1→PERF). */
|
|
@@ -161,16 +422,123 @@ export const CodeReviewRoleOutput = z
|
|
|
161
422
|
findings: z.array(Finding).max(12),
|
|
162
423
|
})
|
|
163
424
|
.strict();
|
|
164
|
-
export const RoleOutput = z.
|
|
425
|
+
export const RoleOutput = z.union([IdeaRoleOutput, CodeReviewRoleOutput]);
|
|
165
426
|
/** The exact JSON a code-review S4 reviewer returns: `CodeReviewRoleOutput` WITHOUT the `workflow`
|
|
166
427
|
* discriminator (§13 — model output carries no `workflow`). Mirrors `IdeaRoleOutputModel` (T6); S4
|
|
167
428
|
* validates the raw call against this, injects `workflow`, then persists as `RoleOutput` (T10). */
|
|
168
429
|
export const CodeReviewRoleOutputModel = CodeReviewRoleOutput.omit({ workflow: true });
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
430
|
+
const StrictIdeaRoleOutputModel = IdeaRoleOutputBase.omit({ workflow: true }).superRefine((submission, ctx) => checkSubmissionRefs({ workflow: 'idea-refinement', ...submission }, ctx));
|
|
431
|
+
/** Case-insensitive match to an exact canonical enum word (plus known aliases); prose never matches. */
|
|
432
|
+
function canonicalEnum(value, canon, aliases = {}) {
|
|
433
|
+
if (typeof value !== 'string')
|
|
434
|
+
return value;
|
|
435
|
+
const upper = value.toUpperCase();
|
|
436
|
+
const mapped = aliases[upper] ?? upper;
|
|
437
|
+
return canon.includes(mapped) ? mapped : value;
|
|
438
|
+
}
|
|
439
|
+
/** Match a leading canonical/alias token with trailing prose ("OPPOSES: <reason>"), the observed
|
|
440
|
+
* 20260714-2142 codex vocabulary. The FIRST word must itself be the token; free prose whose first
|
|
441
|
+
* word is not a known token still never matches. */
|
|
442
|
+
function canonicalEnumLeadingToken(value, canon, aliases = {}) {
|
|
443
|
+
if (typeof value !== 'string')
|
|
444
|
+
return value;
|
|
445
|
+
const token = value.trim().match(/^[A-Za-z_]+/)?.[0]?.toUpperCase();
|
|
446
|
+
if (!token)
|
|
447
|
+
return value;
|
|
448
|
+
const mapped = aliases[token] ?? token;
|
|
449
|
+
return canon.includes(mapped) ? mapped : value;
|
|
450
|
+
}
|
|
451
|
+
/** Canonicalize enum spellings observed in live provider repairs (SUPPORT, current, Current);
|
|
452
|
+
* anything that is not the exact word in some casing stays invalid. */
|
|
453
|
+
function canonicalizeIdeaRoleOutputModel(input) {
|
|
454
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
455
|
+
return input;
|
|
456
|
+
const output = input;
|
|
457
|
+
if (!Array.isArray(output.evidence))
|
|
458
|
+
return input;
|
|
459
|
+
return {
|
|
460
|
+
...output,
|
|
461
|
+
evidence: output.evidence.map((item) => {
|
|
462
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
463
|
+
return item;
|
|
464
|
+
const evidence = item;
|
|
465
|
+
return {
|
|
466
|
+
...evidence,
|
|
467
|
+
support: canonicalEnumLeadingToken(evidence.support, ['SUPPORTS', 'CONTRADICTS', 'CONTEXT_ONLY'], { SUPPORT: 'SUPPORTS', OPPOSES: 'CONTRADICTS', OPPOSE: 'CONTRADICTS' }),
|
|
468
|
+
freshness: canonicalEnum(evidence.freshness, ['CURRENT', 'DATED', 'UNKNOWN']),
|
|
469
|
+
};
|
|
470
|
+
}),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/** Deterministic last resort after a failed §14 repair. Three live failures shaped it:
|
|
474
|
+
* run 20260712-0011 wrote evidence prose into `support`; run 20260713-1503 added an unknown `content`
|
|
475
|
+
* key to every card and leaked `MODEL_KNOWLEDGE` into a position's `basis`; run 20260714-2142 killed
|
|
476
|
+
* BOTH seats via an over-cap calculation ledger and coverage/question entries the old salvage never
|
|
477
|
+
* touched. Policy, strictly deterministic (never invents a value):
|
|
478
|
+
* - unknown extra keys are stripped (zod `.strip()` re-parse);
|
|
479
|
+
* - an evidence card that still fails is dropped, and its id scrubbed from positions;
|
|
480
|
+
* - a position that still fails is dropped, and its id scrubbed from depends_on / coverage /
|
|
481
|
+
* decision_questions — one bad position costs one position, not the whole seat;
|
|
482
|
+
* - a calculation that still fails, or whose position/evidence anchors were dropped, is dropped;
|
|
483
|
+
* the survivors are truncated to the schema cap in order (one bad ledger costs one ledger);
|
|
484
|
+
* - a coverage entry or decision question that still fails is dropped (a dropped coverage entry
|
|
485
|
+
* becomes a visible structural hole downstream, never a dead seat);
|
|
486
|
+
* - a seat with NO surviving position stays a hard failure (nothing to analyze). */
|
|
487
|
+
export function salvageIdeaRoleOutputModel(input) {
|
|
488
|
+
const canonical = canonicalizeIdeaRoleOutputModel(input);
|
|
489
|
+
if (!canonical || typeof canonical !== 'object' || Array.isArray(canonical))
|
|
490
|
+
return canonical;
|
|
491
|
+
const output = canonical;
|
|
492
|
+
if (!Array.isArray(output.evidence) || !Array.isArray(output.positions))
|
|
493
|
+
return canonical;
|
|
494
|
+
const evidence = output.evidence
|
|
495
|
+
.map((item) => EvidenceCardBase.strip().superRefine(checkEvidenceCard).safeParse(item))
|
|
496
|
+
.flatMap((result) => (result.success ? [result.data] : []));
|
|
497
|
+
const keptEvidence = new Set(evidence.map((card) => card.id));
|
|
498
|
+
const parsedPositions = output.positions
|
|
499
|
+
.map((item) => ClaimPosition.strip().safeParse(item))
|
|
500
|
+
.flatMap((result) => (result.success ? [result.data] : []));
|
|
501
|
+
if (parsedPositions.length === 0)
|
|
502
|
+
return canonical; // empty claim set → let strict validation fail it
|
|
503
|
+
const keptPositions = new Set(parsedPositions.map((position) => position.local_id));
|
|
504
|
+
const positions = parsedPositions.map((position) => ({
|
|
505
|
+
...position,
|
|
506
|
+
evidence_ids: position.evidence_ids.filter((id) => keptEvidence.has(id)),
|
|
507
|
+
depends_on: position.depends_on.filter((id) => keptPositions.has(id)),
|
|
508
|
+
}));
|
|
509
|
+
const scrubIds = (value, key) => {
|
|
510
|
+
if (!Array.isArray(value))
|
|
511
|
+
return value;
|
|
512
|
+
return value.map((item) => {
|
|
513
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
514
|
+
return item;
|
|
515
|
+
const entry = item;
|
|
516
|
+
if (!Array.isArray(entry[key]))
|
|
517
|
+
return item;
|
|
518
|
+
return { ...entry, [key]: entry[key].filter((id) => keptPositions.has(id)) };
|
|
519
|
+
});
|
|
520
|
+
};
|
|
521
|
+
const parseOrDrop = (value, parse) => Array.isArray(value)
|
|
522
|
+
? value.map((item) => parse(item)).flatMap((result) => (result.success ? [result.data] : []))
|
|
523
|
+
: value;
|
|
524
|
+
const calculations = Array.isArray(output.calculations)
|
|
525
|
+
? parseOrDrop(output.calculations, (item) => CalculationLedger.strip().safeParse(item))
|
|
526
|
+
.filter((calc) => keptPositions.has(calc.claim_id)
|
|
527
|
+
&& calc.inputs.every((input) => input.evidence_ids.every((id) => keptEvidence.has(id))))
|
|
528
|
+
.slice(0, 8)
|
|
529
|
+
: output.calculations;
|
|
530
|
+
return {
|
|
531
|
+
...output,
|
|
532
|
+
evidence,
|
|
533
|
+
positions,
|
|
534
|
+
calculations,
|
|
535
|
+
coverage: parseOrDrop(scrubIds(output.coverage, 'position_ids'), (item) => CoverageEntryBase.strip().superRefine(checkCoverageEntry).safeParse(item)),
|
|
536
|
+
decision_questions: parseOrDrop(scrubIds(output.decision_questions, 'claim_ids'), (item) => DecisionQuestion.strip().safeParse(item)),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
/** The model-facing S4 shape. Exact known enum aliases are canonicalized, then the strict schema validates
|
|
540
|
+
* the full output; persisted `IdeaRoleOutput` remains canonical-only. */
|
|
541
|
+
export const IdeaRoleOutputModel = z.preprocess(canonicalizeIdeaRoleOutputModel, StrictIdeaRoleOutputModel);
|
|
174
542
|
// ── S3: StagePrompts (§9, §13) ──────────────────────────────────────────────
|
|
175
543
|
//
|
|
176
544
|
// S3 output: the role-specific S4 prompts with every {{SLOT}} filled. Deterministic validator
|
|
@@ -191,10 +559,59 @@ export const ClaimGroups = z
|
|
|
191
559
|
groups: z.array(z.array(z.string().min(1)).min(2)),
|
|
192
560
|
})
|
|
193
561
|
.strict();
|
|
562
|
+
// ── R2: typed decision graph ────────────────────────────────────────────────
|
|
563
|
+
const GraphPosition = ClaimPosition.extend({
|
|
564
|
+
id: z.string().min(1),
|
|
565
|
+
provider: ProviderIdSchema,
|
|
566
|
+
source_id: z.string().min(1),
|
|
567
|
+
});
|
|
568
|
+
const GraphEvidence = EvidenceCardBase.extend({
|
|
569
|
+
id: z.string().min(1),
|
|
570
|
+
provider: ProviderIdSchema,
|
|
571
|
+
source_id: z.string().min(1),
|
|
572
|
+
}).superRefine(checkEvidenceCard);
|
|
573
|
+
const GraphCalculation = CalculationLedger.extend({
|
|
574
|
+
id: z.string().min(1),
|
|
575
|
+
claim_id: z.string().min(1),
|
|
576
|
+
provider: ProviderIdSchema,
|
|
577
|
+
source_id: z.string().min(1),
|
|
578
|
+
});
|
|
579
|
+
export const CalculationCheck = z.object({
|
|
580
|
+
calculation_id: z.string().min(1),
|
|
581
|
+
claim_id: z.string().min(1),
|
|
582
|
+
status: z.enum(['PASS', 'FAIL']),
|
|
583
|
+
issues: z.array(z.string()),
|
|
584
|
+
}).strict();
|
|
585
|
+
export const DecisionClaim = z.object({
|
|
586
|
+
id: z.string().min(1),
|
|
587
|
+
proposition: z.string().min(1),
|
|
588
|
+
position_ids: z.array(z.string().min(1)).min(1),
|
|
589
|
+
state: z.enum(['CONSENSUS', 'SHARED_CONCERN', 'DISAGREEMENT', 'UNIQUE', 'UNCERTAIN']),
|
|
590
|
+
evidence_state: z.enum(['SUPPORTED', 'CONFLICTED', 'UNVERIFIED']),
|
|
591
|
+
load_bearing: z.boolean(),
|
|
592
|
+
if_false: z.enum(['STOP', 'PIVOT', 'CONDITION', 'MINOR']),
|
|
593
|
+
sensitivity: z.enum(['DECISIVE', 'MATERIAL', 'LOW']),
|
|
594
|
+
});
|
|
595
|
+
export const DecisionGraph = z.object({
|
|
596
|
+
positions: z.array(GraphPosition),
|
|
597
|
+
evidence: z.array(GraphEvidence),
|
|
598
|
+
calculations: z.array(GraphCalculation).default([]),
|
|
599
|
+
calculation_checks: z.array(CalculationCheck).default([]),
|
|
600
|
+
claims: z.array(DecisionClaim),
|
|
601
|
+
edges: z.array(z.object({
|
|
602
|
+
from: z.string().min(1),
|
|
603
|
+
to: z.string().min(1),
|
|
604
|
+
type: z.enum(['DEPENDS_ON', 'SUPPORTS', 'ATTACKS', 'CONTRADICTS']),
|
|
605
|
+
})),
|
|
606
|
+
holes: z.object({
|
|
607
|
+
coverage: z.array(z.object({ dimension_id: z.string().min(1), label: z.string().min(1) })),
|
|
608
|
+
evidence: z.array(z.object({ claim_id: z.string().min(1), reason: z.string().min(1) })),
|
|
609
|
+
}),
|
|
610
|
+
});
|
|
194
611
|
// ── S8: Verification (§13) ──────────────────────────────────────────────────
|
|
195
612
|
//
|
|
196
613
|
// `Verification` is the per-item verdict (§9 "per-item Verification"). `VerificationSet` is the
|
|
197
|
-
// actual S8 stage output
|
|
614
|
+
// actual S8 stage output. Each item is judged independently; no verdict distribution is required.
|
|
198
615
|
export const Verification = z
|
|
199
616
|
.object({
|
|
200
617
|
target_id: z.string().min(1),
|
|
@@ -206,10 +623,65 @@ export const Verification = z
|
|
|
206
623
|
export const VerificationSet = z
|
|
207
624
|
.object({
|
|
208
625
|
verifications: z.array(Verification),
|
|
209
|
-
//
|
|
626
|
+
// Accepted for backward compatibility with pre-R1 artifacts; the R1 prompt no longer requests it.
|
|
210
627
|
all_confirmed_justification: z.string().optional(),
|
|
211
628
|
})
|
|
212
629
|
.strict();
|
|
630
|
+
export const ClaimVerification = z.object({
|
|
631
|
+
claim_id: z.string().min(1),
|
|
632
|
+
status: z.enum(['VERIFIED', 'PARTIAL', 'CONTRADICTED', 'UNVERIFIABLE']),
|
|
633
|
+
reasoning: z.string().min(1),
|
|
634
|
+
evidence_ids: z.array(z.string().min(1)),
|
|
635
|
+
calculation_check: z.enum(['PASS', 'FAIL', 'NOT_APPLICABLE']).optional(),
|
|
636
|
+
missing_evidence: z.array(z.string().min(1)),
|
|
637
|
+
}).strict();
|
|
638
|
+
export const ClaimVerificationSet = z.object({
|
|
639
|
+
verifications: z.array(ClaimVerification),
|
|
640
|
+
}).strict();
|
|
641
|
+
// ── R5: bounded, append-only rebuttal events ───────────────────────────────
|
|
642
|
+
const RebuttalResponseBase = z.object({
|
|
643
|
+
claim_id: z.string().min(1),
|
|
644
|
+
response: z.enum(['CONCEDE', 'COUNTER', 'NARROW', 'UNRESOLVED']),
|
|
645
|
+
reasoning: z.string().min(1),
|
|
646
|
+
evidence_ids: z.array(z.string().min(1)),
|
|
647
|
+
narrowed_proposition: z.string().min(1).optional(),
|
|
648
|
+
}).strict();
|
|
649
|
+
const checkNarrowedRebuttal = (event, ctx) => {
|
|
650
|
+
if (event.response === 'NARROW' && !event.narrowed_proposition) {
|
|
651
|
+
ctx.addIssue({
|
|
652
|
+
code: z.ZodIssueCode.custom,
|
|
653
|
+
path: ['narrowed_proposition'],
|
|
654
|
+
message: 'NARROW requires narrowed_proposition',
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
export const RebuttalResponse = RebuttalResponseBase.superRefine(checkNarrowedRebuttal);
|
|
659
|
+
/** Exact model output for one scout's single rebuttal round. */
|
|
660
|
+
export const RebuttalResponseSet = z.object({
|
|
661
|
+
events: z.array(RebuttalResponse).max(3),
|
|
662
|
+
}).strict();
|
|
663
|
+
export const RebuttalEvent = RebuttalResponseBase.extend({
|
|
664
|
+
id: z.string().min(1),
|
|
665
|
+
round: z.literal(1),
|
|
666
|
+
responder: ProviderIdSchema,
|
|
667
|
+
target_position_ids: z.array(z.string().min(1)),
|
|
668
|
+
}).superRefine(checkNarrowedRebuttal);
|
|
669
|
+
/** Persisted separately from DecisionGraph so original claims/evidence remain immutable. */
|
|
670
|
+
export const RebuttalEventSet = z.object({
|
|
671
|
+
round: z.literal(1),
|
|
672
|
+
selected_claim_ids: z.array(z.string().min(1)).max(3),
|
|
673
|
+
events: z.array(RebuttalEvent).max(6),
|
|
674
|
+
stop_reason: z.enum([
|
|
675
|
+
'NO_ESCALATIONS',
|
|
676
|
+
'NO_ELIGIBLE_SCOUT',
|
|
677
|
+
'BUDGET_RESERVED',
|
|
678
|
+
'ROUND_COMPLETE',
|
|
679
|
+
'NO_NEW_EVIDENCE',
|
|
680
|
+
'CALL_CAP_REACHED',
|
|
681
|
+
]),
|
|
682
|
+
}).strict();
|
|
683
|
+
/** Shared artifact slot: code review keeps the v1 cross-exam shape; idea refinement uses R4 claims. */
|
|
684
|
+
export const VerificationArtifact = z.union([VerificationSet, ClaimVerificationSet]);
|
|
213
685
|
// ── S7: DisagreementMap (§7, §9) ────────────────────────────────────────────
|
|
214
686
|
//
|
|
215
687
|
// The plan (§7/§9) names only the four arrays. `Claim` shape is from §6. NOTE two under-specified
|
|
@@ -280,16 +752,69 @@ const Adjudication = z
|
|
|
280
752
|
id: z.string().min(1), // disputed item id
|
|
281
753
|
ruling: z.enum(['UPHOLD', 'REJECT', 'UNRESOLVED']),
|
|
282
754
|
reasoning: z.string().min(1), // ≤3 sentences
|
|
283
|
-
evidence_cited: z.string().min(1),
|
|
755
|
+
evidence_cited: z.string().min(1).optional(), // code-review / legacy idea artifacts
|
|
756
|
+
evidence_ids: z.array(z.string().min(1)).optional(), // R4 idea chair citations, validated by reference
|
|
757
|
+
effect_on_decision: z.string().min(1).optional(), // R5 idea chair; optional for legacy/code-review artifacts
|
|
758
|
+
what_would_change_it: z.string().min(1).optional(), // required by S9 when an idea ruling is UNRESOLVED
|
|
284
759
|
})
|
|
285
|
-
.strict()
|
|
760
|
+
.strict()
|
|
761
|
+
.refine((item) => item.evidence_cited || item.evidence_ids?.length, { message: 'adjudication requires evidence' });
|
|
286
762
|
export const Recommendation = z.enum(['PROCEED', 'PROCEED_WITH_CONDITIONS', 'PIVOT', 'STOP']);
|
|
763
|
+
const DecisionSnapshotItem = z.object({
|
|
764
|
+
label: z.string().min(1),
|
|
765
|
+
value: z.string().min(1),
|
|
766
|
+
meaning: z.string().min(1),
|
|
767
|
+
claim_ids: z.array(z.string().min(1)).min(1).max(4),
|
|
768
|
+
}).strict();
|
|
769
|
+
const DecisionOption = z.object({
|
|
770
|
+
label: z.string().min(1),
|
|
771
|
+
commitment: z.string().min(1),
|
|
772
|
+
commitment_kind: z.enum(['KNOWN', 'TARGET_CAP', 'UNKNOWN']),
|
|
773
|
+
tradeoff: z.string().min(1),
|
|
774
|
+
claim_ids: z.array(z.string().min(1)).max(4),
|
|
775
|
+
}).strict().superRefine((option, ctx) => {
|
|
776
|
+
if (option.commitment_kind !== 'UNKNOWN' && option.claim_ids.length === 0) {
|
|
777
|
+
ctx.addIssue({
|
|
778
|
+
code: z.ZodIssueCode.custom,
|
|
779
|
+
path: ['claim_ids'],
|
|
780
|
+
message: 'known commitments and target caps require a graph claim anchor',
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
/** Reader-first, graph-anchored numeric comparison for decision tasks where amounts or thresholds matter. */
|
|
785
|
+
export const DecisionSnapshot = z.object({
|
|
786
|
+
decisive_numbers: z.array(DecisionSnapshotItem).min(1).max(5),
|
|
787
|
+
payback: z.object({
|
|
788
|
+
status: z.enum(['ACHIEVED', 'NOT_ACHIEVED', 'NOT_COMPUTABLE']),
|
|
789
|
+
result: z.string().min(1),
|
|
790
|
+
basis: z.string().min(1),
|
|
791
|
+
claim_ids: z.array(z.string().min(1)).min(1).max(4),
|
|
792
|
+
}).strict().optional(),
|
|
793
|
+
options: z.array(DecisionOption).min(2).max(4),
|
|
794
|
+
tripwire: z.object({
|
|
795
|
+
metric: z.string().min(1),
|
|
796
|
+
threshold: z.string().min(1),
|
|
797
|
+
decision_rule: z.string().min(1),
|
|
798
|
+
claim_ids: z.array(z.string().min(1)).min(1).max(4),
|
|
799
|
+
}).strict().optional(),
|
|
800
|
+
}).strict();
|
|
287
801
|
const JudgeReportBase = z
|
|
288
802
|
.object({
|
|
289
803
|
adjudications: z.array(Adjudication),
|
|
290
804
|
verdict: z.string().min(1), // the recommendation + core reason (idea: 2-5 sentences; grounded in adjudicated + consensus claims)
|
|
291
805
|
recommendation: Recommendation.optional(), // idea workflow; code-review omits it
|
|
292
806
|
conditions: z.array(z.string().min(1)).max(6).optional(), // present only for PROCEED_WITH_CONDITIONS
|
|
807
|
+
recommendation_claim_ids: z.array(z.string().min(1)).min(1).max(8).optional(),
|
|
808
|
+
condition_claim_ids: z.array(z.string().min(1)).min(1).max(8).optional(),
|
|
809
|
+
pivot: z.object({
|
|
810
|
+
changed_claim_id: z.string().min(1),
|
|
811
|
+
new_risk_claim_id: z.string().min(1),
|
|
812
|
+
}).strict().optional(),
|
|
813
|
+
strongest_counter_case: z.object({
|
|
814
|
+
claim_ids: z.array(z.string().min(1)).min(1).max(4),
|
|
815
|
+
reasoning: z.string().min(1),
|
|
816
|
+
}).strict().optional(),
|
|
817
|
+
decision_snapshot: DecisionSnapshot.optional(),
|
|
293
818
|
key_points: z.array(z.string()).max(10).optional(), // chairman's bulleted reasoning (idea workflow); code-review omits it
|
|
294
819
|
dissent: z.array(z.string()).min(1), // ≥1 — empty dissent is invalid (§9); strongest counter-argument
|
|
295
820
|
confidence_notes: z.string().min(1), // which conclusions are HIGH/MEDIUM/LOW and why
|
|
@@ -319,7 +844,61 @@ export const JudgeReport = JudgeReportBase.superRefine((r, ctx) => {
|
|
|
319
844
|
export const JudgeReportModel = JudgeReportBase.extend({
|
|
320
845
|
dissent: z.array(z.string()),
|
|
321
846
|
});
|
|
847
|
+
/** R5 idea-chair boundary. Persisted JudgeReport keeps its legacy adjudication names so code-review
|
|
848
|
+
* and old run readers stay compatible; S9 translates only after this exact model shape validates. */
|
|
849
|
+
export const IdeaChairRuling = z.object({
|
|
850
|
+
claim_id: z.string().min(1),
|
|
851
|
+
ruling: z.enum(['HOLDS', 'FAILS', 'UNRESOLVED']),
|
|
852
|
+
reasoning: z.string().min(1),
|
|
853
|
+
evidence_ids: z.array(z.string().min(1)).min(1),
|
|
854
|
+
effect_on_decision: z.string().min(1),
|
|
855
|
+
what_would_change_it: z.string().min(1).optional(),
|
|
856
|
+
}).strict().superRefine((ruling, ctx) => {
|
|
857
|
+
if (ruling.ruling === 'UNRESOLVED' && !ruling.what_would_change_it) {
|
|
858
|
+
ctx.addIssue({
|
|
859
|
+
code: z.ZodIssueCode.custom,
|
|
860
|
+
path: ['what_would_change_it'],
|
|
861
|
+
message: 'UNRESOLVED requires what_would_change_it',
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
export const IdeaChairReportModel = JudgeReportModel.omit({ adjudications: true }).extend({
|
|
866
|
+
adjudications: z.array(IdeaChairRuling),
|
|
867
|
+
});
|
|
322
868
|
// ── S9b: ActionPlan (idea-refinement report v3) ─────────────────────────────
|
|
869
|
+
export const FeatureBacklog = z.object({
|
|
870
|
+
must: z.array(z.object({
|
|
871
|
+
feature: z.string().min(1),
|
|
872
|
+
user_value: z.string().min(1),
|
|
873
|
+
rationale: z.string().min(1),
|
|
874
|
+
effort: z.enum(['S', 'M', 'L']),
|
|
875
|
+
}).strict()).min(1).max(6),
|
|
876
|
+
should: z.array(z.object({
|
|
877
|
+
feature: z.string().min(1),
|
|
878
|
+
user_value: z.string().min(1),
|
|
879
|
+
rationale: z.string().min(1),
|
|
880
|
+
effort: z.enum(['S', 'M', 'L']),
|
|
881
|
+
}).strict()).max(6),
|
|
882
|
+
later: z.array(z.object({
|
|
883
|
+
feature: z.string().min(1),
|
|
884
|
+
user_value: z.string().min(1),
|
|
885
|
+
rationale: z.string().min(1),
|
|
886
|
+
effort: z.enum(['S', 'M', 'L']),
|
|
887
|
+
}).strict()).max(6),
|
|
888
|
+
wont: z.array(z.object({
|
|
889
|
+
feature: z.string().min(1),
|
|
890
|
+
reason: z.string().min(1),
|
|
891
|
+
}).strict()).max(6),
|
|
892
|
+
}).strict();
|
|
893
|
+
export const ImplementationPlan = z.object({
|
|
894
|
+
milestones: z.array(z.object({
|
|
895
|
+
order: z.number().int().min(1),
|
|
896
|
+
timebox: z.string().min(1),
|
|
897
|
+
outcome: z.string().min(1),
|
|
898
|
+
tasks: z.array(z.string().min(1)).min(1).max(6),
|
|
899
|
+
acceptance_test: z.string().min(1),
|
|
900
|
+
}).strict()).min(1).max(7),
|
|
901
|
+
}).strict();
|
|
323
902
|
export const ActionPlan = z
|
|
324
903
|
.object({
|
|
325
904
|
actions: z
|
|
@@ -336,8 +915,35 @@ export const ActionPlan = z
|
|
|
336
915
|
.min(1)
|
|
337
916
|
.max(7),
|
|
338
917
|
sequencing_note: z.string().min(1),
|
|
918
|
+
feature_backlog: FeatureBacklog.optional(),
|
|
919
|
+
implementation_plan: ImplementationPlan.optional(),
|
|
339
920
|
})
|
|
340
921
|
.strict();
|
|
922
|
+
export const PlannerUnavailable = z.object({
|
|
923
|
+
kind: z.literal('PlannerUnavailable'),
|
|
924
|
+
reason: z.enum(['budget_exhausted', 'planner_failed']),
|
|
925
|
+
unresolved_questions: z.array(z.string().min(1)).min(1).max(10),
|
|
926
|
+
}).strict();
|
|
927
|
+
export const ActionPlanArtifact = z.union([ActionPlan, PlannerUnavailable]);
|
|
928
|
+
/** R6 quick mode: one strong analyst produces the analysis, recommendation, and plan in one call. */
|
|
929
|
+
export const QuickDecisionModel = z.object({
|
|
930
|
+
analysis: IdeaRoleOutputModel,
|
|
931
|
+
verdict: z.string().min(1),
|
|
932
|
+
recommendation: Recommendation,
|
|
933
|
+
conditions: z.array(z.string().min(1)).max(6),
|
|
934
|
+
key_points: z.array(z.string().min(1)).min(2).max(8),
|
|
935
|
+
dissent: z.array(z.string().min(1)).min(1).max(4),
|
|
936
|
+
confidence_notes: z.string().min(1),
|
|
937
|
+
action_plan: ActionPlan,
|
|
938
|
+
}).strict().superRefine((decision, ctx) => {
|
|
939
|
+
const hasConditions = decision.conditions.length > 0;
|
|
940
|
+
if (decision.recommendation === 'PROCEED_WITH_CONDITIONS' && !hasConditions) {
|
|
941
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['conditions'], message: 'conditions are required for PROCEED_WITH_CONDITIONS' });
|
|
942
|
+
}
|
|
943
|
+
if (decision.recommendation !== 'PROCEED_WITH_CONDITIONS' && hasConditions) {
|
|
944
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['conditions'], message: 'conditions are only valid for PROCEED_WITH_CONDITIONS' });
|
|
945
|
+
}
|
|
946
|
+
});
|
|
341
947
|
// ── RunMeta (§15, §16) ──────────────────────────────────────────────────────
|
|
342
948
|
//
|
|
343
949
|
// Written by the artifact writer; assembled by the engine's RunCtx (T5). Internal → not strict.
|
|
@@ -345,6 +951,7 @@ export const ActionPlan = z
|
|
|
345
951
|
export const CallRecord = z.object({
|
|
346
952
|
provider: ProviderIdSchema,
|
|
347
953
|
stage: z.string(), // e.g. "S4", "S1"
|
|
954
|
+
category: z.enum(['discovery', 'verification', 'repair', 'planning']).optional(),
|
|
348
955
|
durationMs: z.number().nonnegative(),
|
|
349
956
|
error: z.enum(['NOT_FOUND', 'AUTH', 'QUOTA', 'TIMEOUT', 'BAD_OUTPUT', 'CRASH']).optional(),
|
|
350
957
|
});
|
|
@@ -360,6 +967,7 @@ const FlagProfileSchema = z.object({
|
|
|
360
967
|
export const RunMeta = z.object({
|
|
361
968
|
run_id: z.string().min(1), // encodes the timestamp (e.g. 20260702-1412-idea-refinement-a3f9)
|
|
362
969
|
workflow: WorkflowIdSchema,
|
|
970
|
+
mode: IdeaModeSchema.optional(),
|
|
363
971
|
provider_versions: z.record(ProviderIdSchema, z.string()), // detected `--version` strings
|
|
364
972
|
flag_profiles: z.record(ProviderIdSchema, FlagProfileSchema),
|
|
365
973
|
roles: z.record(z.string(), ProviderIdSchema), // role name → assigned provider
|
|
@@ -367,8 +975,23 @@ export const RunMeta = z.object({
|
|
|
367
975
|
calls: z.array(CallRecord),
|
|
368
976
|
call_count: z.number().int().nonnegative(),
|
|
369
977
|
budget: z.object({ limit: z.number().int().positive(), used: z.number().int().nonnegative() }),
|
|
978
|
+
receipt: z.object({
|
|
979
|
+
discovery: z.number().int().nonnegative(),
|
|
980
|
+
verification: z.number().int().nonnegative(),
|
|
981
|
+
repair: z.number().int().nonnegative(),
|
|
982
|
+
planning: z.number().int().nonnegative(),
|
|
983
|
+
}).optional(),
|
|
370
984
|
exit_status: z.enum(['ok', 'failed', 'aborted', 'partial']),
|
|
371
985
|
aborted: z.boolean(), // §16: Ctrl+C finalizes meta with aborted:true
|
|
372
986
|
// §16 report-header flags; absent = none.
|
|
373
|
-
flags: z.array(z.enum([
|
|
987
|
+
flags: z.array(z.enum([
|
|
988
|
+
'synthesis_suspect',
|
|
989
|
+
'low_diversity',
|
|
990
|
+
'plan_skipped',
|
|
991
|
+
'plan_fallback',
|
|
992
|
+
'headless_intent',
|
|
993
|
+
'verification_skipped',
|
|
994
|
+
'research_ungrounded',
|
|
995
|
+
'single_model',
|
|
996
|
+
])).optional(),
|
|
374
997
|
});
|