@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
@@ -2,6 +2,12 @@ import type { ResourceManifest } from "@telorun/sdk";
2
2
  import type { LoadedGraph, LoadedModule } from "./loaded-types.js";
3
3
  import type { LoadedFile } from "./loaded-types.js";
4
4
  import { isModuleKind } from "./module-kinds.js";
5
+ import {
6
+ injectedDeclarations,
7
+ readLibraryLifecycle,
8
+ readResourceInputs,
9
+ type ResourceInput,
10
+ } from "./resource-input.js";
5
11
  import type { ZoneModuleDocuments } from "./zone-module-documents.js";
6
12
 
7
13
  /** One parsed `exports.resources` / `exports.kinds` entry. `name` is the exported
@@ -23,7 +29,7 @@ export function parseExportEntry(entry: string): ParsedExportEntry {
23
29
  }
24
30
 
25
31
  /** The import-boundary forwarding rule, shared by `flattenForAnalyzer` (the
26
- * CLI / kernel loader path) and the telo-editor's workspace projection so the
32
+ * CLI / kernel loader path) and telo studio's workspace projection so the
27
33
  * two cannot drift. Given one module's stamped manifests and whether that
28
34
  * module is the analysis entry (root), returns the manifests that cross into
29
35
  * the consumer's flat analysis list:
@@ -414,6 +420,12 @@ function forwardReExports(graph: LoadedGraph, result: ResourceManifest[]): void
414
420
  // the kernel parses them. A module that declares none is absent, leaving importers ungated
415
421
  // (see stampExportedKinds).
416
422
  const declaredKinds = new Map<string, readonly string[]>();
423
+ /** Each library's declared `resources:` block, by module name — the inward
424
+ * half, stamped onto its importers below. */
425
+ const requiredResources = new Map<string, readonly ResourceInput[]>();
426
+ /** Libraries declaring `lifecycle: shared` — a singleton every import
427
+ * resolves to, so a per-import override of it is a contradiction. */
428
+ const sharedModules = new Set<string>();
417
429
  for (const [source, mod] of graph.modules) {
418
430
  if (source === graph.rootSource) continue; // root is an Application — no exports
419
431
  const libDoc = mod.owner.manifests.find((m) => m && isModuleKind(m.kind)) as
@@ -424,6 +436,9 @@ function forwardReExports(graph: LoadedGraph, result: ResourceManifest[]): void
424
436
  ownerSourceOf.set(moduleName, mod.owner.source);
425
437
  specs.push(...reExportSpecsFromExports(moduleName, libDoc.exports?.resources));
426
438
  kindModules.push({ module: moduleName, exportsKinds: libDoc.exports?.kinds });
439
+ const inputs = readResourceInputs(libDoc);
440
+ if (inputs.length > 0) requiredResources.set(moduleName, inputs);
441
+ if (readLibraryLifecycle(libDoc) === "shared") sharedModules.add(moduleName);
427
442
  if (libDoc.exports?.kinds !== undefined) {
428
443
  declaredKinds.set(moduleName, libDoc.exports.kinds.map((e) => parseExportEntry(e).name));
429
444
  }
@@ -440,15 +455,100 @@ function forwardReExports(graph: LoadedGraph, result: ResourceManifest[]): void
440
455
  // Telo.Import manifests so the analyzer can register the re-export mappings.
441
456
  const exportedKinds = resolveExportedKinds(kindModules, aliasToModule);
442
457
  const imports: Array<{ manifest: ResourceManifest; targetModule: string }> = [];
458
+ const sources: Array<{ manifest: ResourceManifest; targetSource: string }> = [];
443
459
  for (const m of result) {
444
460
  if (m.kind !== "Telo.Import") continue;
445
461
  const owner = (m.metadata as { source?: string } | undefined)?.source;
446
462
  const alias = m.metadata?.name as string | undefined;
447
- const target = owner && alias ? graph.importEdges.get(owner)?.get(alias)?.targetModuleName : undefined;
448
- if (target) imports.push({ manifest: m, targetModule: target });
463
+ const edge = owner && alias ? graph.importEdges.get(owner)?.get(alias) : undefined;
464
+ if (edge?.targetModuleName) imports.push({ manifest: m, targetModule: edge.targetModuleName });
465
+ if (edge?.targetSource) sources.push({ manifest: m, targetSource: edge.targetSource });
449
466
  }
467
+ stampResolvedSource(sources);
450
468
  stampReExportedKinds(imports, exportedKinds);
451
469
  stampExportedKinds(imports, declaredKinds);
470
+ stampRequiredResources(imports, requiredResources, aliasToModule);
471
+ stampSharedLifecycle(imports, sharedModules);
472
+ }
473
+
474
+ /** Stamp `metadata.resolvedSource` — the canonical resolved URL of an import's
475
+ * target — onto every `Telo.Import`. It is the identity the kernel keys a
476
+ * singleton's registry on, so a check that has to decide whether two imports
477
+ * reach the SAME library has to key on it too: `resolvedModuleName` is the
478
+ * module's own name, which two versions of it share. */
479
+ export function stampResolvedSource(
480
+ imports: ReadonlyArray<{ manifest: ResourceManifest; targetSource: string }>,
481
+ ): void {
482
+ for (const { manifest, targetSource } of imports) {
483
+ (manifest.metadata as Record<string, unknown>).resolvedSource = targetSource;
484
+ }
485
+ }
486
+
487
+ /** Stamp `metadata.sharedLibrary` onto every `Telo.Import` whose target declares
488
+ * `lifecycle: shared`, so the consumer's pass can reject the per-import
489
+ * overrides a singleton has no room for. Stamped on `metadata` for the same
490
+ * reason `exportedKinds` is — the `Telo.Import` schema forbids extra top-level
491
+ * fields. */
492
+ export function stampSharedLifecycle(
493
+ imports: ReadonlyArray<{ manifest: ResourceManifest; targetModule: string }>,
494
+ shared: ReadonlySet<string>,
495
+ ): void {
496
+ for (const { manifest, targetModule } of imports) {
497
+ if (!shared.has(targetModule)) continue;
498
+ (manifest.metadata as Record<string, unknown>).sharedLibrary = true;
499
+ }
500
+ }
501
+
502
+ /** Stamp `metadata.requiredResources` — the target library's declared
503
+ * `resources:` block, with each kind constraint CANONICALIZED in the library's
504
+ * own alias scope — onto every `Telo.Import` whose target declares one.
505
+ *
506
+ * Canonicalized here because this is the only point where the target's own
507
+ * import edges are in hand: the consumer's pass sees the flattened list, from
508
+ * which the library doc and its alias scope are both gone, so an entry written
509
+ * `Sql.Connection` would otherwise be read against whatever the CONSUMER
510
+ * happens to alias `Sql` to. Stamped on `metadata` for the same reason
511
+ * `exportedKinds` is — the `Telo.Import` schema forbids extra top-level
512
+ * fields. An entry whose alias resolves to nothing is stamped with its kind
513
+ * unchanged; `validate-resource-inputs.ts` reports that against the library
514
+ * that wrote it. */
515
+ export function stampRequiredResources(
516
+ imports: ReadonlyArray<{ manifest: ResourceManifest; targetModule: string }>,
517
+ declared: ReadonlyMap<string, readonly ResourceInput[]>,
518
+ aliasToModule: (module: string, alias: string) => string | undefined,
519
+ ): void {
520
+ for (const { manifest, targetModule } of imports) {
521
+ const inputs = declared.get(targetModule);
522
+ if (!inputs?.length) continue;
523
+ // A plain name → canonical-kind MAP, deliberately not a list of
524
+ // `{name, kind}` objects: that shape is character-identical to a resolved
525
+ // `!ref`, and every reference walk in the analyzer recognises a reference by
526
+ // exactly those two keys — so the stamp would be read as a malformed
527
+ // reference sitting in `metadata`.
528
+ const table: Record<string, string> = {};
529
+ for (const input of inputs) {
530
+ table[input.name] = canonicalizeInputKind(input.kind, targetModule, aliasToModule);
531
+ }
532
+ (manifest.metadata as Record<string, unknown>).requiredResources = table;
533
+ }
534
+ }
535
+
536
+ /** `Alias.Kind` → `<owning module>.Kind` in the DECLARING library's scope.
537
+ * `Self.` names the library itself and `Telo.` the kernel built-ins, both of
538
+ * which cross no import edge. An unresolvable prefix is returned unchanged. */
539
+ function canonicalizeInputKind(
540
+ kind: string,
541
+ ownModule: string,
542
+ aliasToModule: (module: string, alias: string) => string | undefined,
543
+ ): string {
544
+ const dot = kind.indexOf(".");
545
+ if (dot <= 0) return kind;
546
+ const alias = kind.slice(0, dot);
547
+ const suffix = kind.slice(dot + 1);
548
+ if (alias === "Self") return `${ownModule}.${suffix}`;
549
+ if (alias === "Telo") return kind;
550
+ const module = aliasToModule(ownModule, alias);
551
+ return module ? `${module}.${suffix}` : kind;
452
552
  }
