@zat-design/sisyphus-scene 4.6.1-beta.5 → 4.6.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.
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@zat-design/sisyphus-react-mcp@4.6.1-beta.5"
7
+ "@zat-design/sisyphus-react-mcp@4.6.1"
8
8
  ]
9
9
  }
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zat-design/sisyphus-scene",
3
- "version": "4.6.1-beta.5",
3
+ "version": "4.6.1",
4
4
  "description": "Sisyphus 业务场景 Agent 技能,跨 Claude Code、Codex、Cursor 分发 Sisyphus 4 + Ant Design 6 场景与 MCP 配置",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -7,9 +7,21 @@ description: 基于与 Scene Agent 完全同版的 @zat-design/sisyphus-react 4.
7
7
 
8
8
  为保险核心系统生成可验证的页面骨架。场景模板只决定结构,组件 MCP 决定真实 API。禁止凭记忆、旧模板或 `node_modules` 推断 Sisyphus 用法。
9
9
 
10
+ ## PC 系统交互基线
11
+
12
+ 用户明确的业务需求是必须满足的上层约束。PC 系统的实现证据发生冲突时,按以下顺序决策:
13
+
14
+ 1. **注册场景结构**:决定页面结构、导航关系、表单所有权和校验边界。
15
+ 2. **Sisyphus MCP API/demo**:决定组件、公开属性、类型和组件组合方式。
16
+ 3. **项目既有约定**:适配路由、请求、权限、状态、目录和命名,不覆盖前两层基线。
17
+ 4. **截图差异方案**:最后处理间距、尺寸、颜色等视觉差异,不据此重构已确认的交互结构。
18
+
19
+ 低优先级证据与高优先级证据冲突时保留高优先级实现,并在交付中记录差异。强制安全规则或无法绕过的项目规则与基线冲突时停止生成并报告,不静默降级或绕过。
20
+
10
21
  ## 不可跳过的约束
11
22
 
12
23
  - 仅支持 React + TypeScript、`assets/ai-contract.json` 声明的 Sisyphus 完全同版组合和 Ant Design 6。
24
+ - 所有生成的 `.ts`、`.tsx` 运行时函数必须使用箭头函数,包括组件、事件处理、配置工厂、service 和回调。禁止函数声明/表达式、对象或类方法、getter、setter 和 constructor;目标项目已有非箭头写法也不得覆盖此规则。
13
25
  - Umi Max、React Router、请求库、权限和状态库都属于目标项目适配,不是技能依赖。
14
26
  - 无论场景是否命中模板,先查询 Sisyphus MCP 的组件列表、完整 API 和 demo。
15
27
  - 只有 Sisyphus MCP 明确给出 `missing` 的能力才可进入 Ant Design 补全;`unverified` 必须先修复 MCP 连接或版本问题。
