@agile-team/wl-skills-kit 2.10.1 → 2.11.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 (196) hide show
  1. package/CHANGELOG.md +51 -8
  2. package/README.md +47 -29
  3. package/bin/wl-skills.js +156 -53
  4. package/docs/agent-pipeline-runbook.md +3 -3
  5. package/docs//345/205/250/347/233/230/345/210/206/346/236/220/344/270/216/346/231/272/350/203/275/344/275/223/346/220/255/345/273/272/346/214/207/345/215/227.md +4 -4
  6. package/files/.github/copilot-instructions.md +23 -465
  7. package/files/.wl-skills/copilot-instructions-full.md +233 -0
  8. package/files/{docs → .wl-skills/docs}/jh-pagination.md +2 -2
  9. package/files/{docs → .wl-skills/docs}/page-query-hook-best-practices.md +3 -3
  10. package/files/.wl-skills/docs/page-spec-schema.md +109 -0
  11. package/files/{.github → .wl-skills}/guides/README.md +1 -1
  12. package/files/{.github → .wl-skills}/guides/architecture.md +5 -5
  13. package/files/{.github → .wl-skills}/guides/mcp-setup.md +1 -1
  14. package/files/{.github → .wl-skills}/guides/usage.md +8 -8
  15. package/files/{.github → .wl-skills}/reports/SYS_MENU_INFO.md +1 -1
  16. package/files/{.github → .wl-skills}/reports/SYS_PERMISSION_INFO.md +1 -1
  17. package/files/{.github → .wl-skills}/reports//347/273/204/344/273/266/346/217/220/345/217/226/345/273/272/350/256/256.md +1 -1
  18. package/files/{.github → .wl-skills}/skills/_best-practices.md +5 -5
  19. package/files/{.github → .wl-skills}/skills/_compat/README.md +1 -1
  20. package/files/{.github → .wl-skills}/skills/_compat/editors.json +1 -1
  21. package/files/{.github → .wl-skills}/skills/_pipeline.md +9 -9
  22. package/files/{.github → .wl-skills}/skills/_registry.md +3 -3
  23. package/files/{.github → .wl-skills}/skills/core/business-doc-extract/SKILL.md +27 -27
  24. package/files/{.github → .wl-skills}/skills/core/business-doc-extract/USAGE.md +22 -22
  25. package/files/{.github → .wl-skills}/skills/core/business-doc-extract/templates/business-index.md +1 -1
  26. package/files/{.github → .wl-skills}/skills/core/business-doc-extract/templates/business-open-questions.md +1 -1
  27. package/files/{.github → .wl-skills}/skills/core/business-doc-extract/templates/module-dictionary.md +1 -1
  28. package/files/{.github → .wl-skills}/skills/core/business-doc-extract/templates/module-field.md +1 -1
  29. package/files/{.github → .wl-skills}/skills/core/business-doc-extract/templates/module-index.md +2 -2
  30. package/files/{.github → .wl-skills}/skills/core/business-doc-extract/templates/module-requirement.md +2 -2
  31. package/files/{.github → .wl-skills}/skills/core/convention-audit/SKILL.md +6 -6
  32. package/files/{.github → .wl-skills}/skills/core/convention-audit/USAGE.md +1 -1
  33. package/files/{.github → .wl-skills}/skills/core/page-codegen/SKILL.md +20 -14
  34. package/files/{.github → .wl-skills}/skills/core/page-codegen/USAGE.md +1 -1
  35. package/files/{.github → .wl-skills}/skills/core/page-codegen/templates/universal/TPL-DETAIL-TABS.md +7 -7
  36. package/files/{.github → .wl-skills}/skills/core/page-codegen/templates/universal/TPL-DRIVEN.md +5 -5
  37. package/files/{.github → .wl-skills}/skills/core/page-codegen/templates/universal/TPL-FORM-ROUTE.md +2 -2
  38. package/files/{.github → .wl-skills}/skills/core/page-codegen/templates/universal/TPL-RECORD-FORM.md +1 -1
  39. package/files/{.github → .wl-skills}/skills/core/page-codegen/templates/universal/TPL-TREE-LIST.md +1 -1
  40. package/files/{.github → .wl-skills}/skills/core/prototype-scan/SKILL.md +11 -11
  41. package/files/{.github → .wl-skills}/skills/core/prototype-scan/USAGE.md +3 -3
  42. package/files/{.github → .wl-skills}/skills/core/spec-doc-parse/SKILL.md +9 -9
  43. package/files/{.github → .wl-skills}/skills/core/spec-doc-parse/USAGE.md +6 -6
  44. package/files/{.github → .wl-skills}/skills/core/template-extract/SKILL.md +1 -1
  45. package/files/{.github → .wl-skills}/skills/core/template-extract/USAGE.md +1 -1
  46. package/files/{.github → .wl-skills}/skills/sync/_mcp-guardrail.md +3 -3
  47. package/files/{.github → .wl-skills}/skills/sync/dict-sync/SKILL.md +5 -5
  48. package/files/{.github → .wl-skills}/skills/sync/dict-sync/USAGE.md +2 -2
  49. package/files/{.github → .wl-skills}/skills/sync/menu-sync/SKILL.md +6 -6
  50. package/files/{.github → .wl-skills}/skills/sync/menu-sync/USAGE.md +3 -3
  51. package/files/{.github → .wl-skills}/skills/sync/menu-sync/env/guide.md +2 -2
  52. package/files/{.github → .wl-skills}/skills/sync/permission-sync/SKILL.md +3 -3
  53. package/files/{.github → .wl-skills}/skills/sync/permission-sync/USAGE.md +1 -1
  54. package/files/{src → .wl-skills/src}/components/local/c_formModal/README.md +1 -1
  55. package/files/{src → .wl-skills/src}/components/local/c_formSections/README.md +2 -2
  56. package/files/{src → .wl-skills/src}/components/remote/BaseForm/README.md +2 -2
  57. package/files/{src → .wl-skills/src}/components/remote/BaseQuery/README.md +4 -4
  58. package/files/{src → .wl-skills/src}/components/remote/BaseTable/README.md +2 -2
  59. package/files/{src → .wl-skills/src}/components/remote/BaseToolbar/README.md +1 -1
  60. package/files/{.github → .wl-skills}/standards/02-code-structure.md +1 -1
  61. package/files/{.github → .wl-skills}/standards/08-git.md +1 -1
  62. package/files/{.github → .wl-skills}/standards/11-form-validation.md +1 -1
  63. package/files/{.github → .wl-skills}/standards/13-platform-components.md +15 -15
  64. package/files/{.github → .wl-skills}/standards/14-layout-containers.md +9 -9
  65. package/files/{demo → .wl-skills/templates}/README.md +3 -3
  66. package/lib/page-spec.js +588 -0
  67. package/lib/safe-fix.js +115 -0
  68. package/lib/vite-plugin-wl-skills.js +97 -0
  69. package/mcp/config.js +3 -3
  70. package/mcp/tools/projectTools.js +10 -0
  71. package/package.json +16 -11
  72. package/files/src/components/global/C_Splitter/index.scss +0 -61
  73. package/files/src/components/global/C_Splitter/index.vue +0 -149
  74. /package/files/{docs → .wl-skills/docs}/jh-date-range.md +0 -0
  75. /package/files/{docs → .wl-skills/docs}/jh-date.md +0 -0
  76. /package/files/{docs → .wl-skills/docs}/jh-dept-picker.md +0 -0
  77. /package/files/{docs → .wl-skills/docs}/jh-drag-row.md +0 -0
  78. /package/files/{docs → .wl-skills/docs}/jh-file-upload.md +0 -0
  79. /package/files/{docs → .wl-skills/docs}/jh-picker.md +0 -0
  80. /package/files/{docs → .wl-skills/docs}/jh-select.md +0 -0
  81. /package/files/{docs → .wl-skills/docs}/jh-text.md +0 -0
  82. /package/files/{docs → .wl-skills/docs}/jh-textarea.md +0 -0
  83. /package/files/{docs → .wl-skills/docs}/jh-user-picker.md +0 -0
  84. /package/files/{docs → .wl-skills/docs}/mock-architecture.md +0 -0
  85. /package/files/{docs → .wl-skills/docs}/request.md +0 -0
  86. /package/files/{.github → .wl-skills}/reports/README.md +0 -0
  87. /package/files/{.github → .wl-skills}/reports/SYS_DICT_INFO.md +0 -0
  88. /package/files/{.github → .wl-skills}/reports//350/247/204/350/214/203/345/256/241/346/237/245/346/212/245/345/221/212.md" +0 -0
  89. /package/files/{.github → .wl-skills}/skills/_compat/headers/agents.txt +0 -0
  90. /package/files/{.github → .wl-skills}/skills/_compat/headers/claude-code.txt +0 -0
  91. /package/files/{.github → .wl-skills}/skills/_compat/headers/cline.txt +0 -0
  92. /package/files/{.github → .wl-skills}/skills/_compat/headers/cursor-mdc.txt +0 -0
  93. /package/files/{.github → .wl-skills}/skills/_compat/headers/cursor-rules.txt +0 -0
  94. /package/files/{.github → .wl-skills}/skills/_compat/headers/github-copilot.txt +0 -0
  95. /package/files/{.github → .wl-skills}/skills/_compat/headers/kiro.txt +0 -0
  96. /package/files/{.github → .wl-skills}/skills/_compat/headers/qoder.txt +0 -0
  97. /package/files/{.github → .wl-skills}/skills/_compat/headers/trae.txt +0 -0
  98. /package/files/{.github → .wl-skills}/skills/_compat/headers/windsurf.txt +0 -0
  99. /package/files/{.github → .wl-skills}/skills/core/api-contract/SKILL.md +0 -0
  100. /package/files/{.github → .wl-skills}/skills/core/api-contract/USAGE.md +0 -0
  101. /package/files/{.github → .wl-skills}/skills/core/page-codegen/templates/_index.md +0 -0
  102. /package/files/{.github → .wl-skills}/skills/core/page-codegen/templates/domains/_CONTRIBUTING.md +0 -0
  103. /package/files/{.github → .wl-skills}/skills/core/page-codegen/templates/domains/produce/TPL-OPERATION-STATION.md +0 -0
  104. /package/files/{.github → .wl-skills}/skills/core/page-codegen/templates/domains/sale/README.md +0 -0
  105. /package/files/{.github → .wl-skills}/skills/core/page-codegen/templates/universal/TPL-CHANGE-HISTORY.md +0 -0
  106. /package/files/{.github → .wl-skills}/skills/core/page-codegen/templates/universal/TPL-LIST.md +0 -0
  107. /package/files/{.github → .wl-skills}/skills/core/page-codegen/templates/universal/TPL-MASTER-DETAIL.md +0 -0
  108. /package/files/{.github → .wl-skills}/skills/domain/README.md +0 -0
  109. /package/files/{.github → .wl-skills}/skills/ops/code-fix/SKILL.md +0 -0
  110. /package/files/{.github → .wl-skills}/skills/ops/code-fix/USAGE.md +0 -0
  111. /package/files/{.github → .wl-skills}/skills/sync/env.local.json +0 -0
  112. /package/files/{.github → .wl-skills}/skills/sync/menu-sync/env/env.local.json +0 -0
  113. /package/files/{src → .wl-skills/src}/components/global/C_ParentView/index.vue +0 -0
  114. /package/files/{src → .wl-skills/src}/components/global/C_RightToolbar/data.ts +0 -0
  115. /package/files/{src → .wl-skills/src}/components/global/C_RightToolbar/index.scss +0 -0
  116. /package/files/{src → .wl-skills/src}/components/global/C_RightToolbar/index.vue +0 -0
  117. /package/files/{src → .wl-skills/src}/components/global/C_SvgIcon/index.scss +0 -0
  118. /package/files/{src → .wl-skills/src}/components/global/C_SvgIcon/index.vue +0 -0
  119. /package/files/{src → .wl-skills/src}/components/global/C_SvgIcon/svgicon.js +0 -0
  120. /package/files/{src → .wl-skills/src}/components/global/C_TagStatus/README.md +0 -0
  121. /package/files/{src → .wl-skills/src}/components/global/C_TagStatus/config.ts +0 -0
  122. /package/files/{src → .wl-skills/src}/components/global/C_TagStatus/index.scss +0 -0
  123. /package/files/{src → .wl-skills/src}/components/global/C_TagStatus/index.vue +0 -0
  124. /package/files/{src → .wl-skills/src}/components/global/C_TagStatus/types.ts +0 -0
  125. /package/files/{src → .wl-skills/src}/components/global/C_Tree/README.md +0 -0
  126. /package/files/{src → .wl-skills/src}/components/global/C_Tree/data.ts +0 -0
  127. /package/files/{src → .wl-skills/src}/components/global/C_Tree/index.scss +0 -0
  128. /package/files/{src → .wl-skills/src}/components/global/C_Tree/index.vue +0 -0
  129. /package/files/{src → .wl-skills/src}/components/global/C_Tree/types.ts +0 -0
  130. /package/files/{src → .wl-skills/src}/components/local/c_formModal/data.ts +0 -0
  131. /package/files/{src → .wl-skills/src}/components/local/c_formModal/index.scss +0 -0
  132. /package/files/{src → .wl-skills/src}/components/local/c_formModal/index.vue +0 -0
  133. /package/files/{src → .wl-skills/src}/components/local/c_formSections/data.ts +0 -0
  134. /package/files/{src → .wl-skills/src}/components/local/c_formSections/index.scss +0 -0
  135. /package/files/{src → .wl-skills/src}/components/local/c_formSections/index.vue +0 -0
  136. /package/files/{src → .wl-skills/src}/components/local/c_listModal/data.ts +0 -0
  137. /package/files/{src → .wl-skills/src}/components/local/c_listModal/index.scss +0 -0
  138. /package/files/{src → .wl-skills/src}/components/local/c_listModal/index.vue +0 -0
  139. /package/files/{src → .wl-skills/src}/components/local/c_spliterTitle/index.scss +0 -0
  140. /package/files/{src → .wl-skills/src}/components/local/c_spliterTitle/index.vue +0 -0
  141. /package/files/{src → .wl-skills/src}/components/remote/AGGrid/README.md +0 -0
  142. /package/files/{src → .wl-skills/src}/types/page.ts +0 -0
  143. /package/files/{.github → .wl-skills}/standards/01-toolchain.md +0 -0
  144. /package/files/{.github → .wl-skills}/standards/03-comments.md +0 -0
  145. /package/files/{.github → .wl-skills}/standards/04-coding-basics.md +0 -0
  146. /package/files/{.github → .wl-skills}/standards/05-logging.md +0 -0
  147. /package/files/{.github → .wl-skills}/standards/06-security.md +0 -0
  148. /package/files/{.github → .wl-skills}/standards/07-config.md +0 -0
  149. /package/files/{.github → .wl-skills}/standards/09-typescript.md +0 -0
  150. /package/files/{.github → .wl-skills}/standards/10-pinia.md +0 -0
  151. /package/files/{.github → .wl-skills}/standards/12-base-table.md +0 -0
  152. /package/files/{.github → .wl-skills}/standards/index.md +0 -0
  153. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-add/api.md +0 -0
  154. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-add/data.ts +0 -0
  155. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-add/index.scss +0 -0
  156. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-add/index.vue +0 -0
  157. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-add-form/data.ts +0 -0
  158. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-add-form/index.scss +0 -0
  159. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-add-form/index.vue +0 -0
  160. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-change/data.ts +0 -0
  161. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-change/index.scss +0 -0
  162. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-change/index.vue +0 -0
  163. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-change-form/data.ts +0 -0
  164. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-change-form/index.scss +0 -0
  165. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-change-form/index.vue +0 -0
  166. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-change-history/data.ts +0 -0
  167. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-change-history/index.scss +0 -0
  168. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-apply-change-history/index.vue +0 -0
  169. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-archive/api.md +0 -0
  170. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-archive/data.ts +0 -0
  171. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-archive/index.scss +0 -0
  172. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-archive/index.vue +0 -0
  173. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-detail/api.md +0 -0
  174. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-detail/data.ts +0 -0
  175. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-detail/index.scss +0 -0
  176. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-customer-detail/index.vue +0 -0
  177. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-temp-customer-archive/api.md +0 -0
  178. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-temp-customer-archive/data.ts +0 -0
  179. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-temp-customer-archive/index.scss +0 -0
  180. /package/files/{demo → .wl-skills/templates}/produce/aiflow/mmwr-temp-customer-archive/index.vue +0 -0
  181. /package/files/{demo → .wl-skills/templates}/sale/demo/add-demo/data.ts +0 -0
  182. /package/files/{demo → .wl-skills/templates}/sale/demo/add-demo/index.scss +0 -0
  183. /package/files/{demo → .wl-skills/templates}/sale/demo/add-demo/index.vue +0 -0
  184. /package/files/{demo → .wl-skills/templates}/sale/demo/billet-flame-cut-plan/data.ts +0 -0
  185. /package/files/{demo → .wl-skills/templates}/sale/demo/billet-flame-cut-plan/index.scss +0 -0
  186. /package/files/{demo → .wl-skills/templates}/sale/demo/billet-flame-cut-plan/index.vue +0 -0
  187. /package/files/{demo → .wl-skills/templates}/sale/demo/domestic-trade-order/data.ts +0 -0
  188. /package/files/{demo → .wl-skills/templates}/sale/demo/domestic-trade-order/index.scss +0 -0
  189. /package/files/{demo → .wl-skills/templates}/sale/demo/domestic-trade-order/index.vue +0 -0
  190. /package/files/{demo → .wl-skills/templates}/sale/demo/heat-batch-return/data.ts +0 -0
  191. /package/files/{demo → .wl-skills/templates}/sale/demo/heat-batch-return/index.scss +0 -0
  192. /package/files/{demo → .wl-skills/templates}/sale/demo/heat-batch-return/index.vue +0 -0
  193. /package/files/{demo → .wl-skills/templates}/sale/demo/heat-batch-return/meltDialog.vue +0 -0
  194. /package/files/{demo → .wl-skills/templates}/sale/demo/metallurgical-spec/data.ts +0 -0
  195. /package/files/{demo → .wl-skills/templates}/sale/demo/metallurgical-spec/index.scss +0 -0
  196. /package/files/{demo → .wl-skills/templates}/sale/demo/metallurgical-spec/index.vue +0 -0
