aiki-cli 0.3.1 → 0.3.2

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 (31) hide show
  1. package/CHANGELOG.md +12 -5
  2. package/README.md +9 -11
  3. package/dist/bench/idea-v3-rating.js +1 -1
  4. package/dist/cli/index.js +1 -1
  5. package/dist/cli/run.js +1 -1
  6. package/dist/council/view.js +52 -12
  7. package/dist/orchestration/context.js +3 -3
  8. package/dist/orchestration/decision-dossier.js +417 -88
  9. package/dist/orchestration/decision-graph.js +31 -0
  10. package/dist/orchestration/legacy-idea-adapter.js +3 -0
  11. package/dist/orchestration/modes.js +15 -9
  12. package/dist/orchestration/preflight.js +17 -3
  13. package/dist/orchestration/quick-analysis.js +21 -4
  14. package/dist/orchestration/stages/s10-render.js +167 -79
  15. package/dist/orchestration/stages/s4-analyze.js +3 -1
  16. package/dist/orchestration/stages/s6-positions.js +18 -0
  17. package/dist/orchestration/stages/s7-decision-graph.js +3 -0
  18. package/dist/orchestration/stages/s8-verify.js +26 -9
  19. package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
  20. package/dist/orchestration/stages/s9-judge.js +36 -13
  21. package/dist/orchestration/stages/s9b-plan.js +192 -42
  22. package/dist/schemas/index.js +67 -3
  23. package/dist/skills/idea-refinement/analyst.md +5 -5
  24. package/dist/skills/idea-refinement/chair.md +18 -0
  25. package/dist/skills/idea-refinement/economics-delivery.md +11 -0
  26. package/dist/skills/idea-refinement/market-adoption.md +12 -0
  27. package/dist/skills/idea-refinement/planner.md +25 -19
  28. package/dist/skills/idea-refinement/rebuttal.md +15 -0
  29. package/dist/skills/idea-refinement/verifier.md +17 -0
  30. package/dist/workflows/idea-refinement.js +42 -14
  31. package/package.json +1 -1
@@ -12,33 +12,62 @@ function clipClaim(text, max = 96) {
12
12
  const boundary = clipped.lastIndexOf(' ');
13
13
  return `${clipped.slice(0, boundary > max * 0.65 ? boundary : max - 1).trimEnd()}…`;
14
14
  }
15
+ /** ≤60-char noun-ish handle for a claim: drop a leading "Verdict:", keep the first clause, clip at a word. */
16
+ export function claimShortLabel(text, max = 60) {
17
+ const base = text.replace(/^\s*Verdict:\s*/i, '').split(/(?<=\S)[;—]|(?<=\.)\s/)[0].trim();
18
+ if (base.length <= max)
19
+ return base;
20
+ const clipped = base.slice(0, max);
21
+ const boundary = clipped.lastIndexOf(' ');
22
+ return `${clipped.slice(0, boundary > max * 0.6 ? boundary : max).trimEnd()}…`;
23
+ }
24
+ /** id → claim text lookup for stripReaderClaimIds substitution; shared by readerClaimLabel's fallback and the Markdown/HTML renderers. */
25
+ export function claimLookup(report) {
26
+ return (id) => report.claims.find((claim) => claim.id === id)?.text ?? null;
27
+ }
15
28
  /** Human-readable labels for reader-facing evidence links. Raw ids stay in the technical audit/JSON. */
16
29
  export function readerClaimLabel(report, id) {
17
30
  const claim = report.claims.find((item) => item.id === id);
18
31
  if (claim)
19
- return clipClaim(claim.text);
20
- return /^G\d+$/.test(id) ? 'Supporting claim' : clipClaim(stripReaderClaimIds(id));
32
+ return claimShortLabel(claim.text);
33
+ return /^G\d+$/.test(id) ? 'Supporting claim' : clipClaim(stripReaderClaimIds(id, claimLookup(report)));
21
34
  }
22
35
  export function readerClaimRefs(report, ids) {
23
36
  return ids.length ? ids.map((id) => readerClaimLabel(report, id)).join('; ') : 'none recorded';
24
37
  }
