@telorun/analyzer 0.65.0 → 0.67.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 (142) hide show
  1. package/dist/analyzer.d.ts +1 -1
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +194 -65
  4. package/dist/builtins.d.ts.map +1 -1
  5. package/dist/builtins.js +67 -16
  6. package/dist/call-graph.d.ts.map +1 -1
  7. package/dist/call-graph.js +7 -1
  8. package/dist/cel-environment.d.ts +12 -0
  9. package/dist/cel-environment.d.ts.map +1 -1
  10. package/dist/cel-environment.js +35 -19
  11. package/dist/cel-scope.d.ts +8 -0
  12. package/dist/cel-scope.d.ts.map +1 -1
  13. package/dist/cel-scope.js +89 -11
  14. package/dist/definition-registry.d.ts +41 -0
  15. package/dist/definition-registry.d.ts.map +1 -1
  16. package/dist/definition-registry.js +76 -0
  17. package/dist/dependency-graph.d.ts.map +1 -1
  18. package/dist/dependency-graph.js +65 -0
  19. package/dist/eval-paths.d.ts +68 -0
  20. package/dist/eval-paths.d.ts.map +1 -1
  21. package/dist/eval-paths.js +138 -0
  22. package/dist/flatten-for-analyzer.d.ts +37 -1
  23. package/dist/flatten-for-analyzer.d.ts.map +1 -1
  24. package/dist/flatten-for-analyzer.js +104 -5
  25. package/dist/import-resolution-diagnostics.d.ts +1 -1
  26. package/dist/import-resolution-diagnostics.js +1 -1
  27. package/dist/index.d.ts +12 -8
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +8 -6
  30. package/dist/inline-imports.d.ts.map +1 -1
  31. package/dist/inline-imports.js +1 -0
  32. package/dist/manifest-visitor.d.ts +4 -0
  33. package/dist/manifest-visitor.d.ts.map +1 -1
  34. package/dist/manifest-visitor.js +36 -1
  35. package/dist/migrations/entries/index.d.ts.map +1 -1
  36. package/dist/migrations/entries/index.js +2 -0
  37. package/dist/migrations/entries/schema-prepare-bucket.json +27 -0
  38. package/dist/peer-binding.d.ts +232 -0
  39. package/dist/peer-binding.d.ts.map +1 -0
  40. package/dist/peer-binding.js +418 -0
  41. package/dist/precompile.d.ts.map +1 -1
  42. package/dist/precompile.js +8 -0
  43. package/dist/ref-slot.d.ts +13 -0
  44. package/dist/ref-slot.d.ts.map +1 -1
  45. package/dist/ref-slot.js +26 -0
  46. package/dist/reference-field-map.d.ts +28 -0
  47. package/dist/reference-field-map.d.ts.map +1 -1
  48. package/dist/reference-field-map.js +25 -0
  49. package/dist/referrer-rule.d.ts +17 -0
  50. package/dist/referrer-rule.d.ts.map +1 -1
  51. package/dist/referrer-rule.js +4 -1
  52. package/dist/release/index.d.ts +1 -1
  53. package/dist/release/index.js +1 -1
  54. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  55. package/dist/resolve-ref-sentinels.js +14 -1
  56. package/dist/resource-input.d.ts +75 -0
  57. package/dist/resource-input.d.ts.map +1 -0
  58. package/dist/resource-input.js +90 -0
  59. package/dist/resource-rule.d.ts +40 -27
  60. package/dist/resource-rule.d.ts.map +1 -1
  61. package/dist/resource-rule.js +61 -7
  62. package/dist/rule-condition.d.ts +11 -0
  63. package/dist/rule-condition.d.ts.map +1 -1
  64. package/dist/rule-condition.js +13 -0
  65. package/dist/schema-projection.d.ts +119 -4
  66. package/dist/schema-projection.d.ts.map +1 -1
  67. package/dist/schema-projection.js +213 -18
  68. package/dist/system-kinds.d.ts +7 -2
  69. package/dist/system-kinds.d.ts.map +1 -1
  70. package/dist/system-kinds.js +7 -2
  71. package/dist/telo-version.d.ts +1 -1
  72. package/dist/telo-version.js +1 -1
  73. package/dist/template-body.d.ts +50 -0
  74. package/dist/template-body.d.ts.map +1 -0
  75. package/dist/template-body.js +58 -0
  76. package/dist/validate-cel-context.d.ts +1 -17
  77. package/dist/validate-cel-context.d.ts.map +1 -1
  78. package/dist/validate-cel-context.js +73 -78
  79. package/dist/validate-identifier-names.d.ts.map +1 -1
  80. package/dist/validate-identifier-names.js +17 -2
  81. package/dist/validate-reference-forms.d.ts +10 -0
  82. package/dist/validate-reference-forms.d.ts.map +1 -1
  83. package/dist/validate-reference-forms.js +23 -0
  84. package/dist/validate-references.d.ts +17 -0
  85. package/dist/validate-references.d.ts.map +1 -1
  86. package/dist/validate-references.js +77 -18
  87. package/dist/validate-referrer-rules.d.ts +38 -4
  88. package/dist/validate-referrer-rules.d.ts.map +1 -1
  89. package/dist/validate-referrer-rules.js +139 -31
  90. package/dist/validate-resource-inputs.d.ts +35 -0
  91. package/dist/validate-resource-inputs.d.ts.map +1 -0
  92. package/dist/validate-resource-inputs.js +319 -0
  93. package/dist/validate-resource-rules.d.ts +11 -2
  94. package/dist/validate-resource-rules.d.ts.map +1 -1
  95. package/dist/validate-resource-rules.js +18 -12
  96. package/dist/validate-schema-projection.d.ts.map +1 -1
  97. package/dist/validate-schema-projection.js +33 -2
  98. package/dist/validate-template-dispatch.d.ts +27 -0
  99. package/dist/validate-template-dispatch.d.ts.map +1 -0
  100. package/dist/validate-template-dispatch.js +95 -0
  101. package/dist/with-synthetic-positions.d.ts +1 -1
  102. package/dist/with-synthetic-positions.js +1 -1
  103. package/package.json +3 -3
  104. package/src/analyzer.ts +236 -70
  105. package/src/builtins.ts +69 -16
  106. package/src/call-graph.ts +7 -1
  107. package/src/cel-environment.ts +48 -19
  108. package/src/cel-scope.ts +113 -19
  109. package/src/definition-registry.ts +78 -0
  110. package/src/dependency-graph.ts +66 -0
  111. package/src/eval-paths.ts +152 -0
  112. package/src/flatten-for-analyzer.ts +117 -4
  113. package/src/import-resolution-diagnostics.ts +1 -1
  114. package/src/index.ts +57 -4
  115. package/src/inline-imports.ts +1 -0
  116. package/src/manifest-visitor.ts +41 -1
  117. package/src/migrations/entries/index.ts +2 -0
  118. package/src/migrations/entries/schema-prepare-bucket.json +27 -0
  119. package/src/peer-binding.ts +641 -0
  120. package/src/precompile.ts +8 -0
  121. package/src/ref-slot.ts +36 -0
  122. package/src/reference-field-map.ts +42 -0
  123. package/src/referrer-rule.ts +20 -1
  124. package/src/release/index.ts +1 -1
  125. package/src/resolve-ref-sentinels.ts +12 -1
  126. package/src/resource-input.ts +132 -0
  127. package/src/resource-rule.ts +69 -7
  128. package/src/rule-condition.ts +15 -0
  129. package/src/schema-projection.ts +302 -20
  130. package/src/system-kinds.ts +7 -2
  131. package/src/telo-version.ts +1 -1
  132. package/src/template-body.ts +104 -0
  133. package/src/validate-cel-context.ts +73 -78
  134. package/src/validate-identifier-names.ts +18 -3
  135. package/src/validate-reference-forms.ts +22 -0
  136. package/src/validate-references.ts +85 -16
  137. package/src/validate-referrer-rules.ts +215 -28
  138. package/src/validate-resource-inputs.ts +367 -0
  139. package/src/validate-resource-rules.ts +26 -12
  140. package/src/validate-schema-projection.ts +50 -1
  141. package/src/validate-template-dispatch.ts +99 -0
  142. package/src/with-synthetic-positions.ts +1 -1
