@telorun/analyzer 0.61.0 → 0.62.1

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 (73) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +130 -9
  3. package/dist/builtins.d.ts.map +1 -1
  4. package/dist/builtins.js +69 -12
  5. package/dist/cel-bindings.d.ts +0 -6
  6. package/dist/cel-bindings.d.ts.map +1 -1
  7. package/dist/cel-bindings.js +3 -28
  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 +31 -2
  11. package/dist/identifier-name.d.ts +114 -0
  12. package/dist/identifier-name.d.ts.map +1 -0
  13. package/dist/identifier-name.js +183 -0
  14. package/dist/index.d.ts +10 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +8 -0
  17. package/dist/manifest-schemas.d.ts +81 -0
  18. package/dist/manifest-schemas.d.ts.map +1 -1
  19. package/dist/manifest-schemas.js +208 -6
  20. package/dist/release/payload-digest.d.ts +7 -3
  21. package/dist/release/payload-digest.d.ts.map +1 -1
  22. package/dist/release/payload-digest.js +7 -3
  23. package/dist/requires-block.d.ts +125 -0
  24. package/dist/requires-block.d.ts.map +1 -0
  25. package/dist/requires-block.js +182 -0
  26. package/dist/schema-keywords.d.ts +68 -0
  27. package/dist/schema-keywords.d.ts.map +1 -0
  28. package/dist/schema-keywords.js +324 -0
  29. package/dist/schema-region.d.ts +12 -1
  30. package/dist/schema-region.d.ts.map +1 -1
  31. package/dist/schema-region.js +12 -1
  32. package/dist/telo-version.d.ts +3 -0
  33. package/dist/telo-version.d.ts.map +1 -0
  34. package/dist/telo-version.js +8 -0
  35. package/dist/types.d.ts +31 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/validate-identifier-names.d.ts +31 -0
  38. package/dist/validate-identifier-names.d.ts.map +1 -0
  39. package/dist/validate-identifier-names.js +144 -0
  40. package/dist/validate-observed-state.d.ts +9 -2
  41. package/dist/validate-observed-state.d.ts.map +1 -1
  42. package/dist/validate-observed-state.js +9 -2
  43. package/dist/validate-references.d.ts.map +1 -1
  44. package/dist/validate-references.js +5 -26
  45. package/dist/validate-requires.d.ts +49 -0
  46. package/dist/validate-requires.d.ts.map +1 -0
  47. package/dist/validate-requires.js +99 -0
  48. package/dist/value-type-keyword.d.ts +1 -1
  49. package/dist/value-type-keyword.d.ts.map +1 -1
  50. package/dist/value-type-keyword.js +1 -0
  51. package/dist/version-range.d.ts +88 -0
  52. package/dist/version-range.d.ts.map +1 -0
  53. package/dist/version-range.js +173 -0
  54. package/package.json +2 -2
  55. package/src/analyzer.ts +146 -10
  56. package/src/builtins.ts +73 -12
  57. package/src/cel-bindings.ts +3 -28
  58. package/src/definition-registry.ts +30 -2
  59. package/src/identifier-name.ts +228 -0
  60. package/src/index.ts +34 -0
  61. package/src/manifest-schemas.ts +223 -4
  62. package/src/release/payload-digest.ts +7 -3
  63. package/src/requires-block.ts +253 -0
  64. package/src/schema-keywords.ts +359 -0
  65. package/src/schema-region.ts +12 -1
  66. package/src/telo-version.ts +9 -0
  67. package/src/types.ts +32 -0
  68. package/src/validate-identifier-names.ts +173 -0
  69. package/src/validate-observed-state.ts +9 -2
  70. package/src/validate-references.ts +5 -26
  71. package/src/validate-requires.ts +129 -0
  72. package/src/value-type-keyword.ts +1 -0
  73. package/src/version-range.ts +238 -0
package/src/analyzer.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  plainChainOf,
9
9
  type CelSurface,
10
10
  } from "@telorun/templating";
