Files
吴让宇 c5905a5b1e refactor: 本仓升为唯一源(原 sap-cli 源码仓归档)
方向反转:此前 SKILL.md 是「模板渲染产物」、sap-cli 是源;现 sap-cli 归档,
sap-cli-skill 承接开发与分发,SKILL.md 回归手工维护的正本。

迁移(来自 sap-cli,共 104 文件):
- tests/           692 例测试(15 个文件的内联 sys.path 改指 assets/)
- openspec/        SDD 规格与归档变更(42 文件)
- docs/            开发文档与 ADT 原理(含 dev/CLAUDE.md、AGENTS.md)
- .claude/         rules 副本 + settings.json(供 Claude Code)
- .github/ .hermes/ .pre-commit-config.yaml .editorconfig CLAUDE.md
- scripts/ 保持仅 setup.py(pack_skill.py 已随旧仓归档,不迁)

修复(迁移暴露的真实缺陷):
- assets/pyproject.toml 的 build-backend 写作 `setuptools.backends._legacy:_Backend`,
  该模块在 setuptools 中不存在 → `pip install -e` 从来装不上。改为 build_meta。
  实测:临时 venv 安装成功,sap-cli --help 正常列出 31 个命令
- pyproject readme 指向不存在的 assets/README.md(editable 安装会失败)→ 改内联文本
- pyproject urls 改指 sap-cli-skill

机制调整:
- .github/workflows/ci.yml 适配 assets/ 布局;顶部注明该工作流仅 GitHub 执行,
  本仓在 Gitee 不会自动跑
- pre-commit 增本地测试门禁(Gitee 上真正生效的那道)
- .gitignore 合并旧仓完整规则(保留 log/ 下 md 知识库入库,只忽略运行日志)
- 大文件上限 100KB→1MB(架构图 512KB)

守卫测试 tests/unit/test_repo_guards.py(10 → 18 例):
- SKILL.md 须记录 parser 全部 CLI 命令 / 铁律 1-5 须为真实小节标题 / 示例不得违反铁律 5
- references/ 规则齐备;.claude/rules 与 references 必须一致(实测抓到一次真实漂移)
- VERSION == sapcli.__version__ == README 版本
- 仓内不得再出现 pack_skill.py / skill-src(防废弃流程回潮)

698 tests OK;editable 安装与 CLI 入口经临时 venv 实测通过。
docs/RELEASING.md 重写为单源开发流程。
2026-09-11 00:40:15 +08:00

