coding-agent-harness 1.0.0 → 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.
Files changed (159) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.en-US.md +14 -0
  3. package/README.md +111 -86
  4. package/README.zh-CN.md +270 -0
  5. package/SKILL.md +116 -189
  6. package/docs-release/README.md +72 -5
  7. package/docs-release/architecture/overview.md +286 -28
  8. package/docs-release/architecture/overview.zh-CN.md +288 -0
  9. package/docs-release/assets/dashboard-overview-en.png +0 -0
  10. package/docs-release/assets/harness-architecture.svg +163 -0
  11. package/docs-release/assets/harness-workflow.svg +64 -0
  12. package/docs-release/guides/agent-installation.en-US.md +214 -0
  13. package/docs-release/guides/agent-installation.md +123 -26
  14. package/docs-release/guides/document-audience-and-surfaces.en-US.md +112 -0
  15. package/docs-release/guides/document-audience-and-surfaces.md +112 -0
  16. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +334 -0
  17. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +334 -0
  18. package/docs-release/guides/legacy-migration-agent-prompt.md +384 -0
  19. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +361 -0
  20. package/docs-release/guides/migration-playbook.en-US.md +325 -0
  21. package/docs-release/guides/migration-playbook.md +329 -0
  22. package/docs-release/guides/parent-control-repository-pattern.en-US.md +252 -0
  23. package/docs-release/guides/parent-control-repository-pattern.md +252 -0
  24. package/docs-release/guides/repository-operating-models.en-US.md +196 -0
  25. package/docs-release/guides/repository-operating-models.md +196 -0
  26. package/docs-release/intl/README.md +15 -0
  27. package/docs-release/intl/de-DE.md +18 -0
  28. package/docs-release/intl/en-US.md +18 -0
  29. package/docs-release/intl/es-ES.md +18 -0
  30. package/docs-release/intl/fr-FR.md +18 -0
  31. package/docs-release/intl/ja-JP.md +18 -0
  32. package/docs-release/intl/ko-KR.md +18 -0
  33. package/docs-release/intl/zh-CN.md +18 -0
  34. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/brief.md +13 -0
  35. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/lesson_candidates.md +24 -0
  36. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +1 -1
  37. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +4 -2
  38. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/{visual_roadmap.md → visual_map.md} +9 -1
  39. package/package.json +22 -1
  40. package/references/agents-md-pattern.md +3 -3
  41. package/references/docs-directory-standard.md +47 -3
  42. package/references/external-source-intake-standard.md +75 -0
  43. package/references/harness-ledger.md +5 -3
  44. package/references/legacy-12-phase-bootstrap.md +41 -0
  45. package/references/lessons-governance.md +23 -6
  46. package/references/planning-loop.md +41 -3
  47. package/references/project-onboarding-audit.md +10 -0
  48. package/references/repo-governance-standard.md +2 -0
  49. package/references/testing-standard.md +50 -0
  50. package/references/walkthrough-closeout.md +6 -5
  51. package/scripts/check-harness.mjs +76 -35
  52. package/scripts/harness.mjs +303 -12
  53. package/scripts/lib/capability-registry.mjs +533 -0
  54. package/scripts/lib/check-profiles.mjs +510 -0
  55. package/scripts/lib/core-shared.mjs +186 -0
  56. package/scripts/lib/dashboard-data.mjs +389 -0
  57. package/scripts/lib/dashboard-workbench.mjs +217 -0
  58. package/scripts/lib/dashboard-writer.mjs +93 -2
  59. package/scripts/lib/harness-core.mjs +10 -1318
  60. package/scripts/lib/lesson-maintenance.mjs +145 -0
  61. package/scripts/lib/markdown-utils.mjs +158 -0
  62. package/scripts/lib/migration-planner.mjs +478 -0
  63. package/scripts/lib/migration-support.mjs +312 -0
  64. package/scripts/lib/task-lifecycle.mjs +755 -0
  65. package/scripts/lib/task-scanner.mjs +682 -0
  66. package/scripts/smoke-dashboard.mjs +22 -0
  67. package/scripts/test-harness.mjs +928 -15
  68. package/templates/AGENTS.md.template +41 -30
  69. package/templates/architecture/Architecture-SSoT.md +21 -0
  70. package/templates/architecture/README.md +49 -0
  71. package/templates/architecture/critical-flows.md +22 -0
  72. package/templates/architecture/local-repo-context.md +20 -0
  73. package/templates/architecture/service-catalog.md +17 -0
  74. package/templates/architecture/services/service-template.md +31 -0
  75. package/templates/architecture/system-map.md +22 -0
  76. package/templates/dashboard/assets/app-src/00-state.js +41 -0
  77. package/templates/dashboard/assets/app-src/10-router.js +76 -0
  78. package/templates/dashboard/assets/app-src/20-overview.js +235 -0
  79. package/templates/dashboard/assets/app-src/30-tasks.js +563 -0
  80. package/templates/dashboard/assets/app-src/40-modules.js +58 -0
  81. package/templates/dashboard/assets/app-src/45-review.js +128 -0
  82. package/templates/dashboard/assets/app-src/50-migration.js +169 -0
  83. package/templates/dashboard/assets/app-src/60-shared.js +61 -0
  84. package/templates/dashboard/assets/app-src/90-bindings.js +382 -0
  85. package/templates/dashboard/assets/app.css +2575 -310
  86. package/templates/dashboard/assets/app.js +1498 -307
  87. package/templates/dashboard/assets/app.manifest.json +11 -0
  88. package/templates/dashboard/assets/i18n.js +429 -44
  89. package/templates/dashboard/assets/mermaid-renderer.js +58 -8
  90. package/templates/development/README.md +52 -0
  91. package/templates/development/codebase-map.md +11 -0
  92. package/templates/development/cross-repo-debugging.md +18 -0
  93. package/templates/development/external-context/service-template.md +33 -0
  94. package/templates/development/external-source-packs/README.md +24 -0
  95. package/templates/development/external-source-packs/digest-template.md +28 -0
  96. package/templates/development/local-setup.md +16 -0
  97. package/templates/development/stubs-and-mocks.md +11 -0
  98. package/templates/integrations/README.md +40 -0
  99. package/templates/integrations/api-contract.md +42 -0
  100. package/templates/integrations/event-contract.md +46 -0
  101. package/templates/integrations/third-party/vendor-template.md +42 -0
  102. package/templates/integrations/webhook-contract.md +41 -0
  103. package/templates/planning/brief.md +32 -0
  104. package/templates/planning/lesson_candidates.md +58 -0
  105. package/templates/planning/long-running-task-contract.md +7 -0
  106. package/templates/planning/module_brief.md +25 -0
  107. package/templates/planning/module_session_prompt.md +6 -0
  108. package/templates/planning/task_plan.md +7 -5
  109. package/templates/planning/{visual_roadmap.md → visual_map.md} +24 -2
  110. package/templates/reference/docs-library-standard.md +31 -0
  111. package/templates/reference/execution-workflow-standard.md +4 -2
  112. package/templates/reference/external-source-intake-standard.md +82 -0
  113. package/templates/reference/harness-ledger-standard.md +1 -0
  114. package/templates/reference/repo-governance-standard.md +6 -4
  115. package/templates/reference/walkthrough-standard.md +2 -1
  116. package/templates/walkthrough/walkthrough-template.md +2 -2
  117. package/templates-zh-CN/AGENTS.md.template +69 -70
  118. package/templates-zh-CN/architecture/Architecture-SSoT.md +21 -0
  119. package/templates-zh-CN/architecture/README.md +51 -0
  120. package/templates-zh-CN/architecture/critical-flows.md +24 -0
  121. package/templates-zh-CN/architecture/local-repo-context.md +20 -0
  122. package/templates-zh-CN/architecture/service-catalog.md +17 -0
  123. package/templates-zh-CN/architecture/services/service-template.md +31 -0
  124. package/templates-zh-CN/architecture/system-map.md +22 -0
  125. package/templates-zh-CN/development/README.md +54 -0
  126. package/templates-zh-CN/development/codebase-map.md +11 -0
  127. package/templates-zh-CN/development/cross-repo-debugging.md +18 -0
  128. package/templates-zh-CN/development/external-context/service-template.md +33 -0
  129. package/templates-zh-CN/development/external-source-packs/README.md +24 -0
  130. package/templates-zh-CN/development/external-source-packs/digest-template.md +28 -0
  131. package/templates-zh-CN/development/local-setup.md +16 -0
  132. package/templates-zh-CN/development/stubs-and-mocks.md +11 -0
  133. package/templates-zh-CN/integrations/README.md +42 -0
  134. package/templates-zh-CN/integrations/api-contract.md +42 -0
  135. package/templates-zh-CN/integrations/event-contract.md +46 -0
  136. package/templates-zh-CN/integrations/third-party/vendor-template.md +42 -0
  137. package/templates-zh-CN/integrations/webhook-contract.md +41 -0
  138. package/templates-zh-CN/planning/brief.md +32 -0
  139. package/templates-zh-CN/planning/lesson_candidates.md +58 -0
  140. package/templates-zh-CN/planning/long-running-task-contract.md +1 -1
  141. package/templates-zh-CN/planning/module_brief.md +25 -0
  142. package/templates-zh-CN/planning/module_plan.md +2 -2
  143. package/templates-zh-CN/planning/module_session_prompt.md +4 -3
  144. package/templates-zh-CN/planning/task_plan.md +10 -4
  145. package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
  146. package/templates-zh-CN/reference/docs-library-standard.md +35 -0
  147. package/templates-zh-CN/reference/execution-workflow-standard.md +9 -2
  148. package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
  149. package/templates-zh-CN/reference/harness-ledger-standard.md +5 -2
  150. package/templates-zh-CN/reference/repo-governance-standard.md +2 -0
  151. package/templates-zh-CN/reference/walkthrough-standard.md +4 -4
  152. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
  153. package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
  154. package/templates-zh-CN/dashboard/assets/app.css +0 -399
  155. package/templates-zh-CN/dashboard/assets/app.js +0 -435
  156. package/templates-zh-CN/dashboard/assets/i18n.js +0 -47
  157. package/templates-zh-CN/dashboard/assets/markdown-reader.js +0 -116
  158. package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +0 -59
  159. package/templates-zh-CN/dashboard/index.html +0 -18
