@ai-content-space/loopx 0.1.9 → 0.2.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/AGENTS.md +50 -0
- package/README.md +59 -446
- package/README.zh-CN.md +59 -457
- package/docs/loopx/design/loopx-skill-suite-v1-design.md +73 -0
- package/docs/loopx/plans/loopx-skill-suite-v1-implementation.md +77 -0
- package/package.json +5 -2
- package/plugins/loopx/.codex-plugin/plugin.json +4 -4
- package/plugins/loopx/scripts/plugin-install.test.mjs +20 -20
- package/plugins/loopx/skills/clarify/SKILL.md +38 -311
- package/plugins/loopx/skills/debug/SKILL.md +1 -1
- package/plugins/loopx/skills/exec/SKILL.md +71 -0
- package/plugins/loopx/skills/finish/SKILL.md +254 -0
- package/plugins/loopx/skills/fix-review/SKILL.md +216 -0
- package/plugins/loopx/skills/go-style/SKILL.md +1 -1
- package/plugins/loopx/skills/kratos/SKILL.md +1 -1
- package/plugins/loopx/skills/plan/SKILL.md +136 -258
- package/plugins/loopx/skills/refactor-plan/SKILL.md +71 -0
- package/plugins/loopx/skills/review/SKILL.md +72 -105
- package/plugins/loopx/skills/review/code-reviewer.md +168 -0
- package/plugins/loopx/skills/spec/DESIGN_SPEC_TEMPLATE.md +323 -0
- package/plugins/loopx/skills/spec/SKILL.md +76 -0
- package/plugins/loopx/skills/subagent-exec/SKILL.md +282 -0
- package/plugins/loopx/skills/subagent-exec/agents/openai.yaml +3 -0
- package/plugins/loopx/skills/subagent-exec/code-quality-reviewer-prompt.md +25 -0
- package/plugins/loopx/skills/subagent-exec/codex-subagents.md +37 -0
- package/plugins/loopx/skills/subagent-exec/implementer-prompt.md +113 -0
- package/plugins/loopx/skills/subagent-exec/spec-reviewer-prompt.md +61 -0
- package/plugins/loopx/skills/tdd/SKILL.md +1 -1
- package/plugins/loopx/skills/verify/SKILL.md +1 -1
- package/scripts/claude-workflow-hook.mjs +109 -0
- package/scripts/codex-workflow-hook.mjs +2 -5
- package/scripts/install-skills.mjs +3 -3
- package/scripts/verify-skills.mjs +32 -1
- package/skills/RESOLVER.md +26 -17
- package/skills/clarify/SKILL.md +38 -311
- package/skills/debug/SKILL.md +1 -1
- package/skills/exec/SKILL.md +71 -0
- package/skills/finish/SKILL.md +254 -0
- package/skills/fix-review/SKILL.md +216 -0
- package/skills/go-style/SKILL.md +1 -1
- package/skills/kratos/SKILL.md +1 -1
- package/skills/plan/SKILL.md +136 -258
- package/skills/refactor-plan/SKILL.md +71 -0
- package/skills/review/SKILL.md +72 -105
- package/skills/review/code-reviewer.md +168 -0
- package/skills/spec/DESIGN_SPEC_TEMPLATE.md +323 -0
- package/skills/spec/SKILL.md +76 -0
- package/skills/subagent-exec/SKILL.md +282 -0
- package/skills/subagent-exec/agents/openai.yaml +3 -0
- package/skills/subagent-exec/code-quality-reviewer-prompt.md +25 -0
- package/skills/subagent-exec/codex-subagents.md +37 -0
- package/skills/subagent-exec/implementer-prompt.md +113 -0
- package/skills/subagent-exec/spec-reviewer-prompt.md +61 -0
- package/skills/tdd/SKILL.md +1 -1
- package/skills/verify/SKILL.md +1 -1
- package/src/autopilot-runtime.mjs +1 -1
- package/src/cli.mjs +79 -5
- package/src/context-manifest.mjs +2 -2
- package/src/html-views.mjs +457 -95
- package/src/install-discovery.mjs +210 -6
- package/src/next-skill.mjs +2 -4
- package/src/plan-runtime.mjs +572 -93
- package/src/runtime-maintenance.mjs +60 -16
- package/src/workflow.mjs +989 -65
- package/templates/architecture.md +58 -16
- package/templates/development-plan.md +42 -12
- package/plugins/loopx/skills/archive/SKILL.md +0 -55
- package/plugins/loopx/skills/autopilot/SKILL.md +0 -93
- package/plugins/loopx/skills/build/SKILL.md +0 -228
- package/skills/archive/SKILL.md +0 -55
- package/skills/autopilot/SKILL.md +0 -93
- package/skills/build/SKILL.md +0 -228
package/src/html-views.mjs
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
|
|
5
|
-
import { nextSkillCommand } from './next-skill.mjs';
|
|
6
5
|
import { statusSummary } from './workflow.mjs';
|
|
7
6
|
|
|
8
7
|
const WORKFLOW_ARTIFACTS = [
|
|
9
|
-
{ name: 'spec.md', label: '需求工作副本', page: 'intake.html' },
|
|
10
|
-
{ name: 'plan.md', label: '计划', page: 'plan.html' },
|
|
11
|
-
{ name: 'architecture.md', label: '架构', page: 'plan.html' },
|
|
12
|
-
{ name: 'development-plan.md', label: '开发计划', page: 'plan.html' },
|
|
13
|
-
{ name: 'test-plan.md', label: '测试计划', page: 'plan.html' },
|
|
14
|
-
{ name: 'requirement-traceability.md', label: '需求覆盖矩阵', page: 'plan.html' },
|
|
15
|
-
{ name: '
|
|
16
|
-
{ name: '
|
|
8
|
+
{ id: 'spec', name: 'spec.md', label: '需求工作副本', page: 'intake.html' },
|
|
9
|
+
{ id: 'plan', name: 'plan.md', label: '计划', page: 'plan.html' },
|
|
10
|
+
{ id: 'architecture', name: 'architecture.md', label: '架构', page: 'plan.html' },
|
|
11
|
+
{ id: 'development-plan', name: 'development-plan.md', label: '开发计划', page: 'plan.html' },
|
|
12
|
+
{ id: 'test-plan', name: 'test-plan.md', label: '测试计划', page: 'plan.html' },
|
|
13
|
+
{ id: 'requirement-traceability', name: 'requirement-traceability.md', label: '需求覆盖矩阵', page: 'plan.html' },
|
|
14
|
+
{ id: 'plan-delegation-decision', name: 'plan-delegation-decision.md', label: '委派决策', page: 'plan.html' },
|
|
15
|
+
{ id: 'change-proposal', name: 'proposal.md', label: '变更提案', page: 'change.html', changeKey: 'proposal' },
|
|
16
|
+
{ id: 'change-spec-delta', name: 'spec-delta.md', label: '规格增量', page: 'change.html', changeKey: 'specDelta' },
|
|
17
|
+
{ id: 'change-design', name: 'design.md', label: '设计方案', page: 'change.html', changeKey: 'design' },
|
|
18
|
+
{ id: 'change-tasks', name: 'tasks.md', label: '任务拆解', page: 'change.html', changeKey: 'tasks' },
|
|
19
|
+
{ id: 'change-slices', name: 'slices.json', label: '垂直切片', page: 'change.html', changeKey: 'slices' },
|
|
20
|
+
{ id: 'execution-record', name: 'execution-record.md', label: '执行记录', page: 'build.html' },
|
|
21
|
+
{ id: 'review-report', name: 'review-report.md', label: '评审报告', page: 'review.html' },
|
|
17
22
|
];
|
|
18
23
|
|
|
19
24
|
const PAGE_GROUPS = [
|
|
20
|
-
{ file: 'intake.html', title: '需求澄清', artifacts: ['spec
|
|
21
|
-
{ file: 'plan.html', title: '计划与架构', artifacts: ['plan
|
|
22
|
-
{ file: '
|
|
23
|
-
{ file: '
|
|
25
|
+
{ file: 'intake.html', title: '需求澄清', artifacts: ['spec'] },
|
|
26
|
+
{ file: 'plan.html', title: '计划与架构', artifacts: ['plan', 'architecture', 'development-plan', 'test-plan', 'requirement-traceability'] },
|
|
27
|
+
{ file: 'change.html', title: '变更设计方案', artifacts: ['change-proposal', 'change-spec-delta', 'change-design', 'change-tasks', 'change-slices'] },
|
|
28
|
+
{ file: 'build.html', title: '执行与验证', artifacts: ['execution-record'] },
|
|
29
|
+
{ file: 'review.html', title: '评审结论', artifacts: ['review-report'] },
|
|
24
30
|
];
|
|
25
31
|
|
|
26
32
|
function escapeHtml(value) {
|
|
@@ -41,26 +47,56 @@ function htmlDoc({ title, body }) {
|
|
|
41
47
|
' <meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
42
48
|
` <title>${escapeHtml(title)}</title>`,
|
|
43
49
|
' <style>',
|
|
44
|
-
' :root { color-scheme: light; --text: #17202a; --muted: #5f6f7f; --line: #d8e0e8; --bg: #f6f8fa; --panel: #ffffff; --accent: #1769aa; }',
|
|
50
|
+
' :root { color-scheme: light; --text: #17202a; --muted: #5f6f7f; --line: #d8e0e8; --bg: #f6f8fa; --panel: #ffffff; --accent: #1769aa; --ok: #1b7f4d; --warn: #a15c00; --bad: #b42318; --soft: #edf4fb; }',
|
|
45
51
|
' * { box-sizing: border-box; }',
|
|
46
52
|
' body { margin: 0; font: 15px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--text); background: var(--bg); }',
|
|
47
|
-
' main { max-width:
|
|
53
|
+
' main { max-width: 1180px; margin: 0 auto; padding: 28px 20px 48px; }',
|
|
48
54
|
' header { margin-bottom: 20px; }',
|
|
49
55
|
' h1 { margin: 0 0 8px; font-size: 28px; line-height: 1.25; }',
|
|
50
56
|
' h2 { margin: 24px 0 10px; font-size: 18px; }',
|
|
51
57
|
' h3 { margin: 18px 0 8px; font-size: 16px; }',
|
|
58
|
+
' h4 { margin: 14px 0 6px; font-size: 14px; }',
|
|
52
59
|
' a { color: var(--accent); text-decoration: none; }',
|
|
53
60
|
' a:hover { text-decoration: underline; }',
|
|
54
61
|
' .muted { color: var(--muted); }',
|
|
55
62
|
' .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }',
|
|
56
63
|
' .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px; }',
|
|
64
|
+
' .panel h2:first-child { margin-top: 0; }',
|
|
57
65
|
' .badge { display: inline-block; border: 1px solid var(--line); border-radius: 999px; padding: 2px 9px; margin: 2px 4px 2px 0; color: var(--muted); background: #fff; font-size: 12px; }',
|
|
66
|
+
' .badge.ok { color: var(--ok); border-color: #b8dfc9; background: #f0fbf4; }',
|
|
67
|
+
' .badge.warn { color: var(--warn); border-color: #f1d29b; background: #fff8eb; }',
|
|
68
|
+
' .badge.bad { color: var(--bad); border-color: #f0b7b1; background: #fff1f0; }',
|
|
69
|
+
' .review-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 18px; }',
|
|
70
|
+
' .review-nav a { border: 1px solid var(--line); border-radius: 8px; padding: 6px 10px; background: #fff; }',
|
|
71
|
+
' .callout { border-left: 4px solid var(--accent); background: var(--soft); padding: 10px 12px; margin: 12px 0; }',
|
|
72
|
+
' .hero { background: linear-gradient(135deg, #fff, #f7fbff); border: 1px solid var(--line); border-radius: 12px; padding: 18px 20px; margin-bottom: 16px; }',
|
|
73
|
+
' .hero-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 14px; }',
|
|
74
|
+
' .hero-kpi { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-top: 12px; }',
|
|
75
|
+
' .kpi { border: 1px solid var(--line); background: #fff; border-radius: 10px; padding: 10px 12px; }',
|
|
76
|
+
' .kpi .label { color: var(--muted); font-size: 12px; }',
|
|
77
|
+
' .kpi .value { font-size: 18px; font-weight: 700; margin-top: 2px; }',
|
|
78
|
+
' .layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 16px; align-items: start; }',
|
|
79
|
+
' .sticky { position: sticky; top: 14px; }',
|
|
80
|
+
' .stack { display: grid; gap: 12px; }',
|
|
81
|
+
' .visual-map { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; }',
|
|
82
|
+
' .visual-card { border: 1px solid var(--line); border-radius: 10px; padding: 12px; background: #fff; }',
|
|
83
|
+
' .visual-card .title { font-weight: 700; margin-bottom: 4px; }',
|
|
84
|
+
' .visual-card .meta { color: var(--muted); font-size: 12px; }',
|
|
85
|
+
' .outline { border-left: 2px solid var(--line); padding-left: 12px; }',
|
|
86
|
+
' .outline a { display: block; color: var(--text); padding: 3px 0; }',
|
|
58
87
|
' table { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); }',
|
|
59
88
|
' th, td { padding: 8px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }',
|
|
60
89
|
' th { color: var(--muted); font-weight: 600; }',
|
|
61
90
|
' pre { overflow: auto; padding: 12px; background: #0f1720; color: #edf4fb; border-radius: 8px; }',
|
|
62
91
|
' code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }',
|
|
63
|
-
' .markdown { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 16px; }',
|
|
92
|
+
' .markdown { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 16px; margin-bottom: 16px; }',
|
|
93
|
+
' .markdown pre { margin: 10px 0 0; }',
|
|
94
|
+
' .markdown table { margin-top: 10px; }',
|
|
95
|
+
' .markdown ul, .markdown ol { margin-top: 10px; padding-left: 22px; }',
|
|
96
|
+
' .markdown p { margin: 10px 0; }',
|
|
97
|
+
' .markdown h2:first-child, .markdown h3:first-child, .markdown h4:first-child { margin-top: 0; }',
|
|
98
|
+
' .artifact-meta { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 10px; }',
|
|
99
|
+
' @media (max-width: 860px) { .hero-grid, .layout { grid-template-columns: 1fr; } .sticky { position: static; } }',
|
|
64
100
|
' </style>',
|
|
65
101
|
'</head>',
|
|
66
102
|
'<body>',
|
|
@@ -72,12 +108,178 @@ function htmlDoc({ title, body }) {
|
|
|
72
108
|
].join('\n');
|
|
73
109
|
}
|
|
74
110
|
|
|
75
|
-
function
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
111
|
+
function languageMetric(text) {
|
|
112
|
+
const chineseChars = String(text || '').match(/[\u3400-\u9fff]/g) || [];
|
|
113
|
+
const latinChars = String(text || '').match(/[A-Za-z]/g) || [];
|
|
114
|
+
const signalChars = chineseChars.length + latinChars.length;
|
|
115
|
+
const chineseRatio = signalChars === 0 ? 0 : chineseChars.length / signalChars;
|
|
116
|
+
return {
|
|
117
|
+
chineseChars: chineseChars.length,
|
|
118
|
+
latinChars: latinChars.length,
|
|
119
|
+
chineseRatio,
|
|
120
|
+
chineseOk: signalChars > 0 && (chineseChars.length >= 40 || (chineseChars.length >= 8 && chineseRatio >= 0.2)),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function artifactMetrics(text) {
|
|
125
|
+
const lines = String(text || '').split('\n');
|
|
126
|
+
const headings = lines.filter((line) => /^#{1,4}\s+/.test(line)).length;
|
|
127
|
+
const tables = lines.filter((line, index) => line.trim().startsWith('|') && lines[index + 1] && /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[index + 1])).length;
|
|
128
|
+
const todoCount = lines.filter((line) => /\b(TBD|TODO|REPLACE_ME)\b/i.test(line)).length;
|
|
129
|
+
return {
|
|
130
|
+
lines: lines.filter((line) => line.trim()).length,
|
|
131
|
+
headings,
|
|
132
|
+
tables,
|
|
133
|
+
todoCount,
|
|
134
|
+
...languageMetric(text),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function artifactId(artifact) {
|
|
139
|
+
return artifact.id || artifact.name;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function anchorForArtifact(artifact) {
|
|
143
|
+
return artifactId(artifact).replace(/[^a-z0-9-]/gi, '-');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveArtifactPath(root, state, artifact) {
|
|
147
|
+
if (artifact.changeKey) {
|
|
148
|
+
return state.change_artifact_paths?.[artifact.changeKey] || null;
|
|
149
|
+
}
|
|
150
|
+
if (artifact.id === 'spec') {
|
|
151
|
+
const candidate = state.spec_artifact_path || state.clarify_spec_path || null;
|
|
152
|
+
if (candidate) {
|
|
153
|
+
return isAbsolute(candidate) ? candidate : resolve(dirname(dirname(root)), candidate);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return join(root, artifact.name);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractHeadings(text) {
|
|
160
|
+
return String(text || '')
|
|
161
|
+
.split('\n')
|
|
162
|
+
.map((line) => line.match(/^(#{1,4})\s+(.+?)\s*$/))
|
|
163
|
+
.filter(Boolean)
|
|
164
|
+
.map((match) => ({
|
|
165
|
+
level: match[1].length,
|
|
166
|
+
title: match[2].replace(/`/g, '').trim(),
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function firstParagraph(text) {
|
|
171
|
+
return String(text || '')
|
|
172
|
+
.split('\n')
|
|
173
|
+
.map((line) => line.trim())
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.filter((line) => !line.startsWith('#'))
|
|
176
|
+
.filter((line) => !line.startsWith('|'))
|
|
177
|
+
.filter((line) => !/^[-*]\s+/.test(line))
|
|
178
|
+
.filter((line) => !/^\d+[.)]\s+/.test(line))
|
|
179
|
+
.find((line) => line.length > 20) || '';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sectionDigest(text) {
|
|
183
|
+
const headings = extractHeadings(text);
|
|
184
|
+
return {
|
|
185
|
+
title: headings[0]?.title || firstParagraph(text) || '无标题产物',
|
|
186
|
+
outline: headings.filter((item) => item.level <= 3).slice(0, 8),
|
|
187
|
+
summary: firstParagraph(text),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function collectHeadingBlocks(text) {
|
|
192
|
+
const lines = String(text || '').split('\n');
|
|
193
|
+
const blocks = [];
|
|
194
|
+
let current = null;
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
const heading = line.match(/^(#{1,4})\s+(.+?)\s*$/);
|
|
197
|
+
if (heading) {
|
|
198
|
+
if (current) {
|
|
199
|
+
blocks.push(current);
|
|
200
|
+
}
|
|
201
|
+
current = {
|
|
202
|
+
level: heading[1].length,
|
|
203
|
+
title: heading[2].replace(/`/g, '').trim(),
|
|
204
|
+
lines: [],
|
|
205
|
+
};
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (current) {
|
|
209
|
+
current.lines.push(line);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (current) {
|
|
213
|
+
blocks.push(current);
|
|
214
|
+
}
|
|
215
|
+
return blocks;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function blockPreview(block) {
|
|
219
|
+
return block.lines
|
|
220
|
+
.map((line) => line.trim())
|
|
221
|
+
.filter(Boolean)
|
|
222
|
+
.find((line) => !line.startsWith('#') && !line.startsWith('|') && !/^[-*]\s+/.test(line) && !/^\d+[.)]\s+/.test(line)) || '';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function parseFrontmatterBlock(text) {
|
|
226
|
+
if (!String(text || '').startsWith('---\n')) {
|
|
227
|
+
return {};
|
|
228
|
+
}
|
|
229
|
+
const end = String(text).indexOf('\n---\n', 4);
|
|
230
|
+
if (end === -1) {
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
return Object.fromEntries(
|
|
234
|
+
String(text)
|
|
235
|
+
.slice(4, end)
|
|
236
|
+
.split('\n')
|
|
237
|
+
.map((line) => line.trim())
|
|
238
|
+
.filter(Boolean)
|
|
239
|
+
.map((line) => {
|
|
240
|
+
const separator = line.indexOf(':');
|
|
241
|
+
const key = line.slice(0, separator).trim();
|
|
242
|
+
const rawValue = line.slice(separator + 1).trim();
|
|
243
|
+
if (rawValue.startsWith('[') || rawValue.startsWith('{')) {
|
|
244
|
+
try {
|
|
245
|
+
return [key, JSON.parse(rawValue)];
|
|
246
|
+
} catch {
|
|
247
|
+
return [key, rawValue];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return [key, rawValue];
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renderListItems(lines) {
|
|
256
|
+
const items = lines
|
|
257
|
+
.map((line) => line.trim())
|
|
258
|
+
.filter((line) => /^[-*]\s+/.test(line) || /^\d+[.)]\s+/.test(line))
|
|
259
|
+
.map((line) => line.replace(/^[-*]\s+/, '').replace(/^\d+[.)]\s+/, '').trim())
|
|
260
|
+
.filter(Boolean);
|
|
261
|
+
return items.length > 0 ? `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>` : '<span class="muted">无</span>';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderKeyValueTable(entries) {
|
|
265
|
+
return `<table><tbody>${entries.map(([key, value]) => `<tr><th>${escapeHtml(key)}</th><td>${value}</td></tr>`).join('')}</tbody></table>`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function summarizeRequirementBlocks(text) {
|
|
269
|
+
const matches = [...String(text || '').matchAll(/^###\s+Requirement:\s*(.+?)\s*$/gim)];
|
|
270
|
+
return matches.map((match) => ({
|
|
271
|
+
name: match[1].trim(),
|
|
272
|
+
preview: '',
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function parseSlicesJson(text) {
|
|
277
|
+
try {
|
|
278
|
+
const payload = JSON.parse(text);
|
|
279
|
+
return Array.isArray(payload.slices) ? payload.slices : [];
|
|
280
|
+
} catch {
|
|
281
|
+
return [];
|
|
79
282
|
}
|
|
80
|
-
return values.map((item) => `<span class="badge">${escapeHtml(item)}</span>`).join(' ');
|
|
81
283
|
}
|
|
82
284
|
|
|
83
285
|
function markdownToHtml(markdown) {
|
|
@@ -186,38 +388,175 @@ function markdownToHtml(markdown) {
|
|
|
186
388
|
return html.join('\n');
|
|
187
389
|
}
|
|
188
390
|
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
|
|
391
|
+
function replaceDocumentReferences(text) {
|
|
392
|
+
return String(text || '')
|
|
393
|
+
.replace(/^#\s+Clarify Spec:\s*/gim, '# ')
|
|
394
|
+
.replace(/^#+\s+loopx\s+/gim, (match) => match.replace(/loopx\s+/i, ''))
|
|
395
|
+
.replace(/`?architecture\.md`?/g, '架构方案')
|
|
396
|
+
.replace(/`?development-plan\.md`?/g, '开发计划')
|
|
397
|
+
.replace(/`?design\.md`?/g, '详细设计')
|
|
398
|
+
.replace(/`?plan\.md`?/g, '计划')
|
|
399
|
+
.replace(/`?test-plan\.md`?/g, '测试计划')
|
|
400
|
+
.replace(/`?requirement-traceability\.md`?/g, '需求覆盖矩阵')
|
|
401
|
+
.replace(/`?plan-delegation-decision\.md`?/g, '委派决策')
|
|
402
|
+
.replace(/`?spec-delta\.md`?/g, '规格增量')
|
|
403
|
+
.replace(/`?tasks\.md`?/g, '任务拆解')
|
|
404
|
+
.replace(/`?slices\.json`?/g, '切片定义');
|
|
192
405
|
}
|
|
193
406
|
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
407
|
+
function displayTextForArtifact(artifact, text) {
|
|
408
|
+
if (artifact.name.endsWith('.json')) {
|
|
409
|
+
return text;
|
|
410
|
+
}
|
|
411
|
+
const removeSectionTitles = artifact.id === 'plan'
|
|
412
|
+
? [/^状态$/, /^推荐执行入口$/, /^Build 前审阅清单$/i]
|
|
413
|
+
: [];
|
|
414
|
+
const lines = String(text || '').split('\n');
|
|
415
|
+
const kept = [];
|
|
416
|
+
let skippingLevel = null;
|
|
417
|
+
for (const line of lines) {
|
|
418
|
+
const heading = line.match(/^(#{1,4})\s+(.+?)\s*$/);
|
|
419
|
+
if (heading) {
|
|
420
|
+
const level = heading[1].length;
|
|
421
|
+
const title = heading[2].replace(/`/g, '').trim();
|
|
422
|
+
if (skippingLevel !== null && level <= skippingLevel) {
|
|
423
|
+
skippingLevel = null;
|
|
424
|
+
}
|
|
425
|
+
if (skippingLevel === null && removeSectionTitles.some((pattern) => pattern.test(title))) {
|
|
426
|
+
skippingLevel = level;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (skippingLevel !== null) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (/\$build\s+/.test(line) || /\.loopx\//.test(line)) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (/^\s*-\s*(iteration|Architect review|Critic verdict|plan package|execution approved)\s*:/i.test(line)) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
kept.push(line);
|
|
440
|
+
}
|
|
441
|
+
return replaceDocumentReferences(kept.join('\n'));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function artifactDetailHtml(artifact, text) {
|
|
445
|
+
const blocks = collectHeadingBlocks(text);
|
|
446
|
+
const topBlocks = blocks.filter((block) => block.level <= 3).slice(0, 12);
|
|
447
|
+
const sectionCards = topBlocks.length > 0
|
|
448
|
+
? [
|
|
449
|
+
'<section class="panel">',
|
|
450
|
+
'<h3>章节速览</h3>',
|
|
451
|
+
'<div class="visual-map">',
|
|
452
|
+
...topBlocks.map((block) => [
|
|
453
|
+
'<div class="visual-card">',
|
|
454
|
+
`<div class="title">${escapeHtml(block.title)}</div>`,
|
|
455
|
+
`<div class="meta">${escapeHtml(blockPreview(block) || '该章节主要由列表、表格或代码块组成。')}</div>`,
|
|
456
|
+
'</div>',
|
|
457
|
+
].join('')),
|
|
458
|
+
'</div>',
|
|
459
|
+
'</section>',
|
|
460
|
+
].join('\n')
|
|
461
|
+
: '';
|
|
462
|
+
|
|
463
|
+
if (artifact.name === 'spec-delta.md') {
|
|
464
|
+
const frontmatter = parseFrontmatterBlock(text);
|
|
465
|
+
const requirements = summarizeRequirementBlocks(text);
|
|
466
|
+
return [
|
|
467
|
+
'<section class="panel">',
|
|
468
|
+
'<h3>规格增量详解</h3>',
|
|
469
|
+
renderKeyValueTable([
|
|
470
|
+
['change_id', escapeHtml(frontmatter.change_id || '(未声明)')],
|
|
471
|
+
['slug', escapeHtml(frontmatter.slug || '(未声明)')],
|
|
472
|
+
['target_domains', escapeHtml(Array.isArray(frontmatter.target_domains) ? frontmatter.target_domains.join(', ') : (frontmatter.target_domains || '(见正文)'))],
|
|
473
|
+
['requirement_blocks', escapeHtml(requirements.length)],
|
|
474
|
+
]),
|
|
475
|
+
requirements.length > 0 ? [
|
|
476
|
+
'<h4>Requirement Blocks</h4>',
|
|
477
|
+
'<div class="visual-map">',
|
|
478
|
+
...requirements.map((item) => `<div class="visual-card"><div class="title">${escapeHtml(item.name)}</div><div class="meta">包含 SHALL/MUST 与 Scenario 的归档候选需求块。</div></div>`),
|
|
479
|
+
'</div>',
|
|
480
|
+
].join('\n') : '',
|
|
481
|
+
'</section>',
|
|
482
|
+
sectionCards,
|
|
483
|
+
].join('\n');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (artifact.name === 'slices.json') {
|
|
487
|
+
const slices = parseSlicesJson(text);
|
|
488
|
+
return [
|
|
489
|
+
'<section class="panel">',
|
|
490
|
+
'<h3>垂直切片详解</h3>',
|
|
491
|
+
'<div class="visual-map">',
|
|
492
|
+
...(slices.length > 0 ? slices.map((slice) => [
|
|
493
|
+
'<div class="visual-card">',
|
|
494
|
+
`<div class="title">${escapeHtml(slice.id || '(no id)')} ${escapeHtml(slice.title || slice.name || '')}</div>`,
|
|
495
|
+
`<div class="meta">type: ${escapeHtml(slice.type || 'unknown')}</div>`,
|
|
496
|
+
`<p>${escapeHtml(slice.behavior || slice.description || '')}</p>`,
|
|
497
|
+
'<h4>验收</h4>',
|
|
498
|
+
renderListItems(Array.isArray(slice.acceptance_criteria) ? slice.acceptance_criteria.map((item) => `- ${item}`) : (Array.isArray(slice.acceptance) ? slice.acceptance.map((item) => `- ${item}`) : [])),
|
|
499
|
+
`<p class="muted">verification: ${escapeHtml(slice.verification_signal || '(missing)')}</p>`,
|
|
500
|
+
'</div>',
|
|
501
|
+
].join('')) : ['<span class="muted">未解析到 slices。</span>']),
|
|
502
|
+
'</div>',
|
|
503
|
+
'</section>',
|
|
504
|
+
].join('\n');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (artifact.name === 'tasks.md') {
|
|
508
|
+
const checklist = String(text || '').split('\n').filter((line) => /^\s*-\s+\[[ xX]\]\s+/.test(line));
|
|
509
|
+
return [
|
|
510
|
+
'<section class="panel">',
|
|
511
|
+
'<h3>任务拆解详解</h3>',
|
|
512
|
+
renderKeyValueTable([
|
|
513
|
+
['checklist_items', escapeHtml(checklist.length)],
|
|
514
|
+
['open_items', escapeHtml(checklist.filter((line) => /\[\s\]/.test(line)).length)],
|
|
515
|
+
['completed_items', escapeHtml(checklist.filter((line) => /\[[xX]\]/.test(line)).length)],
|
|
516
|
+
]),
|
|
517
|
+
'</section>',
|
|
518
|
+
sectionCards,
|
|
519
|
+
].join('\n');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return sectionCards;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function cleanDocumentTitle(title) {
|
|
526
|
+
return String(title || '')
|
|
527
|
+
.replace(/^loopx\s+/i, '')
|
|
528
|
+
.replace(/^工作流\s+/i, '')
|
|
529
|
+
.replace(/^Clarify Spec:\s*/i, '')
|
|
530
|
+
.trim() || '技术方案';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function pageIntro(group) {
|
|
534
|
+
if (group.file === 'plan.html') {
|
|
535
|
+
return '本页汇总需求方案、架构方案、开发计划、测试计划和需求覆盖,供直接阅读和人工确认。';
|
|
536
|
+
}
|
|
537
|
+
if (group.file === 'change.html') {
|
|
538
|
+
return '本页汇总变更提案、规格增量、详细设计和任务拆解,供确认具体实现边界。';
|
|
539
|
+
}
|
|
540
|
+
if (group.file === 'intake.html') {
|
|
541
|
+
return '本页汇总需求澄清结果,供确认范围、非目标、约束和验收口径。';
|
|
542
|
+
}
|
|
543
|
+
if (group.file === 'build.html') {
|
|
544
|
+
return '本页汇总执行结果和验证证据。';
|
|
545
|
+
}
|
|
546
|
+
if (group.file === 'review.html') {
|
|
547
|
+
return '本页汇总评审结论、问题和后续处理建议。';
|
|
548
|
+
}
|
|
549
|
+
return '本页汇总相关方案内容。';
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function solutionTitle(artifactRows) {
|
|
553
|
+
const preferred = artifactRows.find((artifact) => ['plan', 'spec', 'change-proposal'].includes(artifactId(artifact)) && artifact.exists)
|
|
554
|
+
|| artifactRows.find((artifact) => artifact.exists);
|
|
555
|
+
if (!preferred) {
|
|
556
|
+
return '技术方案';
|
|
557
|
+
}
|
|
558
|
+
const text = await readFile(preferred.path, 'utf8');
|
|
559
|
+
return cleanDocumentTitle(sectionDigest(text).title);
|
|
221
560
|
}
|
|
222
561
|
|
|
223
562
|
async function renderWorkflowPages(status) {
|
|
@@ -226,66 +565,99 @@ async function renderWorkflowPages(status) {
|
|
|
226
565
|
await mkdir(viewRoot, { recursive: true });
|
|
227
566
|
|
|
228
567
|
const artifactRows = WORKFLOW_ARTIFACTS.map((artifact) => {
|
|
229
|
-
const artifactPath =
|
|
568
|
+
const artifactPath = resolveArtifactPath(root, status.state || {}, artifact);
|
|
230
569
|
return {
|
|
231
570
|
...artifact,
|
|
232
571
|
path: artifactPath,
|
|
233
|
-
exists: existsSync(artifactPath),
|
|
572
|
+
exists: Boolean(artifactPath) && existsSync(artifactPath),
|
|
234
573
|
};
|
|
235
574
|
});
|
|
236
575
|
|
|
576
|
+
const renderArtifactBody = async (artifact) => {
|
|
577
|
+
const text = await readFile(artifact.path, 'utf8');
|
|
578
|
+
const displayText = displayTextForArtifact(artifact, text);
|
|
579
|
+
const metrics = artifactMetrics(displayText);
|
|
580
|
+
const digest = sectionDigest(displayText);
|
|
581
|
+
const isJson = artifact.name.endsWith('.json');
|
|
582
|
+
const detailHtml = artifactDetailHtml(artifact, displayText);
|
|
583
|
+
return {
|
|
584
|
+
text: displayText,
|
|
585
|
+
metrics,
|
|
586
|
+
digest,
|
|
587
|
+
detailHtml,
|
|
588
|
+
body: isJson
|
|
589
|
+
? `<pre><code>${escapeHtml(displayText)}</code></pre>`
|
|
590
|
+
: markdownToHtml(displayText),
|
|
591
|
+
};
|
|
592
|
+
};
|
|
593
|
+
|
|
237
594
|
for (const group of PAGE_GROUPS) {
|
|
238
595
|
const sections = [];
|
|
239
|
-
|
|
240
|
-
|
|
596
|
+
const availableArtifacts = artifactRows.filter((artifact) => group.artifacts.includes(artifactId(artifact)) && artifact.exists);
|
|
597
|
+
const nav = availableArtifacts.length > 0
|
|
598
|
+
? `<nav class="review-nav">${availableArtifacts.map((artifact) => `<a href="#${escapeHtml(anchorForArtifact(artifact))}">${escapeHtml(artifact.label)}</a>`).join('')}</nav>`
|
|
599
|
+
: '';
|
|
600
|
+
for (const artifactIdValue of group.artifacts) {
|
|
601
|
+
const artifact = artifactRows.find((item) => artifactId(item) === artifactIdValue);
|
|
241
602
|
if (!artifact?.exists) {
|
|
242
603
|
continue;
|
|
243
604
|
}
|
|
244
|
-
const
|
|
605
|
+
const rendered = await renderArtifactBody(artifact);
|
|
245
606
|
sections.push([
|
|
246
|
-
`<section class="markdown">`,
|
|
247
|
-
`<
|
|
248
|
-
|
|
607
|
+
`<section class="markdown" id="${escapeHtml(anchorForArtifact(artifact))}">`,
|
|
608
|
+
`<h2>${escapeHtml(artifact.label)}</h2>`,
|
|
609
|
+
`<p>${escapeHtml(rendered.digest.summary || '该部分正文如下。')}</p>`,
|
|
610
|
+
rendered.detailHtml,
|
|
611
|
+
rendered.body,
|
|
249
612
|
'</section>',
|
|
250
613
|
].join('\n'));
|
|
251
614
|
}
|
|
252
615
|
await writeFile(join(viewRoot, group.file), htmlDoc({
|
|
253
|
-
title:
|
|
616
|
+
title: group.title,
|
|
254
617
|
body: [
|
|
255
618
|
'<header>',
|
|
256
619
|
`<h1>${escapeHtml(group.title)}</h1>`,
|
|
257
|
-
`<
|
|
258
|
-
'<p><a href="index.html"
|
|
620
|
+
`<div class="callout">${escapeHtml(pageIntro(group))}</div>`,
|
|
621
|
+
'<p><a href="index.html">返回方案总览</a></p>',
|
|
622
|
+
nav,
|
|
259
623
|
'</header>',
|
|
260
624
|
sections.length > 0 ? sections.join('\n') : '<section class="panel muted">暂无对应产物。</section>',
|
|
261
625
|
].join('\n'),
|
|
262
626
|
}));
|
|
263
627
|
}
|
|
264
628
|
|
|
629
|
+
const title = await solutionTitle(artifactRows);
|
|
630
|
+
const pageRows = PAGE_GROUPS.map((group) => {
|
|
631
|
+
const availableArtifacts = artifactRows.filter((artifact) => group.artifacts.includes(artifactId(artifact)) && artifact.exists);
|
|
632
|
+
if (availableArtifacts.length === 0) {
|
|
633
|
+
return '';
|
|
634
|
+
}
|
|
635
|
+
return [
|
|
636
|
+
'<tr>',
|
|
637
|
+
`<td><a href="${escapeHtml(group.file)}">${escapeHtml(group.title)}</a></td>`,
|
|
638
|
+
`<td>${escapeHtml(availableArtifacts.map((artifact) => artifact.label).join('、'))}</td>`,
|
|
639
|
+
`<td>${escapeHtml(pageIntro(group))}</td>`,
|
|
640
|
+
'</tr>',
|
|
641
|
+
].join('');
|
|
642
|
+
}).filter(Boolean);
|
|
643
|
+
|
|
265
644
|
const indexBody = [
|
|
266
645
|
'<header>',
|
|
267
|
-
|
|
268
|
-
`<p class="muted"
|
|
646
|
+
'<h1>技术方案总览</h1>',
|
|
647
|
+
`<p class="muted">${escapeHtml(title)}</p>`,
|
|
648
|
+
'<div class="callout">本页面提供完整 HTML 阅读入口;各页面已内嵌方案正文,可直接阅读和确认。</div>',
|
|
269
649
|
'</header>',
|
|
270
|
-
statusPanels(status),
|
|
271
650
|
'<section class="panel">',
|
|
272
|
-
'<h2
|
|
273
|
-
'<table><thead><tr><th
|
|
274
|
-
...
|
|
275
|
-
'<tr>',
|
|
276
|
-
`<td>${escapeHtml(artifact.label)}</td>`,
|
|
277
|
-
`<td>${artifact.exists ? '存在' : '缺失'}</td>`,
|
|
278
|
-
`<td>${artifact.exists ? `<a href="${escapeHtml(artifact.page)}">${escapeHtml(basename(artifact.page))}</a>` : '<span class="muted">无</span>'}</td>`,
|
|
279
|
-
`<td>${artifact.exists ? artifactLink(viewRoot, artifact.path, artifact.name) : '<span class="muted">无</span>'}</td>`,
|
|
280
|
-
'</tr>',
|
|
281
|
-
].join('')),
|
|
651
|
+
'<h2>方案阅读目录</h2>',
|
|
652
|
+
'<table><thead><tr><th>页面</th><th>包含内容</th><th>阅读重点</th></tr></thead><tbody>',
|
|
653
|
+
...pageRows,
|
|
282
654
|
'</tbody></table>',
|
|
283
655
|
'</section>',
|
|
284
656
|
].join('\n');
|
|
285
657
|
|
|
286
658
|
const workflowViewPath = join(viewRoot, 'index.html');
|
|
287
659
|
await writeFile(workflowViewPath, htmlDoc({
|
|
288
|
-
title:
|
|
660
|
+
title: '技术方案总览',
|
|
289
661
|
body: indexBody,
|
|
290
662
|
}));
|
|
291
663
|
|
|
@@ -301,27 +673,20 @@ async function renderWorkspaceIndex(workspaceStatus, renderedSlugs = []) {
|
|
|
301
673
|
const link = rendered.has(workflow.slug)
|
|
302
674
|
? `<a href="${escapeHtml(href)}">${escapeHtml(workflow.slug)}</a>`
|
|
303
675
|
: escapeHtml(workflow.slug);
|
|
304
|
-
return
|
|
305
|
-
'<tr>',
|
|
306
|
-
`<td>${link}</td>`,
|
|
307
|
-
`<td>${escapeHtml(workflow.current_stage || '(none)')}</td>`,
|
|
308
|
-
`<td>${escapeHtml(workflow.contract)}</td>`,
|
|
309
|
-
`<td>${escapeHtml(workflow.missing_artifact_count)}</td>`,
|
|
310
|
-
'</tr>',
|
|
311
|
-
].join('');
|
|
676
|
+
return `<tr><td>${link}</td><td>技术方案总览</td></tr>`;
|
|
312
677
|
});
|
|
313
678
|
const workspaceViewPath = join(viewsRoot, 'index.html');
|
|
314
679
|
await writeFile(workspaceViewPath, htmlDoc({
|
|
315
|
-
title: '
|
|
680
|
+
title: '方案阅读入口',
|
|
316
681
|
body: [
|
|
317
682
|
'<header>',
|
|
318
|
-
'<h1
|
|
319
|
-
|
|
683
|
+
'<h1>方案阅读入口</h1>',
|
|
684
|
+
'<p class="muted">选择一个需求方案,进入完整 HTML 阅读页。</p>',
|
|
320
685
|
'</header>',
|
|
321
686
|
'<section class="panel">',
|
|
322
|
-
'<h2
|
|
323
|
-
'<table><thead><tr><th
|
|
324
|
-
rows.join('\n') || '<tr><td colspan="
|
|
687
|
+
'<h2>方案列表</h2>',
|
|
688
|
+
'<table><thead><tr><th>方案</th><th>阅读入口</th></tr></thead><tbody>',
|
|
689
|
+
rows.join('\n') || '<tr><td colspan="2" class="muted">暂无方案。</td></tr>',
|
|
325
690
|
'</tbody></table>',
|
|
326
691
|
'</section>',
|
|
327
692
|
].join('\n'),
|
|
@@ -338,9 +703,6 @@ export async function renderHtmlViews(cwd, { slug = null, all = false } = {}) {
|
|
|
338
703
|
const workflowViews = [];
|
|
339
704
|
if (all || !slug) {
|
|
340
705
|
for (const workflow of workspaceStatus.workflows) {
|
|
341
|
-
if (workflow.legacy) {
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
706
|
const workflowStatus = await statusSummary(cwd, workflow.slug);
|
|
345
707
|
workflowViews.push({
|
|
346
708
|
slug: workflow.slug,
|
|
@@ -349,7 +711,7 @@ export async function renderHtmlViews(cwd, { slug = null, all = false } = {}) {
|
|
|
349
711
|
}
|
|
350
712
|
} else if (slug) {
|
|
351
713
|
const workflowStatus = await statusSummary(cwd, slug);
|
|
352
|
-
if (!workflowStatus.state
|
|
714
|
+
if (!workflowStatus.state) {
|
|
353
715
|
throw new Error('render_workflow_not_available');
|
|
354
716
|
}
|
|
355
717
|
workflowViews.push({
|