11
- import type { DiagnosticFix } from "./types.js";
11
+ import type { DiagnosticData, DiagnosticFix } from "./types.js";
12
12
  import {
13
13
  AliasResolver,
14
14
  moduleScopedDefResolver,
@@ -89,17 +89,19 @@ import {
89
89
  BINDINGS_ANNOTATION,
90
90
  bindingContextProperties,
91
91
  bindingPathChain,
92
- CEL_RESERVED_WORDS,
93
92
  findBindingSites,
94
93
  resolveBindingOrder,
95
94
  schemaAtChain,
96
95
  type BindingSites,
97
96
  } from "./cel-bindings.js";
97
+ import { CEL_RESERVED_WORDS, checkName } from "./identifier-name.js";
98
+ import { validateIdentifierNames } from "./validate-identifier-names.js";
98
99
  import { validateExtends } from "./validate-extends.js";
99
100
  import { validateLogging } from "./validate-logging.js";
100
101
  import { validateModuleArtifact } from "./validate-module-artifact.js";
101
102
  import { validateIncludePlacement } from "./validate-include-placement.js";
102
103
  import { validateModuleMetadata } from "./validate-module-metadata.js";
104
+ import { validateRequires } from "./validate-requires.js";
103
105
  import { validateBaseMapping } from "./validate-base-mapping.js";
104
106
  import { validateInvocationContract } from "./validate-invocation-contract.js";
105
107
  import { collectStepInputIssues } from "./validate-step-inputs.js";
@@ -1029,6 +1031,86 @@ export interface StaticAnalyzerOptions {
1029
1031
  celHandlers?: CelHandlers;
1030
1032
  }
1031
1033
 
1034
+ /**
1035
+ * Files belonging to a module this runtime declared itself unable to read.
1036
+ *
1037
+ * Attribution is by FILE rather than by resource identity, the same choice
1038
+ * `remapMigratedPaths` makes and for the same reason: a diagnostic carries at
1039
+ * most two routing facts and routinely only one, so indexing by resource would
1040
+ * leave every diagnostic without `data.resource` unreachable.
1041
+ *
1042
+ * **A module NAME is not a graph-unique key.** Names are module-scoped, so two
1043
+ * libraries may both be called `Store` — CLAUDE.md names this exact hazard for
1044
+ * migration provenance ("two libraries declaring a Store would share one
1045
+ * bucket"), where the answer is to narrow or not remap at all. Same answer here:
1046
+ * the gated doc's own `metadata.source` is always suppressed, and a name is used
1047
+ * to reach its `include:` partials only when that name identifies exactly ONE
1048
+ * module doc in the set. Where it does not, the partials keep their diagnostics
1049
+ * rather than risk silencing an unrelated library's — a stray extra diagnostic is
1050
+ * a far cheaper failure than a hidden one.
1051
+ *
1052
+ * A module doc with no `source` contributes nothing — suppressing on a guess
1053
+ * would hide diagnostics belonging to files nobody named.
1054
+ */
1055
+ function filesOfUnreadableModules(
1056
+ manifests: ResourceManifest[],
1057
+ requiresDiagnostics: AnalysisDiagnostic[],
1058
+ ): ReadonlySet<string> {
1059
+ const files = new Set<string>();
1060
+ const gatedNames = new Set<string>();
1061
+
1062
+ for (const d of requiresDiagnostics) {
1063
+ if (d.code !== "MODULE_REQUIRES_NEWER_RUNTIME") continue;
1064
+ const data = d.data as DiagnosticData | undefined;
1065
+ // The gated document itself, addressed by the file it was declared in.
1066
+ if (typeof data?.filePath === "string" && data.filePath) files.add(data.filePath);
1067
+ const name = data?.resource?.name;
1068
+ if (typeof name === "string") gatedNames.add(name);
1069
+ }
1070
+ if (files.size === 0 && gatedNames.size === 0) return files;
1071
+
1072
+ // How many module docs answer to each gated name — the ambiguity test.
1073
+ const docsPerName = new Map<string, number>();
1074
+ for (const m of manifests) {
1075
+ if (m.kind !== "Telo.Application" && m.kind !== "Telo.Library") continue;
1076
+ const name = (m.metadata as { name?: string } | undefined)?.name;
1077
+ if (typeof name === "string" && gatedNames.has(name)) {
1078
+ docsPerName.set(name, (docsPerName.get(name) ?? 0) + 1);
1079
+ }
1080
+ }
1081
+
1082
+ for (const m of manifests) {
1083
+ const metadata = (m.metadata ?? {}) as Record<string, unknown>;
1084
+ const owner = metadata.module;
1085
+ if (typeof owner !== "string" || !gatedNames.has(owner)) continue;
1086
+ if (docsPerName.get(owner) !== 1) continue; // ambiguous — do not guess
1087
+ if (typeof metadata.source === "string" && metadata.source) files.add(metadata.source);
1088
+ }
1089
+ return files;
1090
+ }
1091
+
1092
+ /**
1093
+ * Drop every diagnostic anchored in a file whose module this runtime cannot
1094
+ * read, except the gate diagnostic itself.
1095
+ *
1096
+ * A filter rather than a guard on each validator: threading "skip this module"
1097
+ * through thirty validators would make each one responsible for a rule none of
1098
+ * them owns, and a validator added later would silently opt out of it. A
1099
+ * diagnostic with no `filePath` is KEPT — suppression must never be the default
1100
+ * for something it cannot attribute.
1101
+ */
1102
+ function suppressUnreadableModuleDiagnostics(
1103
+ diagnostics: AnalysisDiagnostic[],
1104
+ unreadableFiles: ReadonlySet<string>,
1105
+ ): AnalysisDiagnostic[] {
1106
+ if (unreadableFiles.size === 0) return diagnostics;
1107
+ return diagnostics.filter((d) => {
1108
+ if (d.code === "MODULE_REQUIRES_NEWER_RUNTIME") return true;
1109
+ const filePath = (d.data as DiagnosticData | undefined)?.filePath;
1110
+ return typeof filePath !== "string" || !unreadableFiles.has(filePath);
1111
+ });
1112
+ }
1113
+
1032
1114
  export class StaticAnalyzer {
1033
1115
  private readonly celEnv: Environment;
1034
1116
 
@@ -1527,6 +1609,25 @@ export class StaticAnalyzer {
1527
1609
  });
1528
1610
  }
