coding-agent-harness 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.en-US.md +14 -0
- package/README.md +111 -86
- package/README.zh-CN.md +270 -0
- package/SKILL.md +116 -189
- package/docs-release/README.md +72 -5
- package/docs-release/architecture/overview.md +286 -28
- package/docs-release/architecture/overview.zh-CN.md +288 -0
- package/docs-release/assets/dashboard-overview-en.png +0 -0
- package/docs-release/assets/harness-architecture.svg +163 -0
- package/docs-release/assets/harness-workflow.svg +64 -0
- package/docs-release/guides/agent-installation.en-US.md +214 -0
- package/docs-release/guides/agent-installation.md +123 -26
- package/docs-release/guides/document-audience-and-surfaces.en-US.md +112 -0
- package/docs-release/guides/document-audience-and-surfaces.md +112 -0
- package/docs-release/guides/full-legacy-migration-subagent-strategy.md +334 -0
- package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +334 -0
- package/docs-release/guides/legacy-migration-agent-prompt.md +384 -0
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +361 -0
- package/docs-release/guides/migration-playbook.en-US.md +325 -0
- package/docs-release/guides/migration-playbook.md +329 -0
- package/docs-release/guides/parent-control-repository-pattern.en-US.md +252 -0
- package/docs-release/guides/parent-control-repository-pattern.md +252 -0
- package/docs-release/guides/repository-operating-models.en-US.md +196 -0
- package/docs-release/guides/repository-operating-models.md +196 -0
- package/docs-release/intl/README.md +15 -0
- package/docs-release/intl/de-DE.md +18 -0
- package/docs-release/intl/en-US.md +18 -0
- package/docs-release/intl/es-ES.md +18 -0
- package/docs-release/intl/fr-FR.md +18 -0
- package/docs-release/intl/ja-JP.md +18 -0
- package/docs-release/intl/ko-KR.md +18 -0
- package/docs-release/intl/zh-CN.md +18 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/brief.md +13 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/lesson_candidates.md +24 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +1 -1
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +4 -2
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/{visual_roadmap.md → visual_map.md} +9 -1
- package/package.json +3 -1
- package/references/agents-md-pattern.md +3 -3
- package/references/docs-directory-standard.md +47 -3
- package/references/external-source-intake-standard.md +75 -0
- package/references/harness-ledger.md +5 -3
- package/references/legacy-12-phase-bootstrap.md +41 -0
- package/references/lessons-governance.md +23 -6
- package/references/planning-loop.md +41 -3
- package/references/project-onboarding-audit.md +10 -0
- package/references/repo-governance-standard.md +2 -0
- package/references/testing-standard.md +50 -0
- package/references/walkthrough-closeout.md +6 -5
- package/scripts/check-harness.mjs +76 -35
- package/scripts/harness.mjs +303 -12
- package/scripts/lib/capability-registry.mjs +533 -0
- package/scripts/lib/check-profiles.mjs +510 -0
- package/scripts/lib/core-shared.mjs +186 -0
- package/scripts/lib/dashboard-data.mjs +389 -0
- package/scripts/lib/dashboard-workbench.mjs +217 -0
- package/scripts/lib/dashboard-writer.mjs +93 -2
- package/scripts/lib/harness-core.mjs +10 -1318
- package/scripts/lib/lesson-maintenance.mjs +145 -0
- package/scripts/lib/markdown-utils.mjs +158 -0
- package/scripts/lib/migration-planner.mjs +478 -0
- package/scripts/lib/migration-support.mjs +312 -0
- package/scripts/lib/task-lifecycle.mjs +755 -0
- package/scripts/lib/task-scanner.mjs +682 -0
- package/scripts/smoke-dashboard.mjs +22 -0
- package/scripts/test-harness.mjs +926 -14
- package/templates/AGENTS.md.template +41 -30
- package/templates/architecture/Architecture-SSoT.md +21 -0
- package/templates/architecture/README.md +49 -0
- package/templates/architecture/critical-flows.md +22 -0
- package/templates/architecture/local-repo-context.md +20 -0
- package/templates/architecture/service-catalog.md +17 -0
- package/templates/architecture/services/service-template.md +31 -0
- package/templates/architecture/system-map.md +22 -0
- package/templates/dashboard/assets/app-src/00-state.js +41 -0
- package/templates/dashboard/assets/app-src/10-router.js +76 -0
- package/templates/dashboard/assets/app-src/20-overview.js +235 -0
- package/templates/dashboard/assets/app-src/30-tasks.js +563 -0
- package/templates/dashboard/assets/app-src/40-modules.js +58 -0
- package/templates/dashboard/assets/app-src/45-review.js +128 -0
- package/templates/dashboard/assets/app-src/50-migration.js +169 -0
- package/templates/dashboard/assets/app-src/60-shared.js +61 -0
- package/templates/dashboard/assets/app-src/90-bindings.js +382 -0
- package/templates/dashboard/assets/app.css +2575 -310
- package/templates/dashboard/assets/app.js +1498 -307
- package/templates/dashboard/assets/app.manifest.json +11 -0
- package/templates/dashboard/assets/i18n.js +429 -44
- package/templates/dashboard/assets/mermaid-renderer.js +58 -8
- package/templates/development/README.md +52 -0
- package/templates/development/codebase-map.md +11 -0
- package/templates/development/cross-repo-debugging.md +18 -0
- package/templates/development/external-context/service-template.md +33 -0
- package/templates/development/external-source-packs/README.md +24 -0
- package/templates/development/external-source-packs/digest-template.md +28 -0
- package/templates/development/local-setup.md +16 -0
- package/templates/development/stubs-and-mocks.md +11 -0
- package/templates/integrations/README.md +40 -0
- package/templates/integrations/api-contract.md +42 -0
- package/templates/integrations/event-contract.md +46 -0
- package/templates/integrations/third-party/vendor-template.md +42 -0
- package/templates/integrations/webhook-contract.md +41 -0
- package/templates/planning/brief.md +32 -0
- package/templates/planning/lesson_candidates.md +58 -0
- package/templates/planning/long-running-task-contract.md +7 -0
- package/templates/planning/module_brief.md +25 -0
- package/templates/planning/module_session_prompt.md +6 -0
- package/templates/planning/task_plan.md +7 -5
- package/templates/planning/{visual_roadmap.md → visual_map.md} +24 -2
- package/templates/reference/docs-library-standard.md +31 -0
- package/templates/reference/execution-workflow-standard.md +4 -2
- package/templates/reference/external-source-intake-standard.md +82 -0
- package/templates/reference/harness-ledger-standard.md +1 -0
- package/templates/reference/repo-governance-standard.md +6 -4
- package/templates/reference/walkthrough-standard.md +2 -1
- package/templates/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/AGENTS.md.template +69 -70
- package/templates-zh-CN/architecture/Architecture-SSoT.md +21 -0
- package/templates-zh-CN/architecture/README.md +51 -0
- package/templates-zh-CN/architecture/critical-flows.md +24 -0
- package/templates-zh-CN/architecture/local-repo-context.md +20 -0
- package/templates-zh-CN/architecture/service-catalog.md +17 -0
- package/templates-zh-CN/architecture/services/service-template.md +31 -0
- package/templates-zh-CN/architecture/system-map.md +22 -0
- package/templates-zh-CN/development/README.md +54 -0
- package/templates-zh-CN/development/codebase-map.md +11 -0
- package/templates-zh-CN/development/cross-repo-debugging.md +18 -0
- package/templates-zh-CN/development/external-context/service-template.md +33 -0
- package/templates-zh-CN/development/external-source-packs/README.md +24 -0
- package/templates-zh-CN/development/external-source-packs/digest-template.md +28 -0
- package/templates-zh-CN/development/local-setup.md +16 -0
- package/templates-zh-CN/development/stubs-and-mocks.md +11 -0
- package/templates-zh-CN/integrations/README.md +42 -0
- package/templates-zh-CN/integrations/api-contract.md +42 -0
- package/templates-zh-CN/integrations/event-contract.md +46 -0
- package/templates-zh-CN/integrations/third-party/vendor-template.md +42 -0
- package/templates-zh-CN/integrations/webhook-contract.md +41 -0
- package/templates-zh-CN/planning/brief.md +32 -0
- package/templates-zh-CN/planning/lesson_candidates.md +58 -0
- package/templates-zh-CN/planning/long-running-task-contract.md +1 -1
- package/templates-zh-CN/planning/module_brief.md +25 -0
- package/templates-zh-CN/planning/module_plan.md +2 -2
- package/templates-zh-CN/planning/module_session_prompt.md +4 -3
- package/templates-zh-CN/planning/task_plan.md +10 -4
- package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
- package/templates-zh-CN/reference/docs-library-standard.md +35 -0
- package/templates-zh-CN/reference/execution-workflow-standard.md +9 -2
- package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
- package/templates-zh-CN/reference/harness-ledger-standard.md +5 -2
- package/templates-zh-CN/reference/repo-governance-standard.md +2 -0
- package/templates-zh-CN/reference/walkthrough-standard.md +4 -4
- package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
- package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/dashboard/assets/app.css +0 -399
- package/templates-zh-CN/dashboard/assets/app.js +0 -435
- package/templates-zh-CN/dashboard/assets/i18n.js +0 -47
- package/templates-zh-CN/dashboard/assets/markdown-reader.js +0 -116
- package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +0 -59
- package/templates-zh-CN/dashboard/index.html +0 -18
|
@@ -1,435 +0,0 @@
|
|
|
1
|
-
const bundle = window.__HARNESS_DASHBOARD__ || {};
|
|
2
|
-
const state = {
|
|
3
|
-
page: "overview",
|
|
4
|
-
lang: "zh",
|
|
5
|
-
theme: localStorage.getItem("harness.theme") || "system",
|
|
6
|
-
density: localStorage.getItem("harness.density") || "comfortable",
|
|
7
|
-
selected: null,
|
|
8
|
-
tab: "plan",
|
|
9
|
-
renderMode: "rendered",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const pageKeys = ["overview", "ledger", "tasks", "modules", "evidence", "lessons", "adoption", "settings"];
|
|
13
|
-
const taskDocTabs = [
|
|
14
|
-
["plan", "task_plan.md"],
|
|
15
|
-
["strategy", "execution_strategy.md"],
|
|
16
|
-
["roadmap", "visual_roadmap.md"],
|
|
17
|
-
["progress", "progress.md"],
|
|
18
|
-
["review", "review.md"],
|
|
19
|
-
["findings", "findings.md"],
|
|
20
|
-
["references", "references/INDEX.md"],
|
|
21
|
-
["artifacts", "artifacts/INDEX.md"],
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
function t(key) {
|
|
25
|
-
return (window.HarnessI18n?.zh || {})[key] || key;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function app() {
|
|
29
|
-
const systemTheme = window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
30
|
-
document.documentElement.dataset.theme = state.theme === "system" ? systemTheme : state.theme;
|
|
31
|
-
document.documentElement.dataset.density = state.density;
|
|
32
|
-
document.documentElement.lang = "zh-CN";
|
|
33
|
-
const root = document.getElementById("app");
|
|
34
|
-
root.innerHTML = `
|
|
35
|
-
<div class="layout">
|
|
36
|
-
<aside class="sidebar">
|
|
37
|
-
<div class="brand">
|
|
38
|
-
<strong>${escapeHtml(bundle.status?.project?.name || "Harness")}</strong>
|
|
39
|
-
<span>${escapeHtml(bundle.status?.project?.root || "TARGET:.")}</span>
|
|
40
|
-
</div>
|
|
41
|
-
<nav class="nav">${pageKeys.map((page) => navButton(page)).join("")}</nav>
|
|
42
|
-
</aside>
|
|
43
|
-
<main class="main">
|
|
44
|
-
${topbar()}
|
|
45
|
-
${renderPage()}
|
|
46
|
-
</main>
|
|
47
|
-
</div>`;
|
|
48
|
-
bind();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function navButton(page) {
|
|
52
|
-
return `<button data-page="${page}" class="${state.page === page ? "active" : ""}">${t(page)}</button>`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function topbar() {
|
|
56
|
-
return `<div class="topbar">
|
|
57
|
-
<div>
|
|
58
|
-
<p class="eyebrow">Coding Agent Harness 控制台</p>
|
|
59
|
-
<h1>${escapeHtml(pageTitle())}</h1>
|
|
60
|
-
</div>
|
|
61
|
-
<div class="controls">
|
|
62
|
-
<div class="control">
|
|
63
|
-
<button data-theme="light" class="${state.theme === "light" ? "active" : ""}">${t("light")}</button>
|
|
64
|
-
<button data-theme="dark" class="${state.theme === "dark" ? "active" : ""}">${t("dark")}</button>
|
|
65
|
-
<button data-theme="system" class="${state.theme === "system" ? "active" : ""}">${t("system")}</button>
|
|
66
|
-
</div>
|
|
67
|
-
<div class="control">
|
|
68
|
-
<button data-density="compact" class="${state.density === "compact" ? "active" : ""}">${t("compact")}</button>
|
|
69
|
-
<button data-density="comfortable" class="${state.density === "comfortable" ? "active" : ""}">${t("comfortable")}</button>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
</div>`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function pageTitle() {
|
|
76
|
-
const project = bundle.status?.project?.name || "project";
|
|
77
|
-
if (state.page === "overview") return `${project} 项目驾驶舱`;
|
|
78
|
-
return t(state.page);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function renderPage() {
|
|
82
|
-
if (state.page === "overview") return overview();
|
|
83
|
-
if (state.page === "ledger") return withDrawer(ledgerTable());
|
|
84
|
-
if (state.page === "tasks") return withDrawer(taskTable());
|
|
85
|
-
if (state.page === "modules") return withDrawer(moduleTable());
|
|
86
|
-
if (state.page === "evidence") return withDrawer(evidenceTable());
|
|
87
|
-
if (state.page === "lessons") return withDrawer(lessonsTable());
|
|
88
|
-
if (state.page === "adoption") return adoption();
|
|
89
|
-
return settings();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function overview() {
|
|
93
|
-
const status = bundle.status || {};
|
|
94
|
-
const evidence = evidenceHealth(status.tasks || []);
|
|
95
|
-
const blockers = status.checkState?.failures || 0;
|
|
96
|
-
const warnings = status.checkState?.warnings || 0;
|
|
97
|
-
return `<section class="status-grid">
|
|
98
|
-
<div class="readiness panel">
|
|
99
|
-
<span class="state ${status.checkState?.status || "warn"}">${t("readiness")}: ${label(status.checkState?.status || "unknown")}</span>
|
|
100
|
-
<h2>${nextActionText()}</h2>
|
|
101
|
-
<p class="muted">这里把真实阻塞、证据缺口和旧版升级建议拆开显示。</p>
|
|
102
|
-
</div>
|
|
103
|
-
${metric(t("activeTasks"), status.tasks?.length || 0)}
|
|
104
|
-
${metric(t("blockers"), blockers)}
|
|
105
|
-
${metric(t("warnings"), warnings)}
|
|
106
|
-
</section>
|
|
107
|
-
<section class="page-grid">
|
|
108
|
-
<div>
|
|
109
|
-
${overviewTasks()}
|
|
110
|
-
${ledgerSummary()}
|
|
111
|
-
${riskPanel()}
|
|
112
|
-
</div>
|
|
113
|
-
${drawer()}
|
|
114
|
-
</section>`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function metric(label, value) {
|
|
118
|
-
return `<div class="metric"><span>${label}</span><strong>${value}</strong></div>`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function nextActionText() {
|
|
122
|
-
const failures = bundle.status?.checkState?.failures || 0;
|
|
123
|
-
if (failures > 0) return "先处理发布阻塞项";
|
|
124
|
-
const advice = bundle.adoption?.warnings?.length || 0;
|
|
125
|
-
if (advice > 0) return "可以继续,但需要处理升级建议";
|
|
126
|
-
return "当前快照没有阻塞项";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function overviewTasks() {
|
|
130
|
-
const rows = (bundle.status?.tasks || []).slice(0, 6);
|
|
131
|
-
return tablePanel(t("tasks"), [t("task"), t("state"), t("completion"), t("roadmapSource")], rows.map((task) => [
|
|
132
|
-
clickable(task.title, "task", task.id),
|
|
133
|
-
tag(task.state, label(task.state)),
|
|
134
|
-
progress(task.completion),
|
|
135
|
-
escapeHtml(label(task.roadmapSource || "unknown")),
|
|
136
|
-
]));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function riskPanel() {
|
|
140
|
-
const warnings = bundle.adoption?.warnings || [];
|
|
141
|
-
const grouped = groupBy(warnings, (item) => item.category);
|
|
142
|
-
return `<section class="panel" style="padding:16px;margin-top:16px">
|
|
143
|
-
<h2>风险与升级建议</h2>
|
|
144
|
-
<div class="risk-list">${Object.entries(grouped).map(([category, items]) => `
|
|
145
|
-
<div class="risk-item"><strong>${escapeHtml(category)}</strong><p class="muted">${items.length} 项</p></div>
|
|
146
|
-
`).join("") || `<p class="empty">没有建议</p>`}</div>
|
|
147
|
-
</section>`;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function taskTable() {
|
|
151
|
-
const rows = bundle.status?.tasks || [];
|
|
152
|
-
return tablePanel(t("tasks"), [t("task"), t("state"), t("completion"), t("evidence"), t("roadmapSource")], rows.map((task) => [
|
|
153
|
-
clickable(task.title, "task", task.id),
|
|
154
|
-
tag(task.state, label(task.state)),
|
|
155
|
-
progress(task.completion),
|
|
156
|
-
progress(evidenceHealth([task])),
|
|
157
|
-
escapeHtml(label(task.roadmapSource || "unknown")),
|
|
158
|
-
]));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function ledgerSummary() {
|
|
162
|
-
const tables = (bundle.tables?.tables || []).filter((table) => table.kind === "harness-ledger");
|
|
163
|
-
if (tables.length === 0) return "";
|
|
164
|
-
const rows = tables.flatMap((table) => table.rows).slice(0, 5);
|
|
165
|
-
return `<section class="panel ledger-summary">
|
|
166
|
-
<div class="panel-head"><h2>${t("ledger")}</h2><span class="muted">${rows.length}</span></div>
|
|
167
|
-
<div class="risk-list">${rows.map((row) => {
|
|
168
|
-
const cells = row.cells || {};
|
|
169
|
-
const title = cells.Task || cells.ID || cells.Item || cells.Title || cells.Module || "Ledger item";
|
|
170
|
-
const state = cells.State || cells.Status || cells.Review || cells["Review State"] || "";
|
|
171
|
-
const action = cells["Next Action"] || cells.Action || cells.Owner || cells["Required Action"] || "";
|
|
172
|
-
return `<div class="risk-item"><strong>${escapeHtml(title)}</strong><p class="muted">${escapeHtml([label(state), action].filter(Boolean).join(" · "))}</p></div>`;
|
|
173
|
-
}).join("")}</div>
|
|
174
|
-
</section>`;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function ledgerTable() {
|
|
178
|
-
const tables = (bundle.tables?.tables || []).filter((table) => table.kind === "harness-ledger");
|
|
179
|
-
if (tables.length === 0) return emptyTable(t("ledger"), "未找到 Harness Ledger。");
|
|
180
|
-
return genericTables(t("ledger"), tables);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function moduleTable() {
|
|
184
|
-
const tables = (bundle.tables?.tables || []).filter((table) => table.kind === "module-registry");
|
|
185
|
-
const graph = graphPanel();
|
|
186
|
-
if (tables.length === 0) return `${graph}${emptyTable(t("modules"), "未找到 Module Registry。")}`;
|
|
187
|
-
return `${graph}${genericTables(t("modules"), tables)}`;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function evidenceTable() {
|
|
191
|
-
const items = evidenceItems();
|
|
192
|
-
return tablePanel(t("evidence"), [t("origin"), t("task"), t("state"), t("title"), t("affected")], items.map((item) => [
|
|
193
|
-
escapeHtml(label(item.source)),
|
|
194
|
-
escapeHtml(item.task || "-"),
|
|
195
|
-
tag(item.state || "present", label(item.state || "present")),
|
|
196
|
-
escapeHtml(item.title),
|
|
197
|
-
escapeHtml(item.affected || ""),
|
|
198
|
-
]));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function evidenceItems() {
|
|
202
|
-
const taskEvidence = (bundle.status?.tasks || []).flatMap((task) => [
|
|
203
|
-
...(task.evidence || []).map((item) => ({
|
|
204
|
-
source: item.type || "task-progress",
|
|
205
|
-
task: task.title,
|
|
206
|
-
state: item.status || "present",
|
|
207
|
-
title: item.summary || item.id,
|
|
208
|
-
affected: item.path || "",
|
|
209
|
-
})),
|
|
210
|
-
...(task.risks || []).map((item) => ({
|
|
211
|
-
source: "task-review",
|
|
212
|
-
task: task.title,
|
|
213
|
-
state: item.open ? "open" : "closed",
|
|
214
|
-
title: item.summary || item.id,
|
|
215
|
-
affected: `${item.severity || ""}${item.blocksRelease ? " · blocks" : ""}`.trim(),
|
|
216
|
-
})),
|
|
217
|
-
]);
|
|
218
|
-
const qaTables = (bundle.tables?.tables || [])
|
|
219
|
-
.filter((table) => ["task-review", "regression-ssot", "cadence-ledger"].includes(table.kind))
|
|
220
|
-
.flatMap((table) => table.rows.slice(0, 12).map((row) => {
|
|
221
|
-
const cells = row.cells || {};
|
|
222
|
-
return {
|
|
223
|
-
source: table.kind,
|
|
224
|
-
task: cells.Task || cells.Module || cells.ID || "-",
|
|
225
|
-
state: cells.Status || cells.State || cells.Open || cells.Verdict || "present",
|
|
226
|
-
title: cells.Finding || cells.Summary || cells.Check || cells.Item || cells.Title || table.source,
|
|
227
|
-
affected: cells["Evidence Checked"] || cells.Path || cells.Owner || cells["Required Action"] || table.source,
|
|
228
|
-
};
|
|
229
|
-
}));
|
|
230
|
-
return [...taskEvidence, ...qaTables];
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function lessonsTable() {
|
|
234
|
-
const tables = (bundle.tables?.tables || []).filter((table) => table.kind === "lessons-ssot");
|
|
235
|
-
if (tables.length === 0) return emptyTable(t("lessons"), "未找到 Lessons SSoT。");
|
|
236
|
-
return genericTables(t("lessons"), tables);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function adoption() {
|
|
240
|
-
const advice = bundle.adoption?.warnings || [];
|
|
241
|
-
return `<section class="page-grid">
|
|
242
|
-
<div>
|
|
243
|
-
${tablePanel(t("adoption"), [t("category"), t("severity"), t("title"), t("affected"), t("requiredAction")], advice.map((item) => [
|
|
244
|
-
escapeHtml(item.category),
|
|
245
|
-
tag(item.severity, label(item.severity)),
|
|
246
|
-
escapeHtml(item.title),
|
|
247
|
-
escapeHtml(item.affected),
|
|
248
|
-
escapeHtml(item.requiredAction),
|
|
249
|
-
]))}
|
|
250
|
-
<section class="panel" style="padding:16px;margin-top:16px">
|
|
251
|
-
<h2>${t("nextAction")}</h2>
|
|
252
|
-
<ol>${((bundle.adoption?.manualSteps || {})[state.lang] || []).map((step) => `<li>${escapeHtml(step)}</li>`).join("")}</ol>
|
|
253
|
-
</section>
|
|
254
|
-
</div>
|
|
255
|
-
${drawer()}
|
|
256
|
-
</section>`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function settings() {
|
|
260
|
-
return `<section class="panel" style="padding:18px">
|
|
261
|
-
<h2>${t("settings")}</h2>
|
|
262
|
-
<p class="muted">这些设置只保存在浏览器本地,不会写回项目。</p>
|
|
263
|
-
<p>${t("rendered")} / ${t("source")}: 在详情抽屉里切换。</p>
|
|
264
|
-
</section>`;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function withDrawer(content) {
|
|
268
|
-
return `<section class="page-grid"><div>${content}</div>${drawer()}</section>`;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function tablePanel(title, headers, rows) {
|
|
272
|
-
return `<section class="table-panel">
|
|
273
|
-
<div class="panel-head"><h2>${escapeHtml(title)}</h2><span class="muted">${rows.length}</span></div>
|
|
274
|
-
<div class="table-wrap"><table>
|
|
275
|
-
<thead><tr>${headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("")}</tr></thead>
|
|
276
|
-
<tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join("")}</tr>`).join("")}</tbody>
|
|
277
|
-
</table></div>
|
|
278
|
-
</section>`;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function genericTables(title, tables) {
|
|
282
|
-
return tables.map((table) => tablePanel(`${title} · ${table.source}`, table.columns, table.rows.map((row) => table.columns.map((column) => escapeHtml(row.cells[column] || ""))))).join("");
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function graphPanel() {
|
|
286
|
-
const graph = bundle.graph || { nodes: [], edges: [] };
|
|
287
|
-
const modules = graph.nodes.filter((node) => node.type === "module");
|
|
288
|
-
const fallbackTasks = graph.nodes.filter((node) => node.type === "task");
|
|
289
|
-
const laneNodes = modules.length > 0 ? modules : fallbackTasks;
|
|
290
|
-
const edges = graph.edges || [];
|
|
291
|
-
return `<section class="panel graph-panel">
|
|
292
|
-
<div class="panel-head"><h2>${t("graph")}</h2><span class="muted">${modules.length} modules · ${fallbackTasks.length} tasks · ${edges.length} edges</span></div>
|
|
293
|
-
<div class="graph-lanes">${laneNodes.slice(0, 12).map((module) => {
|
|
294
|
-
const owned = edges.filter((edge) => edge.from === module.id).slice(0, 8);
|
|
295
|
-
return `<div class="lane"><strong>${escapeHtml(module.label)}</strong><span>${label(module.state || "")}</span>${owned.map((edge) => `<small>${escapeHtml(edge.type)} → ${escapeHtml(edge.to.replace(/^step:/, ""))}</small>`).join("")}</div>`;
|
|
296
|
-
}).join("") || `<p class="empty">暂无模块图数据</p>`}</div>
|
|
297
|
-
</section>`;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function emptyTable(title, message) {
|
|
301
|
-
return `<section class="table-panel"><div class="panel-head"><h2>${escapeHtml(title)}</h2></div><p class="empty">${escapeHtml(message)}</p></section>`;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function drawer() {
|
|
305
|
-
const selection = state.selected;
|
|
306
|
-
if (!selection) {
|
|
307
|
-
return `<aside class="drawer"><div class="drawer-head"><h2>${t("detail")}</h2></div><div class="drawer-body empty">${t("noSelection")}</div></aside>`;
|
|
308
|
-
}
|
|
309
|
-
const docs = docsForSelection(selection);
|
|
310
|
-
const active = docs.find((doc) => doc.tab === state.tab) || docs[0];
|
|
311
|
-
return `<aside class="drawer">
|
|
312
|
-
<div class="drawer-head"><h2>${escapeHtml(selection.label)}</h2><p class="muted">${escapeHtml(active?.path || "")}</p></div>
|
|
313
|
-
<div class="drawer-tabs">${docs.map((doc) => `<button data-tab="${doc.tab}" class="${active?.tab === doc.tab ? "active" : ""}">${t(doc.tab)}</button>`).join("")}</div>
|
|
314
|
-
<div class="drawer-mode"><button data-render-mode="rendered" class="${state.renderMode === "rendered" ? "active" : ""}">${t("rendered")}</button><button data-render-mode="source" class="${state.renderMode === "source" ? "active" : ""}">${t("source")}</button></div>
|
|
315
|
-
<div class="drawer-body"><article class="markdown">${active ? window.HarnessMarkdown.render(active.content, state.renderMode) : t("noSelection")}</article></div>
|
|
316
|
-
</aside>`;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function docsForSelection(selection) {
|
|
320
|
-
if (selection.type !== "task") return [];
|
|
321
|
-
const task = (bundle.status?.tasks || []).find((item) => item.id === selection.id);
|
|
322
|
-
if (!task) return [];
|
|
323
|
-
return taskDocTabs.map(([tab, suffix]) => {
|
|
324
|
-
const doc = findDocument(`${task.path}/${suffix}`);
|
|
325
|
-
return doc ? { tab, ...doc } : null;
|
|
326
|
-
}).filter(Boolean);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function findDocument(pathSuffix) {
|
|
330
|
-
return (bundle.documents?.documents || []).find((doc) => doc.path.endsWith(pathSuffix));
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function clickable(label, type, id) {
|
|
334
|
-
return `<button class="linklike" data-select-type="${type}" data-select-id="${id}" data-select-label="${escapeAttr(label)}">${escapeHtml(label)}</button>`;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function tag(value, text = value) {
|
|
338
|
-
const raw = String(value || "unknown");
|
|
339
|
-
const klass = /fail|blocked|open/i.test(raw) ? "fail" : /warn|advice|planned|missing/i.test(raw) ? "warn" : /pass|done|present|verified/i.test(raw) ? "pass" : "";
|
|
340
|
-
return `<span class="tag ${klass}">${escapeHtml(text)}</span>`;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function label(value) {
|
|
344
|
-
const labels = {
|
|
345
|
-
pass: "通过",
|
|
346
|
-
warn: "需关注",
|
|
347
|
-
fail: "阻塞",
|
|
348
|
-
in_progress: "进行中",
|
|
349
|
-
planned: "计划中",
|
|
350
|
-
done: "完成",
|
|
351
|
-
blocked: "阻塞",
|
|
352
|
-
missing: "缺失",
|
|
353
|
-
present: "已有",
|
|
354
|
-
closed: "关闭",
|
|
355
|
-
advice: "建议",
|
|
356
|
-
standalone: "独立文件",
|
|
357
|
-
legacy: "旧版兼容",
|
|
358
|
-
"task-review": "审查",
|
|
359
|
-
"task-progress": "进度",
|
|
360
|
-
"regression-ssot": "回归",
|
|
361
|
-
"cadence-ledger": "节奏",
|
|
362
|
-
unknown: "未知",
|
|
363
|
-
};
|
|
364
|
-
return labels[value] || value;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function progress(value) {
|
|
368
|
-
const score = Math.max(0, Math.min(100, Number(value) || 0));
|
|
369
|
-
return `<span>${score}%</span><div class="bar"><i style="width:${score}%"></i></div>`;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function evidenceHealth(tasks) {
|
|
373
|
-
const phases = tasks.flatMap((task) => task.phases || []).filter((phase) => phase.state !== "skipped");
|
|
374
|
-
if (phases.length === 0) return 0;
|
|
375
|
-
const score = phases.reduce((sum, phase) => {
|
|
376
|
-
if (["present", "waived"].includes(phase.evidenceStatus)) return sum + 100;
|
|
377
|
-
if (phase.evidenceStatus === "partial") return sum + 50;
|
|
378
|
-
return sum;
|
|
379
|
-
}, 0);
|
|
380
|
-
return Math.round(score / phases.length);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function groupBy(items, fn) {
|
|
384
|
-
return items.reduce((acc, item) => {
|
|
385
|
-
const key = fn(item);
|
|
386
|
-
acc[key] ||= [];
|
|
387
|
-
acc[key].push(item);
|
|
388
|
-
return acc;
|
|
389
|
-
}, {});
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function bind() {
|
|
393
|
-
document.querySelectorAll("[data-page]").forEach((button) => button.addEventListener("click", () => {
|
|
394
|
-
state.page = button.dataset.page;
|
|
395
|
-
state.selected = null;
|
|
396
|
-
app();
|
|
397
|
-
}));
|
|
398
|
-
document.querySelectorAll("[data-theme]").forEach((button) => button.addEventListener("click", () => {
|
|
399
|
-
state.theme = button.dataset.theme;
|
|
400
|
-
localStorage.setItem("harness.theme", state.theme);
|
|
401
|
-
app();
|
|
402
|
-
}));
|
|
403
|
-
document.querySelectorAll("[data-density]").forEach((button) => button.addEventListener("click", () => {
|
|
404
|
-
state.density = button.dataset.density;
|
|
405
|
-
localStorage.setItem("harness.density", state.density);
|
|
406
|
-
app();
|
|
407
|
-
}));
|
|
408
|
-
document.querySelectorAll("[data-select-type]").forEach((button) => button.addEventListener("click", () => {
|
|
409
|
-
state.selected = { type: button.dataset.selectType, id: button.dataset.selectId, label: button.dataset.selectLabel };
|
|
410
|
-
state.tab = "plan";
|
|
411
|
-
app();
|
|
412
|
-
}));
|
|
413
|
-
document.querySelectorAll("[data-tab]").forEach((button) => button.addEventListener("click", () => {
|
|
414
|
-
state.tab = button.dataset.tab;
|
|
415
|
-
app();
|
|
416
|
-
}));
|
|
417
|
-
document.querySelectorAll("[data-render-mode]").forEach((button) => button.addEventListener("click", () => {
|
|
418
|
-
state.renderMode = button.dataset.renderMode;
|
|
419
|
-
app();
|
|
420
|
-
}));
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function escapeHtml(value) {
|
|
424
|
-
return String(value ?? "")
|
|
425
|
-
.replaceAll("&", "&")
|
|
426
|
-
.replaceAll("<", "<")
|
|
427
|
-
.replaceAll(">", ">")
|
|
428
|
-
.replaceAll('"', """);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function escapeAttr(value) {
|
|
432
|
-
return escapeHtml(value).replaceAll("'", "'");
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
app();
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
window.HarnessI18n = {
|
|
2
|
-
zh: {
|
|
3
|
-
overview: "总览",
|
|
4
|
-
ledger: "总表",
|
|
5
|
-
tasks: "任务",
|
|
6
|
-
modules: "模块",
|
|
7
|
-
evidence: "证据",
|
|
8
|
-
lessons: "教训",
|
|
9
|
-
adoption: "升级",
|
|
10
|
-
settings: "设置",
|
|
11
|
-
system: "跟随系统",
|
|
12
|
-
readiness: "发布状态",
|
|
13
|
-
blockers: "阻塞",
|
|
14
|
-
warnings: "建议",
|
|
15
|
-
nextAction: "下一步",
|
|
16
|
-
activeTasks: "活跃任务",
|
|
17
|
-
evidenceHealth: "证据完整度",
|
|
18
|
-
detail: "详情",
|
|
19
|
-
plan: "计划",
|
|
20
|
-
strategy: "执行策略",
|
|
21
|
-
roadmap: "可视化路线",
|
|
22
|
-
progress: "进度",
|
|
23
|
-
review: "审查",
|
|
24
|
-
findings: "发现",
|
|
25
|
-
references: "参考",
|
|
26
|
-
artifacts: "产物",
|
|
27
|
-
source: "源码",
|
|
28
|
-
rendered: "渲染",
|
|
29
|
-
light: "白天",
|
|
30
|
-
dark: "黑夜",
|
|
31
|
-
compact: "紧凑",
|
|
32
|
-
comfortable: "舒适",
|
|
33
|
-
noSelection: "选择一行查看文档快照",
|
|
34
|
-
task: "任务",
|
|
35
|
-
state: "状态",
|
|
36
|
-
completion: "完成度",
|
|
37
|
-
roadmapSource: "路线来源",
|
|
38
|
-
category: "类别",
|
|
39
|
-
severity: "级别",
|
|
40
|
-
title: "标题",
|
|
41
|
-
affected: "影响对象",
|
|
42
|
-
requiredAction: "建议动作",
|
|
43
|
-
message: "详情",
|
|
44
|
-
graph: "依赖图",
|
|
45
|
-
origin: "来源",
|
|
46
|
-
},
|
|
47
|
-
};
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
window.HarnessMarkdown = {
|
|
2
|
-
render(markdown = "", mode = "rendered") {
|
|
3
|
-
if (mode === "source") return `<pre><code>${escapeHtml(markdown)}</code></pre>`;
|
|
4
|
-
return renderBlocks(markdown);
|
|
5
|
-
},
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
function renderBlocks(markdown) {
|
|
9
|
-
const lines = String(markdown || "").split(/\r?\n/);
|
|
10
|
-
const blocks = [];
|
|
11
|
-
let index = 0;
|
|
12
|
-
while (index < lines.length) {
|
|
13
|
-
const line = lines[index];
|
|
14
|
-
if (!line.trim()) {
|
|
15
|
-
index += 1;
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
if (line.startsWith("```")) {
|
|
19
|
-
const lang = line.slice(3).trim();
|
|
20
|
-
const code = [];
|
|
21
|
-
index += 1;
|
|
22
|
-
while (index < lines.length && !lines[index].startsWith("```")) {
|
|
23
|
-
code.push(lines[index]);
|
|
24
|
-
index += 1;
|
|
25
|
-
}
|
|
26
|
-
index += 1;
|
|
27
|
-
blocks.push(lang === "mermaid" ? window.HarnessMermaid.render(code.join("\n")) : `<pre><code>${escapeHtml(code.join("\n"))}</code></pre>`);
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (/^#{1,3}\s+/.test(line)) {
|
|
31
|
-
const level = line.match(/^#+/)[0].length;
|
|
32
|
-
blocks.push(`<h${level}>${inline(line.replace(/^#{1,3}\s+/, ""))}</h${level}>`);
|
|
33
|
-
index += 1;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (isTableStart(lines, index)) {
|
|
37
|
-
const table = [];
|
|
38
|
-
while (index < lines.length && lines[index].trim().startsWith("|")) {
|
|
39
|
-
table.push(lines[index]);
|
|
40
|
-
index += 1;
|
|
41
|
-
}
|
|
42
|
-
blocks.push(renderTable(table));
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
if (/^[-*]\s+/.test(line)) {
|
|
46
|
-
const items = [];
|
|
47
|
-
while (index < lines.length && /^[-*]\s+/.test(lines[index])) {
|
|
48
|
-
items.push(`<li>${inline(lines[index].replace(/^[-*]\s+/, ""))}</li>`);
|
|
49
|
-
index += 1;
|
|
50
|
-
}
|
|
51
|
-
blocks.push(`<ul>${items.join("")}</ul>`);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
const paragraph = [];
|
|
55
|
-
while (index < lines.length && lines[index].trim() && !/^#{1,3}\s+/.test(lines[index]) && !lines[index].startsWith("```") && !isTableStart(lines, index)) {
|
|
56
|
-
paragraph.push(lines[index]);
|
|
57
|
-
index += 1;
|
|
58
|
-
}
|
|
59
|
-
blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
|
|
60
|
-
}
|
|
61
|
-
return blocks.join("");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function isTableStart(lines, index) {
|
|
65
|
-
return lines[index]?.trim().startsWith("|") && /^(\s*\|?\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[index + 1] || "");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function renderTable(lines) {
|
|
69
|
-
const rows = lines.map(splitMarkdownRow);
|
|
70
|
-
const header = rows[0] || [];
|
|
71
|
-
const body = rows.slice(2).filter((row) => row.length === header.length);
|
|
72
|
-
return `<div class="rendered-table-wrap"><table class="rendered-table">
|
|
73
|
-
<thead><tr>${header.map((cell) => `<th>${inline(cell)}</th>`).join("")}</tr></thead>
|
|
74
|
-
<tbody>${body.map((row) => `<tr>${row.map((cell) => `<td>${inline(cell)}</td>`).join("")}</tr>`).join("")}</tbody>
|
|
75
|
-
</table></div>`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function splitMarkdownRow(line) {
|
|
79
|
-
let text = String(line || "").trim();
|
|
80
|
-
if (text.startsWith("|")) text = text.slice(1);
|
|
81
|
-
if (text.endsWith("|") && !text.endsWith("\\|")) text = text.slice(0, -1);
|
|
82
|
-
const cells = [];
|
|
83
|
-
let current = "";
|
|
84
|
-
let inCode = false;
|
|
85
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
86
|
-
const char = text[index];
|
|
87
|
-
if (char === "\\" && text[index + 1] === "|") {
|
|
88
|
-
current += "|";
|
|
89
|
-
index += 1;
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (char === "`") inCode = !inCode;
|
|
93
|
-
if (char === "|" && !inCode) {
|
|
94
|
-
cells.push(current.trim());
|
|
95
|
-
current = "";
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
current += char;
|
|
99
|
-
}
|
|
100
|
-
cells.push(current.trim());
|
|
101
|
-
return cells;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function inline(value) {
|
|
105
|
-
return escapeHtml(value)
|
|
106
|
-
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
107
|
-
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function escapeHtml(value) {
|
|
111
|
-
return String(value ?? "")
|
|
112
|
-
.replaceAll("&", "&")
|
|
113
|
-
.replaceAll("<", "<")
|
|
114
|
-
.replaceAll(">", ">")
|
|
115
|
-
.replaceAll('"', """);
|
|
116
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
window.HarnessMermaid = {
|
|
2
|
-
render(code = "") {
|
|
3
|
-
const parsed = parseFlowchart(code);
|
|
4
|
-
if (!parsed) {
|
|
5
|
-
return `<figure class="mermaid-fallback">
|
|
6
|
-
<figcaption>Mermaid source</figcaption>
|
|
7
|
-
<pre>${escapeMermaid(code)}</pre>
|
|
8
|
-
</figure>`;
|
|
9
|
-
}
|
|
10
|
-
return renderSvg(parsed);
|
|
11
|
-
},
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
function parseFlowchart(code) {
|
|
15
|
-
const lines = String(code || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
16
|
-
if (!/^flowchart\s+(LR|TB|TD|RL|BT)/i.test(lines[0] || "")) return null;
|
|
17
|
-
const nodes = new Map();
|
|
18
|
-
const edges = [];
|
|
19
|
-
for (const line of lines.slice(1)) {
|
|
20
|
-
const edgeMatch = line.match(/^([A-Za-z0-9_-]+)(?:\["([^"]+)"\])?\s*-+>\s*([A-Za-z0-9_-]+)(?:\["([^"]+)"\])?/);
|
|
21
|
-
if (!edgeMatch) continue;
|
|
22
|
-
const [, from, fromLabel, to, toLabel] = edgeMatch;
|
|
23
|
-
nodes.set(from, fromLabel || from);
|
|
24
|
-
nodes.set(to, toLabel || to);
|
|
25
|
-
edges.push([from, to]);
|
|
26
|
-
}
|
|
27
|
-
if (nodes.size === 0) return null;
|
|
28
|
-
return { nodes: [...nodes.entries()], edges };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function renderSvg({ nodes, edges }) {
|
|
32
|
-
const width = Math.max(360, nodes.length * 150);
|
|
33
|
-
const height = 150;
|
|
34
|
-
const positions = new Map(nodes.map(([id], index) => [id, { x: 70 + index * 140, y: 70 }]));
|
|
35
|
-
const nodeSvg = nodes.map(([id, label]) => {
|
|
36
|
-
const pos = positions.get(id);
|
|
37
|
-
return `<g class="mermaid-node">
|
|
38
|
-
<rect x="${pos.x - 54}" y="${pos.y - 24}" width="108" height="48" rx="8"></rect>
|
|
39
|
-
<text x="${pos.x}" y="${pos.y + 4}" text-anchor="middle">${escapeMermaid(label).replaceAll("\\n", " ")}</text>
|
|
40
|
-
</g>`;
|
|
41
|
-
}).join("");
|
|
42
|
-
const edgeSvg = edges.map(([from, to]) => {
|
|
43
|
-
const a = positions.get(from);
|
|
44
|
-
const b = positions.get(to);
|
|
45
|
-
if (!a || !b) return "";
|
|
46
|
-
return `<path class="mermaid-edge" d="M ${a.x + 54} ${a.y} L ${b.x - 62} ${b.y}"></path><path class="mermaid-arrow" d="M ${b.x - 62} ${b.y} l -7 -5 v 10 z"></path>`;
|
|
47
|
-
}).join("");
|
|
48
|
-
return `<figure class="mermaid-rendered">
|
|
49
|
-
<figcaption>Mermaid</figcaption>
|
|
50
|
-
<svg viewBox="0 0 ${width} ${height}" role="img" aria-label="Mermaid flowchart">${edgeSvg}${nodeSvg}</svg>
|
|
51
|
-
</figure>`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function escapeMermaid(value) {
|
|
55
|
-
return String(value ?? "")
|
|
56
|
-
.replaceAll("&", "&")
|
|
57
|
-
.replaceAll("<", "<")
|
|
58
|
-
.replaceAll(">", ">");
|
|
59
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Harness 控制台</title>
|
|
7
|
-
<link rel="icon" href="data:,">
|
|
8
|
-
<link rel="stylesheet" href="./assets/app.css">
|
|
9
|
-
</head>
|
|
10
|
-
<body>
|
|
11
|
-
<div id="app" class="app-shell" aria-live="polite"></div>
|
|
12
|
-
<script src="./assets/dashboard-data.js"></script>
|
|
13
|
-
<script src="./assets/i18n.js"></script>
|
|
14
|
-
<script src="./assets/markdown-reader.js"></script>
|
|
15
|
-
<script src="./assets/mermaid-renderer.js"></script>
|
|
16
|
-
<script src="./assets/app.js"></script>
|
|
17
|
-
</body>
|
|
18
|
-
</html>
|