feat(游戏共创): 贡献归集与归因(接口 + 契约 + 文档;反洗稿改判搁置)
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled

产品口径:「子代所有的都算父代的」,并要能溯源「多少是子代给的、多少是自己的」。本期**只做计算**,
不含资金/分成结算。

- 递归定义:`inherited(W) = Σ_{c∈直接子代} total(c)`,`total(W) = own(W) + inherited(W)`;每个后代在它的每个
  祖先里只计一次。
- 接口:`GET /api/game-distribution/games/{game_id}/contribution`(Bearer + `no-store`)——**仅该作品作者**:
  未登录/失效 401(中间件)、非作者 403(复用既有 owner-mismatch → FORBIDDEN,不新增第二套鉴权)、
  作品不存在或根自身已软删 404。响应:`{ gameId, own, inherited, total, byGeneration[], directChildren[],
  nodeCount, truncated, truncatedReason }`;指标结构 `ContributionTotals{playCount}` 可扩展(将来加点赞/收藏/收入)。
- 不变量(测试钉住):`inherited == Σ byGeneration.total == Σ directChildren.total`、`total == own + inherited`、
  `nodeCount == Σ byGeneration.gameCount`;`byGeneration` 只含后代(绝对代际,升序)。
- 上限与截断:节点 500 / 深度 32;超限如实标 `truncated: true` + `truncatedReason ∈ {node_limit, depth_limit}`,
  已计入部分仍自洽(与族谱 `truncated` 同约定,不静默给半个数);遍历只用血缘表既有索引,**不新增表/索引**。
- 契约:shared-contracts 4 个 DTO + TS 镜像 + parity(+4 组 → 66 组);7 个生成绑定(含 procedure)。
- 文档:技术方案新增 `§3.11 贡献归集与权益归因`(定义/归因分解/接口/上限与截断/鉴权口径与理由)+ §3.4 路由行 +
  §5.4 实施证据;**任务 B**:反洗稿(相似度校验)从「待拍板」改为「**产品已决定搁置(2026-10-06)**」并从待拍板
  清单移除,结算口径统一为「本期只做贡献归集与归因计算」。
- 测试:module-game-distribution **131 passed**(contribution 12 条,含 A→B→C 三层链路不变量);spacetime-module
  **304 passed / 1 ignored**(+3 结构断言);spacetime-client 43 passed;api-server game_distribution **107 passed**(+3)。

门禁:wasm build 0;`cargo check --all-targets` 0;`SPACETIME_SCHEMA_BASE_REF=9f4c7d76 npm run check:spacetime-schema`
0(98 表);DTO parity 0(66 组 / 17 构建器 / 15 手拼类型);`check:project-bundle-policy-parity` OK;
`check:encoding` 0(5543 files);`cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check` 0;`git diff --check` 0。

