@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.
- package/mcp/sisyphus-react.json +1 -1
- package/package.json +1 -1
- package/skills/sisyphus-scene/SKILL.md +22 -3
- package/skills/sisyphus-scene/assets/ai-contract.json +1 -1
- package/skills/sisyphus-scene/evals/fixtures/valid-decisions.json +4 -4
- package/skills/sisyphus-scene/evals/generation-cases.yaml +19 -0
- package/skills/sisyphus-scene/references/approved-examples.yaml +1 -1
- package/skills/sisyphus-scene/references/integration-contracts.md +4 -0
- package/skills/sisyphus-scene/references/validation-rules.md +7 -2
- package/skills/sisyphus-scene/references/workflow.md +12 -0
- package/skills/sisyphus-scene/scripts/inspect-project.mjs +117 -6
- package/skills/sisyphus-scene/scripts/validate-scaffold.mjs +148 -0
package/mcp/sisyphus-react.json
CHANGED
package/package.json
CHANGED
|
@@ -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。
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
"apiEvidence": {
|
|
9
9
|
"tool": "sisyphus-get-component-api",
|
|
10
10
|
"target": "ProForm",
|
|
11
|
-
"libraryVersion": "4.6.1
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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(
|
|
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
|
-
|
|
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' },
|