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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-workflow-logic",
3
- "version": "0.1.4",
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.4",
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.1",
19
- "work-ctrl-flow-logic": "0.1.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 = new FilesystemFlowRunCheckpointStore({ storeRoot: options.storeRoot });
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 current = fs.existsSync(locations.checkpointFile)
264
- ? readCheckpointFile(locations.checkpointFile, this.#realStoreRoot)
319
+ const currentStorage = fs.existsSync(locations.checkpointFile)
320
+ ? readCheckpointStorage(locations.checkpointFile, this.#realStoreRoot)
265
321
  : undefined;
266
- if (current) this.#assertCheckpointIdentity(current, key, descriptor.definition);
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
- writeCheckpointCandidate(locations.stagingFile, checkpoint, this.#realStoreRoot);
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
- fs.renameSync(locations.stagingFile, locations.checkpointFile);
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(parent: string, child: string, realRoot: string): void {
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
- fs.mkdirSync(child);
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
- return normalizeFlowRunCheckpoint(value);
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: AIWorkflowFlowRunCheckpoint,
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
- writeBytesNoFollow(file, stableJson(checkpoint));
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
+ }