diff --git a/.agents/notes/implemented/process/2026-09-11-persistence-type-history.i18n.yaml b/.agents/notes/implemented/process/2026-09-11-persistence-type-history.i18n.yaml index 246e224b7f..74cacfcfd5 100644 --- a/.agents/notes/implemented/process/2026-09-11-persistence-type-history.i18n.yaml +++ b/.agents/notes/implemented/process/2026-09-11-persistence-type-history.i18n.yaml @@ -9,14 +9,14 @@ en: c8e926bfc65d7195 zh: e0e007960b68fe80 /agent-note-in-tree-persistence-type-history/decision: - en: 94be3d6c7df3063b - zh: 7e8fb08d7fd95f85 + en: 3d4b2935c4db54b2 + zh: eb0da7083e301ef4 /agent-note-in-tree-persistence-type-history/decision/finalized-checkpoints: en: b508035a163340a3 zh: ba5b2b7e8cef4b2f /agent-note-in-tree-persistence-type-history/alternatives-considered: - en: 1cf6d0a5555ae50a - zh: 70ea1ba535e09442 + en: a581d63455788d48 + zh: 0a3781d2f827e35d /agent-note-in-tree-persistence-type-history/consequences: en: 672c8cb59d291dc2 zh: cbec90fbca92320a diff --git a/.agents/notes/implemented/process/2026-09-11-persistence-type-history.md b/.agents/notes/implemented/process/2026-09-11-persistence-type-history.md index add0904f42..5c98588230 100644 --- a/.agents/notes/implemented/process/2026-09-11-persistence-type-history.md +++ b/.agents/notes/implemented/process/2026-09-11-persistence-type-history.md @@ -16,6 +16,8 @@ One normalized type model supplies the readable persistence catalog, the complet The dedicated [persistence-change records](../../../../docs/persistence-changes/README.md) bind acknowledgement to the after digest of each affected root. A generated companion stores complete after schemas. The initial record covers all roots; later records name each root's predecessor. A predecessor's after schema supplies the next before schema. Verification rejects ambiguous history and requires current source to match the terminal recorded state without consulting Git history or remote services. +Every schema file stores one node table keyed by a 16-hexadecimal-digit prefix of each node's canonical subgraph digest; roots and types locate their graphs through their own digests. A type change therefore rewrites only the changed nodes, their ancestors, and the affected digests. Parsers accept only the exact generated text, and all committed files share this layout. + Automatic classification permits optional body additions, required-to-optional body changes, and ordinary event additions within one version. The scoped [source-attribution policy](2026-09-17-persistence-schema-review.md) also permits qualified new producer kinds when both saved schemas opt in. The [event-payload version rule](../../../../docs/persistence-changes/README.md#compatibility-rules) admits higher numeric payload versions when every old alternative remains readable and records the older-reader limitation. Other structural changes require an explicit same-version choice in the recording CLI or an increasing header version. Every structural difference needs an acknowledgement; its Compatibility and Verification sections explain both reader directions and executed checks. CI validates saved decisions without inferring how they were authored. The [format-version procedure](../../../../docs/cookbook/adding-a-session-format-version.md) owns adjacent migration work when a bump is required. The persistence-change document kind preserves compatibility reasoning and evidence for a historical type transition. Agent Notes retain mechanism-level decisions; they do not become a growing inventory of individual acknowledgements. Machine declarations remain identical across the bilingual pair and are parsed once. @@ -44,6 +46,8 @@ Finalization records acceptance independently of publication. The [status record **Infer behavior from types or require proof of human review.** A type graph cannot establish replay semantics, and a recorded choice cannot establish how it was authored. The checker validates declared transitions and header-version constraints. Reviewers inspect actual reader behavior and the acknowledgement’s explanation independently. +**Keep positional node indices or also read the previous layout.** Positional indices renumber every later node when a union gains a variant, and per-root graphs repeat shared types, so a one-variant change rewrote thousands of lines. Digest-keyed nodes keep diffs proportional to the change. Reading two layouts would leave two parsers to maintain and two accepted texts per inventory; migrating every committed file once keeps a single format. + ## Consequences Authors retain one snapshot per affected root per accepted transition and resolve history forks when integrating competing changes to that root. Independent roots can advance without rewriting unrelated acknowledgements. The [cookbook](../../../../docs/cookbook/reviewing-persistence-type-changes.md) provides the local authoring and verification procedure. diff --git a/.agents/notes/implemented/process/2026-09-11-persistence-type-history.zh.md b/.agents/notes/implemented/process/2026-09-11-persistence-type-history.zh.md index fade8ff41b..dae77a7810 100644 --- a/.agents/notes/implemented/process/2026-09-11-persistence-type-history.zh.md +++ b/.agents/notes/implemented/process/2026-09-11-persistence-type-history.zh.md @@ -16,6 +16,8 @@ Status: implemented 专用的[持久化变更记录](../../../../docs/persistence-changes/README.zh.md)将确认绑定到每个受影响根的变更后摘要。生成的伴随文件保存完整的变更后 schema。初始记录覆盖所有根;后续记录为每个根指定前驱。前驱的变更后 schema 提供下一次的变更前 schema。验证拒绝有歧义的历史,并要求当前源码匹配最终记录状态,无需查询 Git 历史或远端服务。 +每个 schema 文件保存一张节点表,以每个节点规范子图摘要的 16 位十六进制前缀为键;根与类型通过各自的摘要定位其图。因此类型变更只改写发生变化的节点、其祖先节点以及受影响的摘要。解析器只接受精确生成的文本,所有已提交文件共用此布局。 + 自动分类允许可选事件体新增、事件体属性从必选改为可选,以及普通事件新增保持同一版本。当双方保存的 schema 均声明支持时,受限的[消息源归属策略](2026-09-17-persistence-schema-review.zh.md)也允许添加符合条件的生产者 kind。[事件载荷版本规则](../../../../docs/persistence-changes/README.zh.md#compatibility-rules)允许在每个旧分支仍可读取时增加更高的数字载荷版本,并记录旧读取器限制。其他结构变更要求在记录 CLI 中显式选择同版本,或递增头版本。每个结构差异都需要确认;其兼容性与验证部分说明两个方向的读取器行为及已执行检查。CI 验证保存的决策,不推断其编写方式。需要升版本时,[格式版本流程](../../../../docs/cookbook/adding-a-session-format-version.zh.md)负责相邻迁移工作。 持久化变更文档类型保留历史类型转换的兼容性说明和证据。Agent Note 保留机制层面的决策,不积累单次确认清单。双语对中的机器声明保持相同,且只解析一次。 @@ -44,6 +46,8 @@ Status: implemented **从类型推断行为,或要求人类评审证明。** 类型图无法证明回放语义,所记录的选择也无法证明其编写方式。检查器验证声明的转换与头版本约束。评审者独立检查实际读取器行为与确认说明。 +**保留按位置编号的节点索引,或同时读取旧布局。** 联合类型新增分支时,按位置编号的索引会为其后的每个节点重新编号,而逐根图会重复共享类型,因此单个分支变更曾改写数千行。以摘要为键的节点使差异规模与变更成正比。同时读取两种布局需要维护两个解析器,且每个清单会有两种可接受文本;将所有已提交文件一次性迁移后只保留单一格式。 + ## 影响 作者为每次已接受转换中的每个受影响根保留一份快照,并在集成该根的竞争变更时解决历史分叉。独立根可以演进而不改写无关确认。[实操手册](../../../../docs/cookbook/reviewing-persistence-type-changes.zh.md)提供本地编写与验证流程。 diff --git a/docs/cookbook/reviewing-persistence-type-changes.i18n.yaml b/docs/cookbook/reviewing-persistence-type-changes.i18n.yaml index eba1d60922..8b7aa60810 100644 --- a/docs/cookbook/reviewing-persistence-type-changes.i18n.yaml +++ b/docs/cookbook/reviewing-persistence-type-changes.i18n.yaml @@ -15,8 +15,8 @@ en: 5fca19ac2bb1ddac zh: 00d78fe7e80ba4f7 /cookbook-reviewing-persistence-type-changes/optional-inspect-the-change: - en: 14049617e3de750d - zh: 200456cf72bef77b + en: 841a75ed33db17bd + zh: ed4e80aae708bfb9 /cookbook-reviewing-persistence-type-changes/1-record-the-change: en: de16b6e1ac883317 zh: 21d367bb78e799d1 diff --git a/docs/cookbook/reviewing-persistence-type-changes.md b/docs/cookbook/reviewing-persistence-type-changes.md index 56620e5a8b..c2498599cf 100644 --- a/docs/cookbook/reviewing-persistence-type-changes.md +++ b/docs/cookbook/reviewing-persistence-type-changes.md @@ -43,7 +43,7 @@ A recorded decision does not hide the transition from this comparison. `--check` Record the commits supplying those files with the report. Add `--json` for structured output. This read-only comparison groups shared changes with their affected roots and uses actual literal `kind`/`form` values instead of union positions. Ambiguous alternatives remain separate additions and removals. Its compatibility section copies every root's authoritative classifier result; the structural explanation does not replace acknowledgement checks. Current catalog labels and declaration names are descriptive metadata; structural anchors and fingerprints identify types. -The current machine inventory stores complete graphs in `roots`. A `types` entry contains `digest`, `names`, and `sources`, plus an explicit `schema` when roots cannot reconstruct that graph exactly. Readers resolve omitted graphs from root subgraphs by digest and validate explicit graphs directly, preserving every type and its metadata. Historical full entries remain readable. `formatVersion` identifies normalization rules; storage compaction does not change root fingerprints or require an acknowledgement. +Every inventory uses the [node-table layout](../persistence-changes/README.md#files-and-ownership); both comparison inputs must be generated by the current tooling. `formatVersion` identifies normalization rules; the storage layout does not change root fingerprints or require an acknowledgement. ## 1. Record the change diff --git a/docs/cookbook/reviewing-persistence-type-changes.zh.md b/docs/cookbook/reviewing-persistence-type-changes.zh.md index 51d3f1b6c2..ccc2e0a2b0 100644 --- a/docs/cookbook/reviewing-persistence-type-changes.zh.md +++ b/docs/cookbook/reviewing-persistence-type-changes.zh.md @@ -43,7 +43,7 @@ pnpm --silent run persistence-review --before .artifacts/base.schema.json --afte 在报告旁记录这些文件对应的 commit。添加 `--json` 可获取结构化输出。此只读比较将共享变更与受影响的根类型归组,使用实际字面量 `kind`/`form` 值代替联合类型位置。无法唯一匹配的候选项保留为独立的新增与删除。兼容性部分复制每个根类型的权威分类结果;结构说明不替代确认检查。当前目录标签和声明名称是描述元数据;结构锚点和指纹标识类型。 -当前机器清单在 `roots` 中保存完整图。每个 `types` 条目包含 `digest`、`names` 和 `sources`;若根无法精确重建该图,条目还会保存显式 `schema`。读取器按摘要从根的子图恢复省略的图,并直接验证显式图,保留每个类型及其元数据。历史完整条目仍然可读。`formatVersion` 标识规范化规则;存储压缩不改变根指纹,也不需要确认记录。 +所有清单都使用[节点表布局](../persistence-changes/README.zh.md#files-and-ownership);两个比较输入都必须由当前工具生成。`formatVersion` 标识规范化规则;存储布局不改变根指纹,也不需要确认记录。 ## 1. 记录变更 diff --git a/docs/persistence-changes/README.i18n.yaml b/docs/persistence-changes/README.i18n.yaml index 85b9cf606e..90a4bd0676 100644 --- a/docs/persistence-changes/README.i18n.yaml +++ b/docs/persistence-changes/README.i18n.yaml @@ -16,7 +16,7 @@ zh: 6e216391ad24c541 /persistence-type-change-records/files-and-ownership: en: 41d4ab88aac68e7e - zh: bc1e688b078e1ac2 + zh: 062d65578a1dbb53 /persistence-type-change-records/compatibility-rules: en: 218b28a5096b5e7d zh: 5b987c852e204893 diff --git a/docs/persistence-changes/README.zh.md b/docs/persistence-changes/README.zh.md index ce654b307f..21e7e2e84c 100644 --- a/docs/persistence-changes/README.zh.md +++ b/docs/persistence-changes/README.zh.md @@ -28,7 +28,7 @@ description: "审阅和维护已记录的会话持久化类型变更、对应 sc 生成的[目录](../persistence-catalog.zh.md)提供可读声明和摘要;[schema 清单](../persistence-schema.json)包含规范化类型。根覆盖逻辑会话头、物理 JSONL 头行、事件封装以及每个仓库内声明的事件。被引用类型的变更会传递到每个受影响根的摘要。 -每个 `.schema.json` 文件存储一张节点表,键为节点子图摘要的前 16 位十六进制字符,每个根类型、类型或节点占一行。根类型和类型通过自身摘要定位其类型图,因此类型变更只改写变化的节点、其祖先节点以及受影响的摘要。生成器决定确切文本;校验器拒绝其他布局。 +每个 `.schema.json` 文件存储一张节点表,键为节点子图摘要的前 16 位十六进制字符,每个根、类型或节点占一行。根和类型通过自身摘要定位其图,因此类型变更只改写变化的节点、其祖先节点以及受影响的摘要。生成器决定确切文本;校验器拒绝其他布局。 每条带日期的记录有四个同目录文件: diff --git a/scripts/persistence-changes.ts b/scripts/persistence-changes.ts index 73691c3d35..6fdcfb7bd4 100644 --- a/scripts/persistence-changes.ts +++ b/scripts/persistence-changes.ts @@ -289,7 +289,9 @@ function parseSnapshot(text: string, historical: boolean): PersistenceSchemaInve } const used = new Set() const graphOf = (graphDigest: string, label: string): CanonicalSchema => { - const index = position(graphDigest.slice(0, SCHEMA_NODE_KEY_LENGTH), label) + const prefix = graphDigest.slice(0, SCHEMA_NODE_KEY_LENGTH) + const index = positions.get(prefix) + if (index === undefined) throw new Error(`${label}: no schema node for digest prefix ${prefix}`) const pending = [index] while (pending.length > 0) { const next = pending.pop() as number @@ -665,7 +667,7 @@ function readPersistenceEntries(root: string, allowIncompleteId?: string): Persi const change = parseDocument(source, filename, allowIncomplete) const snapshotName = `${change.id}.schema.json` if (!snapshots.delete(snapshotName)) throw new Error(`${filename}: missing schema snapshot ${snapshotName}`) - const snapshot = parsePersistenceSnapshot(readFileSync(join(directory, snapshotName), 'utf8')) + const snapshot = parsePersistenceSnapshot(readFileSync(join(directory, snapshotName), 'utf8').replaceAll('\r\n', '\n')) const translatedName = `${change.id}.zh.md` if (!files.includes(translatedName)) throw new Error(`${filename}: missing Chinese counterpart`) const translated = readFileSync(join(directory, translatedName), 'utf8').replaceAll('\r\n', '\n') diff --git a/scripts/persistence-schema-snapshot.spec.ts b/scripts/persistence-schema-snapshot.spec.ts index 43702a10b5..72a49dd98a 100644 --- a/scripts/persistence-schema-snapshot.spec.ts +++ b/scripts/persistence-schema-snapshot.spec.ts @@ -84,17 +84,18 @@ describe('persistence snapshot text', () => { it('rejects tampered node keys, unknown references, orphan nodes, and digest mismatches', () => { const text = encodePersistenceSnapshot(inventory()) - const [first, second] = Object.keys((JSON.parse(text) as SnapshotJson).nodes) as [string, string] + const rootKey = inventory().roots[0]!.digest.slice(0, 16) + const second = Object.keys((JSON.parse(text) as SnapshotJson).nodes).find(key => key !== rootKey)! expect(() => parsePersistenceSnapshot(edit(text, (value) => { - value.nodes = Object.fromEntries(Object.entries(value.nodes).map(([key, node]) => [key === first ? 'f'.repeat(16) : key, node])) - }))).toThrow(/key does not match|unknown schema node/u) - expect(() => parsePersistenceSnapshot(edit(text, (value) => { value.nodes[first] = { kind: 'array', element: '0'.repeat(16) } }))) + value.nodes = Object.fromEntries(Object.entries(value.nodes).map(([key, node]) => [key === rootKey ? 'f'.repeat(16) : key, node])) + }))).toThrow('key does not match') + expect(() => parsePersistenceSnapshot(edit(text, (value) => { value.nodes[rootKey] = { kind: 'array', element: '0'.repeat(16) } }))) .toThrow('unknown schema node') expect(() => parsePersistenceSnapshot(edit(text, (value) => { value.nodes[second] = { kind: 'primitive', type: 'number' } }))) .toThrow('key does not match') expect(() => parsePersistenceSnapshot(edit(encodePersistenceSnapshot({ ...inventory(), roots: [] }), (value) => { value.types = [] }))) .toThrow('not reachable from any root or type') - expect(() => parsePersistenceSnapshot(edit(text, (value) => { value.roots[0]!.digest = 'f'.repeat(64) }))).toThrow('unknown schema node') + expect(() => parsePersistenceSnapshot(edit(text, (value) => { value.roots[0]!.digest = 'f'.repeat(64) }))).toThrow('no schema node for digest prefix') }) it('rejects node entries whose properties or union members are not in canonical order', () => {