flow-step-space-logic 0.1.0 → 0.1.1

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": "flow-step-space-logic",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "description": "Exact explicit-ref StepSpace loading, normalization, and profile assembly",
6
6
  "type": "module",
@@ -10,7 +10,7 @@
10
10
  "./filesystem": "./src/filesystem.ts"
11
11
  },
12
12
  "dependencies": {
13
- "flow-step-space-contract": "0.1.0",
13
+ "flow-step-space-contract": "0.1.1",
14
14
  "xnl-core": "^0.1.8"
15
15
  },
16
16
  "files": [
package/src/filesystem.ts CHANGED
@@ -11,6 +11,19 @@ import {
11
11
  export interface FilesystemDefinitionStepRuntimeOptions {
12
12
  /** Host policy lives in the runtime adapter, never in semantic Processor input. */
13
13
  readonly rootDir: string;
14
+ readonly extensionCodecs?: DefinitionStepAssemblyRuntime['extensionCodecs'];
15
+ }
16
+
17
+ export interface FilesystemDefinitionStepProfileRuntime extends DefinitionStepAssemblyRuntime {
18
+ readonly profileSources: {
19
+ readTopLevelXnlSources(): readonly { readonly ref: string; readonly content: string }[];
20
+ };
21
+ }
22
+
23
+ export interface AssembleDefinitionStepProfileDirectoryInput {}
24
+
25
+ export interface AssembleDefinitionStepProfileDirectoryConfig extends DefinitionStepProfileLoadOptions {
26
+ readonly profileRoot: string;
14
27
  }
15
28
 
16
29
  export function createFilesystemDefinitionStepRuntime(
@@ -46,30 +59,59 @@ export function createFilesystemDefinitionStepRuntime(
46
59
  return new Uint8Array(fs.readFileSync(realFile));
47
60
  },
48
61
  }),
62
+ extensionCodecs: options.extensionCodecs ?? Object.freeze({ resolve: () => undefined }),
49
63
  });
50
64
  }
51
65
 
52
- /** Reads only top-level profile/facet files, then follows the exact declared closure. */
66
+ export function createFilesystemDefinitionStepProfileRuntime(
67
+ options: FilesystemDefinitionStepRuntimeOptions,
68
+ ): FilesystemDefinitionStepProfileRuntime {
69
+ const absoluteRoot = path.resolve(options.rootDir);
70
+ const stepRuntime = createFilesystemDefinitionStepRuntime(options);
71
+ return Object.freeze({
72
+ ...stepRuntime,
73
+ profileSources: Object.freeze({
74
+ readTopLevelXnlSources() {
75
+ return fs.readdirSync(absoluteRoot, { withFileTypes: true })
76
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.xnl'))
77
+ .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)
78
+ .map((entry) => Object.freeze({
79
+ ref: entry.name,
80
+ content: fs.readFileSync(path.join(absoluteRoot, entry.name), 'utf8'),
81
+ }));
82
+ },
83
+ }),
84
+ });
85
+ }
86
+
87
+ /** Processor authority; the host root is closed inside the runtime adapter. */
53
88
  export function assembleDefinitionStepProfileDirectory(
89
+ runtime: FilesystemDefinitionStepProfileRuntime,
90
+ _input: AssembleDefinitionStepProfileDirectoryInput,
91
+ config: AssembleDefinitionStepProfileDirectoryConfig,
92
+ ) {
93
+ if (!runtime?.profileSources || typeof runtime.profileSources.readTopLevelXnlSources !== 'function') {
94
+ throw diagnostic('step-profile-runtime-invalid', 'runtime.profileSources.readTopLevelXnlSources is required');
95
+ }
96
+ const sources = runtime.profileSources.readTopLevelXnlSources();
97
+ return assembleDefinitionStepProfileSourcesWithRuntime(
98
+ { sources: runtime.sources, extensionCodecs: runtime.extensionCodecs },
99
+ sources,
100
+ config.profileRoot,
101
+ config,
102
+ );
103
+ }
104
+
105
+ /** @deprecated Compatibility wrapper; new code must inject a filesystem runtime. */
106
+ export function assembleDefinitionStepProfileDirectoryFromRoot(
54
107
  rootDir: string,
55
108
  profileRoot: string,
56
109
  options: DefinitionStepProfileLoadOptions = {},
57
110
  ) {
58
- const absoluteRoot = path.resolve(rootDir);
59
- const sources = fs.existsSync(absoluteRoot)
60
- ? fs.readdirSync(absoluteRoot, { withFileTypes: true })
61
- .filter((entry) => entry.isFile() && entry.name.endsWith('.xnl'))
62
- .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)
63
- .map((entry) => ({
64
- name: entry.name,
65
- content: fs.readFileSync(path.join(absoluteRoot, entry.name), 'utf8'),
66
- }))
67
- : [];
68
- return assembleDefinitionStepProfileSourcesWithRuntime(
69
- createFilesystemDefinitionStepRuntime({ rootDir: absoluteRoot }),
70
- sources,
71
- profileRoot,
72
- options,
111
+ return assembleDefinitionStepProfileDirectory(
112
+ createFilesystemDefinitionStepProfileRuntime({ rootDir }),
113
+ {},
114
+ { ...options, profileRoot },
73
115
  );
74
116
  }