453
553
 
454
554
  /** Collect every imported library's FULL document set for the zone stage's
@@ -498,7 +598,20 @@ function collectModuleManifests(mod: LoadedModule): ResourceManifest[] {
498
598
  for (const p of mod.partials) {
499
599
  partials.push(...stampFile(p, ownerModuleName(mod.owner)));
500
600
  }
501
- return [...owner, ...partials];
601
+ const all = [...owner, ...partials];
602
+ // A library's `resources:` block declares the instances it requires from
603
+ // whoever imports it. Standing a kind-only declaration behind each entry is
604
+ // what makes `!ref connection` resolve and `resources.connection.<field>` type
605
+ // in the library's OWN pass — the only pass that sees its internals, since a
606
+ // consumer's flattened analysis drops the library doc. `selectModuleManifests-
607
+ // ForAnalysis` drops them again for a non-root module (they are not exported),
608
+ // and the kernel's import controller filters them out in favour of the
609
+ // borrowed instance itself.
610
+ const moduleDoc = all.find((m) => isModuleKind(m.kind));
611
+ if (moduleDoc) {
612
+ all.push(...injectedDeclarations(moduleDoc, moduleDoc.metadata?.name as string | undefined));
613
+ }
614
+ return all;
502
615
  }
503
616
 
504
617
  function ownerModuleName(file: LoadedFile): string | undefined {
@@ -46,7 +46,7 @@ function messageFor(e: GraphLoadError, code: ReturnType<typeof classify>): strin
46
46
  /**
47
47
  * Convert a graph's import failures (`graph.errors`) into structured, coded
48
48
  * diagnostics. This is the single source of truth for surfacing a broken
49
- * import — every host (CLI, VS Code, telo-editor) routes these instead of each
49
+ * import — every host (CLI, VS Code, telo studio) routes these instead of each
50
50
  * re-deriving the channel and drifting (the VS Code extension used to drop it
51
51
  * entirely, showing nothing for a broken import).
52
52
  *
package/src/index.ts CHANGED
@@ -21,10 +21,31 @@ export {
21
21
  selectModuleManifestsForAnalysis,
22
22
  stampExportedKinds,
23
23
  stampReExportedKinds,
24
+ stampRequiredResources,
25
+ stampResolvedSource,
26
+ stampSharedLifecycle,
24
27
  type ParsedExportEntry,
25
28
  type ReExportSpec,
26
29
  } from "./flatten-for-analyzer.js";
27
- export { buildEvalPaths, evalPathCovers } from "./eval-paths.js";
30
+ export {
31
+ buildEvalPaths,
32
+ celEvalModeAt,
33
+ celEvalSites,
34
+ declaresCelRegion,
35
+ evalPathCovers,
36
+ mergeCelEvalSites,
37
+ pathMatchesScope,
38
+ NO_CEL_EVAL_SITES,
39
+ } from "./eval-paths.js";
40
+ export type { CelEvalSites } from "./eval-paths.js";
41
+ export {
42
+ injectedDeclarations,
43
+ isInjectedDeclaration,
44
+ readLibraryLifecycle,
45
+ readResourceInputs,
46
+ readSuppliedResources,
47
+ } from "./resource-input.js";
48
+ export type { LibraryLifecycle, ResourceInput } from "./resource-input.js";
28
49
  export {
29
50
  BINDINGS_ANNOTATION,
30
51
  bindingContextProperties,
@@ -96,8 +117,17 @@ export type {
96
117
  ResourceGraphNode,
97
118
  StepGraphNode,
98
119
  } from "./call-graph.js";
99
- export { buildReferenceFieldMap, isRefEntry, isScopeEntry } from "./reference-field-map.js";
100
- export type { ReferenceFieldMap, RefFieldEntry } from "./reference-field-map.js";
120
+ export {
121
+ buildReferenceFieldMap,
122
+ isRefEntry,
123
+ isScopeEntry,
124
+ satisfiesValueBranch,
125
+ } from "./reference-field-map.js";
126
+ export type {
127
+ ReferenceFieldMap,
128
+ RefFieldEntry,
129
+ ValueBranchValidator,
130
+ } from "./reference-field-map.js";
101
131
  export {
102
132
  hasDeclaredUse,
103
133
  isRefSlot,
@@ -181,6 +211,8 @@ export {
181
211
  readRawResourceRules,
182
212
  resolveRuleSubjects,
183
213
  findDynamicLeaf,
214
+ dynamicNode,
215
+ type DynamicLeaf,
184
216
  type ResourceRule,
185
217
  type ResourceRuleSeverity,
186
218
  type RuleSubject,
@@ -211,11 +243,32 @@ export {
211
243
  type ReferrerRuleIssue,
212
244
  type ReferrerRuleFinding,
213
245
  type ReferrerRuleDiagnostic,
246
+ type ReferrerRuleContext,
247
+ type ReferrerRuleDeclarationContext,
214
248
  } from "./validate-referrer-rules.js";
215
- export { RULE_BUDGET_MS } from "./rule-condition.js";
249
+ export {
250
+ PeerBinder,
251
+ analyzerPeerBinder,
252
+ analyzerPeersTarget,
253
+ entryBoundary,
254
+ navigatePath,
255
+ referenceValueOf,
256
+ shapeMatches,
257
+ type DeclarationLookup,
258
+ type PeerAliasScope,
259
+ type PeerBinderEnv,
260
+ type PeerBinderRegistry,
261
+ type PeerBinding,
262
+ type PeerBindingFailure,
263
+ type PeerBindingResult,
264
+ type PeersTarget,
265
+ } from "./peer-binding.js";
266
+ export { RULE_BUDGET_MS, UNTAGGED_CONDITION } from "./rule-condition.js";
216
267
  export {
217
268
  readSchemaProjection,
269
+ type ProjectionReference,
218
270
  readSchemaMap,
271
+ schemaMapBranch,
219
272
  readProjectionFrom,
220
273
  readProjectionRef,
221
274
  manifestListScope,
@@ -54,6 +54,7 @@ export function inlineImportManifests(
54
54
  source,
55
55
  ...(entry.variables !== undefined ? { variables: entry.variables } : {}),
56
56
  ...(entry.secrets !== undefined ? { secrets: entry.secrets } : {}),
57
+ ...(entry.resources !== undefined ? { resources: entry.resources } : {}),
57
58
  ...(entry.runtime !== undefined ? { runtime: entry.runtime } : {}),
58
59
  ...(entry.logging !== undefined ? { logging: entry.logging } : {}),
59
60
  } as unknown as ResourceManifest;
@@ -16,6 +16,8 @@ import {
16
16
  type RefFieldEntry,
17
17
  type SchemaFromFieldEntry,
18
18
  } from "./reference-field-map.js";
19
+ import type { ModuleScopes } from "./alias-resolver.js";
20
+ import { templateBodies, withTemplateSelf } from "./template-body.js";
19
21
  import { extractContextsFromSchema, pathMatchesScope } from "./validate-cel-context.js";
20
22
 
21
23
  /**
@@ -150,6 +152,10 @@ export interface VisitOptions {
150
152
  * Opt-in: the validators / dependency graph must NOT enable it (those refs
151
153
  * are runtime-resolved, not boot dependencies). */
