@epoint-testtech/ep-stage-skill 0.0.3-alpha.1
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 -0
- package/codex-skill/ep-stage/create-project/SKILL.md +59 -0
- package/codex-skill/ep-stage/glue-test/SKILL.md +258 -0
- package/codex-skill/ep-stage/glue-test/references/crud-pipeline.md +139 -0
- package/codex-skill/ep-stage/glue-test/references/gap-review-protocol.md +43 -0
- package/codex-skill/ep-stage/glue-test/references/harness-principles.md +46 -0
- package/codex-skill/ep-stage/glue-test/scripts/generate-crud-spec.mjs +149 -0
- package/codex-skill/ep-stage/glue-testcase/SKILL.md +31 -0
- package/codex-skill/ep-stage/glue-testcase/examples/observable-testcase.json +40 -0
- package/codex-skill/ep-stage/glue-testcase/references/testcase-schema.md +67 -0
- package/codex-skill/ep-stage/recording-to-glue/SKILL.md +27 -0
- package/codex-skill/ep-stage/scripts/validate-skill.mjs +73 -0
- package/dist/src/capability/coverage-diff.d.ts +34 -0
- package/dist/src/capability/coverage-diff.d.ts.map +1 -0
- package/dist/src/capability/coverage-diff.js +91 -0
- package/dist/src/capability/page-structure.d.ts +31 -0
- package/dist/src/capability/page-structure.d.ts.map +1 -0
- package/dist/src/capability/page-structure.js +50 -0
- package/dist/src/capability/scenario-inference.d.ts +36 -0
- package/dist/src/capability/scenario-inference.d.ts.map +1 -0
- package/dist/src/capability/scenario-inference.js +114 -0
- package/dist/src/cli/generate-crud-contract.d.ts +2 -0
- package/dist/src/cli/generate-crud-contract.d.ts.map +1 -0
- package/dist/src/cli/generate-crud-contract.js +77 -0
- package/dist/src/cli/generate-playwright-tests.d.ts +30 -0
- package/dist/src/cli/generate-playwright-tests.d.ts.map +1 -0
- package/dist/src/cli/generate-playwright-tests.js +81 -0
- package/dist/src/cli/run-gap-pipeline.d.ts +256 -0
- package/dist/src/cli/run-gap-pipeline.d.ts.map +1 -0
- package/dist/src/cli/run-gap-pipeline.js +1468 -0
- package/dist/src/context/stage-context.d.ts +63 -0
- package/dist/src/context/stage-context.d.ts.map +1 -0
- package/dist/src/context/stage-context.js +297 -0
- package/dist/src/contracts/crud-business-module.d.ts +645 -0
- package/dist/src/contracts/crud-business-module.d.ts.map +1 -0
- package/dist/src/contracts/crud-business-module.js +1 -0
- package/dist/src/contracts/gap-inference.d.ts +213 -0
- package/dist/src/contracts/gap-inference.d.ts.map +1 -0
- package/dist/src/contracts/gap-inference.js +11 -0
- package/dist/src/contracts/observable-chain.d.ts +250 -0
- package/dist/src/contracts/observable-chain.d.ts.map +1 -0
- package/dist/src/contracts/observable-chain.js +1 -0
- package/dist/src/extractors/code-list.d.ts +40 -0
- package/dist/src/extractors/code-list.d.ts.map +1 -0
- package/dist/src/extractors/code-list.js +225 -0
- package/dist/src/extractors/html-page.d.ts +67 -0
- package/dist/src/extractors/html-page.d.ts.map +1 -0
- package/dist/src/extractors/html-page.js +195 -0
- package/dist/src/extractors/java-action.d.ts +8 -0
- package/dist/src/extractors/java-action.d.ts.map +1 -0
- package/dist/src/extractors/java-action.js +53 -0
- package/dist/src/extractors/spec-yaml.d.ts +28 -0
- package/dist/src/extractors/spec-yaml.d.ts.map +1 -0
- package/dist/src/extractors/spec-yaml.js +29 -0
- package/dist/src/gap-planner/action-candidates.d.ts +9 -0
- package/dist/src/gap-planner/action-candidates.d.ts.map +1 -0
- package/dist/src/gap-planner/action-candidates.js +66 -0
- package/dist/src/gap-planner/list-gap-workflows.d.ts +17 -0
- package/dist/src/gap-planner/list-gap-workflows.d.ts.map +1 -0
- package/dist/src/gap-planner/list-gap-workflows.js +47 -0
- package/dist/src/gap-planner/plan-agent-workflows.d.ts +26 -0
- package/dist/src/gap-planner/plan-agent-workflows.d.ts.map +1 -0
- package/dist/src/gap-planner/plan-agent-workflows.js +116 -0
- package/dist/src/gap-planner/skeleton-coverage.d.ts +9 -0
- package/dist/src/gap-planner/skeleton-coverage.d.ts.map +1 -0
- package/dist/src/gap-planner/skeleton-coverage.js +41 -0
- package/dist/src/gap-planner/stable-id.d.ts +16 -0
- package/dist/src/gap-planner/stable-id.d.ts.map +1 -0
- package/dist/src/gap-planner/stable-id.js +19 -0
- package/dist/src/generalization/generalization-eval.d.ts +71 -0
- package/dist/src/generalization/generalization-eval.d.ts.map +1 -0
- package/dist/src/generalization/generalization-eval.js +53 -0
- package/dist/src/generators/agent-inferred-workflow-script.d.ts +22 -0
- package/dist/src/generators/agent-inferred-workflow-script.d.ts.map +1 -0
- package/dist/src/generators/agent-inferred-workflow-script.js +230 -0
- package/dist/src/generators/stage-skeleton-script.d.ts +107 -0
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -0
- package/dist/src/generators/stage-skeleton-script.js +607 -0
- package/dist/src/index.d.ts +52 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +26 -0
- package/dist/src/material/material-inventory.d.ts +92 -0
- package/dist/src/material/material-inventory.d.ts.map +1 -0
- package/dist/src/material/material-inventory.js +191 -0
- package/dist/src/normalizers/crud-contract.d.ts +107 -0
- package/dist/src/normalizers/crud-contract.d.ts.map +1 -0
- package/dist/src/normalizers/crud-contract.js +1068 -0
- package/dist/src/testcase/testcase-generator.d.ts +43 -0
- package/dist/src/testcase/testcase-generator.d.ts.map +1 -0
- package/dist/src/testcase/testcase-generator.js +152 -0
- package/dist/src/testcase/testcase-skeleton.d.ts +91 -0
- package/dist/src/testcase/testcase-skeleton.d.ts.map +1 -0
- package/dist/src/testcase/testcase-skeleton.js +121 -0
- package/dist/src/testcase/testcase-spec-assembly.d.ts +11 -0
- package/dist/src/testcase/testcase-spec-assembly.d.ts.map +1 -0
- package/dist/src/testcase/testcase-spec-assembly.js +24 -0
- package/dist/src/trace/review-summary.d.ts +17 -0
- package/dist/src/trace/review-summary.d.ts.map +1 -0
- package/dist/src/trace/review-summary.js +34 -0
- package/dist/src/trace/trace-writer.d.ts +17 -0
- package/dist/src/trace/trace-writer.d.ts.map +1 -0
- package/dist/src/trace/trace-writer.js +81 -0
- package/dist/test/crud-contract.test.d.ts +2 -0
- package/dist/test/crud-contract.test.d.ts.map +1 -0
- package/dist/test/crud-contract.test.js +819 -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 +141 -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/observable-pipeline.test.d.ts +2 -0
- package/dist/test/observable-pipeline.test.d.ts.map +1 -0
- package/dist/test/observable-pipeline.test.js +461 -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/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 +263 -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/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 +134 -0
- package/docs/mvp-usage-guide.md +298 -0
- package/examples/schemeresource-observable-docs/schemeresource.context.md +20 -0
- package/examples/schemeresource.module-hints.json +38 -0
- package/examples/schemeresource.observable.code_list.md +37 -0
- package/examples/zwplace-observable-docs/zwplace.context.md +16 -0
- package/examples/zwplace-placecategory-validation.json +29 -0
- package/examples/zwplace.module-hints.json +69 -0
- package/examples/zwplace.observable.code_list.md +37 -0
- package/package.json +38 -0
|
@@ -0,0 +1,1468 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { extractCodeListSummary } from '../extractors/code-list.js';
|
|
7
|
+
import { extractHtmlPage } from '../extractors/html-page.js';
|
|
8
|
+
import { extractJavaAction } from '../extractors/java-action.js';
|
|
9
|
+
import { extractSpecYaml } from '../extractors/spec-yaml.js';
|
|
10
|
+
import { buildCrudBusinessModuleContract } from '../normalizers/crud-contract.js';
|
|
11
|
+
import { collectActionCandidates } from '../gap-planner/action-candidates.js';
|
|
12
|
+
import { createSkeletonCoverage } from '../gap-planner/skeleton-coverage.js';
|
|
13
|
+
import { planAgentInferredWorkflows } from '../gap-planner/plan-agent-workflows.js';
|
|
14
|
+
import { resolveStageContext } from '../context/stage-context.js';
|
|
15
|
+
import { createTraceWriter } from '../trace/trace-writer.js';
|
|
16
|
+
import { renderReviewSummary } from '../trace/review-summary.js';
|
|
17
|
+
import { resolveMaterialInventory } from '../material/material-inventory.js';
|
|
18
|
+
import { summarizePageStructure } from '../capability/page-structure.js';
|
|
19
|
+
import { inferPageScenarios } from '../capability/scenario-inference.js';
|
|
20
|
+
import { createCoverageDiff } from '../capability/coverage-diff.js';
|
|
21
|
+
import { generateGlueTestcases } from '../testcase/testcase-generator.js';
|
|
22
|
+
import { summarizeGeneralizationRun } from '../generalization/generalization-eval.js';
|
|
23
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
24
|
+
const repoRoot = path.resolve(packageRoot, '../..');
|
|
25
|
+
const UNRESOLVED_MENU_PLACEHOLDER = '未确认菜单路径';
|
|
26
|
+
const PROTECTED_KNOWLEDGE_PROJECT_ROOT = path.join(repoRoot, 'knowledge-project');
|
|
27
|
+
export const runtimeSummaryMarker = '@@EP_STAGE_RUNTIME_SUMMARY@@';
|
|
28
|
+
/**
|
|
29
|
+
* 把相对仓库根的路径解析为绝对路径。
|
|
30
|
+
*
|
|
31
|
+
* @param value - 可能相对的路径。
|
|
32
|
+
* @returns 绝对路径。
|
|
33
|
+
*/
|
|
34
|
+
function resolveFromRepo(value) {
|
|
35
|
+
return path.isAbsolute(value) ? value : path.resolve(repoRoot, value);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 解析可观测 pipeline 的命令行参数。
|
|
39
|
+
*
|
|
40
|
+
* 默认入口只需 --code-list;--scenario 为场景 gate 的人工确认值(缺省时停在 scenario gate)。
|
|
41
|
+
* 调试覆盖参数 --docs/--webapp/--java-actions/--hints 始终优先于 code_list 钻探。
|
|
42
|
+
*
|
|
43
|
+
* @param argv - process.argv.slice(2)。
|
|
44
|
+
* @returns 解析后的 ObservablePipelineArgs。
|
|
45
|
+
* @throws 缺少 --code-list 或参数对非法时抛错。
|
|
46
|
+
*/
|
|
47
|
+
export function parseObservablePipelineArgs(argv) {
|
|
48
|
+
const result = {};
|
|
49
|
+
const normalizedArgv = argv.filter((arg) => arg !== '--');
|
|
50
|
+
for (let index = 0; index < normalizedArgv.length; index += 2) {
|
|
51
|
+
const key = normalizedArgv[index];
|
|
52
|
+
const value = normalizedArgv[index + 1];
|
|
53
|
+
if (!key?.startsWith('--') || !value) {
|
|
54
|
+
throw new Error(`无效的参数对,靠近 ${key ?? '<empty>'}`);
|
|
55
|
+
}
|
|
56
|
+
result[key.slice(2)] = value;
|
|
57
|
+
}
|
|
58
|
+
if (!result['code-list']) {
|
|
59
|
+
throw new Error('缺少 code_list.md 路径。默认入口:run:gap-pipeline -- --code-list <path/to/code_list.md>');
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
codeList: resolveFromRepo(result['code-list']),
|
|
63
|
+
stageContext: result['stage-context'] ? resolveFromRepo(result['stage-context']) : undefined,
|
|
64
|
+
projectDir: result['project-dir'] ? resolveFromRepo(result['project-dir']) : undefined,
|
|
65
|
+
moduleId: result['module-id'],
|
|
66
|
+
menu: result.menu,
|
|
67
|
+
scenario: result.scenario,
|
|
68
|
+
confirmTestcases: result['confirm-testcases'] === 'true',
|
|
69
|
+
contractOut: result['contract-out'] ? resolveFromRepo(result['contract-out']) : undefined,
|
|
70
|
+
specOut: result['spec-out'] ? resolveFromRepo(result['spec-out']) : undefined,
|
|
71
|
+
playwrightProject: result.project ?? 'chromium',
|
|
72
|
+
headless: result.headless === 'true',
|
|
73
|
+
debugOverrides: {
|
|
74
|
+
docs: result.docs ? resolveFromRepo(result.docs) : undefined,
|
|
75
|
+
webapp: result.webapp ? resolveFromRepo(result.webapp) : undefined,
|
|
76
|
+
javaActions: result['java-actions'] ? resolveFromRepo(result['java-actions']) : undefined,
|
|
77
|
+
hints: result.hints ? resolveFromRepo(result.hints) : undefined,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 判断目标路径是否位于主仓 knowledge-project 受保护只读区。
|
|
83
|
+
*
|
|
84
|
+
* @param targetPath - 待写入路径。
|
|
85
|
+
* @returns true 表示该路径受主仓 knowledge-project 只读约束保护。
|
|
86
|
+
*/
|
|
87
|
+
export function isProtectedKnowledgeProjectPath(targetPath) {
|
|
88
|
+
const relativePath = path.relative(PROTECTED_KNOWLEDGE_PROJECT_ROOT, path.resolve(targetPath));
|
|
89
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 把路径统一渲染为 code_list.md 中更稳定的相对路径(POSIX 分隔符)。
|
|
93
|
+
*
|
|
94
|
+
* @param codeListPath - code_list.md 路径。
|
|
95
|
+
* @param targetPath - 目标物料路径。
|
|
96
|
+
* @returns 相对 code_list.md 的路径字符串。
|
|
97
|
+
*/
|
|
98
|
+
function renderRelativeCodeListPath(codeListPath, targetPath) {
|
|
99
|
+
return path.relative(path.dirname(codeListPath), targetPath).split(path.sep).join('/');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 将标量字段写回 code_list.md;已存在时覆盖,不存在时插入到正文前。
|
|
103
|
+
*
|
|
104
|
+
* @param content - 原始 code_list.md 内容。
|
|
105
|
+
* @param key - 字段名。
|
|
106
|
+
* @param value - 字段值。
|
|
107
|
+
* @returns 更新后的内容。
|
|
108
|
+
*/
|
|
109
|
+
function upsertCodeListScalar(content, key, value) {
|
|
110
|
+
const renderedLine = `${key}: ${JSON.stringify(value)}`;
|
|
111
|
+
const scalarPattern = new RegExp(`^\\s*${key}\\s*:\\s*.+$`, 'm');
|
|
112
|
+
if (scalarPattern.test(content)) {
|
|
113
|
+
return content.replace(scalarPattern, renderedLine);
|
|
114
|
+
}
|
|
115
|
+
const firstDivider = content.match(/^---\s*$/m);
|
|
116
|
+
if (firstDivider?.index !== undefined) {
|
|
117
|
+
return `${content.slice(0, firstDivider.index)}${renderedLine}\n${content.slice(firstDivider.index)}`;
|
|
118
|
+
}
|
|
119
|
+
const titleMatch = content.match(/^#.*(?:\r?\n|$)/);
|
|
120
|
+
if (titleMatch?.index === 0) {
|
|
121
|
+
const insertIndex = titleMatch[0].length;
|
|
122
|
+
return `${content.slice(0, insertIndex)}${renderedLine}\n\n${content.slice(insertIndex)}`;
|
|
123
|
+
}
|
|
124
|
+
return `${renderedLine}\n\n${content}`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 将补齐后的字段和物料引用写回 code_list.md 内容。
|
|
128
|
+
*
|
|
129
|
+
* @param content - 原始 code_list.md 内容。
|
|
130
|
+
* @param input - 待写回字段。
|
|
131
|
+
* @returns 更新后的 code_list.md 内容。
|
|
132
|
+
*/
|
|
133
|
+
export function applyCodeListWriteBack(content, input) {
|
|
134
|
+
let nextContent = content;
|
|
135
|
+
if (input.knowledgeRoot) {
|
|
136
|
+
nextContent = upsertCodeListScalar(nextContent, 'knowledgeRoot', input.knowledgeRoot);
|
|
137
|
+
}
|
|
138
|
+
if (input.menu) {
|
|
139
|
+
nextContent = upsertCodeListScalar(nextContent, 'menu', input.menu);
|
|
140
|
+
}
|
|
141
|
+
const missingRefs = input.materialRefs.filter((materialRef) => !nextContent.includes(`\`${materialRef}\``));
|
|
142
|
+
if (missingRefs.length === 0) {
|
|
143
|
+
return nextContent;
|
|
144
|
+
}
|
|
145
|
+
return `${nextContent.trimEnd()}\n\n## 交互补齐物料\n\n${missingRefs
|
|
146
|
+
.map((materialRef) => `- \`${materialRef}\``)
|
|
147
|
+
.join('\n')}\n`;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 从用户提供的目录或文件路径中展开可写回的物料引用。
|
|
151
|
+
*
|
|
152
|
+
* HTML 目录会展开为全部 `.html` 文件;Java Action 目录会展开为全部 `*Action.java` 文件;
|
|
153
|
+
* 其余文件按单文件写回。
|
|
154
|
+
*
|
|
155
|
+
* @param codeListPath - code_list.md 路径。
|
|
156
|
+
* @param inputPath - 用户输入的文件或目录路径。
|
|
157
|
+
* @param kind - 物料类别。
|
|
158
|
+
* @returns 可写回 code_list.md 的相对路径列表。
|
|
159
|
+
*/
|
|
160
|
+
export function collectCodeListMaterialRefsFromPath(codeListPath, inputPath, kind) {
|
|
161
|
+
const resolvedPath = path.resolve(inputPath);
|
|
162
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
163
|
+
throw new Error(`交互补齐的路径不存在:${resolvedPath}`);
|
|
164
|
+
}
|
|
165
|
+
const stat = fs.statSync(resolvedPath);
|
|
166
|
+
if (!stat.isDirectory()) {
|
|
167
|
+
return [renderRelativeCodeListPath(codeListPath, resolvedPath)];
|
|
168
|
+
}
|
|
169
|
+
const fileNames = fs.readdirSync(resolvedPath).sort();
|
|
170
|
+
const matchedFiles = fileNames.filter((fileName) => {
|
|
171
|
+
if (kind === 'html') {
|
|
172
|
+
return fileName.endsWith('.html');
|
|
173
|
+
}
|
|
174
|
+
if (kind === 'java_action') {
|
|
175
|
+
return fileName.endsWith('Action.java');
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
});
|
|
179
|
+
return matchedFiles.map((fileName) => renderRelativeCodeListPath(codeListPath, path.join(resolvedPath, fileName)));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 判断当前终端是否适合做交互补齐。
|
|
183
|
+
*
|
|
184
|
+
* @returns true 表示 stdin/stdout 都是 TTY,可安全提问。
|
|
185
|
+
*/
|
|
186
|
+
function canPromptInteractively() {
|
|
187
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 校验路径存在(调试覆盖参数指向的路径)。
|
|
191
|
+
*/
|
|
192
|
+
function ensurePathExists(value, flag) {
|
|
193
|
+
if (!fs.existsSync(value)) {
|
|
194
|
+
throw new Error(`参数 ${flag} 指向的路径不存在:${value}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 逐项提问,读取用户输入;空输入视为跳过。
|
|
199
|
+
*
|
|
200
|
+
* @param promptText - 提示语。
|
|
201
|
+
* @param initialValue - 可选默认值。
|
|
202
|
+
* @returns 用户输入;空串返回 undefined。
|
|
203
|
+
*/
|
|
204
|
+
async function askOptionalQuestion(promptText, initialValue) {
|
|
205
|
+
const terminal = createInterface({
|
|
206
|
+
input: process.stdin,
|
|
207
|
+
output: process.stdout,
|
|
208
|
+
});
|
|
209
|
+
try {
|
|
210
|
+
const suffix = initialValue ? ` [默认: ${initialValue}]` : '';
|
|
211
|
+
const answer = (await terminal.question(`${promptText}${suffix}: `)).trim();
|
|
212
|
+
if (!answer) {
|
|
213
|
+
return initialValue?.trim() || undefined;
|
|
214
|
+
}
|
|
215
|
+
return answer;
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
terminal.close();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 在 material/menu 信息不全时,TTY 下交互补齐可写回的字段。
|
|
223
|
+
*
|
|
224
|
+
* 本轮只覆盖默认入口缺失最常见的 6 个入口字段:
|
|
225
|
+
* `knowledgeRoot`、`moduleId`、`webappDir`、`javaActionsDir`、`hintsPath`、`menu`。
|
|
226
|
+
*
|
|
227
|
+
* @param input - 当前 code_list、解析结果和 stage-context。
|
|
228
|
+
* @returns 补齐结果;若用户放弃或无新信息则返回空补充。
|
|
229
|
+
*/
|
|
230
|
+
async function promptInteractiveCodeListSupplements(input) {
|
|
231
|
+
const codeListDir = path.dirname(input.codeListPath);
|
|
232
|
+
const wantsSupplement = await askOptionalQuestion('检测到 code_list.md 信息不完整,是否现在补齐并继续?输入 y 继续,直接回车跳过', 'y');
|
|
233
|
+
if ((wantsSupplement ?? '').toLowerCase() !== 'y') {
|
|
234
|
+
return { supplements: {}, wroteBack: false };
|
|
235
|
+
}
|
|
236
|
+
const supplements = {};
|
|
237
|
+
if (!input.codeList.knowledgeRoot) {
|
|
238
|
+
supplements.knowledgeRoot = await askOptionalQuestion('请输入 knowledgeRoot(上游物料根目录,可留空跳过)');
|
|
239
|
+
}
|
|
240
|
+
if (input.materialResult.resolvedInputs.moduleId.status === 'missing') {
|
|
241
|
+
supplements.moduleId = await askOptionalQuestion('请输入 moduleId(如 zwplace)');
|
|
242
|
+
}
|
|
243
|
+
if (input.materialResult.resolvedInputs.webappDir.status !== 'resolved') {
|
|
244
|
+
supplements.webappDir = await askOptionalQuestion('请输入 webappDir(HTML 目录或单个 html 文件路径)');
|
|
245
|
+
}
|
|
246
|
+
if (input.materialResult.resolvedInputs.javaActionsDir.status !== 'resolved') {
|
|
247
|
+
supplements.javaActionsDir = await askOptionalQuestion('请输入 javaActionsDir(Action 目录或单个 Action.java 路径)');
|
|
248
|
+
}
|
|
249
|
+
if (input.materialResult.resolvedInputs.hintsPath.status !== 'resolved') {
|
|
250
|
+
supplements.hintsPath = await askOptionalQuestion('请输入 hintsPath(可选,ModuleHints 文件路径)');
|
|
251
|
+
}
|
|
252
|
+
if (input.materialResult.resolvedInputs.menu.status !== 'resolved') {
|
|
253
|
+
supplements.menu = await askOptionalQuestion('请输入菜单路径 menu(如 一级菜单>二级菜单)');
|
|
254
|
+
}
|
|
255
|
+
const hasSupplements = Object.values(supplements).some((value) => Boolean(value?.trim()));
|
|
256
|
+
if (!hasSupplements) {
|
|
257
|
+
return { supplements: {}, wroteBack: false };
|
|
258
|
+
}
|
|
259
|
+
if (isProtectedKnowledgeProjectPath(input.codeListPath)) {
|
|
260
|
+
return {
|
|
261
|
+
supplements,
|
|
262
|
+
wroteBack: false,
|
|
263
|
+
skippedWriteBackReason: `目标 code_list 位于主仓只读 knowledge-project,已仅对本次运行生效:${input.codeListPath}`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const materialRefs = [
|
|
267
|
+
...(supplements.webappDir
|
|
268
|
+
? collectCodeListMaterialRefsFromPath(input.codeListPath, supplements.webappDir, 'html')
|
|
269
|
+
: []),
|
|
270
|
+
...(supplements.javaActionsDir
|
|
271
|
+
? collectCodeListMaterialRefsFromPath(input.codeListPath, supplements.javaActionsDir, 'java_action')
|
|
272
|
+
: []),
|
|
273
|
+
...(supplements.hintsPath
|
|
274
|
+
? collectCodeListMaterialRefsFromPath(input.codeListPath, supplements.hintsPath, 'module_hints')
|
|
275
|
+
: []),
|
|
276
|
+
];
|
|
277
|
+
const nextContent = applyCodeListWriteBack(fs.readFileSync(input.codeListPath, 'utf8'), {
|
|
278
|
+
knowledgeRoot: supplements.knowledgeRoot
|
|
279
|
+
? renderRelativeCodeListPath(input.codeListPath, path.resolve(supplements.knowledgeRoot))
|
|
280
|
+
: undefined,
|
|
281
|
+
menu: supplements.menu,
|
|
282
|
+
materialRefs,
|
|
283
|
+
});
|
|
284
|
+
fs.mkdirSync(codeListDir, { recursive: true });
|
|
285
|
+
fs.writeFileSync(input.codeListPath, nextContent, 'utf8');
|
|
286
|
+
return { supplements, wroteBack: true };
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 在 menu gate 停下时交互确认菜单,并尽量写回 code_list.md。
|
|
290
|
+
*
|
|
291
|
+
* @param codeListPath - code_list.md 路径。
|
|
292
|
+
* @param menuResolution - 当前菜单解析结果。
|
|
293
|
+
* @returns 人工确认后的菜单;未输入时返回 undefined。
|
|
294
|
+
*/
|
|
295
|
+
async function promptMenuAtGate(codeListPath, menuResolution) {
|
|
296
|
+
const initialValue = menuResolution.status === 'candidate'
|
|
297
|
+
? menuResolution.candidates[0]
|
|
298
|
+
: undefined;
|
|
299
|
+
const menu = await askOptionalQuestion(menuResolution.status === 'candidate'
|
|
300
|
+
? `请输入菜单路径 menu(候选:${menuResolution.candidates.join(' | ')})`
|
|
301
|
+
: '请输入菜单路径 menu', initialValue);
|
|
302
|
+
if (!menu) {
|
|
303
|
+
return { supplements: {}, wroteBack: false };
|
|
304
|
+
}
|
|
305
|
+
if (isProtectedKnowledgeProjectPath(codeListPath)) {
|
|
306
|
+
return {
|
|
307
|
+
supplements: { menu },
|
|
308
|
+
wroteBack: false,
|
|
309
|
+
skippedWriteBackReason: `目标 code_list 位于主仓只读 knowledge-project,menu 仅对本次运行生效:${codeListPath}`,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const nextContent = applyCodeListWriteBack(fs.readFileSync(codeListPath, 'utf8'), {
|
|
313
|
+
menu,
|
|
314
|
+
materialRefs: [],
|
|
315
|
+
});
|
|
316
|
+
fs.writeFileSync(codeListPath, nextContent, 'utf8');
|
|
317
|
+
return {
|
|
318
|
+
supplements: { menu },
|
|
319
|
+
wroteBack: true,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* 读取契约 JSON(Task 9B spec assembly 复用)。
|
|
324
|
+
*/
|
|
325
|
+
function readContract(contractPath) {
|
|
326
|
+
return JSON.parse(fs.readFileSync(contractPath, 'utf8'));
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* 读取 glue-report run-info(Task 9B 报告阶段复用)。
|
|
330
|
+
*/
|
|
331
|
+
function readRunInfo(projectDir) {
|
|
332
|
+
const filePath = path.join(projectDir, 'glue-report', 'run-info.json');
|
|
333
|
+
if (!fs.existsSync(filePath)) {
|
|
334
|
+
return {};
|
|
335
|
+
}
|
|
336
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* 列出目录下所有 .html 文件(按名排序)。
|
|
340
|
+
*/
|
|
341
|
+
function htmlFiles(dir) {
|
|
342
|
+
return fs
|
|
343
|
+
.readdirSync(dir)
|
|
344
|
+
.filter((fileName) => fileName.endsWith('.html'))
|
|
345
|
+
.sort()
|
|
346
|
+
.map((fileName) => path.join(dir, fileName));
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* 列出目录下所有 *Action.java 文件(按名排序)。
|
|
350
|
+
*/
|
|
351
|
+
function javaFiles(dir) {
|
|
352
|
+
return fs
|
|
353
|
+
.readdirSync(dir)
|
|
354
|
+
.filter((fileName) => fileName.endsWith('Action.java'))
|
|
355
|
+
.sort()
|
|
356
|
+
.map((fileName) => path.join(dir, fileName));
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* 解析 spec.yaml;缺失时回退为低置信 spec(保留 module id 与 code_list 标题)。
|
|
360
|
+
*
|
|
361
|
+
* @param docsDir - 文档目录。
|
|
362
|
+
* @param moduleId - 模块 id。
|
|
363
|
+
* @param codeListSummary - code_list 摘要(取 moduleLabel 作为 fallback label)。
|
|
364
|
+
* @returns ExtractedSpecYaml。
|
|
365
|
+
*/
|
|
366
|
+
function resolveSpec(docsDir, moduleId, codeListSummary) {
|
|
367
|
+
const specPath = path.join(docsDir, 'spec.yaml');
|
|
368
|
+
if (fs.existsSync(specPath)) {
|
|
369
|
+
return extractSpecYaml(specPath);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
path: specPath,
|
|
373
|
+
module: {
|
|
374
|
+
id: moduleId,
|
|
375
|
+
label: codeListSummary.moduleLabel,
|
|
376
|
+
},
|
|
377
|
+
fields: [],
|
|
378
|
+
businessRules: [],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* 把 MaterialResolution 解析为路径;未 resolved 时抛错(不应进入下一阶段)。
|
|
383
|
+
*/
|
|
384
|
+
function resolvedPath(input, key) {
|
|
385
|
+
if (input.status !== 'resolved') {
|
|
386
|
+
throw new Error(`${key} 未解析,不能进入下一阶段`);
|
|
387
|
+
}
|
|
388
|
+
return input.path;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* 把 ValueResolution 解析为值;未 resolved 时抛错。
|
|
392
|
+
*/
|
|
393
|
+
function resolvedValue(input, key) {
|
|
394
|
+
if (input.status !== 'resolved') {
|
|
395
|
+
throw new Error(`${key} 未解析,不能进入下一阶段`);
|
|
396
|
+
}
|
|
397
|
+
return input.value;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* 解析本轮 pipeline 的菜单路由。
|
|
401
|
+
*
|
|
402
|
+
* 优先使用已确认的 menu;缺失时根据 contract 的模块名和列表页标题生成候选,
|
|
403
|
+
* 但候选仍需人工确认,不得直接进入可执行阶段。
|
|
404
|
+
*
|
|
405
|
+
* @param input - menu 槽位与 CRUD 契约。
|
|
406
|
+
* @returns 已确认菜单或候选菜单。
|
|
407
|
+
*/
|
|
408
|
+
export function resolveMenuRouteForPipeline(input) {
|
|
409
|
+
if (input.menu.status === 'resolved' || input.menu.status === 'candidate') {
|
|
410
|
+
return input.menu;
|
|
411
|
+
}
|
|
412
|
+
const moduleLabel = input.contract.module.label.trim();
|
|
413
|
+
const listPageTitle = input.contract.pages.list.title.trim();
|
|
414
|
+
const candidates = Array.from(new Set([
|
|
415
|
+
moduleLabel && listPageTitle ? `${moduleLabel}>${listPageTitle}` : undefined,
|
|
416
|
+
listPageTitle || undefined,
|
|
417
|
+
].filter((item) => Boolean(item && item.trim().length > 0))));
|
|
418
|
+
if (candidates.length > 0) {
|
|
419
|
+
return {
|
|
420
|
+
status: 'candidate',
|
|
421
|
+
candidates,
|
|
422
|
+
evidence: 'menu 候选来自 contract.module.label 与 contract.pages.list.title',
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return input.menu;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* 写入单阶段 trace 并追加阶段摘要。
|
|
429
|
+
*
|
|
430
|
+
* @param writer - trace writer。
|
|
431
|
+
* @param stageSummaries - 阶段摘要累积列表。
|
|
432
|
+
* @param stage - 阶段名称。
|
|
433
|
+
* @param payload - 阶段 trace payload。
|
|
434
|
+
*/
|
|
435
|
+
function recordStage(writer, stageSummaries, stage, payload) {
|
|
436
|
+
writer.writeStage(stage, payload);
|
|
437
|
+
stageSummaries.push({
|
|
438
|
+
stage,
|
|
439
|
+
stageLabel: payload.stageLabel,
|
|
440
|
+
status: payload.gate.status,
|
|
441
|
+
conclusion: payload.reasoningSummary?.conclusion ?? `${payload.stageLabel}完成`,
|
|
442
|
+
needsHumanReview: payload.reasoningSummary?.needsHumanReview ?? false,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* 能力候选阶段 trace 转换:动作候选 + 页面结构摘要。
|
|
447
|
+
*/
|
|
448
|
+
function capabilityCandidatesToTrace(input) {
|
|
449
|
+
return {
|
|
450
|
+
stage: 'capability-candidates',
|
|
451
|
+
stageLabel: '能力候选追踪',
|
|
452
|
+
inputs: [{ kind: 'material_inventory', path: 'material-inventory.json' }],
|
|
453
|
+
outputs: input.actionCandidates.map((candidate) => ({
|
|
454
|
+
kind: 'action_candidate',
|
|
455
|
+
path: candidate.actionId,
|
|
456
|
+
description: candidate.label,
|
|
457
|
+
})),
|
|
458
|
+
reasoningSummary: {
|
|
459
|
+
conclusion: `识别动作候选 ${input.actionCandidates.length} 个,页面结构摘要 ${input.pageStructures.length} 个。`,
|
|
460
|
+
evidenceChain: input.actionCandidates.flatMap((candidate) => candidate.evidence.map((evidence) => ({
|
|
461
|
+
source: 'html',
|
|
462
|
+
path: evidence.path,
|
|
463
|
+
text: evidence.evidenceText ?? candidate.label,
|
|
464
|
+
}))),
|
|
465
|
+
alternatives: [],
|
|
466
|
+
confidence: 'medium',
|
|
467
|
+
risks: [],
|
|
468
|
+
needsHumanReview: false,
|
|
469
|
+
},
|
|
470
|
+
candidateCount: input.actionCandidates.length,
|
|
471
|
+
unresolvedCount: 0,
|
|
472
|
+
gate: { status: 'confirmed', confirmedBy: 'policy', reason: '能力候选来自确定性 extractor' },
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* 骨架能力模型阶段 trace 转换。
|
|
477
|
+
*/
|
|
478
|
+
function skeletonCoverageToTrace(coverage) {
|
|
479
|
+
return {
|
|
480
|
+
stage: 'skeleton-capability-model',
|
|
481
|
+
stageLabel: '骨架能力模型追踪',
|
|
482
|
+
inputs: [{ kind: 'capability_candidates', path: 'capability-candidates.json' }],
|
|
483
|
+
outputs: coverage.coveredWorkflows.map((workflow) => ({ kind: 'skeleton_workflow', path: workflow })),
|
|
484
|
+
reasoningSummary: {
|
|
485
|
+
conclusion: `当前骨架覆盖 ${coverage.coveredWorkflows.join('、')}。`,
|
|
486
|
+
evidenceChain: [],
|
|
487
|
+
alternatives: [],
|
|
488
|
+
confidence: 'high',
|
|
489
|
+
risks: [],
|
|
490
|
+
needsHumanReview: false,
|
|
491
|
+
},
|
|
492
|
+
candidateCount: coverage.coveredWorkflows.length,
|
|
493
|
+
unresolvedCount: 0,
|
|
494
|
+
gate: { status: 'confirmed', confirmedBy: 'policy', reason: '骨架能力模型已生成' },
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* coverage diff 阶段 trace 转换。
|
|
499
|
+
*/
|
|
500
|
+
function coverageDiffToTrace(diff) {
|
|
501
|
+
return {
|
|
502
|
+
stage: 'coverage-diff',
|
|
503
|
+
stageLabel: '覆盖差异追踪',
|
|
504
|
+
inputs: [
|
|
505
|
+
{ kind: 'skeleton_capability_model', path: 'skeleton-capability-model.json' },
|
|
506
|
+
{ kind: 'baseline_gap_planner', path: 'baselineWorkflows' },
|
|
507
|
+
],
|
|
508
|
+
outputs: [
|
|
509
|
+
...diff.covered.map((item) => ({ kind: 'covered_action', path: item.actionId, description: item.label })),
|
|
510
|
+
...diff.gapCandidates.map((item) => ({ kind: 'gap_candidate', path: item.actionId, description: item.label })),
|
|
511
|
+
],
|
|
512
|
+
reasoningSummary: diff.reasoningSummary,
|
|
513
|
+
candidateCount: diff.covered.length + diff.gapCandidates.length + diff.nestedCrudCandidates.length,
|
|
514
|
+
unresolvedCount: diff.unresolved.length + diff.nestedCrudCandidates.length,
|
|
515
|
+
gate: diff.reasoningSummary.needsHumanReview
|
|
516
|
+
? { status: 'needs_review', reason: 'coverage diff 存在 gap 或嵌套 CRUD 候选' }
|
|
517
|
+
: { status: 'confirmed', confirmedBy: 'policy', reason: 'coverage diff 无需人工确认' },
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* 用例生成阶段 trace 转换。
|
|
522
|
+
*/
|
|
523
|
+
function testcaseGenerationToTrace(testcases) {
|
|
524
|
+
const needsHumanReview = testcases.reasoningSummary.needsHumanReview ||
|
|
525
|
+
testcases.cases.some((item) => item.reviewStatus !== 'confirmed');
|
|
526
|
+
return {
|
|
527
|
+
stage: 'testcase-generation',
|
|
528
|
+
stageLabel: '用例生成追踪',
|
|
529
|
+
inputs: [{ kind: 'coverage_diff', path: 'coverage-diff.json' }],
|
|
530
|
+
outputs: testcases.cases.map((item) => ({ kind: 'glue_testcase', path: item.caseId, description: item.title })),
|
|
531
|
+
reasoningSummary: testcases.reasoningSummary,
|
|
532
|
+
candidateCount: testcases.cases.length,
|
|
533
|
+
unresolvedCount: needsHumanReview ? 1 : 0,
|
|
534
|
+
gate: needsHumanReview
|
|
535
|
+
? { status: 'needs_review', reason: '存在未确认用例' }
|
|
536
|
+
: { status: 'confirmed', confirmedBy: 'policy', reason: '用例均已确认' },
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* 将 testcase gate 的人工确认结果写回结构化用例文档。
|
|
541
|
+
*
|
|
542
|
+
* @param testcases - 原始用例文档。
|
|
543
|
+
* @returns 所有 case 标记为 confirmed 的新文档。
|
|
544
|
+
*/
|
|
545
|
+
export function confirmGlueTestcaseDocument(testcases) {
|
|
546
|
+
return {
|
|
547
|
+
...testcases,
|
|
548
|
+
cases: testcases.cases.map((item) => ({
|
|
549
|
+
...item,
|
|
550
|
+
reviewStatus: 'confirmed',
|
|
551
|
+
})),
|
|
552
|
+
reasoningSummary: {
|
|
553
|
+
...testcases.reasoningSummary,
|
|
554
|
+
conclusion: `${testcases.reasoningSummary.conclusion} 人工已通过 --confirm-testcases=true 确认 testcase gate。`,
|
|
555
|
+
evidenceChain: [
|
|
556
|
+
...testcases.reasoningSummary.evidenceChain,
|
|
557
|
+
{
|
|
558
|
+
source: 'human',
|
|
559
|
+
path: 'cli --confirm-testcases',
|
|
560
|
+
text: 'confirmed=true',
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
needsHumanReview: false,
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* 回写 testcase markdown frontmatter 的 reviewStatus。
|
|
569
|
+
*
|
|
570
|
+
* @param markdown - 原始 testcase markdown。
|
|
571
|
+
* @param reviewStatus - 目标审阅状态。
|
|
572
|
+
* @returns 更新后的 markdown。
|
|
573
|
+
*/
|
|
574
|
+
export function applyTestcaseReviewStatusToMarkdown(markdown, reviewStatus) {
|
|
575
|
+
return markdown.replace(/^reviewStatus:\s*".*"$/m, `reviewStatus: "${reviewStatus}"`);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* 把 runtime gap 推理摘要转换为可审阅 trace payload。
|
|
579
|
+
*
|
|
580
|
+
* Task 9B 接入 runtime 后写入 runtime-gap-inference.json。
|
|
581
|
+
* 运行时只能补定位和运行态证据,不允许修改业务意图和断言预期。
|
|
582
|
+
*
|
|
583
|
+
* @param runtime - runtime runner 产出的摘要。
|
|
584
|
+
* @returns runtime-gap-inference 阶段 trace payload。
|
|
585
|
+
*/
|
|
586
|
+
export function runtimeSummaryToTrace(runtime) {
|
|
587
|
+
const runtimeStatus = runtime.runtimeStatus ?? 'resolved';
|
|
588
|
+
const runtimeFailed = runtimeStatus !== 'resolved';
|
|
589
|
+
const needsHumanReview = runtime.needsReviewWorkflows.length > 0 || runtimeFailed;
|
|
590
|
+
const runtimeError = runtime.errorMessage ?? runtime.stderr ?? runtime.stdout;
|
|
591
|
+
const runtimeErrorPreview = runtimeError?.slice(0, 500);
|
|
592
|
+
const mismatchSelectors = (runtime.pageStructureSignals ?? [])
|
|
593
|
+
.filter((signal) => signal.runtimeMismatch)
|
|
594
|
+
.map((signal) => signal.selector);
|
|
595
|
+
const needsReviewReasonMap = new Map((runtime.needsReviewWorkflowReasons ?? []).map((item) => [item.workflowId, item.reason]));
|
|
596
|
+
const conclusion = runtimeFailed
|
|
597
|
+
? `运行时推理未完成:${runtimeStatus}。`
|
|
598
|
+
: `运行时已解决 ${runtime.resolvedWorkflows.length} 个 workflow,${runtime.needsReviewWorkflows.length} 个仍需人工审阅。`;
|
|
599
|
+
return {
|
|
600
|
+
stage: 'runtime-gap-inference',
|
|
601
|
+
stageLabel: '运行时 gap 推理追踪',
|
|
602
|
+
inputs: [{ kind: 'playwright_spec', path: runtime.specPath }],
|
|
603
|
+
outputs: [
|
|
604
|
+
...runtime.resolvedWorkflows.map((workflowId) => ({
|
|
605
|
+
kind: 'resolved_runtime_workflow',
|
|
606
|
+
path: workflowId,
|
|
607
|
+
})),
|
|
608
|
+
...(runtime.pageStructureSignals ?? []).map((signal) => ({
|
|
609
|
+
kind: 'runtime_page_structure',
|
|
610
|
+
path: signal.selector,
|
|
611
|
+
description: signal.runtimeVisible ? 'visible' : 'not_visible',
|
|
612
|
+
})),
|
|
613
|
+
],
|
|
614
|
+
reasoningSummary: {
|
|
615
|
+
conclusion,
|
|
616
|
+
evidenceChain: [
|
|
617
|
+
...runtime.resolvedWorkflows.map((workflowId) => ({
|
|
618
|
+
source: 'runtime',
|
|
619
|
+
path: runtime.specPath,
|
|
620
|
+
text: `resolved=${workflowId}`,
|
|
621
|
+
})),
|
|
622
|
+
...runtime.needsReviewWorkflows.map((workflowId) => {
|
|
623
|
+
const reason = needsReviewReasonMap.get(workflowId);
|
|
624
|
+
return {
|
|
625
|
+
source: 'runtime',
|
|
626
|
+
path: runtime.specPath,
|
|
627
|
+
text: reason ? `needs_review=${workflowId} reason=${reason}` : `needs_review=${workflowId}`,
|
|
628
|
+
};
|
|
629
|
+
}),
|
|
630
|
+
...(runtime.pageStructureSignals ?? []).map((signal) => ({
|
|
631
|
+
source: 'runtime',
|
|
632
|
+
path: runtime.specPath,
|
|
633
|
+
text: signal.evidenceText,
|
|
634
|
+
})),
|
|
635
|
+
...(runtimeErrorPreview
|
|
636
|
+
? [{
|
|
637
|
+
source: 'runtime',
|
|
638
|
+
path: runtime.specPath,
|
|
639
|
+
text: runtimeErrorPreview,
|
|
640
|
+
}]
|
|
641
|
+
: []),
|
|
642
|
+
],
|
|
643
|
+
alternatives: ['runtime locator resolve', 'manual review'],
|
|
644
|
+
confidence: runtimeFailed ? 'low' : needsHumanReview ? 'medium' : 'high',
|
|
645
|
+
risks: [
|
|
646
|
+
...(runtimeErrorPreview ? [runtimeErrorPreview] : []),
|
|
647
|
+
...runtime.needsReviewWorkflows.map((workflowId) => {
|
|
648
|
+
const reason = needsReviewReasonMap.get(workflowId);
|
|
649
|
+
return reason ? `${workflowId} 仍需人工审阅:${reason}` : `${workflowId} 仍需人工审阅`;
|
|
650
|
+
}),
|
|
651
|
+
...mismatchSelectors.map((selector) => `${selector} 静态存在但运行时不可见`),
|
|
652
|
+
],
|
|
653
|
+
needsHumanReview,
|
|
654
|
+
},
|
|
655
|
+
candidateCount: runtime.resolvedWorkflows.length + runtime.needsReviewWorkflows.length,
|
|
656
|
+
unresolvedCount: runtime.needsReviewWorkflows.length,
|
|
657
|
+
gate: needsHumanReview
|
|
658
|
+
? { status: 'needs_review', reason: runtimeFailed ? `运行时推理未完成:${runtimeStatus}` : '存在运行时未确认 workflow' }
|
|
659
|
+
: { status: 'confirmed', confirmedBy: 'policy', reason: '运行时 workflow 已解决' },
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* 从结构化 glue-report JSON 中提取失败工作流与 gap finding 摘要。
|
|
664
|
+
*
|
|
665
|
+
* @param reportJsonPath - stage-glue report JSON 路径。
|
|
666
|
+
* @returns 可用于 trace 的失败摘要。
|
|
667
|
+
*/
|
|
668
|
+
function readGlueReportDetails(reportJsonPath) {
|
|
669
|
+
if (!reportJsonPath || !fs.existsSync(reportJsonPath)) {
|
|
670
|
+
return {};
|
|
671
|
+
}
|
|
672
|
+
const report = JSON.parse(fs.readFileSync(reportJsonPath, 'utf8'));
|
|
673
|
+
const failedWorkflows = (report.workflows ?? []).filter((workflow) => workflow.status === 'failed');
|
|
674
|
+
return {
|
|
675
|
+
failedWorkflowSummaries: failedWorkflows.map((workflow) => `${workflow.workflowId}${workflow.workflowName ? `(${workflow.workflowName})` : ''}: ${workflow.errorMessage ?? 'failed'}`),
|
|
676
|
+
gapFindingSummaries: failedWorkflows
|
|
677
|
+
.filter((workflow) => workflow.gapFinding?.kind)
|
|
678
|
+
.map((workflow) => `${workflow.workflowId}: ${workflow.gapFinding?.kind} - ${workflow.gapFinding?.summary ?? '无摘要'}${workflow.gapFinding?.observed ? `;observed=${workflow.gapFinding.observed}` : ''}`),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* 从 `.env` 文件中提取登录 URL。
|
|
683
|
+
*
|
|
684
|
+
* @param envFilePath - `.env` 文件路径。
|
|
685
|
+
* @returns `LOGIN_SYSTEM_URL`;不存在时返回 undefined。
|
|
686
|
+
*/
|
|
687
|
+
export function extractLoginSystemUrlFromEnvFile(envFilePath) {
|
|
688
|
+
if (!fs.existsSync(envFilePath)) {
|
|
689
|
+
return undefined;
|
|
690
|
+
}
|
|
691
|
+
const content = fs.readFileSync(envFilePath, 'utf8');
|
|
692
|
+
const match = content.match(/^\s*LOGIN_SYSTEM_URL\s*=\s*(.+?)\s*$/m);
|
|
693
|
+
if (!match?.[1]) {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
return match[1].trim().replace(/^["']|["']$/g, '');
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* 用轻量 HTTP 头请求补证当前登录环境是否可访问。
|
|
700
|
+
*
|
|
701
|
+
* @param loginUrl - 登录地址。
|
|
702
|
+
* @returns 简短环境证据文本。
|
|
703
|
+
*/
|
|
704
|
+
function probeLoginEnvironment(loginUrl) {
|
|
705
|
+
const result = spawnSync('curl', ['-I', '--max-time', '10', '-sS', loginUrl], {
|
|
706
|
+
cwd: repoRoot,
|
|
707
|
+
encoding: 'utf8',
|
|
708
|
+
});
|
|
709
|
+
const output = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
|
|
710
|
+
if (result.status === 0) {
|
|
711
|
+
const firstLine = output.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
712
|
+
return firstLine ? `curl -I: ${firstLine}` : 'curl -I: success';
|
|
713
|
+
}
|
|
714
|
+
return output ? `curl -I: ${output}` : `curl -I exitCode=${result.status ?? 1}`;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* 胶水测试组装阶段 trace 转换。
|
|
718
|
+
*
|
|
719
|
+
* @param summary - spec assembly 摘要。
|
|
720
|
+
* @returns glue-spec-assembly 阶段 trace payload。
|
|
721
|
+
*/
|
|
722
|
+
export function glueSpecAssemblyToTrace(summary) {
|
|
723
|
+
const assemblyError = summary.errorMessage ?? summary.stderr ?? summary.stdout;
|
|
724
|
+
const assemblyErrorPreview = assemblyError?.slice(0, 500);
|
|
725
|
+
return {
|
|
726
|
+
stage: 'glue-spec-assembly',
|
|
727
|
+
stageLabel: '胶水测试组装追踪',
|
|
728
|
+
inputs: [
|
|
729
|
+
{ kind: 'crud_contract', path: summary.contractPath },
|
|
730
|
+
{ kind: 'glue_testcase', path: summary.testcasePath },
|
|
731
|
+
],
|
|
732
|
+
outputs: summary.generated ? [{ kind: 'playwright_spec', path: summary.specPath }] : [],
|
|
733
|
+
reasoningSummary: {
|
|
734
|
+
conclusion: summary.generated ? '已根据确认用例组装 Playwright spec。' : '未生成 Playwright spec。',
|
|
735
|
+
evidenceChain: [
|
|
736
|
+
{ source: 'human', path: summary.testcasePath, text: `menu=${summary.menu}` },
|
|
737
|
+
...(assemblyErrorPreview
|
|
738
|
+
? [{
|
|
739
|
+
source: 'runtime',
|
|
740
|
+
path: summary.specPath,
|
|
741
|
+
text: assemblyErrorPreview,
|
|
742
|
+
}]
|
|
743
|
+
: []),
|
|
744
|
+
],
|
|
745
|
+
alternatives: [],
|
|
746
|
+
confidence: summary.generated ? 'high' : 'low',
|
|
747
|
+
risks: summary.generated ? [] : [assemblyErrorPreview ?? 'spec assembly 未产出文件'],
|
|
748
|
+
needsHumanReview: !summary.generated,
|
|
749
|
+
},
|
|
750
|
+
candidateCount: 1,
|
|
751
|
+
unresolvedCount: summary.generated ? 0 : 1,
|
|
752
|
+
gate: summary.generated
|
|
753
|
+
? { status: 'confirmed', confirmedBy: 'policy', reason: '已生成 Playwright spec' }
|
|
754
|
+
: { status: 'needs_review', reason: 'Playwright spec 未生成' },
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Playwright 执行阶段 trace 转换。
|
|
759
|
+
*
|
|
760
|
+
* @param summary - Playwright 执行摘要。
|
|
761
|
+
* @returns playwright-execution 阶段 trace payload。
|
|
762
|
+
*/
|
|
763
|
+
export function playwrightExecutionToTrace(summary) {
|
|
764
|
+
const passed = summary.status === 'passed';
|
|
765
|
+
return {
|
|
766
|
+
stage: 'playwright-execution',
|
|
767
|
+
stageLabel: '测试执行追踪',
|
|
768
|
+
inputs: [{ kind: 'playwright_spec', path: summary.specPath }],
|
|
769
|
+
outputs: [{ kind: 'playwright_result', path: String(summary.exitCode), description: summary.status }],
|
|
770
|
+
reasoningSummary: {
|
|
771
|
+
conclusion: passed
|
|
772
|
+
? 'Playwright 执行通过。'
|
|
773
|
+
: `Playwright 执行未通过:${summary.status}。`,
|
|
774
|
+
evidenceChain: [
|
|
775
|
+
...(summary.stdout
|
|
776
|
+
? [{ source: 'runtime', path: summary.specPath, text: summary.stdout.slice(0, 500) }]
|
|
777
|
+
: []),
|
|
778
|
+
...(summary.stderr
|
|
779
|
+
? [{ source: 'runtime', path: summary.specPath, text: summary.stderr.slice(0, 500) }]
|
|
780
|
+
: []),
|
|
781
|
+
...(summary.loginUrl && summary.environmentEvidence
|
|
782
|
+
? [{
|
|
783
|
+
source: 'runtime',
|
|
784
|
+
path: summary.loginUrl,
|
|
785
|
+
text: `环境预检:${summary.environmentEvidence}`,
|
|
786
|
+
}]
|
|
787
|
+
: []),
|
|
788
|
+
],
|
|
789
|
+
alternatives: ['business failure', 'environment unavailable', 'test implementation failure'],
|
|
790
|
+
confidence: passed ? 'high' : 'medium',
|
|
791
|
+
risks: passed ? [] : [summary.stderr ?? summary.stdout ?? 'Playwright 执行失败'],
|
|
792
|
+
needsHumanReview: !passed,
|
|
793
|
+
},
|
|
794
|
+
gate: passed
|
|
795
|
+
? { status: 'confirmed', confirmedBy: 'policy', reason: 'Playwright exitCode=0' }
|
|
796
|
+
: { status: 'needs_review', reason: `Playwright exitCode=${summary.exitCode}` },
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* glue-report 阶段 trace 转换。
|
|
801
|
+
*
|
|
802
|
+
* @param summary - glue-report 摘要。
|
|
803
|
+
* @returns glue-report 阶段 trace payload。
|
|
804
|
+
*/
|
|
805
|
+
export function glueReportToTrace(summary) {
|
|
806
|
+
const outputs = [
|
|
807
|
+
...(summary.stageGlueReportJsonPath
|
|
808
|
+
? [{ kind: 'glue_report_json', path: summary.stageGlueReportJsonPath }]
|
|
809
|
+
: []),
|
|
810
|
+
...(summary.stageGlueReportHtmlPath
|
|
811
|
+
? [{ kind: 'glue_report_html', path: summary.stageGlueReportHtmlPath }]
|
|
812
|
+
: []),
|
|
813
|
+
...(summary.playwrightReportDirPath
|
|
814
|
+
? [{ kind: 'playwright_report_dir', path: summary.playwrightReportDirPath }]
|
|
815
|
+
: []),
|
|
816
|
+
];
|
|
817
|
+
return {
|
|
818
|
+
stage: 'glue-report',
|
|
819
|
+
stageLabel: '执行报告追踪',
|
|
820
|
+
inputs: [{ kind: 'project_dir', path: summary.projectDir }],
|
|
821
|
+
outputs,
|
|
822
|
+
reasoningSummary: {
|
|
823
|
+
conclusion: outputs.length > 0
|
|
824
|
+
? summary.gapFindingSummaries?.length
|
|
825
|
+
? `已收集 glue-report 输出;报告识别到 ${summary.gapFindingSummaries.length} 个业务缺口。`
|
|
826
|
+
: '已收集 glue-report 输出。'
|
|
827
|
+
: '未发现 glue-report 输出。',
|
|
828
|
+
evidenceChain: [
|
|
829
|
+
...outputs.map((item) => ({ source: 'runtime', path: item.path, text: item.kind })),
|
|
830
|
+
...(summary.failedWorkflowSummaries ?? []).map((text) => ({
|
|
831
|
+
source: 'runtime',
|
|
832
|
+
path: summary.stageGlueReportJsonPath ?? summary.projectDir,
|
|
833
|
+
text,
|
|
834
|
+
})),
|
|
835
|
+
...(summary.gapFindingSummaries ?? []).map((text) => ({
|
|
836
|
+
source: 'runtime',
|
|
837
|
+
path: summary.stageGlueReportJsonPath ?? summary.projectDir,
|
|
838
|
+
text,
|
|
839
|
+
})),
|
|
840
|
+
],
|
|
841
|
+
alternatives: [],
|
|
842
|
+
confidence: outputs.length > 0 ? 'high' : 'low',
|
|
843
|
+
risks: outputs.length > 0 ? (summary.gapFindingSummaries ?? []) : ['glue-report 输出缺失'],
|
|
844
|
+
needsHumanReview: outputs.length === 0,
|
|
845
|
+
},
|
|
846
|
+
gate: outputs.length > 0
|
|
847
|
+
? { status: 'confirmed', confirmedBy: 'policy', reason: '报告路径已收集' }
|
|
848
|
+
: { status: 'needs_review', reason: '未发现报告路径' },
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* 解析 pipeline 输出路径(contract / spec / testcase),未显式指定时给默认值。
|
|
853
|
+
*/
|
|
854
|
+
function resolvePipelineOutputPaths(input) {
|
|
855
|
+
return {
|
|
856
|
+
contractOut: input.args.contractOut ??
|
|
857
|
+
resolveFromRepo(`packages/ep-stage-skill/output/${input.moduleId}.crud.contract.json`),
|
|
858
|
+
specOut: input.args.specOut ??
|
|
859
|
+
path.join(input.projectDir, 'src', 'tests', `generated-${input.moduleId}.gap.spec.ts`),
|
|
860
|
+
testcaseJsonPath: path.join(input.projectDir, 'testcases', `${input.moduleId}.glue-testcase.json`),
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* 捕获式运行 pnpm 子命令,返回 status/stdout/stderr(不 process.exit)。
|
|
865
|
+
*
|
|
866
|
+
* @param commandArgs - pnpm 参数。
|
|
867
|
+
* @param cwd - 工作目录,默认仓库根。
|
|
868
|
+
* @param env - 额外环境变量。
|
|
869
|
+
* @returns status / stdout / stderr。
|
|
870
|
+
*/
|
|
871
|
+
function runPnpmCapture(commandArgs, cwd = repoRoot, env) {
|
|
872
|
+
const result = spawnSync('pnpm', commandArgs, {
|
|
873
|
+
cwd,
|
|
874
|
+
stdio: 'pipe',
|
|
875
|
+
encoding: 'utf8',
|
|
876
|
+
env: { ...process.env, ...env },
|
|
877
|
+
});
|
|
878
|
+
return {
|
|
879
|
+
status: result.status ?? 1,
|
|
880
|
+
stdout: result.stdout ?? '',
|
|
881
|
+
stderr: result.stderr ?? '',
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* 把契约写入 contractOut(供 generate:playwright-tests 消费)。
|
|
886
|
+
*/
|
|
887
|
+
function writeContractArtifact(contract, contractOut) {
|
|
888
|
+
fs.mkdirSync(path.dirname(contractOut), { recursive: true });
|
|
889
|
+
fs.writeFileSync(contractOut, `${JSON.stringify(contract, null, 2)}\n`, 'utf8');
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* 运行 generate:playwright-tests 组装 Playwright spec。
|
|
893
|
+
*/
|
|
894
|
+
function runSpecAssembly(input) {
|
|
895
|
+
fs.mkdirSync(path.dirname(input.specOut), { recursive: true });
|
|
896
|
+
const result = runPnpmCapture([
|
|
897
|
+
'--filter',
|
|
898
|
+
'@epoint-testtech/ep-stage-skill',
|
|
899
|
+
'generate:playwright-tests',
|
|
900
|
+
'--',
|
|
901
|
+
'--contract',
|
|
902
|
+
input.contractOut,
|
|
903
|
+
'--out',
|
|
904
|
+
input.specOut,
|
|
905
|
+
'--menu',
|
|
906
|
+
input.menu,
|
|
907
|
+
'--testcase',
|
|
908
|
+
input.testcasePath,
|
|
909
|
+
]);
|
|
910
|
+
if (result.status !== 0) {
|
|
911
|
+
return {
|
|
912
|
+
contractPath: input.contractOut,
|
|
913
|
+
specPath: input.specOut,
|
|
914
|
+
menu: input.menu,
|
|
915
|
+
testcasePath: input.testcasePath,
|
|
916
|
+
generated: false,
|
|
917
|
+
exitCode: result.status,
|
|
918
|
+
stdout: result.stdout,
|
|
919
|
+
stderr: result.stderr,
|
|
920
|
+
errorMessage: result.stderr || result.stdout || 'generate:playwright-tests failed',
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
contractPath: input.contractOut,
|
|
925
|
+
specPath: input.specOut,
|
|
926
|
+
menu: input.menu,
|
|
927
|
+
testcasePath: input.testcasePath,
|
|
928
|
+
generated: fs.existsSync(input.specOut),
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* 判断 runtime 子进程失败是否更像环境不可达。
|
|
933
|
+
*
|
|
934
|
+
* @param text - stderr + stdout 文本。
|
|
935
|
+
* @returns 是否属于环境或登录态不可用。
|
|
936
|
+
*/
|
|
937
|
+
function isRuntimeEnvironmentUnavailable(text) {
|
|
938
|
+
return /ERR_EMPTY_RESPONSE|ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|net::ERR|登录失败|LOGIN_|login timeout|navigation timeout/i.test(text);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* 解析 runtime runner 带哨兵的 JSON 输出。
|
|
942
|
+
*
|
|
943
|
+
* @param stdout - runtime runner stdout。
|
|
944
|
+
* @returns RuntimeSummary。
|
|
945
|
+
*/
|
|
946
|
+
export function parseRuntimeSummaryOutput(stdout) {
|
|
947
|
+
const markerIndex = stdout.lastIndexOf(runtimeSummaryMarker);
|
|
948
|
+
const payload = markerIndex >= 0
|
|
949
|
+
? stdout.slice(markerIndex + runtimeSummaryMarker.length).trim()
|
|
950
|
+
: stdout.trim();
|
|
951
|
+
return JSON.parse(payload);
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* 将 runtime 子进程失败转成可观测摘要,避免吞掉实现错误。
|
|
955
|
+
*
|
|
956
|
+
* @param input - 子进程失败信息与计划 workflow。
|
|
957
|
+
* @returns RuntimeSummary。
|
|
958
|
+
*/
|
|
959
|
+
export function createRuntimeFailureSummary(input) {
|
|
960
|
+
const combinedOutput = [input.stderr, input.stdout].filter(Boolean).join('\n');
|
|
961
|
+
const environmentUnavailable = isRuntimeEnvironmentUnavailable(combinedOutput);
|
|
962
|
+
return {
|
|
963
|
+
resolvedWorkflows: [],
|
|
964
|
+
skippedWorkflows: input.plannedWorkflows,
|
|
965
|
+
needsReviewWorkflows: input.plannedWorkflows,
|
|
966
|
+
specPath: input.specPath,
|
|
967
|
+
runtimeStatus: environmentUnavailable ? 'environment_unavailable' : 'failed',
|
|
968
|
+
runtimePageStructureStatus: environmentUnavailable ? 'environment_unavailable' : 'failed',
|
|
969
|
+
pageStructureSignals: [],
|
|
970
|
+
exitCode: input.status,
|
|
971
|
+
stdout: input.stdout,
|
|
972
|
+
stderr: input.stderr,
|
|
973
|
+
errorMessage: combinedOutput || `runtime runner exited with ${input.status}`,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* 运行 runtime gap resolve(node --import tsx runtime-runner.ts),返回 RuntimeSummary。
|
|
978
|
+
*
|
|
979
|
+
* 无 planned workflow 时返回空摘要;子命令失败(如内网不可达)标记 environment_unavailable,
|
|
980
|
+
* 全部 workflow 计入 needsReview,不抛错。
|
|
981
|
+
*/
|
|
982
|
+
function runRuntimeResolve(input) {
|
|
983
|
+
const contract = readContract(input.contractOut);
|
|
984
|
+
const plannedWorkflows = (contract.agentInferredWorkflows ?? [])
|
|
985
|
+
.filter((workflow) => workflow.status === 'planned')
|
|
986
|
+
.map((workflow) => workflow.workflowId);
|
|
987
|
+
if (plannedWorkflows.length === 0) {
|
|
988
|
+
return {
|
|
989
|
+
resolvedWorkflows: [],
|
|
990
|
+
skippedWorkflows: [],
|
|
991
|
+
needsReviewWorkflows: [],
|
|
992
|
+
specPath: input.specOut,
|
|
993
|
+
runtimeStatus: 'resolved',
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
const scriptPath = path.join(input.projectDir, 'src', 'gap-executor', 'runtime-runner.ts');
|
|
997
|
+
const result = runPnpmCapture([
|
|
998
|
+
'--filter',
|
|
999
|
+
'@epoint-testtech/ep-stage-skill',
|
|
1000
|
+
'exec',
|
|
1001
|
+
'node',
|
|
1002
|
+
'--import',
|
|
1003
|
+
'tsx',
|
|
1004
|
+
scriptPath,
|
|
1005
|
+
'--contract',
|
|
1006
|
+
input.contractOut,
|
|
1007
|
+
'--spec',
|
|
1008
|
+
input.specOut,
|
|
1009
|
+
'--project-dir',
|
|
1010
|
+
input.projectDir,
|
|
1011
|
+
'--menu',
|
|
1012
|
+
input.menu,
|
|
1013
|
+
'--module-id',
|
|
1014
|
+
input.moduleId,
|
|
1015
|
+
'--workflow-ids',
|
|
1016
|
+
plannedWorkflows.join(','),
|
|
1017
|
+
'--headless',
|
|
1018
|
+
String(input.headless),
|
|
1019
|
+
]);
|
|
1020
|
+
if (result.status !== 0) {
|
|
1021
|
+
return createRuntimeFailureSummary({
|
|
1022
|
+
plannedWorkflows,
|
|
1023
|
+
specPath: input.specOut,
|
|
1024
|
+
status: result.status,
|
|
1025
|
+
stdout: result.stdout,
|
|
1026
|
+
stderr: result.stderr,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
return {
|
|
1030
|
+
...parseRuntimeSummaryOutput(result.stdout),
|
|
1031
|
+
runtimeStatus: 'resolved',
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* 运行 Playwright 执行目标 spec,返回执行摘要。
|
|
1036
|
+
*
|
|
1037
|
+
* 失败时按 stderr/stdout 判定 environment_unavailable 或 failed,不抛错。
|
|
1038
|
+
*/
|
|
1039
|
+
function runPlaywright(input) {
|
|
1040
|
+
const loginUrl = extractLoginSystemUrlFromEnvFile(path.join(input.projectDir, '.env'));
|
|
1041
|
+
const specArg = path.relative(input.projectDir, input.specOut);
|
|
1042
|
+
const result = runPnpmCapture(['--dir', input.projectDir, 'exec', 'playwright', 'test', specArg, '--project', input.playwrightProject], repoRoot, { HEADLESS: String(input.headless) });
|
|
1043
|
+
const status = result.status === 0
|
|
1044
|
+
? 'passed'
|
|
1045
|
+
: /ERR_EMPTY_RESPONSE|ECONNREFUSED|net::ERR/i.test(result.stderr + result.stdout)
|
|
1046
|
+
? 'environment_unavailable'
|
|
1047
|
+
: 'failed';
|
|
1048
|
+
const environmentEvidence = status === 'environment_unavailable' && loginUrl ? probeLoginEnvironment(loginUrl) : undefined;
|
|
1049
|
+
return {
|
|
1050
|
+
specPath: input.specOut,
|
|
1051
|
+
project: input.playwrightProject,
|
|
1052
|
+
status,
|
|
1053
|
+
exitCode: result.status,
|
|
1054
|
+
stdout: result.stdout,
|
|
1055
|
+
stderr: result.stderr,
|
|
1056
|
+
loginUrl,
|
|
1057
|
+
environmentEvidence,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* 可观测胶水链路完整入口。
|
|
1062
|
+
*
|
|
1063
|
+
* 数据流:context → material → evidence → capability → scenario(gate) →
|
|
1064
|
+
* skeleton → coverage-diff → testcase(gate) → menu(gate) →
|
|
1065
|
+
* spec assembly → runtime → playwright → glue-report。
|
|
1066
|
+
*
|
|
1067
|
+
* 任一 gate needs_review 时停止(退出码 2),不继续后续阶段。
|
|
1068
|
+
* runtime/playwright 失败写入 trace,内网不可访问标记 environment_unavailable,不视为链路失败。
|
|
1069
|
+
*/
|
|
1070
|
+
async function main() {
|
|
1071
|
+
const pipelineArgs = parseObservablePipelineArgs(process.argv.slice(2));
|
|
1072
|
+
const contextResult = resolveStageContext({
|
|
1073
|
+
cwd: process.cwd(),
|
|
1074
|
+
codeListPath: pipelineArgs.codeList,
|
|
1075
|
+
projectDir: pipelineArgs.projectDir,
|
|
1076
|
+
stageContextPath: pipelineArgs.stageContext,
|
|
1077
|
+
});
|
|
1078
|
+
const writer = createTraceWriter({
|
|
1079
|
+
projectDir: contextResult.context.projectDir,
|
|
1080
|
+
codeListPath: pipelineArgs.codeList,
|
|
1081
|
+
});
|
|
1082
|
+
const stageSummaries = [];
|
|
1083
|
+
recordStage(writer, stageSummaries, 'context-resolution', contextResult.trace);
|
|
1084
|
+
if (contextResult.trace.gate.status === 'needs_review') {
|
|
1085
|
+
writer.writeSummary(stageSummaries);
|
|
1086
|
+
process.stdout.write(renderReviewSummary({
|
|
1087
|
+
title: '项目上下文需要确认',
|
|
1088
|
+
reasoningSummary: contextResult.trace.reasoningSummary,
|
|
1089
|
+
nextAction: '补齐项目 .env 登录配置,或通过 --stage-context / ~/.ep-stage/projects.index.json5 指向正确项目后重跑',
|
|
1090
|
+
}));
|
|
1091
|
+
process.exit(2);
|
|
1092
|
+
}
|
|
1093
|
+
let codeList = extractCodeListSummary(pipelineArgs.codeList, {
|
|
1094
|
+
knowledgeRoot: contextResult.context.knowledgeRoot,
|
|
1095
|
+
});
|
|
1096
|
+
let effectiveOverrides = {
|
|
1097
|
+
...pipelineArgs.debugOverrides,
|
|
1098
|
+
moduleId: pipelineArgs.moduleId,
|
|
1099
|
+
menu: pipelineArgs.menu,
|
|
1100
|
+
};
|
|
1101
|
+
let materialResult = resolveMaterialInventory({
|
|
1102
|
+
codeListPath: pipelineArgs.codeList,
|
|
1103
|
+
codeList,
|
|
1104
|
+
stageContext: contextResult.context,
|
|
1105
|
+
debugOverrides: effectiveOverrides,
|
|
1106
|
+
});
|
|
1107
|
+
if (canPromptInteractively() &&
|
|
1108
|
+
(materialResult.gate.status === 'needs_review' ||
|
|
1109
|
+
materialResult.resolvedInputs.menu.status !== 'resolved' ||
|
|
1110
|
+
materialResult.resolvedInputs.hintsPath.status !== 'resolved')) {
|
|
1111
|
+
const supplementResult = await promptInteractiveCodeListSupplements({
|
|
1112
|
+
codeListPath: pipelineArgs.codeList,
|
|
1113
|
+
codeList,
|
|
1114
|
+
materialResult,
|
|
1115
|
+
});
|
|
1116
|
+
if (supplementResult.skippedWriteBackReason) {
|
|
1117
|
+
process.stdout.write(`${supplementResult.skippedWriteBackReason}\n`);
|
|
1118
|
+
}
|
|
1119
|
+
if (Object.keys(supplementResult.supplements).length > 0) {
|
|
1120
|
+
effectiveOverrides = {
|
|
1121
|
+
...effectiveOverrides,
|
|
1122
|
+
...(supplementResult.supplements.moduleId ? { moduleId: supplementResult.supplements.moduleId } : {}),
|
|
1123
|
+
...(supplementResult.supplements.menu ? { menu: supplementResult.supplements.menu } : {}),
|
|
1124
|
+
...(supplementResult.supplements.webappDir ? { webapp: path.resolve(supplementResult.supplements.webappDir) } : {}),
|
|
1125
|
+
...(supplementResult.supplements.javaActionsDir
|
|
1126
|
+
? { javaActions: path.resolve(supplementResult.supplements.javaActionsDir) }
|
|
1127
|
+
: {}),
|
|
1128
|
+
...(supplementResult.supplements.hintsPath ? { hints: path.resolve(supplementResult.supplements.hintsPath) } : {}),
|
|
1129
|
+
};
|
|
1130
|
+
codeList = extractCodeListSummary(pipelineArgs.codeList, {
|
|
1131
|
+
knowledgeRoot: supplementResult.supplements.knowledgeRoot
|
|
1132
|
+
? path.resolve(supplementResult.supplements.knowledgeRoot)
|
|
1133
|
+
: contextResult.context.knowledgeRoot,
|
|
1134
|
+
});
|
|
1135
|
+
materialResult = resolveMaterialInventory({
|
|
1136
|
+
codeListPath: pipelineArgs.codeList,
|
|
1137
|
+
codeList,
|
|
1138
|
+
stageContext: contextResult.context,
|
|
1139
|
+
debugOverrides: effectiveOverrides,
|
|
1140
|
+
});
|
|
1141
|
+
const baseReasoning = materialResult.trace.reasoningSummary ?? {
|
|
1142
|
+
conclusion: '已完成交互补齐后的物料重解析。',
|
|
1143
|
+
evidenceChain: [],
|
|
1144
|
+
alternatives: [],
|
|
1145
|
+
confidence: 'medium',
|
|
1146
|
+
risks: [],
|
|
1147
|
+
needsHumanReview: false,
|
|
1148
|
+
};
|
|
1149
|
+
materialResult.trace.reasoningSummary = {
|
|
1150
|
+
...baseReasoning,
|
|
1151
|
+
conclusion: supplementResult.wroteBack
|
|
1152
|
+
? `${baseReasoning.conclusion} 已将交互补齐结果写回 code_list.md。`
|
|
1153
|
+
: supplementResult.skippedWriteBackReason
|
|
1154
|
+
? `${baseReasoning.conclusion} 交互补齐仅用于本次运行,未写回 code_list.md。`
|
|
1155
|
+
: baseReasoning.conclusion,
|
|
1156
|
+
evidenceChain: [
|
|
1157
|
+
...baseReasoning.evidenceChain,
|
|
1158
|
+
...Object.entries(supplementResult.supplements)
|
|
1159
|
+
.filter(([, value]) => Boolean(value))
|
|
1160
|
+
.map(([key, value]) => ({
|
|
1161
|
+
source: 'human',
|
|
1162
|
+
path: pipelineArgs.codeList,
|
|
1163
|
+
text: `${key}=${value}`,
|
|
1164
|
+
})),
|
|
1165
|
+
],
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
recordStage(writer, stageSummaries, 'material-inventory', materialResult.trace);
|
|
1170
|
+
if (materialResult.gate.status === 'needs_review') {
|
|
1171
|
+
writer.writeSummary(stageSummaries);
|
|
1172
|
+
process.stdout.write(renderReviewSummary({
|
|
1173
|
+
title: '物料清单需要确认',
|
|
1174
|
+
reasoningSummary: materialResult.trace.reasoningSummary,
|
|
1175
|
+
nextAction: '补充 --webapp / --java-actions / --module-id,或修正 stage-context.md 后重跑',
|
|
1176
|
+
}));
|
|
1177
|
+
process.exit(2);
|
|
1178
|
+
}
|
|
1179
|
+
const materialInputs = materialResult.resolvedInputs;
|
|
1180
|
+
const moduleId = resolvedValue(materialInputs.moduleId, 'moduleId');
|
|
1181
|
+
const docsDir = materialInputs.docsDir.status === 'resolved'
|
|
1182
|
+
? materialInputs.docsDir.path
|
|
1183
|
+
: path.dirname(pipelineArgs.codeList);
|
|
1184
|
+
const webappDir = resolvedPath(materialInputs.webappDir, 'webappDir');
|
|
1185
|
+
const javaActionsDir = resolvedPath(materialInputs.javaActionsDir, 'javaActionsDir');
|
|
1186
|
+
const hints = materialInputs.hintsPath.status === 'resolved'
|
|
1187
|
+
? JSON.parse(fs.readFileSync(materialInputs.hintsPath.path, 'utf8'))
|
|
1188
|
+
: undefined;
|
|
1189
|
+
const spec = resolveSpec(docsDir, moduleId, codeList);
|
|
1190
|
+
const pages = htmlFiles(webappDir).map(extractHtmlPage);
|
|
1191
|
+
const actions = javaFiles(javaActionsDir).map(extractJavaAction);
|
|
1192
|
+
const pageStructures = pages.map(summarizePageStructure);
|
|
1193
|
+
const evidenceTrace = {
|
|
1194
|
+
stage: 'evidence-extraction',
|
|
1195
|
+
stageLabel: '证据抽取追踪',
|
|
1196
|
+
inputs: [
|
|
1197
|
+
{ kind: 'docs_dir', path: docsDir },
|
|
1198
|
+
{ kind: 'webapp_dir', path: webappDir },
|
|
1199
|
+
{ kind: 'java_actions_dir', path: javaActionsDir },
|
|
1200
|
+
],
|
|
1201
|
+
outputs: [
|
|
1202
|
+
{ kind: 'html_pages', path: String(pages.length) },
|
|
1203
|
+
{ kind: 'java_actions', path: String(actions.length) },
|
|
1204
|
+
],
|
|
1205
|
+
reasoningSummary: {
|
|
1206
|
+
conclusion: `抽取 HTML 页面 ${pages.length} 个,Java Action ${actions.length} 个。`,
|
|
1207
|
+
evidenceChain: pages.map((page) => ({
|
|
1208
|
+
source: 'html',
|
|
1209
|
+
path: page.path,
|
|
1210
|
+
text: page.title || page.pageId,
|
|
1211
|
+
})),
|
|
1212
|
+
alternatives: [],
|
|
1213
|
+
confidence: 'high',
|
|
1214
|
+
risks: [],
|
|
1215
|
+
needsHumanReview: false,
|
|
1216
|
+
},
|
|
1217
|
+
gate: { status: 'confirmed', confirmedBy: 'policy', reason: '确定性 extractor 已完成' },
|
|
1218
|
+
};
|
|
1219
|
+
recordStage(writer, stageSummaries, 'evidence-extraction', evidenceTrace);
|
|
1220
|
+
const contract = buildCrudBusinessModuleContract({
|
|
1221
|
+
moduleId,
|
|
1222
|
+
spec,
|
|
1223
|
+
codeList,
|
|
1224
|
+
pages,
|
|
1225
|
+
actions,
|
|
1226
|
+
hints,
|
|
1227
|
+
});
|
|
1228
|
+
let menuResolution = resolveMenuRouteForPipeline({
|
|
1229
|
+
menu: materialInputs.menu,
|
|
1230
|
+
contract,
|
|
1231
|
+
});
|
|
1232
|
+
const skeletonCoverage = createSkeletonCoverage(contract, hints);
|
|
1233
|
+
const actionCandidates = collectActionCandidates(contract);
|
|
1234
|
+
const baselineWorkflows = planAgentInferredWorkflows({ contract, actions, hints });
|
|
1235
|
+
recordStage(writer, stageSummaries, 'capability-candidates', capabilityCandidatesToTrace({ moduleId, actionCandidates, pageStructures }));
|
|
1236
|
+
const scenarioTrace = inferPageScenarios({
|
|
1237
|
+
moduleId,
|
|
1238
|
+
pages: pageStructures,
|
|
1239
|
+
skeletonCapabilities: skeletonCoverage.coveredWorkflows,
|
|
1240
|
+
});
|
|
1241
|
+
const scenarioCandidateIds = scenarioTrace.candidates.map((item) => item.scenario);
|
|
1242
|
+
if (pipelineArgs.scenario && !scenarioCandidateIds.includes(pipelineArgs.scenario)) {
|
|
1243
|
+
writer.writeSummary(stageSummaries);
|
|
1244
|
+
process.stdout.write(renderReviewSummary({
|
|
1245
|
+
title: '场景确认值无效',
|
|
1246
|
+
reasoningSummary: {
|
|
1247
|
+
...scenarioTrace.reasoningSummary,
|
|
1248
|
+
risks: [
|
|
1249
|
+
...scenarioTrace.reasoningSummary.risks,
|
|
1250
|
+
`--scenario=${pipelineArgs.scenario} 不在候选场景中;候选=${scenarioCandidateIds.join(', ')}`,
|
|
1251
|
+
],
|
|
1252
|
+
needsHumanReview: true,
|
|
1253
|
+
},
|
|
1254
|
+
nextAction: '从候选场景中选择一个 --scenario 后重跑,或补充证据后重新推理',
|
|
1255
|
+
}));
|
|
1256
|
+
process.exit(2);
|
|
1257
|
+
}
|
|
1258
|
+
const confirmedScenarios = pipelineArgs.scenario ? [pipelineArgs.scenario] : [];
|
|
1259
|
+
const scenarioPayload = {
|
|
1260
|
+
stage: 'scenario-inference',
|
|
1261
|
+
stageLabel: '场景推理追踪',
|
|
1262
|
+
inputs: [{ kind: 'capability_candidates', path: 'capability-candidates.json' }],
|
|
1263
|
+
outputs: [{ kind: 'scenario_candidates', path: 'scenario-inference.json' }],
|
|
1264
|
+
reasoningSummary: pipelineArgs.scenario
|
|
1265
|
+
? {
|
|
1266
|
+
...scenarioTrace.reasoningSummary,
|
|
1267
|
+
evidenceChain: [
|
|
1268
|
+
...scenarioTrace.reasoningSummary.evidenceChain,
|
|
1269
|
+
{ source: 'human', path: 'cli --scenario', text: `confirmed=${pipelineArgs.scenario}` },
|
|
1270
|
+
],
|
|
1271
|
+
needsHumanReview: false,
|
|
1272
|
+
}
|
|
1273
|
+
: scenarioTrace.reasoningSummary,
|
|
1274
|
+
candidateCount: scenarioTrace.candidates.length,
|
|
1275
|
+
unresolvedCount: scenarioTrace.candidates.flatMap((item) => item.unresolved).length,
|
|
1276
|
+
gate: pipelineArgs.scenario
|
|
1277
|
+
? { status: 'confirmed', confirmedBy: 'human', reason: `人工确认场景:${pipelineArgs.scenario}` }
|
|
1278
|
+
: scenarioTrace.gate,
|
|
1279
|
+
};
|
|
1280
|
+
recordStage(writer, stageSummaries, 'scenario-inference', scenarioPayload);
|
|
1281
|
+
// inferPageScenarios 的 gate 恒为 needs_review;默认入口在此停下(退出码 2),
|
|
1282
|
+
// 人工审阅后用 --scenario <场景候选> 复跑表示已确认,方可继续到 skeleton/coverage-diff/testcase。
|
|
1283
|
+
if (scenarioPayload.gate.status === 'needs_review') {
|
|
1284
|
+
writer.writeSummary(stageSummaries);
|
|
1285
|
+
process.stdout.write(renderReviewSummary({
|
|
1286
|
+
title: '场景推理需要确认',
|
|
1287
|
+
reasoningSummary: scenarioTrace.reasoningSummary,
|
|
1288
|
+
nextAction: '人工确认后用 --scenario <场景候选> 复跑以继续(confirm / correct / add-evidence / skip)',
|
|
1289
|
+
}));
|
|
1290
|
+
process.exit(2);
|
|
1291
|
+
}
|
|
1292
|
+
recordStage(writer, stageSummaries, 'skeleton-capability-model', skeletonCoverageToTrace(skeletonCoverage));
|
|
1293
|
+
const coverageDiff = createCoverageDiff({
|
|
1294
|
+
moduleId,
|
|
1295
|
+
actionCandidates,
|
|
1296
|
+
skeletonCoverage,
|
|
1297
|
+
baselineWorkflows,
|
|
1298
|
+
pageStructures,
|
|
1299
|
+
});
|
|
1300
|
+
recordStage(writer, stageSummaries, 'coverage-diff', coverageDiffToTrace(coverageDiff));
|
|
1301
|
+
// 泛化指标快照(非 gating):供 Phase 2 跨系统用 compareObservableRuns 对比,不参与阶段判定。
|
|
1302
|
+
const generalizationMetrics = summarizeGeneralizationRun({
|
|
1303
|
+
moduleId,
|
|
1304
|
+
resolvedInputs: materialResult.resolvedInputs,
|
|
1305
|
+
scenario: scenarioTrace,
|
|
1306
|
+
coverageDiff,
|
|
1307
|
+
});
|
|
1308
|
+
fs.writeFileSync(path.join(writer.traceDir, 'generalization-evaluation.json'), `${JSON.stringify(generalizationMetrics, null, 2)}\n`, 'utf8');
|
|
1309
|
+
const testcases = generateGlueTestcases({
|
|
1310
|
+
contract,
|
|
1311
|
+
menu: menuResolution.status === 'resolved' ? menuResolution.value : UNRESOLVED_MENU_PLACEHOLDER,
|
|
1312
|
+
menuReviewHint: menuResolution.status === 'candidate'
|
|
1313
|
+
? `menu.candidate: ${menuResolution.candidates.join(' | ')}`
|
|
1314
|
+
: undefined,
|
|
1315
|
+
scenarios: scenarioTrace.candidates.map((item) => item.scenario),
|
|
1316
|
+
confirmedScenarios,
|
|
1317
|
+
coverageDiff,
|
|
1318
|
+
});
|
|
1319
|
+
let testcasesForGate = pipelineArgs.confirmTestcases
|
|
1320
|
+
? confirmGlueTestcaseDocument(testcases.json)
|
|
1321
|
+
: testcases.json;
|
|
1322
|
+
let testcaseMarkdownForGate = pipelineArgs.confirmTestcases
|
|
1323
|
+
? applyTestcaseReviewStatusToMarkdown(testcases.markdown, 'confirmed')
|
|
1324
|
+
: testcases.markdown;
|
|
1325
|
+
if (menuResolution.status === 'candidate') {
|
|
1326
|
+
testcasesForGate.reasoningSummary = {
|
|
1327
|
+
...testcasesForGate.reasoningSummary,
|
|
1328
|
+
evidenceChain: [
|
|
1329
|
+
...testcasesForGate.reasoningSummary.evidenceChain,
|
|
1330
|
+
{
|
|
1331
|
+
source: 'human',
|
|
1332
|
+
path: 'menu-candidates',
|
|
1333
|
+
text: `menuCandidates=${menuResolution.candidates.join(' | ')}`,
|
|
1334
|
+
},
|
|
1335
|
+
],
|
|
1336
|
+
risks: [
|
|
1337
|
+
...testcasesForGate.reasoningSummary.risks,
|
|
1338
|
+
`菜单候选待确认:${menuResolution.candidates.join(' | ')}`,
|
|
1339
|
+
],
|
|
1340
|
+
needsHumanReview: true,
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
const testcaseDir = path.join(contextResult.context.projectDir, 'testcases');
|
|
1344
|
+
fs.mkdirSync(testcaseDir, { recursive: true });
|
|
1345
|
+
fs.writeFileSync(path.join(testcaseDir, `${moduleId}.glue-testcase.json`), `${JSON.stringify(testcasesForGate, null, 2)}\n`, 'utf8');
|
|
1346
|
+
fs.writeFileSync(path.join(testcaseDir, `${moduleId}.glue-testcase.md`), testcaseMarkdownForGate, 'utf8');
|
|
1347
|
+
recordStage(writer, stageSummaries, 'testcase-generation', testcaseGenerationToTrace(testcasesForGate));
|
|
1348
|
+
if (testcasesForGate.cases.some((item) => item.reviewStatus !== 'confirmed')) {
|
|
1349
|
+
writer.writeSummary(stageSummaries);
|
|
1350
|
+
process.stdout.write(renderReviewSummary({
|
|
1351
|
+
title: '用例生成需要确认',
|
|
1352
|
+
reasoningSummary: testcasesForGate.reasoningSummary,
|
|
1353
|
+
nextAction: '确认 testcase 中的 candidate / unresolved 后用 --confirm-testcases true 重跑 spec assembly',
|
|
1354
|
+
}));
|
|
1355
|
+
process.exit(2);
|
|
1356
|
+
}
|
|
1357
|
+
if (menuResolution.status !== 'resolved') {
|
|
1358
|
+
if (canPromptInteractively()) {
|
|
1359
|
+
const menuSupplement = await promptMenuAtGate(pipelineArgs.codeList, menuResolution);
|
|
1360
|
+
if (menuSupplement.skippedWriteBackReason) {
|
|
1361
|
+
process.stdout.write(`${menuSupplement.skippedWriteBackReason}\n`);
|
|
1362
|
+
}
|
|
1363
|
+
if (menuSupplement.supplements.menu) {
|
|
1364
|
+
menuResolution = {
|
|
1365
|
+
status: 'resolved',
|
|
1366
|
+
value: menuSupplement.supplements.menu,
|
|
1367
|
+
evidence: menuSupplement.wroteBack
|
|
1368
|
+
? 'menu 来自交互补齐并已写回 code_list.md'
|
|
1369
|
+
: 'menu 来自交互补齐,仅本次运行生效',
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (menuResolution.status !== 'resolved') {
|
|
1375
|
+
writer.writeSummary(stageSummaries);
|
|
1376
|
+
process.stdout.write(renderReviewSummary({
|
|
1377
|
+
title: '菜单路径需要确认',
|
|
1378
|
+
reasoningSummary: testcasesForGate.reasoningSummary,
|
|
1379
|
+
nextAction: '补充 code_list.md 的 menu,或用 --menu / stage-context.md 显式确认后重跑 spec assembly',
|
|
1380
|
+
}));
|
|
1381
|
+
process.exit(2);
|
|
1382
|
+
}
|
|
1383
|
+
const outputPaths = resolvePipelineOutputPaths({
|
|
1384
|
+
args: pipelineArgs,
|
|
1385
|
+
projectDir: contextResult.context.projectDir,
|
|
1386
|
+
moduleId,
|
|
1387
|
+
});
|
|
1388
|
+
writeContractArtifact(contract, outputPaths.contractOut);
|
|
1389
|
+
// 契约存在 unresolvedSlots 时停在 gate(与前半段 gate 风格一致),未解析槽位不得进入可执行 spec 组装。
|
|
1390
|
+
if ((contract.unresolvedSlots?.length ?? 0) > 0) {
|
|
1391
|
+
const unresolvedIds = contract.unresolvedSlots.map((slot) => slot.slotId);
|
|
1392
|
+
const unresolvedGate = {
|
|
1393
|
+
stage: 'glue-spec-assembly',
|
|
1394
|
+
stageLabel: '胶水测试组装追踪',
|
|
1395
|
+
inputs: [{ kind: 'crud_contract', path: outputPaths.contractOut }],
|
|
1396
|
+
outputs: [],
|
|
1397
|
+
reasoningSummary: {
|
|
1398
|
+
conclusion: `契约仍有 ${unresolvedIds.length} 个 unresolvedSlots,未达可执行 spec 组装条件。`,
|
|
1399
|
+
evidenceChain: unresolvedIds.map((slotId) => ({
|
|
1400
|
+
source: 'human',
|
|
1401
|
+
path: outputPaths.contractOut,
|
|
1402
|
+
text: `unresolved=${slotId}`,
|
|
1403
|
+
})),
|
|
1404
|
+
alternatives: [],
|
|
1405
|
+
confidence: 'medium',
|
|
1406
|
+
risks: unresolvedIds.map((slotId) => `${slotId} 需人工补充`),
|
|
1407
|
+
needsHumanReview: true,
|
|
1408
|
+
},
|
|
1409
|
+
candidateCount: 0,
|
|
1410
|
+
unresolvedCount: unresolvedIds.length,
|
|
1411
|
+
gate: {
|
|
1412
|
+
status: 'needs_review',
|
|
1413
|
+
reason: `contract 存在 unresolvedSlots:${unresolvedIds.join(', ')}`,
|
|
1414
|
+
},
|
|
1415
|
+
};
|
|
1416
|
+
recordStage(writer, stageSummaries, 'glue-spec-assembly', unresolvedGate);
|
|
1417
|
+
writer.writeSummary(stageSummaries);
|
|
1418
|
+
process.stdout.write(renderReviewSummary({
|
|
1419
|
+
title: '契约存在未解析槽位(unresolvedSlots)',
|
|
1420
|
+
reasoningSummary: unresolvedGate.reasoningSummary,
|
|
1421
|
+
nextAction: '补充 ModuleHints/spec 解析这些槽位后重跑;未解析槽位不得进入可执行 spec',
|
|
1422
|
+
}));
|
|
1423
|
+
process.exit(2);
|
|
1424
|
+
}
|
|
1425
|
+
const specSummary = runSpecAssembly({
|
|
1426
|
+
contractOut: outputPaths.contractOut,
|
|
1427
|
+
specOut: outputPaths.specOut,
|
|
1428
|
+
menu: resolvedValue(menuResolution, 'menu'),
|
|
1429
|
+
testcasePath: outputPaths.testcaseJsonPath,
|
|
1430
|
+
});
|
|
1431
|
+
const specTrace = glueSpecAssemblyToTrace(specSummary);
|
|
1432
|
+
recordStage(writer, stageSummaries, 'glue-spec-assembly', specTrace);
|
|
1433
|
+
if (!specSummary.generated) {
|
|
1434
|
+
writer.writeSummary(stageSummaries);
|
|
1435
|
+
process.stdout.write(renderReviewSummary({
|
|
1436
|
+
title: '胶水测试组装失败',
|
|
1437
|
+
reasoningSummary: specTrace.reasoningSummary,
|
|
1438
|
+
nextAction: '查看 glue-spec-assembly.json 中的 stderr/stdout,修复 generate:playwright-tests 输入后重跑',
|
|
1439
|
+
}));
|
|
1440
|
+
process.exit(2);
|
|
1441
|
+
}
|
|
1442
|
+
const runtimeSummary = runRuntimeResolve({
|
|
1443
|
+
moduleId,
|
|
1444
|
+
menu: resolvedValue(menuResolution, 'menu'),
|
|
1445
|
+
contractOut: outputPaths.contractOut,
|
|
1446
|
+
specOut: outputPaths.specOut,
|
|
1447
|
+
projectDir: contextResult.context.projectDir,
|
|
1448
|
+
headless: pipelineArgs.headless,
|
|
1449
|
+
});
|
|
1450
|
+
recordStage(writer, stageSummaries, 'runtime-gap-inference', runtimeSummaryToTrace(runtimeSummary));
|
|
1451
|
+
const playwrightSummary = runPlaywright({
|
|
1452
|
+
projectDir: contextResult.context.projectDir,
|
|
1453
|
+
specOut: outputPaths.specOut,
|
|
1454
|
+
playwrightProject: pipelineArgs.playwrightProject,
|
|
1455
|
+
headless: pipelineArgs.headless,
|
|
1456
|
+
});
|
|
1457
|
+
recordStage(writer, stageSummaries, 'playwright-execution', playwrightExecutionToTrace(playwrightSummary));
|
|
1458
|
+
const reportSummary = {
|
|
1459
|
+
projectDir: contextResult.context.projectDir,
|
|
1460
|
+
...readRunInfo(contextResult.context.projectDir),
|
|
1461
|
+
};
|
|
1462
|
+
Object.assign(reportSummary, readGlueReportDetails(reportSummary.stageGlueReportJsonPath));
|
|
1463
|
+
recordStage(writer, stageSummaries, 'glue-report', glueReportToTrace(reportSummary));
|
|
1464
|
+
writer.writeSummary(stageSummaries);
|
|
1465
|
+
}
|
|
1466
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
1467
|
+
void main();
|
|
1468
|
+
}
|