@epoint-testtech/ep-stage-skill 0.0.10 → 0.0.12

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 (46) hide show
  1. package/codex-skill/ep-stage/code-list-generate/SKILL.md +168 -0
  2. package/codex-skill/ep-stage/code-list-generate/assets/code_list.template.json5 +325 -0
  3. package/codex-skill/ep-stage/code-list-generate/assets/example-zwplace.json5 +588 -0
  4. package/codex-skill/ep-stage/code-list-generate/evals/evals.json +47 -0
  5. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/action/SchemeResourceAddAction.java +119 -0
  6. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/action/SchemeResourceDetailAction.java +70 -0
  7. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/action/SchemeResourceEditAction.java +107 -0
  8. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/action/SchemeResourceListAction.java +167 -0
  9. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/api/ISchemeResourceService.java +84 -0
  10. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/api/entity/SchemeResource.java +218 -0
  11. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/impl/SchemeResourceService.java +146 -0
  12. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/impl/SchemeResourceServiceImpl.java +110 -0
  13. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/webapp/perpage/schemeresource/schemeresourceadd.html +158 -0
  14. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/webapp/perpage/schemeresource/schemeresourcedetail.html +89 -0
  15. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/webapp/perpage/schemeresource/schemeresourceedit.html +125 -0
  16. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/webapp/perpage/schemeresource/schemeresourcelist.html +224 -0
  17. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/docs//345/234/272/346/211/200/347/252/227/345/217/243/344/277/241/346/201/257/347/256/241/347/220/206/345/217/212/345/220/204/347/253/257/347/273/237/350/256/241/346/243/200/347/264/242/346/237/245/347/234/213_prd.md +602 -0
  18. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/action/GxhZwPlaceAddAction.java +210 -0
  19. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/action/GxhZwPlaceDetailAction.java +69 -0
  20. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/action/GxhZwPlaceEditAction.java +235 -0
  21. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/action/GxhZwPlaceListAction.java +145 -0
  22. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/api/IGxhZwPlaceService.java +87 -0
  23. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/api/entity/GxhZwPlace.java +306 -0
  24. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/api/entity/GxhZwPlaceHistory.java +205 -0
  25. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/api/entity/GxhZwWindow.java +184 -0
  26. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/impl/GxhZwPlaceService.java +151 -0
  27. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/impl/GxhZwPlaceServiceImpl.java +96 -0
  28. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/webapp/perpage/zwplace/gxhzwplaceadd.html +253 -0
  29. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/webapp/perpage/zwplace/gxhzwplacedetail.html +146 -0
  30. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/webapp/perpage/zwplace/gxhzwplaceedit.html +275 -0
  31. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/webapp/perpage/zwplace/gxhzwplacelist.html +168 -0
  32. package/codex-skill/ep-stage/code-list-generate/evals/grade-code-list.mjs +184 -0
  33. package/codex-skill/ep-stage/code-list-generate/references/epoint-extraction.md +106 -0
  34. package/codex-skill/ep-stage/code-list-generate/references/field-contract.md +122 -0
  35. package/codex-skill/ep-stage/code-list-generate/scripts/validate-code-list.mjs +202 -0
  36. package/codex-skill/ep-stage/glue-create-project/SKILL.md +227 -223
  37. package/dist/src/cli/index.js +0 -0
  38. package/dist/src/extractors/code-list-json5.d.ts +5 -0
  39. package/dist/src/extractors/code-list-json5.d.ts.map +1 -1
  40. package/dist/src/generators/stage-skeleton-script.d.ts +4 -0
  41. package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
  42. package/dist/src/generators/stage-skeleton-script.js +389 -362
  43. package/dist/src/testcase/contract-testcase-assembler.d.ts +10 -1
  44. package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -1
  45. package/dist/src/testcase/contract-testcase-assembler.js +80 -21
  46. package/package.json +14 -12
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * code-list-generate eval 断言评分脚本
4
+ *
5
+ * 用法:node grade-code-list.mjs <code_list.json5路径> <evalId:0|1> <grading.json输出路径>
6
+ *
7
+ * 产出 grading.json:{ expectations: [{ text, passed, evidence }] }(viewer 依赖这三个字段名)
8
+ * 只评可程序化断言;qualitative 断言由人工/grader 补充合并。
9
+ */
10
+
11
+ import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs';
12
+ import path from 'node:path';
13
+ import { fileURLToPath, pathToFileURL } from 'node:url';
14
+
15
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
16
+
17
+ function findJson5Entry() {
18
+ let dir = scriptDir;
19
+ while (true) {
20
+ const plain = path.join(dir, 'node_modules', 'json5', 'lib', 'index.js');
21
+ if (existsSync(plain)) return plain;
22
+ const pnpmStore = path.join(dir, 'node_modules', '.pnpm');
23
+ if (existsSync(pnpmStore)) {
24
+ const hit = readdirSync(pnpmStore).find((d) => d.startsWith('json5@'));
25
+ if (hit) {
26
+ const p = path.join(pnpmStore, hit, 'node_modules', 'json5', 'lib', 'index.js');
27
+ if (existsSync(p)) return p;
28
+ }
29
+ }
30
+ const parent = path.dirname(dir);
31
+ if (parent === dir) return null;
32
+ dir = parent;
33
+ }
34
+ }
35
+
36
+ let JSON5;
37
+ try {
38
+ ({ default: JSON5 } = await import('json5'));
39
+ } catch {
40
+ ({ default: JSON5 } = await import(pathToFileURL(findJson5Entry()).href));
41
+ }
42
+
43
+ const [codeListPath, evalId, outPath] = process.argv.slice(2);
44
+ const expectations = [];
45
+ const check = (text, passed, evidence) => expectations.push({ text, passed: Boolean(passed), evidence: String(evidence ?? '') });
46
+
47
+ /* ──────── 解析 ──────── */
48
+ let data = null;
49
+ if (!existsSync(codeListPath)) {
50
+ check('产物文件存在且 JSON5 可解析', false, `文件不存在:${codeListPath}`);
51
+ } else {
52
+ try {
53
+ data = JSON5.parse(readFileSync(codeListPath, 'utf-8'));
54
+ check('产物文件存在且 JSON5 可解析', true, '解析成功');
55
+ } catch (e) {
56
+ check('产物文件存在且 JSON5 可解析', false, `解析失败:${e.message}`);
57
+ }
58
+ }
59
+
60
+ if (data && typeof data === 'object') { /* ──────── 通用断言 ──────── */
61
+ const missing = ['module', 'moduleName', 'systems', 'pages'].filter((k) => !data[k]);
62
+ check('四大必填字段齐全(module/moduleName/systems/pages)', missing.length === 0, missing.length ? `缺:${missing.join(',')}` : '齐全');
63
+
64
+ const expectedModule = evalId === '0' ? 'zwplace' : 'schemeresource';
65
+ check(`module 标识正确(${expectedModule})`, data.module === expectedModule, `实际:${data.module}`);
66
+
67
+ const roles = new Set((data.pages ?? []).map((p) => p?.role));
68
+ const needRoles = ['list', 'add', 'edit', 'detail'];
69
+ check('pages 覆盖 list/add/edit/detail 四角色', needRoles.every((r) => roles.has(r)), `实际角色:${[...roles].join(',') || '无'}`);
70
+
71
+ const listPage = (data.pages ?? []).find((p) => p?.role === 'list');
72
+ const listActions = new Set((listPage?.buttons ?? []).map((b) => b?.action));
73
+
74
+ if (evalId === '0') {
75
+ const hasCore = ['create', 'delete', 'export'].every((a) => listActions.has(a));
76
+ const syncBtn = (listPage?.buttons ?? []).find((b) => /同步/.test(b?.label ?? ''));
77
+ check('list 页按钮分类:含 create/delete/export', hasCore, `实际 action:${[...listActions].join(',') || '无'}`);
78
+ check('「同步窗口管理系统」分类为 custom', syncBtn?.action === 'custom', `实际:${syncBtn ? syncBtn.action : '未找到该按钮'}`);
79
+
80
+ const qf = listPage?.queryFields ?? [];
81
+ const byLabel = (s) => qf.find((f) => (f?.label ?? '').includes(s));
82
+ const isInput = (f) => ['mini-textbox', 'input'].includes(f?.component ?? f?.type ?? '');
83
+ const isListbox = (f) => ['mini-combobox', 'listbox'].includes(f?.component ?? f?.type ?? '');
84
+ check(
85
+ 'queryFields ≥3 且输入框/下拉区分正确(场所名称=textbox;场所分类/状态=combobox)',
86
+ qf.length >= 3 && isInput(byLabel('场所名称')) && isListbox(byLabel('场所分类')) && isListbox(byLabel('状态')),
87
+ `实际:${qf.map((f) => `${f?.label}=${f?.component ?? f?.type ?? '?'}`).join(',') || '无 queryFields'}`,
88
+ );
89
+
90
+ const addPage = (data.pages ?? []).find((p) => p?.role === 'add');
91
+ const grids = addPage?.childGrids ?? [];
92
+ const winGrid = grids.find?.((g) => JSON.stringify(g).includes('window'));
93
+ check(
94
+ 'add 页含窗口信息子表(≥2 列且含 windowname 字段)',
95
+ Boolean(winGrid) && (winGrid.columns ?? []).length >= 2 && JSON.stringify(winGrid.columns).includes('windowname'),
96
+ winGrid ? `子表列:${(winGrid.columns ?? []).map((c) => c?.field).join(',')}` : '未找到子表',
97
+ );
98
+
99
+ const deleteRule = (data.businessRules ?? []).find((r) => (r?.scope ?? []).includes('delete'));
100
+ const hint = deleteRule?.assertionHint ?? deleteRule?.assertion ?? deleteRule?.description ?? '';
101
+ check('businessRules 含删除阻断规则且提示含「不可删除」', Boolean(deleteRule) && hint.includes('不可删除'), deleteRule ? `hint:${hint}` : '未找到 delete 规则');
102
+
103
+ const tables = (data.entities ?? []).map((e) => e?.table);
104
+ check('entities 覆盖 gxh_zw_place 与 gxh_zw_window', ['gxh_zw_place', 'gxh_zw_window'].every((t) => tables.includes(t)), `实际:${tables.join(',') || '无'}`);
105
+
106
+ const kinds = new Set((data.dataModels ?? []).map((m) => m?.sourceKind));
107
+ check('dataModels 区分 dictionary_runtime 与 static_options', kinds.has('dictionary_runtime') && kinds.has('static_options'), `实际 sourceKind:${[...kinds].join(',') || '无 dataModels'}`);
108
+
109
+ const editPage = (data.pages ?? []).find((p) => p?.role === 'edit');
110
+ const saveClose = (editPage?.buttons ?? []).find((b) => (b?.label ?? '').includes('保存并关闭'));
111
+ check('edit 页「保存并关闭」action=update 且 backendMethod=save', saveClose?.action === 'update' && saveClose?.backendMethod === 'save', saveClose ? `action=${saveClose.action}, backendMethod=${saveClose.backendMethod}` : '未找到该按钮');
112
+ }
113
+
114
+ if (evalId === '1') {
115
+ check('list 页按钮含 create/delete/export 分类', ['create', 'delete', 'export'].every((a) => listActions.has(a)), `实际 action:${[...listActions].join(',') || '无'}`);
116
+ const qf = listPage?.queryFields ?? [];
117
+ const labels = qf.map((f) => f?.label ?? '');
118
+ check('list 页 queryFields ≥2 且 label 来自源码(名称/类型/简介)', qf.length >= 2 && ['名称', '类型'].every((s) => labels.some((l) => l.includes(s))), `实际:${labels.join(',') || '无 queryFields'}`);
119
+ }
120
+
121
+ /* sourceRoot 与路径解析 */
122
+ const root = data.sourceRoot?.root;
123
+ if (!root) {
124
+ check('sourceRoot.root 提供且 pages[].path 全部可解析', false, '未提供 sourceRoot.root');
125
+ } else if (!existsSync(path.resolve(root))) {
126
+ check('sourceRoot.root 提供且 pages[].path 全部可解析', false, `root 不存在:${root}`);
127
+ } else {
128
+ const bad = (data.pages ?? []).filter((p) => p?.path && !existsSync(path.resolve(root, p.path))).map((p) => p.path);
129
+ check('sourceRoot.root 提供且 pages[].path 全部可解析', bad.length === 0, bad.length ? `不可解析:${bad.join(',')}` : '全部可解析');
130
+ }
131
+
132
+ /* 凭据禁区 */
133
+ const secrets = [];
134
+ (function scan(obj, trail) {
135
+ if (!obj || typeof obj !== 'object') return;
136
+ for (const [k, v] of Object.entries(obj)) {
137
+ if (/password|passwd|secret|token/i.test(k) && typeof v === 'string' && v.trim()) secrets.push(trail ? `${trail}.${k}` : k);
138
+ scan(v, trail ? `${trail}.${k}` : k);
139
+ }
140
+ })(data, '');
141
+ check('无明文凭据(password 类字段全部留空)', secrets.length === 0, secrets.length ? `泄漏:${secrets.join(',')}` : '无');
142
+
143
+ /* evidence 溯源 */
144
+ const hasEvidence =
145
+ (data.pages ?? []).some((p) => Array.isArray(p?.evidence) && p.evidence.length) ||
146
+ (data.businessRules ?? []).some((r) => Array.isArray(r?.evidence) && r.evidence.length);
147
+ check('关键事实带 evidence 溯源(pages 或 businessRules)', hasEvidence, hasEvidence ? '有 evidence' : '全文无 evidence');
148
+ } else {
149
+ // 解析失败:本 eval 其余程序化断言全部判不通过(产物不可消费,无法评估)
150
+ const rest =
151
+ evalId === '0'
152
+ ? [
153
+ '四大必填字段齐全(module/moduleName/systems/pages)',
154
+ 'module 标识正确(zwplace)',
155
+ 'pages 覆盖 list/add/edit/detail 四角色',
156
+ 'list 页按钮分类:含 create/delete/export',
157
+ '「同步窗口管理系统」分类为 custom',
158
+ 'queryFields ≥3 且输入框/下拉区分正确(场所名称=textbox;场所分类/状态=combobox)',
159
+ 'add 页含窗口信息子表(≥2 列且含 windowname 字段)',
160
+ 'businessRules 含删除阻断规则且提示含「不可删除」',
161
+ 'entities 覆盖 gxh_zw_place 与 gxh_zw_window',
162
+ 'dataModels 区分 dictionary_runtime 与 static_options',
163
+ 'edit 页「保存并关闭」action=update 且 backendMethod=save',
164
+ 'sourceRoot.root 提供且 pages[].path 全部可解析',
165
+ '无明文凭据(password 类字段全部留空)',
166
+ '关键事实带 evidence 溯源(pages 或 businessRules)',
167
+ ]
168
+ : [
169
+ '四大必填字段齐全(module/moduleName/systems/pages)',
170
+ 'module 标识正确(schemeresource)',
171
+ 'pages 覆盖 list/add/edit/detail 四角色',
172
+ 'list 页按钮含 create/delete/export 分类',
173
+ 'list 页 queryFields ≥2 且 label 来自源码(名称/类型/简介)',
174
+ 'sourceRoot.root 提供且 pages[].path 全部可解析',
175
+ '无明文凭据(password 类字段全部留空)',
176
+ '关键事实带 evidence 溯源(pages 或 businessRules)',
177
+ ];
178
+ for (const text of rest) check(text, false, '产物不可解析,无法评估');
179
+ }
180
+
181
+ mkdirSync(path.dirname(outPath), { recursive: true });
182
+ writeFileSync(outPath, JSON.stringify({ expectations }, null, 2), 'utf-8');
183
+ const passed = expectations.filter((e) => e.passed).length;
184
+ console.log(`${passed}/${expectations.length} 通过 → ${outPath}`);
@@ -0,0 +1,106 @@
1
+ # epoint 系源码提取方法
2
+
3
+ 本文件说明每类 code_list 事实在 epoint 系源码(MiniUI 前端 + Java Action/Service/Entity 后端)里的位置和抠取方法。提取时始终记住:你是给下游测试流程写「事实清单」,每个结论都要能在源码里指出来处。
4
+
5
+ ## 模块定位
6
+
7
+ - 前端:`src/main/webapp/perpage/<module>/*.html`
8
+ - 后端:`src/main/java/com/epoint/<module>/`,典型分层 `action/`(*Action.java)、`api/`(接口 + entity/)、`impl/`(Service 实现 + DAO)
9
+
10
+ 模块标识 `module` 取 perpage 目录名 / java 包名(通常一致,如 `zwplace`)。
11
+
12
+ ## 页面四角色(pages[].role)
13
+
14
+ 按文件名与页面行为分类:
15
+
16
+ | role | 文件名特征 | 行为特征 |
17
+ |---|---|---|
18
+ | `list` | `*list.html` | 含 mini-datagrid + 查询区 + 工具栏 |
19
+ | `add` | `*add.html` | 空表单,保存走 `add` 方法 |
20
+ | `edit` | `*edit.html` | 带 guid 加载(pageLoad),保存走 `save` 方法 |
21
+ | `detail` | `*detail.html` | 只读,无保存按钮 |
22
+
23
+ `pageId` 取文件名去 `.html`(如 `gxhzwplacelist`);`initAction` 取页面对应 Action 的 restController 名(见下)。
24
+
25
+ ## 按钮(buttons[] / toolbarActions[])
26
+
27
+ 在 HTML 里找这两类来源:
28
+
29
+ 1. **mini-button**:`<a class="mini-button" ...>按钮文字</a>`,看 `onclick` 调的 JS 函数(`actionName`),再追该函数里 Ajax 调的 Action 方法(`backendMethod`)
30
+ 2. **mini-dataexport**:导出组件(`<div id="dataexport" ...>` 或类似),固定归类 `action: 'export'`,`backendMethod` 一般是 `getExportModel`
31
+
32
+ 行操作在 datagrid 列定义里找 renderer(如 `actionRenderer`)生成的链接/图标,常见「修改」「查看」,对应 JS 函数 `openEdit` / `openDetail`。
33
+
34
+ **action 语义分类判断顺序**:导出组件 → `export`;调删除方法(deleteSelect 等)→ `delete`;打开新增页 → `create`;新增页提交到 `add` 方法 → `create`;编辑页提交到 `save` 方法 → `update`;草稿保存 → `save`(补 customAction);关闭 → `cancel`;其余业务按钮 → `custom`(必须补 `customAction` + `backendMethod`)。
35
+
36
+ ## 查询字段(queryFields[])
37
+
38
+ 列表页查询区的表单控件:
39
+
40
+ - `mini-textbox` → 输入框。`label` 取控件旁的中文标签或控件自身 `label` 属性;`bind` 取 `name`/`bind` 属性里的 `dataBean.xxx`
41
+ - `mini-combobox` → 下拉。额外记 `modelRef`:看 `action`/`url` 属性指向的 Action 方法(如 `getPlacecategoryModel` → ref 取名 `placecategoryModel`),或 `dataOptions` 里的字典 code
42
+
43
+ ## 表单字段(formFields[])
44
+
45
+ 新增/编辑页表单区控件,同 queryFields 的抠法,另外从控件属性抠 `required: true`(`required="true"`)和 `maxLength`。
46
+
47
+ edit 页与 add 页字段完全相同时,写 `formFields: ['same_as_pages.add.formFields']` 引用,避免重复维护——下游会解析这个引用。
48
+
49
+ ## 子表(childGrids[])
50
+
51
+ 表单页内嵌的 mini-datagrid(如窗口信息子表):
52
+
53
+ - `id`:grid 的 DOM id(如 `windowGrid`)
54
+ - `columns[]`:列定义的 `field` / 表头中文 `label`;列带编辑器且必填标 `required: true`;下拉列标 `modelRef`
55
+ - `component`:**只写 `input` 或 `listbox`**(文本编辑器列 / 下拉编辑器列),这不是 MiniUI 组件名——写 `mini-textbox` 之类会被下游契约门禁直接拒绝(有实锤)。拿不准就整列省略 component,下游会读 HTML 的 editor.type 自行推导
56
+ - `actions[]`:子表工具条按钮(添加窗口/删除选中 → actionName 取 JS 函数名)
57
+ - `editMode` 可省(下游从 HTML 信号推断);推断不对时用 `cell_edit` / `row_button_dialog` / `none` 覆盖
58
+
59
+ ## 系统名与模块名(取源纪律)
60
+
61
+ - **系统名**(`systems[].name`):最终用户登录系统的正式中文全称。找它的顺序:PRD/需求文档里的业务系统名 → 运行系统登录页/标题 → plan/project_context 的 products[].name。**警惕产品/项目代号**(如「综管」这类内部简称)——它不是系统名,实测中被误取过。只有代号可用时填代号,并在 downstreamRuntimeNeeds 登记待确认。
62
+ - **模块名**(`moduleName`):需求/PRD 里的模块业务名。不要用单个页面的 `<title>`(那是页面名,进 `pages[].title`)。
63
+
64
+ ## 落盘位置(下游需求名推导契约)
65
+
66
+ 产物必须写到 `<需求名目录>/_docs/code_list.json5`。下游按「code_list.json5 父目录的父目录的 basename」推导需求名——父目录必须是 `_docs`。**`_docs/<需求名>/code_list.json5` 这类布局是错的**(需求名会被算成 `_docs`)。
67
+
68
+ ## 数据模型(dataModels[] 的 sourceKind 判断)
69
+
70
+ | 情况 | sourceKind |
71
+ |---|---|
72
+ | combobox 数据来自代码项/字典(`dataOptions: "{code:'场所分类'}"` 或 Action 方法读代码项表) | `dictionary_runtime`,记 `dictionaryCode`,标 `optionsKnownAtStaticTime: false` |
73
+ | 选项写死在 HTML/JS 里(如是/否、固定区划列表) | `static_options`,`options[]` 全量列出 |
74
+ | 来自业务库表查询(如按区划查街道) | `db_runtime`,记 `dependency`(级联字段) |
75
+
76
+ **判断要点**:找到给 combobox 供数的 Action 方法,读它的实现——查代码项表是 dictionary_runtime;return 字面量数组是 static_options;查业务表是 db_runtime。选项值运行态才知道时,绝不把猜的选项写进 `options`。
77
+
78
+ ## 业务规则(businessRules[])
79
+
80
+ 在 Action 方法里找这些信号:
81
+
82
+ - **校验/阻断消息**:`throw new ...Exception("...")`、`return "...不可..."`、前端 `alert("...")`。每条消息就是一条规则,`assertionHint` 填消息原文
83
+ - **状态约束**:如删除前判断 `status != DRAFT` → `scope: ['delete']` 规则(这条对下游删除用例断言起决定作用,务必抠全)
84
+ - **唯一性校验**:`checkXxxUnique` 类方法 → `scope: ['create', 'update']`
85
+ - **副作用事实**:如"保存并关闭时版本号+1、归档历史" → `scope: ['update']` 规则
86
+
87
+ 前端 JS 里的校验(如子表行必填 `"第N行窗口名称不能为空!"`)同样是规则,scope 带上子表 id。
88
+
89
+ ## 实体(entities[])
90
+
91
+ `api/entity/*.java`:类名 → `name`;类/注解上的表名 → `table`;字段 → `fields[]`(name/类型+长度/中文注释 label)。业务主键(列表页按它查询定位数据、唯一性校验针对它)标 `dataKey: true`。
92
+
93
+ ## requirement 与对照缺口
94
+
95
+ 从 PRD/需求文档提取:`id/name/type/priority/targetEnds/businessRoles[]`(角色配 `label/targetEnd/responsibilities`,evidence 指 PRD 章节)。
96
+
97
+ 然后做**双向对照**:
98
+
99
+ - `implementedScope[]`:源码里真实存在的功能点(有页面/方法支撑),`implementedBy` 指到具体页面和后端方法
100
+ - `prdMentionedButNotImplemented[]`:PRD 写了但源码找不到对应页面/实现的,`reason` 写明"未在 sourceFiles 中发现对应文件"。只能写静态扫描可证实的缺口,不揣测
101
+
102
+ ## evidence 写法
103
+
104
+ - 源码:`相对源码根路径#方法或锚点`,如 `src/main/java/com/epoint/zwplace/action/GxhZwPlaceListAction.java#deleteSelect`
105
+ - 需求文档:`文件名#章节号`,如 `场所窗口信息管理及各端统计检索查看_prd.md#2.1 核心用户角色`
106
+ - 内部引用:`sourceFiles.frontend[role=list]` 形式
@@ -0,0 +1,122 @@
1
+ # code_list.json5 逐字段契约
2
+
3
+ 本文档是 `code_list.json5`(schemaVersion: `ep-stage-code-list/v1-draft`)的完整字段说明。每个字段标注:**必填性**、**下游消费点**(下游 CLI 拿它做什么)。标注「下游硬消费」的字段缺失或错误会直接让用例生成缺斤短两;标注「事实记录」的字段主要供人审与溯源。
4
+
5
+ 校验权威:下游硬校验只看 `module` / `moduleName` / `systems[]` / `pages[]` 四项存在性(`extractors/code-list-json5.ts`),其余靠本契约自律。
6
+
7
+ ## 顶层字段
8
+
9
+ | 字段 | 必填 | 消费点 | 说明 |
10
+ |---|---|---|---|
11
+ | `schemaVersion` | 推荐 | 事实记录 | 固定 `ep-stage-code-list/v1-draft` |
12
+ | `module` | **是** | 下游硬消费 | 模块英文标识(kebab/lower-case),作 moduleId、用例 ID 前缀 |
13
+ | `moduleName` | **是** | 下游硬消费 | 模块中文**业务名**(取需求/PRD 的模块名,如"场所窗口信息管理"),不取单页 title——页面 title 进 `pages[].title` |
14
+ | `description` | 推荐 | 事实记录 | 一句话概括本次出码覆盖范围 |
15
+ | `requirement` | 推荐 | 角色回退来源 | 需求事实:`id/name/type/priority/targetEnds/businessRoles[]`。**`businessRoles[]` 是 PRD 角色事实全量**(含无实现入口的角色,如实记录不删);下游只在顶层 `roles[]` 缺失时才拿它当凭据角色来源。**`requirement.id` 缺失时用需求目录名兜底**(如 `014-测试需求10-20260622`),并在注释说明兜底来源 |
16
+ | `sourceRoot` | 推荐 | 下游硬消费 | `root`(源码根绝对路径)+ `pathBase` + `sourceFilesAreRelativeTo`。下游用它把 `pages[].path` 解析成真实文件做核验 |
17
+ | `systems` | **是** | 下游硬消费 | 业务系统清单,见下 |
18
+ | `testAccess` | 可选 | 角色来源 | 测试访问数据模板,见下 |
19
+ | `moduleInfo` | 推荐 | 事实记录 | `id/name/packagePath/frontendBasePath/backendBasePath/deriveRule` |
20
+ | `implementedScope` | 推荐 | 事实记录 | 本次实现的功能点清单:`featureId/featureName/implementedBy[]`,`implementedBy` 指向页面与后端方法 |
21
+ | `prdMentionedButNotImplemented` | 推荐 | 事实记录 | PRD 有但未实现的部分:`featureIds/featureName/reason`。只能写静态扫描可证实的缺口 |
22
+ | `sourceFiles` | 推荐 | 事实记录+证据锚 | 出码文件清单:`frontend/actions/services/entities` 四类,页面 `evidence` 引用这里 |
23
+ | `pages` | **是** | 下游硬消费 | 页面事实,见下 |
24
+ | `backend` | 推荐 | 事实记录 | `callChain` + `actions[].methods[]` + `services[].methods[]` |
25
+ | `dataModels` | 可选 | 下游硬消费(有下拉时) | 下拉数据模型,见下 |
26
+ | `entities` | 可选 | 下游硬消费(dataKey) | 实体与表字段,见下 |
27
+ | `businessRules` | 推荐 | 下游硬消费 | 业务规则,见下 |
28
+ | `integrations` | 可选 | 事实记录 | 第三方集成点,含 `currentImplementation`(如 `service_stub`)与 PRD 期望 |
29
+ | `routeHints` | 可选 | 事实记录 | 直达链接推导规则:`contextPathHint` + 各页 `routePath`。**`contextPathHint` 有来源才写**(如真实部署的 context path、loginUrl 反推);无来源时留空并登记 `downstreamRuntimeNeeds`,不要沿用模板默认值顶包 |
30
+ | `downstreamRuntimeNeeds` | 推荐 | 事实记录 | 运行时才可确认的信息清单,防止把运行时值编造成静态事实 |
31
+
32
+ ## systems[]
33
+
34
+ | 字段 | 说明 |
35
+ |---|---|
36
+ | `name` | 系统**正式中文全称**(最终用户登录的那个系统)。**须与下游 credentials.json5 的 systemName 严格一致**。取源优先级:PRD/需求文档写明的业务系统名 > 运行系统登录页/标题 > project_context 的 products[].name。**产品/项目代号不是系统名**(实测坑:误取代号「综管」,真实系统名是「集成受理平台」);只有代号可用时填代号,并在 `downstreamRuntimeNeeds` 登记"系统名待确认" |
37
+ | `version` / `frameVersion` | 平台与框架版本 |
38
+ | `targetEnd` | 目标端(PC-管理端 / H5-移动端) |
39
+ | `defaultLoginUrl` | 登录地址样板。知道就填;下游用它推导业务页直达链接的 contextPath |
40
+
41
+ ## testAccess.systems[].roles[]
42
+
43
+ 登录访问模板骨架。每行:`role`、`username`、`password`、`permissions[]`。**username/password 一律留空**,由下游交互补齐。角色集与顶层 `roles[]` 对齐。
44
+
45
+ ## 顶层 roles[](凭据角色的事实源)
46
+
47
+ 下游 `listRequiredRoles` 的消费优先级:**顶层 `roles[]` > `requirement.businessRoles[]` > `testAccess.systems[].roles[]`**——命中即停。凭据模板行数与 `requiredRoles` 由命中的那一级决定。
48
+
49
+ 因此:**PRD 角色(businessRoles)≠ 需登录角色时,必须写顶层 `roles[]` 圈定**(如 PRD 有业务管理员/领导/群众三角色,但领导用统计大屏、群众用 H5,本次均未出码——顶层 roles[] 只写业务管理员)。不写顶层 roles[] 而只精简 testAccess 是无效的(实测假修复:凭据模板仍按 businessRoles 全量生成)。
50
+
51
+ 每项字段:`systemName`(须在 systems[].name 中存在)/ `role`(角色中文名)/ `loginMethod` / `permission` / `description` / `menuPath`(均可选,role 必填)。
52
+
53
+ ## pages[]
54
+
55
+ 每页必填:`path`(相对 sourceRoot.root)、`role`、`description`、`buttons[]`、`rowActions[]`。
56
+
57
+ | 字段 | 必填 | 说明 |
58
+ |---|---|---|
59
+ | `pageId` | 推荐 | 页面标识(文件名去后缀),下游拼 iframe src 关键字 |
60
+ | `role` | **是** | `list` / `add` / `edit` / `detail` / `custom` |
61
+ | `title` | 推荐 | HTML `<title>` 原文 |
62
+ | `path` | **是** | 相对源码根路径,如 `src/main/webapp/perpage/zwplace/gxhzwplacelist.html` |
63
+ | `initAction` | 推荐 | 页面初始化的 restController 名 |
64
+ | `directUrl` | 可留空 | 待测页面直达链接。只有 list 页真正关键;留空时下游交互补齐 |
65
+ | `queryFields[]` | list 页应有 | 查询条件,见下 |
66
+ | `toolbarActions[]` | 推荐 | 工具栏按钮完整事实(label/actionName/opens/backendMethod/elementHints) |
67
+ | `buttons[]` | **是** | 按钮语义摘要,action 枚举见 SKILL.md 分类表 |
68
+ | `rowActions[]` | **是** | 行内操作;没有就空数组 |
69
+ | `dataGrid` | list 页应有 | `id/idField/backendMethod/columns[]`(field/label/renderer/dataOptions) |
70
+ | `formFields[]` | add/edit 页应有 | 表单字段,见下;edit 与 add 相同可写 `['same_as_pages.add.formFields']` |
71
+ | `childGrids[]` | 有子表时必填 | 子表,见下;edit 可用 `same_as_pages.add.childGrids` 引用 |
72
+ | `backendMethods[]` | 推荐 | 该页面对应 Action 的方法名清单 |
73
+ | `evidence[]` | 推荐 | 证据引用,如 `sourceFiles.frontend[role=list]` |
74
+
75
+ ### queryFields[] / formFields[]
76
+
77
+ | 字段 | 说明 |
78
+ |---|---|
79
+ | `label` | **必填**,中文标签(下游按 label 生成定位表达式) |
80
+ | `bind` | `dataBean.<field>`,下游据此推英文字段名 |
81
+ | `component` | `mini-textbox`(输入框)/ `mini-combobox`(下拉)/ 其他 MiniUI 组件原名 |
82
+ | `modelRef` | 下拉时必填,指向顶层 `dataModels[].ref` |
83
+ | `required` / `maxLength` | 表单字段的校验事实 |
84
+ | `elementHints` | 元素线索(attr/value 对),供下游拼 locator,非最终 locator |
85
+
86
+ > 已知边界(实测观察):下游 create 用例对"必填但不是查询条件"的表单字段依赖内置「默认填充」假设,无法从 code_list 落约具体填值。上游能做的是**把 formFields 的 required/component 标全标准**——必填事实越准,下游默认填充的命中面越清楚。
87
+
88
+ ### childGrids[]
89
+
90
+ `id` / `label` / `actions[]`(label+actionName)/ `columns[]`(field/label/required/modelRef/component)。可选 `editMode: 'cell_edit' | 'row_button_dialog' | 'none'` 覆盖推断。
91
+
92
+ **columns[].component 枚举硬约束**(实测踩坑):只能写 `'input'`(文本编辑器列)或 `'listbox'`(下拉编辑器列)——这不是 MiniUI 组件名,是下游契约门禁的合法值(`validation/contract.ts` 会对 childGrids 列 component 强校验,写 `mini-textbox` 之类会被直接拒绝)。也可以**整列省略 component**,下游会读 HTML 的 editor.type 自行推导;前提是该页 HTML 在 sourceRoot 下真实可用。
93
+
94
+ ## dataModels[]
95
+
96
+ | 字段 | 说明 |
97
+ |---|---|
98
+ | `ref` | 模型标识,与 queryFields/formFields 的 `modelRef` 对应 |
99
+ | `label` | 中文名 |
100
+ | `sourceKind` | `dictionary_runtime`(运行态字典,配 `dictionaryCode` + `optionsKnownAtStaticTime: false`)/ `static_options`(配 `options[]` 全量列出)/ `db_runtime`(库表查询,配 `dependency` 说明级联) |
101
+ | `actionMethods[]` | 提供该模型的 Action 方法(`XxxAction.getXxxModel`) |
102
+
103
+ ## entities[]
104
+
105
+ `name` / `table` / `description` / `fields[]`(name/type/label)。业务主键字段标 `dataKey: true`——下游用它做测试数据种子(时间戳前缀策略)。
106
+
107
+ ## businessRules[]
108
+
109
+ | 字段 | 说明 |
110
+ |---|---|
111
+ | `ruleId` | 规则标识,如 `delete.onlyDraftAllowed` |
112
+ | `description` | 规则中文描述 |
113
+ | `scope` | **关键**。作用域数组:`create`/`update`/`delete`/`ui`/子表 id 等。**含 `delete` 的规则会把下游删除用例断言改为「阻断」,下游取阻断语义最明确的一条作为策略来源(装配器优先选 assertionHint 含"不可删除"类文案的规则,兜底取第一条)**——因此只有「记录状态/条件阻断删除」的规则才标 `delete`;「未勾选拦截」「二次确认」等操作前置提示标 `ui`,不要混入 delete scope |
114
+ | `sourceKind` | 通常 `static_scan` |
115
+ | `assertionHint` | 阻断/校验提示**主文案**(如"在用数据不可删除!"),下游直接用作预期断言值。**一条规则只放一个主文案**(下游只消费单值);同规则的其余文案写进 `description`,不要塞数组 |
116
+ | `evidence[]` | 源码相对路径#方法锚点 |
117
+
118
+ ## 留空与省略约定
119
+
120
+ - 可留空:`directUrl`、`loginUrl`、`username`、`password`(下游交互补齐)
121
+ - 可省略:无子表则省 `childGrids`;无行操作则 `rowActions: []`;无自定义按钮则不写 action=custom
122
+ - 不可省:`pages[].path/role/description/buttons/rowActions`,顶层四大必填字段
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * code_list.json5 自校验脚本(code-list-generate skill 配套)
4
+ *
5
+ * 用法:node validate-code-list.mjs <code_list.json5路径>
6
+ *
7
+ * 校验分两级:
8
+ * 错误(exit 1)——下游 CLI 会失败或生成错误用例的问题
9
+ * 警告(exit 0)——建议补齐但不阻断的问题
10
+ */
11
+
12
+ import { existsSync, readdirSync } from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath, pathToFileURL } from 'node:url';
15
+
16
+ /* ──────── json5 依赖解析:bare specifier → 向上找 node_modules(含 pnpm store)──────── */
17
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ function findJson5Entry() {
20
+ let dir = scriptDir;
21
+ while (true) {
22
+ const plain = path.join(dir, 'node_modules', 'json5', 'lib', 'index.js');
23
+ if (existsSync(plain)) return plain;
24
+ const pnpmStore = path.join(dir, 'node_modules', '.pnpm');
25
+ if (existsSync(pnpmStore)) {
26
+ const hit = readdirSync(pnpmStore).find((d) => d.startsWith('json5@'));
27
+ if (hit) {
28
+ const p = path.join(pnpmStore, hit, 'node_modules', 'json5', 'lib', 'index.js');
29
+ if (existsSync(p)) return p;
30
+ }
31
+ }
32
+ const parent = path.dirname(dir);
33
+ if (parent === dir) return null;
34
+ dir = parent;
35
+ }
36
+ }
37
+
38
+ let JSON5;
39
+ try {
40
+ ({ default: JSON5 } = await import('json5'));
41
+ } catch {
42
+ const entry = findJson5Entry();
43
+ if (entry) {
44
+ ({ default: JSON5 } = await import(pathToFileURL(entry).href));
45
+ } else {
46
+ console.error('缺少 json5 依赖。请先运行:npm install json5(或在含 json5 的工程内运行本脚本)');
47
+ process.exit(2);
48
+ }
49
+ }
50
+
51
+ const filePath = process.argv[2];
52
+ if (!filePath) {
53
+ console.error('用法:node validate-code-list.mjs <code_list.json5路径>');
54
+ process.exit(2);
55
+ }
56
+
57
+ const absPath = path.resolve(filePath);
58
+ if (!existsSync(absPath)) {
59
+ console.error(`文件不存在:${absPath}`);
60
+ process.exit(2);
61
+ }
62
+
63
+ const errors = [];
64
+ const warnings = [];
65
+
66
+ /* ──────── 解析 ──────── */
67
+ let data;
68
+ try {
69
+ const { readFileSync } = await import('node:fs');
70
+ data = JSON5.parse(readFileSync(absPath, 'utf-8'));
71
+ } catch (e) {
72
+ console.error(`JSON5 解析失败:${e.message}`);
73
+ process.exit(1);
74
+ }
75
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
76
+ console.error('顶层必须是对象');
77
+ process.exit(1);
78
+ }
79
+
80
+ /* ──────── 顶层必填(对齐下游 extractors/code-list-json5.ts 硬校验)──────── */
81
+ for (const key of ['module', 'moduleName']) {
82
+ if (!data[key]) errors.push(`缺少必填字段:${key}`);
83
+ }
84
+ if (!Array.isArray(data.systems) || data.systems.length === 0) errors.push('缺少必填字段:systems(非空数组)');
85
+ if (!Array.isArray(data.pages) || data.pages.length === 0) errors.push('缺少必填字段:pages(非空数组)');
86
+
87
+ /* ──────── 源码路径真实性 ──────── */
88
+ const sourceRoot = data.sourceRoot?.root;
89
+ if (!sourceRoot) {
90
+ warnings.push('未提供 sourceRoot.root,下游无法用源码核验 pages[].path');
91
+ } else if (!existsSync(path.resolve(sourceRoot))) {
92
+ errors.push(`sourceRoot.root 不存在:${sourceRoot}`);
93
+ }
94
+
95
+ function checkSourcePath(relPath, where) {
96
+ if (!sourceRoot || !relPath) return;
97
+ const p = path.resolve(sourceRoot, relPath);
98
+ if (!existsSync(p)) errors.push(`${where}:路径在 sourceRoot 下不存在 → ${relPath}`);
99
+ }
100
+
101
+ /* ──────── 落盘位置契约(下游按「父目录的父目录 basename」推导需求名)──────── */
102
+ if (path.basename(path.dirname(absPath)) !== '_docs') {
103
+ errors.push(`落盘位置不符契约:code_list.json5 的父目录必须是 _docs(即 <需求名目录>/_docs/code_list.json5),否则下游需求名会被算成「${path.basename(path.dirname(path.dirname(absPath)))}」。当前:${absPath}`);
104
+ }
105
+
106
+ /* ──────── pages 逐页校验 ──────── */
107
+ const PAGE_ROLES = new Set(['list', 'add', 'edit', 'detail', 'custom']);
108
+ const BUTTON_ACTIONS = new Set(['create', 'delete', 'export', 'custom', 'save', 'update', 'cancel']);
109
+ const CHILDGRID_COL_COMPONENTS = new Set(['input', 'listbox']);
110
+ const rolesSeen = new Set();
111
+
112
+ for (const [i, page] of (data.pages ?? []).entries()) {
113
+ const where = `pages[${i}](${page?.pageId ?? page?.path ?? '?'})`;
114
+ for (const key of ['path', 'role', 'description']) {
115
+ if (!page?.[key]) errors.push(`${where} 缺少必填字段:${key}`);
116
+ }
117
+ if (page?.role && !PAGE_ROLES.has(page.role)) errors.push(`${where} role 非法:${page.role}(应为 ${[...PAGE_ROLES].join('/')})`);
118
+ if (page?.role) rolesSeen.add(page.role);
119
+ if (page?.path) checkSourcePath(page.path, where);
120
+ if (!Array.isArray(page?.buttons)) errors.push(`${where} 缺少 buttons 数组`);
121
+ if (!Array.isArray(page?.rowActions)) errors.push(`${where} 缺少 rowActions 数组(无行操作也要给空数组)`);
122
+
123
+ for (const btn of page?.buttons ?? []) {
124
+ if (!btn?.label) errors.push(`${where} 有按钮缺 label`);
125
+ if (!btn?.action) {
126
+ errors.push(`${where} 按钮「${btn?.label}」缺 action 分类`);
127
+ } else if (!BUTTON_ACTIONS.has(btn.action)) {
128
+ errors.push(`${where} 按钮「${btn.label}」action 非法:${btn.action}(应为 ${[...BUTTON_ACTIONS].join('/')})`);
129
+ }
130
+ if (btn?.action === 'custom' && !btn?.customAction) {
131
+ warnings.push(`${where} custom 按钮「${btn.label}」建议补 customAction,否则下游 AI 用例缺少标识`);
132
+ }
133
+ }
134
+
135
+ if (page?.role === 'list') {
136
+ if (!Array.isArray(page.queryFields) || page.queryFields.length === 0) warnings.push(`${where} 列表页无 queryFields,下游无法生成查询相关用例`);
137
+ if (!page.directUrl) warnings.push(`${where} directUrl 为空,下游生成用例时会交互询问`);
138
+ }
139
+ if ((page?.role === 'add' || page?.role === 'edit') && !page.formFields) {
140
+ warnings.push(`${where} ${page.role} 页无 formFields,下游无法判断查询字段是否可写`);
141
+ }
142
+
143
+ // 子表列 component 枚举(下游契约门禁强校验 input/listbox;same_as 引用跳过)
144
+ for (const grid of page?.childGrids ?? []) {
145
+ if (typeof grid === 'string') continue; // same_as_pages.* 引用
146
+ for (const col of grid?.columns ?? []) {
147
+ if (col?.component !== undefined && !CHILDGRID_COL_COMPONENTS.has(col.component)) {
148
+ errors.push(`${where} 子表 ${grid.id ?? '?'} 列「${col.label ?? col.field}」component 非法:${col.component}(只接受 input/listbox,或省略该键由下游推导)`);
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ if (!rolesSeen.has('list')) errors.push('pages 中缺少 role=list 的列表页(下游 CRUD 用例的入口)');
155
+
156
+ /* ──────── 角色(凭据模板行数来源:顶层 roles[] > businessRoles > testAccess)──────── */
157
+ const topRoles = Array.isArray(data.roles) ? data.roles.length : 0;
158
+ const prdRoles = data.requirement?.businessRoles?.length ?? 0;
159
+ const accessRoles = (data.testAccess?.systems ?? []).flatMap((s) => s.roles ?? []).length;
160
+ if (!topRoles && !prdRoles && !accessRoles) {
161
+ warnings.push('未提供任何角色信息(roles / requirement.businessRoles / testAccess.systems[].roles),下游凭据模板将为空');
162
+ } else if (!topRoles && prdRoles > accessRoles) {
163
+ warnings.push(`顶层 roles[] 缺失:下游将按 requirement.businessRoles 全量(${prdRoles} 个)生成凭据模板,而 testAccess 只有 ${accessRoles} 个需登录角色。若 PRD 角色含无实现入口者,请写顶层 roles[] 圈定需登录角色`);
164
+ }
165
+
166
+ /* ──────── 禁区与占位符检查 ──────── */
167
+ function scanValues(obj, trail) {
168
+ if (!obj || typeof obj !== 'object') return;
169
+ for (const [k, v] of Object.entries(obj)) {
170
+ const here = trail ? `${trail}.${k}` : k;
171
+ if (/password|passwd|secret|token/i.test(k) && typeof v === 'string' && v.trim()) {
172
+ errors.push(`疑似凭据泄漏:${here} 非空。code_list 禁止写账号密码(应留空由下游交互补齐)`);
173
+ }
174
+ // 模板占位符形如 <模块中文名>;真实值一般不含 <...> 包裹的整段中文/占位词
175
+ if (typeof v === 'string' && /^<[^<>]+>$/.test(v.trim())) {
176
+ warnings.push(`疑似未替换的模板占位符:${here} = ${v}`);
177
+ }
178
+ scanValues(v, here);
179
+ }
180
+ }
181
+ scanValues(data, '');
182
+
183
+ /* ──────── 质量建议 ──────── */
184
+ if (!Array.isArray(data.businessRules) || data.businessRules.length === 0) {
185
+ warnings.push('无 businessRules:若源码存在删除阻断/唯一性校验等规则,缺失会让下游删除断言退化为「删除成功」');
186
+ } else {
187
+ const hasDeleteRule = data.businessRules.some((r) => Array.isArray(r?.scope) && r.scope.includes('delete'));
188
+ if (!hasDeleteRule) warnings.push('businessRules 中没有 scope 含 delete 的规则;若删除有状态约束请补上(含 assertionHint)');
189
+ }
190
+ if (!Array.isArray(data.entities) || !data.entities.some((e) => (e.fields ?? []).some((f) => f.dataKey))) {
191
+ warnings.push('entities 中未标 dataKey 字段,下游将用默认种子值(建议给业务主键标 dataKey: true)');
192
+ }
193
+
194
+ /* ──────── 输出 ──────── */
195
+ for (const w of warnings) console.warn(`警告:${w}`);
196
+ for (const e of errors) console.error(`错误:${e}`);
197
+
198
+ if (errors.length) {
199
+ console.error(`\n校验未通过:${errors.length} 个错误,${warnings.length} 个警告`);
200
+ process.exit(1);
201
+ }
202
+ console.log(`校验通过(${warnings.length} 个警告)`);