75
117
 
package/src/index.ts CHANGED
@@ -10,12 +10,19 @@ import type {
10
10
  DefinitionStepDiagnostic,
11
11
  DefinitionStepDigest,
12
12
  DefinitionStepExtension,
13
+ DefinitionStepExtensionRegistration,
13
14
  DefinitionStepForest,
14
15
  DefinitionStepForestNode,
15
16
  DefinitionStepGroup,
16
17
  DefinitionStepProfileLoadOptions,
18
+ DefinitionStepProfileAssemblyConfig,
19
+ DefinitionStepProfileAssemblyInput,
17
20
  DefinitionStepSourceCollection,
18
21
  DefinitionStepSourceRef,
22
+ DefinitionStepMutationConfig,
23
+ DefinitionStepMutationInvocation,
24
+ DefinitionStepMutationRuntime,
25
+ DefinitionStepSelector,
19
26
  DefinitionStepValue,
20
27
  ImmutableStepRecord,
21
28
  StepMerkleDiff,
@@ -276,7 +283,6 @@ function nodeProjection(element: Element, sourceRef: string): DefinitionStepCore
276
283
 
277
284
  type AssemblyState = {
278
285
  readonly runtime: DefinitionStepAssemblyRuntime;
279
- readonly config: DefinitionStepAssemblyConfig;
280
286
  readonly closure: DefinitionStepClosureEntry[];
281
287
  readonly closureByRef: Map<string, DefinitionStepClosureEntry>;
282
288
  readonly steps: Record<string, DefinitionStep>;
@@ -326,7 +332,7 @@ function readExtension(
326
332
  if (idOf(refElement)) fail('step-extension-ref-id-unsupported', 'ExtensionRef must not declare an id', undefined, stepId);
327
333
  const attrs = exactAttrs(refElement, ['kind', 'src', 'schema'], state.activeRefs.at(-1) ?? '');
328
334
  const kind = requiredString(attrs, 'kind', state.activeRefs.at(-1) ?? '');
329
- const registration = state.config.extensions[kind];
335
+ const registration = resolveExtensionRegistration(state.runtime, kind);
330
336
  if (!registration) fail('step-extension-kind-undeclared', `extension kind ${kind} is not registered`, undefined, stepId);
331
337
  const ref = normalizeDefinitionStepSourceRef(requiredString(attrs, 'src', state.activeRefs.at(-1) ?? ''));
332
338
  const declaredSchema = attrs.schema;
@@ -350,12 +356,25 @@ function readExtension(
350
356
  }
351
357
  const extensionId = idOf(root);
352
358
  if (!extensionId) fail('step-extension-id-missing', `${ref} StepExtension requires an id`, ref, stepId);
359
+ const rawValue = closedValue(rootAttrs.value ?? null, `${ref}:value`);
360
+ let normalizedValue = rawValue;
361
+ try {
362
+ normalizedValue = closedValue(registration.codec.normalize(rawValue, {
363
+ kind,
364
+ ...(typeof declaredSchema === 'string' ? { schemaRef: declaredSchema } : {}),
365
+ sourceRef: ref,
366
+ stepId,
367
+ }), `${ref}:codec`);
368
+ } catch (error) {
369
+ if (error instanceof DefinitionStepAssemblyError) throw error;
370
+ fail('step-extension-codec-invalid', `${ref} codec rejected the extension: ${(error as Error).message}`, ref, stepId);
371
+ }
353
372
  return Object.freeze({
354
373
  id: extensionId,
355
374
  kind,
356
375
  sourceRef: ref,
357
376
  ...(typeof declaredSchema === 'string' ? { schemaRef: declaredSchema } : {}),
358
- value: closedValue(rootAttrs.value ?? null, `${ref}:value`),
377
+ value: normalizedValue,
359
378
  digest: entry.digest,
360
379
  });
361
380
  });
@@ -443,37 +462,128 @@ function normalizeAssemblyBoundary(
443
462
  inputValue: DefinitionStepAssemblyInput,
444
463
  configValue: DefinitionStepAssemblyConfig,
445
464
  ) {
446
- const runtime = exactOwnDataRecord(runtimeValue, ['sources'], 'runtime');
465
+ const runtime = exactOwnDataRecord(runtimeValue, ['sources', 'extensionCodecs'], 'runtime');
447
466
  const sources = exactOwnDataRecord(ownValue(runtime, 'sources'), ['readSource'], 'runtime.sources');
448
467
  if (typeof ownValue(sources, 'readSource') !== 'function') fail('step-runtime-invalid', 'runtime.sources.readSource must be a function');
449
- const input = exactOwnDataRecord(inputValue, ['stepSpaceRef'], 'input');
450
- const config = exactOwnDataRecord(configValue, ['extensions'], 'config');
451
- const extensionRecord = exactOwnDataRecord(ownValue(config, 'extensions'), Object.keys(ownValue(config, 'extensions') as object), 'config.extensions');
452
- for (const [kind, registrationValue] of Object.entries(extensionRecord)) {
453
- if (!kind || kind !== kind.trim()) fail('step-extension-registration-invalid', 'extension kind must be an exact non-empty string');
454
- const registration = exactOwnDataRecord(registrationValue, ['schemaRef'], `config.extensions.${kind}`);
455
- const schemaRef = ownValue(registration, 'schemaRef');
456
- if (schemaRef !== undefined && (typeof schemaRef !== 'string' || !schemaRef)) {
457
- fail('step-extension-registration-invalid', `${kind}.schemaRef must be a non-empty string`);
458
- }
468
+ const extensionCodecs = exactOwnDataRecord(ownValue(runtime, 'extensionCodecs'), ['resolve'], 'runtime.extensionCodecs');
469
+ if (typeof ownValue(extensionCodecs, 'resolve') !== 'function') {
470
+ fail('step-runtime-invalid', 'runtime.extensionCodecs.resolve must be a function');
459
471
  }
472
+ const input = exactOwnDataRecord(inputValue, ['stepSpaceRef'], 'input');
473
+ exactOwnDataRecord(configValue, [], 'config');
460
474
  return {
461
475
  runtime: runtimeValue,
462
476
  input: { stepSpaceRef: normalizeDefinitionStepSourceRef(ownValue(input, 'stepSpaceRef')) },
463
- config: configValue,
464
477
  };
465
478
  }
466
479
 
480
+ function resolveExtensionRegistration(
481
+ runtime: Pick<DefinitionStepAssemblyRuntime, 'extensionCodecs'> | Pick<DefinitionStepMutationRuntime, 'extensionCodecs'>,
482
+ kind: string,
483
+ ): DefinitionStepExtensionRegistration | undefined {
484
+ if (!kind || kind !== kind.trim()) fail('step-extension-registration-invalid', 'extension kind must be an exact non-empty string');
485
+ const registrationValue = runtime.extensionCodecs.resolve(kind);
486
+ if (registrationValue === undefined) return undefined;
487
+ const registration = exactOwnDataRecord(registrationValue, ['schemaRef', 'codec'], `runtime.extensionCodecs.${kind}`);
488
+ const schemaRef = ownValue(registration, 'schemaRef');
489
+ if (schemaRef !== undefined && (typeof schemaRef !== 'string' || !schemaRef)) {
490
+ fail('step-extension-registration-invalid', `${kind}.schemaRef must be a non-empty string`);
491
+ }
492
+ const codec = ownValue(registration, 'codec');
493
+ if (codec === undefined) fail('step-extension-registration-invalid', `${kind}.codec is required`);
494
+ const codecRecord = exactOwnDataRecord(codec, ['normalize'], `runtime.extensionCodecs.${kind}.codec`);
495
+ if (typeof ownValue(codecRecord, 'normalize') !== 'function') {
496
+ fail('step-extension-registration-invalid', `${kind}.codec.normalize must be a function`);
497
+ }
498
+ return registrationValue as DefinitionStepExtensionRegistration;
499
+ }
500
+
501
+ function normalizeRegisteredExtensionValue(
502
+ registration: DefinitionStepExtensionRegistration,
503
+ value: unknown,
504
+ context: { readonly kind: string; readonly sourceRef: string; readonly stepId: string },
505
+ ): DefinitionStepValue {
506
+ const closed = closedValue(value, `mutation.${context.stepId}.${context.kind}`);
507
+ try {
508
+ return closedValue(registration.codec.normalize(closed, {
509
+ ...context,
510
+ ...(registration.schemaRef ? { schemaRef: registration.schemaRef } : {}),
511
+ }), `mutation.${context.stepId}.${context.kind}.codec`);
512
+ } catch (error) {
513
+ if (error instanceof DefinitionStepAssemblyError) throw error;
514
+ fail('step-extension-codec-invalid', `${context.kind} codec rejected the mutation: ${(error as Error).message}`, context.sourceRef, context.stepId);
515
+ }
516
+ }
517
+
518
+ /** Targeted Processor form: output = fn(runtime, selector, invocation, config). */
519
+ export function mutateDefinitionStep(
520
+ runtime: DefinitionStepMutationRuntime,
521
+ selector: DefinitionStepSelector,
522
+ invocation: DefinitionStepMutationInvocation,
523
+ config: DefinitionStepMutationConfig,
524
+ ): DefinitionStep {
525
+ if (!runtime || typeof runtime !== 'object'
526
+ || !runtime.steps || typeof runtime.steps.readStep !== 'function'
527
+ || typeof runtime.steps.writeStep !== 'function'
528
+ || !runtime.extensionCodecs || typeof runtime.extensionCodecs.resolve !== 'function') {
529
+ fail('step-mutation-runtime-invalid', 'runtime must provide steps and extensionCodecs ports');
530
+ }
531
+ exactOwnDataRecord(config, [], 'config');
532
+ if (!selector || typeof selector !== 'object' || typeof selector.stepId !== 'string'
533
+ || !selector.stepId || selector.stepId !== selector.stepId.trim()) {
534
+ fail('step-mutation-selector-invalid', 'selector.stepId must be an exact non-empty string');
535
+ }
536
+ const before = runtime.steps.readStep(selector.stepId);
537
+ if (!before) fail('step-mutation-target-missing', `step ${selector.stepId} does not exist`, undefined, selector.stepId);
538
+ if (invocation.expectedDigest !== before.digest) {
539
+ fail('step-mutation-version-conflict', `step ${selector.stepId} digest changed`, before.sourceRef, selector.stepId);
540
+ }
541
+ const extensions: Record<string, DefinitionStepExtension> = { ...before.extensions };
542
+ for (const [kind, value] of Object.entries(invocation.extensions ?? {})) {
543
+ const registration = resolveExtensionRegistration(runtime, kind);
544
+ const prior = before.extensions[kind];
545
+ if (!registration || !prior) {
546
+ fail('step-extension-kind-undeclared', `extension kind ${kind} is not admitted for step ${selector.stepId}`, before.sourceRef, selector.stepId);
547
+ }
548
+ if (!registration.codec || typeof registration.codec.normalize !== 'function') {
549
+ fail('step-extension-registration-invalid', `${kind}.codec.normalize is required for mutation`, before.sourceRef, selector.stepId);
550
+ }
551
+ if (registration.schemaRef !== prior.schemaRef) {
552
+ fail('step-extension-schema-mismatch', `${kind} mutation registry does not match the admitted schema`, before.sourceRef, selector.stepId);
553
+ }
554
+ const normalized = normalizeRegisteredExtensionValue(registration, value, {
555
+ kind,
556
+ sourceRef: prior.sourceRef,
557
+ stepId: selector.stepId,
558
+ });
559
+ extensions[kind] = Object.freeze({
560
+ ...prior,
561
+ value: normalized,
562
+ digest: digest(canonicalJson({ kind, schemaRef: prior.schemaRef ?? null, value: normalized })),
563
+ });
564
+ }
565
+ const core = invocation.core === undefined
566
+ ? before.core
567
+ : normalizeCore(invocation.core, `mutation.${selector.stepId}.core`);
568
+ const next = Object.freeze({
569
+ ...before,
570
+ core,
571
+ extensions: Object.freeze(extensions),
572
+ digest: digest(JSON.stringify({ id: before.id, core, extensions })),
573
+ });
574
+ runtime.steps.writeStep(next, before.digest);
575
+ return next;
576
+ }
577
+
467
578
  /** Processor form: output = fn(runtime, input, config). */
468
579
  export function assembleDefinitionStepForest(
469
580
  runtimeValue: DefinitionStepAssemblyRuntime,
470
581
  inputValue: DefinitionStepAssemblyInput,
471
582
  configValue: DefinitionStepAssemblyConfig,
472
583
  ): DefinitionStepForest {
473
- const { runtime, input, config } = normalizeAssemblyBoundary(runtimeValue, inputValue, configValue);
584
+ const { runtime, input } = normalizeAssemblyBoundary(runtimeValue, inputValue, configValue);
474
585
  const state: AssemblyState = {
475
586
  runtime,
476
- config,
477
587
  closure: [],
478
588
  closureByRef: new Map(),
479
589
  steps: {},
@@ -652,6 +762,7 @@ export function normalizeDefinitionStepForest(value: unknown): DefinitionStepFor
652
762
 
653
763
  export function createDefinitionStepSourceRuntime(
654
764
  sources: CompatibleSourceCollection,
765
+ extensionCodecs: DefinitionStepAssemblyRuntime['extensionCodecs'] = Object.freeze({ resolve: () => undefined }),
655
766
  ): DefinitionStepAssemblyRuntime {
656
767
  const entries = Array.isArray(sources)
657
768
  ? sources.map((source) => ['ref' in source ? source.ref : source.name, source.content] as const)
@@ -672,6 +783,7 @@ export function createDefinitionStepSourceRuntime(
672
783
  return bytes.slice();
673
784
  },
674
785
  }),
786
+ extensionCodecs,
675
787
  });
676
788
  }
