@telorun/analyzer 0.62.0 → 0.63.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 (146) hide show
  1. package/dist/analysis-registry.d.ts.map +1 -1
  2. package/dist/analysis-registry.js +6 -0
  3. package/dist/analyzer.d.ts +1 -1
  4. package/dist/analyzer.d.ts.map +1 -1
  5. package/dist/analyzer.js +343 -13
  6. package/dist/call-graph.d.ts.map +1 -1
  7. package/dist/call-graph.js +2 -7
  8. package/dist/flatten-for-analyzer.js +1 -1
  9. package/dist/import-resolution-diagnostics.d.ts +9 -2
  10. package/dist/import-resolution-diagnostics.d.ts.map +1 -1
  11. package/dist/import-resolution-diagnostics.js +26 -7
  12. package/dist/index.d.ts +15 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +13 -1
  15. package/dist/invocation-contract.d.ts +6 -1
  16. package/dist/invocation-contract.d.ts.map +1 -1
  17. package/dist/invocation-contract.js +27 -4
  18. package/dist/loaded-types.d.ts +12 -3
  19. package/dist/loaded-types.d.ts.map +1 -1
  20. package/dist/manifest-loader.d.ts +0 -1
  21. package/dist/manifest-loader.d.ts.map +1 -1
  22. package/dist/manifest-loader.js +62 -41
  23. package/dist/manifest-schemas.d.ts +699 -0
  24. package/dist/manifest-schemas.d.ts.map +1 -1
  25. package/dist/manifest-schemas.js +306 -14
  26. package/dist/module-compatibility.d.ts +40 -0
  27. package/dist/module-compatibility.d.ts.map +1 -0
  28. package/dist/module-compatibility.js +52 -0
  29. package/dist/ref-slot-reading.d.ts +30 -0
  30. package/dist/ref-slot-reading.d.ts.map +1 -0
  31. package/dist/ref-slot-reading.js +139 -0
  32. package/dist/referrer-rule.d.ts +67 -0
  33. package/dist/referrer-rule.d.ts.map +1 -0
  34. package/dist/referrer-rule.js +96 -0
  35. package/dist/release/index.d.ts +1 -1
  36. package/dist/release/index.d.ts.map +1 -1
  37. package/dist/release/index.js +1 -1
  38. package/dist/release/payload-digest.d.ts +7 -3
  39. package/dist/release/payload-digest.d.ts.map +1 -1
  40. package/dist/release/payload-digest.js +7 -3
  41. package/dist/release/version-stamp.d.ts +16 -0
  42. package/dist/release/version-stamp.d.ts.map +1 -1
  43. package/dist/release/version-stamp.js +52 -0
  44. package/dist/release/workspace-config.d.ts +10 -6
  45. package/dist/release/workspace-config.d.ts.map +1 -1
  46. package/dist/release/workspace-config.js +10 -6
  47. package/dist/resolve-schema-ref-kinds.d.ts +4 -3
  48. package/dist/resolve-schema-ref-kinds.d.ts.map +1 -1
  49. package/dist/resolve-schema-ref-kinds.js +16 -0
  50. package/dist/resolve-throws-union.d.ts.map +1 -1
  51. package/dist/resolve-throws-union.js +4 -3
  52. package/dist/resolve-zone-containment.d.ts +99 -0
  53. package/dist/resolve-zone-containment.d.ts.map +1 -0
  54. package/dist/resolve-zone-containment.js +212 -0
  55. package/dist/resolve-zone-requirements.d.ts +5 -0
  56. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  57. package/dist/resolve-zone-requirements.js +48 -7
  58. package/dist/resource-rule.d.ts +106 -0
  59. package/dist/resource-rule.d.ts.map +1 -0
  60. package/dist/resource-rule.js +236 -0
  61. package/dist/rule-condition.d.ts +50 -0
  62. package/dist/rule-condition.d.ts.map +1 -0
  63. package/dist/rule-condition.js +108 -0
  64. package/dist/schema-keywords.d.ts.map +1 -1
  65. package/dist/schema-keywords.js +20 -0
  66. package/dist/schema-projection.d.ts +171 -0
  67. package/dist/schema-projection.d.ts.map +1 -0
  68. package/dist/schema-projection.js +317 -0
  69. package/dist/step-slot.d.ts +39 -0
  70. package/dist/step-slot.d.ts.map +1 -0
  71. package/dist/step-slot.js +49 -0
  72. package/dist/telo-version.d.ts +1 -1
  73. package/dist/telo-version.js +1 -1
  74. package/dist/validate-cel-context.d.ts.map +1 -1
  75. package/dist/validate-cel-context.js +17 -3
  76. package/dist/validate-durable-regions.d.ts +15 -0
  77. package/dist/validate-durable-regions.d.ts.map +1 -0
  78. package/dist/validate-durable-regions.js +336 -0
  79. package/dist/validate-references.d.ts.map +1 -1
  80. package/dist/validate-references.js +44 -2
  81. package/dist/validate-referrer-rules.d.ts +120 -0
  82. package/dist/validate-referrer-rules.d.ts.map +1 -0
  83. package/dist/validate-referrer-rules.js +255 -0
  84. package/dist/validate-resource-rules.d.ts +114 -0
  85. package/dist/validate-resource-rules.d.ts.map +1 -0
  86. package/dist/validate-resource-rules.js +283 -0
  87. package/dist/validate-schema-projection.d.ts +32 -0
  88. package/dist/validate-schema-projection.d.ts.map +1 -0
  89. package/dist/validate-schema-projection.js +110 -0
  90. package/dist/validate-step-inputs.d.ts +2 -1
  91. package/dist/validate-step-inputs.d.ts.map +1 -1
  92. package/dist/validate-step-inputs.js +5 -3
  93. package/dist/validate-throws-coverage.d.ts.map +1 -1
  94. package/dist/validate-throws-coverage.js +11 -5
  95. package/dist/validate-zone-slots.d.ts +2 -2
  96. package/dist/validate-zone-slots.d.ts.map +1 -1
  97. package/dist/validate-zone-slots.js +202 -6
  98. package/dist/validate-zone-violations.d.ts +12 -0
  99. package/dist/validate-zone-violations.d.ts.map +1 -0
  100. package/dist/validate-zone-violations.js +169 -0
  101. package/dist/value-type-keyword.d.ts +1 -1
  102. package/dist/value-type-keyword.d.ts.map +1 -1
  103. package/dist/value-type-keyword.js +4 -0
  104. package/dist/zone-slot.d.ts +61 -0
  105. package/dist/zone-slot.d.ts.map +1 -1
  106. package/dist/zone-slot.js +86 -5
  107. package/package.json +2 -2
  108. package/src/analysis-registry.ts +6 -0
  109. package/src/analyzer.ts +395 -14
  110. package/src/call-graph.ts +2 -8
  111. package/src/flatten-for-analyzer.ts +1 -1
  112. package/src/import-resolution-diagnostics.ts +25 -7
  113. package/src/index.ts +73 -0
  114. package/src/invocation-contract.ts +45 -2
  115. package/src/loaded-types.ts +12 -3
  116. package/src/manifest-loader.ts +87 -55
  117. package/src/manifest-schemas.ts +328 -14
  118. package/src/module-compatibility.ts +72 -0
  119. package/src/ref-slot-reading.ts +156 -0
  120. package/src/referrer-rule.ts +116 -0
  121. package/src/release/index.ts +1 -0
  122. package/src/release/payload-digest.ts +7 -3
  123. package/src/release/version-stamp.ts +58 -0
  124. package/src/release/workspace-config.ts +10 -6
  125. package/src/resolve-schema-ref-kinds.ts +28 -4
  126. package/src/resolve-throws-union.ts +4 -3
  127. package/src/resolve-zone-containment.ts +329 -0
  128. package/src/resolve-zone-requirements.ts +67 -7
  129. package/src/resource-rule.ts +252 -0
  130. package/src/rule-condition.ts +126 -0
  131. package/src/schema-keywords.ts +24 -0
  132. package/src/schema-projection.ts +403 -0
  133. package/src/step-slot.ts +64 -0
  134. package/src/telo-version.ts +1 -1
  135. package/src/validate-cel-context.ts +17 -3
  136. package/src/validate-durable-regions.ts +401 -0
  137. package/src/validate-references.ts +57 -2
  138. package/src/validate-referrer-rules.ts +373 -0
  139. package/src/validate-resource-rules.ts +401 -0
  140. package/src/validate-schema-projection.ts +184 -0
  141. package/src/validate-step-inputs.ts +5 -3
  142. package/src/validate-throws-coverage.ts +12 -5
  143. package/src/validate-zone-slots.ts +192 -8
  144. package/src/validate-zone-violations.ts +190 -0
  145. package/src/value-type-keyword.ts +4 -0
  146. package/src/zone-slot.ts +114 -5
