@telorun/analyzer 1.1.0 → 1.3.0
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/LICENSE +2 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +21 -1
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +14 -0
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +27 -13
- package/dist/manifest-loader.d.ts +23 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +66 -3
- package/dist/position-metadata.d.ts +6 -1
- package/dist/position-metadata.d.ts.map +1 -1
- package/dist/position-metadata.js +10 -2
- package/dist/precompile.d.ts.map +1 -1
- package/dist/precompile.js +9 -1
- package/dist/reference-field-map.js +58 -6
- package/dist/resolve-ref-sentinels.d.ts +27 -0
- package/dist/resolve-ref-sentinels.d.ts.map +1 -0
- package/dist/resolve-ref-sentinels.js +114 -0
- package/dist/schema-compat.d.ts +7 -1
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +19 -2
- package/dist/system-kinds.d.ts +25 -0
- package/dist/system-kinds.d.ts.map +1 -0
- package/dist/system-kinds.js +34 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +32 -6
- package/package.json +4 -3
- package/src/analyzer.ts +23 -1
- package/src/builtins.ts +14 -0
- package/src/dependency-graph.ts +27 -14
- package/src/manifest-loader.ts +69 -4
- package/src/position-metadata.ts +10 -2
- package/src/precompile.ts +8 -1
- package/src/reference-field-map.ts +83 -6
- package/src/resolve-ref-sentinels.ts +127 -0
- package/src/schema-compat.ts +19 -2
- package/src/system-kinds.ts +37 -0
- package/src/types.ts +12 -0
- package/src/validate-references.ts +34 -6
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SUSTAINABLE USE LICENSE (Fair-code)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
3
|
+
Copyright (c) 2026 CodeNet Sp. z o.o.
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute the Software for any purpose—including commercial purposes—subject to the following conditions:
|
|
6
6
|
|
|
@@ -14,4 +14,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
|
14
14
|
|
|
15
15
|
5. DISCLAIMER: The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
|
|
16
16
|
|
|
17
|
-
For commercial licensing, managed hosting exemptions, or enterprise inquiries, please contact
|
|
17
|
+
For commercial licensing, managed hosting exemptions, or enterprise inquiries, please contact <contact@codenet.pl>.
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAe9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA+c/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IA6dvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CAAC,SAAS,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,EAAE;IAexF,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
|
package/dist/analyzer.js
CHANGED
|
@@ -7,6 +7,7 @@ import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kerne
|
|
|
7
7
|
import { computeSuggestKind } from "./kind-suggest.js";
|
|
8
8
|
import { isModuleKind } from "./module-kinds.js";
|
|
9
9
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
10
|
+
import { resolveRefSentinels } from "./resolve-ref-sentinels.js";
|
|
10
11
|
import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
|
|
11
12
|
import { celTypeSatisfiesJsonSchema, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
|
|
12
13
|
import { DiagnosticSeverity } from "./types.js";
|
|
@@ -492,6 +493,20 @@ export class StaticAnalyzer {
|
|
|
492
493
|
}
|
|
493
494
|
// Phase 2: extract inline resources from x-telo-ref slots into first-class manifests
|
|
494
495
|
const allManifests = normalizeInlineResources(manifests, defs, aliases, aliasesByModule);
|
|
496
|
+
// Phase 2.5: resolve `!ref <name>` sentinels at every ref slot to canonical
|
|
497
|
+
// {kind, name} objects so downstream phases (validation, dependency graph,
|
|
498
|
+
// kernel controllers) see a uniform shape. Runs after normalize so both
|
|
499
|
+
// original and inline-extracted manifests have their sentinels resolved.
|
|
500
|
+
resolveRefSentinels(allManifests, defs, aliases, aliasesByModule);
|
|
501
|
+
// Trusted-input fast path: when the caller has already attested that
|
|
502
|
+
// this exact manifest set passes analysis (e.g. via the kernel's
|
|
503
|
+
// hash-stamped `.validated.json` cache), skip the validation walk.
|
|
504
|
+
// Registration of identities / aliases / definitions and inline-resource
|
|
505
|
+
// normalisation have already run above; that's all downstream
|
|
506
|
+
// consumers (prepare, init loop) require.
|
|
507
|
+
if (options?.skipValidation) {
|
|
508
|
+
return diagnostics;
|
|
509
|
+
}
|
|
495
510
|
// Build a name→manifest map for looking up referenced resources
|
|
496
511
|
const byName = new Map();
|
|
497
512
|
for (const m of allManifests) {
|
|
@@ -779,7 +794,12 @@ export class StaticAnalyzer {
|
|
|
779
794
|
}
|
|
780
795
|
normalize(manifests, registry) {
|
|
781
796
|
const ctx = registry._context();
|
|
782
|
-
|
|
797
|
+
const normalized = normalizeInlineResources(manifests, ctx.definitions, ctx.aliases, ctx.aliasesByModule);
|
|
798
|
+
// Resolve !ref sentinels after normalize so both the original and
|
|
799
|
+
// inline-extracted manifests get their refs canonicalized to
|
|
800
|
+
// {kind, name} for the kernel that consumes this output.
|
|
801
|
+
resolveRefSentinels(normalized, ctx.definitions, ctx.aliases, ctx.aliasesByModule);
|
|
802
|
+
return normalized;
|
|
783
803
|
}
|
|
784
804
|
prepare(manifests, registry) {
|
|
785
805
|
const ctx = registry._context();
|
package/dist/builtins.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EAiU/C,CAAC"}
|
package/dist/builtins.js
CHANGED
|
@@ -218,6 +218,20 @@ export const KERNEL_BUILTINS = [
|
|
|
218
218
|
anyOf: [
|
|
219
219
|
{ type: "string", "x-telo-ref": "telo#Runnable" },
|
|
220
220
|
{ type: "string", "x-telo-ref": "telo#Service" },
|
|
221
|
+
// Post-resolution shape that `resolveRefSentinels`
|
|
222
|
+
// substitutes a `!ref <name>` sentinel into. The
|
|
223
|
+
// adjacent `x-telo-ref` constraints govern the kind
|
|
224
|
+
// check; this branch only admits the structural form so
|
|
225
|
+
// AJV doesn't reject a resolved ref.
|
|
226
|
+
{
|
|
227
|
+
type: "object",
|
|
228
|
+
required: ["kind", "name"],
|
|
229
|
+
properties: {
|
|
230
|
+
kind: { type: "string" },
|
|
231
|
+
name: { type: "string" },
|
|
232
|
+
},
|
|
233
|
+
additionalProperties: true,
|
|
234
|
+
},
|
|
221
235
|
],
|
|
222
236
|
},
|
|
223
237
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dependency-graph.d.ts","sourceRoot":"","sources":["../src/dependency-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"dependency-graph.d.ts","sourceRoot":"","sources":["../src/dependency-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAInE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B;kDAC8C;IAC9C,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACpC;oFACgF;IAChF,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;CACrC;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC3C,eAAe,CAkHjB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,MAAM,CAOtE"}
|
package/dist/dependency-graph.js
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
1
2
|
import { isRefEntry, isScopeEntry, resolveFieldValues } from "./reference-field-map.js";
|
|
2
|
-
|
|
3
|
-
* Module-identity docs (Telo.Application, Telo.Library) are intentionally
|
|
4
|
-
* not in this set: an Application's `targets` use `x-telo-ref` to real
|
|
5
|
-
* Runnable/Service resources, so the Application legitimately depends on
|
|
6
|
-
* them in boot order — modeling that as a graph edge is correct. A Library
|
|
7
|
-
* has no `targets`, so it becomes a zero-edge node, which is harmless.
|
|
8
|
-
* If the graph is ever consumed as "things to init", skip these kinds at
|
|
9
|
-
* the consumer site; the controller already runs them separately. */
|
|
10
|
-
const SYSTEM_KINDS = new Set(["Telo.Definition", "Telo.Import"]);
|
|
3
|
+
import { DEPENDENCY_GRAPH_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
11
4
|
const nodeKey = (kind, name) => `${kind}\0${name}`;
|
|
12
5
|
/**
|
|
13
6
|
* Builds a directed acyclic graph (DAG) of runtime resource dependencies and
|
|
@@ -22,13 +15,19 @@ const nodeKey = (kind, name) => `${kind}\0${name}`;
|
|
|
22
15
|
* not pre-compute or pass field maps separately.
|
|
23
16
|
*/
|
|
24
17
|
export function buildDependencyGraph(resources, registry, aliases, aliasesByModule) {
|
|
25
|
-
// --- Build node set ---
|
|
18
|
+
// --- Build node set + name index ---
|
|
26
19
|
const nodes = new Map();
|
|
20
|
+
// Sentinel lookup (`!ref <name>`) needs to resolve a bare name to its
|
|
21
|
+
// declared kind. Names are unique within a manifest scope, so a flat
|
|
22
|
+
// map suffices and lets the sentinel branch below avoid a full
|
|
23
|
+
// O(N) scan of the node set on every reference.
|
|
24
|
+
const nodesByName = new Map();
|
|
27
25
|
for (const r of resources) {
|
|
28
26
|
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind))
|
|
29
27
|
continue;
|
|
30
|
-
const
|
|
31
|
-
nodes.set(
|
|
28
|
+
const node = { kind: r.kind, name: r.metadata.name };
|
|
29
|
+
nodes.set(nodeKey(node.kind, node.name), node);
|
|
30
|
+
nodesByName.set(node.name, node);
|
|
32
31
|
}
|
|
33
32
|
// --- Build adjacency: from → deps (from depends on dep) ---
|
|
34
33
|
const deps = new Map();
|
|
@@ -66,7 +65,22 @@ export function buildDependencyGraph(resources, registry, aliases, aliasesByModu
|
|
|
66
65
|
if (!isRefEntry(entry))
|
|
67
66
|
continue;
|
|
68
67
|
for (const val of resolveFieldValues(r, fieldPath)) {
|
|
69
|
-
if (!val
|
|
68
|
+
if (!val)
|
|
69
|
+
continue;
|
|
70
|
+
// `!ref <name>` sentinel — look up the target's kind from the
|
|
71
|
+
// name (resources are unique by name) so the edge carries the
|
|
72
|
+
// concrete kind, matching the {kind, name} edge shape below.
|
|
73
|
+
if (isRefSentinel(val)) {
|
|
74
|
+
const refName = val.source;
|
|
75
|
+
if (scopedNames.has(refName))
|
|
76
|
+
continue;
|
|
77
|
+
const node = nodesByName.get(refName);
|
|
78
|
+
if (node) {
|
|
79
|
+
deps.get(sourceKey).add(nodeKey(node.kind, node.name));
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (typeof val !== "object")
|
|
70
84
|
continue;
|
|
71
85
|
const ref = val;
|
|
72
86
|
if (!ref.kind || !ref.name)
|
|
@@ -6,12 +6,25 @@ export declare class Loader {
|
|
|
6
6
|
* caller (kernel) and a raw-mode caller (analyzer/editor) on the same file
|
|
7
7
|
* get distinct entries, so neither sees the wrong manifest tree. */
|
|
8
8
|
private readonly fileCache;
|
|
9
|
+
/** requestUrl → canonical `source`. Lets `loadFile` skip the source read
|
|
10
|
+
* when a URL it has already canonicalised is requested again — kernel
|
|
11
|
+
* load → boot and the import-controller each ask the loader for the same
|
|
12
|
+
* modules. Without this fast path every duplicate request re-runs the
|
|
13
|
+
* source's `read()` (a `fetch` for `RegistrySource`, a disk read for
|
|
14
|
+
* `LocalFileSource`). */
|
|
15
|
+
private readonly urlToSource;
|
|
9
16
|
protected sources: ManifestSource[];
|
|
10
17
|
private readonly celEnv;
|
|
11
18
|
constructor(extraSourcesOrOptions?: ManifestSource[] | LoaderInitOptions);
|
|
12
19
|
register(source: ManifestSource): this;
|
|
13
20
|
private pick;
|
|
14
21
|
resolveEntryPoint(url: string): Promise<string>;
|
|
22
|
+
/** Returns the canonical source URL the loader has already mapped `url`
|
|
23
|
+
* to during a prior `loadFile`/`loadModule`/`loadGraph` call, or
|
|
24
|
+
* `undefined` when the URL has not been seen. Callers use it to test
|
|
25
|
+
* set-membership against a previous graph walk's modules without
|
|
26
|
+
* triggering an extra source read. */
|
|
27
|
+
canonicalize(url: string): string | undefined;
|
|
15
28
|
/** Read one file via the source chain and parse it into a LoadedFile.
|
|
16
29
|
* The result is shared with `Loader.fileCache`. Callers that want a
|
|
17
30
|
* private mutable copy must call `parseLoadedFile` directly with the
|
|
@@ -26,7 +39,16 @@ export declare class Loader {
|
|
|
26
39
|
* `importEdges` mapping each importing file's PascalCase aliases to their
|
|
27
40
|
* target's canonical source. */
|
|
28
41
|
loadGraph(entryUrl: string, options?: LoadOptions): Promise<LoadedGraph>;
|
|
29
|
-
|
|
42
|
+
/** Resolve an `import` URL against the file it appears in. Relative /
|
|
43
|
+
* absolute-path forms run through the owning `ManifestSource`'s
|
|
44
|
+
* `resolveRelative`; registry refs and full URLs pass through
|
|
45
|
+
* unchanged. Exposed so the import-controller (and any other
|
|
46
|
+
* caller-side resolver) lands on the *exact same* canonical URL the
|
|
47
|
+
* loader used when walking the entry graph — divergent resolution
|
|
48
|
+
* would silently break optimizations like `canonicalize()`-keyed
|
|
49
|
+
* cache hits whenever a non-trivial `ManifestSource.resolveRelative`
|
|
50
|
+
* is in play. */
|
|
51
|
+
resolveImportUrl(fromSource: string, importSource: string): string;
|
|
30
52
|
private assertSingleModuleDeclaration;
|
|
31
53
|
private assertNoSystemKindsInPartialContext;
|
|
32
54
|
private assertImportTargetIsLibrary;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAGV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AASpB,qBAAa,MAAM;IACjB;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAE3D,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,qBAAqB,GAAE,cAAc,EAAE,GAAG,iBAAsB;IAmB5E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAGV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AASpB,qBAAa,MAAM;IACjB;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAE3D;;;;;8BAK0B;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAEzD,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,qBAAqB,GAAE,cAAc,EAAE,GAAG,iBAAsB;IAmB5E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUrD;;;;2CAIuC;IACvC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAM7C;;;+BAG2B;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAgDvE;;wEAEoE;IAC9D,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAsB3E;;;qCAGiC;IAC3B,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAwG9E;;;;;;;;sBAQkB;IAClB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAOlE,OAAO,CAAC,6BAA6B;IAarC,OAAO,CAAC,mCAAmC;IAc3C,OAAO,CAAC,2BAA2B;YAkCrB,eAAe;IAmB7B;;;0CAGsC;IAChC,gBAAgB,CACpB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CA0B5D"}
|
package/dist/manifest-loader.js
CHANGED
|
@@ -16,6 +16,13 @@ export class Loader {
|
|
|
16
16
|
* caller (kernel) and a raw-mode caller (analyzer/editor) on the same file
|
|
17
17
|
* get distinct entries, so neither sees the wrong manifest tree. */
|
|
18
18
|
fileCache = new Map();
|
|
19
|
+
/** requestUrl → canonical `source`. Lets `loadFile` skip the source read
|
|
20
|
+
* when a URL it has already canonicalised is requested again — kernel
|
|
21
|
+
* load → boot and the import-controller each ask the loader for the same
|
|
22
|
+
* modules. Without this fast path every duplicate request re-runs the
|
|
23
|
+
* source's `read()` (a `fetch` for `RegistrySource`, a disk read for
|
|
24
|
+
* `LocalFileSource`). */
|
|
25
|
+
urlToSource = new Map();
|
|
19
26
|
sources;
|
|
20
27
|
celEnv;
|
|
21
28
|
constructor(extraSourcesOrOptions = []) {
|
|
@@ -45,8 +52,21 @@ export class Loader {
|
|
|
45
52
|
return s;
|
|
46
53
|
}
|
|
47
54
|
async resolveEntryPoint(url) {
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
// Route through `loadFile` so the resolved source URL and parsed
|
|
56
|
+
// entry are populated in `urlToSource` + `fileCache` in one read.
|
|
57
|
+
// Callers (kernel.load) immediately call `loadGraph(entryUrl)`
|
|
58
|
+
// afterwards — without this priming, the entry file would be read
|
|
59
|
+
// twice (twice over the network for `RegistrySource`).
|
|
60
|
+
const file = await this.loadFile(url);
|
|
61
|
+
return file.source;
|
|
62
|
+
}
|
|
63
|
+
/** Returns the canonical source URL the loader has already mapped `url`
|
|
64
|
+
* to during a prior `loadFile`/`loadModule`/`loadGraph` call, or
|
|
65
|
+
* `undefined` when the URL has not been seen. Callers use it to test
|
|
66
|
+
* set-membership against a previous graph walk's modules without
|
|
67
|
+
* triggering an extra source read. */
|
|
68
|
+
canonicalize(url) {
|
|
69
|
+
return this.urlToSource.get(url);
|
|
50
70
|
}
|
|
51
71
|
// --- New API: returns LoadedFile / LoadedModule / LoadedGraph ----------
|
|
52
72
|
/** Read one file via the source chain and parse it into a LoadedFile.
|
|
@@ -54,8 +74,42 @@ export class Loader {
|
|
|
54
74
|
* private mutable copy must call `parseLoadedFile` directly with the
|
|
55
75
|
* LoadedFile's `text`. */
|
|
56
76
|
async loadFile(url, options) {
|
|
77
|
+
const compileKey = options?.compile ? "compiled" : "raw";
|
|
78
|
+
const knownSource = this.urlToSource.get(url);
|
|
79
|
+
if (knownSource) {
|
|
80
|
+
const cached = this.fileCache.get(`${compileKey}:${knownSource}`);
|
|
81
|
+
if (cached)
|
|
82
|
+
return cached;
|
|
83
|
+
// The other compile-mode entry is cached — reparse from its text
|
|
84
|
+
// instead of re-reading the source.
|
|
85
|
+
//
|
|
86
|
+
// NOTE for watch-mode reactivation (cli/nodejs/src/commands/run.ts
|
|
87
|
+
// currently has `setupWatchMode` commented out): this branch
|
|
88
|
+
// assumes file contents don't change underneath a single Loader.
|
|
89
|
+
// Reviving watch mode will need a public `invalidate(url)` (or
|
|
90
|
+
// similar) that drops both `urlToSource[url]` and the cached
|
|
91
|
+
// entries for its canonical source before the loader serves the
|
|
92
|
+
// file again.
|
|
93
|
+
const altKey = `${compileKey === "compiled" ? "raw" : "compiled"}:${knownSource}`;
|
|
94
|
+
const alt = this.fileCache.get(altKey);
|
|
95
|
+
if (alt) {
|
|
96
|
+
const reparsed = parseLoadedFile(knownSource, url, alt.text, {
|
|
97
|
+
compile: options?.compile,
|
|
98
|
+
celEnv: this.celEnv,
|
|
99
|
+
});
|
|
100
|
+
this.fileCache.set(`${compileKey}:${knownSource}`, reparsed);
|
|
101
|
+
return reparsed;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
57
104
|
const { text, source } = await this.pick(url).read(url);
|
|
58
|
-
|
|
105
|
+
this.urlToSource.set(url, source);
|
|
106
|
+
// Also map the canonical source to itself so subsequent `loadFile`
|
|
107
|
+
// calls that already received a canonical URL — `kernel.load` passes
|
|
108
|
+
// the result of `resolveEntryPoint` to `loadGraph`, which then asks
|
|
109
|
+
// for that exact URL — hit the urlToSource fast path instead of
|
|
110
|
+
// falling through to a redundant `pick(url).read(url)`.
|
|
111
|
+
this.urlToSource.set(source, source);
|
|
112
|
+
const cacheKey = `${compileKey}:${source}`;
|
|
59
113
|
const cached = this.fileCache.get(cacheKey);
|
|
60
114
|
if (cached && cached.text === text)
|
|
61
115
|
return cached;
|
|
@@ -190,6 +244,15 @@ export class Loader {
|
|
|
190
244
|
}
|
|
191
245
|
return { rootSource, entry, modules, importEdges, errors };
|
|
192
246
|
}
|
|
247
|
+
/** Resolve an `import` URL against the file it appears in. Relative /
|
|
248
|
+
* absolute-path forms run through the owning `ManifestSource`'s
|
|
249
|
+
* `resolveRelative`; registry refs and full URLs pass through
|
|
250
|
+
* unchanged. Exposed so the import-controller (and any other
|
|
251
|
+
* caller-side resolver) lands on the *exact same* canonical URL the
|
|
252
|
+
* loader used when walking the entry graph — divergent resolution
|
|
253
|
+
* would silently break optimizations like `canonicalize()`-keyed
|
|
254
|
+
* cache hits whenever a non-trivial `ManifestSource.resolveRelative`
|
|
255
|
+
* is in play. */
|
|
193
256
|
resolveImportUrl(fromSource, importSource) {
|
|
194
257
|
if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
195
258
|
return this.pick(fromSource).resolveRelative(fromSource, importSource);
|
|
@@ -15,7 +15,12 @@ export interface DocumentPosition {
|
|
|
15
15
|
export declare function buildDocumentPositions(text: string, parsedDocs: Document[]): DocumentPosition[];
|
|
16
16
|
/** Line numbers (0-indexed) where each YAML document in a multi-doc file
|
|
17
17
|
* starts. The first document is always at line 0; subsequent entries point
|
|
18
|
-
* to the line after each `---`
|
|
18
|
+
* to the line after each `---` separator.
|
|
19
|
+
*
|
|
20
|
+
* A `---` at line 0 is the doc-start marker for doc 0 (the parser still
|
|
21
|
+
* emits a single document), not a separator before an empty doc — skipping
|
|
22
|
+
* it keeps `offsets.length === parsedDocs.length` so diagnostics for doc N
|
|
23
|
+
* don't land inside doc N-1's text. */
|
|
19
24
|
export declare function documentLineOffsets(text: string): number[];
|
|
20
25
|
/** Byte-offset → start-of-line lookup table. Index `i` is the byte offset of
|
|
21
26
|
* the first character on line `i`. Used with `offsetToPosition` to turn a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"position-metadata.d.ts","sourceRoot":"","sources":["../src/position-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,QAAQ,EAAE,MAAM,MAAM,CAAC;AACrE,OAAO,KAAK,EAAY,aAAa,EAAE,MAAM,YAAY,CAAC;AAE1D;;;;;oBAKoB;AAEpB,qFAAqF;AACrF,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,kEAAkE;AAClE,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,QAAQ,EAAE,GACrB,gBAAgB,EAAE,CAOpB;AAED
|
|
1
|
+
{"version":3,"file":"position-metadata.d.ts","sourceRoot":"","sources":["../src/position-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,QAAQ,EAAE,MAAM,MAAM,CAAC;AACrE,OAAO,KAAK,EAAY,aAAa,EAAE,MAAM,YAAY,CAAC;AAE1D;;;;;oBAKoB;AAEpB,qFAAqF;AACrF,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,kEAAkE;AAClE,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,QAAQ,EAAE,GACrB,gBAAgB,EAAE,CAOpB;AAED;;;;;;;wCAOwC;AACxC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAW1D;AAED;;kDAEkD;AAClD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAMvD;AAaD;;;;;+CAK+C;AAC/C,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,aAAa,CA0CtF"}
|
|
@@ -10,14 +10,22 @@ export function buildDocumentPositions(text, parsedDocs) {
|
|
|
10
10
|
}
|
|
11
11
|
/** Line numbers (0-indexed) where each YAML document in a multi-doc file
|
|
12
12
|
* starts. The first document is always at line 0; subsequent entries point
|
|
13
|
-
* to the line after each `---`
|
|
13
|
+
* to the line after each `---` separator.
|
|
14
|
+
*
|
|
15
|
+
* A `---` at line 0 is the doc-start marker for doc 0 (the parser still
|
|
16
|
+
* emits a single document), not a separator before an empty doc — skipping
|
|
17
|
+
* it keeps `offsets.length === parsedDocs.length` so diagnostics for doc N
|
|
18
|
+
* don't land inside doc N-1's text. */
|
|
14
19
|
export function documentLineOffsets(text) {
|
|
15
20
|
const offsets = [0];
|
|
16
21
|
const lines = text.split("\n");
|
|
17
22
|
for (let i = 0; i < lines.length; i++) {
|
|
18
23
|
const t = lines[i].trimEnd();
|
|
19
|
-
if (t === "---" || t.startsWith("--- "))
|
|
24
|
+
if (t === "---" || t.startsWith("--- ")) {
|
|
25
|
+
if (i === 0)
|
|
26
|
+
continue;
|
|
20
27
|
offsets.push(i + 1);
|
|
28
|
+
}
|
|
21
29
|
}
|
|
22
30
|
return offsets;
|
|
23
31
|
}
|
package/dist/precompile.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"precompile.d.ts","sourceRoot":"","sources":["../src/precompile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAIxD;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"precompile.d.ts","sourceRoot":"","sources":["../src/precompile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAIxD;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CA0CrE"}
|
package/dist/precompile.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isCompiledValue } from "@telorun/sdk";
|
|
2
|
-
import { compileString, defaultRegistry, isTaggedSentinel } from "@telorun/templating";
|
|
2
|
+
import { compileString, defaultRegistry, isRefSentinel, isTaggedSentinel } from "@telorun/templating";
|
|
3
3
|
/**
|
|
4
4
|
* Walks a raw YAML document and replaces all `${{ expr }}` strings (and
|
|
5
5
|
* `!cel`-tagged sentinels) with CompiledValue wrappers. Throws on CEL syntax
|
|
@@ -19,6 +19,14 @@ export function precompileDoc(doc, env) {
|
|
|
19
19
|
// analyzer's diagnostic walk can identify it on compiled trees too;
|
|
20
20
|
// engines returning plain values (e.g. `literal` → a string) pass through
|
|
21
21
|
// verbatim — the runtime contract is "any scalar value is fine."
|
|
22
|
+
// `!ref` sentinels are identity markers, not templating values. They must
|
|
23
|
+
// survive precompile intact so the analyzer's `resolveRefSentinels` pass
|
|
24
|
+
// can substitute them with `{kind, name}` objects against the resolved
|
|
25
|
+
// resource manifest. Running the engine's `compile` here would prematurely
|
|
26
|
+
// collapse the sentinel into its source string and the ref slot would
|
|
27
|
+
// arrive at the controller as a bare name with no kind.
|
|
28
|
+
if (isRefSentinel(doc))
|
|
29
|
+
return doc;
|
|
22
30
|
if (isTaggedSentinel(doc)) {
|
|
23
31
|
const engine = defaultRegistry().get(doc.engine);
|
|
24
32
|
if (!engine) {
|
|
@@ -98,7 +98,7 @@ export function buildReferenceFieldMap(schema) {
|
|
|
98
98
|
const map = new Map();
|
|
99
99
|
if (schema.properties) {
|
|
100
100
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
101
|
-
traverseNode(propSchema, key, map);
|
|
101
|
+
traverseNode(propSchema, key, map, schema);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
return map;
|
|
@@ -123,10 +123,23 @@ function collectRefs(node) {
|
|
|
123
123
|
* hid behind the schema-from indirection. */
|
|
124
124
|
export function buildFieldMapAtPath(schema, pathPrefix) {
|
|
125
125
|
const map = new Map();
|
|
126
|
-
traverseNode(schema, pathPrefix, map);
|
|
126
|
+
traverseNode(schema, pathPrefix, map, schema);
|
|
127
127
|
return map;
|
|
128
128
|
}
|
|
129
|
-
function traverseNode(node, path, map) {
|
|
129
|
+
function traverseNode(node, path, map, root, visitedRefs = new Set()) {
|
|
130
|
+
// Local `$ref` is intentionally NOT followed. Descending into shared
|
|
131
|
+
// `$defs` (notably `Run.Sequence`'s `step` definition) would surface
|
|
132
|
+
// ref slots like `steps[].invoke` that Phase 5 then injects live
|
|
133
|
+
// instances into; today's `Run.Sequence` controller calls
|
|
134
|
+
// `instance.invoke()` directly when handed an instance, bypassing
|
|
135
|
+
// the kernel's `runInvoke` emit-Invoked path. The walker fix and the
|
|
136
|
+
// dispatcher fix need to land together — see the follow-up in
|
|
137
|
+
// [kernel/nodejs/plans/reference-syntax-unification.md] and the
|
|
138
|
+
// stopgap in `resource-context.ts:resolveChildren`. `visitedRefs`
|
|
139
|
+
// stays as a parameter so the recursive calls below thread the right
|
|
140
|
+
// signature; turning the descent back on is a single-branch change.
|
|
141
|
+
if (typeof node?.$ref === "string")
|
|
142
|
+
return;
|
|
130
143
|
// Scope slot — record and stop; do not recurse into scope contents
|
|
131
144
|
if ("x-telo-scope" in node) {
|
|
132
145
|
map.set(path, { scope: node["x-telo-scope"] });
|
|
@@ -148,12 +161,32 @@ function traverseNode(node, path, map) {
|
|
|
148
161
|
}
|
|
149
162
|
// Array — recurse into items
|
|
150
163
|
if (node.type === "array" && node.items) {
|
|
151
|
-
traverseNode(node.items, path + "[]", map);
|
|
164
|
+
traverseNode(node.items, path + "[]", map, root, visitedRefs);
|
|
152
165
|
}
|
|
153
166
|
// Object — recurse into properties
|
|
154
167
|
if (node.properties) {
|
|
155
168
|
for (const [key, propSchema] of Object.entries(node.properties)) {
|
|
156
|
-
traverseNode(propSchema, `${path}.${key}`, map);
|
|
169
|
+
traverseNode(propSchema, `${path}.${key}`, map, root, visitedRefs);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Variant branches — descend into every alternative's properties / items.
|
|
173
|
+
// Schemas that discriminate on shape (Run.Sequence's step kinds:
|
|
174
|
+
// `oneOf: [{properties: {invoke}}, {properties: {try}}, ...]`) hide ref
|
|
175
|
+
// slots inside the branch. Walking each branch surfaces those slots into
|
|
176
|
+
// the field map so downstream passes (ref validation, sentinel
|
|
177
|
+
// resolution, dependency graph) cover them without a runtime fallback.
|
|
178
|
+
// The same field path may be added by multiple branches; the later
|
|
179
|
+
// assignment wins, which is fine — branches with the same field path
|
|
180
|
+
// share the same ref/context configuration (any divergence is already
|
|
181
|
+
// a schema bug).
|
|
182
|
+
for (const variantKey of ["oneOf", "anyOf", "allOf"]) {
|
|
183
|
+
const variants = node[variantKey];
|
|
184
|
+
if (!Array.isArray(variants))
|
|
185
|
+
continue;
|
|
186
|
+
for (const variant of variants) {
|
|
187
|
+
if (!variant || typeof variant !== "object")
|
|
188
|
+
continue;
|
|
189
|
+
traverseVariant(variant, path, map, root, visitedRefs);
|
|
157
190
|
}
|
|
158
191
|
}
|
|
159
192
|
// Map — `additionalProperties: { ... }` describes every value in an
|
|
@@ -162,6 +195,25 @@ function traverseNode(node, path, map) {
|
|
|
162
195
|
if (node.additionalProperties &&
|
|
163
196
|
typeof node.additionalProperties === "object" &&
|
|
164
197
|
!Array.isArray(node.additionalProperties)) {
|
|
165
|
-
traverseNode(node.additionalProperties, `${path}.{}`, map);
|
|
198
|
+
traverseNode(node.additionalProperties, `${path}.{}`, map, root, visitedRefs);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Walk a single variant of a `oneOf` / `anyOf` / `allOf` branch. Only
|
|
202
|
+
* the properties / items / map slots are followed — collectRefs at the
|
|
203
|
+
* variant root is handled by the parent's `collectRefs(node)` already
|
|
204
|
+
* (anyOf of x-telo-ref branches is the canonical multi-ref shape). */
|
|
205
|
+
function traverseVariant(variant, path, map, root, visitedRefs = new Set()) {
|
|
206
|
+
if (variant.properties) {
|
|
207
|
+
for (const [key, propSchema] of Object.entries(variant.properties)) {
|
|
208
|
+
traverseNode(propSchema, `${path}.${key}`, map, root, visitedRefs);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (variant.type === "array" && variant.items) {
|
|
212
|
+
traverseNode(variant.items, path + "[]", map, root, visitedRefs);
|
|
213
|
+
}
|
|
214
|
+
if (variant.additionalProperties &&
|
|
215
|
+
typeof variant.additionalProperties === "object" &&
|
|
216
|
+
!Array.isArray(variant.additionalProperties)) {
|
|
217
|
+
traverseNode(variant.additionalProperties, `${path}.{}`, map, root, visitedRefs);
|
|
166
218
|
}
|
|
167
219
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
|
+
/**
|
|
5
|
+
* Walks every `x-telo-ref` slot in every non-system resource and rewrites
|
|
6
|
+
* `!ref <name>` sentinels in-place to `{kind: <resolved-kind>, name}`.
|
|
7
|
+
*
|
|
8
|
+
* The downstream pipeline (inline normalization, dependency graph, kernel
|
|
9
|
+
* controllers) expects every ref-slot value to be either a `{kind, name}`
|
|
10
|
+
* object, an inline-definition object, or a legacy bare string — resolving
|
|
11
|
+
* sentinels here keeps that contract intact so each consumer doesn't need
|
|
12
|
+
* its own sentinel branch.
|
|
13
|
+
*
|
|
14
|
+
* The walker assigns `kind` by name lookup (resource names are unique
|
|
15
|
+
* within a manifest scope). When the name doesn't resolve in the local
|
|
16
|
+
* `byName` map, the sentinel is left in place so `validateReferences`
|
|
17
|
+
* can emit the `UNRESOLVED_REFERENCE` diagnostic with full context.
|
|
18
|
+
*
|
|
19
|
+
* Mutation strategy: the field-path walker descends the resource tree
|
|
20
|
+
* directly and replaces the sentinel on its parent container. Re-parsing
|
|
21
|
+
* a string-encoded concrete path (the earlier shape) coupled the writer
|
|
22
|
+
* to the path-encoding rules of `resolveFieldEntries` — any new path
|
|
23
|
+
* marker would silently break this writer. Descending directly avoids
|
|
24
|
+
* that coupling.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveRefSentinels(resources: ResourceManifest[], registry: DefinitionRegistry, aliases?: AliasResolver, aliasesByModule?: Map<string, AliasResolver>): void;
|
|
27
|
+
//# sourceMappingURL=resolve-ref-sentinels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-ref-sentinels.d.ts","sourceRoot":"","sources":["../src/resolve-ref-sentinels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAInE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC3C,IAAI,CAsBN"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
2
|
+
import { isRefEntry } from "./reference-field-map.js";
|
|
3
|
+
import { REF_RESOLUTION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
4
|
+
/**
|
|
5
|
+
* Walks every `x-telo-ref` slot in every non-system resource and rewrites
|
|
6
|
+
* `!ref <name>` sentinels in-place to `{kind: <resolved-kind>, name}`.
|
|
7
|
+
*
|
|
8
|
+
* The downstream pipeline (inline normalization, dependency graph, kernel
|
|
9
|
+
* controllers) expects every ref-slot value to be either a `{kind, name}`
|
|
10
|
+
* object, an inline-definition object, or a legacy bare string — resolving
|
|
11
|
+
* sentinels here keeps that contract intact so each consumer doesn't need
|
|
12
|
+
* its own sentinel branch.
|
|
13
|
+
*
|
|
14
|
+
* The walker assigns `kind` by name lookup (resource names are unique
|
|
15
|
+
* within a manifest scope). When the name doesn't resolve in the local
|
|
16
|
+
* `byName` map, the sentinel is left in place so `validateReferences`
|
|
17
|
+
* can emit the `UNRESOLVED_REFERENCE` diagnostic with full context.
|
|
18
|
+
*
|
|
19
|
+
* Mutation strategy: the field-path walker descends the resource tree
|
|
20
|
+
* directly and replaces the sentinel on its parent container. Re-parsing
|
|
21
|
+
* a string-encoded concrete path (the earlier shape) coupled the writer
|
|
22
|
+
* to the path-encoding rules of `resolveFieldEntries` — any new path
|
|
23
|
+
* marker would silently break this writer. Descending directly avoids
|
|
24
|
+
* that coupling.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveRefSentinels(resources, registry, aliases, aliasesByModule) {
|
|
27
|
+
const byName = new Map();
|
|
28
|
+
for (const r of resources) {
|
|
29
|
+
if (r.metadata?.name && !SYSTEM_KINDS.has(r.kind)) {
|
|
30
|
+
byName.set(r.metadata.name, r);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const r of resources) {
|
|
34
|
+
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind))
|
|
35
|
+
continue;
|
|
36
|
+
const fieldMap = aliases && aliasesByModule
|
|
37
|
+
? registry.expandedFieldMapForResource(r, aliases, aliasesByModule)
|
|
38
|
+
: registry.getFieldMapForKind(r.kind, aliases);
|
|
39
|
+
if (!fieldMap)
|
|
40
|
+
continue;
|
|
41
|
+
for (const [fieldPath, entry] of fieldMap) {
|
|
42
|
+
if (!isRefEntry(entry))
|
|
43
|
+
continue;
|
|
44
|
+
replaceSentinelsAtPath(r, fieldPath, byName);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Walks `obj` along `fieldPath` (dot notation with `[]` for arrays and
|
|
49
|
+
* `{}` for additionalProperties-typed maps) and replaces any `!ref`
|
|
50
|
+
* sentinel value at the terminal slot with `{kind, name}` looked up
|
|
51
|
+
* via `byName`. Mutates the parent container in place; no string-path
|
|
52
|
+
* round-trip. */
|
|
53
|
+
function replaceSentinelsAtPath(obj, fieldPath, byName) {
|
|
54
|
+
const parts = fieldPath.split(".");
|
|
55
|
+
descend(obj, parts, byName);
|
|
56
|
+
}
|
|
57
|
+
function descend(obj, parts, byName) {
|
|
58
|
+
if (obj == null || typeof obj !== "object" || parts.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
const [head, ...rest] = parts;
|
|
61
|
+
// Map iteration: descend into every value of the current object.
|
|
62
|
+
if (head === "{}") {
|
|
63
|
+
const container = obj;
|
|
64
|
+
for (const key of Object.keys(container)) {
|
|
65
|
+
const child = container[key];
|
|
66
|
+
if (rest.length === 0) {
|
|
67
|
+
if (isRefSentinel(child)) {
|
|
68
|
+
const target = byName.get(child.source);
|
|
69
|
+
if (target)
|
|
70
|
+
container[key] = { kind: target.kind, name: child.source };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
descend(child, rest, byName);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const isArr = head.endsWith("[]");
|
|
80
|
+
const key = isArr ? head.slice(0, -2) : head;
|
|
81
|
+
const container = obj;
|
|
82
|
+
const val = container[key];
|
|
83
|
+
if (val == null)
|
|
84
|
+
return;
|
|
85
|
+
if (isArr) {
|
|
86
|
+
if (!Array.isArray(val))
|
|
87
|
+
return;
|
|
88
|
+
for (let i = 0; i < val.length; i++) {
|
|
89
|
+
if (rest.length === 0) {
|
|
90
|
+
const elem = val[i];
|
|
91
|
+
if (isRefSentinel(elem)) {
|
|
92
|
+
const target = byName.get(elem.source);
|
|
93
|
+
if (target)
|
|
94
|
+
val[i] = { kind: target.kind, name: elem.source };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
descend(val[i], rest, byName);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
if (rest.length === 0) {
|
|
104
|
+
if (isRefSentinel(val)) {
|
|
105
|
+
const target = byName.get(val.source);
|
|
106
|
+
if (target)
|
|
107
|
+
container[key] = { kind: target.kind, name: val.source };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
descend(val, rest, byName);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|