@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,5 +1,5 @@
|
|
|
1
1
|
import { type JsonObject, type JsonValue } from "./json.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ScopeStateValues, StateScope } from "./state.ts";
|
|
3
3
|
export declare const DEFAULT_HISTORY_LIMIT = 7;
|
|
4
4
|
export declare const MAX_HISTORY_LIMIT = 100;
|
|
5
5
|
export interface RecentScopePatch {
|
|
@@ -7,8 +7,8 @@ export interface RecentScopePatch {
|
|
|
7
7
|
patch: {
|
|
8
8
|
[key: string]: JsonValue | undefined;
|
|
9
9
|
intents?: JsonObject | null;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
strategy?: JsonObject | null;
|
|
11
|
+
tactics?: JsonObject | null;
|
|
12
12
|
artifacts?: JsonObject | null;
|
|
13
13
|
lazy?: JsonObject | null;
|
|
14
14
|
response?: string | null;
|
|
@@ -25,6 +25,6 @@ export interface RecentTransition extends AcceptedTransition {
|
|
|
25
25
|
}
|
|
26
26
|
export type RecentTransitionWindow = RecentTransition[];
|
|
27
27
|
export declare function validateRecentTransition(value: unknown): asserts value is RecentTransition;
|
|
28
|
-
export declare function createAcceptedTransition(currentStates:
|
|
28
|
+
export declare function createAcceptedTransition(currentStates: ScopeStateValues, nextStates: ScopeStateValues, id?: string): AcceptedTransition | undefined;
|
|
29
29
|
/** Preserve the configured per-scope budget, filtering in selected-lineage order. */
|
|
30
30
|
export declare function projectRecentTransitionsWithLimit(limit: number, lineage: readonly RecentTransition[]): RecentTransitionWindow;
|
|
@@ -3,7 +3,7 @@ import { isJsonValue, isObject, sameJson, validatePatch } from "./json.js";
|
|
|
3
3
|
export const DEFAULT_HISTORY_LIMIT = 7;
|
|
4
4
|
export const MAX_HISTORY_LIMIT = 100;
|
|
5
5
|
const SCOPES = new Set(["global", "cwd", "session"]);
|
|
6
|
-
const PATCH_KEYS = new Set(["artifacts", "
|
|
6
|
+
const PATCH_KEYS = new Set(["artifacts", "strategy", "tactics", "intents", "response", "lazy"]);
|
|
7
7
|
/** Normalize accepted replacements into recursive-merge replay, including removals. */
|
|
8
8
|
function replayPatch(before, after) {
|
|
9
9
|
const entries = [];
|
|
@@ -28,6 +28,8 @@ function validateScopedPatch(value) {
|
|
|
28
28
|
throw new Error(`Recent State Flow transition has an unknown scope: ${String(value.scope)}`);
|
|
29
29
|
}
|
|
30
30
|
validatePatch(value.patch);
|
|
31
|
+
if (Object.hasOwn(value.patch, "contract") || Object.hasOwn(value.patch, "working"))
|
|
32
|
+
throw new Error("Retired State Flow semantic plane in target patch");
|
|
31
33
|
for (const [key, field] of Object.entries(value.patch)) {
|
|
32
34
|
const valid = key === "response"
|
|
33
35
|
? value.scope === "session" && (field === null || typeof field === "string")
|
|
@@ -54,10 +56,20 @@ export function validateRecentTransition(value) {
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
export function createAcceptedTransition(currentStates, nextStates, id) {
|
|
59
|
+
const owners = Object.keys(currentStates);
|
|
60
|
+
if (owners.length === 0 || owners.some((scope) => !SCOPES.has(scope))
|
|
61
|
+
|| Object.keys(nextStates).length !== owners.length || owners.some((scope) => !Object.hasOwn(nextStates, scope))) {
|
|
62
|
+
throw new Error("Accepted State Flow replay requires matching nonempty owner sets");
|
|
63
|
+
}
|
|
57
64
|
const transitions = [];
|
|
58
65
|
for (const scope of SCOPES) {
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
if (!Object.hasOwn(currentStates, scope))
|
|
67
|
+
continue;
|
|
68
|
+
const before = currentStates[scope], after = nextStates[scope];
|
|
69
|
+
if (!isObject(before) || !isObject(after))
|
|
70
|
+
throw new Error("Accepted State Flow replay requires present owner values");
|
|
71
|
+
const patch = replayPatch(before, after);
|
|
72
|
+
if ((before.response ?? "") === (after.response ?? ""))
|
|
61
73
|
delete patch.response;
|
|
62
74
|
if (Object.keys(patch).length > 0)
|
|
63
75
|
transitions.push({ scope, patch });
|
|
@@ -7,6 +7,8 @@ export interface MissingDeletion {
|
|
|
7
7
|
path: string;
|
|
8
8
|
unavailablePath: string;
|
|
9
9
|
}
|
|
10
|
+
/** Internal path-copying step; callers must own the detached draft and collect failures privately. */
|
|
11
|
+
export declare function applyOwnedPatch(state: JsonObject, patch: JsonObject, missingDeletions?: MissingDeletion[], path?: string, unavailablePath?: string): JsonObject;
|
|
10
12
|
/** Remove empty object fields without removing array slots or mutating the input. */
|
|
11
13
|
export declare function pruneEmptyObjects(value: JsonObject): JsonObject;
|
|
12
14
|
export declare function pruneEmptyObjects(value: JsonValue): JsonValue;
|
|
@@ -34,7 +34,8 @@ function applyOwnedArrayPatch(state, patch, missingDeletions, path, unavailableP
|
|
|
34
34
|
}
|
|
35
35
|
return next;
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
/** Internal path-copying step; callers must own the detached draft and collect failures privately. */
|
|
38
|
+
export function applyOwnedPatch(state, patch, missingDeletions, path = "", unavailablePath) {
|
|
38
39
|
let next = state;
|
|
39
40
|
for (const [key, value] of Object.entries(patch)) {
|
|
40
41
|
const owns = Object.hasOwn(next, key);
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ScopeStateValues, type ScopedStates, type StateScope } from "./state.ts";
|
|
2
|
+
import { type ScopeSelection } from "./selection.ts";
|
|
2
3
|
export declare function retainedMemoryScopes(states: ScopedStates): Record<StateScope, boolean>;
|
|
4
|
+
export declare function retainedMemoryScopes(states: ScopeStateValues, selection: ScopeSelection): Partial<Record<StateScope, boolean>>;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { assertScopeStateValues } from "./state.js";
|
|
2
|
+
import { captureScopeSelection, DEFAULT_SCOPE_SELECTION } from "./selection.js";
|
|
1
3
|
function hasSemanticMemory(state) {
|
|
2
|
-
return Object.keys(state.
|
|
4
|
+
return Object.keys(state.strategy ?? {}).length > 0 || Object.keys(state.tactics ?? {}).length > 0;
|
|
3
5
|
}
|
|
4
|
-
export function retainedMemoryScopes(states) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
session: hasSemanticMemory(states.session),
|
|
9
|
-
};
|
|
6
|
+
export function retainedMemoryScopes(states, selection = DEFAULT_SCOPE_SELECTION) {
|
|
7
|
+
const selected = captureScopeSelection(selection);
|
|
8
|
+
assertScopeStateValues(states, selected);
|
|
9
|
+
return Object.fromEntries(selected.map(scope => [scope, hasSemanticMemory(states[scope])]));
|
|
10
10
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type JsonObject } from "./json.ts";
|
|
2
|
+
import { type RetainedModules } from "./semantic-module.ts";
|
|
3
|
+
import { type StateScope } from "./state.ts";
|
|
4
|
+
import { type ScopeStream } from "./temporal.ts";
|
|
5
|
+
/** Owns temporary pure semantic-dialect conversion; excludes I/O, publication, backup and lifecycle authority. */
|
|
6
|
+
export interface SemanticMigrationCandidate {
|
|
7
|
+
sourceVersion: 1 | 2 | 3 | 4;
|
|
8
|
+
targetVersion: 3 | 4;
|
|
9
|
+
/** A format conversion is not an authored semantic transition or revision. */
|
|
10
|
+
requiresConversion: boolean;
|
|
11
|
+
stream: ScopeStream;
|
|
12
|
+
}
|
|
13
|
+
export type ScopeMigrationFile = "checkpoint" | "patches" | "meta";
|
|
14
|
+
export type ScopeMigrationSources = Partial<Record<ScopeMigrationFile, Uint8Array>>;
|
|
15
|
+
/** Detached evidence for later backup/CAS, not a publication capability or a filesystem observation. */
|
|
16
|
+
export interface MigrationByteReceipt {
|
|
17
|
+
bytes: Uint8Array;
|
|
18
|
+
identity: `sha256:${string}`;
|
|
19
|
+
}
|
|
20
|
+
export interface SerializedSemanticMigrationCandidate extends SemanticMigrationCandidate {
|
|
21
|
+
basis: Record<ScopeMigrationFile, MigrationByteReceipt>;
|
|
22
|
+
target: Record<ScopeMigrationFile, string>;
|
|
23
|
+
}
|
|
24
|
+
/** Translate a raw legacy checkpoint or replay patch before any known-field filtering. */
|
|
25
|
+
export declare function translateLegacySemanticObject(value: unknown): JsonObject;
|
|
26
|
+
/** Build a detached target candidate; publication still belongs to the guarded storage transaction. */
|
|
27
|
+
export declare function prepareSemanticMigration(value: unknown, scope: StateScope, sourceVersion: number, historyLimit?: number, targetModules?: RetainedModules): SemanticMigrationCandidate;
|
|
28
|
+
/** Decode a complete raw owner cohort before filtering; never load files, normalize authority or publish. */
|
|
29
|
+
export declare function prepareSerializedScopeMigration(sources: ScopeMigrationSources, scope: StateScope, expectedCwd?: string, targetModules?: RetainedModules): SerializedSemanticMigrationCandidate;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { parseArtifactProvenanceRegistry } from "./artifact.js";
|
|
4
|
+
import { MAX_HISTORY_LIMIT } from "./history.js";
|
|
5
|
+
import { canonicalJson, isJsonValue, isObject, validatePatch } from "./json.js";
|
|
6
|
+
import { parseStateReadPath, parseValuePath } from "./query.js";
|
|
7
|
+
import { assertRetainedModules, validateRetainedModules } from "./semantic-module.js";
|
|
8
|
+
import { assertNoRetiredSemanticPlanes } from "./state.js";
|
|
9
|
+
import { validateScopeStream } from "./temporal.js";
|
|
10
|
+
const LEGACY_PLANES = new Map([["contract", "strategy"], ["working", "tactics"]]);
|
|
11
|
+
const LEGACY_SEMANTIC_PLANES = new Set(["intents", "contract", "working", "artifacts", "response", "lazy"]);
|
|
12
|
+
const LEGACY_LOCATOR = /^(?:((?:effective|global|cwd|session)(?:\[\d+\])?(?:\.patches(?:\[\d+\])?)?)\.)?(contract|working)(?=$|\.|\[)/;
|
|
13
|
+
const MARKED_LOCATOR = /(?<![\p{L}\p{N}_$.[\]:/\\-])\$([\p{L}\p{N}_$.[\]:-]+)(?![\p{L}\p{N}_$.[\]:/\\-])/gu;
|
|
14
|
+
function translateLocator(path) {
|
|
15
|
+
const match = LEGACY_LOCATOR.exec(path);
|
|
16
|
+
if (!match)
|
|
17
|
+
return path;
|
|
18
|
+
const [, prefix, plane] = match;
|
|
19
|
+
const suffix = path.slice(match[0].length);
|
|
20
|
+
try {
|
|
21
|
+
if (prefix)
|
|
22
|
+
parseStateReadPath(prefix, MAX_HISTORY_LIMIT);
|
|
23
|
+
// The unchanged lazy root lets the shared parser validate selectors after legacy names retire.
|
|
24
|
+
parseValuePath(`lazy${suffix}`);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return path;
|
|
28
|
+
}
|
|
29
|
+
return `${prefix ? `${prefix}.` : ""}${LEGACY_PLANES.get(plane)}${suffix}`;
|
|
30
|
+
}
|
|
31
|
+
function translateReferences(value) {
|
|
32
|
+
if (typeof value === "string")
|
|
33
|
+
return value.replace(MARKED_LOCATOR, (_token, path) => `$${translateLocator(path)}`);
|
|
34
|
+
if (Array.isArray(value))
|
|
35
|
+
return value.map(translateReferences);
|
|
36
|
+
if (!isObject(value))
|
|
37
|
+
return value;
|
|
38
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key,
|
|
39
|
+
key === "$ref" && typeof child === "string" ? translateLocator(child) : translateReferences(child)]));
|
|
40
|
+
}
|
|
41
|
+
function translateArtifacts(value) {
|
|
42
|
+
if (!isObject(value))
|
|
43
|
+
return translateReferences(value);
|
|
44
|
+
return Object.fromEntries(Object.entries(value).map(([path, card]) => [path, !isObject(card) ? translateReferences(card)
|
|
45
|
+
: Object.fromEntries(Object.entries(card).map(([key, child]) => [key,
|
|
46
|
+
// Retired embedded provenance is evidence, never a semantic reference namespace.
|
|
47
|
+
["hash", "compiler", "compiled_at"].includes(key) ? structuredClone(child) : translateReferences(child)]))]));
|
|
48
|
+
}
|
|
49
|
+
/** Translate a raw legacy checkpoint or replay patch before any known-field filtering. */
|
|
50
|
+
export function translateLegacySemanticObject(value) {
|
|
51
|
+
validatePatch(value);
|
|
52
|
+
for (const plane of LEGACY_PLANES.values()) {
|
|
53
|
+
if (Object.hasOwn(value, plane))
|
|
54
|
+
throw new Error(`Target State Flow semantic plane in legacy format: ${plane}`);
|
|
55
|
+
}
|
|
56
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [LEGACY_PLANES.get(key) ?? key,
|
|
57
|
+
key === "artifacts" ? translateArtifacts(child) : LEGACY_SEMANTIC_PLANES.has(key) ? translateReferences(child) : structuredClone(child)]));
|
|
58
|
+
}
|
|
59
|
+
/** Build a detached target candidate; publication still belongs to the guarded storage transaction. */
|
|
60
|
+
export function prepareSemanticMigration(value, scope, sourceVersion, historyLimit = MAX_HISTORY_LIMIT, targetModules) {
|
|
61
|
+
if (sourceVersion !== 1 && sourceVersion !== 2 && sourceVersion !== 3 && sourceVersion !== 4)
|
|
62
|
+
throw new Error("Unsupported State Flow migration source version");
|
|
63
|
+
validatePatch(value);
|
|
64
|
+
if (!isObject(value.checkpoint) || !Array.isArray(value.patches))
|
|
65
|
+
throw new Error("Invalid temporal checkpoint/tail envelope");
|
|
66
|
+
if (targetModules !== undefined)
|
|
67
|
+
validateRetainedModules(targetModules);
|
|
68
|
+
if (sourceVersion === 4) {
|
|
69
|
+
validateRetainedModules(value.modules);
|
|
70
|
+
if (targetModules !== undefined)
|
|
71
|
+
assertRetainedModules(value.modules, targetModules);
|
|
72
|
+
}
|
|
73
|
+
else if (Object.hasOwn(value, "modules"))
|
|
74
|
+
throw new Error("Legacy State Flow stream cannot declare modules");
|
|
75
|
+
const translate = (state) => {
|
|
76
|
+
if (sourceVersion !== 4 && targetModules !== undefined && isObject(state)
|
|
77
|
+
&& Object.keys(state).some(key => key.startsWith("x_")))
|
|
78
|
+
throw new Error("Legacy State Flow external namespace collision");
|
|
79
|
+
if (sourceVersion < 3)
|
|
80
|
+
return translateLegacySemanticObject(state);
|
|
81
|
+
validatePatch(state);
|
|
82
|
+
assertNoRetiredSemanticPlanes(state);
|
|
83
|
+
return structuredClone(state);
|
|
84
|
+
};
|
|
85
|
+
// Preserve envelope fields until strict target validation; malformed input must not be sanitized into acceptance.
|
|
86
|
+
const stream = {
|
|
87
|
+
...structuredClone(value),
|
|
88
|
+
...(targetModules === undefined ? {} : { modules: structuredClone(targetModules) }),
|
|
89
|
+
checkpoint: { ...structuredClone(value.checkpoint), state: translate(value.checkpoint.state) },
|
|
90
|
+
patches: value.patches.map((record) => {
|
|
91
|
+
if (!isObject(record))
|
|
92
|
+
throw new Error("Invalid temporal patch envelope");
|
|
93
|
+
return { ...structuredClone(record), patch: translate(record.patch) };
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
validateScopeStream(stream, scope, historyLimit);
|
|
97
|
+
const targetVersion = sourceVersion === 4 || targetModules !== undefined ? 4 : 3;
|
|
98
|
+
return { sourceVersion, targetVersion, requiresConversion: sourceVersion !== targetVersion, stream };
|
|
99
|
+
}
|
|
100
|
+
function captureMigrationBytes(value, label) {
|
|
101
|
+
if (!(value instanceof Uint8Array))
|
|
102
|
+
throw new Error(`Incomplete State Flow migration cohort: ${label} bytes are required`);
|
|
103
|
+
const bytes = new Uint8Array(value);
|
|
104
|
+
return { bytes, identity: `sha256:${createHash("sha256").update(bytes).digest("hex")}` };
|
|
105
|
+
}
|
|
106
|
+
function decodeMigrationText(bytes, label) {
|
|
107
|
+
try {
|
|
108
|
+
// Do not silently repair invalid UTF-8 or strip a BOM that ordinary JSON decoding would reject.
|
|
109
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw new Error(`State Flow migration ${label} is not valid UTF-8`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function parseMigrationJson(source, label) {
|
|
116
|
+
let value;
|
|
117
|
+
try {
|
|
118
|
+
value = JSON.parse(source);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new Error(`State Flow migration ${label} contains invalid JSON`);
|
|
122
|
+
}
|
|
123
|
+
if (!isJsonValue(value))
|
|
124
|
+
throw new Error(`State Flow migration ${label} must contain finite JSON data`);
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
/** Decode a complete raw owner cohort before filtering; never load files, normalize authority or publish. */
|
|
128
|
+
export function prepareSerializedScopeMigration(sources, scope, expectedCwd, targetModules) {
|
|
129
|
+
const basis = {
|
|
130
|
+
checkpoint: captureMigrationBytes(sources.checkpoint, "checkpoint"),
|
|
131
|
+
patches: captureMigrationBytes(sources.patches, "patches"),
|
|
132
|
+
meta: captureMigrationBytes(sources.meta, "metadata"),
|
|
133
|
+
};
|
|
134
|
+
const metaText = decodeMigrationText(basis.meta.bytes, "metadata");
|
|
135
|
+
const metadata = parseMigrationJson(metaText, "metadata");
|
|
136
|
+
if (!isObject(metadata))
|
|
137
|
+
throw new Error("Invalid State Flow migration metadata");
|
|
138
|
+
const version = metadata.version;
|
|
139
|
+
if (version !== 1 && version !== 2 && version !== 3 && version !== 4)
|
|
140
|
+
throw new Error("Unsupported State Flow migration source version");
|
|
141
|
+
if (version === 4)
|
|
142
|
+
validateRetainedModules(metadata.modules);
|
|
143
|
+
else if (targetModules !== undefined && Object.hasOwn(metadata, "modules"))
|
|
144
|
+
throw new Error("Legacy State Flow modules metadata collision");
|
|
145
|
+
if (scope === "cwd") {
|
|
146
|
+
if (expectedCwd === undefined)
|
|
147
|
+
throw new Error("State Flow CWD migration requires its canonical identity");
|
|
148
|
+
if (metadata.owner === undefined)
|
|
149
|
+
throw new Error("State Flow CWD scope identity is missing");
|
|
150
|
+
if (!isObject(metadata.owner) || Object.keys(metadata.owner).join(",") !== "cwd" || metadata.owner.cwd !== resolve(expectedCwd)) {
|
|
151
|
+
throw new Error("State Flow CWD migration identity mismatch");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (expectedCwd !== undefined)
|
|
155
|
+
throw new Error("Only State Flow CWD migration accepts a CWD identity");
|
|
156
|
+
if (Object.hasOwn(metadata, "artifacts"))
|
|
157
|
+
parseArtifactProvenanceRegistry(metadata.artifacts, "State Flow migration provenance");
|
|
158
|
+
const temporal = metadata.temporal;
|
|
159
|
+
if (!isObject(temporal) || !Object.hasOwn(temporal, "checkpoint") || !Array.isArray(temporal.patches)) {
|
|
160
|
+
throw new Error("Invalid State Flow migration temporal metadata");
|
|
161
|
+
}
|
|
162
|
+
if (version >= 3 && !Object.hasOwn(temporal, "revision"))
|
|
163
|
+
throw new Error("State Flow target migration metadata requires a revision");
|
|
164
|
+
const checkpointText = decodeMigrationText(basis.checkpoint.bytes, "checkpoint");
|
|
165
|
+
const patchesText = decodeMigrationText(basis.patches.bytes, "tail");
|
|
166
|
+
const checkpoint = parseMigrationJson(checkpointText, "checkpoint");
|
|
167
|
+
const patches = patchesText.split(/\r?\n/).flatMap((line, index) => line.trim().length === 0 ? []
|
|
168
|
+
: [parseMigrationJson(line, `tail line ${index + 1}`)]);
|
|
169
|
+
const boundaries = temporal.patches;
|
|
170
|
+
if (boundaries.length !== patches.length)
|
|
171
|
+
throw new Error("State Flow migration metadata does not match its semantic tail");
|
|
172
|
+
const candidate = prepareSemanticMigration({
|
|
173
|
+
...(version === 4 ? { modules: metadata.modules } : {}),
|
|
174
|
+
revision: temporal.revision === undefined ? patches.length : temporal.revision,
|
|
175
|
+
checkpoint: { through: temporal.checkpoint, state: checkpoint },
|
|
176
|
+
patches: patches.map((patch, index) => ({ transition: boundaries[index], patch })),
|
|
177
|
+
}, scope, version, MAX_HISTORY_LIMIT, targetModules);
|
|
178
|
+
const target = !candidate.requiresConversion && targetModules === undefined ? { checkpoint: checkpointText, patches: patchesText, meta: metaText } : {
|
|
179
|
+
checkpoint: `${canonicalJson(candidate.stream.checkpoint.state)}\n`,
|
|
180
|
+
patches: candidate.stream.patches.map((record) => `${canonicalJson(record.patch)}\n`).join(""),
|
|
181
|
+
meta: `${canonicalJson({ ...metadata, version: candidate.targetVersion,
|
|
182
|
+
...(candidate.stream.modules === undefined ? {} : { modules: candidate.stream.modules }),
|
|
183
|
+
temporal: { ...temporal, revision: candidate.stream.revision } })}\n`,
|
|
184
|
+
};
|
|
185
|
+
return { ...candidate, basis, target };
|
|
186
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type JsonValue, type MissingDeletion } from "./json.ts";
|
|
2
|
+
import { type ScopeSelection } from "./selection.ts";
|
|
3
|
+
import type { AtomicScopePatches, StateScope } from "./state.ts";
|
|
4
|
+
/** Anchor eligible flat current references, never text/resource/effective/historical references. */
|
|
5
|
+
export declare function canonicalizeModelReferences(value: unknown, scopes: ScopeSelection, external?: readonly string[]): JsonValue;
|
|
6
|
+
/** Strip only validated same-owner current semantic reference prefixes; canonical data stays untouched. */
|
|
7
|
+
export declare function projectModelReferences(value: unknown, scopes: ScopeSelection, external?: readonly string[]): JsonValue;
|
|
8
|
+
/** Value normalization only; callers must validate their original request/owner before invoking it. */
|
|
9
|
+
export declare function normalizeModelPatches(value: unknown, scopes: ScopeSelection, external?: readonly string[]): AtomicScopePatches;
|
|
10
|
+
/** Compilation target syntax, not a query locator: preserve the exact opaque source key. */
|
|
11
|
+
export declare function projectModelArtifactTarget(scope: StateScope, path: string, scopes: ScopeSelection): string;
|
|
12
|
+
/** Known deletion-report JSON pointers only; do not parse source keys or rewrite diagnostics. */
|
|
13
|
+
export declare function projectModelMissingDeletion(deletion: MissingDeletion, scopes: ScopeSelection): MissingDeletion;
|
|
14
|
+
/** Sole-owner locators bind to that exact owner; multi-owner effective aliases keep their existing meaning. */
|
|
15
|
+
export declare function normalizeModelReadPath(path: string, scopes: ScopeSelection, historyLimit?: number): string;
|
|
16
|
+
/** Presentation locator only: history keeps an explicit projection root and does not become an array index. */
|
|
17
|
+
export declare function projectModelReadPath(path: string, scopes: ScopeSelection, historyLimit?: number): string;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Domain: detached model/canonical patch, locator and structured-reference translation.
|
|
2
|
+
// Excludes: SDK schemas, mode, acquisition, existence checks, storage and request/publication authority.
|
|
3
|
+
import { isJsonValue, isObject } from "./json.js";
|
|
4
|
+
import { DEFAULT_HISTORY_LIMIT } from "./history.js";
|
|
5
|
+
import { parseStateReadPath, parseValuePath } from "./query.js";
|
|
6
|
+
import { captureScopeSelection } from "./selection.js";
|
|
7
|
+
import { MODEL_PLANE_NAMES } from "./planes.js";
|
|
8
|
+
const PATCH_PLANES = MODEL_PLANE_NAMES;
|
|
9
|
+
const SEMANTIC_ROOT = /^(?:intents|strategy|tactics|artifacts|response|lazy|x_[a-z][a-z0-9_]*)(?=\.|\[|$)/;
|
|
10
|
+
const PATCH_PATH = /^(?:effective|global|cwd|session)\.patches(?:\[\d+\])?$/;
|
|
11
|
+
function dense(value) {
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
for (let i = 0; i < value.length; i++)
|
|
14
|
+
if (!Object.hasOwn(value, i) || !dense(value[i]))
|
|
15
|
+
return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return !isObject(value) || Object.values(value).every(dense);
|
|
19
|
+
}
|
|
20
|
+
function capture(value) {
|
|
21
|
+
if (!isJsonValue(value))
|
|
22
|
+
throw new Error("Model interface requires finite acyclic JSON data");
|
|
23
|
+
let copy;
|
|
24
|
+
try {
|
|
25
|
+
copy = structuredClone(value);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error("Model interface requires detached JSON data");
|
|
29
|
+
}
|
|
30
|
+
if (!isJsonValue(copy) || !dense(copy))
|
|
31
|
+
throw new Error("Model interface requires finite dense JSON data");
|
|
32
|
+
return copy;
|
|
33
|
+
}
|
|
34
|
+
function translateReferences(value, translate) {
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
return value.map(child => translateReferences(child, translate));
|
|
37
|
+
if (!isObject(value))
|
|
38
|
+
return value;
|
|
39
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key,
|
|
40
|
+
key === "$ref" && typeof child === "string" ? translate(child) : translateReferences(child, translate)]));
|
|
41
|
+
}
|
|
42
|
+
function semanticReference(path, external) {
|
|
43
|
+
const root = SEMANTIC_ROOT.exec(path)?.[0];
|
|
44
|
+
if (!root || root.startsWith("x_") && external !== undefined && !external.includes(root))
|
|
45
|
+
return false;
|
|
46
|
+
try {
|
|
47
|
+
parseValuePath(path);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Anchor eligible flat current references, never text/resource/effective/historical references. */
|
|
55
|
+
export function canonicalizeModelReferences(value, scopes, external = []) {
|
|
56
|
+
const selected = captureScopeSelection(scopes), copy = capture(value);
|
|
57
|
+
return selected.length !== 1 ? copy : translateReferences(copy, ref => semanticReference(ref, external) ? `${selected[0]}.${ref}` : ref);
|
|
58
|
+
}
|
|
59
|
+
/** Strip only validated same-owner current semantic reference prefixes; canonical data stays untouched. */
|
|
60
|
+
export function projectModelReferences(value, scopes, external = []) {
|
|
61
|
+
const selected = captureScopeSelection(scopes), copy = capture(value), prefix = `${selected[0]}.`;
|
|
62
|
+
return selected.length !== 1 ? copy : translateReferences(copy, ref => ref.startsWith(prefix) && semanticReference(ref.slice(prefix.length), external) ? ref.slice(prefix.length) : ref);
|
|
63
|
+
}
|
|
64
|
+
function admitKeys(value, selected, external) {
|
|
65
|
+
const keys = Object.keys(value);
|
|
66
|
+
if (!keys.length)
|
|
67
|
+
throw new Error("Model patch requires at least one supplied branch");
|
|
68
|
+
if (selected.length === 1) {
|
|
69
|
+
if (keys.some(key => !PATCH_PLANES.includes(key) && !external.includes(key)))
|
|
70
|
+
throw new Error("Single-owner model patches require flat semantic planes");
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
for (const key of keys) {
|
|
74
|
+
if (!selected.includes(key))
|
|
75
|
+
throw new Error(`Multi-owner model patches require participating explicit owners; does not accept field ${key}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function planePatch(value, external) {
|
|
79
|
+
if (!isObject(value) || !Object.keys(value).length)
|
|
80
|
+
throw new Error("Model scope patch must be a nonempty object");
|
|
81
|
+
for (const key of Object.keys(value)) {
|
|
82
|
+
if (!PATCH_PLANES.includes(key) && !external.includes(key))
|
|
83
|
+
throw new Error(`Model patch does not accept semantic plane ${key}`);
|
|
84
|
+
if (!isObject(value[key]))
|
|
85
|
+
throw new Error(`Model patch plane ${key} must be a JSON object`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Value normalization only; callers must validate their original request/owner before invoking it. */
|
|
89
|
+
export function normalizeModelPatches(value, scopes, external = []) {
|
|
90
|
+
const selected = captureScopeSelection(scopes);
|
|
91
|
+
if (!isObject(value))
|
|
92
|
+
throw new Error("Model patch must be an object");
|
|
93
|
+
// Reject shape/excluded owners before touching a supplied owner's payload or cloning it.
|
|
94
|
+
admitKeys(value, selected, external);
|
|
95
|
+
const copy = (selected.length === 1 ? canonicalizeModelReferences(value, selected, external) : capture(value));
|
|
96
|
+
admitKeys(copy, selected, external);
|
|
97
|
+
if (selected.length === 1) {
|
|
98
|
+
planePatch(copy, external);
|
|
99
|
+
return { [selected[0]]: copy };
|
|
100
|
+
}
|
|
101
|
+
for (const key of Object.keys(copy))
|
|
102
|
+
planePatch(copy[key], external);
|
|
103
|
+
return copy;
|
|
104
|
+
}
|
|
105
|
+
/** Compilation target syntax, not a query locator: preserve the exact opaque source key. */
|
|
106
|
+
export function projectModelArtifactTarget(scope, path, scopes) {
|
|
107
|
+
const selected = captureScopeSelection(scopes);
|
|
108
|
+
if (!selected.includes(scope))
|
|
109
|
+
throw new Error("Model artifact target scope is excluded");
|
|
110
|
+
if (typeof path !== "string")
|
|
111
|
+
throw new Error("Model artifact target path must be a string");
|
|
112
|
+
return `${selected.length === 1 ? "" : scope + "."}artifacts[${JSON.stringify(path)}]`;
|
|
113
|
+
}
|
|
114
|
+
/** Known deletion-report JSON pointers only; do not parse source keys or rewrite diagnostics. */
|
|
115
|
+
export function projectModelMissingDeletion(deletion, scopes) {
|
|
116
|
+
const selected = captureScopeSelection(scopes);
|
|
117
|
+
const project = (path) => {
|
|
118
|
+
if (typeof path !== "string")
|
|
119
|
+
throw new Error("Model deletion locator must be a string");
|
|
120
|
+
const prefix = `/${selected[0]}`;
|
|
121
|
+
return selected.length === 1 && (path === prefix || path.startsWith(prefix + "/")) ? path.slice(prefix.length) : path;
|
|
122
|
+
};
|
|
123
|
+
return { path: project(deletion.path), unavailablePath: project(deletion.unavailablePath) };
|
|
124
|
+
}
|
|
125
|
+
function validateReadPath(path, selected, historyLimit) {
|
|
126
|
+
const query = PATCH_PATH.test(path) ? parseStateReadPath(path, historyLimit) : parseStateReadPath(parseValuePath(path).root, historyLimit);
|
|
127
|
+
if (query.scope !== undefined && !selected.includes(query.scope))
|
|
128
|
+
throw new Error("Model read scope is excluded");
|
|
129
|
+
}
|
|
130
|
+
/** Sole-owner locators bind to that exact owner; multi-owner effective aliases keep their existing meaning. */
|
|
131
|
+
export function normalizeModelReadPath(path, scopes, historyLimit = DEFAULT_HISTORY_LIMIT) {
|
|
132
|
+
const selected = captureScopeSelection(scopes);
|
|
133
|
+
if (typeof path !== "string")
|
|
134
|
+
throw new Error("Model read path must be a string");
|
|
135
|
+
let canonical = path;
|
|
136
|
+
if (selected.length === 1) {
|
|
137
|
+
if (/^patches(?:\[\d+\])?$/.test(path))
|
|
138
|
+
canonical = `${selected[0]}.${path}`;
|
|
139
|
+
else if (SEMANTIC_ROOT.test(path))
|
|
140
|
+
canonical = `${selected[0]}.${path}`;
|
|
141
|
+
else if (/^effective(?=\.|\[|$)/.test(path))
|
|
142
|
+
canonical = selected[0] + path.slice("effective".length);
|
|
143
|
+
}
|
|
144
|
+
validateReadPath(canonical, selected, historyLimit);
|
|
145
|
+
return canonical;
|
|
146
|
+
}
|
|
147
|
+
/** Presentation locator only: history keeps an explicit projection root and does not become an array index. */
|
|
148
|
+
export function projectModelReadPath(path, scopes, historyLimit = DEFAULT_HISTORY_LIMIT) {
|
|
149
|
+
const selected = captureScopeSelection(scopes), canonical = normalizeModelReadPath(path, selected, historyLimit);
|
|
150
|
+
if (selected.length !== 1)
|
|
151
|
+
return canonical;
|
|
152
|
+
const prefix = `${selected[0]}.`;
|
|
153
|
+
if (canonical.startsWith(prefix) && /^patches(?:\[\d+\])?$/.test(canonical.slice(prefix.length)))
|
|
154
|
+
return canonical.slice(prefix.length);
|
|
155
|
+
if (canonical.startsWith(prefix) && semanticReference(canonical.slice(prefix.length)))
|
|
156
|
+
return canonical.slice(prefix.length);
|
|
157
|
+
return "effective" + canonical.slice(selected[0].length);
|
|
158
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { JsonObject, JsonValue } from "./json.ts";
|
|
3
|
+
import type { StateFlowModule } from "./semantic-module.ts";
|
|
4
|
+
type SessionManager = ExtensionContext["sessionManager"];
|
|
5
|
+
type Definition = StateFlowModule<JsonObject, JsonValue>;
|
|
6
|
+
export interface StateFlowModuleCatalog {
|
|
7
|
+
readonly modules: readonly Definition[];
|
|
8
|
+
readonly signal: AbortSignal;
|
|
9
|
+
assertCurrent(): void;
|
|
10
|
+
}
|
|
11
|
+
/** Registry metadata alone grants no content, source, private or publication authority. */
|
|
12
|
+
export declare function registerStateFlowModule<Content extends JsonObject, Input extends JsonValue>(manager: SessionManager, definition: StateFlowModule<Content, Input>): () => void;
|
|
13
|
+
/** One composition-root owner; catalogs are revoked independently of memory mode or storage. */
|
|
14
|
+
export declare class StateFlowModuleHost {
|
|
15
|
+
#private;
|
|
16
|
+
bind(manager: SessionManager, checkOwner: () => void): void;
|
|
17
|
+
captureIfBound(): StateFlowModuleCatalog | undefined;
|
|
18
|
+
capture(): StateFlowModuleCatalog;
|
|
19
|
+
dispose(): void;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const BRIDGE = Symbol.for("@llblab/pi-state-flow/modules/v1");
|
|
2
|
+
function bridge(create = false) {
|
|
3
|
+
const globals = globalThis;
|
|
4
|
+
let value = globals[BRIDGE];
|
|
5
|
+
if (value === undefined && create)
|
|
6
|
+
globals[BRIDGE] = value = { version: 1, hosts: new WeakMap() };
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (!value || typeof value !== "object")
|
|
10
|
+
throw new Error("Incompatible State Flow module registration bridge");
|
|
11
|
+
const candidate = value;
|
|
12
|
+
if (candidate.version !== 1 || !(candidate.hosts instanceof WeakMap))
|
|
13
|
+
throw new Error("Incompatible State Flow module registration bridge");
|
|
14
|
+
return candidate;
|
|
15
|
+
}
|
|
16
|
+
function captureDefinition(definition) {
|
|
17
|
+
if (!definition || typeof definition !== "object")
|
|
18
|
+
throw new Error("Invalid State Flow module registration");
|
|
19
|
+
const { apiVersion, id, schemaVersion, immutable, validate, context, captureInput, produce } = definition;
|
|
20
|
+
if (apiVersion !== 1)
|
|
21
|
+
throw new Error("Unsupported State Flow module API version");
|
|
22
|
+
if (typeof id !== "string" || !/^x_[a-z][a-z0-9_]*$/.test(id))
|
|
23
|
+
throw new Error("External State Flow module id must match x_[a-z][a-z0-9_]*");
|
|
24
|
+
if (!Number.isSafeInteger(schemaVersion) || schemaVersion < 1)
|
|
25
|
+
throw new Error("Invalid State Flow module schema version");
|
|
26
|
+
if (typeof immutable !== "boolean" || typeof validate !== "function" || typeof context !== "function"
|
|
27
|
+
|| (captureInput !== undefined && typeof captureInput !== "function") || (produce !== undefined && typeof produce !== "function")) {
|
|
28
|
+
throw new Error("Invalid State Flow module callbacks or authoring policy");
|
|
29
|
+
}
|
|
30
|
+
// Preserve class/constructor-injected services while capturing policy and callback identities.
|
|
31
|
+
return Object.freeze({ apiVersion, id, schemaVersion, immutable, validate: validate.bind(definition), context: context.bind(definition),
|
|
32
|
+
...(captureInput ? { captureInput: captureInput.bind(definition) } : {}), ...(produce ? { produce: produce.bind(definition) } : {}) });
|
|
33
|
+
}
|
|
34
|
+
/** Registry metadata alone grants no content, source, private or publication authority. */
|
|
35
|
+
export function registerStateFlowModule(manager, definition) {
|
|
36
|
+
const host = bridge()?.hosts.get(manager);
|
|
37
|
+
if (!host)
|
|
38
|
+
throw new Error("State Flow module host unavailable; load State Flow first and register at session_start");
|
|
39
|
+
return host.register(definition);
|
|
40
|
+
}
|
|
41
|
+
/** One composition-root owner; catalogs are revoked independently of memory mode or storage. */
|
|
42
|
+
export class StateFlowModuleHost {
|
|
43
|
+
#binding;
|
|
44
|
+
bind(manager, checkOwner) {
|
|
45
|
+
checkOwner();
|
|
46
|
+
const hosts = bridge(true).hosts;
|
|
47
|
+
const existing = hosts.get(manager);
|
|
48
|
+
if (existing && existing !== this.#binding)
|
|
49
|
+
throw new Error("State Flow module host already bound to this native session manager");
|
|
50
|
+
this.dispose();
|
|
51
|
+
if (this.#binding || hosts.has(manager))
|
|
52
|
+
throw new Error("State Flow module host changed during rebind");
|
|
53
|
+
checkOwner();
|
|
54
|
+
const binding = { manager, checkOwner, modules: new Map(), controller: new AbortController(), register: definition => this.#register(binding, definition) };
|
|
55
|
+
this.#binding = binding;
|
|
56
|
+
hosts.set(manager, binding);
|
|
57
|
+
}
|
|
58
|
+
#check(binding) {
|
|
59
|
+
if (this.#binding !== binding || bridge()?.hosts.get(binding.manager) !== binding)
|
|
60
|
+
throw new Error("State Flow module host ownership changed");
|
|
61
|
+
binding.checkOwner();
|
|
62
|
+
}
|
|
63
|
+
#changed(binding) {
|
|
64
|
+
const previous = binding.controller;
|
|
65
|
+
binding.controller = new AbortController();
|
|
66
|
+
previous.abort(new Error("State Flow module catalog changed"));
|
|
67
|
+
}
|
|
68
|
+
#register(binding, definition) {
|
|
69
|
+
this.#check(binding);
|
|
70
|
+
const captured = captureDefinition(definition);
|
|
71
|
+
this.#check(binding);
|
|
72
|
+
if (binding.modules.has(captured.id))
|
|
73
|
+
throw new Error(`State Flow module id already registered: ${captured.id}`);
|
|
74
|
+
binding.modules.set(captured.id, captured);
|
|
75
|
+
this.#changed(binding);
|
|
76
|
+
return () => {
|
|
77
|
+
if (this.#binding !== binding || binding.modules.get(captured.id) !== captured)
|
|
78
|
+
return;
|
|
79
|
+
binding.modules.delete(captured.id);
|
|
80
|
+
this.#changed(binding);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
captureIfBound() {
|
|
84
|
+
return this.#binding ? this.capture() : undefined;
|
|
85
|
+
}
|
|
86
|
+
capture() {
|
|
87
|
+
const binding = this.#binding;
|
|
88
|
+
if (!binding)
|
|
89
|
+
throw new Error("State Flow module host unavailable");
|
|
90
|
+
this.#check(binding);
|
|
91
|
+
const signal = binding.controller.signal;
|
|
92
|
+
return Object.freeze({ modules: Object.freeze([...binding.modules.values()]), signal, assertCurrent: () => {
|
|
93
|
+
this.#check(binding);
|
|
94
|
+
signal.throwIfAborted();
|
|
95
|
+
} });
|
|
96
|
+
}
|
|
97
|
+
dispose() {
|
|
98
|
+
const binding = this.#binding;
|
|
99
|
+
if (!binding)
|
|
100
|
+
return;
|
|
101
|
+
this.#binding = undefined;
|
|
102
|
+
const hosts = bridge()?.hosts;
|
|
103
|
+
if (hosts?.get(binding.manager) === binding)
|
|
104
|
+
hosts.delete(binding.manager);
|
|
105
|
+
binding.modules.clear();
|
|
106
|
+
binding.controller.abort(new Error("State Flow module host retired"));
|
|
107
|
+
}
|
|
108
|
+
}
|