package/src/analyzer.ts CHANGED
@@ -51,8 +51,39 @@ import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
51
51
  import { resolveRefSentinels } from "./resolve-ref-sentinels.js";
52
52
  import { resolveSchemaRefKinds, type RefConstraintIssue } from "./resolve-schema-ref-kinds.js";
53
53
  import { runZoneAnalysis, type ZoneExportCache } from "./resolve-zone-requirements.js";
54
+ import { validateDurableRegions } from "./validate-durable-regions.js";
55
+ import { validateZoneViolations } from "./validate-zone-violations.js";
54
56
  import { MANIFEST_SCHEMA_URI, ManifestRootSchema } from "./manifest-schemas.js";
55
57
  import { validateZoneSlotDeclarations, type ZoneSlotIssue } from "./validate-zone-slots.js";
58
+ import {
59
+ validateSchemaProjection,
60
+ type SchemaProjectionIssue,
61
+ } from "./validate-schema-projection.js";
62
+ import {
63
+ evaluateResourceRules,
64
+ reportResourceRules,
65
+ reportUnexercisedRule,
66
+ ruleExercised,
67
+ validateResourceRuleDeclarations,
68
+ type ResourceRuleDiagnostic,
69
+ type ResourceRuleIssue,
70
+ } from "./validate-resource-rules.js";
71
+ import { readResourceRules, type ResourceRule } from "./resource-rule.js";
72
+ import { readReferrerRules, type ReferrerRule } from "./referrer-rule.js";
73
+ import {
74
+ evaluateReferrerRules,
75
+ referrerRuleExercised,
76
+ reportReferrerRules,
77
+ reportUnexercisedReferrerRule,
78
+ validateReferrerRuleDeclarations,
79
+ type Referrer,
80
+ type ReferrerRuleDiagnostic,
81
+ type ReferrerRuleIssue,
82
+ } from "./validate-referrer-rules.js";
83
+ import {
84
+ describeProjectionFailure,
85
+ type ProjectionFailure,
86
+ } from "./schema-projection.js";
56
87
  import {
57
88
  validateDynamicSelectors,
58
89
  validateRefSlotDeclarations,
@@ -111,6 +142,7 @@ import { validateReferences } from "./validate-references.js";
111
142
  import { validateReferenceForms } from "./validate-reference-forms.js";
112
143
  import { validateUnusedDeclarations } from "./validate-unused-declarations.js";
113
144
  import { validateThrowsCoverage } from "./validate-throws-coverage.js";
145
+ import { readStepSlot } from "./step-slot.js";
114
146
 
115
147
  const SELF_PREFIX = "Self.";
116
148
 
@@ -373,7 +405,7 @@ export function gatherPropertySchemas(
373
405
  }
374
406
 
375
407
  /**
376
- * Generic, role-driven walk over an `x-telo-step-context` step array. Calls
408
+ * Generic, role-driven walk over a step array. Calls
377
409
  * `visit(step, stepPath)` for every step — top-level and nested through the
378
410
  * `x-telo-topology-role` forms (`branch`, `branch-list`, `case-map`). This is
379
411
  * the single definition of how steps nest, shared by `buildStepContextSchema`
@@ -440,7 +472,7 @@ export function walkStepArray(
440
472
  }
441
473
 
442
474
  /**
443
- * Build a `steps` context schema from `x-telo-step-context` annotation.
475
+ * Build a `steps` context schema for a kind's step body.
444
476
  * Walks each step in the manifest array, resolves the invoked resource's output
445
477
  * contract, and builds `steps.<name>.result` context entries.
446
478
  *
@@ -461,7 +493,7 @@ export function walkStepArray(
461
493
  * sub-properties (e.g. elseif: [{ if, then }]).
462
494
  * - `case-map` — value is an object whose values are step arrays (e.g. cases).
463
495
  * No specific Run.Sequence field name is hardcoded; any kind that uses
464
- * `x-telo-step-context` and tags its branch fields with these roles works.
496
+ * a step body and tags its branch fields with these roles works.
465
497
  */
466
498
  function buildStepContextSchema(
467
499
  manifest: Record<string, any>,
@@ -478,7 +510,7 @@ function buildStepContextSchema(
478
510
  const readingModule = (manifest.metadata as { module?: string } | undefined)?.module;
479
511
 
480
512
  for (const [fieldName, fieldSchema] of Object.entries(props)) {
481
- const stepCtx = fieldSchema["x-telo-step-context"] as Record<string, string> | undefined;
513
+ const stepCtx = readStepSlot(fieldSchema);
482
514
  if (!stepCtx) continue;
483
515
 
484
516
  const invokeField = stepCtx.invoke;
@@ -647,7 +679,7 @@ const NO_ENTRY_POINT_CAPABILITIES = new Set([
647
679
  ]);
648
680
 
649
681
  /**
650
- * Validate `x-telo-step-context` step `invoke` references (e.g. `Run.Sequence`
682
+ * Validate step `invoke` references (e.g. `Run.Sequence`
651
683
  * steps).
652
684
  *
653
685
  * The reference field map deliberately does NOT descend into step `invoke`
@@ -666,7 +698,7 @@ const NO_ENTRY_POINT_CAPABILITIES = new Set([
666
698
  * contract has no entry point → `REFERENCE_KIND_MISMATCH` (runtime
667
699
  * `ERR_RESOURCE_NOT_INVOKABLE`).
668
700
  *
669
- * Generic and topology-driven — it walks steps via the same `x-telo-step-context`
701
+ * Generic and topology-driven — it walks steps via the same step-slot
670
702
  * / `x-telo-topology-role` annotations `buildStepContextSchema` uses (through the
671
703
  * shared `walkStepArray`), so nested branches (then/else/do/catch/cases) are
672
704
  * covered and no `Run.Sequence` field name is hardcoded. The cross-module
@@ -795,8 +827,7 @@ function validateStepInvokeReferences(
795
827
  for (const [fieldName, fieldSchema] of Object.entries(
796
828
  defSchema.properties as Record<string, any>,
797
829
  )) {
798
- const stepCtx = fieldSchema["x-telo-step-context"] as Record<string, string> | undefined;
799
- const invokeField = stepCtx?.invoke;
830
+ const invokeField = readStepSlot(fieldSchema)?.invoke;
800
831
  if (!invokeField) continue;
801
832
  const steps = m[fieldName];
802
833
  if (!Array.isArray(steps)) continue;
@@ -1253,14 +1284,18 @@ export class StaticAnalyzer {
1253
1284
  }
1254
1285
  seen.add(alias);
1255
1286
  }
1256
- if (alias && source) {
1257
- const targetModule =
1258
- resolvedModuleName ?? source.split("/").filter(Boolean).pop() ?? source;
1287
+ // An import whose target identity was never established registers NO
1288
+ // alias. The name is never guessed from the source string: a guess that
1289
+ // is usually right is what turned "this import did not resolve" into an
1290
+ // assertion that a published dependency was malformed, naming a module
1291
+ // no registry could ever hold. With no alias, every use degrades to
1292
+ // "cannot resolve alias '<X>'" — which points at the import the author
1293
+ // has to fix, and which the loader has already reported on its own line.
1294
+ if (alias && source && resolvedModuleName) {
1295
+ const targetModule = resolvedModuleName;
1259
1296
  // Module identity is registered globally so x-telo-ref resolution sees
1260
1297
  // transitively-imported modules regardless of which scope brought them in.
1261
- if (resolvedModuleName) {
1262
- defs.registerModuleIdentity(resolvedNamespace ?? null, resolvedModuleName);
1263
- }
1298
+ defs.registerModuleIdentity(resolvedNamespace ?? null, resolvedModuleName);
1264
1299
  // `metadata.reExportedKinds` (stamped by flattenForAnalyzer / the editor projection)
1265
1300
  // maps an exported suffix to the true owning module's canonical kind for kinds this
1266
1301
  // import transitively re-exports (`exports.kinds: [Alias.Kind]`).
@@ -1318,6 +1353,61 @@ export class StaticAnalyzer {
1318
1353
  const refConstraintIssues: RefConstraintIssue[] = [];
1319
1354
  const refSlotIssues: RefSlotIssue[] = [];
1320
1355
  const zoneSlotIssues: ZoneSlotIssue[] = [];
1356
+ // One place a rule report becomes a diagnostic. The pass decided WHAT and
1357
+ // WHERE; this only carries it across to the diagnostic shape.
1358
+ const SEVERITY = {
1359
+ error: DiagnosticSeverity.Error,
1360
+ warning: DiagnosticSeverity.Warning,
1361
+ information: DiagnosticSeverity.Information,
1362
+ } as const;
1363
+ const resourceRuleDiagnostic = (report: ResourceRuleDiagnostic): AnalysisDiagnostic => ({
1364
+ severity: SEVERITY[report.severity],
1365
+ code: report.code,
1366
+ source: SOURCE,
1367
+ message: report.message,
1368
+ data: {
1369
+ resource: {
1370
+ kind: report.manifest.kind,
1371
+ name: report.manifest.metadata?.name as string,
1372
+ },
1373
+ filePath: (report.manifest.metadata as { source?: string } | undefined)?.source,
1374
+ path: report.path,
1375
+ rule: report.rule,
1376
+ },
1377
+ });
1378
+ const referrerRuleDiagnostic = (report: ReferrerRuleDiagnostic): AnalysisDiagnostic => ({
1379
+ severity: SEVERITY[report.severity],
1380
+ code: report.code,
1381
+ source: SOURCE,
1382
+ message: report.message,
1383
+ data: {
1384
+ resource: {
1385
+ kind: report.manifest.kind,
1386
+ name: report.manifest.metadata?.name as string,
1387
+ },
1388
+ filePath: (report.manifest.metadata as { source?: string } | undefined)?.source,
1389
+ path: report.path,
1390
+ rule: report.rule,
1391
+ },
1392
+ });
1393
+ const projectionIssues: SchemaProjectionIssue[] = [];
1394
+ const resourceRuleIssues: ResourceRuleIssue[] = [];
1395
+ const referrerRuleIssues: ReferrerRuleIssue[] = [];
1396
+ // A rule that never had anything to iterate is never proven — the second way
1397
+ // coverage varies invisibly, beside the dynamic-leaf skip. Tracked across the
1398
+ // whole run and reported once, since "empty on every resource" is not a fact
1399
+ // any single resource can establish.
1400
+ const ruleExercise = new Map<
1401
+ string,
1402
+ { manifest: ResourceManifest; rule: ResourceRule; exercised: boolean; seen: boolean }
1403
+ >();
1404
+ // Same for a referrer rule, where "never exercised" means nothing the
1405
+ // `referrer:` filter matches ever referenced a resource of the kind — which
1406
+ // is exactly what a typo in that filter looks like from the outside.
1407
+ const referrerRuleExercise = new Map<
1408
+ string,
1409
+ { manifest: ResourceManifest; rule: ReferrerRule; exercised: boolean; seen: boolean }
1410
+ >();
1321
1411
  // `x-telo-type` is checked on EVERY manifest, not only on definition docs: a
1322
1412
  // schema fragment is written wherever a kind declares a schema-valued field,
1323
1413
  // so an inline `inputType:` on an ordinary resource carries one just as a
@@ -1350,6 +1440,35 @@ export class StaticAnalyzer {
1350
1440
  refConstraintIssues.push(...issues);
1351
1441
  refSlotIssues.push(...validateRefSlotDeclarations(m as unknown as ResourceManifest));
1352
1442
  zoneSlotIssues.push(...validateZoneSlotDeclarations(m as unknown as ResourceManifest));
1443
+ projectionIssues.push(...validateSchemaProjection(m as unknown as ResourceManifest));
1444
+ // Checked against the MERGED schema, so an `in:` pointer naming an
1445
+ // inherited field resolves — which is what lets a rule shared by every
1446
+ // backend be declared once on the abstract they extend.
1447
+ resourceRuleIssues.push(
1448
+ ...validateResourceRuleDeclarations(
1449
+ m as unknown as ResourceManifest,
1450
+ effectiveAuthorSchema(m as any, (k) => defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k)),
1451
+ ),
1452
+ );
1453
+ referrerRuleIssues.push(
1454
+ ...validateReferrerRuleDeclarations(m as unknown as ResourceManifest),
1455
+ );
1456
+ for (const rule of readReferrerRules((m as Record<string, unknown>).schema)) {
1457
+ referrerRuleExercise.set(`${m.metadata?.module}.${m.metadata?.name}#${rule.code}`, {
1458
+ manifest: m as unknown as ResourceManifest,
1459
+ rule,
1460
+ exercised: false,
1461
+ seen: false,
1462
+ });
1463
+ }
1464
+ for (const rule of readResourceRules((m as Record<string, unknown>).schema)) {
1465
+ ruleExercise.set(`${m.metadata?.module}.${m.metadata?.name}#${rule.code}`, {
1466
+ manifest: m as unknown as ResourceManifest,
1467
+ rule,
1468
+ exercised: false,
1469
+ seen: false,
1470
+ });
1471
+ }
1353
1472
  }
1354
1473
  const resolvedCapability = def.capability
1355
1474
  ? (scopeResolver.resolveKind(def.capability) ?? def.capability)
@@ -1407,6 +1526,25 @@ export class StaticAnalyzer {
1407
1526
  };
1408
1527
  const filePath = (issue.manifest.metadata as { source?: string } | undefined)?.source;
1409
1528
  const data = { resource, filePath, path: issue.path };
1529
+ if (issue.annotation === "referrer") {
1530
+ // Mirrors ZONE_PROVIDER_UNRESOLVED: a filter naming no kind matches no
1531
+ // referrer, so the rule would pass on every manifest while checking
1532
+ // nothing — reported at the kind that wrote it, since the consumer
1533
+ // cannot see that the check is inert.
1534
+ diagnostics.push({
1535
+ severity: DiagnosticSeverity.Error,
1536
+ code: "REFERRER_RULE_INVALID",
1537
+ source: SOURCE,
1538
+ message:
1539
+ `x-telo-referrer-rules 'referrer: ${issue.ref}' at '${issue.path}' names no kind. ` +
1540
+ `The prefix must be an import alias declared in this file's 'imports:' map, ` +
1541
+ `'Self' for a kind in this library, or 'Telo' for a built-in. A filter that ` +
1542
+ `matches nothing leaves the rule inert. Known aliases: ` +
1543
+ `${issue.knownAliases?.join(", ") || "(none)"}.`,
1544
+ data,
1545
+ });
1546
+ continue;
1547
+ }
1410
1548
  if (issue.annotation === "zone") {
1411
1549
  // Mirrors X_TELO_REF_UNRESOLVED: an unresolvable provider kind would
1412
1550
  // leave the requirement silently unenforced — no provider ever
@@ -1493,6 +1631,57 @@ export class StaticAnalyzer {
1493
1631
  // OPPOSITE directions — a dropped requirement is silently unenforced, a
1494
1632
  // dropped provision invents failures — so neither can be left to
1495
1633
  // leniency.
1634
+ // A projection nothing can read does not fail — it stops typing the
1635
+ // consumers counting on it, which puts a misspelled field back where the
1636
+ // projection exists to catch it earlier.
1637
+ for (const issue of resourceRuleIssues) {
1638
+ diagnostics.push({
1639
+ severity: DiagnosticSeverity.Error,
1640
+ code: issue.code,
1641
+ source: SOURCE,
1642
+ message: issue.message,
1643
+ data: {
1644
+ resource: {
1645
+ kind: issue.manifest.kind,
1646
+ name: issue.manifest.metadata?.name as string,
1647
+ },
1648
+ filePath: (issue.manifest.metadata as { source?: string } | undefined)?.source,
1649
+ path: issue.path,
1650
+ },
1651
+ });
1652
+ }
1653
+ for (const issue of referrerRuleIssues) {
1654
+ diagnostics.push({
1655
+ severity: DiagnosticSeverity.Error,
1656
+ code: issue.code,
1657
+ source: SOURCE,
1658
+ message: issue.message,
1659
+ data: {
1660
+ resource: {
1661
+ kind: issue.manifest.kind,
1662
+ name: issue.manifest.metadata?.name as string,
1663
+ },
1664
+ filePath: (issue.manifest.metadata as { source?: string } | undefined)?.source,
1665
+ path: issue.path,
1666
+ },
1667
+ });
1668
+ }
1669
+ for (const issue of projectionIssues) {
1670
+ diagnostics.push({
1671
+ severity: DiagnosticSeverity.Error,
1672
+ code: issue.code,
1673
+ source: SOURCE,
1674
+ message: issue.message,
1675
+ data: {
1676
+ resource: {
1677
+ kind: issue.manifest.kind,
1678
+ name: issue.manifest.metadata?.name as string,
1679
+ },
1680
+ filePath: (issue.manifest.metadata as { source?: string } | undefined)?.source,
1681
+ path: issue.path,
1682
+ },
1683
+ });
1684
+ }
1496
1685
  for (const issue of zoneSlotIssues) {
1497
1686
  diagnostics.push({
1498
1687
  severity: DiagnosticSeverity.Error,
@@ -1532,6 +1721,57 @@ export class StaticAnalyzer {
1532
1721
  aliasesByModule,
1533
1722
  }));
1534
1723
 
1724
+ /**
1725
+ * Whether a referrer of `kind` satisfies a referrer rule's `referrer:`
1726
+ * filter, which is canonical by the time it gets here (`resolveSchemaRefKinds`
1727
+ * rewrote it in the DECLARING module's scope). The referring manifest's own
1728
+ * `kind:` is not — it is whatever alias its author imported the kind under —
1729
+ * so it is resolved the same way every other kind comparison in this pass
1730
+ * resolves one. Liskov-substitutable, matching `checkKind`: a child of the
1731
+ * named kind is one.
1732
+ */
1733
+ const kindMatches = (filter: string, kind: string): boolean => {
1734
+ const resolved = aliases.resolveKind(kind) ?? kind;
1735
+ if (resolved === filter) return true;
1736
+ return defs
1737
+ .getByExtends(filter)
1738
+ .some((d) => `${d.metadata.module}.${d.metadata.name}` === resolved);
1739
+ };
1740
+
1741
+ /**
1742
+ * The resources that reach `manifest`, with the slot each one reaches it
1743
+ * through. A step's edge is attributed to the resource whose body declares
1744
+ * it: a step is not a manifest, and the requirement is about the resource
1745
+ * that has to declare something.
1746
+ *
1747
+ * Deduplication is the evaluation's, not this function's — a referrer
1748
+ * reaching one resource through two slots is two sites, and which one anchors
1749
+ * the diagnostic is a reporting decision.
1750
+ */
1751
+ const referrersOf = (
1752
+ manifest: ResourceManifest,
1753
+ graph: ReturnType<typeof buildCallGraph>,
1754
+ ): Referrer[] => {
1755
+ const name = manifest.metadata?.name as string | undefined;
1756
+ if (!name) return [];
1757
+ const node = graph.resource(manifest.kind, name) ?? graph.resourceByName(name);
1758
+ if (!node) return [];
1759
+ const out: Referrer[] = [];
1760
+ for (const edge of graph.edgesTo(node.id)) {
1761
+ const from = graph.nodes.get(edge.from);
1762
+ if (!from) continue;
1763
+ const owner = from.type === "step" ? graph.nodes.get(from.owner) : from;
1764
+ if (!owner || owner.type !== "resource") continue;
1765
+ out.push({
1766
+ manifest: owner.manifest,
1767
+ kind: owner.kind,
1768
+ name: owner.name,
1769
+ path: edge.path,
1770
+ });
1771
+ }
1772
+ return out;
1773
+ };
1774
+
1535
1775
  // A `use` case map's selector written in CEL is a hard diagnostic — a call
1536
1776
  // graph known only at runtime is not statically analyzable, and no fallback
1537
1777
  // is conservative for every consumer. Scoped to the entry's own modules:
@@ -1567,6 +1807,32 @@ export class StaticAnalyzer {
1567
1807
  cache: zoneExportCache,
1568
1808
  }),
1569
1809
  );
1810
+
1811
+ // Durable regions — the SAME graph again, walked DOWNWARD this time.
1812
+ // Every rule here keys off a zone attribute rather than off any kind, so
1813
+ // a backend that ships its own workflow kind is covered without the
1814
+ // analyzer knowing it exists: going native costs a module, not a change
1815
+ // here.
1816
+ const resolveRegionDef = (kind: string, module?: string) => {
1817
+ const scope = (module ? aliasesByModule.get(module) : undefined) ?? aliases;
1818
+ const canonical = scope.resolveKind(kind);
1819
+ return defs.resolve(kind) ?? (canonical ? defs.resolve(canonical) : undefined);
1820
+ };
1821
+ diagnostics.push(
1822
+ ...validateDurableRegions({
1823
+ graph: getCallGraph(),
1824
+ resolveDef: resolveRegionDef,
1825
+ reportModules: rootModules,
1826
+ }),
1827
+ // The same walk once more, over EVERY attribute rather than the two
1828
+ // durability names — a region must not contain a resource that declares
1829
+ // it cannot honour what the region promises.
1830
+ ...validateZoneViolations({
1831
+ graph: getCallGraph(),
1832
+ resolveDef: resolveRegionDef,
1833
+ reportModules: rootModules,
1834
+ }),
1835
+ );
1570
1836
  }
1571
1837
 
1572
1838
  // Phase 2.6: register each named `Telo.Type` resource's schema under its
@@ -1963,6 +2229,69 @@ export class StaticAnalyzer {
1963
2229
  }
1964
2230
  }
