@llblab/pi-kit 0.37.1 → 0.38.0
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 +5 -0
- package/README.md +2 -2
- 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 +2 -2
|
@@ -1,25 +1,35 @@
|
|
|
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
8
|
import {
|
|
9
|
-
assertOwnedFileUpdates, captureTemporalFileBases, parseScopeProvenance, parseScopeStream, restoreDurableFileBases,
|
|
10
|
-
serializeScopeMetadata, sessionRuntimePaths, temporalScopePaths, temporalStateFileUpdates, writeOwnedFileUpdates,
|
|
11
|
-
type DurableFileBase, type OwnedFileUpdate,
|
|
9
|
+
assertOwnedFileUpdates, captureTemporalFileBases, captureOwnedFileBases, captureOwnedFilePresence, classifyScopeStream, parseScopeProvenance, parseScopeStream, restoreDurableFileBases,
|
|
10
|
+
serializeScopeMetadata, serializeScopeStream, sessionRuntimePaths, temporalScopePaths, temporalStateFileUpdates, writeOwnedFileUpdates,
|
|
11
|
+
type DurableFileBase, type OwnedFileUpdate, type OwnedFilePresence, type SessionAddress,
|
|
12
12
|
} from "./durable.ts";
|
|
13
|
-
import type
|
|
14
|
-
import { MAX_HISTORY_LIMIT } from "./history.ts";
|
|
15
|
-
import { hashJson, sameJson } from "./json.ts";
|
|
16
|
-
import {
|
|
13
|
+
import { isInterpretableArtifactProvenance, parseArtifactProvenanceRegistry, type ArtifactProvenanceRegistry } from "./artifact.ts";
|
|
14
|
+
import { MAX_HISTORY_LIMIT, type AcceptedTransition } from "./history.ts";
|
|
15
|
+
import { canonicalJson, hashJson, isJsonValue, isObject, sameJson, type JsonObject } from "./json.ts";
|
|
16
|
+
import { prepareSerializedScopeMigration } from "./migration.ts";
|
|
17
|
+
import { RevisionUnavailableError, isFileRevision, parseSessionRuntime, prepareSessionPolicyMigration, captureLifecycleSnapshot, capturePrivateCausalBasis, serializeSessionRuntime, type FileRevision, type SessionRuntime, type SessionPolicyMigrationCandidate, type Snapshot } from "./snapshot.ts";
|
|
17
18
|
export { isFileRevision, type FileRevision } from "./snapshot.ts";
|
|
18
|
-
import type { StateScope } from "./state.ts";
|
|
19
|
-
import { validateTemporalState, type TemporalState } from "./temporal.ts";
|
|
19
|
+
import type { SemanticState, StateScope } from "./state.ts";
|
|
20
|
+
import { validateTemporalState, selectTemporalState, restoreSelectedTemporalState, temporalCausalWitnesses, constrainTemporalState, advanceTemporalState, createSelectedTemporalState, adoptSelectedTemporalStreams, validateScopeLineage, scopeLineageMatches, captureTemporalWitnesses, readTemporalView, type TemporalState, type SelectedTemporalState, type ExcludedTransitionWitness, type ScopeStream } from "./temporal.ts";
|
|
21
|
+
import { captureScopeSelection, type ScopeSelection } from "./selection.ts";
|
|
22
|
+
import { validateRetainedModules, type RetainedModules } from "./semantic-module.ts";
|
|
20
23
|
|
|
21
24
|
const SCOPES = ["global", "cwd", "session"] as const;
|
|
22
25
|
export interface TemporalFileBase { files: DurableFileBase[] }
|
|
26
|
+
type FilePublication = { base: TemporalFileBase; revision: FileRevision; changed: boolean; migrationBackup?: string };
|
|
27
|
+
|
|
28
|
+
/** Opaque CAS capture stays byte-exact; semantic consumers alone require lossless text. */
|
|
29
|
+
export function temporalFileContents(base: TemporalFileBase): Map<string, string | undefined> {
|
|
30
|
+
return new Map(base.files.map((file) => [file.path, file.bytes === undefined ? file.content
|
|
31
|
+
: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(file.bytes)]));
|
|
32
|
+
}
|
|
23
33
|
|
|
24
34
|
export function assertStorageDirectory(path: string): void {
|
|
25
35
|
const root = resolve(path);
|
|
@@ -94,6 +104,20 @@ export function withStoragePublicationLock<T>(repositoryRoot: string, action: (r
|
|
|
94
104
|
export interface StorageTransaction {
|
|
95
105
|
readonly capture: typeof captureTemporalFileBase;
|
|
96
106
|
readonly publish: typeof publishTemporalStateToFiles;
|
|
107
|
+
readonly prepareSelected: (cwd: string, sessionId: string, selection: ScopeSelection, root: string, sessionKey?: string) => SelectedFileCohort | undefined;
|
|
108
|
+
readonly observeSelectedShared: (cwd: string, selection: ScopeSelection, root: string) => SelectedReadObservation;
|
|
109
|
+
readonly persistSelectedPassive: (cwd: string, sessionId: string, base: SelectedFileCohort, root: string, sessionKey?: string) => SelectedFilePublication;
|
|
110
|
+
readonly hasSelectedCurrentAuthority: (cwd: string, sessionId: string, selection: ScopeSelection, root: string, sessionKey?: string) => boolean;
|
|
111
|
+
readonly inspectSelectedLifecycle: (cwd: string, sessionId: string, selection: ScopeSelection, root: string, sessionKey?: string) => SessionPolicyMigrationCandidate | undefined;
|
|
112
|
+
readonly prepareSelectedCurrent: (cwd: string, sessionId: string, selection: ScopeSelection, historyLimit: number, root: string, sessionKey?: string, expectedLifecycle?: TemporalFileBase) => SelectedCurrentCandidate;
|
|
113
|
+
readonly acceptSelectedCurrent: (cwd: string, sessionId: string, request: SelectedCurrentRequest, base: SelectedCurrentCandidate, root: string, sessionKey?: string) => SelectedFilePublication;
|
|
114
|
+
readonly publishSelected: (cwd: string, sessionId: string, request: SelectedPublicationRequest, base: SelectedFileCohort, root: string, sessionKey?: string) => SelectedFilePublication;
|
|
115
|
+
/** Caller must independently own a genuinely new session origin; absence alone grants no authority. */
|
|
116
|
+
readonly initializeSelectedSharedOrigin: (cwd: string, sessionId: string, request: SelectedOriginRequest, root: string, sessionKey?: string) => SelectedFilePublication;
|
|
117
|
+
readonly acquireSelectedSession: (cwd: string, sessionId: string, request: SelectedSessionAcquisitionRequest, root: string, sessionKey?: string) => SelectedFilePublication;
|
|
118
|
+
/** Native caller owns the fresh fork identity; never look up a parent while Session is excluded. */
|
|
119
|
+
readonly deferSelectedFork: (cwd: string, sessionId: string, request: SelectedOriginRequest, root: string, sessionKey?: string) => SelectedFilePublication;
|
|
120
|
+
readonly copySelectedFork: (cwd: string, sessionId: string, request: SelectedForkCopyRequest, root: string, sessionKey?: string) => SelectedFilePublication;
|
|
97
121
|
}
|
|
98
122
|
|
|
99
123
|
export class PublicationBusyError extends Error {
|
|
@@ -168,6 +192,7 @@ export async function withStorageTransaction<T>(
|
|
|
168
192
|
const root = resolve(repositoryRoot);
|
|
169
193
|
return withFilePublicationLock(resolve(root, ".state-flow-publication.lock"), async () => {
|
|
170
194
|
let active = true;
|
|
195
|
+
let selectedPublicationUsed = false;
|
|
171
196
|
const guard = (requestedRoot: string): void => {
|
|
172
197
|
if (!active) throw new Error("State Flow storage transaction has ended");
|
|
173
198
|
if (resolve(requestedRoot) !== root) throw new Error("State Flow storage transaction belongs to a different store");
|
|
@@ -178,6 +203,72 @@ export async function withStorageTransaction<T>(
|
|
|
178
203
|
guard(requestedRoot);
|
|
179
204
|
return { files: captureTemporalFileBases(cwd, sessionId, root, sessionKey) };
|
|
180
205
|
},
|
|
206
|
+
prepareSelected: (cwd, sessionId, selection, requestedRoot, sessionKey = sessionId) => {
|
|
207
|
+
guard(requestedRoot);
|
|
208
|
+
return prepareSelectedFileCohort(cwd, sessionId, selection, root, sessionKey);
|
|
209
|
+
},
|
|
210
|
+
observeSelectedShared: (cwd, selection, requestedRoot) => {
|
|
211
|
+
guard(requestedRoot);
|
|
212
|
+
return observeSelectedShared(cwd, selection, root);
|
|
213
|
+
},
|
|
214
|
+
persistSelectedPassive: (cwd, sessionId, base, requestedRoot, sessionKey = sessionId) => {
|
|
215
|
+
guard(requestedRoot);
|
|
216
|
+
if (selectedPublicationUsed) throw new Error("Selected State Flow publication capability was already used");
|
|
217
|
+
selectedPublicationUsed = true;
|
|
218
|
+
return persistSelectedPassive(cwd, sessionId, base, root, sessionKey);
|
|
219
|
+
},
|
|
220
|
+
hasSelectedCurrentAuthority: (cwd, sessionId, selection, requestedRoot, sessionKey = sessionId) => {
|
|
221
|
+
guard(requestedRoot);
|
|
222
|
+
return captureSelectedLifecycle(cwd, sessionId, captureScopeSelection(selection), root, sessionKey).lifecycle !== undefined;
|
|
223
|
+
},
|
|
224
|
+
inspectSelectedLifecycle: (cwd, sessionId, selection, requestedRoot, sessionKey = sessionId) => {
|
|
225
|
+
guard(requestedRoot);
|
|
226
|
+
return captureSelectedLifecycle(cwd, sessionId, captureScopeSelection(selection), root, sessionKey).lifecycle;
|
|
227
|
+
},
|
|
228
|
+
prepareSelectedCurrent: (cwd, sessionId, selection, historyLimit, requestedRoot, sessionKey = sessionId, expectedLifecycle) => {
|
|
229
|
+
guard(requestedRoot);
|
|
230
|
+
return prepareSelectedCurrent(cwd, sessionId, selection, historyLimit, root, sessionKey, expectedLifecycle);
|
|
231
|
+
},
|
|
232
|
+
acceptSelectedCurrent: (cwd, sessionId, request, base, requestedRoot, sessionKey = sessionId) => {
|
|
233
|
+
guard(requestedRoot);
|
|
234
|
+
if (selectedPublicationUsed) throw new Error("Selected State Flow publication capability was already used");
|
|
235
|
+
selectedPublicationUsed = true;
|
|
236
|
+
const current = prepareSelectedCurrent(cwd, sessionId, request.selection, request.historyLimit, root, sessionKey);
|
|
237
|
+
assertSelectedFileCohortBasis(base, current);
|
|
238
|
+
if ((request.lifecycle?.config.mode ?? current.lifecycle.runtime.config.mode) === "off") 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) throw new Error("Selected State Flow publication capability was already used");
|
|
245
|
+
selectedPublicationUsed = true;
|
|
246
|
+
return publishSelectedFileCohort(cwd, sessionId, request, base, root, sessionKey);
|
|
247
|
+
},
|
|
248
|
+
initializeSelectedSharedOrigin: (cwd, sessionId, request, requestedRoot, sessionKey = sessionId) => {
|
|
249
|
+
guard(requestedRoot);
|
|
250
|
+
if (selectedPublicationUsed) throw new Error("Selected State Flow publication capability was already used");
|
|
251
|
+
selectedPublicationUsed = true;
|
|
252
|
+
return acceptSelectedOrigin(cwd, sessionId, { ...request, kind: "shared-new" }, root, sessionKey);
|
|
253
|
+
},
|
|
254
|
+
acquireSelectedSession: (cwd, sessionId, request, requestedRoot, sessionKey = sessionId) => {
|
|
255
|
+
guard(requestedRoot);
|
|
256
|
+
if (selectedPublicationUsed) throw new Error("Selected State Flow publication capability was already used");
|
|
257
|
+
selectedPublicationUsed = true;
|
|
258
|
+
return acceptSelectedOrigin(cwd, sessionId, request, root, sessionKey);
|
|
259
|
+
},
|
|
260
|
+
deferSelectedFork: (cwd, sessionId, request, requestedRoot, sessionKey = sessionId) => {
|
|
261
|
+
guard(requestedRoot);
|
|
262
|
+
if (selectedPublicationUsed) throw new Error("Selected State Flow publication capability was already used");
|
|
263
|
+
selectedPublicationUsed = true;
|
|
264
|
+
return acceptSelectedOrigin(cwd, sessionId, { ...request, kind: "shared-deferred" }, root, sessionKey);
|
|
265
|
+
},
|
|
266
|
+
copySelectedFork: (cwd, sessionId, request, requestedRoot, sessionKey = sessionId) => {
|
|
267
|
+
guard(requestedRoot);
|
|
268
|
+
if (selectedPublicationUsed) throw new Error("Selected State Flow publication capability was already used");
|
|
269
|
+
selectedPublicationUsed = true;
|
|
270
|
+
return acceptSelectedOrigin(cwd, sessionId, request, root, sessionKey);
|
|
271
|
+
},
|
|
181
272
|
publish: (cwd, sessionId, view, scopes, base, requestedRoot, runtime, sessionKey, provenance, runtimeOnly) => {
|
|
182
273
|
guard(requestedRoot);
|
|
183
274
|
return publishLockedTemporalStateToFiles(cwd, sessionId, view, scopes, base, root, runtime, sessionKey, provenance, runtimeOnly);
|
|
@@ -202,14 +293,21 @@ export function planTemporalPublication(
|
|
|
202
293
|
provenance?: Readonly<Record<StateScope, ArtifactProvenanceRegistry>>,
|
|
203
294
|
): { updates: OwnedFileUpdate[]; changedScopes: StateScope[] } {
|
|
204
295
|
const candidates = temporalStateFileUpdates(cwd, sessionId, view, scopes, root, sessionKey);
|
|
205
|
-
const
|
|
296
|
+
const decoded = temporalFileContents(current);
|
|
297
|
+
const files = new Map(current.files.map((file) => [file.path, { ...file, content: decoded.get(file.path) }]));
|
|
206
298
|
if (runtimeOnly && (scopes.length !== 0 || provenance !== undefined)) throw new Error("Runtime-only publication cannot write semantic scopes or artifact provenance");
|
|
207
299
|
const changedScopes: StateScope[] = [];
|
|
300
|
+
const migrations = new Map<StateScope, ReturnType<typeof prepareSerializedScopeMigration>>();
|
|
208
301
|
for (const scope of SCOPES) {
|
|
209
302
|
const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
210
303
|
const previous = parseScopeStream(files.get(paths.checkpoint)!.content, files.get(paths.patches)!.content, scope,
|
|
211
304
|
scope === "cwd" ? cwd : undefined, files.get(paths.meta)!.content);
|
|
212
|
-
if (runtimeOnly
|
|
305
|
+
if (!runtimeOnly && scopes.includes(scope) && previous !== undefined && JSON.parse(files.get(paths.meta)!.content!).version !== (view.scopes[scope].modules === undefined ? 3 : 4)) {
|
|
306
|
+
migrations.set(scope, prepareSerializedScopeMigration({
|
|
307
|
+
checkpoint: files.get(paths.checkpoint)!.bytes, patches: files.get(paths.patches)!.bytes, meta: files.get(paths.meta)!.bytes,
|
|
308
|
+
}, scope, scope === "cwd" ? cwd : undefined, view.scopes[scope].modules));
|
|
309
|
+
}
|
|
310
|
+
if (runtimeOnly || (previous !== undefined && !migrations.has(scope) && sameJson(previous, view.scopes[scope]))) continue;
|
|
213
311
|
if (!scopes.includes(scope)) throw new Error(`Temporal scope update omitted a changed stream: ${scope}`);
|
|
214
312
|
changedScopes.push(scope);
|
|
215
313
|
}
|
|
@@ -222,7 +320,21 @@ export function planTemporalPublication(
|
|
|
222
320
|
const currentFile = files.get(paths.meta)!;
|
|
223
321
|
if (!changedScopes.includes(scope)
|
|
224
322
|
&& (provenance === undefined || sameJson(parseScopeProvenance(currentFile.content, paths.meta), registry))) continue;
|
|
225
|
-
const
|
|
323
|
+
const migration = migrations.get(scope);
|
|
324
|
+
const unchangedRegistry = sameJson(parseScopeProvenance(currentFile.content, paths.meta), registry);
|
|
325
|
+
let content = serializeScopeMetadata(unchangedRegistry ? undefined : registry, view.scopes[scope], scope,
|
|
326
|
+
scope === "cwd" ? cwd : undefined, migration?.target.meta ?? currentFile.content);
|
|
327
|
+
if (migration && !unchangedRegistry) {
|
|
328
|
+
const raw = JSON.parse(migration.target.meta);
|
|
329
|
+
const next = JSON.parse(content);
|
|
330
|
+
const prior = parseScopeProvenance(currentFile.content, paths.meta);
|
|
331
|
+
if (isObject(raw.artifacts) && isObject(next.artifacts)) {
|
|
332
|
+
for (const [path, value] of Object.entries(raw.artifacts)) if (prior[path]?.malformed) {
|
|
333
|
+
if (!Object.hasOwn(next.artifacts, path)) Object.defineProperty(next.artifacts, path, { value, enumerable: true });
|
|
334
|
+
}
|
|
335
|
+
content = `${canonicalJson(next)}\n`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
226
338
|
if (currentFile.content !== content) provenanceUpdates.push({ path: paths.meta, content });
|
|
227
339
|
}
|
|
228
340
|
}
|
|
@@ -262,7 +374,7 @@ function fileRevision(base: TemporalFileBase, root: string): FileRevision {
|
|
|
262
374
|
}
|
|
263
375
|
|
|
264
376
|
function decodeFileCohort(cwd: string, sessionId: string, root: string, base: TemporalFileBase, sessionKey = sessionId) {
|
|
265
|
-
const files =
|
|
377
|
+
const files = temporalFileContents(base);
|
|
266
378
|
const scopes = {} as TemporalState["scopes"];
|
|
267
379
|
for (const scope of SCOPES) {
|
|
268
380
|
const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
@@ -284,6 +396,610 @@ function decodeFileCohort(cwd: string, sessionId: string, root: string, base: Te
|
|
|
284
396
|
return { runtime, view, provenance };
|
|
285
397
|
}
|
|
286
398
|
|
|
399
|
+
export interface SelectedFileCohort {
|
|
400
|
+
/** Exact byte dependencies only; unrelated excluded bodies are not a CAS input. */
|
|
401
|
+
basis: TemporalFileBase;
|
|
402
|
+
/** Separate existence/type dependencies, not byte-CAS receipts or write permission. */
|
|
403
|
+
presence: OwnedFilePresence[];
|
|
404
|
+
lifecycle: SessionPolicyMigrationCandidate;
|
|
405
|
+
view: SelectedTemporalState;
|
|
406
|
+
witnesses: ExcludedTransitionWitness[];
|
|
407
|
+
provenance: Partial<Record<StateScope, ArtifactProvenanceRegistry>>;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Read-only Start/refresh candidate, not an admitted persisted view or a publication capability. */
|
|
411
|
+
export interface SelectedCurrentCandidate extends SelectedFileCohort {
|
|
412
|
+
alignment: "retained" | "current-origin";
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export type SelectedProvenanceUpdates = Partial<Record<StateScope, ArtifactProvenanceRegistry>>;
|
|
416
|
+
|
|
417
|
+
/** Reject excluded owners before their values/getters; metadata values remain runtime-owned evidence, not source authentication. */
|
|
418
|
+
export function captureSelectedProvenanceUpdates(value: SelectedProvenanceUpdates | undefined, selection: ScopeSelection): SelectedProvenanceUpdates | undefined {
|
|
419
|
+
if (value === undefined) return undefined;
|
|
420
|
+
if (!isObject(value)) throw new Error("Selected State Flow provenance updates must be an object");
|
|
421
|
+
for (const scope of Object.keys(value)) if (!selection.includes(scope as StateScope)) throw new Error(`State Flow ${scope} provenance owner is excluded`);
|
|
422
|
+
if (!isJsonValue(value)) throw new Error("Selected State Flow provenance updates must be finite JSON");
|
|
423
|
+
const result: SelectedProvenanceUpdates = {};
|
|
424
|
+
for (const scope of selection) if (Object.hasOwn(value, scope)) {
|
|
425
|
+
const registry = parseArtifactProvenanceRegistry(value[scope]);
|
|
426
|
+
if (!sameJson(registry, value[scope]) || Object.values(registry).some((entry) => entry.malformed)) throw new Error("Invalid selected State Flow provenance updates");
|
|
427
|
+
if (Object.keys(registry).length > 0) result[scope] = structuredClone(registry);
|
|
428
|
+
}
|
|
429
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Host-admitted additions only; never inferred from an explicit output view or a registry. */
|
|
433
|
+
export type SelectedModuleAdditions = Partial<Record<StateScope, RetainedModules>>;
|
|
434
|
+
|
|
435
|
+
export function captureSelectedModuleAdditions(value: unknown, selection: ScopeSelection): SelectedModuleAdditions | undefined {
|
|
436
|
+
if (value === undefined) return undefined;
|
|
437
|
+
if (!isObject(value)) throw new Error("Invalid selected State Flow module additions");
|
|
438
|
+
const keys = Reflect.ownKeys(value);
|
|
439
|
+
if (keys.some(key => typeof key !== "string" || !selection.includes(key as StateScope))) throw new Error("Selected State Flow module owner is excluded");
|
|
440
|
+
const captured: SelectedModuleAdditions = {};
|
|
441
|
+
for (const key of keys as StateScope[]) {
|
|
442
|
+
const property = Object.getOwnPropertyDescriptor(value, key)!;
|
|
443
|
+
if (!property.enumerable || !("value" in property)) throw new Error("Selected State Flow module additions require detached data");
|
|
444
|
+
const modules: unknown = property.value;
|
|
445
|
+
validateRetainedModules(modules);
|
|
446
|
+
if (Object.keys(modules).length === 0) 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
|
+
|
|
452
|
+
export interface SelectedCurrentRequest {
|
|
453
|
+
selection: ScopeSelection;
|
|
454
|
+
historyLimit: number;
|
|
455
|
+
lifecycle?: Snapshot;
|
|
456
|
+
transition?: AcceptedTransition;
|
|
457
|
+
provenance?: SelectedProvenanceUpdates;
|
|
458
|
+
moduleAdditions?: SelectedModuleAdditions;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export interface SelectedPublicationRequest {
|
|
462
|
+
view: SelectedTemporalState;
|
|
463
|
+
historyLimit: number;
|
|
464
|
+
/** Exact authored replay, including runtime-owned cascades/response when the caller owns them. */
|
|
465
|
+
transition?: AcceptedTransition;
|
|
466
|
+
/** Closed run/mode fields only. Identity, acquisition and causal witnesses cannot be supplied here. */
|
|
467
|
+
lifecycle?: Snapshot;
|
|
468
|
+
provenance?: SelectedProvenanceUpdates;
|
|
469
|
+
moduleAdditions?: SelectedModuleAdditions;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export interface SelectedOriginRequest {
|
|
473
|
+
selection: ScopeSelection;
|
|
474
|
+
snapshot: Snapshot;
|
|
475
|
+
historyLimit: number;
|
|
476
|
+
/** Synchronous first authoring on the same source-derived origin, before any owner is published. */
|
|
477
|
+
author?: (basis: { view: SelectedTemporalState; snapshot: Snapshot; provenance: SelectedProvenanceUpdates },
|
|
478
|
+
publish: (snapshot: Snapshot, transition?: AcceptedTransition, provenance?: SelectedProvenanceUpdates, moduleAdditions?: SelectedModuleAdditions) => SelectedFilePublication) => void;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export type SelectedSessionAcquisitionRequest = SelectedOriginRequest & (
|
|
482
|
+
| { kind: "new" }
|
|
483
|
+
| { kind: "unacquired"; base: SelectedFileCohort }
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
export type SelectedForkCopyRequest = SelectedOriginRequest & { source: SessionAddress } & (
|
|
487
|
+
| { kind: "fork-new" }
|
|
488
|
+
| { kind: "fork-deferred"; base: SelectedFileCohort }
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
type SelectedOriginAdmission = SelectedSessionAcquisitionRequest | SelectedForkCopyRequest | (SelectedOriginRequest & { kind: "shared-new" | "shared-deferred" });
|
|
492
|
+
|
|
493
|
+
/** Read-only availability, not a lifecycle cohort, accepted publication basis or private acquisition claim. */
|
|
494
|
+
export interface SelectedReadObservation {
|
|
495
|
+
selection: ScopeSelection;
|
|
496
|
+
view?: SelectedTemporalState;
|
|
497
|
+
provenance: SelectedProvenanceUpdates;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function observeSelectedShared(cwd: string, selection: ScopeSelection, root: string): SelectedReadObservation {
|
|
501
|
+
const wanted = captureScopeSelection(selection);
|
|
502
|
+
if (wanted.includes("session")) throw new Error("Shared State Flow observation cannot select Session");
|
|
503
|
+
const layouts = wanted.map(scope => ({ scope, paths: temporalScopePaths(cwd, "", scope, root) }));
|
|
504
|
+
if (captureOwnedFilePresence(layouts.map(({ paths }) => resolve(paths.directory, "state.json")), root).some(file => file.present)) {
|
|
505
|
+
throw new Error("Unsupported State Flow selected semantic layout");
|
|
506
|
+
}
|
|
507
|
+
const basis = { files: captureOwnedFileBases(layouts.flatMap(({ paths }) => [paths.checkpoint, paths.patches, paths.meta]), root) };
|
|
508
|
+
const sources = temporalFileContents(basis);
|
|
509
|
+
const streams: Partial<Record<StateScope, ScopeStream>> = {};
|
|
510
|
+
const provenance: SelectedProvenanceUpdates = {};
|
|
511
|
+
for (const { scope, paths } of layouts) {
|
|
512
|
+
const bodies = [sources.get(paths.checkpoint), sources.get(paths.patches), sources.get(paths.meta)];
|
|
513
|
+
if (bodies.every(body => body === undefined)) continue;
|
|
514
|
+
if (bodies.some(body => body === undefined)) throw new Error(`Incomplete selected State Flow ${scope} observation`);
|
|
515
|
+
streams[scope] = parseScopeStream(bodies[0], bodies[1], scope, scope === "cwd" ? cwd : undefined, bodies[2])!;
|
|
516
|
+
provenance[scope] = parseScopeProvenance(bodies[2], paths.meta);
|
|
517
|
+
}
|
|
518
|
+
const present = wanted.filter(scope => streams[scope] !== undefined);
|
|
519
|
+
return { selection: wanted, provenance, ...(present.length === 0 ? {} : {
|
|
520
|
+
view: adoptSelectedTemporalStreams(streams, captureScopeSelection(present), `observation:${randomUUID()}`, MAX_HISTORY_LIMIT),
|
|
521
|
+
}) };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export interface SelectedFilePublication {
|
|
525
|
+
cohort: SelectedFileCohort;
|
|
526
|
+
changed: boolean;
|
|
527
|
+
migrationBackup?: string;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Await a fresh guarded transaction; no private initialization or fork acquisition is admitted. */
|
|
531
|
+
export async function publishSelectedTemporalStateToFiles(
|
|
532
|
+
cwd: string, sessionId: string, request: SelectedPublicationRequest, base: SelectedFileCohort, root: string, sessionKey = sessionId, signal?: AbortSignal,
|
|
533
|
+
): Promise<SelectedFilePublication> {
|
|
534
|
+
return withStorageTransaction(root, (transaction) => transaction.publishSelected(cwd, sessionId, request, base, root, sessionKey), signal);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function publishSelectedFileCohort(
|
|
538
|
+
cwd: string, sessionId: string, request: SelectedPublicationRequest, base: SelectedFileCohort, root: string, sessionKey: string,
|
|
539
|
+
): SelectedFilePublication {
|
|
540
|
+
const current = prepareSelectedFileCohort(cwd, sessionId, base.view.selection, root, sessionKey);
|
|
541
|
+
if (!current) throw new Error("Selected State Flow source authority is unavailable");
|
|
542
|
+
assertSelectedFileCohortBasis(base, current);
|
|
543
|
+
const expected = selectedReplaySuccessor(current.view, request.historyLimit, request.transition, request.moduleAdditions);
|
|
544
|
+
if (!sameJson(expected, request.view)) throw new Error("Selected State Flow output is not the admitted replay successor");
|
|
545
|
+
return publishSelectedSuccessor(cwd, sessionId, current, expected, request.historyLimit, request.lifecycle, root, sessionKey, request.provenance);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Strictly lifecycle-only; a mode choice cannot repair, convert or refresh semantic authority. */
|
|
549
|
+
function persistSelectedPassive(cwd: string, sessionId: string, base: SelectedFileCohort, root: string, sessionKey: string): SelectedFilePublication {
|
|
550
|
+
const paths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
551
|
+
const lifecyclePaths = new Set([paths.config, paths.runtime]);
|
|
552
|
+
const lifecycleBasis = { files: base.basis.files.filter(file => lifecyclePaths.has(file.path)) };
|
|
553
|
+
const current = prepareSelectedFileCohort(cwd, sessionId, base.view.selection, root, sessionKey, lifecycleBasis);
|
|
554
|
+
if (!current) throw new Error("Selected State Flow source authority is unavailable");
|
|
555
|
+
assertSelectedFileCohortBasis(base, current);
|
|
556
|
+
const source = current.lifecycle.runtime.meta;
|
|
557
|
+
const snapshot = captureLifecycleSnapshot({ config: { mode: "passive" }, meta: { step: source.step,
|
|
558
|
+
...Object.fromEntries(["specification", "bootstrap", "validation"].filter(key => Object.hasOwn(source, key)).map(key => [key, source[key]])) } });
|
|
559
|
+
return publishSelectedSuccessor(cwd, sessionId, current, current.view, MAX_HISTORY_LIMIT, snapshot, root, sessionKey, undefined, true);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function selectedReplaySuccessor(
|
|
563
|
+
view: SelectedTemporalState, historyLimit: number, transition?: AcceptedTransition, additions?: SelectedModuleAdditions,
|
|
564
|
+
): SelectedTemporalState {
|
|
565
|
+
const current = constrainTemporalState(view, historyLimit);
|
|
566
|
+
const admitted = captureSelectedModuleAdditions(additions, current.selection);
|
|
567
|
+
if (admitted !== undefined) {
|
|
568
|
+
for (const [owner, modules] of Object.entries(admitted)) {
|
|
569
|
+
const scope = owner as StateScope, stream = current.scopes[scope]!;
|
|
570
|
+
for (const id of Object.keys(modules)) {
|
|
571
|
+
if (Object.hasOwn(stream.modules ?? {}, id)) throw new Error("Selected State Flow module descriptor already exists");
|
|
572
|
+
if (!transition?.transitions.some(item => item.scope === scope && Object.hasOwn(item.patch, id))) {
|
|
573
|
+
throw new Error("Selected State Flow module admission requires a content transition");
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
stream.modules = { ...stream.modules, ...modules };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (transition === undefined) return current;
|
|
580
|
+
if (!isObject(transition)) throw new Error("Invalid selected State Flow replay request");
|
|
581
|
+
const next = advanceTemporalState(current, transition.transitions, transition.id, historyLimit);
|
|
582
|
+
for (const [owner, modules] of Object.entries(admitted ?? {})) {
|
|
583
|
+
const content = readTemporalView(next, 0, owner as StateScope, historyLimit);
|
|
584
|
+
for (const id of Object.keys(modules)) {
|
|
585
|
+
const value = content[id];
|
|
586
|
+
if (!isObject(value) || Object.keys(value).length === 0) throw new Error("Selected State Flow module admission requires nonempty content");
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return next;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Merge known owned evidence without curating unbound or forward-compatible metadata. */
|
|
593
|
+
function mergeSelectedArtifactProvenance(
|
|
594
|
+
priorMeta: string | undefined, path: string, before: SemanticState["artifacts"], after: SemanticState["artifacts"],
|
|
595
|
+
updates?: ArtifactProvenanceRegistry, inherited?: { registry: ArtifactProvenanceRegistry; raw?: JsonObject },
|
|
596
|
+
) {
|
|
597
|
+
const previous = parseScopeProvenance(priorMeta, path);
|
|
598
|
+
const registry = { ...previous, ...inherited?.registry, ...updates };
|
|
599
|
+
const removed = Object.keys(registry).filter(key => isInterpretableArtifactProvenance(registry[key])
|
|
600
|
+
&& Object.hasOwn(before ?? {}, key) && !Object.hasOwn(after ?? {}, key));
|
|
601
|
+
for (const key of removed) delete registry[key];
|
|
602
|
+
const raw = priorMeta === undefined ? { version: 3 } : JSON.parse(priorMeta);
|
|
603
|
+
const artifacts = { ...raw.artifacts, ...inherited?.raw, ...updates };
|
|
604
|
+
for (const key of removed) delete artifacts[key];
|
|
605
|
+
const changed = !sameJson(previous, registry) || (inherited?.raw !== undefined
|
|
606
|
+
&& (raw.artifacts === undefined || !sameJson(raw.artifacts, artifacts)));
|
|
607
|
+
return { registry, changed, metadata: changed ? canonicalJson({ ...raw, artifacts }) : priorMeta };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Check raw predecessor namespaces before selected metadata3→4 admission can hide ignored data. */
|
|
611
|
+
function assertSelectedModuleConversion(basis: TemporalFileBase, paths: ReturnType<typeof temporalScopePaths>, scope: StateScope, cwd: string, modules: RetainedModules | undefined): void {
|
|
612
|
+
if (modules === undefined) return;
|
|
613
|
+
const files = new Map(basis.files.map(file => [file.path, file.bytes]));
|
|
614
|
+
prepareSerializedScopeMigration({ checkpoint: files.get(paths.checkpoint), patches: files.get(paths.patches), meta: files.get(paths.meta) },
|
|
615
|
+
scope, scope === "cwd" ? cwd : undefined, modules);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** One byte planner for replay successors and current-only adoption; neither admits new private authority. */
|
|
619
|
+
function publishSelectedSuccessor(
|
|
620
|
+
cwd: string, sessionId: string, current: Pick<SelectedFileCohort, "basis" | "presence" | "lifecycle" | "view">,
|
|
621
|
+
expected: SelectedTemporalState, historyLimit: number, requestedLifecycle: Snapshot | undefined, root: string, sessionKey: string,
|
|
622
|
+
requestedProvenance?: SelectedProvenanceUpdates, lifecycleOnly = false,
|
|
623
|
+
): SelectedFilePublication {
|
|
624
|
+
const provenance = captureSelectedProvenanceUpdates(requestedProvenance, expected.selection);
|
|
625
|
+
const nextProvenance: SelectedProvenanceUpdates = {};
|
|
626
|
+
const lifecycleKeys = ["step", "specification", "bootstrap", "validation"];
|
|
627
|
+
const lifecycle = requestedLifecycle === undefined ? undefined : captureLifecycleSnapshot(requestedLifecycle);
|
|
628
|
+
const paths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
629
|
+
const metadata = { ...current.lifecycle.runtime.meta };
|
|
630
|
+
if (lifecycle) {
|
|
631
|
+
for (const key of lifecycleKeys) delete metadata[key];
|
|
632
|
+
Object.assign(metadata, lifecycle.meta);
|
|
633
|
+
}
|
|
634
|
+
if (!lifecycleOnly && metadata.privateAcquisition === "acquired") {
|
|
635
|
+
if (expected.selection.includes("session")) delete metadata.privateCausalBasis;
|
|
636
|
+
else if (current.lifecycle.sourceVersion === 1 || current.lifecycle.runtime.config.scopes.includes("session")) {
|
|
637
|
+
// Retain the admitted source claim before replacing the selected causal origin.
|
|
638
|
+
// The requested view may already omit Session or use a new shared-only origin.
|
|
639
|
+
if (current.lifecycle.sourceVersion === 1 && !sameJson(current.view.lineage, current.lifecycle.runtime.meta.lineage)) {
|
|
640
|
+
throw new Error("Insufficient State Flow private causal basis before legacy realignment");
|
|
641
|
+
}
|
|
642
|
+
metadata.privateCausalBasis = capturePrivateCausalBasis({ lineage: current.lifecycle.runtime.meta.lineage,
|
|
643
|
+
causalWitnesses: current.lifecycle.sourceVersion === 1 ? temporalCausalWitnesses(current.view, MAX_HISTORY_LIMIT)
|
|
644
|
+
: current.lifecycle.runtime.meta.causalWitnesses ?? [] });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
metadata.lineage = structuredClone(expected.lineage);
|
|
648
|
+
metadata.causalWitnesses = temporalCausalWitnesses(expected, historyLimit);
|
|
649
|
+
const targetLifecycle = prepareSessionPolicyMigration(
|
|
650
|
+
`${canonicalJson({ mode: lifecycle === undefined ? current.lifecycle.runtime.config.mode : lifecycle.config.mode, scopes: expected.selection })}\n`,
|
|
651
|
+
`${canonicalJson(metadata)}\n`, cwd, sessionId, expected.selection,
|
|
652
|
+
)!;
|
|
653
|
+
const sources = temporalFileContents(current.basis);
|
|
654
|
+
const updates: OwnedFileUpdate[] = [];
|
|
655
|
+
const convertedScopes: StateScope[] = [];
|
|
656
|
+
for (const scope of expected.selection) {
|
|
657
|
+
const scopePaths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
658
|
+
const priorMeta = sources.get(scopePaths.meta);
|
|
659
|
+
const converting = priorMeta !== undefined && JSON.parse(priorMeta).version !== (expected.scopes[scope]!.modules === undefined ? 3 : 4);
|
|
660
|
+
if (converting) {
|
|
661
|
+
assertSelectedModuleConversion(current.basis, scopePaths, scope, cwd, expected.scopes[scope]!.modules);
|
|
662
|
+
convertedScopes.push(scope);
|
|
663
|
+
}
|
|
664
|
+
const previous = parseScopeStream(sources.get(scopePaths.checkpoint), sources.get(scopePaths.patches), scope, scope === "cwd" ? cwd : undefined, priorMeta);
|
|
665
|
+
const merged = mergeSelectedArtifactProvenance(priorMeta, scopePaths.meta,
|
|
666
|
+
readTemporalView(current.view, 0, scope, MAX_HISTORY_LIMIT).artifacts,
|
|
667
|
+
readTemporalView(expected, 0, scope, MAX_HISTORY_LIMIT).artifacts, provenance?.[scope]);
|
|
668
|
+
nextProvenance[scope] = merged.registry;
|
|
669
|
+
const metadataChanged = merged.changed;
|
|
670
|
+
const streamChanged = converting || previous === undefined || !sameJson(previous, expected.scopes[scope]);
|
|
671
|
+
if (!streamChanged && !metadataChanged) continue;
|
|
672
|
+
const stream = expected.scopes[scope]!;
|
|
673
|
+
if (streamChanged) {
|
|
674
|
+
const wire = serializeScopeStream(stream, scope, scope === "cwd" ? cwd : undefined);
|
|
675
|
+
updates.push({ path: scopePaths.checkpoint, content: wire.checkpoint }, { path: scopePaths.patches, content: wire.patches });
|
|
676
|
+
}
|
|
677
|
+
updates.push({ path: scopePaths.meta, content: serializeScopeMetadata(undefined, stream, scope, scope === "cwd" ? cwd : undefined, merged.metadata) });
|
|
678
|
+
}
|
|
679
|
+
if (sources.get(paths.config) !== targetLifecycle.target.config) updates.push({ path: paths.config, content: targetLifecycle.target.config });
|
|
680
|
+
if (sources.get(paths.runtime) !== targetLifecycle.target.runtime) updates.push({ path: paths.runtime, content: targetLifecycle.target.runtime });
|
|
681
|
+
if (lifecycleOnly && updates.some(update => update.path !== paths.config && update.path !== paths.runtime)) {
|
|
682
|
+
throw new Error("Selected Passive persistence requires lifecycle v2 and semantic v3 without semantic changes");
|
|
683
|
+
}
|
|
684
|
+
const receipts = { files: temporalFileReceipts(current.basis, updates) };
|
|
685
|
+
const cohort = decodeSelectedFileCohort(cwd, sessionId, expected.selection, root, sessionKey, { lifecycle: targetLifecycle, basis: receipts, presence: current.presence });
|
|
686
|
+
if (!sameJson(cohort.view, expected) || !sameJson(cohort.provenance, nextProvenance)) throw new Error("Selected State Flow receipt does not match accepted replay");
|
|
687
|
+
const lifecycleConversion = current.lifecycle.sourceVersion === 1 ? { sourceVersion: 1 as const, targetVersion: 2 as const } : undefined;
|
|
688
|
+
const assertPresence = (): void => {
|
|
689
|
+
if (!sameJson(current.presence, captureOwnedFilePresence(current.presence.map((file) => file.path), root))) throw new Error("Selected State Flow presence dependencies changed concurrently");
|
|
690
|
+
};
|
|
691
|
+
assertPresence();
|
|
692
|
+
const migrationBackup = convertedScopes.length > 0 || lifecycleConversion !== undefined
|
|
693
|
+
? preserveMigrationBasis(current.basis, root, cwd, sessionId, sessionKey, { scopes: convertedScopes, lifecycle: lifecycleConversion, presence: current.presence }, updates) : undefined;
|
|
694
|
+
assertPresence();
|
|
695
|
+
publishFileUpdates(current.basis.files, updates, root);
|
|
696
|
+
return { cohort, changed: updates.length > 0, ...(migrationBackup ? { migrationBackup } : {}) };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Initialize only genuinely absent private authority or first acquisition from an admitted unacquired owner. */
|
|
700
|
+
function acceptSelectedOrigin(
|
|
701
|
+
cwd: string, sessionId: string, request: SelectedOriginAdmission, root: string, sessionKey: string,
|
|
702
|
+
): SelectedFilePublication {
|
|
703
|
+
const wanted = captureScopeSelection(request.selection);
|
|
704
|
+
if (!["shared-new", "shared-deferred", "new", "unacquired", "fork-new", "fork-deferred"].includes(request.kind)) throw new Error("Invalid selected origin admission kind");
|
|
705
|
+
const copying = request.kind === "fork-new" || request.kind === "fork-deferred";
|
|
706
|
+
const acquiring = request.kind === "new" || request.kind === "unacquired" || copying;
|
|
707
|
+
if (acquiring !== wanted.includes("session")) throw new Error(acquiring ? "Selected private acquisition requires Session" : "Selected shared origin cannot acquire Session");
|
|
708
|
+
const snapshot = captureLifecycleSnapshot(request.snapshot);
|
|
709
|
+
if (snapshot.config.mode === "off") throw new Error("Off cannot initialize State Flow memory");
|
|
710
|
+
if ((request.kind === "shared-deferred" || request.kind === "fork-new") && (snapshot.meta.step !== 0 || snapshot.meta.specification !== undefined || snapshot.meta.validation !== undefined)) {
|
|
711
|
+
throw new Error("Selected fork deferral requires a fresh child lifecycle");
|
|
712
|
+
}
|
|
713
|
+
const { lifecycle, lifecycleFiles, presence } = captureSelectedLifecycle(cwd, sessionId, wanted, root, sessionKey);
|
|
714
|
+
let prior: SelectedFileCohort | undefined;
|
|
715
|
+
if (request.kind === "unacquired" || request.kind === "fork-deferred") {
|
|
716
|
+
const required = request.kind === "fork-deferred" ? "deferred" : "unacquired";
|
|
717
|
+
if (!lifecycle || lifecycle.sourceVersion !== 2 || lifecycle.runtime.meta.privateAcquisition !== required) {
|
|
718
|
+
throw new Error(`Selected private acquisition requires admitted lifecycle-v2 ${required} authority`);
|
|
719
|
+
}
|
|
720
|
+
prior = prepareSelectedFileCohort(cwd, sessionId, request.base.view.selection, root, sessionKey);
|
|
721
|
+
if (!prior) throw new Error("Selected private acquisition source is unavailable");
|
|
722
|
+
assertSelectedFileCohortBasis(request.base, prior);
|
|
723
|
+
const fields = Object.fromEntries(["step", "specification", "bootstrap", "validation"]
|
|
724
|
+
.filter((key) => Object.hasOwn(lifecycle.runtime.meta, key)).map((key) => [key, lifecycle.runtime.meta[key]]));
|
|
725
|
+
if (!sameJson(fields, snapshot.meta)) throw new Error("Selected private acquisition cannot reset run lifecycle fields");
|
|
726
|
+
} else if (lifecycle) throw new Error("Existing State Flow session requires current-memory acceptance, not a new origin");
|
|
727
|
+
const copy = copying ? captureSelectedForkSource(cwd, sessionId, sessionKey, request.source, root) : undefined;
|
|
728
|
+
const scopePaths = wanted.map((scope) => ({ scope, paths: temporalScopePaths(cwd, sessionId, scope, root, sessionKey) }));
|
|
729
|
+
const layouts = captureOwnedFilePresence(scopePaths.filter(({ scope }) => scope !== "session").map(({ paths }) => resolve(paths.directory, "state.json")), root);
|
|
730
|
+
if (layouts.some((file) => file.present)) throw new Error("Unsupported State Flow selected semantic layout");
|
|
731
|
+
presence.push(...layouts);
|
|
732
|
+
const scopeFiles = captureOwnedFileBases(scopePaths.flatMap(({ paths }) => [paths.checkpoint, paths.patches, paths.meta]), root);
|
|
733
|
+
const basis = { files: [...new Map([...lifecycleFiles, ...scopeFiles, ...(prior?.basis.files ?? []), ...(copy?.basis.files ?? [])].map((file) => [file.path, file])).values()] };
|
|
734
|
+
if (prior) {
|
|
735
|
+
const recaptured = new Map([...lifecycleFiles, ...scopeFiles].map((file) => [file.path, file.identity]));
|
|
736
|
+
if (prior.basis.files.some((file) => recaptured.has(file.path) && recaptured.get(file.path) !== file.identity)) throw new Error("Selected acquisition source changed concurrently");
|
|
737
|
+
}
|
|
738
|
+
const sources = temporalFileContents(basis);
|
|
739
|
+
const origin = `files:selected-origin:${randomUUID()}`;
|
|
740
|
+
const fresh = createSelectedTemporalState(Object.fromEntries(wanted.map((scope) => [scope, {}])), wanted, `${origin}:empty`, request.historyLimit);
|
|
741
|
+
const streams: Partial<Record<StateScope, ScopeStream>> = {};
|
|
742
|
+
const existing = new Set<StateScope>();
|
|
743
|
+
for (const { scope, paths } of scopePaths) {
|
|
744
|
+
const values = [paths.checkpoint, paths.patches, paths.meta].map((path) => sources.get(path));
|
|
745
|
+
if (scope === "session" && values.some((value) => value !== undefined)) throw new Error("Selected private acquisition target is occupied");
|
|
746
|
+
if (values.some((value) => value !== undefined) && values.some((value) => value === undefined)) throw new Error(`State Flow ${scope} origin requires complete or wholly absent scope files`);
|
|
747
|
+
if (scope === "session" && copy) { streams.session = copy.stream; continue; }
|
|
748
|
+
const source = classifyScopeStream(values[0], values[1], scope, scope === "cwd" ? cwd : undefined, values[2]);
|
|
749
|
+
if (source.kind === "present") { streams[scope] = source.stream; existing.add(scope); }
|
|
750
|
+
else streams[scope] = fresh.scopes[scope]!;
|
|
751
|
+
}
|
|
752
|
+
const originView = existing.size === 0 && !copy ? fresh : adoptSelectedTemporalStreams(streams, wanted, origin, request.historyLimit);
|
|
753
|
+
const finish = (nextSnapshot: Snapshot, transition?: AcceptedTransition, requestedProvenance?: SelectedProvenanceUpdates, moduleAdditions?: SelectedModuleAdditions): SelectedFilePublication => {
|
|
754
|
+
const snapshot = captureLifecycleSnapshot(nextSnapshot);
|
|
755
|
+
if (snapshot.config.mode === "off") throw new Error("Off cannot initialize State Flow memory");
|
|
756
|
+
const provenance = captureSelectedProvenanceUpdates(requestedProvenance, wanted);
|
|
757
|
+
const view = selectedReplaySuccessor(originView, request.historyLimit, transition, moduleAdditions);
|
|
758
|
+
const metadata = { ...prior?.lifecycle.runtime.meta };
|
|
759
|
+
for (const key of ["step", "specification", "bootstrap", "validation"]) delete metadata[key];
|
|
760
|
+
Object.assign(metadata, snapshot.meta);
|
|
761
|
+
const target = prepareSessionPolicyMigration(
|
|
762
|
+
`${canonicalJson({ mode: snapshot.config.mode, scopes: wanted })}\n`,
|
|
763
|
+
`${canonicalJson({ ...metadata, version: 2, identity: { cwd: resolve(cwd), sessionId }, lineage: view.lineage,
|
|
764
|
+
privateAcquisition: acquiring ? "acquired" : request.kind === "shared-deferred" ? "deferred" : "unacquired", causalWitnesses: temporalCausalWitnesses(view, request.historyLimit) })}\n`, cwd, sessionId, wanted,
|
|
765
|
+
)!;
|
|
766
|
+
const updates: OwnedFileUpdate[] = [];
|
|
767
|
+
const convertedScopes: StateScope[] = [];
|
|
768
|
+
for (const { scope, paths } of scopePaths) {
|
|
769
|
+
const prior = sources.get(paths.meta);
|
|
770
|
+
const converting = prior !== undefined && JSON.parse(prior).version !== (view.scopes[scope]!.modules === undefined ? 3 : 4);
|
|
771
|
+
if (converting) {
|
|
772
|
+
assertSelectedModuleConversion(basis, paths, scope, cwd, view.scopes[scope]!.modules);
|
|
773
|
+
convertedScopes.push(scope);
|
|
774
|
+
}
|
|
775
|
+
const merged = mergeSelectedArtifactProvenance(prior, paths.meta,
|
|
776
|
+
readTemporalView(originView, 0, scope, MAX_HISTORY_LIMIT).artifacts,
|
|
777
|
+
readTemporalView(view, 0, scope, MAX_HISTORY_LIMIT).artifacts, provenance?.[scope], scope === "session" && copy ? { registry: copy.provenance, raw: copy.rawArtifacts } : undefined);
|
|
778
|
+
if (existing.has(scope) && !converting && sameJson(streams[scope], view.scopes[scope]) && !merged.changed) continue;
|
|
779
|
+
const stream = view.scopes[scope]!;
|
|
780
|
+
const wire = serializeScopeStream(stream, scope, scope === "cwd" ? cwd : undefined);
|
|
781
|
+
updates.push({ path: paths.checkpoint, content: wire.checkpoint }, { path: paths.patches, content: wire.patches },
|
|
782
|
+
{ path: paths.meta, content: serializeScopeMetadata(undefined, stream, scope, scope === "cwd" ? cwd : undefined, merged.metadata) });
|
|
783
|
+
}
|
|
784
|
+
const paths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
785
|
+
updates.push({ path: paths.config, content: target.target.config }, { path: paths.runtime, content: target.target.runtime });
|
|
786
|
+
const receipts = { files: temporalFileReceipts(basis, updates) };
|
|
787
|
+
const privatePaths = temporalScopePaths(cwd, sessionId, "session", root, sessionKey);
|
|
788
|
+
const created = new Set(acquiring ? [privatePaths.checkpoint, privatePaths.patches, privatePaths.meta] : []);
|
|
789
|
+
const targetPresence = presence.map((file) => created.has(file.path) ? { ...file, present: true } : file);
|
|
790
|
+
const cohort = decodeSelectedFileCohort(cwd, sessionId, wanted, root, sessionKey, { lifecycle: target, basis: receipts, presence: targetPresence });
|
|
791
|
+
if (!sameJson(cohort.view, view)) throw new Error("Selected State Flow origin receipts do not match adopted values");
|
|
792
|
+
const assertOriginBasis = () => {
|
|
793
|
+
assertTemporalFileBase(basis, { files: captureOwnedFileBases(basis.files.map(file => file.path), root) });
|
|
794
|
+
if (!sameJson(presence, captureOwnedFilePresence(presence.map(file => file.path), root))) throw new Error("Selected origin presence changed concurrently");
|
|
795
|
+
copy?.assertCurrent();
|
|
796
|
+
};
|
|
797
|
+
assertOriginBasis();
|
|
798
|
+
const migrationBackup = convertedScopes.length > 0
|
|
799
|
+
? preserveMigrationBasis(basis, root, cwd, sessionId, sessionKey, { scopes: convertedScopes, presence }, updates) : undefined;
|
|
800
|
+
assertOriginBasis();
|
|
801
|
+
publishFileUpdates(basis.files, updates, root);
|
|
802
|
+
return { cohort, changed: true, ...(migrationBackup ? { migrationBackup } : {}) };
|
|
803
|
+
};
|
|
804
|
+
if (!request.author) return finish(snapshot);
|
|
805
|
+
let consumed = false;
|
|
806
|
+
let accepted: SelectedFilePublication | undefined;
|
|
807
|
+
const provenance = Object.fromEntries(scopePaths.map(({ scope, paths }) => [scope,
|
|
808
|
+
scope === "session" && copy ? copy.provenance : parseScopeProvenance(sources.get(paths.meta), paths.meta)]));
|
|
809
|
+
let authorActive = true;
|
|
810
|
+
try {
|
|
811
|
+
request.author({ view: structuredClone(originView), snapshot: structuredClone(snapshot), provenance }, (next, transition, evidence, moduleAdditions) => {
|
|
812
|
+
if (!authorActive) throw new Error("Selected first authoring callback has ended");
|
|
813
|
+
if (consumed) throw new Error("Selected first authoring publication was already consumed");
|
|
814
|
+
consumed = true;
|
|
815
|
+
accepted = finish(next, transition, evidence, moduleAdditions);
|
|
816
|
+
return accepted;
|
|
817
|
+
});
|
|
818
|
+
} finally { authorActive = false; }
|
|
819
|
+
if (!accepted) throw new Error("Selected first authoring requires one synchronous publication");
|
|
820
|
+
return accepted;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** Select a retained private basis only when current policy excluded it; selected Session cannot use stale proof as fallback. */
|
|
824
|
+
function validateSelectedPrivateStream(lifecycle: SessionPolicyMigrationCandidate, stream: ScopeStream): void {
|
|
825
|
+
if (lifecycle.runtime.meta.privateAcquisition !== "acquired") throw new Error("State Flow private memory requires acquired authority");
|
|
826
|
+
const metadata = lifecycle.runtime.meta;
|
|
827
|
+
const witnesses = lifecycle.sourceVersion === 2 ? captureTemporalWitnesses(metadata.lineage,
|
|
828
|
+
Object.hasOwn(metadata, "causalWitnesses") ? metadata.causalWitnesses : [], MAX_HISTORY_LIMIT) : [];
|
|
829
|
+
const retained = lifecycle.sourceVersion === 2 && !lifecycle.runtime.config.scopes.includes("session")
|
|
830
|
+
? metadata.privateCausalBasis : undefined;
|
|
831
|
+
if (retained) {
|
|
832
|
+
restoreSelectedTemporalState({ session: stream }, ["session"], retained.lineage, retained.causalWitnesses, MAX_HISTORY_LIMIT);
|
|
833
|
+
} else validateScopeLineage(stream, "session", metadata.lineage, MAX_HISTORY_LIMIT);
|
|
834
|
+
for (const witness of witnesses.filter(item => item.owners.includes("session"))) {
|
|
835
|
+
if (!stream.patches.some(record => sameJson(record.transition, witness.transition))) throw new Error("Accepted temporal witness is missing its selected owner patch");
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/** Admit only this direct parent's private stream; native lineage/parent authority belongs to the caller. */
|
|
840
|
+
function captureSelectedForkSource(cwd: string, childId: string, childKey: string, source: SessionAddress, root: string) {
|
|
841
|
+
if (!source || typeof source.id !== "string" || typeof source.key !== "string" || source.id === childId || source.key === childKey) {
|
|
842
|
+
throw new Error("Selected fork requires a distinct direct-parent identity and key");
|
|
843
|
+
}
|
|
844
|
+
const { lifecycle, lifecycleFiles, presence } = captureSelectedLifecycle(cwd, source.id, ["session"], root, source.key);
|
|
845
|
+
if (!lifecycle || lifecycle.runtime.meta.privateAcquisition !== "acquired") throw new Error("Selected fork parent has no acquired current private memory");
|
|
846
|
+
const paths = temporalScopePaths(cwd, source.id, "session", root, source.key);
|
|
847
|
+
const scopeFiles = captureOwnedFileBases([paths.checkpoint, paths.patches, paths.meta], root);
|
|
848
|
+
const basis = { files: [...lifecycleFiles, ...scopeFiles] };
|
|
849
|
+
const files = temporalFileContents(basis);
|
|
850
|
+
const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), "session", undefined, files.get(paths.meta));
|
|
851
|
+
if (!stream) throw new Error("Selected fork parent private stream is unavailable");
|
|
852
|
+
validateSelectedPrivateStream(lifecycle, stream);
|
|
853
|
+
const provenance = parseScopeProvenance(files.get(paths.meta), paths.meta);
|
|
854
|
+
// Raw inheritance is bound to the same admitted parent metadata bytes as interpreted evidence.
|
|
855
|
+
const rawMetadata = files.get(paths.meta);
|
|
856
|
+
const rawArtifacts = rawMetadata === undefined ? undefined : JSON.parse(rawMetadata).artifacts as JsonObject | undefined;
|
|
857
|
+
return { basis, stream, provenance, rawArtifacts, assertCurrent: () => {
|
|
858
|
+
const current = { files: captureOwnedFileBases(basis.files.map((file) => file.path), root) };
|
|
859
|
+
assertTemporalFileBase(basis, current);
|
|
860
|
+
if (!sameJson(presence, captureOwnedFilePresence(presence.map((file) => file.path), root))) throw new Error("Selected fork parent presence changed concurrently");
|
|
861
|
+
} };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/** Read-only selected admission under exclusion; never initialize a store or grant publication/acquisition. */
|
|
865
|
+
export async function loadSelectedFileCohort(
|
|
866
|
+
cwd: string, sessionId: string, selection: ScopeSelection, root: string, sessionKey = sessionId, signal?: AbortSignal,
|
|
867
|
+
): Promise<SelectedFileCohort | undefined> {
|
|
868
|
+
signal?.throwIfAborted();
|
|
869
|
+
const wanted = captureScopeSelection(selection);
|
|
870
|
+
assertStorageDirectory(root);
|
|
871
|
+
if (!lstatSync(root, { throwIfNoEntry: false })) return undefined;
|
|
872
|
+
return withStorageTransaction(root, (transaction) => transaction.prepareSelected(cwd, sessionId, wanted, root, sessionKey), signal);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/** Exact recapture comparison; future selected publishers must repeat both dependency checks under exclusion. */
|
|
876
|
+
export function assertSelectedFileCohortBasis(expected: SelectedFileCohort, current: SelectedFileCohort): void {
|
|
877
|
+
if (!sameJson(expected.view.selection, current.view.selection)) throw new Error("Selected State Flow policy changed concurrently");
|
|
878
|
+
assertTemporalFileBase(expected.basis, current.basis);
|
|
879
|
+
if (!sameJson(expected.presence, current.presence)) throw new Error("Selected State Flow presence dependencies changed concurrently");
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function captureSelectedLifecycle(cwd: string, sessionId: string, wanted: ScopeSelection, root: string, sessionKey: string, expectedLifecycle?: TemporalFileBase) {
|
|
883
|
+
const privatePaths = temporalScopePaths(cwd, sessionId, "session", root, sessionKey);
|
|
884
|
+
const privateFiles = [privatePaths.checkpoint, privatePaths.patches, privatePaths.meta];
|
|
885
|
+
const legacyPrivate = resolve(privatePaths.directory, "state.json");
|
|
886
|
+
const presence = captureOwnedFilePresence([...privateFiles, legacyPrivate], root);
|
|
887
|
+
if (presence.at(-1)!.present) throw new Error("Unsupported State Flow private semantic layout");
|
|
888
|
+
const runtimePaths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
889
|
+
const lifecycleFiles = captureOwnedFileBases([runtimePaths.config, runtimePaths.runtime], root);
|
|
890
|
+
// Compare before parsing: a changed legacy epoch must not widen a selected mode-only read.
|
|
891
|
+
if (expectedLifecycle) assertTemporalFileBase(expectedLifecycle, { files: lifecycleFiles });
|
|
892
|
+
const lifecycleText = temporalFileContents({ files: lifecycleFiles });
|
|
893
|
+
const lifecycle = prepareSessionPolicyMigration(lifecycleText.get(runtimePaths.config), lifecycleText.get(runtimePaths.runtime), cwd, sessionId, wanted);
|
|
894
|
+
if (!lifecycle) {
|
|
895
|
+
if (presence.some((file) => file.present)) throw new Error("Orphan State Flow private semantics without lifecycle authority");
|
|
896
|
+
return { lifecycle, lifecycleFiles, presence };
|
|
897
|
+
}
|
|
898
|
+
const acquired = lifecycle.runtime.meta.privateAcquisition === "acquired";
|
|
899
|
+
if (acquired && presence.slice(0, 3).some((file) => !file.present)) throw new Error("Acquired State Flow private semantics are incomplete");
|
|
900
|
+
if (!acquired && presence.some((file) => file.present)) throw new Error("Unacquired State Flow private target is occupied");
|
|
901
|
+
return { lifecycle, lifecycleFiles, presence };
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function prepareSelectedFileCohort(cwd: string, sessionId: string, selection: ScopeSelection, root: string, sessionKey: string, expectedLifecycle?: TemporalFileBase): SelectedFileCohort | undefined {
|
|
905
|
+
const wanted = captureScopeSelection(selection);
|
|
906
|
+
const captured = captureSelectedCohortSource(cwd, sessionId, wanted, root, sessionKey, expectedLifecycle, expectedLifecycle !== undefined);
|
|
907
|
+
return captured ? decodeSelectedFileCohort(cwd, sessionId, wanted, root, sessionKey, captured) : undefined;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function captureSelectedCohortSource(cwd: string, sessionId: string, wanted: ScopeSelection, root: string, sessionKey: string, expectedLifecycle?: TemporalFileBase, requireV2 = false) {
|
|
911
|
+
const { lifecycle, lifecycleFiles, presence } = captureSelectedLifecycle(cwd, sessionId, wanted, root, sessionKey, expectedLifecycle);
|
|
912
|
+
if (requireV2 && lifecycle?.sourceVersion !== 2) throw new Error("Selected Passive persistence requires lifecycle v2 and semantic v3");
|
|
913
|
+
if (!lifecycle) return undefined;
|
|
914
|
+
if (lifecycle.runtime.meta.privateAcquisition !== "acquired" && wanted.includes("session")) throw new Error("Selected State Flow Session requires explicit private acquisition");
|
|
915
|
+
const dependencies = lifecycle.sourceVersion === 1 ? SCOPES : wanted;
|
|
916
|
+
const scopePaths = dependencies.map((scope) => ({ scope, paths: temporalScopePaths(cwd, sessionId, scope, root, sessionKey) }));
|
|
917
|
+
const layouts = captureOwnedFilePresence(scopePaths.filter(({ scope }) => scope !== "session").map(({ paths }) => resolve(paths.directory, "state.json")), root);
|
|
918
|
+
if (layouts.some((file) => file.present)) throw new Error("Unsupported State Flow selected semantic layout");
|
|
919
|
+
presence.push(...layouts);
|
|
920
|
+
const scopeFiles = captureOwnedFileBases(scopePaths.flatMap(({ paths }) => [paths.checkpoint, paths.patches, paths.meta]), root);
|
|
921
|
+
const basis = { files: [...lifecycleFiles, ...scopeFiles] };
|
|
922
|
+
return { lifecycle, basis, presence };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/** Current-only admission may replace stale shared alignment, never malformed private/stream/witness evidence. */
|
|
926
|
+
function prepareSelectedCurrent(cwd: string, sessionId: string, selection: ScopeSelection, historyLimit: number, root: string, sessionKey: string, expectedLifecycle?: TemporalFileBase): SelectedCurrentCandidate {
|
|
927
|
+
const wanted = captureScopeSelection(selection);
|
|
928
|
+
const source = captureSelectedCohortSource(cwd, sessionId, wanted, root, sessionKey, expectedLifecycle);
|
|
929
|
+
if (!source) throw new Error("Selected State Flow current session authority is unavailable");
|
|
930
|
+
const { lifecycle, basis, presence } = source;
|
|
931
|
+
const lineage = lifecycle.runtime.meta.lineage;
|
|
932
|
+
const witnesses = lifecycle.sourceVersion === 2 ? captureTemporalWitnesses(lineage, Object.hasOwn(lifecycle.runtime.meta, "causalWitnesses") ? lifecycle.runtime.meta.causalWitnesses : [], MAX_HISTORY_LIMIT) : [];
|
|
933
|
+
const dependencies = lifecycle.sourceVersion === 1 ? SCOPES : wanted;
|
|
934
|
+
const files = temporalFileContents(basis);
|
|
935
|
+
const origin = `files:selected-current:${randomUUID()}`;
|
|
936
|
+
const empty = createSelectedTemporalState(Object.fromEntries(wanted.map((scope) => [scope, {}])), wanted, `${origin}:empty`, historyLimit);
|
|
937
|
+
const streams: Partial<Record<StateScope, ScopeStream>> = {};
|
|
938
|
+
const provenance: SelectedFileCohort["provenance"] = {};
|
|
939
|
+
let needsOrigin = false;
|
|
940
|
+
for (const scope of dependencies) {
|
|
941
|
+
const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
942
|
+
const values = [paths.checkpoint, paths.patches, paths.meta].map((path) => files.get(path));
|
|
943
|
+
if (values.some((value) => value !== undefined) && values.some((value) => value === undefined)) throw new Error(`Selected current State Flow ${scope} requires complete or wholly absent files`);
|
|
944
|
+
const stream = parseScopeStream(values[0], values[1], scope, scope === "cwd" ? cwd : undefined, values[2]);
|
|
945
|
+
if (!stream && (scope === "session" || lifecycle.sourceVersion === 1)) throw new Error(`Selected State Flow ${scope} stream is unavailable`);
|
|
946
|
+
const registry = parseScopeProvenance(values[2], paths.meta);
|
|
947
|
+
if (scope === "session") validateSelectedPrivateStream(lifecycle, stream!);
|
|
948
|
+
const aligned = stream !== undefined && scopeLineageMatches(stream, scope, lineage, MAX_HISTORY_LIMIT);
|
|
949
|
+
if (!aligned) needsOrigin = true;
|
|
950
|
+
// Unchanged owners must still substantiate every selected-owner claim; drift discards old shared history only.
|
|
951
|
+
if (aligned && scope !== "session") for (const witness of witnesses.filter((item) => item.owners.includes(scope))) {
|
|
952
|
+
if (!stream!.patches.some((record) => sameJson(record.transition, witness.transition))) throw new Error("Accepted temporal witness is missing its selected owner patch");
|
|
953
|
+
}
|
|
954
|
+
if (wanted.includes(scope)) { streams[scope] = stream ?? empty.scopes[scope]!; provenance[scope] = registry; }
|
|
955
|
+
}
|
|
956
|
+
if (!needsOrigin) {
|
|
957
|
+
const decoded = decodeSelectedFileCohort(cwd, sessionId, wanted, root, sessionKey, source);
|
|
958
|
+
const view = constrainTemporalState(decoded.view, historyLimit);
|
|
959
|
+
return { ...decoded, alignment: "retained", view, witnesses: temporalCausalWitnesses(view, historyLimit) };
|
|
960
|
+
}
|
|
961
|
+
const view = adoptSelectedTemporalStreams(streams, wanted, origin, historyLimit);
|
|
962
|
+
return { basis, presence, lifecycle, provenance, view, witnesses: temporalCausalWitnesses(view, historyLimit), alignment: "current-origin" };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** Decode predicted receipts too; no canonical reread is needed to establish an accepted output basis. */
|
|
966
|
+
function decodeSelectedFileCohort(
|
|
967
|
+
cwd: string, sessionId: string, wanted: ScopeSelection, root: string, sessionKey: string,
|
|
968
|
+
captured: Pick<SelectedFileCohort, "lifecycle" | "basis" | "presence">,
|
|
969
|
+
): SelectedFileCohort {
|
|
970
|
+
const { lifecycle } = captured;
|
|
971
|
+
const dependencies = lifecycle.sourceVersion === 1 ? SCOPES : wanted;
|
|
972
|
+
const scopePaths = dependencies.map((scope) => ({ scope, paths: temporalScopePaths(cwd, sessionId, scope, root, sessionKey) }));
|
|
973
|
+
const paths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
974
|
+
const bytePaths = new Set([paths.config, paths.runtime, ...scopePaths.flatMap(({ paths }) => [paths.checkpoint, paths.patches, paths.meta])]);
|
|
975
|
+
const privatePaths = temporalScopePaths(cwd, sessionId, "session", root, sessionKey);
|
|
976
|
+
const presencePaths = new Set([privatePaths.checkpoint, privatePaths.patches, privatePaths.meta, resolve(privatePaths.directory, "state.json"),
|
|
977
|
+
...scopePaths.filter(({ scope }) => scope !== "session").map(({ paths }) => resolve(paths.directory, "state.json"))]);
|
|
978
|
+
const basis = { files: captured.basis.files.filter(({ path }) => bytePaths.has(path)) };
|
|
979
|
+
const presence = captured.presence.filter(({ path }) => presencePaths.has(path));
|
|
980
|
+
if (basis.files.length !== bytePaths.size || presence.length !== presencePaths.size) throw new Error("Incomplete selected State Flow dependency receipts");
|
|
981
|
+
const files = temporalFileContents(basis);
|
|
982
|
+
const streams: Partial<Record<StateScope, ScopeStream>> = {};
|
|
983
|
+
const provenance: Partial<Record<StateScope, ArtifactProvenanceRegistry>> = {};
|
|
984
|
+
for (const { scope, paths } of scopePaths) {
|
|
985
|
+
const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope, scope === "cwd" ? cwd : undefined, files.get(paths.meta));
|
|
986
|
+
if (!stream) throw new Error(`Selected State Flow ${scope} stream is unavailable`);
|
|
987
|
+
streams[scope] = stream;
|
|
988
|
+
const registry = parseScopeProvenance(files.get(paths.meta), paths.meta);
|
|
989
|
+
if (wanted.includes(scope)) provenance[scope] = registry;
|
|
990
|
+
}
|
|
991
|
+
let view: SelectedTemporalState;
|
|
992
|
+
if (lifecycle.sourceVersion === 1) {
|
|
993
|
+
const complete: TemporalState = { lineage: lifecycle.runtime.meta.lineage, scopes: streams as TemporalState["scopes"] };
|
|
994
|
+
validateTemporalState(complete, MAX_HISTORY_LIMIT);
|
|
995
|
+
view = selectTemporalState(complete, wanted, MAX_HISTORY_LIMIT);
|
|
996
|
+
} else {
|
|
997
|
+
const evidence = Object.hasOwn(lifecycle.runtime.meta, "causalWitnesses") ? lifecycle.runtime.meta.causalWitnesses : [];
|
|
998
|
+
view = restoreSelectedTemporalState(streams, wanted, lifecycle.runtime.meta.lineage, evidence, MAX_HISTORY_LIMIT);
|
|
999
|
+
}
|
|
1000
|
+
return { basis, presence, lifecycle, view, witnesses: temporalCausalWitnesses(view, MAX_HISTORY_LIMIT), provenance };
|
|
1001
|
+
}
|
|
1002
|
+
|
|
287
1003
|
export function captureTemporalFileBase(cwd: string, sessionId: string, root: string, sessionKey = sessionId): TemporalFileBase {
|
|
288
1004
|
return withStoragePublicationLock(root, (locked) => ({ files: captureTemporalFileBases(cwd, sessionId, locked, sessionKey) }));
|
|
289
1005
|
}
|
|
@@ -303,7 +1019,7 @@ export function publishTemporalStateToFiles(
|
|
|
303
1019
|
cwd: string, sessionId: string, view: TemporalState, scopes: readonly StateScope[],
|
|
304
1020
|
base: TemporalFileBase, root: string, runtime: SessionRuntime, sessionKey = sessionId,
|
|
305
1021
|
provenance?: Readonly<Record<StateScope, ArtifactProvenanceRegistry>>, runtimeOnly = false,
|
|
306
|
-
):
|
|
1022
|
+
): FilePublication {
|
|
307
1023
|
return withStoragePublicationLock(root, (locked) => publishLockedTemporalStateToFiles(
|
|
308
1024
|
cwd, sessionId, view, scopes, base, locked, runtime, sessionKey, provenance, runtimeOnly,
|
|
309
1025
|
));
|
|
@@ -314,15 +1030,51 @@ function publishLockedTemporalStateToFiles(
|
|
|
314
1030
|
cwd: string, sessionId: string, view: TemporalState, scopes: readonly StateScope[],
|
|
315
1031
|
base: TemporalFileBase, root: string, runtime: SessionRuntime, sessionKey = sessionId,
|
|
316
1032
|
provenance?: Readonly<Record<StateScope, ArtifactProvenanceRegistry>>, runtimeOnly = false,
|
|
317
|
-
):
|
|
1033
|
+
): FilePublication {
|
|
318
1034
|
const current = { files: captureTemporalFileBases(cwd, sessionId, root, sessionKey) };
|
|
319
1035
|
assertTemporalFileBase(base, current);
|
|
320
1036
|
const { updates } = planTemporalPublication(cwd, sessionId, view, scopes, current, root, runtime, runtimeOnly, sessionKey, provenance);
|
|
321
1037
|
const next = { files: temporalFileReceipts(current, updates) };
|
|
322
1038
|
decodeFileCohort(cwd, sessionId, root, next, sessionKey);
|
|
323
1039
|
const revision = fileRevision(next, root);
|
|
1040
|
+
const convertedScopes = SCOPES.filter((scope) => {
|
|
1041
|
+
const path = temporalScopePaths(cwd, sessionId, scope, root, sessionKey).meta;
|
|
1042
|
+
const previous = current.files.find((file) => file.path === path)?.content;
|
|
1043
|
+
const content = updates.find((file) => file.path === path)?.content;
|
|
1044
|
+
return previous !== undefined && content !== undefined && JSON.parse(previous).version !== JSON.parse(content).version;
|
|
1045
|
+
});
|
|
1046
|
+
const migrationBackup = convertedScopes.length > 0 ? preserveMigrationBasis(current, root, cwd, sessionId, sessionKey, { scopes: convertedScopes }, updates) : undefined;
|
|
324
1047
|
publishFileUpdates(current.files, updates, root);
|
|
325
|
-
return { base: next, revision, changed: updates.length > 0 };
|
|
1048
|
+
return { base: next, revision, changed: updates.length > 0, ...(migrationBackup ? { migrationBackup } : {}) };
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/** Preserve exactly the converted transaction's owned basis, never discover other sessions or source files. */
|
|
1052
|
+
function preserveMigrationBasis(base: TemporalFileBase, root: string, cwd: string, sessionId: string, sessionKey: string,
|
|
1053
|
+
conversion: { scopes: readonly StateScope[]; lifecycle?: { sourceVersion: 1; targetVersion: 2 }; presence?: readonly OwnedFilePresence[] }, updates: readonly OwnedFileUpdate[]): string {
|
|
1054
|
+
const targetVersion = updates.some(file => file.path.endsWith("/meta.json") && file.content !== undefined && JSON.parse(file.content).version === 4) ? 4 : 3;
|
|
1055
|
+
const content = `${canonicalJson({ version: 1, kind: "state-flow-semantic-migration", targetVersion,
|
|
1056
|
+
root: resolve(root), cwd: resolve(cwd), sessionId, sessionKey, convertedScopes: conversion.scopes,
|
|
1057
|
+
...(conversion.lifecycle ? { lifecycleConversion: conversion.lifecycle } : {}),
|
|
1058
|
+
...(conversion.presence ? { presenceDependencies: conversion.presence.map((file) => ({ path: relative(root, file.path), present: file.present })) } : {}),
|
|
1059
|
+
updatedPaths: updates.map((file) => relative(root, file.path)),
|
|
1060
|
+
files: base.files.map((file) => ({ path: relative(root, file.path), identity: file.identity,
|
|
1061
|
+
bytes: file.bytes === undefined ? null : Buffer.from(file.bytes).toString("base64") })),
|
|
1062
|
+
})}\n`;
|
|
1063
|
+
const directory = resolve(dirname(root), `${basename(root)}-migration-backups`);
|
|
1064
|
+
assertStorageDirectory(directory);
|
|
1065
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
1066
|
+
const directoryStat = lstatSync(directory);
|
|
1067
|
+
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink() || (directoryStat.mode & 0o077) !== 0) {
|
|
1068
|
+
throw new Error("State Flow migration backup directory must be private; canonical files preserved");
|
|
1069
|
+
}
|
|
1070
|
+
const path = resolve(directory, `${createHash("sha256").update(content).digest("hex")}.json`);
|
|
1071
|
+
try { writeFileSync(path, content, { flag: "wx", mode: 0o600 }); }
|
|
1072
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
|
|
1073
|
+
const stat = lstatSync(path);
|
|
1074
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || readFileSync(path, { encoding: "utf8", flag: constants.O_RDONLY | constants.O_NOFOLLOW }) !== content) {
|
|
1075
|
+
throw new Error("State Flow migration backup is unavailable or changed; canonical files preserved");
|
|
1076
|
+
}
|
|
1077
|
+
return path;
|
|
326
1078
|
}
|
|
327
1079
|
|
|
328
1080
|
function publishFileUpdates(bases: readonly DurableFileBase[], updates: readonly OwnedFileUpdate[], root: string): void {
|