ai-workflow-logic 0.1.4 → 0.1.6
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/package.json +6 -4
- package/src/checkpoint-filesystem.ts +152 -13
- package/src/checkpoint-profile.ts +175 -0
- package/src/checkpoint-sidecar.ts +547 -0
- package/src/checkpoint-store.ts +73 -18
- package/src/errors.ts +17 -0
- package/src/index.ts +18 -0
- package/src/run-checkpoint.ts +43 -1
- package/src/step-extension.ts +121 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-workflow-logic",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "AI workflow domain projections and pure runtime helpers with an explicit Halfcode filesystem adapter",
|
|
6
6
|
"type": "module",
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
"./run-freeze": "./src/run-freeze.ts"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"ai-workflow-contract": "0.1.
|
|
16
|
+
"ai-workflow-contract": "0.1.6",
|
|
17
|
+
"flow-step-space-contract": "0.1.1",
|
|
18
|
+
"flow-step-space-logic": "0.1.1",
|
|
17
19
|
"halfcode-compiler.xnl": "0.2.2",
|
|
18
|
-
"work-ctrl-flow-contract": "0.1.
|
|
19
|
-
"work-ctrl-flow-logic": "0.1.
|
|
20
|
+
"work-ctrl-flow-contract": "0.1.3",
|
|
21
|
+
"work-ctrl-flow-logic": "0.1.3"
|
|
20
22
|
},
|
|
21
23
|
"files": [
|
|
22
24
|
"src"
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
import type { WorkCtrlFlowSnapshot } from 'work-ctrl-flow-contract';
|
|
16
16
|
import {
|
|
17
17
|
createMaterializedWorkCtrlFlowEngineFromDirectory,
|
|
18
|
+
loadFrozenCtrlDefinitionStepIds,
|
|
18
19
|
loadMaterializedFlowInstance,
|
|
19
20
|
materializeFlowInstance,
|
|
20
21
|
type MaterializedWorkCtrlFlowDirectoryOptions,
|
|
@@ -30,6 +31,24 @@ import {
|
|
|
30
31
|
normalizeFlowClosedObject,
|
|
31
32
|
normalizeFlowRunCheckpoint,
|
|
32
33
|
} from './run-checkpoint.js';
|
|
34
|
+
import {
|
|
35
|
+
checkpointHeadJson,
|
|
36
|
+
createDirectoryDurably,
|
|
37
|
+
encodeCheckpointSidecars,
|
|
38
|
+
isCheckpointHead,
|
|
39
|
+
persistCheckpointSidecars,
|
|
40
|
+
readCheckpointHeadAndLogical,
|
|
41
|
+
renameDurably,
|
|
42
|
+
writeDurableCandidate,
|
|
43
|
+
type CheckpointDurabilityRuntime,
|
|
44
|
+
type FlowRunCheckpointHead,
|
|
45
|
+
} from './checkpoint-sidecar.js';
|
|
46
|
+
import type { StepMerkleTree } from 'flow-step-space-contract';
|
|
47
|
+
import type { DefinitionStepExtensionCodecRegistryPort } from 'flow-step-space-contract';
|
|
48
|
+
import {
|
|
49
|
+
validateCheckpointAgainstFrozenProfile,
|
|
50
|
+
type FlowRunProfileAuthorityPort,
|
|
51
|
+
} from './checkpoint-profile.js';
|
|
33
52
|
|
|
34
53
|
const CHECKPOINT_FILE = 'checkpoint.json';
|
|
35
54
|
const CHECKPOINT_STAGING_FILE = 'checkpoint.staging.json';
|
|
@@ -47,6 +66,31 @@ const NO_FOLLOW = fs.constants.O_NOFOLLOW ?? 0;
|
|
|
47
66
|
export interface FilesystemFlowRunCheckpointStoreOptions {
|
|
48
67
|
/** Injected support root; the domain contract never derives a host path. */
|
|
49
68
|
readonly storeRoot: string;
|
|
69
|
+
readonly durability?: CheckpointDurabilityRuntime;
|
|
70
|
+
readonly profileAuthority?: FlowRunProfileAuthorityPort;
|
|
71
|
+
readonly extensionCodecs?: DefinitionStepExtensionCodecRegistryPort;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface FilesystemFlowRunCheckpointRuntime {
|
|
75
|
+
readonly storage: FilesystemFlowRunCheckpointStoreOptions;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CreateFilesystemFlowRunCheckpointStoreInput {}
|
|
79
|
+
export interface CreateFilesystemFlowRunCheckpointStoreConfig {}
|
|
80
|
+
|
|
81
|
+
export function createFilesystemFlowRunCheckpointRuntime(
|
|
82
|
+
options: FilesystemFlowRunCheckpointStoreOptions,
|
|
83
|
+
): FilesystemFlowRunCheckpointRuntime {
|
|
84
|
+
return Object.freeze({ storage: Object.freeze({ ...options }) });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Processor authority; host root policy is injected only through runtime.storage. */
|
|
88
|
+
export function createFilesystemFlowRunCheckpointStore(
|
|
89
|
+
runtime: FilesystemFlowRunCheckpointRuntime,
|
|
90
|
+
_input: CreateFilesystemFlowRunCheckpointStoreInput,
|
|
91
|
+
_config: CreateFilesystemFlowRunCheckpointStoreConfig,
|
|
92
|
+
): FilesystemFlowRunCheckpointStore {
|
|
93
|
+
return new FilesystemFlowRunCheckpointStore(runtime.storage);
|
|
50
94
|
}
|
|
51
95
|
|
|
52
96
|
export interface CanonicalMaterializedWorkCtrlFlowOptions
|
|
@@ -85,7 +129,11 @@ export function createCanonicalMaterializedWorkCtrlFlowEngine(
|
|
|
85
129
|
'bundleDir and source identity are required to admit a new canonical WorkCtrl instance.',
|
|
86
130
|
);
|
|
87
131
|
})();
|
|
88
|
-
const checkpointStore =
|
|
132
|
+
const checkpointStore = createFilesystemFlowRunCheckpointStore(
|
|
133
|
+
createFilesystemFlowRunCheckpointRuntime({ storeRoot: options.storeRoot }),
|
|
134
|
+
{},
|
|
135
|
+
{},
|
|
136
|
+
);
|
|
89
137
|
const store = createWorkCtrlFlowCheckpointStore(
|
|
90
138
|
{ checkpointStore },
|
|
91
139
|
{
|
|
@@ -222,12 +270,18 @@ type FlowRunFilesystemLocations = {
|
|
|
222
270
|
export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<FlowRunProfileState> {
|
|
223
271
|
readonly #storeRoot: string;
|
|
224
272
|
readonly #realStoreRoot: string;
|
|
273
|
+
readonly #durability: CheckpointDurabilityRuntime | undefined;
|
|
274
|
+
readonly #profileAuthority: FlowRunProfileAuthorityPort | undefined;
|
|
275
|
+
readonly #extensionCodecs: DefinitionStepExtensionCodecRegistryPort;
|
|
225
276
|
|
|
226
277
|
constructor(options: FilesystemFlowRunCheckpointStoreOptions) {
|
|
227
278
|
const root = path.resolve(options.storeRoot);
|
|
228
279
|
assertExistingDirectory(root, undefined, 'flow-run-store-root-invalid');
|
|
229
280
|
this.#storeRoot = root;
|
|
230
281
|
this.#realStoreRoot = fs.realpathSync(root);
|
|
282
|
+
this.#durability = options.durability;
|
|
283
|
+
this.#profileAuthority = options.profileAuthority;
|
|
284
|
+
this.#extensionCodecs = options.extensionCodecs ?? Object.freeze({ resolve: () => undefined });
|
|
231
285
|
}
|
|
232
286
|
|
|
233
287
|
async load(key: FlowRunCheckpointKey): Promise<AIWorkflowFlowRunCheckpoint | undefined> {
|
|
@@ -241,6 +295,7 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
241
295
|
if (!fs.existsSync(locations.checkpointFile)) return undefined;
|
|
242
296
|
const checkpoint = readCheckpointFile(locations.checkpointFile, this.#realStoreRoot);
|
|
243
297
|
this.#assertCheckpointIdentity(checkpoint, key, descriptor.definition);
|
|
298
|
+
this.#validateFrozenProfile(checkpoint);
|
|
244
299
|
return checkpoint;
|
|
245
300
|
}
|
|
246
301
|
|
|
@@ -254,24 +309,44 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
254
309
|
this.#preflightExistingRunBoundary(locations);
|
|
255
310
|
const descriptor = this.#requireInstance(locations.instanceDir, key.instanceId);
|
|
256
311
|
this.#assertCheckpointIdentity(checkpoint, key, descriptor.definition);
|
|
312
|
+
const frozenAuthority = this.#validateFrozenProfile(checkpoint);
|
|
257
313
|
|
|
258
314
|
this.#ensureRunDirectory(locations);
|
|
259
315
|
const lock = await acquireLock(locations.runDir, locations.lockFile, this.#realStoreRoot);
|
|
260
316
|
try {
|
|
261
317
|
this.#assertRunPhysicalBoundary(locations);
|
|
262
318
|
removeUncommittedStaging(locations.stagingFile, locations.runDir, this.#realStoreRoot);
|
|
263
|
-
const
|
|
264
|
-
?
|
|
319
|
+
const currentStorage = fs.existsSync(locations.checkpointFile)
|
|
320
|
+
? readCheckpointStorage(locations.checkpointFile, this.#realStoreRoot)
|
|
265
321
|
: undefined;
|
|
266
|
-
|
|
322
|
+
const current = currentStorage?.checkpoint;
|
|
323
|
+
if (current) {
|
|
324
|
+
this.#assertCheckpointIdentity(current, key, descriptor.definition);
|
|
325
|
+
this.#validateFrozenProfile(current);
|
|
326
|
+
}
|
|
267
327
|
const actualVersion = current?.version ?? null;
|
|
268
328
|
if (actualVersion !== input.expectedVersion) {
|
|
269
329
|
throw new FlowRunCheckpointVersionConflictError(input.expectedVersion, actualVersion);
|
|
270
330
|
}
|
|
271
331
|
validateFlowRunCheckpointTransition(current, checkpoint);
|
|
272
|
-
|
|
332
|
+
ensureSidecarDirectories(locations.runDir, this.#realStoreRoot, this.#durability);
|
|
333
|
+
const definitionStepIds = frozenAuthority?.definitionStepIds ?? (checkpoint.profile.kind === 'AIDataWorkflow'
|
|
334
|
+
? undefined
|
|
335
|
+
: loadFrozenCtrlDefinitionStepIds(
|
|
336
|
+
path.join(locations.instanceDir, 'definition'),
|
|
337
|
+
checkpoint.profile.kind,
|
|
338
|
+
));
|
|
339
|
+
const encoded = encodeCheckpointSidecars(checkpoint, currentStorage?.tree, {
|
|
340
|
+
definitionStepIds,
|
|
341
|
+
previousMaterialRecordDigests: currentStorage?.materialRecordDigests,
|
|
342
|
+
});
|
|
343
|
+
removeUnacceptedSidecarReceipt(locations.runDir, checkpoint.version, this.#realStoreRoot);
|
|
344
|
+
persistCheckpointSidecars(locations.runDir, encoded, this.#durability);
|
|
345
|
+
assertSidecarDirectories(locations.runDir, this.#realStoreRoot);
|
|
346
|
+
writeCheckpointCandidate(locations.stagingFile, encoded.head, this.#realStoreRoot, this.#durability);
|
|
273
347
|
const staged = readCheckpointFile(locations.stagingFile, this.#realStoreRoot);
|
|
274
348
|
this.#assertCheckpointIdentity(staged, key, descriptor.definition);
|
|
349
|
+
this.#validateFrozenProfile(staged);
|
|
275
350
|
if (stableJson(staged) !== stableJson(checkpoint)) {
|
|
276
351
|
throw new FlowRunFilesystemStoreError(
|
|
277
352
|
'flow-run-checkpoint-readback-mismatch',
|
|
@@ -283,9 +358,10 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
283
358
|
if (fs.existsSync(locations.checkpointFile)) {
|
|
284
359
|
assertRegularFile(locations.checkpointFile, this.#realStoreRoot, 'flow-run-checkpoint-invalid');
|
|
285
360
|
}
|
|
286
|
-
|
|
361
|
+
renameDurably(locations.stagingFile, locations.checkpointFile, 'head', this.#durability);
|
|
287
362
|
const accepted = readCheckpointFile(locations.checkpointFile, this.#realStoreRoot);
|
|
288
363
|
this.#assertCheckpointIdentity(accepted, key, descriptor.definition);
|
|
364
|
+
this.#validateFrozenProfile(accepted);
|
|
289
365
|
if (stableJson(accepted) !== stableJson(checkpoint)) {
|
|
290
366
|
throw new FlowRunFilesystemStoreError(
|
|
291
367
|
'flow-run-checkpoint-readback-mismatch',
|
|
@@ -491,9 +567,16 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
491
567
|
}
|
|
492
568
|
}
|
|
493
569
|
|
|
570
|
+
#validateFrozenProfile(checkpoint: AIWorkflowFlowRunCheckpoint) {
|
|
571
|
+
return validateCheckpointAgainstFrozenProfile(checkpoint, {
|
|
572
|
+
...(this.#profileAuthority === undefined ? {} : { profileAuthority: this.#profileAuthority }),
|
|
573
|
+
extensionCodecs: this.#extensionCodecs,
|
|
574
|
+
}, { requireAIDataAuthority: true });
|
|
575
|
+
}
|
|
576
|
+
|
|
494
577
|
#ensureRunDirectory(locations: FlowRunFilesystemLocations): void {
|
|
495
|
-
ensureChildDirectory(locations.instanceDir, locations.runsDir, this.#realStoreRoot);
|
|
496
|
-
ensureChildDirectory(locations.runsDir, locations.runDir, this.#realStoreRoot);
|
|
578
|
+
ensureChildDirectory(locations.instanceDir, locations.runsDir, this.#realStoreRoot, this.#durability, 'head');
|
|
579
|
+
ensureChildDirectory(locations.runsDir, locations.runDir, this.#realStoreRoot, this.#durability, 'head');
|
|
497
580
|
this.#assertRunPhysicalBoundary(locations);
|
|
498
581
|
}
|
|
499
582
|
|
|
@@ -919,20 +1002,46 @@ function assertRealContainment(realRoot: string, file: string, code: string): vo
|
|
|
919
1002
|
}
|
|
920
1003
|
}
|
|
921
1004
|
|
|
922
|
-
function ensureChildDirectory(
|
|
1005
|
+
function ensureChildDirectory(
|
|
1006
|
+
parent: string,
|
|
1007
|
+
child: string,
|
|
1008
|
+
realRoot: string,
|
|
1009
|
+
durability?: CheckpointDurabilityRuntime,
|
|
1010
|
+
durabilityRole: 'record' | 'tree' | 'receipt' | 'head' = 'record',
|
|
1011
|
+
): void {
|
|
923
1012
|
assertExistingDirectory(parent, realRoot, 'flow-run-boundary-invalid');
|
|
924
1013
|
if (fs.existsSync(child)) {
|
|
925
1014
|
assertExistingDirectory(child, realRoot, 'flow-run-symlink-boundary');
|
|
926
1015
|
return;
|
|
927
1016
|
}
|
|
928
|
-
|
|
1017
|
+
createDirectoryDurably(child, durabilityRole, durability);
|
|
929
1018
|
assertExistingDirectory(child, realRoot, 'flow-run-boundary-invalid');
|
|
930
1019
|
}
|
|
931
1020
|
|
|
932
1021
|
function readCheckpointFile(file: string, realRoot: string): AIWorkflowFlowRunCheckpoint {
|
|
1022
|
+
return readCheckpointStorage(file, realRoot).checkpoint;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function readCheckpointStorage(
|
|
1026
|
+
file: string,
|
|
1027
|
+
realRoot: string,
|
|
1028
|
+
): {
|
|
1029
|
+
readonly checkpoint: AIWorkflowFlowRunCheckpoint;
|
|
1030
|
+
readonly tree?: StepMerkleTree;
|
|
1031
|
+
readonly materialRecordDigests?: readonly import('flow-step-space-contract').DefinitionStepDigest[];
|
|
1032
|
+
} {
|
|
933
1033
|
const value = readJsonFile(file, realRoot, 'flow-run-checkpoint-invalid');
|
|
934
1034
|
try {
|
|
935
|
-
|
|
1035
|
+
if (isCheckpointHead(value)) {
|
|
1036
|
+
assertSidecarDirectories(path.dirname(file), realRoot);
|
|
1037
|
+
const loaded = readCheckpointHeadAndLogical(file);
|
|
1038
|
+
return {
|
|
1039
|
+
checkpoint: loaded.checkpoint,
|
|
1040
|
+
tree: loaded.tree,
|
|
1041
|
+
materialRecordDigests: loaded.materialRecordDigests,
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
return { checkpoint: normalizeFlowRunCheckpoint(value) };
|
|
936
1045
|
} catch (error) {
|
|
937
1046
|
throw new FlowRunFilesystemStoreError(
|
|
938
1047
|
'flow-run-checkpoint-invalid',
|
|
@@ -959,14 +1068,44 @@ function readJsonFile(file: string, realRoot: string, code: string): unknown {
|
|
|
959
1068
|
|
|
960
1069
|
function writeCheckpointCandidate(
|
|
961
1070
|
file: string,
|
|
962
|
-
checkpoint:
|
|
1071
|
+
checkpoint: FlowRunCheckpointHead,
|
|
963
1072
|
realRoot: string,
|
|
1073
|
+
durability?: CheckpointDurabilityRuntime,
|
|
964
1074
|
): void {
|
|
965
1075
|
if (fs.existsSync(file)) {
|
|
966
1076
|
throw new FlowRunFilesystemStoreError('flow-run-staging-invalid', 'Checkpoint staging file already exists after recovery.');
|
|
967
1077
|
}
|
|
968
1078
|
assertExistingDirectory(path.dirname(file), realRoot, 'flow-run-boundary-invalid');
|
|
969
|
-
|
|
1079
|
+
writeDurableCandidate(file, checkpointHeadJson(checkpoint), 'head', durability);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function ensureSidecarDirectories(
|
|
1083
|
+
runDir: string,
|
|
1084
|
+
realRoot: string,
|
|
1085
|
+
durability?: CheckpointDurabilityRuntime,
|
|
1086
|
+
): void {
|
|
1087
|
+
const sidecar = path.join(runDir, 'step-space');
|
|
1088
|
+
ensureChildDirectory(runDir, sidecar, realRoot, durability, 'record');
|
|
1089
|
+
ensureChildDirectory(sidecar, path.join(sidecar, 'controls'), realRoot, durability, 'record');
|
|
1090
|
+
ensureChildDirectory(sidecar, path.join(sidecar, 'records'), realRoot, durability, 'record');
|
|
1091
|
+
ensureChildDirectory(sidecar, path.join(sidecar, 'trees'), realRoot, durability, 'tree');
|
|
1092
|
+
ensureChildDirectory(sidecar, path.join(sidecar, 'receipts'), realRoot, durability, 'receipt');
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function assertSidecarDirectories(runDir: string, realRoot: string): void {
|
|
1096
|
+
const sidecar = path.join(runDir, 'step-space');
|
|
1097
|
+
assertExistingDirectory(sidecar, realRoot, 'flow-run-sidecar-boundary-invalid');
|
|
1098
|
+
assertExistingDirectory(path.join(sidecar, 'controls'), realRoot, 'flow-run-sidecar-boundary-invalid');
|
|
1099
|
+
assertExistingDirectory(path.join(sidecar, 'records'), realRoot, 'flow-run-sidecar-boundary-invalid');
|
|
1100
|
+
assertExistingDirectory(path.join(sidecar, 'trees'), realRoot, 'flow-run-sidecar-boundary-invalid');
|
|
1101
|
+
assertExistingDirectory(path.join(sidecar, 'receipts'), realRoot, 'flow-run-sidecar-boundary-invalid');
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function removeUnacceptedSidecarReceipt(runDir: string, version: number, realRoot: string): void {
|
|
1105
|
+
const file = path.join(runDir, 'step-space', 'receipts', `${version}.json`);
|
|
1106
|
+
if (!fs.existsSync(file)) return;
|
|
1107
|
+
assertRegularFile(file, realRoot, 'flow-run-sidecar-receipt-invalid');
|
|
1108
|
+
fs.unlinkSync(file);
|
|
970
1109
|
}
|
|
971
1110
|
|
|
972
1111
|
function writeBytesNoFollow(file: string, bytes: string): void {
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AIWorkflowFlowRunCheckpoint,
|
|
3
|
+
FlowClosedObject,
|
|
4
|
+
FlowClosedValue,
|
|
5
|
+
FlowDefinitionArtifactIdentity,
|
|
6
|
+
FlowRunProfileState,
|
|
7
|
+
FlowRunStepExtensions,
|
|
8
|
+
} from 'ai-workflow-contract';
|
|
9
|
+
import type { DefinitionStepExtensionCodecRegistryPort } from 'flow-step-space-contract';
|
|
10
|
+
import { normalizeFlowClosedValue } from './run-checkpoint.js';
|
|
11
|
+
import { FlowRunCheckpointTransitionError } from './errors.js';
|
|
12
|
+
|
|
13
|
+
export interface FrozenRunStepExtensionDefinition {
|
|
14
|
+
readonly schemaRef?: string;
|
|
15
|
+
readonly initialValue: FlowClosedValue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FrozenFlowRunProfileAuthority {
|
|
19
|
+
readonly profileKind: FlowRunProfileState['kind'];
|
|
20
|
+
readonly definitionStepIds: readonly string[];
|
|
21
|
+
readonly binding?: FlowClosedObject;
|
|
22
|
+
readonly extensionsByStepId: Readonly<
|
|
23
|
+
Record<string, Readonly<Record<string, FrozenRunStepExtensionDefinition>>>
|
|
24
|
+
>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FlowRunProfileAuthorityRequest {
|
|
28
|
+
readonly instanceId: string;
|
|
29
|
+
readonly definition: FlowDefinitionArtifactIdentity;
|
|
30
|
+
readonly profileKind: FlowRunProfileState['kind'];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Runtime-only adapter over a materialized frozen instance definition. */
|
|
34
|
+
export interface FlowRunProfileAuthorityPort {
|
|
35
|
+
load(request: FlowRunProfileAuthorityRequest): FrozenFlowRunProfileAuthority;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface FlowRunCheckpointAdmissionRuntime {
|
|
39
|
+
readonly profileAuthority?: FlowRunProfileAuthorityPort;
|
|
40
|
+
readonly extensionCodecs: DefinitionStepExtensionCodecRegistryPort;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const compareCodeUnits = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0;
|
|
44
|
+
const stable = (value: unknown): string => {
|
|
45
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
46
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
47
|
+
const record = value as Readonly<Record<string, unknown>>;
|
|
48
|
+
return `{${Object.keys(record).sort(compareCodeUnits).map((key) => `${JSON.stringify(key)}:${stable(record[key])}`).join(',')}}`;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function createInitialRunStepExtensions(
|
|
52
|
+
authority: FrozenFlowRunProfileAuthority,
|
|
53
|
+
): FlowRunStepExtensions | undefined {
|
|
54
|
+
const byStepId: Record<string, Record<string, {
|
|
55
|
+
readonly schemaRef?: string;
|
|
56
|
+
readonly revision: number;
|
|
57
|
+
readonly value: FlowClosedValue;
|
|
58
|
+
}>> = {};
|
|
59
|
+
for (const stepId of Object.keys(authority.extensionsByStepId).sort(compareCodeUnits)) {
|
|
60
|
+
const kinds = authority.extensionsByStepId[stepId];
|
|
61
|
+
const facts: Record<string, { readonly schemaRef?: string; readonly revision: number; readonly value: FlowClosedValue }> = {};
|
|
62
|
+
for (const kind of Object.keys(kinds).sort(compareCodeUnits)) {
|
|
63
|
+
const extension = kinds[kind];
|
|
64
|
+
facts[kind] = Object.freeze({
|
|
65
|
+
...(extension.schemaRef === undefined ? {} : { schemaRef: extension.schemaRef }),
|
|
66
|
+
revision: 0,
|
|
67
|
+
value: normalizeFlowClosedValue(extension.initialValue, `profile.extensions.${stepId}.${kind}`),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (Object.keys(facts).length > 0) byStepId[stepId] = Object.freeze(facts);
|
|
71
|
+
}
|
|
72
|
+
return Object.keys(byStepId).length === 0
|
|
73
|
+
? undefined
|
|
74
|
+
: Object.freeze({
|
|
75
|
+
schemaVersion: 'depa.flow-run-step-extensions/v1' as const,
|
|
76
|
+
byStepId: Object.freeze(byStepId),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function validateCheckpointAgainstFrozenProfile(
|
|
81
|
+
checkpoint: AIWorkflowFlowRunCheckpoint,
|
|
82
|
+
runtime: FlowRunCheckpointAdmissionRuntime,
|
|
83
|
+
options: { readonly requireAIDataAuthority?: boolean } = {},
|
|
84
|
+
): FrozenFlowRunProfileAuthority | undefined {
|
|
85
|
+
const authority = runtime.profileAuthority?.load({
|
|
86
|
+
instanceId: checkpoint.instanceId,
|
|
87
|
+
definition: checkpoint.definition,
|
|
88
|
+
profileKind: checkpoint.profile.kind,
|
|
89
|
+
});
|
|
90
|
+
if (!authority) {
|
|
91
|
+
if (checkpoint.stepExtensions !== undefined) {
|
|
92
|
+
throw new FlowRunCheckpointTransitionError('Run step extension facts require frozen profile authority.');
|
|
93
|
+
}
|
|
94
|
+
if (options.requireAIDataAuthority && checkpoint.profile.kind === 'AIDataWorkflow') {
|
|
95
|
+
throw new FlowRunCheckpointTransitionError('AI Data checkpoint admission requires frozen profile authority.');
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
if (authority.profileKind !== checkpoint.profile.kind) {
|
|
100
|
+
throw new FlowRunCheckpointTransitionError('Frozen profile kind differs from the checkpoint profile.');
|
|
101
|
+
}
|
|
102
|
+
const ids = [...authority.definitionStepIds];
|
|
103
|
+
if (ids.some((id) => typeof id !== 'string' || !id || id !== id.trim()) || new Set(ids).size !== ids.length) {
|
|
104
|
+
throw new FlowRunCheckpointTransitionError('Frozen profile step membership is invalid.');
|
|
105
|
+
}
|
|
106
|
+
for (const stepId of Object.keys(authority.extensionsByStepId)) {
|
|
107
|
+
if (!ids.includes(stepId)) {
|
|
108
|
+
throw new FlowRunCheckpointTransitionError(`Frozen extension step ${stepId} is not a definition member.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (checkpoint.profile.kind === 'AIDataWorkflow') {
|
|
112
|
+
if (!authority.binding) throw new FlowRunCheckpointTransitionError('Frozen AI Data authority is missing its binding.');
|
|
113
|
+
if (stable(checkpoint.profile.runGraph.binding) !== stable(authority.binding)) {
|
|
114
|
+
throw new FlowRunCheckpointTransitionError('AI Data binding differs from the materialized frozen definition.');
|
|
115
|
+
}
|
|
116
|
+
const binding = authority.binding as Record<string, FlowClosedValue>;
|
|
117
|
+
const definition = binding.definition as Record<string, FlowClosedValue> | undefined;
|
|
118
|
+
if (!definition || stable(definition.declarationOrder) !== stable(ids)) {
|
|
119
|
+
throw new FlowRunCheckpointTransitionError('Frozen AI Data binding and StepSpace membership differ.');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const initial = createInitialRunStepExtensions(authority);
|
|
123
|
+
if (initial === undefined) {
|
|
124
|
+
if (checkpoint.stepExtensions !== undefined) {
|
|
125
|
+
throw new FlowRunCheckpointTransitionError('Checkpoint contains extensions absent from the frozen definition.');
|
|
126
|
+
}
|
|
127
|
+
return authority;
|
|
128
|
+
}
|
|
129
|
+
const actual = checkpoint.stepExtensions;
|
|
130
|
+
if (!actual) throw new FlowRunCheckpointTransitionError('Checkpoint omits extensions required by the frozen definition.');
|
|
131
|
+
if (stable(Object.keys(actual.byStepId).sort(compareCodeUnits)) !== stable(Object.keys(initial.byStepId).sort(compareCodeUnits))) {
|
|
132
|
+
throw new FlowRunCheckpointTransitionError('Checkpoint extension step membership differs from the frozen definition.');
|
|
133
|
+
}
|
|
134
|
+
for (const stepId of Object.keys(initial.byStepId).sort(compareCodeUnits)) {
|
|
135
|
+
const expectedKinds = Object.keys(initial.byStepId[stepId]).sort(compareCodeUnits);
|
|
136
|
+
const actualKinds = Object.keys(actual.byStepId[stepId] ?? {}).sort(compareCodeUnits);
|
|
137
|
+
if (stable(actualKinds) !== stable(expectedKinds)) {
|
|
138
|
+
throw new FlowRunCheckpointTransitionError(`Checkpoint extension kinds differ for ${stepId}.`);
|
|
139
|
+
}
|
|
140
|
+
for (const kind of expectedKinds) {
|
|
141
|
+
const definition = authority.extensionsByStepId[stepId][kind];
|
|
142
|
+
const fact = actual.byStepId[stepId][kind];
|
|
143
|
+
if (definition.schemaRef !== fact.schemaRef) {
|
|
144
|
+
throw new FlowRunCheckpointTransitionError(`Checkpoint extension schema differs for ${stepId}/${kind}.`);
|
|
145
|
+
}
|
|
146
|
+
const registration = runtime.extensionCodecs.resolve(kind);
|
|
147
|
+
if (!registration?.codec || typeof registration.codec.normalize !== 'function') {
|
|
148
|
+
throw new FlowRunCheckpointTransitionError(`Checkpoint extension ${stepId}/${kind} has no runtime codec.`);
|
|
149
|
+
}
|
|
150
|
+
if (registration.schemaRef !== definition.schemaRef) {
|
|
151
|
+
throw new FlowRunCheckpointTransitionError(`Runtime codec schema differs for ${stepId}/${kind}.`);
|
|
152
|
+
}
|
|
153
|
+
let normalized: FlowClosedValue;
|
|
154
|
+
try {
|
|
155
|
+
normalized = normalizeFlowClosedValue(registration.codec.normalize(fact.value as never, {
|
|
156
|
+
kind,
|
|
157
|
+
...(definition.schemaRef === undefined ? {} : { schemaRef: definition.schemaRef }),
|
|
158
|
+
sourceRef: `frozen-step://${stepId}/${kind}`,
|
|
159
|
+
stepId,
|
|
160
|
+
}), `checkpoint.stepExtensions.${stepId}.${kind}`);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new FlowRunCheckpointTransitionError(
|
|
163
|
+
`Checkpoint extension codec rejected ${stepId}/${kind}: ${(error as Error).message}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (stable(normalized) !== stable(fact.value)) {
|
|
167
|
+
throw new FlowRunCheckpointTransitionError(`Checkpoint extension ${stepId}/${kind} is not normalized.`);
|
|
168
|
+
}
|
|
169
|
+
if (checkpoint.version === 0 && (fact.revision !== 0 || stable(fact.value) !== stable(definition.initialValue))) {
|
|
170
|
+
throw new FlowRunCheckpointTransitionError(`Initial checkpoint extension ${stepId}/${kind} differs from the frozen definition.`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return authority;
|
|
175
|
+
}
|