@telorun/analyzer 0.62.1 → 0.64.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 (171) hide show
  1. package/dist/analysis-registry.d.ts +24 -0
  2. package/dist/analysis-registry.d.ts.map +1 -1
  3. package/dist/analysis-registry.js +41 -0
  4. package/dist/analyzer.d.ts +3 -37
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/analyzer.js +396 -476
  7. package/dist/call-graph.d.ts.map +1 -1
  8. package/dist/call-graph.js +2 -7
  9. package/dist/cel-scope-query.d.ts +109 -0
  10. package/dist/cel-scope-query.d.ts.map +1 -0
  11. package/dist/cel-scope-query.js +270 -0
  12. package/dist/cel-scope.d.ts +166 -0
  13. package/dist/cel-scope.d.ts.map +1 -0
  14. package/dist/cel-scope.js +377 -0
  15. package/dist/definition-registry.d.ts +15 -0
  16. package/dist/definition-registry.d.ts.map +1 -1
  17. package/dist/definition-registry.js +25 -9
  18. package/dist/find-manifest.d.ts +10 -0
  19. package/dist/find-manifest.d.ts.map +1 -0
  20. package/dist/find-manifest.js +12 -0
  21. package/dist/flatten-for-analyzer.js +1 -1
  22. package/dist/import-resolution-diagnostics.d.ts +9 -2
  23. package/dist/import-resolution-diagnostics.d.ts.map +1 -1
  24. package/dist/import-resolution-diagnostics.js +26 -7
  25. package/dist/index.d.ts +22 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +22 -1
  28. package/dist/invocation-contract.d.ts +17 -1
  29. package/dist/invocation-contract.d.ts.map +1 -1
  30. package/dist/invocation-contract.js +42 -4
  31. package/dist/loaded-types.d.ts +12 -3
  32. package/dist/loaded-types.d.ts.map +1 -1
  33. package/dist/manifest-analysis.d.ts +73 -0
  34. package/dist/manifest-analysis.d.ts.map +1 -0
  35. package/dist/manifest-analysis.js +78 -0
  36. package/dist/manifest-loader.d.ts +0 -1
  37. package/dist/manifest-loader.d.ts.map +1 -1
  38. package/dist/manifest-loader.js +62 -41
  39. package/dist/manifest-path.d.ts +18 -0
  40. package/dist/manifest-path.d.ts.map +1 -0
  41. package/dist/manifest-path.js +37 -0
  42. package/dist/manifest-schemas.d.ts +699 -0
  43. package/dist/manifest-schemas.d.ts.map +1 -1
  44. package/dist/manifest-schemas.js +306 -14
  45. package/dist/module-compatibility.d.ts +40 -0
  46. package/dist/module-compatibility.d.ts.map +1 -0
  47. package/dist/module-compatibility.js +52 -0
  48. package/dist/ref-slot-reading.d.ts +30 -0
  49. package/dist/ref-slot-reading.d.ts.map +1 -0
  50. package/dist/ref-slot-reading.js +139 -0
  51. package/dist/referrer-rule.d.ts +67 -0
  52. package/dist/referrer-rule.d.ts.map +1 -0
  53. package/dist/referrer-rule.js +96 -0
  54. package/dist/release/index.d.ts +1 -1
  55. package/dist/release/index.d.ts.map +1 -1
  56. package/dist/release/index.js +1 -1
  57. package/dist/release/version-stamp.d.ts +16 -0
  58. package/dist/release/version-stamp.d.ts.map +1 -1
  59. package/dist/release/version-stamp.js +52 -0
  60. package/dist/release/workspace-config.d.ts +10 -6
  61. package/dist/release/workspace-config.d.ts.map +1 -1
  62. package/dist/release/workspace-config.js +10 -6
  63. package/dist/resolve-schema-ref-kinds.d.ts +4 -3
  64. package/dist/resolve-schema-ref-kinds.d.ts.map +1 -1
  65. package/dist/resolve-schema-ref-kinds.js +16 -0
  66. package/dist/resolve-throws-union.d.ts.map +1 -1
  67. package/dist/resolve-throws-union.js +4 -3
  68. package/dist/resolve-zone-containment.d.ts +99 -0
  69. package/dist/resolve-zone-containment.d.ts.map +1 -0
  70. package/dist/resolve-zone-containment.js +212 -0
  71. package/dist/resolve-zone-requirements.d.ts +5 -0
  72. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  73. package/dist/resolve-zone-requirements.js +48 -7
  74. package/dist/resource-rule.d.ts +106 -0
  75. package/dist/resource-rule.d.ts.map +1 -0
  76. package/dist/resource-rule.js +236 -0
  77. package/dist/rule-condition.d.ts +50 -0
  78. package/dist/rule-condition.d.ts.map +1 -0
  79. package/dist/rule-condition.js +108 -0
  80. package/dist/schema-keywords.d.ts.map +1 -1
  81. package/dist/schema-keywords.js +20 -0
  82. package/dist/schema-projection.d.ts +171 -0
  83. package/dist/schema-projection.d.ts.map +1 -0
  84. package/dist/schema-projection.js +317 -0
  85. package/dist/schema-walk.d.ts +25 -0
  86. package/dist/schema-walk.d.ts.map +1 -0
  87. package/dist/schema-walk.js +126 -0
  88. package/dist/step-slot.d.ts +39 -0
  89. package/dist/step-slot.d.ts.map +1 -0
  90. package/dist/step-slot.js +49 -0
  91. package/dist/telo-version.d.ts +1 -1
  92. package/dist/telo-version.js +1 -1
  93. package/dist/validate-cel-context.d.ts.map +1 -1
  94. package/dist/validate-cel-context.js +17 -3
  95. package/dist/validate-durable-regions.d.ts +15 -0
  96. package/dist/validate-durable-regions.d.ts.map +1 -0
  97. package/dist/validate-durable-regions.js +336 -0
  98. package/dist/validate-references.d.ts.map +1 -1
  99. package/dist/validate-references.js +44 -2
  100. package/dist/validate-referrer-rules.d.ts +120 -0
  101. package/dist/validate-referrer-rules.d.ts.map +1 -0
  102. package/dist/validate-referrer-rules.js +255 -0
  103. package/dist/validate-resource-rules.d.ts +114 -0
  104. package/dist/validate-resource-rules.d.ts.map +1 -0
  105. package/dist/validate-resource-rules.js +283 -0
  106. package/dist/validate-schema-projection.d.ts +32 -0
  107. package/dist/validate-schema-projection.d.ts.map +1 -0
  108. package/dist/validate-schema-projection.js +110 -0
  109. package/dist/validate-step-inputs.d.ts +19 -1
  110. package/dist/validate-step-inputs.d.ts.map +1 -1
  111. package/dist/validate-step-inputs.js +102 -9
  112. package/dist/validate-throws-coverage.d.ts.map +1 -1
  113. package/dist/validate-throws-coverage.js +11 -5
  114. package/dist/validate-zone-slots.d.ts +2 -2
  115. package/dist/validate-zone-slots.d.ts.map +1 -1
  116. package/dist/validate-zone-slots.js +202 -6
  117. package/dist/validate-zone-violations.d.ts +12 -0
  118. package/dist/validate-zone-violations.d.ts.map +1 -0
  119. package/dist/validate-zone-violations.js +169 -0
  120. package/dist/value-type-keyword.d.ts +1 -1
  121. package/dist/value-type-keyword.d.ts.map +1 -1
  122. package/dist/value-type-keyword.js +4 -0
  123. package/dist/zone-slot.d.ts +61 -0
  124. package/dist/zone-slot.d.ts.map +1 -1
  125. package/dist/zone-slot.js +86 -5
  126. package/package.json +2 -2
  127. package/src/analysis-registry.ts +43 -0
  128. package/src/analyzer.ts +452 -595
  129. package/src/call-graph.ts +2 -8
  130. package/src/cel-scope-query.ts +337 -0
  131. package/src/cel-scope.ts +570 -0
  132. package/src/definition-registry.ts +31 -13
  133. package/src/find-manifest.ts +19 -0
  134. package/src/flatten-for-analyzer.ts +1 -1
  135. package/src/import-resolution-diagnostics.ts +25 -7
  136. package/src/index.ts +86 -0
  137. package/src/invocation-contract.ts +67 -2
  138. package/src/loaded-types.ts +12 -3
  139. package/src/manifest-analysis.ts +132 -0
  140. package/src/manifest-loader.ts +87 -55
  141. package/src/manifest-path.ts +34 -0
  142. package/src/manifest-schemas.ts +328 -14
  143. package/src/module-compatibility.ts +72 -0
  144. package/src/ref-slot-reading.ts +156 -0
  145. package/src/referrer-rule.ts +116 -0
  146. package/src/release/index.ts +1 -0
  147. package/src/release/version-stamp.ts +58 -0
  148. package/src/release/workspace-config.ts +10 -6
  149. package/src/resolve-schema-ref-kinds.ts +28 -4
  150. package/src/resolve-throws-union.ts +4 -3
  151. package/src/resolve-zone-containment.ts +329 -0
  152. package/src/resolve-zone-requirements.ts +67 -7
  153. package/src/resource-rule.ts +252 -0
  154. package/src/rule-condition.ts +126 -0
  155. package/src/schema-keywords.ts +24 -0
  156. package/src/schema-projection.ts +403 -0
  157. package/src/schema-walk.ts +144 -0
  158. package/src/step-slot.ts +64 -0
  159. package/src/telo-version.ts +1 -1
  160. package/src/validate-cel-context.ts +17 -3
  161. package/src/validate-durable-regions.ts +401 -0
  162. package/src/validate-references.ts +57 -2
  163. package/src/validate-referrer-rules.ts +373 -0
  164. package/src/validate-resource-rules.ts +401 -0
  165. package/src/validate-schema-projection.ts +184 -0
  166. package/src/validate-step-inputs.ts +148 -10
  167. package/src/validate-throws-coverage.ts +12 -5
  168. package/src/validate-zone-slots.ts +192 -8
  169. package/src/validate-zone-violations.ts +190 -0
  170. package/src/value-type-keyword.ts +4 -0
  171. package/src/zone-slot.ts +114 -5
