@telorun/analyzer 0.66.0 → 0.68.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 (99) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +60 -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/extends-resolution.d.ts +20 -0
  14. package/dist/extends-resolution.d.ts.map +1 -1
  15. package/dist/extends-resolution.js +29 -0
  16. package/dist/flatten-for-analyzer.d.ts +36 -0
  17. package/dist/flatten-for-analyzer.d.ts.map +1 -1
  18. package/dist/flatten-for-analyzer.js +103 -4
  19. package/dist/index.d.ts +8 -4
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +6 -4
  22. package/dist/inline-imports.d.ts.map +1 -1
  23. package/dist/inline-imports.js +1 -0
  24. package/dist/invocation-contract.d.ts +20 -0
  25. package/dist/invocation-contract.d.ts.map +1 -1
  26. package/dist/invocation-contract.js +74 -0
  27. package/dist/manifest-visitor.d.ts +4 -0
  28. package/dist/manifest-visitor.d.ts.map +1 -1
  29. package/dist/manifest-visitor.js +28 -0
  30. package/dist/precompile.d.ts.map +1 -1
  31. package/dist/precompile.js +8 -0
  32. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  33. package/dist/resolve-ref-sentinels.js +14 -1
  34. package/dist/resource-input.d.ts +75 -0
  35. package/dist/resource-input.d.ts.map +1 -0
  36. package/dist/resource-input.js +90 -0
  37. package/dist/schema-keywords.d.ts +16 -1
  38. package/dist/schema-keywords.d.ts.map +1 -1
  39. package/dist/schema-keywords.js +20 -0
  40. package/dist/schema-projection.d.ts +13 -0
  41. package/dist/schema-projection.d.ts.map +1 -1
  42. package/dist/schema-projection.js +7 -0
  43. package/dist/system-kinds.d.ts +7 -2
  44. package/dist/system-kinds.d.ts.map +1 -1
  45. package/dist/system-kinds.js +7 -2
  46. package/dist/telo-version.d.ts +1 -1
  47. package/dist/telo-version.js +1 -1
  48. package/dist/template-body.d.ts +50 -0
  49. package/dist/template-body.d.ts.map +1 -0
  50. package/dist/template-body.js +58 -0
  51. package/dist/validate-cel-context.d.ts.map +1 -1
  52. package/dist/validate-cel-context.js +68 -8
  53. package/dist/validate-extends.d.ts.map +1 -1
  54. package/dist/validate-extends.js +42 -1
  55. package/dist/validate-identifier-names.d.ts.map +1 -1
  56. package/dist/validate-identifier-names.js +17 -2
  57. package/dist/validate-references.d.ts +17 -0
  58. package/dist/validate-references.d.ts.map +1 -1
  59. package/dist/validate-references.js +68 -16
  60. package/dist/validate-resource-inputs.d.ts +35 -0
  61. package/dist/validate-resource-inputs.d.ts.map +1 -0
  62. package/dist/validate-resource-inputs.js +319 -0
  63. package/dist/validate-sensitive-slots.d.ts +28 -0
  64. package/dist/validate-sensitive-slots.d.ts.map +1 -0
  65. package/dist/validate-sensitive-slots.js +75 -0
  66. package/dist/validate-template-dispatch.d.ts +27 -0
  67. package/dist/validate-template-dispatch.d.ts.map +1 -0
  68. package/dist/validate-template-dispatch.js +95 -0
  69. package/dist/value-type-keyword.d.ts +1 -1
  70. package/dist/value-type-keyword.d.ts.map +1 -1
  71. package/dist/value-type-keyword.js +1 -0
  72. package/package.json +3 -3
  73. package/src/analyzer.ts +71 -2
  74. package/src/builtins.ts +69 -16
  75. package/src/cel-scope.ts +90 -14
  76. package/src/definition-registry.ts +36 -0
  77. package/src/dependency-graph.ts +66 -0
  78. package/src/extends-resolution.ts +33 -0
  79. package/src/flatten-for-analyzer.ts +116 -3
  80. package/src/index.ts +16 -0
  81. package/src/inline-imports.ts +1 -0
  82. package/src/invocation-contract.ts +71 -0
  83. package/src/manifest-visitor.ts +33 -0
  84. package/src/precompile.ts +8 -0
  85. package/src/resolve-ref-sentinels.ts +12 -1
  86. package/src/resource-input.ts +132 -0
  87. package/src/schema-keywords.ts +33 -1
  88. package/src/schema-projection.ts +19 -0
  89. package/src/system-kinds.ts +7 -2
  90. package/src/telo-version.ts +1 -1
  91. package/src/template-body.ts +104 -0
  92. package/src/validate-cel-context.ts +67 -7
  93. package/src/validate-extends.ts +51 -1
  94. package/src/validate-identifier-names.ts +18 -3
  95. package/src/validate-references.ts +70 -14
  96. package/src/validate-resource-inputs.ts +367 -0
  97. package/src/validate-sensitive-slots.ts +109 -0
  98. package/src/validate-template-dispatch.ts +99 -0
  99. package/src/value-type-keyword.ts +1 -0
