@xneog/dsh-session-format 0.1.3-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/session/session-format/README.md
5
+ README.md: 7c1c00bb3bace78ae9a38c9a6ddea13f0eff536f
6
+ README.zh.md: 9171f554bdf8deee2fe0136cc253742025e3619c
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ ---
2
+ description: "Pure adjacent Session format planning, lossless JSON snapshots, header-only migration, and physical codec dispatch."
3
+ kind: "package-library"
4
+ ---
5
+
6
+ # @xneog/dsh-session-format
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ `dsh-session-format` lets persistence code restore a current Session directly or compose a unique sequence of adjacent whole-artifact migrations. It snapshots every durable input and output as detached lossless JSON, validates exact version progress, and keeps header-only listing separate from body reads. Physical framing, compression, immutable generation naming, exclusive publication, and Cordis lifecycle behavior remain outside this pure library.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ ### When to use it
29
+
30
+ Use this library from persistence or format-catalog code that must classify a physical Session header, restore current logical values, or compose released adjacent migrations. It is not a Cordis plugin and has no profile mount row. No runtime invariant companion is published because every operation validates its borrowed artifact before returning and retains no cross-call mutable state.
31
+
32
+ ### Entry point
33
+
34
+ ```text
35
+ const catalog = createSessionFormatCatalog({ currentVersion, codecs, encodeCurrentArtifact, migrations, restoreCurrent, restoreCurrentHeader })
36
+ const descriptor = catalog.readHeader(physicalHeader)
37
+ ```
38
+
39
+ `createSessionFormatCatalog()` accepts one frozen decoder per supported version, the current format's encoder, one migration per adjacent version pair, and current artifact and header restorers. `readHeader()` returns a `current`, `migration-required`, `unsupported`, or `malformed` descriptor without reading events. Each edge validates its target header before the final current-header restorer runs. Body readers call `decodeArtifact()` or `decodeRecoverableArtifact()`, then `migrate()`; writers call `encodeCurrent()` only with a validated current artifact. Frozen v0/v1 codec exports retain their format-specific `packChunks` option without adding that historical control to the current writer or common decoder interface.
40
+
41
+ The recoverable decoder returns the accepted logical prefix. A codec may drop one malformed or sequence-gapped row and its uncommitted suffix, but a later decoded `turn/end` makes the original issue fatal.
42
+
43
+ -----
44
+
45
+ <a id="understand-the-implementation"></a>
46
+ ## Understand the implementation
47
+
48
+ <details>
49
+ <summary>Implementation internals — click to expand</summary>
50
+
51
+ The chain validates unique gap-free ordering at construction. A current artifact bypasses every migration callback and passes through only the current restorer. An old artifact runs each adjacent whole-document function in memory; only the caller decides whether and how to publish the final result.
52
+
53
+ | File | Role |
54
+ |---|---|
55
+ | [`src/chain.ts`](src/chain.ts) | Adjacent plan construction and current bypass |
56
+ | [`src/catalog.ts`](src/catalog.ts) | Physical version dispatch and header classification |
57
+ | [`src/json.ts`](src/json.ts) | Detached lossless JSON snapshots and common coordinate checks |
58
+ | [`src/filename.ts`](src/filename.ts) | Canonical `session[.vN].jsonl` basename shared by persistence, export, and fixtures |
59
+
60
+ </details>
61
+
62
+ -----
63
+
64
+ <a id="further-exploration"></a>
65
+ ## Further Exploration
66
+
67
+ - [Released v0 to v1 edge](../session-format-v0-to-v1/README.md) — frozen historical decoding and identity conversion.
68
+ - [Static catalog](../session-format-catalog/README.md) — first-party codec and migration assembly.
69
+ - [JSONL persistence](../session-persistence-jsonl/README.md) — durable framing and generation publication.
70
+
71
+ -----
72
+
73
+ <a id="model-experience"></a>
74
+ ## Model Experience
75
+
76
+ ### Session restoration
77
+
78
+ #### What the model sees
79
+
80
+ Nothing directly. Consumers reconstruct model history from the validated current artifact through `deriveMessages()`.
81
+
82
+ #### Token effect
83
+
84
+ Zero direct tokens.
85
+
86
+ #### KV Cache effect
87
+
88
+ No direct effect. A migration that changes current history can change the cache identity owned by request reconstruction.
89
+
90
+ ## Known Limitations and Deferred Work
91
+
92
+ <a id="known-limitations-and-deferred-work"></a>
93
+
94
+ - **Whole-artifact memory use** — supported migrations materialize the complete logical Session; streamed transformation is deferred until measured artifacts require it.
95
+ - **Adjacent integer versions only** — the library does not expose spans, stable event identities, or a general reference-rewrite algebra.
96
+
97
+ <a id="dev-note"></a>
98
+ ### Dev Note
99
+
100
+ <details>
101
+ <summary>Working context for maintainers — click to expand</summary>
102
+
103
+ None.
104
+
105
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,105 @@
1
+ ---
2
+ description: "纯函数式相邻 Session 格式规划、无损 JSON 快照、仅标头迁移与物理编解码分派。"
3
+ kind: "package-library"
4
+ ---
5
+
6
+ # @xneog/dsh-session-format
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ `dsh-session-format` 让持久化代码可以直接还原当前 Session,或组合唯一的相邻全产物迁移序列。它会把每个持久化输入和输出快照为分离的无损 JSON,校验精确的版本推进,并把仅标头的列表读取与正文读取分开。物理分帧、压缩、不可变 generation 命名、排他发布和 Cordis 生命周期行为不属于这个纯函数库。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ ### 何时使用
29
+
30
+ 当持久化或格式目录代码需要分类物理 Session header、还原当前逻辑值或组合已发布相邻迁移时,使用本库。它不是 Cordis 插件,也没有 profile 挂载行。它不发布运行时不变式伴生入口,因为每个操作都会在返回前校验借入的完整 artifact,且不保留跨调用的可变状态。
31
+
32
+ ### 入口
33
+
34
+ ```text
35
+ const catalog = createSessionFormatCatalog({ currentVersion, codecs, encodeCurrentArtifact, migrations, restoreCurrent, restoreCurrentHeader })
36
+ const descriptor = catalog.readHeader(physicalHeader)
37
+ ```
38
+
39
+ `createSessionFormatCatalog()` 接收每个受支持版本的一个冻结解码器、当前格式的编码器、每组相邻版本的一个迁移,以及当前产物与标头还原器。`readHeader()` 在不读取事件的情况下返回 `current`、`migration-required`、`unsupported` 或 `malformed` 描述符。每个迁移边会先校验自己的目标标头,然后再运行最终的当前标头还原器。正文读取方调用 `decodeArtifact()` 或 `decodeRecoverableArtifact()`,然后调用 `migrate()`;写入方只使用经过校验的当前产物调用 `encodeCurrent()`。冻结的 v0/v1 编解码器导出会保留其格式专用的 `packChunks` 选项,但不会把这项历史控制加入当前 writer 或通用解码器接口。
40
+
41
+ 可恢复解码器返回已接受的逻辑前缀。编解码器可以丢弃一个格式错误或序号不连续的行及其未提交后缀,但后续成功解码的 `turn/end` 会使原始问题成为致命错误。
42
+
43
+ -----
44
+
45
+ <a id="understand-the-implementation"></a>
46
+ ## 理解实现
47
+
48
+ <details>
49
+ <summary>实现细节——点击展开</summary>
50
+
51
+ 迁移链在构造时校验唯一且无缺口的顺序。当前产物绕过所有迁移回调,只经过当前格式还原器。旧产物在内存中依次运行每个相邻的全产物函数;只有调用方决定是否发布最终结果以及如何发布。
52
+
53
+ | 文件 | 职责 |
54
+ |---|---|
55
+ | [`src/chain.ts`](src/chain.ts) | 相邻计划构造与当前格式绕过 |
56
+ | [`src/catalog.ts`](src/catalog.ts) | 物理版本分派与标头分类 |
57
+ | [`src/json.ts`](src/json.ts) | 分离的无损 JSON 快照与通用坐标校验 |
58
+ | [`src/filename.ts`](src/filename.ts) | 持久化、导出与 fixture 共用的规范 `session[.vN].jsonl` 文件名 |
59
+
60
+ </details>
61
+
62
+ -----
63
+
64
+ <a id="further-exploration"></a>
65
+ ## 进一步探索
66
+
67
+ - [已发布 v0 到 v1 迁移边](../session-format-v0-to-v1/README.zh.md)——冻结的历史解码与恒等转换。
68
+ - [静态目录](../session-format-catalog/README.zh.md)——第一方编解码器与迁移装配。
69
+ - [JSONL 持久化](../session-persistence-jsonl/README.zh.md)——持久化分帧与代际发布。
70
+
71
+ -----
72
+
73
+ <a id="model-experience"></a>
74
+ ## 模型体验
75
+
76
+ ### Session 还原
77
+
78
+ #### 模型看到什么
79
+
80
+ 没有直接内容。消费方通过 `deriveMessages()` 从经过校验的当前产物重建模型历史。
81
+
82
+ #### Token 影响
83
+
84
+ 不直接产生 token。
85
+
86
+ #### KV Cache 影响
87
+
88
+ 没有直接影响。迁移若改变当前历史,可能改变由请求重建逻辑拥有的缓存身份。
89
+
90
+ ## 已知限制与延期工作
91
+
92
+ <a id="known-limitations-and-deferred-work"></a>
93
+
94
+ - **全产物内存占用**——受支持的迁移会物化完整逻辑 Session;只有实测产物规模提出要求时,才会引入流式转换。
95
+ - **仅支持相邻整数版本**——本库不暴露 span、稳定事件身份或通用引用重写代数。
96
+
97
+ <a id="dev-note"></a>
98
+ ### 开发备注
99
+
100
+ <details>
101
+ <summary>维护者的工作上下文——点击展开</summary>
102
+
103
+ 无。
104
+
105
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,356 @@
1
+ import { deepFreeze, snapshotJsonValue } from "@xneog/dsh-util-values";
2
+ //#region lib/types/error.js
3
+ /** Error raised when a durable Session artifact cannot be restored or migrated losslessly. */
4
+ var SessionFormatError = class extends Error {
5
+ name = "SessionFormatError";
6
+ };
7
+ /** A readable artifact whose released source policy has no supported migration. */
8
+ var SessionFormatUnsupportedMigrationError = class extends SessionFormatError {
9
+ name = "SessionFormatUnsupportedMigrationError";
10
+ };
11
+ //#endregion
12
+ //#region lib/types/json.js
13
+ /**
14
+ * Test whether a value is a non-null, non-array object.
15
+ * @param value - candidate value.
16
+ * @returns whether the value is an object record.
17
+ */
18
+ function isSessionFormatJsonObject(value) {
19
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20
+ }
21
+ /**
22
+ * Require a non-negative safe integer without the JSON-unstable negative zero.
23
+ * @param value - candidate count.
24
+ * @param label - diagnostic subject.
25
+ * @returns validated count.
26
+ */
27
+ function sessionFormatCount(value, label) {
28
+ if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) throw new SessionFormatError(`${label} must be a non-negative safe integer`);
29
+ return value;
30
+ }
31
+ /**
32
+ * Require a safe integer without the JSON-unstable negative zero.
33
+ * @param value - candidate integer.
34
+ * @param label - diagnostic subject.
35
+ * @returns validated integer.
36
+ */
37
+ function sessionFormatSafeInteger(value, label) {
38
+ if (!Number.isSafeInteger(value) || Object.is(value, -0)) throw new SessionFormatError(`${label} must be a safe integer`);
39
+ return value;
40
+ }
41
+ /**
42
+ * Require a non-negative integral format version.
43
+ * @param value - candidate version.
44
+ * @param label - diagnostic subject.
45
+ * @returns validated version.
46
+ */
47
+ function sessionFormatVersion(value, label = "Session format version") {
48
+ return sessionFormatCount(value, label);
49
+ }
50
+ /**
51
+ * Read only the version required for directional dispatch.
52
+ * @param headerValue - untrusted physical header value.
53
+ * @returns validated stored version.
54
+ */
55
+ function inspectSessionFormatVersion(headerValue) {
56
+ if (!isSessionFormatJsonObject(headerValue)) throw new SessionFormatError("Session header must be a JSON object");
57
+ return sessionFormatVersion(headerValue["version"]);
58
+ }
59
+ /**
60
+ * Detach and deeply freeze a caller-supplied lossless JSON value.
61
+ * @param value - borrowed candidate.
62
+ * @param label - diagnostic subject.
63
+ * @returns an immutable detached JSON snapshot.
64
+ */
65
+ function snapshotSessionFormatJson(value, label = "Session value") {
66
+ const snapshot = snapshotJsonValue(value);
67
+ if (snapshot === void 0) throw new SessionFormatError(`${label} is not lossless JSON`);
68
+ return deepFreeze(snapshot);
69
+ }
70
+ /**
71
+ * Snapshot one complete artifact and validate its shared coordinates.
72
+ * @param artifact - borrowed logical artifact.
73
+ * @param label - diagnostic subject.
74
+ * @returns immutable detached artifact.
75
+ */
76
+ function snapshotSessionFormatArtifact(artifact, label = "Session artifact") {
77
+ const snapshot = snapshotSessionFormatJson(artifact, label);
78
+ const header = snapshot["header"];
79
+ const inheritedEventCount = snapshot["inheritedEventCount"];
80
+ const events = snapshot["events"];
81
+ if (!isSessionFormatJsonObject(header)) throw new SessionFormatError(`${label} header must be a JSON object`);
82
+ inspectSessionFormatVersion(header);
83
+ sessionFormatCount(inheritedEventCount, `${label} inheritedEventCount`);
84
+ if (!Array.isArray(events)) throw new SessionFormatError(`${label} events must be an array`);
85
+ for (let index = 0; index < events.length; index += 1) {
86
+ const event = events[index];
87
+ if (!isSessionFormatJsonObject(event)) throw new SessionFormatError(`${label} event ${index} must be a JSON object`);
88
+ if (event["seq"] !== index) throw new SessionFormatError(`${label} event ${index} has non-dense seq ${String(event["seq"])}`);
89
+ if (typeof event["type"] !== "string" || event["type"].length === 0) throw new SessionFormatError(`${label} event ${index} type must be a non-empty string`);
90
+ sessionFormatSafeInteger(event["time"], `${label} event ${index} time`);
91
+ if (!Object.hasOwn(event, "data")) throw new SessionFormatError(`${label} event ${index} lacks data`);
92
+ }
93
+ if (inheritedEventCount > events.length) throw new SessionFormatError(`${label} inheritedEventCount exceeds its event count`);
94
+ return snapshot;
95
+ }
96
+ /**
97
+ * Snapshot one logical header without inspecting an event body.
98
+ * @param header - borrowed logical header.
99
+ * @param label - diagnostic subject.
100
+ * @returns immutable detached header.
101
+ */
102
+ function snapshotSessionFormatHeader(header, label = "Session header") {
103
+ const snapshot = snapshotSessionFormatJson(header, label);
104
+ if (!isSessionFormatJsonObject(snapshot)) throw new SessionFormatError(`${label} must be a JSON object`);
105
+ inspectSessionFormatVersion(snapshot);
106
+ if (typeof snapshot["id"] !== "string") throw new SessionFormatError(`${label} id must be a string`);
107
+ sessionFormatCount(snapshot["createdAt"], `${label} createdAt`);
108
+ if (typeof snapshot["isSeeded"] !== "boolean") throw new SessionFormatError(`${label} isSeeded must be a boolean`);
109
+ sessionFormatCount(snapshot["delegationDepth"], `${label} delegationDepth`);
110
+ return snapshot;
111
+ }
112
+ //#endregion
113
+ //#region lib/types/chain.js
114
+ /**
115
+ * Validate and freeze one adjacent migration declaration.
116
+ * @param migration - named exact adjacent conversion.
117
+ * @returns immutable validated declaration.
118
+ */
119
+ function defineSessionFormatMigration(migration) {
120
+ if (typeof migration.name !== "string" || migration.name.length === 0) throw new SessionFormatError("Session migration name must be a non-empty string");
121
+ const from = sessionFormatVersion(migration.fromVersion, `${migration.name} fromVersion`);
122
+ if (sessionFormatVersion(migration.toVersion, `${migration.name} toVersion`) !== from + 1) throw new SessionFormatError(`${migration.name} must declare adjacent v${from}->v${from + 1}`);
123
+ return Object.freeze({ ...migration });
124
+ }
125
+ /**
126
+ * Compile a unique, complete adjacent migration chain.
127
+ * @param options - current version, adjacent declarations, and current restorer.
128
+ * @returns immutable planner and whole-artifact runner.
129
+ */
130
+ function createSessionFormatChain(options) {
131
+ return new CompiledSessionFormatChain(options);
132
+ }
133
+ var CompiledSessionFormatChain = class {
134
+ currentVersion;
135
+ migrations;
136
+ restoreCurrent;
137
+ restoreCurrentHeader;
138
+ constructor(options) {
139
+ this.currentVersion = sessionFormatVersion(options.currentVersion, "current Session format version");
140
+ this.restoreCurrent = options.restoreCurrent;
141
+ this.restoreCurrentHeader = options.restoreCurrentHeader;
142
+ const byFrom = /* @__PURE__ */ new Map();
143
+ const names = /* @__PURE__ */ new Set();
144
+ for (const candidate of options.migrations) {
145
+ const migration = defineSessionFormatMigration(candidate);
146
+ if (byFrom.has(migration.fromVersion)) throw new SessionFormatError(`Session migration v${migration.fromVersion}->v${migration.toVersion} is duplicated`);
147
+ if (names.has(migration.name)) throw new SessionFormatError(`Session migration name ${JSON.stringify(migration.name)} is duplicated`);
148
+ byFrom.set(migration.fromVersion, migration);
149
+ names.add(migration.name);
150
+ }
151
+ const ordered = [];
152
+ for (let version = 0; version < this.currentVersion; version += 1) {
153
+ const migration = byFrom.get(version);
154
+ if (migration === void 0) throw new SessionFormatUnsupportedMigrationError(`Session migration v${version}->v${version + 1} is missing`);
155
+ ordered.push(migration);
156
+ }
157
+ if (byFrom.size !== ordered.length) throw new SessionFormatError(`Session migration from v${[...byFrom.keys()].find((version) => version >= this.currentVersion)} does not lead to current v${this.currentVersion}`);
158
+ this.migrations = Object.freeze(ordered);
159
+ }
160
+ plan(fromVersion) {
161
+ const from = sessionFormatVersion(fromVersion, "stored Session format version");
162
+ if (from > this.currentVersion) throw new SessionFormatUnsupportedMigrationError(`stored Session uses newer format v${from}; this build writes v${this.currentVersion}`);
163
+ return Object.freeze(this.migrations.slice(from));
164
+ }
165
+ migrate(source) {
166
+ const storedVersion = inspectSessionFormatVersion(source.header);
167
+ let current = snapshotSessionFormatArtifact(source, `format v${storedVersion} source`);
168
+ if (storedVersion === this.currentVersion) {
169
+ current = snapshotSessionFormatArtifact(this.restoreCurrent(current), "current Session restoration");
170
+ this.assertCurrent(current);
171
+ return current;
172
+ }
173
+ for (const migration of this.plan(storedVersion)) {
174
+ let migrated;
175
+ try {
176
+ migrated = migration.migrate(snapshotSessionFormatArtifact(current, `${migration.name} input`));
177
+ } catch (error) {
178
+ throwUnsupportedRefusal(migration, error);
179
+ }
180
+ current = snapshotSessionFormatArtifact(migrated, `${migration.name} output`);
181
+ if (current.header.version !== migration.toVersion) throw new SessionFormatError(`${migration.name} returned v${current.header.version}; expected v${migration.toVersion}`);
182
+ try {
183
+ migration.validateTarget(current);
184
+ } catch (error) {
185
+ throwUnsupportedRefusal(migration, error);
186
+ }
187
+ }
188
+ current = snapshotSessionFormatArtifact(this.restoreCurrent(current), "current Session restoration");
189
+ this.assertCurrent(current);
190
+ return current;
191
+ }
192
+ migrateHeader(source) {
193
+ let current = snapshotSessionFormatHeader(source, "stored Session header");
194
+ for (const migration of this.plan(current.version)) {
195
+ let migrated;
196
+ try {
197
+ migrated = migration.migrateHeader(snapshotSessionFormatHeader(current, `${migration.name} header input`));
198
+ } catch (error) {
199
+ throwUnsupportedRefusal(migration, error, "Session header");
200
+ }
201
+ current = snapshotSessionFormatHeader(migrated, `${migration.name} header output`);
202
+ if (current.version !== migration.toVersion) throw new SessionFormatError(`${migration.name} header returned v${current.version}; expected v${migration.toVersion}`);
203
+ try {
204
+ migration.validateTargetHeader(current);
205
+ } catch (error) {
206
+ throwUnsupportedRefusal(migration, error, "Session header");
207
+ }
208
+ }
209
+ current = snapshotSessionFormatHeader(this.restoreCurrentHeader(current), "current Session header restoration");
210
+ if (current.version !== this.currentVersion) throw new SessionFormatError(`current Session header restorer returned v${current.version}; expected v${this.currentVersion}`);
211
+ return current;
212
+ }
213
+ assertCurrent(artifact) {
214
+ if (artifact.header.version !== this.currentVersion) throw new SessionFormatError(`current Session restorer returned v${artifact.header.version}; expected v${this.currentVersion}`);
215
+ }
216
+ };
217
+ function throwUnsupportedRefusal(migration, error, subject = "Session") {
218
+ if (error instanceof SessionFormatUnsupportedMigrationError) throw error;
219
+ const detail = error instanceof Error ? error.message : String(error);
220
+ throw new SessionFormatUnsupportedMigrationError(`${migration.name} refuses this format v${migration.fromVersion} ${subject}: ${detail}`, { cause: error });
221
+ }
222
+ //#endregion
223
+ //#region lib/types/catalog.js
224
+ /**
225
+ * Compile a build-static physical codec and adjacent migration catalog.
226
+ * @param options - complete codecs, migrations, current version, and restorer.
227
+ * @returns immutable physical dispatch and migration operations.
228
+ */
229
+ function createSessionFormatCatalog(options) {
230
+ const chain = createSessionFormatChain(options);
231
+ const codecs = /* @__PURE__ */ new Map();
232
+ for (const codec of options.codecs) {
233
+ const version = sessionFormatVersion(codec.version, "Session format codec version");
234
+ if (codecs.has(version)) throw new SessionFormatError(`Session format codec v${version} is duplicated`);
235
+ codecs.set(version, Object.freeze({ ...codec }));
236
+ }
237
+ for (let version = 0; version <= chain.currentVersion; version += 1) if (!codecs.has(version)) throw new SessionFormatError(`Session format codec v${version} is missing`);
238
+ if (codecs.size !== chain.currentVersion + 1) throw new SessionFormatError(`Session format codec v${[...codecs.keys()].find((version) => version > chain.currentVersion)} is newer than current v${chain.currentVersion}`);
239
+ function readHeader(headerValue) {
240
+ let storedVersion;
241
+ try {
242
+ storedVersion = inspectSessionFormatVersion(headerValue);
243
+ } catch (error) {
244
+ return malformed(chain.currentVersion, error);
245
+ }
246
+ if (storedVersion > chain.currentVersion) return Object.freeze({
247
+ status: "unsupported",
248
+ storedVersion,
249
+ targetVersion: chain.currentVersion,
250
+ reason: `stored Session uses newer format v${storedVersion}; this build writes v${chain.currentVersion}`
251
+ });
252
+ const codec = codecs.get(storedVersion);
253
+ /* v8 ignore next -- construction proves every supported version has exactly one codec. */
254
+ if (codec === void 0) return Object.freeze({
255
+ status: "unsupported",
256
+ storedVersion,
257
+ targetVersion: chain.currentVersion,
258
+ reason: `this build has no Session format codec for v${storedVersion}`
259
+ });
260
+ try {
261
+ const decoded = snapshotSessionFormatHeader(codec.decodeHeader(headerValue), `format v${storedVersion} header`);
262
+ const header = chain.migrateHeader(decoded);
263
+ return Object.freeze({
264
+ status: storedVersion === chain.currentVersion ? "current" : "migration-required",
265
+ storedVersion,
266
+ targetVersion: chain.currentVersion,
267
+ header
268
+ });
269
+ } catch (error) {
270
+ if (error instanceof SessionFormatUnsupportedMigrationError) return Object.freeze({
271
+ status: "unsupported",
272
+ storedVersion,
273
+ targetVersion: chain.currentVersion,
274
+ reason: error.message
275
+ });
276
+ return malformed(chain.currentVersion, error, storedVersion);
277
+ }
278
+ }
279
+ function artifactCodec(headerValue) {
280
+ const storedVersion = inspectSessionFormatVersion(headerValue);
281
+ if (storedVersion > chain.currentVersion) throw new SessionFormatUnsupportedMigrationError(`stored Session uses newer format v${storedVersion}; this build writes v${chain.currentVersion}`);
282
+ const codec = codecs.get(storedVersion);
283
+ /* v8 ignore next -- construction proves every supported version has exactly one codec. */
284
+ if (codec === void 0) throw new SessionFormatUnsupportedMigrationError(`this build has no Session format codec for v${storedVersion}`);
285
+ return {
286
+ storedVersion,
287
+ codec
288
+ };
289
+ }
290
+ function decodeArtifact(headerValue, rowValues) {
291
+ const { storedVersion, codec } = artifactCodec(headerValue);
292
+ return snapshotSessionFormatArtifact(codec.decodeArtifact(headerValue, rowValues), `format v${storedVersion} decoded artifact`);
293
+ }
294
+ function decodeRecoverableArtifact(headerValue, rowValues) {
295
+ const { storedVersion, codec } = artifactCodec(headerValue);
296
+ return snapshotSessionFormatArtifact(codec.decodeRecoverableArtifact(headerValue, rowValues), `format v${storedVersion} recoverable artifact`);
297
+ }
298
+ function encodeCurrent(artifact) {
299
+ if (inspectSessionFormatVersion(artifact.header) !== chain.currentVersion) throw new SessionFormatError(`encodeCurrent requires Session format v${chain.currentVersion}`);
300
+ const encoded = options.encodeCurrentArtifact(artifact);
301
+ const header = snapshotSessionFormatJson(encoded.header, "encoded current Session header");
302
+ const rows = Object.freeze(encoded.rows.map((row, index) => snapshotSessionFormatJson(row, `encoded current Session row ${index}`)));
303
+ if (inspectSessionFormatVersion(header) !== chain.currentVersion) throw new SessionFormatError("current Session codec returned a non-current header");
304
+ return Object.freeze({
305
+ header,
306
+ rows
307
+ });
308
+ }
309
+ return Object.freeze({
310
+ currentVersion: chain.currentVersion,
311
+ readHeader,
312
+ decodeArtifact,
313
+ decodeRecoverableArtifact,
314
+ migrate: chain.migrate.bind(chain),
315
+ encodeCurrent
316
+ });
317
+ }
318
+ function malformed(targetVersion, error, storedVersion) {
319
+ return Object.freeze({
320
+ status: "malformed",
321
+ ...storedVersion === void 0 ? {} : { storedVersion },
322
+ targetVersion,
323
+ reason: error instanceof Error ? error.message : String(error)
324
+ });
325
+ }
326
+ //#endregion
327
+ //#region lib/types/filename.js
328
+ /** Canonical raw log basename shared by every generation-addressed Session artifact. */
329
+ const CANONICAL_LOG_FILENAME = /^session(?:\.v([1-9][0-9]*))?\.jsonl$/u;
330
+ /**
331
+ * Name the raw JSONL log of one immutable Session format generation. Version
332
+ * zero keeps the original `session.jsonl`; every later generation carries a
333
+ * lowercase numeric `.vN` component before the `.jsonl` suffix.
334
+ * @param version - non-negative safe integer Session format version.
335
+ * @returns the canonical basename, without any compression suffix.
336
+ */
337
+ function sessionFormatLogFilename(version) {
338
+ const generation = sessionFormatVersion(version, "Session log generation version");
339
+ return generation === 0 ? "session.jsonl" : `session.v${generation}.jsonl`;
340
+ }
341
+ /**
342
+ * Read the generation named by one raw JSONL log basename. Temporary,
343
+ * uppercase, leading-zero, `.v0`, and compression-suffixed names are not
344
+ * canonical.
345
+ * @param filename - one basename from a Session directory or archive.
346
+ * @returns its Session format version, or `undefined` when the name is not canonical.
347
+ */
348
+ function parseSessionFormatLogFilename(filename) {
349
+ const match = CANONICAL_LOG_FILENAME.exec(filename);
350
+ if (match === null) return void 0;
351
+ if (match[1] === void 0) return 0;
352
+ const version = Number(match[1]);
353
+ return Number.isSafeInteger(version) ? version : void 0;
354
+ }
355
+ //#endregion
356
+ export { SessionFormatError, SessionFormatUnsupportedMigrationError, createSessionFormatCatalog, createSessionFormatChain, defineSessionFormatMigration, inspectSessionFormatVersion, isSessionFormatJsonObject, parseSessionFormatLogFilename, sessionFormatCount, sessionFormatLogFilename, sessionFormatSafeInteger, sessionFormatVersion, snapshotSessionFormatArtifact, snapshotSessionFormatHeader, snapshotSessionFormatJson };
@@ -0,0 +1,8 @@
1
+ import type { SessionFormatCatalog, SessionFormatCatalogOptions } from './types.ts';
2
+ /**
3
+ * Compile a build-static physical codec and adjacent migration catalog.
4
+ * @param options - complete codecs, migrations, current version, and restorer.
5
+ * @returns immutable physical dispatch and migration operations.
6
+ */
7
+ export declare function createSessionFormatCatalog(options: SessionFormatCatalogOptions): SessionFormatCatalog;
8
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1,14 @@
1
+ import type { SessionFormatChain, SessionFormatChainOptions, SessionFormatMigration } from './types.ts';
2
+ /**
3
+ * Validate and freeze one adjacent migration declaration.
4
+ * @param migration - named exact adjacent conversion.
5
+ * @returns immutable validated declaration.
6
+ */
7
+ export declare function defineSessionFormatMigration(migration: SessionFormatMigration): SessionFormatMigration;
8
+ /**
9
+ * Compile a unique, complete adjacent migration chain.
10
+ * @param options - current version, adjacent declarations, and current restorer.
11
+ * @returns immutable planner and whole-artifact runner.
12
+ */
13
+ export declare function createSessionFormatChain(options: SessionFormatChainOptions): SessionFormatChain;
14
+ //# sourceMappingURL=chain.d.ts.map
@@ -0,0 +1,9 @@
1
+ /** Error raised when a durable Session artifact cannot be restored or migrated losslessly. */
2
+ export declare class SessionFormatError extends Error {
3
+ readonly name: string;
4
+ }
5
+ /** A readable artifact whose released source policy has no supported migration. */
6
+ export declare class SessionFormatUnsupportedMigrationError extends SessionFormatError {
7
+ readonly name = "SessionFormatUnsupportedMigrationError";
8
+ }
9
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1,18 @@
1
+ /** Canonical raw log basename shared by every generation-addressed Session artifact. */
2
+ /**
3
+ * Name the raw JSONL log of one immutable Session format generation. Version
4
+ * zero keeps the original `session.jsonl`; every later generation carries a
5
+ * lowercase numeric `.vN` component before the `.jsonl` suffix.
6
+ * @param version - non-negative safe integer Session format version.
7
+ * @returns the canonical basename, without any compression suffix.
8
+ */
9
+ export declare function sessionFormatLogFilename(version: number): string;
10
+ /**
11
+ * Read the generation named by one raw JSONL log basename. Temporary,
12
+ * uppercase, leading-zero, `.v0`, and compression-suffixed names are not
13
+ * canonical.
14
+ * @param filename - one basename from a Session directory or archive.
15
+ * @returns its Session format version, or `undefined` when the name is not canonical.
16
+ */
17
+ export declare function parseSessionFormatLogFilename(filename: string): number | undefined;
18
+ //# sourceMappingURL=filename.d.ts.map
@@ -0,0 +1,8 @@
1
+ /** Pure adjacent whole-artifact Session format migration machinery. */
2
+ export * from './chain.ts';
3
+ export * from './catalog.ts';
4
+ export * from './error.ts';
5
+ export * from './filename.ts';
6
+ export * from './json.ts';
7
+ export * from './types.ts';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,56 @@
1
+ import type { SessionFormatArtifact, SessionFormatHeader, SessionFormatJsonValue } from './types.ts';
2
+ /**
3
+ * Test whether a value is a non-null, non-array object.
4
+ * @param value - candidate value.
5
+ * @returns whether the value is an object record.
6
+ */
7
+ export declare function isSessionFormatJsonObject(value: unknown): value is Record<string, unknown>;
8
+ /**
9
+ * Require a non-negative safe integer without the JSON-unstable negative zero.
10
+ * @param value - candidate count.
11
+ * @param label - diagnostic subject.
12
+ * @returns validated count.
13
+ */
14
+ export declare function sessionFormatCount(value: unknown, label: string): number;
15
+ /**
16
+ * Require a safe integer without the JSON-unstable negative zero.
17
+ * @param value - candidate integer.
18
+ * @param label - diagnostic subject.
19
+ * @returns validated integer.
20
+ */
21
+ export declare function sessionFormatSafeInteger(value: unknown, label: string): number;
22
+ /**
23
+ * Require a non-negative integral format version.
24
+ * @param value - candidate version.
25
+ * @param label - diagnostic subject.
26
+ * @returns validated version.
27
+ */
28
+ export declare function sessionFormatVersion(value: unknown, label?: string): number;
29
+ /**
30
+ * Read only the version required for directional dispatch.
31
+ * @param headerValue - untrusted physical header value.
32
+ * @returns validated stored version.
33
+ */
34
+ export declare function inspectSessionFormatVersion(headerValue: unknown): number;
35
+ /**
36
+ * Detach and deeply freeze a caller-supplied lossless JSON value.
37
+ * @param value - borrowed candidate.
38
+ * @param label - diagnostic subject.
39
+ * @returns an immutable detached JSON snapshot.
40
+ */
41
+ export declare function snapshotSessionFormatJson(value: unknown, label?: string): SessionFormatJsonValue;
42
+ /**
43
+ * Snapshot one complete artifact and validate its shared coordinates.
44
+ * @param artifact - borrowed logical artifact.
45
+ * @param label - diagnostic subject.
46
+ * @returns immutable detached artifact.
47
+ */
48
+ export declare function snapshotSessionFormatArtifact(artifact: SessionFormatArtifact, label?: string): SessionFormatArtifact;
49
+ /**
50
+ * Snapshot one logical header without inspecting an event body.
51
+ * @param header - borrowed logical header.
52
+ * @param label - diagnostic subject.
53
+ * @returns immutable detached header.
54
+ */
55
+ export declare function snapshotSessionFormatHeader(header: SessionFormatHeader, label?: string): SessionFormatHeader;
56
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1,125 @@
1
+ /** Scalar value admitted at the durable Session JSON boundary. */
2
+ export type SessionFormatJsonPrimitive = null | boolean | number | string;
3
+ /** Lossless JSON value admitted at the durable Session boundary. */
4
+ export type SessionFormatJsonValue = SessionFormatJsonPrimitive | readonly SessionFormatJsonValue[] | SessionFormatJsonObject;
5
+ /** Lossless JSON object admitted at the durable Session boundary. */
6
+ export interface SessionFormatJsonObject {
7
+ readonly [key: string]: SessionFormatJsonValue;
8
+ }
9
+ /** Logical Session metadata shared by supported historical and current formats. */
10
+ export interface SessionFormatHeader extends SessionFormatJsonObject {
11
+ readonly version: number;
12
+ readonly id: string;
13
+ readonly createdAt: number;
14
+ readonly cwd?: string;
15
+ readonly parentSession?: string;
16
+ readonly isSeeded: boolean;
17
+ readonly origin?: 'subagent';
18
+ readonly delegationDepth: number;
19
+ readonly agentPreset?: string;
20
+ }
21
+ /** One decoded logical Session event. */
22
+ export interface SessionFormatEvent extends SessionFormatJsonObject {
23
+ readonly type: string;
24
+ readonly seq: number;
25
+ readonly time: number;
26
+ readonly data: SessionFormatJsonValue;
27
+ }
28
+ /** One detached complete logical Session artifact. */
29
+ export interface SessionFormatArtifact {
30
+ readonly header: SessionFormatHeader;
31
+ /** Exact inherited prefix length, available only after a body read. */
32
+ readonly inheritedEventCount: number;
33
+ readonly events: readonly SessionFormatEvent[];
34
+ }
35
+ /** One independently maintained adjacent whole-artifact migration. */
36
+ export interface SessionFormatMigration {
37
+ readonly name: string;
38
+ readonly fromVersion: number;
39
+ readonly toVersion: number;
40
+ /** Convert one header without reading event bodies. */
41
+ migrateHeader(header: SessionFormatHeader): SessionFormatHeader;
42
+ /** Convert one detached complete artifact to exactly {@link toVersion}. */
43
+ migrate(artifact: SessionFormatArtifact): SessionFormatArtifact;
44
+ /** Refuse any artifact that the adjacent target writer cannot emit. */
45
+ validateTarget(artifact: SessionFormatArtifact): void;
46
+ /** Refuse any header that the adjacent target writer cannot emit. */
47
+ validateTargetHeader(header: SessionFormatHeader): void;
48
+ }
49
+ /** Inputs that compile the unique complete migration chain. */
50
+ export interface SessionFormatChainOptions {
51
+ readonly currentVersion: number;
52
+ readonly migrations: readonly SessionFormatMigration[];
53
+ /** Restore and validate a detached current artifact through the current parser. */
54
+ readonly restoreCurrent: (artifact: SessionFormatArtifact) => SessionFormatArtifact;
55
+ /** Restore and validate a detached current header without reading event bodies. */
56
+ readonly restoreCurrentHeader: (header: SessionFormatHeader) => SessionFormatHeader;
57
+ }
58
+ /** Pure adjacent planner and whole-artifact migration runner. */
59
+ export interface SessionFormatChain {
60
+ readonly currentVersion: number;
61
+ /** Return the complete ordered plan from one supported stored version. */
62
+ plan(fromVersion: number): readonly SessionFormatMigration[];
63
+ /** Restore current input directly or migrate old input entirely in memory. */
64
+ migrate(artifact: SessionFormatArtifact): SessionFormatArtifact;
65
+ /** Convert only a supported header to the current logical representation. */
66
+ migrateHeader(header: SessionFormatHeader): SessionFormatHeader;
67
+ }
68
+ /** Physical JSON records emitted by one format-specific codec. */
69
+ export interface EncodedSessionFormatArtifact {
70
+ readonly header: SessionFormatJsonObject;
71
+ readonly rows: readonly SessionFormatJsonObject[];
72
+ }
73
+ /** Options that affect only physical row layout, never logical contents. */
74
+ export interface SessionFormatEncodeOptions {
75
+ readonly packChunks: boolean;
76
+ }
77
+ /** Pure physical JSON codec frozen with one released Session format. */
78
+ export interface SessionFormatCodec {
79
+ readonly version: number;
80
+ /** Decode one physical header into body-independent logical metadata. */
81
+ decodeHeader(value: unknown): SessionFormatHeader;
82
+ /** Decode one complete physical header and row sequence into logical events. */
83
+ decodeArtifact(headerValue: unknown, rowValues: readonly unknown[]): SessionFormatArtifact;
84
+ /** Decode the row-atomic recoverable prefix used by crash-tail repair. */
85
+ decodeRecoverableArtifact(headerValue: unknown, rowValues: readonly unknown[]): SessionFormatArtifact;
86
+ }
87
+ /** Header-only classification that never inspects event rows. */
88
+ export type SessionFormatHeaderReadResult = {
89
+ readonly status: 'current' | 'migration-required';
90
+ readonly storedVersion: number;
91
+ readonly targetVersion: number;
92
+ /** Latest logical header. The exact inherited cut requires a body read. */
93
+ readonly header: SessionFormatHeader;
94
+ } | {
95
+ readonly status: 'unsupported';
96
+ readonly storedVersion: number;
97
+ readonly targetVersion: number;
98
+ readonly reason: string;
99
+ } | {
100
+ readonly status: 'malformed';
101
+ readonly storedVersion?: number;
102
+ readonly targetVersion: number;
103
+ readonly reason: string;
104
+ };
105
+ /** Inputs for a build-static physical codec and migration catalog. */
106
+ export interface SessionFormatCatalogOptions extends SessionFormatChainOptions {
107
+ readonly codecs: readonly SessionFormatCodec[];
108
+ /** Encode one already-restored current artifact through its format-specific writer. */
109
+ readonly encodeCurrentArtifact: (artifact: SessionFormatArtifact) => EncodedSessionFormatArtifact;
110
+ }
111
+ /** Build-static physical dispatch and adjacent migration catalog. */
112
+ export interface SessionFormatCatalog {
113
+ readonly currentVersion: number;
114
+ /** Classify and translate one header without reading event rows. */
115
+ readHeader(headerValue: unknown): SessionFormatHeaderReadResult;
116
+ /** Dispatch a complete physical JSON artifact through its frozen version codec. */
117
+ decodeArtifact(headerValue: unknown, rowValues: readonly unknown[]): SessionFormatArtifact;
118
+ /** Dispatch a physical artifact through its released row-prefix recovery rules. */
119
+ decodeRecoverableArtifact(headerValue: unknown, rowValues: readonly unknown[]): SessionFormatArtifact;
120
+ /** Restore current input directly or run all required adjacent migrations in memory. */
121
+ migrate(artifact: SessionFormatArtifact): SessionFormatArtifact;
122
+ /** Encode one current artifact that `migrate` returned or a live Session produced; it is not re-validated here. */
123
+ encodeCurrent(artifact: SessionFormatArtifact): EncodedSessionFormatArtifact;
124
+ }
125
+ //# sourceMappingURL=types.d.ts.map
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@xneog/dsh-session-format",
3
+ "description": "Pure adjacent whole-artifact Session format migration machinery",
4
+ "version": "0.1.3-alpha.1",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/gomes007-alt/xneog-harness.git",
11
+ "directory": "packages/session/session-format"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./src/*": "./src/*",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "lib/index.js",
26
+ "lib/types/**/*.d.ts"
27
+ ],
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@xneog/dsh-util-values": "^0.1.3-alpha.1"
31
+ },
32
+ "peerDependencies": {
33
+ "@xneog/cordis": "^4.0.2"
34
+ },
35
+ "devDependencies": {
36
+ "@xneog/cordis": "^4.0.2"
37
+ }
38
+ }