@telorun/analyzer 0.66.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 (76) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +37 -2
  3. package/dist/builtins.d.ts.map +1 -1
  4. package/dist/builtins.js +67 -16
  5. package/dist/cel-scope.d.ts +8 -0
  6. package/dist/cel-scope.d.ts.map +1 -1
  7. package/dist/cel-scope.js +66 -8
  8. package/dist/definition-registry.d.ts +17 -0
  9. package/dist/definition-registry.d.ts.map +1 -1
  10. package/dist/definition-registry.js +35 -0
  11. package/dist/dependency-graph.d.ts.map +1 -1
  12. package/dist/dependency-graph.js +65 -0
  13. package/dist/flatten-for-analyzer.d.ts +36 -0
  14. package/dist/flatten-for-analyzer.d.ts.map +1 -1
  15. package/dist/flatten-for-analyzer.js +103 -4
  16. package/dist/index.d.ts +4 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -2
  19. package/dist/inline-imports.d.ts.map +1 -1
  20. package/dist/inline-imports.js +1 -0
  21. package/dist/manifest-visitor.d.ts +4 -0
  22. package/dist/manifest-visitor.d.ts.map +1 -1
  23. package/dist/manifest-visitor.js +28 -0
  24. package/dist/precompile.d.ts.map +1 -1
  25. package/dist/precompile.js +8 -0
  26. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  27. package/dist/resolve-ref-sentinels.js +14 -1
  28. package/dist/resource-input.d.ts +75 -0
  29. package/dist/resource-input.d.ts.map +1 -0
  30. package/dist/resource-input.js +90 -0
  31. package/dist/schema-projection.d.ts +13 -0
  32. package/dist/schema-projection.d.ts.map +1 -1
  33. package/dist/schema-projection.js +7 -0
  34. package/dist/system-kinds.d.ts +7 -2
  35. package/dist/system-kinds.d.ts.map +1 -1
  36. package/dist/system-kinds.js +7 -2
  37. package/dist/telo-version.d.ts +1 -1
  38. package/dist/telo-version.js +1 -1
  39. package/dist/template-body.d.ts +50 -0
  40. package/dist/template-body.d.ts.map +1 -0
  41. package/dist/template-body.js +58 -0
  42. package/dist/validate-cel-context.d.ts.map +1 -1
  43. package/dist/validate-cel-context.js +68 -8
  44. package/dist/validate-identifier-names.d.ts.map +1 -1
  45. package/dist/validate-identifier-names.js +17 -2
  46. package/dist/validate-references.d.ts +17 -0
  47. package/dist/validate-references.d.ts.map +1 -1
  48. package/dist/validate-references.js +68 -16
  49. package/dist/validate-resource-inputs.d.ts +35 -0
  50. package/dist/validate-resource-inputs.d.ts.map +1 -0
  51. package/dist/validate-resource-inputs.js +319 -0
  52. package/dist/validate-template-dispatch.d.ts +27 -0
  53. package/dist/validate-template-dispatch.d.ts.map +1 -0
  54. package/dist/validate-template-dispatch.js +95 -0
  55. package/package.json +3 -3
  56. package/src/analyzer.ts +45 -2
  57. package/src/builtins.ts +69 -16
  58. package/src/cel-scope.ts +90 -14
  59. package/src/definition-registry.ts +36 -0
  60. package/src/dependency-graph.ts +66 -0
  61. package/src/flatten-for-analyzer.ts +116 -3
  62. package/src/index.ts +12 -0
  63. package/src/inline-imports.ts +1 -0
  64. package/src/manifest-visitor.ts +33 -0
  65. package/src/precompile.ts +8 -0
  66. package/src/resolve-ref-sentinels.ts +12 -1
  67. package/src/resource-input.ts +132 -0
  68. package/src/schema-projection.ts +19 -0
  69. package/src/system-kinds.ts +7 -2
  70. package/src/telo-version.ts +1 -1
  71. package/src/template-body.ts +104 -0
  72. package/src/validate-cel-context.ts +67 -7
  73. package/src/validate-identifier-names.ts +18 -3
  74. package/src/validate-references.ts +70 -14
  75. package/src/validate-resource-inputs.ts +367 -0
  76. package/src/validate-template-dispatch.ts +99 -0
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" },
package/src/cel-scope.ts CHANGED
@@ -42,6 +42,7 @@ import {
42
42
  } from "./kernel-globals.js";
