/** * session-sync 的**内容判据**——回答一个 id-集合分类器回答不了的问题: * 「两条 entry id 集合相等的日志,**内容**是否也相同?」 * * ── 为什么需要它(深挖第一轮 + 2026-07-26 顺着往下查出的第二半)──────────────────────────────────── * §7 的 `classifySyncRelationshipByIds` 只比 **id 集合**,而 entry id 是 uuidv7、**不是内容寻址** ⇒ * 「同 id、异载荷」结构上完全可能(wire 本身就收调用方给的 entries)。第一半的后果已知:Phase A 判 `identical` * ⇒ 整段跳过、调用方拿 200 而目的端仍是自己那份内容(真 HTTP 复现过)。 * * 🔴 **第二半是我一开始判错的地方**:我在黑板 [ref] 说「server 单方面修不了」。那句只对 **Phase A** 成立 * (那时 server 手上没有源端内容,必须靠调用方送摘要)。但 **commit 时 server 同时握着两份日志** * (staged + dst),所以内容比较**根本不需要调用方配合** —— 而 4 处 commit/import 路径当时都写着 * 「`relation === "identical"` ⇒ 跳过 entries 写入」,把「id 集合相等」当成了「内容相同」。 * 后果:一个**已经把整条日志上传完**的调用方(没送摘要、或送了但不可比),在 commit 处仍然拿到 no-op。 * ⇒ 两半都要修:Phase A 需要调用方的 `logDigest`(省掉无谓上传),commit 这一半是 server 自己的活。 * * ── 判据来源:core 1.414 导出的 `sessionLogDigest` ────────────────────────────────────────────────── * 覆盖每条 entry 的**全部字段**(id / parentId / type / timestamp / 载荷)且**顺序显著**,逐条带长度前缀 * (所以两份日志不可能被重新切分成同样的字节)。core 明确它可以两端各算 —— 与 `boundInputHash` 不同, * 因为这里**分叉的代价是多同步一趟,不是错答案、也不是拒绝**(审批路径上分叉会拒掉一个合法的人类决定, * 方向相反)。 * * ── ⚠️ v1 摘要下本判据的**退化行为**(core 黑板 [ref];1.415.0 已修,现行下限已远高于此 —— * 承重下限与理由见 `test/core-dependency-floor.test.ts`,别在这里记版本号)──────────────── * v1 的摘要区分 `{a: undefined}` 与 `{}`,而 core 自己成规模产 own-key=undefined 的 entry ⇒ 同一份日志的 * 内存形与 JSON 往返形摘要必然不同。本函数在 commit 处比的是 **staged**(刚从 wire 解析,JSON 形)与 **dst** * (从存储读回,也是 JSON 形)—— 两侧同形,所以**这一处**不受那条影响;但若某个后端把 staged 留在内存形, * 判据会退化成「恒不相等 ⇒ 恒改写」。 * 🔴 那个退化方向是**安全**的(多写一遍,不会丢数据),这是刻意选的:判据不确定时**宁可改写**, * 因为反方向的错(误判"内容相同"而跳过写入)正是本函数要修的那个缺陷。 * ⇒ 所以这里**不加 scheme 门**。(Phase A 那条路曾加过 scheme **名字门**,后被 tsc 判恒真并撤掉, * 换成「依赖下限 + 行为门」—— 名字门只认识旧名字,守不住这条性质;见 server.ts Phase A 段注释与 * `test/sync-content-digest.test.ts`。) * * ⚠️ 抽成共享函数而不是在 4 处各写一遍:core [ref] [ref] 的教训(宣布"同族类修完成"而三个实现只落了一个), * 本仓今天也刚在 33 处 exec→FileError 包装上吃过同款。判据收成一处,才谈得上机器看守。 */ import { type SessionTreeEntry } from "@sema-agent/core"; /** * id 集合已判 `identical` 的两条日志,**内容是否真的相同**。 * * `true` ⇒ 内容也相同,跳过 entries 写入是**对的**(只需 owner 重戳)。 * `false` ⇒ **同 id 异内容**:必须真正改写目的端,否则调用方拿到的是一句"已同步"的谎。 * * 🔴 这里**不抛错、不拒绝**:core 定的消费口径是「digest 不符永远不得变成 error / 409 / 拒绝同步」—— * 不符只意味着"多干一趟活",而不是"这次同步非法"。 */ export declare function identicalIdsAlsoIdenticalContent(staged: readonly SessionTreeEntry[], dst: readonly SessionTreeEntry[] | null): boolean; /** * `fast_forward` 的**内容判据**([ref] 审计 A5/F2,2-i)——id 分类器的另一格盲区: * `dst ⊆ src` 只说明 **id 集合**是子集,共享 id 的**载荷**可能已分叉(目的端把某条 entry * 补写/纠正过、id 不变,源端拿着旧版本 + 新追加)。此时按「clean append」整段换装, * 目的端那几条纠正会被**静默销毁** —— §7「会丢目的端历史必须 409(除非显式 overwrite-dst)」 * 在载荷层同样成立。与 `identical` 格的口径**方向不同**是刻意的:identical 格 core 已定 * 「不符=多干一趟活、照常改写」(那里 src 按 id 完整覆盖 dst,push 语义=以源为准);本格 * dst 的共享版本与 src 的追加互为对方没有的东西,是真分叉,归 409 那一族。 * * 入参:`stagedShared` = staged 里 id ∈ dst 集合的那部分(**staged 顺序**);`dst` = 目的端全量 * (**dst 顺序**)。两侧都保留各自顺序 —— 摘要顺序显著,共享段的**乱序**同样是分叉。 * * 返回 `null` = 无分叉(安全 append);返回非空数组 = 分叉的 entry id(dst 顺序): * 逐条单 entry 摘要不等的 id;若逐条全等而整段摘要不等(纯乱序),列出位置错开的 id。 */ export declare function fastForwardSharedContentDiverged(stagedShared: readonly SessionTreeEntry[], dst: readonly SessionTreeEntry[]): string[] | null; /** * 把 fast_forward 的内容级分叉铸成 §7 冲突载荷(SyncConflictError.relation,wire 上是 409 的 * `{relation}` 详情)。用现有 `fork` 成员而不是扩词表:内容层它**就是** fork(双方各握着对方 * 没有的东西),且 SDK/客户端的关系词表是闭集,扩成员=BREAKING。字段口径(内容级,与 id 级 * 注释的措辞差异在此登记):`srcExclusive` = 源端要追加的新 id;`dstExclusive` = 目的端将被 * 销毁的共享版本的 id;`commonAncestor` = null(未逐条定位分叉点 —— 定位成本与收益不配)。 */ export declare function contentForkRelation(divergedIds: readonly string[], newEntryIds: readonly string[]): { relation: "fork"; commonAncestor: null; srcExclusive: string[]; dstExclusive: string[]; }; //# sourceMappingURL=session-sync-content.d.ts.map