@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.
Files changed (42) hide show
  1. package/LICENSE +2 -2
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +21 -1
  4. package/dist/builtins.d.ts.map +1 -1
  5. package/dist/builtins.js +14 -0
  6. package/dist/dependency-graph.d.ts.map +1 -1
  7. package/dist/dependency-graph.js +27 -13
  8. package/dist/manifest-loader.d.ts +23 -1
  9. package/dist/manifest-loader.d.ts.map +1 -1
  10. package/dist/manifest-loader.js +66 -3
  11. package/dist/position-metadata.d.ts +6 -1
  12. package/dist/position-metadata.d.ts.map +1 -1
  13. package/dist/position-metadata.js +10 -2
  14. package/dist/precompile.d.ts.map +1 -1
  15. package/dist/precompile.js +9 -1
  16. package/dist/reference-field-map.js +58 -6
  17. package/dist/resolve-ref-sentinels.d.ts +27 -0
  18. package/dist/resolve-ref-sentinels.d.ts.map +1 -0
  19. package/dist/resolve-ref-sentinels.js +114 -0
  20. package/dist/schema-compat.d.ts +7 -1
  21. package/dist/schema-compat.d.ts.map +1 -1
  22. package/dist/schema-compat.js +19 -2
  23. package/dist/system-kinds.d.ts +25 -0
  24. package/dist/system-kinds.d.ts.map +1 -0
  25. package/dist/system-kinds.js +34 -0
  26. package/dist/types.d.ts +12 -0
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/validate-references.d.ts.map +1 -1
  29. package/dist/validate-references.js +32 -6
  30. package/package.json +4 -3
  31. package/src/analyzer.ts +23 -1
  32. package/src/builtins.ts +14 -0
  33. package/src/dependency-graph.ts +27 -14
  34. package/src/manifest-loader.ts +69 -4
  35. package/src/position-metadata.ts +10 -2
  36. package/src/precompile.ts +8 -1
  37. package/src/reference-field-map.ts +83 -6
  38. package/src/resolve-ref-sentinels.ts +127 -0
  39. package/src/schema-compat.ts +19 -2
  40. package/src/system-kinds.ts +37 -0
  41. package/src/types.ts +12 -0
  42. package/src/validate-references.ts +34 -6
@@ -1,6 +1,12 @@
1
1
  declare const Ajv: any;
2
2
  /** Creates a configured AJV instance (allErrors, strict: false, with formats).
3
- * Called once for the module-level instance and once per DefinitionRegistry instance. */
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;0FAC0F;AAC1F,wBAAgB,SAAS,IAAI,YAAY,CAAC,OAAO,GAAG,CAAC,CAMpD;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,CA2BT"}
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"}
@@ -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
- * Called once for the module-level instance and once per DefinitionRegistry instance. */
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
@@ -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;AAGrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAkD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CA2UtB"}
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.1.0",
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.0.0"
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
- return normalizeInlineResources(
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
  },
@@ -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 key = nodeKey(r.kind, r.metadata.name as string);
55
- nodes.set(key, { kind: r.kind, name: r.metadata.name as string });
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 || typeof val !== "object") continue;
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
@@ -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
- const { source } = await this.pick(url).read(url);
71
- return source;
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
- const cacheKey = `${options?.compile ? "compiled" : "raw"}:${source}`;
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
- private resolveImportUrl(fromSource: string, importSource: string): string {
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
  }
@@ -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 `---` directive. */
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("--- ")) offsets.push(i + 1);
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) {