@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/dist/schema-compat.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
declare const Ajv: any;
|
|
2
2
|
/** Creates a configured AJV instance (allErrors, strict: false, with formats).
|
|
3
|
-
*
|
|
3
|
+
* Also registers the kernel manifest root schema under `telo://manifest` so
|
|
4
|
+
* module YAMLs can `$ref` into the shared `$defs/ResourceRef` (and any future
|
|
5
|
+
* shared fragments) from this analyzer's AJV without each module having to
|
|
6
|
+
* bundle its own copy.
|
|
7
|
+
*
|
|
8
|
+
* Called once for the module-level instance and once per
|
|
9
|
+
* DefinitionRegistry instance. */
|
|
4
10
|
export declare function createAjv(): InstanceType<typeof Ajv>;
|
|
5
11
|
export interface CompatibilityResult {
|
|
6
12
|
compatible: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,GAAG,KAA0C,CAAC;AAEpD;
|
|
1
|
+
{"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,GAAG,KAA0C,CAAC;AAEpD;;;;;;;mCAOmC;AACnC,wBAAgB,SAAS,IAAI,YAAY,CAAC,OAAO,GAAG,CAAC,CAOpD;AAKD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;oEAEoE;AACpE,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,mBAAmB,CAIrB;AAiDD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAelD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAGxE;AAuBD,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,0GAA0G;AAC1G,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,WAAW,EAAE,CAe/F;AAED;qFACqF;AACrF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ7E;AAED;;;;6DAI6D;AAC7D,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAsBjC;AAED,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,MAAM,CAuBnF;AAED,wFAAwF;AACxF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAqBhG;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAiB5E;AA2BD;iGACiG;AACjG,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC/B,OAAO,CAqCT"}
|
package/dist/schema-compat.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import AjvModule from "ajv";
|
|
2
2
|
import addFormats from "ajv-formats";
|
|
3
|
-
import { isTaggedSentinel } from "@telorun/templating";
|
|
3
|
+
import { isRefSentinel, isTaggedSentinel, ManifestRootSchema } from "@telorun/templating";
|
|
4
4
|
const Ajv = AjvModule.default ?? AjvModule;
|
|
5
5
|
/** Creates a configured AJV instance (allErrors, strict: false, with formats).
|
|
6
|
-
*
|
|
6
|
+
* Also registers the kernel manifest root schema under `telo://manifest` so
|
|
7
|
+
* module YAMLs can `$ref` into the shared `$defs/ResourceRef` (and any future
|
|
8
|
+
* shared fragments) from this analyzer's AJV without each module having to
|
|
9
|
+
* bundle its own copy.
|
|
10
|
+
*
|
|
11
|
+
* Called once for the module-level instance and once per
|
|
12
|
+
* DefinitionRegistry instance. */
|
|
7
13
|
export function createAjv() {
|
|
8
14
|
const instance = new Ajv({ allErrors: true, strict: false });
|
|
9
15
|
addFormats.default
|
|
10
16
|
? addFormats.default(instance)
|
|
11
17
|
: addFormats(instance);
|
|
18
|
+
instance.addSchema(ManifestRootSchema);
|
|
12
19
|
return instance;
|
|
13
20
|
}
|
|
14
21
|
const ajv = createAjv();
|
|
@@ -270,6 +277,16 @@ export function substituteCelFields(data, schema, rootSchema) {
|
|
|
270
277
|
if (typeof data === "string" && CEL_PURE_RE.test(data)) {
|
|
271
278
|
return celPlaceholderForSchema(resolved);
|
|
272
279
|
}
|
|
280
|
+
// `!ref <name>` sentinels are identity markers, not runtime values —
|
|
281
|
+
// schemas that opt into `$ref: "telo://manifest#/$defs/ResourceRef"`
|
|
282
|
+
// (or `anyOf` it alongside other shapes) need the actual sentinel
|
|
283
|
+
// object so AJV validates it against ResourceRefSchema. Collapsing it
|
|
284
|
+
// to a CEL placeholder would either fail the schema (when the slot
|
|
285
|
+
// expects the ResourceRef shape) or mask validation errors (when the
|
|
286
|
+
// slot expects something else entirely).
|
|
287
|
+
if (isRefSentinel(data)) {
|
|
288
|
+
return data;
|
|
289
|
+
}
|
|
273
290
|
if (isTaggedSentinel(data)) {
|
|
274
291
|
return celPlaceholderForSchema(resolved);
|
|
275
292
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Resource-kind sets used by analysis passes to decide what counts as a
|
|
2
|
+
* user-defined instance vs. a system-level blueprint. Pulled into one
|
|
3
|
+
* place so the three passes (reference validation, dependency graph,
|
|
4
|
+
* ref-sentinel resolution) don't drift; each pass exports its own
|
|
5
|
+
* scoped view with a comment explaining what's in and what's out. */
|
|
6
|
+
/** Skipped by reference validation: type blueprints whose own ref slots
|
|
7
|
+
* belong to a different phase (definition schema validation rather than
|
|
8
|
+
* per-resource validation). Telo.Application and Telo.Library
|
|
9
|
+
* intentionally fall through — Application has `targets` (real refs) and
|
|
10
|
+
* Library is harmless (no ref-bearing fields). Telo.Import is also
|
|
11
|
+
* intentionally not skipped — its `source` is not an x-telo-ref slot, so
|
|
12
|
+
* walking it is cheap and consistent. */
|
|
13
|
+
export declare const REF_VALIDATION_SKIP_KINDS: ReadonlySet<string>;
|
|
14
|
+
/** Excluded from the dependency graph: kinds that are not runtime nodes.
|
|
15
|
+
* Telo.Abstract is intentionally not in this set today — abstracts have
|
|
16
|
+
* no resource manifests, so they never reach graph construction; if
|
|
17
|
+
* that ever changes, add it explicitly. */
|
|
18
|
+
export declare const DEPENDENCY_GRAPH_SKIP_KINDS: ReadonlySet<string>;
|
|
19
|
+
/** Skipped by `!ref` sentinel resolution: kinds whose bodies are
|
|
20
|
+
* blueprints or import-time metadata, not resource instances with
|
|
21
|
+
* user-referenced ref slots. Mirrors `REF_VALIDATION_SKIP_KINDS` but
|
|
22
|
+
* also drops Telo.Import (its `source` isn't a ref slot, and walking
|
|
23
|
+
* the field map on it is pointless since there's no registered kind). */
|
|
24
|
+
export declare const REF_RESOLUTION_SKIP_KINDS: ReadonlySet<string>;
|
|
25
|
+
//# sourceMappingURL=system-kinds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-kinds.d.ts","sourceRoot":"","sources":["../src/system-kinds.ts"],"names":[],"mappings":"AAAA;;;;sEAIsE;AAEtE;;;;;;0CAM0C;AAC1C,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAGxD,CAAC;AAEH;;;4CAG4C;AAC5C,eAAO,MAAM,2BAA2B,EAAE,WAAW,CAAC,MAAM,CAG1D,CAAC;AAEH;;;;0EAI0E;AAC1E,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAIxD,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Resource-kind sets used by analysis passes to decide what counts as a
|
|
2
|
+
* user-defined instance vs. a system-level blueprint. Pulled into one
|
|
3
|
+
* place so the three passes (reference validation, dependency graph,
|
|
4
|
+
* ref-sentinel resolution) don't drift; each pass exports its own
|
|
5
|
+
* scoped view with a comment explaining what's in and what's out. */
|
|
6
|
+
/** Skipped by reference validation: type blueprints whose own ref slots
|
|
7
|
+
* belong to a different phase (definition schema validation rather than
|
|
8
|
+
* per-resource validation). Telo.Application and Telo.Library
|
|
9
|
+
* intentionally fall through — Application has `targets` (real refs) and
|
|
10
|
+
* Library is harmless (no ref-bearing fields). Telo.Import is also
|
|
11
|
+
* intentionally not skipped — its `source` is not an x-telo-ref slot, so
|
|
12
|
+
* walking it is cheap and consistent. */
|
|
13
|
+
export const REF_VALIDATION_SKIP_KINDS = new Set([
|
|
14
|
+
"Telo.Definition",
|
|
15
|
+
"Telo.Abstract",
|
|
16
|
+
]);
|
|
17
|
+
/** Excluded from the dependency graph: kinds that are not runtime nodes.
|
|
18
|
+
* Telo.Abstract is intentionally not in this set today — abstracts have
|
|
19
|
+
* no resource manifests, so they never reach graph construction; if
|
|
20
|
+
* that ever changes, add it explicitly. */
|
|
21
|
+
export const DEPENDENCY_GRAPH_SKIP_KINDS = new Set([
|
|
22
|
+
"Telo.Definition",
|
|
23
|
+
"Telo.Import",
|
|
24
|
+
]);
|
|
25
|
+
/** Skipped by `!ref` sentinel resolution: kinds whose bodies are
|
|
26
|
+
* blueprints or import-time metadata, not resource instances with
|
|
27
|
+
* user-referenced ref slots. Mirrors `REF_VALIDATION_SKIP_KINDS` but
|
|
28
|
+
* also drops Telo.Import (its `source` isn't a ref slot, and walking
|
|
29
|
+
* the field map on it is pointless since there's no registered kind). */
|
|
30
|
+
export const REF_RESOLUTION_SKIP_KINDS = new Set([
|
|
31
|
+
"Telo.Definition",
|
|
32
|
+
"Telo.Abstract",
|
|
33
|
+
"Telo.Import",
|
|
34
|
+
]);
|
package/dist/types.d.ts
CHANGED
|
@@ -73,6 +73,18 @@ export interface LoaderInitOptions {
|
|
|
73
73
|
}
|
|
74
74
|
export interface AnalysisOptions {
|
|
75
75
|
strictContexts?: boolean;
|
|
76
|
+
/** When true, `analyze()` runs the state-mutating setup (module identity /
|
|
77
|
+
* alias / definition registration plus `normalizeInlineResources`) but
|
|
78
|
+
* skips every diagnostic-producing pass — per-resource validation, the
|
|
79
|
+
* Library `env:` check, `validateExtends`, `validateProviderCoherence`,
|
|
80
|
+
* and `validateThrowsCoverage`. Used by the kernel when a previous load
|
|
81
|
+
* has already stamped the manifest set as valid (by content hash), so
|
|
82
|
+
* the registry still gets populated without paying the validation walk
|
|
83
|
+
* on every cold start. The caller takes responsibility for the
|
|
84
|
+
* correctness guarantee — pass this only when something durable
|
|
85
|
+
* (on-disk stamp) attests that the manifests passed a real analyze
|
|
86
|
+
* pass at the same analyzer / kernel version. */
|
|
87
|
+
skipValidation?: boolean;
|
|
76
88
|
}
|
|
77
89
|
/** Pre-seeded state for incremental analysis. Passed to StaticAnalyzer.analyze() so it does
|
|
78
90
|
* not rebuild from scratch on every call. The provided instances are mutated — new definitions
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;qEAEiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;qEAEiE;IACjE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,YAAY,CAAC,EAAE,cAAc,EAAE,CAAC;IAChC,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,yDAAyD;IACzD,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;6FACyF;IACzF,WAAW,CAAC,EAAE,OAAO,sBAAsB,EAAE,WAAW,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;IACpE;;;;+EAI2E;IAC3E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC,CAAC;CAC5E"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;qEAEiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;qEAEiE;IACjE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,YAAY,CAAC,EAAE,cAAc,EAAE,CAAC;IAChC,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,yDAAyD;IACzD,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;6FACyF;IACzF,WAAW,CAAC,EAAE,OAAO,sBAAsB,EAAE,WAAW,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;;;sDAUkD;IAClD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;IACpE;;;;+EAI2E;IAC3E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC,CAAC;CAC5E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA4C/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CA2WtB"}
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
1
2
|
import { isRefEntry, isScopeEntry, isSchemaFromEntry, isInlineResource, resolveFieldEntries, resolveFieldValues } from "./reference-field-map.js";
|
|
2
3
|
import { navigateJsonPointer } from "./schema-compat.js";
|
|
4
|
+
import { REF_VALIDATION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
3
5
|
import { DiagnosticSeverity } from "./types.js";
|
|
4
6
|
const SOURCE = "telo-analyzer";
|
|
5
|
-
/** Kinds skipped by reference validation. Telo.Application and Telo.Library
|
|
6
|
-
* are intentionally not here: Application has `targets` with x-telo-ref that
|
|
7
|
-
* must be validated, and Library has no ref-bearing fields so flows through
|
|
8
|
-
* harmlessly. Telo.Import is also not here for the same reason — its
|
|
9
|
-
* `source` field isn't x-telo-ref, so nothing gets checked. */
|
|
10
|
-
const SYSTEM_KINDS = new Set(["Telo.Definition", "Telo.Abstract"]);
|
|
11
7
|
/**
|
|
12
8
|
* Checks whether `kind` satisfies the ref constraint in `entry`.
|
|
13
9
|
* Returns an empty array when valid, or mismatch error strings when not.
|
|
@@ -122,6 +118,36 @@ export function validateReferences(resources, context) {
|
|
|
122
118
|
for (const { value: val, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
|
|
123
119
|
if (!val)
|
|
124
120
|
continue;
|
|
121
|
+
// `!ref <name>` sentinel — bare resource name marked at parse time as a
|
|
122
|
+
// reference. Look it up against the slot's x-telo-ref constraint exactly
|
|
123
|
+
// like the legacy bare-string path; the only difference is the value's
|
|
124
|
+
// shape (a TaggedSentinel rather than a raw string), which removed the
|
|
125
|
+
// string/inline ambiguity at the source.
|
|
126
|
+
if (isRefSentinel(val)) {
|
|
127
|
+
const refName = val.source;
|
|
128
|
+
const target = byName.get(refName) ?? visibleScopeManifests.find((m) => m.metadata?.name === refName);
|
|
129
|
+
if (!target) {
|
|
130
|
+
diagnostics.push({
|
|
131
|
+
severity: DiagnosticSeverity.Error,
|
|
132
|
+
code: "UNRESOLVED_REFERENCE",
|
|
133
|
+
source: SOURCE,
|
|
134
|
+
message: `${resourceLabel}: reference at '${concretePath}' → resource '${refName}' not found`,
|
|
135
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const kindErrors = checkKind(target.kind, entry, registry, aliases);
|
|
140
|
+
if (kindErrors.length > 0) {
|
|
141
|
+
diagnostics.push({
|
|
142
|
+
severity: DiagnosticSeverity.Error,
|
|
143
|
+
code: "REFERENCE_KIND_MISMATCH",
|
|
144
|
+
source: SOURCE,
|
|
145
|
+
message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
|
|
146
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
125
151
|
// Name-only reference (plain string) — look up by name to validate.
|
|
126
152
|
// Qualified references use "Kind.Name" format (e.g. "Http.Api.PaymentApi");
|
|
127
153
|
// extract the resource name from the last dot segment.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"bun": "./src/index.ts",
|
|
30
30
|
"import": "./dist/index.js",
|
|
31
31
|
"default": "./dist/index.js"
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
33
34
|
},
|
|
34
35
|
"files": [
|
|
35
36
|
"dist/**",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"ajv-formats": "^3.0.1",
|
|
42
43
|
"jsonpath-plus": "^10.3.0",
|
|
43
44
|
"yaml": "^2.8.3",
|
|
44
|
-
"@telorun/templating": "1.
|
|
45
|
+
"@telorun/templating": "1.1.0"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/node": "^20.0.0",
|
package/src/analyzer.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kerne
|
|
|
14
14
|
import { computeSuggestKind } from "./kind-suggest.js";
|
|
15
15
|
import { isModuleKind } from "./module-kinds.js";
|
|
16
16
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
17
|
+
import { resolveRefSentinels } from "./resolve-ref-sentinels.js";
|
|
17
18
|
import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
|
|
18
19
|
import {
|
|
19
20
|
celTypeSatisfiesJsonSchema,
|
|
@@ -623,6 +624,22 @@ export class StaticAnalyzer {
|
|
|
623
624
|
// Phase 2: extract inline resources from x-telo-ref slots into first-class manifests
|
|
624
625
|
const allManifests = normalizeInlineResources(manifests, defs, aliases, aliasesByModule);
|
|
625
626
|
|
|
627
|
+
// Phase 2.5: resolve `!ref <name>` sentinels at every ref slot to canonical
|
|
628
|
+
// {kind, name} objects so downstream phases (validation, dependency graph,
|
|
629
|
+
// kernel controllers) see a uniform shape. Runs after normalize so both
|
|
630
|
+
// original and inline-extracted manifests have their sentinels resolved.
|
|
631
|
+
resolveRefSentinels(allManifests, defs, aliases, aliasesByModule);
|
|
632
|
+
|
|
633
|
+
// Trusted-input fast path: when the caller has already attested that
|
|
634
|
+
// this exact manifest set passes analysis (e.g. via the kernel's
|
|
635
|
+
// hash-stamped `.validated.json` cache), skip the validation walk.
|
|
636
|
+
// Registration of identities / aliases / definitions and inline-resource
|
|
637
|
+
// normalisation have already run above; that's all downstream
|
|
638
|
+
// consumers (prepare, init loop) require.
|
|
639
|
+
if (options?.skipValidation) {
|
|
640
|
+
return diagnostics;
|
|
641
|
+
}
|
|
642
|
+
|
|
626
643
|
// Build a name→manifest map for looking up referenced resources
|
|
627
644
|
const byName = new Map<string, ResourceManifest>();
|
|
628
645
|
for (const m of allManifests) {
|
|
@@ -972,12 +989,17 @@ export class StaticAnalyzer {
|
|
|
972
989
|
|
|
973
990
|
normalize(manifests: ResourceManifest[], registry: AnalysisRegistry): ResourceManifest[] {
|
|
974
991
|
const ctx = registry._context();
|
|
975
|
-
|
|
992
|
+
const normalized = normalizeInlineResources(
|
|
976
993
|
manifests,
|
|
977
994
|
ctx.definitions!,
|
|
978
995
|
ctx.aliases,
|
|
979
996
|
ctx.aliasesByModule,
|
|
980
997
|
);
|
|
998
|
+
// Resolve !ref sentinels after normalize so both the original and
|
|
999
|
+
// inline-extracted manifests get their refs canonicalized to
|
|
1000
|
+
// {kind, name} for the kernel that consumes this output.
|
|
1001
|
+
resolveRefSentinels(normalized, ctx.definitions!, ctx.aliases, ctx.aliasesByModule);
|
|
1002
|
+
return normalized;
|
|
981
1003
|
}
|
|
982
1004
|
|
|
983
1005
|
prepare(
|
package/src/builtins.ts
CHANGED
|
@@ -220,6 +220,20 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
220
220
|
anyOf: [
|
|
221
221
|
{ type: "string", "x-telo-ref": "telo#Runnable" },
|
|
222
222
|
{ type: "string", "x-telo-ref": "telo#Service" },
|
|
223
|
+
// Post-resolution shape that `resolveRefSentinels`
|
|
224
|
+
// substitutes a `!ref <name>` sentinel into. The
|
|
225
|
+
// adjacent `x-telo-ref` constraints govern the kind
|
|
226
|
+
// check; this branch only admits the structural form so
|
|
227
|
+
// AJV doesn't reject a resolved ref.
|
|
228
|
+
{
|
|
229
|
+
type: "object",
|
|
230
|
+
required: ["kind", "name"],
|
|
231
|
+
properties: {
|
|
232
|
+
kind: { type: "string" },
|
|
233
|
+
name: { type: "string" },
|
|
234
|
+
},
|
|
235
|
+
additionalProperties: true,
|
|
236
|
+
},
|
|
223
237
|
],
|
|
224
238
|
},
|
|
225
239
|
},
|
package/src/dependency-graph.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
2
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
5
|
import { isRefEntry, isScopeEntry, resolveFieldValues } from "./reference-field-map.js";
|
|
6
|
+
import { DEPENDENCY_GRAPH_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
5
7
|
|
|
6
8
|
export interface ResourceNode {
|
|
7
9
|
kind: string;
|
|
@@ -17,16 +19,6 @@ export interface DependencyGraph {
|
|
|
17
19
|
cycle?: ReadonlyArray<ResourceNode>;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
/** System resource kinds that are not runtime nodes in the dependency graph.
|
|
21
|
-
* Module-identity docs (Telo.Application, Telo.Library) are intentionally
|
|
22
|
-
* not in this set: an Application's `targets` use `x-telo-ref` to real
|
|
23
|
-
* Runnable/Service resources, so the Application legitimately depends on
|
|
24
|
-
* them in boot order — modeling that as a graph edge is correct. A Library
|
|
25
|
-
* has no `targets`, so it becomes a zero-edge node, which is harmless.
|
|
26
|
-
* If the graph is ever consumed as "things to init", skip these kinds at
|
|
27
|
-
* the consumer site; the controller already runs them separately. */
|
|
28
|
-
const SYSTEM_KINDS = new Set(["Telo.Definition", "Telo.Import"]);
|
|
29
|
-
|
|
30
22
|
const nodeKey = (kind: string, name: string) => `${kind}\0${name}`;
|
|
31
23
|
|
|
32
24
|
/**
|
|
@@ -47,12 +39,18 @@ export function buildDependencyGraph(
|
|
|
47
39
|
aliases?: AliasResolver,
|
|
48
40
|
aliasesByModule?: Map<string, AliasResolver>,
|
|
49
41
|
): DependencyGraph {
|
|
50
|
-
// --- Build node set ---
|
|
42
|
+
// --- Build node set + name index ---
|
|
51
43
|
const nodes = new Map<string, ResourceNode>();
|
|
44
|
+
// Sentinel lookup (`!ref <name>`) needs to resolve a bare name to its
|
|
45
|
+
// declared kind. Names are unique within a manifest scope, so a flat
|
|
46
|
+
// map suffices and lets the sentinel branch below avoid a full
|
|
47
|
+
// O(N) scan of the node set on every reference.
|
|
48
|
+
const nodesByName = new Map<string, ResourceNode>();
|
|
52
49
|
for (const r of resources) {
|
|
53
50
|
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind)) continue;
|
|
54
|
-
const
|
|
55
|
-
nodes.set(
|
|
51
|
+
const node = { kind: r.kind, name: r.metadata.name as string };
|
|
52
|
+
nodes.set(nodeKey(node.kind, node.name), node);
|
|
53
|
+
nodesByName.set(node.name, node);
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
// --- Build adjacency: from → deps (from depends on dep) ---
|
|
@@ -90,7 +88,22 @@ export function buildDependencyGraph(
|
|
|
90
88
|
if (!isRefEntry(entry)) continue;
|
|
91
89
|
|
|
92
90
|
for (const val of resolveFieldValues(r, fieldPath)) {
|
|
93
|
-
if (!val
|
|
91
|
+
if (!val) continue;
|
|
92
|
+
|
|
93
|
+
// `!ref <name>` sentinel — look up the target's kind from the
|
|
94
|
+
// name (resources are unique by name) so the edge carries the
|
|
95
|
+
// concrete kind, matching the {kind, name} edge shape below.
|
|
96
|
+
if (isRefSentinel(val)) {
|
|
97
|
+
const refName = val.source;
|
|
98
|
+
if (scopedNames.has(refName)) continue;
|
|
99
|
+
const node = nodesByName.get(refName);
|
|
100
|
+
if (node) {
|
|
101
|
+
deps.get(sourceKey)!.add(nodeKey(node.kind, node.name));
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof val !== "object") continue;
|
|
94
107
|
const ref = val as Record<string, unknown>;
|
|
95
108
|
if (!ref.kind || !ref.name) continue;
|
|
96
109
|
// Edges to scoped resources are runtime deps, not boot-time deps — exclude from DAG
|
package/src/manifest-loader.ts
CHANGED
|
@@ -33,6 +33,14 @@ export class Loader {
|
|
|
33
33
|
* get distinct entries, so neither sees the wrong manifest tree. */
|
|
34
34
|
private readonly fileCache = new Map<string, LoadedFile>();
|
|
35
35
|
|
|
36
|
+
/** requestUrl → canonical `source`. Lets `loadFile` skip the source read
|
|
37
|
+
* when a URL it has already canonicalised is requested again — kernel
|
|
38
|
+
* load → boot and the import-controller each ask the loader for the same
|
|
39
|
+
* modules. Without this fast path every duplicate request re-runs the
|
|
40
|
+
* source's `read()` (a `fetch` for `RegistrySource`, a disk read for
|
|
41
|
+
* `LocalFileSource`). */
|
|
42
|
+
private readonly urlToSource = new Map<string, string>();
|
|
43
|
+
|
|
36
44
|
protected sources: ManifestSource[];
|
|
37
45
|
private readonly celEnv: Environment;
|
|
38
46
|
|
|
@@ -67,8 +75,22 @@ export class Loader {
|
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
async resolveEntryPoint(url: string): Promise<string> {
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
// Route through `loadFile` so the resolved source URL and parsed
|
|
79
|
+
// entry are populated in `urlToSource` + `fileCache` in one read.
|
|
80
|
+
// Callers (kernel.load) immediately call `loadGraph(entryUrl)`
|
|
81
|
+
// afterwards — without this priming, the entry file would be read
|
|
82
|
+
// twice (twice over the network for `RegistrySource`).
|
|
83
|
+
const file = await this.loadFile(url);
|
|
84
|
+
return file.source;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Returns the canonical source URL the loader has already mapped `url`
|
|
88
|
+
* to during a prior `loadFile`/`loadModule`/`loadGraph` call, or
|
|
89
|
+
* `undefined` when the URL has not been seen. Callers use it to test
|
|
90
|
+
* set-membership against a previous graph walk's modules without
|
|
91
|
+
* triggering an extra source read. */
|
|
92
|
+
canonicalize(url: string): string | undefined {
|
|
93
|
+
return this.urlToSource.get(url);
|
|
72
94
|
}
|
|
73
95
|
|
|
74
96
|
// --- New API: returns LoadedFile / LoadedModule / LoadedGraph ----------
|
|
@@ -78,8 +100,42 @@ export class Loader {
|
|
|
78
100
|
* private mutable copy must call `parseLoadedFile` directly with the
|
|
79
101
|
* LoadedFile's `text`. */
|
|
80
102
|
async loadFile(url: string, options?: LoadOptions): Promise<LoadedFile> {
|
|
103
|
+
const compileKey = options?.compile ? "compiled" : "raw";
|
|
104
|
+
const knownSource = this.urlToSource.get(url);
|
|
105
|
+
if (knownSource) {
|
|
106
|
+
const cached = this.fileCache.get(`${compileKey}:${knownSource}`);
|
|
107
|
+
if (cached) return cached;
|
|
108
|
+
// The other compile-mode entry is cached — reparse from its text
|
|
109
|
+
// instead of re-reading the source.
|
|
110
|
+
//
|
|
111
|
+
// NOTE for watch-mode reactivation (cli/nodejs/src/commands/run.ts
|
|
112
|
+
// currently has `setupWatchMode` commented out): this branch
|
|
113
|
+
// assumes file contents don't change underneath a single Loader.
|
|
114
|
+
// Reviving watch mode will need a public `invalidate(url)` (or
|
|
115
|
+
// similar) that drops both `urlToSource[url]` and the cached
|
|
116
|
+
// entries for its canonical source before the loader serves the
|
|
117
|
+
// file again.
|
|
118
|
+
const altKey = `${compileKey === "compiled" ? "raw" : "compiled"}:${knownSource}`;
|
|
119
|
+
const alt = this.fileCache.get(altKey);
|
|
120
|
+
if (alt) {
|
|
121
|
+
const reparsed = parseLoadedFile(knownSource, url, alt.text, {
|
|
122
|
+
compile: options?.compile,
|
|
123
|
+
celEnv: this.celEnv,
|
|
124
|
+
});
|
|
125
|
+
this.fileCache.set(`${compileKey}:${knownSource}`, reparsed);
|
|
126
|
+
return reparsed;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
81
130
|
const { text, source } = await this.pick(url).read(url);
|
|
82
|
-
|
|
131
|
+
this.urlToSource.set(url, source);
|
|
132
|
+
// Also map the canonical source to itself so subsequent `loadFile`
|
|
133
|
+
// calls that already received a canonical URL — `kernel.load` passes
|
|
134
|
+
// the result of `resolveEntryPoint` to `loadGraph`, which then asks
|
|
135
|
+
// for that exact URL — hit the urlToSource fast path instead of
|
|
136
|
+
// falling through to a redundant `pick(url).read(url)`.
|
|
137
|
+
this.urlToSource.set(source, source);
|
|
138
|
+
const cacheKey = `${compileKey}:${source}`;
|
|
83
139
|
const cached = this.fileCache.get(cacheKey);
|
|
84
140
|
if (cached && cached.text === text) return cached;
|
|
85
141
|
|
|
@@ -224,7 +280,16 @@ export class Loader {
|
|
|
224
280
|
return { rootSource, entry, modules, importEdges, errors };
|
|
225
281
|
}
|
|
226
282
|
|
|
227
|
-
|
|
283
|
+
/** Resolve an `import` URL against the file it appears in. Relative /
|
|
284
|
+
* absolute-path forms run through the owning `ManifestSource`'s
|
|
285
|
+
* `resolveRelative`; registry refs and full URLs pass through
|
|
286
|
+
* unchanged. Exposed so the import-controller (and any other
|
|
287
|
+
* caller-side resolver) lands on the *exact same* canonical URL the
|
|
288
|
+
* loader used when walking the entry graph — divergent resolution
|
|
289
|
+
* would silently break optimizations like `canonicalize()`-keyed
|
|
290
|
+
* cache hits whenever a non-trivial `ManifestSource.resolveRelative`
|
|
291
|
+
* is in play. */
|
|
292
|
+
resolveImportUrl(fromSource: string, importSource: string): string {
|
|
228
293
|
if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
229
294
|
return this.pick(fromSource).resolveRelative(fromSource, importSource);
|
|
230
295
|
}
|
package/src/position-metadata.ts
CHANGED
|
@@ -29,13 +29,21 @@ export function buildDocumentPositions(
|
|
|
29
29
|
|
|
30
30
|
/** Line numbers (0-indexed) where each YAML document in a multi-doc file
|
|
31
31
|
* starts. The first document is always at line 0; subsequent entries point
|
|
32
|
-
* to the line after each `---`
|
|
32
|
+
* to the line after each `---` separator.
|
|
33
|
+
*
|
|
34
|
+
* A `---` at line 0 is the doc-start marker for doc 0 (the parser still
|
|
35
|
+
* emits a single document), not a separator before an empty doc — skipping
|
|
36
|
+
* it keeps `offsets.length === parsedDocs.length` so diagnostics for doc N
|
|
37
|
+
* don't land inside doc N-1's text. */
|
|
33
38
|
export function documentLineOffsets(text: string): number[] {
|
|
34
39
|
const offsets = [0];
|
|
35
40
|
const lines = text.split("\n");
|
|
36
41
|
for (let i = 0; i < lines.length; i++) {
|
|
37
42
|
const t = lines[i].trimEnd();
|
|
38
|
-
if (t === "---" || t.startsWith("--- "))
|
|
43
|
+
if (t === "---" || t.startsWith("--- ")) {
|
|
44
|
+
if (i === 0) continue;
|
|
45
|
+
offsets.push(i + 1);
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
48
|
return offsets;
|
|
41
49
|
}
|
package/src/precompile.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
2
2
|
import { isCompiledValue } from "@telorun/sdk";
|
|
3
|
-
import { compileString, defaultRegistry, isTaggedSentinel } from "@telorun/templating";
|
|
3
|
+
import { compileString, defaultRegistry, isRefSentinel, isTaggedSentinel } from "@telorun/templating";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Walks a raw YAML document and replaces all `${{ expr }}` strings (and
|
|
@@ -21,6 +21,13 @@ export function precompileDoc(doc: unknown, env: Environment): unknown {
|
|
|
21
21
|
// analyzer's diagnostic walk can identify it on compiled trees too;
|
|
22
22
|
// engines returning plain values (e.g. `literal` → a string) pass through
|
|
23
23
|
// verbatim — the runtime contract is "any scalar value is fine."
|
|
24
|
+
// `!ref` sentinels are identity markers, not templating values. They must
|
|
25
|
+
// survive precompile intact so the analyzer's `resolveRefSentinels` pass
|
|
26
|
+
// can substitute them with `{kind, name}` objects against the resolved
|
|
27
|
+
// resource manifest. Running the engine's `compile` here would prematurely
|
|
28
|
+
// collapse the sentinel into its source string and the ref slot would
|
|
29
|
+
// arrive at the controller as a bare name with no kind.
|
|
30
|
+
if (isRefSentinel(doc)) return doc;
|
|
24
31
|
if (isTaggedSentinel(doc)) {
|
|
25
32
|
const engine = defaultRegistry().get(doc.engine);
|
|
26
33
|
if (!engine) {
|