@@ -0,0 +1,588 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * lib/page-spec.js — page-spec 落盘 + spec-align 确定性比对引擎
5
+ *
6
+ * 解决的核心问题:
7
+ * page-codegen / spec-doc-parse / prototype-scan 的"精准实现"约定
8
+ * (查询字段顺序、表格列顺序、按钮顺序与颜色、操作列严格对应、按钮文字保真)
9
+ * 过去只活在对话上下文里,没有机器可比对的真值,validate 无法验证"是否按约定实现"。
10
+ *
11
+ * 本模块把 page-spec 固化为页面目录下的 `page-spec.json`(单一真值),
12
+ * 再用 AST/括号匹配解析 data.ts 的 queryDef/columnsDef/toolbarDef,
13
+ * 与 page-spec 做确定性比对,输出 S1~S5 偏差:
14
+ * S1: 查询字段顺序不一致(query) → warn
15
+ * S2: 表格列顺序/集合不一致(columns) → error
16
+ * S3: 工具栏按钮顺序/集合/颜色不一致(toolbar)→ error
17
+ * S4: 操作列按钮集合不一致(operations) → error
18
+ * S5: 按钮/列 label 文字与原型不保真 → warn
19
+ *
20
+ * 设计原则:
21
+ * - 找不到 page-spec.json 时静默跳过(不是所有页面都有 spec)
22
+ * - 解析失败降级为 info 提示,不误报阻断
23
+ * - 仅做"约定 vs 代码"的确定性核对,不做语义推断
24
+ */
25
+
26
+ const fs = require("fs");
27
+ const path = require("path");
28
+
29
+ // ─── page-spec JSON Schema(文档 + 运行时校验依据)────────────────────────
30
+ //
31
+ // {
32
+ // "page": "客户档案", // 页面中文名
33
+ // "dir": "src/views/mdata/customer", // 页面目录(相对项目根)
34
+ // "mode": "LIST", // 交互模式
35
+ // "query": [{ "name": "code", "label": "客户编码" }, ...], // 查询字段(左→右、上→下)
36
+ // "columns": [{ "name": "code", "label": "客户编码" }, ...], // 表格列(左→右,selection/index 在前可省略)
37
+ // "toolbar": [{ "label": "新增", "color": "primary", "plain": false }, ...], // 工具栏按钮(左→右)
38
+ // "operations": [{ "label": "编辑" }, { "label": "删除" }] // 操作列按钮
39
+ // }
40
+ //
41
+ // color 取值:primary / danger / warning / success / default
42
+
43
+ const SPEC_FILENAME = "page-spec.json";
44
+
45
+ const VALID_COLORS = new Set([
46
+ "primary",
47
+ "danger",
48
+ "warning",
49
+ "success",
50
+ "default",
51
+ ]);
52
+
53
+ /**
54
+ * 在页面目录中查找 page-spec.json
55
+ * @returns {string|null} 绝对路径
56
+ */
57
+ function findPageSpecPath(absDir) {
58
+ const p = path.join(absDir, SPEC_FILENAME);
59
+ return fs.existsSync(p) ? p : null;
60
+ }
61
+
62
+ /**
63
+ * 读取并解析 page-spec.json
64
+ * @returns {{ spec: object|null, error: string|null }}
65
+ */
66
+ function readPageSpec(absDir) {
67
+ const p = findPageSpecPath(absDir);
68
+ if (!p) return { spec: null, error: null };
69
+ try {
70
+ const spec = JSON.parse(fs.readFileSync(p, "utf8"));
71
+ return { spec, error: null };
72
+ } catch (e) {
73
+ return { spec: null, error: "page-spec.json 解析失败:" + e.message };
74
+ }
75
+ }
76
+
77
+ /**
78
+ * 校验 page-spec 结构合法性(写入前/读取后均可调用)
79
+ * @returns {string[]} 错误列表(空数组 = 合法)
80
+ */
81
+ function validateSpecShape(spec) {
82
+ const errs = [];
83
+ if (!spec || typeof spec !== "object") {
84
+ return ["page-spec 不是合法对象"];
85
+ }
86
+ if (!spec.page || typeof spec.page !== "string") {
87
+ errs.push("缺少 page(页面中文名)");
88
+ }
89
+ for (const key of ["query", "columns", "toolbar", "operations"]) {
90
+ if (spec[key] !== undefined && !Array.isArray(spec[key])) {
91
+ errs.push(key + " 必须是数组");
92
+ }
93
+ }
94
+ for (const btn of spec.toolbar || []) {
95
+ if (btn && btn.color && !VALID_COLORS.has(btn.color)) {
96
+ errs.push(
97
+ '工具栏按钮 "' +
98
+ (btn.label || "?") +
99
+ '" 的 color 非法:' +
100
+ btn.color +
101
+ "(合法值:primary/danger/warning/success/default)",
102
+ );
103
+ }
104
+ }
105
+ return errs;
106
+ }
107
+
108
+ // ─── data.ts 解析:括号匹配提取方法体 ────────────────────────────────────
109
+
110
+ /**
111
+ * 提取形如 `methodName() { ... }` 或 `methodName(): Type { ... }` 的方法体内容
112
+ * 用括号配平精确截取,避免正则误吞。
113
+ * @returns {string|null}
114
+ */
115
+ function extractMethodBody(source, methodName) {
116
+ if (!source) return null;
117
+ // 匹配方法签名起点:methodName ( ... ) ... {
118
+ const sigRe = new RegExp(methodName + "\\s*\\([^)]*\\)[^{]*\\{");
119
+ const m = sigRe.exec(source);
120
+ if (!m) return null;
121
+ const start = m.index + m[0].length; // { 之后
122
+ let depth = 1;
123
+ let i = start;
124
+ while (i < source.length && depth > 0) {
125
+ const ch = source[i];
126
+ if (ch === "{") depth++;
127
+ else if (ch === "}") depth--;
128
+ i++;
129
+ }
130
+ if (depth !== 0) return null;
131
+ return source.slice(start, i - 1);
132
+ }
133
+
134
+ /**
135
+ * 去注释/字符串(保留引号结构),用于结构匹配前清洗
136
+ */
137
+ function stripNoise(code) {
138
+ if (!code) return "";
139
+ let r = code.replace(/\/\*[\s\S]*?\*\//g, "");
140
+ r = r.replace(/\/\/[^\n]*/g, "");
141
+ return r;
142
+ }
143
+
144
+ /**
145
+ * 从方法体中按出现顺序提取顶层对象的 name 与 label。
146
+ * 仅提取数组元素级别的 name/label(不深入嵌套对象),用顺序保真比对。
147
+ *
148
+ * 返回 [{ name, label }],顺序即代码顺序。
149
+ */
150
+ function extractFieldSequence(methodBody) {
151
+ if (!methodBody) return [];
152
+ const result = [];
153
+ // 以对象起始 `{` 为切分点,逐个对象提取首个 name/label
154
+ // 通过括号配平拆分数组中的顶层对象
155
+ const items = splitTopLevelObjects(methodBody);
156
+ for (const item of items) {
157
+ const nameM = item.match(/(?:^|[\s,{])name\s*:\s*["'`]([^"'`]+)["'`]/);
158
+ const labelM = item.match(/(?:^|[\s,{])label\s*:\s*["'`]([^"'`]+)["'`]/);
159
+ if (nameM || labelM) {
160
+ result.push({
161
+ name: nameM ? nameM[1] : null,
162
+ label: labelM ? labelM[1] : null,
163
+ });
164
+ }
165
+ }
166
+ return result;
167
+ }
168
+
169
+ /**
170
+ * 把方法体内最外层数组中的顶层对象切分出来。
171
+ * 找到第一个 `[`,在其内做括号配平,按逗号在 depth=1 处分割对象。
172
+ */
173
+ function splitTopLevelObjects(body) {
174
+ const clean = stripNoise(body);
175
+ const lb = clean.indexOf("[");
176
+ if (lb < 0) return [];
177
+ let depth = 0;
178
+ let i = lb;
179
+ let arrEnd = -1;
180
+ for (; i < clean.length; i++) {
181
+ const ch = clean[i];
182
+ if (ch === "[") depth++;
183
+ else if (ch === "]") {
184
+ depth--;
185
+ if (depth === 0) {
186
+ arrEnd = i;
187
+ break;
188
+ }
189
+ }
190
+ }
191
+ if (arrEnd < 0) return [];
192
+ const arrBody = clean.slice(lb + 1, arrEnd);
193
+ // 在 arrBody 中按顶层 `{...}` 提取对象
194
+ const objects = [];
195
+ let objDepth = 0;
196
+ let objStart = -1;
197
+ for (let j = 0; j < arrBody.length; j++) {
198
+ const ch = arrBody[j];
199
+ if (ch === "{") {
200
+ if (objDepth === 0) objStart = j;
201
+ objDepth++;
202
+ } else if (ch === "}") {
203
+ objDepth--;
204
+ if (objDepth === 0 && objStart >= 0) {
205
+ objects.push(arrBody.slice(objStart, j + 1));
206
+ objStart = -1;
207
+ }
208
+ }
209
+ }
210
+ return objects;
211
+ }
212
+
213
+ /**
214
+ * 提取工具栏按钮序列(含颜色推断)。
215
+ * 工具栏对象常见结构:{ name: "primary", label: "新增", plain: true, type: "danger" }
216
+ * 颜色来源优先级:type > name(name 既是语义也是颜色)
217
+ */
218
+ function extractToolbarSequence(methodBody) {
219
+ if (!methodBody) return [];
220
+ const items = splitTopLevelObjects(methodBody);
221
+ const result = [];
222
+ for (const item of items) {
223
+ const labelM = item.match(/(?:^|[\s,{])label\s*:\s*["'`]([^"'`]+)["'`]/);
224
+ if (!labelM) continue;
225
+ const nameM = item.match(/(?:^|[\s,{])name\s*:\s*["'`]([^"'`]+)["'`]/);
226
+ const typeM = item.match(/(?:^|[\s,{])type\s*:\s*["'`]([^"'`]+)["'`]/);
227
+ const plainM = /(?:^|[\s,{])plain\s*:\s*true/.test(item);
228
+ let color = typeM ? typeM[1] : nameM ? nameM[1] : "default";
229
+ if (!VALID_COLORS.has(color)) color = "default";
230
+ result.push({ label: labelM[1], color, plain: plainM });
231
+ }
232
+ return result;
233
+ }
234
+
235
+ /**
236
+ * 提取操作列按钮序列。
237
+ * 操作列由 renderOps([{ type, label, onClick }]) 渲染,或 operations: [...]
238
+ * label 缺省时按 type 推断中文(edit→编辑 / del|danger→删除 / view→查看)
239
+ */
240
+ function extractOperationSequence(dataContent) {
241
+ if (!dataContent) return [];
242
+ const clean = stripNoise(dataContent);
243
+ // 优先匹配 renderOps([...])
244
+ const renderM = /renderOps\s*\(\s*\[/.exec(clean);
245
+ let body = null;
246
+ if (renderM) {
247
+ const start = renderM.index + renderM[0].length - 1; // 指向 [
248
+ body = extractBracketBody(clean, start);
249
+ }
250
+ // 兼容旧写法 operations: [...],validate 其他规则仍会提示改用 renderOps()
251
+ if (!body) {
252
+ const operationsM = /\boperations\s*:\s*\[/.exec(clean);
253
+ if (operationsM) {
254
+ const start = operationsM.index + operationsM[0].length - 1; // 指向 [
255
+ body = extractBracketBody(clean, start);
256
+ }
257
+ }
258
+ if (!body) return [];
259
+ const items = splitTopLevelObjects("[" + body + "]");
260
+ const TYPE_LABEL = { edit: "编辑", del: "删除", danger: "删除", view: "查看" };
261
+ const result = [];
262
+ for (const item of items) {
263
+ const labelM = item.match(/(?:^|[\s,{])label\s*:\s*["'`]([^"'`]+)["'`]/);
264
+ const typeM = item.match(/(?:^|[\s,{])type\s*:\s*["'`]([^"'`]+)["'`]/);
265
+ let label = labelM ? labelM[1] : typeM ? TYPE_LABEL[typeM[1]] : null;
266
+ if (label) result.push({ label });
267
+ }
268
+ return result;
269
+ }
270
+
271
+ /** 从 `[` 位置做括号配平,返回内部内容(不含外层括号) */
272
+ function extractBracketBody(source, openIdx) {
273
+ if (source[openIdx] !== "[") return null;
274
+ let depth = 0;
275
+ for (let i = openIdx; i < source.length; i++) {
276
+ const ch = source[i];
277
+ if (ch === "[") depth++;
278
+ else if (ch === "]") {
279
+ depth--;
280
+ if (depth === 0) return source.slice(openIdx + 1, i);
281
+ }
282
+ }
283
+ return null;
284
+ }
285
+
286
+ // ─── 比对 ────────────────────────────────────────────────────────────────
287
+
288
+ function seqNames(seq) {
289
+ return seq.map((x) => x.name).filter(Boolean);
290
+ }
291
+ function seqLabels(seq) {
292
+ return seq.map((x) => x.label).filter(Boolean);
293
+ }
294
+
295
+ /** 数组顺序是否严格相等 */
296
+ function arrayEq(a, b) {
297
+ if (a.length !== b.length) return false;
298
+ return a.every((x, i) => x === b[i]);
299
+ }
300
+
301
+ /** 集合是否相等(忽略顺序) */
302
+ function setEq(a, b) {
303
+ if (a.length !== b.length) return false;
304
+ const sa = new Set(a);
305
+ return b.every((x) => sa.has(x)) && a.every((x) => new Set(b).has(x));
306
+ }
307
+
308
+ function pushMissingImplementationIssue(issues, level, dir, rule, target) {
309
+ issues.push({
310
+ level,
311
+ dir,
312
+ rule,
313
+ text: "page-spec 声明了 " + target + ",但 data.ts 中未解析到对应实现",
314
+ });
315
+ }
316
+
317
+ /**
318
+ * 比对 page-spec 与 data.ts 实际实现
319
+ * @param {object} spec page-spec.json 对象
320
+ * @param {string} dataContent data.ts 源码
321
+ * @param {string} dir 页面相对目录(用于 issue.dir)
322
+ * @returns {Array<{level,dir,text,rule}>}
323
+ */
324
+ function compareSpecToCode(spec, dataContent, dir) {
325
+ const issues = [];
326
+ if (!spec) return issues;
327
+
328
+ // S1: 查询字段顺序(query)
329
+ if (Array.isArray(spec.query) && spec.query.length > 0) {
330
+ const body = extractMethodBody(dataContent, "queryDef");
331
+ const specNames = spec.query.map((q) => q.name).filter(Boolean);
332
+ if (!body) {
333
+ pushMissingImplementationIssue(issues, "warn", dir, "S1", "queryDef()");
334
+ } else {
335
+ const actual = extractFieldSequence(body);
336
+ const actualNames = seqNames(actual);
337
+ if (specNames.length && actualNames.length === 0) {
338
+ pushMissingImplementationIssue(issues, "warn", dir, "S1", "queryDef() 查询字段");
339
+ } else if (specNames.length && !setEq(specNames, actualNames)) {
340
+ const missing = specNames.filter((n) => !actualNames.includes(n));
341
+ const extra = actualNames.filter((n) => !specNames.includes(n));
342
+ issues.push({
343
+ level: "warn",
344
+ dir,
345
+ rule: "S1",
346
+ text:
347
+ "查询字段与 page-spec 不一致" +
348
+ (missing.length ? "(缺:" + missing.join(",") + ")" : "") +
349
+ (extra.length ? "(多:" + extra.join(",") + ")" : ""),
350
+ });
351
+ } else if (specNames.length && !arrayEq(specNames, actualNames)) {
352
+ issues.push({
353
+ level: "warn",
354
+ dir,
355
+ rule: "S1",
356
+ text:
357
+ "查询字段顺序与原型不一致:spec[" +
358
+ specNames.join(",") +
359
+ "] vs code[" +
360
+ actualNames.join(",") +
361
+ "]",
362
+ });
363
+ }
364
+ }
365
+ }
366
+
367
+ // S2: 表格列顺序/集合(columns)
368
+ if (Array.isArray(spec.columns) && spec.columns.length > 0) {
369
+ const body = extractMethodBody(dataContent, "columnsDef");
370
+ const specNames = spec.columns.map((c) => c.name).filter(Boolean);
371
+ if (!body) {
372
+ pushMissingImplementationIssue(issues, "error", dir, "S2", "columnsDef()");
373
+ } else {
374
+ const actual = extractFieldSequence(body);
375
+ // 过滤掉框架内置列(selection/index/_action)
376
+ const actualNames = seqNames(actual).filter(
377
+ (n) => !["selection", "index", "_action"].includes(n),
378
+ );
379
+ if (specNames.length && actualNames.length === 0) {
380
+ pushMissingImplementationIssue(issues, "error", dir, "S2", "columnsDef() 表格列");
381
+ } else if (specNames.length && !setEq(specNames, actualNames)) {
382
+ const missing = specNames.filter((n) => !actualNames.includes(n));
383
+ const extra = actualNames.filter((n) => !specNames.includes(n));
384
+ issues.push({
385
+ level: "error",
386
+ dir,
387
+ rule: "S2",
388
+ text:
389
+ "表格列与 page-spec 不一致" +
390
+ (missing.length ? "(缺:" + missing.join(",") + ")" : "") +
391
+ (extra.length ? "(多:" + extra.join(",") + ")" : ""),
392
+ });
393
+ } else if (specNames.length && !arrayEq(specNames, actualNames)) {
394
+ issues.push({
395
+ level: "error",
396
+ dir,
397
+ rule: "S2",
398
+ text:
399
+ "表格列顺序与原型不一致:spec[" +
400
+ specNames.join(",") +
401
+ "] vs code[" +
402
+ actualNames.join(",") +
403
+ "]",
404
+ });
405
+ }
406
+ }
407
+ }
408
+
409
+ // S3: 工具栏按钮顺序/集合/颜色(toolbar)
410
+ if (Array.isArray(spec.toolbar) && spec.toolbar.length > 0) {
411
+ const body = extractMethodBody(dataContent, "toolbarDef");
412
+ const specLabels = spec.toolbar.map((b) => b.label).filter(Boolean);
413
+ if (!body) {
414
+ pushMissingImplementationIssue(issues, "error", dir, "S3", "toolbarDef()");
415
+ } else {
416
+ const actual = extractToolbarSequence(body);
417
+ const actualLabels = actual.map((b) => b.label);
418
+ if (specLabels.length && actualLabels.length === 0) {
419
+ pushMissingImplementationIssue(issues, "error", dir, "S3", "toolbarDef() 工具栏按钮");
420
+ } else if (specLabels.length && !setEq(specLabels, actualLabels)) {
421
+ const missing = specLabels.filter((l) => !actualLabels.includes(l));
422
+ const extra = actualLabels.filter((l) => !specLabels.includes(l));
423
+ issues.push({
424
+ level: "error",
425
+ dir,
426
+ rule: "S3",
427
+ text:
428
+ "工具栏按钮与 page-spec 不一致" +
429
+ (missing.length ? "(缺:" + missing.join(",") + ")" : "") +
430
+ (extra.length ? "(多:" + extra.join(",") + ")" : ""),
431
+ });
432
+ } else if (specLabels.length && !arrayEq(specLabels, actualLabels)) {
433
+ issues.push({
434
+ level: "error",
435
+ dir,
436
+ rule: "S3",
437
+ text:
438
+ "工具栏按钮顺序与原型不一致:spec[" +
439
+ specLabels.join(",") +
440
+ "] vs code[" +
441
+ actualLabels.join(",") +
442
+ "]",
443
+ });
444
+ }
445
+ // 颜色比对(仅在集合一致时逐个核对)
446
+ if (specLabels.length && actualLabels.length && setEq(specLabels, actualLabels)) {
447
+ const actualByLabel = new Map(actual.map((b) => [b.label, b]));
448
+ for (const sb of spec.toolbar) {
449
+ if (!sb.label || !sb.color) continue;
450
+ const ab = actualByLabel.get(sb.label);
451
+ if (ab && ab.color !== sb.color) {
452
+ issues.push({
453
+ level: "warn",
454
+ dir,
455
+ rule: "S3",
456
+ text:
457
+ '按钮"' +
458
+ sb.label +
459
+ '"颜色与原型不一致:spec=' +
460
+ sb.color +
461
+ " vs code=" +
462
+ ab.color,
463
+ });
464
+ }
465
+ }
466
+ }
467
+ }
468
+ }
469
+
470
+ // S4: 操作列按钮集合(operations)
471
+ if (Array.isArray(spec.operations) && spec.operations.length > 0) {
472
+ const actual = extractOperationSequence(dataContent);
473
+ const specLabels = spec.operations.map((o) => o.label).filter(Boolean);
474
+ const actualLabels = actual.map((o) => o.label);
475
+ if (specLabels.length && actualLabels.length === 0) {
476
+ pushMissingImplementationIssue(issues, "error", dir, "S4", "renderOps()/operations 操作列按钮");
477
+ } else if (specLabels.length && !setEq(specLabels, actualLabels)) {
478
+ const missing = specLabels.filter((l) => !actualLabels.includes(l));
479
+ const extra = actualLabels.filter((l) => !specLabels.includes(l));
480
+ issues.push({
481
+ level: "error",
482
+ dir,
483
+ rule: "S4",
484
+ text:
485
+ "操作列按钮与 page-spec 不一致" +
486
+ (missing.length ? "(缺:" + missing.join(",") + ")" : "") +
487
+ (extra.length ? "(多:" + extra.join(",") + ",禁止自行添加原型外按钮)" : ""),
488
+ });
489
+ }
490
+ }
491
+
492
+ // S5: label 文字保真(query + columns 中 name 相同但 label 被简化/篡改)
493
+ // 例:原型"新增申请"被简化为"新增"、"客户编码"被改为"编码"
494
+ {
495
+ const checkLabelFidelity = (specArr, methodName) => {
496
+ if (!Array.isArray(specArr) || specArr.length === 0) return;
497
+ const body = extractMethodBody(dataContent, methodName);
498
+ if (!body) return;
499
+ const actual = extractFieldSequence(body);
500
+ const actualByName = new Map(
501
+ actual.filter((a) => a.name).map((a) => [a.name, a.label]),
502
+ );
503
+ for (const sf of specArr) {
504
+ if (!sf.name || !sf.label) continue;
505
+ if (!actualByName.has(sf.name)) continue;
506
+ const codeLabel = actualByName.get(sf.name);
507
+ if (codeLabel && codeLabel !== sf.label) {
508
+ issues.push({
509
+ level: "warn",
510
+ dir,
511
+ rule: "S5",
512
+ text:
513
+ '字段"' +
514
+ sf.name +
515
+ '"label 与原型不保真:spec="' +
516
+ sf.label +
517
+ '" vs code="' +
518
+ codeLabel +
519
+ '"',
520
+ });
521
+ }
522
+ }
523
+ };
524
+ checkLabelFidelity(spec.query, "queryDef");
525
+ checkLabelFidelity(spec.columns, "columnsDef");
526
+ }
527
+
528
+ return issues;
529
+ }
530
+
531
+ /**
532
+ * 对单个页面目录执行 spec-align 比对
533
+ * @returns {{ issues: Array, hasSpec: boolean }}
534
+ */
535
+ function alignPage(absDir, relDir) {
536
+ const { spec, error } = readPageSpec(absDir);
537
+ if (error) {
538
+ return { issues: [{ level: "info", dir: relDir, text: error, rule: "S0" }], hasSpec: false };
539
+ }
540
+ if (!spec) return { issues: [], hasSpec: false };
541
+
542
+ const shapeErrs = validateSpecShape(spec);
543
+ if (shapeErrs.length) {
544
+ return {
545
+ issues: shapeErrs.map((e) => ({
546
+ level: "warn",
547
+ dir: relDir,
548
+ rule: "S0",
549
+ text: "page-spec.json 结构问题:" + e,
550
+ })),
551
+ hasSpec: true,
552
+ };
553
+ }
554
+
555
+ const dataPath = path.join(absDir, "data.ts");
556
+ if (!fs.existsSync(dataPath)) {
557
+ return {
558
+ issues: [
559
+ {
560
+ level: "warn",
561
+ dir: relDir,
562
+ rule: "S0",
563
+ text: "存在 page-spec.json 但缺 data.ts,无法做 spec-align 比对",
564
+ },
565
+ ],
566
+ hasSpec: true,
567
+ };
568
+ }
569
+ const dataContent = fs.readFileSync(dataPath, "utf8");
570
+ return { issues: compareSpecToCode(spec, dataContent, relDir), hasSpec: true };
571
+ }
572
+
573
+ module.exports = {
574
+ SPEC_FILENAME,
575
+ VALID_COLORS,
576
+ findPageSpecPath,
577
+ readPageSpec,
578
+ validateSpecShape,
579
+ extractMethodBody,
580
+ extractFieldSequence,
581
+ extractToolbarSequence,
582
+ extractOperationSequence,
583
+ splitTopLevelObjects,
584
+ compareSpecToCode,
585
+ alignPage,
586
+ arrayEq,
587
+ setEq,
588
+ };
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * lib/safe-fix.js — 确定性机械修复引擎(v2.11.1+)
5
+ *
6
+ * 解决的问题:
7
+ * 过去"修复"完全靠 AI 改源码,机械性偏差(缺 render-type、::v-deep、未用 import 等)
8
+ * 也走 AI,慢且不确定。本模块对一批"幂等、零语义判断"的偏差做确定性自动修复,
9
+ * AI 只处理需要语义判断的部分。
10
+ *
11
+ * 覆盖的安全修复(F1~F5,全部幂等):
12
+ * F1: <BaseTable> 缺 render-type="agGrid" → 补 render-type="agGrid"
13
+ * F2: ::v-deep / /deep/ → :deep()
14
+ * F3: import C_Splitter / <C_Splitter> → 仅标记,不自动改(需人工换布局)
15
+ * F4: 行尾多余空白 → 清除
16
+ * F5: 文件末尾缺换行 → 补 \n
17
+ *
18
+ * 设计原则:
19
+ * - 只做"改了一定对"的修复,任何有歧义的改动一律跳过并交回 AI
20
+ * - dryRun 模式只报告将改什么,不写盘
21
+ * - 返回每个文件的改动条目,供 CLI 汇总输出
22
+ */
23
+
24
+ const fs = require("fs");
25
+ const path = require("path");
26
+
27
+ const SKIP_DIRS = ["node_modules", "dist", ".git", "demo"];
28
+
29
+ function walk(dir, base, out) {
30
+ out = out || [];
31
+ if (!fs.existsSync(dir)) return out;
32
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
33
+ if (entry.isDirectory()) {
34
+ if (SKIP_DIRS.includes(entry.name)) continue;
35
+ walk(path.join(dir, entry.name), base, out);
36
+ } else if (/\.(vue|ts|scss)$/.test(entry.name)) {
37
+ out.push(path.join(dir, entry.name));
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ /**
44
+ * 对单个文件内容做安全修复
45
+ * @returns {{ content: string, changes: string[] }}
46
+ */
47
+ function fixContent(content, ext) {
48
+ const changes = [];
49
+ let out = content;
50
+
51
+ // F1: BaseTable 缺 render-type(仅 .vue)
52
+ if (ext === ".vue") {
53
+ out = out.replace(/<BaseTable\b([^>]*?)>/g, (full, attrs) => {
54
+ if (/render-type\s*=/.test(attrs)) return full;
55
+ changes.push('F1: BaseTable 补 render-type="agGrid"');
56
+ return '<BaseTable render-type="agGrid"' + attrs + ">";
57
+ });
58
+ }
59
+
60
+ // F2: ::v-deep / /deep/ → :deep()(.vue/.scss)
61
+ if (ext === ".vue" || ext === ".scss") {
62
+ if (/::v-deep\b|\/deep\//.test(out)) {
63
+ // ::v-deep .foo → :deep(.foo) ;保守处理:仅替换写法标记,复杂选择器交 AI
64
+ const before = out;
65
+ out = out.replace(/::v-deep\s+([^\s{,]+)/g, ":deep($1)");
66
+ out = out.replace(/\/deep\/\s+([^\s{,]+)/g, ":deep($1)");
67
+ if (out !== before) changes.push("F2: ::v-deep//deep/ → :deep()");
68
+ }
69
+ }
70
+
71
+ // F4: 行尾空白
72
+ const trimmed = out.replace(/[ \t]+$/gm, "");
73
+ if (trimmed !== out) {
74
+ changes.push("F4: 清除行尾空白");
75
+ out = trimmed;
76
+ }
77
+
78
+ // F5: 文件末尾换行
79
+ if (out.length > 0 && !out.endsWith("\n")) {
80
+ out += "\n";
81
+ changes.push("F5: 补文件末尾换行");
82
+ }
83
+
84
+ return { content: out, changes };
85
+ }
86
+
87
+ /**
88
+ * 扫描目录并执行安全修复
89
+ * @param {string} targetDir
90
+ * @param {string} scanRel
91
+ * @param {object} options { dryRun }
92
+ * @returns {{ files: Array<{rel,changes}>, fixedCount, fileCount }}
93
+ */
94
+ function runSafeFix(targetDir, scanRel, options) {
95
+ options = options || {};
96
+ const scanDir = path.join(targetDir, scanRel || "src/views");
97
+ const files = walk(scanDir, targetDir);
98
+ const result = [];
99
+ let fixedCount = 0;
100
+
101
+ for (const abs of files) {
102
+ const ext = path.extname(abs);
103
+ const content = fs.readFileSync(abs, "utf8");
104
+ const { content: fixed, changes } = fixContent(content, ext);
105
+ if (changes.length === 0) continue;
106
+ fixedCount += changes.length;
107
+ const rel = path.relative(targetDir, abs).replace(/\\/g, "/");
108
+ result.push({ rel, changes });
109
+ if (!options.dryRun) fs.writeFileSync(abs, fixed, "utf8");
110
+ }
111
+
112
+ return { files: result, fixedCount, fileCount: files.length };
113
+ }
114
+
115
+ module.exports = { runSafeFix, fixContent };