@@ -0,0 +1,75 @@
1
+ import { SCHEMA_REGION_KEYS } from "./schema-region.js";
2
+ const ANNOTATION = "x-telo-sensitive";
3
+ /** The only regions the kernel reads the annotation from. */
4
+ const CONTRACT_KEYS = new Set(["inputType", "outputType"]);
5
+ export function validateSensitiveSlots(manifest) {
6
+ const issues = [];
7
+ const walk = (node, path, seen) => {
8
+ if (!node || typeof node !== "object")
9
+ return;
10
+ if (seen.has(node))
11
+ return;
12
+ seen.add(node);
13
+ if (Array.isArray(node)) {
14
+ node.forEach((child, i) => walk(child, [...path, i], seen));
15
+ return;
16
+ }
17
+ const record = node;
18
+ if (Object.hasOwn(record, ANNOTATION)) {
19
+ const dotted = path.join(".");
20
+ if (record[ANNOTATION] !== true) {
21
+ issues.push({
22
+ code: "SENSITIVE_ANNOTATION_INVALID",
23
+ manifest,
24
+ path: dotted,
25
+ message: `'${ANNOTATION}' must be \`true\`; got ${JSON.stringify(record[ANNOTATION])}. ` +
26
+ `It is a marker, not a level — a value other than \`true\` reads as "not sensitive".`,
27
+ });
28
+ }
29
+ else {
30
+ const region = enclosingSchemaRegion(path);
31
+ if (region === undefined || !CONTRACT_KEYS.has(region)) {
32
+ issues.push({
33
+ code: "SENSITIVE_ANNOTATION_MISPLACED",
34
+ manifest,
35
+ path: dotted,
36
+ message: `'${ANNOTATION}' is only read from a resource's declared contract ` +
37
+ `(\`inputType\` / \`outputType\`), and this node is ` +
38
+ (region === undefined
39
+ ? "not inside a schema at all"
40
+ : `inside \`${region}\``) +
41
+ `. The kernel will not redact it, so the value would still reach trace ` +
42
+ `payloads and the debug wire. Move the mark onto the contract property ` +
43
+ `that carries the value.`,
44
+ });
45
+ }
46
+ }
47
+ }
48
+ for (const [key, child] of Object.entries(record)) {
49
+ walk(child, [...path, key], seen);
50
+ }
51
+ };
52
+ walk(manifest, [], new Set());
53
+ return issues;
54
+ }
55
+ /**
56
+ * The OUTERMOST segment naming a schema-valued key, or `undefined` when the node
57
+ * is not inside a schema.
58
+ *
59
+ * Outermost, not nearest, because a contract is routinely written in the inline
60
+ * `{kind: Telo.JsonSchema, schema: …}` form — so the path to a marked property is
61
+ * `outputType.schema.properties.headers`, and the nearest region key is that
62
+ * wrapper's own `schema`. Reading it as a kind's configuration would report every
63
+ * correctly-marked contract as misplaced, which is how this check first behaved.
64
+ *
65
+ * It is the same reasoning `expandManifestFragments` uses when it keys on the
66
+ * top-level slot: the object a contract resolver is handed is the one under
67
+ * `inputType` / `outputType`, whatever nesting the authoring form adds beneath.
68
+ */
69
+ function enclosingSchemaRegion(path) {
70
+ for (const segment of path) {
71
+ if (typeof segment === "string" && SCHEMA_REGION_KEYS.includes(segment))
72
+ return segment;
73
+ }
74
+ return undefined;
75
+ }
@@ -0,0 +1,27 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+ import { type AnalysisDiagnostic } from "./types.js";
3
+ /**
4
+ * A `!ref` at a definition's dispatch slot must name a sibling `resources:`
5
+ * entry.
6
+ *
7
+ * `!ref` is the one spelling an author — and the editor's rename, completion and
8
+ * go-to-definition — expects to be RESOLVED. But a `Telo.Definition` is in both
9
+ * `REF_VALIDATION_SKIP_KINDS` and `REF_RESOLUTION_SKIP_KINDS`, and the dispatch
10
+ * slots carry no `x-telo-ref` (the accepted targets are the definition's own
11
+ * template-internal entries, not resources of any module), so no reference pass
12
+ * reaches them. Introducing the tag at a slot nothing resolves would be worse
13
+ * than the string form it replaces: a typo that used to be an obvious runtime
14
+ * miss becomes a typo in a construct that advertises static resolution.
15
+ *
16
+ * Decidable only when every sibling name is LITERAL. A template routinely names
17
+ * its entries with CEL (`name: !cel "self.name + '-query'"`), and an expression
18
+ * could expand to the referenced name — so one dynamic sibling switches the
19
+ * check off for that definition rather than inventing a miss.
20
+ *
21
+ * Entry-module-scoped, like every other declaration check: a published
22
+ * dependency's template body is not the consumer's to fix.
23
+ *
24
+ * Browser-safe.
25
+ */
26
+ export declare function validateTemplateDispatch(manifests: ResourceManifest[], rootModules: ReadonlySet<string>): AnalysisDiagnostic[];
27
+ //# sourceMappingURL=validate-template-dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-template-dispatch.d.ts","sourceRoot":"","sources":["../src/validate-template-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AASzE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,GAC/B,kBAAkB,EAAE,CAgDtB"}
@@ -0,0 +1,95 @@
1
+ import { isRefSentinel } from "@telorun/templating";
2
+ import { distance } from "./levenshtein.js";
3
+ import { DiagnosticSeverity } from "./types.js";
4
+ const SOURCE = "telo-analyzer";
5
+ /** The four slots a `Telo.Definition` names its dispatch target in. Each takes
6
+ * the same grammar; which one is legal for a given capability is
7
+ * `validate-provider-coherence`'s question, not this one's. */
8
+ const DISPATCH_FIELDS = ["invoke", "run", "provide", "mount"];
9
+ /**
10
+ * A `!ref` at a definition's dispatch slot must name a sibling `resources:`
11
+ * entry.
12
+ *
13
+ * `!ref` is the one spelling an author — and the editor's rename, completion and
14
+ * go-to-definition — expects to be RESOLVED. But a `Telo.Definition` is in both
15
+ * `REF_VALIDATION_SKIP_KINDS` and `REF_RESOLUTION_SKIP_KINDS`, and the dispatch
16
+ * slots carry no `x-telo-ref` (the accepted targets are the definition's own
17
+ * template-internal entries, not resources of any module), so no reference pass
18
+ * reaches them. Introducing the tag at a slot nothing resolves would be worse
19
+ * than the string form it replaces: a typo that used to be an obvious runtime
20
+ * miss becomes a typo in a construct that advertises static resolution.
21
+ *
22
+ * Decidable only when every sibling name is LITERAL. A template routinely names
23
+ * its entries with CEL (`name: !cel "self.name + '-query'"`), and an expression
24
+ * could expand to the referenced name — so one dynamic sibling switches the
25
+ * check off for that definition rather than inventing a miss.
26
+ *
27
+ * Entry-module-scoped, like every other declaration check: a published
28
+ * dependency's template body is not the consumer's to fix.
29
+ *
30
+ * Browser-safe.
31
+ */
32
+ export function validateTemplateDispatch(manifests, rootModules) {
33
+ const out = [];
34
+ for (const m of manifests) {
35
+ if (m.kind !== "Telo.Definition")
36
+ continue;
37
+ const meta = m.metadata;
38
+ const name = meta?.name;
39
+ if (!name)
40
+ continue;
41
+ if (meta?.module && !rootModules.has(meta.module))
42
+ continue;
43
+ const bodies = m.resources;
44
+ if (!Array.isArray(bodies))
45
+ continue;
46
+ const siblings = [];
47
+ let anyDynamic = false;
48
+ for (const entry of bodies) {
49
+ const entryName = entry?.metadata?.name;
50
+ if (typeof entryName === "string" && !entryName.includes("${{"))
51
+ siblings.push(entryName);
52
+ else if (entryName !== undefined)
53
+ anyDynamic = true;
54
+ }
55
+ if (anyDynamic)
56
+ continue;
57
+ for (const field of DISPATCH_FIELDS) {
58
+ const value = m[field];
59
+ if (!isRefSentinel(value))
60
+ continue;
61
+ const source = value.source;
62
+ const target = source.startsWith("Self.") ? source.slice("Self.".length) : source;
63
+ if (siblings.includes(target))
64
+ continue;
65
+ const suggestion = nearest(target, siblings);
66
+ out.push({
67
+ severity: DiagnosticSeverity.Error,
68
+ code: "TEMPLATE_DISPATCH_UNKNOWN",
69
+ source: SOURCE,
70
+ message: `${m.kind}/${name}: '${field}: !ref ${source}' names no entry in 'resources:'. ` +
71
+ `Available: ${siblings.join(", ") || "(none)"}.` +
72
+ (suggestion ? ` Did you mean '${suggestion}'?` : ""),
73
+ data: {
74
+ resource: { kind: m.kind, name },
75
+ filePath: meta?.source,
76
+ path: field,
77
+ ...(suggestion ? { fix: { replacement: suggestion } } : {}),
78
+ },
79
+ });
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+ /** The closest sibling name, when one is close enough to be a typo rather than a
85
+ * different name. */
86
+ function nearest(target, candidates) {
87
+ let best;
88
+ for (const candidate of candidates) {
89
+ const d = distance(target, candidate);
90
+ if (!best || d < best.d)
91
+ best = { name: candidate, d };
92
+ }
93
+ const limit = Math.max(1, Math.floor(target.length / 3));
94
+ return best && best.d <= limit ? best.name : undefined;
95
+ }
@@ -28,7 +28,7 @@ import type { KeywordDefinition } from "ajv";
28
28
  *
29
29
  * `x-telo-type` is deliberately absent — it is the one that emits code.
30
30
  */
31
- export declare const ANNOTATION_KEYWORDS: readonly ["x-telo-bindings-from", "x-telo-catches-for", "x-telo-context", "x-telo-context-collection-from", "x-telo-context-element-from", "x-telo-context-from", "x-telo-context-from-ref-kind", "x-telo-context-from-root", "x-telo-context-ref-from", "x-telo-error-context", "x-telo-eval", "x-telo-inline", "x-telo-outcome-list", "x-telo-provides-zone", "x-telo-ref", "x-telo-requires-zone", "x-telo-resource-rules", "x-telo-schema-from", "x-telo-schema-map", "x-telo-schema-projection", "x-telo-schema-projection-from", "x-telo-scope", "x-telo-step-context", "x-telo-topology-role", "x-telo-value-schema-from", "x-telo-widget"];
31
+ export declare const ANNOTATION_KEYWORDS: readonly ["x-telo-bindings-from", "x-telo-catches-for", "x-telo-context", "x-telo-context-collection-from", "x-telo-context-element-from", "x-telo-context-from", "x-telo-context-from-ref-kind", "x-telo-context-from-root", "x-telo-context-ref-from", "x-telo-error-context", "x-telo-eval", "x-telo-inline", "x-telo-outcome-list", "x-telo-provides-zone", "x-telo-ref", "x-telo-requires-zone", "x-telo-resource-rules", "x-telo-schema-from", "x-telo-schema-map", "x-telo-schema-projection", "x-telo-schema-projection-from", "x-telo-scope", "x-telo-sensitive", "x-telo-step-context", "x-telo-topology-role", "x-telo-value-schema-from", "x-telo-widget"];
32
32
  /**
33
33
  * The `x-telo-type` keyword.
34
34
  *
@@ -1 +1 @@
1
- {"version":3,"file":"value-type-keyword.d.ts","sourceRoot":"","sources":["../src/value-type-keyword.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;+EAqB+E;AAG/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,KAAK,CAAC;AAiB7C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,onBA2BtB,CAAC;AAEX;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,IAAI,iBAAiB,CA+BpD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IACxC,UAAU,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC;CACzD,GAAG,IAAI,CAGP"}
1
+ {"version":3,"file":"value-type-keyword.d.ts","sourceRoot":"","sources":["../src/value-type-keyword.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;+EAqB+E;AAG/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,KAAK,CAAC;AAiB7C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,woBA4BtB,CAAC;AAEX;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,IAAI,iBAAiB,CA+BpD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IACxC,UAAU,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC;CACzD,GAAG,IAAI,CAGP"}
@@ -54,6 +54,7 @@ export const ANNOTATION_KEYWORDS = [
54
54
  "x-telo-schema-projection",
55
55
  "x-telo-schema-projection-from",
56
56
  "x-telo-scope",
57
+ "x-telo-sensitive",
57
58
  "x-telo-step-context",
58
59
  "x-telo-topology-role",
59
60
  "x-telo-value-schema-from",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.66.0",
3
+ "version": "0.68.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -43,13 +43,13 @@
43
43
  "jsonpath-plus": "^10.3.0",
44
44
  "packageurl-js": "^2.0.1",
45
45
  "yaml": "^2.8.3",
46
- "@telorun/templating": "0.17.0"
46
+ "@telorun/templating": "0.18.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^20.0.0",
50
50
  "typescript": "^5.0.0",
51
51
  "vitest": "^2.1.8",
52
- "@telorun/sdk": "0.82.1"
52
+ "@telorun/sdk": "0.83.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@telorun/sdk": "*"
package/src/analyzer.ts CHANGED
@@ -108,6 +108,10 @@ import {
108
108
  validateValueTypeSlots,
109
109
  type ValueTypeSlotIssue,
110
110
  } from "./validate-value-type-slots.js";
111
+ import {
112
+ validateSensitiveSlots,
113
+ type SensitiveSlotIssue,
114
+ } from "./validate-sensitive-slots.js";
111
115
  import { resolveSchemaTypeRefs } from "./resolve-schema-type-refs.js";
112
116
  import { validateSchemaTypeRefs } from "./validate-schema-type-refs.js";
113
117
  import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
@@ -157,8 +161,11 @@ import { validateInvocationContract } from "./validate-invocation-contract.js";
157
161
  import { collectRefInputIssues, collectStepInputIssues } from "./validate-step-inputs.js";
158
162
  import { validateNestedInlineResources } from "./validate-nested-inline.js";
159
163
  import { validateProviderCoherence } from "./validate-provider-coherence.js";
160
- import { validateReferences } from "./validate-references.js";
164
+ import { kindSatisfies, validateReferences } from "./validate-references.js";
161
165
  import { validateReferenceForms } from "./validate-reference-forms.js";
166
+ import { isInjectedDeclaration } from "./resource-input.js";
167
+ import { validateResourceInputs } from "./validate-resource-inputs.js";
168
+ import { validateTemplateDispatch } from "./validate-template-dispatch.js";
162
169
  import { validateUnusedDeclarations } from "./validate-unused-declarations.js";
163
170
  import { validateThrowsCoverage } from "./validate-throws-coverage.js";
164
171
  import { readStepSlot } from "./step-slot.js";
@@ -976,10 +983,16 @@ export class StaticAnalyzer {
976
983
  // definition's `schema:` does. Same scoping as every other schema issue —
977
984
  // the entry's own modules, since a dependency is not the consumer's to fix.
978
985
  const valueTypeSlotIssues: ValueTypeSlotIssue[] = [];
986
+ // `x-telo-sensitive` rides the same walk and the same scoping. It is read by
987
+ // ONE consumer — the kernel, off a bound contract — so an occurrence outside
988
+ // `inputType` / `outputType` is inert; for a security control, inert-and-
989
+ // silent is the failure worth reporting.
990
+ const sensitiveSlotIssues: SensitiveSlotIssue[] = [];
979
991
  for (const m of manifests) {
980
992
  const declaringModule = (m.metadata as { module?: string } | undefined)?.module;
981
993
  if (!declaringModule || rootModules.has(declaringModule)) {
982
994
  valueTypeSlotIssues.push(...validateValueTypeSlots(m as unknown as ResourceManifest));
995
+ sensitiveSlotIssues.push(...validateSensitiveSlots(m as unknown as ResourceManifest));
983
996
  }
984
997
  }
985
998
  for (const m of manifests) {
@@ -1209,6 +1222,22 @@ export class StaticAnalyzer {
1209
1222
  },
1210
1223
  });
1211
1224
  }
1225
+ for (const issue of sensitiveSlotIssues) {
1226
+ diagnostics.push({
1227
+ severity: DiagnosticSeverity.Error,
1228
+ code: issue.code,
1229
+ source: SOURCE,
1230
+ message: issue.message,
1231
+ data: {
1232
+ resource: {
1233
+ kind: issue.manifest.kind,
1234
+ name: issue.manifest.metadata?.name as string,
1235
+ },
1236
+ filePath: (issue.manifest.metadata as { source?: string } | undefined)?.source,
1237
+ path: issue.path,
1238
+ },
1239
+ });
1240
+ }
1212
1241
  // Same split for the two zone annotations. Unreadable ones fail in
1213
1242
  // OPPOSITE directions — a dropped requirement is silently unenforced, a
1214
1243
  // dropped provision invents failures — so neither can be left to
@@ -1627,6 +1656,16 @@ export class StaticAnalyzer {
1627
1656
  const runReachable = reportsObservedState
1628
1657
  ? collectRunReachableNames(getCallGraph())
1629
1658
  : new Set<string>();
1659
+ // "Nothing starts this" is DECLARATION-derived, and a library does not
1660
+ // declare its injected inputs — whether the application starts the instance
1661
+ // it hands down is answerable only where that instance is declared. Inside
1662
+ // the library the question has no answer, so the name is treated as
1663
+ // reachable rather than reported on a `targets:` list the author cannot
1664
+ // write. The check still runs at the injection site, against the real
1665
+ // declaration.
1666
+ for (const m of allManifests) {
1667
+ if (isInjectedDeclaration(m)) runReachable.add(m.metadata?.name as string);
1668
+ }
1630
1669
 
1631
1670
  // Build typed kernel globals schema so x-telo-context chain validation
1632
1671
  // recognises variables, secrets, resources, env automatically
@@ -1762,6 +1801,15 @@ export class StaticAnalyzer {
1762
1801
  continue;
1763
1802
  }
1764
1803
 
1804
+ // A kind-only stand-in for a `resources:` entry is a DECLARATION, not an
1805
+ // instantiation: its kind is routinely an abstract and its configuration
1806
+ // is the importer's to supply, so validating it here would report a
1807
+ // non-instantiable kind and a page of missing required fields against a
1808
+ // block that is correct. Its kind is checked where it was written, by
1809
+ // `validate-resource-inputs`; it participates here only as a resolution
1810
+ // target and as a CEL type.
1811
+ if (isInjectedDeclaration(m)) continue;
1812
+
1765
1813
  const resource = { kind: m.kind, name: m.metadata?.name as string };
1766
1814
 
1767
1815
  // Resolve kind through alias if needed; direct lookup takes priority so that
@@ -1861,6 +1909,11 @@ export class StaticAnalyzer {
1861
1909
  ) as Record<string, any>;
1862
1910
  if (!ownModule || rootModules.has(ownModule)) {
1863
1911
  for (const failure of projectionFailures) {
1912
+ // A projection through a library's own resource INPUT is
1913
+ // unanswerable here — the entries belong to the declaration the
1914
+ // importer supplies — so it is not a defect in the block that named
1915
+ // it. The check runs at the injection site instead.
1916
+ if (failure.reason === "injected") continue;
1864
1917
  diagnostics.push({
1865
1918
  severity: DiagnosticSeverity.Error,
1866
1919
  code: "SCHEMA_PROJECTION_FROM_UNRESOLVED",
@@ -2556,7 +2609,7 @@ export class StaticAnalyzer {
2556
2609
  }
2557
2610
  },
2558
2611
  },
2559
- { aliases },
2612
+ { aliases, aliasesByModule, rootModules },
2560
2613
  );
2561
2614
 
2562
2615
  // The two halves of "does this expression fit the slot it flows into" meet
@@ -2645,6 +2698,22 @@ export class StaticAnalyzer {
2645
2698
  // Warn about declared variables / secrets / ports that no CEL references.
2646
2699
  diagnostics.push(...validateUnusedDeclarations(allManifests, this.celEnv));
2647
2700
 
2701
+ // A `!ref` at a definition's dispatch slot must name a sibling `resources:`
2702
+ // entry — the slot no reference pass reaches, so the tag would otherwise
2703
+ // advertise a resolution nothing performs.
2704
+ diagnostics.push(...validateTemplateDispatch(allManifests, rootModules));
2705
+
2706
+ // A library's declared resource inputs, and every import that supplies them.
2707
+ diagnostics.push(
2708
+ ...validateResourceInputs(
2709
+ allManifests,
2710
+ defs,
2711
+ aliases,
2712
+ rootModules,
2713
+ (supplied, required) => kindSatisfies(supplied, required, defs),
2714
+ ),
2715
+ );
2716
+
2648
2717
  // Reroute diagnostics on synthetic (inline-extracted) resources back to
2649
2718
  // the chain root so position-index lookups land on the parent doc.
2650
2719
  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" },