685 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 批量同步设计 — 项目清单驱动
created: 2026-05-25
updated: 2026-05-25
tags:
- SAP
- ADT
- design
parent: "[[sap-cli/doc/sap-cli使用指南|sap-cli使用指南]]"
related:
- "[[sap-cli/doc/技术说明|技术说明]]"
- "[[sap-cli/doc/sap-cli使用指南#4.3 源代码同步激活 (`sync`)|sync 命令]]"
---
# 批量同步设计 — 项目清单驱动
> [!abstract] 概述
> 为 sap-cli 工具增加**项目清单**机制,支持批量源代码同步。核心思路:在源代码目录下维护 `manifest.json` 清单文件,记录每个开发对象在 SAP 系统中的状态,后续操作基于清单驱动,避免重复查询。
> [!info] 设计动机
> 现有 `sync` 命令只支持单对象操作,缺少以下能力:
> - 批量扫描本地文件并识别对象
> - 预查询 SAP 系统状态,制定执行计划
> - 按依赖关系排序,批量执行同步
> - 缓存对象状态,避免每次重复查询
## 1. 目录约定
### 1.1 项目目录结构
项目根目录下按对象类型组织子目录,`manifest.json` 位于根目录:
```
project/ ← --path 指向这里(项目根目录)
├── manifest.json ← 状态清单(init 自动生成)
├── reports/
│ └── {对象名小写}.abap
├── classes/
│ └── {对象名小写}.abap
├── interfaces/
│ └── {对象名小写}.abap
├── functions/
│ └── {函数组名小写}/
│ └── {函数模块名小写}.abap
├── domains/
│ └── {对象名小写}.abap
├── dataelements/
│ └── {对象名小写}.abap
├── tables/
│ └── {对象名小写}.abap
└── tabletypes/
└── {对象名小写}.abap
```
### 1.2 目录名 → 类型映射
`init` 扫描时按以下映射表从目录名推断对象类型:
| 目录名 | 对象类型 | 文件 → 对象名规则 |
|--------|---------|------------------|
| `reports/` | `report` | `zmy_report.abap``ZMY_REPORT` |
| `classes/` | `class` | `zcl_my_class.abap``ZCL_MY_CLASS` |
| `interfaces/` | `interface` | `zif_my_interface.abap``ZIF_MY_INTERFACE` |
| `functions/` | `function` | `zgroup/z_my_func.abap``ZGROUP/Z_MY_FUNC` |
| `domains/` | `domain` | `z_status.abap``Z_STATUS` |
| `dataelements/` | `dataelement` | `z_status.abap``Z_STATUS` |
| `tables/` | `table` | `zmy_table.abap``ZMY_TABLE` |
| `tabletypes/` | `tabletype` | `zty_table.abap``ZTY_TABLE` |
> [!note] 文件名 → 对象名规则
> - 文件名去掉 `.abap` 后缀后转为大写即为对象名
> - `functions/` 下的子目录名作为函数组名,子目录内的文件名作为函数模块名
> - 对象名为 `函数组名大写/函数模块名大写` 格式
## 2. 清单文件 (`manifest.json`)
### 2.1 结构定义
```json
{
"version": 1,
"last_init": "2026-05-25T10:00:00",
"last_refresh": "2026-05-25T14:00:00",
"objects": {
"<对象名>": {
"type": "<对象类型>",
"file": "<相对文件路径>",
"system_status": "<状态>",
"corr_nr": "<传输请求号或null>",
"depends_on": ["<依赖对象名>"],
"last_sync": "<ISO时间戳或null>",
"last_sync_result": "<结果>"
}
}
}
```
### 2.2 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `version` | int | 清单格式版本号,当前为 `1` |
| `last_init` | string | 最近一次 `init` 的时间戳 |
| `last_refresh` | string | 最近一次 `refresh` 的时间戳 |
| `objects` | object | 以对象名为 key 的字典 |
| `objects.<name>.type` | string | 对象类型(report/class/interface/... |
| `objects.<name>.file` | string | 相对于项目根目录的文件路径 |
| `objects.<name>.system_status` | string | `active` / `inactive` / `not_exists` |
| `objects.<name>.corr_nr` | string\|null | 传输请求号,`null` 表示本地对象 `$TMP` |
| `objects.<name>.depends_on` | string[] | 依赖的对象名列表 |
| `objects.<name>.last_sync` | string\|null | 最近一次成功同步的时间 |
| `objects.<name>.last_sync_result` | string | `success` / `failed` / `pending` |
### 2.3 示例
```json
{
"version": 1,
"last_init": "2026-05-25T10:00:00",
"last_refresh": null,
"objects": {
"Z_STATUS": {
"type": "domain",
"file": "domains/z_status.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": [],
"last_sync": "2026-05-25T10:30:00",
"last_sync_result": "success"
},
"ZIF_MY_INTERFACE": {
"type": "interface",
"file": "interfaces/zif_my_interface.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": [],
"last_sync": "2026-05-25T10:30:00",
"last_sync_result": "success"
},
"ZCL_MY_CLASS": {
"type": "class",
"file": "classes/zcl_my_class.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": ["ZIF_MY_INTERFACE"],
"last_sync": "2026-05-25T11:00:00",
"last_sync_result": "success"
},
"ZMY_REPORT": {
"type": "report",
"file": "reports/zmy_report.abap",
"system_status": "inactive",
"corr_nr": null,
"depends_on": ["ZCL_MY_CLASS"],
"last_sync": null,
"last_sync_result": "pending"
},
"ZGROUP/Z_MY_FUNC": {
"type": "function",
"file": "functions/zgroup/z_my_func.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": [],
"last_sync": "2026-05-25T10:00:00",
"last_sync_result": "success"
}
}
}
```
## 3. 命令变更一览
| 命令 | 变更类型 | 说明 |
|------|---------|------|
| `init` | **新增** | 扫描项目目录 → 查询 SAP → 生成 `manifest.json` |
| `create` | **变更** | 新增 `--path` 参数;创建成功后自动写入清单 |
| `sync` | **变更** | 新增 `--all` 批量模式;单对象模式操作后更新清单 |
| `delete` | **变更** | 新增 `--path` 参数;删除成功后从清单移除 |
| `refresh` | **新增** | 重新查询清单中所有对象的 SAP 状态 |
## 4. 命令详细设计
### 4.1 `init` — 项目初始化(新增)
#### 命令格式
```bash
python main.py init --path <项目根目录>
```
#### 执行流程
```
扫描项目目录
├─ 按目录映射表识别文件 → 生成 {对象名, 类型, 文件路径} 列表
├─ 输出: "扫描到 N 个本地文件"
└─ 忽略非 .abap 文件和隐藏文件
逐个查询 SAP 系统
├─ 对象存在 → 查询 info → 获取 system_status + corr_nr
├─ 对象不存在 → 标记 system_status: "not_exists"
└─ 查询失败 → 标记 system_status: "unknown"
应用类型默认优先级生成初始 depends_on
(init 时只设置类型层面的默认依赖,用户可后续手动编辑)
domain/dataelement/table → 无依赖
tabletype → 依赖同项目中的 table
interface → 无依赖
class → 依赖同项目中被引用的 interface
function → 无显式依赖
report → 无显式依赖
写入 manifest.json
输出汇总报告
```
#### 输出示例
```
============================================================
sap-cli 项目初始化
============================================================
项目路径: ./project
扫描到 5 个本地文件
→ 正在查询 SAP 系统状态...
✓ Z_STATUS (domain) active corr: DEVK901362
✓ ZIF_MY_INTERFACE (interface) active corr: DEVK901362
✓ ZCL_MY_CLASS (class) active corr: DEVK901362
⚠ ZMY_REPORT (report) inactive corr: 本地对象
✗ ZCL_NEW_CLASS (class) 不存在
✓ 清单已生成: ./project/manifest.json
5 个对象: 3 已激活, 1 未激活, 1 不存在
```
> [!tip] 清单可手动编辑
> `manifest.json` 生成后,用户可以手动编辑 `depends_on` 字段来补充对象间的显式依赖关系。
> `init` 只设置类型层面的默认依赖,具体的引用关系(如某个 class 实现了哪个 interface)需要用户自行标注。
---
### 4.2 `sync --all` — 批量同步(新增)
#### 命令格式
```bash
python main.py sync --all --path <项目根目录> [--corr_nr <传输请求号>] [--fail-fast] [--dry-run]
```
| 参数 | 必需 | 说明 |
|------|------|------|
| `--all` | 是 | 启用批量模式 |
| `--path` | 是 | 项目根目录(包含 `manifest.json` |
| `--corr_nr` | 否 | 统一传输请求号(跳过交互选择,所有对象共用) |
| `--fail-fast` | 否 | 遇到失败立即停止(默认:跳过并继续) |
| `--dry-run` | 否 | 仅输出执行计划,不实际操作 |
#### 执行流程
```
读取 manifest.json
├─ 不存在 → 报错: "请先执行 init"
└─ 存在 → 继续
扫描本地文件 vs 清单差异
├─ 本地有但清单没有的文件 → 提示: "N 个未纳入清单的文件,建议先 create"
└─ 清单有但本地被删的文件 → 警告,跳过
过滤待处理对象
├─ system_status == "active" && last_sync_result == "success" → 跳过(已同步)
├─ system_status == "not_exists" → 标记为 [create + sync]
└─ 其他 → 标记为 [sync]
拓扑排序
├─ 按 depends_on 建有向图
├─ 无显式依赖的按类型默认优先级排序
└─ 检测循环依赖 → 报错退出
--dry-run 模式:
输出执行计划,不实际操作
传输请求预选择(一次性交互):
├─ 扫描待处理对象,统计需要传输请求号的数量
├─ 指定了 --corr_nr → 直接使用,无交互
├─ 全部对象已绑定请求号或为本地对象 → 无交互
└─ 有对象需要请求号 → 弹出一次选择,选定后全局复用:
├─ 选择已有请求号 → 后续所有需要请求号的对象都使用此请求号
├─ 新建请求号 → 同上
└─ 不使用请求号 → 所需对象都作为本地对象处理
执行阶段(按排序顺序逐个处理,使用预选的传输请求号):
对每个对象:
├─ 本地文件不存在 → 跳过,警告
├─ system_status == "not_exists" → 先执行 create(使用预选请求号)
├─ 执行 sync 逻辑(使用预选请求号,跳过逐对象交互):
│ ├─ 对象已绑定请求号 → 使用对象自身绑定的请求号
│ ├─ 本地对象($TMP)→ 无需请求号
│ └─ 未绑定 → 使用预选的统一请求号(不再弹出交互)
├─ 成功 → 更新 manifest:
│ system_status: "active"
│ corr_nr: 实际使用的请求号
│ last_sync: 当前时间
│ last_sync_result: "success"
└─ 失败 →
├─ --fail-fast → 停止,输出已处理和未处理的对象
└─ 默认 → 标记 last_sync_result: "failed",跳过,继续下一个
后续依赖此对象的对象也自动跳过(标记 skipped)
输出汇总报告
```
#### dry-run 输出示例
```
============================================================
sap-cli 批量同步 (dry-run)
============================================================
项目路径: ./project
清单对象: 5
执行计划(按依赖排序):
1. Z_STATUS (domain) → sync [inactive → active]
2. ZIF_MY_INTERFACE (interface) → 跳过 [已是最新]
3. ZCL_MY_CLASS (class) → sync [依赖: ZIF_MY_INTERFACE ✓]
4. ZMY_REPORT (report) → sync [依赖: ZCL_MY_CLASS]
5. ZGROUP/Z_MY_FUNC (function) → 跳过 [已是最新]
待处理: 3 个 | 跳过: 2 个
```
#### 执行输出示例
```
============================================================
sap-cli 批量同步
============================================================
项目路径: ./project
待处理: 3 个对象
→ 预检传输请求需求...
2 个对象已绑定请求号: Z_STATUS(DEVK901362), ZCL_MY_CLASS(DEVK901362)
1 个对象需要传输请求: ZMY_REPORT
→ 查询可用的传输请求...
找到 2 个可修改的传输请求:
1. DEVK901362 Implement sales validation (所有者: ADMIN2)
2. DEVK901365 Bugfix for invoice printing (所有者: ADMIN2)
3. 新建传输请求
0. 不使用传输请求(本地对象)
请选择 [0-3]: 1
✓ 统一传输请求: DEVK901362(后续所有对象共用此请求号)
── [1/3] Z_STATUS (domain) ──
✓ 锁定成功(对象已绑定传输请求: DEVK901362
✓ 源代码写入成功
✓ 解锁成功
✓ 语法检查通过
✓ 激活成功
── [2/3] ZCL_MY_CLASS (class) ──
✓ 锁定成功(对象已绑定传输请求: DEVK901362
✓ 源代码写入成功
✓ 解锁成功
✓ 语法检查通过
✓ 激活成功
── [3/3] ZMY_REPORT (report) ──
✓ 锁定成功(使用统一传输请求: DEVK901362
✓ 源代码写入成功
✓ 解锁成功
✗ 语法检查未通过! 1 个错误
[错误] 行 10: Field "XXX" is unknown
⊘ 跳过(标记为 failed
============================================================
批量同步完成
============================================================
✓ 成功: Z_STATUS, ZCL_MY_CLASS
✗ 失败: ZMY_REPORT (语法错误,行 10)
⊘ 跳过: (无)
清单已更新: ./project/manifest.json
```
> [!tip] 指定 `--corr_nr` 跳过交互
> 如果已知传输请求号,使用 `--corr_nr` 参数可以直接跳过交互选择:
> ```bash
> python main.py sync --all --path ./project --corr_nr DEVK901362
> ```
> 此时所有未绑定请求号的对象都会使用指定的请求号,**全程零交互**。
---
### 4.3 `refresh` — 刷新清单状态(新增)
#### 命令格式
```bash
python main.py refresh --path <项目根目录>
```
#### 执行流程
```
读取 manifest.json
逐个重新查询 SAP info
更新每个对象的 system_status 和 corr_nr
(不修改 last_sync 和 last_sync_result
输出变更摘要
```
#### 使用场景
- 其他人修改了 SAP 端的对象
- 传输请求状态发生变化(释放、导入等)
- 需要确认当前系统状态但不想执行同步
#### 输出示例
```
============================================================
sap-cli 清单刷新
============================================================
项目路径: ./project
清单对象: 5
→ 正在查询 SAP 系统状态...
✓ Z_STATUS (domain) active corr: DEVK901362 (未变化)
✓ ZIF_MY_INTERFACE (interface) active corr: DEVK901362 (未变化)
~ ZCL_MY_CLASS (class) inactive corr: DEVK901362 (状态变更: active → inactive)
✓ ZMY_REPORT (report) inactive corr: null (未变化)
✓ ZGROUP/Z_MY_FUNC (function) active corr: DEVK901362 (未变化)
✓ 刷新完成
变更: 1 个 | 未变化: 4 个
```
---
### 4.4 `create` — 变更:自动写入清单
#### 变更内容
- 新增 `--path` 参数:指向项目根目录
- 创建成功后:自动在 `manifest.json` 中新增一条记录
#### 新增的清单记录
```json
{
"ZCL_NEW_CLASS": {
"type": "class",
"file": "classes/zcl_new_class.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": [],
"last_sync": "2026-05-25T14:00:00",
"last_sync_result": "success"
}
}
```
#### 向后兼容
- 如果未指定 `--path`,或指定目录下没有 `manifest.json`:仅创建对象,不写清单
- 现有不含 `--path` 的用法不受影响
---
### 4.5 `delete` — 变更:从清单移除
#### 变更内容
- 新增 `--path` 参数:指向项目根目录
- 删除成功后:从 `manifest.json` 中移除该对象记录
#### 向后兼容
- 如果未指定 `--path`,或指定目录下没有 `manifest.json`:仅删除对象,不更新清单
---
### 4.6 `sync`(单对象) — 变更:操作后更新清单
#### 变更内容
- 新增 `--path` 参数:指向项目根目录(可选)
- sync 成功或失败后:更新 `manifest.json` 中对应对象的记录
#### 向后兼容
- 如果未指定 `--path`,或指定目录下没有 `manifest.json`:行为与现有完全一致
## 5. 依赖排序策略
### 5.1 类型默认优先级
无显式 `depends_on` 时,按类型优先级排序:
| 优先级 | 数值 | 类型 | 说明 |
|--------|------|------|------|
| 1 | 10 | `domain` | 基础域定义 |
| 2 | 20 | `dataelement` | 依赖 domain |
| 3 | 30 | `table` | 依赖 dataelement |
| 4 | 40 | `tabletype` | 依赖 table |
| 5 | 50 | `interface` | 接口定义 |
| 6 | 60 | `class` | 可能实现 interface |
| 7 | 70 | `function` | 函数模块 |
| 8 | 80 | `report` | 程序,可能引用以上所有 |
### 5.2 排序算法
采用拓扑排序(Kahn 算法):
```
1. 以 manifest 中所有对象为节点
2. 建立有向边:
- 显式依赖: depends_on 中列出的对象 → 当前对象
- 类型默认优先级: 仅当两个对象之间没有显式依赖时作为 tie-breaker
3. 执行 Kahn 拓扑排序
4. 检测循环依赖 → 若存在未入队的节点则报错,输出循环链
```
### 5.3 依赖失败传播
批量 `sync` 时,若对象 A 依赖对象 B,而 B 同步失败:
```
A.depends_on = ["B"]
B.last_sync_result = "failed"
→ A 自动跳过,标记 last_sync_result: "skipped"
→ 依赖 A 的对象同样跳过(级联跳过)
```
## 6. 边界情况处理
| 场景 | 处理方式 |
|------|---------|
| 本地新增了 .abap 文件但清单中没有 | `sync --all` 时提示: "N 个未纳入清单的文件",建议先执行 `create` |
| 清单中有对象但本地文件被删了 | `sync --all` 时警告并跳过该对象 |
| `manifest.json` 不存在 | `sync --all` 报错,提示先执行 `init` |
| 依赖对象 sync 失败 | 跳过所有依赖它的对象(级联标记 skipped) |
| 循环依赖 | 检测到后报错,输出循环链中的对象名 |
| 非项目目录(无子目录结构) | `init` 时只扫描当前目录下的 `.abap` 文件 |
| SAP 连接中断 | 标记已处理的对象,输出中断位置,下次可从断点继续 |
| `manifest.json` 版本不匹配 | 提示版本不兼容,建议重新 `init` |
| 部分对象已绑定请求号,部分未绑定 | 已绑定的使用自身请求号,未绑定的使用预选的统一请求号 |
| 全部对象都是本地对象($TMP) | 无需传输请求,直接执行,零交互 |
| 指定了 `--corr_nr` 但部分对象已绑定其他请求号 | 已绑定的使用自身请求号(不受 `--corr_nr` 覆盖) |
## 7. 项目生命周期
```
┌─────────────────────────────────────────────────────────┐
│ │
│ init ────────────────────────────────────────────── │
│ │ 扫描本地文件 → 查询 SAP → 生成 manifest.json │
│ │ │
│ ▼ │
│ create(新增对象时) ──────────────────────────────── │
│ │ 在 SAP 创建 → 自动写入 manifest │
│ │ │
│ ▼ │
│ sync --all(日常同步)───────────────────────────── │
│ │ 读 manifest → 拓扑排序 → 逐个 sync → 更新 manifest │
│ │ │
│ ▼ │
│ refresh(SAP 端有变化时)──────────────────────────── │
│ 重新查询 SAP → 更新 manifest 中的状态字段 │
│ │
└─────────────────────────────────────────────────────────┘
```
> [!tip] 典型工作流
> ```bash
> # 1. 首次使用:初始化项目清单
> python main.py init --path ./my_project
>
> # 2. 日常开发:新增对象
> python main.py create --name ZCL_NEW --type class --path ./my_project --description "新类"
>
> # 3. 日常开发:修改本地 .abap 文件后,批量同步
> python main.py sync --all --path ./my_project
>
> # 4. 如需查看执行计划(不实际操作)
> python main.py sync --all --path ./my_project --dry-run
>
> # 5. 其他人修改了 SAP 端对象后,刷新清单
> python main.py refresh --path ./my_project
> ```
## 8. 模块设计(实现层面)
### 8.1 新增模块
| 模块 | 职责 |
|------|------|
| `sapcli/manifest.py` | 清单文件的读取、写入、查询、更新 |
| `sapcli/scanner.py` | 扫描本地目录,识别对象名和类型 |
| `sapcli/sorter.py` | 拓扑排序,依赖检测 |
### 8.2 清单模块 (`manifest.py`) 接口
```python
@dataclass
class ManifestEntry:
name: str
type: str
file: str
system_status: str # "active" / "inactive" / "not_exists"
corr_nr: str | None
depends_on: list[str]
last_sync: str | None # ISO timestamp
last_sync_result: str # "success" / "failed" / "pending"
class Manifest:
version: int
last_init: str | None
last_refresh: str | None
objects: dict[str, ManifestEntry]
@classmethod
def load(cls, project_path: str) -> "Manifest": ...
def save(self) -> None: ...
def upsert(self, entry: ManifestEntry) -> None: ...
def remove(self, name: str) -> None: ...
def get(self, name: str) -> ManifestEntry | None: ...
def pending_objects(self) -> list[ManifestEntry]: ...
```
### 8.3 扫描模块 (`scanner.py`) 接口
```python
DIRECTORY_TYPE_MAP = {
"reports": "report",
"classes": "class",
"interfaces": "interface",
"functions": "function",
"domains": "domain",
"dataelements": "dataelement",
"tables": "table",
"tabletypes": "tabletype",
}
@dataclass
class ScannedObject:
name: str # 大写对象名,function 类型含 /
type: str # 对象类型
file: str # 相对于项目根目录的路径
def scan_project(project_path: str) -> list[ScannedObject]: ...
```
### 8.4 排序模块 (`sorter.py`) 接口
```python
TYPE_PRIORITY = {
"domain": 10,
"dataelement": 20,
"table": 30,
"tabletype": 40,
"interface": 50,
"class": 60,
"function": 70,
"report": 80,
}
class CyclicDependencyError(SapCliError): ...
def topological_sort(
objects: list[ManifestEntry],
) -> list[ManifestEntry]: ...
# → 按 depends_on + TYPE_PRIORITY 排序
# → 检测循环依赖,抛出 CyclicDependencyError
```
## 9. 相关笔记
- [[sap-cli/doc/sap-cli使用指南|sap-cli 使用指南]] — 现有命令的使用说明
- [[sap-cli/doc/技术说明|技术说明]] — 技术架构与模块设计
- [[sap-cli/doc/ADT/03.修改代码-原理|修改代码原理]] — 锁定/编辑/激活工作流