lili 17727d08c3
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(storage): 接入游戏内容对象存储与运行时提交门
冻结 GameArtifactManifest/GameSourceArchive 契约和 V34 pending/committed 状态记录,增加专用 writer 的全量回读物化校验,并让 Runtime API 在消费和发布前对账 committed、GamePackage checksum 与实际 engineBundle hash。隔离 staging 已验证真实消费拒绝未提交版本;源码桶权限仍保持 fail-closed,正式金标不切换。
2026-07-29 12:02:22 -07:00

120 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
topic: 契约总览
canonical: true
date: 2026-06-18
---
# 契约总览
> **这是什么**:绘境AI 全部契约族的导航与口径收口表 —— 有哪几类契约、各落在哪、各算"第几"、怎么镜像同步到代码、防漂移门到底有没有。
> **给谁看**:跨仓对接的工程师、做契约变更与评审的人、第一次 `ls contracts/` 想搞清楚目录里这堆 schema 各是什么的新人。
字段级定义不在这里 —— 它们在各 `.schema.json` / `.d.ts` / `-api` 包,以及生成主线 execution §5。本档只给指针,负责回答"到底有几类、各算第几"。导航起点是 [`docs/agent-specs/_index.md`](../../agent-specs/_index.md) 活地图与 [`contracts/README.md`](../../../contracts/README.md)。
## 契约全景
绘境AI 的契约分布在三个物理落点,落点本身就决定了一类契约的归属和拆仓时的命运。
跨三仓五工位共享的业务契约落 `contracts/` 顶层,是 Day-0 全员锁定的单一事实源:任何接口、数据结构、事件、SDK 签名改动,先改这里、再写代码。game-runtime 内部的两类接线契约不落 `contracts/`,落 `game-runtime/src/` —— 它们只在 game-runtime 内被消费,落 `contracts/` 会让跨仓 SSOT 反向相对 import `../game-runtime/src`,拆仓即断(理由整段写在两个 `.d.ts` 头注释里,见下文)。Java 侧则各模块一份 `-api` 包,是 `contracts/` 的代码镜像,后端先写它的 VO/DTO、前端据此定 TS。
```mermaid
flowchart TB
subgraph C["contracts/ 顶层 · 跨三仓五工位 SSOT"]
C1["跨仓 8 类<br/>API yaml · DB Flyway · SDK · GamePackage<br/>events · Dify(废) · ad-slot · prompts"]
C2["agent-loop/ · 生成 QA 闭环 + 源项目 keystone"]
C3["templates/ · 13 个品类/形态 schema"]
end
subgraph R["game-runtime/src/ · 内部接线面 · 不落 contracts(拆仓即断)"]
R1["第 9 类 game-host.d.ts<br/>游戏宿主装载契约"]
R2["第 10 类 runtime-api-2d.d.ts<br/>运行时访问约定 rt"]
end
subgraph A["game-cloud/game-module-*/*-api · Java 镜像"]
A1["VO / DTO / 错误码常量<br/>(后端先写 → 前端据此定 TS)"]
end
C1 -->|后端镜像| A1
C2 -->|生成线产出落库 · 引擎线消费| R2
R1 --> R2
```
下面这张表是"到底有几类"的唯一答案。状态列如实标"已建 / ACTIVE / 废 / future 占位"。
| 契约族 | 文件 / 落点 | owner | 产方 → 消方 | 状态 |
|---|---|---|---|---|
| **跨仓 #1 API** | `contracts/api-schemas/*.yaml`(12 个:aigc/ad/biz/community/compliance/feed/passport/project/runtime/studio/telemetry/trade) | WS1 lead | 后端定义 → 前端 mock / 各模块互调 | 已建 |
| **跨仓 #2 DB** | `contracts/db-schemas/V*.sql` | WS1 | 后端各模块 | 已建(镜像已漂移,见下节) |
| **跨仓 #3 SDK** | `contracts/sdk-interface.d.ts` | WS3 | game-studio 宿主 ↔ 游戏侧 postMessage | 已建 |
| **跨仓 #4 GamePackage** | `contracts/game-package.schema.json` + `contracts/game-artifact-manifest.schema.json` + `contracts/game-source-archive.schema.json` | WS3 + WS2 | 生成 → 编译 → 源归档/对象存储 → 预览 → 发布 → 运行全链路 | GamePackage 已建;制品索引和引擎无关源归档契约已建、生产接线分波迁移 |
| **跨仓 #5 events** | `contracts/events.schema.json` | WS5 | 前端埋点 / SDK 上报 → 看板 | 已建 |
| **跨仓 #6 Dify I/O** | `contracts/dify-workflow-io.json` | WS2 | ~~aigc 壳 ↔ Dify~~ | **废**(降远期 / 从未部署;现行=new-api,见 `DEPRECATED-dify-workflow-io.md`) |
| **跨仓 #7 ad-slot** | `contracts/ad-slot.schema.json` | WS5 | ad 模块 / SDK Plugin.Ad | 已建 |
| **跨仓 #8 prompts** | `contracts/prompts/`(`registry.yaml` + 8 阶段 01-safety…08-meta) | 各 prompt owner 工位 | aigc / 生成链路 | 已建 |
| **第 9 类 装载契约** | `game-runtime/src/core/game-host.d.ts` | Runner v2 P1 | host boot(装游戏)+ 生成 agent(产出靶) | 已建 · additive |
| **第 10 类 rt 约定** | `game-runtime/src/host/runtime-api-2d.d.ts` | 引擎线(plan 2026-06-18-001 U1) | U2 build-from-source 装配器 + U3 GAMEDEF_SYSTEM prompt | 已建 · additive(是否升格 contracts/ 待 6c6g 定) |
| **生成主线 keystone** | `contracts/agent-loop/source-project.schema.json` | 后端线(Opus 设计) | 后端线产出 / 落库 → 引擎线 2D 适配器消费 | ACTIVE(两开发线交汇 keystone) |
| **生成 QA 闭环** | `contracts/agent-loop/game-design.schema.json` · `verdict.schema.json` · `skeletons/{tictactoe,breakout,simon}.json` | WS2(HJ-AGENT-LOOP) | 策划 LLM 产 GameDesign / judge.py 产 Verdict → 对抗评审、JSONL 账本、eval 回流 | ACTIVE |
| **品类/形态 schema** | `contracts/templates/*.schema.json`(13 个:clicker/dodge/match/narrative/tycoon/merge/business-sim/puzzle/idle/heritage/generic/runner/trpg) | 生成线 | 后端 `parseAndValidate` / `PromptResourceLoader` | 已建(部分退场仍须就位,见下) |
`templates/generic.schema.json` 有个反直觉的约束:P3 走引擎 bundle 路后,后端对 GameConfig 玩法字段的校验已退场,但这 13 个 schema 文件仍须就位 —— 缺一个,`PromptResourceLoader.isReady()` 就因 `getAllTemplateSchemaTexts` 链加载不到资源而卡在 false([generic.schema.json:5](../../../contracts/templates/generic.schema.json))。退场不等于可删。
#4 内部有三份用途互斥的 JSON。`game-package.schema.json` 是宿主真正解析的运行清单,版本前缀下文件名仍为 `manifest.json``game-artifact-manifest.schema.json` 是平台上传、下载和逐文件校验使用的运行包/素材/证据对象索引,文件名固定为 `artifact-manifest.json``game-source-archive.schema.json` 是引擎无关源码归档索引,文件名固定为 `source-manifest.json`,存放在独立 `game-sources` 桶。对象索引不能作为 `manifestUrl` 返回给宿主,不能携带 candidate/published/retired 状态,也不保存签名 URL。生产版本身份来自 project 数据库Tier2 源工程继续绑定现有 SourceProjectStoreLittleJS/Canvas 等游戏通过 `game_source_archive` provider 绑定引擎无关源归档。对象存储提交记录由 V34 `game_artifact_storage` 保存,不能把 MinIO marker 当作 committed。
`agent-loop/``templates/` 这两个目录在 `contracts/README.md` 的目录结构图里没有 —— 那张图只画了跨仓 8 类。新人 `ls` 到它们时无从对应,这是 README 自身的覆盖缺口,在此补登:`agent-loop/` = 生成线的源项目 keystone 加 QA 闭环工件契约族,`templates/` = 13 个品类/形态 schema。
## "第几类"口径收口
仓里出现过至少五套互相冲突的"第 N 契约"编号。四套都带"第 9",两套都叫"8 契约",数字撞得很凶,语义却各不相干。一个新读者顺着任意一处文档读进来,几乎必然误判"到底有几类、各算第几"。
跨仓主编号轴只有一条:`contracts/README.md` 的 1-8 类,加上后来 additive 进来的第 9(装载)、第 10(rt 约定)。这是唯一一条"类"的编号轴。所有别的"第 N 契约"字样,都属于某个局部上下文,不在这条轴上 —— 它们撞数字、不撞语义。
| 出现过的"第 N 契约"字样 | 它属于哪个上下文 | 与主轴的关系 |
|---|---|---|
| 跨仓 **第 9 类**(装载契约 game-host.d.ts) | game-runtime 内部 additive | **就是主轴上的第 9 类** |
| **第 9 契约组 9a-9g**(9a 模板协议 … 9g 收益归因) | 2026-06-12 生成系统总体架构 review 的生成工厂契约包 | 生成工厂的内部编号,与装载第 9 类**同数字、零语义重叠** |
| **9c**(渠道版 GameConfig 的 c 子项) | 渠道发行档(运营/渠道发行.md) | 渠道侧 GameConfig 的局部子项,future;与上面两个"9"都不是一回事 |
| **生成主线 8 契约 ①-⑧**(①源项目 schema … ⑧trace+cost) | 2026-06-17 固定游戏架构与 SAA execution §5(ACTIVE) | 生成两开发线交汇的契约集,与跨仓 README 的 8 类**几乎零重叠**;其中 ① = `contracts/agent-loop/source-project.schema.json` |
引用契约时认上下文、不认数字。看到"第 9 契约"先问是哪条轴上的:跨仓 additive 的装载面,还是生成工厂 review 的 9a-9g,还是渠道的 9c。`agent-loop/` 下的 `game-design` / `verdict` 这两件 QA 闭环工件,任何"第 N 类"编号体系都没把它们收编,它们走自己的 `工件契约①/②` 编号(HJ-AGENT-LOOP)。
## 契约怎么同步进代码
声明的同步序是单向的:后端先写 `-api` 包的 VO/DTO,前端据此定义 TS 类型;改任何 API 必先改 `contracts/` 再写代码([contracts/README.md:63](../../../contracts/README.md))。Java 侧的镜像分布在每模块一份 `-api` 包 —— `game-module-{aigc,ad,biz,community,compliance,feed,project,runtime,studio,telemetry,trade}-api`,加上沿用 huijing 框架的 `huijing-module-{infra,pay,system,bpm}-api`。GamePackage 这类跨链路契约通过字段镜像进 `-api`:`game_version.package_url`/`checksum`/`bundle_size` 的权威写者是 runtime 编译成功后回写,aigc 只回填生成元数据,不得写编译产物字段(这条跨模块对齐在 [README §2.1](../../../contracts/README.md))。
DB 契约是个例外,它有两份物理副本,而声明的方向和实际的方向相反。
```mermaid
flowchart LR
A["contracts/db-schemas/<br/>README 声称=授权源<br/>18 个 V*.sql"]
B["game-cloud/.../db/migration/<br/>Flyway 执行副本<br/>25 个 V*.sql"]
A -.->|README §41 声称:diff 须一致<br/>实际:已破| B
B ==>|事实全集 · 真授权源| A
note["缺口 V14-V20 只在执行副本:<br/>trade withdraw / aigc level / trace<br/>source / source_project / modify / uk"]
B -.- note
```
`contracts/README.md:41``contracts/db-schemas/` 称作授权源,要求执行副本与它保持 Flyway 校验和 diff 一致。这条承诺实际已破:执行副本 25 个 `V*.sql`,`contracts` 只有 18 个,缺 V14-V20 七个(trade withdraw 引用、aigc 加 level、trace readiness、aigc source、source_project 建表、aigc modify、source_project 唯一键 —— 全部只在执行副本)。连两边共有的文件也 diff:V1.0.0、V2.0.0 都是"Huijing 审计列"(contracts)对"Yudao 审计列"(执行副本)的注释级差异 —— 2026-06-15 命名空间改名只改了执行副本、没回灌 contracts 镜像。真正的授权源其实是执行副本那 25 个全集,`contracts/db-schemas` 是一份滞后的部分镜像。
## 防漂移门:声明的 vs 代码里的
`contracts/` 的纪律靠几道"防漂移门"撑着,文档把它们写成 CI 会自动拦截。核到代码里,这些门目前都不存在 —— 它们是文档承诺,不是运行中的机器门。
| 防漂移承诺 | 文档出处 | 代码实情 |
|---|---|---|
| `contracts/db-schemas` ↔ 执行副本 Flyway diff 一致 | [README.md:41](../../../contracts/README.md) | **已漂移**:18 vs 25,加 Huijing/Yudao 审计列注释差 |
| CI 跑 `flyway validate` 阻断不合规 | [README.md:62](../../../contracts/README.md) | 全仓唯一 CI = `game-cloud/.github/workflows/maven.yml`,是 yudao fork 继承件:`on push to master`(本仓分支 dev/2.0.0、默认 dev/1.0.0,**永不触发**)、`-Dmaven.test.skip=true`(跳测)、**无 flyway validate、无 contracts↔migration diff 步** |
| prompt 变更过"四道闸(CI)" | [registry.yaml:3](../../../contracts/prompts/registry.yaml) | 只是 YAML 注释,未见对应 workflow |
| 改契约先改 contracts 再写码 | README.md:4 / [contract-first-development.md](../../../.agents/skills/contract-first-development.md) | 纯口头纪律,无脚本校验 |
`deploy/smoke-test.sh` 里 grep `flyway`/`contracts`/`diff`/`cmp` 零命中,确认这套门在部署 smoke 阶段也不存在。同步当成纯人工纪律在跑,DB 镜像已经因此漂掉。
这是一个待决策位,不在本档自裁:要么补一道真门 —— 在 wave-close 或 pre-commit 加一步 `contracts/db-schemas` ↔ migration 的 `cmp`,漂移即红线拦截;要么干脆裁定执行副本为唯一源、把 `contracts/db-schemas` 降为只读快照或删掉,消除"声称授权源却滞后"的矛盾。无论哪条,先得让 README §41 的口径和代码实情对上。
更重的两套机器化契约手段是**远期待引、MVP 不引**(创始人 2026-06-22 历史回收判定:不捡,记远期待引)。一是服务间的 Pact 消费者驱动契约测试,二是事件的 Schema Registry。MVP 阶段不上它们,靠现有的人工纪律(改契约先改 `contracts/`)加 CI diff、加 events 的枚举守门人就够;真正把它们引进来的时机是微服务拆分、事件消费方变多、人工纪律开始压不住漂移的时候。登记在此是为了不让"现在没有机器门"被误读成"以后也不该有"——它们是有意推迟的升级路径,不是缺口。
## 版本与兼容铁律
规则正文在 [`contracts/README.md` §2](../../../contracts/README.md),不在此重抄,只列锚点:API 走 URL Path 版本,新增字段不算 breaking、删除或重命名要升版本、旧版本保留 ≥ 3 个月;事件每条带 `schema_version`,新增字段给默认值、老消费者忽略未知字段、不兼容变更用新事件名并行消费;DB 迁移已合入的禁改,回滚写新补偿迁移。跨模块对齐的三条硬约束在 [§2.1](../../../contracts/README.md):两个 `qualityScore` 不同量纲不可互相回灌(aigc 0-1 生成质量分 vs telemetry/feed 0-100 运营质量分)、`package_url` 权威写者=runtime、`play_count` 权威源=telemetry `game_play_start` 事件聚合。
## 落点速查
跨三仓共享契约 → `contracts/` 顶层。game-runtime 内部接线契约(第 9 / 第 10 类)→ `game-runtime/src/{core,host}/*.d.ts`,这两类"为什么落 game-runtime 不落 contracts"的完整 rationale 写在各自 `.d.ts` 头注释(game-host.d.ts:9-12 / runtime-api-2d.d.ts:4-7),不在此重抄。Java 侧镜像 → `game-cloud/game-module-*/*-api`。错误码段每模块独占,project=100 / aigc=101 / runtime=102 / feed=103 / telemetry=104 / pay=105 / trade=106 / community=107 / ip=108 / compliance=109 / biz=110 / ad=111 / studio=112,端前缀 `/app-api`·`/admin-api` 由框架按包名自动加([README.md:71-81](../../../contracts/README.md))。