152
154
  discoverNestedRefs?: boolean;
155
+ /** Root (consumer-owned) module names, paired with `aliasesByModule` so a
156
+ * nested kind inside an imported library's definition resolves through THAT
157
+ * library's aliases. */
158
+ rootModules?: ReadonlySet<string>;
153
159
  }
154
160
 
155
161
  /** Synthetic entry for a value-tree-discovered ref — these carry no declared
@@ -204,6 +210,15 @@ const pathUnderPrefix = (fieldPath: string, prefix: string): boolean =>
204
210
  fieldPath.startsWith(prefix + ".") ||
205
211
  fieldPath.startsWith(prefix + "[");
206
212
 
213
+ /** The per-declaring-module alias tables, when the caller threaded them. A
214
+ * nested kind inside an imported library's definition is written through THAT
215
+ * library's aliases. */
216
+ function moduleScopes(options: VisitOptions): ModuleScopes | undefined {
217
+ return options.aliasesByModule && options.rootModules
218
+ ? { aliasesByModule: options.aliasesByModule, rootModules: options.rootModules }
219
+ : undefined;
220
+ }
221
+
207
222
  export function visitManifest(
208
223
  resources: ResourceManifest[],
209
224
  registry: DefinitionRegistry,
@@ -359,7 +374,32 @@ export function visitManifest(
359
374
  }
360
375
 
361
376
  if (wantsCel) {
362
- const contexts = definition?.schema ? extractContextsFromSchema(definition.schema) : [];
377
+ // The INHERITANCE-RESOLVED schema: an `extends` child is authored against
378
+ // merge(parent, own), so a context region the parent declares is in force
379
+ // on the child. Reading the child's own schema typed the region on
380
+ // whichever kind happened to declare it and left every descendant's
381
+ // expressions unchecked — silently, which for a typing rule means the
382
+ // check simply stops existing.
383
+ const authorSchema = registry.effectiveSchemaOf(definition);
384
+ const contexts = authorSchema ? extractContextsFromSchema(authorSchema) : [];
385
+ // A `Telo.Definition`'s `resources:` entries are DECLARATIONS of another
386
+ // kind, so the CEL inside one belongs to THAT kind — its own
387
+ // `x-telo-context` regions, rebased under the entry's path. `self` is
388
+ // merged into each, since the nested kind knows nothing about the
389
+ // configuration its enclosing template was given. Without this a nested
390
+ // body saw one fixed permissive context: `inputs` and `item` undefined
391
+ // wherever the nested kind declares them, `error` offered everywhere.
392
+ for (const body of templateBodies(r, registry, aliases, moduleScopes(options))) {
393
+ const bodySchema = registry.effectiveSchemaOf(body.definition);
394
+ if (!bodySchema) continue;
395
+ for (const ctx of extractContextsFromSchema(bodySchema, body.scopePrefix)) {
396
+ contexts.push({ scope: ctx.scope, schema: withTemplateSelf(ctx.schema) });
397
+ }
398
+ }
399
+ // Longest scope wins, as `extractContextsFromSchema` already orders its
400
+ // own: a nested kind's region is deeper than the enclosing definition's
401
+ // blanket `$.resources[*]`, so it takes precedence.
402
+ contexts.sort((a, b) => b.scope.length - a.scope.length);
363
403
  walkCelExpressions(r, "", (expr, path, engineName, surface) => {
364
404
  let contextSchema: Record<string, any> | undefined;
365
405
  let matchedScope: string | undefined;
@@ -2,9 +2,11 @@
2
2
  // Source: analyzer/migrations/*.json (lexically ordered).
3
3
  import e0 from "./normalize-value-types.json" with { type: "json" };
4
4
  import e1 from "./ref-slot-scalar-type.json" with { type: "json" };
5
+ import e2 from "./schema-prepare-bucket.json" with { type: "json" };
5
6
 
6
7
  /** Every entry file, in the order the driver applies them. */
7
8
  export const MIGRATION_ENTRY_FILES: ReadonlyArray<readonly [file: string, data: unknown]> = [
8
9
  ["normalize-value-types.json", e0],
9
10
  ["ref-slot-scalar-type.json", e1],
11
+ ["schema-prepare-bucket.json", e2],
10
12
  ];
@@ -0,0 +1,27 @@
1
+ {
2
+ "$comment": "`inKind` names the two backend schema kinds by their canonical `<module>.<Kind>` spelling, which is what a document's `kind:` carries after alias resolution. `under` is the key itself: the bucket is a top-level field of the resource, so the region and the match are the same node \u2014 the tightest containment this vocabulary can state. NAMING A MODULE'S KIND FROM A CORE ENTRY IS A DEBT, not a pattern: it is the one place the analyzer knows a standard-library kind by name. It is contained because `inKind` is a filter \u2014 an entry naming an absent kind matches nothing \u2014 and it MOVES into `modules/postgres` and `modules/sqlite` when the module-migration surface lands. Do not add another; see analyzer/migrations/README.md.",
3
+ "id": "schema-prepare-bucket",
4
+ "code": "SQL_BEFORE_MIGRATIONS_RENAMED",
5
+ "severity": "warning",
6
+ "reason": "`beforeMigrations:` is now `prepare:`. Every documented use of it was data preparation for a narrowing the reconciliation pass is about to attempt \u2014 backfill before NOT NULL, make values fit before a length reduction, convert before a non-widening type change \u2014 and each is what a refusal already tells the author to write. The old name said where it ran relative to the OTHER bucket; the new one says what it is for, which is what those refusals need to name. Nothing re-runs: the ledger stores the migration key alone, so moving an entry between the two maps changes no history.",
7
+ "rules": [
8
+ {
9
+ "match": {
10
+ "key": "beforeMigrations",
11
+ "inKind": [
12
+ "Postgres.Schema",
13
+ "SQLite.Schema"
14
+ ],
15
+ "under": [
16
+ "beforeMigrations"
17
+ ]
18
+ },
19
+ "patch": [
20
+ {
21
+ "op": "rename-key",
22
+ "to": "prepare"
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }