@epoint-testtech/ep-stage-skill 0.0.10 → 0.5.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +27 -27
- package/codex-skill/ep-stage/glue-create-project/SKILL.md +223 -223
- package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +275 -275
- package/codex-skill/ep-stage/glue-generate-testcase/examples/observable-testcase.json +40 -40
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +95 -95
- package/codex-skill/ep-stage/glue-run-test/SKILL.md +273 -296
- package/codex-skill/ep-stage/glue-run-test/references/ai-runtime-loop.md +30 -30
- package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +132 -132
- package/codex-skill/ep-stage/glue-run-test/references/gap-review-protocol.md +44 -44
- package/codex-skill/ep-stage/glue-run-test/references/harness-principles.md +46 -46
- package/codex-skill/ep-stage/glue-run-test/references/playwright-cli-odav.md +127 -166
- package/codex-skill/ep-stage/glue-run-test/scripts/generate-crud-spec.mjs +149 -149
- package/codex-skill/ep-stage/recording-to-glue/SKILL.md +28 -28
- package/codex-skill/ep-stage/scripts/validate-skill.mjs +95 -98
- package/dist/src/cli/generate-testcase.d.ts +0 -1
- package/dist/src/cli/generate-testcase.d.ts.map +1 -1
- package/dist/src/cli/generate-testcase.js +1 -6
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +14 -38
- package/dist/src/cli/run.d.ts +0 -2
- package/dist/src/cli/run.d.ts.map +1 -1
- package/dist/src/cli/run.js +21 -70
- package/dist/src/cli/spec.d.ts.map +1 -1
- package/dist/src/cli/spec.js +0 -9
- package/dist/src/contracts/glue-contract-v1.d.ts +0 -35
- package/dist/src/contracts/glue-contract-v1.d.ts.map +1 -1
- package/dist/src/extractors/code-list-json5.d.ts +1 -34
- package/dist/src/extractors/code-list-json5.d.ts.map +1 -1
- package/dist/src/extractors/code-list-json5.js +0 -1
- package/dist/src/extractors/html-page.d.ts +0 -9
- package/dist/src/extractors/html-page.d.ts.map +1 -1
- package/dist/src/extractors/html-page.js +2 -26
- package/dist/src/generators/agent-inferred-workflow-script.js +66 -66
- package/dist/src/generators/stage-skeleton-script.d.ts +0 -50
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.js +116 -449
- package/dist/src/runtime/ai-runtime-trace.d.ts.map +1 -1
- package/dist/src/runtime/ai-runtime-trace.js +0 -8
- package/dist/src/testcase/contract-testcase-assembler.d.ts +0 -28
- package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -1
- package/dist/src/testcase/contract-testcase-assembler.js +75 -329
- package/dist/src/validation/contract.d.ts.map +1 -1
- package/dist/src/validation/contract.js +0 -29
- package/dist/test/cli/generate-testcase-json5.test.d.ts +2 -0
- package/dist/test/cli/generate-testcase-json5.test.d.ts.map +1 -0
- package/dist/test/cli/generate-testcase-json5.test.js +154 -0
- package/dist/test/cli/index.test.d.ts +2 -0
- package/dist/test/cli/index.test.d.ts.map +1 -0
- package/dist/test/cli/index.test.js +114 -0
- package/dist/test/cli/run.test.d.ts +2 -0
- package/dist/test/cli/run.test.d.ts.map +1 -0
- package/dist/test/cli/run.test.js +429 -0
- package/dist/test/cli/spec.test.d.ts +2 -0
- package/dist/test/cli/spec.test.d.ts.map +1 -0
- package/dist/test/cli/spec.test.js +112 -0
- package/dist/test/code-list-json5.test.d.ts +2 -0
- package/dist/test/code-list-json5.test.d.ts.map +1 -0
- package/dist/test/code-list-json5.test.js +84 -0
- package/dist/test/code-list.test.d.ts +2 -0
- package/dist/test/code-list.test.d.ts.map +1 -0
- package/dist/test/code-list.test.js +39 -0
- package/dist/test/gap-inference.test.d.ts +2 -0
- package/dist/test/gap-inference.test.d.ts.map +1 -0
- package/dist/test/gap-inference.test.js +597 -0
- package/dist/test/generalization.test.d.ts +2 -0
- package/dist/test/generalization.test.d.ts.map +1 -0
- package/dist/test/generalization.test.js +73 -0
- package/dist/test/material-inventory.test.d.ts +2 -0
- package/dist/test/material-inventory.test.d.ts.map +1 -0
- package/dist/test/material-inventory.test.js +155 -0
- package/dist/test/observable-chain.test.d.ts +2 -0
- package/dist/test/observable-chain.test.d.ts.map +1 -0
- package/dist/test/observable-chain.test.js +123 -0
- package/dist/test/page-structure.test.d.ts +2 -0
- package/dist/test/page-structure.test.d.ts.map +1 -0
- package/dist/test/page-structure.test.js +45 -0
- package/dist/test/runtime/ai-runtime-trace.test.d.ts +2 -0
- package/dist/test/runtime/ai-runtime-trace.test.d.ts.map +1 -0
- package/dist/test/runtime/ai-runtime-trace.test.js +78 -0
- package/dist/test/scenario-inference.test.d.ts +2 -0
- package/dist/test/scenario-inference.test.d.ts.map +1 -0
- package/dist/test/scenario-inference.test.js +73 -0
- package/dist/test/stage-context.test.d.ts +2 -0
- package/dist/test/stage-context.test.d.ts.map +1 -0
- package/dist/test/stage-context.test.js +395 -0
- package/dist/test/testcase/testcase-markdown.test.d.ts +2 -0
- package/dist/test/testcase/testcase-markdown.test.d.ts.map +1 -0
- package/dist/test/testcase/testcase-markdown.test.js +48 -0
- package/dist/test/testcase-generator.test.d.ts +2 -0
- package/dist/test/testcase-generator.test.d.ts.map +1 -0
- package/dist/test/testcase-generator.test.js +276 -0
- package/dist/test/testcase-skeleton.test.d.ts +2 -0
- package/dist/test/testcase-skeleton.test.d.ts.map +1 -0
- package/dist/test/testcase-skeleton.test.js +185 -0
- package/dist/test/testcase-spec-assembly.test.d.ts +2 -0
- package/dist/test/testcase-spec-assembly.test.d.ts.map +1 -0
- package/dist/test/testcase-spec-assembly.test.js +105 -0
- package/dist/test/util/credentials.test.d.ts +2 -0
- package/dist/test/util/credentials.test.d.ts.map +1 -0
- package/dist/test/util/credentials.test.js +64 -0
- package/dist/test/util/i18n-testcase.test.d.ts +2 -0
- package/dist/test/util/i18n-testcase.test.d.ts.map +1 -0
- package/dist/test/util/i18n-testcase.test.js +119 -0
- package/dist/test/util/softlink.test.d.ts +2 -0
- package/dist/test/util/softlink.test.d.ts.map +1 -0
- package/dist/test/util/softlink.test.js +82 -0
- package/dist/test/validation/contract.test.d.ts +2 -0
- package/dist/test/validation/contract.test.d.ts.map +1 -0
- package/dist/test/validation/contract.test.js +116 -0
- package/dist/test/validation/credentials.test.d.ts +2 -0
- package/dist/test/validation/credentials.test.d.ts.map +1 -0
- package/dist/test/validation/credentials.test.js +74 -0
- package/dist/test/validation/projects-index.test.d.ts +2 -0
- package/dist/test/validation/projects-index.test.d.ts.map +1 -0
- package/dist/test/validation/projects-index.test.js +48 -0
- package/dist/test/validation/testcase-v1-draft.test.d.ts +2 -0
- package/dist/test/validation/testcase-v1-draft.test.d.ts.map +1 -0
- package/dist/test/validation/testcase-v1-draft.test.js +173 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +7 -0
- package/docs/README.md +124 -124
- package/examples/schemeresource-observable-docs/schemeresource.context.md +20 -20
- package/examples/schemeresource.module-hints.json +38 -38
- package/examples/schemeresource.observable.code_list.md +37 -37
- package/examples/zwplace-observable-docs/zwplace.context.md +16 -16
- package/examples/zwplace-placecategory-validation.json +29 -29
- package/examples/zwplace.module-hints.json +69 -69
- package/examples/zwplace.observable.code_list.md +37 -37
- package/package.json +19 -13
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +0 -72
- package/codex-skill/ep-stage/glue-run-test/scripts/extract-page-elements-eval.mjs +0 -233
- package/codex-skill/ep-stage/page-element-scan/SKILL.md +0 -91
- package/codex-skill/ep-stage/page-element-scan/evals/evals.json +0 -37
- package/codex-skill/ep-stage/page-element-scan/evals/fixtures/sample-list-page.html +0 -85
- package/codex-skill/ep-stage/page-element-scan/references/pagination-controls.md +0 -153
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements-eval.mjs +0 -233
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements.mjs +0 -298
- package/codex-skill/ep-stage/page-element-scan/scripts/scan-page.mjs +0 -749
- package/dist/src/cli/probe.d.ts +0 -46
- package/dist/src/cli/probe.d.ts.map +0 -1
- package/dist/src/cli/probe.js +0 -447
- package/dist/src/cli/run-report.d.ts +0 -46
- package/dist/src/cli/run-report.d.ts.map +0 -1
- package/dist/src/cli/run-report.js +0 -331
- package/dist/src/extractors/prd-extractor.d.ts +0 -70
- package/dist/src/extractors/prd-extractor.d.ts.map +0 -1
- package/dist/src/extractors/prd-extractor.js +0 -248
- package/dist/src/testcase/button-interaction-inference.d.ts +0 -37
- package/dist/src/testcase/button-interaction-inference.d.ts.map +0 -1
- package/dist/src/testcase/button-interaction-inference.js +0 -385
- package/dist/src/testcase/prd-button-mapper.d.ts +0 -72
- package/dist/src/testcase/prd-button-mapper.d.ts.map +0 -1
- package/dist/src/testcase/prd-button-mapper.js +0 -148
- package/dist/src/testcase/testcase-v2.d.ts +0 -50
- package/dist/src/testcase/testcase-v2.d.ts.map +0 -1
- package/dist/src/testcase/testcase-v2.js +0 -1
- package/docs/mvp-usage-guide.md +0 -298
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* run-report.ts — run 子命令的综合报告渲染(glue 风格单文件 HTML)。
|
|
3
|
-
*
|
|
4
|
-
* 输入:Playwright JSON reporter 产物(PLAYWRIGHT_JSON_OUTPUT_NAME 指定落盘)。
|
|
5
|
-
* 输出:<reportDir>/index.html —— 与脚手架 stage-glue 报告同款样式
|
|
6
|
-
* (概览 + 统计卡片 + 状态色卡片 + 页脚原生报告入口),
|
|
7
|
-
* 但数据来自 Playwright 真实执行结果(覆盖全部用例,含 AI snippet)。
|
|
8
|
-
*/
|
|
9
|
-
function escapeHtml(value) {
|
|
10
|
-
return value
|
|
11
|
-
.replace(/&/g, '&')
|
|
12
|
-
.replace(/</g, '<')
|
|
13
|
-
.replace(/>/g, '>')
|
|
14
|
-
.replace(/"/g, '"');
|
|
15
|
-
}
|
|
16
|
-
/** Playwright 错误文本含终端 ANSI 颜色码,写进 HTML 前必须剥掉。 */
|
|
17
|
-
function stripAnsi(value) {
|
|
18
|
-
// eslint-disable-next-line no-control-regex
|
|
19
|
-
return value.replace(/\[[0-9;]*m/g, '');
|
|
20
|
-
}
|
|
21
|
-
function mapTestStatus(status) {
|
|
22
|
-
switch (status) {
|
|
23
|
-
case 'expected':
|
|
24
|
-
return 'passed';
|
|
25
|
-
case 'unexpected':
|
|
26
|
-
return 'failed';
|
|
27
|
-
case 'flaky':
|
|
28
|
-
return 'flaky';
|
|
29
|
-
default:
|
|
30
|
-
return 'skipped';
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* 把 Playwright JSON reporter 输出摊平为报告用例条目。
|
|
35
|
-
*/
|
|
36
|
-
export function parsePlaywrightJsonResults(jsonText) {
|
|
37
|
-
const parsed = JSON.parse(jsonText);
|
|
38
|
-
const items = [];
|
|
39
|
-
const walk = (suite, trail) => {
|
|
40
|
-
const nextTrail = suite.title ? [...trail, suite.title] : trail;
|
|
41
|
-
for (const spec of suite.specs ?? []) {
|
|
42
|
-
for (const test of spec.tests ?? []) {
|
|
43
|
-
const lastResult = test.results?.[test.results.length - 1];
|
|
44
|
-
const errorMessage = lastResult?.error?.message ?? lastResult?.errors?.[0]?.message;
|
|
45
|
-
items.push({
|
|
46
|
-
title: spec.title,
|
|
47
|
-
suitePath: nextTrail.join(' › '),
|
|
48
|
-
projectName: test.projectName ?? '',
|
|
49
|
-
status: mapTestStatus(test.status),
|
|
50
|
-
durationMs: lastResult?.duration ?? 0,
|
|
51
|
-
location: spec.file !== undefined && spec.line !== undefined
|
|
52
|
-
? `${spec.file}:${spec.line}`
|
|
53
|
-
: spec.file,
|
|
54
|
-
errorMessage: errorMessage
|
|
55
|
-
? stripAnsi(errorMessage).split('\n').slice(0, 12).join('\n')
|
|
56
|
-
: undefined,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
for (const child of suite.suites ?? []) {
|
|
61
|
-
walk(child, nextTrail);
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
for (const suite of parsed.suites ?? []) {
|
|
65
|
-
walk(suite, []);
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
suiteTitle: parsed.suites?.[0]?.title ?? 'Playwright 测试套件',
|
|
69
|
-
durationMs: Math.round(parsed.stats?.duration ?? 0),
|
|
70
|
-
startedAt: parsed.stats?.startTime ?? '',
|
|
71
|
-
items,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
const STATUS_LABEL = {
|
|
75
|
-
passed: '通过',
|
|
76
|
-
failed: '失败',
|
|
77
|
-
flaky: '不稳定',
|
|
78
|
-
skipped: '跳过',
|
|
79
|
-
};
|
|
80
|
-
const STATUS_BADGE_CLASS = {
|
|
81
|
-
passed: 'passed',
|
|
82
|
-
failed: 'failed',
|
|
83
|
-
flaky: 'needs_review',
|
|
84
|
-
skipped: 'unresolved',
|
|
85
|
-
};
|
|
86
|
-
function formatDuration(ms) {
|
|
87
|
-
if (ms >= 60_000) {
|
|
88
|
-
return `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`;
|
|
89
|
-
}
|
|
90
|
-
if (ms >= 1000) {
|
|
91
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
92
|
-
}
|
|
93
|
-
return `${ms}ms`;
|
|
94
|
-
}
|
|
95
|
-
function formatDisplayTimestamp(iso) {
|
|
96
|
-
if (!iso)
|
|
97
|
-
return '-';
|
|
98
|
-
const d = new Date(iso);
|
|
99
|
-
if (Number.isNaN(d.getTime()))
|
|
100
|
-
return iso;
|
|
101
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
102
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
103
|
-
}
|
|
104
|
-
/** 解析用例ID:标题前缀(TC01001 …)优先,其次按 AI 任务标题匹配。 */
|
|
105
|
-
function resolveCaseId(item, aiTasks) {
|
|
106
|
-
const prefix = item.title.match(/^([A-Z]{1,6}\d+)\s/)?.[1];
|
|
107
|
-
if (prefix)
|
|
108
|
-
return prefix;
|
|
109
|
-
return aiTasks.find((task) => task.title === item.title)?.caseId;
|
|
110
|
-
}
|
|
111
|
-
/** 判定执行策略:标题带用例ID前缀且该 ID 属于 AI 任务,或标题与 AI 任务相同 → Agent 推理。 */
|
|
112
|
-
function resolveStrategy(item, aiTasks) {
|
|
113
|
-
const caseId = resolveCaseId(item, aiTasks);
|
|
114
|
-
if (caseId && aiTasks.some((task) => task.caseId === caseId))
|
|
115
|
-
return 'agent_inferred';
|
|
116
|
-
if (aiTasks.some((task) => task.title === item.title))
|
|
117
|
-
return 'agent_inferred';
|
|
118
|
-
return 'glue_code';
|
|
119
|
-
}
|
|
120
|
-
function renderTestCard(item, strategy, caseId) {
|
|
121
|
-
const errorHtml = item.errorMessage
|
|
122
|
-
? `<div class="error"><strong>错误信息:</strong><pre>${escapeHtml(item.errorMessage)}</pre></div>`
|
|
123
|
-
: '';
|
|
124
|
-
return `
|
|
125
|
-
<article class="workflow workflow--${escapeHtml(STATUS_BADGE_CLASS[item.status])}" data-strategy="${escapeHtml(strategy)}" data-status="${escapeHtml(item.status)}">
|
|
126
|
-
<header class="workflow__header">
|
|
127
|
-
<div class="workflow__title">
|
|
128
|
-
<h2>${escapeHtml(item.title)}</h2>
|
|
129
|
-
<code class="workflow__id">${escapeHtml(item.location ?? '')}</code>
|
|
130
|
-
</div>
|
|
131
|
-
<div class="workflow__meta">
|
|
132
|
-
<span class="badge badge--kind">${escapeHtml(item.projectName)}</span>
|
|
133
|
-
<span class="badge badge--source">${escapeHtml(strategy === 'agent_inferred' ? 'Agent推理执行' : '胶水编码执行')}</span>
|
|
134
|
-
<span class="badge badge--status badge--${escapeHtml(STATUS_BADGE_CLASS[item.status])}">${escapeHtml(STATUS_LABEL[item.status])}</span>
|
|
135
|
-
</div>
|
|
136
|
-
</header>
|
|
137
|
-
<dl class="workflow__body">
|
|
138
|
-
${caseId ? `<dt>用例ID</dt><dd><code>${escapeHtml(caseId)}</code></dd>` : ''}
|
|
139
|
-
<dt>套件路径</dt><dd>${escapeHtml(item.suitePath)}</dd>
|
|
140
|
-
<dt>耗时</dt><dd>${escapeHtml(formatDuration(item.durationMs))}</dd>
|
|
141
|
-
</dl>
|
|
142
|
-
${errorHtml}
|
|
143
|
-
</article>
|
|
144
|
-
`;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* 渲染 glue 风格综合报告(单文件、内联样式、无外部依赖)。
|
|
148
|
-
*/
|
|
149
|
-
export function renderRunReportHtml(data) {
|
|
150
|
-
const count = (s) => data.items.filter((i) => i.status === s).length;
|
|
151
|
-
const agentCount = data.items.filter((i) => resolveStrategy(i, data.aiTasks) === 'agent_inferred').length;
|
|
152
|
-
// 卡片按用例ID 数字序排列(TC01001→TC01015),与 testcase.json5 顺序一致,便于人工对照;
|
|
153
|
-
// Playwright JSON 结果树的原始顺序是「外层 suite 直接 specs 在前、子 describe 在后」,不符合审阅习惯。
|
|
154
|
-
const sortedItems = [...data.items].sort((a, b) => {
|
|
155
|
-
const idA = resolveCaseId(a, data.aiTasks);
|
|
156
|
-
const idB = resolveCaseId(b, data.aiTasks);
|
|
157
|
-
const numA = idA ? Number(idA.replace(/^\D+/, '')) : Number.NaN;
|
|
158
|
-
const numB = idB ? Number(idB.replace(/^\D+/, '')) : Number.NaN;
|
|
159
|
-
if (Number.isNaN(numA) && Number.isNaN(numB))
|
|
160
|
-
return 0;
|
|
161
|
-
if (Number.isNaN(numA))
|
|
162
|
-
return 1;
|
|
163
|
-
if (Number.isNaN(numB))
|
|
164
|
-
return -1;
|
|
165
|
-
return numA - numB;
|
|
166
|
-
});
|
|
167
|
-
const overviewRows = [
|
|
168
|
-
['测试套件', data.suiteTitle],
|
|
169
|
-
['执行时间', formatDisplayTimestamp(data.startedAt)],
|
|
170
|
-
['执行耗时', formatDuration(data.durationMs)],
|
|
171
|
-
['Spec 文件', data.specFile ?? '-'],
|
|
172
|
-
];
|
|
173
|
-
const overviewHtml = overviewRows
|
|
174
|
-
.map(([label, value]) => `<tr><th>${escapeHtml(label)}</th><td>${escapeHtml(value)}</td></tr>`)
|
|
175
|
-
.join('');
|
|
176
|
-
const strategyCards = [
|
|
177
|
-
['总测试流程', data.items.length, 'total', ''],
|
|
178
|
-
['胶水编码执行', data.items.length - agentCount, 'glue', 'glue_code'],
|
|
179
|
-
['Agent推理执行', agentCount, 'agent', 'agent_inferred'],
|
|
180
|
-
];
|
|
181
|
-
const resultCards = [
|
|
182
|
-
['通过', count('passed'), 'passed', 'passed'],
|
|
183
|
-
['失败', count('failed'), 'failed', 'failed'],
|
|
184
|
-
['不稳定', count('flaky'), 'review', 'flaky'],
|
|
185
|
-
['跳过', count('skipped'), 'unresolved', 'skipped'],
|
|
186
|
-
];
|
|
187
|
-
const renderCards = (cards, filterGroup) => cards
|
|
188
|
-
.map(([label, value, kind, filterValue]) => `
|
|
189
|
-
<div class="summary-card summary-card--${kind}" data-filter-group="${filterGroup}" data-filter-value="${filterValue}" role="button" tabindex="0">
|
|
190
|
-
<div class="summary-card__value">${value}</div>
|
|
191
|
-
<div class="summary-card__label">${escapeHtml(label)}</div>
|
|
192
|
-
</div>`)
|
|
193
|
-
.join('');
|
|
194
|
-
const workflowsHtml = sortedItems.length
|
|
195
|
-
? sortedItems
|
|
196
|
-
.map((item) => renderTestCard(item, resolveStrategy(item, data.aiTasks), resolveCaseId(item, data.aiTasks)))
|
|
197
|
-
.join('\n')
|
|
198
|
-
: '<p class="muted">(本次运行未发现任何用例结果)</p>';
|
|
199
|
-
const footerLinks = [
|
|
200
|
-
data.playwrightReportHref
|
|
201
|
-
? `<a class="pw-link" href="${escapeHtml(data.playwrightReportHref)}" target="_blank" rel="noopener">Playwright 原生报告(trace / 截图 / 视频)</a>`
|
|
202
|
-
: '',
|
|
203
|
-
data.stageReportHref
|
|
204
|
-
? `<a class="pw-link pw-link--secondary" href="${escapeHtml(data.stageReportHref)}" target="_blank" rel="noopener">stage-core 流程报告(PDF)</a>`
|
|
205
|
-
: '',
|
|
206
|
-
]
|
|
207
|
-
.filter(Boolean)
|
|
208
|
-
.join(' ');
|
|
209
|
-
const style = `
|
|
210
|
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; background: #f7f8fa; color: #1f2933; margin: 0; padding: 24px; }
|
|
211
|
-
h1 { margin: 0 0 16px; font-size: 22px; }
|
|
212
|
-
h2 { font-size: 16px; margin: 16px 0 8px; }
|
|
213
|
-
section.overview { background: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); margin-bottom: 24px; }
|
|
214
|
-
.overview-table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
|
215
|
-
.overview-table th, .overview-table td { text-align: left; padding: 6px 12px; border-bottom: 1px solid #eee; font-size: 13px; }
|
|
216
|
-
.overview-table th { width: 140px; color: #52606d; font-weight: 600; }
|
|
217
|
-
.summary-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
|
|
218
|
-
.summary-card { background: #f0f4f8; border-radius: 6px; padding: 12px; text-align: center; cursor: pointer; transition: box-shadow 0.15s, transform 0.1s; }
|
|
219
|
-
.summary-card:hover { box-shadow: 0 2px 6px rgba(0,0,0,0.1); transform: translateY(-1px); }
|
|
220
|
-
.summary-card--active { box-shadow: 0 0 0 2px #2563eb; }
|
|
221
|
-
.summary-card__value { font-size: 22px; font-weight: 600; }
|
|
222
|
-
.summary-card__label { font-size: 12px; color: #52606d; margin-top: 4px; }
|
|
223
|
-
.summary-card--passed { background: #e3fcec; }
|
|
224
|
-
.summary-card--failed { background: #ffe3e3; }
|
|
225
|
-
.summary-card--review { background: #fff4d6; }
|
|
226
|
-
.summary-card--unresolved { background: #f0e8ff; }
|
|
227
|
-
article.workflow { background: #fff; border-radius: 8px; padding: 16px 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); border-left: 4px solid #cbd2d9; }
|
|
228
|
-
article.workflow--passed { border-left-color: #2f9e44; }
|
|
229
|
-
article.workflow--failed { border-left-color: #e03131; }
|
|
230
|
-
article.workflow--needs_review { border-left-color: #f59f00; }
|
|
231
|
-
article.workflow--unresolved { border-left-color: #7048e8; }
|
|
232
|
-
.workflow__header { display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px; }
|
|
233
|
-
.workflow__title h2 { margin: 0; font-size: 16px; }
|
|
234
|
-
.workflow__id { font-size: 12px; color: #52606d; }
|
|
235
|
-
.workflow__meta { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
236
|
-
.badge { display: inline-block; padding: 2px 8px; font-size: 12px; border-radius: 12px; background: #e4e7eb; color: #1f2933; }
|
|
237
|
-
.badge--passed { background: #2f9e44; color: #fff; }
|
|
238
|
-
.badge--failed { background: #e03131; color: #fff; }
|
|
239
|
-
.badge--needs_review { background: #f59f00; color: #fff; }
|
|
240
|
-
.badge--unresolved { background: #7048e8; color: #fff; }
|
|
241
|
-
dl.workflow__body { display: grid; grid-template-columns: 100px 1fr; gap: 6px 12px; margin: 12px 0 4px; font-size: 13px; }
|
|
242
|
-
dl.workflow__body dt { color: #52606d; font-weight: 600; }
|
|
243
|
-
code { background: #f0f4f8; padding: 1px 4px; border-radius: 3px; font-size: 12px; }
|
|
244
|
-
.error pre { background: #fff5f5; color: #c92a2a; padding: 8px; border-radius: 4px; overflow-x: auto; font-size: 12px; white-space: pre-wrap; }
|
|
245
|
-
.muted { color: #829ab1; font-size: 13px; }
|
|
246
|
-
footer.footer { background: #fff; border-radius: 8px; padding: 16px 20px; margin-top: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
|
|
247
|
-
.pw-link { display: inline-block; padding: 8px 16px; background: #2563eb; color: #fff; border-radius: 4px; text-decoration: none; font-size: 14px; margin-right: 8px; }
|
|
248
|
-
.pw-link:hover { background: #1d4ed8; }
|
|
249
|
-
.pw-link--secondary { background: #334e68; }
|
|
250
|
-
.workflow-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 16px 0 8px; }
|
|
251
|
-
.workflow-toolbar h2 { margin: 0; }
|
|
252
|
-
.workflow-filters { display: flex; gap: 8px; }
|
|
253
|
-
.workflow-filters select { padding: 4px 8px; font-size: 13px; border: 1px solid #cbd2d9; border-radius: 4px; background: #fff; color: #1f2933; }
|
|
254
|
-
.summary-section-title { font-size: 13px; color: #52606d; margin: 12px 0 4px; font-weight: 600; }
|
|
255
|
-
`;
|
|
256
|
-
return `<!DOCTYPE html>
|
|
257
|
-
<html lang="zh-CN">
|
|
258
|
-
<head>
|
|
259
|
-
<meta charset="UTF-8" />
|
|
260
|
-
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
261
|
-
<title>胶水编码-测试执行</title>
|
|
262
|
-
<style>${style}</style>
|
|
263
|
-
</head>
|
|
264
|
-
<body>
|
|
265
|
-
<section class="overview">
|
|
266
|
-
<h1>胶水编码-测试执行(综合报告)</h1>
|
|
267
|
-
<table class="overview-table">${overviewHtml}</table>
|
|
268
|
-
<h3 class="summary-section-title">执行策略</h3>
|
|
269
|
-
<div class="summary-grid summary-grid--strategy">${renderCards(strategyCards, 'strategy')}</div>
|
|
270
|
-
<h3 class="summary-section-title">执行结果</h3>
|
|
271
|
-
<div class="summary-grid summary-grid--result">${renderCards(resultCards, 'status')}</div>
|
|
272
|
-
</section>
|
|
273
|
-
<section class="workflows">
|
|
274
|
-
<div class="workflow-toolbar">
|
|
275
|
-
<h2>用例执行明细</h2>
|
|
276
|
-
<div class="workflow-filters">
|
|
277
|
-
<select id="filter-strategy">
|
|
278
|
-
<option value="">全部执行策略</option>
|
|
279
|
-
<option value="glue_code">胶水编码执行</option>
|
|
280
|
-
<option value="agent_inferred">Agent推理执行</option>
|
|
281
|
-
</select>
|
|
282
|
-
<select id="filter-status">
|
|
283
|
-
<option value="">全部执行结果</option>
|
|
284
|
-
<option value="passed">通过</option>
|
|
285
|
-
<option value="failed">失败</option>
|
|
286
|
-
<option value="flaky">不稳定</option>
|
|
287
|
-
<option value="skipped">跳过</option>
|
|
288
|
-
</select>
|
|
289
|
-
</div>
|
|
290
|
-
</div>
|
|
291
|
-
${workflowsHtml}
|
|
292
|
-
</section>
|
|
293
|
-
<footer class="footer">
|
|
294
|
-
${footerLinks || '<p class="muted">本次未发现关联报告产物</p>'}
|
|
295
|
-
</footer>
|
|
296
|
-
<script>
|
|
297
|
-
(function() {
|
|
298
|
-
var fs = document.getElementById('filter-strategy');
|
|
299
|
-
var fst = document.getElementById('filter-status');
|
|
300
|
-
var cards = document.querySelectorAll('article.workflow');
|
|
301
|
-
var summaryCards = document.querySelectorAll('.summary-card');
|
|
302
|
-
function apply() {
|
|
303
|
-
var s = fs.value, st = fst.value;
|
|
304
|
-
for (var i = 0; i < cards.length; i++) {
|
|
305
|
-
var c = cards[i];
|
|
306
|
-
var show = (!s || c.dataset.strategy === s) && (!st || c.dataset.status === st);
|
|
307
|
-
c.style.display = show ? '' : 'none';
|
|
308
|
-
}
|
|
309
|
-
for (var j = 0; j < summaryCards.length; j++) {
|
|
310
|
-
var card = summaryCards[j];
|
|
311
|
-
var select = card.dataset.filterGroup === 'strategy' ? fs : fst;
|
|
312
|
-
card.classList.toggle('summary-card--active', select.value !== '' && select.value === card.dataset.filterValue);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
fs.addEventListener('change', apply);
|
|
316
|
-
fst.addEventListener('change', apply);
|
|
317
|
-
for (var j = 0; j < summaryCards.length; j++) {
|
|
318
|
-
(function(card) {
|
|
319
|
-
card.addEventListener('click', function() {
|
|
320
|
-
var select = card.dataset.filterGroup === 'strategy' ? fs : fst;
|
|
321
|
-
select.value = select.value === card.dataset.filterValue ? '' : card.dataset.filterValue;
|
|
322
|
-
apply();
|
|
323
|
-
});
|
|
324
|
-
})(summaryCards[j]);
|
|
325
|
-
}
|
|
326
|
-
})();
|
|
327
|
-
</script>
|
|
328
|
-
</body>
|
|
329
|
-
</html>
|
|
330
|
-
`;
|
|
331
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PRD markdown 解析器。
|
|
3
|
-
*
|
|
4
|
-
* 职责:
|
|
5
|
-
* 1. 在需求目录中发现 PRD markdown 文件。
|
|
6
|
-
* 2. 解析 PRD 章节 4.1(功能列表)和 4.2.x(功能详细说明)。
|
|
7
|
-
* 3. 输出结构化的 PrdFunctionEntry[],供 testcase 装配阶段确定性使用。
|
|
8
|
-
*
|
|
9
|
-
* 设计取舍:
|
|
10
|
-
* - 全程使用正则/行解析,不调用 LLM,保证幂等性。
|
|
11
|
-
* - 只识别当前观察到的 PRD 结构(`4.1` 表格 + `4.2.x` 子章节 + 粗体字段标题)。
|
|
12
|
-
* - 解析失败时返回空数组并在 inferenceTrace 中记录,不阻断 pipeline。
|
|
13
|
-
*/
|
|
14
|
-
/** PRD 中的单个功能条目。 */
|
|
15
|
-
export type PrdFunctionEntry = {
|
|
16
|
-
/** 功能编号,如 F001。 */
|
|
17
|
-
featureId: string;
|
|
18
|
-
/** 功能名称,如「同步窗口管理系统」。 */
|
|
19
|
-
featureName: string;
|
|
20
|
-
/** 功能模块,如「大厅地址管理」。 */
|
|
21
|
-
featureModule?: string;
|
|
22
|
-
/** 功能描述。 */
|
|
23
|
-
description: string;
|
|
24
|
-
/** 输入条件。 */
|
|
25
|
-
inputConditions?: string;
|
|
26
|
-
/** 处理逻辑,保留列表顺序。 */
|
|
27
|
-
processingLogic: string[];
|
|
28
|
-
/** 输出结果,保留列表顺序。 */
|
|
29
|
-
outputResult: string[];
|
|
30
|
-
/** 业务规则,保留列表顺序。 */
|
|
31
|
-
businessRules: string[];
|
|
32
|
-
/** 约束条件,保留列表顺序。 */
|
|
33
|
-
constraints: string[];
|
|
34
|
-
};
|
|
35
|
-
/** PRD 提取产物。 */
|
|
36
|
-
export type PrdExtraction = {
|
|
37
|
-
/** PRD 文件绝对路径。 */
|
|
38
|
-
prdPath: string;
|
|
39
|
-
/** 解析出的功能条目,按 PRD 顺序排列。 */
|
|
40
|
-
functions: PrdFunctionEntry[];
|
|
41
|
-
/** 解析时间 ISO 字符串,仅用于审计,不影响 testcase 生成。 */
|
|
42
|
-
generatedAt: string;
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* 在目录中查找 PRD markdown 文件。
|
|
46
|
-
*
|
|
47
|
-
* 规则(满足任一即返回):
|
|
48
|
-
* 1. 文件名包含 `prd`(大小写不敏感)且以 `.md` 结尾。
|
|
49
|
-
* 2. 文件正文包含 `## 4. 功能需求` 标题。
|
|
50
|
-
*
|
|
51
|
-
* @param dir - 需求文档目录。
|
|
52
|
-
* @returns PRD 文件绝对路径;不存在时返回 undefined。
|
|
53
|
-
*/
|
|
54
|
-
export declare function findPrdMarkdown(dir: string): string | undefined;
|
|
55
|
-
/**
|
|
56
|
-
* 读取 PRD 文件并提取功能条目。
|
|
57
|
-
*
|
|
58
|
-
* @param prdPath - PRD markdown 文件绝对路径。
|
|
59
|
-
* @param now - 可选的解析时间戳,用于测试冻结。
|
|
60
|
-
* @returns PRD 提取产物。
|
|
61
|
-
*/
|
|
62
|
-
export declare function loadPrdExtraction(prdPath: string, now?: string): PrdExtraction;
|
|
63
|
-
/**
|
|
64
|
-
* 解析 PRD markdown 文本,提取功能列表 + 功能详细说明。
|
|
65
|
-
*
|
|
66
|
-
* @param markdown - PRD 完整 markdown 文本。
|
|
67
|
-
* @returns 按 PRD 顺序排列的功能条目数组。
|
|
68
|
-
*/
|
|
69
|
-
export declare function extractPrdFunctions(markdown: string): PrdFunctionEntry[];
|
|
70
|
-
//# sourceMappingURL=prd-extractor.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"prd-extractor.d.ts","sourceRoot":"","sources":["../../../src/extractors/prd-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,oBAAoB;AACpB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,mBAAmB;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,sBAAsB;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY;IACZ,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB;IACnB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,mBAAmB;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mBAAmB;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,mBAAmB;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AAEF,gBAAgB;AAChB,MAAM,MAAM,aAAa,GAAG;IAC1B,kBAAkB;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAsB/D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,SAA2B,GAAG,aAAa,CAIhG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAqBxE"}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PRD markdown 解析器。
|
|
3
|
-
*
|
|
4
|
-
* 职责:
|
|
5
|
-
* 1. 在需求目录中发现 PRD markdown 文件。
|
|
6
|
-
* 2. 解析 PRD 章节 4.1(功能列表)和 4.2.x(功能详细说明)。
|
|
7
|
-
* 3. 输出结构化的 PrdFunctionEntry[],供 testcase 装配阶段确定性使用。
|
|
8
|
-
*
|
|
9
|
-
* 设计取舍:
|
|
10
|
-
* - 全程使用正则/行解析,不调用 LLM,保证幂等性。
|
|
11
|
-
* - 只识别当前观察到的 PRD 结构(`4.1` 表格 + `4.2.x` 子章节 + 粗体字段标题)。
|
|
12
|
-
* - 解析失败时返回空数组并在 inferenceTrace 中记录,不阻断 pipeline。
|
|
13
|
-
*/
|
|
14
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
15
|
-
import path from 'node:path';
|
|
16
|
-
/**
|
|
17
|
-
* 在目录中查找 PRD markdown 文件。
|
|
18
|
-
*
|
|
19
|
-
* 规则(满足任一即返回):
|
|
20
|
-
* 1. 文件名包含 `prd`(大小写不敏感)且以 `.md` 结尾。
|
|
21
|
-
* 2. 文件正文包含 `## 4. 功能需求` 标题。
|
|
22
|
-
*
|
|
23
|
-
* @param dir - 需求文档目录。
|
|
24
|
-
* @returns PRD 文件绝对路径;不存在时返回 undefined。
|
|
25
|
-
*/
|
|
26
|
-
export function findPrdMarkdown(dir) {
|
|
27
|
-
if (!existsSync(dir)) {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
const candidates = readdirSync(dir).filter((file) => file.endsWith('.md'));
|
|
31
|
-
for (const file of candidates) {
|
|
32
|
-
const fullPath = path.join(dir, file);
|
|
33
|
-
if (/prd/i.test(file)) {
|
|
34
|
-
return fullPath;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
for (const file of candidates) {
|
|
38
|
-
const fullPath = path.join(dir, file);
|
|
39
|
-
const content = readFileSync(fullPath, 'utf8');
|
|
40
|
-
if (/##\s*4\.\s*功能需求/.test(content)) {
|
|
41
|
-
return fullPath;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* 读取 PRD 文件并提取功能条目。
|
|
48
|
-
*
|
|
49
|
-
* @param prdPath - PRD markdown 文件绝对路径。
|
|
50
|
-
* @param now - 可选的解析时间戳,用于测试冻结。
|
|
51
|
-
* @returns PRD 提取产物。
|
|
52
|
-
*/
|
|
53
|
-
export function loadPrdExtraction(prdPath, now = new Date().toISOString()) {
|
|
54
|
-
const markdown = readFileSync(prdPath, 'utf8');
|
|
55
|
-
const functions = extractPrdFunctions(markdown);
|
|
56
|
-
return { prdPath, functions, generatedAt: now };
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* 解析 PRD markdown 文本,提取功能列表 + 功能详细说明。
|
|
60
|
-
*
|
|
61
|
-
* @param markdown - PRD 完整 markdown 文本。
|
|
62
|
-
* @returns 按 PRD 顺序排列的功能条目数组。
|
|
63
|
-
*/
|
|
64
|
-
export function extractPrdFunctions(markdown) {
|
|
65
|
-
const functionList = parseFunctionList(markdown);
|
|
66
|
-
const details = parseFunctionDetails(markdown);
|
|
67
|
-
const detailMap = new Map(details.map((d) => [d.featureId, d]));
|
|
68
|
-
// 以功能列表为基准顺序,合并详细说明;若详细说明缺失则使用列表中的描述兜底。
|
|
69
|
-
return functionList.map((listItem) => {
|
|
70
|
-
const detail = detailMap.get(listItem.featureId);
|
|
71
|
-
return {
|
|
72
|
-
featureId: listItem.featureId,
|
|
73
|
-
featureName: listItem.featureName,
|
|
74
|
-
featureModule: listItem.featureModule,
|
|
75
|
-
description: detail?.description ?? listItem.description,
|
|
76
|
-
inputConditions: detail?.inputConditions,
|
|
77
|
-
processingLogic: detail?.processingLogic ?? [],
|
|
78
|
-
outputResult: detail?.outputResult ?? [],
|
|
79
|
-
businessRules: detail?.businessRules ?? [],
|
|
80
|
-
constraints: detail?.constraints ?? [],
|
|
81
|
-
};
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* 解析 4.1 功能列表表格。
|
|
86
|
-
*
|
|
87
|
-
* 表格列(按当前 PRD 约定):
|
|
88
|
-
* | 功能模块 | 功能编号 | 功能名称 | 功能描述 | 功能类型 | ... |
|
|
89
|
-
*/
|
|
90
|
-
function parseFunctionList(markdown) {
|
|
91
|
-
const result = [];
|
|
92
|
-
const sectionMatch = markdown.match(/###\s*4\.1\s*功能列表[\s\S]*?(?=###\s*4\.2|##\s+5|\n##\s)/);
|
|
93
|
-
if (!sectionMatch) {
|
|
94
|
-
return result;
|
|
95
|
-
}
|
|
96
|
-
const lines = sectionMatch[0].split('\n');
|
|
97
|
-
for (const line of lines) {
|
|
98
|
-
const trimmed = line.trim();
|
|
99
|
-
if (!trimmed.startsWith('|'))
|
|
100
|
-
continue;
|
|
101
|
-
if (trimmed.includes('功能编号'))
|
|
102
|
-
continue; // header / separator
|
|
103
|
-
if (/^\|\s*-+/.test(trimmed))
|
|
104
|
-
continue; // markdown separator
|
|
105
|
-
const cells = trimmed
|
|
106
|
-
.split('|')
|
|
107
|
-
.map((c) => c.trim())
|
|
108
|
-
.filter((c) => c.length > 0);
|
|
109
|
-
if (cells.length < 4)
|
|
110
|
-
continue;
|
|
111
|
-
const featureModule = cells[0];
|
|
112
|
-
const featureId = cells[1];
|
|
113
|
-
const featureName = cells[2];
|
|
114
|
-
const description = cells[3];
|
|
115
|
-
if (!/^F\d{3,}$/i.test(featureId))
|
|
116
|
-
continue;
|
|
117
|
-
result.push({ featureId: featureId.toUpperCase(), featureName, featureModule, description });
|
|
118
|
-
}
|
|
119
|
-
return result;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* 解析 4.2.x 功能详细说明。
|
|
123
|
-
*/
|
|
124
|
-
function parseFunctionDetails(markdown) {
|
|
125
|
-
const result = [];
|
|
126
|
-
// 匹配每个 4.2.x 子章节:#### 4.2.x Fxxx 功能名称
|
|
127
|
-
const sectionRegex = /####\s*4\.2\.(\d+)\s+((F\d{3,})\s+(.+?))\s*(?=\n)/g;
|
|
128
|
-
let match;
|
|
129
|
-
const sections = [];
|
|
130
|
-
while ((match = sectionRegex.exec(markdown)) !== null) {
|
|
131
|
-
sections.push({
|
|
132
|
-
start: match.index,
|
|
133
|
-
featureId: match[3].toUpperCase(),
|
|
134
|
-
featureName: match[4].trim(),
|
|
135
|
-
end: match.index + match[0].length,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
for (let i = 0; i < sections.length; i++) {
|
|
139
|
-
const current = sections[i];
|
|
140
|
-
const nextSection = sections[i + 1];
|
|
141
|
-
const sectionEnd = findSectionEnd(current.end, markdown, nextSection?.start);
|
|
142
|
-
const sectionBody = markdown.slice(current.end, sectionEnd);
|
|
143
|
-
result.push(parseFunctionDetailSection(current.featureId, current.featureName, sectionBody));
|
|
144
|
-
}
|
|
145
|
-
return result;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* 计算子章节结束位置。
|
|
149
|
-
*
|
|
150
|
-
* 结束位置取以下候选的最小值:
|
|
151
|
-
* 1. 下一个同层 `####` 子章节的开始位置。
|
|
152
|
-
* 2. 下一个更高层标题(`###` 或 `##`)的开始位置。
|
|
153
|
-
* 3. markdown 末尾。
|
|
154
|
-
*/
|
|
155
|
-
function findSectionEnd(start, markdown, nextSectionStart) {
|
|
156
|
-
const nextHeadingMatch = markdown.slice(start).match(/\n#{1,3}\s/m);
|
|
157
|
-
const nextHeadingStart = nextHeadingMatch
|
|
158
|
-
? start + nextHeadingMatch.index + 1 // +1 跳过前面的换行
|
|
159
|
-
: undefined;
|
|
160
|
-
const candidates = [nextSectionStart, nextHeadingStart].filter((n) => n !== undefined);
|
|
161
|
-
return candidates.length > 0 ? Math.min(...candidates) : markdown.length;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* 解析单个 4.2.x 子章节内容。
|
|
165
|
-
*/
|
|
166
|
-
function parseFunctionDetailSection(featureId, featureName, sectionBody) {
|
|
167
|
-
const fields = splitDetailFields(sectionBody);
|
|
168
|
-
return {
|
|
169
|
-
featureId,
|
|
170
|
-
featureName,
|
|
171
|
-
description: fields['功能描述']?.join('\n').trim() ?? '',
|
|
172
|
-
inputConditions: fields['输入条件']?.join('\n').trim() || undefined,
|
|
173
|
-
processingLogic: extractListItems(fields['处理逻辑']),
|
|
174
|
-
outputResult: extractListItems(fields['输出结果']),
|
|
175
|
-
businessRules: extractListItems(fields['业务规则']),
|
|
176
|
-
constraints: extractListItems(fields['约束条件']),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* 按粗体字段标题把子章节内容切分成多个字段块。
|
|
181
|
-
*
|
|
182
|
-
* 字段标题形如 `**功能描述**:` 或 `**功能描述**:`(注意中文冒号)。
|
|
183
|
-
*/
|
|
184
|
-
function splitDetailFields(body) {
|
|
185
|
-
const lines = body.split('\n');
|
|
186
|
-
const fields = {};
|
|
187
|
-
let currentField;
|
|
188
|
-
const fieldHeaderRegex = /^\*\*([^*]+)\*\*[::]\s*(.*)$/;
|
|
189
|
-
for (const rawLine of lines) {
|
|
190
|
-
const line = rawLine.trimEnd();
|
|
191
|
-
const headerMatch = line.trim().match(fieldHeaderRegex);
|
|
192
|
-
if (headerMatch) {
|
|
193
|
-
currentField = headerMatch[1].trim();
|
|
194
|
-
const rest = headerMatch[2].trim();
|
|
195
|
-
fields[currentField] = [];
|
|
196
|
-
if (rest) {
|
|
197
|
-
fields[currentField].push(rest);
|
|
198
|
-
}
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
if (currentField && line.trim().length > 0) {
|
|
202
|
-
// 跳过空行,但保留列表项和缩进内容
|
|
203
|
-
fields[currentField].push(line);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return fields;
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* 从字段块中提取列表项。
|
|
210
|
-
*
|
|
211
|
-
* 支持:
|
|
212
|
-
* - 有序列表 `1. ...`
|
|
213
|
-
* - 无序列表 `- ...`
|
|
214
|
-
* - 缩进列表(二级列表作为前一项的延续,不单独拆分)
|
|
215
|
-
*
|
|
216
|
-
* 过滤:
|
|
217
|
-
* - 空项
|
|
218
|
-
* - 纯 `--` 等表格/排版分隔符
|
|
219
|
-
*/
|
|
220
|
-
function extractListItems(lines) {
|
|
221
|
-
if (!lines || lines.length === 0)
|
|
222
|
-
return [];
|
|
223
|
-
const items = [];
|
|
224
|
-
for (const rawLine of lines) {
|
|
225
|
-
const line = rawLine.trim();
|
|
226
|
-
if (line.length === 0)
|
|
227
|
-
continue;
|
|
228
|
-
if (/^-+$/.test(line))
|
|
229
|
-
continue; // 过滤 '--' 分隔符
|
|
230
|
-
const listMatch = line.match(/^(?:\d+\.|-)\s+(.+)$/);
|
|
231
|
-
if (listMatch) {
|
|
232
|
-
const content = listMatch[1].trim();
|
|
233
|
-
if (content)
|
|
234
|
-
items.push(content);
|
|
235
|
-
}
|
|
236
|
-
else if (line.startsWith('-')) {
|
|
237
|
-
const content = line.replace(/^-\s*/, '').trim();
|
|
238
|
-
if (content)
|
|
239
|
-
items.push(content);
|
|
240
|
-
}
|
|
241
|
-
else if (items.length > 0 && rawLine.startsWith(' ')) {
|
|
242
|
-
// 缩进内容追加到前一项
|
|
243
|
-
items[items.length - 1] += ' ' + line;
|
|
244
|
-
}
|
|
245
|
-
// 非列表行(如普通段落)忽略,避免把标题残留混入
|
|
246
|
-
}
|
|
247
|
-
return items;
|
|
248
|
-
}
|