flow-step-space-logic 0.1.1 → 0.1.2

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.
Files changed (3) hide show
  1. package/package.json +4 -3
  2. package/src/core.ts +911 -0
  3. package/src/index.ts +1 -911
package/src/index.ts CHANGED
@@ -1,912 +1,2 @@
1
- import { createHash } from 'node:crypto';
2
- import { parseXnl, wordToString, type DataElementNode, type XnlNode } from 'xnl-core';
3
- import type {
4
- DefinitionStep,
5
- DefinitionStepAssemblyConfig,
6
- DefinitionStepAssemblyInput,
7
- DefinitionStepAssemblyRuntime,
8
- DefinitionStepClosureEntry,
9
- DefinitionStepCoreNode,
10
- DefinitionStepDiagnostic,
11
- DefinitionStepDigest,
12
- DefinitionStepExtension,
13
- DefinitionStepExtensionRegistration,
14
- DefinitionStepForest,
15
- DefinitionStepForestNode,
16
- DefinitionStepGroup,
17
- DefinitionStepProfileLoadOptions,
18
- DefinitionStepProfileAssemblyConfig,
19
- DefinitionStepProfileAssemblyInput,
20
- DefinitionStepSourceCollection,
21
- DefinitionStepSourceRef,
22
- DefinitionStepMutationConfig,
23
- DefinitionStepMutationInvocation,
24
- DefinitionStepMutationRuntime,
25
- DefinitionStepSelector,
26
- DefinitionStepValue,
27
- ImmutableStepRecord,
28
- StepMerkleDiff,
29
- StepMerkleNode,
30
- StepMerkleTree,
31
- } from 'flow-step-space-contract';
32
-
33
- export * from 'flow-step-space-contract';
1
+ export * from './core.js';
34
2
  export * from './filesystem.js';
