# Graph Linker：.csem 与 C++ 迁移约定

本原型依据本仓库 README.md、show1.png、prompt.md，以及 2026-10-07 用户补充规则实现。当前赛程文件为 v3、战队资料文件为 v1；内部已绑定图模型仍为 v2。新增文件分离约定见 [TEAM_INFO_FORMAT.md](TEAM_INFO_FORMAT.md)，以下规则与旧图结构保持一致。尚未接入 CSEMTV；C++ 编译、读取和 Godot 运行均未验证。

## 文件与模型

`.csem` 为二进制封装：开头是 ASCII/UTF-8 的 `CSEM-GRAPH-LINKER/3\n`（20 字节，无 BOM，末尾 LF），随后是 gzip 压缩的紧凑 UTF-8 JSON。识别码先于解压校验；其他格式报“文件识别码错误”。gzip 损坏、UTF-8 无效、JSON 或模型类型错误分别拒绝导入，保持当前文档。压缩文件最多 2,001,024 字节，解压内容最多 2,000,000 字节，读取流累计超限即终止。gzip 是压缩混淆，不是加密，也不提供保密。

JSON 根字段为 `schema: "csem.graph-linker"`、`version: 3`、`name`、`appearance`、`seeds`、`stages`、`matches`、`results`、`groups`、`links`。codec.js 按识别码识别赛程/战队用途并处理字节封装；team_info.js 负责 `.tinf` 字段校验及 v3 ID 引用绑定，再交给 engine.parse 校验内部 v2 图。`.tinf` 识别码为 `CSEM-TEAM-INFO/1\n`（17 字节），包含 TeamData 属性与 TeamDatabase 快照日期。旧 `/2` 封装仍支持图结构读取，但嵌入战队数据不覆盖外部资料。旧浏览器 JSON v1 草稿补 groups 和默认起始坐标；同阶段同区域的并列位，以原排名为左端，在区间不超人数范围、不与其他名次冲突时自动构建共享赛果大框，入边改指 group，原 ID、队伍、名次和独立出口保留。无法安全转换时保持旧位并报告校验问题。读取迁移不覆盖浏览器存储，后续用户编辑保存到新的 v3 赛程草稿与独立战队缓存，旧键保留。已有 v2 草稿不自动改分组。外部裸 JSON .csem 无识别码，明确拒绝；不能通过自动 JSON 回退绕过识别码校验。

| 数组 | 字段与含义 |
| --- | --- |
| seeds | 文件仅有 `id`。名称、seedRank/liveRank 与 TeamData 属性来自独立 `.tinf`；运行内部绑定为 `id/name/seedRank/liveRank`。阶段末排名初值 0，原型旧战队实时排名为 0。 |
| stages | `id/name/size/sort/x/y`。`size` 为预计收齐的 X 支不同队伍；三个 `sort` 项按先后比较 `seedRank/stageRank/liveRank`，也可空选；x/y 为可拖动起始层位置。 |
| matches | `id/stage/name/bo/schedule/x/y`。每场一个共用入口（两根通道），胜/败出口各一根。比赛时序仅为占位文本。 |
| results | `id/stage/name/kind/rank/x/y`。`advance` 提供阶段末排名与下一阶段通道；`placement` 提供最终排名，无输出。真实大框成员以大框的 rankStart 为准，其他位使用 rank。 |
| groups | `id/stage/mode/type/members/x/y/label/algorithm/rankStart`。mode 为 merged（真实合并）或 label（仅标签）；members 为按视觉顺序排列的 `{kind:'match'|'result',id}`。真实框 type 为 match/advance/placement；仅标签框为 match/result；空框 type=null。成员同阶段且只能归属一个大框。 |
| links | `id/source/target`。保存端口引用，不嵌套复制队伍或节点。 |
| appearance | `unresolved/winner/loser`。概念色彩接口，分别对应深蓝、淡墨绿、正红；原型未实现素材库。 |

