chore: 清理 CraftKit 迁移资料
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 项目目标
|
## 项目目标
|
||||||
|
|
||||||
CraftKit 面向 Codex 插件市场,提供简洁、中性、可独立安装的 Skill。所有能力必须能够脱离来源项目独立理解、验证和维护。
|
CraftKit 面向 Codex 插件市场,提供简洁、中性、可独立安装和维护的 Skill。每项能力都应具备清晰边界,并能够独立理解与验证。
|
||||||
|
|
||||||
## 沟通与改动
|
## 沟通与改动
|
||||||
|
|
||||||
@@ -23,8 +23,7 @@ CraftKit 面向 Codex 插件市场,提供简洁、中性、可独立安装的
|
|||||||
| `plugins/doc/` | 文档转换、整理与写作 Skill |
|
| `plugins/doc/` | 文档转换、整理与写作 Skill |
|
||||||
| `plugins/git/` | Git 操作与交付 Skill |
|
| `plugins/git/` | Git 操作与交付 Skill |
|
||||||
| `plugins/knowledge/` | 交接、复盘与知识维护 Skill |
|
| `plugins/knowledge/` | 交接、复盘与知识维护 Skill |
|
||||||
| `plugins/skill/` | Skill 创建、迁移、检查与同步工具 |
|
| `plugins/skill/` | Skill 与项目规范辅助工具 |
|
||||||
| `migration/` | 来源指纹、迁移计划和状态追踪,不进入插件发布内容 |
|
|
||||||
|
|
||||||
## Skill 设计规则
|
## Skill 设计规则
|
||||||
|
|
||||||
@@ -44,143 +43,51 @@ plugins/<plugin>/skills/<skill>/
|
|||||||
├─ SKILL.md
|
├─ SKILL.md
|
||||||
├─ agents/openai.yaml # 有 UI 元数据时使用
|
├─ agents/openai.yaml # 有 UI 元数据时使用
|
||||||
├─ scripts/ # 有确定性自动化时使用
|
├─ scripts/ # 有确定性自动化时使用
|
||||||
├─ references/ # 有条件加载的详细资料时使用
|
├─ references/ # 有条件加载的详细资料
|
||||||
└─ assets/ # 生成产物需要的素材
|
└─ assets/ # 生成产物需要的素材
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 维护原则
|
||||||
|
|
||||||
|
- 以当前 Codex 能力、项目实际情况、公开标准和用户需求为依据设计 Skill。
|
||||||
|
- 技术规则应注明适用版本;依赖外部资料时记录资料入口和验证日期。
|
||||||
|
- 新增内容应具备明确用途,避免重复能力、过期说明和仅用于记录开发过程的文档。
|
||||||
|
|
||||||
|
## 新增与修改流程
|
||||||
|
|
||||||
|
1. 阅读目标插件、相邻 Skill、适用的 `AGENTS.md` 和当前 Git 状态,确认职责边界与已有改动。
|
||||||
|
2. 明确用户目标、输入、输出、触发条件、副作用、权限和不包含事项。
|
||||||
|
3. 检查是否应复用或扩展已有 Skill,避免名称不同但职责重复的实现。
|
||||||
|
4. 修改前向用户说明目标文件、实现方式、风险和验证方法;高风险或范围不明确时等待确认。
|
||||||
|
5. 按最小必要结构实现,优先使用项目证据和官方资料,不猜测接口、版本或运行行为。
|
||||||
|
6. 完成结构、引用、插件清单及必要行为验证,并分别记录通过项和未验证边界。
|
||||||
|
7. 展示实际变更、验证结果、剩余风险和建议提交信息;只有用户明确授权后才执行本地提交。
|
||||||
|
|
||||||
## CraftKit 项目目录
|
## CraftKit 项目目录
|
||||||
|
|
||||||
- `.craftkit/` 是项目级 Agent 配置、规范、知识和运行状态的统一目录,目录名固定为全小写;不能把整个目录视为本地缓存或整体排除。
|
- `.craftkit/` 是项目级 Agent 配置、规范、知识和运行状态的统一目录,目录名固定为全小写;不能把整个目录视为本地缓存或整体排除。
|
||||||
- 可共享内容包括 `project.json`、`agents/`、`standards/`、`knowledge/` 和 `handoff/`,可以根据用户确认正常提交和同步。
|
- 可共享内容包括 `project.json`、`agents/`、`standards/`、`knowledge/` 和 `handoff/`,可以根据用户确认正常提交和同步。
|
||||||
- `project.json` 记录项目类型、技术栈、源码与包边界、内部依赖标识、常用命令和规范入口;不得记录凭据、完整连接信息或个人机器绝对路径。
|
- `project.json` 记录项目类型、技术栈、源码与包边界、内部依赖标识、常用命令和规范入口;不得记录凭据、完整连接信息或个人机器绝对路径。
|
||||||
- 项目初始化区分成熟项目与空项目:成熟项目优先延续当前项目已有且有充分证据的风格;空项目根据需求、用户选择和已授权的参考项目建立最小上下文,不自行猜测框架、包名或内部依赖。
|
- 成熟项目优先延续已有且有充分证据的风格;空项目根据需求、用户选择和已授权参考建立最小上下文,不自行猜测框架、包名或内部依赖。
|
||||||
- 引用其他项目时先确认参考范围,只提炼结构、依赖、命名、测试、规范或工具约定;共享配置优先记录相对路径,不能共享的本地位置放入 `local/`。
|
- 引用其他项目时先确认参考范围,只提炼结构、依赖、命名、测试、规范或工具约定;共享配置优先记录相对路径,不能共享的本地位置放入 `local/`。
|
||||||
- 公开框架版本差异由 CraftKit 公共 profile 基于官方资料独立维护;项目只在 `project.json` 选择实际版本和 profile,并在 `standards/` 保存内部框架与覆盖规则。普通任务不得跨 profile 混读,升级或版本比较除外。
|
- 公共框架版本差异由 CraftKit 公共 profile 维护;项目在 `project.json` 选择实际版本和 profile,并在 `standards/` 保存项目补充规则。
|
||||||
- `agents/` 保存项目对特定 Agent 的补充说明;`standards/` 保存项目开发规范;`knowledge/pitfalls/` 保存经验证且可复用的问题经验;`knowledge/decisions/` 保存重要技术决策。
|
- `agents/` 保存项目对特定 Agent 的补充说明;`standards/` 保存项目开发规范;`knowledge/pitfalls/` 保存经验证且可复用的问题经验;`knowledge/decisions/` 保存重要技术决策。
|
||||||
- 本地内容统一放入 `local/`,缓存统一放入 `cache/`。`.craftkit/.gitignore` 必须排除 `/local/` 和 `/cache/`,但不得排除整个 `.craftkit/`。
|
- 本地内容统一放入 `local/`,缓存统一放入 `cache/`。`.craftkit/.gitignore` 必须排除 `/local/` 和 `/cache/`,但不得排除整个 `.craftkit/`。
|
||||||
- 本地交接默认位于 `.craftkit/local/handoff/current.md`;只有用户明确选择共享时才写入 `.craftkit/handoff/current.md`。
|
- 本地交接默认位于 `.craftkit/local/handoff/current.md`;只有用户明确选择共享时才写入 `.craftkit/handoff/current.md`。
|
||||||
- 只在实际需要时创建目录,不一次生成空的 `agents/`、`standards/`、`knowledge/`、`handoff/`、`local/` 或 `cache/`。
|
- 只在实际需要时创建目录,不一次生成空的 `agents/`、`standards/`、`knowledge/`、`handoff/`、`local/` 或 `cache/`。
|
||||||
- Git Skill 默认排除 `.craftkit/local/**` 和 `.craftkit/cache/**`;其他 `.craftkit` 内容按普通项目资产评估,并在提交建议中单独标识为 Agent、规范或知识变更。
|
- Git Skill 默认排除 `.craftkit/local/**` 和 `.craftkit/cache/**`;其他 `.craftkit` 内容按普通项目资产评估,并在提交建议中单独标识。
|
||||||
- Git 交付或变更导出默认排除环境配置、本地配置和机器配置。只有适用的 `AGENTS.md`、`.craftkit/agents/` 或 `.craftkit/standards/` 明确要求交付,并经用户查看预览后再次确认,才可纳入;私钥、真实密钥和检测到的凭据始终禁止导出。
|
|
||||||
- 如果本地目录已经被 Git 跟踪,或整个 `.craftkit/` 被全局规则、`.git/info/exclude` 或项目规则忽略,应提示冲突并停止自动处理;不得自动修改索引或历史。
|
- 如果本地目录已经被 Git 跟踪,或整个 `.craftkit/` 被全局规则、`.git/info/exclude` 或项目规则忽略,应提示冲突并停止自动处理;不得自动修改索引或历史。
|
||||||
- `.craftkit/` 不得保存凭据、令牌、私钥、个人机器绝对路径或无必要的个人信息。
|
- `.craftkit/` 不得保存凭据、令牌、私钥、个人机器绝对路径或无必要的个人信息。
|
||||||
|
|
||||||
## 迁移与知识产权边界
|
|
||||||
|
|
||||||
- 迁移采用“分析能力 → 编写中性规格 → 脱离原文独立实现”的方式,不做目录复制、批量替词或近义改写。
|
|
||||||
- 不复制来源 Skill 的正文、脚本、提示词、模板、示例、规则库、注释或独特文档结构。
|
|
||||||
- 不写入公司名称、产品名称、内部域名、内部包名、项目名、人员信息、业务规则或环境路径。
|
|
||||||
- 公司框架 API、组件契约、审批流程、版本矩阵和内部消息格式不得直接迁移;评估时应先抽象其通用问题,并优先设计基于项目配置、公开标准或用户显式输入的中性平替。只有不存在独立通用价值、无法安全替代或与 Codex 场景不成立时才排除。
|
|
||||||
- 通用技术规范必须根据公开标准或官方资料重新设计;需要引用时记录来源和许可证。
|
|
||||||
- 来源文件只用于人工分析和哈希追踪,不得进入 `plugins/` 发布目录。
|
|
||||||
- 上游变化只触发复核,不自动覆盖已迁移 Skill。
|
|
||||||
|
|
||||||
详细顺序和门禁见 [迁移计划](migration/MIGRATION_PLAN.md)。
|
|
||||||
|
|
||||||
## Skill 迁移工作流
|
|
||||||
|
|
||||||
所有新增迁移、来源更新和已迁移 Skill 的重新评估,都必须按以下顺序处理。用户未明确确认前,只能进行只读分析,不得创建、复制、改写或删除目标 Skill 文件。
|
|
||||||
|
|
||||||
### 批次策略
|
|
||||||
|
|
||||||
- 迁移前期采用“特殊样本”方式,不以固定数量为目标,而是优先覆盖不同结构、依赖、权限和风险类型。
|
|
||||||
- 特殊样本至少覆盖纯指令、脚本与生成物、只读 Git、有副作用 Git、模板化文档、大型参考资料和复合工作流;缺少某一类型时不得宣告样本阶段完成。
|
|
||||||
- 每个特殊样本原则上单独完成迁移前确认、实现验证和提交确认,以便及时修正规则。
|
|
||||||
- 特殊样本全部通过后,应先总结可复用的命名、目录、改写、验证和状态同步规则,再进入批量阶段。
|
|
||||||
- 批量阶段按同一插件、相近能力和相同风险类型组织,每批建议 6~12 个 Skill;高度同质时可整组处理,但不得跨越不同权限边界强行合批。
|
|
||||||
- 批量迁移仍执行完整的两次确认。任何样本失败或出现新类型,都应暂停扩批并补充对应特殊样本。
|
|
||||||
|
|
||||||
### 连续迁移波次
|
|
||||||
|
|
||||||
当用户明确要求将一组已完整列明的剩余来源“按顺序一次迁移完,再统一确认提交”时,可以建立连续迁移波次:
|
|
||||||
|
|
||||||
- 迁移前必须一次性展示全部来源、目标映射、合并或排除关系、执行顺序、写入范围和总体验收门禁,并取得用户明确确认。
|
|
||||||
- 确认后可以按既定阶段连续实现,不再逐阶段请求迁移确认;不得加入方案外来源、目标 Skill 或新权限。
|
|
||||||
- 每个阶段仍须独立运行对应测试和敏感内容扫描。阶段失败时先在既定范围内修复;出现需扩大范围、改变目标或无法安全替代的情况时暂停并重新确认。
|
|
||||||
- 波次执行期间不创建 Git 提交。全部阶段完成后统一展示来源状态、目标文件、测试证据、未验证边界和建议提交序列,等待一次最终确认。
|
|
||||||
- 最终一次“确认提交”可以授权按已展示顺序创建多个逻辑清晰的本地提交;不得因此推送、发布、创建标签或修改远端历史。
|
|
||||||
|
|
||||||
### 1. 阅读迁移计划和现状
|
|
||||||
|
|
||||||
- 完整阅读 `migration/MIGRATION_PLAN.md`、`migration/README.md` 和 `migration/source-lock.json`。
|
|
||||||
- 检查 Git 状态、目标插件目录和现有 Skill,避免覆盖未提交改动或重复实现。
|
|
||||||
- 只读检查相关来源 Skill 及其辅助资源,确认来源提交、文件哈希、依赖关系和变化范围。
|
|
||||||
- 区分首次迁移、上游更新、目标重构和排除项复核,不把不同类型混为一次迁移。
|
|
||||||
|
|
||||||
### 2. 列出来源 Skill 和作用
|
|
||||||
|
|
||||||
在任何写入前,向用户列出本批计划处理的来源 Skill:
|
|
||||||
|
|
||||||
| 项目 | 内容 |
|
|
||||||
| --- | --- |
|
|
||||||
| 来源标识 | 使用本地迁移标识,不在发布目录写入公司品牌 |
|
|
||||||
| 来源 Skill | 原始名称和相对路径 |
|
|
||||||
| 当前作用 | 根据源码确认的能力、输入、输出和关键边界 |
|
|
||||||
| 迁移类型 | 新增、更新、合并、中性平替、公开资料重建或排除 |
|
|
||||||
| 风险 | 公司专属知识、内部依赖、重复能力和兼容性问题 |
|
|
||||||
|
|
||||||
来源作用必须有文件依据;无法从静态内容确认的运行行为应明确标记为未验证。
|
|
||||||
|
|
||||||
同一目标能力存在多个来源时,应在一个迁移方案中共同评估,明确合并、取舍、中性平替或排除关系,不按来源机械创建多个目标 Skill。来源含有大量专有规则时,不得只罗列删除项;必须说明通用问题如何由配置、标准接口、用户输入或项目资料替代。
|
|
||||||
|
|
||||||
### 3. 给出目标名称、作用和组织结构
|
|
||||||
|
|
||||||
针对每个来源 Skill,先给出建议方案:
|
|
||||||
|
|
||||||
| 项目 | 内容 |
|
|
||||||
| --- | --- |
|
|
||||||
| 目标插件 | `dev`、`doc`、`git`、`knowledge` 或 `skill` |
|
|
||||||
| 目标名称 | 简短、中性、动作或明确领域导向的名称 |
|
|
||||||
| 目标作用 | 迁移后保留的通用能力和明确排除的内容 |
|
|
||||||
| 组织结构 | `SKILL.md` 及确有需要的 `agents/`、`scripts/`、`references/`、`assets/` |
|
|
||||||
| 合并关系 | 独立 Skill、合并到已有 Skill、替代旧 Skill 或排除 |
|
|
||||||
| 验证方案 | 静态校验、脚本测试、真实请求和敏感内容扫描 |
|
|
||||||
|
|
||||||
此阶段只输出设计,不创建目标目录。若多个来源能力可以合并,应优先给出合并方案,避免一一照搬来源结构。
|
|
||||||
|
|
||||||
### 4. 与用户确认迁移方案
|
|
||||||
|
|
||||||
- 展示本批范围、目标映射、组织结构、排除内容和验证方式后,明确请求用户确认。
|
|
||||||
- 用户可以确认全部方案,也可以调整名称、拆分或合并方式、迁移顺序和排除项。
|
|
||||||
- 只有“确认执行”“按此方案迁移”等明确授权才允许进入写入阶段。
|
|
||||||
- 用户仅要求评估、查看方案或继续讨论时,不视为迁移授权。
|
|
||||||
- 确认后的授权只覆盖已展示范围;新增 Skill、扩大来源范围或改变组织方式必须重新确认。
|
|
||||||
|
|
||||||
### 5. 执行迁移并更新文档
|
|
||||||
|
|
||||||
获得确认后:
|
|
||||||
|
|
||||||
1. 按中性能力规格独立实现目标 Skill,不复制来源目录或正文。
|
|
||||||
2. 运行 `AGENTS.md` 中规定的全部验证,并记录真实结果和未验证边界。
|
|
||||||
3. 更新 `migration/source-lock.json` 的来源哈希、目标路径、状态和复核日期。
|
|
||||||
4. 更新 `migration/MIGRATION_PLAN.md` 的执行清单和批次进度。
|
|
||||||
5. 更新根 `README.md`、所属插件说明或其他确实受影响的文档;不得创建重复状态文档。
|
|
||||||
6. 向用户报告新增、更新、合并和排除结果,以及测试、扫描和剩余风险。
|
|
||||||
7. 展示本批待提交文件清单和建议的 Conventional Commit 提交信息,进入第二次确认。
|
|
||||||
|
|
||||||
### 6. 与用户确认并提交到本地
|
|
||||||
|
|
||||||
- 迁移前确认只授权执行迁移,不自动授权 Git 提交。
|
|
||||||
- 迁移和文档同步完成后,必须再次等待用户明确确认;“确认提交”“提交到本地”等表述才视为提交授权。
|
|
||||||
- 用户要求调整、补测或继续检查时,先完成相应工作并重新展示结果,不得沿用此前的提交确认。
|
|
||||||
- 获得确认后,只暂存本批迁移方案中已展示且验证通过的文件,不使用会混入其他改动的宽泛暂存命令。
|
|
||||||
- 暂存后执行 `git diff --cached --check`,并复核暂存文件清单;发现额外文件或敏感内容时停止提交并报告。
|
|
||||||
- 使用中文 Conventional Commit 信息创建本地提交,然后检查提交内容和工作区状态。
|
|
||||||
- 本地提交成功后向用户报告提交哈希、提交信息、文件范围和剩余未提交改动。
|
|
||||||
- 第二次确认只授权本地提交,不包含推送、创建标签、发布插件或修改远端历史;这些操作必须另行获得明确授权。
|
|
||||||
- 连续迁移波次按“连续迁移波次”约定执行一次最终确认;若计划创建多个本地提交,必须在确认前展示每个提交的范围和建议信息。
|
|
||||||
|
|
||||||
## 验证要求
|
## 验证要求
|
||||||
|
|
||||||
每个新增或修改的 Skill 至少完成:
|
每个新增或修改的 Skill 至少完成:
|
||||||
|
|
||||||
1. 检查目录名、frontmatter 和引用路径。
|
1. 检查目录名、frontmatter、引用路径和工具名称。
|
||||||
2. 运行 Skill 校验器。
|
2. 运行仓库现有的 Skill 校验器及所属插件校验器。
|
||||||
3. 运行所属插件 manifest 校验器。
|
3. 检查凭据、无效路径、未替换占位符和无效工具名称。
|
||||||
4. 执行公司标识、内部路径、凭据和占位符扫描。
|
4. 有脚本时运行核心行为测试;有生成物时检查实际产物。
|
||||||
5. 有脚本时运行核心行为测试;有生成物时检查实际产物。
|
5. 以真实请求检查触发边界、权限边界和输出是否符合描述。
|
||||||
6. 用一个真实请求验证触发边界和输出结果。
|
|
||||||
7. 更新 `migration/source-lock.json` 中的状态、目标路径和复核日期。
|
|
||||||
|
|
||||||
静态检查通过不代表真实行为已经验证,交付时应分别说明静态校验、脚本测试和实际场景验证结果。
|
静态检查通过不代表真实行为已经验证,交付时应分别说明静态校验、脚本测试和实际场景验证结果。
|
||||||
|
|
||||||
@@ -189,5 +96,5 @@ plugins/<plugin>/skills/<skill>/
|
|||||||
- 使用 Conventional Commits,提交说明使用中文。
|
- 使用 Conventional Commits,提交说明使用中文。
|
||||||
- 推荐类型:`feat`、`fix`、`docs`、`refactor`、`test`、`chore`。
|
- 推荐类型:`feat`、`fix`、`docs`、`refactor`、`test`、`chore`。
|
||||||
- 提交前执行 `git status --short`、`git diff --cached --check` 并检查暂存文件清单。
|
- 提交前执行 `git status --short`、`git diff --cached --check` 并检查暂存文件清单。
|
||||||
- 不提交本地来源配置、凭据、缓存、运行产物和 IDE 私有状态。
|
- 不提交本地配置、凭据、缓存、运行产物和 IDE 私有状态。
|
||||||
- 不使用破坏性历史改写,也不推送,除非用户明确授权。
|
- 不使用破坏性历史改写,也不推送,除非用户明确授权。
|
||||||
|
|||||||
@@ -1,30 +1,23 @@
|
|||||||
# CraftKit
|
# CraftKit
|
||||||
|
|
||||||
CraftKit 是一组面向 Codex 插件市场的中性 Skill 工具。项目从通用问题出发独立实现能力,不携带任何公司的品牌、内部框架、业务规则、源码、模板或环境信息。
|
CraftKit 是面向 Codex 的通用插件工具集,覆盖软件开发、文档处理、Git 交付、项目知识维护和 Skill 管理。每个插件均可独立安装,能力描述保持简洁、中性,并以当前 Codex 能力和公开标准为基础维护。
|
||||||
|
|
||||||
## 文档导航
|
|
||||||
|
|
||||||
- [协作约定](AGENTS.md):项目范围、Skill 设计、迁移边界、验证和 Git 规则。
|
|
||||||
- [迁移计划](migration/MIGRATION_PLAN.md):迁移批次、依赖顺序、合并原则和验收门禁。
|
|
||||||
- [迁移追踪说明](migration/README.md):来源指纹和状态维护方式。
|
|
||||||
|
|
||||||
## 插件组成
|
## 插件组成
|
||||||
|
|
||||||
| 插件 | 用途 | 当前状态 |
|
| 插件 | 用途 |
|
||||||
| --- | --- | --- |
|
| --- | --- |
|
||||||
| `dev` | 软件设计、实现、审查、测试、升级与估算 | 已迁移 22 个 Skill,覆盖完整通用开发流程 |
|
| `dev` | 软件需求分析、设计、实现、审查、测试、升级与估算 |
|
||||||
| `doc` | 文档转换、整理与写作 | 已迁移 8 个 Skill,新增报告、消息与需求整理 |
|
| `doc` | 文档转换、整理、归档与写作 |
|
||||||
| `git` | 分支、提交、变更提取、集成与发布 | 已迁移 6 个 Skill,发布操作按阶段分别授权 |
|
| `git` | 分支、提交、变更提取、集成与发布准备 |
|
||||||
| `knowledge` | 项目初始化、交接、复盘与经验 | 已迁移 `handoff`、`init`、`trace`、`distill`、`lessons`、`worklog` |
|
| `knowledge` | 项目初始化、任务交接、问题复盘与经验维护 |
|
||||||
| `skill` | 项目规范及 Skill 创建、迁移与维护 | 已迁移 `guidance`、`guidance-edit` |
|
| `skill` | 项目规范检索、维护与 Skill 辅助工具 |
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
CraftKit/
|
CraftKit/
|
||||||
├─ .agents/plugins/marketplace.json # 仓库级 Codex 市场清单
|
├─ .agents/plugins/marketplace.json # 仓库级 Codex 插件市场清单
|
||||||
├─ AGENTS.md # Codex 与贡献者协作约定
|
├─ AGENTS.md # 项目协作与维护约定
|
||||||
├─ migration/ # 迁移计划、来源指纹与状态追踪
|
|
||||||
├─ plugins/ # 可独立安装的插件
|
├─ plugins/ # 可独立安装的插件
|
||||||
│ ├─ dev/
|
│ ├─ dev/
|
||||||
│ ├─ doc/
|
│ ├─ doc/
|
||||||
@@ -34,15 +27,16 @@ CraftKit/
|
|||||||
└─ README.md
|
└─ README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## 开发约束
|
## 设计原则
|
||||||
|
|
||||||
- Skill 必须独立重写,不复制公司插件的正文、脚本、模板、示例或规则库。
|
- 每个 Skill 聚焦一个清晰能力,名称与触发描述准确,避免宽泛兜底或重复功能。
|
||||||
- 专有能力应先抽象为可配置、基于公开标准或用户显式输入的中性平替;不能仅因专有内容较多就整体排除。
|
- 优先读取当前项目的代码、配置、规范和用户输入,不把特定组织、产品或环境假设写入通用能力。
|
||||||
- 发布内容不得出现公司名称、产品名称、内部域名、内部包名、项目名或人员信息。
|
- 公共技术规则基于适用版本的官方资料维护;项目专属规则保存在项目自己的 `.craftkit/standards/` 中。
|
||||||
- 每个 Skill 文件夹名必须与 `SKILL.md` frontmatter 的 `name` 一致。
|
- 涉及写文件、Git 状态、外部系统或发布的操作必须明确边界,并按风险取得用户授权。
|
||||||
- 新增或修改插件后必须执行 Skill 校验、插件校验和去公司化扫描。
|
- 静态检查、脚本测试和真实场景验证分别记录,不用其中一种替代另一种。
|
||||||
- 上游变化只用于触发人工复核,不得自动覆盖已迁移内容。
|
|
||||||
|
|
||||||
## 本地使用
|
## 本地使用
|
||||||
|
|
||||||
这是仓库级 marketplace。安装前先将本仓库注册为本地 marketplace,再按需安装其中的插件。正式发布前还需要补充许可证、公开仓库地址、作者信息、隐私政策及市场素材。
|
本仓库提供仓库级 marketplace。将仓库注册为本地 marketplace 后,可按需安装 `dev`、`doc`、`git`、`knowledge` 或 `skill` 插件。
|
||||||
|
|
||||||
|
正式公开发布前,还需补充许可证、公开仓库地址、作者信息、隐私政策和市场素材。
|
||||||
|
|||||||
@@ -1,305 +0,0 @@
|
|||||||
# Skill 迁移计划
|
|
||||||
|
|
||||||
## 1. 目标
|
|
||||||
|
|
||||||
将两个本地来源中的通用能力整理为 CraftKit Skill。迁移结果必须适用于 Codex 插件市场,并与来源项目的品牌、内部框架、业务知识和运行环境解耦。
|
|
||||||
|
|
||||||
迁移不追求来源 Skill 与目标 Skill 一一对应。重复能力应合并;公司专属实现不得迁移,但应优先将其解决的通用问题重建为中性平替,确无独立价值或无法安全替代时才排除。目标数量以职责清晰和实际复用价值为准。
|
|
||||||
|
|
||||||
## 2. 迁移路径
|
|
||||||
|
|
||||||
```text
|
|
||||||
来源发现
|
|
||||||
→ 风险分类
|
|
||||||
→ 重复能力合并
|
|
||||||
→ 中性能力规格
|
|
||||||
→ 独立实现
|
|
||||||
→ 行为验证
|
|
||||||
→ 敏感内容扫描
|
|
||||||
→ Codex 与插件校验
|
|
||||||
→ 更新迁移状态
|
|
||||||
```
|
|
||||||
|
|
||||||
禁止采用“完整复制来源目录后批量替换名称”的方式。中性能力规格形成后,目标实现应基于该规格、当前 Codex 能力和公开资料完成。
|
|
||||||
|
|
||||||
## 3. 状态模型
|
|
||||||
|
|
||||||
| 状态 | 含义 | 进入条件 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `pending` | 待评估 | 已发现来源 Skill |
|
|
||||||
| `specified` | 已完成中性规格 | 已明确目标、边界、依赖和排除内容 |
|
|
||||||
| `rewriting` | 独立实现中 | 已创建目标 Skill |
|
|
||||||
| `review` | 待复核 | 功能完成,但版权、依赖或行为仍需确认 |
|
|
||||||
| `migrated` | 已迁移 | 全部门禁通过 |
|
|
||||||
| `excluded` | 不迁移 | 公司专属或没有独立价值 |
|
|
||||||
| `superseded` | 已合并 | 能力已由另一个目标 Skill 覆盖 |
|
|
||||||
|
|
||||||
## 4. 迁移批次
|
|
||||||
|
|
||||||
迁移分为“特殊样本”和“同质批量”两个阶段。前者用于覆盖迁移机制的不同风险类型,后者在规则稳定后提高吞吐量。批次数量不固定:特殊样本原则上逐个迁移,批量阶段每批建议 6~12 个同质 Skill。
|
|
||||||
|
|
||||||
### 第 0 批:迁移基础设施
|
|
||||||
|
|
||||||
第 0 批属于仓库内部维护工具,不发布为市场 Skill:
|
|
||||||
|
|
||||||
1. `migration/scripts/scan_sources.py`:只读发现来源 Skill、辅助资源和文件哈希。
|
|
||||||
2. `AGENTS.md` 迁移工作流:完成分类、目标设计和用户确认。
|
|
||||||
3. `migration/scripts/check_skill.py`:检查结构、引用、占位符、敏感内容和凭据风险。
|
|
||||||
4. `migration/scripts/update_lock.py`:预览或更新来源指纹和迁移状态。
|
|
||||||
|
|
||||||
完成门槛:能够对全部来源 Skill 生成稳定清单,并在不复制来源内容的情况下更新状态。
|
|
||||||
|
|
||||||
### 第 1 阶段:特殊样本
|
|
||||||
|
|
||||||
特殊样本不追求模块连续性,而是覆盖不同迁移机制:
|
|
||||||
|
|
||||||
1. `doc/format-md`:纯指令型样本,已完成。
|
|
||||||
2. `doc/docx-to-md`:多来源合并、脚本、依赖和生成物样本,已完成。
|
|
||||||
3. `git/commit-msg`:只读 Git 状态分析样本,已完成。
|
|
||||||
4. `git/branch`:修改仓库状态和二次授权边界样本,已完成。
|
|
||||||
5. `knowledge/handoff`:模板化文档与项目上下文样本,已完成。
|
|
||||||
6. `skill/guidance`:大型参考资料、索引和渐进式加载样本,已完成;只检索项目资料和基于公开一级资料独立重建的中性公共基线。
|
|
||||||
7. `knowledge/init`:成熟项目与空项目双模式初始化、参考项目提炼和共享/本地信息边界样本,已完成。
|
|
||||||
8. `skill/migrate`:不迁移。来源能力用于把 Claude Code Skill 转换为 Codex Skill;CraftKit 自始按 Codex 规范开发,不存在平台转换需求。来源扫描、目标检查和状态追踪继续由 `migration/scripts/` 作为仓库维护设施承担。
|
|
||||||
|
|
||||||
完成门槛:适用样本均通过对应验证,不适用样本记录排除依据,并形成可复用的命名、目录、独立实现、测试、扫描和状态同步规则。出现未覆盖的新结构或权限类型时,应补充样本,不直接扩批。当前特殊样本阶段已完成,可以进入同质批量迁移。
|
|
||||||
|
|
||||||
### 第 2 阶段:同质批量迁移
|
|
||||||
|
|
||||||
特殊样本完成后,按同一插件、相近能力和相同风险类型组织批量迁移:
|
|
||||||
|
|
||||||
- 每批建议 6~12 个 Skill,高度同质时可以整组处理。
|
|
||||||
- 不把只读能力与有副作用能力、纯指令与复杂脚本、普通迁移与公开资料重建强行合为一批。
|
|
||||||
- 每批仍需迁移前确认和提交前确认,并统一更新 README、迁移计划和来源状态。
|
|
||||||
- 任一验收门禁失败时暂停该批,不继续扩大范围。
|
|
||||||
|
|
||||||
### 文档转换批次
|
|
||||||
|
|
||||||
所属插件:`doc`
|
|
||||||
|
|
||||||
1. `docx-to-md`
|
|
||||||
2. `md-to-docx`(已完成)
|
|
||||||
3. `xlsx-to-md`(已完成)
|
|
||||||
4. `archive`(已完成,以可配置规则平替固定目录、业务文件名、专有章节拆分和内部系统校验)
|
|
||||||
|
|
||||||
重点验证图片、表格、合并单元格、编码、覆盖策略和路径安全。脚本及测试数据必须独立创建。
|
|
||||||
|
|
||||||
### Git 工作流批次
|
|
||||||
|
|
||||||
所属插件:`git`
|
|
||||||
|
|
||||||
1. `branch`(已完成)
|
|
||||||
2. `identity`(已完成,只管理 Git 提交用户名和邮箱)
|
|
||||||
3. `export`(已完成,环境配置默认排除,规范明确要求并再次确认后才可导出)
|
|
||||||
4. `integrate`(已完成,按项目规范通过隔离 worktree 评估、准备和发布预集成分支)
|
|
||||||
|
|
||||||
`commit-msg` 将作为只读 Git 特殊样本先行完成。涉及提交、合并和远端操作的 Skill 必须保留明确授权边界,并保护脏工作区。
|
|
||||||
|
|
||||||
### 知识管理批次
|
|
||||||
|
|
||||||
所属插件:`knowledge`
|
|
||||||
|
|
||||||
1. `trace`(已完成)
|
|
||||||
2. `distill`(已完成)
|
|
||||||
3. `lessons`(已完成,合并初始化、维护、审计和提升)
|
|
||||||
4. `worklog`(已完成)
|
|
||||||
5. `init`(已完成)
|
|
||||||
|
|
||||||
来源中与经验初始化、提升和回扫相关的多个能力统一合并为 `maintain-lessons`,通过模式区分具体工作。
|
|
||||||
|
|
||||||
### 开发主流程批次
|
|
||||||
|
|
||||||
所属插件:`dev`
|
|
||||||
|
|
||||||
```text
|
|
||||||
plan-change
|
|
||||||
├─ design-backend → implement-backend → test-backend
|
|
||||||
└─ design-frontend → implement-frontend → test-ui
|
|
||||||
↓
|
|
||||||
review-code
|
|
||||||
```
|
|
||||||
|
|
||||||
建议顺序:
|
|
||||||
|
|
||||||
1. `plan-change`(已完成)
|
|
||||||
2. `design-backend`(已完成)
|
|
||||||
3. `design-frontend`(已完成)
|
|
||||||
4. `prepare-api`(已完成)
|
|
||||||
5. `implement-backend`(已完成)
|
|
||||||
6. `implement-frontend`(已完成)
|
|
||||||
7. `review-code`(已完成)
|
|
||||||
8. `test-backend`(已完成)
|
|
||||||
9. `test-ui`(已完成)
|
|
||||||
10. `analyze-bugs`(已完成)
|
|
||||||
|
|
||||||
多个来源中的代码检查、代码审查能力合并为 `review-code`,通过工作区、提交和分支三种模式覆盖。
|
|
||||||
|
|
||||||
### 项目规范与前端辅助批次
|
|
||||||
|
|
||||||
1. `dev/component`(已完成,基于项目证据完成组件选型与契约查证)
|
|
||||||
2. `dev/style`(已完成,基于项目视觉基线设计样式)
|
|
||||||
3. `dev/form`(已完成,设计表单结构、响应布局和可访问性)
|
|
||||||
4. `skill/guidance`(特殊样本已完成)
|
|
||||||
5. `skill/guidance-edit`(已完成,建立、检查和维护项目规范索引)
|
|
||||||
|
|
||||||
这里只实现读取和维护“当前项目自身规范”的机制,不随 CraftKit 提供任何来源项目规范。
|
|
||||||
原前端导航能力由上述 Skill 的精确触发描述和 Codex 自动发现替代,不再维护独立路由 Skill。混合任务按 `style` → `component` → `form` → 前端设计或实现的依赖顺序处理。
|
|
||||||
|
|
||||||
### 基于公开资料重建批次
|
|
||||||
|
|
||||||
以下能力不从来源文本改写,而是根据官方资料重新设计:
|
|
||||||
|
|
||||||
1. `dev/design-api`(已完成)
|
|
||||||
2. `dev/design-db`(已完成)
|
|
||||||
3. `dev/review-java`(已完成)
|
|
||||||
4. `dev/review-frontend`(已完成)
|
|
||||||
5. `dev/design-frontend-data`(已完成)
|
|
||||||
6. `dev/review-mybatis`(已完成)
|
|
||||||
7. `dev/design-workflow`(已完成)
|
|
||||||
|
|
||||||
中性规格必须记录所采用的公开标准、官方文档和许可证信息。
|
|
||||||
|
|
||||||
### 平替或排除结果
|
|
||||||
|
|
||||||
- 公司框架知识与专属规范由 `skill/guidance`、项目源码和 `.craftkit/standards/` 平替。
|
|
||||||
- 内部组件契约、审批规则和消息格式不迁移,目标 Skill 只读取项目证据和用户输入。
|
|
||||||
- 升级、估算与发布已重建为通用能力,不携带内部版本矩阵和仓库流程。
|
|
||||||
- 浏览器代理配置由 Codex 浏览器或计算机操作能力平替。
|
|
||||||
- Skill 路由依靠精确触发描述和 Codex 自动发现,不增加独立路由器。
|
|
||||||
- 非 Codex 平台转换能力保持排除。
|
|
||||||
|
|
||||||
### 剩余迁移波次
|
|
||||||
|
|
||||||
截至 2026-08-26,原有 29 个 `pending` 来源已在一个连续波次内全部处理完成。所有来源均已闭合为 `migrated`、`superseded` 或 `excluded`;本波次完成验证后统一等待本地提交确认。
|
|
||||||
|
|
||||||
#### 阶段 1:公开基线与专项设计
|
|
||||||
|
|
||||||
| 来源能力 | 目标 Skill | 处理方式 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| API 约定 | `dev/design-api` | 基于 HTTP、OpenAPI 等官方资料独立重建 |
|
|
||||||
| 数据库约定 | `dev/design-db` | 基于目标数据库官方资料和项目证据独立重建 |
|
|
||||||
| Java 约定 | `dev/review-java` | 基于 Java 与所用框架对应版本官方资料独立重建 |
|
|
||||||
| 前端约定 | `dev/review-frontend` | 基于当前框架版本官方资料独立重建 |
|
|
||||||
| 前端数据约定 | `dev/design-frontend-data` | 建立请求、状态与视图模型的通用设计边界 |
|
|
||||||
| MyBatis 约定 | `dev/review-mybatis` | 基于 MyBatis 官方资料独立重建 |
|
|
||||||
| 工作流约定 | `dev/design-workflow` | 以项目工作流契约和用户输入平替内部审批规则 |
|
|
||||||
|
|
||||||
公开资料只采用一级官方来源,记录适用版本、重建日期和许可证或引用边界;不得改写来源规范正文。
|
|
||||||
|
|
||||||
#### 阶段 2:代码实现
|
|
||||||
|
|
||||||
| 来源 Skill | 目标 Skill | 状态关系 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `back-code`、同类后端编码来源 | `dev/implement-backend` | 两个来源合并,一个迁移、一个 `superseded` |
|
|
||||||
| `front-code`、同类前端编码来源 | `dev/implement-frontend` | 两个来源合并,一个迁移、一个 `superseded` |
|
|
||||||
|
|
||||||
实现 Skill 直接修改业务源码,必须保护脏工作区、使用项目真实版本与规范、执行风险相称的验证,且不自动提交或推送。
|
|
||||||
|
|
||||||
#### 阶段 3:审查、测试与问题分析
|
|
||||||
|
|
||||||
| 来源能力 | 目标 Skill | 处理方式 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 两套代码检查与一套代码审查 | `dev/review-code` | 合并为工作区、提交和分支审查模式 |
|
|
||||||
| 后端单元测试 | `dev/test-backend` | 按当前测试框架和项目模式生成或修改测试 |
|
|
||||||
| UI 测试 | `dev/test-ui` | 按用户可观察行为设计并执行浏览器测试 |
|
|
||||||
| Bug 列表分析 | `dev/analyze-bugs` | 解析通用结构化问题清单并形成证据化报告 |
|
|
||||||
|
|
||||||
本阶段不得把静态检查、单元测试、浏览器测试和真实环境验收混为同一结论。
|
|
||||||
|
|
||||||
#### 阶段 4:交付、升级与估算
|
|
||||||
|
|
||||||
| 来源能力 | 目标 Skill 或处理结果 | 处理方式 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 后端升级、前端升级 | `dev/upgrade` | 合并为基于源版本、目标版本和官方迁移资料的通用升级流程 |
|
|
||||||
| 版本发布 | `git/release` | 只准备版本、变更摘要、标签和发布检查;远端动作单独授权 |
|
|
||||||
| 工作量评估 | `dev/estimate` | 基于范围、依赖、风险和假设输出区间估算 |
|
|
||||||
| 框架约定 | `skill/guidance` | `superseded`,内部规则由项目 `.craftkit/standards/` 提供 |
|
|
||||||
| 框架知识查询 | `skill/guidance` | `superseded`,改为检索项目证据和公开官方资料 |
|
|
||||||
|
|
||||||
#### 阶段 5:写作与平台工具收尾
|
|
||||||
|
|
||||||
| 来源能力 | 目标 Skill 或处理结果 | 处理方式 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 领导汇报 | `doc/report` | 中性化为面向不同受众的事实型工作汇报 |
|
|
||||||
| 消息模板 | `doc/message` | 中性化为项目消息与通知草稿 |
|
|
||||||
| 技术内容转需求 | `doc/requirements` | 将技术输入转换为可确认的需求说明 |
|
|
||||||
| 能力创建器 | 官方 `skill-creator` | `superseded`,不重复发布同类 CraftKit Skill |
|
|
||||||
| 浏览器代理配置 | Codex 浏览器或计算机操作能力 | `superseded`,不迁移宿主专属代理配置 |
|
|
||||||
| Skill 转 Cursor | 无 | `excluded`,CraftKit 只面向 Codex |
|
|
||||||
|
|
||||||
#### 波次总门禁
|
|
||||||
|
|
||||||
1. 29 个来源全部更新为 `migrated`、`superseded` 或 `excluded`,不得遗留 `pending`。
|
|
||||||
2. 所有目标 Skill 通过结构、引用、敏感内容和真实请求边界测试。
|
|
||||||
3. 公开基线记录官方来源、适用版本和重建边界。
|
|
||||||
4. 有脚本的 Skill 完成隔离行为测试;有外部工具的 Skill 明确权限和未验证边界。
|
|
||||||
5. README、插件版本、迁移计划和来源锁同步一致。
|
|
||||||
6. 全量测试、JSON 校验和 `git diff --check` 通过。
|
|
||||||
7. 波次完成后只展示提交方案,不创建提交;用户统一确认后按逻辑范围创建本地提交。
|
|
||||||
|
|
||||||
## 5. 合并原则
|
|
||||||
|
|
||||||
来源数量不等于目标数量。优先执行以下合并:
|
|
||||||
|
|
||||||
| 来源能力类型 | 目标 Skill |
|
|
||||||
| --- | --- |
|
|
||||||
| 代码检查、提交审查、分支审查 | `review-code` |
|
|
||||||
| 多套开发计划 | `plan-change` |
|
|
||||||
| 多套后端设计 | `design-backend` |
|
|
||||||
| 多套前端设计 | `design-frontend` |
|
|
||||||
| 多套后端实现 | `implement-backend` |
|
|
||||||
| 多套前端实现 | `implement-frontend` |
|
|
||||||
| 多套 Word 转 Markdown | `docx-to-md` |
|
|
||||||
| 经验初始化、提升、回扫 | `maintain-lessons` |
|
|
||||||
| 规范检索和索引维护 | `guidance`、`guidance-edit` |
|
|
||||||
|
|
||||||
目标 Skill 总量不设硬指标,预期控制在约 30 个,避免细碎能力和重复触发。
|
|
||||||
|
|
||||||
## 6. 单个 Skill 的迁移步骤
|
|
||||||
|
|
||||||
1. 记录来源相对路径、提交和 SHA-256。
|
|
||||||
2. 判断该能力是迁移、合并、重建还是排除。
|
|
||||||
3. 只提炼目标、输入、输出、关键边界和真实用例,形成中性规格。
|
|
||||||
4. 确定目标插件和简短 Skill 名称。
|
|
||||||
5. 基于中性规格和公开资料独立实现。
|
|
||||||
6. 对脚本执行单元或行为测试,对文档型 Skill 执行真实请求测试。
|
|
||||||
7. 扫描敏感内容、来源残留、无效工具名和宿主绑定表达。
|
|
||||||
8. 运行 Skill 与插件校验。
|
|
||||||
9. 将状态更新为 `migrated`,记录目标路径、目标版本和复核日期。
|
|
||||||
10. 展示迁移结果、验证证据、待提交文件和建议提交信息,等待用户再次确认。
|
|
||||||
11. 用户确认后精确暂存本批文件并创建本地提交;推送和发布仍需独立授权。
|
|
||||||
|
|
||||||
## 7. 每批验收门禁
|
|
||||||
|
|
||||||
- Skill 名称简洁,目录名与 frontmatter 一致。
|
|
||||||
- `description` 能准确触发,不是大而全的能力描述。
|
|
||||||
- 不存在来源正文、脚本、模板、示例或独特结构的直接复制。
|
|
||||||
- 公司标识、内部域名、包名、路径和人员信息扫描为零。
|
|
||||||
- 所有引用文件存在,脚本和生成物经过实际验证。
|
|
||||||
- Skill 校验和所属插件校验通过。
|
|
||||||
- 至少一个真实请求用例通过。
|
|
||||||
- `source-lock.json` 已更新。
|
|
||||||
|
|
||||||
任一门禁未通过时,不进入下一批的大规模迁移。
|
|
||||||
|
|
||||||
## 8. 当前执行顺序
|
|
||||||
|
|
||||||
- [x] 扩展 `source-lock.json` 的 Skill 级记录结构。
|
|
||||||
- [x] 实现来源只读扫描脚本。
|
|
||||||
- [x] 在 `AGENTS.md` 中固化分类、设计和确认流程。
|
|
||||||
- [x] 实现 Skill 与敏感内容检查脚本。
|
|
||||||
- [x] 实现迁移台账预览和更新脚本。
|
|
||||||
- [x] 迁移并验证首个样板 `doc/format-md`。
|
|
||||||
- [x] 迁移并验证脚本型特殊样本 `doc/docx-to-md`。
|
|
||||||
- [x] 迁移并验证只读 Git 特殊样本 `git/commit-msg`。
|
|
||||||
- [x] 迁移并验证有副作用 Git 特殊样本 `git/branch`。
|
|
||||||
- [x] 迁移并验证模板化交接特殊样本 `knowledge/handoff`。
|
|
||||||
- [x] 完成其余特殊样本并总结批量迁移规则。
|
|
||||||
- [ ] 按插件和风险类型继续推进同质批量迁移。
|
|
||||||
- [x] 完成首个同质批量:`md-to-docx`、`xlsx-to-md`、`archive`。
|
|
||||||
- [x] 完成 Git 本地操作批次:`identity`、`export`。
|
|
||||||
- [x] 完成 Git 高风险隔离集成样本:`integrate`。
|
|
||||||
- [x] 完成知识管理批次:`trace`、`distill`、`lessons`、`worklog`。
|
|
||||||
- [x] 完成前端辅助批次:`component`、`style`、`form`,并以精确触发替代独立路由。
|
|
||||||
- [x] 完成项目规范维护能力:`guidance-edit`。
|
|
||||||
- [x] 完成开发分析与设计批次:`plan-change`、`design-backend`、`design-frontend`、`prepare-api`。
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 迁移追踪说明
|
|
||||||
|
|
||||||
本目录只记录来源指纹、迁移状态和人工复核结果,不存放或打包公司插件内容。
|
|
||||||
|
|
||||||
- [迁移计划](MIGRATION_PLAN.md):批次顺序、依赖、合并策略与验收门禁。
|
|
||||||
- `source-lock.json`:可提交的来源指纹和迁移状态。
|
|
||||||
- `local-sources.json`:本地来源路径,不提交;结构参考 `local-sources.example.json`。
|
|
||||||
- `scripts/scan_sources.py`:只读生成来源 Skill 指纹清单。
|
|
||||||
- `scripts/check_skill.py`:检查迁移后 Skill 的结构、引用和敏感内容。
|
|
||||||
- `scripts/update_lock.py`:根据扫描报告预览或更新迁移台账。
|
|
||||||
- `tests/`:迁移脚本的隔离行为测试。
|
|
||||||
|
|
||||||
扫描报告可能包含来源 Skill 名称和相对路径,应写入本地临时目录或忽略文件,不进入插件发布内容。
|
|
||||||
|
|
||||||
同步流程:
|
|
||||||
|
|
||||||
1. 读取来源仓库当前提交与 Skill 文件哈希。
|
|
||||||
2. 与 `source-lock.json` 比较,生成新增、修改、删除清单。
|
|
||||||
3. 人工判断变化是否属于可公开迁移的通用能力。
|
|
||||||
4. 在 CraftKit 中独立重写并完成验证。
|
|
||||||
5. 更新来源指纹、目标路径、状态和复核日期。
|
|
||||||
|
|
||||||
允许的状态:`pending`、`specified`、`rewriting`、`review`、`migrated`、`excluded`、`superseded`。
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"source-a": "D:/path/to/local/source-a",
|
|
||||||
"source-b": "D:/path/to/local/source-b"
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""检查 CraftKit Skill 的基础结构、引用和敏感内容。"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
||||||
LINK_RE = re.compile(r"\[[^]]+]\((?!https?://|#)([^)]+)\)")
|
|
||||||
SECRET_RE = re.compile(
|
|
||||||
r"(?i)(api[_-]?key|password|private[_-]?key|access[_-]?token)\s*[:=]\s*[^\s]+"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def frontmatter(text: str) -> dict[str, str]:
|
|
||||||
"""解析当前校验所需的简单顶层 frontmatter 字段。"""
|
|
||||||
if not text.startswith("---\n"):
|
|
||||||
return {}
|
|
||||||
end = text.find("\n---", 4)
|
|
||||||
if end < 0:
|
|
||||||
return {}
|
|
||||||
values: dict[str, str] = {}
|
|
||||||
for line in text[4:end].splitlines():
|
|
||||||
if ":" in line and not line.startswith((" ", "\t")):
|
|
||||||
key, value = line.split(":", 1)
|
|
||||||
values[key.strip()] = value.strip().strip("'\"")
|
|
||||||
return values
|
|
||||||
|
|
||||||
|
|
||||||
def check_skill(root: Path, forbidden: list[str]) -> dict:
|
|
||||||
"""返回结构化问题列表,不修改被检查目录。"""
|
|
||||||
issues: list[dict[str, str]] = []
|
|
||||||
skill_md = root / "SKILL.md"
|
|
||||||
if not skill_md.is_file():
|
|
||||||
return {"skill": root.name, "issues": [{"code": "missing-skill-md", "path": "SKILL.md"}]}
|
|
||||||
text = skill_md.read_text(encoding="utf-8-sig")
|
|
||||||
metadata = frontmatter(text)
|
|
||||||
name = metadata.get("name", "")
|
|
||||||
if not NAME_RE.fullmatch(name):
|
|
||||||
issues.append({"code": "invalid-name", "path": "SKILL.md"})
|
|
||||||
if name != root.name:
|
|
||||||
issues.append({"code": "name-path-mismatch", "path": "SKILL.md"})
|
|
||||||
if not metadata.get("description"):
|
|
||||||
issues.append({"code": "missing-description", "path": "SKILL.md"})
|
|
||||||
|
|
||||||
for path in sorted(item for item in root.rglob("*") if item.is_file()):
|
|
||||||
relative = path.relative_to(root).as_posix()
|
|
||||||
content = path.read_text(encoding="utf-8-sig", errors="replace")
|
|
||||||
if "[TODO:" in content:
|
|
||||||
issues.append({"code": "todo-placeholder", "path": relative})
|
|
||||||
if SECRET_RE.search(content):
|
|
||||||
issues.append({"code": "possible-secret", "path": relative})
|
|
||||||
lowered = content.casefold()
|
|
||||||
for term in forbidden:
|
|
||||||
if term.casefold() in lowered:
|
|
||||||
issues.append({"code": f"forbidden:{term}", "path": relative})
|
|
||||||
if path.suffix.lower() == ".md":
|
|
||||||
for link in LINK_RE.findall(content):
|
|
||||||
target = link.split("#", 1)[0]
|
|
||||||
if target and not (path.parent / target).resolve().is_file():
|
|
||||||
issues.append({"code": "broken-link", "path": f"{relative}:{link}"})
|
|
||||||
return {"skill": root.name, "issues": issues}
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(description="检查 CraftKit Skill")
|
|
||||||
parser.add_argument("skill", nargs="+", type=Path)
|
|
||||||
parser.add_argument("--forbid", action="append", default=[])
|
|
||||||
args = parser.parse_args()
|
|
||||||
results = [check_skill(path.resolve(), args.forbid) for path in args.skill]
|
|
||||||
print(json.dumps({"results": results}, ensure_ascii=False, indent=2))
|
|
||||||
raise SystemExit(1 if any(item["issues"] for item in results) else 0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""只读扫描来源目录中的 Skill,并输出稳定的 JSON 清单。"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def file_hash(path: Path) -> str:
|
|
||||||
"""按二进制内容计算单文件 SHA-256。"""
|
|
||||||
digest = hashlib.sha256()
|
|
||||||
with path.open("rb") as stream:
|
|
||||||
for block in iter(lambda: stream.read(65536), b""):
|
|
||||||
digest.update(block)
|
|
||||||
return digest.hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def directory_hash(root: Path) -> tuple[str, int]:
|
|
||||||
"""把相对路径和文件内容共同纳入哈希,确保结构变化也可被识别。"""
|
|
||||||
digest = hashlib.sha256()
|
|
||||||
files = sorted(path for path in root.rglob("*") if path.is_file())
|
|
||||||
for path in files:
|
|
||||||
relative = path.relative_to(root).as_posix()
|
|
||||||
digest.update(relative.encode("utf-8"))
|
|
||||||
digest.update(b"\0")
|
|
||||||
digest.update(bytes.fromhex(file_hash(path)))
|
|
||||||
return digest.hexdigest(), len(files)
|
|
||||||
|
|
||||||
|
|
||||||
def read_skill_name(skill_md: Path) -> str:
|
|
||||||
"""从 YAML frontmatter 中读取 name;解析失败时回退为目录名。"""
|
|
||||||
for line in skill_md.read_text(encoding="utf-8-sig").splitlines():
|
|
||||||
if line.startswith("name:"):
|
|
||||||
return line.split(":", 1)[1].strip().strip("'\"") or skill_md.parent.name
|
|
||||||
return skill_md.parent.name
|
|
||||||
|
|
||||||
|
|
||||||
def git_commit(root: Path) -> str | None:
|
|
||||||
"""尽力读取来源提交;非 Git 目录时返回空值,不阻塞文件扫描。"""
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "-c", f"safe.directory={root.as_posix()}", "-C", str(root), "rev-parse", "HEAD"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
return result.stdout.strip() if result.returncode == 0 else None
|
|
||||||
|
|
||||||
|
|
||||||
def scan_source(source_id: str, root: Path) -> dict:
|
|
||||||
"""扫描单个来源,结果仅包含相对路径和指纹,不包含本地绝对路径。"""
|
|
||||||
if not root.is_dir():
|
|
||||||
raise ValueError(f"来源目录不存在:{root}")
|
|
||||||
skills = []
|
|
||||||
for skill_md in sorted(root.rglob("SKILL.md")):
|
|
||||||
skill_root = skill_md.parent
|
|
||||||
digest, file_count = directory_hash(skill_root)
|
|
||||||
children = sorted(
|
|
||||||
child.name for child in skill_root.iterdir() if child.is_dir()
|
|
||||||
)
|
|
||||||
skills.append(
|
|
||||||
{
|
|
||||||
"name": read_skill_name(skill_md),
|
|
||||||
"relativePath": skill_root.relative_to(root).as_posix(),
|
|
||||||
"sha256": digest,
|
|
||||||
"fileCount": file_count,
|
|
||||||
"resources": children,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"id": source_id,
|
|
||||||
"commit": git_commit(root),
|
|
||||||
"skillCount": len(skills),
|
|
||||||
"skills": skills,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_source(value: str) -> tuple[str, Path]:
|
|
||||||
"""解析 id=path 参数,避免把本地路径写入输出文件。"""
|
|
||||||
if "=" not in value:
|
|
||||||
raise argparse.ArgumentTypeError("来源参数必须使用 id=path 格式")
|
|
||||||
source_id, raw_path = value.split("=", 1)
|
|
||||||
if not source_id.strip() or not raw_path.strip():
|
|
||||||
raise argparse.ArgumentTypeError("来源标识和路径均不能为空")
|
|
||||||
return source_id.strip(), Path(raw_path).expanduser().resolve()
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(description="只读扫描 Skill 来源目录")
|
|
||||||
parser.add_argument("--source", action="append", required=True, type=parse_source)
|
|
||||||
parser.add_argument("--output", type=Path, help="可选 JSON 输出路径;省略时输出到终端")
|
|
||||||
args = parser.parse_args()
|
|
||||||
payload = {
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
|
||||||
"sources": [scan_source(source_id, root) for source_id, root in args.source],
|
|
||||||
}
|
|
||||||
content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
|
|
||||||
if args.output:
|
|
||||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
args.output.write_text(content, encoding="utf-8")
|
|
||||||
else:
|
|
||||||
print(content, end="")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""根据扫描报告更新迁移台账;默认预览,显式 --apply 才写入。"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
from datetime import date
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
VALID_STATUS = {
|
|
||||||
"pending", "specified", "rewriting", "review",
|
|
||||||
"migrated", "excluded", "superseded",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def merge(lock: dict, report: dict) -> dict:
|
|
||||||
"""保留人工维护字段,仅同步来源提交、数量和 Skill 指纹。"""
|
|
||||||
lock.setdefault("schemaVersion", 1)
|
|
||||||
lock["updatedAt"] = date.today().isoformat()
|
|
||||||
lock.setdefault("sources", {})
|
|
||||||
lock.setdefault("skills", {})
|
|
||||||
for source in report.get("sources", []):
|
|
||||||
source_id = source["id"]
|
|
||||||
lock["sources"][source_id] = {
|
|
||||||
"commit": source.get("commit"),
|
|
||||||
"skillCount": source["skillCount"],
|
|
||||||
}
|
|
||||||
for skill in source["skills"]:
|
|
||||||
# 发布台账不保存来源名称和路径;本地扫描报告负责提供可读映射。
|
|
||||||
path_hash = hashlib.sha256(skill["relativePath"].encode("utf-8")).hexdigest()
|
|
||||||
key = f"{source_id}:{path_hash[:16]}"
|
|
||||||
current = lock["skills"].get(key, {})
|
|
||||||
status = current.get("status", "pending")
|
|
||||||
if status not in VALID_STATUS:
|
|
||||||
raise ValueError(f"非法迁移状态:{key}={status}")
|
|
||||||
lock["skills"][key] = {
|
|
||||||
**current,
|
|
||||||
"sourcePathHash": path_hash,
|
|
||||||
"sourceSha256": skill["sha256"],
|
|
||||||
"status": status,
|
|
||||||
}
|
|
||||||
return lock
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(description="更新 Skill 迁移台账")
|
|
||||||
parser.add_argument("--lock", required=True, type=Path)
|
|
||||||
parser.add_argument("--report", required=True, type=Path)
|
|
||||||
parser.add_argument("--apply", action="store_true", help="确认写入台账")
|
|
||||||
args = parser.parse_args()
|
|
||||||
lock = json.loads(args.lock.read_text(encoding="utf-8"))
|
|
||||||
report = json.loads(args.report.read_text(encoding="utf-8"))
|
|
||||||
content = json.dumps(merge(lock, report), ensure_ascii=False, indent=2) + "\n"
|
|
||||||
if args.apply:
|
|
||||||
temporary = args.lock.with_suffix(args.lock.suffix + ".tmp")
|
|
||||||
temporary.write_text(content, encoding="utf-8")
|
|
||||||
temporary.replace(args.lock)
|
|
||||||
else:
|
|
||||||
print(content, end="")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,538 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"updatedAt": "2026-08-26",
|
|
||||||
"sources": {
|
|
||||||
"source-a": {
|
|
||||||
"commit": "31883f7e81c2bdb19505e591ba9db8a3d9fd4352",
|
|
||||||
"skillCount": 12
|
|
||||||
},
|
|
||||||
"source-b": {
|
|
||||||
"commit": "2947a163538cd119a4e241c9d36f061d6dcae841",
|
|
||||||
"skillCount": 52
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"skills": {
|
|
||||||
"source-a:2c317f4ba8859f5e": {
|
|
||||||
"sourcePathHash": "2c317f4ba8859f5e210c18e69e99e8e472d38d97e193c5f213508d4afb672c11",
|
|
||||||
"sourceSha256": "ed08a64bf0f5004d43a9c09a40087aebec8f52323c5a47d8fc73f9f9aca9be0e",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/review-code",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-a:1b7843684955ce5a": {
|
|
||||||
"sourcePathHash": "1b7843684955ce5a0beb3ef16c6e751c5e9eb5a17409084fed806b74ec0c522f",
|
|
||||||
"sourceSha256": "eabf9fabba7b751a0b85547b141d92821f461e8d904f4fb15d5746eca1965769",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/plan-change",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-a:5d76364c580248a5": {
|
|
||||||
"sourcePathHash": "5d76364c580248a54449e5ec7ede0079f19df21fef95922d3538fb82c095b181",
|
|
||||||
"sourceSha256": "9a664d17f276b01311f661aa6e8fd40063708550c97cc3340246922194d8390b",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/implement-backend",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-a:a0b791b6dac96b81": {
|
|
||||||
"sourcePathHash": "a0b791b6dac96b8180df558278b5c7ef643b4806cc4e534ffe6395b9236666d3",
|
|
||||||
"sourceSha256": "603b4fd6e4b52c9b5d368c5ba6bdff333d32f3e03e9b5393686b0729eb4ffc7c",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/design-backend",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-a:0124493991019160": {
|
|
||||||
"sourcePathHash": "0124493991019160a06c65d6200db0a50abb212a79bcefab4be4631be85c07c8",
|
|
||||||
"sourceSha256": "eca051fb929c5bd74c34564631872c2d6755b4427a7c4fbb4d6240eeb6331ee3",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/prepare-api",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-a:b80c5913e146321c": {
|
|
||||||
"sourcePathHash": "b80c5913e146321ca5efef5e45f11172e79c542855c2ae65478fe253414e66ea",
|
|
||||||
"sourceSha256": "5e33d979bb08fdcc47b2dee2e0d5cb39c24825b391f8bbf5606e57cbd4c2ef2c",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/implement-frontend",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-a:cd36505dd718e889": {
|
|
||||||
"sourcePathHash": "cd36505dd718e88966ad74421c65e6c7cccd71a1436f2843eb8cfa7a4f1534c5",
|
|
||||||
"sourceSha256": "a0137bd615988c49a8967b4e0129618858268b90391ab2ed912cc72f338f41f8",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/design-frontend",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-a:e14c1a3cd3b041f6": {
|
|
||||||
"sourcePathHash": "e14c1a3cd3b041f650d71eb5701c71c0de76cb7a909e9a419f1d277159cd763a",
|
|
||||||
"sourceSha256": "7780de4d979a6bafd1f5f503f08261858907f110acddb630dfab36d32e54c6a8",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "official:skill-creator",
|
|
||||||
"reason": "Codex 官方 skill-creator 已覆盖通用 Skill 创建与更新能力",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-a:fdde0da43c1ec4c7": {
|
|
||||||
"sourcePathHash": "fdde0da43c1ec4c7fddad96b9172435af6be96ceafa2cc824f70e04a3cf11085",
|
|
||||||
"sourceSha256": "46a02dbff9c3fff57a0802853e2424983927f53c7f1cc3d8a42542403e62a161",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/doc/skills/docx-to-md",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-a:4efb6f9dd0f39dff": {
|
|
||||||
"sourcePathHash": "4efb6f9dd0f39dff13339e62ff24b672552efcbcedb007e9ea86fe41950adbb0",
|
|
||||||
"sourceSha256": "a914823bdf50d9e5c8ee70d6363c5c0e7aea341ed390587a9285c70a7047558e",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/doc/skills/xlsx-to-md",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-a:32e7cfc21d252985": {
|
|
||||||
"sourcePathHash": "32e7cfc21d25298559bffbe7f0918f0f6e7d2aac29e3eca39d1f43d8226cf934",
|
|
||||||
"sourceSha256": "0f0486e3f25279a1a1cb8cecfef4dc01830471b5d31b52a9d17a425919997bc2",
|
|
||||||
"status": "excluded",
|
|
||||||
"reason": "CraftKit 原生面向 Codex,不需要 Claude Code 到 Codex 的平台转换能力",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-a:bc91b57fa2a50049": {
|
|
||||||
"sourcePathHash": "bc91b57fa2a500498b31b0f1a87dfa8ab83c32e4b361b4ea1614ea6721234b7c",
|
|
||||||
"sourceSha256": "1436c7e25325c927d55d1dfc2a3117ec8e30845d66e06d2e6d7cc75abe4e0792",
|
|
||||||
"status": "excluded",
|
|
||||||
"reason": "CraftKit 仅面向 Codex,不增加 Cursor 平台转换能力",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:3fb4282b0b034d52": {
|
|
||||||
"sourcePathHash": "3fb4282b0b034d5286bccd9924c37bedbe857c4673798aa844f540fe0ee5186a",
|
|
||||||
"sourceSha256": "196cd701c67d06fce4ee3b9894cbed95e6bc9101cc2089f83ae8f35b61a11332",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/git/skills/release",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:0dedfcd01cd89e28": {
|
|
||||||
"sourcePathHash": "0dedfcd01cd89e28e83d841f78c270b1210399cc867fd3e1865c14acf1ea865a",
|
|
||||||
"sourceSha256": "be6a694d4de9b8a792e19e1ed116e2fdd54d91731d9efcadfca1b0055a4a3145",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/dev/skills/implement-backend",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "后端实现能力已与另一来源合并并按项目证据中性重建",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:3f05784a0a86ba5f": {
|
|
||||||
"sourcePathHash": "3f05784a0a86ba5fb8f268797ca1ceabe8ca237b3c9d3c4396e622c1ef421cc6",
|
|
||||||
"sourceSha256": "9a39cd7af138519fe3de3f7a3cb031ea5da1243ff5779504a13215fa03a5a3c2",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/dev/skills/design-backend",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "后端设计能力已与另一来源合并并按项目证据中性重建",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:41934e94a1922a1b": {
|
|
||||||
"sourcePathHash": "41934e94a1922a1bbccec4377246e67bd84f1ffbd2b9d1bf4027c1ed15d12260",
|
|
||||||
"sourceSha256": "a0db48d88dab0537e061146edf6dbdeb4b7f8a935febd1556f798ef234b29e5b",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/test-backend",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:0cfb12fd60357a34": {
|
|
||||||
"sourcePathHash": "0cfb12fd60357a346c8e0079536540fba81e406d58205098f1a5162c6ce7e9d1",
|
|
||||||
"sourceSha256": "aa722e0dbcb613748b75bc2f5c81b8a97cbff07905b1fcee6d58cb176135fc08",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/dev/skills/review-code",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "代码检查能力已合并到统一代码审查入口",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:f171b05e239c2e2c": {
|
|
||||||
"sourcePathHash": "f171b05e239c2e2c5f2a0c636dfac0c2456b9d3b5215fdaa7fb86a33be1ee188",
|
|
||||||
"sourceSha256": "cd16d17204009509e6798badab786b9bd4d3969117b3ad4b9005337b76a4791b",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/dev/skills/plan-change",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "开发计划能力已与另一来源合并为通用变更计划",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:3b29e4441f642e65": {
|
|
||||||
"sourcePathHash": "3b29e4441f642e65d442fd284f37a45364cebb7a6c53446c354d15c70e532410",
|
|
||||||
"sourceSha256": "d2689d3ac45070389473cb8cc414df44e82ad267915d8cd7c53c023f5b8f3696",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/dev/skills/implement-frontend",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "前端实现能力已与另一来源合并并按项目证据中性重建",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:f6ec7bd5501e6ea5": {
|
|
||||||
"sourcePathHash": "f6ec7bd5501e6ea5f6e907a586a8d7cbe5fbbeebd3e5c8efead9b250742a148e",
|
|
||||||
"sourceSha256": "25905b3b294b0be2e245099acb1a557793215f157b96b9e964e78170838652f0",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/component",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:c49d1db42acd7220": {
|
|
||||||
"sourcePathHash": "c49d1db42acd722071d61420be58ba8ccbc54f5370bdf951abfb5981914271cf",
|
|
||||||
"sourceSha256": "20875df87fb84c7d5830d6eaaff544c9e32e3f9a2359ed099de5f3a4a36eaa7d",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/dev/skills/design-frontend",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "前端设计能力已与另一来源合并并移除内部组件与固定版本约束",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:9a3f3d3c43d9017a": {
|
|
||||||
"sourcePathHash": "9a3f3d3c43d9017a080a9832df5b975e6ddd3f8c39947ee768aeb973fc23974e",
|
|
||||||
"sourceSha256": "47c072e792d1cf80ddcc69536236ccb525d21e7d5408ed41cca55483556e8acb",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/dev/skills",
|
|
||||||
"reason": "前端导航能力已由 component、style、form 的精确触发描述和 Codex 自动发现替代",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:eb730c56bc7a4a15": {
|
|
||||||
"sourcePathHash": "eb730c56bc7a4a158ddec74c7497a10fdea2d0b628783b968e3ee9ee4572b15c",
|
|
||||||
"sourceSha256": "89aa46526fa7531958df46be4b162a9f7b9ea597e3e9121991cf25e9f25158f5",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/skill/skills/guidance",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:422174b0c00bbd65": {
|
|
||||||
"sourcePathHash": "422174b0c00bbd657f4c08415d4a722584556cfa5f3edc997b11e8ab0ef8656a",
|
|
||||||
"sourceSha256": "017a4ca0b7648ed91003deeb97a3757e8f17acd6963c6019140389a907f1345a",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/style",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:742ba3611bee4e34": {
|
|
||||||
"sourcePathHash": "742ba3611bee4e34dfee6dadbe192dd8be677c4b9f49108e2f04d9e22dc8801f",
|
|
||||||
"sourceSha256": "7582e333966b45ee2d145601831449d75c4f3159670c31df25e5d18e6a357bbe",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/test-ui",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:d6d4166800584a61": {
|
|
||||||
"sourcePathHash": "d6d4166800584a61ad6cbf96af57bb8ee269a16babba753f267b48efd66f6d2d",
|
|
||||||
"sourceSha256": "60f8708db24f368ecec55bd3f326683fac60bd6776ac69e0fafaf4c77333e703",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/doc/skills/docx-to-md",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:023767d9c7be6295": {
|
|
||||||
"sourcePathHash": "023767d9c7be62956d12fd01c94ac676b9fe6719741acd6f412c8b446efa2760",
|
|
||||||
"sourceSha256": "ba579f8d6a62b4745d8d3922b33e3c20974d8081ae4d1c87727d1a9e4b609945",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/doc/skills/format-md",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:b127c47139270a7f": {
|
|
||||||
"sourcePathHash": "b127c47139270a7f9157e18ad6a0c975b99caf07b9a0874daeac86d9a99ce48c",
|
|
||||||
"sourceSha256": "0214b80071f07beaff4f5837b983546ff71dbaa65bc29e34a5a23c9d946b683b",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/doc/skills/md-to-docx",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:34ab291ce3cfed2e": {
|
|
||||||
"sourcePathHash": "34ab291ce3cfed2e69cc6f08309b1a41142be91235b9a573daf98639b4a001cc",
|
|
||||||
"sourceSha256": "239d7efb1f9a53fc1b40f367954a6f6eaaf9d7a56cdf712128dcf393dc5a5067",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/design-api",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:3c8596a5d95de214": {
|
|
||||||
"sourcePathHash": "3c8596a5d95de214d381b2f04898dd7042a266aa7889569981a97a1cf50045d1",
|
|
||||||
"sourceSha256": "cc19258fcf63018ce2dac4d7253e23fbfb71596ee27235871ad78aae743b57e6",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/design-db",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:a388287911bc39cb": {
|
|
||||||
"sourcePathHash": "a388287911bc39cb3f88824044426782df3c3e6450c4db9e4948c28a47dbf9f1",
|
|
||||||
"sourceSha256": "20e13c3458f6af8d0b131f650f573eb824978438b52475a2f480a67c2f846cd6",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/skill/skills/guidance",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "框架约定改由通用规范检索结合项目真实依赖解析",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:ca08ee8839bccc15": {
|
|
||||||
"sourcePathHash": "ca08ee8839bccc1504c253cdb9bfc09803ddf77f948605f8563d04e0466a2ce7",
|
|
||||||
"sourceSha256": "0086c8da048d94303e9d5765b91b7eff9a10a798ffa73de9f245be5422cf3255",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/dev/skills/component",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "组件约定与契约查证能力已合并到 component",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:1b4cee54060ef399": {
|
|
||||||
"sourcePathHash": "1b4cee54060ef3995d6c98e99dee3c3200ab2e90dd62136cef07b9b09e31364d",
|
|
||||||
"sourceSha256": "24df169fbb18f39be5ec4046cc8d33535b1b86d2114673ee35d1a2a0324c1b25",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/review-frontend",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:7b7552c8c6e1b5f5": {
|
|
||||||
"sourcePathHash": "7b7552c8c6e1b5f56a283db304272da8cd63210aaf7327231a7b26f3cf903082",
|
|
||||||
"sourceSha256": "9c373fe6bb955d4431e92bba66239cb707a6179082a624b74bc77fa36663fc61",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/design-frontend-data",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:5f525e6aeda55dd8": {
|
|
||||||
"sourcePathHash": "5f525e6aeda55dd81c26d38cf59097cb52abbcfc5b05e35fe7e8101a1a3353b5",
|
|
||||||
"sourceSha256": "8068a6137951e94a01bc68cf9011db69f3d759304a3167291afa4c238531e34a",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/form",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:f2079362cf82e719": {
|
|
||||||
"sourcePathHash": "f2079362cf82e719e2dde6098c55ce984709a97446a7f66113f8b5f317f1f55b",
|
|
||||||
"sourceSha256": "f57f3c0d5175c3c1700b74e8489d820eed4ef36d10e79ab14e096cb2a20a0cec",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/review-java",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:b78122ad4813e9ba": {
|
|
||||||
"sourcePathHash": "b78122ad4813e9baef0ee5281bddb910b01fd89bdbbd2753011ac3deb52a8ce5",
|
|
||||||
"sourceSha256": "c06d7eb2674dcbb4bad01a8c65c6d372dffd95c585a16ab2c3f1bba19df199e0",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/skill/skills/guidance",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "框架知识查询改由通用规范检索与项目源码证据完成",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:7fd143975b0071c3": {
|
|
||||||
"sourcePathHash": "7fd143975b0071c3efa271a9ad19c3d7e422f64d3e23aa08d7a091c805cb9467",
|
|
||||||
"sourceSha256": "33582dcda068247a5c56238134dad308d1f66b03abbfdc7bc282a9ac9ff918a2",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/review-mybatis",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:affd0805467646c1": {
|
|
||||||
"sourcePathHash": "affd0805467646c1fcb4c196776e9cb610fcec721e1a44df3bc02f5c08b84aeb",
|
|
||||||
"sourceSha256": "c7f565da3a1f412d3f15642fc9ad44733adee73c4d13cdebffaff1de4ca5652c",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/design-workflow",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:2a64acb63e8b2030": {
|
|
||||||
"sourcePathHash": "2a64acb63e8b203031c4ef292f957c0fe0d3722e079dc9a299730015b9e3359c",
|
|
||||||
"sourceSha256": "580da97b2fb16543db9aef62359f153fe5aca382d88987f10f233289bc02790d",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/git/skills/commit-msg",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:3a5d583c511959b3": {
|
|
||||||
"sourcePathHash": "3a5d583c511959b3d1934d15eaa9b0cd0590c558535b6a8307f870d7ce0c8dd0",
|
|
||||||
"sourceSha256": "6b69964b6e65ddc6eefa857fa1016587d3b2d2fac22d2bda4196762233fb025b",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/git/skills/branch",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:13bb77c66fee0964": {
|
|
||||||
"sourcePathHash": "13bb77c66fee09641fe61781ffbba4c51ed47b14ce225972ea35219e489405a3",
|
|
||||||
"sourceSha256": "abe529dcc85b1ac88791c95cf7be12cf5b1f545988b515f10edb55f0a8c94fb1",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/git/skills/export",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:fd03e996181699d9": {
|
|
||||||
"sourcePathHash": "fd03e996181699d95a09c1940f685ac366daeeda1aa901175d26dcf153f755fb",
|
|
||||||
"sourceSha256": "b4ef1f7847dd9081fdacaecae8853b2946e22c899b08bbe48ef280d21546e044",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/git/skills/identity",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:4ca0940d73b4b626": {
|
|
||||||
"sourcePathHash": "4ca0940d73b4b6264a37e1a344d669367dc313364a0077abf8bcaf97cfed42a1",
|
|
||||||
"sourceSha256": "92b3cea563f544806493245bdf099e2357112ccbce15007593c94d62c26c258c",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/git/skills/integrate",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:0b917cc63232dff3": {
|
|
||||||
"sourcePathHash": "0b917cc63232dff38c375d3bec3fabe57390201c34c3ce96f7885eb23bca91d7",
|
|
||||||
"sourceSha256": "5ecfa8040f5d71f347cfcfb89040a5cbfdb96f71867fb11b750b78d4aa17d66a",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/knowledge/skills/init",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:905e74ea51281da1": {
|
|
||||||
"sourcePathHash": "905e74ea51281da179c1eb31677af71569a95139a74fb139d39dadd52db2ce62",
|
|
||||||
"sourceSha256": "cfc1eb3fafb15dc51b730dd36d3ad507b5ae415d5f1a5da5138471170afdde38",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/knowledge/skills/trace",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:274c4aac900a23c3": {
|
|
||||||
"sourcePathHash": "274c4aac900a23c3ca0506807c4df8e09c95e21b84fd633951bae146f6c41028",
|
|
||||||
"sourceSha256": "b4beacdc745cd06ee4be20694d60bae0c9c6122fb61b917b12b66451497651e9",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/knowledge/skills/lessons",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:6e1a4d182b2e41fd": {
|
|
||||||
"sourcePathHash": "6e1a4d182b2e41fddd28051c20d7170821a87b518e49684cc4f12b731789831d",
|
|
||||||
"sourceSha256": "cf106df97100aabf260db7c8d45c2d6d499c643b4898d7e46c98852a90b7134d",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/knowledge/skills/lessons",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "经验提升能力已合并到 lessons 的 promote 模式",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:cd4c15d66d5cfba0": {
|
|
||||||
"sourcePathHash": "cd4c15d66d5cfba0c568b6f3ac4ceac9a7cea797eca8c6bf5eab81187e70f7d7",
|
|
||||||
"sourceSha256": "a9b109dc659e8e65645b6507ab2a39a04598d0c0ed63feaef0985765d3ff9f3f",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/knowledge/skills/lessons",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "经验回扫能力已合并到 lessons 的 audit 模式",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:a6062c4036828615": {
|
|
||||||
"sourcePathHash": "a6062c4036828615945db311cf68c2ac886feef91c79b53d882bbe41fd71143f",
|
|
||||||
"sourceSha256": "94b7e58d4c4903498cf29365fe466aaba8bfdf4675754f65cd30bad37c99e02f",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/knowledge/skills/distill",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:f58c6cd52bc983e8": {
|
|
||||||
"sourcePathHash": "f58c6cd52bc983e87d265308ade0da8d89bd74d5d4f1e96fc463ef299a9fe94a",
|
|
||||||
"sourceSha256": "98392eb0fe65dab6877495e6863a6722cd2777f5767dfd95b3a829ed7798a5e7",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/knowledge/skills/handoff",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:258ee10299925494": {
|
|
||||||
"sourcePathHash": "258ee1029992549420c8465c8de2d63c1bfaf9334b812d6b79a07a9823b49733",
|
|
||||||
"sourceSha256": "b4958d4aee3c06b778273d87a07dd423cf15fc8fbfdde27e00552ff60138795e",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/analyze-bugs",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:52592d4a59314c74": {
|
|
||||||
"sourcePathHash": "52592d4a59314c74de9b58cfb4cda6e9f5d9514bb05c9b586044c12754982d52",
|
|
||||||
"sourceSha256": "e8be1b2c17ab3aaccbb5559a07b64da8a7c718293e83cb8dd685c87708c7cbdc",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/upgrade",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:f478193705d1d02d": {
|
|
||||||
"sourcePathHash": "f478193705d1d02d6bb6375728dc10d34487e3f67ab8d164a80104f6a71fccdc",
|
|
||||||
"sourceSha256": "218eb591d1944b2280853bbf39cb028f9a41e55dee01218838b14f87755fad72",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "official:browser-computer-use",
|
|
||||||
"reason": "Codex 浏览器与计算机操作能力已覆盖宿主浏览器代理配置",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:e4b14f4b2719e28b": {
|
|
||||||
"sourcePathHash": "e4b14f4b2719e28b409bc66980b3698588684dde81a0c627e862262e76c34b2f",
|
|
||||||
"sourceSha256": "de185022a0cf8bfce340709f47052578ec761664c96723172c48bdd4f10e71f1",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/dev/skills/review-code",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "分支、提交与目录审查已合并到统一代码审查入口",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:aa42f7c1a045bb49": {
|
|
||||||
"sourcePathHash": "aa42f7c1a045bb496bbc9e62beb7a7b74544153ed9551492f5b801ca4c9df098",
|
|
||||||
"sourceSha256": "aff6fad01afa2a15756a36d210c67b229a1eb2155981292450f0487c3005125d",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/knowledge/skills/worklog",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:3763e17cff825df6": {
|
|
||||||
"sourcePathHash": "3763e17cff825df6920f57d84736093082014f2a0316d9e84ee18ac8078ff6e5",
|
|
||||||
"sourceSha256": "bd2e4ae7f8a9bded1f7a983289fb49556f30cd5d6c6382500dd8c464fdd66e02",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/doc/skills/archive",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:9e3e75840a46af96": {
|
|
||||||
"sourcePathHash": "9e3e75840a46af96c33a1885f192813239b511eb972a4962b30b76de7392c88c",
|
|
||||||
"sourceSha256": "379c41e41d833880653ce065a63380e9cf29d2b7663f7ee91a3d77594202d042",
|
|
||||||
"status": "superseded",
|
|
||||||
"target": "plugins/dev/skills/upgrade",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reason": "前后端升级合并为按真实版本与官方迁移资料执行的统一能力",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:766516f82e8e9be3": {
|
|
||||||
"sourcePathHash": "766516f82e8e9be32e1ff3f29123202b6bad85bf9a88e36f87f256446ea31eaf",
|
|
||||||
"sourceSha256": "c66ca1651923b20cd62395ba2cc6b8532093d5df10f420c1673a103a88dbf5aa",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/doc/skills/message",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:31dec2fa162b06d3": {
|
|
||||||
"sourcePathHash": "31dec2fa162b06d379aa676d426d2de01c3b082061ff15adb7a8c721cd5635af",
|
|
||||||
"sourceSha256": "a146d79ccfec7b1f9275cda8e6cf3ab1190430d0c3bc11886a722ce1d6fabc90",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/skill/skills/guidance-edit",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:988b200efd0b8be0": {
|
|
||||||
"sourcePathHash": "988b200efd0b8be024060e0785da2f864bf7ee30d7c7b434fde7ab1e4cdcc9d8",
|
|
||||||
"sourceSha256": "e7fa073d592167c07b06698d664192baafb126afa1a6bfdc50a056e1ea13e56f",
|
|
||||||
"status": "excluded",
|
|
||||||
"reason": "CraftKit 原生面向 Codex,不需要 Claude Code 到 Codex 的平台转换能力",
|
|
||||||
"reviewedAt": "2026-08-25"
|
|
||||||
},
|
|
||||||
"source-b:8a7cdf719d8f8a3e": {
|
|
||||||
"sourcePathHash": "8a7cdf719d8f8a3e0200781a82062cc5a839e4d2c2fd400982771968245d62dd",
|
|
||||||
"sourceSha256": "3461485ba35dceffc5fc0e9266793003a0355d412aa8a609c93709745226d00a",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/dev/skills/estimate",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:6e5eb2b9ab9273cf": {
|
|
||||||
"sourcePathHash": "6e5eb2b9ab9273cf889bd908ff446eda59cc3e121ee7a1861fb2664d7fa049e9",
|
|
||||||
"sourceSha256": "31eef261e6e3e930cd88b18c938176c3ef1d3e8d039986712ee6c3191481872d",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/doc/skills/report",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
},
|
|
||||||
"source-b:a985b2c0fb927117": {
|
|
||||||
"sourcePathHash": "a985b2c0fb9271174d9d46425eb6792d2483ebcd6da89ecc1cbfb61407259499",
|
|
||||||
"sourceSha256": "e2f92df8dc489e79bfdd74de8817abf742f425422765c76c4a46263d1fbd7a9e",
|
|
||||||
"status": "migrated",
|
|
||||||
"target": "plugins/doc/skills/requirements",
|
|
||||||
"targetVersion": "0.1.0",
|
|
||||||
"reviewedAt": "2026-08-26"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
"""开发分析与设计批次的职责边界测试。"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
SKILLS = ROOT / "plugins" / "dev" / "skills"
|
|
||||||
|
|
||||||
|
|
||||||
def read_skill(name: str) -> str:
|
|
||||||
"""合并读取 Skill 入口及引用文件。"""
|
|
||||||
folder = SKILLS / name
|
|
||||||
files = [folder / "SKILL.md", *sorted((folder / "references").glob("*.md"))]
|
|
||||||
return "\n".join(path.read_text(encoding="utf-8") for path in files)
|
|
||||||
|
|
||||||
|
|
||||||
class DevDesignBatchTest(unittest.TestCase):
|
|
||||||
"""验证四个 Skill 的通用化设计和相互边界。"""
|
|
||||||
|
|
||||||
def test_plan_is_evidence_based_and_does_not_implement(self) -> None:
|
|
||||||
content = read_skill("plan-change")
|
|
||||||
self.assertIn("不执行设计或编码", content)
|
|
||||||
self.assertIn("验收标准", content)
|
|
||||||
self.assertIn("运行行为标为待验证", content)
|
|
||||||
self.assertNotIn("docs/{moduleCode}", content)
|
|
||||||
|
|
||||||
def test_backend_design_uses_project_version(self) -> None:
|
|
||||||
content = read_skill("design-backend")
|
|
||||||
self.assertIn("精确版本", content)
|
|
||||||
self.assertIn("guidance", content)
|
|
||||||
self.assertIn("事务边界", content)
|
|
||||||
self.assertIn("不生成业务代码", content)
|
|
||||||
|
|
||||||
def test_frontend_design_composes_existing_helpers(self) -> None:
|
|
||||||
content = read_skill("design-frontend")
|
|
||||||
for helper in ("component", "style", "form", "prepare-api"):
|
|
||||||
self.assertIn(helper, content)
|
|
||||||
self.assertIn("不编造组件", content)
|
|
||||||
self.assertIn("不跨版本混用", content)
|
|
||||||
|
|
||||||
def test_prepare_api_does_not_assume_contract(self) -> None:
|
|
||||||
content = read_skill("prepare-api")
|
|
||||||
self.assertIn("不预设请求客户端", content)
|
|
||||||
self.assertIn("未匹配字段", content)
|
|
||||||
self.assertIn("不发起真实请求", content)
|
|
||||||
self.assertIn("不能反向覆盖", content)
|
|
||||||
|
|
||||||
def test_descriptions_are_distinct(self) -> None:
|
|
||||||
descriptions = []
|
|
||||||
for name in ("plan-change", "design-backend", "design-frontend", "prepare-api"):
|
|
||||||
text = (SKILLS / name / "SKILL.md").read_text(encoding="utf-8")
|
|
||||||
descriptions.append(text.split("---", 2)[1])
|
|
||||||
self.assertEqual(4, len(set(descriptions)))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
"""验证文档批次三个独立实现的核心行为和安全边界。"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from docx import Document
|
|
||||||
from openpyxl import Workbook
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
MD_SCRIPT = ROOT / "plugins/doc/skills/md-to-docx/scripts/convert.py"
|
|
||||||
XLSX_SCRIPT = ROOT / "plugins/doc/skills/xlsx-to-md/scripts/convert.py"
|
|
||||||
ARCHIVE_SCRIPT = ROOT / "plugins/doc/skills/archive/scripts/archive.py"
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentBatchTests(unittest.TestCase):
|
|
||||||
"""使用临时目录执行真实转换,不依赖仓库外测试材料。"""
|
|
||||||
|
|
||||||
def run_script(self, script: Path, *args: object) -> subprocess.CompletedProcess[str]:
|
|
||||||
"""使用当前受控 Python 运行时执行目标脚本。"""
|
|
||||||
|
|
||||||
return subprocess.run(
|
|
||||||
[str(Path(__import__("sys").executable)), str(script), *(str(value) for value in args)],
|
|
||||||
capture_output=True, text=True, encoding="utf-8", check=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_markdown_to_docx_preserves_common_blocks(self) -> None:
|
|
||||||
"""标题、列表、表格和代码块应生成可重新打开的 DOCX。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
folder = Path(temp); source = folder / "sample.md"; output = folder / "sample.docx"
|
|
||||||
source.write_text("# 标题\n\n- 项目\n\n| 名称 | 值 |\n| --- | --- |\n| A | 1 |\n\n```py\nprint('ok')\n```\n", encoding="utf-8")
|
|
||||||
result = self.run_script(MD_SCRIPT, source)
|
|
||||||
self.assertEqual(0, result.returncode, result.stderr)
|
|
||||||
document = Document(output)
|
|
||||||
self.assertEqual("标题", document.paragraphs[0].text)
|
|
||||||
self.assertEqual(1, len(document.tables))
|
|
||||||
self.assertNotEqual(0, self.run_script(MD_SCRIPT, source).returncode)
|
|
||||||
|
|
||||||
def test_xlsx_to_markdown_handles_merges_and_escaping(self) -> None:
|
|
||||||
"""合并区域应填充值,表格特殊字符应安全转义。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
folder = Path(temp); source = folder / "sample.xlsx"
|
|
||||||
workbook = Workbook(); sheet = workbook.active; sheet.title = "数据"
|
|
||||||
sheet.append(["名称", "值"]); sheet.append(["A|B", "=1+1"])
|
|
||||||
sheet.merge_cells("A3:B3"); sheet["A3"] = "合并"
|
|
||||||
workbook.create_sheet("空表"); workbook.save(source)
|
|
||||||
result = self.run_script(XLSX_SCRIPT, source)
|
|
||||||
self.assertEqual(0, result.returncode, result.stderr)
|
|
||||||
markdown = source.with_suffix(".md").read_text(encoding="utf-8")
|
|
||||||
self.assertIn(r"A\|B", markdown)
|
|
||||||
self.assertIn("| 合并 | 合并 |", markdown)
|
|
||||||
self.assertIn("_空工作表_", markdown)
|
|
||||||
|
|
||||||
def test_archive_is_dry_run_first_and_keeps_source(self) -> None:
|
|
||||||
"""预演不得写目标,执行后仍须保留来源并只抽取指定章节。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
root = Path(temp); source = root / "specs/api.md"; source.parent.mkdir()
|
|
||||||
source.write_text("---\nowner: team\n---\n# 概述\n正文\n## API\n接口\n## 其他\n忽略\n", encoding="utf-8")
|
|
||||||
config = root / "rules.json"
|
|
||||||
config.write_text(json.dumps({"version": 1, "archiveRoot": "archive", "rules": [{"match": "specs/*.md", "target": "{stem}.md", "section": "API", "stripFrontmatter": True, "requiredText": ["接口"]}]}, ensure_ascii=False), encoding="utf-8")
|
|
||||||
target = root / "archive/api.md"
|
|
||||||
preview = self.run_script(ARCHIVE_SCRIPT, "--root", root, "--config", config)
|
|
||||||
self.assertEqual(0, preview.returncode, preview.stderr); self.assertFalse(target.exists())
|
|
||||||
applied = self.run_script(ARCHIVE_SCRIPT, "--root", root, "--config", config, "--apply")
|
|
||||||
self.assertEqual(0, applied.returncode, applied.stderr)
|
|
||||||
self.assertTrue(source.exists()); self.assertEqual("## API\n接口\n", target.read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import importlib.util
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
import zipfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
SCRIPT = (
|
|
||||||
Path(__file__).parents[2]
|
|
||||||
/ "plugins"
|
|
||||||
/ "doc"
|
|
||||||
/ "skills"
|
|
||||||
/ "docx-to-md"
|
|
||||||
/ "scripts"
|
|
||||||
/ "convert.py"
|
|
||||||
)
|
|
||||||
SPEC = importlib.util.spec_from_file_location("docx_to_md_convert", SCRIPT)
|
|
||||||
MODULE = importlib.util.module_from_spec(SPEC)
|
|
||||||
assert SPEC.loader is not None
|
|
||||||
sys.modules[SPEC.name] = MODULE
|
|
||||||
SPEC.loader.exec_module(MODULE)
|
|
||||||
|
|
||||||
|
|
||||||
class DocxToMarkdownTest(unittest.TestCase):
|
|
||||||
"""验证转换器的核心内容、覆盖保护和输入边界。"""
|
|
||||||
|
|
||||||
def make_docx(self, path: Path) -> None:
|
|
||||||
"""创建只包含公开 OOXML 结构的最小测试文档。"""
|
|
||||||
|
|
||||||
document = """<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
||||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
||||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
|
||||||
<w:body>
|
|
||||||
<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:t>测试标题</w:t></w:r></w:p>
|
|
||||||
<w:p><w:r><w:rPr><w:b/></w:rPr><w:t>加粗正文</w:t></w:r>
|
|
||||||
<w:hyperlink r:id="rLink"><w:r><w:t>示例链接</w:t></w:r></w:hyperlink></w:p>
|
|
||||||
<w:p><w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr>
|
|
||||||
<w:r><w:t>列表项目</w:t></w:r></w:p>
|
|
||||||
<w:p><w:r><w:drawing><a:blip r:embed="rImage"/></w:drawing></w:r></w:p>
|
|
||||||
<w:tbl>
|
|
||||||
<w:tr><w:tc><w:p><w:r><w:t>名称</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>值</w:t></w:r></w:p></w:tc></w:tr>
|
|
||||||
<w:tr><w:tc><w:p><w:r><w:t>A</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>1</w:t></w:r></w:p></w:tc></w:tr>
|
|
||||||
</w:tbl>
|
|
||||||
</w:body>
|
|
||||||
</w:document>"""
|
|
||||||
styles = """<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
||||||
<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/></w:style>
|
|
||||||
</w:styles>"""
|
|
||||||
numbering = """<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
||||||
<w:abstractNum w:abstractNumId="0"><w:lvl w:ilvl="0"><w:numFmt w:val="bullet"/></w:lvl></w:abstractNum>
|
|
||||||
<w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>
|
|
||||||
</w:numbering>"""
|
|
||||||
relationships = """<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
||||||
<Relationship Id="rLink" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="https://example.com" TargetMode="External"/>
|
|
||||||
<Relationship Id="rImage" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/test.png"/>
|
|
||||||
</Relationships>"""
|
|
||||||
with zipfile.ZipFile(path, "w") as archive:
|
|
||||||
archive.writestr("word/document.xml", document)
|
|
||||||
archive.writestr("word/styles.xml", styles)
|
|
||||||
archive.writestr("word/numbering.xml", numbering)
|
|
||||||
archive.writestr("word/_rels/document.xml.rels", relationships)
|
|
||||||
archive.writestr("word/media/test.png", b"\x89PNG\r\n\x1a\n")
|
|
||||||
archive.writestr("word/comments.xml", "<comments/>")
|
|
||||||
|
|
||||||
def test_convert_common_content_and_report_warning(self) -> None:
|
|
||||||
"""常见结构应转换,无法处理的批注应明确告警。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
root = Path(temp)
|
|
||||||
source = root / "sample.docx"
|
|
||||||
output = root / "result"
|
|
||||||
self.make_docx(source)
|
|
||||||
|
|
||||||
result = MODULE.DocxConverter(source, output).convert()
|
|
||||||
markdown = result.output_file.read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
self.assertIn("# 测试标题", markdown)
|
|
||||||
self.assertIn("**加粗正文**", markdown)
|
|
||||||
self.assertIn("[示例链接](https://example.com)", markdown)
|
|
||||||
self.assertIn("- 列表项目", markdown)
|
|
||||||
self.assertIn("| 名称 | 值 |", markdown)
|
|
||||||
self.assertIn("
|
|
||||||
self.assertEqual(result.images, 1)
|
|
||||||
self.assertTrue(any("批注" in warning for warning in result.warnings))
|
|
||||||
|
|
||||||
def test_existing_output_requires_force(self) -> None:
|
|
||||||
"""默认不得写入已有输出目录,显式覆盖后才可继续。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
root = Path(temp)
|
|
||||||
source = root / "sample.docx"
|
|
||||||
output = root / "result"
|
|
||||||
self.make_docx(source)
|
|
||||||
output.mkdir()
|
|
||||||
|
|
||||||
with self.assertRaises(FileExistsError):
|
|
||||||
MODULE.DocxConverter(source, output).convert()
|
|
||||||
|
|
||||||
result = MODULE.DocxConverter(source, output, force=True).convert()
|
|
||||||
self.assertTrue(result.output_file.exists())
|
|
||||||
|
|
||||||
def test_cli_rejects_non_docx(self) -> None:
|
|
||||||
"""命令行入口应拒绝扩展名不正确的文件。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
source = Path(temp) / "sample.txt"
|
|
||||||
source.write_text("not docx", encoding="utf-8")
|
|
||||||
self.assertEqual(MODULE.main([str(source)]), 2)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
"""前端辅助 Skill 的结构与行为边界测试。"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
DEV_SKILLS = ROOT / "plugins" / "dev" / "skills"
|
|
||||||
|
|
||||||
|
|
||||||
def read_skill(name: str) -> str:
|
|
||||||
"""读取指定 Skill 的入口及引用文档。"""
|
|
||||||
folder = DEV_SKILLS / name
|
|
||||||
files = [folder / "SKILL.md", *sorted((folder / "references").glob("*.md"))]
|
|
||||||
return "\n".join(path.read_text(encoding="utf-8") for path in files)
|
|
||||||
|
|
||||||
|
|
||||||
class FrontendHelpersTest(unittest.TestCase):
|
|
||||||
"""验证三个 Skill 能互补协作且不恢复来源专有规则。"""
|
|
||||||
|
|
||||||
def test_component_requires_project_evidence(self) -> None:
|
|
||||||
content = read_skill("component")
|
|
||||||
self.assertIn(".craftkit/project.json", content)
|
|
||||||
self.assertIn(".craftkit/standards/frontend/components.md", content)
|
|
||||||
self.assertIn("不编造结论", content)
|
|
||||||
self.assertIn("官方文档", content)
|
|
||||||
self.assertIn("不复制依赖库文档", content)
|
|
||||||
|
|
||||||
def test_style_uses_existing_visual_authority(self) -> None:
|
|
||||||
content = read_skill("style")
|
|
||||||
self.assertIn("设计令牌", content)
|
|
||||||
self.assertIn("经用户确认后再修改", content)
|
|
||||||
self.assertIn("不内置固定矩阵", content)
|
|
||||||
|
|
||||||
def test_form_does_not_invent_business_rules(self) -> None:
|
|
||||||
content = read_skill("form")
|
|
||||||
self.assertIn("不自行确定字段必填", content)
|
|
||||||
self.assertIn("不设计接口", content)
|
|
||||||
self.assertIn("component", content)
|
|
||||||
|
|
||||||
def test_precise_skills_replace_router(self) -> None:
|
|
||||||
self.assertFalse((DEV_SKILLS / "guide").exists())
|
|
||||||
descriptions = {
|
|
||||||
name: (DEV_SKILLS / name / "SKILL.md").read_text(encoding="utf-8").split("---", 2)[1]
|
|
||||||
for name in ("component", "style", "form")
|
|
||||||
}
|
|
||||||
self.assertEqual(3, len(set(descriptions.values())))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
"""验证 Git 变更导出的选择、配置保护和写入边界。"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
SCRIPT = ROOT / "plugins/git/skills/export/scripts/export.py"
|
|
||||||
|
|
||||||
|
|
||||||
class GitExportTests(unittest.TestCase):
|
|
||||||
"""在临时 Git 仓库中验证真实命令行为。"""
|
|
||||||
|
|
||||||
def git(self, repo: Path, *args: str) -> str:
|
|
||||||
"""执行测试仓库内的 Git 命令。"""
|
|
||||||
|
|
||||||
result = subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True, encoding="utf-8", check=True)
|
|
||||||
return result.stdout.strip()
|
|
||||||
|
|
||||||
def run_export(self, repo: Path, target: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
|
||||||
"""使用当前受控 Python 运行导出脚本。"""
|
|
||||||
|
|
||||||
return subprocess.run([sys.executable, str(SCRIPT), "--repo", str(repo), "--target", str(target), *args], capture_output=True, text=True, encoding="utf-8", check=False)
|
|
||||||
|
|
||||||
def make_repo(self, folder: Path) -> Path:
|
|
||||||
"""创建具有稳定身份和首个提交的隔离仓库。"""
|
|
||||||
|
|
||||||
repo = folder / "repo"; repo.mkdir()
|
|
||||||
self.git(repo, "init", "-q"); self.git(repo, "config", "user.name", "Test User"); self.git(repo, "config", "user.email", "test@example.invalid")
|
|
||||||
(repo / "src").mkdir(); (repo / "src/app.txt").write_text("v1\n", encoding="utf-8")
|
|
||||||
self.git(repo, "add", "src/app.txt"); self.git(repo, "commit", "-q", "-m", "init")
|
|
||||||
return repo
|
|
||||||
|
|
||||||
def test_worktree_preview_classifies_shared_and_protected_files(self) -> None:
|
|
||||||
"""共享 CraftKit 文档可导出,环境配置默认受保护,私钥永久阻断。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
root = Path(temp); repo = self.make_repo(root); target = root / "out"
|
|
||||||
(repo / "src/app.txt").write_text("v2\n", encoding="utf-8")
|
|
||||||
(repo / "application-dev.yml").write_text("server: local\n", encoding="utf-8")
|
|
||||||
(repo / "private.pem").write_text("-----BEGIN PRIVATE KEY-----\nabc\n", encoding="utf-8")
|
|
||||||
standard = repo / ".craftkit/standards/export.md"; standard.parent.mkdir(parents=True); standard.write_text("规范\n", encoding="utf-8")
|
|
||||||
result = self.run_export(repo, target, "--mode", "worktree", "--snapshot", "worktree", "--include-untracked")
|
|
||||||
self.assertEqual(2, result.returncode)
|
|
||||||
manifest = json.loads(result.stdout)
|
|
||||||
self.assertIn("src/app.txt", manifest["exported"])
|
|
||||||
self.assertIn(".craftkit/standards/export.md", manifest["exported"])
|
|
||||||
self.assertIn("application-dev.yml", manifest["protected"])
|
|
||||||
self.assertIn("private.pem", manifest["blocked"])
|
|
||||||
self.assertFalse(target.exists())
|
|
||||||
|
|
||||||
def test_protected_file_requires_matching_project_evidence(self) -> None:
|
|
||||||
"""环境配置只有在项目规范明确点名且执行确认后才写入。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
root = Path(temp); repo = self.make_repo(root); target = root / "out"
|
|
||||||
(repo / "application-dev.yml").write_text("server: local\n", encoding="utf-8")
|
|
||||||
evidence = repo / ".craftkit/standards/delivery.md"; evidence.parent.mkdir(parents=True)
|
|
||||||
evidence.write_text("交付必须包含 application-dev.yml。\n", encoding="utf-8")
|
|
||||||
result = self.run_export(repo, target, "--mode", "worktree", "--snapshot", "worktree", "--include-untracked",
|
|
||||||
"--allow-protected", "--policy-evidence", ".craftkit/standards/delivery.md", "--apply")
|
|
||||||
self.assertEqual(0, result.returncode, result.stderr)
|
|
||||||
self.assertTrue((target / "application-dev.yml").is_file())
|
|
||||||
manifest = json.loads((target / "export-manifest.json").read_text(encoding="utf-8"))
|
|
||||||
self.assertEqual(".craftkit/standards/delivery.md", manifest["policyEvidence"])
|
|
||||||
|
|
||||||
def test_range_uses_explicit_snapshot_and_records_deleted_file(self) -> None:
|
|
||||||
"""范围负责选路径,快照负责取内容;快照中删除的文件只进入清单。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
root = Path(temp); repo = self.make_repo(root); target = root / "out"
|
|
||||||
base = self.git(repo, "rev-parse", "HEAD")
|
|
||||||
(repo / "src/app.txt").unlink(); (repo / "src/new.txt").write_text("new\n", encoding="utf-8")
|
|
||||||
self.git(repo, "add", "-A"); self.git(repo, "commit", "-q", "-m", "change")
|
|
||||||
head = self.git(repo, "rev-parse", "HEAD")
|
|
||||||
result = self.run_export(repo, target, "--mode", "range", "--base", base, "--head", head, "--snapshot", head)
|
|
||||||
self.assertEqual(0, result.returncode, result.stderr)
|
|
||||||
manifest = json.loads(result.stdout)
|
|
||||||
self.assertIn("src/app.txt", manifest["deleted"])
|
|
||||||
self.assertIn("src/new.txt", manifest["exported"])
|
|
||||||
|
|
||||||
def test_time_mode_selects_committed_paths_without_writing(self) -> None:
|
|
||||||
"""时间模式应选择提交中出现的路径,并保持预演只读。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
root = Path(temp); repo = self.make_repo(root); target = root / "out"
|
|
||||||
(repo / "src/app.txt").write_text("v2\n", encoding="utf-8")
|
|
||||||
self.git(repo, "add", "src/app.txt"); self.git(repo, "commit", "-q", "-m", "update")
|
|
||||||
result = self.run_export(repo, target, "--mode", "time", "--since", "2000-01-01", "--snapshot", "HEAD")
|
|
||||||
self.assertEqual(0, result.returncode, result.stderr)
|
|
||||||
self.assertIn("src/app.txt", json.loads(result.stdout)["exported"])
|
|
||||||
self.assertFalse(target.exists())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
"""验证预集成 Skill 的文档契约和隔离 Git 行为。"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
SKILL = ROOT / "plugins/git/skills/integrate/SKILL.md"
|
|
||||||
|
|
||||||
|
|
||||||
class GitIntegrateTests(unittest.TestCase):
|
|
||||||
"""在临时仓库中验证 worktree 集成不会移动输入引用。"""
|
|
||||||
|
|
||||||
def git(self, repo: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
||||||
"""执行测试仓库中的 Git 命令并保留失败状态供冲突断言。"""
|
|
||||||
|
|
||||||
return subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True, encoding="utf-8", check=check)
|
|
||||||
|
|
||||||
def make_repo(self, root: Path, conflict: bool = False) -> tuple[Path, str, str]:
|
|
||||||
"""创建具有源、目标分叉的隔离仓库。"""
|
|
||||||
|
|
||||||
repo = root / "repo"; repo.mkdir(); self.git(repo, "init", "-q")
|
|
||||||
self.git(repo, "config", "user.name", "Test User"); self.git(repo, "config", "user.email", "test@example.invalid")
|
|
||||||
(repo / "base.txt").write_text("base\n", encoding="utf-8")
|
|
||||||
self.git(repo, "add", "base.txt"); self.git(repo, "commit", "-q", "-m", "base")
|
|
||||||
self.git(repo, "branch", "target"); self.git(repo, "switch", "-q", "-c", "source")
|
|
||||||
source_file = repo / ("base.txt" if conflict else "source.txt")
|
|
||||||
source_file.write_text("source\n", encoding="utf-8")
|
|
||||||
self.git(repo, "add", "-A"); self.git(repo, "commit", "-q", "-m", "source")
|
|
||||||
source_hash = self.git(repo, "rev-parse", "source").stdout.strip()
|
|
||||||
self.git(repo, "switch", "-q", "target")
|
|
||||||
target_file = repo / ("base.txt" if conflict else "target.txt")
|
|
||||||
target_file.write_text("target\n", encoding="utf-8")
|
|
||||||
self.git(repo, "add", "-A"); self.git(repo, "commit", "-q", "-m", "target")
|
|
||||||
target_hash = self.git(repo, "rev-parse", "target").stdout.strip()
|
|
||||||
return repo, source_hash, target_hash
|
|
||||||
|
|
||||||
def test_isolated_merge_preserves_source_and_target_refs(self) -> None:
|
|
||||||
"""预集成分支发生 merge 后,源和目标引用必须保持原哈希。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
root = Path(temp); repo, source_hash, target_hash = self.make_repo(root)
|
|
||||||
worktree = root / "integration"
|
|
||||||
self.git(repo, "worktree", "add", "-q", "-b", "pre/integration", str(worktree), "target")
|
|
||||||
self.git(worktree, "merge", "--no-edit", "source")
|
|
||||||
self.assertEqual(source_hash, self.git(repo, "rev-parse", "source").stdout.strip())
|
|
||||||
self.assertEqual(target_hash, self.git(repo, "rev-parse", "target").stdout.strip())
|
|
||||||
self.assertTrue((worktree / "source.txt").is_file())
|
|
||||||
self.assertTrue((worktree / "target.txt").is_file())
|
|
||||||
|
|
||||||
def test_conflict_stops_before_automatic_resolution(self) -> None:
|
|
||||||
"""真实冲突应停留在未合并状态,不移动源或目标引用。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
root = Path(temp); repo, source_hash, target_hash = self.make_repo(root, conflict=True)
|
|
||||||
worktree = root / "integration"
|
|
||||||
self.git(repo, "worktree", "add", "-q", "-b", "pre/conflict", str(worktree), "target")
|
|
||||||
result = self.git(worktree, "merge", "--no-edit", "source", check=False)
|
|
||||||
self.assertNotEqual(0, result.returncode)
|
|
||||||
self.assertIn("UU base.txt", self.git(worktree, "status", "--short").stdout)
|
|
||||||
self.assertEqual(source_hash, self.git(repo, "rev-parse", "source").stdout.strip())
|
|
||||||
self.assertEqual(target_hash, self.git(repo, "rev-parse", "target").stdout.strip())
|
|
||||||
|
|
||||||
def test_skill_separates_stages_and_protects_delivery(self) -> None:
|
|
||||||
"""Skill 必须保留阶段授权、环境配置阻断和强推禁用边界。"""
|
|
||||||
|
|
||||||
text = SKILL.read_text(encoding="utf-8")
|
|
||||||
for value in ("assess", "prepare", "publish", "独立 worktree", "环境配置", "私钥"):
|
|
||||||
self.assertIn(value, text)
|
|
||||||
publish = (SKILL.parent / "references/publish.md").read_text(encoding="utf-8")
|
|
||||||
self.assertIn("默认不改用 `--force`", publish)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
"""规范索引维护 Skill 的确定性行为测试。"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
SKILL = ROOT / "plugins" / "skill" / "skills" / "guidance-edit"
|
|
||||||
SCRIPT = SKILL / "scripts" / "check_index.py"
|
|
||||||
|
|
||||||
|
|
||||||
class GuidanceEditTest(unittest.TestCase):
|
|
||||||
"""验证索引检查器和读写职责边界。"""
|
|
||||||
|
|
||||||
def run_check(self, standards: Path) -> subprocess.CompletedProcess[str]:
|
|
||||||
"""以 JSON 模式执行检查器。"""
|
|
||||||
return subprocess.run(
|
|
||||||
[sys.executable, str(SCRIPT), str(standards), "--json"],
|
|
||||||
check=False,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_clean_index_is_reachable(self) -> None:
|
|
||||||
"""根索引能够逐级访问所有规范时应检查通过。"""
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
standards = Path(temp)
|
|
||||||
(standards / "frontend").mkdir()
|
|
||||||
(standards / "index.md").write_text(
|
|
||||||
"[前端](frontend/index.md)\n", encoding="utf-8"
|
|
||||||
)
|
|
||||||
(standards / "frontend" / "index.md").write_text(
|
|
||||||
"[组件](components.md)\n", encoding="utf-8"
|
|
||||||
)
|
|
||||||
(standards / "frontend" / "components.md").write_text(
|
|
||||||
"# 组件\n", encoding="utf-8"
|
|
||||||
)
|
|
||||||
result = self.run_check(standards)
|
|
||||||
self.assertEqual(0, result.returncode, result.stderr)
|
|
||||||
report = json.loads(result.stdout)
|
|
||||||
self.assertEqual([], report["unindexed"])
|
|
||||||
self.assertEqual([], report["broken"])
|
|
||||||
|
|
||||||
def test_reports_unindexed_broken_and_cycle(self) -> None:
|
|
||||||
"""未索引文件、失效链接和循环引用应同时报告。"""
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
standards = Path(temp)
|
|
||||||
(standards / "index.md").write_text(
|
|
||||||
"[主题](topic.md)\n[缺失](missing.md)\n", encoding="utf-8"
|
|
||||||
)
|
|
||||||
(standards / "topic.md").write_text(
|
|
||||||
"[返回](index.md)\n", encoding="utf-8"
|
|
||||||
)
|
|
||||||
(standards / "orphan.md").write_text("# 未索引\n", encoding="utf-8")
|
|
||||||
result = self.run_check(standards)
|
|
||||||
self.assertEqual(1, result.returncode)
|
|
||||||
report = json.loads(result.stdout)
|
|
||||||
self.assertEqual(["orphan.md"], report["unindexed"])
|
|
||||||
self.assertEqual("missing.md", report["broken"][0]["target"])
|
|
||||||
self.assertTrue(report["cycles"])
|
|
||||||
|
|
||||||
def test_missing_entry_is_input_error(self) -> None:
|
|
||||||
"""入口不存在时应返回输入错误,而不是覆盖问题。"""
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
result = self.run_check(Path(temp))
|
|
||||||
self.assertEqual(2, result.returncode)
|
|
||||||
self.assertIn("索引入口不存在", result.stderr)
|
|
||||||
|
|
||||||
def test_skill_separates_read_and_write_modes(self) -> None:
|
|
||||||
"""检查默认只读,所有写入均需预览与确认。"""
|
|
||||||
content = (SKILL / "SKILL.md").read_text(encoding="utf-8")
|
|
||||||
self.assertIn("检查默认只读", content)
|
|
||||||
self.assertIn("经用户确认后写入", content)
|
|
||||||
self.assertIn("不强制创建固定层级", content)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import json
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).parents[2]
|
|
||||||
GUIDANCE = ROOT / "plugins" / "skill" / "skills" / "guidance"
|
|
||||||
INIT = ROOT / "plugins" / "knowledge" / "skills" / "init"
|
|
||||||
|
|
||||||
|
|
||||||
class GuidanceInitTest(unittest.TestCase):
|
|
||||||
"""验证规范检索和项目初始化之间的职责边界。"""
|
|
||||||
|
|
||||||
def test_guidance_is_read_only_and_uses_progressive_sources(self) -> None:
|
|
||||||
"""检索 Skill 应声明项目优先、渐进读取且不隐式初始化。"""
|
|
||||||
|
|
||||||
content = (GUIDANCE / "SKILL.md").read_text(encoding="utf-8")
|
|
||||||
self.assertIn("以只读方式", content)
|
|
||||||
self.assertIn("不得先递归加载整个知识库", content)
|
|
||||||
self.assertIn("不得在检索过程中隐式初始化", content)
|
|
||||||
self.assertIn(".craftkit/project.json", content)
|
|
||||||
|
|
||||||
def test_public_baseline_routes_frontend_and_backend(self) -> None:
|
|
||||||
"""公共基线应提供前后端入口,并让索引引用实际规则文件。"""
|
|
||||||
|
|
||||||
root = GUIDANCE / "references" / "guidance"
|
|
||||||
index = (root / "index.md").read_text(encoding="utf-8")
|
|
||||||
self.assertIn("frontend/index.md", index)
|
|
||||||
self.assertIn("backend/index.md", index)
|
|
||||||
self.assertEqual(6, len(list((root / "frontend").glob("*.md"))))
|
|
||||||
self.assertEqual(6, len(list((root / "backend").glob("*.md"))))
|
|
||||||
|
|
||||||
sources = (root / "sources.md").read_text(encoding="utf-8")
|
|
||||||
for authority in ("RFC 9110", "WCAG 2.2", "OWASP", "MDN Fetch API"):
|
|
||||||
self.assertIn(authority, sources)
|
|
||||||
|
|
||||||
def test_framework_profiles_are_selected_by_project_version(self) -> None:
|
|
||||||
"""普通任务只能使用当前 profile,且不存在的 profile 不得被猜测。"""
|
|
||||||
|
|
||||||
routing = (GUIDANCE / "references" / "profile-routing.md").read_text(
|
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
self.assertIn("普通开发任务只加载当前 profile", routing)
|
|
||||||
self.assertIn("升级、迁移或版本比较", routing)
|
|
||||||
self.assertIn("不默认最新版本", routing)
|
|
||||||
self.assertIn("不创建空目录", routing)
|
|
||||||
|
|
||||||
init = (INIT / "SKILL.md").read_text(encoding="utf-8")
|
|
||||||
config = (INIT / "references" / "project-config.md").read_text(
|
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
self.assertIn("精确版本、公共 profile", init)
|
|
||||||
for field in ('"name"', '"version"', '"profile"'):
|
|
||||||
self.assertIn(field, config)
|
|
||||||
|
|
||||||
def test_init_supports_existing_and_new_projects(self) -> None:
|
|
||||||
"""初始化 Skill 应同时包含成熟项目与空项目流程。"""
|
|
||||||
|
|
||||||
content = (INIT / "SKILL.md").read_text(encoding="utf-8")
|
|
||||||
self.assertIn("成熟项目", content)
|
|
||||||
self.assertIn("空项目", content)
|
|
||||||
self.assertIn("参考项目", content)
|
|
||||||
self.assertTrue((INIT / "references" / "existing.md").is_file())
|
|
||||||
self.assertTrue((INIT / "references" / "new.md").is_file())
|
|
||||||
|
|
||||||
def test_project_template_has_safe_shared_fields(self) -> None:
|
|
||||||
"""项目模板应包含检索入口,且不得预置内部标识或机器路径。"""
|
|
||||||
|
|
||||||
path = INIT / "assets" / "project.json"
|
|
||||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
||||||
self.assertEqual(1, payload["schemaVersion"])
|
|
||||||
self.assertIn(payload["initialization"]["mode"], {"existing", "new"})
|
|
||||||
self.assertEqual([], payload["dependencies"]["internal"])
|
|
||||||
self.assertEqual([], payload["frontend"]["componentLibraries"])
|
|
||||||
self.assertEqual([], payload["frontend"]["componentRoots"])
|
|
||||||
self.assertEqual([], payload["frontend"]["documentation"])
|
|
||||||
self.assertEqual(
|
|
||||||
".craftkit/standards/index.md",
|
|
||||||
payload["guidance"]["standardsIndex"],
|
|
||||||
)
|
|
||||||
serialized = json.dumps(payload, ensure_ascii=False)
|
|
||||||
self.assertNotIn(":\\", serialized)
|
|
||||||
|
|
||||||
def test_init_collects_frontend_component_metadata(self) -> None:
|
|
||||||
"""初始化应记录组件库证据和入口,但不能默认选择组件库。"""
|
|
||||||
|
|
||||||
content = "\n".join(
|
|
||||||
path.read_text(encoding="utf-8")
|
|
||||||
for path in (
|
|
||||||
INIT / "SKILL.md",
|
|
||||||
INIT / "references" / "project-config.md",
|
|
||||||
INIT / "references" / "existing.md",
|
|
||||||
INIT / "references" / "new.md",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertIn("componentLibraries", content)
|
|
||||||
self.assertIn("组件根", content)
|
|
||||||
self.assertIn("不默认选择流行方案", content)
|
|
||||||
self.assertTrue((INIT / "assets" / "frontend-components.md").is_file())
|
|
||||||
|
|
||||||
def test_local_directories_are_the_only_nested_ignores(self) -> None:
|
|
||||||
"""初始化模板只排除本地状态和缓存,不得排除共享资料。"""
|
|
||||||
|
|
||||||
rules = (INIT / "assets" / "craftkit.gitignore").read_text(
|
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
active = [line for line in rules.splitlines() if line and not line.startswith("#")]
|
|
||||||
self.assertEqual(["/local/", "/cache/"], active)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
"""验证知识管理批次的职责边界、中性模板和模式路由。"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
KNOWLEDGE = ROOT / "plugins/knowledge/skills"
|
|
||||||
|
|
||||||
|
|
||||||
class KnowledgeBatchTests(unittest.TestCase):
|
|
||||||
"""检查四个 Skill 不重叠,并保持项目知识目录一致。"""
|
|
||||||
|
|
||||||
def read(self, relative: str) -> str:
|
|
||||||
"""读取待验证的 Skill 资源。"""
|
|
||||||
|
|
||||||
return (KNOWLEDGE / relative).read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
def test_trace_is_evidence_based_and_does_not_auto_write(self) -> None:
|
|
||||||
"""偏离复盘应使用中性失效分类,不能自动修改规则。"""
|
|
||||||
|
|
||||||
text = self.read("trace/SKILL.md")
|
|
||||||
for value in ("missing", "unread", "mismatch", "misapplied", "insufficient", "尚未执行"):
|
|
||||||
self.assertIn(value, text)
|
|
||||||
self.assertNotIn("七问", text)
|
|
||||||
|
|
||||||
def test_distill_routes_long_term_knowledge_and_excludes_handoff(self) -> None:
|
|
||||||
"""知识提炼和任务交接必须具有不同产物边界。"""
|
|
||||||
|
|
||||||
text = self.read("distill/SKILL.md")
|
|
||||||
routing = self.read("distill/references/routing.md")
|
|
||||||
self.assertIn("不生成接续提示词", text)
|
|
||||||
for path in (".craftkit/knowledge/pitfalls/", ".craftkit/knowledge/decisions/", ".craftkit/standards/"):
|
|
||||||
self.assertIn(path, routing)
|
|
||||||
|
|
||||||
def test_lessons_merges_full_lifecycle_without_fixed_categories(self) -> None:
|
|
||||||
"""经验初始化、维护、审计和提升应共享一个目录与中性模板。"""
|
|
||||||
|
|
||||||
text = self.read("lessons/SKILL.md")
|
|
||||||
for mode in ("`init`", "`curate`", "`audit`", "`promote`"):
|
|
||||||
self.assertIn(mode, text)
|
|
||||||
self.assertIn(".craftkit/knowledge/pitfalls/", text)
|
|
||||||
index = self.read("lessons/assets/index.md")
|
|
||||||
self.assertIn("当前尚未建立分类", index)
|
|
||||||
self.assertNotIn("lowcode", index.casefold())
|
|
||||||
|
|
||||||
def test_worklog_requires_git_evidence_and_separate_write_permission(self) -> None:
|
|
||||||
"""工作日志不能推测工作量或自动写入日报文件。"""
|
|
||||||
|
|
||||||
text = self.read("worklog/SKILL.md")
|
|
||||||
self.assertIn("不把提交数量当作工作量", text)
|
|
||||||
self.assertIn("范围内确实没有提交时如实说明", text)
|
|
||||||
self.assertIn("需要新的明确授权", text)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class LocalStateTest(unittest.TestCase):
|
|
||||||
"""验证 `.craftkit/` 能同时承载共享资料和本地状态。"""
|
|
||||||
|
|
||||||
def run_git(self, root: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
|
||||||
"""在隔离仓库执行 Git,并保留输出供断言使用。"""
|
|
||||||
|
|
||||||
return subprocess.run(
|
|
||||||
["git", "-C", str(root), *args],
|
|
||||||
check=False,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_nested_gitignore_separates_shared_and_local_content(self) -> None:
|
|
||||||
"""共享资料应可提交,本地目录和缓存应由嵌套规则排除。"""
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
|
||||||
root = Path(temp)
|
|
||||||
initialized = self.run_git(root, "init")
|
|
||||||
self.assertEqual(initialized.returncode, 0, initialized.stderr)
|
|
||||||
|
|
||||||
craftkit = root / ".craftkit"
|
|
||||||
craftkit.mkdir()
|
|
||||||
(craftkit / ".gitignore").write_text(
|
|
||||||
"# Local CraftKit data\n/local/\n/cache/\n",
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
(craftkit / "README.md").write_text("# 项目资料\n", encoding="utf-8")
|
|
||||||
shared = craftkit / "standards" / "coding" / "rules.md"
|
|
||||||
shared.parent.mkdir(parents=True)
|
|
||||||
shared.write_text("# 编码规范\n", encoding="utf-8")
|
|
||||||
local = craftkit / "local" / "handoff" / "current.md"
|
|
||||||
local.parent.mkdir(parents=True)
|
|
||||||
local.write_text("# 本地交接\n", encoding="utf-8")
|
|
||||||
cache = craftkit / "cache" / "result.json"
|
|
||||||
cache.parent.mkdir(parents=True)
|
|
||||||
cache.write_text("{}\n", encoding="utf-8")
|
|
||||||
|
|
||||||
ignored = self.run_git(
|
|
||||||
root,
|
|
||||||
"check-ignore",
|
|
||||||
"--no-index",
|
|
||||||
"-v",
|
|
||||||
".craftkit/local/handoff/current.md",
|
|
||||||
)
|
|
||||||
self.assertEqual(ignored.returncode, 0, ignored.stderr)
|
|
||||||
self.assertIn(".craftkit/.gitignore", ignored.stdout.replace("\\", "/"))
|
|
||||||
|
|
||||||
cache_ignored = self.run_git(
|
|
||||||
root,
|
|
||||||
"check-ignore",
|
|
||||||
"--no-index",
|
|
||||||
".craftkit/cache/result.json",
|
|
||||||
)
|
|
||||||
self.assertEqual(cache_ignored.returncode, 0, cache_ignored.stderr)
|
|
||||||
|
|
||||||
status = self.run_git(root, "status", "--short", "--untracked-files=all")
|
|
||||||
self.assertEqual(status.returncode, 0, status.stderr)
|
|
||||||
normalized = status.stdout.replace("\\", "/")
|
|
||||||
self.assertIn(".craftkit/.gitignore", normalized)
|
|
||||||
self.assertIn(".craftkit/README.md", normalized)
|
|
||||||
self.assertIn(".craftkit/standards/coding/rules.md", normalized)
|
|
||||||
self.assertNotIn(".craftkit/local", normalized)
|
|
||||||
self.assertNotIn(".craftkit/cache", normalized)
|
|
||||||
self.assertFalse((root / ".gitignore").exists())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
"""迁移内部脚本的最小行为测试。"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import importlib.util
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
SCRIPT_ROOT = Path(__file__).parents[1] / "scripts"
|
|
||||||
|
|
||||||
|
|
||||||
def load_module(name: str):
|
|
||||||
"""从脚本路径加载模块,避免要求项目安装为 Python 包。"""
|
|
||||||
spec = importlib.util.spec_from_file_location(name, SCRIPT_ROOT / f"{name}.py")
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
assert spec.loader
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
scan_sources = load_module("scan_sources")
|
|
||||||
check_skill = load_module("check_skill")
|
|
||||||
update_lock = load_module("update_lock")
|
|
||||||
|
|
||||||
|
|
||||||
class MigrationScriptTests(unittest.TestCase):
|
|
||||||
def test_scan_uses_relative_paths_and_stable_hash(self):
|
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
|
||||||
root = Path(directory)
|
|
||||||
skill = root / "group" / "demo"
|
|
||||||
skill.mkdir(parents=True)
|
|
||||||
(skill / "SKILL.md").write_text(
|
|
||||||
"---\nname: demo\ndescription: 示例\n---\n", encoding="utf-8"
|
|
||||||
)
|
|
||||||
first = scan_sources.scan_source("source-a", root)
|
|
||||||
second = scan_sources.scan_source("source-a", root)
|
|
||||||
self.assertEqual("group/demo", first["skills"][0]["relativePath"])
|
|
||||||
self.assertEqual(first["skills"][0]["sha256"], second["skills"][0]["sha256"])
|
|
||||||
self.assertNotIn(str(root), json.dumps(first, ensure_ascii=False))
|
|
||||||
|
|
||||||
def test_check_reports_name_mismatch_and_forbidden_term(self):
|
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
|
||||||
root = Path(directory) / "expected"
|
|
||||||
root.mkdir()
|
|
||||||
(root / "SKILL.md").write_text(
|
|
||||||
"---\nname: other\ndescription: 示例\n---\n内部标识",
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
result = check_skill.check_skill(root, ["内部标识"])
|
|
||||||
codes = {issue["code"] for issue in result["issues"]}
|
|
||||||
self.assertIn("name-path-mismatch", codes)
|
|
||||||
self.assertIn("forbidden:内部标识", codes)
|
|
||||||
|
|
||||||
def test_update_lock_preserves_manual_fields(self):
|
|
||||||
path_hash = hashlib.sha256("group/demo".encode("utf-8")).hexdigest()
|
|
||||||
key = f"source-a:{path_hash[:16]}"
|
|
||||||
lock = {
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"sources": {},
|
|
||||||
"skills": {
|
|
||||||
key: {
|
|
||||||
"status": "specified",
|
|
||||||
"target": "plugins/skill/skills/demo",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
report = {
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"id": "source-a",
|
|
||||||
"commit": "abc",
|
|
||||||
"skillCount": 1,
|
|
||||||
"skills": [
|
|
||||||
{
|
|
||||||
"name": "demo",
|
|
||||||
"relativePath": "group/demo",
|
|
||||||
"sha256": "123",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
merged = update_lock.merge(lock, report)
|
|
||||||
entry = merged["skills"][key]
|
|
||||||
self.assertEqual("specified", entry["status"])
|
|
||||||
self.assertEqual("plugins/skill/skills/demo", entry["target"])
|
|
||||||
self.assertEqual("123", entry["sourceSha256"])
|
|
||||||
self.assertNotIn("name", entry)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
"""公开基线与专项设计 Skill 测试。"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
SKILLS = ROOT / "plugins" / "dev" / "skills"
|
|
||||||
NAMES = (
|
|
||||||
"design-api", "design-db", "review-java", "review-frontend",
|
|
||||||
"design-frontend-data", "review-mybatis", "design-workflow",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PublicBaselineBatchTest(unittest.TestCase):
|
|
||||||
"""验证公开来源登记和版本边界。"""
|
|
||||||
|
|
||||||
def test_sources_are_official_and_dated(self) -> None:
|
|
||||||
for name in NAMES:
|
|
||||||
source = (SKILLS / name / "references" / "sources.md").read_text(encoding="utf-8")
|
|
||||||
self.assertIn("重建日期:2026-08-25", source)
|
|
||||||
self.assertIn("https://", source)
|
|
||||||
|
|
||||||
def test_project_version_is_not_replaced_by_latest(self) -> None:
|
|
||||||
content = "\n".join(
|
|
||||||
(SKILLS / name / "SKILL.md").read_text(encoding="utf-8") for name in NAMES
|
|
||||||
)
|
|
||||||
self.assertIn("不得默认最新", content)
|
|
||||||
self.assertIn("规则混用", content)
|
|
||||||
self.assertIn("精确版本", content)
|
|
||||||
|
|
||||||
def test_workflow_does_not_embed_approval_rules(self) -> None:
|
|
||||||
content = (SKILLS / "design-workflow" / "SKILL.md").read_text(encoding="utf-8")
|
|
||||||
self.assertIn("不内置审批节点", content)
|
|
||||||
self.assertIn("项目实际流程引擎", content)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
"""剩余来源连续迁移波次验收测试。"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import json
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
DEV = ROOT / "plugins" / "dev" / "skills"
|
|
||||||
DOC = ROOT / "plugins" / "doc" / "skills"
|
|
||||||
GIT = ROOT / "plugins" / "git" / "skills"
|
|
||||||
|
|
||||||
TARGETS = {
|
|
||||||
DEV: (
|
|
||||||
"design-api", "design-db", "review-java", "review-frontend",
|
|
||||||
"design-frontend-data", "review-mybatis", "design-workflow",
|
|
||||||
"implement-backend", "implement-frontend", "review-code",
|
|
||||||
"test-backend", "test-ui", "analyze-bugs", "upgrade", "estimate",
|
|
||||||
),
|
|
||||||
DOC: ("report", "message", "requirements"),
|
|
||||||
GIT: ("release",),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RemainingWaveTest(unittest.TestCase):
|
|
||||||
"""验证目标完整性、操作边界和来源状态闭合。"""
|
|
||||||
|
|
||||||
def read_skill(self, root: Path, name: str) -> str:
|
|
||||||
return (root / name / "SKILL.md").read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
def test_all_targets_have_skill_and_ui_metadata(self) -> None:
|
|
||||||
names = []
|
|
||||||
for root, targets in TARGETS.items():
|
|
||||||
for name in targets:
|
|
||||||
names.append(name)
|
|
||||||
self.assertTrue((root / name / "SKILL.md").is_file(), name)
|
|
||||||
self.assertTrue((root / name / "agents" / "openai.yaml").is_file(), name)
|
|
||||||
self.assertEqual(19, len(names))
|
|
||||||
self.assertEqual(len(names), len(set(names)))
|
|
||||||
|
|
||||||
def test_implementation_preserves_workspace_and_authorization(self) -> None:
|
|
||||||
content = "\n".join(
|
|
||||||
self.read_skill(DEV, name)
|
|
||||||
for name in ("implement-backend", "implement-frontend")
|
|
||||||
)
|
|
||||||
self.assertIn("保留用户已有改动", content)
|
|
||||||
self.assertIn("精确版本", content)
|
|
||||||
self.assertIn("不自动暂存", content)
|
|
||||||
self.assertIn("环境配置", content)
|
|
||||||
|
|
||||||
def test_review_and_test_results_are_separated(self) -> None:
|
|
||||||
review = self.read_skill(DEV, "review-code")
|
|
||||||
backend = self.read_skill(DEV, "test-backend")
|
|
||||||
ui = self.read_skill(DEV, "test-ui")
|
|
||||||
self.assertIn("只读", review)
|
|
||||||
self.assertIn("不启动完整应用", backend)
|
|
||||||
self.assertIn("授权后", ui)
|
|
||||||
self.assertIn("不能替代浏览器结果", ui)
|
|
||||||
|
|
||||||
def test_delivery_actions_are_independently_authorized(self) -> None:
|
|
||||||
upgrade = self.read_skill(DEV, "upgrade")
|
|
||||||
estimate = self.read_skill(DEV, "estimate")
|
|
||||||
release = self.read_skill(GIT, "release")
|
|
||||||
self.assertIn("源版本", upgrade)
|
|
||||||
self.assertIn("目标版本", upgrade)
|
|
||||||
self.assertIn("区间", estimate)
|
|
||||||
self.assertIn("假设", estimate)
|
|
||||||
self.assertIn("分阶段授权", release)
|
|
||||||
|
|
||||||
def test_writing_skills_do_not_send_or_invent(self) -> None:
|
|
||||||
content = "\n".join(self.read_skill(DOC, name) for name in TARGETS[DOC])
|
|
||||||
self.assertIn("不得虚构", content)
|
|
||||||
self.assertIn("默认只生成草稿", content)
|
|
||||||
|
|
||||||
def test_source_lock_has_no_pending_items(self) -> None:
|
|
||||||
data = json.loads((ROOT / "migration" / "source-lock.json").read_text(encoding="utf-8"))
|
|
||||||
skills = data["skills"]
|
|
||||||
self.assertEqual(64, len(skills))
|
|
||||||
self.assertNotIn("pending", {item["status"] for item in skills.values()})
|
|
||||||
self.assertEqual(
|
|
||||||
{"migrated", "superseded", "excluded"},
|
|
||||||
{item["status"] for item in skills.values()},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
Reference in New Issue
Block a user