package/src/analyzer.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  buildCelEnvironment,
21
21
  buildImportInputCelEnvironment,
22
22
  buildTypedCelEnvironment,
23
+ isKindDocument,
23
24
  type CelHandlers,
24
25
  } from "./cel-environment.js";
25
26
  import { DefinitionRegistry } from "./definition-registry.js";
@@ -78,7 +79,7 @@ import {
78
79
  type ResourceRuleDiagnostic,
79
80
  type ResourceRuleIssue,
80
81
  } from "./validate-resource-rules.js";
81
- import { readResourceRules, type ResourceRule } from "./resource-rule.js";
82
+ import { pointerToPath, readResourceRules, type ResourceRule } from "./resource-rule.js";
82
83
  import { readReferrerRules, type ReferrerRule } from "./referrer-rule.js";
83
84
  import {
84
85
  evaluateReferrerRules,
@@ -87,11 +88,15 @@ import {
87
88
  reportUnexercisedReferrerRule,
88
89
  validateReferrerRuleDeclarations,
89
90
  type Referrer,
91
+ type ReferrerRuleContext,
90
92
  type ReferrerRuleDiagnostic,
91
93
  type ReferrerRuleIssue,
92
94
  } from "./validate-referrer-rules.js";
95
+ import { analyzerPeerBinder, analyzerPeersTarget } from "./peer-binding.js";
93
96
  import {
94
97
  describeProjectionFailure,
98
+ manifestListScope,
99
+ resolveSchemaProjections,
95
100
  type ProjectionFailure,
96
101
  } from "./schema-projection.js";
97
102
  import {
@@ -118,14 +123,18 @@ import { collectValueSchemaIssues } from "./validate-value-schema.js";
118
123
  import { DiagnosticSeverity, type AnalysisDiagnostic, type AnalysisOptions } from "./types.js";
119
124
  import {
120
125
  extractAccessChains,
121
- extractCelRegionScopes,
122
126
  extractContextsFromSchema,
123
127
  getManifestItem,
124
- pathMatchesScope,
125
128
  resolveContextAnnotations,
126
129
  resolveTypeFieldToSchema,
127
130
  } from "./validate-cel-context.js";
128
- import { buildEvalPaths, evalPathsCover } from "./eval-paths.js";
131
+ import {
132
+ celEvalModeAt,
133
+ celEvalSites,
134
+ mergeCelEvalSites,
135
+ NO_CEL_EVAL_SITES,
136
+ type CelEvalSites,
137
+ } from "./eval-paths.js";
129
138
  import {
130
139
  BINDINGS_ANNOTATION,
131
140
  bindingContextProperties,
@@ -148,8 +157,11 @@ import { validateInvocationContract } from "./validate-invocation-contract.js";
148
157
  import { collectRefInputIssues, collectStepInputIssues } from "./validate-step-inputs.js";
149
158
  import { validateNestedInlineResources } from "./validate-nested-inline.js";
150
159
  import { validateProviderCoherence } from "./validate-provider-coherence.js";
151
- import { validateReferences } from "./validate-references.js";
160
+ import { kindSatisfies, validateReferences } from "./validate-references.js";
152
161
  import { validateReferenceForms } from "./validate-reference-forms.js";
162
+ import { isInjectedDeclaration } from "./resource-input.js";
163
+ import { validateResourceInputs } from "./validate-resource-inputs.js";
164
+ import { validateTemplateDispatch } from "./validate-template-dispatch.js";
153
165
  import { validateUnusedDeclarations } from "./validate-unused-declarations.js";
154
166
  import { validateThrowsCoverage } from "./validate-throws-coverage.js";
155
167
  import { readStepSlot } from "./step-slot.js";
@@ -706,7 +718,7 @@ export class StaticAnalyzer {
706
718
  * `DUPLICATE_RESOURCE_NAME` reads those fields to tell a pipeline echo
707
719
  * apart from a genuine collision, and downstream diagnostic positioning
708
720
  * depends on them too. Real callers stamp positions already (the `Loader`,
709
- * `flattenForAnalyzer`, the telo-editor's `emitDocsFor`, the VSCode
721
+ * `flattenForAnalyzer`, telo studio's `emitDocsFor`, the VSCode
710
722
  * extension). Programmatic callers — tests, ad-hoc scripts — should pass
711
723
  * their inputs through `withSyntheticPositions(...)` before calling
712
724
  * `analyze()`. A missing position throws a clear error rather than
@@ -941,6 +953,11 @@ export class StaticAnalyzer {
941
953
  const projectionIssues: SchemaProjectionIssue[] = [];
942
954
  const resourceRuleIssues: ResourceRuleIssue[] = [];
943
955
  const referrerRuleIssues: ReferrerRuleIssue[] = [];
956
+ /** Definition docs of the entry's own modules that may declare rules. Their
957
+ * referrer-rule declarations are checked after every kind is registered,
958
+ * because a `peers:` pointer is checked against the REFERRER kind's schema
959
+ * and that kind may be declared later in the same file. */
960
+ const ownRuleDeclarers: ResourceManifest[] = [];
944
961
  // A rule that never had anything to iterate is never proven — the second way
945
962
  // coverage varies invisibly, beside the dynamic-leaf skip. Tracked across the
946
963
  // whole run and reported once, since "empty on every resource" is not a fact
@@ -998,9 +1015,10 @@ export class StaticAnalyzer {
998
1015
  effectiveAuthorSchema(m as any, (k) => defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k)),
999
1016
  ),
1000
1017
  );
1001
- referrerRuleIssues.push(
1002
- ...validateReferrerRuleDeclarations(m as unknown as ResourceManifest),
1003
- );
1018
+ // Deferred to after the registration loop: the `peers:` half is checked
1019
+ // against the REFERRER kind's schema, and a rule may name a kind
1020
+ // declared later in the same file.
1021
+ ownRuleDeclarers.push(m as unknown as ResourceManifest);
1004
1022
  for (const rule of readReferrerRules((m as Record<string, unknown>).schema)) {
1005
1023
  referrerRuleExercise.set(`${m.metadata?.module}.${m.metadata?.name}#${rule.code}`, {
1006
1024
  manifest: m as unknown as ResourceManifest,
@@ -1056,6 +1074,25 @@ export class StaticAnalyzer {
1056
1074
  defs.register(normalized);
1057
1075
  }
1058
1076
 
1077
+ /**
1078
+ * What a peer rule's `peers:` pointer names in the kind its `referrer:`
1079
+ * filters to.
1080
+ *
1081
+ * Liskov in BOTH directions, which is what the check has to be: the filter
1082
+ * is usually an abstract (`Sql.Schema`, so one rule serves every backend)
1083
+ * while the collection is declared by the backends that implement it, so a
1084
+ * pointer resolving on any candidate resolves the rule. Reported only when
1085
+ * NO candidate declares it, or when every candidate that does holds plain
1086
+ * data — the two shapes where the rule would see no declaration at all.
1087
+ */
1088
+ const peersTarget = analyzerPeersTarget(defs);
1089
+
1090
+ if (!options?.skipValidation) {
1091
+ for (const declarer of ownRuleDeclarers) {
1092
+ referrerRuleIssues.push(...validateReferrerRuleDeclarations(declarer, { peersTarget }));
1093
+ }
1094
+ }
1095
+
1059
1096
  // Reference-form validation — enforce `!ref` as the only reference shape.
1060
1097
  // Runs on the RAW manifests, BEFORE inline extraction and sentinel
1061
1098
  // resolution, while an author-written `{kind, name}` is still
@@ -1498,13 +1535,12 @@ export class StaticAnalyzer {
1498
1535
  return suppressUnreadableModuleDiagnostics(diagnostics, unreadableFiles);
1499
1536
  }
1500
1537
 
1501
- // Build a name→manifest map for looking up referenced resources
1502
- const byName = new Map<string, ResourceManifest>();
1503
- for (const m of allManifests) {
1504
- if (m.metadata?.name) {
1505
- byName.set(m.metadata.name as string, m);
1506
- }
1507
- }
1538
+ // ONE binder for the whole run, built beside the binding it serves: it caches
1539
+ // each referrer's resolved collection, which is what keeps a rule over an
1540
+ // n-entry collection from re-resolving that collection once per entry.
1541
+ const referrerRuleContext: ReferrerRuleContext = {
1542
+ peerBinder: analyzerPeerBinder(defs, aliases, allManifests as ResourceManifest[]),
1543
+ };
1508
1544
 
1509
1545
  // Fail loud on definition schemas AJV cannot compile. `validateAgainstSchema`
1510
1546
  // and `validateWithRefs` swallow compile failures (returning no issues),
@@ -1594,6 +1630,16 @@ export class StaticAnalyzer {
1594
1630
  const runReachable = reportsObservedState
1595
1631
  ? collectRunReachableNames(getCallGraph())
1596
1632
  : new Set<string>();
1633
+ // "Nothing starts this" is DECLARATION-derived, and a library does not
1634
+ // declare its injected inputs — whether the application starts the instance
1635
+ // it hands down is answerable only where that instance is declared. Inside
1636
+ // the library the question has no answer, so the name is treated as
1637
+ // reachable rather than reported on a `targets:` list the author cannot
1638
+ // write. The check still runs at the injection site, against the real
1639
+ // declaration.
1640
+ for (const m of allManifests) {
1641
+ if (isInjectedDeclaration(m)) runReachable.add(m.metadata?.name as string);
1642
+ }
1597
1643
 
1598
1644
  // Build typed kernel globals schema so x-telo-context chain validation
1599
1645
  // recognises variables, secrets, resources, env automatically
@@ -1651,6 +1697,54 @@ export class StaticAnalyzer {
1651
1697
  observedStateContext,
1652
1698
  });
1653
1699
 
1700
+ /**
1701
+ * The schema a resource of this kind is validated against, per DEFINITION.
1702
+ *
1703
+ * Both halves derive a fresh object — `effectiveAuthorSchema` merges along
1704
+ * `extends`, and the closed-schema branch spreads `kind` / `metadata` in —
1705
+ * so asking per resource handed `validateResourceConfig` a schema object it
1706
+ * had never seen. That registry memoizes its compiled AJV validator by
1707
+ * object IDENTITY, precisely because every resource of a kind is checked
1708
+ * against the same one at keystroke time, so a fresh object per resource
1709
+ * recompiled the whole kind schema per resource. Keyed on the definition,
1710
+ * which is stable for the run.
1711
+ */
1712
+ const authorSchemaCache = new WeakMap<object, Record<string, any>>();
1713
+ const validationSchemaFor = (def: ResourceDefinition): Record<string, any> => {
1714
+ const cached = authorSchemaCache.get(def as unknown as object);
1715
+ if (cached) return cached;
1716
+ const authorSchema = effectiveAuthorSchema(def, (k) =>
1717
+ defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k),
1718
+ );
1719
+ // `kind` and `metadata` are implicit on every resource — inject them so
1720
+ // module authors don't have to repeat them under `additionalProperties:
1721
+ // false`.
1722
+ const schema =
1723
+ authorSchema.additionalProperties === false
1724
+ ? {
1725
+ ...authorSchema,
1726
+ properties: {
1727
+ kind: { type: "string" },
1728
+ metadata: { type: "object" },
1729
+ ...authorSchema.properties,
1730
+ },
1731
+ }
1732
+ : authorSchema;
1733
+ authorSchemaCache.set(def as unknown as object, schema);
1734
+ return schema;
1735
+ };
1736
+
1737
+ // One scope for the whole run: it closes over the manifest list and the
1738
+ // registry, neither of which changes per resource, and it is asked once per
1739
+ // schema-valued slot that carries a projection.
1740
+ const resourceProjectionScope = manifestListScope(
1741
+ allManifests as Record<string, any>[],
1742
+ (kind) =>
1743
+ (defs.resolve(kind) ?? defs.resolve(aliases.resolveKind(kind) ?? kind)) as
1744
+ | Record<string, any>
1745
+ | undefined,
1746
+ );
1747
+
1654
1748
  // Validate each non-definition, non-system resource
1655
1749
  for (const m of allManifests) {
1656
1750
  const filePath = (m.metadata as { source?: string } | undefined)?.source;
@@ -1681,6 +1775,15 @@ export class StaticAnalyzer {
1681
1775
  continue;
1682
1776
  }
1683
1777
 
1778
+ // A kind-only stand-in for a `resources:` entry is a DECLARATION, not an
1779
+ // instantiation: its kind is routinely an abstract and its configuration
1780
+ // is the importer's to supply, so validating it here would report a
1781
+ // non-instantiable kind and a page of missing required fields against a
1782
+ // block that is correct. Its kind is checked where it was written, by
1783
+ // `validate-resource-inputs`; it participates here only as a resolution
1784
+ // target and as a CEL type.
1785
+ if (isInjectedDeclaration(m)) continue;
1786
+
1684
1787
  const resource = { kind: m.kind, name: m.metadata?.name as string };
1685
1788
 
1686
1789
  // Resolve kind through alias if needed; direct lookup takes priority so that
@@ -1749,39 +1852,59 @@ export class StaticAnalyzer {
1749
1852
  }
1750
1853
 
1751
1854
  // Validate resource config against the definition's AUTHOR-FACING schema —
1752
- // inheritance-resolved: with `base:` the child's own schema (parent config
1753
- // is internal), else `merge(parent, own)` so a `base:`-less `extends` child
1754
- // is validated against the inherited fields it may set. For a definition
1755
- // that neither extends nor uses `base:` this is exactly its own schema.
1756
- // `kind` and `metadata` are implicit on every resource — inject them so module
1757
- // authors don't have to repeat them when using additionalProperties: false.
1758
- const authorSchema = effectiveAuthorSchema(definition, (k) =>
1759
- defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k),
1760
- );
1761
- if (authorSchema && Object.keys(authorSchema).length > 0) {
1762
- const schema =
1763
- authorSchema.additionalProperties === false
1764
- ? {
1765
- ...authorSchema,
1766
- properties: {
1767
- kind: { type: "string" },
1768
- metadata: { type: "object" },
1769
- ...authorSchema.properties,
1770
- },
1771
- }
1772
- : authorSchema;
1855
+ // inheritance-resolved, with `kind` / `metadata` injected. See
1856
+ // `validationSchemaFor`, which is where both derivations and the reason
1857
+ // they are memoized per definition live.
1858
+ const schema = validationSchemaFor(definition);
1859
+ if (Object.keys(schema).length > 0) {
1773
1860
  // Phase 1: collect the pure-CEL leaves and the schema of the slot each
1774
1861
  // flows into. The expression's own type is resolved later, by the
1775
1862
  // engine walk that owns type-checking; this half only knows the target.
1776
1863
  for (const slot of collectCelValueSlots(m, schema, "")) {
1777
1864
  celReturnSlots.push({ manifest: m, resource, filePath, ...slot });
1778
1865
  }
1866
+ // A kind's own schema may point a slot at a PROJECTION — of a resource it
1867
+ // references, or (the empty pointer) of this very declaration, which is
1868
+ // what types a table's seed rows against its own `columns:`. Resolved
1869
+ // PER RESOURCE, because a projection is declaration-derived: the same
1870
+ // kind schema yields a different row shape for every table.
1871
+ //
1872
+ // A failure here is REPORTED, never dropped. An entry that could not be
1873
+ // read vanishes from the projected shape, so the slot typed from it then
1874
+ // rejects a field the author did declare ("'status' is not allowed") with
1875
+ // nothing saying why — the projection's own failure mode, blaming the
1876
+ // wrong line. Entry-module-scoped, like every other schema issue.
1877
+ const projectionFailures: ProjectionFailure[] = [];
1878
+ const projected = resolveSchemaProjections(
1879
+ schema,
1880
+ m as Record<string, any>,
1881
+ resourceProjectionScope,
1882
+ projectionFailures,
1883
+ ) as Record<string, any>;
1884
+ if (!ownModule || rootModules.has(ownModule)) {
1885
+ for (const failure of projectionFailures) {
1886
+ // A projection through a library's own resource INPUT is
1887
+ // unanswerable here — the entries belong to the declaration the
1888
+ // importer supplies — so it is not a defect in the block that named
1889
+ // it. The check runs at the injection site instead.
1890
+ if (failure.reason === "injected") continue;
1891
+ diagnostics.push({
1892
+ severity: DiagnosticSeverity.Error,
1893
+ code: "SCHEMA_PROJECTION_FROM_UNRESOLVED",
1894
+ source: SOURCE,
1895
+ message: `${m.kind}/${resource.name}: ${describeProjectionFailure(failure)}`,
1896
+ data: { resource, filePath, path: pointerToPath(failure.pointer) },
1897
+ });
1898
+ }
1899
+ }
1779
1900
  // Phase 2+3: AJV on substituted data — CEL fields replaced with typed
1780
1901
  // placeholders. Through the REGISTRY, so a kind whose schema references
1781
1902
  // a shape declared elsewhere is checked on the instance that holds it.
1782
1903
  const ajvIssues = defs.validateResourceConfig(
1783
- substituteCelFields(m, schema, undefined, { external: (ref) => defs.schemaForId(ref) }),
1784
- schema,
1904
+ substituteCelFields(m, projected, undefined, {
1905
+ external: (ref) => defs.schemaForId(ref),
1906
+ }),
1907
+ projected,
1785
1908
  );
1786
1909
  // Phase 4: value slots that must satisfy a type declared elsewhere on
1787
1910
  // the resource (`x-telo-value-schema-from`) — e.g. every row of a
@@ -1816,12 +1939,12 @@ export class StaticAnalyzer {
1816
1939
  for (const report of reportResourceRules(
1817
1940
  m as unknown as ResourceManifest,
1818
1941
  definition as unknown as ResourceManifest,
1819
- evaluateResourceRules(m as unknown as ResourceManifest, authorSchema),
1942
+ evaluateResourceRules(m as unknown as ResourceManifest, schema),
1820
1943
  !ruleDeclarer || rootModules.has(ruleDeclarer),
1821
1944
  )) {
1822
1945
  diagnostics.push(resourceRuleDiagnostic(report));
1823
1946
  }
1824
- for (const rule of readResourceRules(authorSchema)) {
1947
+ for (const rule of readResourceRules(schema)) {
1825
1948
  const key = `${definition.metadata?.module}.${definition.metadata?.name}#${rule.code}`;
1826
1949
  const tracked = ruleExercise.get(key);
1827
1950
  if (!tracked) continue;
@@ -1835,7 +1958,7 @@ export class StaticAnalyzer {
1835
1958
  // literal appears on the referring side, where the spelling would be the
1836
1959
  // consumer's import alias rather than anything the rule's author controls.
1837
1960
  // A consumer of the shared call graph, never a second traversal.
1838
- const referrerRules = readReferrerRules(authorSchema);
1961
+ const referrerRules = readReferrerRules(schema);
1839
1962
  if (referrerRules.length > 0) {
1840
1963
  const referrers = referrersOf(m as unknown as ResourceManifest, getCallGraph());
1841
1964
  for (const report of reportReferrerRules(
@@ -1843,9 +1966,10 @@ export class StaticAnalyzer {
1843
1966
  definition as unknown as ResourceManifest,
1844
1967
  evaluateReferrerRules(
1845
1968
  m as unknown as ResourceManifest,
1846
- authorSchema,
1969
+ schema,
1847
1970
  referrers,
1848
1971
  kindMatches,
1972
+ referrerRuleContext,
1849
1973
  ),
1850
1974
  !ruleDeclarer || rootModules.has(ruleDeclarer),
1851
1975
  )) {
@@ -1857,13 +1981,25 @@ export class StaticAnalyzer {
1857
1981
  continue;
1858
1982
  }
1859
1983
  diagnostics.push(referrerRuleDiagnostic(report));
1984
+ // A rule that VIOLATED plainly ran, whatever its peer set held. The
1985
+ // unexercised report exists to catch silence — a `peers:` naming
1986
+ // nothing makes every `!peers.exists(…)` pass vacuously — so a finding
1987
+ // is exactly the evidence it asks for.
1988
+ if (report.code === "REFERRER_RULE_VIOLATED") {
1989
+ const violated = referrerRuleExercise.get(
1990
+ `${definition.metadata?.module}.${definition.metadata?.name}#${report.rule}`,
1991
+ );
1992
+ if (violated) violated.exercised = true;
1993
+ }
1860
1994
  }
1861
1995
  for (const rule of referrerRules) {
1862
1996
  const key = `${definition.metadata?.module}.${definition.metadata?.name}#${rule.code}`;
1863
1997
  const tracked = referrerRuleExercise.get(key);
1864
1998
  if (!tracked) continue;
1865
1999
  tracked.seen = true;
1866
- if (referrerRuleExercised(rule, referrers, kindMatches)) tracked.exercised = true;
2000
+ if (referrerRuleExercised(rule, referrers, kindMatches, referrerRuleContext)) {
2001
+ tracked.exercised = true;
2002
+ }
1867
2003
  }
1868
2004
  }
1869
2005
 
@@ -2065,18 +2201,15 @@ export class StaticAnalyzer {
2065
2201
  // same schema an expression is typed against.
2066
2202
  let celStepContextSchema: Record<string, any> | undefined;
2067
2203
  let celErrorScopes: ReadonlyMap<string, Record<string, any>> = new Map();
2068
- // Region coverage for the "CEL in a non-eval field" check: the union of
2069
- // `x-telo-eval` paths (own + capability) and `x-telo-context` /
2070
- // `x-telo-step-context` / `x-telo-error-context` scopes. A `!cel` outside
2071
- // every region is read as a literal the runtime never evaluates it.
2072
- let celEvalPaths: string[] = [];
2204
+ // Where this kind says its values are evaluated — `x-telo-eval` paths (own +
2205
+ // capability) and the regions covering their contents. A `!cel` outside every
2206
+ // one of them is read as a literal, and one under a compile path resolves at
2207
+ // startup, where observed state cannot exist yet. Both questions are
2208
+ // `celEvalModeAt`, which the editor asks of the same sites.
2209
+ let celSites: CelEvalSites = NO_CEL_EVAL_SITES;
2073
2210
  // The bindings field this kind declares (if any), read by the CEL sites that
2074
2211
  // see the names it introduces.
2075
2212
  let celBindingSites: BindingSites | undefined;
2076
- // The compile half alone: a field that resolves at startup, where observed
2077
- // state cannot exist yet.
2078
- let celCompilePaths: string[] = [];
2079
- let celRegionScopes: string[] = [];
2080
2213
  let celRuleApplies = false;
2081
2214
 
2082
2215
  visitManifest(
@@ -2231,22 +2364,28 @@ export class StaticAnalyzer {
2231
2364
  celRuleApplies =
2232
2365
  !!e.definition?.schema && capability !== undefined && capability !== "Telo.Template";
2233
2366
  if (celRuleApplies) {
2234
- const ownSchema = e.definition!.schema as Record<string, any>;
2235
- const own = buildEvalPaths(ownSchema);
2367
+ // The INHERITANCE-RESOLVED schema, not the kind's own: without
2368
+ // `base:`, an `extends` child is authored against merge(parent, own),
2369
+ // so a CEL-bearing field the parent declares is a legal field on the
2370
+ // child and the kernel expands it — it stamps the merged schema at
2371
+ // definition registration and builds eval paths from that. Reading
2372
+ // the own schema here reported `CEL_IN_NON_EVAL_FIELD` for an
2373
+ // expression the runtime evaluates correctly, which is the two halves
2374
+ // disagreeing about what the manifest means.
2375
+ const ownSchema = effectiveAuthorSchema(
2376
+ e.definition as unknown as ResourceDefinition,
2377
+ (k) => defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k),
2378
+ ) as Record<string, any>;
2236
2379
  const capabilityDef = capability ? defs.resolve(capability) : undefined;
2237
- const parent = capabilityDef?.schema
2238
- ? buildEvalPaths(capabilityDef.schema as Record<string, any>)
2239
- : { compile: [], runtime: [] };
2240
- celEvalPaths = [...own.compile, ...own.runtime, ...parent.compile, ...parent.runtime];
2241
2380
  // A `Telo.Provider`'s fields are implicitly compile-eval — the
2242
2381
  // capability abstract carries the root annotation — so its reads are
2243
2382
  // covered here without the provider restating anything.
2244
- celCompilePaths = [...own.compile, ...parent.compile];
2245
- celRegionScopes = extractCelRegionScopes(ownSchema);
2383
+ celSites = mergeCelEvalSites(
2384
+ celEvalSites(ownSchema),
2385
+ celEvalSites(capabilityDef?.schema as Record<string, any> | undefined),
2386
+ );
2246
2387
  } else {
2247
- celEvalPaths = [];
2248
- celCompilePaths = [];
2249
- celRegionScopes = [];
2388
+ celSites = NO_CEL_EVAL_SITES;
2250
2389
  }
2251
2390
  },
2252
2391
  onCel: (e) => {
@@ -2264,8 +2403,7 @@ export class StaticAnalyzer {
2264
2403
  celRuleApplies &&
2265
2404
  engineName === "cel" &&
2266
2405
  celScope.invocationContextSchema === undefined &&
2267
- !evalPathsCover(celEvalPaths, path) &&
2268
- !celRegionScopes.some((scope) => pathMatchesScope(path, scope)) &&
2406
+ celEvalModeAt(celSites, path) === null &&
2269
2407
  !pathCrossesNestedResource(m, path)
2270
2408
  ) {
2271
2409
  diagnostics.push({
@@ -2293,7 +2431,7 @@ export class StaticAnalyzer {
2293
2431
  read.alias ? `${read.alias}.${read.name}` : read.name,
2294
2432
  );
2295
2433
 
2296
- if (celRuleApplies && evalPathsCover(celCompilePaths, path)) {
2434
+ if (celRuleApplies && celEvalModeAt(celSites, path) === "compile") {
2297
2435
  diagnostics.push({
2298
2436
  severity: DiagnosticSeverity.Error,
2299
2437
  code: "OBSERVED_STATE_IN_STARTUP_FIELD",
@@ -2340,7 +2478,19 @@ export class StaticAnalyzer {
2340
2478
  matchedScope,
2341
2479
  });
2342
2480
 
2343
- const result = engine.analyze(expr, { celEnv: typedEnv, contextSchema: effectiveContext });
2481
+ const result = engine.analyze(expr, {
2482
+ celEnv: typedEnv,
2483
+ contextSchema: effectiveContext,
2484
+ // `scopeFor` registers every kernel global and every name the site's
2485
+ // context declares, so a root this environment does not know is one
2486
+ // nothing puts in scope. Two places where the CEL belongs to another
2487
+ // scope, both already recognised by the non-eval-field check: a kind
2488
+ // document, whose CEL is written for whoever instantiates the kind,
2489
+ // and anything below a nested inline `{ kind }`, whose CEL the
2490
+ // nested kind evaluates — and which is analyzed again, in its own
2491
+ // scope, as the resource it was extracted into.
2492
+ rootsDeclared: !isKindDocument(m) && !pathCrossesNestedResource(m, path),
2493
+ });
2344
2494
 
2345
2495
  if (result.type !== undefined) {
2346
2496
  let byPath = celTypeByPath.get(m);
@@ -2377,7 +2527,7 @@ export class StaticAnalyzer {
2377
2527
  // intent (a boot timestamp, a run id), so it warns rather than
2378
2528
  // blocking. The engine reports which calls re-evaluate; the eval mode
2379
2529
  // is manifest policy and stays here.
2380
- if (celRuleApplies && evalPathsCover(celCompilePaths, path)) {
2530
+ if (celRuleApplies && celEvalModeAt(celSites, path) === "compile") {
2381
2531
  const volatile = [
2382
2532
  ...new Set(result.calls.filter((c) => c.deterministic === false).map((c) => c.name)),
2383
2533
  ].sort();
@@ -2433,7 +2583,7 @@ export class StaticAnalyzer {
2433
2583
  }
2434
2584
  },
2435
2585
  },
2436
- { aliases },
2586
+ { aliases, aliasesByModule, rootModules },
2437
2587
  );
2438
2588
 
2439
2589
  // The two halves of "does this expression fit the slot it flows into" meet
@@ -2522,6 +2672,22 @@ export class StaticAnalyzer {
2522
2672
  // Warn about declared variables / secrets / ports that no CEL references.
2523
2673
  diagnostics.push(...validateUnusedDeclarations(allManifests, this.celEnv));
2524
2674
 
2675
+ // A `!ref` at a definition's dispatch slot must name a sibling `resources:`
2676
+ // entry — the slot no reference pass reaches, so the tag would otherwise
2677
+ // advertise a resolution nothing performs.
2678
+ diagnostics.push(...validateTemplateDispatch(allManifests, rootModules));
2679
+
2680
+ // A library's declared resource inputs, and every import that supplies them.
2681
+ diagnostics.push(
2682
+ ...validateResourceInputs(
2683
+ allManifests,
2684
+ defs,
2685
+ aliases,
2686
+ rootModules,
2687
+ (supplied, required) => kindSatisfies(supplied, required, defs),
2688
+ ),
2689
+ );
2690
+
2525
2691
  // Reroute diagnostics on synthetic (inline-extracted) resources back to
2526
2692
  // the chain root so position-index lookups land on the parent doc.
2527
2693
  return rewriteSyntheticOrigins(
package/src/builtins.ts CHANGED
@@ -232,6 +232,35 @@ const ROOT_LOGGING_SCHEMA = {
232
232
  additionalProperties: false,
233
233
  };
234
234
 
235
+ /** A `Telo.Library`'s declared resource inputs — the instances it requires from
236
+ * whoever imports it, the inward half of the symmetry `exports.resources`
237
+ * already had outward. Each entry is constrained by KIND ONLY, through the same
238
+ * alias-qualified grammar `extends:` and `x-telo-ref` use; there is no `use:`,
239
+ * because the boundary is a dependency edge for init order whatever the library
240
+ * does with the instance. See `analyzer/nodejs/src/resource-input.ts`. */
241
+ const LIBRARY_RESOURCE_INPUTS_SCHEMA = {
242
+ type: "object",
243
+ additionalProperties: {
244
+ type: "object",
245
+ required: ["kind"],
246
+ properties: {
247
+ kind: { type: "string" },
248
+ description: { type: "string" },
249
+ },
250
+ additionalProperties: false,
251
+ },
252
+ };
253
+
254
+ /** The importer's side of the same block: entry name → `!ref` to the instance
255
+ * supplied for it. Left open because the accepted KIND is declared by the
256
+ * target library, not by this schema — the constraint is checked by
257
+ * `validate-resource-inputs.ts`, which reads the target's declared block off
258
+ * the `metadata.requiredResources` stamp. */
259
+ const IMPORT_RESOURCE_INPUTS_SCHEMA = {
260
+ type: "object",
261
+ additionalProperties: {},
262
+ };
263
+
235
264
  export const KERNEL_BUILTINS: ResourceDefinition[] = [
236
265
  { kind: "Telo.Abstract", metadata: { name: "Template", module: "Telo" } },
237
266
  // "Control can be transferred to this" — the parent of Invocable and Runnable,
@@ -427,28 +456,26 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
427
456
  items: {
428
457
  type: "object",
429
458
  additionalProperties: true,
430
- // Resource bodies are `self`-only for config: per-call `inputs` is
431
- // NOT in scope here. Each entry is a persistent child created once at
432
- // init() and reused, so its config cannot depend on call-time data —
433
- // that flows through the top-level `inputs:` sibling into the dispatch
434
- // target's invoke().
459
+ // A `resources:` entry is a DECLARATION of another kind, so the
460
+ // CEL inside it belongs to THAT kind: its `x-telo-context` regions
461
+ // are rebased under this entry's path and take precedence (they are
462
+ // deeper), which is what puts `inputs`, `item`, `request`, `steps`
463
+ // and a `catch:`'s `error` in scope exactly where the nested kind
464
+ // declares them — see `analyzer/nodejs/src/template-body.ts`.
435
465
  //
436
- // The exception is CEL the child's OWN controller evaluates later
437
- // against a runtime context it owns (e.g. an Http.Api evaluating route
438
- // CEL per request). Those `request` / `result` / `steps` / `error`
439
- // variables are deferred the template controller preserves them
440
- // untouched (see resource-template-controller.ts) so they are
441
- // exposed here permissively. Their deep shape is the child kind's
442
- // concern, not the template's, so they type as open values.
466
+ // What stays here is `self` alone, in force throughout the entry:
467
+ // it is how a body reaches the configuration its enclosing template
468
+ // was given, and no nested kind knows about it. The four names that
469
+ // used to sit beside it (`request` / `result` / `steps` / `error`)
470
+ // were a fixed permissive stand-in for the nested kind's own
471
+ // regions which is why `error` was offered outside every `catch:`
472
+ // while `inputs` and `item` were undefined wherever a body actually
473
+ // reads them.
443
474
  "x-telo-context": {
444
475
  type: "object",
445
476
  additionalProperties: false,
446
477
  properties: {
447
478
  self: { "x-telo-context-from-root": "schema" },
448
- request: {},
449
- result: {},
450
- steps: {},
451
- error: {},
452
479
  },
453
480
  },
454
481
  },
@@ -611,6 +638,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
611
638
  integrity: { type: "string" },
612
639
  variables: { type: "object" },
613
640
  secrets: { type: "object" },
641
+ resources: IMPORT_RESOURCE_INPUTS_SCHEMA,
614
642
  runtime: {
615
643
  oneOf: [
616
644
  { type: "string" },
@@ -757,6 +785,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
757
785
  integrity: { type: "string" },
758
786
  variables: { type: "object" },
759
787
  secrets: { type: "object" },
788
+ resources: IMPORT_RESOURCE_INPUTS_SCHEMA,
760
789
  runtime: {
761
790
  oneOf: [
762
791
  { type: "string" },
@@ -872,6 +901,29 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
872
901
  },
873
902
  variables: { type: "object" },
874
903
  secrets: { type: "object" },
904
+ // How many times this library is instantiated in one application.
905
+ //
906
+ // `isolated` (the default) is what every published module was written
907
+ // against: each import declaration builds its own child scope with its
908
+ // own instances, so two libraries importing a third get two of
909
+ // everything in it. `shared` makes the library a SINGLETON — every
910
+ // import of it resolves to one instantiation, owned by the root and
911
+ // torn down after everything that borrowed it.
912
+ //
913
+ // Default `isolated` rather than `shared` — the opposite of the
914
+ // Application field's — because flipping it would silently collapse
915
+ // every existing app's resource graph and turn per-import `variables:`
916
+ // into a conflict. The `exports.kinds` precedent: private-by-default is
917
+ // the better end state and still needs the ecosystem republished first.
918
+ lifecycle: {
919
+ type: "string",
920
+ enum: ["shared", "isolated"],
921
+ default: "isolated",
922
+ },
923
+ // The inward half of `exports.resources`: instances this library
924
+ // requires from whoever imports it. Library-only — an Application is a
925
+ // root with no importer, so it owns its instances outright.
926
+ resources: LIBRARY_RESOURCE_INPUTS_SCHEMA,
875
927
  include: {
876
928
  type: "array",
877
929
  items: { type: "string" },
@@ -902,6 +954,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
902
954
  integrity: { type: "string" },
903
955
  variables: { type: "object" },
904
956
  secrets: { type: "object" },
957
+ resources: IMPORT_RESOURCE_INPUTS_SCHEMA,
905
958
  runtime: {
906
959
  oneOf: [
907
960
  { type: "string" },