1965
2231
 
2232
+ // Resource rules — relationships between this resource's own fields that
2233
+ // JSON Schema cannot state, declared by the kind as CEL over `self` and
2234
+ // `this`. Read off the AUTHOR-FACING schema, so an `extends` child without
2235
+ // `base:` inherits its parent's rules and one that declares its own
2236
+ // replaces them, exactly as the rest of the config contract merges.
2237
+ // The finding→diagnostic mapping lives with the finding vocabulary in
2238
+ // `validate-resource-rules.ts`, the shape every neighbouring pass uses:
2239
+ // issues out, one emit here.
2240
+ const ruleDeclarer = (definition.metadata as { module?: string } | undefined)?.module;
2241
+ for (const report of reportResourceRules(
2242
+ m as unknown as ResourceManifest,
2243
+ definition as unknown as ResourceManifest,
2244
+ evaluateResourceRules(m as unknown as ResourceManifest, authorSchema),
2245
+ !ruleDeclarer || rootModules.has(ruleDeclarer),
2246
+ )) {
2247
+ diagnostics.push(resourceRuleDiagnostic(report));
2248
+ }
2249
+ for (const rule of readResourceRules(authorSchema)) {
2250
+ const key = `${definition.metadata?.module}.${definition.metadata?.name}#${rule.code}`;
2251
+ const tracked = ruleExercise.get(key);
2252
+ if (!tracked) continue;
2253
+ tracked.seen = true;
2254
+ if (ruleExercised(m as unknown as ResourceManifest, rule)) tracked.exercised = true;
2255
+ }
2256
+
2257
+ // Referrer rules — what must be true of whoever REFERENCES this resource,
2258
+ // declared by the kind that has the requirement rather than by the kind
2259
+ // that must satisfy it. The subject is chosen by the EDGE, so no kind
2260
+ // literal appears on the referring side, where the spelling would be the
2261
+ // consumer's import alias rather than anything the rule's author controls.
2262
+ // A consumer of the shared call graph, never a second traversal.
2263
+ const referrerRules = readReferrerRules(authorSchema);
2264
+ if (referrerRules.length > 0) {
2265
+ const referrers = referrersOf(m as unknown as ResourceManifest, getCallGraph());
2266
+ for (const report of reportReferrerRules(
2267
+ m as unknown as ResourceManifest,
2268
+ definition as unknown as ResourceManifest,
2269
+ evaluateReferrerRules(
2270
+ m as unknown as ResourceManifest,
2271
+ authorSchema,
2272
+ referrers,
2273
+ kindMatches,
2274
+ ),
2275
+ !ruleDeclarer || rootModules.has(ruleDeclarer),
2276
+ )) {
2277
+ // A VIOLATION is the referrer's data, so it is reported only when that
2278
+ // manifest is the entry's own — the same direction a resource-rule
2279
+ // violation takes, one hop further out.
2280
+ const owner = (report.manifest.metadata as { module?: string } | undefined)?.module;
2281
+ if (report.code === "REFERRER_RULE_VIOLATED" && owner && !rootModules.has(owner)) {
2282
+ continue;
2283
+ }
2284
+ diagnostics.push(referrerRuleDiagnostic(report));
2285
+ }
2286
+ for (const rule of referrerRules) {
2287
+ const key = `${definition.metadata?.module}.${definition.metadata?.name}#${rule.code}`;
2288
+ const tracked = referrerRuleExercise.get(key);
2289
+ if (!tracked) continue;
2290
+ tracked.seen = true;
2291
+ if (referrerRuleExercised(rule, referrers, kindMatches)) tracked.exercised = true;
2292
+ }
2293
+ }
2294
+
1966
2295
  // Validate inline resources nested inside this resource's body (e.g. a
1967
2296
  // Run.Sequence step's `invoke: { kind, ...config }`). These sit at
1968
2297
  // x-telo-ref slots reached only through local `$ref`s, which the
@@ -1998,6 +2327,24 @@ export class StaticAnalyzer {
1998
2327
  // (Invocation context compatibility check is handled via x-telo-context in the CEL pass below)
1999
2328
  }
2000
2329
 
2330
+ // A rule whose collection was empty on every resource of its kind is a rule
2331
+ // nothing proved. `check()` types `self` only shallowly (cel-js takes a flat
2332
+ // field map, so `columns` is `map`), which means a typo below the first level
2333
+ // survives declaration validation and is caught only by evaluation — so a
2334
+ // rule that never evaluated has been verified by nothing at all. Reported
2335
+ // only when the kind HAS resources here: a kind nobody instantiated in this
2336
+ // workspace says nothing about the rule.
2337
+ for (const tracked of ruleExercise.values()) {
2338
+ if (!tracked.seen || tracked.exercised) continue;
2339
+ diagnostics.push(resourceRuleDiagnostic(reportUnexercisedRule(tracked.manifest, tracked.rule)));
2340
+ }
2341
+ for (const tracked of referrerRuleExercise.values()) {
2342
+ if (!tracked.seen || tracked.exercised) continue;
2343
+ diagnostics.push(
2344
+ referrerRuleDiagnostic(reportUnexercisedReferrerRule(tracked.manifest, tracked.rule)),
2345
+ );
2346
+ }
2347
+
2001
2348
  // Template-body structural validations: check that template entry-points produce
2002
2349
  // values matching the contract of their dispatch target and (for `provide:`)
2003
2350
  // the abstract this definition `extends`. CEL fields inside the templated
@@ -2092,6 +2439,40 @@ export class StaticAnalyzer {
2092
2439
  }
2093
2440
  }
2094
2441
 
2442
+ // A consumer's slot typed from a referenced declaration
2443
+ // (`x-telo-schema-projection-from`) fails in the direction that is hardest
2444
+ // to notice: the contract silently reopens, so a misspelled field passes
2445
+ // `telo check` exactly as it did before the projection existed. That is the
2446
+ // failure the projection exists to move earlier, so it is reported here
2447
+ // rather than left to degrade. Entry-module-scoped, like
2448
+ // `X_TELO_REF_UNRESOLVED` — a published dependency's slot is not the
2449
+ // consumer's to fix.
2450
+ for (const m of allManifests) {
2451
+ const md = m as Record<string, any>;
2452
+ if (typeof md.kind !== "string" || md.kind.startsWith("Telo.")) continue;
2453
+ const ownModule = (md.metadata as { module?: string } | undefined)?.module;
2454
+ if (ownModule && !rootModules.has(ownModule)) continue;
2455
+ const definition = contractScope.resolveIn(md.kind, ownModule);
2456
+ if (!definition) continue;
2457
+ const failures: ProjectionFailure[] = [];
2458
+ for (const direction of ["inputType", "outputType"] as const) {
2459
+ resolveContract(direction, md, definition, contractScope, failures);
2460
+ }
2461
+ for (const failure of failures) {
2462
+ diagnostics.push({
2463
+ severity: DiagnosticSeverity.Error,
2464
+ code: "SCHEMA_PROJECTION_FROM_UNRESOLVED",
2465
+ source: SOURCE,
2466
+ message: `${md.kind}: ${describeProjectionFailure(failure)}`,
2467
+ data: {
2468
+ resource: { kind: md.kind, name: (md.metadata as any)?.name as string },
2469
+ filePath: (md.metadata as { source?: string } | undefined)?.source,
2470
+ path: failure.pointer.replace(/^\//, ""),
2471
+ },
2472
+ });
2473
+ }
2474
+ }
2475
+
2095
2476
  // Validate CEL syntax and context variable access in all manifests. The
2096
2477
  // walker discovers every compiled CEL node by scanning the value tree and
2097
2478
  // hands back the `x-telo-context` schema matched at the enclosing path; the
package/src/call-graph.ts CHANGED
@@ -55,6 +55,7 @@ import {
55
55
  type RefUse,
56
56
  type RefUseCases,
57
57
  } from "./ref-slot.js";
58
+ import { isStepSlot } from "./step-slot.js";
58
59
  import { isRefEntry, resolveFieldEntries, type RefFieldEntry } from "./reference-field-map.js";
59
60
  import { DEPENDENCY_GRAPH_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
60
61
 
@@ -292,12 +293,6 @@ function resolveUseAtSite(
292
293
  return { use: possibleUses(slot), unresolved: entry.useCases, unresolvedReason };
293
294
  }
294
295
 
295
- /** Names the step-list annotation on an array property, if any. */
296
- function stepContextOf(schema: Record<string, any> | undefined): Record<string, any> | undefined {
297
- const annotation = schema?.["x-telo-step-context"];
298
- return annotation && typeof annotation === "object" ? annotation : undefined;
299
- }
300
-
301
296
  /** A resolved plain reference value (`{kind, name}`, optionally `alias`) — the
302
297
  * shape `resolveRefSentinels` leaves at a ref site. NOT a step: a bare boot
303
298
  * target written `!ref X` must not mint a step node. */
@@ -545,8 +540,7 @@ export function buildCallGraph(
545
540
  if (!schema) return;
546
541
  const collected: StepGraphNode[] = [];
547
542
  for (const [key, propSchema] of propertySchemas(schema)) {
548
- const annotation = stepContextOf(propSchema);
549
- if (!annotation) continue;
543
+ if (!isStepSlot(propSchema)) continue;
550
544
  const value = (node.manifest as Record<string, unknown>)[key];
551
545
  if (!Array.isArray(value)) continue;
552
546
  walkSteps(value, key, undefined, {
@@ -204,7 +204,7 @@ export function flattenForAnalyzer(graph: LoadedGraph): ResourceManifest[] {
204
204
  const alias = m.metadata?.name as string | undefined;
205
205
  if (!owner || !alias) continue;
206
206
  const edge = graph.importEdges.get(owner)?.get(alias);
207
- if (!edge?.targetModuleName) continue;
207
+ if (!edge) continue;
208
208
 
209
209
  const newMetadata: Record<string, unknown> = {
210
210
  ...m.metadata,
@@ -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
  });