677
789
 
@@ -705,6 +817,20 @@ export interface DefinitionStepProfileAssemblyResult {
705
817
  readonly diagnostics: readonly DefinitionStepDiagnostic[];
706
818
  }
707
819
 
820
+ /** Processor authority for profile composition: output = fn(runtime, input, config). */
821
+ export function assembleDefinitionStepProfile(
822
+ runtime: DefinitionStepAssemblyRuntime,
823
+ input: DefinitionStepProfileAssemblyInput,
824
+ config: DefinitionStepProfileAssemblyConfig,
825
+ ): DefinitionStepProfileAssemblyResult {
826
+ return assembleDefinitionStepProfileSourcesWithRuntime(
827
+ runtime,
828
+ input.sources,
829
+ config.profileRoot,
830
+ config,
831
+ );
832
+ }
833
+
708
834
  /**
709
835
  * Pure-source composition helper used by profile owners. It only recognizes the
710
836
  * exact profileSourceRef and follows the explicit StepSpace closure.
@@ -714,11 +840,13 @@ export function assembleDefinitionStepProfileSources(
714
840
  profileRoot: string,
715
841
  options: DefinitionStepProfileLoadOptions = {},
716
842
  ): DefinitionStepProfileAssemblyResult {
717
- return assembleDefinitionStepProfileSourcesWithRuntime(
843
+ const normalizedSources: DefinitionStepSourceCollection = Array.isArray(sources)
844
+ ? sources.map((source) => ({ ref: 'ref' in source ? source.ref : source.name, content: source.content }))
845
+ : Object.entries(sources).map(([ref, content]) => ({ ref, content }));
846
+ return assembleDefinitionStepProfile(
718
847
  createDefinitionStepSourceRuntime(sources),
719
- sources,
720
- profileRoot,
721
- options,
848
+ { sources: normalizedSources },
849
+ { ...options, profileRoot },
722
850
  );
723
851
  }
724
852
 
@@ -756,9 +884,9 @@ export function assembleDefinitionStepProfileSourcesWithRuntime(
756
884
  fail('step-space-ref-invalid', 'StepSpaceRef must contain only src', profileSourceRef);
757
885
  }
758
886
  const forest = assembleDefinitionStepForest(
759
- runtime,
887
+ { sources: runtime.sources, extensionCodecs: runtime.extensionCodecs },
760
888
  { stepSpaceRef: requiredString(refAttrs, 'src', profileSourceRef) },
761
- { extensions: options.extensions ?? {} },
889
+ {},
762
890
  );
763
891
  const retainedSections = sectionChildren(root).filter((section) => section.tag !== 'StepSpaceRef');
764
892
  const projectedRoot = {