aiki-cli 0.3.0 → 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 (34) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +16 -8
  3. package/dist/bench/idea-v3-rating.js +1 -1
  4. package/dist/cli/index.js +1 -1
  5. package/dist/cli/run.js +10 -7
  6. package/dist/council/view.js +146 -46
  7. package/dist/orchestration/context.js +3 -3
  8. package/dist/orchestration/decision-dossier.js +467 -80
  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 +16 -4
  12. package/dist/orchestration/preflight.js +43 -9
  13. package/dist/orchestration/quick-analysis.js +35 -6
  14. package/dist/orchestration/stages/s10-render.js +179 -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 +208 -35
  22. package/dist/orchestration/url-sources.js +200 -0
  23. package/dist/schemas/index.js +134 -6
  24. package/dist/skills/idea-refinement/analyst.md +5 -5
  25. package/dist/skills/idea-refinement/chair.md +18 -0
  26. package/dist/skills/idea-refinement/economics-delivery.md +11 -0
  27. package/dist/skills/idea-refinement/market-adoption.md +12 -0
  28. package/dist/skills/idea-refinement/planner.md +25 -19
  29. package/dist/skills/idea-refinement/rebuttal.md +15 -0
  30. package/dist/skills/idea-refinement/verifier.md +17 -0
  31. package/dist/storage/runs.js +2 -1
  32. package/dist/tui/app.js +19 -4
  33. package/dist/workflows/idea-refinement.js +51 -15
  34. package/package.json +1 -1
@@ -5,12 +5,69 @@ function cell(value) {
5
5
  function refs(ids) {
6
6
  return ids.length ? ids.map((id) => `\`${id}\``).join(', ') : 'none recorded';
7
7
  }
8
+ function clipClaim(text, max = 96) {
9
+ if (text.length <= max)
10
+ return text;
11
+ const clipped = text.slice(0, max - 1);
12
+ const boundary = clipped.lastIndexOf(' ');
13
+ return `${clipped.slice(0, boundary > max * 0.65 ? boundary : max - 1).trimEnd()}…`;
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
+ }
28
+ /** Human-readable labels for reader-facing evidence links. Raw ids stay in the technical audit/JSON. */
29
+ export function readerClaimLabel(report, id) {
30
+ const claim = report.claims.find((item) => item.id === id);
31
+ if (claim)
32
+ return claimShortLabel(claim.text);
33
+ return /^G\d+$/.test(id) ? 'Supporting claim' : clipClaim(stripReaderClaimIds(id, claimLookup(report)));
34
+ }
35
+ export function readerClaimRefs(report, ids) {
36
+ return ids.length ? ids.map((id) => readerClaimLabel(report, id)).join('; ') : 'none recorded';
37
+ }
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();
57
+ }
8
58
  function providerName(id) {
9
59
  return id in DISPLAY_NAME ? DISPLAY_NAME[id] : id;
10
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
+ };
11
68
  function degradation(report, flags) {
12
69
  const active = flags.filter((flag) => report.flags.includes(flag));
13
- return active.length ? [`> ⚠ DEGRADED: ${active.join(', ')}`, ''] : [];
70
+ return active.flatMap((flag) => [`> ⚠ DEGRADED: ${flag}${FLAG_EXPLAIN[flag] ? ` — ${FLAG_EXPLAIN[flag]}.` : ''}`, '']);
14
71
  }
15
72
  function pct(value) {
16
73
  return `${Math.round(value * 100)}%`;
@@ -28,29 +85,258 @@ function councilRead(report) {
28
85
  const resolved = report.disagreements.filter((item) => item.status === 'RESOLVED').length;
29
86
  return `${report.disagreements.length} genuine disagreement${report.disagreements.length === 1 ? '' : 's'} reached the chair; ${resolved} ${resolved === 1 ? 'was' : 'were'} resolved.`;
30
87
  }
31
- /** Canonical R7 Markdown. HTML and Copy-Markdown consume the same persisted dossier object. */
32
- 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) {
33
305
  const { dossier } = report;
306
+ const labelFor = claimLookup(report);
34
307
  const keyFindings = report.keyFindings?.length ? report.keyFindings : [dossier.recommendation.reason];
35
308
  const criticalUnknowns = report.criticalUnknowns?.length ? report.criticalUnknowns : report.openQuestions.slice(0, 3);
36
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
+ });
37
321
  const L = [report.mode === 'quick' ? '# Single-Model Decision Report' : '# Multi-Model Decision Report', ''];
