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