端口格式：种子出口 `seed:TEAM_ID`；起始层入口 `start:STAGE_ID`，出口 `start:STAGE_ID:INDEX`（INDEX 从 0 开始）；比赛入口 `match:MATCH_ID`，出口 `match:MATCH_ID:win` / `:lose`；赛果位入口、推送出口均为 `result:RESULT_ID`，依照 source/target 区分方向。真实大框入口为 `group:GROUP_ID`，无共享出口；成员的自身入口隐藏并禁止接线，胜败/推送出口保持原格式。仅标签框没有端口，成员端口全部不变。合并/拖入时原入边改指 group；解除归属按原通道顺序与成员位置拆回 2/1 根，编辑后需重新校验。

真实比赛框算法值为 sequential/interleaved/outer/custom。对收齐的 2N 支确认队伍按当前阶段唯一 startRank 升序排序，再按 (2i,2i+1)/(i,i+N)/(i,2N-1-i) 分配，i=0…N-1。custom 仅占位，不执行文件内代码，也不回退为预设算法。真实赛果框在全部 N 支队伍收齐后按 startRank 分配成员，共享 stageRank/finalRank=L，附带 rankEnd=L+N-1；区间表示占据名次范围，不为每队额外赋予不同最终名次。

赛程可选 `teamInfoFile` 仅提示文件名，未匹配 ID 产生 TEAM_MISSING，允许编辑但禁止演算。编辑器战队属性只读，导出删除内存中的名称和排名，只保留 ID。缺失战队和数据未知是不同状态；未知 VRS/体力等不影响当前随机胜负原型，也不冒充已接入正式比赛模拟。

首阶段起始层只接受全局种子。后续阶段可以混合接受未使用的全局种子与紧邻上一阶段的推送位，以表达后段直接参赛队伍。一个全局种子仅分配一次。README 中“假设本阶段 X 则种子层有 X 个出口”与“全阶段所有队伍”的用词存在歧义；原型使用全局种子表加各阶段 X 个起始出口解释，仍需产品确认。

文件只保存可编辑结构、布局与外观接口，不保存随机结果、候选包或演算历史。导入后从初始状态重演，并同步紧邻阶段的推送连接；草稿可导入导出，但校验未通过时不能步进。本地运行历史也不自动保存到草稿。完整游戏存档与 `.csem` 赛制文件是不同对象。

## 编辑约定（2026-10-07 用户反馈修正）

创建下一阶段、改变赛果区域、删除阶段、载入/导入时同步晋级边：advance 位只连接紧邻下一阶段的 start；placement 无输出；末阶段等待下一阶段建立时无输出。现有正确边的 ID 保留，重复/过时的赛果出口边移除，缺边补齐。这是编辑器行为，落盘仍为普通 links，不添加额外 schema 字段。已有目标阶段人数不会自动调整；自动边超过容量时保留队伍并报 START_SIZE，由玩家调整。新增阶段默认人数取上一阶段晋级数（当前 2–64 限制，不足时仍需手工调整）。手动删除通道保留为可修复草稿；切换阶段保留已选出口，允许重新手动连接。

Shift + 点击出口按点击顺序增选/取消选择；普通点击替换为单选。点击入口时整批检查出口是否已有通道、目标剩余容量、端口方向、阶段边界与 DAG。全部合法后一次写入、一项结构撤销；失败不生成部分边，并保留选择供修正。确认同时到达仍按 links 顺序，故批量接线顺序不能随意更改。

Del / Backspace 删除单选或批量节点/通道，清理附着边；删大框同时删成员。输入框、下拉框和可编辑文本保留原生快捷键。起始层可拖动但不参与区域框选或大框吸附。新增比赛/赛果/两种大框统一固定在当前可见画布右上角，以 scrollLeft/scrollTop、clientWidth 和 zoom 换算逻辑坐标并吸附网格，不随节点数偏移。连续新增可重叠。框宽 235px，网格 23.5px；单框拖动绝对坐标吸附，多框以首个选中框对齐网格并保留相对位置。普通旧坐标不自动重排；大框成员以所属框重新紧密排列。

大框空高 2G，非空高 2G+N×H，上下各留 G；比赛 H=6G，赛果 H=2.5G，G=23.5。逻辑宽 10G，视觉外框为圆角四边形（圆角 9px），左右各扩展 G/5；共享入口在顶部格子的最左端，标签加粗居中，伪大框边框用虚线区分。兼容同类拖入自动接线与排列；不兼容和起始层保持独立。可能产生共享入口环路的合并/自动吸附拒绝，不部分写入。