@@ -0,0 +1,510 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import {
5
+ repoRoot,
6
+ legacyChecker,
7
+ visualMapFile,
8
+ legacyVisualRoadmapFile,
9
+ lessonCandidatesFile,
10
+ allowedReviewDispositions,
11
+ allowedPhaseStates,
12
+ allowedEvidenceStatus,
13
+ normalizeTarget,
14
+ toPosix,
15
+ readFileSafe,
16
+ walkFiles,
17
+ } from "./core-shared.mjs";
18
+ import {
19
+ tableAfterHeading,
20
+ getColumn,
21
+ getColumnAny,
22
+ splitList,
23
+ firstColumn,
24
+ contentHasAny,
25
+ } from "./markdown-utils.mjs";
26
+ import {
27
+ capabilityDefinitions,
28
+ validateCapabilities,
29
+ } from "./capability-registry.mjs";
30
+ import {
31
+ collectTasks,
32
+ listTaskPlanPaths,
33
+ parseTaskBudget,
34
+ parseTaskContractInfo,
35
+ readVisualMapContractFile,
36
+ parsePhases,
37
+ taskCutoverCounters,
38
+ } from "./task-scanner.mjs";
39
+
40
+ export function runLegacyCheck(target) {
41
+ const checkTarget = target.docsOnly ? target.projectRoot : target.input;
42
+ const result = spawnSync(process.execPath, [legacyChecker, checkTarget], {
43
+ cwd: repoRoot,
44
+ encoding: "utf8",
45
+ });
46
+ return {
47
+ status: result.status === 0 ? "pass" : "fail",
48
+ code: result.status ?? 1,
49
+ stdout: result.stdout || "",
50
+ stderr: result.stderr || "",
51
+ };
52
+ }
53
+
54
+ export function validateReviewSchema(target, { strict = true } = {}) {
55
+ const failures = [];
56
+ const warnings = [];
57
+ const report = (message) => {
58
+ if (strict) failures.push(message);
59
+ else warnings.push(`adoption-needed: ${message}`);
60
+ };
61
+ const reviewPaths = walkFiles(target.docsRoot)
62
+ .filter((file) => file.endsWith("review.md"))
63
+ .filter((file) => !file.includes(`${path.sep}_task-template${path.sep}`))
64
+ .filter((file) => !file.includes(`${path.sep}_optional-structures${path.sep}`))
65
+ .filter((file) => !file.includes(`${path.sep}_archive${path.sep}`));
66
+
67
+ for (const reviewPath of reviewPaths) {
68
+ const relative = toPosix(path.relative(target.projectRoot, reviewPath));
69
+ const content = readFileSafe(reviewPath);
70
+ const requiredSections = [
71
+ ["Reviewer Identity", "Reviewer 身份", "审查者身份"],
72
+ ["Confidence Challenge", "信心挑战"],
73
+ ["Evidence Checked", "已检查 Evidence", "已检查证据"],
74
+ ["Final Confidence Basis", "最终信心依据"],
75
+ ];
76
+ for (const [label, ...aliases] of requiredSections) {
77
+ if (!contentHasAny(content, [label, ...aliases])) {
78
+ if (strict) failures.push(`${relative} missing ${label}`);
79
+ else warnings.push(`${relative} missing ${label}`);
80
+ }
81
+ }
82
+ const evidenceTable = tableAfterHeading(content, /^(Evidence ID|证据 ID)$/i);
83
+ if (strict && evidenceTable.rows.length === 0) {
84
+ failures.push(`${relative} Evidence Checked table needs at least one evidence row`);
85
+ }
86
+ const usesVerifier = /verifier-backed|(^|\|)[^|\n]*\|\s*verifier\s*\|/im.test(content);
87
+ if (usesVerifier) {
88
+ if (!/template_id:\s*`?harness-verifier\/v1`?/i.test(content)) {
89
+ report(`${relative} verifier-backed review missing template_id: harness-verifier/v1`);
90
+ }
91
+ if (!/verdict:\s*`?(pass|fail|inconclusive)`?/i.test(content)) {
92
+ report(`${relative} verifier-backed review missing verdict`);
93
+ }
94
+ }
95
+ const { header, rows } = tableAfterHeading(content, /^ID$/i);
96
+ if (rows.length === 0) continue;
97
+ const severityIndex = getColumnAny(header, ["Severity", "严重级别"]);
98
+ const openIndex = getColumnAny(header, ["Open", "是否开放"]);
99
+ const dispositionIndex = getColumnAny(header, ["Disposition", "处置"]);
100
+ const blocksIndex = getColumnAny(header, ["Blocks Release", "是否阻塞发布"]);
101
+ const followUpIndex = getColumnAny(header, ["Follow-up", "跟进"]);
102
+ const evidenceCheckedIndex = getColumnAny(header, ["Evidence Checked", "已检查证据"]);
103
+ if ([severityIndex, openIndex, dispositionIndex, blocksIndex].some((index) => index < 0)) {
104
+ report(`${relative} findings table missing Severity/Open/Disposition/Blocks Release columns`);
105
+ continue;
106
+ }
107
+ for (const row of rows) {
108
+ const id = row[0] || "";
109
+ const severity = row[severityIndex] || "";
110
+ if (!/^P[0-3]$/.test(severity) && !/^(R|SR)-\d+/i.test(id)) continue;
111
+ const open = (row[openIndex] || "").toLowerCase();
112
+ const disposition = (row[dispositionIndex] || "").toLowerCase();
113
+ const blocks = (row[blocksIndex] || "").toLowerCase();
114
+ const followUp = row[followUpIndex] || "";
115
+ if (!/^P[0-3]$/.test(severity)) report(`${relative} ${id} invalid severity: ${severity}`);
116
+ if (!["yes", "no"].includes(open)) report(`${relative} ${id} invalid Open value: ${open}`);
117
+ if (!allowedReviewDispositions.has(disposition)) report(`${relative} ${id} invalid Disposition: ${disposition}`);
118
+ if (!["yes", "no"].includes(blocks)) report(`${relative} ${id} invalid Blocks Release value: ${blocks}`);
119
+ if ((open === "yes" || blocks === "yes") && /^P[01]$/.test(severity)) {
120
+ report(`${relative} ${id} has release-blocking open ${severity}`);
121
+ }
122
+ if (["accepted-risk", "deferred"].includes(disposition) && (!followUp || /^none|无$/i.test(followUp))) {
123
+ report(`${relative} ${id} ${disposition} requires follow-up routing`);
124
+ }
125
+ if (strict && evidenceCheckedIndex >= 0) {
126
+ const refs = splitList(row[evidenceCheckedIndex] || "");
127
+ const evidenceIds = new Set(evidenceTable.rows.map((evidenceRow) => evidenceRow[0]));
128
+ for (const ref of refs) {
129
+ if (ref !== "none" && /^E-\d+/i.test(ref) && !evidenceIds.has(ref)) {
130
+ failures.push(`${relative} ${id} references missing evidence id: ${ref}`);
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ return { failures, warnings };
137
+ }
138
+
139
+ export function validateVisualMaps(target) {
140
+ const failures = [];
141
+ const warnings = [];
142
+ for (const taskPlanPath of listTaskPlanPaths(target)) {
143
+ const taskDir = path.dirname(taskPlanPath);
144
+ const visualMapPath = path.join(taskDir, visualMapFile);
145
+ const legacyPath = path.join(taskDir, legacyVisualRoadmapFile);
146
+ const relative = toPosix(path.relative(target.projectRoot, visualMapPath));
147
+ const taskPlan = readFileSafe(taskPlanPath);
148
+ const visualMap = readVisualMapContractFile(taskDir, taskPlan);
149
+ const { header, rows } = tableAfterHeading(visualMap.content, /^Phase ID$/i);
150
+ if (rows.length > 0) {
151
+ for (const column of ["Phase ID", "Depends On", "State", "Completion", "Output", "Required Evidence", "Evidence Status", "Blocking Risk", "Owner / Handoff"]) {
152
+ if (getColumn(header, column) < 0) failures.push(`${relative} Visual Map missing column: ${column}`);
153
+ }
154
+ }
155
+ const phases = parsePhases(visualMap.content);
156
+ for (const phase of phases) {
157
+ if (!allowedPhaseStates.has(phase.state)) failures.push(`${relative} phase ${phase.id} invalid state: ${phase.state}`);
158
+ if (!allowedEvidenceStatus.has(phase.evidenceStatus)) {
159
+ failures.push(`${relative} phase ${phase.id} invalid evidence status: ${phase.evidenceStatus}`);
160
+ }
161
+ if (!Number.isInteger(phase.completion) || phase.completion < 0 || phase.completion > 100) {
162
+ failures.push(`${relative} phase ${phase.id} completion must be integer 0..100`);
163
+ }
164
+ if (phase.state === "done" && phase.completion !== 100) failures.push(`${relative} phase ${phase.id} done must be 100`);
165
+ if (phase.state === "planned" && phase.completion !== 0) failures.push(`${relative} phase ${phase.id} planned must be 0`);
166
+ }
167
+ if (visualMap.source === "canonical" && !/Visual Map Contract:\s*v1\.0/i.test(visualMap.content)) {
168
+ failures.push(`${relative} missing Visual Map Contract: v1.0`);
169
+ }
170
+ if (visualMap.source === "canonical" && phases.length === 0) warnings.push(`${relative} has no Visual Map phase table`);
171
+ if (visualMap.source === "legacy" && fs.existsSync(legacyPath)) {
172
+ warnings.push(`${relative} missing; legacy visual_roadmap.md is rewrite input only`);
173
+ } else if (visualMap.source === "legacy" && phases.length > 0) {
174
+ warnings.push(`${relative} missing; using legacy task_plan.md visual map fallback`);
175
+ }
176
+ }
177
+ return { failures, warnings };
178
+ }
179
+
180
+ export function validatePlanContracts(target, { strict = true } = {}) {
181
+ const failures = [];
182
+ const warnings = [];
183
+ const report = (message) => {
184
+ if (strict) failures.push(message);
185
+ else warnings.push(`adoption-needed: ${message}`);
186
+ };
187
+ for (const taskPlanPath of listTaskPlanPaths(target)) {
188
+ const taskDir = path.dirname(taskPlanPath);
189
+ const relativeDir = toPosix(path.relative(target.projectRoot, taskDir));
190
+ const taskPlanContent = readFileSafe(taskPlanPath);
191
+ const budget = parseTaskBudget(taskPlanContent);
192
+ const taskContract = parseTaskContractInfo(taskPlanContent);
193
+ if (!taskContract.generated) {
194
+ warnings.push(`adoption-needed: ${relativeDir} missing Task Contract: harness-task/v1 marker`);
195
+ }
196
+ const requiredFiles = budget === "simple" ? [visualMapFile] : ["execution_strategy.md", visualMapFile, lessonCandidatesFile];
197
+ for (const fileName of requiredFiles) {
198
+ if (!fs.existsSync(path.join(taskDir, fileName))) {
199
+ if (taskContract.generated) failures.push(`${relativeDir} missing ${fileName}`);
200
+ else report(`${relativeDir} missing ${fileName}`);
201
+ }
202
+ }
203
+ }
204
+ return { failures, warnings };
205
+ }
206
+
207
+ export function validateTaskPresetContracts(target) {
208
+ const failures = [];
209
+ const allowedMigrationLevels = new Set([
210
+ "migration-baseline",
211
+ "migration-current-cutover",
212
+ "migration-full-cutover",
213
+ "migration-deferred",
214
+ ]);
215
+ for (const task of collectTasks(target)) {
216
+ if (!task.taskPreset || task.taskPreset === "none") continue;
217
+ if (task.taskPreset !== "legacy-migration") {
218
+ failures.push(`${task.path} unsupported Task Preset: ${task.taskPreset}`);
219
+ continue;
220
+ }
221
+ if (task.budget !== "complex") failures.push(`${task.path} legacy-migration preset requires Selected budget: complex`);
222
+ if (!task.presetVersion) failures.push(`${task.path} legacy-migration preset missing Preset Version`);
223
+ if (!task.taskKind || task.taskKind === "general") failures.push(`${task.path} legacy-migration preset missing Task Kind`);
224
+ if (!allowedMigrationLevels.has(task.migrationTargetLevel)) {
225
+ failures.push(`${task.path} legacy-migration preset invalid Migration Target Level: ${task.migrationTargetLevel || "(missing)"}`);
226
+ }
227
+ const achievedLevel = task.migrationAchievedLevel || "";
228
+ if (achievedLevel !== "pending" && !allowedMigrationLevels.has(achievedLevel)) {
229
+ failures.push(`${task.path} legacy-migration preset invalid Migration Achieved Level: ${achievedLevel || "(missing)"}`);
230
+ }
231
+ if (!task.evidenceBundle) {
232
+ failures.push(`${task.path} legacy-migration preset missing Evidence Bundle`);
233
+ } else if (!task.migrationSnapshot?.evidencePresent) {
234
+ failures.push(`${task.path} legacy-migration preset Evidence Bundle missing: ${task.evidenceBundle}`);
235
+ } else if (!task.migrationSnapshot?.sessionPresent) {
236
+ failures.push(`${task.path} legacy-migration preset Evidence Bundle missing session.json`);
237
+ }
238
+ if (achievedLevel === "migration-full-cutover") {
239
+ const snapshot = task.migrationSnapshot || {};
240
+ const blockers = [];
241
+ if (!snapshot.sessionPresent) blockers.push("missing session evidence");
242
+ if (snapshot.sessionResult !== "complete") blockers.push(`session result is ${snapshot.sessionResult || "(missing)"}`);
243
+ if (snapshot.strictDeferred) blockers.push("strictDeferred is present");
244
+ if (snapshot.strictStatus !== "pass") blockers.push(`strict status is ${snapshot.strictStatus || "(missing)"}`);
245
+ for (const [field, value] of [
246
+ ["warnings", snapshot.warnings],
247
+ ["taskActions", snapshot.taskActions],
248
+ ["reviewSchemaGaps", snapshot.reviewSchemaGaps],
249
+ ["legacyReferenceGaps", snapshot.legacyReferenceGaps],
250
+ ["legacyResiduals", snapshot.legacyResiduals],
251
+ ]) {
252
+ if (Number(value || 0) !== 0) blockers.push(`${field}=${value}`);
253
+ }
254
+ if (snapshot.fullCutoverEligible !== true) blockers.push("fullCutoverEligible is not true");
255
+ if (blockers.length) {
256
+ failures.push(`${task.path} migration-full-cutover is not proven: ${blockers.join("; ")}`);
257
+ }
258
+ }
259
+ }
260
+ return { failures, warnings: [] };
261
+ }
262
+
263
+ export function validateContextDocs(target, { strict = true } = {}) {
264
+ const failures = [];
265
+ const warnings = [];
266
+ const report = (message) => {
267
+ if (strict) failures.push(message);
268
+ else warnings.push(`adoption-needed: ${message}`);
269
+ };
270
+ const contextRoots = ["03-ARCHITECTURE", "04-DEVELOPMENT", "06-INTEGRATIONS"];
271
+ const files = contextRoots.flatMap((root) => walkFiles(path.join(target.docsRoot, root))).filter((file) => file.endsWith(".md"));
272
+ for (const file of files) {
273
+ if (file.includes(`${path.sep}_archive${path.sep}`)) continue;
274
+ const relative = toPosix(path.relative(target.projectRoot, file));
275
+ const content = readFileSafe(file);
276
+ if (!/Context Doc Type:\s*\S+/i.test(content) && !/上下文文档类型[::]\s*\S+/.test(content)) report(`${relative} missing Context Doc Type`);
277
+ if (path.basename(file) === "README.md") continue;
278
+ if (!contentHasAny(content, [/Source Evidence/i, "来源证据"])) report(`${relative} missing Source Evidence field`);
279
+ if (!/Last Verified:\s*\S+|Last Verified\s*\|/i.test(content) && !/最近验证[::]\s*\S+|最近验证\s*\|/.test(content)) report(`${relative} missing Last Verified field`);
280
+ if (!/Confidence:\s*(high|medium|low|unknown)|Confidence\s*\|/i.test(content) && !/信心[::]\s*(high|medium|low|unknown|高|中|低|未知)|信心\s*\|/.test(content)) report(`${relative} missing Confidence field`);
281
+ if (/03-ARCHITECTURE\/service-catalog\.md$/.test(relative)) {
282
+ for (const [column, ...aliases] of [
283
+ ["Service / Component", "服务 / 组件"],
284
+ ["Interfaces", "接口"],
285
+ ["Source Evidence", "来源证据"],
286
+ ["Last Verified", "最近验证"],
287
+ ["Confidence", "信心"],
288
+ ]) {
289
+ if (!contentHasAny(content, [column, ...aliases])) report(`${relative} service catalog missing column: ${column}`);
290
+ }
291
+ }
292
+ if (/04-DEVELOPMENT\/external-context\/[^/]+\.md$/.test(relative)) {
293
+ for (const [heading, ...aliases] of [
294
+ ["Development Use", "开发用途"],
295
+ ["Do Not Assume", "不要假设"],
296
+ ["Mocks / Stubs", "Mock / Stub", "模拟 / 桩"],
297
+ ]) {
298
+ if (!contentHasAny(content, [heading, ...aliases])) report(`${relative} external context missing section: ${heading}`);
299
+ }
300
+ }
301
+ if (/06-INTEGRATIONS\/(?:[^/_][^/]*|third-party\/[^/_][^/]*)\.md$/.test(relative)) {
302
+ for (const [heading, ...aliases] of [
303
+ ["Contract Type", "合同类型"],
304
+ ["Auth", "认证"],
305
+ ["Payload", "载荷"],
306
+ ["Errors", "错误"],
307
+ ["Contract Tests", "合同测试"],
308
+ ]) {
309
+ if (!contentHasAny(content, [heading, ...aliases])) report(`${relative} integration contract missing section: ${heading}`);
310
+ }
311
+ }
312
+ }
313
+ return { failures, warnings };
314
+ }
315
+
316
+ export function buildStatus(targetInput, options = {}) {
317
+ const target = normalizeTarget(targetInput);
318
+ const capabilityState = validateCapabilities(target);
319
+ const declaredCapabilities = new Set(capabilityState.registry.capabilities.map((capability) => capability.name));
320
+ const safeAdoptionMode = declaredCapabilities.has("safe-adoption");
321
+ const shouldRunLegacy = !options.skipLegacyCheck && (capabilityState.registry.mode === "legacy-compat" || safeAdoptionMode);
322
+ const legacy = shouldRunLegacy ? runLegacyCheck(target) : { status: "skipped", code: 0, stdout: "", stderr: "" };
323
+ const contractStrict = Boolean(options.strict) || (capabilityState.registry.mode !== "legacy-compat" && !safeAdoptionMode);
324
+ const reviews = validateReviewSchema(target, { strict: contractStrict });
325
+ const visualMaps = validateVisualMaps(target);
326
+ const planContracts = validatePlanContracts(target, { strict: contractStrict });
327
+ const presetContracts = validateTaskPresetContracts(target);
328
+ const contextDocs = validateContextDocs(target, { strict: contractStrict });
329
+ const failures = [...capabilityState.failures, ...reviews.failures, ...visualMaps.failures, ...planContracts.failures, ...presetContracts.failures, ...contextDocs.failures];
330
+ const warnings = [...capabilityState.warnings, ...reviews.warnings, ...visualMaps.warnings, ...planContracts.warnings, ...presetContracts.warnings, ...contextDocs.warnings];
331
+ if (legacy.status === "fail") {
332
+ if (options.strictLegacy) failures.push("legacy check failed");
333
+ else warnings.push(`adoption-needed: legacy check failed: ${(legacy.stderr || legacy.stdout).trim()}`);
334
+ }
335
+
336
+ const tasks = collectTasks(target);
337
+ const briefReady = tasks.filter((task) => task.briefSource === "standalone").length;
338
+ const briefMissing = tasks.length - briefReady;
339
+ for (const task of tasks) {
340
+ if (task.stateSource === "invalid") {
341
+ const message = `${task.path}/progress.md invalid task state: ${task.stateRaw}`;
342
+ if (contractStrict || options.strictLegacy) failures.push(message);
343
+ else warnings.push(`adoption-needed: ${message}`);
344
+ }
345
+ }
346
+ const capabilityNames = new Map(capabilityState.registry.capabilities.map((capability) => [capability.name, capability]));
347
+ for (const detected of capabilityState.detected) {
348
+ if (!capabilityNames.has(detected)) capabilityNames.set(detected, { name: detected, state: "configured" });
349
+ }
350
+ const cutoverCounters = taskCutoverCounters(tasks);
351
+ const fullCutoverEligible =
352
+ failures.length === 0 &&
353
+ warnings.length === 0 &&
354
+ cutoverCounters.legacyVisualOnlyCount === 0 &&
355
+ cutoverCounters.unknownClassificationCount === 0 &&
356
+ cutoverCounters.weakBriefCount === 0 &&
357
+ cutoverCounters.missingCanonicalVisualMapCount === 0;
358
+
359
+ return {
360
+ project: {
361
+ name: path.basename(target.projectRoot),
362
+ root: `TARGET:${target.docsOnly ? toPosix(path.relative(target.projectRoot, target.docsRoot)) : "."}`,
363
+ docsOnly: target.docsOnly,
364
+ },
365
+ schemaVersion: 2,
366
+ generatedAt: new Date().toISOString(),
367
+ mode: capabilityState.registry.mode,
368
+ checkState: {
369
+ status: failures.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "pass",
370
+ failures: failures.length,
371
+ warnings: warnings.length,
372
+ details: { failures, warnings },
373
+ legacy,
374
+ },
375
+ summary: {
376
+ tasks: tasks.length,
377
+ briefCoverage: {
378
+ ready: briefReady,
379
+ missing: briefMissing,
380
+ total: tasks.length,
381
+ },
382
+ visualMapCoverage: {
383
+ canonical: tasks.filter((task) => task.visualMapSource === "canonical").length,
384
+ legacyOnly: cutoverCounters.legacyVisualOnlyCount,
385
+ missing: tasks.filter((task) => task.visualMapStatus === "missing").length,
386
+ total: tasks.length,
387
+ },
388
+ fullCutoverEligible,
389
+ legacyVisualOnlyCount: cutoverCounters.legacyVisualOnlyCount,
390
+ unknownClassificationCount: cutoverCounters.unknownClassificationCount,
391
+ weakBriefCount: cutoverCounters.weakBriefCount,
392
+ visualMapRequiredCount: cutoverCounters.visualMapRequiredCount,
393
+ missingCanonicalVisualMapCount: cutoverCounters.missingCanonicalVisualMapCount,
394
+ },
395
+ capabilities: [...capabilityNames.values()].map((capability) => ({
396
+ name: capability.name,
397
+ state: capability.state || "configured",
398
+ dependencyStatus: capabilityDefinitions[capability.name]?.dependencies.every((dependency) => capabilityNames.has(dependency))
399
+ ? "valid"
400
+ : "invalid",
401
+ warnings: capabilityState.warnings.filter((warning) => warning.includes(capability.name)),
402
+ })),
403
+ tasks,
404
+ handoffs: tasks.flatMap((task) => task.handoffs || []),
405
+ recentActivity: tasks.slice(0, 8).map((task) => ({ at: new Date().toISOString(), type: "task", summary: task.title })),
406
+ };
407
+ }
408
+
409
+ export function renderDashboard(status) {
410
+ const taskCards = status.tasks
411
+ .map((task) => {
412
+ const phases = task.phases
413
+ .map(
414
+ (phase) => `<div class="phase ${escapeHtml(phase.state)}">
415
+ <div class="phase-top"><strong>${escapeHtml(phase.id)}</strong><span>${phase.completion}%</span></div>
416
+ <div class="phase-output">${escapeHtml(phase.output)}</div>
417
+ <div class="meter"><i style="width:${phase.completion}%"></i></div>
418
+ <div class="muted">${escapeHtml(phase.state)} · evidence ${escapeHtml(phase.evidenceStatus)}</div>
419
+ </div>`,
420
+ )
421
+ .join("");
422
+ const risks = task.risks
423
+ .map((risk) => `<span class="risk ${risk.open || risk.blocksRelease ? "open" : ""}">${escapeHtml(risk.severity)} ${escapeHtml(risk.summary)}</span>`)
424
+ .join("");
425
+ const evidence = task.evidence
426
+ .map((item) => `<span class="evidence">${escapeHtml(item.type)} · ${escapeHtml(item.summary)}</span>`)
427
+ .join("");
428
+ const evidenceMeter = evidenceCompletion(task.phases);
429
+ return `<section class="task">
430
+ <div class="task-head">
431
+ <div><h2>${escapeHtml(task.title)}</h2><p>${escapeHtml(task.path)}</p></div>
432
+ <div class="score">${task.completion}%</div>
433
+ </div>
434
+ <div class="meter"><i style="width:${task.completion}%"></i></div>
435
+ <div class="phases">${phases || '<div class="empty">No phase table</div>'}</div>
436
+ <div class="evidence-row"><strong>Evidence</strong><div class="meter small"><i style="width:${evidenceMeter}%"></i></div>${evidence || '<span class="empty">No evidence</span>'}</div>
437
+ <div class="risks">${risks || '<span class="ok">No open visual risk</span>'}</div>
438
+ </section>`;
439
+ })
440
+ .join("");
441
+ const chips = status.capabilities
442
+ .map((capability) => `<span class="chip ${escapeHtml(capability.state)}">${escapeHtml(capability.name)} · ${escapeHtml(capability.state)}</span>`)
443
+ .join("");
444
+ const failures = status.checkState.details.failures.map((failure) => `<li>${escapeHtml(failure)}</li>`).join("");
445
+ const warnings = status.checkState.details.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("");
446
+ const handoffs = status.handoffs
447
+ .map((handoff) => `<span class="handoff">${escapeHtml(handoff.state)} · ${escapeHtml(handoff.summary)}</span>`)
448
+ .join("");
449
+ const activity = status.recentActivity
450
+ .map((item) => `<li><strong>${escapeHtml(item.type)}</strong> ${escapeHtml(item.summary)}</li>`)
451
+ .join("");
452
+ return `<!doctype html>
453
+ <html lang="zh-CN">
454
+ <head>
455
+ <meta charset="utf-8">
456
+ <meta name="viewport" content="width=device-width, initial-scale=1">
457
+ <title>${escapeHtml(status.project.name)} Harness Dashboard</title>
458
+ <style>
459
+ :root{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#17202a;background:#f6f7f9}
460
+ body{margin:0}.shell{max-width:1180px;margin:0 auto;padding:28px}
461
+ header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}
462
+ h1,h2{margin:0;letter-spacing:0}h1{font-size:30px}h2{font-size:18px}p{margin:6px 0;color:#687382}
463
+ .pill,.chip,.risk,.ok{display:inline-flex;align-items:center;border-radius:999px;padding:6px 10px;font-size:12px;margin:4px;background:#e8edf3;color:#273444}
464
+ .pass,.verified{background:#dff5e8;color:#125c32}.warn,.configured{background:#fff0cc;color:#765100}.fail,.open{background:#ffe1df;color:#8a1c12}.scaffolded{background:#e8edf3;color:#273444}
465
+ .grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:20px}.stat,.task{background:#fff;border:1px solid #e4e8ee;border-radius:8px;padding:16px}
466
+ .stat strong{font-size:24px;display:block}.capabilities{margin-bottom:20px}.task{margin-bottom:16px}.task-head{display:flex;justify-content:space-between;gap:16px}
467
+ .score{font-size:28px;font-weight:700;color:#223047}.meter{height:8px;background:#edf1f5;border-radius:99px;overflow:hidden;margin:10px 0}.meter i{display:block;height:100%;background:#2f6fed}.meter.small{height:6px;max-width:180px}
468
+ .evidence,.handoff{display:inline-flex;padding:5px 8px;margin:4px;border-radius:6px;background:#edf7ff;color:#214d72;font-size:12px}.handoff{background:#fff3d8;color:#745000}
469
+ .phases{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin-top:12px}.phase{border:1px solid #e5eaf0;border-radius:8px;padding:12px;background:#fbfcfe}.phase-top{display:flex;justify-content:space-between}.phase-output{min-height:38px;margin-top:8px}
470
+ .risks{margin-top:12px}.empty{color:#8a95a3}.panel{background:#fff;border:1px solid #e4e8ee;border-radius:8px;padding:16px;margin-top:16px}
471
+ @media(max-width:760px){.shell{padding:16px}header{display:block}.grid{grid-template-columns:1fr 1fr}.task-head{display:block}}
472
+ </style>
473
+ </head>
474
+ <body><main class="shell">
475
+ <header>
476
+ <div><h1>${escapeHtml(status.project.name)} Harness Dashboard</h1><p>${escapeHtml(status.project.root)} · ${escapeHtml(status.generatedAt)}</p></div>
477
+ <span class="pill ${escapeHtml(status.checkState.status)}">${escapeHtml(status.checkState.status)} · ${escapeHtml(status.mode)}</span>
478
+ </header>
479
+ <section class="grid">
480
+ <div class="stat"><strong>${status.tasks.length}</strong><span>Tasks</span></div>
481
+ <div class="stat"><strong>${status.capabilities.length}</strong><span>Capabilities</span></div>
482
+ <div class="stat"><strong>${status.checkState.failures}</strong><span>Failures</span></div>
483
+ <div class="stat"><strong>${status.checkState.warnings}</strong><span>Warnings</span></div>
484
+ </section>
485
+ <section class="capabilities">${chips}</section>
486
+ <section class="panel"><h2>Handoffs</h2>${handoffs || '<span class="ok">No pending handoff</span>'}</section>
487
+ ${taskCards || '<section class="task">No tasks found.</section>'}
488
+ <section class="panel"><h2>Recent Activity</h2><ul>${activity || "<li>None</li>"}</ul></section>
489
+ <section class="panel"><h2>Failures</h2><ul>${failures || "<li>None</li>"}</ul><h2>Warnings</h2><ul>${warnings || "<li>None</li>"}</ul></section>
490
+ </main></body></html>`;
491
+ }
492
+
493
+ function escapeHtml(value) {
494
+ return String(value ?? "")
495
+ .replaceAll("&", "&amp;")
496
+ .replaceAll("<", "&lt;")
497
+ .replaceAll(">", "&gt;")
498
+ .replaceAll('"', "&quot;");
499
+ }
500
+
501
+ function evidenceCompletion(phases) {
502
+ const scored = phases.filter((phase) => phase.state !== "skipped");
503
+ if (scored.length === 0) return 0;
504
+ const score = scored.reduce((sum, phase) => {
505
+ if (["present", "waived"].includes(phase.evidenceStatus)) return sum + 100;
506
+ if (phase.evidenceStatus === "partial") return sum + 50;
507
+ return sum;
508
+ }, 0);
509
+ return Math.round(score / scored.length);
510
+ }