35
-
36
- type CompatibleSourceCollection = DefinitionStepSourceCollection
37
- | readonly { readonly name: string; readonly content: string }[];
38
-
39
- export class DefinitionStepAssemblyError extends Error {
40
- constructor(public readonly diagnostic: DefinitionStepDiagnostic) {
41
- super(`${diagnostic.code}: ${diagnostic.message}`);
42
- this.name = 'DefinitionStepAssemblyError';
43
- }
44
- }
45
-
46
- type Element = DataElementNode;
47
-
48
- const isElement = (value: unknown): value is Element =>
49
- !!value && typeof value === 'object' && (value as Element).kind === 'DataElement';
50
-
51
- const bodyChildren = (element: Element): Element[] => (element.body ?? []).filter(isElement);
52
-
53
- const sectionChildren = (element: Element): Element[] => {
54
- if (!element.extend) return [];
55
- const order = element.extend.order ?? Object.keys(element.extend.children);
56
- return order.map((key) => element.extend!.children[key]).filter(isElement);
57
- };
58
-
59
- const idOf = (element: Element): string => wordToString(element.id) ?? '';
60
-
61
- function fail(code: string, message: string, sourceRef?: string, stepId?: string): never {
62
- throw new DefinitionStepAssemblyError({
63
- code,
64
- message,
65
- ...(sourceRef === undefined ? {} : { sourceRef }),
66
- ...(stepId === undefined ? {} : { stepId }),
67
- });
68
- }
69
-
70
- function compareCodeUnits(left: string, right: string): number {
71
- return left < right ? -1 : left > right ? 1 : 0;
72
- }
73
-
74
- function digest(bytes: Uint8Array | string): DefinitionStepDigest {
75
- return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
76
- }
77
-
78
- function canonicalJson(value: DefinitionStepValue): string {
79
- if (value === null || typeof value !== 'object') return JSON.stringify(value);
80
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
81
- const record = value as Readonly<Record<string, DefinitionStepValue>>;
82
- return `{${Object.keys(record).sort(compareCodeUnits).map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
83
- }
84
-
85
- export function createImmutableStepRecord(keyValue: unknown, value: unknown): ImmutableStepRecord {
86
- if (typeof keyValue !== 'string' || !keyValue || keyValue !== keyValue.trim()) {
87
- fail('step-record-key-invalid', 'immutable step record key must be an exact non-empty string');
88
- }
89
- const normalized = closedValue(value, `record.${keyValue}`);
90
- const fact = canonicalJson({
91
- schemaVersion: 'depa.immutable-step-record/v1',
92
- key: keyValue,
93
- value: normalized,
94
- });
95
- return Object.freeze({
96
- schemaVersion: 'depa.immutable-step-record/v1',
97
- key: keyValue,
98
- value: normalized,
99
- digest: digest(fact),
100
- });
101
- }
102
-
103
- /** Pure mechanics only: the checkpoint owner decides if/when this root is admitted. */
104
- export function buildStepMerkleTree(recordsValue: readonly ImmutableStepRecord[]): StepMerkleTree {
105
- if (!Array.isArray(recordsValue)) fail('step-merkle-records-invalid', 'records must be a dense array');
106
- closedValue(recordsValue, 'merkle.records');
107
- const records = [...recordsValue].sort((left, right) => compareCodeUnits(left.key, right.key));
108
- const keyOrder = records.map((record) => record.key);
109
- if (new Set(keyOrder).size !== keyOrder.length) fail('step-merkle-key-duplicate', 'record keys must be unique');
110
- if (records.length === 0) fail('step-merkle-empty', 'Merkle tree requires at least one record');
111
- const nodes: Record<string, StepMerkleNode> = {};
112
- const leafByKey: Record<string, DefinitionStepDigest> = {};
113
- let level = records.map((record) => {
114
- const nodeDigest = digest(canonicalJson({ kind: 'leaf', key: record.key, recordDigest: record.digest }));
115
- const leaf = Object.freeze({ kind: 'leaf' as const, key: record.key, recordDigest: record.digest, digest: nodeDigest });
116
- nodes[nodeDigest] = leaf;
117
- leafByKey[record.key] = nodeDigest;
118
- return { digest: nodeDigest, firstKey: record.key, lastKey: record.key };
119
- });
120
- while (level.length > 1) {
121
- const next: { digest: DefinitionStepDigest; firstKey: string; lastKey: string }[] = [];
122
- for (let index = 0; index < level.length; index += 2) {
123
- const left = level[index];
124
- const right = level[index + 1] ?? left;
125
- const nodeDigest = digest(canonicalJson({
126
- kind: 'branch',
127
- firstKey: left.firstKey,
128
- lastKey: right.lastKey,
129
- leftDigest: left.digest,
130
- rightDigest: right.digest,
131
- }));
132
- const branch = Object.freeze({
133
- kind: 'branch' as const,
134
- firstKey: left.firstKey,
135
- lastKey: right.lastKey,
136
- leftDigest: left.digest,
137
- rightDigest: right.digest,
138
- digest: nodeDigest,
139
- });
140
- nodes[nodeDigest] = branch;
141
- next.push({ digest: nodeDigest, firstKey: left.firstKey, lastKey: right.lastKey });
142
- }
143
- level = next;
144
- }
145
- return Object.freeze({
146
- schemaVersion: 'depa.step-merkle-tree/v1',
147
- rootDigest: level[0].digest,
148
- keyOrder: Object.freeze(keyOrder),
149
- leafByKey: Object.freeze(leafByKey),
150
- nodes: Object.freeze(nodes),
151
- });
152
- }
153
-
154
- export function diffStepMerkleTrees(
155
- before: StepMerkleTree | undefined,
156
- after: StepMerkleTree,
157
- ): StepMerkleDiff {
158
- const beforeDigests = new Set(Object.keys(before?.nodes ?? {}) as DefinitionStepDigest[]);
159
- const afterDigests = new Set(Object.keys(after.nodes) as DefinitionStepDigest[]);
160
- return Object.freeze({
161
- addedNodeDigests: Object.freeze([...afterDigests].filter((value) => !beforeDigests.has(value)).sort(compareCodeUnits)),
162
- reusedNodeDigests: Object.freeze([...afterDigests].filter((value) => beforeDigests.has(value)).sort(compareCodeUnits)),
163
- unreachableNodeDigests: Object.freeze([...beforeDigests].filter((value) => !afterDigests.has(value)).sort(compareCodeUnits)),
164
- });
165
- }
166
-
167
- function exactOwnDataRecord(
168
- value: unknown,
169
- allowed: readonly string[],
170
- label: string,
171
- ): Record<string, unknown> {
172
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
173
- fail('closed-data-record', `${label} must be a plain data record`);
174
- }
175
- const prototype = Object.getPrototypeOf(value);
176
- if (prototype !== Object.prototype && prototype !== null) {
177
- fail('closed-data-record', `${label} must use a plain object prototype`);
178
- }
179
- const record = value as Record<string, unknown>;
180
- const ownKeys = Reflect.ownKeys(record);
181
- for (const key of ownKeys) {
182
- if (typeof key !== 'string') fail('closed-data-key', `${label} has a non-string own key`);
183
- const descriptor = Object.getOwnPropertyDescriptor(record, key);
184
- if (!descriptor || !('value' in descriptor)) fail('closed-data-accessor', `${label}.${String(key)} must be a data property`);
185
- if (!allowed.includes(key)) fail('closed-data-field', `${label}.${key} is not allowed`);
186
- }
187
- return record;
188
- }
189
-
190
- function ownValue(record: Record<string, unknown>, key: string): unknown {
191
- const descriptor = Object.getOwnPropertyDescriptor(record, key);
192
- if (descriptor && !('value' in descriptor)) fail('closed-data-accessor', `${key} must be a data property`);
193
- return descriptor && 'value' in descriptor ? descriptor.value : undefined;
194
- }
195
-
196
- function closedValue(value: unknown, label: string): DefinitionStepValue {
197
- if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
198
- if (typeof value === 'number' && Number.isFinite(value)) return value;
199
- if (Array.isArray(value)) {
200
- for (let index = 0; index < value.length; index += 1) {
201
- if (!Object.prototype.hasOwnProperty.call(value, index)) {
202
- fail('closed-data-sparse-array', `${label} must not contain sparse array slots`);
203
- }
204
- }
205
- return Object.freeze(value.map((entry, index) => closedValue(entry, `${label}[${index}]`)));
206
- }
207
- const record = exactOwnDataRecord(value, Object.keys(value as object), label);
208
- return Object.freeze(Object.fromEntries(
209
- Object.keys(record).sort(compareCodeUnits).map((key) => [key, closedValue(ownValue(record, key), `${label}.${key}`)]),
210
- ));
211
- }
212
-
213
- function exactAttrs(element: Element, allowed: readonly string[], sourceRef: string): Record<string, XnlNode> {
214
- const attrs = (element.attributes ?? {}) as Record<string, XnlNode>;
215
- for (const key of Object.keys(attrs)) {
216
- if (!allowed.includes(key)) fail('step-field-unsupported', `<${element.tag}>.${key} is not allowed`, sourceRef);
217
- }
218
- return attrs;
219
- }
220
-
221
- function requiredString(attrs: Record<string, XnlNode>, key: string, sourceRef: string): string {
222
- const value = attrs[key];
223
- if (typeof value !== 'string' || value.length === 0 || value !== value.trim()) {
224
- fail('step-field-invalid', `${key} must be a non-empty exact string`, sourceRef);
225
- }
226
- return value;
227
- }
228
-
229
- /** Closed virtual ref: relative slash segments only, with no traversal or platform path syntax. */
230
- export function normalizeDefinitionStepSourceRef(value: unknown): DefinitionStepSourceRef {
231
- if (typeof value !== 'string' || value.length === 0 || value !== value.trim()) {
232
- fail('step-source-ref-invalid', 'source ref must be a non-empty exact string');
233
- }
234
- if (value.includes('\\') || value.startsWith('/') || /^[A-Za-z]:/.test(value) || value.includes('\0')) {
235
- fail('step-source-ref-invalid', `source ref ${value} is not resource-local`);
236
- }
237
- const segments = value.split('/');
238
- if (segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) {
239
- fail('step-source-ref-invalid', `source ref ${value} contains an invalid segment`);
240
- }
241
- return value;
242
- }
243
-
244
- function parseRoot(bytes: Uint8Array, sourceRef: string, expectedTag: string): { root: Element; content: string } {
245
- let content: string;
246
- try {
247
- content = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
248
- } catch (error) {
249
- fail('step-source-decode-invalid', `${sourceRef} is not valid UTF-8: ${(error as Error).message}`, sourceRef);
250
- }
251
- let parsed: ReturnType<typeof parseXnl>;
252
- try {
253
- parsed = parseXnl(content);
254
- } catch (error) {
255
- fail('step-source-parse-invalid', `${sourceRef}: ${(error as Error).message}`, sourceRef);
256
- }
257
- if ((parsed.warnings ?? []).length > 0) {
258
- fail('step-source-parser-warning', `${sourceRef}: parser warnings are fatal`, sourceRef);
259
- }
260
- const roots = parsed.nodes.filter(isElement);
261
- if (roots.length !== 1 || roots[0].tag !== expectedTag) {
262
- fail('step-source-root-invalid', `${sourceRef} must contain exactly one <${expectedTag}> root`, sourceRef);
263
- }
264
- return { root: roots[0], content };
265
- }
266
-
267
- function nodeProjection(element: Element, sourceRef: string): DefinitionStepCoreNode {
268
- const metadata = closedValue(element.metadata ?? {}, `${sourceRef}:metadata`) as Record<string, DefinitionStepValue>;
269
- const attrs = closedValue(element.attributes ?? {}, `${sourceRef}:attrs`) as Record<string, DefinitionStepValue>;
270
- const sections = Object.freeze(Object.fromEntries(sectionChildren(element).map((section) => [
271
- section.tag,
272
- nodeProjection(section, sourceRef),
273
- ])));
274
- return Object.freeze({
275
- tag: element.tag,
276
- ...(idOf(element) ? { id: idOf(element) } : {}),
277
- metadata,
278
- attrs,
279
- children: Object.freeze(bodyChildren(element).map((child) => nodeProjection(child, sourceRef))),
280
- sections,
281
- });
282
- }
283
-
284
- type AssemblyState = {
285
- readonly runtime: DefinitionStepAssemblyRuntime;
286
- readonly closure: DefinitionStepClosureEntry[];
287
- readonly closureByRef: Map<string, DefinitionStepClosureEntry>;
288
- readonly steps: Record<string, DefinitionStep>;
289
- readonly stepOrder: string[];
290
- readonly groupIds: Set<string>;
291
- readonly activeRefs: string[];
292
- };
293
-
294
- function readRoot(state: AssemblyState, refValue: unknown, expectedTag: string): { root: Element; entry: DefinitionStepClosureEntry } {
295
- const ref = normalizeDefinitionStepSourceRef(refValue);
296
- let entry = state.closureByRef.get(ref);
297
- if (!entry) {
298
- let bytes: Uint8Array;
299
- try {
300
- bytes = state.runtime.sources.readSource(ref);
301
- } catch (error) {
302
- if (error instanceof DefinitionStepAssemblyError) throw error;
303
- fail('step-source-read-failed', `${ref}: ${(error as Error).message}`, ref);
304
- }
305
- if (!(bytes instanceof Uint8Array)) fail('step-source-read-invalid', `${ref} reader must return Uint8Array`, ref);
306
- const parsed = parseRoot(bytes, ref, expectedTag);
307
- entry = Object.freeze({ ref, digest: digest(bytes), content: parsed.content });
308
- state.closure.push(entry);
309
- state.closureByRef.set(ref, entry);
310
- return { root: parsed.root, entry };
311
- }
312
- return { root: parseRoot(new TextEncoder().encode(entry.content), ref, expectedTag).root, entry };
313
- }
314
-
315
- function withActiveRef<T>(state: AssemblyState, ref: string, operation: () => T): T {
316
- if (state.activeRefs.includes(ref)) {
317
- fail('step-reference-cycle', `reference cycle: ${[...state.activeRefs, ref].join(' -> ')}`, ref);
318
- }
319
- state.activeRefs.push(ref);
320
- try {
321
- return operation();
322
- } finally {
323
- state.activeRefs.pop();
324
- }
325
- }
326
-
327
- function readExtension(
328
- state: AssemblyState,
329
- refElement: Element,
330
- stepId: string,
331
- ): DefinitionStepExtension {
332
- if (idOf(refElement)) fail('step-extension-ref-id-unsupported', 'ExtensionRef must not declare an id', undefined, stepId);
333
- const attrs = exactAttrs(refElement, ['kind', 'src', 'schema'], state.activeRefs.at(-1) ?? '');
334
- const kind = requiredString(attrs, 'kind', state.activeRefs.at(-1) ?? '');
335
- const registration = resolveExtensionRegistration(state.runtime, kind);
336
- if (!registration) fail('step-extension-kind-undeclared', `extension kind ${kind} is not registered`, undefined, stepId);
337
- const ref = normalizeDefinitionStepSourceRef(requiredString(attrs, 'src', state.activeRefs.at(-1) ?? ''));
338
- const declaredSchema = attrs.schema;
339
- if (declaredSchema !== undefined && typeof declaredSchema !== 'string') {
340
- fail('step-extension-schema-invalid', `extension ${kind} schema must be a string`, ref, stepId);
341
- }
342
- if (registration.schemaRef !== declaredSchema) {
343
- fail('step-extension-schema-mismatch', `extension ${kind} schema does not match its registry`, ref, stepId);
344
- }
345
- return withActiveRef(state, ref, () => {
346
- const { root, entry } = readRoot(state, ref, 'StepExtension');
347
- if (bodyChildren(root).length > 0 || sectionChildren(root).length > 0) {
348
- fail('step-extension-shape-invalid', `${ref} extension must be attribute-only`, ref, stepId);
349
- }
350
- const rootAttrs = exactAttrs(root, ['kind', 'schema', 'value'], ref);
351
- if (requiredString(rootAttrs, 'kind', ref) !== kind) {
352
- fail('step-extension-kind-mismatch', `${ref} kind does not match ExtensionRef`, ref, stepId);
353
- }
354
- if (rootAttrs.schema !== declaredSchema) {
355
- fail('step-extension-schema-mismatch', `${ref} schema does not match ExtensionRef`, ref, stepId);
356
- }
357
- const extensionId = idOf(root);
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
- }
372
- return Object.freeze({
373
- id: extensionId,
374
- kind,
375
- sourceRef: ref,
376
- ...(typeof declaredSchema === 'string' ? { schemaRef: declaredSchema } : {}),
377
- value: normalizedValue,
378
- digest: entry.digest,
379
- });
380
- });
381
- }
382
-
383
- function readStep(state: AssemblyState, refElement: Element): DefinitionStepForestNode {
384
- const declaredId = idOf(refElement);
385
- if (!declaredId) fail('step-ref-id-missing', 'StepRef requires an id');
386
- const attrs = exactAttrs(refElement, ['src'], state.activeRefs.at(-1) ?? '');
387
- const ref = normalizeDefinitionStepSourceRef(requiredString(attrs, 'src', state.activeRefs.at(-1) ?? ''));
388
- return withActiveRef(state, ref, () => {
389
- const { root, entry } = readRoot(state, ref, 'Step');
390
- const stepId = idOf(root);
391
- if (stepId !== declaredId) fail('step-id-mismatch', `${ref} id ${stepId || '<missing>'} does not match StepRef ${declaredId}`, ref, declaredId);
392
- if (state.steps[stepId]) fail('step-id-duplicate', `step id ${stepId} is duplicated`, ref, stepId);
393
- exactAttrs(root, [], ref);
394
- if (bodyChildren(root).length > 0) fail('step-shape-invalid', `${ref} Step body must be empty`, ref, stepId);
395
- const sections = sectionChildren(root);
396
- const coreSections = sections.filter((section) => section.tag === 'Core');
397
- const extensionSections = sections.filter((section) => section.tag === 'Extensions');
398
- if (sections.some((section) => section.tag !== 'Core' && section.tag !== 'Extensions')
399
- || coreSections.length !== 1 || extensionSections.length > 1) {
400
- fail('step-shape-invalid', `${ref} requires one Core and at most one Extensions section`, ref, stepId);
401
- }
402
- const coreChildren = bodyChildren(coreSections[0]);
403
- if (coreChildren.length !== 1 || sectionChildren(coreSections[0]).length > 0) {
404
- fail('step-core-invalid', `${ref} Core requires exactly one executable child`, ref, stepId);
405
- }
406
- if (idOf(coreChildren[0]) !== stepId) {
407
- fail('step-core-id-mismatch', `${ref} Core id must equal Step id ${stepId}`, ref, stepId);
408
- }
409
- const extensions: Record<string, DefinitionStepExtension> = {};
410
- for (const extensionRef of extensionSections.flatMap(bodyChildren)) {
411
- if (extensionRef.tag !== 'ExtensionRef') fail('step-extension-ref-invalid', `${ref} Extensions accepts only ExtensionRef`, ref, stepId);
412
- const extension = readExtension(state, extensionRef, stepId);
413
- if (extensions[extension.kind]) fail('step-extension-kind-duplicate', `${stepId} extension ${extension.kind} is duplicated`, ref, stepId);
414
- extensions[extension.kind] = extension;
415
- }
416
- const step = Object.freeze({
417
- id: stepId,
418
- sourceRef: ref,
419
- core: nodeProjection(coreChildren[0], ref),
420
- extensions: Object.freeze(extensions),
421
- digest: entry.digest,
422
- });
423
- state.steps[stepId] = step;
424
- state.stepOrder.push(stepId);
425
- return Object.freeze({ kind: 'step' as const, stepId });
426
- });
427
- }
428
-
429
- function readGroupChildren(state: AssemblyState, group: Element, sourceRef?: string): DefinitionStepGroup {
430
- const id = idOf(group);
431
- if (!id) fail('step-group-id-missing', 'StepGroup requires an id', sourceRef);
432
- if (state.groupIds.has(id)) fail('step-group-id-duplicate', `group id ${id} is duplicated`, sourceRef);
433
- state.groupIds.add(id);
434
- exactAttrs(group, [], sourceRef ?? state.activeRefs.at(-1) ?? '');
435
- if (sectionChildren(group).length > 0) fail('step-group-shape-invalid', `StepGroup ${id} cannot have sections`, sourceRef);
436
- const children = bodyChildren(group).map((child) => readForestNode(state, child));
437
- if (children.length === 0) fail('step-group-empty', `StepGroup ${id} must not be empty`, sourceRef);
438
- return Object.freeze({ kind: 'group', id, ...(sourceRef ? { sourceRef } : {}), children: Object.freeze(children) });
439
- }
440
-
441
- function readGroupRef(state: AssemblyState, refElement: Element): DefinitionStepGroup {
442
- const declaredId = idOf(refElement);
443
- if (!declaredId) fail('step-group-ref-id-missing', 'StepGroupRef requires an id');
444
- const attrs = exactAttrs(refElement, ['src'], state.activeRefs.at(-1) ?? '');
445
- const ref = normalizeDefinitionStepSourceRef(requiredString(attrs, 'src', state.activeRefs.at(-1) ?? ''));
446
- return withActiveRef(state, ref, () => {
447
- const { root } = readRoot(state, ref, 'StepGroup');
448
- if (idOf(root) !== declaredId) fail('step-group-id-mismatch', `${ref} id must match StepGroupRef ${declaredId}`, ref);
449
- return readGroupChildren(state, root, ref);
450
- });
451
- }
452
-
453
- function readForestNode(state: AssemblyState, element: Element): DefinitionStepForestNode {
454
- if (element.tag === 'StepRef') return readStep(state, element);
455
- if (element.tag === 'StepGroup') return readGroupChildren(state, element);
456
- if (element.tag === 'StepGroupRef') return readGroupRef(state, element);
457
- fail('step-space-child-invalid', `StepSpace/StepGroup cannot contain <${element.tag}>`, state.activeRefs.at(-1));
458
- }
459
-
460
- function normalizeAssemblyBoundary(
461
- runtimeValue: DefinitionStepAssemblyRuntime,
462
- inputValue: DefinitionStepAssemblyInput,
463
- configValue: DefinitionStepAssemblyConfig,
464
- ) {
465
- const runtime = exactOwnDataRecord(runtimeValue, ['sources', 'extensionCodecs'], 'runtime');
466
- const sources = exactOwnDataRecord(ownValue(runtime, 'sources'), ['readSource'], 'runtime.sources');
467
- if (typeof ownValue(sources, 'readSource') !== 'function') fail('step-runtime-invalid', 'runtime.sources.readSource must be a function');
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');
471
- }
472
- const input = exactOwnDataRecord(inputValue, ['stepSpaceRef'], 'input');
473
- exactOwnDataRecord(configValue, [], 'config');
474
- return {
475
- runtime: runtimeValue,
476
- input: { stepSpaceRef: normalizeDefinitionStepSourceRef(ownValue(input, 'stepSpaceRef')) },
477
- };
478
- }
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
-
578
- /** Processor form: output = fn(runtime, input, config). */
579
- export function assembleDefinitionStepForest(
580
- runtimeValue: DefinitionStepAssemblyRuntime,
581
- inputValue: DefinitionStepAssemblyInput,
582
- configValue: DefinitionStepAssemblyConfig,
583
- ): DefinitionStepForest {
584
- const { runtime, input } = normalizeAssemblyBoundary(runtimeValue, inputValue, configValue);
585
- const state: AssemblyState = {
586
- runtime,
587
- closure: [],
588
- closureByRef: new Map(),
589
- steps: {},
590
- stepOrder: [],
591
- groupIds: new Set(),
592
- activeRefs: [],
593
- };
594
- const rootRef = input.stepSpaceRef;
595
- state.activeRefs.push(rootRef);
596
- const root = readRoot(state, rootRef, 'StepSpace').root;
597
- exactAttrs(root, [], rootRef);
598
- if (sectionChildren(root).length > 0) fail('step-space-shape-invalid', 'StepSpace cannot have sections', rootRef);
599
- const id = idOf(root);
600
- if (!id) fail('step-space-id-missing', 'StepSpace requires an id', rootRef);
601
- const metadata = root.metadata ?? {};
602
- const apiVersion = typeof metadata.apiVersion === 'string' ? metadata.apiVersion : 'depa.flows/v1';
603
- const version = typeof metadata.version === 'string' ? metadata.version : '1';
604
- for (const key of Object.keys(metadata)) {
605
- if (key !== 'apiVersion' && key !== 'version') fail('step-space-metadata-unsupported', `StepSpace.${key} is not allowed`, rootRef);
606
- }
607
- let roots: DefinitionStepForestNode[];
608
- try {
609
- roots = bodyChildren(root).map((child) => readForestNode(state, child));
610
- } finally {
611
- state.activeRefs.pop();
612
- }
613
- if (roots.length === 0) fail('step-space-empty', 'StepSpace must not be empty', rootRef);
614
- const artifactFact = state.closure.map(({ ref, digest: value }) => [ref, value]);
615
- return normalizeDefinitionStepForest({
616
- schemaVersion: 'depa.definition-step-forest/v1',
617
- id,
618
- apiVersion,
619
- version,
620
- rootRef,
621
- roots,
622
- steps: state.steps,
623
- stepOrder: state.stepOrder,
624
- closure: state.closure,
625
- artifactDigest: digest(JSON.stringify(artifactFact)),
626
- });
627
- }
628
-
629
- function normalizeCore(value: unknown, label: string): DefinitionStepCoreNode {
630
- const record = exactOwnDataRecord(value, ['tag', 'id', 'metadata', 'attrs', 'children', 'sections'], label);
631
- const tag = ownValue(record, 'tag');
632
- const id = ownValue(record, 'id');
633
- if (typeof tag !== 'string' || !tag) fail('step-forest-invalid', `${label}.tag must be a string`);
634
- if (id !== undefined && (typeof id !== 'string' || !id)) fail('step-forest-invalid', `${label}.id must be a string`);
635
- const childrenValue = ownValue(record, 'children');
636
- if (!Array.isArray(childrenValue)) fail('step-forest-invalid', `${label}.children must be an array`);
637
- const children = closedValue(childrenValue, `${label}.children`) as readonly unknown[];
638
- const sectionsValue = exactOwnDataRecord(ownValue(record, 'sections'), Object.keys(ownValue(record, 'sections') as object), `${label}.sections`);
639
- return Object.freeze({
640
- tag,
641
- ...(typeof id === 'string' ? { id } : {}),
642
- metadata: closedValue(ownValue(record, 'metadata'), `${label}.metadata`) as Record<string, DefinitionStepValue>,
643
- attrs: closedValue(ownValue(record, 'attrs'), `${label}.attrs`) as Record<string, DefinitionStepValue>,
644
- children: Object.freeze(children.map((child, index) => normalizeCore(child, `${label}.children[${index}]`))),
645
- sections: Object.freeze(Object.fromEntries(Object.keys(sectionsValue).sort(compareCodeUnits).map((key) => [
646
- key,
647
- normalizeCore(ownValue(sectionsValue, key), `${label}.sections.${key}`),
648
- ]))),
649
- });
650
- }
651
-
652
- function normalizeForestNode(value: unknown, label: string): DefinitionStepForestNode {
653
- const kindRecord = exactOwnDataRecord(value, ['kind', 'stepId', 'id', 'sourceRef', 'children'], label);
654
- const kind = ownValue(kindRecord, 'kind');
655
- if (kind === 'step') {
656
- const stepId = ownValue(kindRecord, 'stepId');
657
- if (typeof stepId !== 'string' || !stepId) fail('step-forest-invalid', `${label}.stepId must be a string`);
658
- if (ownValue(kindRecord, 'id') !== undefined || ownValue(kindRecord, 'children') !== undefined || ownValue(kindRecord, 'sourceRef') !== undefined) {
659
- fail('step-forest-invalid', `${label} step leaf has group fields`);
660
- }
661
- return Object.freeze({ kind, stepId });
662
- }
663
- if (kind !== 'group') fail('step-forest-invalid', `${label}.kind is invalid`);
664
- const id = ownValue(kindRecord, 'id');
665
- const sourceRef = ownValue(kindRecord, 'sourceRef');
666
- const children = ownValue(kindRecord, 'children');
667
- if (typeof id !== 'string' || !id || !Array.isArray(children)) fail('step-forest-invalid', `${label} group is invalid`);
668
- if (sourceRef !== undefined) normalizeDefinitionStepSourceRef(sourceRef);
669
- return Object.freeze({
670
- kind,
671
- id,
672
- ...(typeof sourceRef === 'string' ? { sourceRef } : {}),
673
- children: Object.freeze(children.map((child, index) => normalizeForestNode(child, `${label}.children[${index}]`))),
674
- });
675
- }
676
-
677
- /** Pure closed-data normalizer; appropriate for decoded facts and tests. */
678
- export function normalizeDefinitionStepForest(value: unknown): DefinitionStepForest {
679
- const record = exactOwnDataRecord(value, [
680
- 'schemaVersion', 'id', 'apiVersion', 'version', 'rootRef', 'roots', 'steps', 'stepOrder', 'closure', 'artifactDigest',
681
- ], 'forest');
682
- if (ownValue(record, 'schemaVersion') !== 'depa.definition-step-forest/v1') fail('step-forest-invalid', 'forest schemaVersion is invalid');
683
- const stringField = (key: string) => {
684
- const result = ownValue(record, key);
685
- if (typeof result !== 'string' || !result) fail('step-forest-invalid', `forest.${key} must be a string`);
686
- return result;
687
- };
688
- const rootRef = normalizeDefinitionStepSourceRef(stringField('rootRef'));
689
- const rootsValue = ownValue(record, 'roots');
690
- const orderValue = ownValue(record, 'stepOrder');
691
- const closureValue = ownValue(record, 'closure');
692
- if (!Array.isArray(rootsValue) || !Array.isArray(orderValue) || !Array.isArray(closureValue)) {
693
- fail('step-forest-invalid', 'forest roots, stepOrder and closure must be arrays');
694
- }
695
- closedValue(orderValue, 'forest.stepOrder');
696
- const stepOrder = orderValue.map((item) => {
697
- if (typeof item !== 'string' || !item) fail('step-forest-invalid', 'forest.stepOrder entries must be strings');
698
- return item;
699
- });
700
- const stepsValue = exactOwnDataRecord(ownValue(record, 'steps'), Object.keys(ownValue(record, 'steps') as object), 'forest.steps');
701
- const steps: Record<string, DefinitionStep> = {};
702
- for (const key of Object.keys(stepsValue).sort(compareCodeUnits)) {
703
- const stepRecord = exactOwnDataRecord(ownValue(stepsValue, key), ['id', 'sourceRef', 'core', 'extensions', 'digest'], `forest.steps.${key}`);
704
- if (ownValue(stepRecord, 'id') !== key) fail('step-forest-invalid', `forest.steps.${key}.id mismatch`);
705
- const extensionsValue = exactOwnDataRecord(ownValue(stepRecord, 'extensions'), Object.keys(ownValue(stepRecord, 'extensions') as object), `forest.steps.${key}.extensions`);
706
- const extensions: Record<string, DefinitionStepExtension> = {};
707
- for (const kind of Object.keys(extensionsValue).sort(compareCodeUnits)) {
708
- const extensionRecord = exactOwnDataRecord(ownValue(extensionsValue, kind), ['id', 'kind', 'sourceRef', 'schemaRef', 'value', 'digest'], `forest.steps.${key}.extensions.${kind}`);
709
- const extensionKind = ownValue(extensionRecord, 'kind');
710
- const extensionId = ownValue(extensionRecord, 'id');
711
- const extensionSourceRef = ownValue(extensionRecord, 'sourceRef');
712
- const schemaRef = ownValue(extensionRecord, 'schemaRef');
713
- const extensionDigest = ownValue(extensionRecord, 'digest');
714
- if (extensionKind !== kind || typeof extensionId !== 'string' || typeof extensionDigest !== 'string') fail('step-forest-invalid', `extension ${kind} is invalid`);
715
- extensions[kind] = Object.freeze({
716
- id: extensionId,
717
- kind,
718
- sourceRef: normalizeDefinitionStepSourceRef(extensionSourceRef),
719
- ...(typeof schemaRef === 'string' ? { schemaRef } : {}),
720
- value: closedValue(ownValue(extensionRecord, 'value'), `forest.steps.${key}.extensions.${kind}.value`),
721
- digest: extensionDigest as DefinitionStepDigest,
722
- });
723
- }
724
- const stepSourceRef = normalizeDefinitionStepSourceRef(ownValue(stepRecord, 'sourceRef'));
725
- const stepDigest = ownValue(stepRecord, 'digest');
726
- if (typeof stepDigest !== 'string') fail('step-forest-invalid', `forest.steps.${key}.digest is invalid`);
727
- steps[key] = Object.freeze({
728
- id: key,
729
- sourceRef: stepSourceRef,
730
- core: normalizeCore(ownValue(stepRecord, 'core'), `forest.steps.${key}.core`),
731
- extensions: Object.freeze(extensions),
732
- digest: stepDigest as DefinitionStepDigest,
733
- });
734
- }
735
- if (new Set(stepOrder).size !== stepOrder.length || stepOrder.some((id) => !steps[id]) || Object.keys(steps).length !== stepOrder.length) {
736
- fail('step-forest-invalid', 'forest stepOrder must exactly identify steps');
737
- }
738
- const closure = closureValue.map((entry, index): DefinitionStepClosureEntry => {
739
- const closureRecord = exactOwnDataRecord(entry, ['ref', 'digest', 'content'], `forest.closure[${index}]`);
740
- const closureDigest = ownValue(closureRecord, 'digest');
741
- const content = ownValue(closureRecord, 'content');
742
- if (typeof closureDigest !== 'string' || typeof content !== 'string') fail('step-forest-invalid', `forest.closure[${index}] is invalid`);
743
- return Object.freeze({
744
- ref: normalizeDefinitionStepSourceRef(ownValue(closureRecord, 'ref')),
745
- digest: closureDigest as DefinitionStepDigest,
746
- content,
747
- });
748
- });
749
- return Object.freeze({
750
- schemaVersion: 'depa.definition-step-forest/v1',
751
- id: stringField('id'),
752
- apiVersion: stringField('apiVersion'),
753
- version: stringField('version'),
754
- rootRef,
755
- roots: Object.freeze(rootsValue.map((node, index) => normalizeForestNode(node, `forest.roots[${index}]`))),
756
- steps: Object.freeze(steps),
757
- stepOrder: Object.freeze(stepOrder),
758
- closure: Object.freeze(closure),
759
- artifactDigest: stringField('artifactDigest') as DefinitionStepDigest,
760
- });
761
- }
762
-
763
- export function createDefinitionStepSourceRuntime(
764
- sources: CompatibleSourceCollection,
765
- extensionCodecs: DefinitionStepAssemblyRuntime['extensionCodecs'] = Object.freeze({ resolve: () => undefined }),
766
- ): DefinitionStepAssemblyRuntime {
767
- const entries = Array.isArray(sources)
768
- ? sources.map((source) => ['ref' in source ? source.ref : source.name, source.content] as const)
769
- : Object.entries(sources);
770
- const byRef = new Map<string, Uint8Array>();
771
- for (const [refValue, content] of entries) {
772
- const ref = normalizeDefinitionStepSourceRef(refValue);
773
- if (byRef.has(ref)) fail('step-source-ref-duplicate', `source ${ref} is duplicated`, ref);
774
- if (typeof content !== 'string') fail('step-source-content-invalid', `source ${ref} content must be a string`, ref);
775
- byRef.set(ref, new TextEncoder().encode(content));
776
- }
777
- return Object.freeze({
778
- sources: Object.freeze({
779
- readSource(refValue: string): Uint8Array {
780
- const ref = normalizeDefinitionStepSourceRef(refValue);
781
- const bytes = byRef.get(ref);
782
- if (!bytes) fail('step-source-not-found', `source ${ref} is not declared`, ref);
783
- return bytes.slice();
784
- },
785
- }),
786
- extensionCodecs,
787
- });
788
- }
789
-
790
- function serializeValue(value: unknown): string {
791
- const normalized = closedValue(value, 'xnl.value');
792
- if (normalized === null || typeof normalized !== 'object') return JSON.stringify(normalized);
793
- if (Array.isArray(normalized)) return `[${normalized.map(serializeValue).join(' ')}]`;
794
- return `{ ${Object.entries(normalized).map(([key, item]) => `${key} = ${serializeValue(item)}`).join(' ')} }`;
795
- }
796
-
797
- function serializeElement(element: Element): string {
798
- const id = idOf(element);
799
- const metadata = Object.entries(element.metadata ?? {}).map(([key, value]) => `${key}=${serializeValue(value)}`).join(' ');
800
- const attrs = Object.entries(element.attributes ?? {}).map(([key, value]) => `${key}=${serializeValue(value)}`).join(' ');
801
- const sections = sectionChildren(element).map(serializeElement).join(' ');
802
- const body = bodyChildren(element).map(serializeElement).join(' ');
803
- return `<${element.tag}${id ? ` #${id}` : ''}${metadata ? ` ${metadata}` : ''}${attrs ? ` { ${attrs} }` : ''}${sections ? ` ( ${sections} )` : ''}${body ? ` [ ${body} ]` : ''}>`;
804
- }
805
-
806
- function coreToXnl(core: DefinitionStepCoreNode): string {
807
- const metadata = Object.entries(core.metadata).map(([key, value]) => `${key}=${serializeValue(value)}`).join(' ');
808
- const attrs = Object.entries(core.attrs).map(([key, value]) => `${key}=${serializeValue(value)}`).join(' ');
809
- const sections = Object.values(core.sections).map(coreToXnl).join(' ');
810
- const children = core.children.map(coreToXnl).join(' ');
811
- return `<${core.tag}${core.id ? ` #${core.id}` : ''}${metadata ? ` ${metadata}` : ''}${attrs ? ` { ${attrs} }` : ''}${sections ? ` ( ${sections} )` : ''}${children ? ` [ ${children} ]` : ''}>`;
812
- }
813
-
814
- export interface DefinitionStepProfileAssemblyResult {
815
- readonly sources: CompatibleSourceCollection;
816
- readonly forest?: DefinitionStepForest;
817
- readonly diagnostics: readonly DefinitionStepDiagnostic[];
818
- }
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
-
834
- /**
835
- * Pure-source composition helper used by profile owners. It only recognizes the
836
- * exact profileSourceRef and follows the explicit StepSpace closure.
837
- */
838
- export function assembleDefinitionStepProfileSources(
839
- sources: CompatibleSourceCollection,
840
- profileRoot: string,
841
- options: DefinitionStepProfileLoadOptions = {},
842
- ): DefinitionStepProfileAssemblyResult {
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(
847
- createDefinitionStepSourceRuntime(sources),
848
- { sources: normalizedSources },
849
- { ...options, profileRoot },
850
- );
851
- }
852
-
853
- /** Runtime-first profile assembly for filesystem/VFS adapters with exact reads. */
854
- export function assembleDefinitionStepProfileSourcesWithRuntime(
855
- runtime: DefinitionStepAssemblyRuntime,
856
- sources: CompatibleSourceCollection,
857
- profileRoot: string,
858
- options: DefinitionStepProfileLoadOptions = {},
859
- ): DefinitionStepProfileAssemblyResult {
860
- const entries = Array.isArray(sources)
861
- ? sources.map((source) => ({ ref: 'ref' in source ? source.ref : source.name, content: source.content }))
862
- : Object.entries(sources).map(([ref, content]) => ({ ref, content }));
863
- const profileSourceRef = options.profileSourceRef ?? 'manifest.xnl';
864
- const entry = entries.find((source) => source.ref === profileSourceRef);
865
- if (!entry) return { sources, diagnostics: [] };
866
- let root: Element;
867
- try {
868
- root = parseRoot(new TextEncoder().encode(entry.content), profileSourceRef, profileRoot).root;
869
- } catch (error) {
870
- // The existing profile loader owns ordinary root/parse diagnostics. A
871
- // StepSpace diagnostic is authoritative only after one exact ref parsed.
872
- if (error instanceof DefinitionStepAssemblyError) return { sources, diagnostics: [] };
873
- throw error;
874
- }
875
- const refs = sectionChildren(root).filter((section) => section.tag === 'StepSpaceRef');
876
- if (refs.length === 0) return { sources, diagnostics: [] };
877
- try {
878
- if (refs.length !== 1) fail('step-space-ref-duplicate', `${profileRoot} requires exactly one StepSpaceRef`, profileSourceRef);
879
- if (bodyChildren(root).length > 0) {
880
- fail('step-topology-authority-conflict', `${profileRoot} cannot combine inline executable nodes with StepSpaceRef`, profileSourceRef);
881
- }
882
- const refAttrs = exactAttrs(refs[0], ['src'], profileSourceRef);
883
- if (idOf(refs[0]) || bodyChildren(refs[0]).length > 0 || sectionChildren(refs[0]).length > 0) {
884
- fail('step-space-ref-invalid', 'StepSpaceRef must contain only src', profileSourceRef);
885
- }
886
- const forest = assembleDefinitionStepForest(
887
- { sources: runtime.sources, extensionCodecs: runtime.extensionCodecs },
888
- { stepSpaceRef: requiredString(refAttrs, 'src', profileSourceRef) },
889
- {},
890
- );
891
- const retainedSections = sectionChildren(root).filter((section) => section.tag !== 'StepSpaceRef');
892
- const projectedRoot = {
893
- ...root,
894
- body: forest.stepOrder.map((stepId) => forest.steps[stepId].core).map((core) => ({ core })),
895
- };
896
- const id = idOf(root);
897
- const metadata = Object.entries(root.metadata ?? {}).map(([key, value]) => `${key}=${serializeValue(value)}`).join(' ');
898
- const attrs = Object.entries(root.attributes ?? {}).map(([key, value]) => `${key}=${serializeValue(value)}`).join(' ');
899
- const sectionText = retainedSections.map(serializeElement).join(' ');
900
- const coreText = forest.stepOrder.map((stepId) => coreToXnl(forest.steps[stepId].core)).join(' ');
901
- const content = `<${root.tag}${id ? ` #${id}` : ''}${metadata ? ` ${metadata}` : ''}${attrs ? ` { ${attrs} }` : ''}${sectionText ? ` ( ${sectionText} )` : ''}${coreText ? ` [ ${coreText} ]` : ''}>`;
902
- const closureRefs = new Set(forest.closure.map((item) => item.ref));
903
- const projectedSources = entries
904
- .filter((source) => !closureRefs.has(source.ref))
905
- .map((source) => ({ ref: source.ref, content: source.ref === profileSourceRef ? content : source.content }));
906
- void projectedRoot;
907
- return { sources: projectedSources, forest, diagnostics: [] };
908
- } catch (error) {
909
- if (!(error instanceof DefinitionStepAssemblyError)) throw error;
910
- return { sources, diagnostics: [error.diagnostic] };
911
- }
912
- }