右键区域选择（并提供空白画布左键区域选择）按完全包含的 DOM 边界选择节点和通道，排除起始层、种子→起始层通道，包含大框时按整体操作。相同小框类型可合并，混合 advance/placement 位可批量改类型；真实大框成员变类型时整框同步。选中对象可批量拖动、复制、剪切、删除。

程序内部缓存只保留类型、相对位置、归属和内部通道，名称/BO/排名/算法/标签等采用新对象默认值，不访问系统剪贴板。Ctrl+C/X 为对象时截获；Ctrl+V 仅画布悬停且有缓存时截获，最小覆盖矩形中心对准鼠标后吸附。文本编辑不截获。孤立通道复制/剪切也可缓存，粘贴时原端口仍须存在且可接；有节点的子图不克隆外部边。画布右键菜单提供同样的缓存粘贴。非法粘贴不部分写入。

Ctrl+Z/Y 与按钮撤销/重做最多 40 项编辑。快照包含文档、模拟状态、当前阶段与选择；新编辑清空 redo。布局变化不重置 FIFO/结果；结构/配对/排名变化重置模拟。撤销结构编辑恢复之前的运行快照。演算退回一步与编辑撤销是两个不同操作。

## 排序与唯一排名

用户确定所有排名只允许升序。第二关键字只在第一项相同时起作用；第三关键字只在前两项均相同时起作用。全部空选直接返回输入顺序。最终相同值按确认到达顺序稳定排列，随后赋予本阶段唯一 `startRank=1…X`，它与输入的 `seedRank/stageRank/liveRank` 分开保存。

原型种子排序 `seedSort` 默认直接返回，实时积分排名来自 `.tinf`，当前旧存档值为 0；不从 VRS 自动推算排名。正式 C++ 可注入种子排序与实时积分排名计算服务，但不能改动上述字典序比较规则。起始出口 INDEX 对应唯一 startRank 减一。

并列末排名必须位于同一真实赛果大框；仅标签框不能豁免重复。真实框区间占用 L…L+N-1，其他位占用单个 rank。每阶段 advance 排名不重且覆盖 1…该阶段晋级人数；全部阶段 placement 排名不重且覆盖 1…全赛事 seeds 人数。全局最终名次校验不把各阶段的推送排名重复计入。范围重叠报 RANK_OVERLAP，空缺/越界报 RANK_COVERAGE。阶段 size、赛果位数量与下阶段实际输入人数仍另行校验。

## 通道和演算

确认包为单个 TeamRef；候选包为 `{preview:true,candidates:[A,B]}`。空值在展示层显示 TBD。候选包在两支已确认队伍的比赛尚未结束时，同时送到胜、败出口的下游展示层。候选包永远不进入起始层收齐计数、赛果确认计数或比赛就绪计数。

1. 种子通过单出口分配到起始层。
2. 起始层收齐 X 支不同队伍后稳定排序，赋予唯一 startRank，再一次性通过 X 个出口推送。
3. 普通比赛入口确认两支不同队伍后进入就绪队列，按到达顺序先上后下。真实大框必须先收齐整个共享入口的不同确认队伍，才按算法为全部成员分配双方并允许入队。候选不计共享容量。仅标签框不改变入队条件。比赛尚未结束时可展示双选预推送。
4. 每次“下一步”只随机结束一场比赛。分别发送胜方和败方单队包，撤除该场候选包，重新传播所有确认信息。
5. 赛果层的所有 X 个位置确认填满后，advance 位同时向紧邻下一阶段起始层发送队伍。placement 位写入最终排名，不继续发送。
6. 最早尚未填满赛果层的阶段为当前执行阶段；后续阶段即使有直接参赛种子，也不提前结束比赛。非末阶段必须有 advance，末阶段必须无 advance。