25
- /** Remove internal graph notation from prose without expanding a short id into a paragraph. */
26
- export function stripReaderClaimIds(text) {
27
- return text
28
- .replace(/\s*\((?:\s*`?G\d+`?\s*,?)+\s*\)/g, '')
29
- .replace(/\bG\d+\s+assumes\b/g, 'This assumes')
30
- .replace(/\bG\d+\b/g, '')
31
- .replace(/\(\s*\)/g, '')
32
- .replace(/\s+([,.;:])/g, '$1')
33
- .replace(/[ \t]{2,}/g, ' ')
34
- .trim();
38
+ /** Remove or substitute internal graph notation in reader prose. With a lookup, ids become quoted short labels
39
+ * instead of vanishing (never bare-delete); without one (no lookup available), ids are dropped — the legacy
40
+ * behavior, kept only for callers that have no claim table to look up against. `sanitizeLabel` escapes each
41
+ * injected label for the caller's output format (markdown passes `cell`; HTML passes nothing — raw). */
42
+ export function stripReaderClaimIds(text, labelFor, sanitizeLabel = (label) => label) {
43
+ const label = (id) => {
44
+ const hit = labelFor?.(id);
45
+ return hit ? `"${sanitizeLabel(claimShortLabel(hit))}"` : 'a related claim';
46
+ };
47
+ let out = text
48
+ // whole parenthetical citation groups — comma OR slash separated — vanish entirely: (G19), (G7/G13), (G1, G2)
49
+ .replace(/\s*\((?:\s*`?G\d+`?\s*[,/]?\s*)+\)/g, '')
50
+ // "G21's kill criteria" → `"the spike kill-gate"'s kill criteria` — keep the possessive on the label itself
51
+ // rather than injecting filler noun text, so it reads correctly for any noun that follows.
52
+ .replace(/\bG(\d+)'s\b/g, (_, n) => (labelFor ? `${label(`G${n}`)}'s` : ''));
53
+ out = labelFor
54
+ ? out.replace(/\bG\d+\b/g, (id) => label(id))
55
+ : out.replace(/\bG\d+\s+assumes\b/g, 'This assumes').replace(/\bG\d+\b/g, '');
56
+ return out.replace(/\s{2,}/g, ' ').replace(/\s+([.,;:])/g, '$1').trim();
35
57
  }
36
58
  function providerName(id) {
37
59
  return id in DISPLAY_NAME ? DISPLAY_NAME[id] : id;
38
60
  }
61
+ const FLAG_EXPLAIN = {
62
+ synthesis_suspect: 'the chair output needed a deterministic repair; phrasing may be less reliable than the underlying graph',
63
+ headless_intent: 'no human confirmed the interpretation; documented defaults were used',
64
+ weak_seat: 'one scout seat contributed far less evidenced material than the other; treat convergence cautiously',
65
+ source_fallback_search: 'a supplied page blocked direct reading; the source-investigation seat searched for accessible sources instead',
66
+ deliverable_gap: 'one or more surviving scouts omitted a requested feature or implementation proposal',
67
+ };
39
68
  function degradation(report, flags) {
40
69
  const active = flags.filter((flag) => report.flags.includes(flag));
41
- return active.length ? [`> ⚠ DEGRADED: ${active.join(', ')}`, ''] : [];
70
+ return active.flatMap((flag) => [`> ⚠ DEGRADED: ${flag}${FLAG_EXPLAIN[flag] ? ` — ${FLAG_EXPLAIN[flag]}.` : ''}`, '']);
42
71
  }
43
72
  function pct(value) {
44
73
  return `${Math.round(value * 100)}%`;
@@ -56,12 +85,239 @@ function councilRead(report) {
56
85
  const resolved = report.disagreements.filter((item) => item.status === 'RESOLVED').length;
57
86
  return `${report.disagreements.length} genuine disagreement${report.disagreements.length === 1 ? '' : 's'} reached the chair; ${resolved} ${resolved === 1 ? 'was' : 'were'} resolved.`;
58
87
  }
59
- /** Canonical R7 Markdown. HTML and Copy-Markdown consume the same persisted dossier object. */
60
- export function renderDecisionDossierMarkdown(report) {
88
+ export function sanitizeReaderText(text, labelFor) {
89
+ return stripReaderClaimIds(text, labelFor)
90
+ .replace(/\b(?:G|C|D)\d+\b/g, '')
91
+ .replace(/\bUNVERIFIABLE\b/gi, 'not independently confirmed')
92
+ .replace(/\bUNVERIFIED\b/gi, 'not yet confirmed')
93
+ .replace(/\bPARTIAL\b/gi, 'partly supported')
94
+ .replace(/\bCONFLICTED\b/gi, 'supported by conflicting evidence')
95
+ .replace(/\bstructural score\b/gi, 'confidence estimate')
96
+ .replace(/\bevidence coverage\b/gi, 'source strength')
97
+ .replace(/\bverification coverage\b/gi, 'source strength')
98
+ .replace(/\b(?:claim|graph) ids?\b/gi, 'internal references')
99
+ .replace(/\b(?:provider|model) calls?\b/gi, 'analysis steps')
100
+ .replace(/\banswer editor\b/gi, 'answer synthesis')
101
+ .replace(/\breader_brief\b/gi, 'reader summary')
102
+ .replace(/\s{2,}/g, ' ')
103
+ .replace(/\s+([.,;:])/g, '$1')
104
+ .trim();
105
+ }
106
+ export function cleanReaderText(report, text) {
107
+ return sanitizeReaderText(text, claimLookup(report));
108
+ }
109
+ const MATERIAL_WARNING_ORDER = [
110
+ 'synthesis_suspect', 'low_diversity', 'weak_seat', 'deliverable_gap', 'plan_skipped',
111
+ 'plan_fallback', 'headless_intent', 'verification_skipped', 'research_ungrounded',
112
+ ];
113
+ const READER_WARNING = {
114
+ synthesis_suspect: 'The final synthesis needed repair; treat its wording cautiously.',
115
+ low_diversity: 'Independent diversity was reduced because one scout seat had to be resampled.',
116
+ weak_seat: 'One scout contributed much less evidenced material; treat convergence cautiously.',
117
+ deliverable_gap: 'At least one scout omitted a requested feature or implementation proposal.',
118
+ plan_skipped: 'The answer-planning step was skipped because its call budget was unavailable.',
119
+ plan_fallback: 'The answer planner failed validation; this report uses a deterministic fallback.',
120
+ headless_intent: 'No person confirmed the interpreted request; documented defaults were used.',
121
+ verification_skipped: 'Independent verification was skipped; important factual claims may remain unchecked.',
122
+ research_ungrounded: 'Source investigation did not produce a cited public source; treat current claims as ungrounded.',
123
+ };
124
+ export function isDecisionSnapshotRelevant(snapshot) {
125
+ if (!snapshot)
126
+ return false;
127
+ const allUnknown = snapshot.options.every((option) => option.commitmentKind === 'UNKNOWN');
128
+ const paybackUnavailable = !snapshot.payback || snapshot.payback.status === 'NOT_COMPUTABLE';
129
+ return !(allUnknown && paybackUnavailable);
130
+ }
131
+ function safeHttpUrl(raw) {
132
+ if (!raw)
133
+ return undefined;
134
+ try {
135
+ const url = new URL(raw);
136
+ return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : undefined;
137
+ }
138
+ catch {
139
+ return undefined;
140
+ }
141
+ }
142
+ function looksLocalPath(value) {
143
+ if (!value)
144
+ return false;
145
+ return /^file:/i.test(value)
146
+ || /^(?:\/|\.\.?[\\/]|[A-Za-z]:[\\/])/.test(value)
147
+ || /^(?:[^:/\\]+[\\/])+[^/\\]+$/.test(value);
148
+ }
149
+ /** One cleaned reader contract consumed by Markdown, HTML, and terminal renderers. */
150
+ export function buildReaderProjection(report) {
151
+ const brief = report.dossier.readerBrief;
152
+ const clean = (text) => cleanReaderText(report, text);
153
+ const sources = [];
154
+ const sourceIndexByKey = new Map();
155
+ for (const id of brief.source_ids) {
156
+ const source = report.dossier.evidence.find((item) => item.id === id);
157
+ if (!source)
158
+ continue;
159
+ const url = safeHttpUrl(source.url) ?? safeHttpUrl(source.source);
160
+ const title = source.title && !looksLocalPath(source.title) ? clean(source.title) : undefined;
161
+ const label = title ?? (url ? url
162
+ : source.sourceKind === 'MODEL_KNOWLEDGE' ? 'Background model knowledge'
163
+ : source.sourceKind === 'USER' || looksLocalPath(source.source) ? 'User-supplied material' : 'Source material');
164
+ const key = url ? `url:${url.toLowerCase()}` : title ? `id:${source.id}` : `label:${label.toLowerCase()}`;
165
+ const citedFor = [...new Set(source.claimIds.flatMap((claimId) => {
166
+ const claim = report.claims.find((item) => item.id === claimId);
167
+ return claim ? [claimShortLabel(clean(claim.text))] : [];
168
+ }))].slice(0, 3);
169
+ const existingIndex = sourceIndexByKey.get(key);
170
+ if (existingIndex !== undefined) {
171
+ sources[existingIndex].citedFor = [...new Set([...sources[existingIndex].citedFor, ...citedFor])].slice(0, 3);
172
+ continue;
173
+ }
174
+ sourceIndexByKey.set(key, sources.length);
175
+ sources.push({ id: source.id, label, ...(url ? { url } : {}), citedFor });
176
+ }
177
+ const cleanBacklog = report.dossier.featureBacklog ? {
178
+ must: report.dossier.featureBacklog.must.map((item) => ({ ...item, feature: clean(item.feature), user_value: clean(item.user_value), rationale: clean(item.rationale) })),
179
+ should: report.dossier.featureBacklog.should.map((item) => ({ ...item, feature: clean(item.feature), user_value: clean(item.user_value), rationale: clean(item.rationale) })),
180
+ later: report.dossier.featureBacklog.later.map((item) => ({ ...item, feature: clean(item.feature), user_value: clean(item.user_value), rationale: clean(item.rationale) })),
181
+ wont: report.dossier.featureBacklog.wont.map((item) => ({ feature: clean(item.feature), reason: clean(item.reason) })),
182
+ } : undefined;
183
+ const cleanPlan = report.dossier.implementationPlan ? {
184
+ milestones: report.dossier.implementationPlan.milestones.map((item) => ({
185
+ ...item,
186
+ timebox: clean(item.timebox),
187
+ outcome: clean(item.outcome),
188
+ tasks: item.tasks.map(clean),
189
+ acceptance_test: clean(item.acceptance_test),
190
+ })),
191
+ } : undefined;
192
+ const snapshot = isDecisionSnapshotRelevant(report.decisionSnapshot) ? {
193
+ decisiveNumbers: report.decisionSnapshot.decisiveNumbers.map((item) => ({ label: clean(item.label), value: clean(item.value), meaning: clean(item.meaning) })),
194
+ ...(report.decisionSnapshot.payback ? { payback: {
195
+ status: report.decisionSnapshot.payback.status,
196
+ result: clean(report.decisionSnapshot.payback.result),
197
+ basis: clean(report.decisionSnapshot.payback.basis),
198
+ } } : {}),
199
+ options: report.decisionSnapshot.options.map((item) => ({
200
+ label: clean(item.label), commitment: clean(item.commitment), commitmentKind: item.commitmentKind, tradeoff: clean(item.tradeoff),
201
+ })),
202
+ ...(report.decisionSnapshot.tripwire ? { tripwire: {
203
+ metric: clean(report.decisionSnapshot.tripwire.metric),
204
+ threshold: clean(report.decisionSnapshot.tripwire.threshold),
205
+ decisionRule: clean(report.decisionSnapshot.tripwire.decisionRule),
206
+ } } : {}),
207
+ } : undefined;
208
+ return {
209
+ headline: clean(brief.headline),
210
+ bottomLine: clean(brief.bottom_line),
211
+ sections: brief.sections.map((section) => ({ heading: clean(section.heading), summary: clean(section.summary), bullets: section.bullets.map(clean) })),
212
+ featureBacklog: cleanBacklog,
213
+ implementationPlan: cleanPlan,
214
+ caveats: brief.caveats.map(clean),
215
+ warnings: MATERIAL_WARNING_ORDER.filter((flag) => report.flags.includes(flag)).map((flag) => ({ flag, message: READER_WARNING[flag] })),
216
+ notices: report.flags.includes('source_fallback_search')
217
+ ? [{ flag: 'source_fallback_search', message: 'Search attempted: a supplied page could not be read directly, so the source-investigation scout searched for accessible sources.' }]
218
+ : [],
219
+ snapshot,
220
+ sources,
221
+ nextStep: clean(brief.next_step),
222
+ };
223
+ }
224
+ function renderReaderBriefMarkdown(report) {
225
+ const projection = buildReaderProjection(report);
226
+ const L = [`# ${projection.headline}`, '', projection.bottomLine, ''];
227
+ for (const warning of projection.warnings)
228
+ L.push(`> ⚠ ${warning.message}`, '');
229
+ for (const notice of projection.notices)
230
+ L.push(`> ℹ ${notice.message}`, '');
231
+ if (projection.snapshot) {
232
+ L.push('## Decision numbers', '', '### Decisive numbers', '', '| Metric | Value | What it means |', '|---|---:|---|');
233
+ for (const item of projection.snapshot.decisiveNumbers)
234
+ L.push(`| ${cell(item.label)} | ${cell(item.value)} | ${cell(item.meaning)} |`);
235
+ if (projection.snapshot.payback) {
236
+ L.push('', `**Payback — ${projection.snapshot.payback.status.replaceAll('_', ' ')}:** ${projection.snapshot.payback.result}`);
237
+ L.push(`Basis: ${projection.snapshot.payback.basis}`, '');
238
+ }
239
+ else
240
+ L.push('');
241
+ L.push('### Options at a glance', '', '| Path | Commitment | Basis | Trade-off |', '|---|---:|---|---|');
242
+ for (const option of projection.snapshot.options) {
243
+ L.push(`| ${cell(option.label)} | ${cell(option.commitment)} | ${option.commitmentKind.replace('_', ' ')} | ${cell(option.tradeoff)} |`);
244
+ }
245
+ if (projection.snapshot.tripwire) {
246
+ const tripwire = projection.snapshot.tripwire;
247
+ L.push('', '### Go/no-go tripwire', '', `**${tripwire.metric}: ${tripwire.threshold}** — ${tripwire.decisionRule}`, '');
248
+ }
249
+ else
250
+ L.push('');
251
+ }
252
+ for (const section of projection.sections) {
253
+ L.push(`## ${section.heading}`, '', section.summary, '');
254
+ for (const bullet of section.bullets)
255
+ L.push(`- ${bullet}`);
256
+ if (section.bullets.length)
257
+ L.push('');
258
+ }
259
+ if (projection.featureBacklog) {
260
+ L.push('## Feature priorities', '', '| Priority | Feature | User value | Why now | Effort |', '|---|---|---|---|---|');
261
+ for (const [priority, items] of [
262
+ ['MUST', projection.featureBacklog.must],
263
+ ['SHOULD', projection.featureBacklog.should],
264
+ ['LATER', projection.featureBacklog.later],
265
+ ]) {
266
+ for (const item of items) {
267
+ L.push(`| ${priority} | ${cell(item.feature)} | ${cell(item.user_value)} | ${cell(item.rationale)} | ${item.effort} |`);
268
+ }
269
+ }
270
+ if (projection.featureBacklog.wont.length) {
271
+ L.push('', '**Not in this scope**', '');
272
+ for (const item of projection.featureBacklog.wont)
273
+ L.push(`- **${item.feature}:** ${item.reason}`);
274
+ }
275
+ L.push('');
276
+ }
277
+ if (projection.implementationPlan) {
278
+ L.push('## Build plan', '', '| # | Timebox | Outcome | Work | Done when |', '|---|---|---|---|---|');
279
+ for (const milestone of projection.implementationPlan.milestones) {
280
+ L.push(`| ${milestone.order} | ${cell(milestone.timebox)} | ${cell(milestone.outcome)} | ${cell(milestone.tasks.join('; '))} | ${cell(milestone.acceptance_test)} |`);
281
+ }
282
+ L.push('');
283
+ }
284
+ if (projection.caveats.length) {
285
+ L.push('## Top caveats', '');
286
+ for (const caveat of projection.caveats)
287
+ L.push(`- ${caveat}`);
288
+ L.push('');
289
+ }
290
+ if (projection.sources.length) {
291
+ L.push('## Sources', '');
292
+ for (const source of projection.sources) {
293
+ const citationScope = source.citedFor.length ? ` — Cited for: ${source.citedFor.join('; ')}` : '';
294
+ L.push(`${source.url ? `- [${source.label.replace(/[\[\]]/g, '')}](${source.url})` : `- ${source.label}`}${citationScope}`);
295
+ }
296
+ L.push('');
297
+ }
298
+ L.push('## Next step', '', projection.nextStep, '');
299
+ const legacy = renderLegacyDecisionDossierMarkdown(report).split('\n').slice(2).join('\n');
300
+ L.push('## Council audit', '', '<details>', '<summary>Show the council reasoning, evidence ledger, dissent, and run receipt</summary>', '', legacy, '', '</details>', '');
301
+ return L.join('\n');
302
+ }
303
+ /** Pre-v5 deterministic report, retained as the replay fallback and the collapsed v5 audit body. */
304
+ function renderLegacyDecisionDossierMarkdown(report) {
61
305
  const { dossier } = report;
306
+ const labelFor = claimLookup(report);
62
307
  const keyFindings = report.keyFindings?.length ? report.keyFindings : [dossier.recommendation.reason];
63
308
  const criticalUnknowns = report.criticalUnknowns?.length ? report.criticalUnknowns : report.openQuestions.slice(0, 3);
64
309
  const coverage = report.confidenceBreakdown.verificationCoverage;
310
+ const allConditions = [...new Set(dossier.recommendation.conditions.map((condition) => stripReaderClaimIds(condition.text, labelFor)))];
311
+ const conditions = allConditions.slice(0, 4);
312
+ const verdictClaimId = report.claims.find((claim) => /^Verdict:/i.test(claim.text))?.id;
313
+ const seenQuestions = new Set();
314
+ const openQuestions = report.openQuestions.filter((question) => {
315
+ const key = question.slice(0, 60).toLowerCase();
316
+ if (seenQuestions.has(key))
317
+ return false;
318
+ seenQuestions.add(key);
319
+ return true;
320
+ });
65
321
  const L = [report.mode === 'quick' ? '# Single-Model Decision Report' : '# Multi-Model Decision Report', ''];
66
322
  L.push('## 1. Decision', '', ...degradation(report, ['synthesis_suspect']));
67
323
  if (report.decisionSnapshot) {
@@ -78,7 +334,9 @@ export function renderDecisionDossierMarkdown(report) {
78
334
  L.push('');
79
335
  }
80
336
  L.push(`**Recommendation:** ${dossier.recommendation.summary}`, '');
81
- L.push(`**Evidence coverage:** ${coverageLabel(coverage)} — ${pct(coverage)} of load-bearing claims independently verified.`);
337
+ L.push(report.confidenceBreakdown.verificationScope === 'FACTUAL'
338
+ ? `**Evidence coverage:** ${coverageLabel(coverage)} — ${pct(coverage)} of checkable factual claims independently verified; design judgments are adjudicated by the chair, not verified.`
339
+ : `**Evidence coverage:** ${coverageLabel(coverage)} — ${pct(coverage)} of load-bearing claims independently verified.`);
82
340
  L.push(coverage < 0.5
83
341
  ? '> Low coverage means important inputs remain unchecked; it is not a probability that the recommendation is correct.'
84
342
  : '> Evidence coverage measures independent checking; it is not a probability that the recommendation is correct.', '');
@@ -107,21 +365,10 @@ export function renderDecisionDossierMarkdown(report) {
107
365
  L.push('- None recorded.');
108
366
  L.push('', `**Critical warning:** ${report.verdict.criticalWarning ?? 'None recorded.'}`);
109
367
  L.push(`**Council read:** ${councilRead(report)}`);
110
- L.push(`Evidence behind the recommendation: ${readerClaimRefs(report, dossier.recommendation.claimIds)}`, '');
368
+ L.push('');
111
369
  if (!dossier.recommendation.claimIds.length)
112
370
  L.push('> ⚠ DEGRADED: recommendation has no stored graph anchor.', '');
113
- L.push('<details>', '<summary>Decision conditions and technical confidence</summary>', '');
114
- L.push(`- Decision state: ${dossier.recommendation.status}`);
115
- L.push(`- Structural score: ${report.confidenceBreakdown.score}/100 (${report.confidenceBreakdown.label}); heuristic, not benchmark-calibrated.`);
116
- L.push(`- Basis: verification ${pct(coverage)} · independent convergence ${pct(report.confidenceBreakdown.independentConvergence)} · evidence quality ${pct(report.confidenceBreakdown.evidenceQuality)} · stability ${pct(report.confidenceBreakdown.stability)} · critical-risk penalty −${report.confidenceBreakdown.criticalRiskPenalty}`);
117
- if (dossier.recommendation.conditions.length) {
118
- L.push('', 'Conditions:');
119
- for (const condition of dossier.recommendation.conditions) {
120
- L.push(`- ${condition.text}`);
121
- }
122
- }
123
- L.push('', '</details>', '');
124
- L.push('## 2. Action plan', '', ...degradation(report, ['plan_fallback', 'plan_skipped']));
371
+ L.push('## 2. Deliverables and action plan', '', ...degradation(report, ['plan_fallback', 'plan_skipped']));
125
372
  if ((dossier.missingRequestedOutputs ?? []).length) {
126
373
  L.push(`> ⚠ DEGRADED: requested output missing: ${dossier.missingRequestedOutputs.join(', ')}`, '');
127
374
  }
@@ -163,66 +410,58 @@ export function renderDecisionDossierMarkdown(report) {
163
410
  L.push('No executable experiment was produced.');
164
411
  L.push('');
165
412
  L.push('## 3. Why this decision', '');
166
- if (dossier.claimChain.length) {
167
- L.push('| Claim | Ruling | Evidence status | Depends on |', '|---|---|---|---|');
168
- for (const claim of dossier.claimChain) {
169
- L.push(`| ${cell(claim.text)} | ${claim.ruling} | ${claim.evidenceStatus} | ${cell(readerClaimRefs(report, claim.dependsOn))} |`);
170
- }
413
+ for (const point of keyFindings)
414
+ L.push(`- ${stripReaderClaimIds(point, labelFor)}`);
415
+ if (!keyFindings.length)
416
+ L.push('No chair reasoning was recorded.');
417
+ if (conditions.length) {
418
+ L.push('', '### Conditions', '');
419
+ for (const condition of conditions)
420
+ L.push(`- ${condition}`);
421
+ if (allConditions.length > conditions.length)
422
+ L.push('', `${allConditions.length - conditions.length} additional condition${allConditions.length - conditions.length === 1 ? '' : 's'} remain in the stored audit JSON.`);
171
423
  }
172
- else
173
- L.push('No graph-anchored decision chain was recorded.');
174
424
  L.push('');
175
425
  L.push('## 4. What could change the decision', '', '### Decision-sensitive facts', '');
176
- if (dossier.sensitivity.length) {
177
- L.push('| Fact | Sensitivity | If false | What would change it | Linked claims |', '|---|---|---|---|---|');
178
- for (const item of dossier.sensitivity) {
179
- L.push(`| ${cell(item.fact)} | ${item.sensitivity} | ${item.impactIfFalse} | ${cell(item.whatWouldChangeIt)} | ${cell(readerClaimRefs(report, item.linkedClaimIds))} |`);
426
+ const decisiveFacts = dossier.sensitivity.filter((item) => item.sensitivity === 'DECISIVE').slice(0, 5);
427
+ if (decisiveFacts.length) {
428
+ L.push('| Fact | If false | What settles it |', '|---|---|---|');
429
+ for (const item of decisiveFacts) {
430
+ L.push(`| ${cell(claimShortLabel(item.fact))} | ${item.impactIfFalse} | ${cell(item.whatWouldChangeIt)} |`);
180
431
  }
181
432
  }
182
433
  else
183
434
  L.push('No verdict-sensitive graph node was recorded.');
184
435
  L.push('', '### Strongest counter-case', '');
185
436
  if (dossier.counterCase.available) {
186
- L.push(dossier.counterCase.reasoning, '', `Evidence behind this counter-case: ${readerClaimRefs(report, dossier.counterCase.claimIds)}`);
437
+ const counterIds = dossier.counterCase.claimIds.filter((id) => id !== verdictClaimId);
438
+ L.push(dossier.counterCase.reasoning, '', `Evidence behind this counter-case: ${readerClaimRefs(report, counterIds)}`);
187
439
  }
188
440
  else
189
441
  L.push(`> ⚠ DEGRADED: ${dossier.counterCase.reasoning}`);
190
442
  L.push('');
191
- L.push('## 5. Evidence and verification', '', ...degradation(report, ['verification_skipped', 'research_ungrounded']));
192
- if (dossier.evidence.length) {
193
- L.push('| Evidence ID | Source | Date | Freshness | Verification | Linked claims |', '|---|---|---|---|---|---|');
194
- for (const evidence of dossier.evidence) {
195
- L.push(`| ${evidence.id} | ${cell(evidence.source)} (${evidence.sourceKind}) | ${evidence.date} | ${evidence.freshness} | ${evidence.verificationStatus} | ${cell(readerClaimRefs(report, evidence.claimIds))} |`);
196
- }
197
- }
198
- else
199
- L.push('No evidence cards were stored.');
200
- L.push('');
201
- L.push('## 6. Risks, gaps, and open questions', '', '### Risks', '');
443
+ L.push('## 5. Risks and open questions', '', ...degradation(report, ['verification_skipped', 'research_ungrounded']), '### Risks', '');
202
444
  if (report.risks.length) {
445
+ const shownRisks = report.risks.slice(0, 8);
203
446
  L.push('| Risk | Severity |', '|---|---|');
204
- for (const risk of report.risks)
447
+ for (const risk of shownRisks)
205
448
  L.push(`| ${cell(risk.risk)} | ${risk.severity} |`);
449
+ if (report.risks.length > shownRisks.length)
450
+ L.push('', `${report.risks.length - shownRisks.length} lower-severity items — more in the technical audit (full list in the stored JSON).`);
206
451
  }
207
452
  else
208
453
  L.push('No material risk was recorded.');
209
- L.push('', '### Coverage', '');
210
- if (dossier.coverage.length) {
211
- L.push('| Dimension | Status | Related claims |', '|---|---|---|');
212
- for (const item of dossier.coverage)
213
- L.push(`| ${cell(item.label)} | ${item.status} | ${cell(readerClaimRefs(report, item.claimIds))} |`);
214
- }
215
- else
216
- L.push('No rubric coverage ledger was recorded.');
217
454
  L.push('', '### Open questions', '');
218
- if (report.openQuestions.length) {
219
- for (const question of report.openQuestions)
455
+ if (openQuestions.length) {
456
+ for (const question of openQuestions.slice(0, 5))
220
457
  L.push(`- ${question}`);
458
+ if (openQuestions.length > 5)
459
+ L.push('', `Showing 5 of ${openQuestions.length} — the rest are in the technical audit.`);
221
460
  }
222
461
  else
223
462
  L.push('No verdict-flipping open question was recorded.');
224
463
  L.push('');
225
- L.push('## 7. Disagreement and dissent', '', ...degradation(report, ['single_model', 'low_diversity']));
464
+ L.push('## 6. Disagreement and dissent', '', ...degradation(report, ['single_model', 'low_diversity']));
226
465
  if (report.disagreements.length) {
227
466
  for (const disagreement of report.disagreements) {
228
467
  L.push(`- **${disagreement.topic}** — ${disagreement.status}; ${disagreement.ruling}.`);
@@ -253,30 +492,61 @@ export function renderDecisionDossierMarkdown(report) {
253
492
  for (const item of report.minority.uniqueOppositions)
254
493
  L.push(`- ${providerName(item.provider)} uniquely opposed: ${item.proposition}`);
255
494
  L.push(`- Decision-blocking status: ${report.minority.blocksDecision}`, '');
256
- L.push('## 8. What the council added', '');
257
- if (dossier.sharedConcerns.length) {
258
- L.push('Shared concerns:');
259
- for (const item of dossier.sharedConcerns)
260
- L.push(`- ${item.text} ${item.evidenceStatus}; ${item.providerIds.map(providerName).join(', ')}.`);
261
- }
262
- else
263
- L.push('Shared concerns: none recorded.');
264
- L.push('');
265
- if (dossier.uniqueSupportedInsights.length) {
266
- L.push('Unique supported insights:');
267
- for (const item of dossier.uniqueSupportedInsights)
268
- L.push(`- ${item.text} ${providerName(item.providerId)}; ${item.verificationStatus}.`);
495
+ L.push('## 7. What the council added', '', ...degradation(report, ['weak_seat']));
496
+ if (dossier.seatStats) {
497
+ const weakSeat = dossier.seatStats.some((seat) => seat.positions < 3 || seat.evidenced / seat.positions < 0.5);
498
+ for (const seat of dossier.seatStats) {
499
+ const weak = seat.positions < 3 || seat.evidenced / seat.positions < 0.5;
500
+ L.push(`- ${providerName(seat.provider)}: ${seat.positions} position${seat.positions === 1 ? '' : 's'}, ${seat.evidenced} with evidence${weak ? ' — weak seat this run.' : '.'}`);
501
+ }
502
+ for (const chair of report.models.filter((model) => model.roles.includes('judge') && !dossier.seatStats.some((seat) => seat.provider === model.provider))) {
503
+ L.push(`- ${chair.name}: chaired the decision and authored no scout claims (clean adjudication).`);
504
+ }
505
+ if (dossier.sharedConcerns.length) {
506
+ L.push('', 'Shared concerns:');
507
+ for (const item of dossier.sharedConcerns)
508
+ L.push(`- ${item.text} — ${item.evidenceStatus}; ${item.providerIds.map(providerName).join(', ')}.`);
509
+ }
510
+ L.push('', report.disagreements.length
511
+ ? councilRead(report)
512
+ : 'No genuine disagreement survived to the chair.');
513
+ if (weakSeat || coverage < 0.5)
514
+ L.push('Convergence with weak verification lowers confidence; it does not raise it.');
515
+ const verifiedContributions = dossier.contributions.filter((item) => item.verifiedUniqueClaimIds.length > 0);
516
+ if (verifiedContributions.length) {
517
+ L.push('', '### Verified unique contributions', '', ...degradation(report, ['verification_skipped', 'single_model', 'low_diversity']));
518
+ L.push('Only unique claims that survived independent verification receive credit.', '');
519
+ L.push('| Provider | Verified unique contributions | Count |', '|---|---|---|');
520
+ for (const contribution of verifiedContributions) {
521
+ L.push(`| ${contribution.name} | ${cell(readerClaimRefs(report, contribution.verifiedUniqueClaimIds))} | ${contribution.verifiedUniqueClaimIds.length} |`);
522
+ }
523
+ }
269
524
  }
270
- else
271
- L.push('Unique supported insights: none recorded.');
272
- L.push('', '### Verified unique contributions', '', ...degradation(report, ['verification_skipped', 'single_model', 'low_diversity']));
273
- L.push('Only unique claims that survived independent verification receive credit.', '');
274
- L.push('| Provider | Verified unique contributions | Count |', '|---|---|---|');
275
- for (const contribution of dossier.contributions) {
276
- L.push(`| ${contribution.name} | ${cell(readerClaimRefs(report, contribution.verifiedUniqueClaimIds))} | ${contribution.verifiedUniqueClaimIds.length} |`);
525
+ else {
526
+ if (dossier.sharedConcerns.length) {
527
+ L.push('Shared concerns:');
528
+ for (const item of dossier.sharedConcerns)
529
+ L.push(`- ${item.text} ${item.evidenceStatus}; ${item.providerIds.map(providerName).join(', ')}.`);
530
+ }
531
+ else
532
+ L.push('Shared concerns: none recorded.');
533
+ L.push('');
534
+ if (dossier.uniqueSupportedInsights.length) {
535
+ L.push('Unique supported insights:');
536
+ for (const item of dossier.uniqueSupportedInsights)
537
+ L.push(`- ${item.text} — ${providerName(item.providerId)}; ${item.verificationStatus}.`);
538
+ }
539
+ else
540
+ L.push('Unique supported insights: none recorded.');
541
+ L.push('', '### Verified unique contributions', '', ...degradation(report, ['verification_skipped', 'single_model', 'low_diversity']));
542
+ L.push('Only unique claims that survived independent verification receive credit.', '');
543
+ L.push('| Provider | Verified unique contributions | Count |', '|---|---|---|');
544
+ for (const contribution of dossier.contributions) {
545
+ L.push(`| ${contribution.name} | ${cell(readerClaimRefs(report, contribution.verifiedUniqueClaimIds))} | ${contribution.verifiedUniqueClaimIds.length} |`);
546
+ }
277
547
  }
278
548
  L.push('');
279
- L.push('## 9. Run details', '');
549
+ L.push('## 8. Run details', '');
280
550
  L.push(`- Report ID: \`${report.reportId}\``);
281
551
  L.push(`- Generated: ${report.generatedAt}`);
282
552
  L.push(`- Original task: ${report.task.original}`);
@@ -292,10 +562,65 @@ export function renderDecisionDossierMarkdown(report) {
292
562
  L.push(`- By provider: ${Object.entries(report.receipt.byProvider).map(([provider, count]) => `${providerName(provider)} ${count}`).join(', ') || 'none'}`);
293
563
  L.push(`- Recorded model time: ${(report.receipt.modelTimeMs / 1000).toFixed(1)}s`);
294
564
  L.push(`- Degradation flags: ${report.flags.join(', ') || 'none'}`, '');
565
+ // This pass runs over already-built (cell-escaped) table rows, so injected labels must be cell-escaped too.
295
566
  for (let index = 0; index < L.length; index++)
296
- L[index] = stripReaderClaimIds(L[index]);
297
- L.push('## 10. Technical audit', '');
298
- L.push('<details>', '<summary>Original submissions and graph events</summary>', '');
567
+ L[index] = stripReaderClaimIds(L[index], labelFor, cell);
568
+ L.push('## 9. Technical audit', '');
569
+ L.push('<details>', '<summary>Evidence, coverage, claims, and graph events</summary>', '');
570
+ L.push('### Decision confidence', '');
571
+ L.push(`- Decision state: ${dossier.recommendation.status}`);
572
+ L.push(`- Structural score: ${report.confidenceBreakdown.score}/100 (${report.confidenceBreakdown.label}); heuristic, not benchmark-calibrated.`);
573
+ L.push(`- Basis: verification ${pct(coverage)} · independent convergence ${pct(report.confidenceBreakdown.independentConvergence)} · evidence quality ${pct(report.confidenceBreakdown.evidenceQuality)} · stability ${pct(report.confidenceBreakdown.stability)} · critical-risk penalty −${report.confidenceBreakdown.criticalRiskPenalty}`, '');
574
+ L.push('### Full claim chain', '');
575
+ if (dossier.claimChain.length) {
576
+ L.push('| Claim | Ruling | Evidence status | Depends on |', '|---|---|---|---|');
577
+ for (const claim of dossier.claimChain) {
578
+ L.push(`| ${cell(claim.text)} | ${claim.ruling} | ${claim.evidenceStatus} | ${cell(readerClaimRefs(report, claim.dependsOn))} |`);
579
+ }
580
+ }
581
+ else
582
+ L.push('No graph-anchored decision chain was recorded.');
583
+ L.push('', '### Full decision-sensitivity ledger', '');
584
+ if (dossier.sensitivity.length) {
585
+ L.push('| Fact | Sensitivity | If false | What would change it | Linked claims |', '|---|---|---|---|---|');
586
+ for (const item of dossier.sensitivity) {
587
+ L.push(`| ${cell(item.fact)} | ${item.sensitivity} | ${item.impactIfFalse} | ${cell(item.whatWouldChangeIt)} | ${cell(readerClaimRefs(report, item.linkedClaimIds))} |`);
588
+ }
589
+ }
590
+ else
591
+ L.push('No verdict-sensitive graph node was recorded.');
592
+ L.push('', '### Evidence and verification', '');
593
+ if (dossier.evidence.length) {
594
+ L.push('| Evidence ID | Source | Date | Freshness | Verification | Linked claims |', '|---|---|---|---|---|---|');
595
+ for (const evidence of dossier.evidence) {
596
+ L.push(`| ${evidence.id} | ${cell(evidence.source)} (${evidence.sourceKind}) | ${evidence.date} | ${evidence.freshness} | ${evidence.verificationStatus} | ${cell(readerClaimRefs(report, evidence.claimIds))} |`);
597
+ }
598
+ }
599
+ else
600
+ L.push('No evidence cards were stored.');
601
+ L.push('', '### Coverage ledger', '');
602
+ if (dossier.coverage.length) {
603
+ L.push('| Dimension | Status | Related claims |', '|---|---|---|');
604
+ for (const item of dossier.coverage)
605
+ L.push(`| ${cell(item.label)} | ${item.status} | ${cell(readerClaimRefs(report, item.claimIds))} |`);
606
+ }
607
+ else
608
+ L.push('No rubric coverage ledger was recorded.');
609
+ L.push('', '### Full risk ledger', '');
610
+ if (report.risks.length) {
611
+ L.push('| Risk | Severity |', '|---|---|');
612
+ for (const risk of report.risks)
613
+ L.push(`| ${cell(risk.risk)} | ${risk.severity} |`);
614
+ }
615
+ else
616
+ L.push('No material risk was recorded.');
617
+ L.push('', '### Full open-question ledger', '');
618
+ if (report.openQuestions.length)
619
+ for (const question of report.openQuestions)
620
+ L.push(`- ${question}`);
621
+ else
622
+ L.push('No verdict-flipping open question was recorded.');
623
+ L.push('', '### Original submissions and graph events', '');
299
624
  for (const submission of dossier.technical.submissions) {
300
625
  L.push(`- **${submission.name}:** ${submission.strongestVersion} (${refs(submission.positionIds)})`);
301
626
  }
@@ -318,3 +643,7 @@ export function renderDecisionDossierMarkdown(report) {
318
643
  L.push('', '</details>', '');
319
644
  return L.join('\n');
320
645
  }
646
+ /** Canonical Markdown. HTML and Copy-Markdown consume the same persisted reader brief. */
647
+ export function renderDecisionDossierMarkdown(report) {
648
+ return report.dossier.readerBrief ? renderReaderBriefMarkdown(report) : renderLegacyDecisionDossierMarkdown(report);
649
+ }