ai-workflow-logic 0.1.5 → 0.1.7

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.5",
3
+ "version": "0.1.7",
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.5",
17
- "flow-step-space-contract": "0.1.0",
18
- "flow-step-space-logic": "0.1.0",
16
+ "ai-workflow-contract": "0.1.7",
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.2",
21
- "work-ctrl-flow-logic": "0.1.2"
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 = new FilesystemFlowRunCheckpointStore({ storeRoot: options.storeRoot });
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) this.#assertCheckpointIdentity(current, key, descriptor.definition);
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 encoded = encodeCheckpointSidecars(checkpoint, currentStorage?.tree);
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
- fs.renameSync(locations.stagingFile, locations.checkpointFile);
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(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 {
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
- fs.mkdirSync(child);
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
- ): { readonly checkpoint: AIWorkflowFlowRunCheckpoint; readonly tree?: StepMerkleTree } {
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 { checkpoint: loaded.checkpoint, tree: loaded.tree };
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
- writeBytesNoFollow(file, checkpointHeadJson(checkpoint));
1079
+ writeDurableCandidate(file, checkpointHeadJson(checkpoint), 'head', durability);
997
1080
  }
998
1081
 
999
- function ensureSidecarDirectories(runDir: string, realRoot: string): void {
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, 'records'), realRoot);
1003
- ensureChildDirectory(sidecar, path.join(sidecar, 'trees'), realRoot);
1004
- ensureChildDirectory(sidecar, path.join(sidecar, 'receipts'), realRoot);
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
+ }