注:本块未提交 `scripts/*.mjs`(`check-game-distribution-lineage-e2e.mjs` / `-theme-e2e.mjs` /
`capture-game-lineage-visual.mjs`)——属其它 owner,工作区保留未提交。
This commit is contained in:
2026-10-06 18:05:49 +08:00
parent 9ab3d5e687
commit 6aa2f97410
17 changed files with 1351 additions and 26 deletions
@@ -14,7 +14,7 @@
- **工程源包 Fork(真·复刻工程,也是唯一能直接发布的路径)**:成品包是 Vite 构建产物——模板工程 `vite.config.js` 只配了 `build: { outDir: 'dist' }`,**未关压缩也未开 sourcemap**,且平台发行校验明确拒收 `*.map`(`server-rs/crates/module-game-distribution/src/package.rs:171-175`)。所以 AI 在成品包上改核心逻辑不可靠;要让「改造完成后可发布」成立,必须有版本级可选资产**工程源包**。
- 两者共用同一套血缘模型;成品包路径是工程源包缺失时(网页端发布、作者不愿公开源码)的天然**降级路径,降级到「试玩 + 素材」,不含发布**。
4. **工程源包有现成规范与现成客户端链路可复用**:排除规则见 `docs/【模板规范】AGC模板包组织指南-2026-09-21.md`;下载→校验 SHA-256→解压→建项全链路已存在于 `apps/ai-game-creator-shell/src-tauri/src/template_library.rs`(`create_project_from_installed_template_at` `:816`)。
5. **本期不做**:创作者收益分成(仓库无收益/分成/结算表,`docs/【技术方案】外部产品支付服务接入-2026-10-03.md:200` 明确人工结算)、相似度反洗稿校验、上链。
5. **本期只做贡献归集与归因计算(§3.11),不含资金 / 分成结算**(口径留待产品决定;仓库无收益 / 分成 / 结算表,`docs/【技术方案】外部产品支付服务接入-2026-10-03.md:200` 明确人工结算);**反洗稿(相似度校验)产品已决定搁置(2026-10-06,见 §2.2 与 §7)**;**不上链**。
6. **顺带必修的相关缺口(已在 M1 修复)**:公开 `author.name` 长期落到兜底文案(创建游戏时 `author_name` 写死 `None`,`api-server/src/modules/game_distribution.rs:1001-1003`;公开 payload 兜底「创作者」)。修法:公开快照改为读时联 `user_account`,账号改名/换头像立即跟随,与后台游戏管理页同口径。
---
@@ -78,8 +78,8 @@
### 2.2 非目标
- 不做收益分成、版税结算、算力成本核算(无账本与口径,属独立议题)。
- 不做相似度反洗稿校验(无判定标准;本方案只保证**来源声明不可伪造**)。
- **本期只做贡献归集与归因计算**(§3.11:递归「子代所有的都算父代的」+ 按代际 / 直接子代归因分解),**不做**资金 / 分成 / 版税结算与算力成本核算——无账本、无结算周期、无金额口径,属独立议题;资金与分成口径留待产品决定。
- **反洗稿(相似度校验)产品已决定搁置(2026-10-06)**:不是没想到,是明确不做——无判定标准,且判定只能基于工程源包做结构化比对,属独立议题;本方案只保证**来源声明不可伪造**(声明只能来自真实取件,见 §3.5.3)。
- 不做上链;「永久溯源」实现为**不可变的父子链 + 平台持久化事实**。
- 不做跨作品类型 Fork(现役只有一种作品:游戏分发)。
- 不做关注 / 粉丝。收藏(收录)原先也在这个「不做」列表里,但**已纳入并落地**(2026-10-06,见 §2.4 / §3.4):它是服务端真实用户态投影(`game_distribution_collection`),不是前端本地状态,因此仍满足「不要虚构收藏状态」这条既有发行合同。
@@ -132,7 +132,7 @@ stateDiagram-v2
| --- | --- |
| `/games/mine` 每张作品卡 | 新增「共创授权」三态设置(仅允许提升,终态 `full` 时只读);新增「被改编 N」入口,弹层列出直接子代(标题 / 作者 / 代际 / 状态);提升授权后若当前公开版本没有工程源包,行内提示「上传工程源码以支持源码级改造」(上传在桌面端客户端完成,网页端只做引导) |
| `/games/publish`、AGC 发布面板 | 新增「授权共创」三态单选(默认「禁止共创」,页面提示:开启后可被他人复刻改编,开启后不可撤销);**上架时随创建请求一起提交**(`forkAuthorization`),不必事后补一次提升;从父作品 Fork 而来时显示只读的「改编自《X》」**且档位由服务端继承父作品当时的档位**——该场景下客户端所选档位不生效(不报错,见 §2.3) |
| AGC 客户端 | ① 详情页「改造这个作品」唤起 AGC(唤起方式**待拍板**,候选见 §7 第 11 条;`genarrative://fork?gameId=<id>` 的 deep link 目前**未注册**,只是候选之一);② AGC 首页/项目入口提供「从平台作品开始创作」(输入 gameId 或从平台跳转) |
| AGC 客户端 | ① 详情页「改造这个作品」唤起 AGC(唤起方式**待拍板**,候选见 §7 第 10 条;`genarrative://fork?gameId=<id>` 的 deep link 目前**未注册**,只是候选之一);② AGC 首页/项目入口提供「从平台作品开始创作」(输入 gameId 或从平台跳转) |
| `/games/detail` 详情页 | 新增「收藏」按钮(收藏 / 已收藏两态)。初始态取自公开详情的 `collected`(登录才有该字段);点击后 `PUT` / `DELETE /api/game-distribution/games/{gameId}/collection`,按钮态改用**响应里的权威投影值**(不写本地乐观状态——前端本地状态正是本功能要消灭的东西)。未公开 / 已软删除的作品返回 409,按钮按失败态提示 |
| `/games/mine`(或「我的」入口)| 新增「我的收藏(收录)」列表:`GET /api/game-distribution/my-collections`,形状与广场一致(封面 / 标题 / 作者 / 游玩数),点击进详情。列表只含**当前公开可读**的作品;已下架作品的收藏行保留,作品重新公开后自动回来(不需要用户重新收藏) |
@@ -191,7 +191,7 @@ sequenceDiagram
API->>API: 校验授权/来源版本/计算 generation 与 root → 写 lineage
```
> 说明:本图是**目标形态**。唤起方式尚未拍板(§7 第 11 条);`GET …/fork-source` **M2a 已实现**(受鉴权元数据 + 取件本体,实际字段与状态码见 §3.4「登录用户」表),但本图里的响应字段名是 M2b 的目标形状,定稿一律以 §3.4 表格为准。
> 说明:本图是**目标形态**。唤起方式尚未拍板(§7 第 10 条);`GET …/fork-source` **M2a 已实现**(受鉴权元数据 + 取件本体,实际字段与状态码见 §3.4「登录用户」表),但本图里的响应字段名是 M2b 的目标形状,定稿一律以 §3.4 表格为准。
**C. 溯源与命名**
@@ -347,6 +347,14 @@ pub(crate) project_bundle_sha256: Option<String>,
| `DELETE /games/{gameId}/collection` | 取消收藏。**不要求 `Idempotency-Key`**:按确定性主键删除,重复调用结果完全相同(不存在也算成功),没有「重放 vs 新意图」需要区分。**也不要求作品仍公开 / 未被删除**:下架后拒绝取消只会给用户留下清理不掉的脏行。响应 `{ collected: false }`(**不带** `replayed`) |
| `GET /my-collections?limit=&cursor=`(2026-10-06 补真分页) | 「我的收藏(收录)」列表:逐条用公开目录同一份 `public_game_payload` 投影,形状与公开目录一致 `{ games, nextCursor }`。**真游标分页**:`limit` 缺省 **20**(网格一屏)、上限 **50**(约束单响应体积;与后台列表的 200 口径**不必相等**),超界**截断**而非报错;`cursor` 形如 `"{createdAtMicros}:{collectionId}"`(与后台列表同一套 `"{micros}:{id}"` 惯例,`collectionId` = `{userId}:{gameId}` 自带冒号,解析只切第一个冒号);**格式非法 → 400**;`nextCursor` 为真实值,**最后一页为 `null`**。排序:**收藏时间倒序,同值用 `collectionId` 升序兜底**(`collectionId` 唯一 ⇒ 全序,翻页不重不漏)。分页顺序定义为「**先按可见性过滤、再排序切页**」——游标位置落在已过滤序列上,否则每翻一页都会漏掉自己的若干条收藏。**只含当前公开可读的作品**;未公开 / 已软删除的行在投影时被跳过但**不删除**,作品重新公开后自动回到列表 |
**贡献归集与归因(作者视角只读,2026-10-06 新增,Bearer,`no-store`)**
口径与不变量见 §3.11;这里是路由层的合同摘要。
| 方法 / 路径 | 说明 |
| --- | --- |
| `GET /games/{gameId}/contribution` | 作者读取自己作品的贡献归集:`{ gameId, own, inherited, total, byGeneration, directChildren, nodeCount, truncated, truncatedReason }`(指标集合当前只有 `playCount`)。**只要求 Bearer、不叠加发布灰度**;未登录 / 失效 → **401**,作品不存在或已软删除 → **404**,非作者 → **403**(沿用既有 owner-mismatch 惯例,理由与代价见 §3.11.5)。子树遍历只用血缘表既有父索引,**不新增表 / 索引**;受节点上限 **500** / 深度上限 **32** 约束,超限时 `truncated: true` + `truncatedReason` ∈ `node_limit` / `depth_limit`,已计入部分的数字仍自洽。**本期只做计算**:响应里没有资金 / 分成 / 结算字段 |
#### 后台(admin)
| 方法 / 路径 | 说明 |
@@ -697,6 +705,114 @@ pub struct GameDistributionThemeMember {
| 数据契约表 | `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` **已随表落地补齐**(`2fa201e0d`,两张表小节;`check:spacetime-schema` 现为 **96** tables) |
| 不改的东西 | 不动 `/api/external/v1`(不新增 External OpenAPI 条目);不新增数据库访问通道;不新增第二套作品系统;游戏 Tab / 广场的公开目录**不变**(主题是额外维度,不改变作品的独立展示) |
### 3.11 贡献归集与权益归因(2026-10-06)
> **口径原话**:「子代所有的都算父代的」,并且要能回答「能溯源多少是子代给的、多少是自己的」。
> **本期只做计算**:只做贡献归集(rollup)与归因(attribution)**计算**——没有账本、没有结算周期、
> 没有金额字段,**不含资金 / 分成 / 结算**,资金与分成口径留待产品决定(见 §2.2 与 §7)。
> 唯一实现在 `server-rs/crates/module-game-distribution/src/contribution.rs`(纯函数,无
> `ReducerContext`)。
#### 3.11.1 递归定义
```text
inherited(W) = Σ_{c ∈ 直接子代(W)} total(c)
total(W) = own(W) + inherited(W)
```
- `own(W)`:作品 W 自己的指标(本期只有 `playCount`)。
- `inherited(W)`:W 的**全部**直接子代按 `total` 汇总;子代的子代已经含在子代的 `total` 里,
因此「子代所有的都算父代的」,而**每个后代在它的每个祖先里只计一次**(不重复、不漏)。
- 等价恒等式(三条不变量里最有用的一条):`inherited(W) == Σ_{d ∈ 后代(W)} own(d)`。
- 指标集合做成结构体 `ContributionTotals { play_count }`(`Default` + `AddAssign` 风格的**单一
合并点**):**将来加点赞 / 收藏 / 收入只需加字段**并改那一处合并,递归定义、归因分解与上限 /
截断语义都不需要动。
#### 3.11.2 归因分解(根节点的明细)
| 字段 | 语义 |
| --- | --- |
| `own` / `inherited` / `total` | 根自身的三个数,`total == own + inherited` |
| `byGeneration[]` | **每一代贡献多少**:只含**后代**(不含根自身),`generation` 用**绝对代际**(与族谱一致),按代际**升序** |
| `directChildren[]` | **每个直接子代带来多少**:每项是该直接子代的 `total`(含它自己的整棵子树),按 `gameId` 升序 |
`byGeneration` 每条的 `total` 是「这一代对祖先 `inherited` 的贡献」,也就是**该代节点自身的量**;
`inherited` 恒为 `0`——该代从更深代际继承到的量已计入更深代际那一条。若把后代也滚进本代,
同一个后代会在多层分解里被重复计入,各代之和就会**大于**祖先的 `inherited`(这正是本功能要避免的事)。
不变量(**在截断后的子树上依然成立**,纯函数单测逐条钉住):
- `inherited == Σ byGeneration.total == Σ directChildren.total`;
- `total == own + inherited`;
- `nodeCount == Σ byGeneration.gameCount`(`nodeCount` = 参与计算的**后代**节点数,**不含根**)。
#### 3.11.3 接口:作者视角只读
`GET /api/game-distribution/games/{gameId}/contribution`
```json
{
"gameId": "game_x",
"own": { "playCount": 120 },
"inherited": { "playCount": 340 },
"total": { "playCount": 460 },
"byGeneration": [ { "generation": 1, "gameCount": 2, "own": {"playCount":90}, "inherited": {"playCount":0}, "total": {"playCount":90} } ],
"directChildren": [ { "gameId": "game_b", "generation": 1, "own": {"playCount":70}, "inherited": {"playCount":40}, "total": {"playCount":110} } ],
"nodeCount": 5,
"truncated": false,
"truncatedReason": null
}
```
口径:
- **遍历只用血缘表既有索引**(`by_game_distribution_lineage_parent_game_id`)逐层下钻;
**不新增表或索引**,也不新建第二套血缘查询。
- **不做公开可见性过滤**:这是作者自己血缘子树的账目,口径就是「血缘上属于我的后代」,
因此**已下架 / 已软删除的后代照常计入**(血缘事实仍在)。入参与纯函数里都没有可见性维度——
要改口径必须显式加字段,不会悄悄漂移。
- 子孙的游戏行缺失(脏数据 / 历史遗留)按不存在处理:跳过该节点,也不拿它当父继续下钻
(与族谱装配同一口径)。
- 缺失 / 悬空行(父不在集合里、自环、重复行)由纯函数安全跳过,不 panic、不发半残节点。
- 除「作品不存在」外,本接口**不把任何异常抛成 404 / 409**:截断与数据问题都走
`truncated` / `truncatedReason` 或 5xx 映射,不用状态码掩盖。
#### 3.11.4 规模上限与截断语义
| 常量 | 值 | 语义 |
| --- | --- | --- |
| `GAME_DISTRIBUTION_CONTRIBUTION_NODE_LIMIT` | **500** | 单次最多计入多少个**后代**(根不计入) |
| `GAME_DISTRIBUTION_CONTRIBUTION_DEPTH_LIMIT` | **32** | 相对深度上限(根 = 0),更深的节点不再计入 |
- 超限时**如实标注**:`truncated: true` + `truncatedReason` ∈ `node_limit` / `depth_limit`
(未截断为 `null`)——与族谱接口 `truncated` 的既有约定一致,**不静默给半个数**。
- 两种上限同时拦住同一批节点时按 **`node_limit`** 报:调用方该去收敛范围,
而不是去怀疑血缘真有 32 层。
- **已计入部分仍自洽**:三条不变量在截断后的子树上依旧成立(数字只覆盖被计入的那部分子树)。
- 事务侧多收**一条**后代、多收**一层**深度:只收满上限时「刚好满」与「还有更多」不可分辨,
截断会被静默吞掉。同层按 `gameId` 升序下钻,截断点因此确定,两次读不会换一批节点。
#### 3.11.5 鉴权口径与理由
| 情形 | 状态码 | 落点 |
| --- | --- | --- |
| 未登录 / 令牌失效 | **401** | 路由层 `require_bearer_auth`(不进入业务;**不叠加**发布灰度——它不是发布动作) |
| 作品不存在或**根自身**已软删除 | **404** | 事务 `Ok(None)` → 客户端折成 `None` → handler 404 |
| 作品存在但不属于当前主体 | **403** | 事务报 `游戏 owner 不匹配`,命中 `map_spacetime_error` 既有的 owner-mismatch → FORBIDDEN 分支 |
**为什么非作者是 403 而不是「也折 404」**(代价一并写明):
1. 沿用仓库既有 owner-mismatch 惯例——`map_spacetime_error` 早已把这类文案映射成 FORBIDDEN,
作者侧写路由同源;在这里另立一套 404 会多出第二种「不属于你」的语义。
2. 这是作者账目页:客户端必须能分辨「我没有权限看」(停手 / 换账号 / 回我的作品)与
「作品没了」(回列表刷新),两者折成同一个 404 只能靠再猜一次。
3. 代价明确且被接受:登录用户可用 403 / 404 之分辨别某个 `gameId` 是否存在。已公开作品本就能从
公开目录枚举;未公开作品会因此多出一个存在性预言机,但本接口只服务作者本人,故接受并显式记录。
响应整组 `Cache-Control: no-store`(per-author 的商业敏感数据,不给任何共享缓存留缝);
procedure 另要求**受信服务身份**(归属判定靠 `owner_user_id`,而 SpacetimeDB 客户端身份本身不携带
作品归属),与其它作者侧 procedure 同口径。
---
## 4. 为什么是 `game` 而不是 `version`
@@ -793,6 +909,22 @@ pub struct GameDistributionThemeMember {
---
### 5.4 贡献归集与权益归因(2026-10-06)实施证据
| 项 | 证据 | 状态 |
| --- | --- | --- |
| 纯函数(递归 + 归因分解 + 上限 / 截断) | 新增 `server-rs/crates/module-game-distribution/src/contribution.rs`:`build_contribution_breakdown`(无 `ReducerContext`)+ `ContributionTotals { play_count }`(`Default` + `AddAssign` 的**单一合并点**,将来加指标只动它);常量 `GAME_DISTRIBUTION_CONTRIBUTION_NODE_LIMIT = 500` / `_DEPTH_LIMIT = 32` 在单测里按**合同值**钉住 | ✅ 已验证(**12 条纯函数单测**:A→B→C 三层链路 `A.inherited = B.own + C.own` / `B.inherited = C.own` / `C.inherited = 0`、根带 3 子 + 1 孙的多分支、无子代、悬空 / 缺失 / 自环 / 重复行安全跳过、绝对代际、两条分解之和 == `inherited`、`inherited == Σ 后代 own`、`node_limit` / `depth_limit` 截断且数字自洽;`cargo test -p module-game-distribution` → **131 passed**) |
| 事务与 procedure | `game_distribution_contribution_tx`(行不存在 / 已软删除 → `Ok(None)`;owner 不匹配 → `Err("游戏 owner 不匹配")`;正常 → 快照)+ `collect_game_distribution_contribution_nodes`(只用 `by_game_distribution_lineage_parent_game_id` 逐层下钻,同层按 `gameId` 排序,**多收一条 / 多收一层**作为截断判据)+ `get_game_distribution_contribution_and_return`(受信服务身份 + 单一 `try_with_tx`,失败路径显式给空) | ✅ 已验证(3 条结构性单测;`cargo test -p spacetime-module` → **304 passed / 1 ignored**) |
| 接口(作者视角只读) | `GET /api/game-distribution/games/{gameId}/contribution` 挂独立 Bearer + `no-store` 路由组(**不叠加发布灰度**);未登录 / 失效 → 401,不存在 / 已软删除 → 404,非作者 → 403(owner-mismatch → FORBIDDEN);响应经 DTO 逐字段搬运,不在 api-server 重算任何数字 | ✅ 已验证(3 条新用例:① 401 + `no-store`;② 403 / 404 映射可分辨;③ 用纯函数构造的节点喂进 `contribution_payload`,逐字段断言 `own` / `inherited` / `total` / `nodeCount` / `byGeneration` / `directChildren` 与纯函数一致,且 `truncated` + `truncatedReason = node_limit` 透传、截断后两条分解之和仍等于 `inherited`;`cargo test -p api-server game_distribution` → **107 passed**) |
| 契约与 parity | Rust DTO `GameDistributionContributionTotals` / `...Generation` / `...Child` / `...Response`(`server-rs/crates/shared-contracts/src/game_distribution.rs`)+ TS 镜像(`packages/shared/src/contracts/gameDistribution.ts`)+ `check-game-distribution-dto-parity` 登记 4 组(`truncatedReason` 的可空性双向比对) | ✅ 已验证(`check-game-distribution-dto-parity` → **66 组** Rust/TS 类型一致) |
| 生成绑定 | `npm run spacetime:generate -- --rust-only`:新增 6 个类型文件 + 1 个 procedure 文件;`module_bindings.rs` 只多出这 7 条登记,**无其它漂移** | ✅ 已验证(`git status` 核对:`module_bindings/` 只新增上述 7 个文件) |
| 迁移与 schema | **不新增表 / 索引 / 列**(只加 procedure 入参 / 结果类型),因此无迁移、schema 基线比对通过 | ✅ 已验证(`SPACETIME_SCHEMA_BASE_REF=9f4c7d76 check:spacetime-schema` → 98 tables 通过) |
| 口径取舍:`byGeneration[i].total` | 取「**这一代对祖先 `inherited` 的贡献**」(= 该代节点自身量),`inherited` 恒为 `0`;把后代也滚进本代会让同一个后代在多层分解里被重复计入,各代之和就会**大于**祖先的 `inherited`,与 §3.11.2 的不变量冲突。`directChildren[i].total` 仍是「含整棵子树」的子代总量 | ✅ 已定(单测与接口用例都断言 `Σ byGeneration.total == inherited`;§3.11.2 已写明语义) |
| 门禁 | wasm build 0;`cargo check --all-targets` 0(仅既有 `TEST_AGC_MODEL_DEFAULT_ID` dead_code 警告);`cargo test -p module-game-distribution` 0(131 passed);`cargo test -p api-server game_distribution` 0(107 passed);`cargo test -p spacetime-module` 0(304 passed / 1 ignored);`cargo test -p spacetime-client` 0(43 passed);`check-game-distribution-dto-parity` 0;`check-project-bundle-policy-parity` 0;`SPACETIME_SCHEMA_BASE_REF=9f4c7d76 check:spacetime-schema` 0;`check:encoding` 0;`cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check` 0;`git diff --check` 0 | ✅ 已验证 |
| 尚未覆盖 | 真实 dev 栈 / 浏览器端到端未跑(需本地库数据与登录态);前台作者账目页 UI 不在本次工作树权限内 | ⏳ 待补 |
---
## 6. 里程碑拆分(评审后逐个开实施计划)
| 里程碑 | 范围 | 交付判据 |
@@ -809,18 +941,21 @@ pub struct GameDistributionThemeMember {
## 7. 未决问题(需要拍板)
> 第 1、8–12 条来自 2026-10-04 的只读侦察(`local://m2a-design.md`),其中 8–12 条是**第二阶段(M2a/M2b)落地前必须拍板**的五项;未拍板前不得据 §3.5.4 的推荐实现。
> 第 1、7–11 条来自 2026-10-04 的只读侦察(`local://m2a-design.md`),其中 7–11 条是**第二阶段(M2a/M2b)落地前必须拍板**的五项;未拍板前不得据 §3.5.4 的推荐实现。
>
> **已移出待拍板并定稿(2026-10-06)**:
> ① **相似度反洗稿校验**——**产品已决定搁置**。这不是「没想到」,而是**明确不做**:无判定标准,且判定只能基于工程源包做结构化比对,属独立议题;本方案只保证来源声明真实、不可伪造。口径落在 §2.2。
> ② **结算**——统一为「**本期只做贡献归集与归因计算**(§3.11),**资金 / 分成不在范围内**(口径留待产品决定)」。原先「做不做分成」的问法作废:本期连账本与金额口径都没有,不预留半成品字段。
1. **成品包路径的事实边界已改写**(原条目「成品包路径默认要做……建议:做」的前提已被否证):成品包路径能试玩、能提供素材,但**不能发布**(§3.5.1)。因此要拍板的不再是「要不要顺带做工程源包」,而是「是否接受对外口径从『一键复刻完整工程』改成『参考改编 / 素材复用』,并把工程源包作为唯一源码级路径」。建议:接受并改口径(§3.5.4 路线 A 先行、B 排后续)。
2. **授权默认值已定口径(2026-10-06,原为待拍板)**:**母版**默认仍取「禁止共创」(`forbidden`,飞书评论里「默认允许 + 发布前合同勾选」的建议未采纳);**衍生作品**改为**创建时继承父作品当时的档位**(不再落 `forbidden`、也不接受客户端另选或收窄),见 §2.3 / §3.9 补充。存量作品不受影响:既有子作品的档位保持原值(继承只在创建时发生,不回填)。
3. **「共创主题」已拍板采纳且服务端已实现(2026-10-06,原为待拍板)**:采纳包仲航提出的「平台命名主题 → 作品树」,且明确作品之间不存在曝光挂靠。设计见 §3.10,实施清单见 `docs/project-memory/plans/【里程碑】共创主题与作品树-2026-10-06.md`(M4)。**已落地**:设计定稿 `063c04a1b`、数据模型与领域纯函数 `2fa201e0d`、公开读路径 `742723a58`、后台写路径 `033e3aa79`(前台共创 Tab / 主题页与后台管理 UI **仍未做**)。**仍待产品拍板的是两点**:① **主题级排序口径**——公开列表当前按「沿用既有 micros 游标」排在 `created_at` 倒序 + `themeId` 升序兜底上,`sort_order` 只用于主题内成员排序;若要求共创 Tab 按运营序展示,需要把游标改成 `(sort_order, theme_id)` 双键并同步前端;② **主题详情 `roots` 的 50 上限**——可见成员 > 50 时静默截断且无「还有更多」标志(`memberCount == roots.len()`),三个备选(去上限 / 给成员加独立游标 / `memberCount` 报真实可见数)见 §3.10.6 与 §3.10.9,**本轮不改契约**。
4. **收益分成**:需求文档要求「每一代均享有权益(署名 / 流量回馈 / 版权分成)」。署名本期做,流量回馈与分成本期不做(无账本、无算力成本口径,`docs/【技术方案】外部产品支付服务接入-2026-10-03.md:200` 明确人工结算)。
5. **相似度反洗稿校验**:需求文档要求「低改动度复刻判定」。本期不做;本方案只保证来源声明真实、不可伪造。若要做,只能基于工程源包做结构化比对,属于独立议题。
6. **「永久链上溯源」表述**:实现为平台持久化的不可变父子链,不上链。需确认该措辞是否可以调整。
7. **`play_count` 死字段**:它被前端展示为「N 次游玩」但永不自增。本方案不依赖它;是否顺带修复(新增游玩上报)需单独立项。
8. **对外表述**:第二阶段是否接受「只能试玩 + 素材复用,不能一键复刻工程」?建议接受并同步改文案(AGC 侧注释 `export.rs:407-408` 已把无 `package.json` 的静态项目排除在发布合同外)。
9. **血缘承载位**:血缘只作为客户端本地注释(项目内独立文件,如 `.agent/fork.json`),还是要平台可查询(服务端新增字段)或随工程包传输(manifest 升 v2,旧客户端读到即失败)?建议先本地,等工程源包(M2b)落地再上平台字段。
10. **是否立刻做工程源包(M2b)**:它需要**新增工程包下载/授权接口**(当前不存在)并定义包内禁项(服务端已拒绝 `node_modules`/`.git`/`.env*`/`*.map`/`*.pem`/`*.key` 与嵌套 zip,`module-game-distribution/src/package.rs:135-142`、`:170-176`),成本最大。建议先做 A 验证需求。
11. **从平台唤起 AGC 的方式**:AGC 内手工输入 gameId / 作品链接,还是平台发取件码在客户端兑换,还是注册 deep link(`tauri.conf.json` 未注册 scheme、无 `tauri-plugin-deep-link`、无单实例插件,需动安装器与升级链路)?建议手工输入起步。
12. **是否允许「伪工程」(C 路线,补声明 phaser 4/vite 的 `package.json` 让成品包过检)用于存量作品兼容**:产出不可再构建,建议不允许;若确要做,需产品书面接受「不可再构建」的语义并限定为一次性兼容。
13. **收藏(收录)已纳入并落地**(2026-10-06):原先它在 §2.2「非目标」里,现已实现为真实用户态投影(表 `game_distribution_collection` + `PUT`/`DELETE /games/{gameId}/collection` + `GET /my-collections` + 公开详情 `collected`),见 §2.4 / §3.4 与 `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` 的 `game_distribution_collection` 小节。仍**不在**范围内的是「收藏数公开计数」「收藏动态流」「关注 / 粉丝」——它们各自需要独立的口径与表。
4. **结算口径(2026-10-06 统一)**:需求文档要求「每一代均享有权益(署名 / 流量回馈 / 版权分成)」。署名本期已做;**本期只做贡献归集与归因计算**(§3.11),**资金 / 分成 / 版税结算与流量回馈不在范围内**,口径留待产品决定(无账本、无算力成本口径,`docs/【技术方案】外部产品支付服务接入-2026-10-03.md:200` 明确人工结算)。贡献数是**计算事实**,不是可结算金额,也没有预留任何金额字段。
5. **「永久链上溯源」表述**:实现为平台持久化的不可变父子链,不上链。需确认该措辞是否可以调整。
6. **`play_count` 死字段**:它被前端展示为「N 次游玩」但永不自增。本方案不依赖它;是否顺带修复(新增游玩上报)需单独立项。
7. **对外表述**:第二阶段是否接受「只能试玩 + 素材复用,不能一键复刻工程」?建议接受并同步改文案(AGC 侧注释 `export.rs:407-408` 已把无 `package.json` 的静态项目排除在发布合同外)。
8. **血缘承载位**:血缘只作为客户端本地注释(项目内独立文件,如 `.agent/fork.json`),还是要平台可查询(服务端新增字段)或随工程包传输(manifest 升 v2,旧客户端读到即失败)?建议先本地,等工程源包(M2b)落地再上平台字段。
9. **是否立刻做工程源包(M2b)**:它需要**新增工程包下载/授权接口**(当前不存在)并定义包内禁项(服务端已拒绝 `node_modules`/`.git`/`.env*`/`*.map`/`*.pem`/`*.key` 与嵌套 zip,`module-game-distribution/src/package.rs:135-142`、`:170-176`),成本最大。建议先做 A 验证需求。
10. **从平台唤起 AGC 的方式**:AGC 内手工输入 gameId / 作品链接,还是平台发取件码在客户端兑换,还是注册 deep link(`tauri.conf.json` 未注册 scheme、无 `tauri-plugin-deep-link`、无单实例插件,需动安装器与升级链路)?建议手工输入起步。
11. **是否允许「伪工程」(C 路线,补声明 phaser 4/vite 的 `package.json` 让成品包过检)用于存量作品兼容**:产出不可再构建,建议不允许;若确要做,需产品书面接受「不可再构建」的语义并限定为一次性兼容。
12. **收藏(收录)已纳入并落地**(2026-10-06):原先它在 §2.2「非目标」里,现已实现为真实用户态投影(表 `game_distribution_collection` + `PUT`/`DELETE /games/{gameId}/collection` + `GET /my-collections` + 公开详情 `collected`),见 §2.4 / §3.4 与 `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` 的 `game_distribution_collection` 小节。仍**不在**范围内的是「收藏数公开计数」「收藏动态流」「关注 / 粉丝」——它们各自需要独立的口径与表。