ndrstnd 0.1.0
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/LICENSE +201 -0
- package/README.md +56 -0
- package/dist/server/agent.d.ts +51 -0
- package/dist/server/agent.js +77 -0
- package/dist/server/agent.js.map +1 -0
- package/dist/server/analysis-core.d.ts +93 -0
- package/dist/server/analysis-core.js +442 -0
- package/dist/server/analysis-core.js.map +1 -0
- package/dist/server/analyze.d.ts +55 -0
- package/dist/server/analyze.js +72 -0
- package/dist/server/analyze.js.map +1 -0
- package/dist/server/artifact.d.ts +8 -0
- package/dist/server/artifact.js +43 -0
- package/dist/server/artifact.js.map +1 -0
- package/dist/server/claude.d.ts +29 -0
- package/dist/server/claude.js +243 -0
- package/dist/server/claude.js.map +1 -0
- package/dist/server/cli-support.d.ts +7 -0
- package/dist/server/cli-support.js +13 -0
- package/dist/server/cli-support.js.map +1 -0
- package/dist/server/cli.d.ts +2 -0
- package/dist/server/cli.js +276 -0
- package/dist/server/cli.js.map +1 -0
- package/dist/server/codex.d.ts +34 -0
- package/dist/server/codex.js +256 -0
- package/dist/server/codex.js.map +1 -0
- package/dist/server/conversation.d.ts +9 -0
- package/dist/server/conversation.js +57 -0
- package/dist/server/conversation.js.map +1 -0
- package/dist/server/evidence-ordering.d.ts +13 -0
- package/dist/server/evidence-ordering.js +143 -0
- package/dist/server/evidence-ordering.js.map +1 -0
- package/dist/server/git-model.d.ts +15 -0
- package/dist/server/git-model.js +156 -0
- package/dist/server/git-model.js.map +1 -0
- package/dist/server/git.d.ts +29 -0
- package/dist/server/git.js +124 -0
- package/dist/server/git.js.map +1 -0
- package/dist/server/http.d.ts +14 -0
- package/dist/server/http.js +139 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/preview-fixture.d.ts +1 -0
- package/dist/server/preview-fixture.js +23 -0
- package/dist/server/preview-fixture.js.map +1 -0
- package/dist/server/review-presentation.d.ts +3 -0
- package/dist/server/review-presentation.js +14 -0
- package/dist/server/review-presentation.js.map +1 -0
- package/dist/server/skill.d.ts +14 -0
- package/dist/server/skill.js +94 -0
- package/dist/server/skill.js.map +1 -0
- package/dist/server/store.d.ts +36 -0
- package/dist/server/store.js +134 -0
- package/dist/server/store.js.map +1 -0
- package/dist/server/version.d.ts +2 -0
- package/dist/server/version.js +13 -0
- package/dist/server/version.js.map +1 -0
- package/dist/shared/analysis-schema.d.ts +325 -0
- package/dist/shared/analysis-schema.js +50 -0
- package/dist/shared/analysis-schema.js.map +1 -0
- package/dist/shared/domain.d.ts +33 -0
- package/dist/shared/domain.js +2 -0
- package/dist/shared/domain.js.map +1 -0
- package/dist/web/evidence-model.d.ts +36 -0
- package/dist/web/evidence-model.js +134 -0
- package/dist/web/evidence-model.js.map +1 -0
- package/dist/web/frozen-review-data.d.ts +3 -0
- package/dist/web/frozen-review-data.js +202 -0
- package/dist/web/frozen-review-data.js.map +1 -0
- package/dist/web/language.d.ts +4 -0
- package/dist/web/language.js +14 -0
- package/dist/web/language.js.map +1 -0
- package/dist/web/page.d.ts +10 -0
- package/dist/web/page.js +1038 -0
- package/dist/web/page.js.map +1 -0
- package/dist/web/review-data.d.ts +20 -0
- package/dist/web/review-data.js +2 -0
- package/dist/web/review-data.js.map +1 -0
- package/dist/web/test-plan-model.d.ts +27 -0
- package/dist/web/test-plan-model.js +89 -0
- package/dist/web/test-plan-model.js.map +1 -0
- package/package.json +47 -0
- package/src/skill-assets/ndrstnd/SKILL.md +22 -0
- package/src/skill-assets/ndrstnd/agents/openai.yaml +4 -0
package/dist/web/page.js
ADDED
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
import { attentionCounts, categoryCounts, chapterMetrics, focusLinesFromRanges, focusedEvidenceLines, isSupportingFile, linePrefix, toUnifiedDiff } from "./evidence-model.js";
|
|
2
|
+
import { resolveLanguage, syntaxHighlighter } from "./language.js";
|
|
3
|
+
import { buildTestThemes, deriveTestSummary, focusedTestLines, isTestPath, testTypeLabel } from "./test-plan-model.js";
|
|
4
|
+
import { parse as parseDiff } from "diff2html";
|
|
5
|
+
function panelIcon(kind) {
|
|
6
|
+
const paths = {
|
|
7
|
+
"collapse-left": '<path d="M9.8 3.8L5.6 8l4.2 4.2"/>',
|
|
8
|
+
"collapse-right": '<path d="M6.2 3.8L10.4 8l-4.2 4.2"/>',
|
|
9
|
+
details: '<rect x="2.2" y="3" width="11.6" height="10" rx="2"/><path d="M9.8 3v10"/>',
|
|
10
|
+
};
|
|
11
|
+
return `<svg viewBox="0 0 16 16" aria-hidden="true">${paths[kind]}</svg>`;
|
|
12
|
+
}
|
|
13
|
+
function actionIcon(kind) {
|
|
14
|
+
const paths = {
|
|
15
|
+
export: '<path d="M8 9.8V2.6M5.2 5.2L8 2.4l2.8 2.8"/><path d="M2.8 9.6v2.6a1.2 1.2 0 0 0 1.2 1.2h8a1.2 1.2 0 0 0 1.2-1.2V9.6"/>',
|
|
16
|
+
copy: '<rect x="5.4" y="5.4" width="7.8" height="8.6" rx="1.6"/><path d="M2.8 10.6V4.2a1.4 1.4 0 0 1 1.4-1.4h5.4"/>',
|
|
17
|
+
};
|
|
18
|
+
return `<svg viewBox="0 0 16 16" aria-hidden="true">${paths[kind]}</svg>`;
|
|
19
|
+
}
|
|
20
|
+
export async function renderArtifact(data) {
|
|
21
|
+
const counts = attentionCounts(data.document);
|
|
22
|
+
const filePaths = new Map(data.files.map((file) => [file.id, file.path]));
|
|
23
|
+
const highlighter = await syntaxHighlighter(data.files);
|
|
24
|
+
// The body is rendered before the head so the token-class table collects
|
|
25
|
+
// every syntax color pair the page uses before it is emitted as CSS.
|
|
26
|
+
const body = `<body data-agent="${escapeHtml(data.agentName)}"><div class="app-shell">
|
|
27
|
+
<aside class="sidebar"><div class="brand"><svg class="brand-mark" viewBox="0 0 20 22" aria-hidden="true"><path d="M4.4 3.4 10 7.6l5.6-4.2"/><path d="M4.4 8.9 10 13.1l5.6-4.2"/><path class="mark-deep" d="M4.4 14.4 10 18.6l5.6-4.2"/></svg><span class="brand-name">ndrstnd</span><button class="collapse-sidebar panel-toggle" aria-label="Collapse navigation" aria-expanded="true">${panelIcon("collapse-left")}</button><button class="mobile-inspector-toggle panel-toggle" aria-label="Show review details" aria-expanded="false">${panelIcon("details")}</button></div><nav aria-label="Review views"><button class="nav-item active" data-view="trailer">${navIcon("story")}Story</button><button class="nav-item" data-view="timeline">${navIcon("timeline")}Timeline</button>${data.document.chapters.some((chapter) => chapter.kind === "test") ? `<button class="nav-item" data-view="tests">${navIcon("tests")}Test plan</button>` : ""}<button class="nav-item" data-view="diff">${navIcon("diff")}Full diff</button></nav></aside>
|
|
28
|
+
<main class="main"><header class="page-header"><div class="masthead"><p class="masthead-overline">Change review</p><h1>${escapeHtml(data.targetRef)}</h1><div class="breadcrumbs"><span>against <strong>${escapeHtml(data.baseRef)}</strong></span><span>merge base <code>${escapeHtml(data.mergeBase.slice(0, 8))}</code></span></div></div></header><div class="view-bar"><span class="view-bar-ref">${escapeHtml(data.targetRef)}</span><div class="story-zoom-controls"><button data-zoom-step="-1" aria-label="Decrease detail">−</button><div class="zoom" id="zoom-control" role="group" aria-label="Story detail level"><div class="zoom-callout" id="zoom-callout" aria-live="polite"><output id="zoom-label">Summary</output><span id="zoom-description">Story claims and summaries</span></div><button data-zoom="0" aria-label="Map" title="Map"></button><button data-zoom="1" aria-label="Summary" title="Summary" class="active"></button><button data-zoom="2" aria-label="Explanation" title="Explanation"></button><button data-zoom="3" aria-label="Evidence" title="Evidence"></button><button data-zoom="4" aria-label="Raw" title="Raw"></button></div><button data-zoom-step="1" aria-label="Increase detail">+</button></div></div>
|
|
29
|
+
<section id="trailer" class="view active"><p class="review-summary">${renderProse(data.document.summary)}</p><p class="coverage">${data.files.length} files · ${data.hunks.length} evidence hunks · ${data.document.chapters.length} story chapters · ${data.document.steps.length} build steps</p><div class="story-toolbar"><div id="map" class="map" hidden>${Object.entries(counts).map(([key, value]) => `<div><span class="dot ${key}"></span>${escapeHtml(key)} <strong>${value}</strong></div>`).join("")}</div><button id="collapse-all">Collapse all</button></div><div class="chapter-list">${renderChapters(data.document, data.hunks, filePaths, data.files, highlighter)}</div>${renderOtherFilesChanged(data.files, data.hunks)}${renderOmitted(data.document, data.hunks)}</section>
|
|
30
|
+
<section id="timeline" class="view"><p class="section-title">Build path</p><p class="view-subtitle">A constructive reconstruction of how you would assemble this change so each step builds on what came before.</p>${renderTimeline(data.document, data.hunks, filePaths, data.files, highlighter)}</section>
|
|
31
|
+
<section id="diff" class="view"><p class="section-title">Every patch hunk</p>${data.files.map((file) => renderFullDiff(file, data.hunks.filter((hunk) => hunk.fileId === file.id), highlighter)).join("")}</section>
|
|
32
|
+
<section id="tests" class="view"><p class="section-title">Test plan</p><p class="test-plan-subtitle">See how the changed behavior was exercised, from high-level themes to raw test evidence.</p>${renderTestPlan(data.document, data.hunks, filePaths, data.files, highlighter)}</section>
|
|
33
|
+
<footer class="colophon"><svg viewBox="0 0 20 22" aria-hidden="true"><path d="M4.4 3.4 10 7.6l5.6-4.2"/><path d="M4.4 8.9 10 13.1l5.6-4.2"/><path class="mark-deep" d="M4.4 14.4 10 18.6l5.6-4.2"/></svg><span>ndrstnd · created by Tomás Ruiz-López</span></footer>
|
|
34
|
+
</main>
|
|
35
|
+
<aside class="inspector" aria-label="Review details"><header class="inspector-header"><h2>At a glance</h2><button class="collapse-inspector panel-toggle" aria-label="Collapse review details" aria-expanded="true">${panelIcon("collapse-right")}</button></header><div class="inspector-content"><section><h3>This change</h3><div class="stat-row"><span>Story chapters</span><strong>${data.document.chapters.length}</strong></div><div class="stat-row"><span>Build steps</span><strong>${data.document.steps.length}</strong></div><div class="stat-row"><span>Changed files</span><strong>${data.files.length}</strong></div><div class="stat-row"><span>Evidence hunks</span><strong>${data.hunks.length}</strong></div></section><section><h3>Focus areas</h3>${Object.entries(categoryCounts(data.document)).map(([category, count]) => `<div class="stat-row stat-category"><span>${categoryIcon(category)}${escapeHtml(category)}</span><strong>${count}</strong></div>`).join("")}</section><section><h3>Actions</h3><button class="inspector-action" data-action="export">${actionIcon("export")}Export review…</button><button class="inspector-action" data-action="copy-summary" title="Copy a concise prompt for asking ${escapeHtml(data.agentName)} about this review">${actionIcon("copy")}Copy ${escapeHtml(data.agentName)} prompt</button></section></div></aside>
|
|
36
|
+
</div>${renderEvidenceLibrary(data, filePaths, highlighter)}<div id="selection-menu" class="selection-menu" hidden><button data-question="Explain the selected lines.">Explain selection</button><button data-question="Trace the callers, effects, and dependencies of the selected lines.">Trace effects</button><button data-question="Why is this included in the change?">Why included?</button><button data-action="ask">Ask a question…</button></div><div id="toast" class="toast" role="status" aria-live="polite" hidden></div><script>${artifactClientScript}${portableEnhancements}</script></body></html>`;
|
|
37
|
+
return `<!doctype html>
|
|
38
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ndrstnd · ${escapeHtml(data.targetRef)}</title><style>${styles}${tokenStyleRules()}</style></head>
|
|
39
|
+
${body}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Every hunk referenced by a step or a chapter is rendered exactly once here;
|
|
43
|
+
* timeline states and chapter details clone from these templates at runtime
|
|
44
|
+
* instead of shipping their own copies, which kept multi-step artifacts at
|
|
45
|
+
* tens of megabytes.
|
|
46
|
+
*/
|
|
47
|
+
function renderEvidenceLibrary(data, filePaths, highlighter) {
|
|
48
|
+
const filesById = new Map(data.files.map((file) => [file.id, file]));
|
|
49
|
+
const stepIndexByEvidence = new Map();
|
|
50
|
+
for (const [index, step] of data.document.steps.entries())
|
|
51
|
+
for (const id of step.evidenceIds)
|
|
52
|
+
stepIndexByEvidence.set(id, index);
|
|
53
|
+
for (const chapter of data.document.chapters) {
|
|
54
|
+
for (const id of chapter.evidenceIds) {
|
|
55
|
+
const hunk = data.hunks.find((candidate) => candidate.id === id);
|
|
56
|
+
if (hunk === undefined || isSupportingFile(filesById.get(hunk.fileId)))
|
|
57
|
+
continue;
|
|
58
|
+
if (!stepIndexByEvidence.has(id))
|
|
59
|
+
stepIndexByEvidence.set(id, undefined);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (stepIndexByEvidence.size === 0)
|
|
63
|
+
return "";
|
|
64
|
+
const templates = [...stepIndexByEvidence.entries()].map(([id, stepIndex]) => {
|
|
65
|
+
const hunk = requireHunk(data.hunks, id);
|
|
66
|
+
return `<template data-evidence-template="${escapeHtml(id)}">${renderEvidence(hunk, filePaths.get(hunk.fileId) ?? hunk.fileId, highlighter, stepIndex, data.document.focus?.[id])}</template>`;
|
|
67
|
+
}).join("");
|
|
68
|
+
return `<div id="evidence-library" hidden>${templates}</div>`;
|
|
69
|
+
}
|
|
70
|
+
function renderChapters(document, hunks, filePaths, files, highlighter) {
|
|
71
|
+
const filesById = new Map(files.map((file) => [file.id, file]));
|
|
72
|
+
const stepsByChapter = new Map();
|
|
73
|
+
const stepIndexByEvidence = new Map();
|
|
74
|
+
for (const [index, step] of document.steps.entries()) {
|
|
75
|
+
for (const chapterId of step.advancesChapterIds) {
|
|
76
|
+
const list = stepsByChapter.get(chapterId) ?? [];
|
|
77
|
+
list.push({ id: step.id, index });
|
|
78
|
+
stepsByChapter.set(chapterId, list);
|
|
79
|
+
}
|
|
80
|
+
for (const evidenceId of step.evidenceIds)
|
|
81
|
+
stepIndexByEvidence.set(evidenceId, index);
|
|
82
|
+
}
|
|
83
|
+
return document.chapters.map((chapter, index) => {
|
|
84
|
+
const metrics = chapterMetrics(chapter, hunks);
|
|
85
|
+
const stepChips = (stepsByChapter.get(chapter.id) ?? []).map((step) => `<button type="button" class="step-chip" data-story-step="${escapeHtml(step.id)}">step ${String(step.index + 1).padStart(2, "0")}</button>`).join("");
|
|
86
|
+
const evidenceIds = chapter.evidenceIds
|
|
87
|
+
.map((id) => requireHunk(hunks, id))
|
|
88
|
+
.filter((hunk) => !isSupportingFile(filesById.get(hunk.fileId)))
|
|
89
|
+
.map((hunk) => hunk.id);
|
|
90
|
+
// The toggle is a div-with-button-role rather than a button so the step chips inside it can stay real, keyboard-reachable buttons.
|
|
91
|
+
return `<article class="chapter" data-chapter="${escapeHtml(chapter.id)}"><div class="chapter-toggle" role="button" tabindex="0" aria-expanded="false"><span class="chapter-number attention-${escapeHtml(chapter.attention)}">${String(index + 1).padStart(2, "0")}</span><span class="chapter-copy"><strong>${renderProse(chapter.title)}</strong><small>${renderProse(chapter.synopsis)}</small>${stepChips ? `<span class="chapter-steps">${stepChips}</span>` : ""}<span class="chapter-tags">${chapter.riskCategories.map((risk) => `<span class="chapter-tag">${categoryIcon(risk)}${escapeHtml(risk)}</span>`).join("")}</span>${renderChapterMapMetrics(metrics)}</span><span class="chevron" aria-hidden="true"><svg viewBox="0 0 12 12"><path d="M2.5 4.25L6 7.75l3.5-3.5"/></svg></span></div><div class="chapter-detail" hidden>${renderSemantic(chapter.before, chapter.after)}${evidenceIds.length > 0 ? `<div class="evidence-stack" data-evidence-list="${escapeHtml(evidenceIds.join(" "))}"></div>` : ""}</div></article>`;
|
|
92
|
+
}).join("");
|
|
93
|
+
}
|
|
94
|
+
const ATTENTION_RANK = { low: 0, contained: 1, elevated: 2, high: 3, critical: 4 };
|
|
95
|
+
function renderTimeline(document, hunks, filePaths, files, highlighter) {
|
|
96
|
+
const chaptersById = new Map(document.chapters.map((chapter) => [chapter.id, chapter]));
|
|
97
|
+
if (document.steps.length === 0)
|
|
98
|
+
return `<p class="empty-note">This change has no meaningful evidence to reconstruct as a build path.</p>`;
|
|
99
|
+
const ordinal = (index) => String(index + 1).padStart(2, "0");
|
|
100
|
+
const attentionFor = (step) => step.advancesChapterIds
|
|
101
|
+
.map((chapterId) => chaptersById.get(chapterId)?.attention ?? "low")
|
|
102
|
+
.reduce((highest, attention) => (ATTENTION_RANK[attention] > ATTENTION_RANK[highest] ? attention : highest), "low");
|
|
103
|
+
const stepIndexByEvidence = new Map();
|
|
104
|
+
for (const [index, step] of document.steps.entries())
|
|
105
|
+
for (const evidenceId of step.evidenceIds)
|
|
106
|
+
stepIndexByEvidence.set(evidenceId, index);
|
|
107
|
+
const stepOrdinalById = new Map(document.steps.map((step, stepIndex) => [step.id, ordinal(stepIndex)]));
|
|
108
|
+
const stepLabel = (id) => (stepOrdinalById.has(id) ? `step ${stepOrdinalById.get(id)}` : id);
|
|
109
|
+
const ticks = document.steps.map((step, index) => `<button class="rail-tick attention-${escapeHtml(attentionFor(step))}${index === 0 ? " active" : ""}" role="tab" aria-selected="${index === 0 ? "true" : "false"}" data-timeline-select="${escapeHtml(step.id)}" data-step-title="${escapeHtml(step.title)}" title="${ordinal(index)} · ${escapeHtml(step.title)}"></button>`).join("");
|
|
110
|
+
const rail = `<div class="timeline-rail"><button class="rail-nav" data-timeline-move="-1" aria-label="Previous step" disabled>${railIcon("previous")}</button><div class="rail-ticks" role="tablist" aria-label="Build steps">${ticks}</div><button class="rail-nav" data-timeline-move="1" aria-label="Next step"${document.steps.length === 1 ? " disabled" : ""}>${railIcon("next")}</button><p class="rail-readout"><output id="rail-step" aria-live="polite">01 / ${String(document.steps.length).padStart(2, "0")}</output><span id="rail-title">${escapeHtml(document.steps[0].title)}</span></p></div>`;
|
|
111
|
+
const plan = `<div class="timeline-plan">${document.steps.map((step, index) => `<button class="timeline-plan-step attention-${escapeHtml(attentionFor(step))}" data-timeline-select="${escapeHtml(step.id)}"><span>${ordinal(index)}</span><div><strong>${renderProse(step.title)}</strong><p>${renderProse(step.goal)}</p></div></button>`).join("")}</div>`;
|
|
112
|
+
const states = document.steps.map((step, index) => {
|
|
113
|
+
const churn = churnFor(step.evidenceIds.map((id) => requireHunk(hunks, id)));
|
|
114
|
+
const filesTouched = [...churn.entries()].map(([fileId, counts]) => `<span class="timeline-file"><span>${escapeHtml(filePaths.get(fileId) ?? fileId)}</span><small><b class="additions">+${counts.additions}</b><b class="deletions">−${counts.deletions}</b></small></span>`).join("");
|
|
115
|
+
const chapters = step.advancesChapterIds.map((chapterId) => chaptersById.get(chapterId)).filter((chapter) => chapter !== undefined);
|
|
116
|
+
const chapterLinks = chapters.map((chapter) => `<button data-step-chapter="${escapeHtml(chapter.id)}">Story · ${escapeHtml(chapter.title)}</button>`).join("");
|
|
117
|
+
const deferred = step.deferred.length === 0 ? `<p class="empty-note">Nothing was postponed at this step.</p>` : `<ul>${step.deferred.map((item) => `<li>${renderProse(item.concern)}${item.resolvedByStepId ? ` <button data-timeline-select="${escapeHtml(item.resolvedByStepId)}">${escapeHtml(stepLabel(item.resolvedByStepId))}</button>` : ""}</li>`).join("")}</ul>`;
|
|
118
|
+
const forwardRefs = Object.entries(step.forwardRefs);
|
|
119
|
+
const refs = forwardRefs.length === 0 ? `<p class="empty-note">Every symbol used here already exists.</p>` : `<ul>${forwardRefs.map(([symbol, targetStepId]) => `<li><code>${escapeHtml(symbol)}</code> is introduced at <button data-timeline-select="${escapeHtml(targetStepId)}">${escapeHtml(stepLabel(targetStepId))}</button></li>`).join("")}</ul>`;
|
|
120
|
+
const buildsOn = step.dependsOn.filter((id) => stepOrdinalById.has(id)).map((id) => `<button data-timeline-select="${escapeHtml(id)}">Builds on · ${escapeHtml(stepLabel(id))}</button>`).join("");
|
|
121
|
+
return `<article class="timeline-state${index === 0 ? " active" : ""}" data-timeline-state="${escapeHtml(step.id)}" data-step-index="${index + 1}"${index === 0 ? "" : " hidden"}><header class="timeline-card"><span class="timeline-index attention-${escapeHtml(attentionFor(step))}">${ordinal(index)}</span><div><h2>${renderProse(step.title)}</h2><p class="timeline-goal">${renderProse(step.goal)}</p>${filesTouched ? `<div class="timeline-files">${filesTouched}</div>` : ""}${chapterLinks || buildsOn ? `<div class="timeline-chapter-links">${chapterLinks}${buildsOn}</div>` : ""}</div></header><div class="timeline-summary"><strong>You now have</strong><p>${renderProse(step.youNowHave)}</p></div><div class="timeline-explanation"><section><h3>Deferred for later steps</h3>${deferred}</section><section><h3>Forward references</h3>${refs}</section></div><div class="timeline-evidence" data-current-evidence="${escapeHtml(step.evidenceIds.join(" "))}"></div><div class="timeline-raw"></div></article>`;
|
|
122
|
+
}).join("");
|
|
123
|
+
return `<div class="timeline">${rail}${plan}<div class="timeline-states">${states}</div></div>`;
|
|
124
|
+
}
|
|
125
|
+
function churnFor(hunks) {
|
|
126
|
+
const churnByFile = new Map();
|
|
127
|
+
for (const hunk of hunks) {
|
|
128
|
+
const churn = churnByFile.get(hunk.fileId) ?? { additions: 0, deletions: 0 };
|
|
129
|
+
for (const line of hunk.lines) {
|
|
130
|
+
if (line.kind === "addition")
|
|
131
|
+
churn.additions += 1;
|
|
132
|
+
if (line.kind === "deletion")
|
|
133
|
+
churn.deletions += 1;
|
|
134
|
+
}
|
|
135
|
+
churnByFile.set(hunk.fileId, churn);
|
|
136
|
+
}
|
|
137
|
+
return churnByFile;
|
|
138
|
+
}
|
|
139
|
+
function renderChapterMapMetrics(metrics) {
|
|
140
|
+
const files = `${metrics.files} file${metrics.files === 1 ? "" : "s"}`;
|
|
141
|
+
const hunks = `${metrics.hunks} hunk${metrics.hunks === 1 ? "" : "s"}`;
|
|
142
|
+
const additions = `${metrics.additions} addition${metrics.additions === 1 ? "" : "s"}`;
|
|
143
|
+
const deletions = `${metrics.deletions} deletion${metrics.deletions === 1 ? "" : "s"}`;
|
|
144
|
+
return `<span class="chapter-map-meta" aria-label="${additions}, ${deletions}, ${files}, ${hunks}"><span class="chapter-churn"><b class="additions">+${metrics.additions}</b><b class="deletions">−${metrics.deletions}</b></span><span>${files}</span><span>${hunks}</span></span><span class="chapter-churn-bar" aria-hidden="true" style="--add:${metrics.addShare};--delete:${metrics.deleteShare}"><i class="additions"></i><i class="deletions"></i></span>`;
|
|
145
|
+
}
|
|
146
|
+
function categoryIcon(category) {
|
|
147
|
+
const paths = {
|
|
148
|
+
behavior: '<path d="M3 8h12M10 4l4 4-4 4"/>',
|
|
149
|
+
security: '<path d="M9 2l5 2v4c0 3.2-2 5.7-5 6.8C6 13.7 4 11.2 4 8V4l5-2z"/><path d="M7 8l1.3 1.3L11 6.6"/>',
|
|
150
|
+
performance: '<path d="M10 2L4 10h4l-1 6 6-8H9l1-6z"/>',
|
|
151
|
+
refactor: '<path d="M5 3v10M5 3l-2 2M5 3l2 2M13 13V3M13 13l-2-2M13 13l2-2"/>',
|
|
152
|
+
formatting: '<path d="M3 4h10M3 8h7M3 12h10"/>',
|
|
153
|
+
};
|
|
154
|
+
return `<svg class="tag-icon" viewBox="0 0 18 18" aria-hidden="true">${paths[category] ?? '<circle cx="9" cy="9" r="3"/>'}</svg>`;
|
|
155
|
+
}
|
|
156
|
+
function railIcon(direction) {
|
|
157
|
+
const paths = {
|
|
158
|
+
previous: '<path d="M9.8 3.8L5.6 8l4.2 4.2"/>',
|
|
159
|
+
next: '<path d="M6.2 3.8L10.4 8l-4.2 4.2"/>',
|
|
160
|
+
};
|
|
161
|
+
return `<svg viewBox="0 0 16 16" aria-hidden="true">${paths[direction]}</svg>`;
|
|
162
|
+
}
|
|
163
|
+
function navIcon(view) {
|
|
164
|
+
const paths = {
|
|
165
|
+
story: '<circle cx="5" cy="4" r="1.5"/><circle cx="5" cy="12" r="1.5"/><path d="M5 5.5v5M9 4h4M9 12h4"/>',
|
|
166
|
+
timeline: '<circle cx="9" cy="9" r="5.5"/><path d="M9 5.5v3.8l2.5 1.5"/>',
|
|
167
|
+
diff: '<path d="M6.5 5L3 9l3.5 4M11.5 5L15 9l-3.5 4"/>',
|
|
168
|
+
tests: '<path d="M3.5 9.5l3.2 3.2 6.8-7"/>',
|
|
169
|
+
};
|
|
170
|
+
return `<span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 18 18">${paths[view]}</svg></span>`;
|
|
171
|
+
}
|
|
172
|
+
function renderSemantic(before, after) {
|
|
173
|
+
if (before === undefined && after === undefined)
|
|
174
|
+
return "";
|
|
175
|
+
return `<div class="semantic"><div><strong>Before</strong><p>${renderProse(before ?? "Not inferred from this patch.")}</p></div><div><strong>After</strong><p>${renderProse(after ?? "Not inferred from this patch.")}</p></div></div>`;
|
|
176
|
+
}
|
|
177
|
+
function renderEvidence(hunk, path, highlighter, stepIndex, focus) {
|
|
178
|
+
const focused = focusLinesFromRanges(hunk.lines, focus) ?? focusedEvidenceLines(hunk.lines);
|
|
179
|
+
if (focused.length === 0)
|
|
180
|
+
return "";
|
|
181
|
+
const language = resolveLanguage(path);
|
|
182
|
+
let previousIndex = -1;
|
|
183
|
+
const excerpt = focused.map(({ line, index }) => {
|
|
184
|
+
const omission = previousIndex >= 0 && index > previousIndex + 1 ? renderOmission() : "";
|
|
185
|
+
previousIndex = index;
|
|
186
|
+
return `${omission}${renderEvidenceLine(line, language, highlighter)}`;
|
|
187
|
+
}).join("");
|
|
188
|
+
const omittedCount = hunk.lines.length - focused.length;
|
|
189
|
+
const tag = stepIndex === undefined ? "" : `<button class="evidence-step-tag" data-story-step-index="${stepIndex + 1}">step ${String(stepIndex + 1).padStart(2, "0")}</button>`;
|
|
190
|
+
// The raw variant is cloned at runtime from the matching Full diff block, so
|
|
191
|
+
// every hunk body ships exactly once in the artifact.
|
|
192
|
+
return `<article class="evidence focused-evidence" data-evidence-id="${escapeHtml(hunk.id)}"><header><span class="evidence-path">${escapeHtml(path)}</span><span class="focused-label">Focused excerpt · @@ −${hunk.oldStart} +${hunk.newStart} @@</span><span class="raw-label">Complete excerpt · @@ −${hunk.oldStart} +${hunk.newStart} @@</span>${tag}</header><pre class="focused-code">${excerpt}</pre>${omittedCount > 0 ? `<p class="evidence-context">${omittedCount} routine line${omittedCount === 1 ? "" : "s"} omitted</p>` : ""}</article>`;
|
|
193
|
+
}
|
|
194
|
+
function renderEvidenceLine(line, language, highlighter) {
|
|
195
|
+
return `<span class="line ${line.kind}"><b>${line.oldLine ?? ""}</b><b>${line.newLine ?? ""}</b><code><span class="diff-prefix">${linePrefix(line.kind)}</span>${highlightCode(line.content, language, highlighter)}</code></span>`;
|
|
196
|
+
}
|
|
197
|
+
function renderOmission() {
|
|
198
|
+
return `<span class="line omission" aria-label="Routine lines omitted"><span class="omission-divider"><i></i><b>⌁</b><i></i></span></span>`;
|
|
199
|
+
}
|
|
200
|
+
function renderOtherFilesChanged(files, hunks) {
|
|
201
|
+
const rows = files.flatMap((file) => {
|
|
202
|
+
if (!isSupportingFile(file))
|
|
203
|
+
return [];
|
|
204
|
+
const changes = hunks.filter((hunk) => hunk.fileId === file.id).flatMap((hunk) => hunk.lines);
|
|
205
|
+
const additions = changes.filter((line) => line.kind === "addition").length;
|
|
206
|
+
const deletions = changes.filter((line) => line.kind === "deletion").length;
|
|
207
|
+
return additions + deletions > 0 ? [{ path: file.path, additions, deletions }] : [];
|
|
208
|
+
});
|
|
209
|
+
if (rows.length === 0)
|
|
210
|
+
return "";
|
|
211
|
+
return `<section class="other-files" aria-label="Other files changed"><h2>Other files changed</h2><ul>${rows.map((row) => `<li><span>${escapeHtml(row.path)}</span><small><b class="additions">+${row.additions}</b><b class="deletions">−${row.deletions}</b></small></li>`).join("")}</ul></section>`;
|
|
212
|
+
}
|
|
213
|
+
/** Files past this many changed lines start collapsed in Full diff so huge branches lay out lazily instead of at first paint. */
|
|
214
|
+
const FULL_DIFF_OPEN_LINE_LIMIT = 400;
|
|
215
|
+
function renderFullDiff(file, hunks, highlighter) {
|
|
216
|
+
const parsed = parseDiff(toUnifiedDiff(file, hunks), { drawFileList: false, outputFormat: "line-by-line" })[0];
|
|
217
|
+
if (parsed === undefined)
|
|
218
|
+
return "";
|
|
219
|
+
const language = resolveLanguage(file.path);
|
|
220
|
+
const lineCount = hunks.reduce((sum, hunk) => sum + hunk.lines.length, 0);
|
|
221
|
+
return `<details class="file full-diff-file"${lineCount <= FULL_DIFF_OPEN_LINE_LIMIT ? " open" : ""} data-file-id="${escapeHtml(file.id)}"><summary><span class="file-path">${escapeHtml(file.path)}</span><small>${escapeHtml(file.signalReason ?? file.status)}</small></summary>${parsed.blocks.map((block, index) => renderDiffBlock(block, language, highlighter, hunks[index]?.id)).join("")}</details>`;
|
|
222
|
+
}
|
|
223
|
+
function renderDiffBlock(block, language, highlighter, hunkId) {
|
|
224
|
+
const tokenLines = highlighter.codeToTokens(block.lines.map((line) => line.content.slice(1)).join("\n"), { lang: language, themes: syntaxThemes }).tokens;
|
|
225
|
+
return `<div class="diff-block"${hunkId === undefined ? "" : ` data-diff-hunk="${escapeHtml(hunkId)}"`}><div class="diff-hunk-header">${escapeHtml(block.header)}</div><pre>${block.lines.map((line, index) => renderDiffLine(line, tokenLines[index] ?? [])).join("")}</pre></div>`;
|
|
226
|
+
}
|
|
227
|
+
function renderDiffLine(line, tokens) {
|
|
228
|
+
const kind = line.type === "insert" ? "addition" : line.type === "delete" ? "deletion" : "context";
|
|
229
|
+
return `<span class="line ${kind}"><b>${line.oldNumber ?? ""}</b><b>${line.newNumber ?? ""}</b><code><span class="diff-prefix">${escapeHtml(line.content.slice(0, 1))}</span>${renderTokens(tokens)}</code></span>`;
|
|
230
|
+
}
|
|
231
|
+
/** Both themes are emitted per token; dark mode flips to the --shiki-dark variable. */
|
|
232
|
+
const syntaxThemes = { light: "github-light", dark: "github-dark" };
|
|
233
|
+
function highlightCode(source, language, highlighter) {
|
|
234
|
+
return renderTokens(highlighter.codeToTokens(source, { lang: language, themes: syntaxThemes }).tokens[0] ?? []);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Syntax colors repeat constantly, so each distinct color pair becomes one CSS
|
|
238
|
+
* class instead of ~90 bytes of inline styles per token. The registry is
|
|
239
|
+
* append-only and process-wide; every rendered page emits the full table.
|
|
240
|
+
*/
|
|
241
|
+
const tokenClassRegistry = new Map();
|
|
242
|
+
function tokenClass(style) {
|
|
243
|
+
let name = tokenClassRegistry.get(style);
|
|
244
|
+
if (name === undefined) {
|
|
245
|
+
name = `c${tokenClassRegistry.size}`;
|
|
246
|
+
tokenClassRegistry.set(style, name);
|
|
247
|
+
}
|
|
248
|
+
return name;
|
|
249
|
+
}
|
|
250
|
+
function tokenStyleRules() {
|
|
251
|
+
return [...tokenClassRegistry].map(([style, name]) => `.${name}{${style}}`).join("");
|
|
252
|
+
}
|
|
253
|
+
function renderTokens(tokens) {
|
|
254
|
+
return tokens.map((token) => {
|
|
255
|
+
const style = token.htmlStyle === undefined
|
|
256
|
+
? (token.color === undefined ? "" : `color:${token.color}`)
|
|
257
|
+
: Object.entries(token.htmlStyle).map(([property, value]) => `${property}:${value}`).join(";");
|
|
258
|
+
const classes = [style === "" ? "" : `tk ${tokenClass(style)}`, token.fontStyle === 1 ? "token-italic" : ""].filter(Boolean).join(" ");
|
|
259
|
+
return `<span${classes === "" ? "" : ` class="${classes}"`}>${escapeHtml(token.content)}</span>`;
|
|
260
|
+
}).join("");
|
|
261
|
+
}
|
|
262
|
+
function renderOmitted(document, hunks) {
|
|
263
|
+
const count = document.omittedGroups.reduce((sum, group) => sum + group.evidenceIds.length, 0);
|
|
264
|
+
const unclassified = document.unclassifiedEvidenceIds.length;
|
|
265
|
+
if (count + unclassified === 0)
|
|
266
|
+
return "";
|
|
267
|
+
const summary = unclassified === 0 ? `${count} low-signal hunks collapsed` : count === 0 ? `${unclassified} unclassified hunks collapsed` : `${count + unclassified} low-signal or unclassified hunks collapsed`;
|
|
268
|
+
const groups = [
|
|
269
|
+
...document.omittedGroups.map((group) => `<p><strong>${renderProse(group.title)}</strong> · ${renderProse(group.reason)}</p>`),
|
|
270
|
+
...(unclassified > 0 ? [`<p><strong>Unclassified evidence</strong> · ${unclassified} hunk${unclassified === 1 ? "" : "s"} the analysis could not attach to a story chapter; see Full diff.</p>`] : []),
|
|
271
|
+
];
|
|
272
|
+
return `<details class="omitted"><summary>${summary}</summary>${groups.join("")}</details>`;
|
|
273
|
+
}
|
|
274
|
+
function renderTestPlan(document, hunks, filePaths, files, highlighter) {
|
|
275
|
+
const themes = buildTestThemes(document, hunks, filePaths);
|
|
276
|
+
const cases = themes.flatMap((theme) => theme.cases);
|
|
277
|
+
const testFiles = [...new Set(cases.map((testCase) => testCase.filePath))];
|
|
278
|
+
const changedTestFiles = files.filter((file) => isTestPath(file.path));
|
|
279
|
+
if (cases.length === 0 && changedTestFiles.length === 0)
|
|
280
|
+
return `<p class="empty-note">No test activity was captured for this change.</p>`;
|
|
281
|
+
const runs = document.testExecution ?? [];
|
|
282
|
+
const summary = [
|
|
283
|
+
`${cases.length} tested behavior${cases.length === 1 ? "" : "s"}`,
|
|
284
|
+
`${new Set([...testFiles, ...changedTestFiles.map((file) => file.path)]).size} test file${new Set([...testFiles, ...changedTestFiles.map((file) => file.path)]).size === 1 ? "" : "s"}`,
|
|
285
|
+
`${runs.length} observed test run${runs.length === 1 ? "" : "s"}`,
|
|
286
|
+
runs.length === 0 ? "Run result not observed" : `Observed result: ${aggregateRunOutcome(runs)}`,
|
|
287
|
+
].map((item) => `<span>${escapeHtml(item)}</span>`).join("");
|
|
288
|
+
return `<div class="test-plan-summary">${summary}</div><div class="test-plan-level test-plan-map">${renderTestPlanMap(themes)}</div><div class="test-plan-level test-plan-summary-level">${renderTestPlanSummary(themes)}</div><div class="test-plan-level test-plan-explanation">${renderTestPlanExplanation(themes)}</div><div class="test-plan-level test-plan-evidence">${renderTestPlanEvidence(themes, runs)}</div><div class="test-plan-level test-plan-raw">${renderTestPlanRaw(themes, runs)}</div>${renderTestExcerptLibrary(themes, highlighter)}`;
|
|
289
|
+
}
|
|
290
|
+
/** Each test hunk's excerpt ships once as a template; the Evidence and Raw levels clone into their slots at runtime. */
|
|
291
|
+
function renderTestExcerptLibrary(themes, highlighter) {
|
|
292
|
+
const seen = new Set();
|
|
293
|
+
const templates = themes.flatMap((theme) => theme.cases).flatMap((testCase) => {
|
|
294
|
+
if (seen.has(testCase.hunk.id))
|
|
295
|
+
return [];
|
|
296
|
+
seen.add(testCase.hunk.id);
|
|
297
|
+
return [`<template data-test-excerpt-template="${escapeHtml(testCase.hunk.id)}">${renderTestExcerpt(testCase.hunk, testCase.filePath, highlighter, false)}</template>`];
|
|
298
|
+
});
|
|
299
|
+
return templates.length === 0 ? "" : `<div id="test-excerpt-library" hidden>${templates.join("")}</div>`;
|
|
300
|
+
}
|
|
301
|
+
function aggregateRunOutcome(runs) {
|
|
302
|
+
if (runs.some((run) => run.outcome === "failed"))
|
|
303
|
+
return "failed";
|
|
304
|
+
if (runs.length > 0 && runs.every((run) => run.outcome === "passed"))
|
|
305
|
+
return "passed";
|
|
306
|
+
if (runs.some((run) => run.outcome === "mixed" || run.outcome === "passed"))
|
|
307
|
+
return "mixed";
|
|
308
|
+
return "unknown";
|
|
309
|
+
}
|
|
310
|
+
function renderTestPlanMap(themes) {
|
|
311
|
+
const derivedSummary = deriveTestSummary(themes);
|
|
312
|
+
return `${derivedSummary ? `<p class="test-generated-summary">${renderProse(derivedSummary)}</p>` : ""}<div class="test-theme-grid">${themes.map((theme) => `<article class="test-theme-card" data-test-theme="${escapeHtml(theme.id)}"><h2>${renderProse(theme.title)}</h2><p>${renderProse(theme.synopsis)}</p><div class="test-theme-meta"><span>${theme.cases.length} tested behavior${theme.cases.length === 1 ? "" : "s"}</span><span>${testTypeLabel(theme.cases)}</span>${theme.storyClaims.length > 0 ? `<span>${theme.storyClaims.length} story claim${theme.storyClaims.length === 1 ? "" : "s"}</span>` : ""}</div>${theme.cases.length === 0 ? `<p class="empty-note">No associated test evidence</p>` : ""}</article>`).join("")}</div>`;
|
|
313
|
+
}
|
|
314
|
+
function renderTestPlanSummary(themes) {
|
|
315
|
+
return themes.map((theme) => `<section class="test-behavior-group"><h2>${escapeHtml(theme.title)}</h2>${theme.cases.length === 0 ? `<p class="empty-note">No associated test evidence</p>` : theme.cases.map((testCase) => `<article class="test-behavior" data-test-case="${escapeHtml(testCase.id)}"><button data-test-jump="${escapeHtml(testCase.id)}"><strong>${escapeHtml(testCase.name)}</strong><span>${renderProse(testCase.chapter.synopsis)}</span></button><div class="test-inline-meta"><span>${escapeHtml(testCase.filePath)}</span><span>${testTypeLabel([testCase])}</span><span>Test implementation found</span></div></article>`).join("")}</section>`).join("");
|
|
316
|
+
}
|
|
317
|
+
function renderTestPlanExplanation(themes) {
|
|
318
|
+
return themes.map((theme) => `<section class="test-explanation-group"><h2>${escapeHtml(theme.title)}</h2>${theme.cases.length === 0 ? `<p class="empty-note">No associated test evidence</p>` : theme.cases.map((testCase) => `<article class="test-explanation-card" data-test-case="${escapeHtml(testCase.id)}"><h3>${escapeHtml(testCase.name)}</h3><p class="test-what">${renderProse(testCase.chapter.synopsis)}</p>${renderSemantic(testCase.chapter.before, testCase.chapter.after)}<div class="test-refs"><span class="test-ref-file">${escapeHtml(testCase.filePath)}</span></div></article>`).join("")}</section>`).join("");
|
|
319
|
+
}
|
|
320
|
+
function renderTestPlanEvidence(themes, runs) {
|
|
321
|
+
const execution = runs.length === 0 ? "Execution evidence not observed" : `${runs[0].command}${runs.length > 1 ? ` and ${runs.length - 1} more run${runs.length === 2 ? "" : "s"}` : ""}`;
|
|
322
|
+
const result = runs.length === 0 ? "Not observed" : `Suite ${aggregateRunOutcome(runs)}`;
|
|
323
|
+
return themes.map((theme) => theme.cases.length === 0 ? `<article class="test-evidence-card"><h2>${escapeHtml(theme.title)}</h2><p class="empty-note">No associated test evidence</p></article>` : theme.cases.map((testCase) => `<details class="test-evidence-card" data-test-case="${escapeHtml(testCase.id)}"><summary><span><strong>${escapeHtml(testCase.name)}</strong><small>${escapeHtml(testCase.filePath)}</small></span><span class="test-state">Test implementation found</span></summary><div class="test-excerpt-slot" data-test-hunk="${escapeHtml(testCase.hunk.id)}"></div><div class="test-evidence-meta"><div><strong>Associated implementation</strong><span>Source mapping unavailable</span></div><div><strong>Observed execution</strong><span>${escapeHtml(execution)}</span></div><div><strong>Result</strong><span>${escapeHtml(result)}</span></div></div></details>`).join("")).join("");
|
|
324
|
+
}
|
|
325
|
+
function renderTestPlanRaw(themes, runs) {
|
|
326
|
+
const cases = themes.flatMap((theme) => theme.cases);
|
|
327
|
+
if (cases.length === 0)
|
|
328
|
+
return `<p class="empty-note">No test activity was captured for this change.</p>`;
|
|
329
|
+
const seen = new Set();
|
|
330
|
+
const artifacts = cases.flatMap((testCase) => {
|
|
331
|
+
if (seen.has(testCase.hunk.id))
|
|
332
|
+
return [];
|
|
333
|
+
seen.add(testCase.hunk.id);
|
|
334
|
+
// The complete hunk body already ships in Full diff; the slot clones it at runtime instead of shipping a second copy.
|
|
335
|
+
return [`<article class="test-raw-artifact"><h2>${escapeHtml(testCase.filePath)}</h2><article class="evidence focused-evidence"><header><span class="evidence-path">${escapeHtml(testCase.filePath)}</span><span>Complete test artifact · @@ −${testCase.hunk.oldStart} +${testCase.hunk.newStart} @@</span></header><div class="test-raw-slot" data-test-hunk="${escapeHtml(testCase.hunk.id)}"></div></article></article>`];
|
|
336
|
+
});
|
|
337
|
+
const commands = runs.length === 0
|
|
338
|
+
? `<p class="empty-note">Execution evidence not observed</p>`
|
|
339
|
+
: runs.map((run) => `<article class="test-run"><code>${escapeHtml(run.command)}</code><span class="run-outcome run-outcome-${escapeHtml(run.outcome)}">${escapeHtml(run.outcome)}</span><p>${renderProse(run.summary)}</p><small>observed in the ${escapeHtml(run.source)}</small></article>`).join("");
|
|
340
|
+
const output = runs.length === 0
|
|
341
|
+
? `<p class="empty-note">Execution evidence not observed</p>`
|
|
342
|
+
: `<p class="empty-note">Only run summaries were captured; full output was not recorded.</p>`;
|
|
343
|
+
return `<section class="test-raw-section"><h2>Changed test files</h2>${artifacts.join("")}</section><section class="test-raw-section"><h2>Observed commands</h2>${commands}</section><section class="test-raw-section"><h2>Complete output</h2>${output}</section>`;
|
|
344
|
+
}
|
|
345
|
+
function renderTestExcerpt(hunk, filePath, highlighter, raw) {
|
|
346
|
+
const language = resolveLanguage(filePath);
|
|
347
|
+
const lines = focusedTestLines(hunk, raw);
|
|
348
|
+
const rendered = lines.map((line) => renderEvidenceLine(line, language, highlighter)).join("");
|
|
349
|
+
return `<article class="evidence focused-evidence"><header><span class="evidence-path">${escapeHtml(filePath)}</span><span>${raw ? "Complete test artifact" : "Focused test excerpt"} · @@ −${hunk.oldStart} +${hunk.newStart} @@</span></header><pre>${rendered}</pre></article>`;
|
|
350
|
+
}
|
|
351
|
+
function requireHunk(hunks, id) {
|
|
352
|
+
const hunk = hunks.find((candidate) => candidate.id === id);
|
|
353
|
+
if (hunk === undefined)
|
|
354
|
+
throw new Error(`Unknown hunk ${id}`);
|
|
355
|
+
return hunk;
|
|
356
|
+
}
|
|
357
|
+
function escapeHtml(value) { return value.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char] ?? char); }
|
|
358
|
+
/**
|
|
359
|
+
* Analysis prose uses inline Markdown: backticked symbols, bold, and italic.
|
|
360
|
+
* Code spans are split out first so emphasis never rewrites their contents,
|
|
361
|
+
* and every part is HTML-escaped before any markup is introduced.
|
|
362
|
+
*/
|
|
363
|
+
function renderProse(value) {
|
|
364
|
+
return value.split(/(`[^`\n]+`)/).map((part) => {
|
|
365
|
+
if (part.length > 2 && part.startsWith("`") && part.endsWith("`"))
|
|
366
|
+
return `<code class="md-code">${escapeHtml(part.slice(1, -1))}</code>`;
|
|
367
|
+
return escapeHtml(part)
|
|
368
|
+
.replace(/\*\*(\S(?:[^*\n]*\S)?)\*\*/g, "<strong>$1</strong>")
|
|
369
|
+
.replace(/__(\S(?:[^_\n]*\S)?)__/g, "<strong>$1</strong>")
|
|
370
|
+
.replace(/(^|[^\w*])\*(\S(?:[^*\n]*\S)?)\*(?![\w*])/g, "$1<em>$2</em>")
|
|
371
|
+
.replace(/(^|[^\w_])_(\S(?:[^_\n]*\S)?)_(?![\w_])/g, "$1<em>$2</em>");
|
|
372
|
+
}).join("");
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The ndrstnd galley system. One stylesheet, three typographic voices:
|
|
376
|
+
* sans speaks UI chrome, mono speaks identifiers, serif speaks the story.
|
|
377
|
+
* Light paper surfaces, hairline rules, a single cobalt accent, no gradients.
|
|
378
|
+
*/
|
|
379
|
+
export const styles = `
|
|
380
|
+
:root{color-scheme:light dark;--sans:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI",system-ui,sans-serif;--mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;--surface:#fff;--rail:#f6f6f4;--well:#fafaf8;--ink:#1c2126;--ink-2:#4a5258;--ink-3:#66707a;--faint:#6d7781;--hair:#e8e7e3;--hair-2:#d7d6d1;--accent:#2757cf;--wash:#eef2fb;--low:#2e8f53;--contained:#4d94c9;--elevated:#a97c15;--high:#b85f2e;--critical:#bf4840;--add-ink:#1c7a41;--add-bg:#eefaf1;--del-ink:#b04a3d;--del-bg:#fdf1ee;--shadow:0 18px 44px #191e2821}
|
|
381
|
+
*{box-sizing:border-box}html{overflow-x:hidden}body{margin:0;min-width:320px;background:var(--surface);color:var(--ink);font:13px/1.5 var(--sans);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
|
382
|
+
[hidden]{display:none!important}
|
|
383
|
+
::selection{background:#d9e3f8}
|
|
384
|
+
button{font-family:inherit}
|
|
385
|
+
button:focus-visible,select:focus-visible,summary:focus-visible,[role="button"]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
386
|
+
|
|
387
|
+
.app-shell{display:grid;grid-template-columns:236px minmax(0,1fr) 264px;min-height:100vh;transition:grid-template-columns 220ms ease}
|
|
388
|
+
.app-shell.sidebar-collapsed{grid-template-columns:64px minmax(0,1fr) 264px}
|
|
389
|
+
.app-shell.inspector-collapsed{grid-template-columns:236px minmax(0,1fr) 64px}
|
|
390
|
+
.app-shell.sidebar-collapsed.inspector-collapsed{grid-template-columns:64px minmax(0,1fr) 64px}
|
|
391
|
+
|
|
392
|
+
.sidebar{position:sticky;top:0;height:100vh;background:var(--rail);border-right:1px solid var(--hair);padding:20px 14px;overflow:hidden}
|
|
393
|
+
.brand{display:flex;align-items:center;gap:9px;padding:2px 6px 24px}
|
|
394
|
+
.brand-mark{width:19px;height:21px;flex:none;color:var(--ink)}
|
|
395
|
+
.brand-mark path{fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
|
|
396
|
+
.brand-mark .mark-deep{stroke:var(--accent)}
|
|
397
|
+
.brand-name{font:600 14px/1 var(--mono);letter-spacing:-.02em;color:var(--ink)}
|
|
398
|
+
.panel-toggle{display:grid;place-items:center;width:30px;height:30px;flex:none;padding:0;border:0;border-radius:7px;background:transparent;color:var(--ink-3);cursor:pointer;transition:background-color 140ms ease,color 140ms ease,transform 200ms ease}
|
|
399
|
+
.panel-toggle svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
|
400
|
+
.panel-toggle:hover{background:#ebebe8;color:var(--ink)}
|
|
401
|
+
.collapse-sidebar{margin-left:auto}
|
|
402
|
+
.mobile-inspector-toggle{display:none}
|
|
403
|
+
.sidebar nav{display:grid;gap:2px}
|
|
404
|
+
.nav-item{position:relative;display:flex;align-items:center;gap:10px;width:100%;min-height:38px;padding:9px 10px;border:0;border-radius:7px;background:transparent;color:var(--ink-2);font:500 13px/1.2 var(--sans);text-align:left;cursor:pointer;transition:background-color 140ms ease,color 140ms ease}
|
|
405
|
+
.nav-icon{display:grid;place-items:center;width:18px;height:18px;flex:none}
|
|
406
|
+
.nav-icon svg{display:block;width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
|
|
407
|
+
.nav-item:hover{background:#ecece9;color:var(--ink)}
|
|
408
|
+
.nav-item.active{color:var(--accent)}
|
|
409
|
+
.nav-item.active::before{content:"";position:absolute;left:-14px;top:9px;bottom:9px;width:2px;border-radius:2px;background:var(--accent)}
|
|
410
|
+
.sidebar.collapsed{padding:20px 10px}
|
|
411
|
+
.sidebar.collapsed .brand{flex-direction:column;gap:12px;padding:2px 0 20px}
|
|
412
|
+
.sidebar.collapsed .brand-name{display:none}
|
|
413
|
+
.sidebar.collapsed .collapse-sidebar{margin:0;transform:rotate(180deg)}
|
|
414
|
+
.sidebar.collapsed .nav-item{justify-content:center;gap:0;min-height:42px;padding:10px 0;font-size:0}
|
|
415
|
+
.sidebar.collapsed .nav-item.active::before{left:-11px}
|
|
416
|
+
|
|
417
|
+
.main{min-width:0;width:100%;max-width:1040px;margin:0 auto;padding:38px 44px 96px}
|
|
418
|
+
.page-header{border-bottom:1px solid var(--hair);padding:0 280px 20px 0}
|
|
419
|
+
.masthead{min-width:0}
|
|
420
|
+
.masthead-overline{margin:0 0 12px;font:600 10px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3)}
|
|
421
|
+
.page-header h1{margin:0 0 9px;font:600 22px/1.3 var(--mono);letter-spacing:-.02em;color:var(--ink);word-break:break-word}
|
|
422
|
+
.breadcrumbs{display:flex;flex-wrap:wrap;align-items:center;gap:14px;font-size:12px;color:var(--ink-3)}
|
|
423
|
+
.breadcrumbs strong{color:var(--ink-2);font-weight:600}
|
|
424
|
+
.breadcrumbs code{font:500 11.5px var(--mono);color:var(--ink-2);background:var(--well);border:1px solid var(--hair);border-radius:6px;padding:2px 6px}
|
|
425
|
+
.view-bar{position:sticky;top:0;z-index:15;height:0;pointer-events:none}
|
|
426
|
+
.view-bar::before{content:"";position:absolute;top:0;left:-44px;right:-44px;height:62px;background:var(--surface);border-bottom:1px solid var(--hair);box-shadow:0 6px 20px #191e280f;opacity:0;transition:opacity 200ms ease}
|
|
427
|
+
.view-bar.view-bar-stuck::before{opacity:1;pointer-events:auto}
|
|
428
|
+
.view-bar .story-zoom-controls{position:absolute;top:-112px;right:0;pointer-events:auto;transition:top 240ms cubic-bezier(.2,.8,.2,1)}
|
|
429
|
+
.view-bar.view-bar-stuck .story-zoom-controls{top:5px}
|
|
430
|
+
.view-bar-ref{position:absolute;left:0;top:20px;max-width:40%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:600 11.5px/1.4 var(--mono);color:var(--ink-3);opacity:0;transition:opacity 200ms ease}
|
|
431
|
+
.view-bar.view-bar-stuck .view-bar-ref{opacity:1}
|
|
432
|
+
|
|
433
|
+
.story-zoom-controls{position:relative;display:flex;align-items:flex-start;gap:2px;height:52px;padding-top:2px}
|
|
434
|
+
.story-zoom-controls[hidden]{display:none!important}
|
|
435
|
+
.story-zoom-controls>button[data-zoom-step]{width:30px;height:30px;border:0;border-radius:8px;background:transparent;color:var(--ink-3);font:400 17px/1 var(--sans);cursor:pointer;transition:background-color 140ms ease,color 140ms ease}
|
|
436
|
+
.story-zoom-controls>button[data-zoom-step]:hover{background:var(--well);color:var(--ink)}
|
|
437
|
+
.zoom{position:relative;display:flex;align-items:flex-end;justify-content:space-between;width:186px;height:30px;padding:0 5px;border:0}
|
|
438
|
+
.zoom::before{content:"";position:absolute;left:9px;right:9px;bottom:3px;height:1px;background:var(--hair-2)}
|
|
439
|
+
.zoom button{position:relative;z-index:1;width:24px;height:30px;min-height:0;padding:0;border:0;background:transparent;cursor:pointer}
|
|
440
|
+
.zoom button::before{content:"";position:absolute;left:50%;bottom:3px;width:2px;height:9px;border-radius:1px 1px 0 0;background:var(--hair-2);transform:translateX(-50%);transition:background-color 160ms ease,height 200ms cubic-bezier(.3,1.3,.4,1),width 160ms ease}
|
|
441
|
+
.zoom button:hover::before{background:var(--ink-3);height:13px}
|
|
442
|
+
.zoom button.active::before{background:var(--accent);height:17px;width:2.5px}
|
|
443
|
+
.zoom button::after{content:"";position:absolute;left:50%;bottom:23px;width:5px;height:5px;border-radius:50%;background:var(--accent);transform:translateX(-50%) scale(0);opacity:0;transition:transform 200ms cubic-bezier(.3,1.3,.4,1),opacity 160ms ease}
|
|
444
|
+
.zoom button.active::after{opacity:1;transform:translateX(-50%) scale(1)}
|
|
445
|
+
.zoom-callout{position:absolute;left:50%;top:calc(100% + 3px);transform:translateX(-50%);white-space:nowrap;text-align:center;pointer-events:none;transition:opacity 160ms ease}
|
|
446
|
+
.zoom-callout output{font:700 10px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--accent)}
|
|
447
|
+
.zoom-callout span{margin-left:8px;font:400 11px/1 var(--sans);color:var(--ink-3)}
|
|
448
|
+
.zoom.is-changing .zoom-callout{opacity:.45}
|
|
449
|
+
|
|
450
|
+
.view{display:none}
|
|
451
|
+
.view.active{display:block;animation:view-in 240ms ease}
|
|
452
|
+
.review-summary{margin:30px 0 10px;max-width:42em;font:400 20px/1.55 var(--sans);letter-spacing:-.01em;color:var(--ink)}
|
|
453
|
+
.coverage{margin:0 0 22px;font-size:12px;color:var(--ink-3);font-variant-numeric:tabular-nums}
|
|
454
|
+
.story-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 12px}
|
|
455
|
+
.map{display:flex;flex-wrap:wrap;gap:8px 18px}
|
|
456
|
+
.map div{display:inline-flex;align-items:center;gap:7px;font:500 11px/1 var(--sans);letter-spacing:.02em;color:var(--ink-2);text-transform:capitalize}
|
|
457
|
+
.map strong{color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums}
|
|
458
|
+
.dot{width:7px;height:7px;border-radius:50%;background:var(--low)}
|
|
459
|
+
.dot.contained{background:var(--contained)}.dot.elevated{background:var(--elevated)}.dot.high{background:var(--high)}.dot.critical{background:var(--critical)}
|
|
460
|
+
#collapse-all{margin-left:auto;border:0;border-radius:7px;background:transparent;min-height:30px;padding:6px 10px;font:500 12px var(--sans);color:var(--ink-3);cursor:pointer;transition:background-color 140ms ease,color 140ms ease}
|
|
461
|
+
#collapse-all:hover{background:var(--well);color:var(--ink)}
|
|
462
|
+
body.story-level-0 #collapse-all,body.story-level-1 #collapse-all{display:none}
|
|
463
|
+
|
|
464
|
+
.chapter-list{border:1px solid var(--hair);border-radius:12px;background:var(--surface);overflow:hidden}
|
|
465
|
+
.chapter{border-bottom:1px solid var(--hair)}
|
|
466
|
+
.chapter:last-child{border-bottom:0}
|
|
467
|
+
.chapter-toggle{display:grid;grid-template-columns:46px minmax(0,1fr) 18px;align-items:start;gap:12px;width:100%;padding:17px 18px 15px;border:0;background:transparent;text-align:left;cursor:pointer;transition:background-color 140ms ease}
|
|
468
|
+
.chapter-toggle:hover,.chapter.open .chapter-toggle{background:var(--well)}
|
|
469
|
+
.chapter-number{position:relative;font:700 12.5px/1 var(--mono);letter-spacing:.08em;color:var(--ink-3);padding:6px 0 15px}
|
|
470
|
+
.chapter-number::after{content:"";position:absolute;left:1px;bottom:2px;width:16px;height:3px;border-radius:2px;background:var(--hair-2)}
|
|
471
|
+
.chapter-number.attention-low{color:var(--low)}
|
|
472
|
+
.chapter-number.attention-contained{color:var(--contained)}
|
|
473
|
+
.chapter-number.attention-elevated{color:var(--elevated)}
|
|
474
|
+
.chapter-number.attention-high{color:var(--high)}
|
|
475
|
+
.chapter-number.attention-critical{color:var(--critical)}
|
|
476
|
+
.chapter-number.attention-low::after{background:var(--low)}
|
|
477
|
+
.chapter-number.attention-contained::after{background:var(--contained)}
|
|
478
|
+
.chapter-number.attention-elevated::after{background:var(--elevated)}
|
|
479
|
+
.chapter-number.attention-high::after{background:var(--high)}
|
|
480
|
+
.chapter-number.attention-critical::after{background:var(--critical)}
|
|
481
|
+
.chapter-copy{display:grid;gap:4px;min-width:0}
|
|
482
|
+
.chapter-copy strong{font:600 14.5px/1.4 var(--sans);color:var(--ink)}
|
|
483
|
+
.chapter-copy small{font:400 12.5px/1.5 var(--sans);color:var(--ink-3)}
|
|
484
|
+
.chapter-tags,.chapter-steps{display:flex;flex-wrap:wrap;gap:5px 6px;margin-top:8px}
|
|
485
|
+
.chapter-tag{display:inline-flex;align-items:center;gap:5px;padding:4px 9px;border:1px solid var(--hair);border-radius:999px;background:var(--surface);font:600 9.5px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3)}
|
|
486
|
+
.step-chip,.evidence-step-tag{display:inline-flex;align-items:center;min-height:22px;padding:3px 8px;border:0;border-radius:7px;background:var(--well);font:700 9.5px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--accent);cursor:pointer}
|
|
487
|
+
.step-chip:hover,.evidence-step-tag:hover{background:var(--wash)}
|
|
488
|
+
.tag-icon{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
|
|
489
|
+
.chevron{display:grid;place-items:center;width:18px;height:18px;margin-top:3px;color:var(--faint);transition:transform 200ms ease,color 140ms ease}
|
|
490
|
+
.chevron svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
|
491
|
+
.chapter-toggle:hover .chevron{color:var(--ink-2)}
|
|
492
|
+
.chapter.open .chevron{transform:rotate(180deg)}
|
|
493
|
+
.chapter-list,.map,.chapter-detail,.semantic,.evidence-stack,.evidence{transition:opacity 220ms ease,transform 220ms ease,max-height 260ms ease,margin 220ms ease,padding 220ms ease}
|
|
494
|
+
.chapter-detail{max-height:0;opacity:0;overflow:hidden;padding:0 18px 0 76px;transform:translateY(-4px)}
|
|
495
|
+
.chapter.open .chapter-detail{max-height:4000px;opacity:1;padding-top:2px;padding-bottom:22px;transform:none}
|
|
496
|
+
|
|
497
|
+
.raw-code,.raw-label{display:none}
|
|
498
|
+
.story-level-0 .chapter-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;border:0;border-radius:0;background:transparent;overflow:visible;pointer-events:none}
|
|
499
|
+
.story-level-0 .chapter{border:1px solid var(--hair);border-radius:12px;background:var(--surface);animation:rise 240ms ease both}
|
|
500
|
+
.story-level-0 .chapter:nth-child(2){animation-delay:35ms}
|
|
501
|
+
.story-level-0 .chapter:nth-child(3){animation-delay:70ms}
|
|
502
|
+
.story-level-0 .chapter:nth-child(4){animation-delay:105ms}
|
|
503
|
+
.story-level-0 .chapter:nth-child(5){animation-delay:140ms}
|
|
504
|
+
.story-level-0 .chapter:nth-child(n+6){animation-delay:175ms}
|
|
505
|
+
.story-level-0 .chapter-toggle{grid-template-columns:38px minmax(0,1fr);gap:12px;min-height:132px;padding:16px;cursor:default}
|
|
506
|
+
.story-level-0 .chapter-toggle:hover{background:transparent}
|
|
507
|
+
.story-level-0 .chapter-copy{min-height:96px;gap:7px}
|
|
508
|
+
.story-level-0 .chapter-copy strong{font-size:15px}
|
|
509
|
+
.story-level-0 .chapter-copy small{display:none}
|
|
510
|
+
.story-level-0 .chevron{display:none}
|
|
511
|
+
.story-level-0 .chapter-detail{display:none!important}
|
|
512
|
+
.chapter-map-meta,.chapter-churn-bar{display:none}
|
|
513
|
+
.story-level-0 .chapter-map-meta{display:flex;align-items:center;flex-wrap:wrap;gap:6px 10px;margin-top:auto;font:400 10.5px/1.3 var(--mono);color:var(--ink-3)}
|
|
514
|
+
.story-level-0 .chapter-churn{display:flex;gap:7px;font:600 11px/1.2 var(--mono)}
|
|
515
|
+
.story-level-0 .chapter-map-meta .additions{color:var(--add-ink)}
|
|
516
|
+
.story-level-0 .chapter-map-meta .deletions{color:var(--del-ink)}
|
|
517
|
+
.story-level-0 .chapter-churn-bar{display:flex;width:100%;height:3px;margin-top:4px;overflow:hidden;border-radius:999px;background:var(--hair)}
|
|
518
|
+
.story-level-0 .chapter-churn-bar i{display:block}
|
|
519
|
+
.story-level-0 .chapter-churn-bar .additions{width:calc(var(--add) * 1%);background:#79bd93}
|
|
520
|
+
.story-level-0 .chapter-churn-bar .deletions{width:calc(var(--delete) * 1%);background:#dd9d92}
|
|
521
|
+
.story-level-0 .map{animation:rise 240ms ease both}
|
|
522
|
+
.zoom-revealed{animation:story-enter 260ms ease both}
|
|
523
|
+
.story-level-1 .chapter-toggle{cursor:default}
|
|
524
|
+
.story-level-1 .chapter-toggle:hover{background:transparent}
|
|
525
|
+
.story-level-1 .chapter-detail{max-height:0!important;opacity:0!important;padding-top:0!important;padding-bottom:0!important;transform:translateY(-4px)!important}
|
|
526
|
+
.story-level-1 .chevron{display:none}
|
|
527
|
+
.story-level-2 .evidence-stack{display:none}
|
|
528
|
+
.story-level-3 .evidence pre{max-height:238px;overflow-y:auto}
|
|
529
|
+
.story-level-4 .focused-code,.story-level-4 .focused-label,.story-level-4 .evidence-context{display:none}
|
|
530
|
+
.story-level-4 .raw-code{display:block}
|
|
531
|
+
.story-level-4 .raw-label{display:inline}
|
|
532
|
+
|
|
533
|
+
.semantic{display:grid;grid-template-columns:1fr 1fr;gap:22px;padding:14px 0 16px;border-bottom:1px solid var(--hair);margin-bottom:14px}
|
|
534
|
+
.semantic>div+div{border-left:1px solid var(--hair);padding-left:22px}
|
|
535
|
+
.semantic strong{display:block;font:600 10px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}
|
|
536
|
+
.semantic p{margin:7px 0 0;font:400 13px/1.6 var(--sans);color:var(--ink-2)}
|
|
537
|
+
.evidence{border:1px solid var(--hair);border-radius:10px;overflow:hidden;margin-top:12px;background:var(--surface)}
|
|
538
|
+
.evidence header{display:flex;justify-content:space-between;align-items:baseline;gap:14px;padding:8px 12px;border-bottom:1px solid var(--hair);background:var(--well);font:400 11px/1.4 var(--mono);color:var(--ink-3)}
|
|
539
|
+
.evidence-path{font-weight:600;color:var(--ink-2);min-width:0;overflow-wrap:anywhere}
|
|
540
|
+
.evidence pre{margin:0;padding:8px 0;background:var(--surface);overflow-x:auto;-webkit-overflow-scrolling:touch;font:12px/1.6 var(--mono)}
|
|
541
|
+
.line{display:grid;grid-template-columns:40px 40px 1fr;min-width:max-content;padding:0 12px}
|
|
542
|
+
.line b{font-weight:400;font-size:11px;color:var(--faint);text-align:right;padding-right:10px;user-select:none;font-variant-numeric:tabular-nums}
|
|
543
|
+
.line code{font:inherit;color:#24292e}
|
|
544
|
+
.line.addition{background:var(--add-bg)}
|
|
545
|
+
.line.deletion{background:var(--del-bg)}
|
|
546
|
+
.diff-prefix{display:inline-block;width:1.2ch;color:var(--faint);user-select:none}
|
|
547
|
+
.line.addition .diff-prefix{color:var(--add-ink)}
|
|
548
|
+
.line.deletion .diff-prefix{color:var(--del-ink)}
|
|
549
|
+
.token-italic{font-style:italic}
|
|
550
|
+
.line.omission{display:block;min-width:0;padding:4px 12px}
|
|
551
|
+
.omission-divider{display:flex;align-items:center;gap:8px;color:var(--faint)}
|
|
552
|
+
.omission-divider i{height:0;flex:1;border-top:1px dashed var(--hair-2)}
|
|
553
|
+
.omission-divider b{font:600 12px/1 var(--mono);letter-spacing:-2px}
|
|
554
|
+
.evidence-context{margin:0;padding:6px 12px;border-top:1px solid var(--hair);background:var(--well);font-size:10.5px;color:var(--ink-3)}
|
|
555
|
+
.omitted{margin-top:16px;border:1px dashed var(--hair-2);border-radius:10px;padding:12px 14px;color:var(--ink-3);font-size:12px}
|
|
556
|
+
.omitted summary{cursor:pointer;color:var(--ink-2);font-weight:500}
|
|
557
|
+
.omitted p{margin:8px 0 0;line-height:1.5}
|
|
558
|
+
.omitted strong{color:var(--ink-2)}
|
|
559
|
+
.other-files{margin-top:16px;border:1px solid var(--hair);border-radius:12px;overflow:hidden;background:var(--surface)}
|
|
560
|
+
.other-files h2{margin:0;padding:11px 14px;border-bottom:1px solid var(--hair);background:var(--well);font:600 10px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}
|
|
561
|
+
.other-files ul{margin:0;padding:0;list-style:none}
|
|
562
|
+
.other-files li{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:38px;padding:9px 14px;font:12px/1.3 var(--mono);color:var(--ink-2)}
|
|
563
|
+
.other-files li>span{min-width:0;overflow-wrap:anywhere}
|
|
564
|
+
.other-files li+li{border-top:1px solid var(--hair)}
|
|
565
|
+
.other-files small{display:flex;gap:8px;font:600 11px/1 var(--mono);font-variant-numeric:tabular-nums}
|
|
566
|
+
.other-files .additions{color:var(--add-ink)}
|
|
567
|
+
.other-files .deletions{color:var(--del-ink)}
|
|
568
|
+
|
|
569
|
+
.section-title{margin:30px 0 6px;font:600 10px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3)}
|
|
570
|
+
.timeline{position:relative;margin-top:16px}
|
|
571
|
+
.timeline-rail{position:sticky;top:70px;z-index:10;display:grid;grid-template-columns:30px minmax(0,1fr) 30px;align-items:center;column-gap:8px;margin:0 0 16px;padding:9px 12px 7px;border:1px solid var(--hair);border-radius:12px;background:var(--surface)}
|
|
572
|
+
.rail-nav{display:grid;place-items:center;width:30px;height:30px;border:0;border-radius:8px;background:transparent;color:var(--ink-3);cursor:pointer;transition:background-color 140ms ease,color 140ms ease}
|
|
573
|
+
.rail-nav:hover{background:var(--well);color:var(--ink)}
|
|
574
|
+
.rail-nav[disabled]{opacity:.35;cursor:default}
|
|
575
|
+
.rail-nav[disabled]:hover{background:transparent;color:var(--ink-3)}
|
|
576
|
+
.rail-nav svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
|
577
|
+
.rail-ticks{position:relative;display:flex;align-items:flex-end;height:28px;min-width:0}
|
|
578
|
+
.rail-ticks::before{content:"";position:absolute;left:2px;right:2px;bottom:4px;height:1px;background:var(--hair-2)}
|
|
579
|
+
.rail-tick{position:relative;flex:1;min-width:4px;height:28px;padding:0;border:0;background:transparent;cursor:pointer}
|
|
580
|
+
.rail-tick::before{content:"";position:absolute;left:50%;bottom:4px;width:2px;height:9px;border-radius:1px 1px 0 0;background:var(--hair-2);transform:translateX(-50%);transition:background-color 160ms ease,height 200ms cubic-bezier(.3,1.3,.4,1),width 160ms ease}
|
|
581
|
+
.rail-tick.attention-low::before{background:var(--low)}
|
|
582
|
+
.rail-tick.attention-contained::before{background:var(--contained)}
|
|
583
|
+
.rail-tick.attention-elevated::before{background:var(--elevated)}
|
|
584
|
+
.rail-tick.attention-high::before{background:var(--high)}
|
|
585
|
+
.rail-tick.attention-critical::before{background:var(--critical)}
|
|
586
|
+
.rail-tick:hover::before{height:13px}
|
|
587
|
+
.rail-tick.active::before{background:var(--accent);height:17px;width:2.5px}
|
|
588
|
+
.rail-readout{grid-column:1/-1;display:flex;align-items:baseline;gap:10px;margin:4px 2px 0!important;max-width:none!important;min-width:0}
|
|
589
|
+
.rail-readout output{flex:none;font:700 10px/1 var(--mono);letter-spacing:.14em;color:var(--accent);font-variant-numeric:tabular-nums}
|
|
590
|
+
.rail-readout span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:500 11.5px/1.3 var(--sans);color:var(--ink-2)}
|
|
591
|
+
.timeline-plan{position:relative;display:none}
|
|
592
|
+
.timeline-plan::before{content:"";position:absolute;left:9px;top:26px;bottom:26px;width:1px;background:var(--hair)}
|
|
593
|
+
.timeline-plan-step{position:relative;display:grid;grid-template-columns:40px minmax(0,1fr);gap:12px;width:100%;padding:15px 2px 15px 30px;border:0;border-bottom:1px solid var(--hair);background:transparent;text-align:left;cursor:pointer}
|
|
594
|
+
.timeline-plan-step:last-child{border-bottom:0}
|
|
595
|
+
.timeline-plan-step::before{content:"";position:absolute;left:5.5px;top:21px;width:8px;height:8px;border-radius:50%;background:var(--low);box-shadow:0 0 0 3px var(--surface)}
|
|
596
|
+
.timeline-plan-step.attention-contained::before{background:var(--contained)}
|
|
597
|
+
.timeline-plan-step.attention-elevated::before{background:var(--elevated)}
|
|
598
|
+
.timeline-plan-step.attention-high::before{background:var(--high)}
|
|
599
|
+
.timeline-plan-step.attention-critical::before{background:var(--critical)}
|
|
600
|
+
.timeline-plan-step>span{padding-top:2px;font:700 12.5px/1 var(--mono);letter-spacing:.08em;color:var(--low)}
|
|
601
|
+
.timeline-plan-step.attention-contained>span{color:var(--contained)}
|
|
602
|
+
.timeline-plan-step.attention-elevated>span{color:var(--elevated)}
|
|
603
|
+
.timeline-plan-step.attention-high>span{color:var(--high)}
|
|
604
|
+
.timeline-plan-step.attention-critical>span{color:var(--critical)}
|
|
605
|
+
.timeline-plan-step strong{display:block;font:600 14.5px/1.4 var(--sans);color:var(--ink);transition:color 140ms ease}
|
|
606
|
+
.timeline-plan-step p{margin:3px 0 0}
|
|
607
|
+
.timeline-plan-step:hover strong{color:var(--accent)}
|
|
608
|
+
.timeline-state{display:none}
|
|
609
|
+
.timeline-state.active{display:block}
|
|
610
|
+
.timeline-card{display:grid;grid-template-columns:44px minmax(0,1fr);gap:12px;padding:16px 0;border-top:1px solid var(--hair);border-bottom:1px solid var(--hair)}
|
|
611
|
+
.timeline-card h2{margin:0 0 6px;font:600 16px/1.35 var(--sans);color:var(--ink)}
|
|
612
|
+
.timeline-index{font:700 12.5px/1 var(--mono);letter-spacing:.08em;color:var(--ink-3);padding-top:5px}
|
|
613
|
+
.timeline-index.attention-low{color:var(--low)}
|
|
614
|
+
.timeline-index.attention-contained{color:var(--contained)}
|
|
615
|
+
.timeline-index.attention-elevated{color:var(--elevated)}
|
|
616
|
+
.timeline-index.attention-high{color:var(--high)}
|
|
617
|
+
.timeline-index.attention-critical{color:var(--critical)}
|
|
618
|
+
.timeline p{margin:0;max-width:60em;font:400 12.5px/1.55 var(--sans);color:var(--ink-3)}
|
|
619
|
+
.timeline-summary,.timeline-explanation{border:1px solid var(--hair);border-radius:10px;background:var(--surface);padding:14px;margin-top:14px}
|
|
620
|
+
.timeline-summary strong,.timeline-explanation h3{display:block;margin:0 0 7px;font:700 10px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}
|
|
621
|
+
.timeline-summary p{color:var(--ink-2)}
|
|
622
|
+
.timeline-explanation{grid-template-columns:1fr 1fr;gap:18px}
|
|
623
|
+
.timeline-explanation section+section{border-left:1px solid var(--hair);padding-left:18px}
|
|
624
|
+
.timeline-explanation ul{margin:0;padding-left:18px;color:var(--ink-2)}
|
|
625
|
+
.timeline-explanation li{margin:4px 0;font-size:12.5px}
|
|
626
|
+
.timeline-explanation button,.timeline-chapter-links button{border:0;border-radius:7px;background:var(--well);min-height:24px;padding:4px 8px;color:var(--accent);font:600 11px/1 var(--sans);cursor:pointer}
|
|
627
|
+
.timeline-chapter-links{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
|
|
628
|
+
.timeline-evidence,.timeline-raw{margin-top:14px}
|
|
629
|
+
.timeline-evidence-divider{margin:20px 0 2px!important;font:600 10px/1 var(--mono)!important;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)!important}
|
|
630
|
+
.timeline-evidence-item{opacity:.62}
|
|
631
|
+
.timeline-evidence-item.current{opacity:1}
|
|
632
|
+
.timeline-evidence-item.current .evidence{border-color:#c9d5f5;box-shadow:0 0 0 1px #d9e3f8}
|
|
633
|
+
.timeline-summary,.timeline-explanation,.timeline-evidence,.timeline-raw{display:none}
|
|
634
|
+
.story-level-0 #timeline .timeline-rail{display:none}
|
|
635
|
+
.story-level-0 #timeline .timeline-plan{display:block}
|
|
636
|
+
.story-level-0 #timeline .timeline-states{display:none}
|
|
637
|
+
.story-level-1 #timeline .timeline-summary{display:block}
|
|
638
|
+
.story-level-2 #timeline .timeline-summary{display:block}
|
|
639
|
+
.story-level-2 #timeline .timeline-explanation{display:grid}
|
|
640
|
+
.story-level-3 #timeline .timeline-explanation{display:grid}
|
|
641
|
+
.story-level-3 #timeline .timeline-evidence{display:block}
|
|
642
|
+
.story-level-4 #timeline .timeline-raw{display:block}
|
|
643
|
+
.timeline-files{display:flex;flex-direction:column;gap:4px;margin-top:10px}
|
|
644
|
+
.timeline-file{display:flex;align-items:center;gap:12px;font:11px/1.4 var(--mono);color:var(--ink-2)}
|
|
645
|
+
.timeline-file>span{min-width:0;overflow-wrap:anywhere}
|
|
646
|
+
.timeline-file small{display:flex;gap:7px;font:600 10.5px/1 var(--mono)}
|
|
647
|
+
.timeline-file .additions{color:var(--add-ink)}
|
|
648
|
+
.timeline-file .deletions{color:var(--del-ink)}
|
|
649
|
+
|
|
650
|
+
.file{border:1px solid var(--hair);border-radius:12px;margin:12px 0;overflow:hidden;background:var(--surface)}
|
|
651
|
+
.file>summary{display:flex;align-items:center;gap:12px;padding:11px 14px;background:var(--well);cursor:pointer;list-style:none}
|
|
652
|
+
.file>summary::-webkit-details-marker{display:none}
|
|
653
|
+
.file-path{font:600 12px/1.4 var(--mono);color:var(--ink-2);min-width:0;overflow-wrap:anywhere}
|
|
654
|
+
.file>summary small{margin-left:auto;flex:none;color:var(--ink-3);font-size:11.5px}
|
|
655
|
+
.file>summary::after{content:"";width:7px;height:7px;flex:none;margin-top:-2px;border-right:1.6px solid var(--ink-3);border-bottom:1.6px solid var(--ink-3);transform:rotate(-45deg);transition:transform 180ms ease}
|
|
656
|
+
.file[open]>summary::after{margin-top:-4px;transform:rotate(45deg)}
|
|
657
|
+
.diff-block{border-top:1px solid var(--hair)}
|
|
658
|
+
.diff-hunk-header{padding:6px 12px;background:var(--well);border-bottom:1px solid var(--hair);font:11px/1.4 var(--mono);color:var(--ink-3)}
|
|
659
|
+
.diff-block pre{margin:0;padding:8px 0;overflow-x:auto;-webkit-overflow-scrolling:touch;font:12px/1.6 var(--mono)}
|
|
660
|
+
|
|
661
|
+
.test-plan-subtitle,.view-subtitle{margin:8px 0 18px;max-width:56em;font:400 13px/1.6 var(--sans);color:var(--ink-2)}
|
|
662
|
+
.test-plan-summary{display:flex;flex-wrap:wrap;gap:7px;margin:0 0 18px}
|
|
663
|
+
.test-plan-summary span,.test-inline-meta span,.test-theme-meta span,.test-refs span,.test-state{display:inline-flex;align-items:center;min-height:24px;padding:3px 10px;border:1px solid var(--hair);border-radius:999px;background:var(--surface);color:var(--ink-2);font:500 11px/1.2 var(--sans);font-variant-numeric:tabular-nums}
|
|
664
|
+
.test-plan-level{display:none}
|
|
665
|
+
.story-level-0 #tests .test-plan-map,.story-level-1 #tests .test-plan-summary-level,.story-level-2 #tests .test-plan-explanation,.story-level-3 #tests .test-plan-evidence,.story-level-4 #tests .test-plan-raw{display:block}
|
|
666
|
+
.test-generated-summary{margin:0 0 14px;max-width:56em;font:400 13px/1.6 var(--sans);color:var(--ink-2)}
|
|
667
|
+
.test-theme-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}
|
|
668
|
+
.test-theme-card{display:flex;flex-direction:column;min-height:128px;border:1px solid var(--hair);border-radius:12px;background:var(--surface);padding:16px}
|
|
669
|
+
.test-theme-card h2{margin:0 0 6px;font:600 14px/1.4 var(--sans);color:var(--ink)}
|
|
670
|
+
.test-theme-card p{margin:0;font:400 12.5px/1.5 var(--sans);color:var(--ink-3)}
|
|
671
|
+
.test-theme-meta{display:flex;flex-wrap:wrap;gap:6px;margin-top:auto;padding-top:14px}
|
|
672
|
+
.test-behavior-group,.test-explanation-group,.test-raw-section{border:1px solid var(--hair);border-radius:12px;background:var(--surface);margin:0 0 14px;padding:16px}
|
|
673
|
+
.test-behavior-group h2,.test-explanation-group h2{margin:0 0 8px;font:600 14px/1.4 var(--sans);color:var(--ink)}
|
|
674
|
+
.test-raw-section>h2{margin:0 0 8px;font:600 10px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}
|
|
675
|
+
.test-behavior{border-top:1px solid var(--hair);padding:12px 0}
|
|
676
|
+
.test-behavior:first-of-type{border-top:0}
|
|
677
|
+
.test-behavior button{display:grid;gap:4px;width:100%;border:0;background:transparent;padding:0;text-align:left;cursor:pointer}
|
|
678
|
+
.test-behavior button:hover strong{color:var(--accent)}
|
|
679
|
+
.test-behavior strong,.test-evidence-card summary strong{font:600 13.5px/1.4 var(--sans);color:var(--ink);transition:color 140ms ease}
|
|
680
|
+
.test-behavior button span,.test-evidence-card summary small{font:400 12px/1.5 var(--sans);color:var(--ink-3)}
|
|
681
|
+
.test-inline-meta{display:flex;flex-wrap:wrap;gap:6px;margin-top:9px}
|
|
682
|
+
.test-explanation-card{border-top:1px solid var(--hair);padding:14px 0}
|
|
683
|
+
.test-explanation-card:first-of-type{border-top:0}
|
|
684
|
+
.test-explanation-card h3{margin:0 0 10px;font:600 13.5px/1.4 var(--sans);color:var(--ink)}
|
|
685
|
+
.test-explanation-card .semantic{margin:0 0 12px;padding-top:0}
|
|
686
|
+
.test-what{margin:0 0 12px;max-width:56em;font:400 13px/1.6 var(--sans);color:var(--ink-2)}
|
|
687
|
+
.test-refs{display:flex;flex-wrap:wrap;gap:6px}
|
|
688
|
+
.test-ref-file{font-family:var(--mono);font-size:10.5px;min-width:0;overflow-wrap:anywhere}
|
|
689
|
+
.test-evidence-card{border:1px solid var(--hair);border-radius:12px;background:var(--surface);margin:0 0 12px;overflow:hidden}
|
|
690
|
+
.test-evidence-card summary{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;cursor:pointer;background:var(--well);list-style:none}
|
|
691
|
+
.test-evidence-card summary::-webkit-details-marker{display:none}
|
|
692
|
+
.test-evidence-card summary>span:first-child{display:grid;gap:3px;min-width:0}
|
|
693
|
+
.test-evidence-card .evidence{border-left:0;border-right:0;border-bottom:0;border-radius:0;margin:0}
|
|
694
|
+
.test-evidence-meta{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;padding:12px 14px;border-top:1px solid var(--hair)}
|
|
695
|
+
.test-evidence-meta div{display:grid;gap:3px}
|
|
696
|
+
.test-evidence-meta strong{font:600 10px/1.4 var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3)}
|
|
697
|
+
.test-evidence-meta span{font-size:12px;color:var(--ink-2)}
|
|
698
|
+
.test-raw-artifact{margin:12px 0 0}
|
|
699
|
+
.test-raw-artifact h2{margin:0 0 8px;font:600 12px/1.4 var(--mono);color:var(--ink-2);overflow-wrap:anywhere}
|
|
700
|
+
.test-raw-section .empty-note{margin:4px 0 0}
|
|
701
|
+
.test-run{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;margin:8px 0;font:12.5px/1.5 var(--sans);color:var(--ink-2)}
|
|
702
|
+
.test-run code{font:11.5px var(--mono);color:var(--ink)}
|
|
703
|
+
.test-run p{margin:0;flex-basis:100%}
|
|
704
|
+
.test-run small{color:var(--ink-3)}
|
|
705
|
+
.run-outcome{font:600 11px/1 var(--sans);text-transform:uppercase;letter-spacing:.04em}
|
|
706
|
+
.run-outcome-passed{color:var(--low)}
|
|
707
|
+
.run-outcome-failed{color:var(--critical)}
|
|
708
|
+
.run-outcome-mixed,.run-outcome-unknown{color:var(--elevated)}
|
|
709
|
+
.empty-note{font:400 12.5px/1.5 var(--sans);color:var(--ink-3)}
|
|
710
|
+
|
|
711
|
+
.inspector{position:sticky;top:0;height:100vh;overflow-y:auto;background:var(--rail);border-left:1px solid var(--hair);padding:22px 20px}
|
|
712
|
+
.inspector-header{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:30px}
|
|
713
|
+
.inspector h2{margin:0;font:600 13px/1.2 var(--sans);color:var(--ink)}
|
|
714
|
+
.inspector-content{min-width:0}
|
|
715
|
+
.inspector section{padding:18px 0;border-bottom:1px solid var(--hair)}
|
|
716
|
+
.inspector section:last-of-type{border-bottom:0}
|
|
717
|
+
.inspector h3{margin:0 0 10px;font:600 10px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}
|
|
718
|
+
.stat-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:5px 0;font-size:12.5px;color:var(--ink-2);font-variant-numeric:tabular-nums}
|
|
719
|
+
.stat-row>span{display:inline-flex;align-items:center;gap:8px;min-width:0}
|
|
720
|
+
.stat-category>span{text-transform:capitalize}
|
|
721
|
+
.stat-row strong{font:600 12px/1.4 var(--mono);color:var(--ink)}
|
|
722
|
+
.stat-row .tag-icon{width:14px;height:14px;flex:none;color:var(--ink-3)}
|
|
723
|
+
.inspector-action{display:flex;align-items:center;gap:9px;width:100%;min-height:36px;margin-top:8px;padding:8px 11px;border:0;border-radius:8px;background:#ecece9;color:var(--ink);font:500 12.5px/1.2 var(--sans);cursor:pointer;transition:background-color 140ms ease}
|
|
724
|
+
.inspector-action:first-of-type{margin-top:0}
|
|
725
|
+
.inspector-action:hover{background:#e2e2de}
|
|
726
|
+
.inspector-action svg{width:15px;height:15px;flex:none;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;color:var(--ink-3)}
|
|
727
|
+
.inspector-collapsed .inspector{padding:22px 10px}
|
|
728
|
+
.inspector-collapsed .inspector-header{justify-content:center}
|
|
729
|
+
.inspector-collapsed .inspector h2,.inspector-collapsed .inspector-content{display:none}
|
|
730
|
+
.inspector-collapsed .collapse-inspector{transform:rotate(180deg)}
|
|
731
|
+
|
|
732
|
+
.colophon{display:flex;align-items:center;gap:9px;margin-top:56px;padding-top:16px;border-top:1px solid var(--hair);font:600 10px/1.6 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}
|
|
733
|
+
.colophon svg{width:12px;height:13px;flex:none;color:var(--faint)}
|
|
734
|
+
.colophon svg path{fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
|
|
735
|
+
.colophon svg .mark-deep{stroke:var(--accent)}
|
|
736
|
+
.selection-menu{position:fixed;z-index:30;display:flex;gap:2px;max-width:calc(100vw - 16px);overflow-x:auto;padding:4px;border:1px solid var(--hair);border-radius:11px;background:var(--surface);box-shadow:var(--shadow)}
|
|
737
|
+
.selection-menu[hidden]{display:none!important}
|
|
738
|
+
.selection-menu button{border:0;background:transparent;min-height:36px;padding:8px 11px;border-radius:8px;color:var(--ink-2);font:500 12px/1.2 var(--sans);cursor:pointer;white-space:nowrap;transition:background-color 120ms ease,color 120ms ease}
|
|
739
|
+
.selection-menu button:hover{background:var(--wash);color:var(--accent)}
|
|
740
|
+
.md-code{font:500 .92em var(--mono);letter-spacing:0;background:var(--well);border-radius:4px;padding:1px 4px;color:var(--ink)}
|
|
741
|
+
.toast{position:fixed;right:20px;bottom:20px;z-index:40;max-width:calc(100vw - 32px);background:#22262b;color:#f5f5f3;border-radius:9px;padding:11px 14px;font:500 12.5px/1.4 var(--sans);box-shadow:var(--shadow);animation:rise 200ms ease}
|
|
742
|
+
|
|
743
|
+
@keyframes story-enter{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
744
|
+
@keyframes rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
|
745
|
+
@keyframes view-in{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
|
|
746
|
+
@keyframes sheet-in{from{opacity:0;transform:translateY(28px)}to{opacity:1;transform:none}}
|
|
747
|
+
@keyframes fade-in{from{opacity:0}to{opacity:1}}
|
|
748
|
+
|
|
749
|
+
@media(max-width:1160px){.app-shell{grid-template-columns:204px minmax(0,1fr) 236px}.app-shell.sidebar-collapsed{grid-template-columns:64px minmax(0,1fr) 236px}.app-shell.inspector-collapsed{grid-template-columns:204px minmax(0,1fr) 64px}.app-shell.sidebar-collapsed.inspector-collapsed{grid-template-columns:64px minmax(0,1fr) 64px}.main{padding:32px 28px 72px}.view-bar::before{left:-28px;right:-28px}.story-level-0 .chapter-list,.test-theme-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
750
|
+
@media(max-width:980px){.test-evidence-meta{grid-template-columns:1fr}}
|
|
751
|
+
@media(max-width:1080px){
|
|
752
|
+
.app-shell,.app-shell.inspector-collapsed{grid-template-columns:204px minmax(0,1fr)}
|
|
753
|
+
.app-shell.sidebar-collapsed,.app-shell.sidebar-collapsed.inspector-collapsed{grid-template-columns:64px minmax(0,1fr)}
|
|
754
|
+
.mobile-inspector-toggle{display:grid;margin-left:6px}
|
|
755
|
+
.inspector{display:none}
|
|
756
|
+
.app-shell.mobile-inspector-open::before{content:"";position:fixed;inset:0;z-index:35;background:rgba(17,21,28,.32);animation:fade-in 220ms ease both}
|
|
757
|
+
.app-shell.mobile-inspector-open .inspector{display:block;position:fixed;z-index:40;inset:auto 10px calc(10px + env(safe-area-inset-bottom)) 10px;height:auto;max-height:min(76vh,600px);overflow:auto;border:1px solid var(--hair);border-radius:14px;padding:18px;background:var(--surface);box-shadow:var(--shadow);animation:sheet-in 300ms cubic-bezier(.2,.9,.25,1) both}
|
|
758
|
+
.app-shell.mobile-inspector-open .inspector-content{display:block}
|
|
759
|
+
.app-shell.mobile-inspector-open .inspector h2{display:block}
|
|
760
|
+
.app-shell.mobile-inspector-open .collapse-inspector{transform:rotate(180deg)}
|
|
761
|
+
.inspector-collapsed .inspector{padding:18px}
|
|
762
|
+
.inspector-collapsed .inspector h2,.inspector-collapsed .inspector-content{display:block}
|
|
763
|
+
.inspector-collapsed .inspector-header{justify-content:space-between}
|
|
764
|
+
}
|
|
765
|
+
@media(max-width:760px){
|
|
766
|
+
.app-shell,.app-shell.sidebar-collapsed,.app-shell.inspector-collapsed,.app-shell.sidebar-collapsed.inspector-collapsed{display:block}
|
|
767
|
+
.sidebar,.sidebar.collapsed{position:sticky;top:0;z-index:20;width:auto;height:auto;padding:8px 14px 0;background:var(--surface);border-right:0;border-bottom:1px solid var(--hair);overflow:visible}
|
|
768
|
+
.brand,.sidebar.collapsed .brand{flex-direction:row;gap:9px;min-height:44px;padding:2px 0 8px}
|
|
769
|
+
.brand-name,.sidebar.collapsed .brand-name{display:inline}
|
|
770
|
+
.collapse-sidebar,.sidebar.collapsed .collapse-sidebar{margin:0 0 0 auto}
|
|
771
|
+
.collapse-sidebar{transform:rotate(90deg)}
|
|
772
|
+
.sidebar.collapsed .collapse-sidebar{transform:rotate(-90deg)}
|
|
773
|
+
.mobile-inspector-toggle{display:grid;margin-left:6px}
|
|
774
|
+
.sidebar nav,.sidebar.collapsed nav{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:0;margin:0 -14px;border-top:1px solid var(--hair);overflow:hidden;max-height:64px;transition:max-height 200ms ease,border-color 200ms ease}
|
|
775
|
+
.sidebar.collapsed nav{max-height:0;border-top-color:transparent}
|
|
776
|
+
.nav-item,.sidebar.collapsed .nav-item{flex-direction:column;justify-content:center;gap:5px;width:auto;min-height:56px;padding:8px 4px;border-radius:0;font:500 11px/1 var(--sans);color:var(--ink-3)}
|
|
777
|
+
.nav-icon svg{width:19px;height:19px}
|
|
778
|
+
.nav-item.active,.sidebar.collapsed .nav-item.active{color:var(--accent);background:transparent;box-shadow:inset 0 -2px var(--accent)}
|
|
779
|
+
.nav-item.active::before{display:none}
|
|
780
|
+
.main{padding:22px 16px 150px}
|
|
781
|
+
.page-header{display:block;padding:0 0 16px}
|
|
782
|
+
.page-header h1{font-size:17px}
|
|
783
|
+
.breadcrumbs{gap:10px;font-size:11.5px}
|
|
784
|
+
.view-bar{position:fixed;left:12px;right:12px;top:auto;bottom:calc(10px + env(safe-area-inset-bottom));z-index:25;display:flex;flex-wrap:wrap;gap:0;height:auto;margin:0;padding:4px 10px 6px;background:var(--surface);border:1px solid var(--hair);border-radius:16px;box-shadow:var(--shadow);pointer-events:auto}
|
|
785
|
+
.view-bar::before{display:none}
|
|
786
|
+
.view-bar .story-zoom-controls{position:relative;top:0;right:auto}
|
|
787
|
+
.view-bar-ref{display:none}
|
|
788
|
+
.view-bar.view-bar-empty{display:none}
|
|
789
|
+
.story-zoom-controls{flex:1;width:100%;height:66px;padding-top:0}
|
|
790
|
+
.zoom{flex:1;width:auto;height:46px;padding:0 6px}
|
|
791
|
+
.zoom button{width:40px;height:46px}
|
|
792
|
+
.zoom::before{left:26px;right:26px;bottom:10px}
|
|
793
|
+
.zoom button::before{bottom:10px}
|
|
794
|
+
.zoom button::after{bottom:32px}
|
|
795
|
+
.story-zoom-controls>button[data-zoom-step]{width:40px;height:46px;font-size:19px}
|
|
796
|
+
.zoom-callout{top:calc(100% - 1px)}
|
|
797
|
+
.review-summary{margin-top:22px;font-size:17px}
|
|
798
|
+
.story-toolbar{flex-wrap:wrap}
|
|
799
|
+
.chapter-toggle{grid-template-columns:38px minmax(0,1fr) 16px;gap:10px;padding:14px}
|
|
800
|
+
.chapter-number{font-size:16px}
|
|
801
|
+
.chapter-detail{padding:0 14px}
|
|
802
|
+
.chapter.open .chapter-detail{padding-top:2px;padding-bottom:18px}
|
|
803
|
+
.semantic{grid-template-columns:1fr;gap:12px}
|
|
804
|
+
.semantic>div+div{border-left:0;border-top:1px solid var(--hair);padding-left:0;padding-top:12px}
|
|
805
|
+
.evidence header{flex-direction:column;align-items:flex-start;gap:3px}
|
|
806
|
+
.line{grid-template-columns:34px 34px 1fr;padding:0 8px}
|
|
807
|
+
.line.omission{padding:4px 8px}
|
|
808
|
+
.file>summary{flex-wrap:wrap}
|
|
809
|
+
.story-level-0 .chapter-list{grid-template-columns:1fr;gap:10px}
|
|
810
|
+
.story-level-0 .chapter-toggle{min-height:0}
|
|
811
|
+
.story-level-0 .chapter-copy{min-height:0}
|
|
812
|
+
.test-theme-grid{grid-template-columns:1fr}
|
|
813
|
+
.test-evidence-card summary{flex-direction:column;align-items:flex-start}
|
|
814
|
+
.timeline-rail{top:8px;grid-template-columns:34px minmax(0,1fr) 34px;padding:8px 8px 6px}
|
|
815
|
+
.rail-nav{width:34px;height:34px}
|
|
816
|
+
.timeline-explanation{grid-template-columns:1fr}
|
|
817
|
+
.story-level-2 #timeline .timeline-explanation,.story-level-3 #timeline .timeline-explanation{grid-template-columns:1fr}
|
|
818
|
+
.timeline-explanation section+section{border-left:0;border-top:1px solid var(--hair);padding-left:0;padding-top:14px}
|
|
819
|
+
.timeline-card{grid-template-columns:36px minmax(0,1fr)}
|
|
820
|
+
.selection-menu{left:8px!important;right:8px;top:auto!important;bottom:calc(94px + env(safe-area-inset-bottom))}
|
|
821
|
+
.toast{left:16px;right:16px;bottom:calc(98px + env(safe-area-inset-bottom));text-align:center}
|
|
822
|
+
}
|
|
823
|
+
@media(max-width:380px){.main{padding-left:12px;padding-right:12px}.breadcrumbs code{font-size:10.5px}.nav-item,.sidebar.collapsed .nav-item{font-size:10px}}
|
|
824
|
+
@media(prefers-color-scheme:dark){
|
|
825
|
+
:root{--surface:#15181c;--rail:#1b1f24;--well:#191d22;--ink:#e7eaed;--ink-2:#b4bbc2;--ink-3:#868f97;--faint:#7c858e;--hair:#272c33;--hair-2:#3a414a;--accent:#7b9ce8;--wash:#1c2536;--low:#46a86c;--contained:#6cb6e6;--elevated:#cfa145;--high:#dd8a5b;--critical:#e07268;--add-ink:#5cb883;--add-bg:#152b1d;--del-ink:#e08a7a;--del-bg:#2f1b17;--shadow:0 18px 44px #00000059}
|
|
826
|
+
::selection{background:#2c3e66}
|
|
827
|
+
.panel-toggle:hover{background:#262c33}
|
|
828
|
+
.nav-item:hover{background:#232930}
|
|
829
|
+
.line code{color:#c9d1d9}
|
|
830
|
+
.line code .tk{color:var(--shiki-dark,currentColor)!important}
|
|
831
|
+
.chapter-churn-bar .additions{background:#3f7f57}
|
|
832
|
+
.chapter-churn-bar .deletions{background:#96584e}
|
|
833
|
+
.inspector-action{background:#232930}
|
|
834
|
+
.inspector-action:hover{background:#2b323a}
|
|
835
|
+
.toast{border:1px solid #3a414a}
|
|
836
|
+
.app-shell.mobile-inspector-open::before{background:rgba(0,0,0,.5)}
|
|
837
|
+
}
|
|
838
|
+
@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}
|
|
839
|
+
`;
|
|
840
|
+
export const artifactClientScript = `
|
|
841
|
+
(() => {
|
|
842
|
+
const byId = (id) => document.getElementById(id);
|
|
843
|
+
const agentName = document.body.dataset.agent || 'your agent';
|
|
844
|
+
let toastTimer = 0;
|
|
845
|
+
const toast = (message) => { const node = byId('toast'); node.textContent = message; node.hidden = false; window.clearTimeout(toastTimer); toastTimer = window.setTimeout(() => { node.hidden = true; }, 3600); };
|
|
846
|
+
let selectedText = '';
|
|
847
|
+
let selectedPath = '';
|
|
848
|
+
const currentStoryLevel = () => Number(document.body.dataset.storyLevel ?? 1);
|
|
849
|
+
const setActiveView = (view, viewButton) => { document.querySelectorAll('[data-view]').forEach((node) => node.classList.toggle('active', node === viewButton)); document.querySelectorAll('.view').forEach((node) => node.classList.toggle('active', node.id === view)); const zoomControls = document.querySelector('.story-zoom-controls'); if (zoomControls) zoomControls.hidden = view !== 'trailer' && view !== 'timeline' && view !== 'tests'; };
|
|
850
|
+
document.addEventListener('click', (event) => {
|
|
851
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
852
|
+
if (!target) return;
|
|
853
|
+
const viewButton = target.closest('[data-view]');
|
|
854
|
+
if (viewButton) { setActiveView(viewButton.getAttribute('data-view'), viewButton); return; }
|
|
855
|
+
const chapterButton = target.closest('.chapter-toggle');
|
|
856
|
+
// Step chips inside the toggle navigate to the Timeline; they must not also flip the chapter.
|
|
857
|
+
if (chapterButton && !target.closest('.step-chip')) { if (currentStoryLevel() <= 1) return; const chapter = chapterButton.closest('.chapter'); openChapter(chapter, !chapter.classList.contains('open')); return; }
|
|
858
|
+
const askButton = target.closest('[data-question], [data-action="ask"]');
|
|
859
|
+
if (askButton) { const menu = byId('selection-menu'); menu.hidden = true; delete menu.dataset.pressed; if (!selectedText) return; const question = askButton.getAttribute('data-question'); if (question) copyPrompt(selectionPrompt(question), 'Prompt copied. Paste it into ' + agentName + ' to continue.'); else copyPrompt(selectionPrompt(), 'Selection copied. Paste it into ' + agentName + ' and add your question.'); return; }
|
|
860
|
+
const action = target.closest('[data-action]')?.getAttribute('data-action');
|
|
861
|
+
if (action === 'export') { downloadReview(); toast('Review exported as an HTML file.'); return; }
|
|
862
|
+
if (action === 'copy-summary') copyPrompt(summaryPrompt(), 'Summary prompt copied for ' + agentName + '.');
|
|
863
|
+
});
|
|
864
|
+
// Elements that carry role=button (the chapter toggles) must react to Enter and Space like real buttons.
|
|
865
|
+
document.addEventListener('keydown', (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; const target = event.target instanceof Element ? event.target.closest('[role="button"][tabindex]') : null; if (!target) return; event.preventDefault(); target.click(); });
|
|
866
|
+
document.addEventListener('selectionchange', () => { const menu = byId('selection-menu'); if (document.body.dataset.selectingPointer === 'true') { if (menu.dataset.pressed !== 'true') menu.hidden = true; return; } const selection = window.getSelection(); const anchor = selection && !selection.isCollapsed ? selection.anchorNode : null; const anchorElement = anchor ? (anchor instanceof Element ? anchor : anchor.parentElement) : null; const source = anchorElement ? anchorElement.closest('.evidence, .full-diff-file') : null; const text = source ? selection.toString().trim() : ''; if (!text) { if (menu.dataset.pressed !== 'true') menu.hidden = true; return; } selectedText = text; selectedPath = source.querySelector('.evidence-path, .file-path')?.textContent?.trim() || ''; const rect = selection.getRangeAt(0).getBoundingClientRect(); menu.style.left = Math.max(8, rect.left) + 'px'; menu.style.top = Math.max(8, rect.top - 42) + 'px'; menu.hidden = false; });
|
|
867
|
+
function openChapter(chapter, open) { if (!chapter) return; chapter.classList.toggle('open', open); chapter.querySelector('.chapter-detail').hidden = !open; chapter.querySelector('.chapter-toggle').setAttribute('aria-expanded', String(open)); }
|
|
868
|
+
function downloadReview() { const blob = new Blob(['<!doctype html>\\n' + document.documentElement.outerHTML], { type: 'text/html' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = document.title.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase() + '.html'; document.body.append(link); link.click(); link.remove(); window.setTimeout(() => URL.revokeObjectURL(link.href), 1000); }
|
|
869
|
+
function copyPrompt(text, successMessage) { const write = navigator.clipboard?.writeText?.(text); if (write && typeof write.then === 'function') { write.then(() => toast(successMessage)).catch(() => showManualCopy(text)); return; } showManualCopy(text); }
|
|
870
|
+
function showManualCopy(text) { window.prompt('Copy this prompt for ' + agentName + ':', text); toast('Copy prompt shown for ' + agentName + '.'); }
|
|
871
|
+
function summaryPrompt() { const story = document.querySelector('#trailer')?.innerText?.trim() || document.querySelector('.main')?.innerText?.trim() || document.title; return 'Use this ndrstnd review summary to help me understand the implementation, decisions, risks, and tests.\\n\\n' + story; }
|
|
872
|
+
function selectionPrompt(question) { const subject = document.title.replace(/^ndrstnd · /, ''); const context = 'Context: ndrstnd review of ' + subject + (selectedPath ? '; selected excerpt from ' + selectedPath : '') + '.\\n\\nSelected lines:\\n' + selectedText; return question ? question + '\\n\\n' + context : context + '\\n\\nMy question: '; }
|
|
873
|
+
})();
|
|
874
|
+
`;
|
|
875
|
+
export const portableEnhancements = `
|
|
876
|
+
(() => {
|
|
877
|
+
const byId = (id) => document.getElementById(id);
|
|
878
|
+
// Analysis-supplied ids (chapters, steps) may contain selector metacharacters; every runtime selector interpolation goes through this.
|
|
879
|
+
const cssAttr = (value) => window.CSS && CSS.escape ? CSS.escape(String(value)) : String(value).replace(/["\\\\]/g, '\\\\$&');
|
|
880
|
+
const storyLevels = [{ name: 'Map', description: 'Themes and risk distribution' }, { name: 'Summary', description: 'Story claims and summaries' }, { name: 'Explanation', description: 'Before and after meaning' }, { name: 'Evidence', description: 'Focused code excerpts' }, { name: 'Raw', description: 'Complete change evidence' }];
|
|
881
|
+
const timelineLevels = [{ name: 'Map', description: 'Complete build path' }, { name: 'Summary', description: 'Goals and postconditions' }, { name: 'Explanation', description: 'Deferred concerns and forward references' }, { name: 'Evidence', description: 'Cumulative evidence by step' }, { name: 'Raw', description: 'Cumulative patch through this step' }];
|
|
882
|
+
const testLevels = [{ name: 'Map', description: 'Change themes and test activity' }, { name: 'Summary', description: 'Tested behaviors' }, { name: 'Explanation', description: 'Behavior meaning' }, { name: 'Evidence', description: 'Test cases and excerpts' }, { name: 'Raw', description: 'Complete test artifacts' }];
|
|
883
|
+
const activeView = () => document.querySelector('[data-view].active')?.getAttribute('data-view') || 'trailer';
|
|
884
|
+
const levelsForView = () => activeView() === 'tests' ? testLevels : activeView() === 'timeline' ? timelineLevels : storyLevels;
|
|
885
|
+
const updateZoomLabel = (level) => {
|
|
886
|
+
const levels = levelsForView();
|
|
887
|
+
const label = document.getElementById('zoom-label');
|
|
888
|
+
if (label) label.textContent = levels[level].name;
|
|
889
|
+
const description = document.getElementById('zoom-description');
|
|
890
|
+
if (description) description.textContent = levels[level].description;
|
|
891
|
+
};
|
|
892
|
+
const ensureRawEvidence = (article) => {
|
|
893
|
+
if (!article || article.querySelector('.raw-code')) return;
|
|
894
|
+
const source = document.querySelector('#diff [data-diff-hunk="' + cssAttr(article.getAttribute('data-evidence-id')) + '"] pre');
|
|
895
|
+
if (!source) return;
|
|
896
|
+
const raw = document.createElement('pre');
|
|
897
|
+
raw.className = 'raw-code';
|
|
898
|
+
raw.innerHTML = source.innerHTML;
|
|
899
|
+
article.appendChild(raw);
|
|
900
|
+
};
|
|
901
|
+
const materializeTestSlots = () => {
|
|
902
|
+
const templateFor = (id) => [...document.querySelectorAll('[data-test-excerpt-template]')].find((node) => node.getAttribute('data-test-excerpt-template') === id);
|
|
903
|
+
document.querySelectorAll('.test-excerpt-slot:not([data-materialized])').forEach((slot) => { const template = templateFor(slot.getAttribute('data-test-hunk')); if (!template) return; slot.appendChild(template.content.cloneNode(true)); slot.dataset.materialized = 'true'; });
|
|
904
|
+
document.querySelectorAll('.test-raw-slot:not([data-materialized])').forEach((slot) => { const id = slot.getAttribute('data-test-hunk'); const block = [...document.querySelectorAll('#diff [data-diff-hunk]')].find((node) => node.getAttribute('data-diff-hunk') === id); const source = block ? block.querySelector('pre') : null; if (!source) return; const copy = document.createElement('pre'); copy.innerHTML = source.innerHTML; slot.appendChild(copy); slot.dataset.materialized = 'true'; });
|
|
905
|
+
};
|
|
906
|
+
const materializeEvidenceStack = (stack) => {
|
|
907
|
+
if (!stack || stack.dataset.materialized === 'true' || stack.dataset.evidenceList === undefined) return;
|
|
908
|
+
stack.dataset.evidenceList.split(' ').filter(Boolean).forEach((id) => { const template = document.querySelector('[data-evidence-template="' + cssAttr(id) + '"]'); if (template) stack.appendChild(template.content.cloneNode(true)); });
|
|
909
|
+
stack.dataset.materialized = 'true';
|
|
910
|
+
if (Number(document.body.dataset.storyLevel ?? 1) >= 4) stack.querySelectorAll('.evidence[data-evidence-id]').forEach((article) => ensureRawEvidence(article));
|
|
911
|
+
};
|
|
912
|
+
const setChapterDetail = (chapter, expanded, animate) => { const detail = chapter.querySelector('.chapter-detail'); if (!detail) return; chapter.querySelector('.chapter-toggle')?.setAttribute('aria-expanded', String(expanded)); window.clearTimeout(Number(detail.dataset.zoomCollapseTimer)); if (expanded) { detail.hidden = false; materializeEvidenceStack(detail.querySelector('.evidence-stack')); if (animate) void detail.offsetHeight; chapter.classList.add('open'); detail.classList.toggle('zoom-revealed', animate); return; } chapter.classList.remove('open'); detail.classList.remove('zoom-revealed'); if (!animate) { detail.hidden = true; return; } detail.dataset.zoomCollapseTimer = String(window.setTimeout(() => { if (!chapter.classList.contains('open')) detail.hidden = true; }, 260)); };
|
|
913
|
+
const setZoom = (level) => { level = Math.max(0, Math.min(4, level)); const current = Number(document.body.dataset.storyLevel ?? 1); const changed = current !== level; const zoom = document.getElementById('zoom-control'); zoom?.classList.toggle('is-changing', changed); document.body.dataset.storyLevel = String(level); document.body.className = document.body.className.replace(/story-level-\\d/g, '') + ' story-level-' + level; document.querySelectorAll('[data-zoom]').forEach((button) => { const active = Number(button.dataset.zoom) === level; button.classList.toggle('active', active); button.setAttribute('aria-pressed', String(active)); }); updateZoomLabel(level); const map = document.getElementById('map'); if (map) map.hidden = level !== 0; document.querySelectorAll('.chapter').forEach((chapter) => setChapterDetail(chapter, level >= 2, changed)); if (level >= 3) materializeTestSlots(); if (level >= 4) document.querySelectorAll('.evidence[data-evidence-id]').forEach((article) => ensureRawEvidence(article)); if (changed) window.setTimeout(() => zoom?.classList.remove('is-changing'), 260); };
|
|
914
|
+
setZoom(1);
|
|
915
|
+
const setZoomControlsVisible = (view) => { const hidden = view !== 'trailer' && view !== 'timeline' && view !== 'tests'; const controls = document.querySelector('.story-zoom-controls'); if (controls) controls.hidden = hidden; document.querySelector('.view-bar')?.classList.toggle('view-bar-empty', hidden); updateZoomLabel(Number(document.body.dataset.storyLevel ?? 1)); };
|
|
916
|
+
setZoomControlsVisible(document.querySelector('[data-view].active')?.getAttribute('data-view') || 'trailer');
|
|
917
|
+
const materializeTimelineState = (state) => {
|
|
918
|
+
if (!state || state.dataset.materialized === 'true') return;
|
|
919
|
+
const evidence = state.querySelector('.timeline-evidence');
|
|
920
|
+
if (!evidence || evidence.dataset.currentEvidence === undefined) return;
|
|
921
|
+
const current = evidence.dataset.currentEvidence.split(' ').filter(Boolean);
|
|
922
|
+
// Prior evidence is the union of earlier steps' own evidence; deriving it here keeps the attributes O(hunks) instead of O(steps x hunks).
|
|
923
|
+
const states = [...document.querySelectorAll('.timeline-state')];
|
|
924
|
+
const prior = states.slice(0, states.indexOf(state)).flatMap((previous) => { const container = previous.querySelector('.timeline-evidence'); return container && container.dataset.currentEvidence ? container.dataset.currentEvidence.split(' ') : []; }).filter(Boolean);
|
|
925
|
+
const append = (id, isCurrent) => { const template = document.querySelector('[data-evidence-template="' + cssAttr(id) + '"]'); if (!template) return; const item = document.createElement('div'); item.className = 'timeline-evidence-item' + (isCurrent ? ' current' : ''); item.setAttribute('data-evidence-id', id); item.appendChild(template.content.cloneNode(true)); evidence.appendChild(item); };
|
|
926
|
+
current.forEach((id) => append(id, true));
|
|
927
|
+
if (prior.length > 0) { const divider = document.createElement('p'); divider.className = 'timeline-evidence-divider'; divider.textContent = 'Already in place from earlier steps'; evidence.appendChild(divider); prior.forEach((id) => append(id, false)); }
|
|
928
|
+
const raw = state.querySelector('.timeline-raw');
|
|
929
|
+
if (raw) { const wanted = new Set(current.concat(prior)); document.querySelectorAll('#diff details.full-diff-file').forEach((file) => { const blocks = [...file.querySelectorAll('[data-diff-hunk]')].filter((block) => wanted.has(block.getAttribute('data-diff-hunk'))); if (blocks.length === 0) return; const copy = document.createElement('details'); copy.className = file.className; copy.open = true; const summary = file.querySelector('summary'); if (summary) copy.appendChild(summary.cloneNode(true)); blocks.forEach((block) => copy.appendChild(block.cloneNode(true))); raw.appendChild(copy); }); }
|
|
930
|
+
if (Number(document.body.dataset.storyLevel ?? 1) >= 4) state.querySelectorAll('.evidence[data-evidence-id]').forEach((article) => ensureRawEvidence(article));
|
|
931
|
+
state.dataset.materialized = 'true';
|
|
932
|
+
};
|
|
933
|
+
const clearTimelineState = (state) => {
|
|
934
|
+
if (!state || state.dataset.materialized !== 'true') return;
|
|
935
|
+
const evidence = state.querySelector('.timeline-evidence');
|
|
936
|
+
if (evidence) evidence.replaceChildren();
|
|
937
|
+
const raw = state.querySelector('.timeline-raw');
|
|
938
|
+
if (raw) raw.replaceChildren();
|
|
939
|
+
state.dataset.materialized = 'false';
|
|
940
|
+
};
|
|
941
|
+
materializeTimelineState(document.querySelector('.timeline-state.active'));
|
|
942
|
+
const selectTimelineStep = (stepId) => { document.querySelectorAll('[data-timeline-state]').forEach((state) => { const active = state.getAttribute('data-timeline-state') === stepId; state.classList.toggle('active', active); state.hidden = !active; if (active) materializeTimelineState(state); else clearTimelineState(state); }); document.querySelectorAll('[data-timeline-select]').forEach((button) => { const active = button.getAttribute('data-timeline-select') === stepId; button.classList.toggle('active', active); if (button.getAttribute('role') === 'tab') button.setAttribute('aria-selected', String(active)); }); const ticks = [...document.querySelectorAll('.rail-tick')]; const index = ticks.findIndex((tick) => tick.getAttribute('data-timeline-select') === stepId); if (index < 0) return; const pad = (value) => String(value).padStart(2, '0'); const stepOutput = byId('rail-step'); if (stepOutput) stepOutput.textContent = pad(index + 1) + ' / ' + pad(ticks.length); const titleOutput = byId('rail-title'); if (titleOutput) titleOutput.textContent = ticks[index].getAttribute('data-step-title') || ''; document.querySelectorAll('[data-timeline-move]').forEach((nav) => { const next = index + Number(nav.getAttribute('data-timeline-move')); nav.toggleAttribute('disabled', next < 0 || next >= ticks.length); }); };
|
|
943
|
+
const moveTimelineStep = (delta) => { const ticks = [...document.querySelectorAll('.rail-tick')]; const index = ticks.findIndex((tick) => tick.classList.contains('active')); const next = ticks[index + delta]; if (next) selectTimelineStep(next.getAttribute('data-timeline-select')); };
|
|
944
|
+
// Chapter ids come from the analysis and may contain anything; matching by attribute value avoids selector parsing entirely.
|
|
945
|
+
const openStoryChapter = (id) => { document.querySelector('[data-view="trailer"]')?.click(); const chapter = [...document.querySelectorAll('.chapter[data-chapter]')].find((node) => node.getAttribute('data-chapter') === id); if (chapter) { setZoom(Math.max(2, Number(document.body.dataset.storyLevel ?? 1))); chapter.classList.add('open'); chapter.querySelector('.chapter-toggle')?.setAttribute('aria-expanded', 'true'); const detail = chapter.querySelector('.chapter-detail'); detail.hidden = false; materializeEvidenceStack(detail.querySelector('.evidence-stack')); chapter.scrollIntoView({ behavior: 'smooth', block: 'center' }); } };
|
|
946
|
+
document.addEventListener('click', (event) => { const target = event.target instanceof Element ? event.target : null; if (!target) return; if (!target.closest('.selection-menu') && !target.closest('.evidence') && !target.closest('.full-diff-file')) byId('selection-menu').hidden = true; const chapterToggle = target.closest('.chapter-toggle'); if (chapterToggle) { const detail = chapterToggle.closest('.chapter')?.querySelector('.chapter-detail'); if (detail && !detail.hidden) materializeEvidenceStack(detail.querySelector('.evidence-stack')); } const timelineMove = target.closest('[data-timeline-move]'); if (timelineMove) { moveTimelineStep(Number(timelineMove.getAttribute('data-timeline-move'))); return; } const timelineSelect = target.closest('[data-timeline-select]'); if (timelineSelect) { selectTimelineStep(timelineSelect.getAttribute('data-timeline-select')); if (Number(document.body.dataset.storyLevel ?? 1) === 0) setZoom(1); return; } const storyStep = target.closest('[data-story-step]'); if (storyStep) { document.querySelector('[data-view="timeline"]')?.click(); selectTimelineStep(storyStep.getAttribute('data-story-step')); setZoom(Math.max(1, Number(document.body.dataset.storyLevel ?? 1))); document.querySelector('.timeline-rail')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); return; } const storyStepIndex = target.closest('[data-story-step-index]'); if (storyStepIndex) { const index = Number(storyStepIndex.getAttribute('data-story-step-index')); const state = document.querySelector('.timeline-state[data-step-index="' + index + '"]'); document.querySelector('[data-view="timeline"]')?.click(); if (state) selectTimelineStep(state.getAttribute('data-timeline-state')); setZoom(3); document.querySelector('.timeline-rail')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); return; } const collapse = target.closest('.collapse-sidebar'); if (collapse) { document.querySelector('.sidebar')?.classList.toggle('collapsed'); return; } const all = target.closest('#collapse-all'); if (all) { document.querySelectorAll('.chapter').forEach((chapter) => { chapter.classList.remove('open'); chapter.querySelector('.chapter-detail').hidden = true; chapter.querySelector('.chapter-toggle')?.setAttribute('aria-expanded', 'false'); }); return; } const testJump = target.closest('[data-test-jump]'); if (testJump) { const id = testJump.getAttribute('data-test-jump'); setZoom(3); window.setTimeout(() => { const card = [...document.querySelectorAll('.test-plan-evidence [data-test-case]')].find((node) => node.getAttribute('data-test-case') === id); if (card) { card.open = true; card.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, 0); return; } const chapterJump = target.closest('[data-step-chapter]'); if (chapterJump) { openStoryChapter(chapterJump.getAttribute('data-step-chapter')); return; } const view = target.closest('[data-view]')?.getAttribute('data-view'); if (view) setZoomControlsVisible(view); const step = target.closest('[data-zoom-step]'); if (step) { const active = Number(document.querySelector('[data-zoom].active')?.getAttribute('data-zoom') ?? 1); setZoom(active + Number(step.getAttribute('data-zoom-step'))); return; } const zoom = target.closest('[data-zoom]'); if (zoom) setZoom(Number(zoom.getAttribute('data-zoom'))); });
|
|
947
|
+
const selectionMenu = byId('selection-menu');
|
|
948
|
+
if (selectionMenu) {
|
|
949
|
+
selectionMenu.addEventListener('mousedown', (event) => event.preventDefault());
|
|
950
|
+
selectionMenu.addEventListener('touchstart', () => { selectionMenu.dataset.pressed = 'true'; window.setTimeout(() => { delete selectionMenu.dataset.pressed; }, 700); }, { passive: true });
|
|
951
|
+
const dismissSelectionMenu = () => { selectionMenu.hidden = true; };
|
|
952
|
+
window.addEventListener('scroll', dismissSelectionMenu, { capture: true, passive: true });
|
|
953
|
+
window.addEventListener('resize', dismissSelectionMenu, { passive: true });
|
|
954
|
+
// The menu appears only when the pointer is released, never mid-drag.
|
|
955
|
+
document.addEventListener('pointerdown', (event) => { const target = event.target instanceof Element ? event.target : null; if (target && target.closest('.selection-menu')) return; document.body.dataset.selectingPointer = 'true'; }, true);
|
|
956
|
+
const releaseSelectionPointer = () => { if (document.body.dataset.selectingPointer !== 'true') return; delete document.body.dataset.selectingPointer; document.dispatchEvent(new Event('selectionchange')); };
|
|
957
|
+
document.addEventListener('pointerup', releaseSelectionPointer, true);
|
|
958
|
+
document.addEventListener('pointercancel', releaseSelectionPointer, true);
|
|
959
|
+
}
|
|
960
|
+
})();
|
|
961
|
+
(() => {
|
|
962
|
+
const shell = document.querySelector('.app-shell');
|
|
963
|
+
const bar = document.querySelector('.view-bar');
|
|
964
|
+
const masthead = document.querySelector('.page-header');
|
|
965
|
+
if (bar && masthead && typeof IntersectionObserver === 'function') {
|
|
966
|
+
new IntersectionObserver((entries) => { bar.classList.toggle('view-bar-stuck', entries[0].boundingClientRect.top < 1); }, { threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] }).observe(masthead);
|
|
967
|
+
}
|
|
968
|
+
document.addEventListener('click', (event) => {
|
|
969
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
970
|
+
if (!target) return;
|
|
971
|
+
if (shell?.classList.contains('mobile-inspector-open') && window.innerWidth <= 1080 && !target.closest('.inspector') && !target.closest('.mobile-inspector-toggle')) {
|
|
972
|
+
shell.classList.remove('mobile-inspector-open');
|
|
973
|
+
document.querySelector('.mobile-inspector-toggle')?.setAttribute('aria-expanded', 'false');
|
|
974
|
+
// A sidebar-collapse click also dismisses the sheet, but its own handling below must still run or the shell grid and aria state desync.
|
|
975
|
+
if (!target.closest('.collapse-sidebar')) return;
|
|
976
|
+
}
|
|
977
|
+
if (target.closest('.collapse-sidebar')) {
|
|
978
|
+
const collapsed = document.querySelector('.sidebar')?.classList.contains('collapsed') ?? false;
|
|
979
|
+
shell?.classList.toggle('sidebar-collapsed', collapsed);
|
|
980
|
+
target.closest('.collapse-sidebar')?.setAttribute('aria-expanded', String(!collapsed));
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const mobileInspector = target.closest('.mobile-inspector-toggle');
|
|
984
|
+
if (mobileInspector) {
|
|
985
|
+
const open = shell?.classList.toggle('mobile-inspector-open') ?? false;
|
|
986
|
+
mobileInspector.setAttribute('aria-expanded', String(open));
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const inspectorToggle = target.closest('.collapse-inspector');
|
|
990
|
+
if (!inspectorToggle) return;
|
|
991
|
+
if (window.innerWidth <= 1080) {
|
|
992
|
+
shell?.classList.remove('mobile-inspector-open');
|
|
993
|
+
document.querySelector('.mobile-inspector-toggle')?.setAttribute('aria-expanded', 'false');
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
const collapsed = shell?.classList.toggle('inspector-collapsed') ?? false;
|
|
997
|
+
inspectorToggle.setAttribute('aria-expanded', String(!collapsed));
|
|
998
|
+
});
|
|
999
|
+
})();
|
|
1000
|
+
(() => {
|
|
1001
|
+
const preferenceKey = 'ndrstnd-artifact-ui-preferences-v1';
|
|
1002
|
+
const readPreferences = () => {
|
|
1003
|
+
try {
|
|
1004
|
+
const value = JSON.parse(localStorage.getItem(preferenceKey) || '{}');
|
|
1005
|
+
return value && typeof value === 'object' ? value : {};
|
|
1006
|
+
} catch { return {}; }
|
|
1007
|
+
};
|
|
1008
|
+
const savePreferences = (update) => {
|
|
1009
|
+
try { localStorage.setItem(preferenceKey, JSON.stringify({ ...readPreferences(), ...update })); } catch {}
|
|
1010
|
+
};
|
|
1011
|
+
const preferences = readPreferences();
|
|
1012
|
+
const shell = document.querySelector('.app-shell');
|
|
1013
|
+
const sidebar = document.querySelector('.sidebar');
|
|
1014
|
+
if (typeof preferences.sidebarCollapsed === 'boolean') {
|
|
1015
|
+
sidebar?.classList.toggle('collapsed', preferences.sidebarCollapsed);
|
|
1016
|
+
shell?.classList.toggle('sidebar-collapsed', preferences.sidebarCollapsed);
|
|
1017
|
+
document.querySelector('.collapse-sidebar')?.setAttribute('aria-expanded', String(!preferences.sidebarCollapsed));
|
|
1018
|
+
}
|
|
1019
|
+
if (typeof preferences.inspectorCollapsed === 'boolean') {
|
|
1020
|
+
shell?.classList.toggle('inspector-collapsed', preferences.inspectorCollapsed);
|
|
1021
|
+
document.querySelector('.collapse-inspector')?.setAttribute('aria-expanded', String(!preferences.inspectorCollapsed));
|
|
1022
|
+
}
|
|
1023
|
+
if (Number.isInteger(preferences.zoom) && preferences.zoom >= 0 && preferences.zoom <= 4) document.querySelector('[data-zoom="' + preferences.zoom + '"]')?.click();
|
|
1024
|
+
// A tampered stored view name must not throw out of this script and take the other startup wiring with it.
|
|
1025
|
+
if (typeof preferences.view === 'string' && /^[a-z-]+$/.test(preferences.view)) document.querySelector('[data-view="' + preferences.view + '"]')?.click();
|
|
1026
|
+
document.addEventListener('click', (event) => {
|
|
1027
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
1028
|
+
if (!target) return;
|
|
1029
|
+
const view = target.closest('[data-view]')?.getAttribute('data-view');
|
|
1030
|
+
if (view) savePreferences({ view });
|
|
1031
|
+
const zoom = target.closest('[data-zoom]')?.getAttribute('data-zoom');
|
|
1032
|
+
if (zoom !== null && zoom !== undefined) savePreferences({ zoom: Number(zoom) });
|
|
1033
|
+
if (target.closest('.collapse-sidebar')) savePreferences({ sidebarCollapsed: document.querySelector('.sidebar')?.classList.contains('collapsed') ?? false });
|
|
1034
|
+
if (target.closest('.collapse-inspector') && window.innerWidth > 1080) savePreferences({ inspectorCollapsed: shell?.classList.contains('inspector-collapsed') ?? false });
|
|
1035
|
+
});
|
|
1036
|
+
})();
|
|
1037
|
+
`;
|
|
1038
|
+
//# sourceMappingURL=page.js.map
|