38
322
  L.push('## 1. Decision', '', ...degradation(report, ['synthesis_suspect']));
39
323
  if (report.decisionSnapshot) {
40
324
  L.push('### Decisive numbers', '', '| Metric | Value | What it means | Evidence |', '|---|---:|---|---|');
41
325
  for (const item of report.decisionSnapshot.decisiveNumbers) {
42
- L.push(`| ${cell(item.label)} | ${cell(item.value)} | ${cell(item.meaning)} | ${refs(item.claimIds)} |`);
326
+ L.push(`| ${cell(item.label)} | ${cell(item.value)} | ${cell(item.meaning)} | ${cell(readerClaimRefs(report, item.claimIds))} |`);
43
327
  }
44
328
  if (report.decisionSnapshot.payback) {
45
329
  const payback = report.decisionSnapshot.payback;
46
330
  L.push('', `**Payback — ${payback.status.replaceAll('_', ' ')}:** ${payback.result}`);
47
- L.push(`Basis: ${payback.basis} (${refs(payback.claimIds)})`, '');
331
+ L.push(`Basis: ${payback.basis}`, '');
48
332
  }
49
333
  else
50
334
  L.push('');
51
335
  }
52
336
  L.push(`**Recommendation:** ${dossier.recommendation.summary}`, '');
