@ai-content-space/loopx 0.1.9 → 0.1.10
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/README.md +5 -1
- package/README.zh-CN.md +5 -1
- package/package.json +1 -1
- package/plugins/loopx/.codex-plugin/plugin.json +1 -1
- package/plugins/loopx/skills/archive/SKILL.md +1 -1
- package/plugins/loopx/skills/autopilot/SKILL.md +1 -1
- package/plugins/loopx/skills/build/SKILL.md +1 -1
- package/plugins/loopx/skills/clarify/SKILL.md +1 -1
- package/plugins/loopx/skills/debug/SKILL.md +1 -1
- 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 +12 -1
- package/plugins/loopx/skills/review/SKILL.md +1 -1
- package/plugins/loopx/skills/tdd/SKILL.md +1 -1
- package/plugins/loopx/skills/verify/SKILL.md +1 -1
- package/skills/archive/SKILL.md +1 -1
- package/skills/autopilot/SKILL.md +1 -1
- package/skills/build/SKILL.md +1 -1
- package/skills/clarify/SKILL.md +1 -1
- package/skills/debug/SKILL.md +1 -1
- package/skills/go-style/SKILL.md +1 -1
- package/skills/kratos/SKILL.md +1 -1
- package/skills/plan/SKILL.md +12 -1
- package/skills/review/SKILL.md +1 -1
- package/skills/tdd/SKILL.md +1 -1
- package/skills/verify/SKILL.md +1 -1
- package/src/cli.mjs +2 -0
- package/src/html-views.mjs +463 -35
- package/src/plan-runtime.mjs +2 -1
- package/src/runtime-maintenance.mjs +55 -14
- package/src/workflow.mjs +140 -13
package/src/html-views.mjs
CHANGED
|
@@ -6,21 +6,28 @@ import { nextSkillCommand } from './next-skill.mjs';
|
|
|
6
6
|
import { statusSummary } from './workflow.mjs';
|
|
7
7
|
|
|
8
8
|
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: '
|
|
9
|
+
{ id: 'spec', name: 'spec.md', label: '需求工作副本', page: 'intake.html' },
|
|
10
|
+
{ id: 'plan', name: 'plan.md', label: '计划', page: 'plan.html' },
|
|
11
|
+
{ id: 'architecture', name: 'architecture.md', label: '架构', page: 'plan.html' },
|
|
12
|
+
{ id: 'development-plan', name: 'development-plan.md', label: '开发计划', page: 'plan.html' },
|
|
13
|
+
{ id: 'test-plan', name: 'test-plan.md', label: '测试计划', page: 'plan.html' },
|
|
14
|
+
{ id: 'requirement-traceability', name: 'requirement-traceability.md', label: '需求覆盖矩阵', page: 'plan.html' },
|
|
15
|
+
{ id: 'plan-delegation-decision', name: 'plan-delegation-decision.md', label: '委派决策', page: 'plan.html' },
|
|
16
|
+
{ id: 'change-proposal', name: 'proposal.md', label: '变更提案', page: 'change.html', changeKey: 'proposal' },
|
|
17
|
+
{ id: 'change-spec-delta', name: 'spec-delta.md', label: '规格增量', page: 'change.html', changeKey: 'specDelta' },
|
|
18
|
+
{ id: 'change-design', name: 'design.md', label: '设计方案', page: 'change.html', changeKey: 'design' },
|
|
19
|
+
{ id: 'change-tasks', name: 'tasks.md', label: '任务拆解', page: 'change.html', changeKey: 'tasks' },
|
|
20
|
+
{ id: 'change-slices', name: 'slices.json', label: '垂直切片', page: 'change.html', changeKey: 'slices' },
|
|
21
|
+
{ id: 'execution-record', name: 'execution-record.md', label: '执行记录', page: 'build.html' },
|
|
22
|
+
{ id: 'review-report', name: 'review-report.md', label: '评审报告', page: 'review.html' },
|
|
17
23
|
];
|
|
18
24
|
|
|
19
25
|
const PAGE_GROUPS = [
|
|
20
|
-
{ file: 'intake.html', title: '需求澄清', artifacts: ['spec
|
|
21
|
-
{ file: 'plan.html', title: '计划与架构', artifacts: ['plan
|
|
22
|
-
{ file: '
|
|
23
|
-
{ file: '
|
|
26
|
+
{ file: 'intake.html', title: '需求澄清', artifacts: ['spec'] },
|
|
27
|
+
{ file: 'plan.html', title: '计划与架构', artifacts: ['plan', 'architecture', 'development-plan', 'test-plan', 'requirement-traceability', 'plan-delegation-decision'] },
|
|
28
|
+
{ file: 'change.html', title: '变更设计方案', artifacts: ['change-proposal', 'change-spec-delta', 'change-design', 'change-tasks', 'change-slices'] },
|
|
29
|
+
{ file: 'build.html', title: '执行与验证', artifacts: ['execution-record'] },
|
|
30
|
+
{ file: 'review.html', title: '评审结论', artifacts: ['review-report'] },
|
|
24
31
|
];
|
|
25
32
|
|
|
26
33
|
function escapeHtml(value) {
|
|
@@ -41,26 +48,56 @@ function htmlDoc({ title, body }) {
|
|
|
41
48
|
' <meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
42
49
|
` <title>${escapeHtml(title)}</title>`,
|
|
43
50
|
' <style>',
|
|
44
|
-
' :root { color-scheme: light; --text: #17202a; --muted: #5f6f7f; --line: #d8e0e8; --bg: #f6f8fa; --panel: #ffffff; --accent: #1769aa; }',
|
|
51
|
+
' :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
52
|
' * { box-sizing: border-box; }',
|
|
46
53
|
' body { margin: 0; font: 15px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--text); background: var(--bg); }',
|
|
47
|
-
' main { max-width:
|
|
54
|
+
' main { max-width: 1180px; margin: 0 auto; padding: 28px 20px 48px; }',
|
|
48
55
|
' header { margin-bottom: 20px; }',
|
|
49
56
|
' h1 { margin: 0 0 8px; font-size: 28px; line-height: 1.25; }',
|
|
50
57
|
' h2 { margin: 24px 0 10px; font-size: 18px; }',
|
|
51
58
|
' h3 { margin: 18px 0 8px; font-size: 16px; }',
|
|
59
|
+
' h4 { margin: 14px 0 6px; font-size: 14px; }',
|
|
52
60
|
' a { color: var(--accent); text-decoration: none; }',
|
|
53
61
|
' a:hover { text-decoration: underline; }',
|
|
54
62
|
' .muted { color: var(--muted); }',
|
|
55
63
|
' .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }',
|
|
56
64
|
' .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px; }',
|
|
65
|
+
' .panel h2:first-child { margin-top: 0; }',
|
|
57
66
|
' .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; }',
|
|
67
|
+
' .badge.ok { color: var(--ok); border-color: #b8dfc9; background: #f0fbf4; }',
|
|
68
|
+
' .badge.warn { color: var(--warn); border-color: #f1d29b; background: #fff8eb; }',
|
|
69
|
+
' .badge.bad { color: var(--bad); border-color: #f0b7b1; background: #fff1f0; }',
|
|
70
|
+
' .review-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 18px; }',
|
|
71
|
+
' .review-nav a { border: 1px solid var(--line); border-radius: 8px; padding: 6px 10px; background: #fff; }',
|
|
72
|
+
' .callout { border-left: 4px solid var(--accent); background: var(--soft); padding: 10px 12px; margin: 12px 0; }',
|
|
73
|
+
' .hero { background: linear-gradient(135deg, #fff, #f7fbff); border: 1px solid var(--line); border-radius: 12px; padding: 18px 20px; margin-bottom: 16px; }',
|
|
74
|
+
' .hero-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 14px; }',
|
|
75
|
+
' .hero-kpi { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-top: 12px; }',
|
|
76
|
+
' .kpi { border: 1px solid var(--line); background: #fff; border-radius: 10px; padding: 10px 12px; }',
|
|
77
|
+
' .kpi .label { color: var(--muted); font-size: 12px; }',
|
|
78
|
+
' .kpi .value { font-size: 18px; font-weight: 700; margin-top: 2px; }',
|
|
79
|
+
' .layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 16px; align-items: start; }',
|
|
80
|
+
' .sticky { position: sticky; top: 14px; }',
|
|
81
|
+
' .stack { display: grid; gap: 12px; }',
|
|
82
|
+
' .visual-map { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; }',
|
|
83
|
+
' .visual-card { border: 1px solid var(--line); border-radius: 10px; padding: 12px; background: #fff; }',
|
|
84
|
+
' .visual-card .title { font-weight: 700; margin-bottom: 4px; }',
|
|
85
|
+
' .visual-card .meta { color: var(--muted); font-size: 12px; }',
|
|
86
|
+
' .outline { border-left: 2px solid var(--line); padding-left: 12px; }',
|
|
87
|
+
' .outline a { display: block; color: var(--text); padding: 3px 0; }',
|
|
58
88
|
' table { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); }',
|
|
59
89
|
' th, td { padding: 8px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }',
|
|
60
90
|
' th { color: var(--muted); font-weight: 600; }',
|
|
61
91
|
' pre { overflow: auto; padding: 12px; background: #0f1720; color: #edf4fb; border-radius: 8px; }',
|
|
62
92
|
' code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }',
|
|
63
|
-
' .markdown { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 16px; }',
|
|
93
|
+
' .markdown { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 16px; margin-bottom: 16px; }',
|
|
94
|
+
' .markdown pre { margin: 10px 0 0; }',
|
|
95
|
+
' .markdown table { margin-top: 10px; }',
|
|
96
|
+
' .markdown ul, .markdown ol { margin-top: 10px; padding-left: 22px; }',
|
|
97
|
+
' .markdown p { margin: 10px 0; }',
|
|
98
|
+
' .markdown h2:first-child, .markdown h3:first-child, .markdown h4:first-child { margin-top: 0; }',
|
|
99
|
+
' .artifact-meta { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 10px; }',
|
|
100
|
+
' @media (max-width: 860px) { .hero-grid, .layout { grid-template-columns: 1fr; } .sticky { position: static; } }',
|
|
64
101
|
' </style>',
|
|
65
102
|
'</head>',
|
|
66
103
|
'<body>',
|
|
@@ -72,6 +109,184 @@ function htmlDoc({ title, body }) {
|
|
|
72
109
|
].join('\n');
|
|
73
110
|
}
|
|
74
111
|
|
|
112
|
+
function languageMetric(text) {
|
|
113
|
+
const chineseChars = String(text || '').match(/[\u3400-\u9fff]/g) || [];
|
|
114
|
+
const latinChars = String(text || '').match(/[A-Za-z]/g) || [];
|
|
115
|
+
const signalChars = chineseChars.length + latinChars.length;
|
|
116
|
+
const chineseRatio = signalChars === 0 ? 0 : chineseChars.length / signalChars;
|
|
117
|
+
return {
|
|
118
|
+
chineseChars: chineseChars.length,
|
|
119
|
+
latinChars: latinChars.length,
|
|
120
|
+
chineseRatio,
|
|
121
|
+
chineseOk: signalChars > 0 && (chineseChars.length >= 40 || (chineseChars.length >= 8 && chineseRatio >= 0.2)),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function artifactMetrics(text) {
|
|
126
|
+
const lines = String(text || '').split('\n');
|
|
127
|
+
const headings = lines.filter((line) => /^#{1,4}\s+/.test(line)).length;
|
|
128
|
+
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;
|
|
129
|
+
const todoCount = lines.filter((line) => /\b(TBD|TODO|REPLACE_ME)\b/i.test(line)).length;
|
|
130
|
+
return {
|
|
131
|
+
lines: lines.filter((line) => line.trim()).length,
|
|
132
|
+
headings,
|
|
133
|
+
tables,
|
|
134
|
+
todoCount,
|
|
135
|
+
...languageMetric(text),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function artifactId(artifact) {
|
|
140
|
+
return artifact.id || artifact.name;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function anchorForArtifact(artifact) {
|
|
144
|
+
return artifactId(artifact).replace(/[^a-z0-9-]/gi, '-');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveArtifactPath(root, state, artifact) {
|
|
148
|
+
if (artifact.changeKey) {
|
|
149
|
+
return state.change_artifact_paths?.[artifact.changeKey] || null;
|
|
150
|
+
}
|
|
151
|
+
return join(root, artifact.name);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function statusBadge(label, status) {
|
|
155
|
+
const normalized = String(status ?? '').toLowerCase();
|
|
156
|
+
const cls = ['true', 'complete', 'approved', 'written', 'ready', 'exists', 'ok'].includes(normalized)
|
|
157
|
+
? 'ok'
|
|
158
|
+
: ['false', 'missing', 'blocked', 'partial', 'failed', 'none', 'needs-review'].includes(normalized)
|
|
159
|
+
? 'bad'
|
|
160
|
+
: 'warn';
|
|
161
|
+
return `<span class="badge ${cls}">${escapeHtml(label)}: ${escapeHtml(status)}</span>`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function extractHeadings(text) {
|
|
165
|
+
return String(text || '')
|
|
166
|
+
.split('\n')
|
|
167
|
+
.map((line) => line.match(/^(#{1,4})\s+(.+?)\s*$/))
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.map((match) => ({
|
|
170
|
+
level: match[1].length,
|
|
171
|
+
title: match[2].replace(/`/g, '').trim(),
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function firstParagraph(text) {
|
|
176
|
+
return String(text || '')
|
|
177
|
+
.split('\n')
|
|
178
|
+
.map((line) => line.trim())
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.filter((line) => !line.startsWith('#'))
|
|
181
|
+
.filter((line) => !line.startsWith('|'))
|
|
182
|
+
.filter((line) => !/^[-*]\s+/.test(line))
|
|
183
|
+
.filter((line) => !/^\d+[.)]\s+/.test(line))
|
|
184
|
+
.find((line) => line.length > 20) || '';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function sectionDigest(text) {
|
|
188
|
+
const headings = extractHeadings(text);
|
|
189
|
+
return {
|
|
190
|
+
title: headings[0]?.title || firstParagraph(text) || '无标题产物',
|
|
191
|
+
outline: headings.filter((item) => item.level <= 3).slice(0, 8),
|
|
192
|
+
summary: firstParagraph(text),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function collectHeadingBlocks(text) {
|
|
197
|
+
const lines = String(text || '').split('\n');
|
|
198
|
+
const blocks = [];
|
|
199
|
+
let current = null;
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
const heading = line.match(/^(#{1,4})\s+(.+?)\s*$/);
|
|
202
|
+
if (heading) {
|
|
203
|
+
if (current) {
|
|
204
|
+
blocks.push(current);
|
|
205
|
+
}
|
|
206
|
+
current = {
|
|
207
|
+
level: heading[1].length,
|
|
208
|
+
title: heading[2].replace(/`/g, '').trim(),
|
|
209
|
+
lines: [],
|
|
210
|
+
};
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (current) {
|
|
214
|
+
current.lines.push(line);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (current) {
|
|
218
|
+
blocks.push(current);
|
|
219
|
+
}
|
|
220
|
+
return blocks;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function blockPreview(block) {
|
|
224
|
+
return block.lines
|
|
225
|
+
.map((line) => line.trim())
|
|
226
|
+
.filter(Boolean)
|
|
227
|
+
.find((line) => !line.startsWith('#') && !line.startsWith('|') && !/^[-*]\s+/.test(line) && !/^\d+[.)]\s+/.test(line)) || '';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseFrontmatterBlock(text) {
|
|
231
|
+
if (!String(text || '').startsWith('---\n')) {
|
|
232
|
+
return {};
|
|
233
|
+
}
|
|
234
|
+
const end = String(text).indexOf('\n---\n', 4);
|
|
235
|
+
if (end === -1) {
|
|
236
|
+
return {};
|
|
237
|
+
}
|
|
238
|
+
return Object.fromEntries(
|
|
239
|
+
String(text)
|
|
240
|
+
.slice(4, end)
|
|
241
|
+
.split('\n')
|
|
242
|
+
.map((line) => line.trim())
|
|
243
|
+
.filter(Boolean)
|
|
244
|
+
.map((line) => {
|
|
245
|
+
const separator = line.indexOf(':');
|
|
246
|
+
const key = line.slice(0, separator).trim();
|
|
247
|
+
const rawValue = line.slice(separator + 1).trim();
|
|
248
|
+
if (rawValue.startsWith('[') || rawValue.startsWith('{')) {
|
|
249
|
+
try {
|
|
250
|
+
return [key, JSON.parse(rawValue)];
|
|
251
|
+
} catch {
|
|
252
|
+
return [key, rawValue];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return [key, rawValue];
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function renderListItems(lines) {
|
|
261
|
+
const items = lines
|
|
262
|
+
.map((line) => line.trim())
|
|
263
|
+
.filter((line) => /^[-*]\s+/.test(line) || /^\d+[.)]\s+/.test(line))
|
|
264
|
+
.map((line) => line.replace(/^[-*]\s+/, '').replace(/^\d+[.)]\s+/, '').trim())
|
|
265
|
+
.filter(Boolean);
|
|
266
|
+
return items.length > 0 ? `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>` : '<span class="muted">无</span>';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function renderKeyValueTable(entries) {
|
|
270
|
+
return `<table><tbody>${entries.map(([key, value]) => `<tr><th>${escapeHtml(key)}</th><td>${value}</td></tr>`).join('')}</tbody></table>`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function summarizeRequirementBlocks(text) {
|
|
274
|
+
const matches = [...String(text || '').matchAll(/^###\s+Requirement:\s*(.+?)\s*$/gim)];
|
|
275
|
+
return matches.map((match) => ({
|
|
276
|
+
name: match[1].trim(),
|
|
277
|
+
preview: '',
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function parseSlicesJson(text) {
|
|
282
|
+
try {
|
|
283
|
+
const payload = JSON.parse(text);
|
|
284
|
+
return Array.isArray(payload.slices) ? payload.slices : [];
|
|
285
|
+
} catch {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
75
290
|
function listItems(items) {
|
|
76
291
|
const values = Array.isArray(items) ? items.filter(Boolean) : [];
|
|
77
292
|
if (values.length === 0) {
|
|
@@ -186,11 +401,137 @@ function markdownToHtml(markdown) {
|
|
|
186
401
|
return html.join('\n');
|
|
187
402
|
}
|
|
188
403
|
|
|
404
|
+
function artifactDetailHtml(artifact, text) {
|
|
405
|
+
const blocks = collectHeadingBlocks(text);
|
|
406
|
+
const topBlocks = blocks.filter((block) => block.level <= 3).slice(0, 12);
|
|
407
|
+
const sectionCards = topBlocks.length > 0
|
|
408
|
+
? [
|
|
409
|
+
'<section class="panel">',
|
|
410
|
+
'<h3>章节速览</h3>',
|
|
411
|
+
'<div class="visual-map">',
|
|
412
|
+
...topBlocks.map((block) => [
|
|
413
|
+
'<div class="visual-card">',
|
|
414
|
+
`<div class="title">${escapeHtml(block.title)}</div>`,
|
|
415
|
+
`<div class="meta">${escapeHtml(blockPreview(block) || '该章节主要由列表、表格或代码块组成。')}</div>`,
|
|
416
|
+
'</div>',
|
|
417
|
+
].join('')),
|
|
418
|
+
'</div>',
|
|
419
|
+
'</section>',
|
|
420
|
+
].join('\n')
|
|
421
|
+
: '';
|
|
422
|
+
|
|
423
|
+
if (artifact.name === 'spec-delta.md') {
|
|
424
|
+
const frontmatter = parseFrontmatterBlock(text);
|
|
425
|
+
const requirements = summarizeRequirementBlocks(text);
|
|
426
|
+
return [
|
|
427
|
+
'<section class="panel">',
|
|
428
|
+
'<h3>规格增量详解</h3>',
|
|
429
|
+
renderKeyValueTable([
|
|
430
|
+
['change_id', escapeHtml(frontmatter.change_id || '(未声明)')],
|
|
431
|
+
['slug', escapeHtml(frontmatter.slug || '(未声明)')],
|
|
432
|
+
['target_domains', escapeHtml(Array.isArray(frontmatter.target_domains) ? frontmatter.target_domains.join(', ') : (frontmatter.target_domains || '(见正文)'))],
|
|
433
|
+
['requirement_blocks', escapeHtml(requirements.length)],
|
|
434
|
+
]),
|
|
435
|
+
requirements.length > 0 ? [
|
|
436
|
+
'<h4>Requirement Blocks</h4>',
|
|
437
|
+
'<div class="visual-map">',
|
|
438
|
+
...requirements.map((item) => `<div class="visual-card"><div class="title">${escapeHtml(item.name)}</div><div class="meta">包含 SHALL/MUST 与 Scenario 的归档候选需求块。</div></div>`),
|
|
439
|
+
'</div>',
|
|
440
|
+
].join('\n') : '',
|
|
441
|
+
'</section>',
|
|
442
|
+
sectionCards,
|
|
443
|
+
].join('\n');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (artifact.name === 'slices.json') {
|
|
447
|
+
const slices = parseSlicesJson(text);
|
|
448
|
+
return [
|
|
449
|
+
'<section class="panel">',
|
|
450
|
+
'<h3>垂直切片详解</h3>',
|
|
451
|
+
'<div class="visual-map">',
|
|
452
|
+
...(slices.length > 0 ? slices.map((slice) => [
|
|
453
|
+
'<div class="visual-card">',
|
|
454
|
+
`<div class="title">${escapeHtml(slice.id || '(no id)')} ${escapeHtml(slice.title || slice.name || '')}</div>`,
|
|
455
|
+
`<div class="meta">type: ${escapeHtml(slice.type || 'unknown')}</div>`,
|
|
456
|
+
`<p>${escapeHtml(slice.behavior || slice.description || '')}</p>`,
|
|
457
|
+
'<h4>验收</h4>',
|
|
458
|
+
renderListItems(Array.isArray(slice.acceptance_criteria) ? slice.acceptance_criteria.map((item) => `- ${item}`) : (Array.isArray(slice.acceptance) ? slice.acceptance.map((item) => `- ${item}`) : [])),
|
|
459
|
+
`<p class="muted">verification: ${escapeHtml(slice.verification_signal || '(missing)')}</p>`,
|
|
460
|
+
'</div>',
|
|
461
|
+
].join('')) : ['<span class="muted">未解析到 slices。</span>']),
|
|
462
|
+
'</div>',
|
|
463
|
+
'</section>',
|
|
464
|
+
].join('\n');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (artifact.name === 'tasks.md') {
|
|
468
|
+
const checklist = String(text || '').split('\n').filter((line) => /^\s*-\s+\[[ xX]\]\s+/.test(line));
|
|
469
|
+
return [
|
|
470
|
+
'<section class="panel">',
|
|
471
|
+
'<h3>任务拆解详解</h3>',
|
|
472
|
+
renderKeyValueTable([
|
|
473
|
+
['checklist_items', escapeHtml(checklist.length)],
|
|
474
|
+
['open_items', escapeHtml(checklist.filter((line) => /\[\s\]/.test(line)).length)],
|
|
475
|
+
['completed_items', escapeHtml(checklist.filter((line) => /\[[xX]\]/.test(line)).length)],
|
|
476
|
+
]),
|
|
477
|
+
'</section>',
|
|
478
|
+
sectionCards,
|
|
479
|
+
].join('\n');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return sectionCards;
|
|
483
|
+
}
|
|
484
|
+
|
|
189
485
|
function artifactLink(viewRoot, artifactPath, label) {
|
|
190
486
|
const href = relative(viewRoot, artifactPath).replaceAll('\\', '/');
|
|
191
487
|
return `<a href="${escapeHtml(href)}">${escapeHtml(label)}</a>`;
|
|
192
488
|
}
|
|
193
489
|
|
|
490
|
+
function approvalPanels(state) {
|
|
491
|
+
const approval = state.approval || {};
|
|
492
|
+
const slug = state.slug || '(slug)';
|
|
493
|
+
const transitions = [
|
|
494
|
+
['clarify -> plan', approval.plan || 'not-requested', `loopx approve ${slug} --from clarify --to plan`],
|
|
495
|
+
['plan -> build', approval.build || 'not-requested', `loopx approve ${slug} --from plan --to build`],
|
|
496
|
+
['build -> review', approval.review || 'not-requested', `loopx approve ${slug} --from build --to review`],
|
|
497
|
+
['review -> done', approval.complete || 'not-requested', `loopx approve ${slug} --from review --to done`],
|
|
498
|
+
];
|
|
499
|
+
return [
|
|
500
|
+
'<section class="panel">',
|
|
501
|
+
'<h2>人工确认点</h2>',
|
|
502
|
+
'<table><thead><tr><th>阶段流转</th><th>授权状态</th><th>命令</th></tr></thead><tbody>',
|
|
503
|
+
...transitions.map(([label, status, command]) => [
|
|
504
|
+
'<tr>',
|
|
505
|
+
`<td>${escapeHtml(label)}</td>`,
|
|
506
|
+
`<td>${statusBadge('approval', status)}</td>`,
|
|
507
|
+
`<td><code>${escapeHtml(command)}</code></td>`,
|
|
508
|
+
'</tr>',
|
|
509
|
+
].join('')),
|
|
510
|
+
'</tbody></table>',
|
|
511
|
+
'</section>',
|
|
512
|
+
].join('\n');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function planGateSummary(state) {
|
|
516
|
+
const gates = [
|
|
517
|
+
['Planner / Architect / Critic', `${state.plan_architect_review_status || 'not-started'} / ${state.plan_critic_verdict || 'none'}`],
|
|
518
|
+
['中文规划文档', state.plan_docs_status || 'missing'],
|
|
519
|
+
['需求覆盖矩阵', state.source_requirements_status || 'unknown'],
|
|
520
|
+
['变更工件', state.change_artifacts_status || 'missing'],
|
|
521
|
+
['Spec Delta', state.spec_delta_status || 'missing'],
|
|
522
|
+
['Vertical Slices', state.slice_artifacts_status || 'missing'],
|
|
523
|
+
['委派决策', state.plan_delegation_mode || 'unknown'],
|
|
524
|
+
];
|
|
525
|
+
return [
|
|
526
|
+
'<section class="panel">',
|
|
527
|
+
'<h2>Plan 审阅门禁</h2>',
|
|
528
|
+
'<table><thead><tr><th>门禁</th><th>状态</th></tr></thead><tbody>',
|
|
529
|
+
...gates.map(([label, value]) => `<tr><td>${escapeHtml(label)}</td><td>${escapeHtml(value)}</td></tr>`),
|
|
530
|
+
'</tbody></table>',
|
|
531
|
+
'</section>',
|
|
532
|
+
].join('\n');
|
|
533
|
+
}
|
|
534
|
+
|
|
194
535
|
function statusPanels(status) {
|
|
195
536
|
const state = status.state || {};
|
|
196
537
|
const readiness = state.readiness || {};
|
|
@@ -204,6 +545,8 @@ function statusPanels(status) {
|
|
|
204
545
|
`<div class="panel"><strong>下一步</strong><br><code>${escapeHtml(nextSkill || status.next_action || 'none')}</code></div>`,
|
|
205
546
|
`<div class="panel"><strong>归档</strong><br>${escapeHtml(state.archive_status || 'pending')}</div>`,
|
|
206
547
|
'</section>',
|
|
548
|
+
approvalPanels(state),
|
|
549
|
+
state.current_stage === 'plan' || state.plan_package_status ? planGateSummary(state) : '',
|
|
207
550
|
'<section class="panel">',
|
|
208
551
|
'<h2>readiness / authorization</h2>',
|
|
209
552
|
'<table><thead><tr><th>关卡</th><th>ready</th><th>authorized</th><th>blockers</th></tr></thead><tbody>',
|
|
@@ -226,26 +569,73 @@ async function renderWorkflowPages(status) {
|
|
|
226
569
|
await mkdir(viewRoot, { recursive: true });
|
|
227
570
|
|
|
228
571
|
const artifactRows = WORKFLOW_ARTIFACTS.map((artifact) => {
|
|
229
|
-
const artifactPath =
|
|
572
|
+
const artifactPath = resolveArtifactPath(root, status.state || {}, artifact);
|
|
230
573
|
return {
|
|
231
574
|
...artifact,
|
|
232
575
|
path: artifactPath,
|
|
233
|
-
exists: existsSync(artifactPath),
|
|
576
|
+
exists: Boolean(artifactPath) && existsSync(artifactPath),
|
|
234
577
|
};
|
|
235
578
|
});
|
|
236
579
|
|
|
580
|
+
const renderArtifactBody = async (artifact) => {
|
|
581
|
+
const text = await readFile(artifact.path, 'utf8');
|
|
582
|
+
const metrics = artifactMetrics(text);
|
|
583
|
+
const digest = sectionDigest(text);
|
|
584
|
+
const isJson = artifact.name.endsWith('.json');
|
|
585
|
+
const detailHtml = artifactDetailHtml(artifact, text);
|
|
586
|
+
const outline = digest.outline.length > 0
|
|
587
|
+
? `<div class="outline">${digest.outline.map((item) => `<span class="badge">${escapeHtml('H'.repeat(item.level))} ${escapeHtml(item.title)}</span>`).join(' ')}</div>`
|
|
588
|
+
: '<span class="muted">无可提取标题</span>';
|
|
589
|
+
return {
|
|
590
|
+
text,
|
|
591
|
+
metrics,
|
|
592
|
+
digest,
|
|
593
|
+
detailHtml,
|
|
594
|
+
body: isJson
|
|
595
|
+
? `<pre><code>${escapeHtml(text)}</code></pre>`
|
|
596
|
+
: markdownToHtml(text),
|
|
597
|
+
meta: [
|
|
598
|
+
statusBadge('产物', artifact.name),
|
|
599
|
+
statusBadge('中文', metrics.chineseOk ? 'ok' : 'needs-review'),
|
|
600
|
+
statusBadge('行数', metrics.lines),
|
|
601
|
+
statusBadge('标题', metrics.headings),
|
|
602
|
+
statusBadge('表格', metrics.tables),
|
|
603
|
+
metrics.todoCount > 0 ? statusBadge('占位符', metrics.todoCount) : '',
|
|
604
|
+
].join(''),
|
|
605
|
+
digestHtml: outline,
|
|
606
|
+
};
|
|
607
|
+
};
|
|
608
|
+
|
|
237
609
|
for (const group of PAGE_GROUPS) {
|
|
238
610
|
const sections = [];
|
|
239
|
-
|
|
240
|
-
|
|
611
|
+
const availableArtifacts = artifactRows.filter((artifact) => group.artifacts.includes(artifactId(artifact)) && artifact.exists);
|
|
612
|
+
const nav = availableArtifacts.length > 0
|
|
613
|
+
? `<nav class="review-nav">${availableArtifacts.map((artifact) => `<a href="#${escapeHtml(anchorForArtifact(artifact))}">${escapeHtml(artifact.label)}</a>`).join('')}</nav>`
|
|
614
|
+
: '';
|
|
615
|
+
for (const artifactIdValue of group.artifacts) {
|
|
616
|
+
const artifact = artifactRows.find((item) => artifactId(item) === artifactIdValue);
|
|
241
617
|
if (!artifact?.exists) {
|
|
242
618
|
continue;
|
|
243
619
|
}
|
|
244
|
-
const
|
|
620
|
+
const rendered = await renderArtifactBody(artifact);
|
|
245
621
|
sections.push([
|
|
246
|
-
`<section class="markdown">`,
|
|
622
|
+
`<section class="markdown" id="${escapeHtml(anchorForArtifact(artifact))}">`,
|
|
623
|
+
'<div class="hero-grid">',
|
|
624
|
+
'<div>',
|
|
625
|
+
`<h2>${escapeHtml(artifact.label)}</h2>`,
|
|
247
626
|
`<p class="muted">${artifactLink(viewRoot, artifact.path, artifact.name)}</p>`,
|
|
248
|
-
|
|
627
|
+
`<p>${escapeHtml(rendered.digest.summary || '该产物没有单独摘要,直接阅读正文。')}</p>`,
|
|
628
|
+
'</div>',
|
|
629
|
+
'<div class="stack">',
|
|
630
|
+
rendered.meta,
|
|
631
|
+
`<div class="panel"><strong>结构预览</strong><div class="outline">${rendered.digestHtml}</div></div>`,
|
|
632
|
+
'</div>',
|
|
633
|
+
'</div>',
|
|
634
|
+
'<div class="artifact-meta">',
|
|
635
|
+
rendered.meta,
|
|
636
|
+
'</div>',
|
|
637
|
+
rendered.detailHtml,
|
|
638
|
+
rendered.body,
|
|
249
639
|
'</section>',
|
|
250
640
|
].join('\n'));
|
|
251
641
|
}
|
|
@@ -255,30 +645,68 @@ async function renderWorkflowPages(status) {
|
|
|
255
645
|
'<header>',
|
|
256
646
|
`<h1>${escapeHtml(group.title)}</h1>`,
|
|
257
647
|
`<p class="muted">工作流:${escapeHtml(status.slug)}</p>`,
|
|
648
|
+
'<div class="callout">这是人工审阅入口。先看上方视觉摘要,再按导航逐项审阅正文;HTML 负责可视化阅读,原始文件仍可点击查看。</div>',
|
|
258
649
|
'<p><a href="index.html">返回工作流首页</a></p>',
|
|
650
|
+
nav,
|
|
259
651
|
'</header>',
|
|
260
652
|
sections.length > 0 ? sections.join('\n') : '<section class="panel muted">暂无对应产物。</section>',
|
|
261
653
|
].join('\n'),
|
|
262
654
|
}));
|
|
263
655
|
}
|
|
264
656
|
|
|
265
|
-
const
|
|
266
|
-
|
|
657
|
+
const artifactTableRows = await Promise.all(artifactRows.map(async (artifact) => {
|
|
658
|
+
if (!artifact.exists) {
|
|
659
|
+
return [
|
|
660
|
+
'<tr>',
|
|
661
|
+
`<td>${escapeHtml(artifact.label)}</td>`,
|
|
662
|
+
'<td>缺失</td>',
|
|
663
|
+
'<td><span class="muted">无</span></td>',
|
|
664
|
+
'<td><span class="muted">无</span></td>',
|
|
665
|
+
'<td><span class="muted">无</span></td>',
|
|
666
|
+
'<td><span class="muted">无</span></td>',
|
|
667
|
+
'</tr>',
|
|
668
|
+
].join('');
|
|
669
|
+
}
|
|
670
|
+
const rendered = await renderArtifactBody(artifact);
|
|
671
|
+
return [
|
|
672
|
+
'<tr>',
|
|
673
|
+
`<td>${escapeHtml(artifact.label)}</td>`,
|
|
674
|
+
'<td>存在</td>',
|
|
675
|
+
`<td>${artifact.page ? `<a href="${escapeHtml(artifact.page)}">${escapeHtml(basename(artifact.page))}</a>` : '<span class="muted">无</span>'}</td>`,
|
|
676
|
+
`<td>${rendered.meta}</td>`,
|
|
677
|
+
`<td>${escapeHtml(rendered.metrics.headings)} 标题 / ${escapeHtml(rendered.metrics.tables)} 表格 / ${escapeHtml(rendered.metrics.lines)} 行</td>`,
|
|
678
|
+
`<td>${artifactLink(viewRoot, artifact.path, artifact.name)}</td>`,
|
|
679
|
+
'</tr>',
|
|
680
|
+
].join('');
|
|
681
|
+
}));
|
|
682
|
+
|
|
683
|
+
const heroSummary = [
|
|
684
|
+
`<div class="hero">`,
|
|
685
|
+
'<div class="hero-grid">',
|
|
686
|
+
'<div>',
|
|
267
687
|
`<h1>工作流 ${escapeHtml(status.slug)}</h1>`,
|
|
268
688
|
`<p class="muted">HTML 是派生阅读视图;Markdown 和 JSON 仍是运行时事实源。</p>`,
|
|
269
|
-
'
|
|
689
|
+
'<div class="hero-kpi">',
|
|
690
|
+
`<div class="kpi"><div class="label">阶段</div><div class="value">${escapeHtml(status.state?.current_stage || '(none)')}</div></div>`,
|
|
691
|
+
`<div class="kpi"><div class="label">状态</div><div class="value">${escapeHtml(status.state?.stage_status || '(unknown)')}</div></div>`,
|
|
692
|
+
`<div class="kpi"><div class="label">中文产物</div><div class="value">${escapeHtml(status.state?.plan_docs_status || 'unknown')}</div></div>`,
|
|
693
|
+
`<div class="kpi"><div class="label">下一步</div><div class="value">${escapeHtml(status.next_action || 'none')}</div></div>`,
|
|
694
|
+
'</div>',
|
|
695
|
+
'</div>',
|
|
696
|
+
'<div class="stack">',
|
|
697
|
+
approvalPanels(status.state || {}),
|
|
698
|
+
'</div>',
|
|
699
|
+
'</div>',
|
|
700
|
+
'</div>',
|
|
701
|
+
].join('\n');
|
|
702
|
+
|
|
703
|
+
const indexBody = [
|
|
704
|
+
heroSummary,
|
|
270
705
|
statusPanels(status),
|
|
271
706
|
'<section class="panel">',
|
|
272
|
-
'<h2
|
|
273
|
-
'<table><thead><tr><th>产物</th><th>状态</th><th>阅读视图</th><th>原始文件</th></tr></thead><tbody>',
|
|
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('')),
|
|
707
|
+
'<h2>关键产物审阅清单</h2>',
|
|
708
|
+
'<table><thead><tr><th>产物</th><th>状态</th><th>阅读视图</th><th>中文</th><th>结构</th><th>原始文件</th></tr></thead><tbody>',
|
|
709
|
+
...artifactTableRows,
|
|
282
710
|
'</tbody></table>',
|
|
283
711
|
'</section>',
|
|
284
712
|
].join('\n');
|
package/src/plan-runtime.mjs
CHANGED
|
@@ -363,7 +363,8 @@ export function createRealPlanAdapter({ model } = {}) {
|
|
|
363
363
|
'}',
|
|
364
364
|
`Deliberate mode: ${Boolean(context.deliberateMode)}`,
|
|
365
365
|
'',
|
|
366
|
-
'
|
|
366
|
+
'planText, architectureText, developmentPlanText, and testPlanText MUST be written in Chinese for human review. Do not write English headings or English prose except literal code paths, API names, commands, enum values, and product terms.',
|
|
367
|
+
'Make the artifacts reviewable: use clear Chinese section headings, concise tables for coverage/options/risks, and explicit approval-ready handoff sections. Avoid generic filler.',
|
|
367
368
|
'Treat the source requirements/PRD as the source of truth. Explicitly cover every named event, field, processing mode, table row, and acceptance item that appears in the source, or clearly mark it out of scope with rationale.',
|
|
368
369
|
'If previous review feedback is present, revise the plan to explicitly resolve it. Do not repeat the same plan unchanged.',
|
|
369
370
|
'Do not ask questions. Do not wrap JSON in markdown.',
|