aiki-cli 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +15 -5
- package/dist/cli/index.js +1 -1
- package/dist/cli/run.js +9 -6
- package/dist/council/view.js +99 -39
- package/dist/orchestration/decision-dossier.js +79 -21
- package/dist/orchestration/modes.js +6 -0
- package/dist/orchestration/preflight.js +28 -8
- package/dist/orchestration/quick-analysis.js +15 -3
- package/dist/orchestration/stages/s10-render.js +12 -0
- package/dist/orchestration/stages/s9b-plan.js +33 -10
- package/dist/orchestration/url-sources.js +200 -0
- package/dist/schemas/index.js +67 -3
- package/dist/storage/runs.js +2 -1
- package/dist/tui/app.js +19 -4
- package/dist/workflows/idea-refinement.js +11 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Public URL snapshots for idea prompts, including an npm registry adapter, private-network rejection,
|
|
7
|
+
auditable `FETCHED` / `BLOCKED` / `FAILED` status, and a research-mode gate that stops before model calls
|
|
8
|
+
when a supplied source cannot be read.
|
|
9
|
+
- Requested-output planning: prompts that ask for a feature list or implementation plan now carry that
|
|
10
|
+
requirement through the decision contract, the existing planner call, machine JSON, Markdown, and HTML.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Explicit research wording selects research mode deterministically when `--mode` is absent; an explicit
|
|
14
|
+
mode remains authoritative. Preflight asks zero to four questions and may not repeat facts already present
|
|
15
|
+
in the user prompt or a fetched source.
|
|
16
|
+
- Reader-facing report sections use human claim text instead of internal graph ids such as `G1`; raw ids
|
|
17
|
+
remain in machine artifacts and the Markdown technical audit.
|
|
18
|
+
|
|
3
19
|
## 0.3.0 — 2026-07-15 — evidence-grounded decision council
|
|
4
20
|
|
|
5
21
|
This release turns idea refinement into a bounded, evidence-linked decision workflow with explicit modes,
|
package/README.md
CHANGED
|
@@ -50,9 +50,9 @@ stop copy-pasting between them by hand.
|
|
|
50
50
|
- **Evidence-grounded decisions.** Supply local sources with `--evidence`; Aiki records their paths and
|
|
51
51
|
hashes, checks freshness and citations, independently verifies selected load-bearing claims, and shows the
|
|
52
52
|
remaining coverage gaps before the chair.
|
|
53
|
-
- **Three
|
|
54
|
-
deliberation, or `research` for current-fact work
|
|
55
|
-
|
|
53
|
+
- **Three bounded modes.** Use `quick` for one structured analyst, `council` for bounded multi-model
|
|
54
|
+
deliberation, or `research` for current-fact work. Explicit research wording selects `research`
|
|
55
|
+
deterministically; `--mode` always overrides it.
|
|
56
56
|
- **A decision dossier, not an essay.** Reports lead with the recommendation, verified evidence coverage,
|
|
57
57
|
decisive facts, first action, strongest counter-case, and critical unknowns. Financial and threshold-heavy
|
|
58
58
|
decisions can include graph-anchored numbers, payback, option commitments, and a go/no-go tripwire.
|
|
@@ -249,6 +249,12 @@ An idea run **auto-opens** its report in your browser when it finishes.
|
|
|
249
249
|
SHA-256 hashes in the run, gives those paths to the read-only scouts, and never copies the source files or
|
|
250
250
|
reads provider credential directories.
|
|
251
251
|
|
|
252
|
+
Public `http(s)` links in an idea are snapshotted once before model calls and stored in
|
|
253
|
+
`00a-url-sources.json`; every preflight reader and analyst sees that same text. npm package links use the
|
|
254
|
+
registry metadata API. A snapshot records `FETCHED`, `BLOCKED`, or `FAILED` honestly. In research mode,
|
|
255
|
+
an unreadable supplied link stops before paid calls and asks for pasted text or a public export instead of
|
|
256
|
+
letting the council guess or grill you for facts that should have come from the link.
|
|
257
|
+
|
|
252
258
|
## The two workflows
|
|
253
259
|
|
|
254
260
|
<p align="center">
|
|
@@ -263,8 +269,10 @@ only the disputes → report.
|
|
|
263
269
|
complementary analyst lanes → deterministic claim/evidence graph audit → only decision-critical verification
|
|
264
270
|
or rebuttal → evidence-linked chair → validation planner. The report is a graph-backed **decision dossier**,
|
|
265
271
|
not an essay.
|
|
266
|
-
Choose `--mode quick` for one structured analyst, `--mode council`
|
|
267
|
-
|
|
272
|
+
Choose `--mode quick` for one structured analyst, `--mode council` for the full decision council, or
|
|
273
|
+
`--mode research` for source-grounded current-fact work. Without a flag, the default is council unless the
|
|
274
|
+
request explicitly asks to research, browse, look up, or check current sources; that conservative rule is
|
|
275
|
+
deterministic, and an explicit flag wins:
|
|
268
276
|
|
|
269
277
|
- a **reader-first decision card** — recommendation, verified evidence coverage, decisive facts, first action,
|
|
270
278
|
strongest counter-case, and critical unknowns before audit detail
|
|
@@ -274,6 +282,8 @@ or `--mode research` for source-grounded current-fact work. Aiki never chooses a
|
|
|
274
282
|
- an **evidence and coverage ledger** — source, date, freshness, verification, `NOT_APPLICABLE`, and missing evidence
|
|
275
283
|
- **genuine disagreements and position changes** — explicit `CONCEDE` / `COUNTER` / `NARROW` events
|
|
276
284
|
- **decision sensitivity and an executable experiment plan** — anchored tests with effort and kill signals
|
|
285
|
+
- **requested product deliverables** — when the prompt asks for them, the same planner call must produce a
|
|
286
|
+
prioritized `MUST / SHOULD / LATER / NOT IN SCOPE` feature list and timeboxed implementation milestones
|
|
277
287
|
- a **verified contribution ledger** — unique provider claims count only after independent verification
|
|
278
288
|
- an **orchestration receipt and technical fold** — calls, degradation, submissions, edges, and graph events
|
|
279
289
|
|
package/dist/cli/index.js
CHANGED
|
@@ -51,7 +51,7 @@ program
|
|
|
51
51
|
.option('--head <ref>', 'code-review: head git ref to diff to (default HEAD)')
|
|
52
52
|
.option('--diff <file>', 'code-review: review a patch file instead of computing a git diff')
|
|
53
53
|
.option('--evidence <path>', 'idea-refinement: local source file/directory (stores paths + hashes, not copies)')
|
|
54
|
-
.option('--mode <mode>', 'idea-refinement: quick | council | research (default council)')
|
|
54
|
+
.option('--mode <mode>', 'idea-refinement: quick | council | research (default council; explicit research wording selects research)')
|
|
55
55
|
.option('--cheap', 'code-review: Gemini+Codex review, Claude judges only disputes (~⅓ the Opus; experimental)')
|
|
56
56
|
.option('--yes', 'skip the run-cost confirmation prompt')
|
|
57
57
|
.action(async (workflow, input, opts) => {
|
package/dist/cli/run.js
CHANGED
|
@@ -11,7 +11,7 @@ import { resolveRunsRoot } from '../storage/paths.js';
|
|
|
11
11
|
import { openCouncilHtml } from '../council/open.js';
|
|
12
12
|
import { buildEvidencePack } from '../orchestration/evidence-pack.js';
|
|
13
13
|
import { IdeaModeSchema } from '../schemas/index.js';
|
|
14
|
-
import { defaultBudgetFor, defaultDeadlineFor, IDEA_MODE_PLANS } from '../orchestration/modes.js';
|
|
14
|
+
import { defaultBudgetFor, defaultDeadlineFor, IDEA_MODE_PLANS, inferIdeaMode } from '../orchestration/modes.js';
|
|
15
15
|
const WORKFLOWS = ['idea-refinement', 'code-review'];
|
|
16
16
|
export function estimateRun(workflow, opts = {}) {
|
|
17
17
|
if (workflow === 'code-review')
|
|
@@ -91,12 +91,14 @@ export async function runCommand(workflow, input, opts = {}) {
|
|
|
91
91
|
}
|
|
92
92
|
let mode;
|
|
93
93
|
if (workflow === 'idea-refinement') {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
if (opts.mode !== undefined) {
|
|
95
|
+
const parsed = IdeaModeSchema.safeParse(opts.mode);
|
|
96
|
+
if (!parsed.success) {
|
|
97
|
+
process.stderr.write(`unknown idea mode "${opts.mode}". Available: quick, council, research\n`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
mode = parsed.data;
|
|
98
101
|
}
|
|
99
|
-
mode = parsed.data;
|
|
100
102
|
}
|
|
101
103
|
else if (opts.mode) {
|
|
102
104
|
process.stderr.write('--mode only applies to idea-refinement.\n');
|
|
@@ -118,6 +120,7 @@ export async function runCommand(workflow, input, opts = {}) {
|
|
|
118
120
|
return 1;
|
|
119
121
|
}
|
|
120
122
|
text = resolved;
|
|
123
|
+
mode ??= inferIdeaMode(text);
|
|
121
124
|
}
|
|
122
125
|
if (opts.evidence) {
|
|
123
126
|
if (workflow !== 'idea-refinement') {
|
package/dist/council/view.js
CHANGED
|
@@ -3,7 +3,7 @@ import { basename, join } from 'node:path';
|
|
|
3
3
|
import { DISPLAY_NAME } from '../providers/types.js';
|
|
4
4
|
import { RoleOutput as RoleOutputSchema } from '../schemas/index.js';
|
|
5
5
|
import { deriveAudit, deriveScorecard, mergeOpenQuestions } from '../orchestration/stages/s10-render.js';
|
|
6
|
-
import { renderDecisionDossierMarkdown } from '../orchestration/decision-dossier.js';
|
|
6
|
+
import { readerClaimLabel, readerClaimRefs, renderDecisionDossierMarkdown, stripReaderClaimIds } from '../orchestration/decision-dossier.js';
|
|
7
7
|
import { IDEA_RUBRIC } from '../workflows/idea-refinement.js';
|
|
8
8
|
import { listArtifacts, readJsonArtifact } from '../storage/runs-read.js';
|
|
9
9
|
import { adaptIdeaOutput, adaptLegacyDecisionGraph } from '../orchestration/legacy-idea-adapter.js';
|
|
@@ -378,30 +378,33 @@ function renderDossierIdeaBody(report) {
|
|
|
378
378
|
const tone = dossier.recommendation.status === 'ACCEPTED' ? 'good'
|
|
379
379
|
: dossier.recommendation.status === 'REJECTED' ? 'risk' : 'caution';
|
|
380
380
|
const conditions = dossier.recommendation.conditions.length
|
|
381
|
-
? `<details class="decision-details"><summary>Conditions and decision state</summary><div><p><strong>Internal state:</strong> ${escapeHtml(dossier.recommendation.status)}</p><ul>${dossier.recommendation.conditions.map((condition) => `<li>${escapeHtml(condition.text)}
|
|
381
|
+
? `<details class="decision-details"><summary>Conditions and decision state</summary><div><p><strong>Internal state:</strong> ${escapeHtml(dossier.recommendation.status)}</p><ul>${dossier.recommendation.conditions.map((condition) => `<li>${escapeHtml(condition.text)}</li>`).join('')}</ul></div></details>`
|
|
382
382
|
: '';
|
|
383
383
|
const startHere = dossier.experiments.actions[0]?.action ?? 'No executable next step was produced.';
|
|
384
|
+
const recommendationLead = stripReaderClaimIds(dossier.recommendation.reason);
|
|
385
|
+
const recommendationDetail = stripReaderClaimIds(dossier.recommendation.summary);
|
|
384
386
|
const factLabels = ['Decisive result', 'Consequence', 'Supporting signal'];
|
|
385
387
|
const facts = keyFindings.slice(0, 3).map((finding, index) => `<article class="decision-fact"><span>${factLabels[index]}</span><p>${escapeHtml(finding)}</p></article>`).join('');
|
|
386
388
|
const snapshot = report.decisionSnapshot;
|
|
387
389
|
const decisiveNumbers = snapshot ? `<div class="decision-numbers">
|
|
388
390
|
<span class="section-eyebrow">Decisive numbers</span>
|
|
389
|
-
<div class="table-wrap"><table class="snapshot-table"><thead><tr><th>Metric</th><th>Value</th><th>What it means</th></tr></thead><tbody>${snapshot.decisiveNumbers.map((item) => `<tr><td>${escapeHtml(item.label)}</td><td class="number-value">${escapeHtml(item.value)}</td><td>${escapeHtml(item.meaning)}
|
|
390
|
-
${snapshot.payback ? `<div class="payback-result"><span>Payback · ${escapeHtml(snapshot.payback.status.replaceAll('_', ' '))}</span><strong>${escapeHtml(snapshot.payback.result)}</strong><p>${escapeHtml(snapshot.payback.basis)}
|
|
391
|
+
<div class="table-wrap"><table class="snapshot-table decisive-table"><thead><tr><th>Metric</th><th>Value</th><th>What it means</th></tr></thead><tbody>${snapshot.decisiveNumbers.map((item) => `<tr><td>${escapeHtml(item.label)}</td><td class="number-value">${escapeHtml(item.value)}</td><td>${escapeHtml(item.meaning)}</td></tr>`).join('')}</tbody></table></div>
|
|
392
|
+
${snapshot.payback ? `<div class="payback-result"><span>Payback · ${escapeHtml(snapshot.payback.status.replaceAll('_', ' '))}</span><strong>${escapeHtml(snapshot.payback.result)}</strong><p>${escapeHtml(snapshot.payback.basis)}</p></div>` : ''}
|
|
391
393
|
</div>` : '';
|
|
392
394
|
const optionComparison = snapshot ? `<div class="option-comparison">
|
|
393
395
|
<span class="section-eyebrow">Options at a glance</span>
|
|
394
|
-
<div class="table-wrap"><table class="snapshot-table"><thead><tr><th>Path</th><th>Commitment</th><th>Basis</th><th>Trade-off</th></tr></thead><tbody>${snapshot.options.map((option) => `<tr><td>${escapeHtml(option.label)}</td><td class="number-value">${escapeHtml(option.commitment)}</td><td><span class="basis-chip ${option.commitmentKind.toLowerCase()}">${escapeHtml(option.commitmentKind.replace('_', ' '))}</span></td><td>${escapeHtml(option.tradeoff)}
|
|
396
|
+
<div class="table-wrap"><table class="snapshot-table options-table"><thead><tr><th>Path</th><th>Commitment</th><th>Basis</th><th>Trade-off</th></tr></thead><tbody>${snapshot.options.map((option) => `<tr><td>${escapeHtml(option.label)}</td><td class="number-value">${escapeHtml(option.commitment)}</td><td><span class="basis-chip ${option.commitmentKind.toLowerCase()}">${escapeHtml(option.commitmentKind.replace('_', ' '))}</span></td><td>${escapeHtml(option.tradeoff)}</td></tr>`).join('')}</tbody></table></div>
|
|
395
397
|
</div>` : '';
|
|
396
|
-
const tripwire = snapshot?.tripwire ? `<div class="tripwire"><span class="tag">Go/no-go tripwire</span><strong>${escapeHtml(snapshot.tripwire.metric)} · ${escapeHtml(snapshot.tripwire.threshold)}</strong><p>${escapeHtml(snapshot.tripwire.decisionRule)}
|
|
398
|
+
const tripwire = snapshot?.tripwire ? `<div class="tripwire"><span class="tag">Go/no-go tripwire</span><strong>${escapeHtml(snapshot.tripwire.metric)} · ${escapeHtml(snapshot.tripwire.threshold)}</strong><p>${escapeHtml(snapshot.tripwire.decisionRule)}</p></div>` : '';
|
|
397
399
|
const topCounterCase = dossier.counterCase.reasoning;
|
|
398
400
|
const unknowns = criticalUnknowns.length
|
|
399
401
|
? `<ul class="critical-unknowns">${criticalUnknowns.map((unknown) => `<li>${escapeHtml(unknown)}</li>`).join('')}</ul>`
|
|
400
402
|
: '<p class="muted">No verdict-flipping unknown was recorded.</p>';
|
|
401
403
|
const recommendation = `<div class="verdict tone-${tone}">
|
|
402
|
-
<span class="pill">Council recommendation</span>
|
|
404
|
+
<div class="decision-status"><span class="pill">Council recommendation</span><span>${escapeHtml(dossier.recommendation.status.replaceAll('_', ' '))}</span></div>
|
|
403
405
|
${decisiveNumbers}
|
|
404
|
-
<p class="verdict-text">${escapeHtml(
|
|
406
|
+
<p class="verdict-text">${escapeHtml(recommendationLead)}</p>
|
|
407
|
+
<p class="verdict-detail">${escapeHtml(recommendationDetail)}</p>
|
|
405
408
|
<div class="evidence-coverage ${coverageLabel.toLowerCase()}">
|
|
406
409
|
<div><span class="fk">Evidence coverage</span><strong>${coverageLabel} · ${dossierPct(confidence.verificationCoverage)}</strong></div>
|
|
407
410
|
<div class="coverage-track"><span style="width:${Math.round(confidence.verificationCoverage * 100)}%"></span></div>
|
|
@@ -418,17 +421,17 @@ function renderDossierIdeaBody(report) {
|
|
|
418
421
|
${report.verdict.criticalWarning ? `<div class="critical-warning"><span class="tag">Critical warning</span><p>${escapeHtml(report.verdict.criticalWarning)}</p></div>` : ''}
|
|
419
422
|
<p class="council-read"><strong>Council read:</strong> ${escapeHtml(dossierCouncilRead(report))}</p>
|
|
420
423
|
${conditions}
|
|
421
|
-
|
|
424
|
+
${dossier.recommendation.claimIds.length ? `<details class="decision-evidence"><summary>Evidence behind this recommendation (${dossier.recommendation.claimIds.length})</summary><ul>${dossier.recommendation.claimIds.map((id) => `<li>${escapeHtml(readerClaimLabel(report, id))}</li>`).join('')}</ul></details>` : ''}
|
|
422
425
|
${dossierWarning(report, ['synthesis_suspect'])}
|
|
423
426
|
${dossier.recommendation.claimIds.length ? '' : '<div class="warns"><span class="warn">⚑ DEGRADED: recommendation has no stored graph anchor</span></div>'}
|
|
424
427
|
</div>`;
|
|
425
|
-
const claimChain = dossierTable(['Claim
|
|
426
|
-
const evidence = `${dossierWarning(report, ['verification_skipped', 'research_ungrounded'])}${dossierTable(['Evidence ID', 'Source', 'Date', 'Freshness', 'Verification', 'Linked claims'], dossier.evidence.map((item) => [item.id, `${item.source} (${item.sourceKind})`, item.date, item.freshness, item.verificationStatus,
|
|
428
|
+
const claimChain = dossierTable(['Claim', 'Ruling', 'Evidence status', 'Depends on'], dossier.claimChain.map((claim) => [claim.text, claim.ruling, claim.evidenceStatus, readerClaimRefs(report, claim.dependsOn)]));
|
|
429
|
+
const evidence = `${dossierWarning(report, ['verification_skipped', 'research_ungrounded'])}${dossierTable(['Evidence ID', 'Source', 'Date', 'Freshness', 'Verification', 'Linked claims'], dossier.evidence.map((item) => [item.id, `${item.source} (${item.sourceKind})`, item.date, item.freshness, item.verificationStatus, readerClaimRefs(report, item.claimIds)]))}`;
|
|
427
430
|
const disagreementCards = report.disagreements.length
|
|
428
|
-
? report.disagreements.map((item) => `<article class="card debate-card"><h3
|
|
431
|
+
? report.disagreements.map((item) => `<article class="card debate-card"><h3>${escapeHtml(item.topic)}</h3>${item.sides.map((side) => `<div class="field"><span class="fk">${escapeHtml(side.stance)} · ${escapeHtml(side.providers.map(providerName).join(', '))}</span><p>${escapeHtml(side.reasoning.join(' '))}</p></div>`).join('')}<div class="field"><span class="fk">Ruling</span><p>${escapeHtml(item.status)} · ${escapeHtml(item.ruling)}</p></div>${item.reasoning ? `<div class="field"><span class="fk">Why</span><p>${escapeHtml(item.reasoning)}</p></div>` : ''}</article>`).join('')
|
|
429
432
|
: `<p class="muted">${report.mode === 'quick' ? 'No cross-model disagreement analysis runs in quick mode.' : 'No genuine disagreements were stored.'}</p>`;
|
|
430
433
|
const changes = dossier.positionChanges.length
|
|
431
|
-
? dossierTable(['Event', 'Claim', 'Responder', 'Change', 'Evidence', 'Detail'], dossier.positionChanges.map((event) => [event.eventId, event.claimId, providerName(event.responder), event.response, dossierRefs(event.evidenceIds), event.narrowedProposition ?? event.reasoning]))
|
|
434
|
+
? dossierTable(['Event', 'Claim', 'Responder', 'Change', 'Evidence', 'Detail'], dossier.positionChanges.map((event) => [event.eventId, readerClaimLabel(report, event.claimId), providerName(event.responder), event.response, dossierRefs(event.evidenceIds), event.narrowedProposition ?? event.reasoning]))
|
|
432
435
|
: '<p class="muted">No CONCEDE, COUNTER, or NARROW event was recorded.</p>';
|
|
433
436
|
const minority = `<h3>Minority report</h3>${report.minority.dissent.length
|
|
434
437
|
? `<ul class="checks">${report.minority.dissent.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>`
|
|
@@ -437,18 +440,29 @@ function renderDossierIdeaBody(report) {
|
|
|
437
440
|
<p class="lede">Decision-blocking status: <strong>${escapeHtml(report.minority.blocksDecision)}</strong></p>`;
|
|
438
441
|
const disagreement = `${dossierWarning(report, ['single_model', 'low_diversity'])}${disagreementCards}<h3>Position changes</h3>${changes}${minority}`;
|
|
439
442
|
const shared = dossier.sharedConcerns.length
|
|
440
|
-
? `<h3>Shared concerns</h3><ul class="agree">${dossier.sharedConcerns.map((item) => `<li><p
|
|
443
|
+
? `<h3>Shared concerns</h3><ul class="agree">${dossier.sharedConcerns.map((item) => `<li><p>${escapeHtml(item.text)}</p><span class="who-names">${escapeHtml(item.providerIds.map(providerName).join(', '))} · ${escapeHtml(item.evidenceStatus)}</span></li>`).join('')}</ul>`
|
|
441
444
|
: '<h3>Shared concerns</h3><p class="muted">None recorded.</p>';
|
|
442
445
|
const unique = dossier.uniqueSupportedInsights.length
|
|
443
|
-
? `<h3>Unique supported insights</h3><ul class="agree">${dossier.uniqueSupportedInsights.map((item) => `<li><p
|
|
446
|
+
? `<h3>Unique supported insights</h3><ul class="agree">${dossier.uniqueSupportedInsights.map((item) => `<li><p>${escapeHtml(item.text)}</p><span class="who-names">${escapeHtml(providerName(item.providerId))} · ${escapeHtml(item.verificationStatus)}</span></li>`).join('')}</ul>`
|
|
444
447
|
: '<h3>Unique supported insights</h3><p class="muted">None recorded.</p>';
|
|
445
|
-
const coverage = dossierTable(['Dimension', 'Status', '
|
|
446
|
-
const sensitivity = dossierTable(['
|
|
447
|
-
const experiments = `${dossierWarning(report, ['plan_fallback', 'plan_skipped'])}${dossier.experiments.status === 'DEGRADED' ? `<div class="warns"><span class="warn">⚑ DEGRADED: ${escapeHtml(dossier.experiments.note)}</span></div>` : ''}${
|
|
448
|
+
const coverage = dossierTable(['Dimension', 'Status', 'Related claims'], dossier.coverage.map((item) => [item.label, item.status, readerClaimRefs(report, item.claimIds)]));
|
|
449
|
+
const sensitivity = dossierTable(['Fact', 'Sensitivity', 'If false', 'What would change it', 'Linked claims'], dossier.sensitivity.map((item) => [item.fact, item.sensitivity, item.impactIfFalse, item.whatWouldChangeIt, readerClaimRefs(report, item.linkedClaimIds)]));
|
|
450
|
+
const experiments = `${dossierWarning(report, ['plan_fallback', 'plan_skipped'])}${dossier.experiments.status === 'DEGRADED' ? `<div class="warns"><span class="warn">⚑ DEGRADED: ${escapeHtml(dossier.experiments.note)}</span></div>` : ''}${dossier.experiments.actions.length ? `<div class="experiment-list">${dossier.experiments.actions.map((action) => `<article class="experiment-card"><div class="experiment-head"><span>${String(action.order).padStart(2, '0')}</span><h4>${escapeHtml(action.action)}</h4><strong>${escapeHtml(action.effort)}</strong></div><p>${escapeHtml(action.why)}</p><dl><div><dt>Validates</dt><dd>${escapeHtml(readerClaimLabel(report, action.validates))}</dd></div><div><dt>Stop if</dt><dd>${escapeHtml(action.killSignal)}</dd></div></dl></article>`).join('')}</div><p class="lede">${escapeHtml(dossier.experiments.note)}</p>` : '<p class="muted">No executable experiment was produced.</p>'}`;
|
|
451
|
+
const missingDeliverables = (dossier.missingRequestedOutputs ?? []).length
|
|
452
|
+
? `<div class="warns"><span class="warn">⚑ DEGRADED: requested output missing: ${escapeHtml(dossier.missingRequestedOutputs.join(', '))}</span></div>`
|
|
453
|
+
: '';
|
|
454
|
+
const backlog = dossier.featureBacklog;
|
|
455
|
+
const featurePriorities = backlog ? `<h3>Feature priorities</h3><div class="feature-groups">${[
|
|
456
|
+
['MUST', 'Build for the first usable release', backlog.must],
|
|
457
|
+
['SHOULD', 'Add after the core path is stable', backlog.should],
|
|
458
|
+
['LATER', 'Keep out of the current build', backlog.later],
|
|
459
|
+
].filter(([, , items]) => items.length).map(([priority, description, items]) => `<section class="feature-group priority-${priority.toLowerCase()}"><header><div><span>${priority}</span><h4>${escapeHtml(description)}</h4></div><strong>${items.length}</strong></header><ul>${items.map((item) => `<li><div class="feature-title"><strong>${escapeHtml(item.feature)}</strong><span>${escapeHtml(item.effort)}</span></div><p>${escapeHtml(item.user_value)}</p><small>${escapeHtml(item.rationale)}</small></li>`).join('')}</ul></section>`).join('')}</div>${backlog.wont.length ? `<details class="inline-fold"><summary>Not in this scope (${backlog.wont.length})</summary><ul>${backlog.wont.map((item) => `<li><strong>${escapeHtml(item.feature)}</strong> — ${escapeHtml(item.reason)}</li>`).join('')}</ul></details>` : ''}` : '';
|
|
460
|
+
const implementationPlan = dossier.implementationPlan ? `<h3>Implementation plan</h3><ol class="milestone-list">${dossier.implementationPlan.milestones.map((milestone) => `<li><div class="milestone-marker"><span>${String(milestone.order).padStart(2, '0')}</span><small>${escapeHtml(milestone.timebox)}</small></div><article><h4>${escapeHtml(milestone.outcome)}</h4><ul>${milestone.tasks.map((task) => `<li>${escapeHtml(task)}</li>`).join('')}</ul><div class="acceptance"><span>Done when</span><p>${escapeHtml(milestone.acceptance_test)}</p></div></article></li>`).join('')}</ol>` : '';
|
|
461
|
+
const actionPlan = `${missingDeliverables}${featurePriorities}${implementationPlan}<h3>Validation plan</h3>${experiments}`;
|
|
448
462
|
const counterCase = dossier.counterCase.available
|
|
449
|
-
? `<article class="card risk-card"><p>${escapeHtml(dossier.counterCase.reasoning)}</p><div class="field"><span class="fk">Evidence
|
|
463
|
+
? `<article class="card risk-card"><p>${escapeHtml(dossier.counterCase.reasoning)}</p><div class="field"><span class="fk">Evidence behind this counter-case</span><p>${escapeHtml(readerClaimRefs(report, dossier.counterCase.claimIds))}</p></div></article>`
|
|
450
464
|
: `<div class="warns"><span class="warn">⚑ DEGRADED: ${escapeHtml(dossier.counterCase.reasoning)}</span></div>`;
|
|
451
|
-
const contributions = `${dossierWarning(report, ['verification_skipped', 'single_model', 'low_diversity'])}<p class="lede">Only unique claims that survived independent verification receive credit.</p>${dossierTable(['Provider', 'Verified unique
|
|
465
|
+
const contributions = `${dossierWarning(report, ['verification_skipped', 'single_model', 'low_diversity'])}<p class="lede">Only unique claims that survived independent verification receive credit.</p>${dossierTable(['Provider', 'Verified unique contributions', 'Count'], dossier.contributions.map((item) => [item.name, readerClaimRefs(report, item.verifiedUniqueClaimIds), String(item.verifiedUniqueClaimIds.length)]))}`;
|
|
452
466
|
const receipt = `<div class="receipt">
|
|
453
467
|
<span>mode ${escapeHtml(report.mode)}</span><span>${report.receipt.calls}/${report.receipt.budget} provider calls</span>
|
|
454
468
|
<span>discovery ${report.receipt.categories.discovery}</span><span>verification ${report.receipt.categories.verification}</span>
|
|
@@ -462,7 +476,7 @@ function renderDossierIdeaBody(report) {
|
|
|
462
476
|
: '<p class="muted">No verdict-flipping open question was recorded.</p>';
|
|
463
477
|
const runDetails = `<article class="card">
|
|
464
478
|
<div class="field"><span class="fk">Report</span><p><code>${escapeHtml(report.reportId)}</code> · ${escapeHtml(report.generatedAt)}</p></div>
|
|
465
|
-
<
|
|
479
|
+
<details class="inline-fold compact"><summary>View original request</summary><p>${escapeHtml(report.task.original)}</p></details>
|
|
466
480
|
<div class="field"><span class="fk">Normalized question</span><p>${escapeHtml(report.task.normalized)}</p></div>
|
|
467
481
|
<div class="field"><span class="fk">Constraints</span><p>${escapeHtml(report.task.constraints.join('; ') || 'none recorded')}</p></div>
|
|
468
482
|
<div class="field"><span class="fk">Success criteria</span><p>${escapeHtml(report.task.successCriteria.join('; ') || 'none recorded')}</p></div>
|
|
@@ -475,18 +489,18 @@ function renderDossierIdeaBody(report) {
|
|
|
475
489
|
<h4 class="fold-h">Graph edges</h4><ul>${dossier.technical.edges.map((edge) => `<li><code>${escapeHtml(edge.from)} —${escapeHtml(edge.type)}→ ${escapeHtml(edge.to)}</code></li>`).join('') || '<li>none</li>'}</ul>
|
|
476
490
|
<h4 class="fold-h">Position-change events</h4><ul>${dossier.technical.events.map((event) => `<li><code>${escapeHtml(event.eventId)}</code>: ${escapeHtml(providerName(event.responder))} ${escapeHtml(event.response)} <code>${escapeHtml(event.claimId)}</code> — ${escapeHtml(event.reasoning)}</li>`).join('') || '<li>none</li>'}</ul>
|
|
477
491
|
</div></details>`;
|
|
478
|
-
|
|
492
|
+
const readerBody = [
|
|
479
493
|
section('01', 'Decision', recommendation, 60),
|
|
480
|
-
section('02', 'Action plan',
|
|
494
|
+
section('02', 'Action plan', actionPlan, 100, 'Requested product priorities, implementation milestones, and the smallest decisive validation test.'),
|
|
481
495
|
section('03', 'Why this decision', claimChain, 140, 'The load-bearing claim chain behind the recommendation.'),
|
|
482
496
|
section('04', 'What could change the decision', `<h3>Decision-sensitive facts</h3>${sensitivity}<h3>Strongest counter-case</h3>${counterCase}`, 180),
|
|
483
497
|
section('05', 'Evidence and verification', evidence, 220),
|
|
484
|
-
section('06', 'Risks, gaps, and open questions', `<h3>Risks</h3>${risks}<
|
|
498
|
+
section('06', 'Risks, gaps, and open questions', `<h3>Risks</h3>${risks}<details class="inline-fold"><summary>View coverage ledger (${dossier.coverage.length})</summary>${coverage}</details><h3>Open questions</h3>${questions}`, 260),
|
|
485
499
|
section('07', 'Disagreement and dissent', disagreement, 300),
|
|
486
500
|
section('08', 'What the council added', `${shared}${unique}<h3>Verified unique contributions</h3>${contributions}`, 340),
|
|
487
501
|
section('09', 'Run details', runDetails, 380),
|
|
488
|
-
section('10', 'Technical audit', technical, 420),
|
|
489
502
|
].join('');
|
|
503
|
+
return `${stripReaderClaimIds(readerBody)}${section('10', 'Technical audit', technical, 420)}`;
|
|
490
504
|
}
|
|
491
505
|
function renderLegacyIdeaBody(view) {
|
|
492
506
|
const risks = view.risks ?? [];
|
|
@@ -912,7 +926,7 @@ export function renderCouncilHtml(view) {
|
|
|
912
926
|
*{box-sizing:border-box;}
|
|
913
927
|
html{-webkit-text-size-adjust:100%;}
|
|
914
928
|
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);font-size:16px;line-height:1.6;}
|
|
915
|
-
main{max-width:
|
|
929
|
+
main{max-width:940px;margin:0 auto;padding:34px 24px 90px;}
|
|
916
930
|
a{color:var(--accent);}
|
|
917
931
|
h1{font-family:var(--serif);font-weight:600;letter-spacing:-.025em;}
|
|
918
932
|
h2,h3,h4{font-family:var(--sans);font-weight:650;letter-spacing:-.01em;}
|
|
@@ -933,12 +947,15 @@ p{margin:0 0 .6em;}
|
|
|
933
947
|
.verdict::before{content:"";position:absolute;left:0;top:0;bottom:0;width:6px;}
|
|
934
948
|
.tone-good::before{background:var(--good);} .tone-caution::before{background:var(--caution);} .tone-risk::before{background:var(--risk);}
|
|
935
949
|
.pill{display:inline-block;font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.02em;
|
|
936
|
-
padding:5px 13px;border-radius:100px;
|
|
950
|
+
padding:5px 13px;border-radius:100px;}
|
|
937
951
|
.tone-good .pill{background:var(--good-bg);color:var(--good);} .tone-caution .pill{background:var(--caution-bg);color:var(--caution);}
|
|
938
952
|
.tone-risk .pill{background:var(--risk-bg);color:var(--risk);}
|
|
939
|
-
.
|
|
953
|
+
.decision-status{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px;}
|
|
954
|
+
.decision-status>span:last-child{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;color:var(--soft);text-transform:uppercase;}
|
|
955
|
+
.verdict-text{font-family:var(--sans);font-weight:700;font-size:clamp(18px,2.3vw,21px);line-height:1.42;color:var(--ink);margin:0;max-width:68ch;letter-spacing:-.012em;}
|
|
956
|
+
.verdict-detail{font-size:14.5px;line-height:1.62;color:var(--soft);margin:9px 0 0;max-width:76ch;}
|
|
940
957
|
.section-eyebrow{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--accent);margin-bottom:9px;}
|
|
941
|
-
.decision-numbers{margin:
|
|
958
|
+
.decision-numbers{margin:0 0 20px;}.snapshot-table{width:100%;border-collapse:collapse;font-size:13.5px;line-height:1.4;}
|
|
942
959
|
.snapshot-table th{padding:8px 10px;text-align:left;font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);border-bottom:1px solid var(--line);}
|
|
943
960
|
.snapshot-table td{padding:11px 10px;vertical-align:top;border-bottom:1px solid var(--line);}.snapshot-table tbody tr:last-child td{border-bottom:0;}
|
|
944
961
|
.snapshot-table .number-value{font-family:var(--serif);font-size:17px;font-weight:700;color:var(--ink);white-space:nowrap;}
|
|
@@ -974,8 +991,9 @@ p{margin:0 0 .6em;}
|
|
|
974
991
|
.decision-details{margin-top:14px;border-top:1px solid var(--line);padding-top:12px;}
|
|
975
992
|
.decision-details summary{cursor:pointer;font-family:var(--mono);font-size:11px;color:var(--soft);text-transform:uppercase;letter-spacing:.08em;}
|
|
976
993
|
.decision-details>div{padding-top:10px;font-size:13.5px;}.decision-details ul{margin:8px 0;padding-left:18px;}
|
|
977
|
-
.evidence
|
|
978
|
-
.evidence
|
|
994
|
+
.decision-evidence{margin-top:12px;border-top:1px solid var(--line);padding-top:12px;}
|
|
995
|
+
.decision-evidence summary{cursor:pointer;font-family:var(--mono);font-size:10.5px;color:var(--faint);letter-spacing:.07em;text-transform:uppercase;}
|
|
996
|
+
.decision-evidence ul{margin:10px 0 0;padding-left:20px;}.decision-evidence li{font-size:13px;color:var(--soft);margin-bottom:5px;}
|
|
979
997
|
|
|
980
998
|
/* top action bar + copy */
|
|
981
999
|
.bar{position:sticky;top:0;z-index:5;display:flex;align-items:center;justify-content:space-between;gap:12px;
|
|
@@ -985,6 +1003,7 @@ p{margin:0 0 .6em;}
|
|
|
985
1003
|
border:0;border-radius:8px;padding:8px 15px;transition:background .15s ease,transform .1s ease;}
|
|
986
1004
|
.copy-btn:hover{filter:brightness(1.05);} .copy-btn:active{transform:translateY(1px);}
|
|
987
1005
|
.copy-btn.ok{background:var(--good);}
|
|
1006
|
+
button:focus-visible,summary:focus-visible,a:focus-visible{outline:3px solid rgba(21,95,85,.28);outline-offset:3px;}
|
|
988
1007
|
|
|
989
1008
|
/* chairman's reasoning */
|
|
990
1009
|
.reasons{margin:0;padding:0;list-style:none;}
|
|
@@ -1061,12 +1080,30 @@ p{margin:0 0 .6em;}
|
|
|
1061
1080
|
.score span{font-size:13.5px;color:var(--ink);}
|
|
1062
1081
|
.score strong{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em;}
|
|
1063
1082
|
.score.contested{border-left:4px solid var(--caution);} .score.examined{border-left:4px solid var(--good);} .score.unexamined{border-left:4px solid var(--risk);}
|
|
1064
|
-
.table-wrap{overflow-x:auto;}
|
|
1083
|
+
.table-wrap{overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:thin;}
|
|
1065
1084
|
.data-table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden;font-size:13.5px;}
|
|
1066
1085
|
.data-table th,.data-table td{text-align:left;vertical-align:top;border-bottom:1px solid var(--line);padding:9px 10px;}
|
|
1067
1086
|
.data-table th{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--soft);background:var(--paper);}
|
|
1068
1087
|
.data-table tr:last-child td{border-bottom:0;}
|
|
1069
1088
|
.data-table code{font-family:var(--mono);font-size:12px;color:var(--accent);}
|
|
1089
|
+
.feature-groups{display:grid;gap:14px;margin-bottom:26px;}
|
|
1090
|
+
.feature-group{background:var(--panel);border:1px solid var(--line);border-radius:14px;overflow:hidden;}
|
|
1091
|
+
.feature-group>header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 18px;border-bottom:1px solid var(--line);background:var(--paper);}
|
|
1092
|
+
.feature-group>header div>span{display:block;font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.13em;color:var(--accent);}
|
|
1093
|
+
.feature-group>header h4{font-size:14px;margin:2px 0 0;}.feature-group>header>strong{font-family:var(--serif);font-size:24px;color:var(--soft);}
|
|
1094
|
+
.feature-group ul{list-style:none;margin:0;padding:0 18px;}.feature-group li{padding:14px 0;border-bottom:1px solid var(--line);}.feature-group li:last-child{border-bottom:0;}
|
|
1095
|
+
.feature-title{display:flex;align-items:baseline;justify-content:space-between;gap:12px;}.feature-title>strong{font-size:15px;}.feature-title>span{font-family:var(--mono);font-size:10px;border:1px solid var(--line);border-radius:99px;padding:1px 7px;color:var(--soft);}
|
|
1096
|
+
.feature-group p{font-size:13.5px;margin:3px 0;color:var(--ink);}.feature-group small{display:block;font-size:12.5px;line-height:1.45;color:var(--soft);}
|
|
1097
|
+
.priority-must{border-left:4px solid var(--accent);}.priority-should{border-left:4px solid var(--caution);}.priority-later{border-left:4px solid var(--slate);}
|
|
1098
|
+
.milestone-list{list-style:none;margin:0 0 28px;padding:0;}.milestone-list>li{display:grid;grid-template-columns:90px 1fr;gap:18px;position:relative;padding-bottom:18px;}
|
|
1099
|
+
.milestone-list>li:not(:last-child)::before{content:"";position:absolute;left:23px;top:37px;bottom:0;border-left:1px solid var(--line);}
|
|
1100
|
+
.milestone-marker{position:relative;z-index:1;display:flex;flex-direction:column;align-items:flex-start;}.milestone-marker>span{display:grid;place-items:center;width:47px;height:35px;border-radius:9px;background:var(--ink);color:var(--paper);font-family:var(--mono);font-size:12px;}.milestone-marker small{font-size:11px;color:var(--soft);margin-top:5px;}
|
|
1101
|
+
.milestone-list article{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px;}.milestone-list h4{font-size:16px;margin:0 0 8px;}.milestone-list article ul{margin:0;padding-left:18px;}.milestone-list article li{font-size:13.5px;margin-bottom:4px;}
|
|
1102
|
+
.acceptance{margin-top:12px;padding-top:10px;border-top:1px solid var(--line);}.acceptance span{font-family:var(--mono);font-size:9.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--good);}.acceptance p{font-size:13px;margin:2px 0 0;}
|
|
1103
|
+
.experiment-list{display:grid;gap:12px;margin-bottom:14px;}.experiment-card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:16px 18px;}
|
|
1104
|
+
.experiment-head{display:grid;grid-template-columns:auto 1fr auto;align-items:start;gap:10px;}.experiment-head>span{font-family:var(--mono);font-size:11px;color:var(--accent);}.experiment-head h4{font-size:15px;margin:0;}.experiment-head>strong{font-family:var(--mono);font-size:10px;border:1px solid var(--line);border-radius:99px;padding:2px 7px;color:var(--soft);}
|
|
1105
|
+
.experiment-card>p{font-size:13.5px;color:var(--soft);margin:8px 0 12px 27px;}.experiment-card dl{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:0 0 0 27px;}.experiment-card dl>div{background:var(--paper);border-radius:8px;padding:9px 11px;}.experiment-card dt{font-family:var(--mono);font-size:9.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint);}.experiment-card dd{font-size:12.5px;line-height:1.45;margin:2px 0 0;}
|
|
1106
|
+
.inline-fold{margin:14px 0;border:1px solid var(--line);border-radius:10px;background:var(--panel);}.inline-fold>summary{cursor:pointer;padding:11px 14px;font-family:var(--mono);font-size:11px;letter-spacing:.05em;color:var(--soft);}.inline-fold>ul,.inline-fold>.table-wrap,.inline-fold>p{margin:0 14px 14px;}.inline-fold.compact{background:var(--paper);}.inline-fold.compact>p{font-size:13.5px;color:var(--soft);}
|
|
1070
1107
|
.debate-card p{margin-bottom:8px;}
|
|
1071
1108
|
.redteam{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
|
|
1072
1109
|
.redteam>div{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px;}
|
|
@@ -1100,7 +1137,14 @@ footer{margin-top:56px;padding-top:18px;border-top:1px solid var(--line);font-fa
|
|
|
1100
1137
|
main{padding:34px 16px 60px;}
|
|
1101
1138
|
.verdict{padding:24px 20px 20px;}
|
|
1102
1139
|
.glance,.bottomline,.checks,.redteam,.decision-safety{grid-template-columns:1fr;}
|
|
1103
|
-
.
|
|
1140
|
+
.decision-status{align-items:flex-start;flex-direction:column;gap:8px;}.decision-status>span:last-child{text-align:left;}.pill{white-space:nowrap;}
|
|
1141
|
+
.snapshot-table{min-width:0;}.snapshot-table thead{display:none;}.snapshot-table tbody,.snapshot-table tr,.snapshot-table td{display:block;width:100%;}
|
|
1142
|
+
.snapshot-table tr{padding:12px 0;border-bottom:1px solid var(--line);}.snapshot-table td{padding:3px 8px;border:0;white-space:normal;}
|
|
1143
|
+
.snapshot-table td::before{display:block;font-family:var(--mono);font-size:9px;line-height:1.4;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);}
|
|
1144
|
+
.decisive-table td:nth-child(1)::before{content:"Metric";}.decisive-table td:nth-child(2)::before{content:"Value";}.decisive-table td:nth-child(3)::before{content:"What it means";}
|
|
1145
|
+
.options-table td:nth-child(1)::before{content:"Path";}.options-table td:nth-child(2)::before{content:"Commitment";}.options-table td:nth-child(3)::before{content:"Basis";}.options-table td:nth-child(4)::before{content:"Trade-off";}
|
|
1146
|
+
.payback-result{grid-template-columns:1fr;}.payback-result p{grid-column:1;}
|
|
1147
|
+
.milestone-list>li{grid-template-columns:64px 1fr;gap:10px;}.experiment-card dl{grid-template-columns:1fr;}.experiment-card>p,.experiment-card dl{margin-left:0;}
|
|
1104
1148
|
}
|
|
1105
1149
|
@media (prefers-reduced-motion:reduce){.reveal{opacity:1;transform:none;animation:none;}}
|
|
1106
1150
|
@media print{.reveal{opacity:1;transform:none;animation:none;}.fold[open]{break-inside:avoid;}}
|
|
@@ -1109,7 +1153,7 @@ footer{margin-top:56px;padding-top:18px;border-top:1px solid var(--line);font-fa
|
|
|
1109
1153
|
<body>
|
|
1110
1154
|
<div class="bar">
|
|
1111
1155
|
<span class="bar-label">${escapeHtml(kicker)}</span>
|
|
1112
|
-
<button
|
|
1156
|
+
<button id="copy-report" class="copy-btn">Copy report (Markdown)</button>
|
|
1113
1157
|
</div>
|
|
1114
1158
|
<main>
|
|
1115
1159
|
<header class="mast">
|
|
@@ -1122,12 +1166,28 @@ footer{margin-top:56px;padding-top:18px;border-top:1px solid var(--line);font-fa
|
|
|
1122
1166
|
</main>
|
|
1123
1167
|
<script>
|
|
1124
1168
|
const REPORT_MD = ${md};
|
|
1125
|
-
function copyReport(btn){
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1169
|
+
async function copyReport(btn){
|
|
1170
|
+
var original=btn.textContent;
|
|
1171
|
+
try {
|
|
1172
|
+
var copied=false;
|
|
1173
|
+
if(navigator.clipboard && navigator.clipboard.writeText){
|
|
1174
|
+
try{ await navigator.clipboard.writeText(REPORT_MD); copied=true; }catch(error){}
|
|
1175
|
+
}
|
|
1176
|
+
if(!copied){
|
|
1177
|
+
var field=document.createElement('textarea');
|
|
1178
|
+
field.value=REPORT_MD; field.setAttribute('readonly','');
|
|
1179
|
+
field.style.position='fixed'; field.style.opacity='0';
|
|
1180
|
+
document.body.appendChild(field); field.select();
|
|
1181
|
+
copied=document.execCommand('copy'); field.remove();
|
|
1182
|
+
if(!copied) throw new Error('copy unavailable');
|
|
1183
|
+
}
|
|
1184
|
+
btn.textContent='✓ Copied'; btn.classList.add('ok');
|
|
1185
|
+
}catch(error){
|
|
1186
|
+
btn.textContent='Copy failed — select the text manually';
|
|
1187
|
+
}
|
|
1188
|
+
setTimeout(function(){ btn.textContent=original; btn.classList.remove('ok'); }, 1800);
|
|
1130
1189
|
}
|
|
1190
|
+
document.getElementById('copy-report').addEventListener('click',function(){ copyReport(this); });
|
|
1131
1191
|
</script>
|
|
1132
1192
|
</body>
|
|
1133
1193
|
</html>`;
|