@@ -43,7 +55,8 @@ description: 基于与 Scene Agent 完全同版的 @zat-design/sisyphus-react 4.
43
55
  ```
44
56
 
45
57
  3. 读取最近层级的 `AGENTS.md`、`CLAUDE.md`、`.cursor/rules/*.mdc`、package.json、lockfile、相邻页面、路由、请求和权限实现。
46
- 4. 同级规则冲突时停止并列出冲突,不静默选择。
58
+ 4. 对生成文件匹配规则文件的 `globs`;读取规则中引用的完整规范和标杆样板,不能只看速览。把目录、类型文件、组件 barrel、请求处理等硬规则记录到 `ProjectProfile`,并把探测结果保存为后续校验使用的 JSON。
59
+ 5. 速览与其声明的完整规范冲突时以完整规范为准;同级规则或标杆样板仍冲突时停止并列出冲突,不静默选择。
47
60
 
48
61
  详细契约见 `references/integration-contracts.md`。
49
62
 
@@ -133,10 +146,15 @@ node scripts/resolve-versions.mjs --root <项目根目录>
133
146
 
134
147
  ### 8. 注入项目适配并生成最小文件集
135
148
 
149
+ - 组件和其他命名导出使用 `export const EntityManagement = (): ReactElement => {}` 形式;需要默认导出时另写 `export default EntityManagement`,不得改回函数声明。
150
+ - handler、配置和 service 使用 `const handleSave = async (...) => {}` 或对象箭头属性。类型中的 interface/type 方法签名不属于运行时函数,可以保留。
136
151
  - 只有确认项目使用 Umi Max 时才生成 `@umijs/max` import。
137
152
  - 只有确认项目使用 React Router 时才生成对应 import。
138
153
  - 无法确认导航实现时注入 `onView`、`onEdit`、`onBack` 等回调。
139
- - 请求、权限、状态和类型文件遵循目标项目证据。
154
+ - 写代码前先按 `fileStructureAdapter.conventions` 输出“职责 → 文件路径”映射;配置、组件、services、models、assets、utils、hooks 和类型文件必须落到项目指定位置,禁止把有明确目录归属的文件平铺到页面根目录。
155
+ - 页面内子组件按项目证据使用独立目录、样式文件和 `components/index.ts` barrel;类型文件严格使用 `typeConvention`,不得自行改名为 `types.ts`。
156
+ - 请求、权限、状态和类型文件遵循目标项目证据。若 `requestAdapter` 声明全局错误拦截,不重复添加请求 `try/catch` 或 `res.status === 200` 判断。
157
+ - 把所有适用规则中的 MUST/禁止项转换为逐文件自检清单;组件内部顺序、Hooks、导出方式、命名和样式规则没有自动校验时也必须人工核对并如实报告。
140
158
  - 无需求时不创建空 `hooks`、`models`、`utils`、`assets`、样式或菜单文件。
141
159
 
142
160
  ### 9. 验证并交付
@@ -147,7 +165,8 @@ node scripts/resolve-versions.mjs --root <项目根目录>
147
165
  node scripts/validate-scaffold.mjs \
148
166
  --root <项目根目录> \
149
167
  --files <逗号分隔的生成文件> \
150
- --decisions <ComponentDecision JSON>
168
+ --decisions <ComponentDecision JSON> \
169
+ --profile <ProjectProfile JSON>
151
170
  ```
152
171
 
153
172
  再运行目标项目的 typecheck、lint、相关测试;有截图时启动页面做视觉核对。完成后运行 GitNexus detect changes。
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.6.1-beta.5",
2
+ "version": "4.6.1",
3
3
  "schemaVersion": 2,
4
4
  "tools": [
5
5
  "sisyphus-get-meta-status",
@@ -8,13 +8,13 @@
8
8
  "apiEvidence": {
9
9
  "tool": "sisyphus-get-component-api",
10
10
  "target": "ProForm",
11
- "libraryVersion": "4.6.1-beta.5",
11
+ "libraryVersion": "4.6.1",
12
12
  "schemaVersion": 2
13
13
  },
14
14
  "demoEvidence": {
15
15
  "tool": "sisyphus-get-example",
16
16
  "target": "ProForm/demos/base.tsx",
17
- "libraryVersion": "4.6.1-beta.5",
17
+ "libraryVersion": "4.6.1",
18
18
  "schemaVersion": 2,
19
19
  "exampleId": "ProForm/demos/base.tsx"
20
20
  }
@@ -28,13 +28,13 @@
28
28
  "apiEvidence": {
29
29
  "tool": "sisyphus-get-component-api",
30
30
  "target": "ProTable",
31
- "libraryVersion": "4.6.1-beta.5",
31
+ "libraryVersion": "4.6.1",
32
32
  "schemaVersion": 2
33
33
  },
34
34
  "demoEvidence": {
35
35
  "tool": "sisyphus-get-example",
36
36
  "target": "ProTable/demos/base.tsx",
37
- "libraryVersion": "4.6.1-beta.5",
37
+ "libraryVersion": "4.6.1",
38
38
  "schemaVersion": 2,
39
39
  "exampleId": "ProTable/demos/base.tsx"
40
40
  }
@@ -18,6 +18,17 @@ cases:
18
18
  - id: unverified-is-not-missing
19
19
  sisyphus_status: unverified
20
20
  expected_result: generation-blocked
21
+ - id: pc-screenshot-cannot-override-baseline
22
+ platform: pc
23
+ registered_scene: tab-detail
24
+ sisyphus_status: covered
25
+ screenshot_difference: alternative-tabs-and-form-layout
26
+ expected_priority:
27
+ - registered-scene-structure
28
+ - sisyphus-mcp-api-demo
29
+ - project-conventions
30
+ - screenshot-difference
31
+ expected_result: preserve-scene-and-mcp-report-visual-difference
21
32
  - id: string-evidence-is-invalid
22
33
  evidence_shape: string
23
34
  expected_result: validation-failure
@@ -32,6 +43,14 @@ cases:
32
43
  antd_status: missing
33
44
  project_component_status: reusable
34
45
  expected_path: project-component-with-evidence
46
+ - id: project-standard-file-structure
47
+ project_profile: confirmed-file-structure-and-request-conventions
48
+ generated_files: flattened-config-data-types
49
+ expected_result: validation-failure
50
+ - id: non-arrow-runtime-function
51
+ generated_syntax: function-declaration-expression-or-method
52
+ expected_rule: function-syntax
53
+ expected_result: validation-failure
35
54
  - id: unresolved-capability
36
55
  sisyphus_status: missing
37
56
  antd_status: missing
@@ -7,7 +7,7 @@ examples: []
7
7
  # status: approved
8
8
  # application: xk-wjs-claim-web
9
9
  # capabilities: [search-form, data-table, table-tabs, drawer-form]
10
- # sisyphus_version: "4.6.1-beta.5"
10
+ # sisyphus_version: "4.6.1"
11
11
  # antd_range: ">=6 <7"
12
12
  # paths:
13
13
  # - src/pages/litigationManagement/index.tsx
@@ -49,7 +49,9 @@ interface ProjectProfile {
49
49
  requestAdapter?: IntegrationAdapter;
50
50
  permissionAdapter?: IntegrationAdapter;
51
51
  stateAdapter?: IntegrationAdapter;
52
+ fileStructureAdapter?: IntegrationAdapter;
52
53
  instructionFiles: string[];
54
+ referencedInstructionFiles: string[];
53
55
  typeConvention?: string;
54
56
  styleConvention?: string;
55
57
  availableCommands: string[];
@@ -59,6 +61,8 @@ interface ProjectProfile {
59
61
 
60
62
  `framework` 只记录探测结果,不参与技能启用判断。没有导航需求时不得因为探测到框架而引入路由代码。
61
63
 
64
+ `fileStructureAdapter.conventions` 记录经项目规则或标杆样板确认的目录、组件入口与样式、barrel 和类型文件名。`requestAdapter.conventions` 记录全局错误拦截、是否省略请求 `try/catch` 和状态码判断。两者都必须保留 `evidenceFiles`,生成文件树与校验器使用同一份 `ProjectProfile`,不得只探测不消费。
65
+
62
66
  ## CapabilityRequirement 与 ComponentDecision
63
67
 
64
68
  ```ts
@@ -27,14 +27,19 @@
27
27
  ## 结构和代码
28
28
 
29
29
  - 组件使用 `.tsx`,纯逻辑使用 `.ts`,样式使用项目确认的 LESS 约定。
30
- - 目录、类型文件、导出方式和 import 顺序遵循最近项目规则。
30
+ - 所有 `.ts`、`.tsx` 运行时函数必须使用箭头函数;函数声明/表达式、对象或类方法、getter、setter 和 constructor 统一报 `function-syntax`。项目约定不得覆盖此硬规则,interface/type 方法签名不参与检查。
31
+ - 注释、字符串、正则和模板字符串文本中的函数字样不报错;模板表达式中的真实非箭头函数仍必须拦截。TypeScript 解析器不可用时明确失败,不得降级为字符串匹配或跳过检查。
32
+ - 目录、类型文件、导出方式和 import 顺序遵循最近项目规则及其引用的完整规范。
33
+ - 生成前必须按 `ProjectProfile.fileStructureAdapter` 确认职责与路径;项目规定 `config/`、`components/`、`services/`、`models/`、`assets/`、`utils/`、`hooks/` 时,不得生成同职责的根目录平铺文件。
34
+ - 项目规定 `propsType.ts` 时不得擅自生成 `type.ts` 或 `types.ts`;页面内子组件的独立目录、样式 colocate 和 barrel 导出以项目证据为准。
35
+ - 项目 request 已有全局错误拦截时,不得重复包裹请求 `try/catch`,也不得重复判断 `res.status === 200`。
31
36
  - 禁止模板中的序号分隔注释。
32
37
  - 禁止默认 placeholder、无用 import、空 catch 和静默失败。
33
38
  - 仅创建需求需要的目录和文件。
34
39
 
35
40
  ## 验证命令
36
41
 
37
- 1. `validate-scaffold.mjs`
42
+ 1. `validate-scaffold.mjs --profile <ProjectProfile JSON>`
38
43
  2. 目标项目 typecheck
39
44
  3. 目标项目 lint
40
45
  4. 相关单元/集成测试
@@ -3,6 +3,7 @@
3
3
  ## 目录
4
4
 
5
5
  - 输入归一化
6
+ - PC 系统交互基线
6
7
  - 场景匹配
7
8
  - Sisyphus 校准
8
9
  - 能力缺口检查
@@ -20,6 +21,17 @@
20
21
  - 项目推断:路由、请求、状态、目录约定
21
22
  - 未决事项:缺少且会影响生成结果的信息
22
23
 
24
+ ## PC 系统交互基线
25
+
26
+ 用户明确的业务需求始终是必须满足的上层约束。对 PC 系统建模和生成时,其余证据按以下顺序决策:
27
+
28
+ 1. 注册场景结构:确定页面结构、导航关系、表单所有权和校验边界。
29
+ 2. Sisyphus MCP API/demo:确定组件、公开属性、类型和组合方式。
30
+ 3. 项目既有约定:只适配路由、请求、权限、状态、目录和命名。
31
+ 4. 截图差异方案:最后处理间距、尺寸、颜色等视觉差异。
32
+
33
+ 低优先级证据不得覆盖高优先级基线。发生冲突时保留高优先级实现,并把未采纳的差异写入交付说明;截图不得驱动已确认交互结构的重构。强制安全规则或无法绕过的项目规则与基线冲突时停止生成并报告。
34
+
23
35
  ## 场景匹配
24
36
 
25
37
  使用 `assets/templates/scene-registry.json` 计算需求能力与注册场景能力的覆盖关系:
@@ -85,6 +85,88 @@ const collectInstructionFiles = (start, boundary) => {
85
85
  return [...new Set(files)];
86
86
  };
87
87
 
88
+ const readText = (filePath) => {
89
+ try {
90
+ return fs.readFileSync(filePath, 'utf8');
91
+ } catch {
92
+ return '';
93
+ }
94
+ };
95
+
96
+ const resolveProjectPath = (reference, sourceFile, projectRoot) => {
97
+ const candidates = [
98
+ path.resolve(projectRoot, reference),
99
+ path.resolve(path.dirname(sourceFile), reference),
100
+ ];
101
+ return candidates.find(
102
+ (candidate) => isWithin(projectRoot, candidate) && fs.existsSync(candidate),
103
+ );
104
+ };
105
+
106
+ const collectReferencedInstructionFiles = (instructionFiles, projectRoot) => {
107
+ const references = [];
108
+ for (const instructionFile of instructionFiles) {
109
+ const content = readText(instructionFile);
110
+ for (const match of content.matchAll(/(?:`|\]\()([^`\s)]+\.md)(?:`|\))/g)) {
111
+ const resolved = resolveProjectPath(match[1], instructionFile, projectRoot);
112
+ if (resolved && fs.statSync(resolved).isFile()) references.push(resolved);
113
+ }
114
+ }
115
+ return [...new Set(references)];
116
+ };
117
+
118
+ const collectApprovedExamples = (instructionFiles, projectRoot) => {
119
+ const examples = [];
120
+ for (const instructionFile of instructionFiles) {
121
+ const content = readText(instructionFile);
122
+ for (const match of content.matchAll(/标杆样板[^`\n]*`([^`]+)`/g)) {
123
+ const resolved = resolveProjectPath(match[1], instructionFile, projectRoot);
124
+ if (resolved && fs.statSync(resolved).isDirectory()) examples.push(resolved);
125
+ }
126
+ }
127
+ return [...new Set(examples)];
128
+ };
129
+
130
+ const findInstructionEvidence = (files, patterns) =>
131
+ files.filter((filePath) => patterns.some((pattern) => pattern.test(readText(filePath))));
132
+
133
+ const buildFileStructureConventions = (files) => {
134
+ const content = files.map(readText).join('\n');
135
+ const conventions = {};
136
+ if (/\*\/assets\//.test(content)) conventions.assetsDirectory = 'assets';
137
+ if (/components\/index\.ts/.test(content)) conventions.componentBarrel = 'components/index.ts';
138
+ if (/components\/<ComponentName>\/index\.tsx/.test(content)) {
139
+ conventions.componentDirectory = 'components/<ComponentName>';
140
+ conventions.componentEntry = 'index.tsx';
141
+ }
142
+ if (/components\/<ComponentName>[^\n]*index\.less/.test(content)) {
143
+ conventions.componentStyle = 'index.less';
144
+ }
145
+ if (/\*\/components\//.test(content) || conventions.componentDirectory) {
146
+ conventions.componentsDirectory = 'components';
147
+ }
148
+ if (/\*\/config\//.test(content)) conventions.configDirectory = 'config';
149
+ if (/\*\/hooks\//.test(content)) conventions.hooksDirectory = 'hooks';
150
+ if (/\*\/models\//.test(content)) conventions.modelsDirectory = 'models';
151
+ if (/\*\/services\//.test(content)) conventions.servicesDirectory = 'services';
152
+ if (/propsType\.ts/.test(content)) conventions.typeFile = 'propsType.ts';
153
+ if (/\*\/utils\//.test(content)) conventions.utilsDirectory = 'utils';
154
+ return conventions;
155
+ };
156
+
157
+ const buildRequestConventions = (files) => {
158
+ const content = files.map(readText).join('\n');
159
+ const conventions = {};
160
+ if (/request[^\n]*全局[^\n]*错误拦截|全局[^\n]*错误拦截[^\n]*request/i.test(content)) {
161
+ conventions.globalErrorHandling = true;
162
+ }
163
+ if (/不需要[^\n]*try\s*catch/i.test(content)) conventions.omitTryCatch = true;
164
+ if (/不需要判断[^\n]*res\.status\s*={2,3}\s*200/i.test(content)) {
165
+ conventions.omitStatusCheck = true;
166
+ }
167
+ return conventions;
168
+ };
169
+
88
170
  const walkFiles = (root, limit = 600) => {
89
171
  const results = [];
90
172
  const queue = [root];
@@ -172,6 +254,25 @@ const stateEvidence = findEvidence(files, [
172
254
  /\buseSetState\s*\(/,
173
255
  /\bcreateContext\s*\(/,
174
256
  ]);
257
+ const instructionFiles = collectInstructionFiles(targetDirectory, projectRoot);
258
+ const referencedInstructionFiles = collectReferencedInstructionFiles(
259
+ instructionFiles,
260
+ projectRoot,
261
+ );
262
+ const conventionFiles = [...instructionFiles, ...referencedInstructionFiles];
263
+ const fileStructureConventions = buildFileStructureConventions(conventionFiles);
264
+ const structureEvidence = findInstructionEvidence(conventionFiles, [
265
+ /\*\/(?:assets|components|config|hooks|models|services|utils)\//,
266
+ /components\/<ComponentName>\/index\.(?:tsx|less)/,
267
+ /components\/index\.ts/,
268
+ /propsType\.ts/,
269
+ ]);
270
+ const requestConventions = buildRequestConventions(conventionFiles);
271
+ const requestConventionEvidence = findInstructionEvidence(conventionFiles, [
272
+ /全局[^\n]*错误拦截/,
273
+ /不需要[^\n]*try\s*catch/i,
274
+ /不需要判断[^\n]*res\.status/i,
275
+ ]);
175
276
 
176
277
  let framework = 'react';
177
278
  let routingKind = 'callback';
@@ -194,9 +295,7 @@ const hasPropsTypeDeclaration = files.some((filePath) => filePath.endsWith('prop
194
295
  const hasPropsTypeModule = files.some((filePath) => filePath.endsWith('propsType.ts'));
195
296
  const typeConvention = hasPropsTypeDeclaration
196
297
  ? 'propsType.d.ts'
197
- : hasPropsTypeModule
198
- ? 'propsType.ts'
199
- : undefined;
298
+ : fileStructureConventions.typeFile || (hasPropsTypeModule ? 'propsType.ts' : undefined);
200
299
 
201
300
  const availableCommands = Object.keys(packageJson.scripts || {}).filter((name) =>
202
301
  /(type|tsc|lint|test|build|start|dev)/i.test(name),
@@ -215,14 +314,26 @@ const profile = {
215
314
  routingAdapter: buildAdapter(routingKind, routingImportSource, routingEvidence, {
216
315
  injectCallbacksWhenUnknown: true,
217
316
  }),
218
- requestAdapter: buildAdapter('project-request', undefined, requestEvidence),
317
+ requestAdapter: buildAdapter(
318
+ 'project-request',
319
+ undefined,
320
+ [...new Set([...requestEvidence, ...requestConventionEvidence])],
321
+ requestConventions,
322
+ ),
219
323
  permissionAdapter: buildAdapter('project-permission', undefined, permissionEvidence),
220
324
  stateAdapter: buildAdapter('project-state', undefined, stateEvidence),
221
- instructionFiles: collectInstructionFiles(targetDirectory, projectRoot),
325
+ fileStructureAdapter: buildAdapter(
326
+ 'project-file-structure',
327
+ undefined,
328
+ structureEvidence,
329
+ fileStructureConventions,
330
+ ),
331
+ instructionFiles,
332
+ referencedInstructionFiles,
222
333
  ...(typeConvention ? { typeConvention } : {}),
223
334
  styleConvention: files.some((filePath) => filePath.endsWith('.less')) ? 'less' : undefined,
224
335
  availableCommands,
225
- approvedExamples: [],
336
+ approvedExamples: collectApprovedExamples(conventionFiles, projectRoot),
226
337
  };
227
338
 
228
339
  process.stdout.write(`${JSON.stringify(profile, null, 2)}\n`);
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import fs from 'node:fs';
4
+ import { createRequire } from 'node:module';
4
5
  import path from 'node:path';
5
6
  import process from 'node:process';
6
7
  import { fileURLToPath } from 'node:url';
@@ -36,6 +37,12 @@ const readDecisions = (filePath) => {
36
37
  return Array.isArray(parsed) ? parsed : parsed.decisions || [];
37
38
  };
38
39
 
40
+ const readProfile = (root, inputPath) => {
41
+ if (!inputPath) return {};
42
+ const profilePath = path.resolve(root, String(inputPath));
43
+ return JSON.parse(fs.readFileSync(profilePath, 'utf8'));
44
+ };
45
+
39
46
  const extractNamedImports = (content, source) => {
40
47
  const escaped = source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41
48
  const regex = new RegExp(
@@ -56,6 +63,59 @@ const extractNamedImports = (content, source) => {
56
63
  return names;
57
64
  };
58
65
 
66
+ const hasRequestClientImport = (content) => {
67
+ for (const match of content.matchAll(/import\s+([\s\S]*?)\s+from\s+['"][^'"]+['"]/g)) {
68
+ if (/\b(?:request|axios)\b/.test(match[1])) return true;
69
+ }
70
+ return false;
71
+ };
72
+
73
+ const loadTypeScript = (root) => {
74
+ const resolvers = [createRequire(path.join(root, 'package.json')), createRequire(import.meta.url)];
75
+ for (const resolver of resolvers) {
76
+ try {
77
+ return resolver('typescript');
78
+ } catch {
79
+ continue;
80
+ }
81
+ }
82
+ return null;
83
+ };
84
+
85
+ const findNonArrowFunctions = (typescript, filePath, content) => {
86
+ const scriptKind = filePath.endsWith('.tsx')
87
+ ? typescript.ScriptKind.TSX
88
+ : typescript.ScriptKind.TS;
89
+ const sourceFile = typescript.createSourceFile(
90
+ filePath,
91
+ content,
92
+ typescript.ScriptTarget.Latest,
93
+ true,
94
+ scriptKind,
95
+ );
96
+ const matches = [];
97
+ const visit = (node) => {
98
+ if (
99
+ typescript.isFunctionDeclaration(node) ||
100
+ typescript.isFunctionExpression(node) ||
101
+ typescript.isMethodDeclaration(node) ||
102
+ typescript.isGetAccessorDeclaration(node) ||
103
+ typescript.isSetAccessorDeclaration(node) ||
104
+ typescript.isConstructorDeclaration(node)
105
+ ) {
106
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
107
+ matches.push({
108
+ column: position.character + 1,
109
+ kind: typescript.SyntaxKind[node.kind],
110
+ line: position.line + 1,
111
+ });
112
+ }
113
+ typescript.forEachChild(node, visit);
114
+ };
115
+ visit(sourceFile);
116
+ return matches;
117
+ };
118
+
59
119
  const args = parseArgs(process.argv.slice(2));
60
120
  const root = path.resolve(String(args.root || process.cwd()));
61
121
  if (!args.files) throw new Error('必须传入 --files <逗号分隔文件>');
@@ -67,6 +127,10 @@ const files = String(args.files)
67
127
  .filter(Boolean);
68
128
  const decisionsPath = path.resolve(root, String(args.decisions));
69
129
  const decisions = readDecisions(decisionsPath);
130
+ const profile = readProfile(root, args.profile);
131
+ const fileStructureConventions = profile.fileStructureAdapter?.conventions || {};
132
+ const requestConventions = profile.requestAdapter?.conventions || {};
133
+ const typescript = loadTypeScript(root);
70
134
  const errors = [];
71
135
  const warnings = [];
72
136
  const checks = [];
@@ -173,6 +237,90 @@ for (const filePath of files) {
173
237
  }
174
238
  const content = fs.readFileSync(filePath, 'utf8');
175
239
  const relative = path.relative(root, filePath);
240
+ const pathSegments = relative.split(path.sep);
241
+ const basename = path.basename(filePath);
242
+
243
+ if (/\.tsx?$/.test(filePath)) {
244
+ if (!typescript) {
245
+ addError(
246
+ relative,
247
+ 'typescript-parser-unavailable',
248
+ '无法加载 TypeScript 解析器,不能验证箭头函数硬规则',
249
+ );
250
+ } else {
251
+ for (const match of findNonArrowFunctions(typescript, filePath, content)) {
252
+ addError(
253
+ relative,
254
+ 'function-syntax',
255
+ `禁用 ${match.kind}(${match.line}:${match.column}),所有运行时函数必须使用箭头函数`,
256
+ );
257
+ }
258
+ }
259
+ }
260
+
261
+ if (
262
+ fileStructureConventions.configDirectory &&
263
+ /^config\.[jt]sx?$/.test(basename) &&
264
+ !pathSegments.includes(fileStructureConventions.configDirectory)
265
+ ) {
266
+ addError(
267
+ relative,
268
+ 'config-directory',
269
+ `表单和表格配置必须收拢到 ${fileStructureConventions.configDirectory}/ 目录`,
270
+ );
271
+ }
272
+
273
+ if (
274
+ fileStructureConventions.typeFile &&
275
+ /^(?:type|types)\.[jt]sx?$/.test(basename) &&
276
+ basename !== fileStructureConventions.typeFile
277
+ ) {
278
+ addError(
279
+ relative,
280
+ 'type-file',
281
+ `类型定义必须使用项目约定文件 ${fileStructureConventions.typeFile}`,
282
+ );
283
+ }
284
+
285
+ const hasDirectRequest =
286
+ hasRequestClientImport(content) &&
287
+ /\b(?:request|axios)(?:\.[A-Za-z]+)?\s*(?:<[^>]+>)?\s*\(/.test(content);
288
+ if (
289
+ hasDirectRequest &&
290
+ fileStructureConventions.servicesDirectory &&
291
+ !pathSegments.includes(fileStructureConventions.servicesDirectory)
292
+ ) {
293
+ addError(
294
+ relative,
295
+ 'service-directory',
296
+ `请求和业务逻辑必须收拢到 ${fileStructureConventions.servicesDirectory}/ 目录`,
297
+ );
298
+ }
299
+ if (
300
+ hasDirectRequest &&
301
+ requestConventions.globalErrorHandling &&
302
+ requestConventions.omitTryCatch &&
303
+ /try\s*\{[\s\S]*?\b(?:request|axios)(?:\.[A-Za-z]+)?\s*(?:<[^>]+>)?\s*\([\s\S]*?catch(?:\s*\([^)]*\))?\s*\{/.test(
304
+ content,
305
+ )
306
+ ) {
307
+ addError(
308
+ relative,
309
+ 'request-global-error-handling',
310
+ '项目 request 已有全局错误拦截,请求调用不得重复添加 try/catch',
311
+ );
312
+ }
313
+ if (
314
+ hasDirectRequest &&
315
+ requestConventions.omitStatusCheck &&
316
+ /\b(?:res|response)\.status\s*={2,3}\s*200\b/.test(content)
317
+ ) {
318
+ addError(
319
+ relative,
320
+ 'request-status-check',
321
+ '项目 request 已统一处理响应,请勿重复判断 res.status === 200',
322
+ );
323
+ }
176
324
 
177
325
  const forbiddenPatterns = [
178
326
  { rule: 'antd4', pattern: /from\s+['"]antd4['"]/, message: '禁止使用 antd4' },