@epoint-testtech/ep-stage-skill 0.0.3-alpha.2 → 0.0.4-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/SKILL.md +2 -2
  2. package/codex-skill/ep-stage/glue-create-project/SKILL.md +186 -0
  3. package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +199 -0
  4. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +112 -0
  5. package/codex-skill/ep-stage/glue-run-test/SKILL.md +249 -0
  6. package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +145 -0
  7. package/codex-skill/ep-stage/{glue-test → glue-run-test}/scripts/generate-crud-spec.mjs +3 -3
  8. package/codex-skill/ep-stage/recording-to-glue/SKILL.md +1 -0
  9. package/codex-skill/ep-stage/scripts/validate-skill.mjs +29 -7
  10. package/dist/src/cli/dev/extract-contract.d.ts +14 -0
  11. package/dist/src/cli/dev/extract-contract.d.ts.map +1 -0
  12. package/dist/src/cli/dev/extract-contract.js +114 -0
  13. package/dist/src/cli/generate-crud-contract.js +7 -77
  14. package/dist/src/cli/generate-playwright-tests.d.ts +0 -28
  15. package/dist/src/cli/generate-playwright-tests.d.ts.map +1 -1
  16. package/dist/src/cli/generate-playwright-tests.js +4 -81
  17. package/dist/src/cli/generate-testcase.d.ts +83 -0
  18. package/dist/src/cli/generate-testcase.d.ts.map +1 -0
  19. package/dist/src/cli/generate-testcase.js +197 -0
  20. package/dist/src/cli/index.d.ts +18 -0
  21. package/dist/src/cli/index.d.ts.map +1 -0
  22. package/dist/src/cli/index.js +55 -0
  23. package/dist/src/cli/probe.d.ts +44 -0
  24. package/dist/src/cli/probe.d.ts.map +1 -0
  25. package/dist/src/cli/probe.js +221 -0
  26. package/dist/src/cli/run-gap-pipeline.js +4 -0
  27. package/dist/src/cli/run.d.ts +63 -0
  28. package/dist/src/cli/run.d.ts.map +1 -0
  29. package/dist/src/cli/run.js +116 -0
  30. package/dist/src/cli/spec.d.ts +45 -0
  31. package/dist/src/cli/spec.d.ts.map +1 -0
  32. package/dist/src/cli/spec.js +74 -0
  33. package/dist/src/context/stage-context.d.ts +72 -8
  34. package/dist/src/context/stage-context.d.ts.map +1 -1
  35. package/dist/src/context/stage-context.js +61 -15
  36. package/dist/src/index.d.ts +2 -2
  37. package/dist/src/index.d.ts.map +1 -1
  38. package/dist/src/index.js +1 -1
  39. package/dist/src/testcase/testcase-generator.d.ts.map +1 -1
  40. package/dist/src/testcase/testcase-generator.js +4 -0
  41. package/dist/src/testcase/testcase-v2.d.ts +50 -0
  42. package/dist/src/testcase/testcase-v2.d.ts.map +1 -0
  43. package/dist/src/testcase/testcase-v2.js +1 -0
  44. package/dist/src/util/credentials.d.ts +12 -0
  45. package/dist/src/util/credentials.d.ts.map +1 -0
  46. package/dist/src/util/credentials.js +19 -0
  47. package/dist/src/util/i18n-testcase.d.ts +8 -0
  48. package/dist/src/util/i18n-testcase.d.ts.map +1 -0
  49. package/dist/src/util/i18n-testcase.js +55 -0
  50. package/dist/src/util/softlink.d.ts +33 -0
  51. package/dist/src/util/softlink.d.ts.map +1 -0
  52. package/dist/src/util/softlink.js +43 -0
  53. package/dist/src/validation/credentials.d.ts +19 -0
  54. package/dist/src/validation/credentials.d.ts.map +1 -0
  55. package/dist/src/validation/credentials.js +38 -0
  56. package/dist/src/validation/index.d.ts +5 -0
  57. package/dist/src/validation/index.d.ts.map +1 -0
  58. package/dist/src/validation/index.js +3 -0
  59. package/dist/src/validation/projects-index.d.ts +13 -0
  60. package/dist/src/validation/projects-index.d.ts.map +1 -0
  61. package/dist/src/validation/projects-index.js +37 -0
  62. package/dist/src/validation/testcase.d.ts +13 -0
  63. package/dist/src/validation/testcase.d.ts.map +1 -0
  64. package/dist/src/validation/testcase.js +53 -0
  65. package/dist/test/cli/extract-contract.test.d.ts +2 -0
  66. package/dist/test/cli/extract-contract.test.d.ts.map +1 -0
  67. package/dist/test/cli/extract-contract.test.js +32 -0
  68. package/dist/test/cli/generate-testcase.test.d.ts +2 -0
  69. package/dist/test/cli/generate-testcase.test.d.ts.map +1 -0
  70. package/dist/test/cli/generate-testcase.test.js +130 -0
  71. package/dist/test/cli/index.test.d.ts +2 -0
  72. package/dist/test/cli/index.test.d.ts.map +1 -0
  73. package/dist/test/cli/index.test.js +93 -0
  74. package/dist/test/cli/run.test.d.ts +2 -0
  75. package/dist/test/cli/run.test.d.ts.map +1 -0
  76. package/dist/test/cli/run.test.js +149 -0
  77. package/dist/test/cli/spec.test.d.ts +2 -0
  78. package/dist/test/cli/spec.test.d.ts.map +1 -0
  79. package/dist/test/cli/spec.test.js +196 -0
  80. package/dist/test/stage-context.test.js +145 -13
  81. package/dist/test/util/credentials.test.d.ts +2 -0
  82. package/dist/test/util/credentials.test.d.ts.map +1 -0
  83. package/dist/test/util/credentials.test.js +64 -0
  84. package/dist/test/util/i18n-testcase.test.d.ts +2 -0
  85. package/dist/test/util/i18n-testcase.test.d.ts.map +1 -0
  86. package/dist/test/util/i18n-testcase.test.js +119 -0
  87. package/dist/test/util/softlink.test.d.ts +2 -0
  88. package/dist/test/util/softlink.test.d.ts.map +1 -0
  89. package/dist/test/util/softlink.test.js +82 -0
  90. package/dist/test/validation/credentials.test.d.ts +2 -0
  91. package/dist/test/validation/credentials.test.d.ts.map +1 -0
  92. package/dist/test/validation/credentials.test.js +72 -0
  93. package/dist/test/validation/projects-index.test.d.ts +2 -0
  94. package/dist/test/validation/projects-index.test.d.ts.map +1 -0
  95. package/dist/test/validation/projects-index.test.js +48 -0
  96. package/dist/test/validation/testcase.test.d.ts +2 -0
  97. package/dist/test/validation/testcase.test.d.ts.map +1 -0
  98. package/dist/test/validation/testcase.test.js +129 -0
  99. package/docs/README.md +6 -6
  100. package/docs/mvp-usage-guide.md +3 -3
  101. package/package.json +9 -4
  102. package/codex-skill/ep-stage/create-project/SKILL.md +0 -59
  103. package/codex-skill/ep-stage/glue-test/SKILL.md +0 -258
  104. package/codex-skill/ep-stage/glue-test/references/crud-pipeline.md +0 -139
  105. package/codex-skill/ep-stage/glue-testcase/SKILL.md +0 -31
  106. package/codex-skill/ep-stage/glue-testcase/references/testcase-schema.md +0 -67
  107. /package/codex-skill/ep-stage/{glue-testcase → glue-generate-testcase}/examples/observable-testcase.json +0 -0
  108. /package/codex-skill/ep-stage/{glue-test → glue-run-test}/references/gap-review-protocol.md +0 -0
  109. /package/codex-skill/ep-stage/{glue-test → glue-run-test}/references/harness-principles.md +0 -0
