ai-workflow-logic 0.1.5 → 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 -6
- package/src/checkpoint-filesystem.ts +108 -19
- package/src/checkpoint-profile.ts +175 -0
- package/src/checkpoint-sidecar.ts +464 -336
- 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,12 +13,12 @@
|
|
|
13
13
|
"./run-freeze": "./src/run-freeze.ts"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"ai-workflow-contract": "0.1.
|
|
17
|
-
"flow-step-space-contract": "0.1.
|
|
18
|
-
"flow-step-space-logic": "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",
|
|
19
19
|
"halfcode-compiler.xnl": "0.2.2",
|
|
20
|
-
"work-ctrl-flow-contract": "0.1.
|
|
21
|
-
"work-ctrl-flow-logic": "0.1.
|
|
20
|
+
"work-ctrl-flow-contract": "0.1.3",
|
|
21
|
+
"work-ctrl-flow-logic": "0.1.3"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
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,
|
|
@@ -32,13 +33,22 @@ import {
|
|
|
32
33
|
} from './run-checkpoint.js';
|
|
33
34
|
import {
|
|
34
35
|
checkpointHeadJson,
|
|
36
|
+
createDirectoryDurably,
|
|
35
37
|
encodeCheckpointSidecars,
|
|
36
38
|
isCheckpointHead,
|
|
37
39
|
persistCheckpointSidecars,
|
|
38
40
|
readCheckpointHeadAndLogical,
|
|
41
|
+
renameDurably,
|
|
42
|
+
writeDurableCandidate,
|
|
43
|
+
type CheckpointDurabilityRuntime,
|
|
39
44
|
type FlowRunCheckpointHead,
|
|
40
45
|
} from './checkpoint-sidecar.js';
|
|
41
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';
|
|
42
52
|
|
|
43
53
|
const CHECKPOINT_FILE = 'checkpoint.json';
|
|
44
54
|
const CHECKPOINT_STAGING_FILE = 'checkpoint.staging.json';
|
|
@@ -56,6 +66,31 @@ const NO_FOLLOW = fs.constants.O_NOFOLLOW ?? 0;
|
|
|
56
66
|
export interface FilesystemFlowRunCheckpointStoreOptions {
|
|
57
67
|
/** Injected support root; the domain contract never derives a host path. */
|
|
58
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);
|
|
59
94
|
}
|
|
60
95
|
|
|
61
96
|
export interface CanonicalMaterializedWorkCtrlFlowOptions
|
|
@@ -94,7 +129,11 @@ export function createCanonicalMaterializedWorkCtrlFlowEngine(
|
|
|
94
129
|
'bundleDir and source identity are required to admit a new canonical WorkCtrl instance.',
|
|
95
130
|
);
|
|
96
131
|
})();
|
|
97
|
-
const checkpointStore =
|
|
132
|
+
const checkpointStore = createFilesystemFlowRunCheckpointStore(
|
|
133
|
+
createFilesystemFlowRunCheckpointRuntime({ storeRoot: options.storeRoot }),
|
|
134
|
+
{},
|
|
135
|
+
{},
|
|
136
|
+
);
|
|
98
137
|
const store = createWorkCtrlFlowCheckpointStore(
|
|
99
138
|
{ checkpointStore },
|
|
100
139
|
{
|
|
@@ -231,12 +270,18 @@ type FlowRunFilesystemLocations = {
|
|
|
231
270
|
export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<FlowRunProfileState> {
|
|
232
271
|
readonly #storeRoot: string;
|
|
233
272
|
readonly #realStoreRoot: string;
|
|
273
|
+
readonly #durability: CheckpointDurabilityRuntime | undefined;
|
|
274
|
+
readonly #profileAuthority: FlowRunProfileAuthorityPort | undefined;
|
|
275
|
+
readonly #extensionCodecs: DefinitionStepExtensionCodecRegistryPort;
|
|
234
276
|
|
|
235
277
|
constructor(options: FilesystemFlowRunCheckpointStoreOptions) {
|
|
236
278
|
const root = path.resolve(options.storeRoot);
|
|
237
279
|
assertExistingDirectory(root, undefined, 'flow-run-store-root-invalid');
|
|
238
280
|
this.#storeRoot = root;
|
|
239
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 });
|
|
240
285
|
}
|
|
241
286
|
|
|
242
287
|
async load(key: FlowRunCheckpointKey): Promise<AIWorkflowFlowRunCheckpoint | undefined> {
|
|
@@ -250,6 +295,7 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
250
295
|
if (!fs.existsSync(locations.checkpointFile)) return undefined;
|
|
251
296
|
const checkpoint = readCheckpointFile(locations.checkpointFile, this.#realStoreRoot);
|
|
252
297
|
this.#assertCheckpointIdentity(checkpoint, key, descriptor.definition);
|
|
298
|
+
this.#validateFrozenProfile(checkpoint);
|
|
253
299
|
return checkpoint;
|
|
254
300
|
}
|
|
255
301
|
|
|
@@ -263,6 +309,7 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
263
309
|
this.#preflightExistingRunBoundary(locations);
|
|
264
310
|
const descriptor = this.#requireInstance(locations.instanceDir, key.instanceId);
|
|
265
311
|
this.#assertCheckpointIdentity(checkpoint, key, descriptor.definition);
|
|
312
|
+
const frozenAuthority = this.#validateFrozenProfile(checkpoint);
|
|
266
313
|
|
|
267
314
|
this.#ensureRunDirectory(locations);
|
|
268
315
|
const lock = await acquireLock(locations.runDir, locations.lockFile, this.#realStoreRoot);
|
|
@@ -273,20 +320,33 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
273
320
|
? readCheckpointStorage(locations.checkpointFile, this.#realStoreRoot)
|
|
274
321
|
: undefined;
|
|
275
322
|
const current = currentStorage?.checkpoint;
|
|
276
|
-
if (current)
|
|
323
|
+
if (current) {
|
|
324
|
+
this.#assertCheckpointIdentity(current, key, descriptor.definition);
|
|
325
|
+
this.#validateFrozenProfile(current);
|
|
326
|
+
}
|
|
277
327
|
const actualVersion = current?.version ?? null;
|
|
278
328
|
if (actualVersion !== input.expectedVersion) {
|
|
279
329
|
throw new FlowRunCheckpointVersionConflictError(input.expectedVersion, actualVersion);
|
|
280
330
|
}
|
|
281
331
|
validateFlowRunCheckpointTransition(current, checkpoint);
|
|
282
|
-
ensureSidecarDirectories(locations.runDir, this.#realStoreRoot);
|
|
283
|
-
const
|
|
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
|
+
});
|
|
284
343
|
removeUnacceptedSidecarReceipt(locations.runDir, checkpoint.version, this.#realStoreRoot);
|
|
285
|
-
persistCheckpointSidecars(locations.runDir, encoded);
|
|
344
|
+
persistCheckpointSidecars(locations.runDir, encoded, this.#durability);
|
|
286
345
|
assertSidecarDirectories(locations.runDir, this.#realStoreRoot);
|
|
287
|
-
writeCheckpointCandidate(locations.stagingFile, encoded.head, this.#realStoreRoot);
|
|
346
|
+
writeCheckpointCandidate(locations.stagingFile, encoded.head, this.#realStoreRoot, this.#durability);
|
|
288
347
|
const staged = readCheckpointFile(locations.stagingFile, this.#realStoreRoot);
|
|
289
348
|
this.#assertCheckpointIdentity(staged, key, descriptor.definition);
|
|
349
|
+
this.#validateFrozenProfile(staged);
|
|
290
350
|
if (stableJson(staged) !== stableJson(checkpoint)) {
|
|
291
351
|
throw new FlowRunFilesystemStoreError(
|
|
292
352
|
'flow-run-checkpoint-readback-mismatch',
|
|
@@ -298,9 +358,10 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
298
358
|
if (fs.existsSync(locations.checkpointFile)) {
|
|
299
359
|
assertRegularFile(locations.checkpointFile, this.#realStoreRoot, 'flow-run-checkpoint-invalid');
|
|
300
360
|
}
|
|
301
|
-
|
|
361
|
+
renameDurably(locations.stagingFile, locations.checkpointFile, 'head', this.#durability);
|
|
302
362
|
const accepted = readCheckpointFile(locations.checkpointFile, this.#realStoreRoot);
|
|
303
363
|
this.#assertCheckpointIdentity(accepted, key, descriptor.definition);
|
|
364
|
+
this.#validateFrozenProfile(accepted);
|
|
304
365
|
if (stableJson(accepted) !== stableJson(checkpoint)) {
|
|
305
366
|
throw new FlowRunFilesystemStoreError(
|
|
306
367
|
'flow-run-checkpoint-readback-mismatch',
|
|
@@ -506,9 +567,16 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
506
567
|
}
|
|
507
568
|
}
|
|
508
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
|
+
|
|
509
577
|
#ensureRunDirectory(locations: FlowRunFilesystemLocations): void {
|
|
510
|
-
ensureChildDirectory(locations.instanceDir, locations.runsDir, this.#realStoreRoot);
|
|
511
|
-
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');
|
|
512
580
|
this.#assertRunPhysicalBoundary(locations);
|
|
513
581
|
}
|
|
514
582
|
|
|
@@ -934,13 +1002,19 @@ function assertRealContainment(realRoot: string, file: string, code: string): vo
|
|
|
934
1002
|
}
|
|
935
1003
|
}
|
|
936
1004
|
|
|
937
|
-
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 {
|
|
938
1012
|
assertExistingDirectory(parent, realRoot, 'flow-run-boundary-invalid');
|
|
939
1013
|
if (fs.existsSync(child)) {
|
|
940
1014
|
assertExistingDirectory(child, realRoot, 'flow-run-symlink-boundary');
|
|
941
1015
|
return;
|
|
942
1016
|
}
|
|
943
|
-
|
|
1017
|
+
createDirectoryDurably(child, durabilityRole, durability);
|
|
944
1018
|
assertExistingDirectory(child, realRoot, 'flow-run-boundary-invalid');
|
|
945
1019
|
}
|
|
946
1020
|
|
|
@@ -951,13 +1025,21 @@ function readCheckpointFile(file: string, realRoot: string): AIWorkflowFlowRunCh
|
|
|
951
1025
|
function readCheckpointStorage(
|
|
952
1026
|
file: string,
|
|
953
1027
|
realRoot: string,
|
|
954
|
-
): {
|
|
1028
|
+
): {
|
|
1029
|
+
readonly checkpoint: AIWorkflowFlowRunCheckpoint;
|
|
1030
|
+
readonly tree?: StepMerkleTree;
|
|
1031
|
+
readonly materialRecordDigests?: readonly import('flow-step-space-contract').DefinitionStepDigest[];
|
|
1032
|
+
} {
|
|
955
1033
|
const value = readJsonFile(file, realRoot, 'flow-run-checkpoint-invalid');
|
|
956
1034
|
try {
|
|
957
1035
|
if (isCheckpointHead(value)) {
|
|
958
1036
|
assertSidecarDirectories(path.dirname(file), realRoot);
|
|
959
1037
|
const loaded = readCheckpointHeadAndLogical(file);
|
|
960
|
-
return {
|
|
1038
|
+
return {
|
|
1039
|
+
checkpoint: loaded.checkpoint,
|
|
1040
|
+
tree: loaded.tree,
|
|
1041
|
+
materialRecordDigests: loaded.materialRecordDigests,
|
|
1042
|
+
};
|
|
961
1043
|
}
|
|
962
1044
|
return { checkpoint: normalizeFlowRunCheckpoint(value) };
|
|
963
1045
|
} catch (error) {
|
|
@@ -988,25 +1070,32 @@ function writeCheckpointCandidate(
|
|
|
988
1070
|
file: string,
|
|
989
1071
|
checkpoint: FlowRunCheckpointHead,
|
|
990
1072
|
realRoot: string,
|
|
1073
|
+
durability?: CheckpointDurabilityRuntime,
|
|
991
1074
|
): void {
|
|
992
1075
|
if (fs.existsSync(file)) {
|
|
993
1076
|
throw new FlowRunFilesystemStoreError('flow-run-staging-invalid', 'Checkpoint staging file already exists after recovery.');
|
|
994
1077
|
}
|
|
995
1078
|
assertExistingDirectory(path.dirname(file), realRoot, 'flow-run-boundary-invalid');
|
|
996
|
-
|
|
1079
|
+
writeDurableCandidate(file, checkpointHeadJson(checkpoint), 'head', durability);
|
|
997
1080
|
}
|
|
998
1081
|
|
|
999
|
-
function ensureSidecarDirectories(
|
|
1082
|
+
function ensureSidecarDirectories(
|
|
1083
|
+
runDir: string,
|
|
1084
|
+
realRoot: string,
|
|
1085
|
+
durability?: CheckpointDurabilityRuntime,
|
|
1086
|
+
): void {
|
|
1000
1087
|
const sidecar = path.join(runDir, 'step-space');
|
|
1001
|
-
ensureChildDirectory(runDir, sidecar, realRoot);
|
|
1002
|
-
ensureChildDirectory(sidecar, path.join(sidecar, '
|
|
1003
|
-
ensureChildDirectory(sidecar, path.join(sidecar, '
|
|
1004
|
-
ensureChildDirectory(sidecar, path.join(sidecar, '
|
|
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');
|
|
1005
1093
|
}
|
|
1006
1094
|
|
|
1007
1095
|
function assertSidecarDirectories(runDir: string, realRoot: string): void {
|
|
1008
1096
|
const sidecar = path.join(runDir, 'step-space');
|
|
1009
1097
|
assertExistingDirectory(sidecar, realRoot, 'flow-run-sidecar-boundary-invalid');
|
|
1098
|
+
assertExistingDirectory(path.join(sidecar, 'controls'), realRoot, 'flow-run-sidecar-boundary-invalid');
|
|
1010
1099
|
assertExistingDirectory(path.join(sidecar, 'records'), realRoot, 'flow-run-sidecar-boundary-invalid');
|
|
1011
1100
|
assertExistingDirectory(path.join(sidecar, 'trees'), realRoot, 'flow-run-sidecar-boundary-invalid');
|
|
1012
1101
|
assertExistingDirectory(path.join(sidecar, 'receipts'), realRoot, 'flow-run-sidecar-boundary-invalid');
|
|
@@ -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
|
+
}
|