@epoint-testtech/ep-stage-skill 0.0.4-alpha.0 → 0.0.4-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.
Files changed (70) hide show
  1. package/codex-skill/ep-stage/.omc/state/sessions/d636ad35-5fc9-4d5e-85b0-2bcad7d86ee5/pre-tool-advisory-throttle.json +10 -0
  2. package/codex-skill/ep-stage/glue-create-project/SKILL.md +22 -2
  3. package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +119 -44
  4. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +72 -0
  5. package/codex-skill/ep-stage/glue-run-test/SKILL.md +45 -5
  6. package/dist/src/cli/generate-testcase.d.ts +1 -0
  7. package/dist/src/cli/generate-testcase.d.ts.map +1 -1
  8. package/dist/src/cli/generate-testcase.js +112 -9
  9. package/dist/src/cli/index.d.ts +8 -0
  10. package/dist/src/cli/index.d.ts.map +1 -1
  11. package/dist/src/cli/index.js +21 -1
  12. package/dist/src/cli/probe.d.ts +2 -0
  13. package/dist/src/cli/probe.d.ts.map +1 -1
  14. package/dist/src/cli/probe.js +239 -13
  15. package/dist/src/cli/spec.d.ts +6 -3
  16. package/dist/src/cli/spec.d.ts.map +1 -1
  17. package/dist/src/cli/spec.js +31 -3
  18. package/dist/src/contracts/inference-trace.d.ts +223 -0
  19. package/dist/src/contracts/inference-trace.d.ts.map +1 -0
  20. package/dist/src/contracts/inference-trace.js +12 -0
  21. package/dist/src/extractors/code-list.d.ts +3 -3
  22. package/dist/src/extractors/code-list.d.ts.map +1 -1
  23. package/dist/src/extractors/code-list.js +46 -11
  24. package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
  25. package/dist/src/generators/stage-skeleton-script.js +9 -0
  26. package/dist/src/index.d.ts +5 -0
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/index.js +2 -0
  29. package/dist/src/inference/module-hints-inference.d.ts +30 -0
  30. package/dist/src/inference/module-hints-inference.d.ts.map +1 -0
  31. package/dist/src/inference/module-hints-inference.js +363 -0
  32. package/dist/src/inference/source-analysis.d.ts +26 -0
  33. package/dist/src/inference/source-analysis.d.ts.map +1 -0
  34. package/dist/src/inference/source-analysis.js +258 -0
  35. package/dist/src/validation/index.d.ts +1 -0
  36. package/dist/src/validation/index.d.ts.map +1 -1
  37. package/dist/src/validation/index.js +1 -0
  38. package/dist/src/validation/inference-trace.d.ts +13 -0
  39. package/dist/src/validation/inference-trace.d.ts.map +1 -0
  40. package/dist/src/validation/inference-trace.js +31 -0
  41. package/dist/test/cli/generate-testcase-inference.test.d.ts +2 -0
  42. package/dist/test/cli/generate-testcase-inference.test.d.ts.map +1 -0
  43. package/dist/test/cli/generate-testcase-inference.test.js +63 -0
  44. package/dist/test/cli/generate-testcase-paths.test.d.ts +2 -0
  45. package/dist/test/cli/generate-testcase-paths.test.d.ts.map +1 -0
  46. package/dist/test/cli/generate-testcase-paths.test.js +165 -0
  47. package/dist/test/cli/generate-testcase-requirements.test.d.ts +2 -0
  48. package/dist/test/cli/generate-testcase-requirements.test.d.ts.map +1 -0
  49. package/dist/test/cli/generate-testcase-requirements.test.js +229 -0
  50. package/dist/test/cli/index.test.js +20 -0
  51. package/dist/test/cli/probe.test.d.ts +2 -0
  52. package/dist/test/cli/probe.test.d.ts.map +1 -0
  53. package/dist/test/cli/probe.test.js +31 -0
  54. package/dist/test/cli/spec-boundary.test.d.ts +2 -0
  55. package/dist/test/cli/spec-boundary.test.d.ts.map +1 -0
  56. package/dist/test/cli/spec-boundary.test.js +198 -0
  57. package/dist/test/cli/spec.test.js +1 -1
  58. package/dist/test/code-list.test.d.ts +2 -0
  59. package/dist/test/code-list.test.d.ts.map +1 -0
  60. package/dist/test/code-list.test.js +39 -0
  61. package/dist/test/crud-contract.test.js +12 -32
  62. package/dist/test/inference/module-hints-inference.test.d.ts +2 -0
  63. package/dist/test/inference/module-hints-inference.test.d.ts.map +1 -0
  64. package/dist/test/inference/module-hints-inference.test.js +25 -0
  65. package/dist/test/inference/source-analysis.test.d.ts +2 -0
  66. package/dist/test/inference/source-analysis.test.d.ts.map +1 -0
  67. package/dist/test/inference/source-analysis.test.js +23 -0
  68. package/dist/test/material-inventory.test.js +13 -0
  69. package/dist/test/observable-pipeline.test.js +12 -3
  70. package/package.json +1 -1