53
- 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.`);
54
340
  L.push(coverage < 0.5
55
341
  ? '> Low coverage means important inputs remain unchecked; it is not a probability that the recommendation is correct.'
56
342
  : '> Evidence coverage measures independent checking; it is not a probability that the recommendation is correct.', '');
@@ -63,11 +349,11 @@ export function renderDecisionDossierMarkdown(report) {
63
349
  if (report.decisionSnapshot) {
64
350
  L.push('### Options at a glance', '', '| Path | Commitment | Basis | Trade-off | Evidence |', '|---|---:|---|---|---|');
65
351
  for (const option of report.decisionSnapshot.options) {
66
- L.push(`| ${cell(option.label)} | ${cell(option.commitment)} | ${option.commitmentKind.replace('_', ' ')} | ${cell(option.tradeoff)} | ${refs(option.claimIds)} |`);
352
+ L.push(`| ${cell(option.label)} | ${cell(option.commitment)} | ${option.commitmentKind.replace('_', ' ')} | ${cell(option.tradeoff)} | ${cell(readerClaimRefs(report, option.claimIds))} |`);
67
353
  }
68
354
  if (report.decisionSnapshot.tripwire) {
69
355
  const tripwire = report.decisionSnapshot.tripwire;
70
- L.push('', '### Go/no-go tripwire', '', `**${tripwire.metric}: ${tripwire.threshold}** — ${tripwire.decisionRule} (${refs(tripwire.claimIds)})`, '');
356
+ L.push('', '### Go/no-go tripwire', '', `**${tripwire.metric}: ${tripwire.threshold}** — ${tripwire.decisionRule}`, '');
71
357
  }
72
358
  }
73
359
  L.push('### What could overturn this', '', dossier.counterCase.available ? dossier.counterCase.reasoning : dossier.counterCase.reasoning, '');
@@ -79,27 +365,44 @@ export function renderDecisionDossierMarkdown(report) {
79
365
  L.push('- None recorded.');
80
366
  L.push('', `**Critical warning:** ${report.verdict.criticalWarning ?? 'None recorded.'}`);
81
367
  L.push(`**Council read:** ${councilRead(report)}`);
82
- L.push(`Evidence anchors: ${refs(dossier.recommendation.claimIds)}`, '');
368
+ L.push('');
83
369
  if (!dossier.recommendation.claimIds.length)
84
370
  L.push('> ⚠ DEGRADED: recommendation has no stored graph anchor.', '');
85
- L.push('<details>', '<summary>Decision conditions and technical confidence</summary>', '');
86
- L.push(`- Decision state: ${dossier.recommendation.status}`);
87
- L.push(`- Structural score: ${report.confidenceBreakdown.score}/100 (${report.confidenceBreakdown.label}); heuristic, not benchmark-calibrated.`);
88
- 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}`);
89
- if (dossier.recommendation.conditions.length) {
90
- L.push('', 'Conditions:');
91
- for (const condition of dossier.recommendation.conditions) {
92
- L.push(`- ${condition.text} (${refs(condition.claimIds)})`);
371
+ L.push('## 2. Deliverables and action plan', '', ...degradation(report, ['plan_fallback', 'plan_skipped']));
372
+ if ((dossier.missingRequestedOutputs ?? []).length) {
373
+ L.push(`> DEGRADED: requested output missing: ${dossier.missingRequestedOutputs.join(', ')}`, '');
374
+ }
375
+ if (dossier.featureBacklog) {
376
+ L.push('### Feature priorities', '', '| Priority | Feature | User value | Why now | Effort |', '|---|---|---|---|---|');
377
+ for (const [priority, items] of [
378
+ ['MUST', dossier.featureBacklog.must],
379
+ ['SHOULD', dossier.featureBacklog.should],
380
+ ['LATER', dossier.featureBacklog.later],
381
+ ]) {
382
+ for (const item of items)
383
+ L.push(`| ${priority} | ${cell(item.feature)} | ${cell(item.user_value)} | ${cell(item.rationale)} | ${item.effort} |`);
384
+ }
385
+ if (dossier.featureBacklog.wont.length) {
386
+ L.push('', '**Not in this scope**', '', '| Feature | Reason |', '|---|---|');
387
+ for (const item of dossier.featureBacklog.wont)
388
+ L.push(`| ${cell(item.feature)} | ${cell(item.reason)} |`);
93
389
  }
390
+ L.push('');
94
391
  }
95
- L.push('', '</details>', '');
96
- L.push('## 2. Action plan', '', ...degradation(report, ['plan_fallback', 'plan_skipped']));
392
+ if (dossier.implementationPlan) {
393
+ L.push('### Implementation plan', '', '| # | Timebox | Outcome | Work | Acceptance test |', '|---|---|---|---|---|');
394
+ for (const milestone of dossier.implementationPlan.milestones) {
395
+ L.push(`| ${milestone.order} | ${cell(milestone.timebox)} | ${cell(milestone.outcome)} | ${cell(milestone.tasks.join('; '))} | ${cell(milestone.acceptance_test)} |`);
396
+ }
397
+ L.push('');
398
+ }
399
+ L.push('### Validation plan', '');
97
400
  if (dossier.experiments.status === 'DEGRADED')
98
401
  L.push(`> ⚠ DEGRADED: ${dossier.experiments.note}`, '');
99
402
  if (dossier.experiments.actions.length) {
100
403
  L.push('| # | Experiment | Why | Validates | Effort | Kill signal |', '|---|---|---|---|---|---|');
101
404
  for (const action of dossier.experiments.actions) {
102
- L.push(`| ${action.order} | ${cell(action.action)} | ${cell(action.why)} | ${action.validates} | ${action.effort} | ${cell(action.killSignal)} |`);
405
+ L.push(`| ${action.order} | ${cell(action.action)} | ${cell(action.why)} | ${cell(readerClaimLabel(report, action.validates))} | ${action.effort} | ${cell(action.killSignal)} |`);
103
406
  }
104
407
  L.push('', dossier.experiments.note);
105
408
  }
@@ -107,69 +410,61 @@ export function renderDecisionDossierMarkdown(report) {
107
410
  L.push('No executable experiment was produced.');
108
411
  L.push('');
109
412
  L.push('## 3. Why this decision', '');
110
- if (dossier.claimChain.length) {
111
- L.push('| Claim ID | Claim | Ruling | Evidence status | Depends on |', '|---|---|---|---|---|');
112
- for (const claim of dossier.claimChain) {
113
- L.push(`| ${claim.claimId} | ${cell(claim.text)} | ${claim.ruling} | ${claim.evidenceStatus} | ${refs(claim.dependsOn)} |`);
114
- }
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.`);
115
423
  }
116
- else
117
- L.push('No graph-anchored decision chain was recorded.');
118
424
  L.push('');
119
425
  L.push('## 4. What could change the decision', '', '### Decision-sensitive facts', '');
120
- if (dossier.sensitivity.length) {
121
- L.push('| Claim ID | Fact | Sensitivity | If false | What would change it | Linked claims |', '|---|---|---|---|---|---|');
122
- for (const item of dossier.sensitivity) {
123
- L.push(`| ${item.claimId} | ${cell(item.fact)} | ${item.sensitivity} | ${item.impactIfFalse} | ${cell(item.whatWouldChangeIt)} | ${refs(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)} |`);
124
431
  }
125
432
  }
126
433
  else
127
434
  L.push('No verdict-sensitive graph node was recorded.');
128
435
  L.push('', '### Strongest counter-case', '');
129
436
  if (dossier.counterCase.available) {
130
- L.push(dossier.counterCase.reasoning, '', `Evidence anchors: ${refs(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)}`);
131
439
  }
132
440
  else
133
441
  L.push(`> ⚠ DEGRADED: ${dossier.counterCase.reasoning}`);
134
442
  L.push('');
135
- L.push('## 5. Evidence and verification', '', ...degradation(report, ['verification_skipped', 'research_ungrounded']));
136
- if (dossier.evidence.length) {
137
- L.push('| Evidence ID | Source | Date | Freshness | Verification | Linked claims |', '|---|---|---|---|---|---|');
138
- for (const evidence of dossier.evidence) {
139
- L.push(`| ${evidence.id} | ${cell(evidence.source)} (${evidence.sourceKind}) | ${evidence.date} | ${evidence.freshness} | ${evidence.verificationStatus} | ${refs(evidence.claimIds)} |`);
140
- }
141
- }
142
- else
143
- L.push('No evidence cards were stored.');
144
- L.push('');
145
- L.push('## 6. Risks, gaps, and open questions', '', '### Risks', '');
443
+ L.push('## 5. Risks and open questions', '', ...degradation(report, ['verification_skipped', 'research_ungrounded']), '### Risks', '');
146
444
  if (report.risks.length) {
445
+ const shownRisks = report.risks.slice(0, 8);
147
446
  L.push('| Risk | Severity |', '|---|---|');
148
- for (const risk of report.risks)
447
+ for (const risk of shownRisks)
149
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).`);
150
451
  }
151
452
  else
152
453
  L.push('No material risk was recorded.');
153
- L.push('', '### Coverage', '');
154
- if (dossier.coverage.length) {
155
- L.push('| Dimension | Status | Claim IDs |', '|---|---|---|');
156
- for (const item of dossier.coverage)
157
- L.push(`| ${cell(item.label)} | ${item.status} | ${refs(item.claimIds)} |`);
158
- }
159
- else
160
- L.push('No rubric coverage ledger was recorded.');
161
454
  L.push('', '### Open questions', '');
162
- if (report.openQuestions.length) {
163
- for (const question of report.openQuestions)
455
+ if (openQuestions.length) {
456
+ for (const question of openQuestions.slice(0, 5))
164
457
  L.push(`- ${question}`);
458
+ if (openQuestions.length > 5)
459
+ L.push('', `Showing 5 of ${openQuestions.length} — the rest are in the technical audit.`);
165
460
  }
166
461
  else
167
462
  L.push('No verdict-flipping open question was recorded.');
168
463
  L.push('');
169
- 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']));
170
465
  if (report.disagreements.length) {
171
466
  for (const disagreement of report.disagreements) {
172
- L.push(`- **${disagreement.id}: ${disagreement.topic}** — ${disagreement.status}; ${disagreement.ruling}.`);
467
+ L.push(`- **${disagreement.topic}** — ${disagreement.status}; ${disagreement.ruling}.`);
173
468
  for (const side of disagreement.sides)
174
469
  L.push(` - ${side.stance} (${side.providers.map(providerName).join(', ')}): ${side.reasoning.join(' ')}`);
175
470
  if (disagreement.reasoning)
@@ -182,7 +477,7 @@ export function renderDecisionDossierMarkdown(report) {
182
477
  if (dossier.positionChanges.length) {
183
478
  L.push('| Event | Claim | Responder | Change | Evidence | Detail |', '|---|---|---|---|---|---|');
184
479
  for (const event of dossier.positionChanges) {
185
- L.push(`| ${event.eventId} | ${event.claimId} | ${providerName(event.responder)} | ${event.response} | ${refs(event.evidenceIds)} | ${cell(event.narrowedProposition ?? event.reasoning)} |`);
480
+ L.push(`| ${event.eventId} | ${cell(readerClaimLabel(report, event.claimId))} | ${providerName(event.responder)} | ${event.response} | ${refs(event.evidenceIds)} | ${cell(event.narrowedProposition ?? event.reasoning)} |`);
186
481
  }
187
482
  }
188
483
  else
@@ -197,30 +492,61 @@ export function renderDecisionDossierMarkdown(report) {
197
492
  for (const item of report.minority.uniqueOppositions)
198
493
  L.push(`- ${providerName(item.provider)} uniquely opposed: ${item.proposition}`);
199
494
  L.push(`- Decision-blocking status: ${report.minority.blocksDecision}`, '');
200
- L.push('## 8. What the council added', '');
201
- if (dossier.sharedConcerns.length) {
202
- L.push('Shared concerns:');
203
- for (const item of dossier.sharedConcerns)
204
- L.push(`- **${item.claimId}** ${item.text} ${item.evidenceStatus}; ${item.providerIds.map(providerName).join(', ')}.`);
205
- }
206
- else
207
- L.push('Shared concerns: none recorded.');
208
- L.push('');
209
- if (dossier.uniqueSupportedInsights.length) {
210
- L.push('Unique supported insights:');
211
- for (const item of dossier.uniqueSupportedInsights)
212
- L.push(`- **${item.claimId}** ${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
+ }
213
524
  }
214
- else
215
- L.push('Unique supported insights: none recorded.');
216
- L.push('', '### Verified unique contributions', '', ...degradation(report, ['verification_skipped', 'single_model', 'low_diversity']));
217
- L.push('Only unique claims that survived independent verification receive credit.', '');
218
- L.push('| Provider | Verified unique claim IDs | Count |', '|---|---|---|');
219
- for (const contribution of dossier.contributions) {
220
- L.push(`| ${contribution.name} | ${refs(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
+ }
221
547
  }
222
548
  L.push('');
223
- L.push('## 9. Run details', '');
549
+ L.push('## 8. Run details', '');
224
550
  L.push(`- Report ID: \`${report.reportId}\``);
225
551
  L.push(`- Generated: ${report.generatedAt}`);
226
552
  L.push(`- Original task: ${report.task.original}`);
@@ -236,8 +562,65 @@ export function renderDecisionDossierMarkdown(report) {
236
562
  L.push(`- By provider: ${Object.entries(report.receipt.byProvider).map(([provider, count]) => `${providerName(provider)} ${count}`).join(', ') || 'none'}`);
237
563
  L.push(`- Recorded model time: ${(report.receipt.modelTimeMs / 1000).toFixed(1)}s`);
238
564
  L.push(`- Degradation flags: ${report.flags.join(', ') || 'none'}`, '');
239
- L.push('## 10. Technical audit', '');
240
- L.push('<details>', '<summary>Original submissions and graph events</summary>', '');
565
+ // This pass runs over already-built (cell-escaped) table rows, so injected labels must be cell-escaped too.
566
+ for (let index = 0; index < L.length; index++)
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', '');
241
624
  for (const submission of dossier.technical.submissions) {
242
625
  L.push(`- **${submission.name}:** ${submission.strongestVersion} (${refs(submission.positionIds)})`);
243
626
  }
@@ -260,3 +643,7 @@ export function renderDecisionDossierMarkdown(report) {
260
643
  L.push('', '</details>', '');
261
644
  return L.join('\n');
262
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
+ }