43
43
  import { gatherPropertySchemas, resolveLocalRef, walkStepArray } from "./schema-walk.js";
44
44
  import { readStepSlot } from "./step-slot.js";
45
+ import { bodyForPath, templateBodies } from "./template-body.js";
45
46
  import {
46
47
  getManifestItem,
47
48
  resolveContextAnnotations,
@@ -415,6 +416,20 @@ export class CelScopeResolver {
415
416
  private stepContext: Record<string, any> | undefined;
416
417
  private invocationContext: Record<string, any> | undefined;
417
418
  private errorScopes: Map<string, Record<string, any>> = new Map();
419
+ /** The same two facts per `resources:` entry of a `Telo.Definition` — a
420
+ * template body is a declaration of ANOTHER kind, so its step body and its
421
+ * error branches are that kind's, not the enclosing definition's. */
422
+ private bodyScopes: Array<{
423
+ prefix: string;
424
+ scopePrefix: string;
425
+ manifest: Record<string, any>;
426
+ stepContext: Record<string, any> | undefined;
427
+ errorScopes: Map<string, Record<string, any>>;
428
+ }> = [];
429
+ /** The enclosing definition's `self` schema, resolved once per resource. A
430
+ * template body's contexts resolve against the BODY, so `self` — the one
431
+ * binding anchored on the definition — is substituted before they do. */
432
+ private selfSchema: Record<string, any> | undefined;
418
433
 
419
434
  constructor(private readonly inputs: CelScopeInputs) {}
420
435
 
@@ -462,6 +477,27 @@ export class CelScopeResolver {
462
477
  )
463
478
  : undefined;
464
479
  this.errorScopes = collectErrorContextScopes(authorSchema);
480
+ this.selfSchema =
481
+ m.kind === "Telo.Definition" ? buildSelfSchema(m as Record<string, any>, defs, aliases) : undefined;
482
+ this.bodyScopes = templateBodies(m, defs, aliases, scopes).map((body) => {
483
+ const bodySchema = defs.effectiveSchemaOf(body.definition) as Record<string, any> | undefined;
484
+ return {
485
+ prefix: body.prefix,
486
+ scopePrefix: body.scopePrefix,
487
+ manifest: body.manifest as Record<string, any>,
488
+ stepContext: bodySchema
489
+ ? buildStepContextSchema(
490
+ body.manifest as Record<string, any>,
491
+ bodySchema,
492
+ allManifests as Record<string, any>[],
493
+ defs,
494
+ aliases,
495
+ scopes,
496
+ )
497
+ : undefined,
498
+ errorScopes: collectErrorContextScopes(bodySchema),
499
+ };
500
+ });
465
501
  }
466
502
 
467
503
  /**
@@ -518,7 +554,14 @@ export class CelScopeResolver {
518
554
  const m = site.source;
519
555
  let matched: Record<string, any> | undefined = site.contextSchema ?? this.invocationContext;
520
556
 
521
- if (this.stepContext) {
557
+ // Inside a template body the step and error regions are the NESTED kind's.
558
+ // Its `steps` accumulator and its `catch:` branches are declared there, and
559
+ // the enclosing definition's (there are none) would say nothing about them.
560
+ const inBody = this.bodyScopes.length > 0 ? bodyForPath(this.bodyScopes, path) : undefined;
561
+ const stepContext = inBody ? inBody.stepContext : this.stepContext;
562
+ const errorScopes = inBody ? inBody.errorScopes : this.errorScopes;
563
+
564
+ if (stepContext) {
522
565
  const base = matched ?? { type: "object", properties: {}, additionalProperties: true };
523
566
  matched = {
524
567
  ...base,
@@ -535,7 +578,7 @@ export class CelScopeResolver {
535
578
  // which is a separate decision from knowing the name is legal.
536
579
  inputs: { type: "object", additionalProperties: true },
537
580
  ...(base.properties ?? {}),
538
- steps: this.stepContext,
581
+ steps: stepContext,
539
582
  },
540
583
  };
541
584
  }
@@ -543,7 +586,7 @@ export class CelScopeResolver {
543
586
  // `error` is only in scope inside an error-bearing branch (e.g. a
544
587
  // `catch:` / `finally:`), so it's merged per-path, not resource-wide.
545
588
  const errorSchema =
546
- this.errorScopes.size > 0 ? errorContextForPath(path, this.errorScopes) : undefined;
589
+ errorScopes.size > 0 ? errorContextForPath(path, errorScopes) : undefined;
547
590
  if (errorSchema) {
548
591
  const base = matched ?? { type: "object", properties: {}, additionalProperties: true };
549
592
  matched = {
@@ -561,16 +604,49 @@ export class CelScopeResolver {
561
604
  }
562
605
 
563
606
  const { defs, aliases, scopes, allManifests, kernelGlobals } = this.inputs;
564
- const manifestItem = site.matchedScope
565
- ? getManifestItem(path, site.matchedScope, m as Record<string, any>)
566
- : (m as Record<string, any>);
567
- const rootForResolver = manifestRootForResolver(
568
- m as Record<string, any>,
569
- defs,
570
- aliases,
571
- allManifests as Record<string, any>[],
572
- scopes,
573
- );
607
+
608
+ // A template body's context annotations are the NESTED kind's, and every one
609
+ // that anchors at a root — `x-telo-context-element-from`,
610
+ // `-collection-from`, `-from-root`, `x-telo-bindings-from` — means the root
611
+ // of the DECLARATION they were written for. Resolving them against the
612
+ // enclosing `Telo.Definition` looked `collection:` up on a document that has
613
+ // no such field, so `item` typed open and a typo below it went unreported —
614
+ // the one place a nested declaration did not answer as the same declaration
615
+ // written at the top level.
616
+ //
617
+ // So the path and the scope are rebased into the body and the body becomes
618
+ // the resolution root. `self` is the single binding that genuinely belongs to
619
+ // the enclosing definition, and it is substituted below rather than left as
620
+ // an annotation the rebased root would misread.
621
+ const body = inBody;
622
+ const localPath = body ? path.slice(body.prefix.length + 1) : path;
623
+ const localScope =
624
+ body && site.matchedScope?.startsWith(`${body.scopePrefix}.`)
625
+ ? `$.${site.matchedScope.slice(body.scopePrefix.length + 1)}`
626
+ : body
627
+ ? undefined
628
+ : site.matchedScope;
629
+ const rootManifest = body ? body.manifest : (m as Record<string, any>);
630
+
631
+ if (body && this.selfSchema && matched.properties?.self) {
632
+ matched = {
633
+ ...matched,
634
+ properties: { ...matched.properties, self: this.selfSchema },
635
+ };
636
+ }
637
+
638
+ const manifestItem = localScope
639
+ ? getManifestItem(localPath, localScope, rootManifest)
640
+ : rootManifest;
641
+ const rootForResolver = body
642
+ ? rootManifest
643
+ : manifestRootForResolver(
644
+ m as Record<string, any>,
645
+ defs,
646
+ aliases,
647
+ allManifests as Record<string, any>[],
648
+ scopes,
649
+ );
574
650
  const resolved = resolveContextAnnotations(matched, manifestItem, {
575
651
  manifestRoot: rootForResolver,
576
652
  defs,
@@ -578,7 +654,7 @@ export class CelScopeResolver {
578
654
  allManifests: allManifests as Record<string, any>[],
579
655
  });
580
656
  return mergeKernelGlobalsIntoContext(
581
- withBindingNames(resolved, m as Record<string, any>),
657
+ withBindingNames(resolved, rootManifest),
582
658
  // Typed in the module that DECLARED this resource — for a manifest
583
659
  // forwarded from an imported library, that is its `moduleGlobals` stamp,
584
660
  // not the consuming application's block.
@@ -449,6 +449,42 @@ export class DefinitionRegistry {
449
449
  return buildFieldMapAtPath(subSchema, fieldPath);
450
450
  }
451
451
 
452
+ /** The kinds a definition descends from DIRECTLY — the same two edges
453
+ * `register` feeds into `extendedBy`, read the other way.
454
+ *
455
+ * Both spellings, because either can carry the edge that satisfies a slot:
456
+ * `capability:` is the legacy implements-this form, `extends:` the canonical
457
+ * one. A consumer walking these by hand would be a second reading of what an
458
+ * inheritance edge IS, and the downward index and the upward walk have to
459
+ * agree about that forever. */
460
+ parentsOf(kind: string): string[] {
461
+ const def = this.defs.get(kind);
462
+ if (!def) return [];
463
+ return [def.capability, def.extends].filter(
464
+ (parent): parent is string => typeof parent === "string" && parent.length > 0,
465
+ );
466
+ }
467
+
468
+ /** Whether every kind this one descends from is registered.
469
+ *
470
+ * When it is, what the kind implements is FULLY KNOWN: a target it does not
471
+ * reach is one it genuinely does not implement, so a mismatch is a verdict.
472
+ * When a hop is missing — an unimported abstract, an alias the declaring file
473
+ * could not resolve — a mismatch cannot be told from a missing dependency,
474
+ * which is the partial context every reference check stays lenient in. */
475
+ ancestryResolved(kind: string): boolean {
476
+ const queue = [kind];
477
+ const seen = new Set<string>();
478
+ while (queue.length > 0) {
479
+ const current = queue.shift()!;
480
+ if (seen.has(current)) continue;
481
+ seen.add(current);
482
+ if (!this.defs.has(current)) return false;
483
+ queue.push(...this.parentsOf(current));
484
+ }
485
+ return true;
486
+ }
487
+
452
488
  /** Returns all definitions that transitively extend the given abstract kind.
453
489
  * Follows the capability chain to any depth (equivalent to instanceof in OOP).
454
490
  * Definitions are included regardless of registration order. */
@@ -2,6 +2,7 @@ import type { ResourceManifest } from "@telorun/sdk";
2
2
  import type { AliasResolver } from "./alias-resolver.js";
3
3
  import { buildCallGraph, projectToPairs, type ResourceGraphNode } from "./call-graph.js";
4
4
  import type { DefinitionRegistry } from "./definition-registry.js";
5
+ import { readSuppliedResources } from "./resource-input.js";
5
6
 
6
7
  export interface ResourceNode {
7
8
  kind: string;
@@ -67,6 +68,8 @@ export function buildDependencyGraph(
67
68
  }
68
69
  for (const key of nodes.keys()) if (!deps.has(key)) deps.set(key, new Set());
69
70
 
71
+ addResourceInputEdges(resources, nodes, deps);
72
+
70
73
  // --- Kahn's topological sort ---
71
74
  // in-degree[X] = number of X's dependencies (size of deps[X])
72
75
  // reverse[dep] = set of nodes that depend on dep (for degree decrement)
@@ -164,3 +167,66 @@ function findCycle(
164
167
 
165
168
  return [];
166
169
  }
170
+
171
+ /**
172
+ * Boot-order edges for the one thing a `Telo.Import` does hold: the instances it
173
+ * hands DOWN to its target library's declared `resources:` inputs.
174
+ *
175
+ * An import is otherwise module wiring rather than a runtime node — it is in
176
+ * `DEPENDENCY_GRAPH_SKIP_KINDS` and the call graph gives it no node at all — but
177
+ * a borrowed instance must exist before the import initializes, and a cycle
178
+ * through one is a cycle like any other. The edges are added here rather than
179
+ * read off the reference field map because the accepted KIND at this slot is
180
+ * declared by the TARGET library, not by the `Telo.Import` schema, so there is
181
+ * no `x-telo-ref` for the map to read; the constraint itself is checked by
182
+ * `validate-resource-inputs`.
183
+ */
184
+ function addResourceInputEdges(
185
+ resources: ResourceManifest[],
186
+ nodes: Map<string, ResourceNode>,
187
+ deps: Map<string, Set<string>>,
188
+ ): void {
189
+ // Every import that supplies inputs becomes a node FIRST, so a cross-module
190
+ // reference below can resolve to the import that exports its target.
191
+ const imports: Array<{ key: string; supplied: Record<string, unknown> }> = [];
192
+ for (const m of resources) {
193
+ if (m.kind !== "Telo.Import") continue;
194
+ const alias = m.metadata?.name as string | undefined;
195
+ const supplied = readSuppliedResources(m);
196
+ if (!alias || Object.keys(supplied).length === 0) continue;
197
+ const key = nodeKey(m.kind, alias);
198
+ nodes.set(key, { kind: m.kind, name: alias });
199
+ if (!deps.has(key)) deps.set(key, new Set<string>());
200
+ imports.push({ key, supplied });
201
+ }
202
+ if (imports.length === 0) return;
203
+
204
+ const byName = new Map<string, string>();
205
+ for (const [key, node] of nodes) byName.set(node.name, key);
206
+ // An import is keyed by its alias, and that is also how a cross-module
207
+ // reference names it — index those so one resolves.
208
+ for (const m of resources) {
209
+ if (m.kind !== "Telo.Import") continue;
210
+ const alias = m.metadata?.name as string | undefined;
211
+ const key = alias ? nodeKey(m.kind, alias) : undefined;
212
+ if (alias && key && nodes.has(key)) byName.set(alias, key);
213
+ }
214
+
215
+ for (const { key, supplied } of imports) {
216
+ const set = deps.get(key)!;
217
+ for (const value of Object.values(supplied)) {
218
+ const ref = value as { name?: unknown; alias?: unknown } | undefined;
219
+ // A CROSS-MODULE reference (`!ref Other.db`) names an instance exported by
220
+ // another import, never a local resource. Looking it up in the local name
221
+ // map would find an unrelated resource of the same name — a wrong edge,
222
+ // and possibly a phantom cycle — or nothing at all. What it depends on is
223
+ // the IMPORT that exports it, which is the projection
224
+ // `localDependencyNames` already makes at runtime.
225
+ const alias = typeof ref?.alias === "string" ? ref.alias : undefined;
226
+ const targetName =
227
+ alias && alias !== "Self" ? alias : typeof ref?.name === "string" ? ref.name : undefined;
228
+ const target = targetName ? byName.get(targetName) : undefined;
229
+ if (target && target !== key) set.add(target);
230
+ }
231
+ }
232
+ }
@@ -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
@@ -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 {
package/src/index.ts CHANGED
@@ -21,6 +21,9 @@ 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";
@@ -31,9 +34,18 @@ export {
31
34
  declaresCelRegion,
32
35
  evalPathCovers,
33
36
  mergeCelEvalSites,
37
+ pathMatchesScope,
34
38
  NO_CEL_EVAL_SITES,
35
39
  } from "./eval-paths.js";
36
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";
37
49
  export {
38
50
  BINDINGS_ANNOTATION,
39
51
  bindingContextProperties,
@@ -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;