@@ -0,0 +1,363 @@
1
+ /** CRUD 骨架与导出覆盖的标准按钮标签集合(归类用)。 */
2
+ const CRUD_BUTTON_LABELS = {
3
+ create: ['新增', '增加', '添加', '新建'],
4
+ search: ['搜索', '查询', '检索'],
5
+ edit: ['修改', '编辑'],
6
+ delete: ['删除', '删除选定', '批量删除'],
7
+ confirm: ['确定', '确认', '保存', '保存并关闭'],
8
+ export: ['导出', '导出选定', '批量导出']
9
+ };
10
+ /**
11
+ * 识别列表页。
12
+ *
13
+ * 优先选 pageId 以 `list` 结尾的页面(文件名约定);无则回落到第一个含 datagrid 的页面。
14
+ * 不能只用 grids.length > 0,因为 add/edit/detail 页也可能内嵌 datagrid(如 014 场景),
15
+ * 会导致搜索条件误从表单页抽取(regression:windowname 被当作搜索条件)。
16
+ *
17
+ * @param pages - 已抽取的 HTML 页面列表。
18
+ * @returns 列表页;无则返回 undefined。
19
+ */
20
+ function findListPage(pages) {
21
+ const byId = pages.find((page) => /list$/i.test(page.pageId));
22
+ if (byId)
23
+ return byId;
24
+ return pages.find((page) => page.grids.length > 0);
25
+ }
26
+ /**
27
+ * 收集列表页搜索区字段名(fui-condition 区的 input bind)。
28
+ *
29
+ * @param listPage - 列表页。
30
+ * @returns 搜索区字段名集合。
31
+ */
32
+ function collectSearchFields(listPage) {
33
+ const fields = new Set();
34
+ if (!listPage)
35
+ return fields;
36
+ for (const field of listPage.fields) {
37
+ fields.add(field.name);
38
+ }
39
+ return fields;
40
+ }
41
+ /**
42
+ * 收集列表页表格数据列 field。
43
+ *
44
+ * @param listPage - 列表页。
45
+ * @returns 表格列 field 集合。
46
+ */
47
+ function collectGridFields(listPage) {
48
+ const fields = new Set();
49
+ if (!listPage)
50
+ return fields;
51
+ for (const grid of listPage.grids) {
52
+ for (const column of grid.dataColumns) {
53
+ if (column.field) {
54
+ fields.add(column.field);
55
+ }
56
+ }
57
+ }
58
+ return fields;
59
+ }
60
+ /**
61
+ * 收集新增/编辑页表单字段名。
62
+ *
63
+ * @param pages - 所有页面。
64
+ * @returns 表单字段名集合。
65
+ */
66
+ function collectFormFields(pages) {
67
+ const fields = new Set();
68
+ for (const page of pages) {
69
+ for (const field of page.fields) {
70
+ if (field.name) {
71
+ fields.add(field.name);
72
+ }
73
+ }
74
+ }
75
+ return fields;
76
+ }
77
+ /**
78
+ * 推理 dataKey.selectedField。
79
+ *
80
+ * 规则:优先选同时出现在列表页搜索区、表格列、新增/编辑页表单的 input 字段。
81
+ * 若无三方交集,回落到搜索区与表格列的交集;再无则取表格第一列。
82
+ *
83
+ * @param listPage - 列表页。
84
+ * @param pages - 所有页面。
85
+ * @returns dataKey 推理过程。
86
+ */
87
+ function inferDataKey(listPage, pages) {
88
+ const searchFields = collectSearchFields(listPage);
89
+ const gridFields = collectGridFields(listPage);
90
+ const formFields = collectFormFields(pages);
91
+ // 三方交集
92
+ const tripleIntersection = Array.from(searchFields).filter((field) => gridFields.has(field) && formFields.has(field));
93
+ if (tripleIntersection.length > 0) {
94
+ return {
95
+ selectedField: tripleIntersection[0],
96
+ reason: '列表页搜索区、表格列、新增/编辑页表单同时出现该字段',
97
+ evidence: tripleIntersection.map((field) => `${field} 出现在搜索区+表格列+表单`)
98
+ };
99
+ }
100
+ // 搜索区与表格列交集
101
+ const dualIntersection = Array.from(searchFields).filter((field) => gridFields.has(field));
102
+ if (dualIntersection.length > 0) {
103
+ return {
104
+ selectedField: dualIntersection[0],
105
+ reason: '列表页搜索区与表格列同时出现该字段(表单未覆盖)',
106
+ evidence: dualIntersection.map((field) => `${field} 出现在搜索区+表格列`)
107
+ };
108
+ }
109
+ // 回落到表格第一列
110
+ if (listPage && listPage.grids.length > 0) {
111
+ const firstColumn = listPage.grids[0].dataColumns[0];
112
+ if (firstColumn?.field) {
113
+ return {
114
+ selectedField: firstColumn.field,
115
+ reason: '搜索区与表格列无交集,回落到表格第一列',
116
+ evidence: [`表格第一列 field=${firstColumn.field}`]
117
+ };
118
+ }
119
+ }
120
+ return {
121
+ reason: '无法从物料中推理出 dataKey',
122
+ evidence: []
123
+ };
124
+ }
125
+ /**
126
+ * 推理搜索条件。
127
+ *
128
+ * 规则:至少包含 dataKey input 条件;listbox 条件缺取值时只进 unresolved。
129
+ *
130
+ * @param listPage - 列表页。
131
+ * @param dataKeyField - 选定的 dataKey 字段。
132
+ * @returns 搜索条件推理过程数组。
133
+ */
134
+ function inferSearchConditions(listPage, dataKeyField) {
135
+ const conditions = [];
136
+ if (!listPage)
137
+ return conditions;
138
+ for (const field of listPage.fields) {
139
+ const isInput = field.controlType.includes('textbox') || field.controlType.includes('input');
140
+ const isListbox = field.controlType.includes('combobox') || field.controlType.includes('listbox');
141
+ if (isInput) {
142
+ conditions.push({
143
+ field: field.name,
144
+ component: 'input',
145
+ resolved: true
146
+ });
147
+ }
148
+ else if (isListbox) {
149
+ // listbox 缺取值时只进 unresolved,不阻断
150
+ conditions.push({
151
+ field: field.name,
152
+ component: 'listbox',
153
+ resolved: false,
154
+ reason: 'listbox 取值需人工或后续补齐'
155
+ });
156
+ }
157
+ }
158
+ // 确保 dataKey 字段在搜索条件中
159
+ if (dataKeyField && !conditions.some((condition) => condition.field === dataKeyField)) {
160
+ conditions.unshift({
161
+ field: dataKeyField,
162
+ component: 'input',
163
+ resolved: true
164
+ });
165
+ }
166
+ return conditions;
167
+ }
168
+ /**
169
+ * 从按钮标签归类 buttonAliases。
170
+ *
171
+ * @param pages - 所有页面。
172
+ * @returns 按钮别名归类映射。
173
+ */
174
+ function inferButtonAliases(pages) {
175
+ const aliases = {};
176
+ const allLabels = new Set();
177
+ for (const page of pages) {
178
+ for (const button of page.buttons) {
179
+ allLabels.add(button.label);
180
+ }
181
+ }
182
+ for (const [alias, patterns] of Object.entries(CRUD_BUTTON_LABELS)) {
183
+ const matched = Array.from(allLabels).filter((label) => patterns.some((pattern) => label.includes(pattern)));
184
+ if (matched.length > 0) {
185
+ aliases[alias] = matched;
186
+ }
187
+ }
188
+ return aliases;
189
+ }
190
+ /**
191
+ * 推理删除策略。
192
+ *
193
+ * 规则:Java messages 含"不可删除"类提示时推为 blocked_by_business_rule。
194
+ *
195
+ * @param actions - Java Action 列表。
196
+ * @returns 删除策略推理过程。
197
+ */
198
+ function inferDeletePolicy(actions) {
199
+ const blockMessages = [];
200
+ for (const action of actions) {
201
+ for (const message of action.messages) {
202
+ if (/不可删除|不允许删除|禁止删除/.test(message)) {
203
+ blockMessages.push(message);
204
+ }
205
+ }
206
+ }
207
+ if (blockMessages.length > 0) {
208
+ return {
209
+ value: 'blocked_by_business_rule',
210
+ reason: 'Java Action 存在不可删除提示',
211
+ evidence: blockMessages
212
+ };
213
+ }
214
+ return {
215
+ reason: '未发现删除限制证据,默认允许删除',
216
+ evidence: []
217
+ };
218
+ }
219
+ /** 已被 CRUD 骨架与导出覆盖的按钮标签模式(customWorkflows 排除用)。 */
220
+ const SKELETON_BUTTON_PATTERNS = [
221
+ ...CRUD_BUTTON_LABELS.create,
222
+ ...CRUD_BUTTON_LABELS.search,
223
+ ...CRUD_BUTTON_LABELS.edit,
224
+ ...CRUD_BUTTON_LABELS.delete,
225
+ ...CRUD_BUTTON_LABELS.confirm,
226
+ ...CRUD_BUTTON_LABELS.export,
227
+ '重置', '关闭', '返回', '取消', '刷新'
228
+ ];
229
+ /**
230
+ * 收集 CRUD/Export 骨架之外的自定义工作流(工具栏按钮)。
231
+ *
232
+ * 规则:有 Java callback message 时填 assertion,否则 unresolved。
233
+ *
234
+ * @param pages - 所有页面。
235
+ * @param actions - Java Action 列表。
236
+ * @returns 自定义工作流推理过程数组。
237
+ */
238
+ function inferCustomWorkflows(pages, actions) {
239
+ // 收集所有 Java callback 消息,用于匹配断言
240
+ const allMessages = [];
241
+ for (const action of actions) {
242
+ allMessages.push(...action.messages);
243
+ }
244
+ const workflows = [];
245
+ const seen = new Set();
246
+ for (const page of pages) {
247
+ for (const button of page.buttons) {
248
+ const label = button.label;
249
+ if (seen.has(label))
250
+ continue;
251
+ const isSkeleton = SKELETON_BUTTON_PATTERNS.some((pattern) => label.includes(pattern));
252
+ if (isSkeleton)
253
+ continue;
254
+ seen.add(label);
255
+ // 尝试从 Java messages 中找到与按钮相关的断言
256
+ const relatedMessage = allMessages.find((message) => {
257
+ // 按钮标签的关键词出现在消息里,或消息里的动作动词与按钮相关
258
+ const keywords = label.replace(/管理系统|信息管理|数据/g, '').trim();
259
+ return keywords.length > 0 && message.includes(keywords);
260
+ });
261
+ if (relatedMessage) {
262
+ workflows.push({
263
+ label,
264
+ actionAlias: button.onclick ? [button.onclick] : undefined,
265
+ expectedIntent: label,
266
+ expectedAssertion: relatedMessage,
267
+ resolved: true,
268
+ evidence: [
269
+ `按钮 onclick=${button.onclick ?? '(未声明)'}`,
270
+ `Java callback: ${relatedMessage}`
271
+ ]
272
+ });
273
+ }
274
+ else {
275
+ workflows.push({
276
+ label,
277
+ actionAlias: button.onclick ? [button.onclick] : undefined,
278
+ expectedIntent: label,
279
+ resolved: false,
280
+ reason: '未找到对应 Java callback 断言',
281
+ evidence: [`按钮 onclick=${button.onclick ?? '(未声明)'}`]
282
+ });
283
+ }
284
+ }
285
+ }
286
+ return workflows;
287
+ }
288
+ /**
289
+ * 基于上游物料推理 ModuleHints 动态槽位。
290
+ *
291
+ * 推理规则:
292
+ * - dataKey.selectedField 优先选同时出现在列表页搜索区、表格列、新增/编辑页表单的 input 字段。
293
+ * - searchConditions 至少包含 dataKey input 条件;listbox 条件缺取值时只进 unresolved。
294
+ * - buttonAliases 从页面按钮标签归类(新增、搜索、修改、删除、确定、导出)。
295
+ * - deletePolicy 若 Java messages 含"不可删除",推为 blocked_by_business_rule。
296
+ * - customWorkflows 收集 CRUD/Export 骨架之外的工具栏按钮;有 Java callback 时填 assertion,否则 unresolved。
297
+ *
298
+ * @param input - 输入参数(sourceAnalysis、pages、actions、可选 explicitHints)。
299
+ * @returns 推理出的 hints 与推理过程。
300
+ */
301
+ export function inferModuleHintsFromMaterials(input) {
302
+ const listPage = findListPage(input.pages);
303
+ const dataKeyInference = inferDataKey(listPage, input.pages);
304
+ const searchConditionInferences = inferSearchConditions(listPage, dataKeyInference.selectedField);
305
+ const buttonAliases = inferButtonAliases(input.pages);
306
+ const deletePolicyInference = inferDeletePolicy(input.actions);
307
+ const customWorkflows = inferCustomWorkflows(input.pages, input.actions);
308
+ // 组装 hints(与 ModuleHints 类型对齐)
309
+ const hints = {
310
+ dataKey: dataKeyInference.selectedField
311
+ ? {
312
+ selectedField: dataKeyInference.selectedField,
313
+ candidateFields: input.sourceAnalysis.fields
314
+ }
315
+ : undefined,
316
+ searchConditions: searchConditionInferences
317
+ .filter((condition) => condition.resolved)
318
+ .map((condition) => ({
319
+ field: condition.field,
320
+ component: condition.component
321
+ })),
322
+ buttonAliases,
323
+ deletePolicy: deletePolicyInference.value,
324
+ customWorkflows: customWorkflows.map((workflow) => ({
325
+ label: workflow.label,
326
+ actionAlias: workflow.actionAlias,
327
+ expectedIntent: workflow.expectedIntent,
328
+ expectedAssertion: workflow.expectedAssertion,
329
+ include: workflow.resolved,
330
+ excludeReason: workflow.resolved ? undefined : '未找到对应断言证据'
331
+ }))
332
+ };
333
+ // 组装 unresolved
334
+ const unresolved = [];
335
+ if (!dataKeyInference.selectedField) {
336
+ unresolved.push('dataKey.selectedField');
337
+ }
338
+ for (const condition of searchConditionInferences) {
339
+ if (!condition.resolved) {
340
+ unresolved.push(`searchConditions.${condition.field}`);
341
+ }
342
+ }
343
+ for (const workflow of customWorkflows) {
344
+ if (!workflow.resolved) {
345
+ unresolved.push(`customWorkflows.${workflow.label}`);
346
+ }
347
+ }
348
+ // 合并显式 hints(若有)
349
+ const mode = input.explicitHints ? 'merged' : 'inferred';
350
+ const confidence = unresolved.length === 0 ? 'high' : unresolved.length <= 2 ? 'medium' : 'low';
351
+ const inference = {
352
+ mode,
353
+ confidence,
354
+ dataKey: dataKeyInference,
355
+ searchConditions: searchConditionInferences,
356
+ buttonAliases,
357
+ deletePolicy: deletePolicyInference,
358
+ assertionPolicy: {},
359
+ customWorkflows,
360
+ unresolved
361
+ };
362
+ return { hints, inference };
363
+ }
@@ -0,0 +1,26 @@
1
+ import type { ExtractedCodeListSummary, ExtractedHtmlPage, ExtractedJavaAction } from '../index.js';
2
+ import type { SourceAnalysis } from '../contracts/inference-trace.js';
3
+ /**
4
+ * buildSourceAnalysis 的输入参数。
5
+ */
6
+ export type BuildSourceAnalysisInput = {
7
+ codeListPath: string;
8
+ docsDir: string;
9
+ codeList: ExtractedCodeListSummary;
10
+ pages: ExtractedHtmlPage[];
11
+ actions: ExtractedJavaAction[];
12
+ };
13
+ /**
14
+ * 构建 sourceAnalysis 章节。
15
+ *
16
+ * 实现规则:
17
+ * - docsDir 下存在 PRD markdown(文件名含 prd 或正文含 `## 4. 功能需求`)时登记 prd_markdown source。
18
+ * - HTML 页面登记为 uiSignals(字段、按钮、表格列、弹窗)。
19
+ * - Java Action 登记为 backendSignals(方法名、callback 消息)。
20
+ * - 无 spec.yaml 时 mode='prd+html+java';有 spec.yaml 时 mode='extracted'。
21
+ *
22
+ * @param input - 输入参数(codeListPath、docsDir、codeList、pages、actions)。
23
+ * @returns 源分析快照。
24
+ */
25
+ export declare function buildSourceAnalysis(input: BuildSourceAnalysisInput): SourceAnalysis;
26
+ //# sourceMappingURL=source-analysis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-analysis.d.ts","sourceRoot":"","sources":["../../../src/inference/source-analysis.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,wBAAwB,EACxB,iBAAiB,EACjB,mBAAmB,EACpB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAEV,cAAc,EAGf,MAAM,iCAAiC,CAAC;AAEzC;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,wBAAwB,CAAC;IACnC,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,OAAO,EAAE,mBAAmB,EAAE,CAAC;CAChC,CAAC;AAgLF;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,cAAc,CAkFnF"}
@@ -0,0 +1,258 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * 判断 docsDir 下是否存在 PRD markdown 文件。
5
+ *
6
+ * PRD 判定规则(满足任一):
7
+ * - 文件名包含 `prd`(大小写不敏感)。
8
+ * - 文件正文包含 `## 4. 功能需求` 标题。
9
+ *
10
+ * @param docsDir - 需求文档目录。
11
+ * @returns PRD 文件绝对路径;不存在时返回 undefined。
12
+ */
13
+ function findPrdMarkdown(docsDir) {
14
+ if (!existsSync(docsDir)) {
15
+ return undefined;
16
+ }
17
+ const candidates = readdirSync(docsDir).filter((file) => file.endsWith('.md'));
18
+ for (const file of candidates) {
19
+ const fullPath = path.join(docsDir, file);
20
+ if (/prd/i.test(file)) {
21
+ return fullPath;
22
+ }
23
+ }
24
+ // 文件名不含 prd 时回落到正文匹配。
25
+ for (const file of candidates) {
26
+ const fullPath = path.join(docsDir, file);
27
+ const content = readFileSync(fullPath, 'utf8');
28
+ if (/##\s*4\.\s*功能需求/.test(content)) {
29
+ return fullPath;
30
+ }
31
+ }
32
+ return undefined;
33
+ }
34
+ /**
35
+ * 判断 docsDir 或 code_list 物料引用中是否存在 spec.yaml。
36
+ *
37
+ * @param codeList - code_list 摘要。
38
+ * @param docsDir - 需求文档目录。
39
+ * @returns spec.yaml 绝对路径;不存在时返回 undefined。
40
+ */
41
+ function findSpecYaml(codeList, docsDir) {
42
+ const fromRefs = codeList.materialRefs.find((ref) => ref.kind === 'spec' && ref.exists);
43
+ if (fromRefs) {
44
+ return fromRefs.path;
45
+ }
46
+ if (existsSync(docsDir)) {
47
+ const direct = readdirSync(docsDir).find((file) => file.endsWith('spec.yaml'));
48
+ if (direct) {
49
+ return path.join(docsDir, direct);
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+ /**
55
+ * 把 HTML 页面字段登记为 UI 信号。
56
+ *
57
+ * @param pages - 已抽取的 HTML 页面列表。
58
+ * @returns UI 信号数组(字段、按钮、表格列、弹窗)。
59
+ */
60
+ function collectUiSignals(pages) {
61
+ const signals = [];
62
+ for (const page of pages) {
63
+ for (const field of page.fields) {
64
+ signals.push({
65
+ pageId: page.pageId,
66
+ kind: 'field',
67
+ label: field.label,
68
+ value: field.name
69
+ });
70
+ }
71
+ for (const button of page.buttons) {
72
+ signals.push({
73
+ pageId: page.pageId,
74
+ kind: 'button',
75
+ label: button.label
76
+ });
77
+ }
78
+ for (const grid of page.grids) {
79
+ for (const column of grid.dataColumns) {
80
+ signals.push({
81
+ pageId: page.pageId,
82
+ kind: 'gridColumn',
83
+ label: column.headerText || column.field,
84
+ value: column.field
85
+ });
86
+ }
87
+ }
88
+ for (const dialog of page.dialogs) {
89
+ signals.push({
90
+ pageId: page.pageId,
91
+ kind: 'dialog',
92
+ label: dialog.title,
93
+ value: dialog.pagePath
94
+ });
95
+ }
96
+ }
97
+ return signals;
98
+ }
99
+ /**
100
+ * 把 Java Action 登记为后端信号。
101
+ *
102
+ * @param actions - 已抽取的 Java Action 列表。
103
+ * @returns 后端信号数组(方法名、callback 消息)。
104
+ */
105
+ function collectBackendSignals(actions) {
106
+ const signals = [];
107
+ for (const action of actions) {
108
+ for (const method of action.methods) {
109
+ signals.push({
110
+ controllerName: action.controllerName,
111
+ kind: 'method',
112
+ value: method
113
+ });
114
+ }
115
+ for (const message of action.messages) {
116
+ signals.push({
117
+ controllerName: action.controllerName,
118
+ kind: 'callbackMessage',
119
+ value: message
120
+ });
121
+ }
122
+ }
123
+ return signals;
124
+ }
125
+ /**
126
+ * 跨页面汇总字段名候选(去重)。
127
+ *
128
+ * @param pages - 已抽取的 HTML 页面列表。
129
+ * @returns 去重后的字段名数组。
130
+ */
131
+ function collectFieldNames(pages) {
132
+ const names = new Set();
133
+ for (const page of pages) {
134
+ for (const field of page.fields) {
135
+ if (field.name) {
136
+ names.add(field.name);
137
+ }
138
+ }
139
+ for (const grid of page.grids) {
140
+ for (const column of grid.dataColumns) {
141
+ if (column.field) {
142
+ names.add(column.field);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ return Array.from(names);
148
+ }
149
+ /**
150
+ * 从 Java callback 消息中提取业务规则线索。
151
+ *
152
+ * @param actions - 已抽取的 Java Action 列表。
153
+ * @returns 业务规则描述数组。
154
+ */
155
+ function collectBusinessRules(actions) {
156
+ const rules = [];
157
+ for (const action of actions) {
158
+ for (const message of action.messages) {
159
+ if (/不可删除|不允许删除|禁止删除/.test(message)) {
160
+ rules.push(`删除受业务规则限制:${message}`);
161
+ }
162
+ }
163
+ }
164
+ return Array.from(new Set(rules));
165
+ }
166
+ /**
167
+ * 构建 sourceAnalysis 章节。
168
+ *
169
+ * 实现规则:
170
+ * - docsDir 下存在 PRD markdown(文件名含 prd 或正文含 `## 4. 功能需求`)时登记 prd_markdown source。
171
+ * - HTML 页面登记为 uiSignals(字段、按钮、表格列、弹窗)。
172
+ * - Java Action 登记为 backendSignals(方法名、callback 消息)。
173
+ * - 无 spec.yaml 时 mode='prd+html+java';有 spec.yaml 时 mode='extracted'。
174
+ *
175
+ * @param input - 输入参数(codeListPath、docsDir、codeList、pages、actions)。
176
+ * @returns 源分析快照。
177
+ */
178
+ export function buildSourceAnalysis(input) {
179
+ const sources = [];
180
+ const unresolved = [];
181
+ const prdPath = findPrdMarkdown(input.docsDir);
182
+ if (prdPath) {
183
+ sources.push({
184
+ kind: 'prd_markdown',
185
+ path: prdPath,
186
+ provides: ['businessRules', 'featureList', 'fieldDescriptions']
187
+ });
188
+ }
189
+ else {
190
+ unresolved.push('docsDir 下未找到 PRD markdown');
191
+ }
192
+ const specYamlPath = findSpecYaml(input.codeList, input.docsDir);
193
+ if (specYamlPath) {
194
+ sources.push({
195
+ kind: 'spec_yaml',
196
+ path: specYamlPath,
197
+ provides: ['contractSlots', 'fieldContracts', 'flowContracts']
198
+ });
199
+ }
200
+ for (const page of input.pages) {
201
+ sources.push({
202
+ kind: 'html',
203
+ path: page.path,
204
+ provides: ['searchFields', 'gridColumns', 'buttons', 'dialogs']
205
+ });
206
+ }
207
+ for (const action of input.actions) {
208
+ sources.push({
209
+ kind: 'java_action',
210
+ path: action.path,
211
+ provides: ['backendMethods', 'callbackMessages', 'deletePolicyEvidence']
212
+ });
213
+ }
214
+ const uiSignals = collectUiSignals(input.pages);
215
+ const backendSignals = collectBackendSignals(input.actions);
216
+ const fields = collectFieldNames(input.pages);
217
+ const businessRules = collectBusinessRules(input.actions);
218
+ const hasPrd = Boolean(prdPath);
219
+ const hasHtml = input.pages.length > 0;
220
+ const hasJava = input.actions.length > 0;
221
+ const hasSpec = Boolean(specYamlPath);
222
+ let mode;
223
+ if (hasSpec) {
224
+ mode = 'extracted';
225
+ }
226
+ else if (hasPrd && hasHtml && hasJava) {
227
+ mode = 'prd+html+java';
228
+ }
229
+ else if (hasHtml || hasJava) {
230
+ mode = 'inferred';
231
+ }
232
+ else {
233
+ mode = 'mixed';
234
+ }
235
+ let confidence;
236
+ if (hasSpec) {
237
+ confidence = 'high';
238
+ }
239
+ else if (hasPrd && hasHtml && hasJava) {
240
+ confidence = 'medium';
241
+ }
242
+ else if (hasHtml || hasJava) {
243
+ confidence = 'medium';
244
+ }
245
+ else {
246
+ confidence = 'low';
247
+ }
248
+ return {
249
+ mode,
250
+ confidence,
251
+ sources,
252
+ fields,
253
+ businessRules,
254
+ uiSignals,
255
+ backendSignals,
256
+ unresolved
257
+ };
258
+ }
@@ -1,5 +1,6 @@
1
1
  export { validateProjectsIndex } from './projects-index.js';
2
2
  export { validateTestcase } from './testcase.js';
3
3
  export { validateCredentials } from './credentials.js';
4
+ export { validateInferenceTrace } from './inference-trace.js';
4
5
  export type { Credential } from './credentials.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/validation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/validation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
@@ -1,3 +1,4 @@
1
1
  export { validateProjectsIndex } from './projects-index.js';
2
2
  export { validateTestcase } from './testcase.js';
3
3
  export { validateCredentials } from './credentials.js';
4
+ export { validateInferenceTrace } from './inference-trace.js';
@@ -0,0 +1,13 @@
1
+ import type { InferenceTraceV1 } from '../contracts/inference-trace.js';
2
+ /**
3
+ * 校验 inference-trace.json 的顶层结构(REQ-INFER-006)。
4
+ *
5
+ * 最小校验:只保证 schemaVersion 正确且 5 个必需顶层章节存在。
6
+ * 章节内部证据数组 / reason 允许扩展(spec §8),不做深校验。
7
+ *
8
+ * @param obj - 从 JSON.parse 得到的任意值。
9
+ * @returns 类型收窄后的 InferenceTraceV1。
10
+ * @throws 顶层非对象、schemaVersion 不符或缺章节时抛错。
11
+ */
12
+ export declare function validateInferenceTrace(obj: unknown): InferenceTraceV1;
13
+ //# sourceMappingURL=inference-trace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inference-trace.d.ts","sourceRoot":"","sources":["../../../src/validation/inference-trace.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAExE;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,CAoBrE"}