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 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 explicit modes.** Use `quick` for one structured analyst, `council` for bounded multi-model
54
- deliberation, or `research` for current-fact work with Codex search. Aiki never hides this choice behind a
55
- learned router.
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` (default) for the full decision council,
267
- or `--mode research` for source-grounded current-fact work. Aiki never chooses a mode with a learned router:
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
- const parsed = IdeaModeSchema.safeParse(opts.mode ?? 'council');
95
- if (!parsed.success) {
96
- process.stderr.write(`unknown idea mode "${opts.mode}". Available: quick, council, research\n`);
97
- return 1;
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') {
@@ -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)} <code>${escapeHtml(dossierRefs(condition.claimIds))}</code></li>`).join('')}</ul></div></details>`
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)} <code>${escapeHtml(dossierRefs(item.claimIds))}</code></td></tr>`).join('')}</tbody></table></div>
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)} <code>${escapeHtml(dossierRefs(snapshot.payback.claimIds))}</code></p></div>` : ''}
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)}${option.claimIds.length ? ` <code>${escapeHtml(dossierRefs(option.claimIds))}</code>` : ''}</td></tr>`).join('')}</tbody></table></div>
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)} <code>${escapeHtml(dossierRefs(snapshot.tripwire.claimIds))}</code></p></div>` : '';
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(dossier.recommendation.summary)}</p>
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
- <div class="evidence-anchors"><span>Evidence anchors</span><code>${escapeHtml(dossierRefs(dossier.recommendation.claimIds))}</code></div>
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 ID', 'Claim', 'Ruling', 'Evidence status', 'Depends on'], dossier.claimChain.map((claim) => [claim.claimId, claim.text, claim.ruling, claim.evidenceStatus, dossierRefs(claim.dependsOn)]));
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, dossierRefs(item.claimIds)]))}`;
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><code>${escapeHtml(item.id)}</code> ${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('')
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><code>${escapeHtml(item.claimId)}</code> ${escapeHtml(item.text)}</p><span class="who-names">${escapeHtml(item.providerIds.map(providerName).join(', '))} · ${escapeHtml(item.evidenceStatus)}</span></li>`).join('')}</ul>`
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><code>${escapeHtml(item.claimId)}</code> ${escapeHtml(item.text)}</p><span class="who-names">${escapeHtml(providerName(item.providerId))} · ${escapeHtml(item.verificationStatus)}</span></li>`).join('')}</ul>`
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', 'Claim IDs'], dossier.coverage.map((item) => [item.label, item.status, dossierRefs(item.claimIds)]));
446
- const sensitivity = dossierTable(['Claim ID', 'Fact', 'Sensitivity', 'If false', 'What would change it', 'Linked claims'], dossier.sensitivity.map((item) => [item.claimId, item.fact, item.sensitivity, item.impactIfFalse, item.whatWouldChangeIt, dossierRefs(item.linkedClaimIds)]));
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>` : ''}${dossierTable(['#', 'Experiment', 'Why', 'Validates', 'Effort', 'Kill signal'], dossier.experiments.actions.map((action) => [String(action.order), action.action, action.why, action.validates, action.effort, action.killSignal]))}${dossier.experiments.actions.length ? `<p class="lede">${escapeHtml(dossier.experiments.note)}</p>` : ''}`;
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 anchors</span><p><code>${escapeHtml(dossierRefs(dossier.counterCase.claimIds))}</code></p></div></article>`
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 claim IDs', 'Count'], dossier.contributions.map((item) => [item.name, dossierRefs(item.verifiedUniqueClaimIds), String(item.verifiedUniqueClaimIds.length)]))}`;
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
- <div class="field"><span class="fk">Original task</span><p>${escapeHtml(report.task.original)}</p></div>
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
- return [
492
+ const readerBody = [
479
493
  section('01', 'Decision', recommendation, 60),
480
- section('02', 'Action plan', experiments, 100, 'The smallest useful test, its effort, and the signal that should stop the idea.'),
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}<h3>Coverage</h3>${coverage}<h3>Open questions</h3>${questions}`, 260),
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:880px;margin:0 auto;padding:34px 24px 90px;}
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;margin-bottom:14px;}
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
- .verdict-text{font-family:var(--serif);font-weight:600;font-size:clamp(22px,3vw,29px);line-height:1.32;color:var(--ink);margin:0;max-width:28ch;}
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:4px 0 22px;}.snapshot-table{width:100%;border-collapse:collapse;font-size:13.5px;line-height:1.4;}
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-anchors{display:flex;gap:10px;align-items:center;margin-top:12px;font-family:var(--mono);font-size:10.5px;color:var(--faint);}
978
- .evidence-anchors span{text-transform:uppercase;letter-spacing:.1em;}
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
- .snapshot-table{min-width:560px;}.payback-result{grid-template-columns:1fr;}.payback-result p{grid-column:1;}
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 class="copy-btn" onclick="copyReport(this)">Copy report (Markdown)</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
- navigator.clipboard.writeText(REPORT_MD).then(function(){
1127
- var o=btn.textContent; btn.textContent='✓ Copied'; btn.classList.add('ok');
1128
- setTimeout(function(){ btn.textContent=o; btn.classList.remove('ok'); }, 1600);
1129
- }).catch(function(){ btn.textContent='Copy failed — select the text manually'; });
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>`;