当前算法通过有限次单调传播求稳定状态，并维护独立 FIFO 比赛队列。初始化收集当前阶段全部双确认、未比赛节点；每步只取队头，确认胜负并传播后，保留原队列中的等待比赛，把新凑齐两支不同确认队伍的比赛加入队尾。仅对同一次初始化/传播同时变为就绪的比赛，以 matches 数组顺序作为稳定的并列顺序；后就绪的比赛不得因较早创建而插队。占位时序文本不驱动队列，A/B 候选包不入队。

JS 运行状态为 `history/queue/queueHistory`：queue 保存比赛 ID，queueHistory 是队列缓存对应的完整事件历史签名。正常步进增量维护队列；derive 只读快照，不在渲染时修改队列。退回一步或读取没有队列缓存的旧历史时，按每条历史事件重新传播和追加入队，不能从最终的就绪集合重新按节点顺序排序。UI 退回调用 backStep，恢复队列及当前执行阶段；修改结构/导入/重置通过 createState(doc) 建立初始队列。随机胜负只在 step 内产生一次，重放已有历史不重新抽取随机数。队列不写入 .csem；未来游戏存档可保存历史并重建，也可保存带一致性检查的队列快照。

允许同阶段败方继续参加后续比赛，因此可以编辑固定结构双败图；可通过起始出口直接接赛果位表达轮空。尚不表达动态重配对的瑞士轮算法、条件性总决赛重赛、分数累积/地图细节，不能宣称已支持所有现实赛制。

## 校验责任

校验识别码/压缩与解压大小/JSON 版本、类型、ID 唯一性、有限坐标、人数、排序项、归属与成员唯一性、共享入口容量、出口独立且恰好一次、普通入口容量、起始入口 X 根、赛果数量 X、排名区间不重且全覆盖、阶段间边界及 DAG 无环。真实大框加入隐含 group→member 边，不能因隐藏小框入口而漏掉循环。空真实大框/custom 占位/未接完草稿允许保存，校验不通过则不能步进；空标签框不阻止推进，标签创建/合并/拖入保留运行状态。非法结构引用与类型拒绝导入。降低人数不静默补删节点。

结构校验不能证明一切条件赛制正确。运行还检查起始层队伍去重、比赛两队去重和等待状态。路径、校验成功、单示例运行均不能替代主项目集成验收。

## C++ 落地顺序

1. 定义 `GraphDocument/Stage/Match/ResultSlot/Group/Link/TeamRef`；端口解析为枚举与类型化 ID。Group 明确区分真实合并和仅标签。
2. 实现按用途分发的 magic+gzip+UTF-8 JSON reader/writer，先识别再限量解压，严格校验赛程 v3、战队 v1，并按 ID 绑定内部图；区分可载入草稿与可执行图。不执行文件内代码；如需要兼容旧格式，须另设明确迁移入口，不作识别码失败后的静默回退。
3. 迁移独立 `GraphValidator`，建立有向图、容量检查、拓扑排序，并携带节点 ID 的诊断。
4. 迁移无 UI 的 `GraphRuntime`。Confirmed / CandidatePair / Empty 为不同类型；ArrivalSequence 单调递增；阶段收齐、稳定排序和唯一排名保持一致。维护 FIFO 和入队 ID 集合，队头结束后再传播并追加新就绪比赛；按事件重放恢复退回后的入队次序，禁止每帧按节点数组重新构造调度顺序。
5. 执行一步产生一个 `MatchResolved` 事件，注入 RNG/比赛模拟器。将事件历史保存到游戏存档，以支持确定重放；`.csem` 仍只包含赛制结构。
6. Godot 前端消费运行快照；未定/胜/负外观通过资源接口替换；模拟视图隐藏种子/起始层和由它们发出的通道。
7. 将 engine.test.js、editor.test.js、features.test.js 和 ui.test.js 的用例迁移，尤其全框输入闸门、三种配对、并列区间、隐含环路与部分输入后 FIFO 追加；再补实际创建、存读档、跨阶段、素材替换和完整交互验收。同步迁移自动边、原子接线、内部缓存与撤销重做约定。

HTML 的布局坐标和编辑器属性是可视化元数据，不用于决定比赛规则。演算规则、编辑规则与界面分别放在 engine.js / editor.js / graph_linker.js，正式迁移时保持同样边界。
