docs(lifecycle): align lifecycle contracts with verified behavior

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-10-15 19:11:53 +08:00
parent 1cff1bbe0a
commit 22177a46e9
7 changed files with 156 additions and 176 deletions

View file

@ -34,10 +34,10 @@ Mock 身份仅按现有开发 profile 使用,不替代真实认证验收。
3. 选择 HTTP/消费方测试验证实际响应和失败路径;至少覆盖此次变化触及的身份/权限边界。
4. [生成漂移脚本](../../../scripts/check-openapi-generated.sh) 会启动依赖并在退出时执行 Compose down;仅在隔离、可清理的本地环境运行,不能作为无副作用的文档检查。
## 已知文档漂移
## 生命周期对齐与剩余例外
2026-10-15 静态核查:docs/14 的撤回描述仍为 DRAFT,而
[withdrawPendingVersion 实现](../../../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java) 设置 UPLOADED。
旧工程 Skill 的状态表也不是新的权威。修改相关功能时对照
[治理测试](../../../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java)和正式需求解决差异;本次迁移不裁定新业务状态。
2026-10-15 已将[生命周期文档](../../../docs/14-skill-lifecycle.md)的撤回目标对齐为 `UPLOADED`,
并补充扫描、PRIVATE 确认、删除与访问约束;依据正式语义、实现及测试,不沿用旧工程 Skill 状态表。
[Latest 章节](../../../docs/14-skill-lifecycle.md#31-latest)仍记录原始 `latestVersionId` 在 PRIVATE 上传中的未发布指针例外;
是否统一持久化不变量需另行设计,不能因此放宽公共分发仅使用 published 版本的约束。
发布审核绕过、可见性、latest 指针、警告确认、删除约束均须核对当前服务与测试,不从历史快照复制规则。

View file

@ -65,7 +65,7 @@
| source_skill_id | bigint | 派生来源(团队技能提升到全局时记录原 skill ID),nullable |
| visibility | enum | `PUBLIC` / `NAMESPACE_ONLY` / `PRIVATE` |
| status | enum | `ACTIVE` / `ARCHIVED` |
| latest_version_id | bigint | latest published pointer,仅指向最新 `PUBLISHED` 版本;若不存在已发布版本则可为 `null` |
| latest_version_id | bigint | 内部生命周期指针;PRIVATE 上传可指向未发布版本。对外最新发布版本由 `publishedVersion` 解析,不直接以此字段授权 |
| download_count | bigint | |
| star_count | int | |
| rating_avg | decimal(3,2) | 平均评分 |
@ -77,7 +77,7 @@
- 唯一约束:`(namespace_id, slug, owner_id)`;面向公众的同名冲突还须按已发布记录检查不同 owner,避免地址解析歧义
- `status` 表示 skill 容器生命周期,不再承载“隐藏”语义。隐藏是独立的治理覆盖层,由 `hidden` / `hidden_at` / `hidden_by` 表达
- 当前代码下的实际可见性判定以 `VisibilityChecker` 为准,规则如下:
- 容器可见性判定以 `VisibilityChecker` 为准;传入 `SUPER_ADMIN` 平台角色时该检查短路放行,但不表示所有调用方都传入角色,也不绕过调用方的其他限制。普通调用规则如下:
- 若 `hidden=true`:仅 skill owner 或该 namespace 的 `ADMIN` / `OWNER` 可读
- 若 `latest_version_id is null`:仅 skill owner 可读;即使 `visibility=PUBLIC` 也不会对外公开
- `PUBLIC`:任意人可读 skill 容器与已发布版本
@ -103,38 +103,19 @@
| changelog | text | |
| manifest_json | json | 文件清单 |
| parsed_metadata_json | json | SKILL.md frontmatter 解析结果 |
| status | enum | `DRAFT` / `PENDING_REVIEW` / `PUBLISHED` / `REJECTED` / `YANKED` |
| status | enum | `DRAFT` / `SCANNING` / `SCAN_FAILED` / `UPLOADED` / `PENDING_REVIEW` / `PUBLISHED` / `REJECTED` / `YANKED` |
| reject_reason | varchar(512) | 拒绝原因 |
| published_by | varchar(128) | |
| published_at | datetime | |
| created_at | datetime | |
- `status` 表示 version 发布生命周期,和 skill 容器状态、review task 状态分离
- 当前代码下的实际迁移约束:
- 普通用户首次上传/重传新版本后,版本直接进入 `PENDING_REVIEW`
- `SUPER_ADMIN` 直发时可直接进入 `PUBLISHED`
- 审核通过:`PENDING_REVIEW → PUBLISHED`
- 审核拒绝:`PENDING_REVIEW → REJECTED`
- 撤回审核:`PENDING_REVIEW → DRAFT`
- 已发布撤回:`PUBLISHED → YANKED`
- 唯一约束:`(skill_id, version)` 防止重复发布
- `YANKED` 状态:已发布后撤回
- 当前代码下的实际读权限补充:
- 普通详情 / 下载 / resolve / tag / 文件读取,只接受 `PUBLISHED`
- owner 可通过常规版本详情预览自己的 `PENDING_REVIEW` 版本
- owner / namespace `ADMIN` / `OWNER` 在版本列表中可看到全部五种状态:`PUBLISHED / PENDING_REVIEW / DRAFT / REJECTED / YANKED`
- 但常规版本详情接口并不会放行 `DRAFT / REJECTED / YANKED`
- 审核详情页走独立 review 读路径,可查看待审版本及完整版本快照
版本号不可变性规则:
| 版本状态 | 版本号处理 |
|---------|-----------|
| DRAFT | 可删除该版本记录,重新使用同版本号 |
| PENDING_REVIEW | 可撤回到 DRAFT |
| REJECTED | 可删除该版本记录,重新使用同版本号 |
| PUBLISHED | 版本号永久占用,不可复用 |
| YANKED | 版本号永久占用,不可复用,版本列表中显示但标记为不可下载 |
- 完整状态定义、上传/扫描/审核转换和治理操作以 [生命周期文档](./14-skill-lifecycle.md) 为准;`DRAFT` 保留为旧数据兼容状态,当前撤回审核回到 `UPLOADED`。
- 上传结果取决于请求可见性、扫描配置与发布权限,不能统一描述为“普通用户直接待审”或“超管发布即代表可安装”。
- 唯一约束:`(skill_id, version)` 防止重复版本记录;已发布版本不可原地覆盖。允许删除的未发布版本删除后可复用版本号,但不能删除技能的最后一个版本。
- `YANKED` 表示已发布后撤回,版本记录和版本号仍保留,不可下载。
- 版本列表、详情/文件预览、下载与公共分发是不同权限路径:管理者能预览的版本不一定能下载,`UPLOADED` / `PENDING_REVIEW` 的受限下载也不代表对外发布。具体边界见 [生命周期文档的对外协议与权限章节](./14-skill-lifecycle.md)。
- 审核详情使用独立 review 读路径,可查看审核快照;不能由普通详情的访问规则推导审核权限。
### skill_file
@ -163,7 +144,7 @@
| updated_by | varchar(128) | |
| updated_at | datetime | |
- `latest` 是系统保留标签,只读,自动跟随 `skill.latest_version_id`;其语义严格等价于“最新已发布版本”,不允许 API 手动移动
- `latest` 是系统保留标签,只读,由发布和撤回流程维护,不允许 API 手动移动;PRIVATE 上传更新内部 `latest_version_id` 不代表已更新公开标签
- 自定义标签(如 `beta`、`stable-2026q1`)允许人工创建和移动
- 唯一约束:`(skill_id, tag_name)`
- `target_version_id` 必须指向 `status = PUBLISHED` 的版本,应用层校验
@ -185,7 +166,7 @@
- 仅用于普通发布审核,"提升到全局"使用独立的 `promotion_request` 表
- `version` 字段用于乐观锁,防止多 Pod 并发审核
- 业务约束:同一 `skill_version_id` 在 `status=PENDING` 时只能存在一条记录,重复提交返回 409 Conflict。撤回时删除 `PENDING` review_task,并将 `skill_version` 回退到 `DRAFT`
- 业务约束:同一 `skill_version_id` 在 `status=PENDING` 时只能存在一条记录,重复提交返回 409 Conflict。撤回时删除 `PENDING` review_task,并将 `skill_version` 回退到 `UPLOADED`
- PostgreSQL 并发约束落地:通过唯一索引 `(skill_version_id)` + 软删除标记实现。`review_task` 表增加 `deleted` 字段(bigint, 默认 0),唯一索引改为 `(skill_version_id, deleted)`。撤回时将 `deleted` 设为 `id`(非零值),新提交时 `deleted=0`,利用唯一索引防止并发重复提交。或者采用更简单的方案:撤回时物理删除 review_task 记录,依赖 `INSERT` 的唯一约束 `(skill_version_id)` 防并发。PostgreSQL 还支持 partial unique index 方案:`CREATE UNIQUE INDEX ON review_task (skill_version_id) WHERE status = 'PENDING'`,更优雅地实现"PENDING 状态唯一"约束
### promotion_request
@ -383,7 +364,7 @@
### skill_search_document
一个 skill 对应一条搜索文档,内容取“最新已发布版本”。实现上可由 `latest_version_id` 作为缓存指针承载,但其语义只能是 latest published pointer。
一个 skill 对应一条搜索文档,内容取 `publishedVersion` 表示的“最新已发布版本”。内部 `latest_version_id` 可能在 PRIVATE 上传时指向未发布版本,不能不校验状态就用于公开搜索。
| 字段 | 类型 | 说明 |
|------|------|------|

View file

@ -57,7 +57,7 @@ WHERE (visibility = 'PUBLIC')
## 3 搜索文档表 skill_search_document
一个 skill 对应一条搜索文档,但文档内容的来源语义应严格收敛为“当前最新已发布版本”。实现上仍可由 `latest_version_id` 作为缓存指针承载,但它只允许指向 `PUBLISHED` 版本;搜索层不能再把它当作泛化的“当前版本”。
一个 skill 对应一条搜索文档,但文档内容的来源语义应严格收敛为“当前最新已发布版本”。搜索消费方必须验证发布与可安装条件,不能仅凭 `latest_version_id` 非空认定可公开分发;该原始指针存在 PRIVATE 上传例外,见[生命周期 Latest 约束](14-skill-lifecycle.md#31-latest)。
| 字段 | 类型 | 说明 |
|------|------|------|
@ -88,7 +88,7 @@ PostgreSQL 全文搜索索引:表增加 `search_vector tsvector` 生成列,
### 5.1 一期数据建模约束
一期“每个 skill 一条搜索文档、内容永远取最新已发布版本”是有意的简化。当前实现仍使用 `latest_version_id` 作为持久化指针,但这里的语义已经收敛为 latest published pointer。这个模型在以下场景下会不够用:
一期“每个 skill 一条搜索文档、内容永远取最新已发布版本”是有意的简化。公开搜索的 latest 语义是 latest published;原始持久化指针的例外不能改变这个公开契约。这个模型在以下场景下会不够用:
- 版本级检索(搜索某个旧版本的内容)
- 自定义标签/通道检索(搜索 `@beta` 标签指向的版本内容)

View file

@ -2,9 +2,7 @@
## 1 发布流程
一期采用同步发布模型:上传、校验、存储、持久化在一次请求中同步完成。前端通过异步上传(带进度条)提升用户体验,但后端处理是同步的。
> **设计决策**:一期暂不考虑异步发布(uploadId、publishId、状态轮询、异步转正等)。一期技能包为文本资源包,体积有限(上限 10MB),同步处理足以满足需求。如后续引入大文件或复杂校验流程,再考虑异步模型。
上传、包校验、对象存储写入和数据库持久化在一次请求中同步完成;接收成功不等于安全扫描和人工审核已经完成。扫描启用时,上传会触发后续扫描,包含直发版本;`PUBLIC` / `NAMESPACE_ONLY` 上传要求扫描器启用。具体状态及分发条件见[生命周期文档](./14-skill-lifecycle.md)。
### 1.1 当前发布流程基线
@ -29,98 +27,71 @@
▼
④ 持久化数据
- 创建或关联 skill 记录(首次发布时创建 skill)
- 创建 skill_version(普通用户进入 `PENDING_REVIEW`,`SUPER_ADMIN` 直达 `PUBLISHED`)
- 创建 skill_version(普通 PRIVATE 为 `UPLOADED`;普通 PUBLIC/NAMESPACE_ONLY 为 `PENDING_REVIEW`;直发为 `PUBLISHED`)
- 创建 skill_file 记录
- 解析 SKILL.md frontmatter → parsed_metadata_json
- 生成 manifest_json
- 直发场景更新 skill.latest_version_id
- 直发及 PRIVATE 上传更新 skill.latest_version_id(指针非空不代表可公开分发)
│
▼
⑤ 同步写入审计日志
│
▼
⑥ 异步触发搜索索引写入
⑥ 按发布分支触发后续处理:扫描启用时触发安全扫描;直发产生发布事件,待审版本等待审核
```
当前版本采用审核流,不再区分“Phase 2 直发”与“Phase 3 恢复审核”两套现实实现:
以下是版本创建时的初始分支;扫描启用时,返回前触发扫描会将非发布版本设为 `SCANNING`,扫描完成后才回到对应的 `UPLOADED` 或 `PENDING_REVIEW`。初始状态不等于响应状态:
- 普通用户发布请求创建 `skill_version(status=PENDING_REVIEW)`
- 同步创建 `review_task(status=PENDING)`
- 审核通过后转为 `PUBLISHED`
- 审核拒绝后转为 `REJECTED`
- 撤回审核时删除 `PENDING review_task`,并将 `skill_version` 回退到 `DRAFT`
- 例外:提交人持有 `SUPER_ADMIN` 平台角色时,发布入口直接创建 `skill_version(status=PUBLISHED)`,跳过 `review_task` 创建,同时不再要求其必须是目标 namespace 成员
- 上述例外必须对 Web、`/api/v1/publish`、`/api/v1/publish` 保持一致
- 若重传新版本时发现旧的 `PENDING_REVIEW` 版本,旧版本会被自动降回 `DRAFT`,再创建新的待审版本
- 普通 `PRIVATE` 上传创建 `UPLOADED` 版本,不创建审核任务;保存私有版本不等于公开发布。
- 普通 `PUBLIC` / `NAMESPACE_ONLY` 上传创建 `PENDING_REVIEW` 版本及 `PENDING` 审核任务。
- 审核通过转为 `PUBLISHED`,审核拒绝转为 `REJECTED`;扫描与可分发条件仍需分别检查。
- 撤回审核将 `PENDING_REVIEW` 版本变为 `UPLOADED`,删除关联待审任务。
- 提交人持有 `SUPER_ADMIN` 时,上传直接创建 `PUBLISHED`,跳过审核任务及目标 namespace 成员检查;内部 `forceAutoPublish` 路径也可直发。直发不等于跳过所有安全扫描与下载就绪检查。
- Web 与 CLI 发布入口共用上述发布语义。
- 普通重传遇到旧 `PENDING_REVIEW` 版本时,先撤回旧审核并将其改为 `UPLOADED`,再按新请求的可见性及直发条件创建版本。
### 1.2 生命周期读模型
当前代码中的 skill 生命周期展示与操作判断,不再依赖旧的 `latestVersionStatus`、`viewingVersionStatus` 一类拼装字段,而统一基于以下 projection:
- `headlineVersion`:当前详情页/我的技能列表主展示版本
- `publishedVersion`:当前最新可公开分发的已发布版本
- `ownerPreviewVersion`:详情 projection 中仅暴露给 owner / namespace 管理者的 `PENDING_REVIEW` 版本
- `publishedVersion`:当前已发布版本投影;详情投影与公开列表的就绪过滤存在区别,不能只凭字段非空判定可下载
- `ownerPreviewVersion`:详情 projection 中暴露给 owner / namespace 管理者的新一轮非公开版本;排除 `PUBLISHED`、`YANKED`,按创建时间及 ID 选择最新,且须比已有已发布版本更新
- `resolutionMode`:`PUBLISHED` / `OWNER_PREVIEW` / `NONE`
业务规则:
- 公开入口只认 `publishedVersion`
- 公开入口以已发布版本为分发候选;`publishedVersion` 不替代各入口的授权与就绪校验
- owner 进入详情页时,如果没有可用 `publishedVersion`,才允许 `headlineVersion = ownerPreviewVersion`
- 推广到全局、安装命令、公开下载都只能绑定到 `publishedVersion`
- 推广、安装及公开分发不能使用未发布预览版本;具体候选版本与下载就绪条件见[生命周期文档](./14-skill-lifecycle.md)
- `hidden` 是独立治理覆盖层,不属于 skill 生命周期状态机
### 1.3 Skill 可见性与角色访问矩阵
### 1.3 读取与操作边界
以下矩阵以当前后端实现为准,综合了 `VisibilityChecker`、`SkillQueryService`、`SkillDownloadService`、`ReviewPermissionChecker` 的实际行为。
完整状态、角色与端点差异以[生命周期文档](./14-skill-lifecycle.md)为准,不能把详情、文件预览、下载、审核与公开解析合并为一张通用权限矩阵:
#### 1.3.1 Skill 容器读取
| 场景 | 需要分别检查的边界 |
|------|--------------------|
| Skill 容器读取 | 可见性、hidden、容器状态及读取入口授权。`latest_version_id` 为空时的限制不能等同于“无 publishedVersion”;PRIVATE 上传也可能将指针设为 `UPLOADED` |
| 版本列表与详情预览 | 先通过容器授权;管理视角可访问非公开版本,不限于 `PENDING_REVIEW`;详情可读不表示可下载 |
| 常规下载 | 要求容器 ACTIVE、通过可见性与 namespace 检查;`PUBLISHED` 还须 downloadReady 且未 yank;`UPLOADED` / `PENDING_REVIEW` 仅允许 owner 或 namespace ADMIN/OWNER 下载 |
| 公开解析与安装 | 使用已发布且符合分发条件的版本,不把 ownerPreviewVersion 当作公开候选 |
| 审核详情与审核包 | 独立审核权限入口;审核绑定下载不计入公开下载量,不能套用普通详情或普通下载的权限结论 |
| 治理与提审 | 提审、撤回审核、归档、隐藏、yank 与删除是不同动作;平台角色是否穿透需按实际入口判断,不能从一个入口推广到所有读取路径 |
| 角色 | PUBLIC | NAMESPACE_ONLY | PRIVATE | hidden 任意 visibility | 无 `publishedVersion`(`latest_version_id=null`) |
|------|--------|----------------|---------|------------------------|-----------------------------------------------|
| 匿名用户 | 可读 | 不可读 | 不可读 | 不可读 | 不可读 |
| 登录非成员 | 可读 | 不可读 | 不可读 | 不可读 | 不可读 |
| namespace MEMBER | 可读 | 可读 | 不可读 | 不可读 | 仅自己是 owner 时可读 |
| skill owner | 可读 | 可读 | 可读 | 可读 | 可读 |
| namespace ADMIN / OWNER | 可读 | 可读 | 可读 | 可读 | 不可读,除非本人也是 skill owner |
| SKILL_ADMIN / SUPER_ADMIN(仅平台角色) | 与普通登录用户一致;普通读路径不会因为平台角色自动穿透 private / hidden / unpublished |
补充:
- `hidden=true` 时,可读权限会收敛为“skill owner 或 namespace `ADMIN` / `OWNER`”
- `visibility=PUBLIC` 也不意味着未发布 skill 可见;当 `latest_version_id` 为空时,只有 owner 能读
#### 1.3.2 Version 状态读取
| 场景 / 角色 | DRAFT | PENDING_REVIEW | PUBLISHED | REJECTED | YANKED |
|------------|-------|----------------|-----------|----------|--------|
| 普通 skill 详情页主版本投影 | 不展示 | owner / namespace 管理者可作为 `ownerPreviewVersion` 展示 | 展示 | 不展示 | 不展示 |
| 普通 `listVersions` 访客 | 不可见 | 不可见 | 可见 | 不可见 | 不可见 |
| `listVersions` 的 owner / namespace ADMIN / OWNER | 可见 | 可见 | 可见 | 可见 | 可见 |
| 常规 `getVersionDetail` | 不可读 | 仅 owner 可读 | 可读 | 不可读 | 不可读 |
| 下载 / resolve / tag / 文件读取 | 不可用 | 不可用 | 可用 | 不可用 | 不可用 |
| review 详情页 | 可见完整快照 | 可见完整快照 | 可见完整快照 | 可见完整快照 | 可见完整快照 |
补充:
- `YANKED` 版本仍出现在管理视角的版本列表中,但不可下载
- `yank` 当前最新已发布版本时,会重算 `latest_version_id` 指向下一个最新的 `PUBLISHED` 版本;若没有,则置空
#### 1.3.3 审核 / 推广 / 治理动作
| 角色 | 发布新版本 | 提交审核 | 审核团队空间 | 审核全局空间 | 提交推广 | 审核推广 | hide / unhide | yank 已发布版本 |
|------|------------|----------|--------------|--------------|----------|----------|---------------|----------------|
| 匿名用户 | 不可 | 不可 | 不可 | 不可 | 不可 | 不可 | 不可 | 不可 |
| namespace MEMBER | 可发布到所属 namespace;新版本进入 `PENDING_REVIEW` | 自己作为 owner 时可;不能代别人提审 | 不可 | 不可 | 自己作为 owner 时可 | 不可 | 不可 | 不可 |
| skill owner | 可 | 可 | 不可 | 不可 | 可 | 不可 | 不可 | 不可 |
| namespace ADMIN / OWNER | 可 | 可为本空间 skill 提交审核 | 可 | 不可 | 可 | 不可 | 不可 | 不可 |
| SKILL_ADMIN | 可提交并可代提审;但普通发布仍非直发 | 可 | 可 | 可 | 可 | 可,但不能审自己的 promotion | 不可 | 可 |
| SUPER_ADMIN | 可跨 namespace 发布且直接 `PUBLISHED`,跳过 membership 检查和 review task | 可 | 可 | 可 | 可 | 可;promotion 和 review 场景下还能审自己的提交 | 可 | 可 |
`YANKED` 可保留在管理版本列表中,但不允许常规下载。yank 命中当前指针时,重算其余已发布版本;不存在时置空。
### 对象存储写入策略
一期同步写入正式路径,不使用临时区:
- 文件直接写入 `skills/{skillId}/{versionId}/{filePath}`
- 如果数据库事务失败,对象存储中的文件成为孤儿对象
- 定时 GC 任务:每天扫描对象存储中存在但数据库中无对应 `skill_file` 记录的文件,清理孤儿对象
- 删除 DRAFT/REJECTED 版本时,同步清理对应的对象存储文件
对象直接写入正式单文件路径 `skills/{skillId}/{versionId}/{filePath}` 与整包路径 `packages/{skillId}/{versionId}/bundle.zip`。数据库事务不能自动回滚对象存储:
- 发布事务注册完成回调;事务未提交时尝试删除本次登记的对象键。清理失败时通过独立事务入口记录补偿;补偿记录本身失败仍可能遗留对象。
- 版本删除允许 `DRAFT`、`REJECTED`、`SCAN_FAILED`、`UPLOADED`,但不允许通过该接口删除最后一个版本。正常事务路径在数据库提交后删除对象;整条技能硬删除也采用提交后清理,并保护共用对象。
- 对象删除失败时调用补偿记录入口。当前提交后回调中的普通补偿记录采用默认事务传播;真实事务下的持久化仍需单独验证,不能仅凭回调或 mock 测试宣称补偿可靠落库。
- 补偿任务针对已记录的失败,每轮完成后间隔 5 分钟重试,单轮最多 100 条;成功标记完成,失败记录尝试。
- 当前未发现每日扫描整个正式对象存储并与 `skill_file` 对账的通用 GC。进程崩溃或补偿记录失败等情况仍可能遗留对象,不能承诺自动回收所有孤儿。
实现依据:`SkillPublishService`、`SkillGovernanceService`、`SkillHardDeleteService`、`SkillStorageDeletionCompensationService` 和 `SkillStorageDeletionCompensationTask`。事务验证要求见[事务与外部副作用](../.trellis/spec/backend/transactions-and-concurrency.md)。
### CLI publish 请求规范
@ -134,11 +105,11 @@ Parts:
一期同步响应:服务端同步完成上传、校验、存储、持久化,返回 `200 OK` + skill_version 信息。
当前 CLI 默认行为:上传 → 进入审核。
如果调用方持有 `SUPER_ADMIN`,则直接发布为 `PUBLISHED`。
普通 CLI 上传启用扫描时,响应状态为 `SCANNING`;扫描完成后 PUBLIC/NAMESPACE_ONLY 进入 `PENDING_REVIEW`,PRIVATE 进入 `UPLOADED`。PRIVATE 关闭扫描时直接返回 `UPLOADED`。
如果调用方持有 `SUPER_ADMIN`,则直接创建 `PUBLISHED` 版本,分发仍受就绪检查约束。
Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确的审核提示。
`/api/v1/publish` 响应:
`/api/v1/publish` 直发响应示例(普通上传的 status 按上述扫描与可见性分支返回):
```json
{
@ -204,25 +175,30 @@ Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确
下载请求
│
▼
① 校验技能状态(ACTIVE)、版本状态(PUBLISHED)
① 校验技能状态(ACTIVE)与目标版本
- PUBLISHED:须 downloadReady 且未 yank
- UPLOADED/PENDING_REVIEW:仅 owner 或 namespace ADMIN/OWNER
│
▼
② 可见性检查
- PUBLIC: 任何人(包括匿名用户)
- NAMESPACE_ONLY: 该 namespace 的成员(需登录)
- PRIVATE: owner 本人 + 该 namespace 的 ADMIN 以上(需登录)
② 可见性及 namespace 状态检查
- PUBLIC、NAMESPACE_ONLY、PRIVATE、hidden 按对应授权规则处理
- 不能因版本可读就跳过容器授权
│
▼
③ 返回预生成包或按文件清单打包
③ 构建下载结果:优先预生成整包,缺失时按可读取文件重新打包
│
▼
④ 审计与统计
- audit_log 同步写入(记录下载人/IP/版本)
- download_count 异步更新(原子 SQL: download_count = download_count + 1)
- 匿名下载:审计记录 IP + User-Agent,不关联用户
- 已登录下载:审计记录用户 ID
④ 对 PUBLISHED 同步更新技能和版本下载计数,再发布 SkillDownloadedEvent
- 授权的未发布版本下载不计入公开下载量
│
▼
⑤ 控制器返回预签名跳转或流式响应
```
计数表示请求通过授权并成功构建下载结果,不证明客户端已完整接收。审核绑定下载是独立授权入口,不计入上述公开下载量。缺失 bundle 时的回退会过滤不存在的单文件,因此“生成 ZIP 成功”也不证明历史包完整;完整性保障需要单独验证。
下载审计仍是业务要求:登录用户记录身份,匿名用户记录 IP / User-Agent 等必要请求信息。当前已核对的普通 Web / CLI 下载链尚未找到对应的同步审计落库;`SkillDownloadedEvent` 仅携带技能与版本 ID,不能替代该审计。覆盖入口、记录时机与失败行为需要独立确认和验证,本次文档对齐不取消此要求。
### download_count 热点行优化预案
一期使用原子 SQL 直接更新,可接受。如出现热点行瓶颈,切换为:
@ -269,9 +245,9 @@ Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确
| 事件 | 触发时机 | 消费方 |
|------|---------|--------|
| `SkillPublishedEvent` | 审核通过 | 搜索索引写入 |
| `SkillPublishedEvent` | 审核通过或直发等产生已发布版本的路径 | 搜索索引写入 |
| `SkillYankedEvent` | 版本撤回 | 搜索索引移除 |
| `SkillDownloadedEvent` | 下载完成 | 下载计数 |
| `SkillDownloadedEvent` | 已发布版本下载结果构建成功并完成同步计数后 | 当前未发现生产消费者;下载计数由下载服务同步完成 |
| `SkillStarredEvent` | 收藏/取消 | 收藏计数 |
| `SkillRatedEvent` | 评分提交 | 评分重算 |
| `ReviewCompletedEvent` | 审核完成 | 预留给后续通知能力(当前可不消费) |
@ -281,7 +257,7 @@ Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确
### 审计日志写入策略
审计日志统一同步落库,与业务操作在同一请求内同步写入,不走异步事件。审计是企业内部平台的刚性需求,不可容忍丢失。
审计要求是同步落库,与业务操作在同一请求内记录,不以异步事件替代。审计是企业内部平台的刚性需求;普通 Web / CLI 下载链的已知实现缺口见第 3 节,不能将本要求视为所有入口均已实现的证明。
异步事件仅用于搜索索引、计数器等可容忍延迟的场景。如果后续需要更强一致性,引入 outbox 模式,不依赖 ApplicationEvent + @Async 承担可靠性。

View file

@ -208,12 +208,14 @@ Public API 的可见性规则:
| GET | `/api/v1/me/stars` | 我的收藏列表 |
| GET | `/api/v1/me/skills` | 我发布的技能列表 |
### 草稿与审核提交(Phase 3 引入)
### 已上传版本与审核提交
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/v1/skills/{namespace}/{slug}/versions/{version}/withdraw-review` | 撤回提审(PENDING_REVIEW → DRAFT,同时删除关联的 PENDING review_task) |
| POST | `/api/v1/skills/{namespace}/{slug}/versions/{version}/withdraw-review` | 撤回提审(PENDING_REVIEW → UPLOADED,同时删除关联的 PENDING review_task) |
| POST | `/api/v1/reviews` | 提交指定 `skillVersionId` 进入审核队列 |
| POST | `/api/v1/skills/{namespace}/{slug}/submit-review` | 提交 `UPLOADED` 或兼容 `DRAFT` 版本;body 为 `version`、`targetVisibility`(`PUBLIC` / `NAMESPACE_ONLY`) |
| POST | `/api/v1/skills/{namespace}/{slug}/confirm-publish` | 确认发布 PRIVATE 技能的 `UPLOADED` 或兼容 `DRAFT` 版本;body 为 `version` |
### 标签管理
@ -229,15 +231,15 @@ Public API 的可见性规则:
|------|------|------|
| POST | `/api/v1/skills/{namespace}/{slug}/archive` | 归档技能(namespace ADMIN 或 owner) |
| POST | `/api/v1/skills/{namespace}/{slug}/unarchive` | 恢复归档(namespace ADMIN 或 owner) |
| DELETE | `/api/v1/skills/{namespace}/{slug}/versions/{version}` | 删除 DRAFT/REJECTED 版本 |
| DELETE | `/api/v1/skills/{namespace}/{slug}/versions/{version}` | 删除 DRAFT/UPLOADED/REJECTED/SCAN_FAILED 版本;不能删除最后一个版本 |
| POST | `/api/v1/skills/{namespace}/{slug}/versions/{version}/yank` | 撤回已发布版本(namespace ADMIN 或 owner;可选 body `{"reason"}`;API Token 需 `skill:publish`) |
| POST | `/api/v1/skills/{namespace}/{slug}/versions/{version}/rerelease` | 从已发布版本重新发出一个新版本(namespace ADMIN 或 owner) |
当前代码中的 skill 生命周期读模型不再依赖 `latestVersionStatus` / `viewingVersionStatus` 一类拼装字段,而统一使用以下 projection:
- `headlineVersion`:当前页面应展示的主版本
- `publishedVersion`:当前最新可分发的已发布版本
- `ownerPreviewVersion`:owner / namespace 管理者可见的 `PENDING_REVIEW` 预览版本
- `publishedVersion`:当前最新已发布版本;能否安装仍需通过可见性与安全门禁
- `ownerPreviewVersion`:owner / namespace 管理者可见、比当前已发布版本更新的未发布版本(排除 `YANKED`);无已发布版本时取最新候选
- `resolutionMode`:`PUBLISHED` / `OWNER_PREVIEW` / `NONE`
其中:
@ -246,7 +248,7 @@ Public API 的可见性规则:
- owner 详情页在没有可展示发布版本时,才允许 `headlineVersion` 落到 `ownerPreviewVersion`
- 推广到全局一律使用 `publishedVersion.id`
- `hidden` 是独立治理覆盖层,不属于生命周期状态机
- 常规版本详情接口只放行 `PUBLISHED`,以及 owner 对自己 `PENDING_REVIEW` 版本的预览;`DRAFT / REJECTED / YANKED` 不通过该接口暴露
- 常规版本详情与文件预览在技能可见性校验后,允许 owner / namespace `ADMIN` / `OWNER` 查看非发布版本;这不授予安装或下载权限。显式下载另对 `UPLOADED` / `PENDING_REVIEW` 放行有权管理的用户,其他未发布状态不放行。完整边界见 [生命周期文档](./14-skill-lifecycle.md)。
发布成功响应中的 `data` 至少包含以下字段:
@ -260,10 +262,11 @@ Public API 的可见性规则:
发布状态约束:
- 普通用户发布成功后,`status` 为 `PENDING_REVIEW`
- 持有 `SUPER_ADMIN` 的用户通过 Web、`/api/v1/publish`、`/api/v1/publish` 发布时,`status` 为 `PUBLISHED`,且不要求其必须是目标 namespace 成员
- 当前版本保持该审核策略,不再提供“全员直发”的运行模式
- 撤回审核不会删除版本记录,而是 `PENDING_REVIEW → DRAFT`
- 普通用户上传 PRIVATE 技能:启用扫描时先进入 `SCANNING`,扫描完成后进入 `UPLOADED`;关闭扫描时直接进入 `UPLOADED`。
- 普通用户上传 PUBLIC / NAMESPACE_ONLY 技能必须启用扫描,先进入 `SCANNING`,扫描完成后进入 `PENDING_REVIEW`。扫描执行失败与扫描发现风险是不同结果,不能把进入待审等同于通过安全验收。
- `SUPER_ADMIN` 可免人工审核、免 namespace 成员校验而直接创建 `PUBLISHED` 版本,但不绕过 PUBLIC / NAMESPACE_ONLY 的扫描启用要求,也不表示扫描和安装门禁已通过。
- 撤回审核保留版本记录,将 `PENDING_REVIEW → UPLOADED`;`DRAFT` 是兼容状态,不是当前撤回目标。
- 确认私有发布、提交审核、扫描重试及安装门禁见 [生命周期文档](./14-skill-lifecycle.md),不要只根据发布响应的 `status` 判断可安装性。
## 7.4 Token API(需登录)
@ -278,7 +281,7 @@ Public API 的可见性规则:
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/v1/whoami` | 当前 Bearer Token 对应的用户信息 |
| POST | `/api/v1/publish` | 发布技能包(普通用户进入审核;`SUPER_ADMIN` 始终直发) |
| POST | `/api/v1/publish` | 上传技能包(结果受可见性、扫描和发布权限影响,见发布状态约束) |
| GET | `/api/v1/resolve/{namespace}/{slug}` | 解析版本 |
| GET | `/api/v1/check/{namespace}/{slug}/{version}` | 本地哈希与远端比对 |
@ -365,16 +368,16 @@ Admin API 按最小权限拆分,不再统一要求 SUPER_ADMIN:
`latest` 自动跟随最新已发布版本,不可手动移动。
- `skill.latest_version_id`:每次审核通过自动更新,始终指向最新 PUBLISHED 版本
- 对外 `latest` 以 `publishedVersion` 表示的最新已发布版本为准;`skill.latest_version_id` 是内部指针,PRIVATE 上传流程可能指向未发布版本,不能单凭该指针判断已发布或可安装。
- `yank` 当前最新已发布版本时,需要同步重算 `latest_version_id` 指向下一个最新的 `PUBLISHED` 版本;若不存在则允许为 `null`
- `latest` 标签:系统保留,只读,自动与 `latest_version_id` 同步
- `latest` 标签:系统保留,只读,由发布和撤回流程维护;不得将 PRIVATE 上传更新内部指针理解为公开标签也已更新。
- 自定义标签(如 `beta`、`stable-2026q1`):允许人工创建和移动,用于固定安装通道
| 场景 | 使用字段 | 说明 |
|------|---------|------|
| 搜索索引内容 | `publishedVersion` / `latest_version_id` | 外部协议仍叫 latest,但内部语义必须等价于最新已发布版本 |
| `/download`(不带版本号) | `publishedVersion` / `latest_version_id` | 下载最新已发布版本 |
| CLI `install @team/skill` | `publishedVersion` / `latest_version_id` | 等同于 `@latest` |
| 搜索索引内容 | `publishedVersion` | 外部协议仍叫 latest,但内部语义必须等价于最新已发布版本 |
| `/download`(不带版本号) | `latest_version_id`,再做下载权限和版本检查 | 公开分发只允许可安装的发布版本;管理者受限下载与公开分发分开判断 |
| CLI `install @team/skill` | `publishedVersion` | 等同于 `@latest` |
| CLI `install @team/skill@beta` | `skill_tag` 查询 | 自定义标签指向的版本 |
## 7.9 Resolve 接口说明
@ -441,7 +444,7 @@ GET /.well-known/clawhub.json
| GET | `/api/v1/search` | 搜索技能 |
| GET | `/api/v1/resolve` | 通过 slug + version 解析版本 |
| GET | `/api/v1/download/{slug}/{version}` | 下载技能 zip 包 |
| POST | `/api/v1/publish` | 发布技能(multipart/form-data,`SUPER_ADMIN` 直发) |
| POST | `/api/v1/publish` | 上传技能(multipart/form-data;状态与安装门禁见发布状态约束) |
### 兼容层请求/响应格式

View file

@ -79,7 +79,7 @@ SUPER_ADMIN 可访问所有管理页面。路由守卫检查用户是否持有
- `headlineVersion`:当前页面主展示版本
- `publishedVersion`:当前最新已发布版本
- `ownerPreviewVersion`:owner / namespace 管理者可见的待审核版本
- `ownerPreviewVersion`:owner / namespace 管理者可见的新一轮未发布版本;不限于待审核状态,选择规则见[生命周期读模型](14-skill-lifecycle.md#32-lifecycle-projection)。展示投影不等于下载或操作权限。
- `resolutionMode`:`PUBLISHED` / `OWNER_PREVIEW` / `NONE`
约束:

View file

@ -1,16 +1,16 @@
# Skill Lifecycle
Date: 2026-03-18
Status: current code-aligned reference
Date: 2026-10-15
Status: contract reference with documented implementation exception
本文件是 skill 生命周期的单一规范入口。结论以当前代码实现为准,并已经同步到领域模型、业务流程、API、前端、搜索和兼容层文档。
本文件是 Skill 生命周期的统一导航入口;业务语义结合正式需求、设计与实现测试维护。发现冲突时应明确记录并确认预期,不能自动以代码或旧文档裁定。本次对齐依据见第 8 节;原始 latest 指针的 PRIVATE 例外见第 3.1 节。本页不定义 SuiteVersion 的状态模型。
## 1. 设计原则
- skill 生命周期不再被建模为一个混杂状态机,而是拆分为容器状态、版本状态、审核工作流状态和可见性覆盖层
- 前端不再从 `status + hidden + latestVersionStatus + viewingVersionStatus` 拼装状态,而统一消费后端 lifecycle projection
- destructive action 和 reversible action 必须分离;`withdraw-review` 只表示撤回提审,不表示删除版本
- 对外仍可保留 `latest` 协议词汇,但内部语义必须严格等价于 latest published
- 对外仍可保留 `latest` 协议词汇,其公共分发语义必须严格等价于 latest published;原始持久化字段的现状另列
## 2. 状态模型
@ -26,19 +26,16 @@ Status: current code-aligned reference
### 2.2 SkillVersion 版本状态
- `DRAFT`
- `PENDING_REVIEW`
- `PUBLISHED`
- `REJECTED`
- `YANKED`
状态含义:
- `DRAFT`:可再次提交审核或删除的非公开版本
- `PENDING_REVIEW`:冻结待审版本
- `PUBLISHED`:当前可分发版本
- `REJECTED`:审核拒绝后保留的版本
- `YANKED`:曾发布、现已撤回分发的版本
| 状态 | 含义 |
|---|---|
| `DRAFT` | 历史兼容状态,可提审或删除;不作为普通上传的新建状态 |
| `SCANNING` | 等待当前安全扫描完成 |
| `SCAN_FAILED` | 扫描执行失败;不等同于发现风险 |
| `UPLOADED` | 上传保留,等待提审或 PRIVATE 确认发布;不保证已经扫描 |
| `PENDING_REVIEW` | 已提交审核,等待审核决定 |
| `PUBLISHED` | 已发布;实际分发还需满足可见性、容器状态与可安装条件 |
| `REJECTED` | 审核拒绝后保留的版本 |
| `YANKED` | 曾发布、现已撤回分发的版本 |
### 2.3 ReviewTask 审核工作流状态
@ -52,10 +49,11 @@ Status: current code-aligned reference
### 3.1 Latest
- `Skill.latestVersionId` 的唯一语义是 latest published pointer
- 它只能指向 `PUBLISHED` 版本
- 若 skill 没有任何已发布版本,则允许为 `null`
- `latest` 系统保留标签自动跟随该指针
- 公共 `latest`、搜索、兼容接口和默认公开分发必须解析到已发布版本,不能把未发布内容作为公开 latest。
- 审核通过及自动发布时,`Skill.latestVersionId` 指向发布版本;无已发布版本时允许为空。
- **已知实现例外:** PRIVATE 上传/重发会将原始 `latestVersionId` 指向新建 `UPLOADED` 版本;启用扫描后该版本可能处于 `SCANNING`。发布测试明确覆盖此指针更新,不能把这个字段单独当作已发布证明。
- 详情和 owner summary 的 projection 检查指针目标是否 `PUBLISHED`,否则查找已发布版本;批量公开摘要只检查原始指针目标是否可安装,不执行该回退。不能假设各读模型都以相同方式修复指针;`latest` 标签和消费者也不能无条件沿用原始指针。
- 是否将 PRIVATE 持久化指针也统一为仅指向已发布版本,需要单独设计并检查消费者、历史数据及测试。本次只记录例外,不改变该行为,也不放宽公共 published 约束。
### 3.2 Lifecycle Projection
@ -63,7 +61,7 @@ Status: current code-aligned reference
- `headlineVersion`:当前页面主展示版本
- `publishedVersion`:最新已发布版本
- `ownerPreviewVersion`:owner / namespace 管理者可见的待审核预览版本
- `ownerPreviewVersion`:有权限的管理者可见的新一轮未发布工作版本(排除 `PUBLISHED` / `YANKED`,且比已有发布版本更新)
- `resolutionMode`:`PUBLISHED` / `OWNER_PREVIEW` / `NONE`
约束:
@ -71,15 +69,22 @@ Status: current code-aligned reference
- 公开浏览、安装、下载、搜索只认 `publishedVersion`
- owner 详情页只有在不存在 `publishedVersion` 时,才允许 `headlineVersion = ownerPreviewVersion`
- promotion、compat latest、默认下载等公开分发行为都只能绑定 `publishedVersion`
- 管理者的授权预览下载与公开分发不同:owner 或 namespace ADMIN/OWNER 可下载 `UPLOADED` / `PENDING_REVIEW`,仍须通过 Skill 可见性检查;`DRAFT` 等其他非发布状态不能据此下载。常规 `downloadLatest` 读取原始指针后也使用相同版本授权判断,因此 PRIVATE 指针例外下,默认下载不一定是已发布版本;该行为不授予公众访问权。
- 已发布版本可安装还要求 `downloadReady=true`、`yankedAt=null`;下载要求容器为 `ACTIVE`,元数据可读不等于可下载
## 4. 代码实际链路
### 4.1 首次上传
- 普通用户上传后直接创建 `PENDING_REVIEW` 版本
- 同时创建 `PENDING` review task
- 不会创建初始 `DRAFT`
- 不会更新 `latestVersionId`
| 上传路径 | 扫描与后续状态 | 审核任务 / latest |
|---|---|---|
| 普通 `PUBLIC` / `NAMESPACE_ONLY` | 要求扫描启用;创建待审版本,触发扫描进入 `SCANNING`,当前扫描完成后回到 `PENDING_REVIEW` | 创建 `PENDING` review task;不移动 published 指针 |
| 普通 `PRIVATE` | 启用扫描时 `SCANNING -> UPLOADED`;未启用时直接 `UPLOADED` | 不自动创建审核任务;原始指针例外见 3.1 |
| 特权自动发布 | 直接 `PUBLISHED`;启用扫描时仍扫描,但不降为 `SCANNING` | 不创建审核任务;更新发布指针 |
- 扫描执行失败可进入 `SCAN_FAILED`。扫描完成不代表无风险,风险发现、警告确认和审核门槛是独立检查;过期扫描结果不能完成新一轮扫描。
- `submit-review` 接受 `UPLOADED`(兼容历史 `DRAFT`),设为 `PENDING_REVIEW` 并创建审核任务,保存请求的目标可见性。
- `confirm-publish` 仅用于 PRIVATE Skill 的 `UPLOADED`(兼容 `DRAFT`):转为 `PUBLISHED`,更新发布时间与 latest,发出发布事件,不触发新扫描。
### 4.2 审核通过
@ -96,16 +101,16 @@ Status: current code-aligned reference
### 4.4 撤回审核
- `withdraw-review` 的统一语义是 `PENDING_REVIEW -> DRAFT`
- `withdraw-review` 的统一语义是 `PENDING_REVIEW -> UPLOADED`
- 同时删除关联的 `PENDING review_task`
- 该操作是可逆、非破坏性的
- 当前代码只允许提交人本人撤回
### 4.5 重传新版本
- 若发现旧的 `PENDING_REVIEW` 版本,会先把旧版本自动降回 `DRAFT`
- 然后创建新的待审版本
- 自动撤回与手动撤回必须保持同一语义
- 若发现旧的 `PENDING_REVIEW` 版本,会先把旧版本自动撤回到 `UPLOADED`
- 然后按可见性及自动发布条件创建新版本,走 4.1 的对应路径
- 普通交互上传的自动撤回与手动撤回保持同一目标状态;Bundle 发布禁止自动撤回,已有待审版本时应阻塞,由用户先处理
### 4.6 已发布版本重发
@ -115,11 +120,11 @@ Status: current code-aligned reference
### 4.7 隐藏 / 恢复 / 归档 / 撤回已发布版本
- 隐藏:只改 `hidden=true`
- 恢复:只改 `hidden=false`
- 归档:`Skill.status = ARCHIVED`
- 隐藏:设置 `hidden=true` 并记录治理信息,不改变版本状态;不抹除 owner / namespace 管理者的访问权限
- 恢复:设置 `hidden=false` 并更新治理信息
- 归档:`Skill.status = ARCHIVED`,保留记录并禁止下载;公开元数据仍可能可读
- 取消归档:`Skill.status = ACTIVE`
- yank:`PUBLISHED -> YANKED`
- yank:`PUBLISHED -> YANKED`,记录撤回人、时间和原因,设置 `downloadReady=false`;没有对应的恢复发布操作
### 4.8 Yank 后指针修正
@ -150,12 +155,27 @@ Status: current code-aligned reference
## 6. 权限边界
- `withdraw-review`:仅提交人本人
- 删除版本:owner 或 namespace 管理者,且仅限 `DRAFT` / `REJECTED`
- 归档 / 取消归档:owner 或 namespace 管理者
- 隐藏 / 恢复技能、撤回已发布版本:平台技能治理权限
- 删除版本:owner 或 namespace ADMIN/OWNER,仅限 `DRAFT` / `REJECTED` / `SCAN_FAILED` / `UPLOADED`,禁止删除最后一个版本。数据库删除提交后清理对象存储,失败调用补偿入口;不是跨存储原子删除,补偿持久化的验证限制见[对象存储写入策略](./05-business-flows.md#对象存储写入策略)
- 归档 / 取消归档:owner 或 namespace ADMIN/OWNER
- 隐藏 / 恢复技能:平台技能治理权限
- yank:owner 或 namespace ADMIN/OWNER 可走普通治理入口;平台治理入口另校验平台权限
- 提交审核:owner 或 namespace ADMIN/OWNER;PRIVATE 确认发布还支持传入平台角色的 SUPER_ADMIN 路径
- 读取:可见性检查区分 PUBLIC、NAMESPACE_ONLY、PRIVATE 和 hidden;没有 latest 指针且未隐藏时,普通 namespace 管理角色不自动获得读取权限,owner 可读。接受平台角色的检查入口另支持 SUPER_ADMIN,不等于每个服务调用均传递该角色
- 写操作授权与读取授权分别校验,不能从管理身份推导所有未发布技能均可读取
## 7. 当前最终约束
- 一个 skill 生命周期的唯一规范入口就是本文件
- 其它文档如 `02-domain-model`、`05-business-flows`、`06-api-design`、`08-frontend-architecture` 必须与本文件保持一致
- 若后续代码再次改变生命周期语义,应先修改代码,再同步更新本文件和相关子文档
- 生命周期行为变化先确认正式需求与验收,再同步修改实现、测试及本文;不能以文档对齐名义静默裁定新业务规则
## 8. 依据与验证锚点
- 正式语义:[核心语义规则 §6](./oss-02-core-semantic-rules.md#61-状态转换表)、[Bundle 发布规格](../openspec/changes/add-suite-bundle-publishing/specs/suite-bundle-publishing/spec.md);Bundle 不自动撤回,SuiteVersion 使用独立模型。
- 上传及 PRIVATE 指针:[SkillPublishService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java)、[发布测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java)。
- 扫描:[SecurityScanService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityScanService.java)、[扫描测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/security/SecurityScanServiceTest.java)。
- 提审 / PRIVATE 确认:[SkillReviewSubmitService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillReviewSubmitService.java)、[提审与确认测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillReviewSubmitServiceTest.java)。
- 撤回 / 删除 / yank:[SkillGovernanceService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java)、[治理测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java)。
- 读模型与分发:[SkillLifecycleProjectionService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillLifecycleProjectionService.java)、[VisibilityChecker](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/VisibilityChecker.java)、[下载测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadServiceTest.java)、[可见性测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/VisibilityCheckerTest.java)。
以上锚点用于核对具体行为,不代表覆盖了所有状态组合,也不等于真实部署或对象存储验收。