1529
1611
  }
1612
+ // Declared runtime requirements, FIRST among the validators and suppressing
1613
+ // the rest for any module this runtime cannot read. A module that adopted
1614
+ // newer syntax also produces the vocabulary errors that syntax causes here —
1615
+ // an unknown `use` token, an object where a zone annotation expects a
1616
+ // pointer, an `additionalProperties` violation against a kernel-owned
1617
+ // schema — every one of which is true and blames the module's author for a
1618
+ // version skew. Reporting them beside the gate would bury the one message
1619
+ // that names the actual cause and the actual fix.
1620
+ const requiresDiagnostics = validateRequires(allManifests as unknown as ResourceManifest[], {
1621
+ teloVersion: options?.teloVersion,
1622
+ hostVersions: options?.hostVersions,
1623
+ entryModules: rootModules,
1624
+ });
1625
+ const unreadableFiles = filesOfUnreadableModules(
1626
+ allManifests as unknown as ResourceManifest[],
1627
+ requiresDiagnostics,
1628
+ );
1629
+ diagnostics.push(...requiresDiagnostics);
1630
+
1530
1631
  if (!options?.skipValidation) {
1531
1632
  diagnostics.push(
1532
1633
  ...validateSchemaTypeRefs(allManifests, defs, aliases, aliasesByModule, rootModules),
@@ -1543,6 +1644,19 @@ export class StaticAnalyzer {
1543
1644
  // these fields, which is precisely why they need a check: a mistyped one
1544
1645
  // has no runtime failure mode that would ever surface it.
1545
1646
  diagnostics.push(...validateModuleMetadata(allManifests, defs, aliases));
1647
+ // Every author-written name. Telo has no lexer, so a name's shape is
1648
+ // unchecked where it is declared and its consequences land at whichever
1649
+ // CEL site reads it — for a hyphen, sometimes as silent arithmetic. Takes
1650
+ // the call graph for step names rather than re-walking the step arrays.
1651
+ diagnostics.push(
1652
+ ...validateIdentifierNames(
1653
+ allManifests as unknown as ResourceManifest[],
1654
+ defs,
1655
+ aliases,
1656
+ rootModules,
1657
+ getCallGraph(),
1658
+ ),
1659
+ );
1546
1660
  // A file embed resolves at resource creation, so one written on a doc that
1547
1661
  // is never instantiated is read by nothing and would ship silently.
1548
1662
  diagnostics.push(...validateIncludePlacement(allManifests));
@@ -1556,7 +1670,7 @@ export class StaticAnalyzer {
1556
1670
  // normalisation have already run above; that's all downstream
1557
1671
  // consumers (prepare, init loop) require.
1558
1672
  if (options?.skipValidation) {
1559
- return diagnostics;
1673
+ return suppressUnreadableModuleDiagnostics(diagnostics, unreadableFiles);
1560
1674
  }
1561
1675
 
1562
1676
  // Build a name→manifest map for looking up referenced resources
@@ -2109,14 +2223,33 @@ export class StaticAnalyzer {
2109
2223
 
2110
2224
  for (const name of Object.keys(declared)) {
2111
2225
  const shadows = inScope.has(name);
2112
- if (!shadows && !keywords.has(name)) continue;
2226
+ if (shadows || keywords.has(name)) {
2227
+ diagnostics.push({
2228
+ severity: DiagnosticSeverity.Error,
2229
+ code: "BINDING_NAME_RESERVED",
2230
+ source: SOURCE,
2231
+ message: shadows
2232
+ ? `${m.kind}/${bindingsName}: binding '${name}' shadows a variable already in scope here (${[...inScope].sort().join(", ")}). Rename the binding — a scope variable always wins, so this one would never be read.`
2233
+ : `${m.kind}/${bindingsName}: binding '${name}' is a CEL keyword, so no expression can read it as a reference. Rename the binding.`,
2234
+ data: {
2235
+ resource: resourceRef,
2236
+ filePath: bindingsFile,
2237
+ path: `${celBindingSites.field}.${name}`,
2238
+ },
2239
+ });
2240
+ continue;
2241
+ }
2242
+ // A binding is read by bare name, so it lives in the same
2243
+ // identifier space as a resource or step name and breaks the
2244
+ // same way. The keyword tier is unreachable here — the check
2245
+ // above owns it, and can also say what is being shadowed.
2246
+ const violation = checkName(name, "value", "binding name");
2247
+ if (!violation) continue;
2113
2248
  diagnostics.push({
2114
- severity: DiagnosticSeverity.Error,
2115
- code: "BINDING_NAME_RESERVED",
2249
+ severity: violation.severity,
2250
+ code: violation.code,
2116
2251
  source: SOURCE,
2117
- message: shadows
2118
- ? `${m.kind}/${bindingsName}: binding '${name}' shadows a variable already in scope here (${[...inScope].sort().join(", ")}). Rename the binding — a scope variable always wins, so this one would never be read.`
2119
- : `${m.kind}/${bindingsName}: binding '${name}' is a CEL keyword, so no expression can read it as a reference. Rename the binding.`,
2252
+ message: `${m.kind}/${bindingsName}: ${violation.message}`,
2120
2253
  data: {
2121
2254
  resource: resourceRef,
2122
2255
  filePath: bindingsFile,
@@ -2514,7 +2647,10 @@ export class StaticAnalyzer {
2514
2647
 
2515
2648
  // Reroute diagnostics on synthetic (inline-extracted) resources back to
2516
2649
  // the chain root so position-index lookups land on the parent doc.
2517
- return rewriteSyntheticOrigins(diagnostics, allManifests);
2650
+ return rewriteSyntheticOrigins(
2651
+ suppressUnreadableModuleDiagnostics(diagnostics, unreadableFiles),
2652
+ allManifests,
2653
+ );
2518
2654
  }
2519
2655
 
2520
2656
  analyzeErrors(
package/src/builtins.ts CHANGED
@@ -1,6 +1,29 @@
1
- import { manifestFragment } from "./manifest-schemas.js";
1
+ import { manifestFragment, manifestFragmentRef, withSchemaFragments } from "./manifest-schemas.js";
2
+
3
+ /** A slot holding author-written JSON Schema. Localized and hoisted by
4
+ * {@link withSchemaFragments} on the enclosing schema, which is what makes the
5
+ * `#/$defs` pointer resolve inside whatever AJV compiles.
6
+ *
7
+ * `KindSchema` and `JsonSchema7` share a body; the name is the discriminator
8
+ * the IDE reads off the `x-telo-fragment` stamp to decide whether the
9
+ * `x-telo-*` vocabulary belongs here. A kind's own `schema:` is where it does;
10
+ * a `status:` block or an `inputType:` describes plain data, where it does not. */
11
+ const kindSchemaSlot = {
12
+ title: "Schema",
13
+ description: "Configuration this kind accepts, as JSON Schema plus `x-telo-*` annotations.",
14
+ $ref: manifestFragmentRef("KindSchema"),
15
+ };
16
+
17
+ /** Observed state a kind reports while running, as a data schema. `required:` is
18
+ * rejected separately by `validateObservedStateDeclarations`, which can say why
19
+ * and what to write instead. */
20
+ const observedStateSlot = {
21
+ title: "Observed state",
22
+ description:
23
+ "What a resource of this kind reports while running, published at `resources.<name>.status.<field>`.",
24
+ $ref: manifestFragmentRef("JsonSchema7"),
25
+ };
2
26
  import type { ResourceDefinition } from "@telorun/sdk";
3
- import { OBSERVED_STATE_SCHEMA } from "./validate-observed-state.js";
4
27
 
5
28
  /** Descriptive provenance a module declares about itself, shared by
6
29
  * `Telo.Application` and `Telo.Library`.
@@ -24,6 +47,32 @@ const PROVENANCE_METADATA = {
24
47
  documentation: { type: "string" },
25
48
  };
26
49
 
50
+ /** The declared runtime requirements block, shared by `Telo.Application` and
51
+ * `Telo.Library` — the two module kinds, whose schemas are otherwise
52
+ * independent and would drift.
53
+ *
54
+ * **Deliberately says only "these are objects", and nothing about the values.**
55
+ * The grammar belongs to `requires-block.ts`, which is the single reader, and
56
+ * every rule that matters — `^` and `~` refused, a bare version refused, bounds
57
+ * that must not exclude each other, an upper bound that must name a version that
58
+ * exists — needs a parse and a comparison, not a schema. Adding `type: "string"`
59
+ * here bought nothing and cost a duplicate: `telo: 80` then produced BOTH a
60
+ * `SCHEMA_VIOLATION` and a `REQUIRES_INVALID` for one node, one of them phrased
61
+ * by a layer that does not know what the value is for.
62
+ *
63
+ * Left open at both tiers for the same reason. An unrecognized axis is reported
64
+ * by the reader with the vocabulary it knows — a far better message than AJV's —
65
+ * and, critically, is SUPPRESSED while the `telo` requirement is itself unmet,
66
+ * since an older runtime not knowing a newer axis is a consequence of the
67
+ * version skew rather than a second defect. AJV cannot express that ordering. */
68
+ const REQUIRES_SCHEMA = {
69
+ type: "object",
70
+ properties: {
71
+ host: { type: "object" },
72
+ },
73
+ additionalProperties: true,
74
+ };
75
+
27
76
  /** Author-declared subset of `files:` that ships in the artifact's lazily
28
77
  * materialized `assets` layer. Optional: an unclaimed file joins the `common`
29
78
  * layer, which is pulled alongside any controller layer, so omitting this costs
@@ -278,13 +327,13 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
278
327
  // kernel registers this controller directly at boot, before any lazy
279
328
  // resolution — it states truthfully who provides it.
280
329
  controllers: [{ runtime: "kernel", entry: "Telo.JsonSchema" }],
281
- schema: {
330
+ schema: withSchemaFragments({
282
331
  type: "object",
283
332
  properties: {
284
333
  schema: {
285
334
  title: "Schema",
286
335
  description: "JSON Schema definition for the declared data type.",
287
- type: "object",
336
+ $ref: manifestFragmentRef("JsonSchema7"),
288
337
  },
289
338
  extends: {
290
339
  title: "Extends",
@@ -319,13 +368,13 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
319
368
  },
320
369
  required: ["schema"],
321
370
  additionalProperties: false,
322
- },
371
+ }),
323
372
  },
324
373
  {
325
374
  kind: "Telo.Definition",
326
375
  metadata: { name: "Abstract", module: "Telo" },
327
376
  capability: "Telo.Template",
328
- schema: {
377
+ schema: withSchemaFragments({
329
378
  type: "object",
330
379
  properties: {
331
380
  kind: { type: "string" },
@@ -336,15 +385,15 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
336
385
  additionalProperties: true,
337
386
  },
338
387
  capability: { type: "string" },
339
- schema: { type: "object", additionalProperties: true },
340
- status: OBSERVED_STATE_SCHEMA,
388
+ schema: kindSchemaSlot,
389
+ status: observedStateSlot,
341
390
  },
342
391
  required: ["metadata"],
343
392
  // Telo.Abstract is an extension point by design — it must accept forward-compatible
344
393
  // fields (e.g. inputType/outputType from the typed-abstracts plan) without requiring
345
394
  // the analyzer to enumerate them here.
346
395
  additionalProperties: true,
347
- },
396
+ }),
348
397
  },
349
398
  {
350
399
  kind: "Telo.Definition",
@@ -362,11 +411,17 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
362
411
  // matching how Run.Sequence steps factor dispatch from data. The dispatch
363
412
  // entry-point (`invoke` / `provide` / `run`) determines how `inputs`/`result`
364
413
  // are interpreted at runtime. See analyzer/nodejs/plans/template-internal-cel-validation.md.
365
- schema: {
414
+ schema: withSchemaFragments({
366
415
  type: "object",
367
416
  additionalProperties: true,
368
417
  properties: {
369
- status: OBSERVED_STATE_SCHEMA,
418
+ // The kind's own configuration contract. Declared as a slot for the
419
+ // first time here: it was reachable only as an unnamed extra property,
420
+ // so nothing could say what belonged in it — no completion inside a
421
+ // `schema:` block, and a misspelled keyword surviving to a runtime
422
+ // failure that named a different field.
423
+ schema: kindSchemaSlot,
424
+ status: observedStateSlot,
370
425
  resources: {
371
426
  type: "array",
372
427
  items: {
@@ -536,7 +591,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
536
591
  },
537
592
  },
538
593
  },
539
- },
594
+ }),
540
595
  },
541
596
  {
542
597
  kind: "Telo.Definition",
@@ -787,6 +842,9 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
787
842
  // CLI flag — so a level derived from the host environment goes through a
788
843
  // `variables:` entry read with `!cel`. See kernel/specs/logging.md §12.
789
844
  logging: ROOT_LOGGING_SCHEMA,
845
+ // The runtime range this module is verified against. See
846
+ // `analyzer/nodejs/src/requires-block.ts`.
847
+ requires: REQUIRES_SCHEMA,
790
848
  },
791
849
  required: ["metadata"],
792
850
  additionalProperties: false,
@@ -877,6 +935,9 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
877
935
  },
878
936
  additionalProperties: true,
879
937
  },
938
+ // The runtime range this module is verified against. See
939
+ // `analyzer/nodejs/src/requires-block.ts`.
940
+ requires: REQUIRES_SCHEMA,
880
941
  },
881
942
  required: ["metadata"],
882
943
  additionalProperties: false,
@@ -20,34 +20,9 @@ export interface BindingSites {
20
20
  scopeNames: Set<string>;
21
21
  }
22
22
 
23
- /**
24
- * CEL keywords. A binding named after one is unreachable — `true` lexes as a
25
- * literal, `in` as an operator so it is reserved alongside the scope names,
26
- * which turns silence into a diagnostic.
27
- */
28
- export const CEL_RESERVED_WORDS: readonly string[] = [
29
- "as",
30
- "break",
31
- "const",
32
- "continue",
33
- "else",
34
- "false",
35
- "for",
36
- "function",
37
- "if",
38
- "import",
39
- "in",
40
- "let",
41
- "loop",
42
- "namespace",
43
- "null",
44
- "package",
45
- "return",
46
- "true",
47
- "var",
48
- "void",
49
- "while",
50
- ];
23
+ // CEL keywords live in `identifier-name.ts` — a binding named after one is
24
+ // unreachable for exactly the reason a resource or step named after one is, so
25
+ // the list belongs to the identifier vocabulary rather than to this file.
51
26
 
52
27
  /** Locate a kind's bindings field and the scope names its annotated contexts
53
28
  * declare. Returns undefined for a kind that declares no bindings region. */
@@ -116,11 +116,36 @@ export class DefinitionRegistry {
116
116
  registerNamedTypeSchema(id: string, schema: Record<string, any>): boolean {
117
117
  if (this.definitionSchemaIds.has(id)) return false;
118
118
  if (this.registeredSchemaIds.has(id) || this.ajv.getSchema(id)) return true;
119
- this.ajv.addSchema(schema, id);
119
+ if (!this.tryAddSchema(schema, id)) return true;
120
120
  this.registeredSchemaIds.add(id);
121
121
  return true;
122
122
  }
123
123
 
124
+ /**
125
+ * Register a schema, surviving one AJV refuses.
126
+ *
127
+ * `addSchema` META-VALIDATES and THROWS, and a throw here escapes the whole
128
+ * analyze pass: one author schema with `minimum: "3"` in it aborted the run
129
+ * with AJV's own unanchored text and took every other diagnostic in the file
130
+ * down with it — including the anchored one that says exactly which keyword is
131
+ * wrong. Registration is a lookup table for `$ref` resolution, so failing to
132
+ * fill one entry costs a reference that could not have resolved anyway.
133
+ *
134
+ * Nothing is swallowed: an unregisterable schema is invalid, and the two
135
+ * checks that report it both run afterwards and both anchor on the offending
136
+ * line — `SCHEMA_VIOLATION` from the `KindSchema` / `JsonSchema7` fragment the
137
+ * slot points at, and `SCHEMA_COMPILE_ERROR` from {@link schemaCompileError},
138
+ * which wraps `compile` for this same reason.
139
+ */
140
+ private tryAddSchema(schema: Record<string, any>, id: string): boolean {
141
+ try {
142
+ this.ajv.addSchema(schema, id);
143
+ return true;
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+
124
149
  /** True when a schema is registered under `id` (a canonical `telo://` type id
125
150
  * or a definition `$id`). Used to flag schema `$ref`s that resolve to nothing. */
126
151
  hasSchemaId(id: string): boolean {
@@ -187,7 +212,10 @@ export class DefinitionRegistry {
187
212
  if (this.ajv.getSchema(id)) {
188
213
  throw new Error(`Duplicate definition schema $id: "${id}" is already registered`);
189
214
  }
190
- this.ajv.addSchema(schema, id);
215
+ // A schema AJV refuses is left unregistered rather than aborting the pass —
216
+ // see {@link tryAddSchema}. The id stays claimed either way, so a later
217
+ // named type cannot quietly take a kind's place.
218
+ this.tryAddSchema(schema, id);
191
219
  this.registeredSchemaIds.add(id);
192
220
  this.definitionSchemaIds.add(id);
193
221
  }