@llblab/pi-kit 0.37.1 → 0.38.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/CHANGELOG.md +10 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-claude-usage/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-claude-usage/README.md +3 -3
- package/node_modules/@llblab/pi-claude-usage/lib/status.ts +1 -1
- package/node_modules/@llblab/pi-claude-usage/lib/usage-store.ts +2 -2
- package/node_modules/@llblab/pi-claude-usage/package.json +1 -1
- package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-clean-room/index.ts +39 -9
- package/node_modules/@llblab/pi-clean-room/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -3
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +679 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-state-flow/README.md +30 -14
- package/node_modules/@llblab/pi-state-flow/api/modules.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/api/modules.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/api/modules.js +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +7 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +77 -36
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +26 -27
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +13 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +27 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +144 -46
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +14 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +60 -50
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1049 -310
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +15 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +7 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +186 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.js +158 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.js +108 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/planes.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/planes.js +84 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +6 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +33 -16
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +21 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +53 -13
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +5 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +10 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +84 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +570 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/selection.d.ts +10 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/selection.js +32 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.d.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.js +187 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +98 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +398 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +7 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +55 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +67 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +129 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +26 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +61 -19
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +14 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +59 -22
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +111 -10
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +728 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +10 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +50 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +40 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +247 -43
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +41 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +132 -39
- package/node_modules/@llblab/pi-state-flow/dist/package.json +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +28 -16
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +12 -8
- package/node_modules/@llblab/pi-state-flow/docs/README.md +9 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +76 -43
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +9 -1
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -0
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +23 -19
- package/node_modules/@llblab/pi-state-flow/docs/protocol-composition-decision.md +66 -0
- package/node_modules/@llblab/pi-state-flow/docs/public-module-contract.md +73 -0
- package/node_modules/@llblab/pi-state-flow/docs/scope-selection-contract.md +814 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-compatibility-decision.md +212 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-extensibility-decision.md +75 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-handoff-contract.md +174 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-memory-evaluation.md +166 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-module-contract.md +84 -0
- package/node_modules/@llblab/pi-state-flow/docs/single-scope-interface-contract.md +201 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +22 -1
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -18
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +68 -31
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +21 -22
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +14 -1
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +142 -46
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +14 -17
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +53 -44
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +897 -297
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -7
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +10 -9
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +189 -0
- package/node_modules/@llblab/pi-state-flow/lib/model-interface.ts +136 -0
- package/node_modules/@llblab/pi-state-flow/lib/module-registry.ts +119 -0
- package/node_modules/@llblab/pi-state-flow/lib/operation.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/lib/planes.ts +98 -0
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +37 -16
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +59 -18
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +5 -2
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +10 -1
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +574 -21
- package/node_modules/@llblab/pi-state-flow/lib/selection.ts +39 -0
- package/node_modules/@llblab/pi-state-flow/lib/semantic-module.ts +239 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +387 -3
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +45 -4
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +156 -16
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +69 -28
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +65 -31
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +770 -18
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +58 -20
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +267 -50
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +184 -52
- package/node_modules/@llblab/pi-state-flow/package.json +6 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +28 -16
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +12 -8
- package/package.json +4 -4
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
// Domain: exact file-cohort publication, current-only recovery, and cooperating worktree exclusion.
|
|
2
2
|
// Excludes: temporal algebra, Pi lifecycle, Git objects/remotes, and backend fallback policy.
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { dirname, relative, resolve } from "node:path";
|
|
6
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
7
7
|
import { setTimeout as delay } from "node:timers/promises";
|
|
8
|
-
import { assertOwnedFileUpdates, captureTemporalFileBases, parseScopeProvenance, parseScopeStream, restoreDurableFileBases, serializeScopeMetadata, sessionRuntimePaths, temporalScopePaths, temporalStateFileUpdates, writeOwnedFileUpdates, } from "./durable.js";
|
|
8
|
+
import { assertOwnedFileUpdates, captureTemporalFileBases, captureOwnedFileBases, captureOwnedFilePresence, classifyScopeStream, parseScopeProvenance, parseScopeStream, restoreDurableFileBases, serializeScopeMetadata, serializeScopeStream, sessionRuntimePaths, temporalScopePaths, temporalStateFileUpdates, writeOwnedFileUpdates, } from "./durable.js";
|
|
9
|
+
import { isInterpretableArtifactProvenance, parseArtifactProvenanceRegistry } from "./artifact.js";
|
|
9
10
|
import { MAX_HISTORY_LIMIT } from "./history.js";
|
|
10
|
-
import { hashJson, sameJson } from "./json.js";
|
|
11
|
-
import {
|
|
11
|
+
import { canonicalJson, hashJson, isJsonValue, isObject, sameJson } from "./json.js";
|
|
12
|
+
import { prepareSerializedScopeMigration } from "./migration.js";
|
|
13
|
+
import { RevisionUnavailableError, isFileRevision, parseSessionRuntime, prepareSessionPolicyMigration, captureLifecycleSnapshot, capturePrivateCausalBasis, serializeSessionRuntime } from "./snapshot.js";
|
|
12
14
|
export { isFileRevision } from "./snapshot.js";
|
|
13
|
-
import { validateTemporalState } from "./temporal.js";
|
|
15
|
+
import { validateTemporalState, selectTemporalState, restoreSelectedTemporalState, temporalCausalWitnesses, constrainTemporalState, advanceTemporalState, createSelectedTemporalState, adoptSelectedTemporalStreams, validateScopeLineage, scopeLineageMatches, captureTemporalWitnesses, readTemporalView } from "./temporal.js";
|
|
16
|
+
import { captureScopeSelection } from "./selection.js";
|
|
17
|
+
import { validateRetainedModules } from "./semantic-module.js";
|
|
14
18
|
const SCOPES = ["global", "cwd", "session"];
|
|
19
|
+
/** Opaque CAS capture stays byte-exact; semantic consumers alone require lossless text. */
|
|
20
|
+
export function temporalFileContents(base) {
|
|
21
|
+
return new Map(base.files.map((file) => [file.path, file.bytes === undefined ? file.content
|
|
22
|
+
: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(file.bytes)]));
|
|
23
|
+
}
|
|
15
24
|
export function assertStorageDirectory(path) {
|
|
16
25
|
const root = resolve(path);
|
|
17
26
|
const parent = dirname(root);
|
|
@@ -178,6 +187,7 @@ export async function withStorageTransaction(repositoryRoot, action, signal, wai
|
|
|
178
187
|
const root = resolve(repositoryRoot);
|
|
179
188
|
return withFilePublicationLock(resolve(root, ".state-flow-publication.lock"), async () => {
|
|
180
189
|
let active = true;
|
|
190
|
+
let selectedPublicationUsed = false;
|
|
181
191
|
const guard = (requestedRoot) => {
|
|
182
192
|
if (!active)
|
|
183
193
|
throw new Error("State Flow storage transaction has ended");
|
|
@@ -190,6 +200,80 @@ export async function withStorageTransaction(repositoryRoot, action, signal, wai
|
|
|
190
200
|
guard(requestedRoot);
|
|
191
201
|
return { files: captureTemporalFileBases(cwd, sessionId, root, sessionKey) };
|
|
192
202
|
},
|
|
203
|
+
prepareSelected: (cwd, sessionId, selection, requestedRoot, sessionKey = sessionId) => {
|
|
204
|
+
guard(requestedRoot);
|
|
205
|
+
return prepareSelectedFileCohort(cwd, sessionId, selection, root, sessionKey);
|
|
206
|
+
},
|
|
207
|
+
observeSelectedShared: (cwd, selection, requestedRoot) => {
|
|
208
|
+
guard(requestedRoot);
|
|
209
|
+
return observeSelectedShared(cwd, selection, root);
|
|
210
|
+
},
|
|
211
|
+
persistSelectedPassive: (cwd, sessionId, base, requestedRoot, sessionKey = sessionId) => {
|
|
212
|
+
guard(requestedRoot);
|
|
213
|
+
if (selectedPublicationUsed)
|
|
214
|
+
throw new Error("Selected State Flow publication capability was already used");
|
|
215
|
+
selectedPublicationUsed = true;
|
|
216
|
+
return persistSelectedPassive(cwd, sessionId, base, root, sessionKey);
|
|
217
|
+
},
|
|
218
|
+
hasSelectedCurrentAuthority: (cwd, sessionId, selection, requestedRoot, sessionKey = sessionId) => {
|
|
219
|
+
guard(requestedRoot);
|
|
220
|
+
return captureSelectedLifecycle(cwd, sessionId, captureScopeSelection(selection), root, sessionKey).lifecycle !== undefined;
|
|
221
|
+
},
|
|
222
|
+
inspectSelectedLifecycle: (cwd, sessionId, selection, requestedRoot, sessionKey = sessionId) => {
|
|
223
|
+
guard(requestedRoot);
|
|
224
|
+
return captureSelectedLifecycle(cwd, sessionId, captureScopeSelection(selection), root, sessionKey).lifecycle;
|
|
225
|
+
},
|
|
226
|
+
prepareSelectedCurrent: (cwd, sessionId, selection, historyLimit, requestedRoot, sessionKey = sessionId, expectedLifecycle) => {
|
|
227
|
+
guard(requestedRoot);
|
|
228
|
+
return prepareSelectedCurrent(cwd, sessionId, selection, historyLimit, root, sessionKey, expectedLifecycle);
|
|
229
|
+
},
|
|
230
|
+
acceptSelectedCurrent: (cwd, sessionId, request, base, requestedRoot, sessionKey = sessionId) => {
|
|
231
|
+
guard(requestedRoot);
|
|
232
|
+
if (selectedPublicationUsed)
|
|
233
|
+
throw new Error("Selected State Flow publication capability was already used");
|
|
234
|
+
selectedPublicationUsed = true;
|
|
235
|
+
const current = prepareSelectedCurrent(cwd, sessionId, request.selection, request.historyLimit, root, sessionKey);
|
|
236
|
+
assertSelectedFileCohortBasis(base, current);
|
|
237
|
+
if ((request.lifecycle?.config.mode ?? current.lifecycle.runtime.config.mode) === "off")
|
|
238
|
+
throw new Error("Off cannot accept current State Flow memory");
|
|
239
|
+
const expected = selectedReplaySuccessor(current.view, request.historyLimit, request.transition, request.moduleAdditions);
|
|
240
|
+
return publishSelectedSuccessor(cwd, sessionId, current, expected, request.historyLimit, request.lifecycle, root, sessionKey, request.provenance);
|
|
241
|
+
},
|
|
242
|
+
publishSelected: (cwd, sessionId, request, base, requestedRoot, sessionKey = sessionId) => {
|
|
243
|
+
guard(requestedRoot);
|
|
244
|
+
if (selectedPublicationUsed)
|
|
245
|
+
throw new Error("Selected State Flow publication capability was already used");
|
|
246
|
+
selectedPublicationUsed = true;
|
|
247
|
+
return publishSelectedFileCohort(cwd, sessionId, request, base, root, sessionKey);
|
|
248
|
+
},
|
|
249
|
+
initializeSelectedSharedOrigin: (cwd, sessionId, request, requestedRoot, sessionKey = sessionId) => {
|
|
250
|
+
guard(requestedRoot);
|
|
251
|
+
if (selectedPublicationUsed)
|
|
252
|
+
throw new Error("Selected State Flow publication capability was already used");
|
|
253
|
+
selectedPublicationUsed = true;
|
|
254
|
+
return acceptSelectedOrigin(cwd, sessionId, { ...request, kind: "shared-new" }, root, sessionKey);
|
|
255
|
+
},
|
|
256
|
+
acquireSelectedSession: (cwd, sessionId, request, requestedRoot, sessionKey = sessionId) => {
|
|
257
|
+
guard(requestedRoot);
|
|
258
|
+
if (selectedPublicationUsed)
|
|
259
|
+
throw new Error("Selected State Flow publication capability was already used");
|
|
260
|
+
selectedPublicationUsed = true;
|
|
261
|
+
return acceptSelectedOrigin(cwd, sessionId, request, root, sessionKey);
|
|
262
|
+
},
|
|
263
|
+
deferSelectedFork: (cwd, sessionId, request, requestedRoot, sessionKey = sessionId) => {
|
|
264
|
+
guard(requestedRoot);
|
|
265
|
+
if (selectedPublicationUsed)
|
|
266
|
+
throw new Error("Selected State Flow publication capability was already used");
|
|
267
|
+
selectedPublicationUsed = true;
|
|
268
|
+
return acceptSelectedOrigin(cwd, sessionId, { ...request, kind: "shared-deferred" }, root, sessionKey);
|
|
269
|
+
},
|
|
270
|
+
copySelectedFork: (cwd, sessionId, request, requestedRoot, sessionKey = sessionId) => {
|
|
271
|
+
guard(requestedRoot);
|
|
272
|
+
if (selectedPublicationUsed)
|
|
273
|
+
throw new Error("Selected State Flow publication capability was already used");
|
|
274
|
+
selectedPublicationUsed = true;
|
|
275
|
+
return acceptSelectedOrigin(cwd, sessionId, request, root, sessionKey);
|
|
276
|
+
},
|
|
193
277
|
publish: (cwd, sessionId, view, scopes, base, requestedRoot, runtime, sessionKey, provenance, runtimeOnly) => {
|
|
194
278
|
guard(requestedRoot);
|
|
195
279
|
return publishLockedTemporalStateToFiles(cwd, sessionId, view, scopes, base, root, runtime, sessionKey, provenance, runtimeOnly);
|
|
@@ -213,14 +297,21 @@ export function assertTemporalFileBase(expected, current) {
|
|
|
213
297
|
/** Plan exact canonical updates; lifecycle-only writes exclude semantic files and provenance. */
|
|
214
298
|
export function planTemporalPublication(cwd, sessionId, view, scopes, current, root, runtime, runtimeOnly = false, sessionKey = sessionId, provenance) {
|
|
215
299
|
const candidates = temporalStateFileUpdates(cwd, sessionId, view, scopes, root, sessionKey);
|
|
216
|
-
const
|
|
300
|
+
const decoded = temporalFileContents(current);
|
|
301
|
+
const files = new Map(current.files.map((file) => [file.path, { ...file, content: decoded.get(file.path) }]));
|
|
217
302
|
if (runtimeOnly && (scopes.length !== 0 || provenance !== undefined))
|
|
218
303
|
throw new Error("Runtime-only publication cannot write semantic scopes or artifact provenance");
|
|
219
304
|
const changedScopes = [];
|
|
305
|
+
const migrations = new Map();
|
|
220
306
|
for (const scope of SCOPES) {
|
|
221
307
|
const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
222
308
|
const previous = parseScopeStream(files.get(paths.checkpoint).content, files.get(paths.patches).content, scope, scope === "cwd" ? cwd : undefined, files.get(paths.meta).content);
|
|
223
|
-
if (runtimeOnly
|
|
309
|
+
if (!runtimeOnly && scopes.includes(scope) && previous !== undefined && JSON.parse(files.get(paths.meta).content).version !== (view.scopes[scope].modules === undefined ? 3 : 4)) {
|
|
310
|
+
migrations.set(scope, prepareSerializedScopeMigration({
|
|
311
|
+
checkpoint: files.get(paths.checkpoint).bytes, patches: files.get(paths.patches).bytes, meta: files.get(paths.meta).bytes,
|
|
312
|
+
}, scope, scope === "cwd" ? cwd : undefined, view.scopes[scope].modules));
|
|
313
|
+
}
|
|
314
|
+
if (runtimeOnly || (previous !== undefined && !migrations.has(scope) && sameJson(previous, view.scopes[scope])))
|
|
224
315
|
continue;
|
|
225
316
|
if (!scopes.includes(scope))
|
|
226
317
|
throw new Error(`Temporal scope update omitted a changed stream: ${scope}`);
|
|
@@ -236,7 +327,22 @@ export function planTemporalPublication(cwd, sessionId, view, scopes, current, r
|
|
|
236
327
|
if (!changedScopes.includes(scope)
|
|
237
328
|
&& (provenance === undefined || sameJson(parseScopeProvenance(currentFile.content, paths.meta), registry)))
|
|
238
329
|
continue;
|
|
239
|
-
const
|
|
330
|
+
const migration = migrations.get(scope);
|
|
331
|
+
const unchangedRegistry = sameJson(parseScopeProvenance(currentFile.content, paths.meta), registry);
|
|
332
|
+
let content = serializeScopeMetadata(unchangedRegistry ? undefined : registry, view.scopes[scope], scope, scope === "cwd" ? cwd : undefined, migration?.target.meta ?? currentFile.content);
|
|
333
|
+
if (migration && !unchangedRegistry) {
|
|
334
|
+
const raw = JSON.parse(migration.target.meta);
|
|
335
|
+
const next = JSON.parse(content);
|
|
336
|
+
const prior = parseScopeProvenance(currentFile.content, paths.meta);
|
|
337
|
+
if (isObject(raw.artifacts) && isObject(next.artifacts)) {
|
|
338
|
+
for (const [path, value] of Object.entries(raw.artifacts))
|
|
339
|
+
if (prior[path]?.malformed) {
|
|
340
|
+
if (!Object.hasOwn(next.artifacts, path))
|
|
341
|
+
Object.defineProperty(next.artifacts, path, { value, enumerable: true });
|
|
342
|
+
}
|
|
343
|
+
content = `${canonicalJson(next)}\n`;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
240
346
|
if (currentFile.content !== content)
|
|
241
347
|
provenanceUpdates.push({ path: paths.meta, content });
|
|
242
348
|
}
|
|
@@ -277,7 +383,7 @@ function fileRevision(base, root) {
|
|
|
277
383
|
return `file:${hashJson({ root: resolve(root), files: base.files.map(({ path, identity }) => [relative(root, path), identity]) })}`;
|
|
278
384
|
}
|
|
279
385
|
function decodeFileCohort(cwd, sessionId, root, base, sessionKey = sessionId) {
|
|
280
|
-
const files =
|
|
386
|
+
const files = temporalFileContents(base);
|
|
281
387
|
const scopes = {};
|
|
282
388
|
for (const scope of SCOPES) {
|
|
283
389
|
const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
@@ -299,6 +405,578 @@ function decodeFileCohort(cwd, sessionId, root, base, sessionKey = sessionId) {
|
|
|
299
405
|
};
|
|
300
406
|
return { runtime, view, provenance };
|
|
301
407
|
}
|
|
408
|
+
/** Reject excluded owners before their values/getters; metadata values remain runtime-owned evidence, not source authentication. */
|
|
409
|
+
export function captureSelectedProvenanceUpdates(value, selection) {
|
|
410
|
+
if (value === undefined)
|
|
411
|
+
return undefined;
|
|
412
|
+
if (!isObject(value))
|
|
413
|
+
throw new Error("Selected State Flow provenance updates must be an object");
|
|
414
|
+
for (const scope of Object.keys(value))
|
|
415
|
+
if (!selection.includes(scope))
|
|
416
|
+
throw new Error(`State Flow ${scope} provenance owner is excluded`);
|
|
417
|
+
if (!isJsonValue(value))
|
|
418
|
+
throw new Error("Selected State Flow provenance updates must be finite JSON");
|
|
419
|
+
const result = {};
|
|
420
|
+
for (const scope of selection)
|
|
421
|
+
if (Object.hasOwn(value, scope)) {
|
|
422
|
+
const registry = parseArtifactProvenanceRegistry(value[scope]);
|
|
423
|
+
if (!sameJson(registry, value[scope]) || Object.values(registry).some((entry) => entry.malformed))
|
|
424
|
+
throw new Error("Invalid selected State Flow provenance updates");
|
|
425
|
+
if (Object.keys(registry).length > 0)
|
|
426
|
+
result[scope] = structuredClone(registry);
|
|
427
|
+
}
|
|
428
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
429
|
+
}
|
|
430
|
+
export function captureSelectedModuleAdditions(value, selection) {
|
|
431
|
+
if (value === undefined)
|
|
432
|
+
return undefined;
|
|
433
|
+
if (!isObject(value))
|
|
434
|
+
throw new Error("Invalid selected State Flow module additions");
|
|
435
|
+
const keys = Reflect.ownKeys(value);
|
|
436
|
+
if (keys.some(key => typeof key !== "string" || !selection.includes(key)))
|
|
437
|
+
throw new Error("Selected State Flow module owner is excluded");
|
|
438
|
+
const captured = {};
|
|
439
|
+
for (const key of keys) {
|
|
440
|
+
const property = Object.getOwnPropertyDescriptor(value, key);
|
|
441
|
+
if (!property.enumerable || !("value" in property))
|
|
442
|
+
throw new Error("Selected State Flow module additions require detached data");
|
|
443
|
+
const modules = property.value;
|
|
444
|
+
validateRetainedModules(modules);
|
|
445
|
+
if (Object.keys(modules).length === 0)
|
|
446
|
+
throw new Error("Empty selected State Flow module additions");
|
|
447
|
+
captured[key] = structuredClone(modules);
|
|
448
|
+
}
|
|
449
|
+
return Object.keys(captured).length === 0 ? undefined : captured;
|
|
450
|
+
}
|
|
451
|
+
function observeSelectedShared(cwd, selection, root) {
|
|
452
|
+
const wanted = captureScopeSelection(selection);
|
|
453
|
+
if (wanted.includes("session"))
|
|
454
|
+
throw new Error("Shared State Flow observation cannot select Session");
|
|
455
|
+
const layouts = wanted.map(scope => ({ scope, paths: temporalScopePaths(cwd, "", scope, root) }));
|
|
456
|
+
if (captureOwnedFilePresence(layouts.map(({ paths }) => resolve(paths.directory, "state.json")), root).some(file => file.present)) {
|
|
457
|
+
throw new Error("Unsupported State Flow selected semantic layout");
|
|
458
|
+
}
|
|
459
|
+
const basis = { files: captureOwnedFileBases(layouts.flatMap(({ paths }) => [paths.checkpoint, paths.patches, paths.meta]), root) };
|
|
460
|
+
const sources = temporalFileContents(basis);
|
|
461
|
+
const streams = {};
|
|
462
|
+
const provenance = {};
|
|
463
|
+
for (const { scope, paths } of layouts) {
|
|
464
|
+
const bodies = [sources.get(paths.checkpoint), sources.get(paths.patches), sources.get(paths.meta)];
|
|
465
|
+
if (bodies.every(body => body === undefined))
|
|
466
|
+
continue;
|
|
467
|
+
if (bodies.some(body => body === undefined))
|
|
468
|
+
throw new Error(`Incomplete selected State Flow ${scope} observation`);
|
|
469
|
+
streams[scope] = parseScopeStream(bodies[0], bodies[1], scope, scope === "cwd" ? cwd : undefined, bodies[2]);
|
|
470
|
+
provenance[scope] = parseScopeProvenance(bodies[2], paths.meta);
|
|
471
|
+
}
|
|
472
|
+
const present = wanted.filter(scope => streams[scope] !== undefined);
|
|
473
|
+
return { selection: wanted, provenance, ...(present.length === 0 ? {} : {
|
|
474
|
+
view: adoptSelectedTemporalStreams(streams, captureScopeSelection(present), `observation:${randomUUID()}`, MAX_HISTORY_LIMIT),
|
|
475
|
+
}) };
|
|
476
|
+
}
|
|
477
|
+
/** Await a fresh guarded transaction; no private initialization or fork acquisition is admitted. */
|
|
478
|
+
export async function publishSelectedTemporalStateToFiles(cwd, sessionId, request, base, root, sessionKey = sessionId, signal) {
|
|
479
|
+
return withStorageTransaction(root, (transaction) => transaction.publishSelected(cwd, sessionId, request, base, root, sessionKey), signal);
|
|
480
|
+
}
|
|
481
|
+
function publishSelectedFileCohort(cwd, sessionId, request, base, root, sessionKey) {
|
|
482
|
+
const current = prepareSelectedFileCohort(cwd, sessionId, base.view.selection, root, sessionKey);
|
|
483
|
+
if (!current)
|
|
484
|
+
throw new Error("Selected State Flow source authority is unavailable");
|
|
485
|
+
assertSelectedFileCohortBasis(base, current);
|
|
486
|
+
const expected = selectedReplaySuccessor(current.view, request.historyLimit, request.transition, request.moduleAdditions);
|
|
487
|
+
if (!sameJson(expected, request.view))
|
|
488
|
+
throw new Error("Selected State Flow output is not the admitted replay successor");
|
|
489
|
+
return publishSelectedSuccessor(cwd, sessionId, current, expected, request.historyLimit, request.lifecycle, root, sessionKey, request.provenance);
|
|
490
|
+
}
|
|
491
|
+
/** Strictly lifecycle-only; a mode choice cannot repair, convert or refresh semantic authority. */
|
|
492
|
+
function persistSelectedPassive(cwd, sessionId, base, root, sessionKey) {
|
|
493
|
+
const paths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
494
|
+
const lifecyclePaths = new Set([paths.config, paths.runtime]);
|
|
495
|
+
const lifecycleBasis = { files: base.basis.files.filter(file => lifecyclePaths.has(file.path)) };
|
|
496
|
+
const current = prepareSelectedFileCohort(cwd, sessionId, base.view.selection, root, sessionKey, lifecycleBasis);
|
|
497
|
+
if (!current)
|
|
498
|
+
throw new Error("Selected State Flow source authority is unavailable");
|
|
499
|
+
assertSelectedFileCohortBasis(base, current);
|
|
500
|
+
const source = current.lifecycle.runtime.meta;
|
|
501
|
+
const snapshot = captureLifecycleSnapshot({ config: { mode: "passive" }, meta: { step: source.step,
|
|
502
|
+
...Object.fromEntries(["specification", "bootstrap", "validation"].filter(key => Object.hasOwn(source, key)).map(key => [key, source[key]])) } });
|
|
503
|
+
return publishSelectedSuccessor(cwd, sessionId, current, current.view, MAX_HISTORY_LIMIT, snapshot, root, sessionKey, undefined, true);
|
|
504
|
+
}
|
|
505
|
+
function selectedReplaySuccessor(view, historyLimit, transition, additions) {
|
|
506
|
+
const current = constrainTemporalState(view, historyLimit);
|
|
507
|
+
const admitted = captureSelectedModuleAdditions(additions, current.selection);
|
|
508
|
+
if (admitted !== undefined) {
|
|
509
|
+
for (const [owner, modules] of Object.entries(admitted)) {
|
|
510
|
+
const scope = owner, stream = current.scopes[scope];
|
|
511
|
+
for (const id of Object.keys(modules)) {
|
|
512
|
+
if (Object.hasOwn(stream.modules ?? {}, id))
|
|
513
|
+
throw new Error("Selected State Flow module descriptor already exists");
|
|
514
|
+
if (!transition?.transitions.some(item => item.scope === scope && Object.hasOwn(item.patch, id))) {
|
|
515
|
+
throw new Error("Selected State Flow module admission requires a content transition");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
stream.modules = { ...stream.modules, ...modules };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (transition === undefined)
|
|
522
|
+
return current;
|
|
523
|
+
if (!isObject(transition))
|
|
524
|
+
throw new Error("Invalid selected State Flow replay request");
|
|
525
|
+
const next = advanceTemporalState(current, transition.transitions, transition.id, historyLimit);
|
|
526
|
+
for (const [owner, modules] of Object.entries(admitted ?? {})) {
|
|
527
|
+
const content = readTemporalView(next, 0, owner, historyLimit);
|
|
528
|
+
for (const id of Object.keys(modules)) {
|
|
529
|
+
const value = content[id];
|
|
530
|
+
if (!isObject(value) || Object.keys(value).length === 0)
|
|
531
|
+
throw new Error("Selected State Flow module admission requires nonempty content");
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return next;
|
|
535
|
+
}
|
|
536
|
+
/** Merge known owned evidence without curating unbound or forward-compatible metadata. */
|
|
537
|
+
function mergeSelectedArtifactProvenance(priorMeta, path, before, after, updates, inherited) {
|
|
538
|
+
const previous = parseScopeProvenance(priorMeta, path);
|
|
539
|
+
const registry = { ...previous, ...inherited?.registry, ...updates };
|
|
540
|
+
const removed = Object.keys(registry).filter(key => isInterpretableArtifactProvenance(registry[key])
|
|
541
|
+
&& Object.hasOwn(before ?? {}, key) && !Object.hasOwn(after ?? {}, key));
|
|
542
|
+
for (const key of removed)
|
|
543
|
+
delete registry[key];
|
|
544
|
+
const raw = priorMeta === undefined ? { version: 3 } : JSON.parse(priorMeta);
|
|
545
|
+
const artifacts = { ...raw.artifacts, ...inherited?.raw, ...updates };
|
|
546
|
+
for (const key of removed)
|
|
547
|
+
delete artifacts[key];
|
|
548
|
+
const changed = !sameJson(previous, registry) || (inherited?.raw !== undefined
|
|
549
|
+
&& (raw.artifacts === undefined || !sameJson(raw.artifacts, artifacts)));
|
|
550
|
+
return { registry, changed, metadata: changed ? canonicalJson({ ...raw, artifacts }) : priorMeta };
|
|
551
|
+
}
|
|
552
|
+
/** Check raw predecessor namespaces before selected metadata3→4 admission can hide ignored data. */
|
|
553
|
+
function assertSelectedModuleConversion(basis, paths, scope, cwd, modules) {
|
|
554
|
+
if (modules === undefined)
|
|
555
|
+
return;
|
|
556
|
+
const files = new Map(basis.files.map(file => [file.path, file.bytes]));
|
|
557
|
+
prepareSerializedScopeMigration({ checkpoint: files.get(paths.checkpoint), patches: files.get(paths.patches), meta: files.get(paths.meta) }, scope, scope === "cwd" ? cwd : undefined, modules);
|
|
558
|
+
}
|
|
559
|
+
/** One byte planner for replay successors and current-only adoption; neither admits new private authority. */
|
|
560
|
+
function publishSelectedSuccessor(cwd, sessionId, current, expected, historyLimit, requestedLifecycle, root, sessionKey, requestedProvenance, lifecycleOnly = false) {
|
|
561
|
+
const provenance = captureSelectedProvenanceUpdates(requestedProvenance, expected.selection);
|
|
562
|
+
const nextProvenance = {};
|
|
563
|
+
const lifecycleKeys = ["step", "specification", "bootstrap", "validation"];
|
|
564
|
+
const lifecycle = requestedLifecycle === undefined ? undefined : captureLifecycleSnapshot(requestedLifecycle);
|
|
565
|
+
const paths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
566
|
+
const metadata = { ...current.lifecycle.runtime.meta };
|
|
567
|
+
if (lifecycle) {
|
|
568
|
+
for (const key of lifecycleKeys)
|
|
569
|
+
delete metadata[key];
|
|
570
|
+
Object.assign(metadata, lifecycle.meta);
|
|
571
|
+
}
|
|
572
|
+
if (!lifecycleOnly && metadata.privateAcquisition === "acquired") {
|
|
573
|
+
if (expected.selection.includes("session"))
|
|
574
|
+
delete metadata.privateCausalBasis;
|
|
575
|
+
else if (current.lifecycle.sourceVersion === 1 || current.lifecycle.runtime.config.scopes.includes("session")) {
|
|
576
|
+
// Retain the admitted source claim before replacing the selected causal origin.
|
|
577
|
+
// The requested view may already omit Session or use a new shared-only origin.
|
|
578
|
+
if (current.lifecycle.sourceVersion === 1 && !sameJson(current.view.lineage, current.lifecycle.runtime.meta.lineage)) {
|
|
579
|
+
throw new Error("Insufficient State Flow private causal basis before legacy realignment");
|
|
580
|
+
}
|
|
581
|
+
metadata.privateCausalBasis = capturePrivateCausalBasis({ lineage: current.lifecycle.runtime.meta.lineage,
|
|
582
|
+
causalWitnesses: current.lifecycle.sourceVersion === 1 ? temporalCausalWitnesses(current.view, MAX_HISTORY_LIMIT)
|
|
583
|
+
: current.lifecycle.runtime.meta.causalWitnesses ?? [] });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
metadata.lineage = structuredClone(expected.lineage);
|
|
587
|
+
metadata.causalWitnesses = temporalCausalWitnesses(expected, historyLimit);
|
|
588
|
+
const targetLifecycle = prepareSessionPolicyMigration(`${canonicalJson({ mode: lifecycle === undefined ? current.lifecycle.runtime.config.mode : lifecycle.config.mode, scopes: expected.selection })}\n`, `${canonicalJson(metadata)}\n`, cwd, sessionId, expected.selection);
|
|
589
|
+
const sources = temporalFileContents(current.basis);
|
|
590
|
+
const updates = [];
|
|
591
|
+
const convertedScopes = [];
|
|
592
|
+
for (const scope of expected.selection) {
|
|
593
|
+
const scopePaths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
594
|
+
const priorMeta = sources.get(scopePaths.meta);
|
|
595
|
+
const converting = priorMeta !== undefined && JSON.parse(priorMeta).version !== (expected.scopes[scope].modules === undefined ? 3 : 4);
|
|
596
|
+
if (converting) {
|
|
597
|
+
assertSelectedModuleConversion(current.basis, scopePaths, scope, cwd, expected.scopes[scope].modules);
|
|
598
|
+
convertedScopes.push(scope);
|
|
599
|
+
}
|
|
600
|
+
const previous = parseScopeStream(sources.get(scopePaths.checkpoint), sources.get(scopePaths.patches), scope, scope === "cwd" ? cwd : undefined, priorMeta);
|
|
601
|
+
const merged = mergeSelectedArtifactProvenance(priorMeta, scopePaths.meta, readTemporalView(current.view, 0, scope, MAX_HISTORY_LIMIT).artifacts, readTemporalView(expected, 0, scope, MAX_HISTORY_LIMIT).artifacts, provenance?.[scope]);
|
|
602
|
+
nextProvenance[scope] = merged.registry;
|
|
603
|
+
const metadataChanged = merged.changed;
|
|
604
|
+
const streamChanged = converting || previous === undefined || !sameJson(previous, expected.scopes[scope]);
|
|
605
|
+
if (!streamChanged && !metadataChanged)
|
|
606
|
+
continue;
|
|
607
|
+
const stream = expected.scopes[scope];
|
|
608
|
+
if (streamChanged) {
|
|
609
|
+
const wire = serializeScopeStream(stream, scope, scope === "cwd" ? cwd : undefined);
|
|
610
|
+
updates.push({ path: scopePaths.checkpoint, content: wire.checkpoint }, { path: scopePaths.patches, content: wire.patches });
|
|
611
|
+
}
|
|
612
|
+
updates.push({ path: scopePaths.meta, content: serializeScopeMetadata(undefined, stream, scope, scope === "cwd" ? cwd : undefined, merged.metadata) });
|
|
613
|
+
}
|
|
614
|
+
if (sources.get(paths.config) !== targetLifecycle.target.config)
|
|
615
|
+
updates.push({ path: paths.config, content: targetLifecycle.target.config });
|
|
616
|
+
if (sources.get(paths.runtime) !== targetLifecycle.target.runtime)
|
|
617
|
+
updates.push({ path: paths.runtime, content: targetLifecycle.target.runtime });
|
|
618
|
+
if (lifecycleOnly && updates.some(update => update.path !== paths.config && update.path !== paths.runtime)) {
|
|
619
|
+
throw new Error("Selected Passive persistence requires lifecycle v2 and semantic v3 without semantic changes");
|
|
620
|
+
}
|
|
621
|
+
const receipts = { files: temporalFileReceipts(current.basis, updates) };
|
|
622
|
+
const cohort = decodeSelectedFileCohort(cwd, sessionId, expected.selection, root, sessionKey, { lifecycle: targetLifecycle, basis: receipts, presence: current.presence });
|
|
623
|
+
if (!sameJson(cohort.view, expected) || !sameJson(cohort.provenance, nextProvenance))
|
|
624
|
+
throw new Error("Selected State Flow receipt does not match accepted replay");
|
|
625
|
+
const lifecycleConversion = current.lifecycle.sourceVersion === 1 ? { sourceVersion: 1, targetVersion: 2 } : undefined;
|
|
626
|
+
const assertPresence = () => {
|
|
627
|
+
if (!sameJson(current.presence, captureOwnedFilePresence(current.presence.map((file) => file.path), root)))
|
|
628
|
+
throw new Error("Selected State Flow presence dependencies changed concurrently");
|
|
629
|
+
};
|
|
630
|
+
assertPresence();
|
|
631
|
+
const migrationBackup = convertedScopes.length > 0 || lifecycleConversion !== undefined
|
|
632
|
+
? preserveMigrationBasis(current.basis, root, cwd, sessionId, sessionKey, { scopes: convertedScopes, lifecycle: lifecycleConversion, presence: current.presence }, updates) : undefined;
|
|
633
|
+
assertPresence();
|
|
634
|
+
publishFileUpdates(current.basis.files, updates, root);
|
|
635
|
+
return { cohort, changed: updates.length > 0, ...(migrationBackup ? { migrationBackup } : {}) };
|
|
636
|
+
}
|
|
637
|
+
/** Initialize only genuinely absent private authority or first acquisition from an admitted unacquired owner. */
|
|
638
|
+
function acceptSelectedOrigin(cwd, sessionId, request, root, sessionKey) {
|
|
639
|
+
const wanted = captureScopeSelection(request.selection);
|
|
640
|
+
if (!["shared-new", "shared-deferred", "new", "unacquired", "fork-new", "fork-deferred"].includes(request.kind))
|
|
641
|
+
throw new Error("Invalid selected origin admission kind");
|
|
642
|
+
const copying = request.kind === "fork-new" || request.kind === "fork-deferred";
|
|
643
|
+
const acquiring = request.kind === "new" || request.kind === "unacquired" || copying;
|
|
644
|
+
if (acquiring !== wanted.includes("session"))
|
|
645
|
+
throw new Error(acquiring ? "Selected private acquisition requires Session" : "Selected shared origin cannot acquire Session");
|
|
646
|
+
const snapshot = captureLifecycleSnapshot(request.snapshot);
|
|
647
|
+
if (snapshot.config.mode === "off")
|
|
648
|
+
throw new Error("Off cannot initialize State Flow memory");
|
|
649
|
+
if ((request.kind === "shared-deferred" || request.kind === "fork-new") && (snapshot.meta.step !== 0 || snapshot.meta.specification !== undefined || snapshot.meta.validation !== undefined)) {
|
|
650
|
+
throw new Error("Selected fork deferral requires a fresh child lifecycle");
|
|
651
|
+
}
|
|
652
|
+
const { lifecycle, lifecycleFiles, presence } = captureSelectedLifecycle(cwd, sessionId, wanted, root, sessionKey);
|
|
653
|
+
let prior;
|
|
654
|
+
if (request.kind === "unacquired" || request.kind === "fork-deferred") {
|
|
655
|
+
const required = request.kind === "fork-deferred" ? "deferred" : "unacquired";
|
|
656
|
+
if (!lifecycle || lifecycle.sourceVersion !== 2 || lifecycle.runtime.meta.privateAcquisition !== required) {
|
|
657
|
+
throw new Error(`Selected private acquisition requires admitted lifecycle-v2 ${required} authority`);
|
|
658
|
+
}
|
|
659
|
+
prior = prepareSelectedFileCohort(cwd, sessionId, request.base.view.selection, root, sessionKey);
|
|
660
|
+
if (!prior)
|
|
661
|
+
throw new Error("Selected private acquisition source is unavailable");
|
|
662
|
+
assertSelectedFileCohortBasis(request.base, prior);
|
|
663
|
+
const fields = Object.fromEntries(["step", "specification", "bootstrap", "validation"]
|
|
664
|
+
.filter((key) => Object.hasOwn(lifecycle.runtime.meta, key)).map((key) => [key, lifecycle.runtime.meta[key]]));
|
|
665
|
+
if (!sameJson(fields, snapshot.meta))
|
|
666
|
+
throw new Error("Selected private acquisition cannot reset run lifecycle fields");
|
|
667
|
+
}
|
|
668
|
+
else if (lifecycle)
|
|
669
|
+
throw new Error("Existing State Flow session requires current-memory acceptance, not a new origin");
|
|
670
|
+
const copy = copying ? captureSelectedForkSource(cwd, sessionId, sessionKey, request.source, root) : undefined;
|
|
671
|
+
const scopePaths = wanted.map((scope) => ({ scope, paths: temporalScopePaths(cwd, sessionId, scope, root, sessionKey) }));
|
|
672
|
+
const layouts = captureOwnedFilePresence(scopePaths.filter(({ scope }) => scope !== "session").map(({ paths }) => resolve(paths.directory, "state.json")), root);
|
|
673
|
+
if (layouts.some((file) => file.present))
|
|
674
|
+
throw new Error("Unsupported State Flow selected semantic layout");
|
|
675
|
+
presence.push(...layouts);
|
|
676
|
+
const scopeFiles = captureOwnedFileBases(scopePaths.flatMap(({ paths }) => [paths.checkpoint, paths.patches, paths.meta]), root);
|
|
677
|
+
const basis = { files: [...new Map([...lifecycleFiles, ...scopeFiles, ...(prior?.basis.files ?? []), ...(copy?.basis.files ?? [])].map((file) => [file.path, file])).values()] };
|
|
678
|
+
if (prior) {
|
|
679
|
+
const recaptured = new Map([...lifecycleFiles, ...scopeFiles].map((file) => [file.path, file.identity]));
|
|
680
|
+
if (prior.basis.files.some((file) => recaptured.has(file.path) && recaptured.get(file.path) !== file.identity))
|
|
681
|
+
throw new Error("Selected acquisition source changed concurrently");
|
|
682
|
+
}
|
|
683
|
+
const sources = temporalFileContents(basis);
|
|
684
|
+
const origin = `files:selected-origin:${randomUUID()}`;
|
|
685
|
+
const fresh = createSelectedTemporalState(Object.fromEntries(wanted.map((scope) => [scope, {}])), wanted, `${origin}:empty`, request.historyLimit);
|
|
686
|
+
const streams = {};
|
|
687
|
+
const existing = new Set();
|
|
688
|
+
for (const { scope, paths } of scopePaths) {
|
|
689
|
+
const values = [paths.checkpoint, paths.patches, paths.meta].map((path) => sources.get(path));
|
|
690
|
+
if (scope === "session" && values.some((value) => value !== undefined))
|
|
691
|
+
throw new Error("Selected private acquisition target is occupied");
|
|
692
|
+
if (values.some((value) => value !== undefined) && values.some((value) => value === undefined))
|
|
693
|
+
throw new Error(`State Flow ${scope} origin requires complete or wholly absent scope files`);
|
|
694
|
+
if (scope === "session" && copy) {
|
|
695
|
+
streams.session = copy.stream;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const source = classifyScopeStream(values[0], values[1], scope, scope === "cwd" ? cwd : undefined, values[2]);
|
|
699
|
+
if (source.kind === "present") {
|
|
700
|
+
streams[scope] = source.stream;
|
|
701
|
+
existing.add(scope);
|
|
702
|
+
}
|
|
703
|
+
else
|
|
704
|
+
streams[scope] = fresh.scopes[scope];
|
|
705
|
+
}
|
|
706
|
+
const originView = existing.size === 0 && !copy ? fresh : adoptSelectedTemporalStreams(streams, wanted, origin, request.historyLimit);
|
|
707
|
+
const finish = (nextSnapshot, transition, requestedProvenance, moduleAdditions) => {
|
|
708
|
+
const snapshot = captureLifecycleSnapshot(nextSnapshot);
|
|
709
|
+
if (snapshot.config.mode === "off")
|
|
710
|
+
throw new Error("Off cannot initialize State Flow memory");
|
|
711
|
+
const provenance = captureSelectedProvenanceUpdates(requestedProvenance, wanted);
|
|
712
|
+
const view = selectedReplaySuccessor(originView, request.historyLimit, transition, moduleAdditions);
|
|
713
|
+
const metadata = { ...prior?.lifecycle.runtime.meta };
|
|
714
|
+
for (const key of ["step", "specification", "bootstrap", "validation"])
|
|
715
|
+
delete metadata[key];
|
|
716
|
+
Object.assign(metadata, snapshot.meta);
|
|
717
|
+
const target = prepareSessionPolicyMigration(`${canonicalJson({ mode: snapshot.config.mode, scopes: wanted })}\n`, `${canonicalJson({ ...metadata, version: 2, identity: { cwd: resolve(cwd), sessionId }, lineage: view.lineage,
|
|
718
|
+
privateAcquisition: acquiring ? "acquired" : request.kind === "shared-deferred" ? "deferred" : "unacquired", causalWitnesses: temporalCausalWitnesses(view, request.historyLimit) })}\n`, cwd, sessionId, wanted);
|
|
719
|
+
const updates = [];
|
|
720
|
+
const convertedScopes = [];
|
|
721
|
+
for (const { scope, paths } of scopePaths) {
|
|
722
|
+
const prior = sources.get(paths.meta);
|
|
723
|
+
const converting = prior !== undefined && JSON.parse(prior).version !== (view.scopes[scope].modules === undefined ? 3 : 4);
|
|
724
|
+
if (converting) {
|
|
725
|
+
assertSelectedModuleConversion(basis, paths, scope, cwd, view.scopes[scope].modules);
|
|
726
|
+
convertedScopes.push(scope);
|
|
727
|
+
}
|
|
728
|
+
const merged = mergeSelectedArtifactProvenance(prior, paths.meta, readTemporalView(originView, 0, scope, MAX_HISTORY_LIMIT).artifacts, readTemporalView(view, 0, scope, MAX_HISTORY_LIMIT).artifacts, provenance?.[scope], scope === "session" && copy ? { registry: copy.provenance, raw: copy.rawArtifacts } : undefined);
|
|
729
|
+
if (existing.has(scope) && !converting && sameJson(streams[scope], view.scopes[scope]) && !merged.changed)
|
|
730
|
+
continue;
|
|
731
|
+
const stream = view.scopes[scope];
|
|
732
|
+
const wire = serializeScopeStream(stream, scope, scope === "cwd" ? cwd : undefined);
|
|
733
|
+
updates.push({ path: paths.checkpoint, content: wire.checkpoint }, { path: paths.patches, content: wire.patches }, { path: paths.meta, content: serializeScopeMetadata(undefined, stream, scope, scope === "cwd" ? cwd : undefined, merged.metadata) });
|
|
734
|
+
}
|
|
735
|
+
const paths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
736
|
+
updates.push({ path: paths.config, content: target.target.config }, { path: paths.runtime, content: target.target.runtime });
|
|
737
|
+
const receipts = { files: temporalFileReceipts(basis, updates) };
|
|
738
|
+
const privatePaths = temporalScopePaths(cwd, sessionId, "session", root, sessionKey);
|
|
739
|
+
const created = new Set(acquiring ? [privatePaths.checkpoint, privatePaths.patches, privatePaths.meta] : []);
|
|
740
|
+
const targetPresence = presence.map((file) => created.has(file.path) ? { ...file, present: true } : file);
|
|
741
|
+
const cohort = decodeSelectedFileCohort(cwd, sessionId, wanted, root, sessionKey, { lifecycle: target, basis: receipts, presence: targetPresence });
|
|
742
|
+
if (!sameJson(cohort.view, view))
|
|
743
|
+
throw new Error("Selected State Flow origin receipts do not match adopted values");
|
|
744
|
+
const assertOriginBasis = () => {
|
|
745
|
+
assertTemporalFileBase(basis, { files: captureOwnedFileBases(basis.files.map(file => file.path), root) });
|
|
746
|
+
if (!sameJson(presence, captureOwnedFilePresence(presence.map(file => file.path), root)))
|
|
747
|
+
throw new Error("Selected origin presence changed concurrently");
|
|
748
|
+
copy?.assertCurrent();
|
|
749
|
+
};
|
|
750
|
+
assertOriginBasis();
|
|
751
|
+
const migrationBackup = convertedScopes.length > 0
|
|
752
|
+
? preserveMigrationBasis(basis, root, cwd, sessionId, sessionKey, { scopes: convertedScopes, presence }, updates) : undefined;
|
|
753
|
+
assertOriginBasis();
|
|
754
|
+
publishFileUpdates(basis.files, updates, root);
|
|
755
|
+
return { cohort, changed: true, ...(migrationBackup ? { migrationBackup } : {}) };
|
|
756
|
+
};
|
|
757
|
+
if (!request.author)
|
|
758
|
+
return finish(snapshot);
|
|
759
|
+
let consumed = false;
|
|
760
|
+
let accepted;
|
|
761
|
+
const provenance = Object.fromEntries(scopePaths.map(({ scope, paths }) => [scope,
|
|
762
|
+
scope === "session" && copy ? copy.provenance : parseScopeProvenance(sources.get(paths.meta), paths.meta)]));
|
|
763
|
+
let authorActive = true;
|
|
764
|
+
try {
|
|
765
|
+
request.author({ view: structuredClone(originView), snapshot: structuredClone(snapshot), provenance }, (next, transition, evidence, moduleAdditions) => {
|
|
766
|
+
if (!authorActive)
|
|
767
|
+
throw new Error("Selected first authoring callback has ended");
|
|
768
|
+
if (consumed)
|
|
769
|
+
throw new Error("Selected first authoring publication was already consumed");
|
|
770
|
+
consumed = true;
|
|
771
|
+
accepted = finish(next, transition, evidence, moduleAdditions);
|
|
772
|
+
return accepted;
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
finally {
|
|
776
|
+
authorActive = false;
|
|
777
|
+
}
|
|
778
|
+
if (!accepted)
|
|
779
|
+
throw new Error("Selected first authoring requires one synchronous publication");
|
|
780
|
+
return accepted;
|
|
781
|
+
}
|
|
782
|
+
/** Select a retained private basis only when current policy excluded it; selected Session cannot use stale proof as fallback. */
|
|
783
|
+
function validateSelectedPrivateStream(lifecycle, stream) {
|
|
784
|
+
if (lifecycle.runtime.meta.privateAcquisition !== "acquired")
|
|
785
|
+
throw new Error("State Flow private memory requires acquired authority");
|
|
786
|
+
const metadata = lifecycle.runtime.meta;
|
|
787
|
+
const witnesses = lifecycle.sourceVersion === 2 ? captureTemporalWitnesses(metadata.lineage, Object.hasOwn(metadata, "causalWitnesses") ? metadata.causalWitnesses : [], MAX_HISTORY_LIMIT) : [];
|
|
788
|
+
const retained = lifecycle.sourceVersion === 2 && !lifecycle.runtime.config.scopes.includes("session")
|
|
789
|
+
? metadata.privateCausalBasis : undefined;
|
|
790
|
+
if (retained) {
|
|
791
|
+
restoreSelectedTemporalState({ session: stream }, ["session"], retained.lineage, retained.causalWitnesses, MAX_HISTORY_LIMIT);
|
|
792
|
+
}
|
|
793
|
+
else
|
|
794
|
+
validateScopeLineage(stream, "session", metadata.lineage, MAX_HISTORY_LIMIT);
|
|
795
|
+
for (const witness of witnesses.filter(item => item.owners.includes("session"))) {
|
|
796
|
+
if (!stream.patches.some(record => sameJson(record.transition, witness.transition)))
|
|
797
|
+
throw new Error("Accepted temporal witness is missing its selected owner patch");
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/** Admit only this direct parent's private stream; native lineage/parent authority belongs to the caller. */
|
|
801
|
+
function captureSelectedForkSource(cwd, childId, childKey, source, root) {
|
|
802
|
+
if (!source || typeof source.id !== "string" || typeof source.key !== "string" || source.id === childId || source.key === childKey) {
|
|
803
|
+
throw new Error("Selected fork requires a distinct direct-parent identity and key");
|
|
804
|
+
}
|
|
805
|
+
const { lifecycle, lifecycleFiles, presence } = captureSelectedLifecycle(cwd, source.id, ["session"], root, source.key);
|
|
806
|
+
if (!lifecycle || lifecycle.runtime.meta.privateAcquisition !== "acquired")
|
|
807
|
+
throw new Error("Selected fork parent has no acquired current private memory");
|
|
808
|
+
const paths = temporalScopePaths(cwd, source.id, "session", root, source.key);
|
|
809
|
+
const scopeFiles = captureOwnedFileBases([paths.checkpoint, paths.patches, paths.meta], root);
|
|
810
|
+
const basis = { files: [...lifecycleFiles, ...scopeFiles] };
|
|
811
|
+
const files = temporalFileContents(basis);
|
|
812
|
+
const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), "session", undefined, files.get(paths.meta));
|
|
813
|
+
if (!stream)
|
|
814
|
+
throw new Error("Selected fork parent private stream is unavailable");
|
|
815
|
+
validateSelectedPrivateStream(lifecycle, stream);
|
|
816
|
+
const provenance = parseScopeProvenance(files.get(paths.meta), paths.meta);
|
|
817
|
+
// Raw inheritance is bound to the same admitted parent metadata bytes as interpreted evidence.
|
|
818
|
+
const rawMetadata = files.get(paths.meta);
|
|
819
|
+
const rawArtifacts = rawMetadata === undefined ? undefined : JSON.parse(rawMetadata).artifacts;
|
|
820
|
+
return { basis, stream, provenance, rawArtifacts, assertCurrent: () => {
|
|
821
|
+
const current = { files: captureOwnedFileBases(basis.files.map((file) => file.path), root) };
|
|
822
|
+
assertTemporalFileBase(basis, current);
|
|
823
|
+
if (!sameJson(presence, captureOwnedFilePresence(presence.map((file) => file.path), root)))
|
|
824
|
+
throw new Error("Selected fork parent presence changed concurrently");
|
|
825
|
+
} };
|
|
826
|
+
}
|
|
827
|
+
/** Read-only selected admission under exclusion; never initialize a store or grant publication/acquisition. */
|
|
828
|
+
export async function loadSelectedFileCohort(cwd, sessionId, selection, root, sessionKey = sessionId, signal) {
|
|
829
|
+
signal?.throwIfAborted();
|
|
830
|
+
const wanted = captureScopeSelection(selection);
|
|
831
|
+
assertStorageDirectory(root);
|
|
832
|
+
if (!lstatSync(root, { throwIfNoEntry: false }))
|
|
833
|
+
return undefined;
|
|
834
|
+
return withStorageTransaction(root, (transaction) => transaction.prepareSelected(cwd, sessionId, wanted, root, sessionKey), signal);
|
|
835
|
+
}
|
|
836
|
+
/** Exact recapture comparison; future selected publishers must repeat both dependency checks under exclusion. */
|
|
837
|
+
export function assertSelectedFileCohortBasis(expected, current) {
|
|
838
|
+
if (!sameJson(expected.view.selection, current.view.selection))
|
|
839
|
+
throw new Error("Selected State Flow policy changed concurrently");
|
|
840
|
+
assertTemporalFileBase(expected.basis, current.basis);
|
|
841
|
+
if (!sameJson(expected.presence, current.presence))
|
|
842
|
+
throw new Error("Selected State Flow presence dependencies changed concurrently");
|
|
843
|
+
}
|
|
844
|
+
function captureSelectedLifecycle(cwd, sessionId, wanted, root, sessionKey, expectedLifecycle) {
|
|
845
|
+
const privatePaths = temporalScopePaths(cwd, sessionId, "session", root, sessionKey);
|
|
846
|
+
const privateFiles = [privatePaths.checkpoint, privatePaths.patches, privatePaths.meta];
|
|
847
|
+
const legacyPrivate = resolve(privatePaths.directory, "state.json");
|
|
848
|
+
const presence = captureOwnedFilePresence([...privateFiles, legacyPrivate], root);
|
|
849
|
+
if (presence.at(-1).present)
|
|
850
|
+
throw new Error("Unsupported State Flow private semantic layout");
|
|
851
|
+
const runtimePaths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
852
|
+
const lifecycleFiles = captureOwnedFileBases([runtimePaths.config, runtimePaths.runtime], root);
|
|
853
|
+
// Compare before parsing: a changed legacy epoch must not widen a selected mode-only read.
|
|
854
|
+
if (expectedLifecycle)
|
|
855
|
+
assertTemporalFileBase(expectedLifecycle, { files: lifecycleFiles });
|
|
856
|
+
const lifecycleText = temporalFileContents({ files: lifecycleFiles });
|
|
857
|
+
const lifecycle = prepareSessionPolicyMigration(lifecycleText.get(runtimePaths.config), lifecycleText.get(runtimePaths.runtime), cwd, sessionId, wanted);
|
|
858
|
+
if (!lifecycle) {
|
|
859
|
+
if (presence.some((file) => file.present))
|
|
860
|
+
throw new Error("Orphan State Flow private semantics without lifecycle authority");
|
|
861
|
+
return { lifecycle, lifecycleFiles, presence };
|
|
862
|
+
}
|
|
863
|
+
const acquired = lifecycle.runtime.meta.privateAcquisition === "acquired";
|
|
864
|
+
if (acquired && presence.slice(0, 3).some((file) => !file.present))
|
|
865
|
+
throw new Error("Acquired State Flow private semantics are incomplete");
|
|
866
|
+
if (!acquired && presence.some((file) => file.present))
|
|
867
|
+
throw new Error("Unacquired State Flow private target is occupied");
|
|
868
|
+
return { lifecycle, lifecycleFiles, presence };
|
|
869
|
+
}
|
|
870
|
+
function prepareSelectedFileCohort(cwd, sessionId, selection, root, sessionKey, expectedLifecycle) {
|
|
871
|
+
const wanted = captureScopeSelection(selection);
|
|
872
|
+
const captured = captureSelectedCohortSource(cwd, sessionId, wanted, root, sessionKey, expectedLifecycle, expectedLifecycle !== undefined);
|
|
873
|
+
return captured ? decodeSelectedFileCohort(cwd, sessionId, wanted, root, sessionKey, captured) : undefined;
|
|
874
|
+
}
|
|
875
|
+
function captureSelectedCohortSource(cwd, sessionId, wanted, root, sessionKey, expectedLifecycle, requireV2 = false) {
|
|
876
|
+
const { lifecycle, lifecycleFiles, presence } = captureSelectedLifecycle(cwd, sessionId, wanted, root, sessionKey, expectedLifecycle);
|
|
877
|
+
if (requireV2 && lifecycle?.sourceVersion !== 2)
|
|
878
|
+
throw new Error("Selected Passive persistence requires lifecycle v2 and semantic v3");
|
|
879
|
+
if (!lifecycle)
|
|
880
|
+
return undefined;
|
|
881
|
+
if (lifecycle.runtime.meta.privateAcquisition !== "acquired" && wanted.includes("session"))
|
|
882
|
+
throw new Error("Selected State Flow Session requires explicit private acquisition");
|
|
883
|
+
const dependencies = lifecycle.sourceVersion === 1 ? SCOPES : wanted;
|
|
884
|
+
const scopePaths = dependencies.map((scope) => ({ scope, paths: temporalScopePaths(cwd, sessionId, scope, root, sessionKey) }));
|
|
885
|
+
const layouts = captureOwnedFilePresence(scopePaths.filter(({ scope }) => scope !== "session").map(({ paths }) => resolve(paths.directory, "state.json")), root);
|
|
886
|
+
if (layouts.some((file) => file.present))
|
|
887
|
+
throw new Error("Unsupported State Flow selected semantic layout");
|
|
888
|
+
presence.push(...layouts);
|
|
889
|
+
const scopeFiles = captureOwnedFileBases(scopePaths.flatMap(({ paths }) => [paths.checkpoint, paths.patches, paths.meta]), root);
|
|
890
|
+
const basis = { files: [...lifecycleFiles, ...scopeFiles] };
|
|
891
|
+
return { lifecycle, basis, presence };
|
|
892
|
+
}
|
|
893
|
+
/** Current-only admission may replace stale shared alignment, never malformed private/stream/witness evidence. */
|
|
894
|
+
function prepareSelectedCurrent(cwd, sessionId, selection, historyLimit, root, sessionKey, expectedLifecycle) {
|
|
895
|
+
const wanted = captureScopeSelection(selection);
|
|
896
|
+
const source = captureSelectedCohortSource(cwd, sessionId, wanted, root, sessionKey, expectedLifecycle);
|
|
897
|
+
if (!source)
|
|
898
|
+
throw new Error("Selected State Flow current session authority is unavailable");
|
|
899
|
+
const { lifecycle, basis, presence } = source;
|
|
900
|
+
const lineage = lifecycle.runtime.meta.lineage;
|
|
901
|
+
const witnesses = lifecycle.sourceVersion === 2 ? captureTemporalWitnesses(lineage, Object.hasOwn(lifecycle.runtime.meta, "causalWitnesses") ? lifecycle.runtime.meta.causalWitnesses : [], MAX_HISTORY_LIMIT) : [];
|
|
902
|
+
const dependencies = lifecycle.sourceVersion === 1 ? SCOPES : wanted;
|
|
903
|
+
const files = temporalFileContents(basis);
|
|
904
|
+
const origin = `files:selected-current:${randomUUID()}`;
|
|
905
|
+
const empty = createSelectedTemporalState(Object.fromEntries(wanted.map((scope) => [scope, {}])), wanted, `${origin}:empty`, historyLimit);
|
|
906
|
+
const streams = {};
|
|
907
|
+
const provenance = {};
|
|
908
|
+
let needsOrigin = false;
|
|
909
|
+
for (const scope of dependencies) {
|
|
910
|
+
const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
911
|
+
const values = [paths.checkpoint, paths.patches, paths.meta].map((path) => files.get(path));
|
|
912
|
+
if (values.some((value) => value !== undefined) && values.some((value) => value === undefined))
|
|
913
|
+
throw new Error(`Selected current State Flow ${scope} requires complete or wholly absent files`);
|
|
914
|
+
const stream = parseScopeStream(values[0], values[1], scope, scope === "cwd" ? cwd : undefined, values[2]);
|
|
915
|
+
if (!stream && (scope === "session" || lifecycle.sourceVersion === 1))
|
|
916
|
+
throw new Error(`Selected State Flow ${scope} stream is unavailable`);
|
|
917
|
+
const registry = parseScopeProvenance(values[2], paths.meta);
|
|
918
|
+
if (scope === "session")
|
|
919
|
+
validateSelectedPrivateStream(lifecycle, stream);
|
|
920
|
+
const aligned = stream !== undefined && scopeLineageMatches(stream, scope, lineage, MAX_HISTORY_LIMIT);
|
|
921
|
+
if (!aligned)
|
|
922
|
+
needsOrigin = true;
|
|
923
|
+
// Unchanged owners must still substantiate every selected-owner claim; drift discards old shared history only.
|
|
924
|
+
if (aligned && scope !== "session")
|
|
925
|
+
for (const witness of witnesses.filter((item) => item.owners.includes(scope))) {
|
|
926
|
+
if (!stream.patches.some((record) => sameJson(record.transition, witness.transition)))
|
|
927
|
+
throw new Error("Accepted temporal witness is missing its selected owner patch");
|
|
928
|
+
}
|
|
929
|
+
if (wanted.includes(scope)) {
|
|
930
|
+
streams[scope] = stream ?? empty.scopes[scope];
|
|
931
|
+
provenance[scope] = registry;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (!needsOrigin) {
|
|
935
|
+
const decoded = decodeSelectedFileCohort(cwd, sessionId, wanted, root, sessionKey, source);
|
|
936
|
+
const view = constrainTemporalState(decoded.view, historyLimit);
|
|
937
|
+
return { ...decoded, alignment: "retained", view, witnesses: temporalCausalWitnesses(view, historyLimit) };
|
|
938
|
+
}
|
|
939
|
+
const view = adoptSelectedTemporalStreams(streams, wanted, origin, historyLimit);
|
|
940
|
+
return { basis, presence, lifecycle, provenance, view, witnesses: temporalCausalWitnesses(view, historyLimit), alignment: "current-origin" };
|
|
941
|
+
}
|
|
942
|
+
/** Decode predicted receipts too; no canonical reread is needed to establish an accepted output basis. */
|
|
943
|
+
function decodeSelectedFileCohort(cwd, sessionId, wanted, root, sessionKey, captured) {
|
|
944
|
+
const { lifecycle } = captured;
|
|
945
|
+
const dependencies = lifecycle.sourceVersion === 1 ? SCOPES : wanted;
|
|
946
|
+
const scopePaths = dependencies.map((scope) => ({ scope, paths: temporalScopePaths(cwd, sessionId, scope, root, sessionKey) }));
|
|
947
|
+
const paths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
948
|
+
const bytePaths = new Set([paths.config, paths.runtime, ...scopePaths.flatMap(({ paths }) => [paths.checkpoint, paths.patches, paths.meta])]);
|
|
949
|
+
const privatePaths = temporalScopePaths(cwd, sessionId, "session", root, sessionKey);
|
|
950
|
+
const presencePaths = new Set([privatePaths.checkpoint, privatePaths.patches, privatePaths.meta, resolve(privatePaths.directory, "state.json"),
|
|
951
|
+
...scopePaths.filter(({ scope }) => scope !== "session").map(({ paths }) => resolve(paths.directory, "state.json"))]);
|
|
952
|
+
const basis = { files: captured.basis.files.filter(({ path }) => bytePaths.has(path)) };
|
|
953
|
+
const presence = captured.presence.filter(({ path }) => presencePaths.has(path));
|
|
954
|
+
if (basis.files.length !== bytePaths.size || presence.length !== presencePaths.size)
|
|
955
|
+
throw new Error("Incomplete selected State Flow dependency receipts");
|
|
956
|
+
const files = temporalFileContents(basis);
|
|
957
|
+
const streams = {};
|
|
958
|
+
const provenance = {};
|
|
959
|
+
for (const { scope, paths } of scopePaths) {
|
|
960
|
+
const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope, scope === "cwd" ? cwd : undefined, files.get(paths.meta));
|
|
961
|
+
if (!stream)
|
|
962
|
+
throw new Error(`Selected State Flow ${scope} stream is unavailable`);
|
|
963
|
+
streams[scope] = stream;
|
|
964
|
+
const registry = parseScopeProvenance(files.get(paths.meta), paths.meta);
|
|
965
|
+
if (wanted.includes(scope))
|
|
966
|
+
provenance[scope] = registry;
|
|
967
|
+
}
|
|
968
|
+
let view;
|
|
969
|
+
if (lifecycle.sourceVersion === 1) {
|
|
970
|
+
const complete = { lineage: lifecycle.runtime.meta.lineage, scopes: streams };
|
|
971
|
+
validateTemporalState(complete, MAX_HISTORY_LIMIT);
|
|
972
|
+
view = selectTemporalState(complete, wanted, MAX_HISTORY_LIMIT);
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
const evidence = Object.hasOwn(lifecycle.runtime.meta, "causalWitnesses") ? lifecycle.runtime.meta.causalWitnesses : [];
|
|
976
|
+
view = restoreSelectedTemporalState(streams, wanted, lifecycle.runtime.meta.lineage, evidence, MAX_HISTORY_LIMIT);
|
|
977
|
+
}
|
|
978
|
+
return { basis, presence, lifecycle, view, witnesses: temporalCausalWitnesses(view, MAX_HISTORY_LIMIT), provenance };
|
|
979
|
+
}
|
|
302
980
|
export function captureTemporalFileBase(cwd, sessionId, root, sessionKey = sessionId) {
|
|
303
981
|
return withStoragePublicationLock(root, (locked) => ({ files: captureTemporalFileBases(cwd, sessionId, locked, sessionKey) }));
|
|
304
982
|
}
|
|
@@ -325,8 +1003,47 @@ function publishLockedTemporalStateToFiles(cwd, sessionId, view, scopes, base, r
|
|
|
325
1003
|
const next = { files: temporalFileReceipts(current, updates) };
|
|
326
1004
|
decodeFileCohort(cwd, sessionId, root, next, sessionKey);
|
|
327
1005
|
const revision = fileRevision(next, root);
|
|
1006
|
+
const convertedScopes = SCOPES.filter((scope) => {
|
|
1007
|
+
const path = temporalScopePaths(cwd, sessionId, scope, root, sessionKey).meta;
|
|
1008
|
+
const previous = current.files.find((file) => file.path === path)?.content;
|
|
1009
|
+
const content = updates.find((file) => file.path === path)?.content;
|
|
1010
|
+
return previous !== undefined && content !== undefined && JSON.parse(previous).version !== JSON.parse(content).version;
|
|
1011
|
+
});
|
|
1012
|
+
const migrationBackup = convertedScopes.length > 0 ? preserveMigrationBasis(current, root, cwd, sessionId, sessionKey, { scopes: convertedScopes }, updates) : undefined;
|
|
328
1013
|
publishFileUpdates(current.files, updates, root);
|
|
329
|
-
return { base: next, revision, changed: updates.length > 0 };
|
|
1014
|
+
return { base: next, revision, changed: updates.length > 0, ...(migrationBackup ? { migrationBackup } : {}) };
|
|
1015
|
+
}
|
|
1016
|
+
/** Preserve exactly the converted transaction's owned basis, never discover other sessions or source files. */
|
|
1017
|
+
function preserveMigrationBasis(base, root, cwd, sessionId, sessionKey, conversion, updates) {
|
|
1018
|
+
const targetVersion = updates.some(file => file.path.endsWith("/meta.json") && file.content !== undefined && JSON.parse(file.content).version === 4) ? 4 : 3;
|
|
1019
|
+
const content = `${canonicalJson({ version: 1, kind: "state-flow-semantic-migration", targetVersion,
|
|
1020
|
+
root: resolve(root), cwd: resolve(cwd), sessionId, sessionKey, convertedScopes: conversion.scopes,
|
|
1021
|
+
...(conversion.lifecycle ? { lifecycleConversion: conversion.lifecycle } : {}),
|
|
1022
|
+
...(conversion.presence ? { presenceDependencies: conversion.presence.map((file) => ({ path: relative(root, file.path), present: file.present })) } : {}),
|
|
1023
|
+
updatedPaths: updates.map((file) => relative(root, file.path)),
|
|
1024
|
+
files: base.files.map((file) => ({ path: relative(root, file.path), identity: file.identity,
|
|
1025
|
+
bytes: file.bytes === undefined ? null : Buffer.from(file.bytes).toString("base64") })),
|
|
1026
|
+
})}\n`;
|
|
1027
|
+
const directory = resolve(dirname(root), `${basename(root)}-migration-backups`);
|
|
1028
|
+
assertStorageDirectory(directory);
|
|
1029
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
1030
|
+
const directoryStat = lstatSync(directory);
|
|
1031
|
+
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink() || (directoryStat.mode & 0o077) !== 0) {
|
|
1032
|
+
throw new Error("State Flow migration backup directory must be private; canonical files preserved");
|
|
1033
|
+
}
|
|
1034
|
+
const path = resolve(directory, `${createHash("sha256").update(content).digest("hex")}.json`);
|
|
1035
|
+
try {
|
|
1036
|
+
writeFileSync(path, content, { flag: "wx", mode: 0o600 });
|
|
1037
|
+
}
|
|
1038
|
+
catch (error) {
|
|
1039
|
+
if (error.code !== "EEXIST")
|
|
1040
|
+
throw error;
|
|
1041
|
+
}
|
|
1042
|
+
const stat = lstatSync(path);
|
|
1043
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || readFileSync(path, { encoding: "utf8", flag: constants.O_RDONLY | constants.O_NOFOLLOW }) !== content) {
|
|
1044
|
+
throw new Error("State Flow migration backup is unavailable or changed; canonical files preserved");
|
|
1045
|
+
}
|
|
1046
|
+
return path;
|
|
330
1047
|
}
|
|
331
1048
|
function publishFileUpdates(bases, updates, root) {
|
|
332
1049
|
writeOwnedFileUpdates(updates, bases, root);
|