@@ -0,0 +1,249 @@
1
+ ---
2
+ name: glue-run-test
3
+ description: 使用时机:当用户对某需求要「跑测试 / 验证 / 端到端执行 / 跑胶水测试 / 看报告」或显式触发 `/glue-run-test <需求目录>` 时使用。本 skill 包装 `@epoint-testtech/ep-stage-skill` CLI 的 `spec` + `run` 双子命令,做 spec 组装与 Playwright 执行 + AI 钻探追加。
4
+ version: 2026-06-25+79c305a
5
+ argument-hint: "<需求目录绝对/相对路径>"
6
+ compatibility: 需要由 stage-create 生成的 glue 模式 Playwright 项目 + 已审过的 `<需求名>/testcase.json5` (v2) + 填好的 `<需求名>/credentials.json5`。
7
+ metadata:
8
+ source-package: "@epoint-testtech/ep-stage-skill"
9
+ contract: "crud-business-module/v1"
10
+ ---
11
+
12
+ # ep-stage: glue-run-test
13
+
14
+ 本 skill 引导 Agent **包装** `@epoint-testtech/ep-stage-skill` CLI 的 `spec` + `run` 双子命令,不重新实现 spec 组装、Playwright 执行或 AI 推理。CLI 内部完成:v1 初始版 spec 生成(spec 子命令)+ 凭据数组加载 + 多角色登录切换 + Playwright 执行 + uncoveredCandidates 中 `planned` 项 AI 钻探追加 + v2 追加版 spec 写回 + 报告生成 + 项目索引回写(run 子命令)。skill 负责:路径解析、反查项目脚手架、缺前置数据引导、任务清单打印、报告路径回显。用户视角全中文,不暴露 CLI 名称。
15
+
16
+ > **钻探已前置**:上游钻探 + contract 提取 + coverage 分析 + uncoveredCandidates 推断由 `/glue-generate-testcase` 阶段完成(REQ-CHAIN-01)。本 skill 只消费 testcase v2 + contract.json,不再自行钻探(REQ-CHAIN-02)。
17
+ >
18
+ > **spec 双生命周期**(REQ-SPEC-01):spec 子命令只产 **v1 初始版**(只含 coveredActions 胶水部分,顶部 `// === 胶水模式生成 ===`),不执行;run 子命令跑 v1 + AI 钻探 uncoveredCandidates → 追加 **v2 追加版**(底部 `// === AI 钻探追加 ===`)。重跑 run 检测已有追加段则**覆盖**,不保留旧追加。
19
+
20
+ ## 1. 自然语言映射
21
+
22
+ ```text
23
+ /glue-run-test /abs/path/.../014-测试需求10-20260622 # 绝对路径
24
+ /glue-run-test ../knowledge-project/.../014-测试需求10-20260622 # 相对路径
25
+ /glue-run-test # 无参数 → 问需求目录
26
+ ```
27
+
28
+ skill 把入参 `path.resolve` 绝对化,`path.basename` 取「需求名」。projectDir 不接受参数,全部从 `~/.ep-stage/projects.index.json5` 反查最新 `mode: 'glue'` 条目(多条 AskUserQuestion 让用户挑);索引缺失走引导场景 A。
29
+
30
+ ## 2. Agent 执行剧本(8 步任务清单协议)
31
+
32
+ **任务清单协议(REQ-UX-01)**:load 后**立即**打印初始任务清单(Step 0);每完成一步输出单行 `✅ 第 N 步 — <中文名> 已完成,进入第 N+1 步 — <下一中文名>...`;Step 8 输出全勾选终态清单 + 摘要(spec 路径 + 报告路径 + lastRunStatus)。
33
+
34
+ ### Step 0 — 打印任务清单
35
+
36
+ ```markdown
37
+ 📋 **执行胶水测试任务清单**
38
+
39
+ - [ ] 第 1 步 — 解析需求路径并反查项目脚手架
40
+ - [ ] 第 2 步 — 校验 testcase.json5 输入
41
+ - [ ] 第 3 步 — 校验 credentials.json5 凭据
42
+ - [ ] 第 4 步 — 调用 spec 组装 CLI(只生成 v1 不执行)
43
+ - [ ] 第 5 步 — 调用 run 执行与 AI 推理 CLI(产 v2)
44
+ - [ ] 第 6 步 — 收集报告产物
45
+ - [ ] 第 7 步 — 写入运行状态到项目索引
46
+ - [ ] 第 8 步 — 输出执行摘要
47
+ ```
48
+
49
+ ### Step 1 — 解析需求路径并反查项目脚手架(缺 → 引导 A)
50
+
51
+ - 从用户消息 / args 抽取需求目录路径,绝对化 + basename。入参缺失 → free text 询问"请给我需求目录的绝对或相对路径"。
52
+ - 读 `~/.ep-stage/projects.index.json5`:文件不存在 / 无 glue 条目 → **引导场景 A**:中文提示"未发现胶水测试脚手架。请先跑 `/glue-create-project <项目名>` 创建脚手架后再回来",退出不调 CLI。多条匹配 → AskUserQuestion 挑 projectDir。
53
+ - 中文复述识别结果:"已识别:需求目录 = `<abs>`,需求名 = `<basename>`,项目目录 = `<projectDir>`"。
54
+
55
+ ### Step 2 — 校验 testcase.json5 输入(缺 → 引导 B)
56
+
57
+ 校验 `<projectDir>/src/tests/<需求名>/testcase.json5` 是否存在 + `schemaVersion: "v2"`。
58
+
59
+ 缺失 / schema 非 v2 → **引导场景 B**,AskUserQuestion 三选一:
60
+
61
+ 1. **跑 `/glue-generate-testcase <需求目录>`**:先生成用例,回头再跑 run。
62
+ 2. **跳过用例直接组装**(不推荐):警告"无 testcase 时 spec 只能生成空胶水骨架,钻探追加无法启动",确认后继续。
63
+ 3. **取消**:直接退出,不调 CLI。
64
+
65
+ ### Step 3 — 校验 credentials.json5 凭据(缺 → 引导 C)
66
+
67
+ 校验 `<projectDir>/src/tests/<需求名>/credentials.json5` 存在 + 数组至少 1 条 + 每条 `url / username / password` 非空(参 §4.3)。
68
+
69
+ 缺失 / 字段空 → **引导场景 C**,先读 `~/.ep-stage/projects.index.json5` 的 `systems[].credentialsCache`,按 testcase.json5 `requiredSystems[].url` 比对算 `cachedSystemsHit`。AskUserQuestion 三选一:
70
+
71
+ 1. **从全局缓存复用**(仅 `cachedSystemsHit` 非空时显示):列出命中的 url + 已缓存角色,自动拼出 credentials.json5(缺失角色用占位符保留)。
72
+ 2. **现在补**:skill 顺序问每条角色的 URL / username / password / systemName(free text),直接写 `<需求名>/credentials.json5`,**回写**全局 `systems[].credentialsCache`(按 url 归并,跨需求复用)。
73
+ 3. **取消**:直接退出,不调 CLI。
74
+
75
+ ### Step 4 — 调用 spec 组装 CLI(只生成 v1 不执行)
76
+
77
+ 固定命令形式(不 which、不扫 monorepo、不 cd、**不喂 stdin**):
78
+
79
+ ```bash
80
+ npx -y @epoint-testtech/ep-stage-skill@latest spec \
81
+ --testcase <projectDir>/src/tests/<需求名>/testcase.json5 \
82
+ --out <projectDir>/src/tests/<需求名>/crud.spec.ts
83
+ ```
84
+
85
+ CLI 内部:读 testcase v2 + `.ep-stage/contract.json` → 基于 `coveredActions` + `cases[]` 生成 **v1 初始版** `crud.spec.ts`(顶部注释 `// === 胶水模式生成 ===`,引用 `../../skeletons` 相对路径,只覆盖 coveredActions 胶水部分,不执行 Playwright)。CLI 非 0 退出 → 复述 stderr,不进 Step 5。
86
+
87
+ ### Step 5 — 调用 run 执行与 AI 推理 CLI(产 v2)
88
+
89
+ 固定命令形式:
90
+
91
+ ```bash
92
+ npx -y @epoint-testtech/ep-stage-skill@latest run \
93
+ --spec <projectDir>/src/tests/<需求名>/crud.spec.ts \
94
+ --testcase <projectDir>/src/tests/<需求名>/testcase.json5 \
95
+ --headless false
96
+ ```
97
+
98
+ CLI 内部全链路(详见 references/crud-pipeline.md「执行 + AI 推理追加」):
99
+
100
+ 1. 读 `<需求名>/credentials.json5`(数组形态多角色)+ testcase.json5(v2)。
101
+ 2. **多角色登录**:通过 `@epoint-testtech/stage-core.loadCredentials(path, role?)` 按 `cases[].requiredRole` / `uncoveredCandidates[].requiredRole` 检索对应角色,启动 `LoginPage`。
102
+ 3. **跑 v1 胶水部分**:执行 spec 顶部 `// === 胶水模式生成 ===` 区段所有 `test(...)` block。
103
+ 4. **AI 钻探追加**:对 `uncoveredCandidates[].status === 'planned'` 项,按 `requiredRole` 切登录 → 页面浏览补 spec(`businessIntent` / `assertionExpectation` / `label` 驱动 test 标题与断言)→ **追加到 spec 底部**产 v2(注释 `// === AI 钻探追加 ===`)。
104
+ 5. **重跑覆盖**:检测到分段标记则**覆盖**追加段(v2 → v2'),不保留旧追加。
105
+ 6. 写报告到 `e2e/glue-code-mvp/glue-report/<run-timestamp>/`。
106
+
107
+ CLI 非 0 退出 → 复述 stderr,按 §4 报告。
108
+
109
+ > **凭据通路**:skill 自身**不调** `loadCredentials`;该工具由 CLI run 子命令生成的 Playwright spec 内部 import 自 `@epoint-testtech/stage-core`。skill 只校验 credentials.json5 + 引导补齐,不解析凭据明文。
110
+
111
+ ### Step 6 — 收集报告产物
112
+
113
+ CLI 退出后用 `ls` / `Read` 校验 §3 验证清单。报告目录缺失视为整体失败,按 §4 报告。
114
+
115
+ ### Step 7 — 写入运行状态到项目索引
116
+
117
+ CLI run 子命令已在 `~/.ep-stage/projects.index.json5` 回写:
118
+
119
+ - `projects[].requirements[<i>].lastRunAt` / `lastRunStatus`(`passed` / `failed` / `partial`)
120
+ - `systems[].credentialsCache`(按 url 归并本次 credentials.json5 用到的角色凭据)
121
+
122
+ skill 校验:本需求条目存在;`lastRunAt` 是本次时间戳;`lastRunStatus` 非 `never`。缺时由 skill 补写。
123
+
124
+ ### Step 8 — 输出执行摘要
125
+
126
+ 打印全勾选终态清单 + §5 完成提示(含 spec 路径 + 报告路径 + `lastRunStatus` + Playwright HTML 报告打开指令 + 重跑提示)。
127
+
128
+ ## 3. 验证清单
129
+
130
+ | 路径 | 说明 |
131
+ |-----------------------------------------------------------------|-----------------------------------------------------|
132
+ | `<projectDir>/src/tests/<需求名>/crud.spec.ts` | v1 或 v2(含 `// === 胶水模式生成 ===` + 可选 `// === AI 钻探追加 ===` 分段)|
133
+ | `<projectDir>/src/tests/<需求名>/testcase.json5` | 必须存在 + `schemaVersion: "v2"` |
134
+ | `<projectDir>/src/tests/<需求名>/credentials.json5` | 必须存在 + 数组 ≥ 1 条 + 必填字段非空 |
135
+ | `<projectDir>/src/tests/<需求名>/.ep-stage/contract.json` | 由 testcase 阶段产,run 阶段只读不写 |
136
+ | `e2e/glue-code-mvp/glue-report/<run-timestamp>/` | run 子命令产;含执行摘要 + Playwright trace 引用 |
137
+ | `~/.ep-stage/projects.index.json5` | 已更新 `lastRunAt / lastRunStatus + systems[].credentialsCache` |
138
+
139
+ ## 4. 错误处理
140
+
141
+ 失败归类与详细排障路径见 `references/crud-pipeline.md`「失败归类」节。本 skill 高频呈现:
142
+
143
+ | 触发时机 | Agent 中文呈现 + 建议 |
144
+ |---------------------------|-----------------------------------------------------------|
145
+ | 引导 A(缺脚手架) | 中文提示+退出,引导先跑 `/glue-create-project <项目名>` |
146
+ | 引导 B(缺 testcase) | AskUserQuestion 三选一:跑 testcase / 强组装 / 取消 |
147
+ | 引导 C(缺凭据) | AskUserQuestion 三选一:缓存复用 / 现在补 / 取消 |
148
+ | spec 子命令 exit ≠ 0 | 复述 stderr 中文转述,不进 Step 5 |
149
+ | run 子命令 exit ≠ 0 | 复述 stderr;归类参 references/crud-pipeline.md 失败归类 |
150
+ | Playwright 业务失败 | 报告 `lastRunStatus: failed`,提示打开 HTML 报告人审 |
151
+ | `coveredActions` 为空 | 阻断 spec 子命令;提示用户回 `/glue-generate-testcase` 补齐 |
152
+ | 用户中途取消 | AskUserQuestion 阶段说「算了/取消」立即终止;CLI 启动后等其退出再复述 |
153
+
154
+ ## 5. 完成提示
155
+
156
+ Step 8 全部通过后,按以下中文格式呈现(不要用 success/done/passed):
157
+
158
+ > ✅ 胶水测试执行完成
159
+ >
160
+ > 需求:`<需求名>`/项目:`<projectDir>`
161
+ > Spec:`<projectDir>/src/tests/<需求名>/crud.spec.ts`(v2 — 胶水 + AI 钻探追加)
162
+ > 报告目录:`e2e/glue-code-mvp/glue-report/<run-timestamp>/`
163
+ > Playwright HTML:`npx playwright show-report --config <projectDir>/playwright.config.ts`
164
+ > 运行状态:`lastRunStatus = <passed/failed/partial>`(已回写 `~/.ep-stage/projects.index.json5`)
165
+ >
166
+ > 下一步:① 失败用例打开 HTML 报告看 trace;② 调整 testcase.json5 / ModuleHints 后重跑 `/glue-run-test <需求目录>`;③ 研发回归独立执行 `cd <projectDir> && npx playwright test src/tests/<需求名>/crud.spec.ts`(**跑文件当前内容**,v1/v2 行为见 §6)
167
+
168
+ **失败时不要发完成提示**:§3 验证项任一缺失或 CLI 非 0 退出 → 按 §4 报告,不允许混合"执行完成"字样。
169
+
170
+ ## 6. spec 双向执行路径与生命周期(REQ-SPEC-01)
171
+
172
+ 生成的 `crud.spec.ts` 是 standard Playwright spec,可被 2 条路径执行;文件有 v1 / v2 两个生命周期:
173
+
174
+ | 状态 | 文件内容 | 由谁产 | 何时存在 |
175
+ |---|---|---|---|
176
+ | **v1 初始版** | 仅 coveredActions 胶水部分(顶部 `// === 胶水模式生成 ===`) | `spec` 子命令 | 跑过 spec 但未跑过 run |
177
+ | **v2 追加版** | v1 + AI 钻探追加(底部 `// === AI 钻探追加 ===`) | `run` 子命令 | 跑过 run 后;重跑覆盖追加段 |
178
+
179
+ - **路径 ①(本 skill 标准链路)**:spec → run 子命令 → AI 推理追加产 v2。
180
+ - **路径 ②(研发回归独立执行)**:`cd <projectDir> && npx playwright test src/tests/<需求名>/crud.spec.ts` — 纯文件 IO 跑当前内容,不区分 v1/v2,不修改文件。
181
+
182
+ **测试资产自包含原则**(v1/v2 都成立):spec 用相对路径 `import { CrudPage } from '../../skeletons/...'`;凭据由同需求目录 `credentials.json5` 加载(通过 stage-core `loadCredentials`),不依赖任何 ep-stage runtime 状态;研发人员把整个 `<需求名>/` 目录拷到任意环境都能跑(含填好的 credentials.json5)。
183
+
184
+ ## 7. 真实系统 E2E 运行约束
185
+
186
+ - **禁止** `--headless true` 跑真实业务系统 E2E。本 skill 调用 run 子命令固定 `--headless false`。
187
+ - 真实系统验证**必须有人在场观察**页面行为、弹窗、登录状态和业务副作用;无人工观察的无头回放不算当前阶段验收证据。
188
+ - `--headless true` 仅允许用于本地结构调试,不作为真实系统验收方式。
189
+ - 观察结论须与 `glue-report/<run-timestamp>/` + Playwright HTML 报告交叉核对后才作为验收证据。
190
+
191
+ ## 8. 内嵌数据契约范例
192
+
193
+ `<需求名>/credentials.json5`(数组形态,REQ-ENV-02;详见 spec §4.3):
194
+
195
+ ```json5
196
+ // 单系统单角色(zwplace 最常见)
197
+ [ { url: "http://192.168.x.x:8100/.../login", username: "admin", password: "..." } ]
198
+
199
+ // 多系统多角色(StageZhgd 类业务)
200
+ [
201
+ { url: "http://sys1/login", username: "atjsdw...", password: "Epoint@2021", role: "建设单位", systemName: "建设单位办公系统" },
202
+ { url: "http://sys1/login", username: "atsgdw...", password: "Epoint@2021", role: "施工单位", systemName: "建设单位办公系统" },
203
+ { url: "http://sys2/login", username: "...", password: "...", role: "审批员", systemName: "审批端" }
204
+ ]
205
+ ```
206
+
207
+ 字段约束:必填 `url / username / password`;可选 `role / systemName`;数组至少 1 条;同 `(url, role)` 组合唯一;文件 gitignore。
208
+
209
+ `~/.ep-stage/projects.index.json5`(run 阶段回写 `requirements[].lastRun*` + `systems[].credentialsCache`):
210
+
211
+ ```json5
212
+ {
213
+ projects: [{
214
+ projectName: "...", projectDir: "...", mode: "glue", stageCreateVersion: "0.0.4-alpha.0",
215
+ requirements: [{
216
+ name: "014-测试需求10-20260622",
217
+ requirementDir: "/abs/.../014-测试需求10-20260622",
218
+ testsDir: "<projectDir>/src/tests/014-测试需求10-20260622",
219
+ lastRunAt: "2026-06-25T10:00:00Z", // run 回写
220
+ lastRunStatus: "passed" // passed | failed | partial | never;run 回写
221
+ }]
222
+ }],
223
+ systems: [{
224
+ url: "http://192.168.x.x:8100/.../login",
225
+ knownRoles: ["建设单位", "施工单位"],
226
+ credentialsCache: { "建设单位": { username: "...", password: "...", lastUsed: "..." } } // run 回写
227
+ }]
228
+ }
229
+ ```
230
+
231
+ ## 9. 引导场景
232
+
233
+ - **A(缺脚手架)**:Step 1 反查 projects.index.json5 无 glue 条目 → 提示先跑 `/glue-create-project` 退出。
234
+ - **B(缺 testcase)**:Step 2 缺失 / schema 非 v2 → AskUserQuestion 三选一(跑 testcase / 强组装 / 取消)。
235
+ - **C(缺凭据)**:Step 3 字段空 → AskUserQuestion 三选一(缓存复用 / 现在补 / 取消,含 `cachedSystemsHit` 命中提示)。
236
+ - **D(缺 code_list.md)**:归 `/glue-generate-testcase` 处理(钻探前置)。
237
+
238
+ ## 10. 按需加载
239
+
240
+ - CRUD 组装 pipeline + 验证命令 + **失败归类**:`references/crud-pipeline.md`
241
+ - harness 治理原则:`references/harness-principles.md`;gap 评审协议:`references/gap-review-protocol.md`
242
+
243
+ ## 11. 关键约束
244
+
245
+ - 钻探前置:本 skill 不再实现 contract 提取 / coverage 分析 / uncoveredCandidates 推断;这些在 `/glue-generate-testcase` 完成。
246
+ - spec 子命令**只生成不执行**(REQ-SPEC-01);Playwright 执行只走 run 子命令 ① 或研发独立 ②。
247
+ - run 子命令仅对 `uncoveredCandidates[].status === 'planned'` 项 AI 推理;`candidate` / `unresolved` 项不得盲目追加 spec(gap 评审协议)。
248
+ - AI 推理阶段不修改 `cases[].businessIntent` / `assertionExpectation`,也不改 `src/skeletons/*.ts`。
249
+ - 用户视角始终全中文;业务断言、selector、iframe 关键字、按钮文案均由骨架 + `ModuleHints` 决策,skill / spec 不发明。
@@ -0,0 +1,145 @@
1
+ # CRUD Pipeline 参考(CRUD Pipeline Reference)
2
+
3
+ > 本文为 `glue-run-test` skill 的参考文档。所有 CLI 调用统一使用 `npx -y @epoint-testtech/ep-stage-skill@latest <subcommand>` 形态(REQ-CLI-01 / REQ-CLI-02)。
4
+
5
+ ## 子命令一览
6
+
7
+ | 子命令 | 入口 | 说明 |
8
+ | --- | --- | --- |
9
+ | `testcase` | `glue-generate-testcase` skill 包装 | 上游钻探 + contract 提取 + coverage 分析 + testcase v2 生成 + credentials.template 生成 + code_list 软链 |
10
+ | `spec` | `glue-run-test` skill 包装(**只生成不执行**) | 读 testcase + contract → 生成 v1 初始版 `crud.spec.ts`(只覆盖 coveredActions 胶水部分) |
11
+ | `run` | `glue-run-test` skill 包装 | 读 spec + testcase + credentials → 启动 Playwright + AI 推理 uncoveredCandidates → 追加产 v2 + 报告 |
12
+
13
+ > **contract 提取已内嵌**到 `testcase` 子命令(REQ-CLI-02),不暴露为独立 CLI 命令。开发期可用 `pnpm generate:crud-contract`(路由到 `src/cli/dev/extract-contract.ts`),仅作调试入口,**不进 SKILL.md**。
14
+
15
+ ## ModuleHints 指引(ModuleHints Guidance)
16
+
17
+ `ModuleHints` 是"已提取事实"与"测试人员策略"之间的边界。
18
+
19
+ 以下内容应放入 hints,而不是由 Agent 自行推断:
20
+
21
+ - `dataKey.selectedField` 和 `dataKey.generationStrategy`
22
+ - `searchConditions[]`,尤其是 listbox 的 `value`
23
+ - `buttonAliases`
24
+ - `autofill.overrideFields`
25
+ - `deletePolicy`
26
+ - 断言策略覆盖
27
+
28
+ 对于多条件 CRUD 冒烟测试,推荐:
29
+
30
+ ```json
31
+ {
32
+ "dataKey": {
33
+ "selectedField": "placename",
34
+ "generationStrategy": "timestamp_prefix"
35
+ },
36
+ "searchConditions": [
37
+ {
38
+ "field": "placename",
39
+ "component": "input",
40
+ "seedValue": "自动化测试名称",
41
+ "strategy": "timestamp_prefix",
42
+ "updatePrefix": "修改"
43
+ },
44
+ {
45
+ "field": "placecategory",
46
+ "component": "listbox",
47
+ "value": "条线大厅",
48
+ "createSelections": [
49
+ { "field": "placecategory", "value": "条线大厅" }
50
+ ]
51
+ }
52
+ ]
53
+ }
54
+ ```
55
+
56
+ `searchConditions` 驱动生成的 `CrudFlowSlots`。`dataKey` 仍作为兼容性和策略锚点保留,但不应与独立的 `searchField` 概念重复。
57
+
58
+ ## 检查 testcase 闸门(Inspect Testcase Gate)
59
+
60
+ testcase 子命令产出后,由 skill 校验 `testcase.json5` 顶层结构(schemaVersion=v2 + scenario + menuPath + requiredSystems + requiredRoles + coveredActions + uncoveredCandidates + cases + reasoningSummary)。`coveredActions` 为空 → 阻断 spec 子命令;`uncoveredCandidates[].status === 'unresolved'` → 提示用户补 `ModuleHints` 或拒签该 case。
61
+
62
+ 详细 schema 见 [glue-generate-testcase/references/testcase-schema.md](../../glue-generate-testcase/references/testcase-schema.md)。
63
+
64
+ ## 生成 Spec(Generate Spec)
65
+
66
+ 任意 cwd 执行(**只生成不执行**,REQ-SPEC-01):
67
+
68
+ ```bash
69
+ npx -y @epoint-testtech/ep-stage-skill@latest spec \
70
+ --testcase <projectDir>/src/tests/<需求名>/testcase.json5 \
71
+ --out <projectDir>/src/tests/<需求名>/crud.spec.ts
72
+ ```
73
+
74
+ 生成的 spec(v1 初始版)应当:
75
+
76
+ - 从 `../../skeletons` 导入 `CrudPage`、`LoginPage`、`MenuPage` 以及 `type CrudFlowSlots`
77
+ - 定义 `const crudSlots: CrudFlowSlots`
78
+ - 调用 `await crudPage.runCrudFlow()`
79
+ - 仅覆盖 `coveredActions`(胶水部分);`uncoveredCandidates` 留给 `run` 子命令 AI 推理追加
80
+ - 避免内联 Playwright 操作流程
81
+
82
+ ## 执行 + AI 推理追加(Run + AI Inference Append)
83
+
84
+ ```bash
85
+ npx -y @epoint-testtech/ep-stage-skill@latest run \
86
+ --spec <projectDir>/src/tests/<需求名>/crud.spec.ts \
87
+ --testcase <projectDir>/src/tests/<需求名>/testcase.json5 \
88
+ --headless false
89
+ ```
90
+
91
+ run 子命令内部:
92
+
93
+ 1. 读 `<需求名>/credentials.json5`(数组形态多角色,REQ-ENV-02)
94
+ 2. 启动 Playwright → 按 `cases[].requiredRole` 切换登录
95
+ 3. 跑 v1 胶水部分 spec
96
+ 4. 对 `uncoveredCandidates[].status === 'planned'` 项 AI 实时推理(页面浏览 + 补 spec),用 `uncoveredCandidates[].requiredRole` 切换登录
97
+ 5. 追加到 spec 底部产 v2(带 `// === AI 钻探追加 ===` 分段注释)
98
+ 6. 报告落 `e2e/glue-code-mvp/glue-report/<run-timestamp>/`
99
+ 7. 回写 `~/.ep-stage/projects.index.json5` 的 `systems[].credentialsCache` + `requirements[].lastRun*`
100
+
101
+ **重跑 run**:检测到 spec 已含 `// === AI 钻探追加 ===` 分段时**覆盖追加段**,不保留旧追加(REQ-SPEC-01 v1/v2 生命周期)。
102
+
103
+ ## 凭据配置(Credentials Config)
104
+
105
+ 凭据下沉到**需求级** `credentials.json5` 数组形态(REQ-ENV-02 / REQ-ENV-03),**不再用 `.env`**:
106
+
107
+ ```json5
108
+ // <projectDir>/src/tests/<需求名>/credentials.json5(gitignore)
109
+ [
110
+ {
111
+ url: "http://192.168.x.x:8100/.../login",
112
+ username: "admin",
113
+ password: "***",
114
+ role: "admin", // 可选,多角色场景必填
115
+ systemName: "主业务系统" // 可选,便于审阅与报告
116
+ }
117
+ ]
118
+ ```
119
+
120
+ 字段约束(Task 1.1 `validateCredentials` 强制):
121
+
122
+ - 必填:`url` / `username` / `password`
123
+ - 可选:`role`(用于 Playwright 多角色登录切换)/ `systemName`
124
+ - `(url, role)` 组合唯一
125
+
126
+ 完整流程参考 [glue-generate-testcase SKILL.md](../../glue-generate-testcase/SKILL.md) 第 5 步「与用户交互确认推断结果」。
127
+
128
+ ## 在消费方 glue 项目中本地编译验证(Optional Local Check)
129
+
130
+ ```bash
131
+ cd <projectDir>
132
+ pnpm --ignore-workspace exec tsc --noEmit
133
+ ```
134
+
135
+ 仅校验 v1 spec 编译通过;不执行测试。Playwright 真实执行走 `run` 子命令。
136
+
137
+ ## 失败归类(Failure Triage)
138
+
139
+ 若运行时失败,将失败原因归类为:
140
+
141
+ - **系统可达性或登录配置** — credentials.json5 缺字段 / URL 不通 / 角色凭据错;引导场景 C
142
+ - **上游物料不匹配** — code_list.md 缺字段;引导场景 D
143
+ - **`ModuleHints` 缺失或错误** — testcase.json5 `coveredActions` 为空 / `uncoveredCandidates` 长期 `unresolved`
144
+ - **骨架/组件行为** — `CrudPage` / `LoginPage` 实现 bug;本地修复后回归
145
+ - **包生成器缺陷** — `spec` / `run` 子命令产物错误;走 ep-stage-skill 仓库 issue
@@ -24,7 +24,7 @@ function help() {
24
24
 
25
25
  Notes:
26
26
  - Relative upstream paths are resolved from the repository root.
27
- - The contract path is passed to @epoint/ep-stage-skill relative to its package root.
27
+ - The contract path is passed to @epoint-testtech/ep-stage-skill relative to its package root.
28
28
  - The script fails when unresolvedSlots is non-empty.
29
29
  `);
30
30
  }
@@ -88,7 +88,7 @@ mkdirSync(path.dirname(contractAbs), { recursive: true });
88
88
 
89
89
  run([
90
90
  '--filter',
91
- '@epoint/ep-stage-skill',
91
+ '@epoint-testtech/ep-stage-skill',
92
92
  'generate:crud-contract',
93
93
  '--',
94
94
  '--module-id',
@@ -125,7 +125,7 @@ mkdirSync(path.dirname(specAbs), { recursive: true });
125
125
 
126
126
  run([
127
127
  '--filter',
128
- '@epoint/ep-stage-skill',
128
+ '@epoint-testtech/ep-stage-skill',
129
129
  'generate:playwright-tests',
130
130
  '--',
131
131
  '--contract',
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: recording-to-glue
3
3
  description: 使用时机:当用户讨论或规划「将录制产出的 Playwright 脚本转换为 ep-stage 胶水骨架(glue skeleton)或组件(component)候选代码」时使用。
4
+ version: 2026-06-23+a9e7576
4
5
  ---
5
6
 
6
7
  # ep-stage: recording-to-glue
@@ -1,4 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ // validate-skill.mjs — 校验 ep-stage scope 下所有 skill 的 SKILL.md frontmatter
3
+ // + 双发现目录(.claude/skills / .agents/skills)symlink 指向。
4
+ //
5
+ // version 字段维护契约(REQ-VER-003):
6
+ // 当 SKILL.md 内容(frontmatter 或正文)发生变更时,维护者必须手动把
7
+ // `version:` 更新为 `YYYY-MM-DD+<7位短SHA>`,其中 <短SHA> 取当前 HEAD。
8
+ // 快捷生成命令:date +"%Y-%m-%d"+$(git rev-parse --short HEAD)
9
+ // pre-commit hook(.husky/pre-commit → check-skill-version-bump.mjs)会
10
+ // 自动校验:改了 SKILL.md 但没改 version 行 → 阻塞 commit。
2
11
  import { lstatSync, readFileSync, realpathSync } from 'node:fs';
3
12
  import path from 'node:path';
4
13
  import { fileURLToPath } from 'node:url';
@@ -8,6 +17,9 @@ const epStageRoot = path.resolve(__dirname, '..');
8
17
  const packageRoot = path.resolve(epStageRoot, '../..');
9
18
  const repoRoot = path.resolve(packageRoot, '../..');
10
19
 
20
+ // version 值正则:YYYY-MM-DD+<7位十六进制短SHA>
21
+ const VERSION_PATTERN = /^\d{4}-\d{2}-\d{2}\+[0-9a-f]{7}$/;
22
+
11
23
  function readFrontmatter(skillRoot) {
12
24
  const skillPath = path.join(skillRoot, 'SKILL.md');
13
25
  const text = readFileSync(skillPath, 'utf8');
@@ -18,17 +30,20 @@ function readFrontmatter(skillRoot) {
18
30
  return {
19
31
  name: frontmatter.match(/^name:\s*(.+)$/m)?.[1]?.trim(),
20
32
  description: frontmatter.match(/^description:\s*(.+)$/m)?.[1]?.trim(),
33
+ version: frontmatter.match(/^version:\s*(.+)$/m)?.[1]?.trim(),
21
34
  skillPath,
22
35
  };
23
36
  }
24
37
 
25
38
  function assertSkill(skillRoot, expectedName) {
26
- const { name, description, skillPath } = readFrontmatter(skillRoot);
39
+ const { name, description, version, skillPath } = readFrontmatter(skillRoot);
27
40
  if (name !== expectedName) throw new Error(`Expected name ${expectedName} in ${skillPath}, got ${name ?? 'missing'}`);
28
41
  if (!description?.startsWith('使用时机') && !description?.startsWith('Use when')) throw new Error(`${skillPath} description 必须以「使用时机」或「Use when」开头`);
29
42
  if (description.length > 1024) throw new Error(`${skillPath} description exceeds 1024 characters`);
30
43
  if (path.basename(skillRoot) !== name) throw new Error(`${skillPath} directory basename must match name`);
31
- return { name, descriptionLength: description.length, skillRoot };
44
+ if (!version) throw new Error(`${skillPath} version 字段缺失;维护者改 SKILL.md 时必须同步更新 version(格式 YYYY-MM-DD+<7位短SHA>)`);
45
+ if (!VERSION_PATTERN.test(version)) throw new Error(`${skillPath} version 格式非法 "${version}";应为 YYYY-MM-DD+<7位短SHA>,如 2026-06-23+a9e7576`);
46
+ return { name, descriptionLength: description.length, version, skillRoot };
32
47
  }
33
48
 
34
49
  function assertSymlink(linkPath, targetPath) {
@@ -40,10 +55,14 @@ function assertSymlink(linkPath, targetPath) {
40
55
  }
41
56
 
42
57
  // ep-stage scope 下四个 skill 的包内真相源目录。
58
+ // 注:Task 0.2 已 git mv 改名 3 个 skill 目录(glue-* 前缀化)。
59
+ // - glue-create-project:Task 2.1 已同步 SKILL.md frontmatter name → 'glue-create-project'。
60
+ // - glue-generate-testcase:Task 3.2 已同步 SKILL.md frontmatter name → 'glue-generate-testcase'。
61
+ // - glue-run-test:Task 4.3 已同步 SKILL.md frontmatter name → 'glue-run-test'。
43
62
  const skillSources = [
44
- { name: 'create-project', source: path.join(epStageRoot, 'create-project') },
45
- { name: 'glue-test', source: path.join(epStageRoot, 'glue-test') },
46
- { name: 'glue-testcase', source: path.join(epStageRoot, 'glue-testcase') },
63
+ { name: 'glue-create-project', source: path.join(epStageRoot, 'glue-create-project') },
64
+ { name: 'glue-generate-testcase', source: path.join(epStageRoot, 'glue-generate-testcase') },
65
+ { name: 'glue-run-test', source: path.join(epStageRoot, 'glue-run-test') },
47
66
  { name: 'recording-to-glue', source: path.join(epStageRoot, 'recording-to-glue') },
48
67
  ];
49
68
 
@@ -57,11 +76,14 @@ const discoveryDirs = [
57
76
  path.join(repoRoot, '.agents/skills'),
58
77
  ];
59
78
 
79
+ // 注:symlink 名总是与包内目录 basename 一致(Phase 0 重建后)。
80
+ // Task 4.3 收敛后,3 个 glue-* skill 的 SKILL.md `name` 与目录 basename 完全一致;symlink 仍以 basename 为准。
60
81
  const symlinks = [];
61
82
  for (const dir of discoveryDirs) {
62
83
  for (const { name, source } of skillSources) {
63
- assertSymlink(path.join(dir, name), source);
64
- symlinks.push({ dir: path.relative(repoRoot, dir), name, pointsTo: path.relative(repoRoot, source) });
84
+ const linkName = path.basename(source);
85
+ assertSymlink(path.join(dir, linkName), source);
86
+ symlinks.push({ dir: path.relative(repoRoot, dir), name, linkName, pointsTo: path.relative(repoRoot, source) });
65
87
  }
66
88
  }
67
89
 
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 开发期 contract 提取入口(REQ-CLI-03)。被 generate:crud-contract pnpm script 调用,
3
+ * 不在 index.ts 注册为子命令,不出现在任何 SKILL.md。
4
+ *
5
+ * 行为与 0.0.3-alpha.2 的 generate-crud-contract.ts 完全等价;本次重构仅把执行
6
+ * 入口包装成可被 pnpm script 与 generate-testcase 子命令复用的纯函数。
7
+ *
8
+ * @param argv - 命令行参数(--module-id/--docs/--code-list/--webapp/--java-actions/--hints/--out)。
9
+ * @returns 写入的 contract.json 绝对路径。
10
+ */
11
+ export declare function runExtractContract(argv: string[]): {
12
+ contractPath: string;
13
+ };
14
+ //# sourceMappingURL=extract-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-contract.d.ts","sourceRoot":"","sources":["../../../../src/cli/dev/extract-contract.ts"],"names":[],"mappings":"AA+GA;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,CAkB3E"}
@@ -0,0 +1,114 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { buildCrudBusinessModuleContract, extractCodeListSummary, extractHtmlPage, extractJavaAction, extractSpecYaml, } from '../../index.js';
4
+ /**
5
+ * 解析 dev/extract-contract.ts 的命令行参数。
6
+ *
7
+ * @param argv - process.argv.slice(2)。
8
+ * @returns 解析后的 ExtractContractArgs。
9
+ * @throws 缺少必填参数或显式使用废弃的 --module 时抛错。
10
+ */
11
+ function parseExtractContractArgs(argv) {
12
+ const result = {};
13
+ const normalizedArgv = argv.filter((arg) => arg !== '--');
14
+ for (let index = 0; index < normalizedArgv.length; index += 2) {
15
+ const key = normalizedArgv[index];
16
+ const value = normalizedArgv[index + 1];
17
+ if (!key?.startsWith('--') || !value) {
18
+ throw new Error(`Invalid argument pair near ${key ?? '<empty>'}`);
19
+ }
20
+ result[key.slice(2)] = value;
21
+ }
22
+ if (result.module) {
23
+ throw new Error('已废弃 --module,请使用 --module-id。');
24
+ }
25
+ for (const key of ['module-id', 'docs', 'code-list', 'webapp', 'java-actions', 'out']) {
26
+ if (!result[key]) {
27
+ throw new Error(`Missing required argument --${key}`);
28
+ }
29
+ }
30
+ return {
31
+ module: result['module-id'],
32
+ docs: result.docs,
33
+ codeList: result['code-list'],
34
+ webapp: result.webapp,
35
+ javaActions: result['java-actions'],
36
+ hints: result.hints,
37
+ out: result.out,
38
+ };
39
+ }
40
+ /**
41
+ * 列出指定目录下所有 .html 文件并返回排序后的绝对路径。
42
+ *
43
+ * @param dir - HTML 物料目录。
44
+ * @returns 排序后的绝对路径数组。
45
+ */
46
+ function htmlFiles(dir) {
47
+ return readdirSync(dir)
48
+ .filter((fileName) => fileName.endsWith('.html'))
49
+ .sort()
50
+ .map((fileName) => path.join(dir, fileName));
51
+ }
52
+ /**
53
+ * 列出指定目录下所有 *Action.java 文件并返回排序后的绝对路径。
54
+ *
55
+ * @param dir - Java Action 物料目录。
56
+ * @returns 排序后的绝对路径数组。
57
+ */
58
+ function javaFiles(dir) {
59
+ return readdirSync(dir)
60
+ .filter((fileName) => fileName.endsWith('Action.java'))
61
+ .sort()
62
+ .map((fileName) => path.join(dir, fileName));
63
+ }
64
+ /**
65
+ * 解析 docs 目录下的 spec.yaml;缺失时返回最小空骨架(保证 contract 构建不阻断)。
66
+ *
67
+ * @param docsDir - 需求文档目录。
68
+ * @param moduleId - 模块 ID。
69
+ * @param codeListSummary - 已解析的 code_list 摘要。
70
+ * @returns ExtractedSpecYaml。
71
+ */
72
+ function resolveSpec(docsDir, moduleId, codeListSummary) {
73
+ const specPath = path.join(docsDir, 'spec.yaml');
74
+ if (existsSync(specPath))
75
+ return extractSpecYaml(specPath);
76
+ return {
77
+ path: specPath,
78
+ module: { id: moduleId, label: codeListSummary.moduleLabel },
79
+ fields: [],
80
+ businessRules: [],
81
+ };
82
+ }
83
+ /**
84
+ * 开发期 contract 提取入口(REQ-CLI-03)。被 generate:crud-contract pnpm script 调用,
85
+ * 不在 index.ts 注册为子命令,不出现在任何 SKILL.md。
86
+ *
87
+ * 行为与 0.0.3-alpha.2 的 generate-crud-contract.ts 完全等价;本次重构仅把执行
88
+ * 入口包装成可被 pnpm script 与 generate-testcase 子命令复用的纯函数。
89
+ *
90
+ * @param argv - 命令行参数(--module-id/--docs/--code-list/--webapp/--java-actions/--hints/--out)。
91
+ * @returns 写入的 contract.json 绝对路径。
92
+ */
93
+ export function runExtractContract(argv) {
94
+ const args = parseExtractContractArgs(argv);
95
+ const hints = args.hints
96
+ ? JSON.parse(readFileSync(args.hints, 'utf8'))
97
+ : undefined;
98
+ const codeList = extractCodeListSummary(args.codeList);
99
+ const contract = buildCrudBusinessModuleContract({
100
+ moduleId: args.module,
101
+ spec: resolveSpec(args.docs, args.module, codeList),
102
+ codeList,
103
+ pages: htmlFiles(args.webapp).map(extractHtmlPage),
104
+ actions: javaFiles(args.javaActions).map(extractJavaAction),
105
+ hints,
106
+ });
107
+ mkdirSync(path.dirname(args.out), { recursive: true });
108
+ writeFileSync(args.out, `${JSON.stringify(contract, null, 2)}\n`, 'utf8');
109
+ console.log(`Wrote ${args.out}`);
110
+ return { contractPath: path.resolve(args.out) };
111
+ }
112
+ if (import.meta.url === `file://${process.argv[1]}`) {
113
+ runExtractContract(process.argv.slice(2));
114
+ }