@@ -20,25 +20,43 @@ function isRecognizedSourceShape(source: string): boolean {
20
20
  );
21
21
  }
22
22
 
23
- function messageFor(e: GraphLoadError, malformed: boolean): string {
23
+ /** Which of the three failures this is. Kept as one function so the code and
24
+ * the sentence can never disagree about which cause was established. */
25
+ function classify(e: GraphLoadError): "INVALID_IMPORT_TARGET" | "INVALID_IMPORT_SOURCE" | "IMPORT_UNRESOLVED" {
26
+ if (e.reason === "unusable-target") return "INVALID_IMPORT_TARGET";
27
+ return isRecognizedSourceShape(e.source ?? e.url) ? "IMPORT_UNRESOLVED" : "INVALID_IMPORT_SOURCE";
28
+ }
29
+
30
+ function messageFor(e: GraphLoadError, code: ReturnType<typeof classify>): string {
24
31
  const authored = e.source ?? e.url;
25
32
  const via = e.alias ? `import '${e.alias}' → '${authored}'` : `'${authored}'`;
26
- if (malformed) {
33
+ if (code === "INVALID_IMPORT_SOURCE") {
27
34
  return (
28
35
  `Cannot resolve ${via}: not a recognized module reference. Expected ` +
29
36
  `'namespace/name@version', 'oci://host/repo@tag', 'https://…', or a relative path.`
30
37
  );
31
38
  }
39
+ // The target WAS obtained, so "cannot resolve" would name the wrong problem —
40
+ // and the wrong person to fix it. The loader's message already says what was
41
+ // fetched and why it is not importable.
42
+ if (code === "INVALID_IMPORT_TARGET") return `Cannot use ${via}: ${e.error.message}`;
32
43
  return `Cannot resolve ${via}: ${e.error.message}`;
33
44
  }
34
45
 
35
46
  /**
36
- * Convert a graph's import-resolution failures (`graph.errors`) into structured,
37
- * coded diagnostics. This is the single source of truth for surfacing a broken
47
+ * Convert a graph's import failures (`graph.errors`) into structured, coded
48
+ * diagnostics. This is the single source of truth for surfacing a broken
38
49
  * import — every host (CLI, VS Code, telo-editor) routes these instead of each
39
50
  * re-deriving the channel and drifting (the VS Code extension used to drop it
40
51
  * entirely, showing nothing for a broken import).
41
52
  *
53
+ * Three codes, because three different people fix them:
54
+ * `INVALID_IMPORT_SOURCE` (the ref is not a module reference at all),
55
+ * `IMPORT_UNRESOLVED` (a well-formed ref that could not be obtained), and
56
+ * `INVALID_IMPORT_TARGET` (obtained, but not an importable library — an
57
+ * application, no library document, or one that names no module). Collapsing
58
+ * the last into the second would tell an author to fix a ref that is correct.
59
+ *
42
60
  * The analyzer owns only this raw channel conversion; the *presentation* policy
43
61
  * — which analysis cascade to hold back for a compromised file — lives in
44
62
  * `@telorun/ide-support`'s `assembleGraphDiagnostics`.
@@ -51,15 +69,15 @@ function messageFor(e: GraphLoadError, malformed: boolean): string {
51
69
  export function importResolutionDiagnostics(graph: LoadedGraph): AnalysisDiagnostic[] {
52
70
  return graph.errors.map((e) => {
53
71
  const filePath = e.fromSource ?? graph.entry.owner.source;
54
- const malformed = !isRecognizedSourceShape(e.source ?? e.url);
72
+ const code = classify(e);
55
73
  const data: { filePath: string; path?: string; sourceLine?: number } = { filePath };
56
74
  if (e.alias) data.path = `imports.${e.alias}`;
57
75
  if (e.sourceLine !== undefined) data.sourceLine = e.sourceLine;
58
76
  return {
59
77
  severity: DiagnosticSeverity.Error,
60
- code: malformed ? "INVALID_IMPORT_SOURCE" : "IMPORT_UNRESOLVED",
78
+ code,
61
79
  source: SOURCE,
62
- message: messageFor(e, malformed),
80
+ message: messageFor(e, code),
63
81
  data,
64
82
  };
65
83
  });
package/src/index.ts CHANGED
@@ -110,6 +110,8 @@ export {
110
110
  transfersControl,
111
111
  } from "./ref-slot.js";
112
112
  export type { RefSlot, RefUse, RefUseCases } from "./ref-slot.js";
113
+ export { isStepSlot, readStepSlot, STEP_FRAGMENT } from "./step-slot.js";
114
+ export type { StepSlot } from "./step-slot.js";
113
115
  export {
114
116
  ANNOTATION_KEYWORDS,
115
117
  registerTeloKeywords,
@@ -131,6 +133,8 @@ export type {
131
133
  RequiresIssue,
132
134
  RequiresVerdict,
133
135
  } from "./requires-block.js";
136
+ export { manifestCompatibility } from "./module-compatibility.js";
137
+ export type { ModuleCompatibility } from "./module-compatibility.js";
134
138
  export { TELO_SURFACE_VERSION } from "./telo-version.js";
135
139
  export { validateRequires } from "./validate-requires.js";
136
140
  export type { ValidateRequiresOptions } from "./validate-requires.js";
@@ -150,8 +154,10 @@ export type {
150
154
  export {
151
155
  hasProvidesZone,
152
156
  hasRequiresZone,
157
+ providedZoneAttributes,
153
158
  readProvidesZone,
154
159
  readRequiresZone,
160
+ readViolatesZone,
155
161
  rewriteRequiresZoneKind,
156
162
  } from "./zone-slot.js";
157
163
  export type { ProvidesZoneSlot, RequiresZoneSlot } from "./zone-slot.js";
@@ -169,6 +175,73 @@ export type {
169
175
  } from "./resolve-zone-requirements.js";
170
176
  export type { ZoneModuleDocuments } from "./zone-module-documents.js";
171
177
  export { validateZoneSlotDeclarations } from "./validate-zone-slots.js";
178
+ export {
179
+ RESOURCE_RULES_ANNOTATION,
180
+ readResourceRules,
181
+ readRawResourceRules,
182
+ resolveRuleSubjects,
183
+ findDynamicLeaf,
184
+ type ResourceRule,
185
+ type ResourceRuleSeverity,
186
+ type RuleSubject,
187
+ } from "./resource-rule.js";
188
+ export {
189
+ validateResourceRuleDeclarations,
190
+ evaluateResourceRules,
191
+ ruleExercised,
192
+ RESOURCE_RULE_BUDGET_MS,
193
+ type ResourceRuleIssue,
194
+ type ResourceRuleFinding,
195
+ } from "./validate-resource-rules.js";
196
+ export {
197
+ REFERRER_RULES_ANNOTATION,
198
+ readReferrerRules,
199
+ readRawReferrerRules,
200
+ hasReferrerRules,
201
+ rewriteReferrerRuleKinds,
202
+ type ReferrerRule,
203
+ } from "./referrer-rule.js";
204
+ export {
205
+ validateReferrerRuleDeclarations,
206
+ evaluateReferrerRules,
207
+ referrerRuleExercised,
208
+ reportReferrerRules,
209
+ reportUnexercisedReferrerRule,
210
+ type Referrer,
211
+ type ReferrerRuleIssue,
212
+ type ReferrerRuleFinding,
213
+ type ReferrerRuleDiagnostic,
214
+ } from "./validate-referrer-rules.js";
215
+ export { RULE_BUDGET_MS } from "./rule-condition.js";
216
+ export {
217
+ readSchemaProjection,
218
+ readSchemaMap,
219
+ readProjectionFrom,
220
+ readProjectionRef,
221
+ manifestListScope,
222
+ projectionKeyMap,
223
+ projectEntries,
224
+ resolveSchemaProjections,
225
+ describeProjectionFailure,
226
+ type SchemaProjection,
227
+ type SchemaMap,
228
+ type ProjectionRef,
229
+ type ProjectionLookup,
230
+ type ProjectionScope,
231
+ type ProjectionFailure,
232
+ } from "./schema-projection.js";
233
+ export { validateSchemaProjection, type SchemaProjectionIssue } from "./validate-schema-projection.js";
234
+ export { validateDurableRegions } from "./validate-durable-regions.js";
235
+ export { validateZoneViolations } from "./validate-zone-violations.js";
236
+ export {
237
+ containmentIndex,
238
+ findZoneRegions,
239
+ regionManifests,
240
+ type ContainedNode,
241
+ type DefinitionLookup,
242
+ type RegionBoundary,
243
+ type ZoneRegion,
244
+ } from "./resolve-zone-containment.js";
172
245
  export type { ZoneSlotIssue } from "./validate-zone-slots.js";
173
246
  export { validateDynamicSelectors, validateRefSlotDeclarations } from "./validate-ref-slots.js";
174
247
  export type { RefSlotIssue } from "./validate-ref-slots.js";
@@ -297,6 +370,19 @@ export { documentToAst, parseToAst } from "./yaml-ast.js";
297
370
  export type { AstDocument, AstMap, AstNode, AstPair, AstScalar, AstSeq } from "./yaml-ast.js";
298
371
  export { CelParseError, buildCelSegments, wrapCelAst } from "./cel-ast.js";
299
372
  export type { CelNode, CelSegment } from "./cel-ast.js";
373
+
374
+ // The CEL scope rule, and the way into it from outside the analysis pass. What
375
+ // completes, what hovers and what type-checks are one answer because they are
376
+ // one function.
377
+ export { CelScopeResolver } from "./cel-scope.js";
378
+ export type { CelScope, CelScopeInputs, CelSiteRef } from "./cel-scope.js";
379
+ export { CelScopeQuery } from "./cel-scope-query.js";
380
+ export type { CelScopeQueryContext, ContextDeclarationSite } from "./cel-scope-query.js";
381
+ // The pairing of a registry and the manifests it analyzed — the one seam a host
382
+ // threads for every question that needs both.
383
+ export { navigateConcretePath } from "./manifest-path.js";
384
+ export { ManifestAnalysis } from "./manifest-analysis.js";
385
+ export type { ManifestRef } from "./manifest-analysis.js";
300
386
  export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity, diagnosticFix } from "./types.js";
301
387
  export type {
302
388
  AnalysisDiagnostic,
@@ -1,4 +1,6 @@
1
1
  import { isLiveSlot, type ResourceDefinition, valueTypeOf } from "@telorun/sdk";
2
+ import { AliasResolver, moduleScopedDefResolver, type ModuleScopes } from "./alias-resolver.js";
3
+ import { DefinitionRegistry } from "./definition-registry.js";
2
4
  import {
3
5
  type ContractDirection,
4
6
  contractDeclarer,
@@ -6,9 +8,34 @@ import {
6
8
  effectiveContractField,
7
9
  } from "./extends-resolution.js";
8
10
  import { resolveTypeFieldToSchema } from "./validate-cel-context.js";
11
+ import {
12
+ manifestListScope,
13
+ resolveSchemaProjections,
14
+ type ProjectionFailure,
15
+ } from "./schema-projection.js";
9
16
 
10
17
  export type { ContractDirection };
11
18
 
19
+ /** The {@link ContractScope} the analyzer resolves invocation contracts in: kinds
20
+ * resolve in the module that declared the definition they were read off (so an
21
+ * `extends` chain crossing module boundaries re-scopes at every hop), and named
22
+ * `telo#Type` references resolve against the flattened manifest list. `resolveIn`
23
+ * is the top-level entry point, where the kind was written by the READING
24
+ * module and there is no declaring definition yet. */
25
+ export function analyzerContractScope(
26
+ defs: DefinitionRegistry,
27
+ aliases: AliasResolver,
28
+ scopes: ModuleScopes,
29
+ allManifests: Record<string, any>[],
30
+ ): ContractScope & { resolveIn(kind: string, module?: string): ResourceDefinition | undefined } {
31
+ const resolve = moduleScopedDefResolver<ResourceDefinition>(defs, aliases, scopes);
32
+ return {
33
+ resolveDefinition: resolve,
34
+ resolveIn: resolve.in,
35
+ typeManifestsFor: () => allManifests,
36
+ };
37
+ }
38
+
12
39
  /**
13
40
  * The one answer to "what is this target's input / output schema".
14
41
  *
@@ -80,11 +107,20 @@ export function resolveContract(
80
107
  manifest: Record<string, any> | undefined,
81
108
  definition: ResourceDefinition | undefined,
82
109
  scope: ContractScope,
110
+ /** Collects declaration-derived slots that could not be projected. Omitted by
111
+ * every caller that only wants the schema; the one pass that REPORTS them
112
+ * passes an array. */
113
+ projectionFailures?: ProjectionFailure[],
83
114
  ): ResolvedContract | undefined {
84
115
  const own = manifest?.[direction];
85
116
  if (own !== undefined && own !== null) {
86
117
  const schema = resolveTypeFieldToSchema(own, scope.typeManifestsFor(definition));
87
- if (schema) return { schema, origin: "instance" };
118
+ if (schema) {
119
+ return {
120
+ schema: projectionResolved(schema, manifest, scope, definition, projectionFailures),
121
+ origin: "instance",
122
+ };
123
+ }
88
124
  }
89
125
 
90
126
  const declared = effectiveContractField(definition, scope.resolveDefinition, direction);
@@ -92,7 +128,36 @@ export function resolveContract(
92
128
  const declarer = contractDeclarer(definition, scope.resolveDefinition, direction);
93
129
  const schema = resolveTypeFieldToSchema(declared, scope.typeManifestsFor(declarer));
94
130
  if (!schema) return undefined;
95
- return { schema, origin: "kind", declaredBy: declarer };
131
+ return {
132
+ schema: projectionResolved(schema, manifest, scope, declarer, projectionFailures),
133
+ origin: "kind",
134
+ declaredBy: declarer,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * A contract may be DECLARATION-derived: a kind whose `outputType` projects the
140
+ * declaration of a resource it references (a repository typing its rows from
141
+ * the table it reads). The manifest is in hand here and nowhere later, so this
142
+ * is where the projection is resolved — and it is resolved for a kind-declared
143
+ * contract and an instance-declared one alike, since either may carry one.
144
+ */
145
+ function projectionResolved(
146
+ schema: Record<string, any>,
147
+ manifest: Record<string, any> | undefined,
148
+ scope: ContractScope,
149
+ declarer: ResourceDefinition | undefined,
150
+ failures?: ProjectionFailure[],
151
+ ): Record<string, any> {
152
+ return resolveSchemaProjections(
153
+ schema,
154
+ manifest,
155
+ manifestListScope(
156
+ scope.typeManifestsFor(declarer),
157
+ (kind) => scope.resolveDefinition(kind) as Record<string, any> | undefined,
158
+ ),
159
+ failures,
160
+ ) as Record<string, any>;
96
161
  }
97
162
 
98
163
  /** {@link resolveContract}, falling back to {@link PERMISSIVE_CONTRACT}. For
@@ -70,9 +70,11 @@ export interface ImportEdge {
70
70
  * this (minus its version), since it names the module's location
71
71
  * independently of what the module declares about itself. */
72
72
  targetRef: string;
73
- /** Target library's `metadata.name`, or `null` when the target had no
74
- * Telo.Library doc (an error case captured in `LoadedGraph.errors`). */
75
- targetModuleName: string | null;
73
+ /** Target library's `metadata.name`. Never absent: an import whose target
74
+ * names no library identity registers no edge at all and is reported in
75
+ * `LoadedGraph.errors`, so an edge that exists is one every consumer can
76
+ * resolve a kind through. */
77
+ targetModuleName: string;
76
78
  /** DEPRECATED. Target library's `metadata.namespace`, or `null` when it
77
79
  * declares none. Feeds only the legacy `<namespace>/<module>#<Kind>` form
78
80
  * of `x-telo-ref`; nothing else reads it. */
@@ -137,5 +139,12 @@ export interface GraphLoadError {
137
139
  alias?: string;
138
140
  /** Line of the `Telo.Import` doc in `fromSource`, for position fallback. */
139
141
  sourceLine?: number;
142
+ /** Why this import is unusable. `"unresolved"` (the default) means the target
143
+ * was never obtained — an unreachable or unrecognizable source, which the
144
+ * author fixes in their own file. `"unusable-target"` means it WAS obtained
145
+ * and is not an importable library, which is a defect in the target module.
146
+ * Kept apart because the two call for different actions; consumers phrase
147
+ * them differently rather than asserting one cause for both. */
148
+ reason?: "unresolved" | "unusable-target";
140
149
  error: Error;
141
150
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * **One analyzed manifest set, and the questions asked of it.**
3
+ *
4
+ * Several answers a host needs require the SAME two things: the registry's
5
+ * definitions and aliases, and the manifest set they were resolved against. The
6
+ * registry deliberately holds no manifests — it is populated per analysis and
7
+ * reused across them — so each such answer would otherwise become another
8
+ * factory on the registry and another optional parameter on every IDE entry
9
+ * point. Four of those arrived in short order (CEL scope, step declarations,
10
+ * context-binding declarations, invocation contracts) and the next one is not
11
+ * hypothetical.
12
+ *
13
+ * So the pairing is named once and the questions hang off it. A host threads ONE
14
+ * object and gains later questions for free; each facet keeps its own honest
15
+ * name rather than accreting onto whichever one happened to exist first.
16
+ *
17
+ * Nothing here re-implements an answer. `contractFor` is the shared
18
+ * {@link resolveContract} — the one `telo check` runs and the kernel binds at
19
+ * dispatch — given the scope to run in; `celScope` is the same
20
+ * {@link CelScopeQuery} the analysis pass's rule is built from. That is the
21
+ * whole point: a completion list is a claim about what the checker accepts, and
22
+ * a second implementation of any of these could not be held to it.
23
+ */
24
+ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
25
+ import { AliasResolver, type ModuleScopes } from "./alias-resolver.js";
26
+ import { CelScopeQuery, type CelScopeQueryContext } from "./cel-scope-query.js";
27
+ import { DefinitionRegistry } from "./definition-registry.js";
28
+ import type { ContractDirection } from "./extends-resolution.js";
29
+ import { analyzerContractScope, resolveContract } from "./invocation-contract.js";
30
+ import { findManifest } from "./find-manifest.js";
31
+ import { isModuleKind } from "./module-kinds.js";
32
+
33
+ /**
34
+ * A reference as the loader leaves it — the internal `{kind, name, alias?}`
35
+ * shape `resolveRefSentinels` rewrites `!ref` into.
36
+ */
37
+ export interface ManifestRef {
38
+ kind?: string;
39
+ name?: string;
40
+ alias?: string;
41
+ }
42
+
43
+ export class ManifestAnalysis {
44
+ private readonly scopes: ModuleScopes;
45
+ private celScopeQuery: CelScopeQuery | undefined;
46
+
47
+ constructor(
48
+ readonly manifests: ResourceManifest[],
49
+ private readonly ctx: CelScopeQueryContext,
50
+ ) {
51
+ const rootModules = new Set<string>();
52
+ for (const m of manifests) {
53
+ if (isModuleKind(m.kind) && m.metadata?.name) rootModules.add(m.metadata.name as string);
54
+ }
55
+ this.scopes = { aliasesByModule: ctx.aliasesByModule, rootModules };
56
+ }
57
+
58
+ /** What CEL sees, per site. Built on first use — its indices are a function of
59
+ * the whole set, and a host that never opens a CEL body should not pay for
60
+ * them. */
61
+ get celScope(): CelScopeQuery {
62
+ return (this.celScopeQuery ??= new CelScopeQuery(this.manifests, this.ctx));
63
+ }
64
+
65
+ /** The manifest a `(kind, name)` pair addresses. */
66
+ resourceFor(kind: string | undefined, name: string | undefined): ResourceManifest | undefined {
67
+ return findManifest(this.manifests, kind, name);
68
+ }
69
+
70
+ /**
71
+ * The invocation contract of the resource a reference names.
72
+ *
73
+ * The shared resolver, so an editor offering a target's input keys is offering
74
+ * exactly what `telo check` validates that call site against and what the
75
+ * kernel binds at dispatch. Layered instance-first: a resource declaring its
76
+ * own `inputType:` narrows the kind's, which is the common case for a
77
+ * `Run.Sequence` used as a handler.
78
+ */
79
+ contractFor(ref: ManifestRef, direction: ContractDirection): Record<string, any> | undefined {
80
+ const target = this.resolveRef(ref);
81
+ const definition = ref.kind ? this.definitionFor(ref.kind) : undefined;
82
+ if (!target && !definition) return undefined;
83
+ return resolveContract(
84
+ direction,
85
+ target as Record<string, any> | undefined,
86
+ definition,
87
+ analyzerContractScope(
88
+ this.ctx.defs,
89
+ this.ctx.aliases,
90
+ this.scopes,
91
+ this.manifests as Record<string, any>[],
92
+ ),
93
+ )?.schema;
94
+ }
95
+
96
+ /**
97
+ * The manifest a reference names.
98
+ *
99
+ * An ALIAS narrows before the name does: a flattened set carries every
100
+ * imported library's exported instances, so two libraries exporting a `store`
101
+ * are two manifests with one name. Matching the alias to its target module
102
+ * picks the right one; where the alias resolves to nothing the name alone is
103
+ * used, which is what a local reference needs anyway.
104
+ */
105
+ private resolveRef(ref: ManifestRef): ResourceManifest | undefined {
106
+ if (!ref.name) return undefined;
107
+ const byName = this.manifests.filter(
108
+ (m) => (m.metadata as { name?: string } | undefined)?.name === ref.name,
109
+ );
110
+ if (byName.length === 0) return undefined;
111
+ if (byName.length === 1) return byName[0];
112
+
113
+ const targetModule = ref.alias ? this.ctx.aliases.moduleForAlias?.(ref.alias) : undefined;
114
+ if (targetModule) {
115
+ const scoped = byName.find(
116
+ (m) => (m.metadata as { module?: string } | undefined)?.module === targetModule,
117
+ );
118
+ if (scoped) return scoped;
119
+ }
120
+ // Several candidates and nothing to choose between them: refusing is the
121
+ // honest answer, since typing a call site against the wrong resource's
122
+ // contract is worse than typing it against none.
123
+ return ref.kind ? byName.find((m) => m.kind === ref.kind) : undefined;
124
+ }
125
+
126
+ private definitionFor(kind: string): ResourceDefinition | undefined {
127
+ const canonical = this.ctx.aliases.resolveKind(kind);
128
+ return this.ctx.defs.resolve(kind) ?? (canonical ? this.ctx.defs.resolve(canonical) : undefined);
129
+ }
130
+ }
131
+
132
+ export type { CelScopeQueryContext, AliasResolver, DefinitionRegistry };
@@ -57,6 +57,62 @@ function collectMigrationDiagnostics(entry: LoadedModule): AnalysisDiagnostic[]
57
57
  return diagnostics;
58
58
  }
59
59
 
60
+ /** The identity an import target contributes to its importer, or the reason it
61
+ * contributes none.
62
+ *
63
+ * Every way an import can be unusable answers here, in one place and without
64
+ * throwing: the target is an application, it carries no library document at
65
+ * all, or it carries one that never names itself. A name is not decoration —
66
+ * it is the kind prefix every `Alias.Kind` in the importer resolves through —
67
+ * so a target that has none is as unusable as one that was never fetched, and
68
+ * is reported the same way rather than left to surface later as an invented
69
+ * module nobody declared. */
70
+ type ImportTargetIdentity =
71
+ | { name: string; namespace: string | null }
72
+ | { unusable: string };
73
+
74
+ function importTargetIdentity(target: LoadedModule, importSource: string): ImportTargetIdentity {
75
+ const library = target.owner.manifests.find((m) => m?.kind === "Telo.Library");
76
+ const application = target.owner.manifests.find((m) => m?.kind === "Telo.Application");
77
+
78
+ if (!library && application) {
79
+ return {
80
+ unusable:
81
+ `Telo.Import target '${importSource}' is a Telo.Application. ` +
82
+ `Only Telo.Library modules may be imported. Applications are run directly, not imported. ` +
83
+ `Point this import at a library, or drop it and run that application on its own.`,
84
+ };
85
+ }
86
+
87
+ if (!library) {
88
+ const kinds = target.owner.manifests
89
+ .map((m) => m?.kind)
90
+ .filter((k): k is string => typeof k === "string");
91
+ const detail = kinds.length
92
+ ? `Fetched ${target.owner.manifests.length} document(s) with kinds [${kinds.join(", ")}].`
93
+ : `Fetched manifest contained no recognizable Telo documents — check that the source ` +
94
+ `serves a Telo.Library manifest and not an upstream error page.`;
95
+ return {
96
+ unusable:
97
+ `Telo.Import target '${importSource}' did not resolve to a Telo.Library. ` +
98
+ `Fetched from: ${target.owner.source}. ${detail}`,
99
+ };
100
+ }
101
+
102
+ const name = library.metadata?.name;
103
+ if (typeof name !== "string" || name === "") {
104
+ return {
105
+ unusable:
106
+ `Telo.Import target '${importSource}' resolved to a Telo.Library that declares no ` +
107
+ `'metadata.name'. Fetched from: ${target.owner.source}. A library's name is the kind ` +
108
+ `prefix its importers reference, so it cannot be imported until the target declares one.`,
109
+ };
110
+ }
111
+
112
+ const namespace = (library.metadata as { namespace?: string | null } | undefined)?.namespace;
113
+ return { name, namespace: typeof namespace === "string" ? namespace : null };
114
+ }
115
+
60
116
  const SYSTEM_KINDS = new Set([
61
117
  "Telo.Application",
62
118
  "Telo.Library",
@@ -320,10 +376,10 @@ export class Loader {
320
376
  // Resolve the file we'll fetch through the source chain to get the
321
377
  // canonical `source` URL — same identity used as the modules-map key.
322
378
  let targetCanonical: string;
323
- let targetModule: LoadedModule | undefined;
379
+ let targetModule: LoadedModule;
324
380
  if (modules.has(resolvedTarget)) {
325
381
  targetCanonical = resolvedTarget;
326
- targetModule = modules.get(resolvedTarget);
382
+ targetModule = modules.get(resolvedTarget)!;
327
383
  } else {
328
384
  try {
329
385
  const loaded = await this.loadModule(resolvedTarget, options);
@@ -332,7 +388,7 @@ export class Loader {
332
388
  modules.set(targetCanonical, loaded);
333
389
  targetModule = loaded;
334
390
  } else {
335
- targetModule = modules.get(targetCanonical);
391
+ targetModule = modules.get(targetCanonical)!;
336
392
  }
337
393
  } catch (err) {
338
394
  const e = err instanceof Error ? err : new Error(String(err));
@@ -349,32 +405,42 @@ export class Loader {
349
405
  }
350
406
  }
351
407
 
352
- // Resolve target identity from its Telo.Library doc and stamp it
353
- // on the edge — flattenForAnalyzer reads from the edge directly,
354
- // never re-deriving from manifest.metadata.
355
- let targetModuleName: string | null = null;
356
- let targetNamespace: string | null = null;
357
- if (targetModule) {
358
- const lib = targetModule.owner.manifests.find(
359
- (d) => d?.kind === "Telo.Library",
360
- );
361
- const libName = lib?.metadata?.name;
362
- if (typeof libName === "string") targetModuleName = libName;
363
- const libNs = (lib?.metadata as { namespace?: string | null } | undefined)
364
- ?.namespace;
365
- if (typeof libNs === "string") targetNamespace = libNs;
408
+ // Resolve target identity from its Telo.Library doc and stamp it on
409
+ // the edge — flattenForAnalyzer reads from the edge directly, never
410
+ // re-deriving from manifest.metadata.
411
+ //
412
+ // Checked per IMPORT rather than per distinct target: an import
413
+ // pointing at a module something else already reached (the entry
414
+ // application above all) would otherwise skip the check entirely and
415
+ // register a nameless edge. An unusable target is recorded exactly
416
+ // like an unfetchable one — an error against this import, no edge,
417
+ // and the rest of the graph still loads — so no consumer has to know
418
+ // the difference between the two ways an import can fail.
419
+ const identity = importTargetIdentity(targetModule, importSource);
420
+ if ("unusable" in identity) {
421
+ const e = new Error(identity.unusable);
422
+ (e as { sourceLine?: number }).sourceLine = sourceLine;
423
+ errors.push({
424
+ url: targetCanonical,
425
+ source: importSource,
426
+ fromSource: file.source,
427
+ alias,
428
+ sourceLine,
429
+ reason: "unusable-target",
430
+ error: e,
431
+ });
432
+ continue;
366
433
  }
367
434
 
368
435
  aliases.set(alias, {
369
436
  targetSource: targetCanonical,
370
437
  targetRef: importSource,
371
- targetModuleName,
372
- targetNamespace,
438
+ targetModuleName: identity.name,
439
+ targetNamespace: identity.namespace,
373
440
  });
374
441
 
375
- if (targetModule && !visited.has(targetCanonical)) {
442
+ if (!visited.has(targetCanonical)) {
376
443
  visited.add(targetCanonical);
377
- this.assertImportTargetIsLibrary(targetModule, importSource, sourceLine);
378
444
  queue.push(targetModule);
379
445
  }
380
446
  }
@@ -444,40 +510,6 @@ export class Loader {
444
510
  }
445
511
  }
446
512
 
447
- private assertImportTargetIsLibrary(
448
- target: LoadedModule,
449
- importSource: string,
450
- sourceLine: number,
451
- ): void {
452
- const importedLibrary = target.owner.manifests.find((m) => m?.kind === "Telo.Library");
453
- const importedApplication = target.owner.manifests.find(
454
- (m) => m?.kind === "Telo.Application",
455
- );
456
- if (importedApplication) {
457
- const e = new Error(
458
- `Telo.Import target '${importSource}' is a Telo.Application. ` +
459
- `Only Telo.Library modules may be imported. Applications are run directly, not imported.`,
460
- );
461
- (e as { sourceLine?: number }).sourceLine = sourceLine;
462
- throw e;
463
- }
464
- if (!importedLibrary) {
465
- const kinds = target.owner.manifests
466
- .map((m) => m?.kind)
467
- .filter((k): k is string => typeof k === "string");
468
- const detail = kinds.length
469
- ? `Fetched ${target.owner.manifests.length} document(s) with kinds [${kinds.join(", ")}].`
470
- : `Fetched manifest contained no recognizable Telo documents — check that the source ` +
471
- `serves a Telo.Library manifest and not an upstream error page.`;
472
- const e = new Error(
473
- `Telo.Import target '${importSource}' did not resolve to a Telo.Library. ` +
474
- `Fetched from: ${target.owner.source}. ${detail}`,
475
- );
476
- (e as { sourceLine?: number }).sourceLine = sourceLine;
477
- throw e;
478
- }
479
- }
480
-
481
513
  private async resolveIncludes(
482
514
  ownerSource: string,
483
515
  patterns: string[],
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Navigating a MANIFEST by a concrete path (`routes[0].request.schema.query`).
3
+ *
4
+ * The counterpart to `schema-walk.ts`, which navigates a schema: this addresses
5
+ * the author's own document, indices and all. Its own module because three
6
+ * places needed it independently — the CEL scope query resolving a context
7
+ * binding's declaration, the call-site checker resolving an argument map, and
8
+ * the IDE resolving the same map for completion — and three copies of one
9
+ * traversal is exactly what the shared-answer rule exists to prevent.
10
+ */
11
+
12
+ /**
13
+ * The value at `path`, or `undefined` when any segment is absent.
14
+ *
15
+ * Absence is what makes a candidate path a CHECK rather than a guess: a caller
16
+ * offering several possible shapes can try each and know a hit is a real node.
17
+ */
18
+ export function navigateConcretePath(root: Record<string, any>, path: string): unknown {
19
+ let current: unknown = root;
20
+ for (const segment of path.split(".")) {
21
+ if (!segment) continue;
22
+ const match = segment.match(/^([^[]*)((?:\[\d+\])*)$/);
23
+ if (!match) return undefined;
24
+ if (match[1]) {
25
+ if (current === null || typeof current !== "object") return undefined;
26
+ current = (current as Record<string, unknown>)[match[1]];
27
+ }
28
+ for (const index of match[2].matchAll(/\[(\d+)\]/g)) {
29
+ if (!Array.isArray(current)) return undefined;
30
+ current = current[Number(index[1])];
31
+ }
32
+ }
33
+ return current;
34
+ }