@zat-design/sisyphus-scene 1.0.0
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/README.md +44 -0
- package/bin/sisyphus-scene.mjs +81 -0
- package/mcp/sisyphus-react.json +8 -0
- package/package.json +36 -0
- package/scripts/install-mcp.mjs +158 -0
- package/scripts/sync-platforms.mjs +87 -0
- package/skills/sisyphus-scene/SKILL.md +149 -0
- package/skills/sisyphus-scene/agents/openai.yaml +8 -0
- package/skills/sisyphus-scene/assets/templates/scene-registry.json +75 -0
- package/skills/sisyphus-scene/evals/fixtures/invalid-antd-fallback.tsx +5 -0
- package/skills/sisyphus-scene/evals/fixtures/invalid-decisions.json +11 -0
- package/skills/sisyphus-scene/evals/fixtures/valid-decisions.json +21 -0
- package/skills/sisyphus-scene/evals/fixtures/valid-page.tsx +25 -0
- package/skills/sisyphus-scene/evals/generation-cases.yaml +36 -0
- package/skills/sisyphus-scene/evals/trigger-cases.yaml +13 -0
- package/skills/sisyphus-scene/references/approved-examples.yaml +16 -0
- package/skills/sisyphus-scene/references/integration-contracts.md +112 -0
- package/skills/sisyphus-scene/references/platform-claude.md +10 -0
- package/skills/sisyphus-scene/references/platform-codex.md +10 -0
- package/skills/sisyphus-scene/references/platform-cursor.md +10 -0
- package/skills/sisyphus-scene/references/scene-model.md +51 -0
- package/skills/sisyphus-scene/references/validation-rules.md +45 -0
- package/skills/sisyphus-scene/references/workflow.md +94 -0
- package/skills/sisyphus-scene/scripts/inspect-project.mjs +229 -0
- package/skills/sisyphus-scene/scripts/install.mjs +100 -0
- package/skills/sisyphus-scene/scripts/resolve-versions.mjs +204 -0
- package/skills/sisyphus-scene/scripts/validate-scaffold.mjs +158 -0
- package/skills/sisyphus-scene/scripts/validate-skill.mjs +48 -0
- package/skills/sisyphus-scene-legacy/SKILL.md +614 -0
- package/skills/sisyphus-scene-legacy/agents/openai.yaml +7 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"requirementId": "search-form",
|
|
4
|
+
"requirement": "查询表单",
|
|
5
|
+
"status": "covered",
|
|
6
|
+
"source": "sisyphus",
|
|
7
|
+
"component": "ProForm",
|
|
8
|
+
"apiEvidence": "sisyphus-get-component-api(ProForm)",
|
|
9
|
+
"demoEvidence": "sisyphus-get-examples(ProForm, 查询)"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"requirementId": "data-table",
|
|
13
|
+
"requirement": "数据表格",
|
|
14
|
+
"status": "covered",
|
|
15
|
+
"source": "sisyphus",
|
|
16
|
+
"component": "ProTable",
|
|
17
|
+
"apiEvidence": "sisyphus-get-component-api(ProTable)",
|
|
18
|
+
"demoEvidence": "sisyphus-get-examples(ProTable, 查询列表)"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Button } from 'antd';
|
|
2
|
+
import { ProForm, ProTable } from '@zat-design/sisyphus-react';
|
|
3
|
+
|
|
4
|
+
const { useAntdTable } = ProTable;
|
|
5
|
+
|
|
6
|
+
export const PolicyQueryPage = () => {
|
|
7
|
+
const [form] = ProForm.useForm();
|
|
8
|
+
const { tableProps, onSearch, onReset } = useAntdTable(async () => ({ data: { list: [], total: 0 } }), {
|
|
9
|
+
form,
|
|
10
|
+
rowKey: 'id',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<ProForm form={form} columns={[]} onFinish={onSearch} onCancel={onReset} />
|
|
16
|
+
<ProTable
|
|
17
|
+
tableId="policy-query-table"
|
|
18
|
+
columns={[]}
|
|
19
|
+
headerRender={<Button type="primary">新增</Button>}
|
|
20
|
+
{...tableProps}
|
|
21
|
+
/>
|
|
22
|
+
</>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
schema_version: 1
|
|
2
|
+
cases:
|
|
3
|
+
- id: exact-query-list
|
|
4
|
+
match_level: exact
|
|
5
|
+
expected_path: registered-template-plus-sisyphus-calibration
|
|
6
|
+
- id: partial-tab-list-batch-actions
|
|
7
|
+
match_level: partial
|
|
8
|
+
expected_path: primitives-plus-sisyphus-demos
|
|
9
|
+
- id: none-new-layout
|
|
10
|
+
match_level: none
|
|
11
|
+
expected_path: temporary-scene-from-sisyphus-demos
|
|
12
|
+
- id: explicit-sisyphus-gap
|
|
13
|
+
sisyphus_status: missing
|
|
14
|
+
expected_path: antd-mcp-fallback-with-reason
|
|
15
|
+
- id: unjustified-antd-rewrite
|
|
16
|
+
sisyphus_status: covered
|
|
17
|
+
expected_result: validation-failure
|
|
18
|
+
- id: unverified-is-not-missing
|
|
19
|
+
sisyphus_status: unverified
|
|
20
|
+
expected_result: generation-blocked
|
|
21
|
+
- id: project-component-fallback
|
|
22
|
+
sisyphus_status: missing
|
|
23
|
+
antd_status: missing
|
|
24
|
+
project_component_status: reusable
|
|
25
|
+
expected_path: project-component-with-evidence
|
|
26
|
+
- id: unresolved-capability
|
|
27
|
+
sisyphus_status: missing
|
|
28
|
+
antd_status: missing
|
|
29
|
+
project_component_status: unavailable
|
|
30
|
+
expected_result: generation-blocked
|
|
31
|
+
framework_cases:
|
|
32
|
+
- umi-max
|
|
33
|
+
- react-router
|
|
34
|
+
- callback-navigation
|
|
35
|
+
- no-navigation
|
|
36
|
+
- custom-request-wrapper
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
schema_version: 1
|
|
2
|
+
should_trigger:
|
|
3
|
+
- "根据这张截图生成一个保单查询列表页骨架"
|
|
4
|
+
- "做一个带状态 Tab 和新增抽屉的理赔配置页面"
|
|
5
|
+
- "参考 claim 应用实现一个多 Tab 详情编辑页"
|
|
6
|
+
- "这个布局不在已有场景里,请根据 Sisyphus demo 组合"
|
|
7
|
+
- "做一个右侧步骤锚点、统一校验的长表单"
|
|
8
|
+
should_not_trigger:
|
|
9
|
+
- "解释一下 React useEffect"
|
|
10
|
+
- "修复这个现有组件的业务 bug"
|
|
11
|
+
- "给 antd4 项目生成 CRUD 页面"
|
|
12
|
+
- "给 Sisyphus v3 项目兼容一个旧页面"
|
|
13
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
schema_version: 1
|
|
2
|
+
examples: []
|
|
3
|
+
|
|
4
|
+
# 仅登记经过团队确认的样例。候选代码不得直接标记 approved。
|
|
5
|
+
# 示例结构:
|
|
6
|
+
# - id: claim-litigation-list
|
|
7
|
+
# status: approved
|
|
8
|
+
# application: xk-wjs-claim-web
|
|
9
|
+
# capabilities: [search-form, data-table, table-tabs, drawer-form]
|
|
10
|
+
# sisyphus_range: ">=4 <5"
|
|
11
|
+
# antd_range: ">=6 <7"
|
|
12
|
+
# paths:
|
|
13
|
+
# - src/pages/litigationManagement/index.tsx
|
|
14
|
+
# verified_at: 2026-07-10
|
|
15
|
+
# notes: 只复用结构,不覆盖 MCP API 证据
|
|
16
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# 数据与项目集成契约
|
|
2
|
+
|
|
3
|
+
## SceneSpec
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
type SceneMatchLevel = 'exact' | 'partial' | 'none';
|
|
7
|
+
type PageMode = 'list' | 'add' | 'edit' | 'view' | 'audit';
|
|
8
|
+
|
|
9
|
+
interface SceneSpec {
|
|
10
|
+
sceneName: string;
|
|
11
|
+
matchLevel: SceneMatchLevel;
|
|
12
|
+
capabilities: SceneCapability[];
|
|
13
|
+
pageName: string;
|
|
14
|
+
moduleName: string;
|
|
15
|
+
targetDirectory: string;
|
|
16
|
+
pageMode: PageMode;
|
|
17
|
+
fields: FieldSpec[];
|
|
18
|
+
tableColumns: TableColumnSpec[];
|
|
19
|
+
sections: SectionSpec[];
|
|
20
|
+
actions: ActionSpec[];
|
|
21
|
+
apis: ApiSpec[];
|
|
22
|
+
requiredCapabilities: CapabilityRequirement[];
|
|
23
|
+
unresolvedItems: UnresolvedItem[];
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
未提供的字段、列、分区等使用空数组,不使用 `undefined` 混淆“未提取”和“不存在”。
|
|
28
|
+
|
|
29
|
+
## ProjectProfile
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
interface IntegrationAdapter {
|
|
33
|
+
kind: string;
|
|
34
|
+
importSource?: string;
|
|
35
|
+
evidenceFiles: string[];
|
|
36
|
+
confidence: 'confirmed' | 'inferred' | 'unknown';
|
|
37
|
+
conventions: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ProjectProfile {
|
|
41
|
+
projectRoot: string;
|
|
42
|
+
targetDirectory: string;
|
|
43
|
+
reactVersion?: string;
|
|
44
|
+
typescriptVersion?: string;
|
|
45
|
+
antdVersion?: string;
|
|
46
|
+
sisyphusVersion?: string;
|
|
47
|
+
framework?: string;
|
|
48
|
+
routingAdapter?: IntegrationAdapter;
|
|
49
|
+
requestAdapter?: IntegrationAdapter;
|
|
50
|
+
permissionAdapter?: IntegrationAdapter;
|
|
51
|
+
stateAdapter?: IntegrationAdapter;
|
|
52
|
+
instructionFiles: string[];
|
|
53
|
+
typeConvention?: string;
|
|
54
|
+
styleConvention?: string;
|
|
55
|
+
availableCommands: string[];
|
|
56
|
+
approvedExamples: string[];
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`framework` 只记录探测结果,不参与技能启用判断。没有导航需求时不得因为探测到框架而引入路由代码。
|
|
61
|
+
|
|
62
|
+
## CapabilityRequirement 与 ComponentDecision
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
type CapabilityStatus = 'covered' | 'partial' | 'missing' | 'unverified';
|
|
66
|
+
type ComponentSource = 'sisyphus' | 'antd' | 'project' | 'unresolved';
|
|
67
|
+
|
|
68
|
+
interface CapabilityRequirement {
|
|
69
|
+
id: string;
|
|
70
|
+
description: string;
|
|
71
|
+
required: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ComponentDecision {
|
|
75
|
+
requirementId: string;
|
|
76
|
+
requirement: string;
|
|
77
|
+
status: CapabilityStatus;
|
|
78
|
+
source: ComponentSource;
|
|
79
|
+
component?: string;
|
|
80
|
+
apiEvidence?: string;
|
|
81
|
+
demoEvidence?: string;
|
|
82
|
+
fallbackReason?: string;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
规则:
|
|
87
|
+
|
|
88
|
+
- 每个必需 requirement 恰好一条 decision。
|
|
89
|
+
- `sisyphus` 来源必须有 API 和 demo 证据。
|
|
90
|
+
- `antd` 来源必须有 API/demo 证据与 fallbackReason。
|
|
91
|
+
- `project` 来源必须有源码路径与调用示例证据。
|
|
92
|
+
- `unresolved` 或 `unverified` 阻断生成。
|
|
93
|
+
|
|
94
|
+
## ComponentDocsProvider
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
interface ComponentDocsProvider {
|
|
98
|
+
listComponents(): Promise<ComponentSummary[]>;
|
|
99
|
+
getComponentApi(name: string): Promise<ComponentApi>;
|
|
100
|
+
getExamples(name: string, scenario?: string): Promise<ComponentExample[]>;
|
|
101
|
+
getVersion(): Promise<string>;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
平台适配器提供:
|
|
106
|
+
|
|
107
|
+
- `SisyphusMcpProvider`
|
|
108
|
+
- `AntdMcpProvider`
|
|
109
|
+
- `Context7AntdProvider`
|
|
110
|
+
|
|
111
|
+
这里只定义能力,不把任何平台的 MCP 工具前缀写入生成代码。
|
|
112
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Claude Code 平台适配
|
|
2
|
+
|
|
3
|
+
- 用户级安装目录:`~/.claude/skills/sisyphus-scene`
|
|
4
|
+
- 项目级安装目录:`.claude/skills/sisyphus-scene`
|
|
5
|
+
- 使用 `${CLAUDE_SKILL_DIR}` 定位 scripts、references 和 assets。
|
|
6
|
+
- 首先通过 ToolSearch 发现 `sisyphus-list-components`、`sisyphus-get-component-api`、`sisyphus-get-examples` 和 `sisyphus-search`。
|
|
7
|
+
- Ant Design MCP 不可用时,通过 Context7 查询 antd 6 官方文档。
|
|
8
|
+
- 读取项目 `CLAUDE.md`,同时显式检查 `AGENTS.md` 和 `.cursor/rules`;同级冲突时停止。
|
|
9
|
+
- 不使用 Claude 专属 frontmatter 驱动共享核心行为,保证同一 SKILL.md 可被其他平台读取。
|
|
10
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Codex 平台适配
|
|
2
|
+
|
|
3
|
+
- 用户级安装目录:`~/.agents/skills/sisyphus-scene`
|
|
4
|
+
- 项目级安装目录:`.agents/skills/sisyphus-scene`
|
|
5
|
+
- 以当前加载的 SKILL.md 所在目录解析相对资源。
|
|
6
|
+
- 从当前工具列表或 MCP 资源中发现 Sisyphus 工具;工具未安装时停止并给出配置建议,不读取 node_modules 替代。
|
|
7
|
+
- `agents/openai.yaml` 只承载 UI 元数据。当前 Sisyphus MCP 是本地 stdio 命令,没有可验证的 streamable HTTP URL,因此不伪造 Codex MCP dependency。
|
|
8
|
+
- 读取最近层级的 `AGENTS.md`,并显式检查 `CLAUDE.md` 和 `.cursor/rules`。
|
|
9
|
+
- 修改已有代码符号前遵循项目 GitNexus impact 约束,完成后执行 detect changes。
|
|
10
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Cursor 平台适配
|
|
2
|
+
|
|
3
|
+
- 用户级安装目录:`~/.cursor/skills/sisyphus-scene`
|
|
4
|
+
- 项目级安装目录:`.cursor/skills/sisyphus-scene`
|
|
5
|
+
- 以当前技能目录解析 scripts、references 和 assets。
|
|
6
|
+
- 从 Cursor MCP 配置发现 `sisyphus-react`、Ant Design 文档提供器和 Context7。
|
|
7
|
+
- 读取并应用 `.cursor/rules/*.mdc`,同时检查项目根 `AGENTS.md` 与 `CLAUDE.md`。
|
|
8
|
+
- Cursor Rules 是持久项目规范,Skill 是按需工作流;不要把整份团队规范复制进 SKILL.md。
|
|
9
|
+
- MCP 工具未出现或缓存版本与目标包不一致时,停止并要求刷新 MCP,不使用旧缓存继续生成。
|
|
10
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# 场景与能力模型
|
|
2
|
+
|
|
3
|
+
## 目录
|
|
4
|
+
|
|
5
|
+
- 页面原语
|
|
6
|
+
- 注册场景
|
|
7
|
+
- 识别规则
|
|
8
|
+
- 组合规则
|
|
9
|
+
|
|
10
|
+
## 页面原语
|
|
11
|
+
|
|
12
|
+
| 能力 | 说明 |
|
|
13
|
+
|---|---|
|
|
14
|
+
| search-form | 查询条件与查询/重置行为 |
|
|
15
|
+
| data-table | 表格、分页、加载和数据源 |
|
|
16
|
+
| table-tabs | 同列表状态过滤或独立子页面切换 |
|
|
17
|
+
| drawer-form | 新增、编辑、查看弹窗或抽屉 |
|
|
18
|
+
| detail-navigation | 列表到详情、返回和 URL 状态 |
|
|
19
|
+
| detail-tabs | 横向 Tab 切换的详情/编辑分区 |
|
|
20
|
+
| detail-sections | 纵向详情或折叠分区 |
|
|
21
|
+
| step-anchor-form | 纵向步骤锚点和跨分区统一校验 |
|
|
22
|
+
|
|
23
|
+
## 注册场景
|
|
24
|
+
|
|
25
|
+
| 中文场景 | 必需能力 | 关键识别信号 |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| 查询列表页 | search-form + data-table | 查询区 + 表格 |
|
|
28
|
+
| Tab 分组列表页 | search-form + data-table + table-tabs | 表格上方状态/业务 Tab |
|
|
29
|
+
| 列表弹窗操作页 | search-form + data-table + drawer-form | 头部或操作列打开表单 |
|
|
30
|
+
| 多 Tab 详情编辑页 | detail-tabs | 横向 Tab,一次显示一个分区 |
|
|
31
|
+
| 分区块详情页 | detail-sections | 纵向分区,无步骤锚点 |
|
|
32
|
+
| 列表跳转详情页 | data-table + detail-navigation + detail-* | 操作列进入独立详情路由 |
|
|
33
|
+
| 步骤条分区表单页 | detail-sections + step-anchor-form | 纵向分区 + 右侧步骤锚点 + 统一校验 |
|
|
34
|
+
|
|
35
|
+
## 识别规则
|
|
36
|
+
|
|
37
|
+
- 同一表格的数据状态过滤优先识别为 `table-tabs`,具体实现仍由 MCP 验证。
|
|
38
|
+
- 不同 Tab 是完全不同的子页面时,记录为多个子场景而不是单表格过滤。
|
|
39
|
+
- 横向 Tab 与纵向步骤锚点不可互换。
|
|
40
|
+
- 只读纵向详情不自动升级为跨 form 步骤表单。
|
|
41
|
+
- “列表跳转详情”是组合能力,不复制列表与详情模板。
|
|
42
|
+
- 截图缺失交互信息时保持 `partial` 或未决,不根据视觉相似度伪造行为。
|
|
43
|
+
|
|
44
|
+
## 组合规则
|
|
45
|
+
|
|
46
|
+
1. 以覆盖主要页面结构的场景作为基底。
|
|
47
|
+
2. 把额外能力作为页面原语追加。
|
|
48
|
+
3. 对每个追加原语查询 Sisyphus demo。
|
|
49
|
+
4. 组合后重新检查表单所有权、校验边界、数据源和导航职责。
|
|
50
|
+
5. 组合冲突时以用户明确交互为准,必要时改为 `none` 并构造临时场景。
|
|
51
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 生成后校验规则
|
|
2
|
+
|
|
3
|
+
## 版本和证据
|
|
4
|
+
|
|
5
|
+
- antd 必须是目标策略允许的最新 6.x。
|
|
6
|
+
- Sisyphus 必须是目标策略允许的最新 4.x。
|
|
7
|
+
- Sisyphus MCP 报告版本必须与目标版本一致。
|
|
8
|
+
- 每个 Sisyphus 组件必须有 API 与 demo 证据。
|
|
9
|
+
- 每个 antd fallback 必须有具体缺口和 fallbackReason。
|
|
10
|
+
|
|
11
|
+
## 组件边界
|
|
12
|
+
|
|
13
|
+
- 禁止用 antd 重写 Sisyphus 已覆盖能力。
|
|
14
|
+
- 禁止在 ProForm 中无证据地嵌套 Form.Item。
|
|
15
|
+
- 禁止用 antd Tabs 替代 ProTable 内置状态筛选。
|
|
16
|
+
- 禁止用 Modal/Drawer 替代 ProDrawerForm 已覆盖能力。
|
|
17
|
+
- 禁止 `@ts-nocheck`;精准类型逃逸必须有原因。
|
|
18
|
+
|
|
19
|
+
## 项目解耦
|
|
20
|
+
|
|
21
|
+
- 无导航需求时不得出现路由 import。
|
|
22
|
+
- 只有证据确认后才引入 `@umijs/max`、`react-router-dom` 或其他路由库。
|
|
23
|
+
- service 调用方式必须来自目标项目证据,不写固定 axios/request 模板。
|
|
24
|
+
- 权限和状态实现必须来自目标项目证据。
|
|
25
|
+
- 不修改未授权的全局布局、入口、依赖或 MCP 配置。
|
|
26
|
+
|
|
27
|
+
## 结构和代码
|
|
28
|
+
|
|
29
|
+
- 组件使用 `.tsx`,纯逻辑使用 `.ts`,样式使用项目确认的 LESS 约定。
|
|
30
|
+
- 目录、类型文件、导出方式和 import 顺序遵循最近项目规则。
|
|
31
|
+
- 禁止模板中的序号分隔注释。
|
|
32
|
+
- 禁止默认 placeholder、无用 import、空 catch 和静默失败。
|
|
33
|
+
- 仅创建需求需要的目录和文件。
|
|
34
|
+
|
|
35
|
+
## 验证命令
|
|
36
|
+
|
|
37
|
+
1. `validate-scaffold.mjs`
|
|
38
|
+
2. 目标项目 typecheck
|
|
39
|
+
3. 目标项目 lint
|
|
40
|
+
4. 相关单元/集成测试
|
|
41
|
+
5. 有截图时进行页面视觉核对
|
|
42
|
+
6. GitNexus detect changes
|
|
43
|
+
|
|
44
|
+
任一检查未执行都必须在交付报告中标注“未验证”,不能省略。
|
|
45
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# 场景生成与能力补全工作流
|
|
2
|
+
|
|
3
|
+
## 目录
|
|
4
|
+
|
|
5
|
+
- 输入归一化
|
|
6
|
+
- 场景匹配
|
|
7
|
+
- Sisyphus 校准
|
|
8
|
+
- 能力缺口检查
|
|
9
|
+
- Ant Design 与项目组件补全
|
|
10
|
+
- 停止条件
|
|
11
|
+
|
|
12
|
+
## 输入归一化
|
|
13
|
+
|
|
14
|
+
将截图和自然语言统一转换为 `SceneSpec`。截图只提供视觉证据,不把不可见的接口、权限、路由和业务状态当作已知事实。
|
|
15
|
+
|
|
16
|
+
必须区分:
|
|
17
|
+
|
|
18
|
+
- 视觉确认:标题、字段标签、表格列、Tab、按钮、分区、布局
|
|
19
|
+
- 需求确认:按钮行为、校验、跳转、保存方式、权限
|
|
20
|
+
- 项目推断:路由、请求、状态、目录约定
|
|
21
|
+
- 未决事项:缺少且会影响生成结果的信息
|
|
22
|
+
|
|
23
|
+
## 场景匹配
|
|
24
|
+
|
|
25
|
+
使用 `assets/templates/scene-registry.json` 计算需求能力与注册场景能力的覆盖关系:
|
|
26
|
+
|
|
27
|
+
- `exact`:注册场景完整覆盖需求,且没有额外布局能力
|
|
28
|
+
- `partial`:某个注册场景覆盖主要结构,但存在新增或组合能力
|
|
29
|
+
- `none`:没有场景覆盖主要结构,或强行归类会改变页面交互
|
|
30
|
+
|
|
31
|
+
复合场景允许组合多个注册场景。例如“Tab 列表跳转多 Tab 详情”由 `tab-list + detail-navigation + tab-detail` 组成,不创建重复大模板。
|
|
32
|
+
|
|
33
|
+
## Sisyphus 校准
|
|
34
|
+
|
|
35
|
+
任何匹配级别都执行相同门禁:
|
|
36
|
+
|
|
37
|
+
1. MCP 列出当前版本组件。
|
|
38
|
+
2. 为候选组件读取完整 API。
|
|
39
|
+
3. 为具体交互读取 demo。
|
|
40
|
+
4. 记录版本、组件、API 和 demo 证据。
|
|
41
|
+
5. 用证据填充结构模板,不复制 demo 中与目标项目无关的代码。
|
|
42
|
+
|
|
43
|
+
### exact
|
|
44
|
+
|
|
45
|
+
加载结构模板,再用 MCP 校准 import、props、类型、组件组合和内置能力。
|
|
46
|
+
|
|
47
|
+
### partial
|
|
48
|
+
|
|
49
|
+
先组合已有页面原语,再针对差异查询 demo。不得因为“最接近某模板”而丢失用户明确提出的能力。
|
|
50
|
+
|
|
51
|
+
### none
|
|
52
|
+
|
|
53
|
+
从需求能力反向搜索 Sisyphus 组件与 demo,生成仅服务本次任务的临时场景结构。除非用户明确要求维护技能,否则不修改场景注册表。
|
|
54
|
+
|
|
55
|
+
## 能力缺口检查
|
|
56
|
+
|
|
57
|
+
每条 `CapabilityRequirement` 必须对应一条 `ComponentDecision`。
|
|
58
|
+
|
|
59
|
+
| 状态 | 判定 | 下一步 |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| covered | API 与 demo 足以实现 | 进入生成 |
|
|
62
|
+
| partial | 需要多个 Sisyphus 原语或项目适配 | 继续组合并复查 |
|
|
63
|
+
| missing | MCP 明确无对应能力 | 允许查询 Ant Design |
|
|
64
|
+
| unverified | MCP 不可用、版本不符或证据不足 | 立即阻断 |
|
|
65
|
+
|
|
66
|
+
不得把“搜索没找到”“白名单没写”“记忆中不存在”判为 `missing`。
|
|
67
|
+
|
|
68
|
+
## Ant Design 与项目组件补全
|
|
69
|
+
|
|
70
|
+
进入 Ant Design 前输出一条具体说明:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
缺口:<需求能力>
|
|
74
|
+
Sisyphus 证据:<查询过的组件/API/demo>
|
|
75
|
+
无法满足原因:<明确原因>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
随后查询 antd 6 的 MCP API 和 demo;平台没有 Ant Design MCP 时使用 Context7 官方文档。只实现缺口,不替换 Sisyphus 已覆盖部分。
|
|
79
|
+
|
|
80
|
+
Ant Design 仍无法覆盖时检索项目公共组件。只使用有源码、调用示例和目标项目兼容证据的组件。修改已有符号前执行 GitNexus impact。
|
|
81
|
+
|
|
82
|
+
## 停止条件
|
|
83
|
+
|
|
84
|
+
出现任一情况即停止代码生成:
|
|
85
|
+
|
|
86
|
+
- 项目不是最新目标版本的 antd 6 或 Sisyphus 4
|
|
87
|
+
- Sisyphus MCP 版本与目标版本不一致
|
|
88
|
+
- 必需能力处于 `unverified`
|
|
89
|
+
- 项目指令文件存在同级冲突
|
|
90
|
+
- Sisyphus、Ant Design 和项目组件均无法覆盖必需能力
|
|
91
|
+
- 无法确认会改变公共接口或业务数据流的关键需求
|
|
92
|
+
|
|
93
|
+
停止时输出已完成的证据、未解决能力、建议的独立前置任务,不用占位代码伪装完成。
|
|
94
|
+
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
|
|
7
|
+
const parseArgs = (argv) => {
|
|
8
|
+
const args = {};
|
|
9
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
10
|
+
const token = argv[index];
|
|
11
|
+
if (!token.startsWith('--')) continue;
|
|
12
|
+
const key = token.slice(2);
|
|
13
|
+
const value = argv[index + 1];
|
|
14
|
+
if (!value || value.startsWith('--')) {
|
|
15
|
+
args[key] = true;
|
|
16
|
+
} else {
|
|
17
|
+
args[key] = value;
|
|
18
|
+
index += 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return args;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const readJson = (filePath) => {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(`无法读取 JSON:${filePath}\n${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const isWithin = (parent, child) => {
|
|
33
|
+
const relative = path.relative(parent, child);
|
|
34
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const findNearestExistingDirectory = (inputPath) => {
|
|
38
|
+
let current = path.resolve(inputPath);
|
|
39
|
+
while (!fs.existsSync(current)) {
|
|
40
|
+
const parent = path.dirname(current);
|
|
41
|
+
if (parent === current) throw new Error(`目标路径不存在且找不到可用父目录:${inputPath}`);
|
|
42
|
+
current = parent;
|
|
43
|
+
}
|
|
44
|
+
const stat = fs.statSync(current);
|
|
45
|
+
return stat.isDirectory() ? current : path.dirname(current);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const findNearestPackageRoot = (start, boundary) => {
|
|
49
|
+
let current = start;
|
|
50
|
+
while (isWithin(boundary, current)) {
|
|
51
|
+
if (fs.existsSync(path.join(current, 'package.json'))) return current;
|
|
52
|
+
if (current === boundary) break;
|
|
53
|
+
current = path.dirname(current);
|
|
54
|
+
}
|
|
55
|
+
return fs.existsSync(path.join(boundary, 'package.json')) ? boundary : null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const collectAncestors = (start, boundary) => {
|
|
59
|
+
const directories = [];
|
|
60
|
+
let current = start;
|
|
61
|
+
while (isWithin(boundary, current)) {
|
|
62
|
+
directories.push(current);
|
|
63
|
+
if (current === boundary) break;
|
|
64
|
+
current = path.dirname(current);
|
|
65
|
+
}
|
|
66
|
+
return directories;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const collectInstructionFiles = (start, boundary) => {
|
|
70
|
+
const files = [];
|
|
71
|
+
for (const directory of collectAncestors(start, boundary)) {
|
|
72
|
+
for (const name of ['AGENTS.md', 'CLAUDE.md']) {
|
|
73
|
+
const candidate = path.join(directory, name);
|
|
74
|
+
if (fs.existsSync(candidate)) files.push(candidate);
|
|
75
|
+
}
|
|
76
|
+
const rulesDirectory = path.join(directory, '.cursor', 'rules');
|
|
77
|
+
if (fs.existsSync(rulesDirectory)) {
|
|
78
|
+
for (const entry of fs.readdirSync(rulesDirectory, { withFileTypes: true })) {
|
|
79
|
+
if (entry.isFile() && /\.(md|mdc)$/i.test(entry.name)) {
|
|
80
|
+
files.push(path.join(rulesDirectory, entry.name));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return [...new Set(files)];
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const walkFiles = (root, limit = 600) => {
|
|
89
|
+
const results = [];
|
|
90
|
+
const queue = [root];
|
|
91
|
+
const ignored = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.umi']);
|
|
92
|
+
while (queue.length > 0 && results.length < limit) {
|
|
93
|
+
const directory = queue.shift();
|
|
94
|
+
let entries = [];
|
|
95
|
+
try {
|
|
96
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
97
|
+
} catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (ignored.has(entry.name)) continue;
|
|
102
|
+
const fullPath = path.join(directory, entry.name);
|
|
103
|
+
if (entry.isDirectory()) queue.push(fullPath);
|
|
104
|
+
if (entry.isFile() && /\.(ts|tsx|js|jsx|less)$/i.test(entry.name)) results.push(fullPath);
|
|
105
|
+
if (results.length >= limit) break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return results;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const findEvidence = (files, patterns, limit = 8) => {
|
|
112
|
+
const matches = [];
|
|
113
|
+
for (const filePath of files) {
|
|
114
|
+
let content = '';
|
|
115
|
+
try {
|
|
116
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (patterns.some((pattern) => pattern.test(content))) matches.push(filePath);
|
|
121
|
+
if (matches.length >= limit) break;
|
|
122
|
+
}
|
|
123
|
+
return matches;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const buildAdapter = (kind, importSource, evidenceFiles, conventions = {}) => ({
|
|
127
|
+
kind,
|
|
128
|
+
...(importSource ? { importSource } : {}),
|
|
129
|
+
evidenceFiles,
|
|
130
|
+
confidence: evidenceFiles.length > 0 ? 'confirmed' : 'unknown',
|
|
131
|
+
conventions,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const args = parseArgs(process.argv.slice(2));
|
|
135
|
+
const projectRoot = path.resolve(String(args.root || process.cwd()));
|
|
136
|
+
if (!fs.existsSync(projectRoot) || !fs.statSync(projectRoot).isDirectory()) {
|
|
137
|
+
throw new Error(`项目根目录不存在:${projectRoot}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const requestedTarget = path.resolve(projectRoot, String(args.target || '.'));
|
|
141
|
+
const targetDirectory = findNearestExistingDirectory(requestedTarget);
|
|
142
|
+
if (!isWithin(projectRoot, targetDirectory)) {
|
|
143
|
+
throw new Error(`目标目录必须位于项目根目录内:${targetDirectory}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const packageRoot = findNearestPackageRoot(targetDirectory, projectRoot);
|
|
147
|
+
if (!packageRoot) throw new Error(`未在项目范围内找到 package.json:${projectRoot}`);
|
|
148
|
+
|
|
149
|
+
const packageJson = readJson(path.join(packageRoot, 'package.json'));
|
|
150
|
+
const dependencies = {
|
|
151
|
+
...(packageJson.dependencies || {}),
|
|
152
|
+
...(packageJson.devDependencies || {}),
|
|
153
|
+
...(packageJson.peerDependencies || {}),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const files = walkFiles(targetDirectory);
|
|
157
|
+
const routingEvidence = findEvidence(files, [
|
|
158
|
+
/from\s+['"]@umijs\/max['"]/,
|
|
159
|
+
/from\s+['"]react-router-dom['"]/,
|
|
160
|
+
/\buseNavigate\s*\(/,
|
|
161
|
+
/\bhistory\.(push|replace)\s*\(/,
|
|
162
|
+
]);
|
|
163
|
+
const requestEvidence = findEvidence(files, [
|
|
164
|
+
/from\s+['"][^'"]*(request|axios|intercept)[^'"]*['"]/i,
|
|
165
|
+
/\bfetch\s*\(/,
|
|
166
|
+
]);
|
|
167
|
+
const permissionEvidence = findEvidence(files, [
|
|
168
|
+
/\b(ButtonAuth|isAuthCode|Access|useAccess|permission)\b/,
|
|
169
|
+
]);
|
|
170
|
+
const stateEvidence = findEvidence(files, [
|
|
171
|
+
/from\s+['"](jotai|zustand|react-redux|redux)['"]/,
|
|
172
|
+
/\buseSetState\s*\(/,
|
|
173
|
+
/\bcreateContext\s*\(/,
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
let framework = 'react';
|
|
177
|
+
let routingKind = 'callback';
|
|
178
|
+
let routingImportSource;
|
|
179
|
+
if (dependencies['@umijs/max']) {
|
|
180
|
+
framework = 'umi-max';
|
|
181
|
+
routingKind = 'umi-max';
|
|
182
|
+
routingImportSource = '@umijs/max';
|
|
183
|
+
} else if (dependencies['react-router-dom']) {
|
|
184
|
+
framework = 'react-router';
|
|
185
|
+
routingKind = 'react-router';
|
|
186
|
+
routingImportSource = 'react-router-dom';
|
|
187
|
+
} else if (dependencies.next) {
|
|
188
|
+
framework = 'next';
|
|
189
|
+
routingKind = 'next';
|
|
190
|
+
routingImportSource = 'next/navigation';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const hasPropsTypeDeclaration = files.some((filePath) => filePath.endsWith('propsType.d.ts'));
|
|
194
|
+
const hasPropsTypeModule = files.some((filePath) => filePath.endsWith('propsType.ts'));
|
|
195
|
+
const typeConvention = hasPropsTypeDeclaration
|
|
196
|
+
? 'propsType.d.ts'
|
|
197
|
+
: hasPropsTypeModule
|
|
198
|
+
? 'propsType.ts'
|
|
199
|
+
: undefined;
|
|
200
|
+
|
|
201
|
+
const availableCommands = Object.keys(packageJson.scripts || {}).filter((name) =>
|
|
202
|
+
/(type|tsc|lint|test|build|start|dev)/i.test(name),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const profile = {
|
|
206
|
+
projectRoot,
|
|
207
|
+
packageRoot,
|
|
208
|
+
targetDirectory: requestedTarget,
|
|
209
|
+
inspectedDirectory: targetDirectory,
|
|
210
|
+
reactVersion: dependencies.react,
|
|
211
|
+
typescriptVersion: dependencies.typescript,
|
|
212
|
+
antdVersion: dependencies.antd,
|
|
213
|
+
sisyphusVersion: dependencies['@zat-design/sisyphus-react'],
|
|
214
|
+
framework,
|
|
215
|
+
routingAdapter: buildAdapter(routingKind, routingImportSource, routingEvidence, {
|
|
216
|
+
injectCallbacksWhenUnknown: true,
|
|
217
|
+
}),
|
|
218
|
+
requestAdapter: buildAdapter('project-request', undefined, requestEvidence),
|
|
219
|
+
permissionAdapter: buildAdapter('project-permission', undefined, permissionEvidence),
|
|
220
|
+
stateAdapter: buildAdapter('project-state', undefined, stateEvidence),
|
|
221
|
+
instructionFiles: collectInstructionFiles(targetDirectory, projectRoot),
|
|
222
|
+
...(typeConvention ? { typeConvention } : {}),
|
|
223
|
+
styleConvention: files.some((filePath) => filePath.endsWith('.less')) ? 'less' : undefined,
|
|
224
|
+
availableCommands,
|
|
225
|
+
approvedExamples: [],
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
process.stdout.write(`${JSON.stringify(profile, null, 2)}\n`);
|
|
229
|
+
|