aiki-cli 0.3.1 → 0.3.3
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 +80 -5
- package/README.md +73 -15
- package/dist/bench/idea-v3-bench.js +104 -5
- package/dist/bench/idea-v3-rating.js +159 -4
- package/dist/cli/bench.js +5 -5
- package/dist/cli/index.js +12 -2
- package/dist/cli/resume.js +20 -0
- package/dist/cli/run.js +76 -5
- package/dist/cli/serve.js +48 -0
- package/dist/config/config.js +7 -2
- package/dist/council/view.js +64 -15
- package/dist/orchestration/auto-profile.js +97 -0
- package/dist/orchestration/claim-groups.js +56 -0
- package/dist/orchestration/context.js +69 -6
- package/dist/orchestration/decision-dossier.js +450 -89
- package/dist/orchestration/decision-graph.js +33 -2
- package/dist/orchestration/engine.js +8 -4
- package/dist/orchestration/evidence-origin.js +17 -0
- package/dist/orchestration/jsonStage.js +47 -5
- package/dist/orchestration/legacy-idea-adapter.js +3 -0
- package/dist/orchestration/modes.js +18 -10
- package/dist/orchestration/preflight.js +36 -3
- package/dist/orchestration/quick-analysis.js +29 -7
- package/dist/orchestration/sanitize-paths.js +10 -0
- package/dist/orchestration/stages/s10-render.js +196 -84
- package/dist/orchestration/stages/s4-analyze.js +10 -2
- package/dist/orchestration/stages/s4b-challenge.js +97 -0
- package/dist/orchestration/stages/s5-drift.js +13 -3
- package/dist/orchestration/stages/s6-positions.js +18 -0
- package/dist/orchestration/stages/s7-decision-graph.js +3 -0
- package/dist/orchestration/stages/s8-verify.js +66 -12
- package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
- package/dist/orchestration/stages/s9-judge.js +52 -14
- package/dist/orchestration/stages/s9b-plan.js +227 -48
- package/dist/orchestration/url-sources.js +21 -0
- package/dist/providers/adapter-core.js +1 -1
- package/dist/providers/claude.js +18 -0
- package/dist/schemas/index.js +112 -3
- package/dist/serve/flight-deck.js +830 -0
- package/dist/serve/followup.js +50 -0
- package/dist/serve/frames.js +168 -0
- package/dist/serve/gates.js +72 -0
- package/dist/serve/projections.js +283 -0
- package/dist/serve/server.js +219 -0
- package/dist/serve/threads.js +145 -0
- package/dist/serve-ui/Five.png +0 -0
- package/dist/serve-ui/app.js +820 -0
- package/dist/serve-ui/index.html +171 -0
- package/dist/serve-ui/workspace.css +662 -0
- package/dist/skills/idea-refinement/analyst.md +5 -5
- package/dist/skills/idea-refinement/chair.md +18 -0
- package/dist/skills/idea-refinement/economics-delivery.md +11 -0
- package/dist/skills/idea-refinement/market-adoption.md +12 -0
- package/dist/skills/idea-refinement/planner.md +25 -19
- package/dist/skills/idea-refinement/rebuttal.md +15 -0
- package/dist/skills/idea-refinement/verifier.md +17 -0
- package/dist/storage/runs.js +2 -1
- package/dist/workflows/idea-refinement.js +130 -24
- package/package.json +2 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { DISPLAY_NAME } from '../providers/types.js';
|
|
2
|
+
import { evidenceOrigin } from './evidence-origin.js';
|
|
3
|
+
import { sanitizeLocalPaths } from './sanitize-paths.js';
|
|
2
4
|
function cell(value) {
|
|
3
5
|
return value.replaceAll('\n', ' ').replaceAll('|', '\\|');
|
|
4
6
|
}
|
|
@@ -12,33 +14,70 @@ function clipClaim(text, max = 96) {
|
|
|
12
14
|
const boundary = clipped.lastIndexOf(' ');
|
|
13
15
|
return `${clipped.slice(0, boundary > max * 0.65 ? boundary : max - 1).trimEnd()}…`;
|
|
14
16
|
}
|
|
17
|
+
/** ≤60-char noun-ish handle for a claim: drop a leading "Verdict:", keep the first clause, clip at a word. */
|
|
18
|
+
export function claimShortLabel(text, max = 60) {
|
|
19
|
+
const base = text.replace(/^\s*Verdict:\s*/i, '').split(/(?<=\S)[;—]|(?<=\.)\s/)[0].trim();
|
|
20
|
+
if (base.length <= max)
|
|
21
|
+
return base;
|
|
22
|
+
const clipped = base.slice(0, max);
|
|
23
|
+
const boundary = clipped.lastIndexOf(' ');
|
|
24
|
+
return `${clipped.slice(0, boundary > max * 0.6 ? boundary : max).trimEnd()}…`;
|
|
25
|
+
}
|
|
26
|
+
/** Human token summary: "~18.4k in / ~3.2k out (2 calls estimated)". `~` iff any call estimated. */
|
|
27
|
+
export function formatTokenLine(t) {
|
|
28
|
+
const k = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n));
|
|
29
|
+
const tilde = t.estimatedCalls > 0 ? '~' : '';
|
|
30
|
+
const suffix = t.estimatedCalls > 0 ? ` (${t.estimatedCalls} calls estimated)` : '';
|
|
31
|
+
return `${tilde}${k(t.inputTokens)} in / ${tilde}${k(t.outputTokens)} out${suffix}`;
|
|
32
|
+
}
|
|
33
|
+
/** id → claim text lookup for stripReaderClaimIds substitution; shared by readerClaimLabel's fallback and the Markdown/HTML renderers. */
|
|
34
|
+
export function claimLookup(report) {
|
|
35
|
+
return (id) => report.claims.find((claim) => claim.id === id)?.text ?? null;
|
|
36
|
+
}
|
|
15
37
|
/** Human-readable labels for reader-facing evidence links. Raw ids stay in the technical audit/JSON. */
|
|
16
38
|
export function readerClaimLabel(report, id) {
|
|
17
39
|
const claim = report.claims.find((item) => item.id === id);
|
|
18
40
|
if (claim)
|
|
19
|
-
return
|
|
20
|
-
return /^G\d+$/.test(id) ? 'Supporting claim' : clipClaim(stripReaderClaimIds(id));
|
|
41
|
+
return claimShortLabel(claim.text);
|
|
42
|
+
return /^G\d+$/.test(id) ? 'Supporting claim' : clipClaim(stripReaderClaimIds(id, claimLookup(report)));
|
|
21
43
|
}
|
|
22
44
|
export function readerClaimRefs(report, ids) {
|
|
23
45
|
return ids.length ? ids.map((id) => readerClaimLabel(report, id)).join('; ') : 'none recorded';
|
|
24
46
|
}
|
|
25
|
-
/** Remove internal graph notation
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
/** Remove or substitute internal graph notation in reader prose. With a lookup, ids become quoted short labels
|
|
48
|
+
* instead of vanishing (never bare-delete); without one (no lookup available), ids are dropped — the legacy
|
|
49
|
+
* behavior, kept only for callers that have no claim table to look up against. `sanitizeLabel` escapes each
|
|
50
|
+
* injected label for the caller's output format (markdown passes `cell`; HTML passes nothing — raw). */
|
|
51
|
+
export function stripReaderClaimIds(text, labelFor, sanitizeLabel = (label) => label) {
|
|
52
|
+
const label = (id) => {
|
|
53
|
+
const hit = labelFor?.(id);
|
|
54
|
+
return hit ? `"${sanitizeLabel(claimShortLabel(hit))}"` : 'a related claim';
|
|
55
|
+
};
|
|
56
|
+
let out = text
|
|
57
|
+
// whole parenthetical citation groups — comma OR slash separated — vanish entirely: (G19), (G7/G13), (G1, G2)
|
|
58
|
+
.replace(/\s*\((?:\s*`?G\d+`?\s*[,/]?\s*)+\)/g, '')
|
|
59
|
+
// "G21's kill criteria" → `"the spike kill-gate"'s kill criteria` — keep the possessive on the label itself
|
|
60
|
+
// rather than injecting filler noun text, so it reads correctly for any noun that follows.
|
|
61
|
+
.replace(/\bG(\d+)'s\b/g, (_, n) => (labelFor ? `${label(`G${n}`)}'s` : ''));
|
|
62
|
+
out = labelFor
|
|
63
|
+
? out.replace(/\bG\d+\b/g, (id) => label(id))
|
|
64
|
+
: out.replace(/\bG\d+\s+assumes\b/g, 'This assumes').replace(/\bG\d+\b/g, '');
|
|
65
|
+
return out.replace(/\s{2,}/g, ' ').replace(/\s+([.,;:])/g, '$1').trim();
|
|
35
66
|
}
|
|
36
67
|
function providerName(id) {
|
|
37
68
|
return id in DISPLAY_NAME ? DISPLAY_NAME[id] : id;
|
|
38
69
|
}
|
|
70
|
+
const FLAG_EXPLAIN = {
|
|
71
|
+
synthesis_suspect: 'the chair output needed a deterministic repair; phrasing may be less reliable than the underlying graph',
|
|
72
|
+
headless_intent: 'no human confirmed the interpretation; documented defaults were used',
|
|
73
|
+
weak_seat: 'one scout seat contributed far less evidenced material than the other; treat convergence cautiously',
|
|
74
|
+
source_fallback_search: 'a supplied page blocked direct reading; the source-investigation seat searched for accessible sources instead',
|
|
75
|
+
deliverable_gap: 'one or more surviving scouts omitted a requested feature or implementation proposal',
|
|
76
|
+
plan_partial: 'the answer planner omitted part of the requested deliverables; the report shows what was produced',
|
|
77
|
+
};
|
|
39
78
|
function degradation(report, flags) {
|
|
40
79
|
const active = flags.filter((flag) => report.flags.includes(flag));
|
|
41
|
-
return active.
|
|
80
|
+
return active.flatMap((flag) => [`> ⚠ DEGRADED: ${flag}${FLAG_EXPLAIN[flag] ? ` — ${FLAG_EXPLAIN[flag]}.` : ''}`, '']);
|
|
42
81
|
}
|
|
43
82
|
function pct(value) {
|
|
44
83
|
return `${Math.round(value * 100)}%`;
|
|
@@ -47,6 +86,12 @@ function coverageLabel(value) {
|
|
|
47
86
|
return value >= 0.75 ? 'High' : value >= 0.5 ? 'Medium' : 'Low';
|
|
48
87
|
}
|
|
49
88
|
function councilRead(report) {
|
|
89
|
+
if (report.fastPath)
|
|
90
|
+
return 'Single-pass analysis; council escalation was not required.';
|
|
91
|
+
if (report.adaptiveAuto)
|
|
92
|
+
return report.autoEscalationReasons?.length
|
|
93
|
+
? `The primary analysis tripped structural gates; two task readings checked the interpretation${report.autoChallengeUsed ? ', and one targeted challenger checked eligible claims' : '; no challenger could add information'}. No full council was convened.`
|
|
94
|
+
: 'Two task readings informed one primary decision analyst. No full council was convened.';
|
|
50
95
|
if (report.mode === 'quick')
|
|
51
96
|
return 'One structured analyst produced this result; no council, consensus, or independent-verification claim is being made.';
|
|
52
97
|
const scouts = report.models.filter((model) => model.roles.includes('scout')).length;
|
|
@@ -56,12 +101,246 @@ function councilRead(report) {
|
|
|
56
101
|
const resolved = report.disagreements.filter((item) => item.status === 'RESOLVED').length;
|
|
57
102
|
return `${report.disagreements.length} genuine disagreement${report.disagreements.length === 1 ? '' : 's'} reached the chair; ${resolved} ${resolved === 1 ? 'was' : 'were'} resolved.`;
|
|
58
103
|
}
|
|
59
|
-
|
|
60
|
-
|
|
104
|
+
export function sanitizeReaderText(text, labelFor) {
|
|
105
|
+
return stripReaderClaimIds(text, labelFor)
|
|
106
|
+
.replace(/\b(?:G|C|D)\d+\b/g, '')
|
|
107
|
+
// v6: UPPERCASE-only — a leaked internal enum is rewritten; ordinary English ("the rules are
|
|
108
|
+
// unverified") is the model's own prose and must survive (f740's "confirmed-unverified" became
|
|
109
|
+
// "confirmed-not yet confirmed" under the old /gi).
|
|
110
|
+
.replace(/\bUNVERIFIABLE\b/g, 'not independently confirmed')
|
|
111
|
+
.replace(/\bUNVERIFIED\b/g, 'not yet confirmed')
|
|
112
|
+
.replace(/\bPARTIAL\b/g, 'partly supported')
|
|
113
|
+
.replace(/\bCONFLICTED\b/g, 'supported by conflicting evidence')
|
|
114
|
+
.replace(/\bstructural score\b/gi, 'confidence estimate')
|
|
115
|
+
.replace(/\bevidence coverage\b/gi, 'source strength')
|
|
116
|
+
.replace(/\bverification coverage\b/gi, 'source strength')
|
|
117
|
+
.replace(/\b(?:claim|graph) ids?\b/gi, 'internal references')
|
|
118
|
+
.replace(/\b(?:provider|model) calls?\b/gi, 'analysis steps')
|
|
119
|
+
.replace(/\banswer editor\b/gi, 'answer synthesis')
|
|
120
|
+
.replace(/\breader_brief\b/gi, 'reader summary')
|
|
121
|
+
.replace(/\s{2,}/g, ' ')
|
|
122
|
+
.replace(/\s+([.,;:])/g, '$1')
|
|
123
|
+
.trim();
|
|
124
|
+
}
|
|
125
|
+
export function cleanReaderText(report, text) {
|
|
126
|
+
return sanitizeReaderText(text, claimLookup(report));
|
|
127
|
+
}
|
|
128
|
+
const MATERIAL_WARNING_ORDER = [
|
|
129
|
+
'synthesis_suspect', 'low_diversity', 'weak_seat', 'deliverable_gap', 'plan_skipped',
|
|
130
|
+
'plan_fallback', 'plan_partial', 'headless_intent', 'verification_skipped', 'research_ungrounded',
|
|
131
|
+
];
|
|
132
|
+
const READER_WARNING = {
|
|
133
|
+
synthesis_suspect: 'The final synthesis needed repair; treat its wording cautiously.',
|
|
134
|
+
low_diversity: 'Independent diversity was reduced because one scout seat had to be resampled.',
|
|
135
|
+
weak_seat: 'One scout contributed much less evidenced material; treat convergence cautiously.',
|
|
136
|
+
deliverable_gap: 'At least one scout omitted a requested feature or implementation proposal.',
|
|
137
|
+
plan_skipped: 'The answer-planning step was skipped because its call budget was unavailable.',
|
|
138
|
+
plan_fallback: 'The answer planner failed validation; this report uses a deterministic fallback.',
|
|
139
|
+
plan_partial: 'The answer planner omitted part of the requested deliverables; the report shows what was produced.',
|
|
140
|
+
headless_intent: 'No person confirmed the interpreted request; documented defaults were used.',
|
|
141
|
+
verification_skipped: 'Independent verification was skipped; important factual claims may remain unchecked.',
|
|
142
|
+
research_ungrounded: 'Source investigation did not produce a cited public source; treat current claims as ungrounded.',
|
|
143
|
+
};
|
|
144
|
+
export function isDecisionSnapshotRelevant(snapshot) {
|
|
145
|
+
if (!snapshot)
|
|
146
|
+
return false;
|
|
147
|
+
const allUnknown = snapshot.options.every((option) => option.commitmentKind === 'UNKNOWN');
|
|
148
|
+
const paybackUnavailable = !snapshot.payback || snapshot.payback.status === 'NOT_COMPUTABLE';
|
|
149
|
+
return !(allUnknown && paybackUnavailable);
|
|
150
|
+
}
|
|
151
|
+
function safeHttpUrl(raw) {
|
|
152
|
+
if (!raw)
|
|
153
|
+
return undefined;
|
|
154
|
+
try {
|
|
155
|
+
const url = new URL(raw);
|
|
156
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : undefined;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function looksLocalPath(value) {
|
|
163
|
+
if (!value)
|
|
164
|
+
return false;
|
|
165
|
+
return /^file:/i.test(value)
|
|
166
|
+
|| /^(?:\/|\.\.?[\\/]|[A-Za-z]:[\\/])/.test(value)
|
|
167
|
+
|| /^(?:[^:/\\]+[\\/])+[^/\\]+$/.test(value);
|
|
168
|
+
}
|
|
169
|
+
/** One cleaned reader contract consumed by Markdown, HTML, and terminal renderers. */
|
|
170
|
+
export function buildReaderProjection(report) {
|
|
171
|
+
const brief = report.dossier.readerBrief;
|
|
172
|
+
const clean = (text) => cleanReaderText(report, text);
|
|
173
|
+
const sources = [];
|
|
174
|
+
const sourceIndexByKey = new Map();
|
|
175
|
+
for (const id of brief.source_ids) {
|
|
176
|
+
const source = report.dossier.evidence.find((item) => item.id === id);
|
|
177
|
+
if (!source)
|
|
178
|
+
continue;
|
|
179
|
+
const url = safeHttpUrl(source.url) ?? safeHttpUrl(source.source);
|
|
180
|
+
const title = source.title && !looksLocalPath(source.title) ? clean(source.title) : undefined;
|
|
181
|
+
const label = title ?? (url ? url
|
|
182
|
+
: source.sourceKind === 'MODEL_KNOWLEDGE' ? 'Background model knowledge'
|
|
183
|
+
: source.sourceKind === 'USER' || looksLocalPath(source.source) ? 'User-supplied material' : 'Source material');
|
|
184
|
+
const key = url ? `url:${url.toLowerCase()}` : title ? `id:${source.id}` : `label:${label.toLowerCase()}`;
|
|
185
|
+
const citedFor = [...new Set(source.claimIds.flatMap((claimId) => {
|
|
186
|
+
const claim = report.claims.find((item) => item.id === claimId);
|
|
187
|
+
return claim ? [claimShortLabel(clean(claim.text))] : [];
|
|
188
|
+
}))].slice(0, 3);
|
|
189
|
+
const existingIndex = sourceIndexByKey.get(key);
|
|
190
|
+
if (existingIndex !== undefined) {
|
|
191
|
+
sources[existingIndex].citedFor = [...new Set([...sources[existingIndex].citedFor, ...citedFor])].slice(0, 3);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
sourceIndexByKey.set(key, sources.length);
|
|
195
|
+
sources.push({ id: source.id, label, ...(url ? { url } : {}), citedFor });
|
|
196
|
+
}
|
|
197
|
+
const cleanBacklog = report.dossier.featureBacklog ? {
|
|
198
|
+
must: report.dossier.featureBacklog.must.map((item) => ({ ...item, feature: clean(item.feature), user_value: clean(item.user_value), rationale: clean(item.rationale) })),
|
|
199
|
+
should: report.dossier.featureBacklog.should.map((item) => ({ ...item, feature: clean(item.feature), user_value: clean(item.user_value), rationale: clean(item.rationale) })),
|
|
200
|
+
later: report.dossier.featureBacklog.later.map((item) => ({ ...item, feature: clean(item.feature), user_value: clean(item.user_value), rationale: clean(item.rationale) })),
|
|
201
|
+
wont: report.dossier.featureBacklog.wont.map((item) => ({ feature: clean(item.feature), reason: clean(item.reason) })),
|
|
202
|
+
} : undefined;
|
|
203
|
+
const cleanPlan = report.dossier.implementationPlan ? {
|
|
204
|
+
milestones: report.dossier.implementationPlan.milestones.map((item) => ({
|
|
205
|
+
...item,
|
|
206
|
+
timebox: clean(item.timebox),
|
|
207
|
+
outcome: clean(item.outcome),
|
|
208
|
+
tasks: item.tasks.map(clean),
|
|
209
|
+
acceptance_test: clean(item.acceptance_test),
|
|
210
|
+
})),
|
|
211
|
+
} : undefined;
|
|
212
|
+
const snapshot = isDecisionSnapshotRelevant(report.decisionSnapshot) ? {
|
|
213
|
+
decisiveNumbers: report.decisionSnapshot.decisiveNumbers.map((item) => ({ label: clean(item.label), value: clean(item.value), meaning: clean(item.meaning) })),
|
|
214
|
+
...(report.decisionSnapshot.payback ? { payback: {
|
|
215
|
+
status: report.decisionSnapshot.payback.status,
|
|
216
|
+
result: clean(report.decisionSnapshot.payback.result),
|
|
217
|
+
basis: clean(report.decisionSnapshot.payback.basis),
|
|
218
|
+
} } : {}),
|
|
219
|
+
options: report.decisionSnapshot.options.map((item) => ({
|
|
220
|
+
label: clean(item.label), commitment: clean(item.commitment), commitmentKind: item.commitmentKind, tradeoff: clean(item.tradeoff),
|
|
221
|
+
})),
|
|
222
|
+
...(report.decisionSnapshot.tripwire ? { tripwire: {
|
|
223
|
+
metric: clean(report.decisionSnapshot.tripwire.metric),
|
|
224
|
+
threshold: clean(report.decisionSnapshot.tripwire.threshold),
|
|
225
|
+
decisionRule: clean(report.decisionSnapshot.tripwire.decisionRule),
|
|
226
|
+
} } : {}),
|
|
227
|
+
} : undefined;
|
|
228
|
+
return {
|
|
229
|
+
headline: clean(brief.headline),
|
|
230
|
+
bottomLine: clean(brief.bottom_line),
|
|
231
|
+
sections: brief.sections.map((section) => ({ heading: clean(section.heading), summary: clean(section.summary), bullets: section.bullets.map(clean) })),
|
|
232
|
+
featureBacklog: cleanBacklog,
|
|
233
|
+
implementationPlan: cleanPlan,
|
|
234
|
+
caveats: brief.caveats.map(clean),
|
|
235
|
+
warnings: MATERIAL_WARNING_ORDER.filter((flag) => report.flags.includes(flag)).map((flag) => ({ flag, message: READER_WARNING[flag] })),
|
|
236
|
+
notices: report.flags.includes('source_fallback_search')
|
|
237
|
+
? [{ flag: 'source_fallback_search', message: 'Search attempted: a supplied page could not be read directly, so the source-investigation scout searched for accessible sources.' }]
|
|
238
|
+
: [],
|
|
239
|
+
snapshot,
|
|
240
|
+
sources,
|
|
241
|
+
nextStep: clean(brief.next_step),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function renderReaderBriefMarkdown(report) {
|
|
245
|
+
const projection = buildReaderProjection(report);
|
|
246
|
+
const L = [`# ${projection.headline}`, '', projection.bottomLine, ''];
|
|
247
|
+
for (const warning of projection.warnings)
|
|
248
|
+
L.push(`> ⚠ ${warning.message}`, '');
|
|
249
|
+
for (const notice of projection.notices)
|
|
250
|
+
L.push(`> ℹ ${notice.message}`, '');
|
|
251
|
+
if (projection.snapshot) {
|
|
252
|
+
L.push('## Decision numbers', '', '### Decisive numbers', '', '| Metric | Value | What it means |', '|---|---:|---|');
|
|
253
|
+
for (const item of projection.snapshot.decisiveNumbers)
|
|
254
|
+
L.push(`| ${cell(item.label)} | ${cell(item.value)} | ${cell(item.meaning)} |`);
|
|
255
|
+
// v6: a NOT_COMPUTABLE payback is noise on a non-financial question — omit, don't announce.
|
|
256
|
+
if (projection.snapshot.payback && projection.snapshot.payback.status !== 'NOT_COMPUTABLE') {
|
|
257
|
+
L.push('', `**Payback — ${projection.snapshot.payback.status.replaceAll('_', ' ')}:** ${projection.snapshot.payback.result}`);
|
|
258
|
+
L.push(`Basis: ${projection.snapshot.payback.basis}`, '');
|
|
259
|
+
}
|
|
260
|
+
else
|
|
261
|
+
L.push('');
|
|
262
|
+
L.push('### Options at a glance', '', '| Path | Commitment | Basis | Trade-off |', '|---|---:|---|---|');
|
|
263
|
+
for (const option of projection.snapshot.options) {
|
|
264
|
+
L.push(`| ${cell(option.label)} | ${cell(option.commitment)} | ${option.commitmentKind.replace('_', ' ')} | ${cell(option.tradeoff)} |`);
|
|
265
|
+
}
|
|
266
|
+
if (projection.snapshot.tripwire) {
|
|
267
|
+
const tripwire = projection.snapshot.tripwire;
|
|
268
|
+
L.push('', '### Go/no-go tripwire', '', `**${tripwire.metric}: ${tripwire.threshold}** — ${tripwire.decisionRule}`, '');
|
|
269
|
+
}
|
|
270
|
+
else
|
|
271
|
+
L.push('');
|
|
272
|
+
}
|
|
273
|
+
for (const section of projection.sections) {
|
|
274
|
+
L.push(`## ${section.heading}`, '', section.summary, '');
|
|
275
|
+
for (const bullet of section.bullets)
|
|
276
|
+
L.push(`- ${bullet}`);
|
|
277
|
+
if (section.bullets.length)
|
|
278
|
+
L.push('');
|
|
279
|
+
}
|
|
280
|
+
if (projection.featureBacklog) {
|
|
281
|
+
L.push('## Feature priorities', '', '| Priority | Feature | User value | Why now | Effort |', '|---|---|---|---|---|');
|
|
282
|
+
for (const [priority, items] of [
|
|
283
|
+
['MUST', projection.featureBacklog.must],
|
|
284
|
+
['SHOULD', projection.featureBacklog.should],
|
|
285
|
+
['LATER', projection.featureBacklog.later],
|
|
286
|
+
]) {
|
|
287
|
+
for (const item of items) {
|
|
288
|
+
L.push(`| ${priority} | ${cell(item.feature)} | ${cell(item.user_value)} | ${cell(item.rationale)} | ${item.effort} |`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (projection.featureBacklog.wont.length) {
|
|
292
|
+
L.push('', '**Not in this scope**', '');
|
|
293
|
+
for (const item of projection.featureBacklog.wont)
|
|
294
|
+
L.push(`- **${item.feature}:** ${item.reason}`);
|
|
295
|
+
}
|
|
296
|
+
L.push('');
|
|
297
|
+
}
|
|
298
|
+
if (projection.implementationPlan) {
|
|
299
|
+
L.push('## Build plan', '', '| # | Timebox | Outcome | Work | Done when |', '|---|---|---|---|---|');
|
|
300
|
+
for (const milestone of projection.implementationPlan.milestones) {
|
|
301
|
+
L.push(`| ${milestone.order} | ${cell(milestone.timebox)} | ${cell(milestone.outcome)} | ${cell(milestone.tasks.join('; '))} | ${cell(milestone.acceptance_test)} |`);
|
|
302
|
+
}
|
|
303
|
+
L.push('');
|
|
304
|
+
}
|
|
305
|
+
if (projection.caveats.length) {
|
|
306
|
+
L.push('## Top caveats', '');
|
|
307
|
+
for (const caveat of projection.caveats)
|
|
308
|
+
L.push(`- ${caveat}`);
|
|
309
|
+
L.push('');
|
|
310
|
+
}
|
|
311
|
+
if (projection.sources.length) {
|
|
312
|
+
L.push('## Sources', '');
|
|
313
|
+
const externalCount = report.dossier.evidence.filter((item) => evidenceOrigin(item) === 'EXTERNAL').length;
|
|
314
|
+
L.push(`${externalCount} independent external source${externalCount === 1 ? '' : 's'}; everything else is your own material or model background.`, '');
|
|
315
|
+
for (const source of projection.sources) {
|
|
316
|
+
const citationScope = source.citedFor.length ? ` — Cited for: ${source.citedFor.join('; ')}` : '';
|
|
317
|
+
L.push(`${source.url ? `- [${source.label.replace(/[\[\]]/g, '')}](${source.url})` : `- ${source.label}`}${citationScope}`);
|
|
318
|
+
}
|
|
319
|
+
L.push('');
|
|
320
|
+
}
|
|
321
|
+
L.push('## Next step', '', projection.nextStep, '');
|
|
322
|
+
const legacy = renderLegacyDecisionDossierMarkdown(report).split('\n').slice(2).join('\n');
|
|
323
|
+
L.push('## Council audit', '', '<details>', '<summary>Show the council reasoning, evidence ledger, dissent, and run receipt</summary>', '', legacy, '', '</details>', '');
|
|
324
|
+
return L.join('\n');
|
|
325
|
+
}
|
|
326
|
+
/** Pre-v5 deterministic report, retained as the replay fallback and the collapsed v5 audit body. */
|
|
327
|
+
function renderLegacyDecisionDossierMarkdown(report) {
|
|
61
328
|
const { dossier } = report;
|
|
329
|
+
const labelFor = claimLookup(report);
|
|
62
330
|
const keyFindings = report.keyFindings?.length ? report.keyFindings : [dossier.recommendation.reason];
|
|
63
331
|
const criticalUnknowns = report.criticalUnknowns?.length ? report.criticalUnknowns : report.openQuestions.slice(0, 3);
|
|
64
332
|
const coverage = report.confidenceBreakdown.verificationCoverage;
|
|
333
|
+
const allConditions = [...new Set(dossier.recommendation.conditions.map((condition) => stripReaderClaimIds(condition.text, labelFor)))];
|
|
334
|
+
const conditions = allConditions.slice(0, 4);
|
|
335
|
+
const verdictClaimId = report.claims.find((claim) => /^Verdict:/i.test(claim.text))?.id;
|
|
336
|
+
const seenQuestions = new Set();
|
|
337
|
+
const openQuestions = report.openQuestions.filter((question) => {
|
|
338
|
+
const key = question.slice(0, 60).toLowerCase();
|
|
339
|
+
if (seenQuestions.has(key))
|
|
340
|
+
return false;
|
|
341
|
+
seenQuestions.add(key);
|
|
342
|
+
return true;
|
|
343
|
+
});
|
|
65
344
|
const L = [report.mode === 'quick' ? '# Single-Model Decision Report' : '# Multi-Model Decision Report', ''];
|
|
66
345
|
L.push('## 1. Decision', '', ...degradation(report, ['synthesis_suspect']));
|
|
67
346
|
if (report.decisionSnapshot) {
|
|
@@ -69,7 +348,7 @@ export function renderDecisionDossierMarkdown(report) {
|
|
|
69
348
|
for (const item of report.decisionSnapshot.decisiveNumbers) {
|
|
70
349
|
L.push(`| ${cell(item.label)} | ${cell(item.value)} | ${cell(item.meaning)} | ${cell(readerClaimRefs(report, item.claimIds))} |`);
|
|
71
350
|
}
|
|
72
|
-
if (report.decisionSnapshot.payback) {
|
|
351
|
+
if (report.decisionSnapshot.payback && report.decisionSnapshot.payback.status !== 'NOT_COMPUTABLE') {
|
|
73
352
|
const payback = report.decisionSnapshot.payback;
|
|
74
353
|
L.push('', `**Payback — ${payback.status.replaceAll('_', ' ')}:** ${payback.result}`);
|
|
75
354
|
L.push(`Basis: ${payback.basis}`, '');
|
|
@@ -78,7 +357,9 @@ export function renderDecisionDossierMarkdown(report) {
|
|
|
78
357
|
L.push('');
|
|
79
358
|
}
|
|
80
359
|
L.push(`**Recommendation:** ${dossier.recommendation.summary}`, '');
|
|
81
|
-
L.push(
|
|
360
|
+
L.push(report.confidenceBreakdown.verificationScope === 'FACTUAL'
|
|
361
|
+
? `**Evidence coverage:** ${coverageLabel(coverage)} — ${pct(coverage)} of checkable factual claims independently verified; design judgments are adjudicated by the chair, not verified.`
|
|
362
|
+
: `**Evidence coverage:** ${coverageLabel(coverage)} — ${pct(coverage)} of load-bearing claims independently verified.`);
|
|
82
363
|
L.push(coverage < 0.5
|
|
83
364
|
? '> Low coverage means important inputs remain unchecked; it is not a probability that the recommendation is correct.'
|
|
84
365
|
: '> Evidence coverage measures independent checking; it is not a probability that the recommendation is correct.', '');
|
|
@@ -107,21 +388,10 @@ export function renderDecisionDossierMarkdown(report) {
|
|
|
107
388
|
L.push('- None recorded.');
|
|
108
389
|
L.push('', `**Critical warning:** ${report.verdict.criticalWarning ?? 'None recorded.'}`);
|
|
109
390
|
L.push(`**Council read:** ${councilRead(report)}`);
|
|
110
|
-
L.push(
|
|
391
|
+
L.push('');
|
|
111
392
|
if (!dossier.recommendation.claimIds.length)
|
|
112
393
|
L.push('> ⚠ DEGRADED: recommendation has no stored graph anchor.', '');
|
|
113
|
-
L.push('
|
|
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']));
|
|
394
|
+
L.push('## 2. Deliverables and action plan', '', ...degradation(report, ['plan_fallback', 'plan_skipped']));
|
|
125
395
|
if ((dossier.missingRequestedOutputs ?? []).length) {
|
|
126
396
|
L.push(`> ⚠ DEGRADED: requested output missing: ${dossier.missingRequestedOutputs.join(', ')}`, '');
|
|
127
397
|
}
|
|
@@ -163,66 +433,58 @@ export function renderDecisionDossierMarkdown(report) {
|
|
|
163
433
|
L.push('No executable experiment was produced.');
|
|
164
434
|
L.push('');
|
|
165
435
|
L.push('## 3. Why this decision', '');
|
|
166
|
-
|
|
167
|
-
L.push(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
436
|
+
for (const point of keyFindings)
|
|
437
|
+
L.push(`- ${stripReaderClaimIds(point, labelFor)}`);
|
|
438
|
+
if (!keyFindings.length)
|
|
439
|
+
L.push('No chair reasoning was recorded.');
|
|
440
|
+
if (conditions.length) {
|
|
441
|
+
L.push('', '### Conditions', '');
|
|
442
|
+
for (const condition of conditions)
|
|
443
|
+
L.push(`- ${condition}`);
|
|
444
|
+
if (allConditions.length > conditions.length)
|
|
445
|
+
L.push('', `${allConditions.length - conditions.length} additional condition${allConditions.length - conditions.length === 1 ? '' : 's'} remain in the stored audit JSON.`);
|
|
171
446
|
}
|
|
172
|
-
else
|
|
173
|
-
L.push('No graph-anchored decision chain was recorded.');
|
|
174
447
|
L.push('');
|
|
175
448
|
L.push('## 4. What could change the decision', '', '### Decision-sensitive facts', '');
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
449
|
+
const decisiveFacts = dossier.sensitivity.filter((item) => item.sensitivity === 'DECISIVE').slice(0, 5);
|
|
450
|
+
if (decisiveFacts.length) {
|
|
451
|
+
L.push('| Fact | If false | What settles it |', '|---|---|---|');
|
|
452
|
+
for (const item of decisiveFacts) {
|
|
453
|
+
L.push(`| ${cell(claimShortLabel(item.fact))} | ${item.impactIfFalse} | ${cell(item.whatWouldChangeIt)} |`);
|
|
180
454
|
}
|
|
181
455
|
}
|
|
182
456
|
else
|
|
183
457
|
L.push('No verdict-sensitive graph node was recorded.');
|
|
184
458
|
L.push('', '### Strongest counter-case', '');
|
|
185
459
|
if (dossier.counterCase.available) {
|
|
186
|
-
|
|
460
|
+
const counterIds = dossier.counterCase.claimIds.filter((id) => id !== verdictClaimId);
|
|
461
|
+
L.push(dossier.counterCase.reasoning, '', `Evidence behind this counter-case: ${readerClaimRefs(report, counterIds)}`);
|
|
187
462
|
}
|
|
188
463
|
else
|
|
189
464
|
L.push(`> ⚠ DEGRADED: ${dossier.counterCase.reasoning}`);
|
|
190
465
|
L.push('');
|
|
191
|
-
L.push('## 5.
|
|
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', '');
|
|
466
|
+
L.push('## 5. Risks and open questions', '', ...degradation(report, ['verification_skipped', 'research_ungrounded']), '### Risks', '');
|
|
202
467
|
if (report.risks.length) {
|
|
468
|
+
const shownRisks = report.risks.slice(0, 8);
|
|
203
469
|
L.push('| Risk | Severity |', '|---|---|');
|
|
204
|
-
for (const risk of
|
|
470
|
+
for (const risk of shownRisks)
|
|
205
471
|
L.push(`| ${cell(risk.risk)} | ${risk.severity} |`);
|
|
472
|
+
if (report.risks.length > shownRisks.length)
|
|
473
|
+
L.push('', `${report.risks.length - shownRisks.length} lower-severity items — more in the technical audit (full list in the stored JSON).`);
|
|
206
474
|
}
|
|
207
475
|
else
|
|
208
476
|
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
477
|
L.push('', '### Open questions', '');
|
|
218
|
-
if (
|
|
219
|
-
for (const question of
|
|
478
|
+
if (openQuestions.length) {
|
|
479
|
+
for (const question of openQuestions.slice(0, 5))
|
|
220
480
|
L.push(`- ${question}`);
|
|
481
|
+
if (openQuestions.length > 5)
|
|
482
|
+
L.push('', `Showing 5 of ${openQuestions.length} — the rest are in the technical audit.`);
|
|
221
483
|
}
|
|
222
484
|
else
|
|
223
485
|
L.push('No verdict-flipping open question was recorded.');
|
|
224
486
|
L.push('');
|
|
225
|
-
L.push('##
|
|
487
|
+
L.push('## 6. Disagreement and dissent', '', ...degradation(report, ['single_model', 'low_diversity']));
|
|
226
488
|
if (report.disagreements.length) {
|
|
227
489
|
for (const disagreement of report.disagreements) {
|
|
228
490
|
L.push(`- **${disagreement.topic}** — ${disagreement.status}; ${disagreement.ruling}.`);
|
|
@@ -253,30 +515,61 @@ export function renderDecisionDossierMarkdown(report) {
|
|
|
253
515
|
for (const item of report.minority.uniqueOppositions)
|
|
254
516
|
L.push(`- ${providerName(item.provider)} uniquely opposed: ${item.proposition}`);
|
|
255
517
|
L.push(`- Decision-blocking status: ${report.minority.blocksDecision}`, '');
|
|
256
|
-
L.push('##
|
|
257
|
-
if (dossier.
|
|
258
|
-
|
|
259
|
-
for (const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
518
|
+
L.push('## 7. What the council added', '', ...degradation(report, ['weak_seat']));
|
|
519
|
+
if (dossier.seatStats) {
|
|
520
|
+
const weakSeat = dossier.seatStats.some((seat) => seat.positions < 3 || seat.evidenced / seat.positions < 0.5);
|
|
521
|
+
for (const seat of dossier.seatStats) {
|
|
522
|
+
const weak = seat.positions < 3 || seat.evidenced / seat.positions < 0.5;
|
|
523
|
+
L.push(`- ${providerName(seat.provider)}: ${seat.positions} position${seat.positions === 1 ? '' : 's'}, ${seat.evidenced} with evidence${weak ? ' — weak seat this run.' : '.'}`);
|
|
524
|
+
}
|
|
525
|
+
for (const chair of report.models.filter((model) => model.roles.includes('judge') && !dossier.seatStats.some((seat) => seat.provider === model.provider))) {
|
|
526
|
+
L.push(`- ${chair.name}: chaired the decision and authored no scout claims (clean adjudication).`);
|
|
527
|
+
}
|
|
528
|
+
if (dossier.sharedConcerns.length) {
|
|
529
|
+
L.push('', 'Shared concerns:');
|
|
530
|
+
for (const item of dossier.sharedConcerns)
|
|
531
|
+
L.push(`- ${item.text} — ${item.evidenceStatus}; ${item.providerIds.map(providerName).join(', ')}.`);
|
|
532
|
+
}
|
|
533
|
+
L.push('', report.disagreements.length
|
|
534
|
+
? councilRead(report)
|
|
535
|
+
: 'No genuine disagreement survived to the chair.');
|
|
536
|
+
if (weakSeat || coverage < 0.5)
|
|
537
|
+
L.push('Convergence with weak verification lowers confidence; it does not raise it.');
|
|
538
|
+
const verifiedContributions = dossier.contributions.filter((item) => item.verifiedUniqueClaimIds.length > 0);
|
|
539
|
+
if (verifiedContributions.length) {
|
|
540
|
+
L.push('', '### Verified unique contributions', '', ...degradation(report, ['verification_skipped', 'single_model', 'low_diversity']));
|
|
541
|
+
L.push('Only unique claims that survived independent verification receive credit.', '');
|
|
542
|
+
L.push('| Provider | Verified unique contributions | Count |', '|---|---|---|');
|
|
543
|
+
for (const contribution of verifiedContributions) {
|
|
544
|
+
L.push(`| ${contribution.name} | ${cell(readerClaimRefs(report, contribution.verifiedUniqueClaimIds))} | ${contribution.verifiedUniqueClaimIds.length} |`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
269
547
|
}
|
|
270
|
-
else
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
548
|
+
else {
|
|
549
|
+
if (dossier.sharedConcerns.length) {
|
|
550
|
+
L.push('Shared concerns:');
|
|
551
|
+
for (const item of dossier.sharedConcerns)
|
|
552
|
+
L.push(`- ${item.text} — ${item.evidenceStatus}; ${item.providerIds.map(providerName).join(', ')}.`);
|
|
553
|
+
}
|
|
554
|
+
else
|
|
555
|
+
L.push('Shared concerns: none recorded.');
|
|
556
|
+
L.push('');
|
|
557
|
+
if (dossier.uniqueSupportedInsights.length) {
|
|
558
|
+
L.push('Unique supported insights:');
|
|
559
|
+
for (const item of dossier.uniqueSupportedInsights)
|
|
560
|
+
L.push(`- ${item.text} — ${providerName(item.providerId)}; ${item.verificationStatus}.`);
|
|
561
|
+
}
|
|
562
|
+
else
|
|
563
|
+
L.push('Unique supported insights: none recorded.');
|
|
564
|
+
L.push('', '### Verified unique contributions', '', ...degradation(report, ['verification_skipped', 'single_model', 'low_diversity']));
|
|
565
|
+
L.push('Only unique claims that survived independent verification receive credit.', '');
|
|
566
|
+
L.push('| Provider | Verified unique contributions | Count |', '|---|---|---|');
|
|
567
|
+
for (const contribution of dossier.contributions) {
|
|
568
|
+
L.push(`| ${contribution.name} | ${cell(readerClaimRefs(report, contribution.verifiedUniqueClaimIds))} | ${contribution.verifiedUniqueClaimIds.length} |`);
|
|
569
|
+
}
|
|
277
570
|
}
|
|
278
571
|
L.push('');
|
|
279
|
-
L.push('##
|
|
572
|
+
L.push('## 8. Run details', '');
|
|
280
573
|
L.push(`- Report ID: \`${report.reportId}\``);
|
|
281
574
|
L.push(`- Generated: ${report.generatedAt}`);
|
|
282
575
|
L.push(`- Original task: ${report.task.original}`);
|
|
@@ -288,14 +581,77 @@ export function renderDecisionDossierMarkdown(report) {
|
|
|
288
581
|
L.push(`- Models and roles: ${report.models.map((model) => `${model.name} (${model.roles.join(', ')})`).join(' · ') || 'none recorded'}`);
|
|
289
582
|
L.push(`- Mode: ${report.mode}`);
|
|
290
583
|
L.push(`- Provider calls: ${report.receipt.calls}/${report.receipt.budget}`);
|
|
584
|
+
if (report.autoEscalationReasons?.length)
|
|
585
|
+
L.push(`- Adaptive escalation: ${report.autoEscalationReasons.join('; ')}`);
|
|
291
586
|
L.push(`- Categories: discovery ${report.receipt.categories.discovery} · verification ${report.receipt.categories.verification} · repair ${report.receipt.categories.repair} · planning ${report.receipt.categories.planning}`);
|
|
292
587
|
L.push(`- By provider: ${Object.entries(report.receipt.byProvider).map(([provider, count]) => `${providerName(provider)} ${count}`).join(', ') || 'none'}`);
|
|
293
588
|
L.push(`- Recorded model time: ${(report.receipt.modelTimeMs / 1000).toFixed(1)}s`);
|
|
589
|
+
if (report.receipt.tokens)
|
|
590
|
+
L.push(`- Tokens: ${formatTokenLine(report.receipt.tokens)}`);
|
|
294
591
|
L.push(`- Degradation flags: ${report.flags.join(', ') || 'none'}`, '');
|
|
592
|
+
// This pass runs over already-built (cell-escaped) table rows, so injected labels must be cell-escaped too.
|
|
295
593
|
for (let index = 0; index < L.length; index++)
|
|
296
|
-
L[index] = stripReaderClaimIds(L[index]);
|
|
297
|
-
L.push('##
|
|
298
|
-
L.push('<details>', '<summary>
|
|
594
|
+
L[index] = stripReaderClaimIds(L[index], labelFor, cell);
|
|
595
|
+
L.push('## 9. Technical audit', '');
|
|
596
|
+
L.push('<details>', '<summary>Evidence, coverage, claims, and graph events</summary>', '');
|
|
597
|
+
L.push('### Decision confidence', '');
|
|
598
|
+
L.push(`- Decision state: ${dossier.recommendation.status}`);
|
|
599
|
+
L.push(`- Structural score: ${report.confidenceBreakdown.score}/100 (${report.confidenceBreakdown.label}); heuristic, not benchmark-calibrated.`);
|
|
600
|
+
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}`, '');
|
|
601
|
+
L.push('### Full claim chain', '');
|
|
602
|
+
if (dossier.claimChain.length) {
|
|
603
|
+
L.push('| Claim | Ruling | Evidence status | Depends on |', '|---|---|---|---|');
|
|
604
|
+
for (const claim of dossier.claimChain) {
|
|
605
|
+
L.push(`| ${cell(claim.text)} | ${claim.ruling} | ${claim.evidenceStatus} | ${cell(readerClaimRefs(report, claim.dependsOn))} |`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else
|
|
609
|
+
L.push('No graph-anchored decision chain was recorded.');
|
|
610
|
+
L.push('', '### Full decision-sensitivity ledger', '');
|
|
611
|
+
if (dossier.sensitivity.length) {
|
|
612
|
+
L.push('| Fact | Sensitivity | If false | What would change it | Linked claims |', '|---|---|---|---|---|');
|
|
613
|
+
for (const item of dossier.sensitivity) {
|
|
614
|
+
L.push(`| ${cell(item.fact)} | ${item.sensitivity} | ${item.impactIfFalse} | ${cell(item.whatWouldChangeIt)} | ${cell(readerClaimRefs(report, item.linkedClaimIds))} |`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else
|
|
618
|
+
L.push('No verdict-sensitive graph node was recorded.');
|
|
619
|
+
L.push('', '### Evidence and verification', '');
|
|
620
|
+
if (dossier.evidence.length) {
|
|
621
|
+
L.push('| Evidence ID | Source | Date | Freshness | Verification | Linked claims |', '|---|---|---|---|---|---|');
|
|
622
|
+
for (const evidence of dossier.evidence) {
|
|
623
|
+
// v6: the user's own material can corroborate what they told us — never "verify" it.
|
|
624
|
+
const verification = evidenceOrigin(evidence) === 'USER_MATERIAL'
|
|
625
|
+
? 'consistent with your materials (not independently checked)'
|
|
626
|
+
: evidence.verificationStatus;
|
|
627
|
+
L.push(`| ${evidence.id} | ${cell(evidence.source)} (${evidence.sourceKind}) | ${evidence.date} | ${evidence.freshness} | ${verification} | ${cell(readerClaimRefs(report, evidence.claimIds))} |`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
else
|
|
631
|
+
L.push('No evidence cards were stored.');
|
|
632
|
+
L.push('', '### Coverage ledger', '');
|
|
633
|
+
if (dossier.coverage.length) {
|
|
634
|
+
L.push('| Dimension | Status | Related claims |', '|---|---|---|');
|
|
635
|
+
for (const item of dossier.coverage)
|
|
636
|
+
L.push(`| ${cell(item.label)} | ${item.status} | ${cell(readerClaimRefs(report, item.claimIds))} |`);
|
|
637
|
+
}
|
|
638
|
+
else
|
|
639
|
+
L.push('No rubric coverage ledger was recorded.');
|
|
640
|
+
L.push('', '### Full risk ledger', '');
|
|
641
|
+
if (report.risks.length) {
|
|
642
|
+
L.push('| Risk | Severity |', '|---|---|');
|
|
643
|
+
for (const risk of report.risks)
|
|
644
|
+
L.push(`| ${cell(risk.risk)} | ${risk.severity} |`);
|
|
645
|
+
}
|
|
646
|
+
else
|
|
647
|
+
L.push('No material risk was recorded.');
|
|
648
|
+
L.push('', '### Full open-question ledger', '');
|
|
649
|
+
if (report.openQuestions.length)
|
|
650
|
+
for (const question of report.openQuestions)
|
|
651
|
+
L.push(`- ${question}`);
|
|
652
|
+
else
|
|
653
|
+
L.push('No verdict-flipping open question was recorded.');
|
|
654
|
+
L.push('', '### Original submissions and graph events', '');
|
|
299
655
|
for (const submission of dossier.technical.submissions) {
|
|
300
656
|
L.push(`- **${submission.name}:** ${submission.strongestVersion} (${refs(submission.positionIds)})`);
|
|
301
657
|
}
|
|
@@ -318,3 +674,8 @@ export function renderDecisionDossierMarkdown(report) {
|
|
|
318
674
|
L.push('', '</details>', '');
|
|
319
675
|
return L.join('\n');
|
|
320
676
|
}
|
|
677
|
+
/** Canonical Markdown. HTML and Copy-Markdown consume the same persisted reader brief.
|
|
678
|
+
* v6: every rendered artifact is path-sanitized; stored run JSON keeps raw locators for audit. */
|
|
679
|
+
export function renderDecisionDossierMarkdown(report) {
|
|
680
|
+
return sanitizeLocalPaths(report.dossier.readerBrief ? renderReaderBriefMarkdown(report) : renderLegacyDecisionDossierMarkdown(report));
|
|
681
|
+
}
|