@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
@@ -9,7 +9,7 @@ import {
9
9
  validateAgainstSchema,
10
10
  } from "./schema-compat.js";
11
11
  import { plainChainOf } from "@telorun/templating";
12
- import { isLiveSlot, valueTypeOf } from "@telorun/sdk";
12
+ import { isLiveSlot, valueTypeOf, type ResourceDefinition } from "@telorun/sdk";
13
13
  import { manifestFragmentOf } from "./manifest-schemas.js";
14
14
  import {
15
15
  analyzerContractScope,
@@ -19,6 +19,13 @@ import {
19
19
  resolveLocalRef,
20
20
  walkStepArray,
21
21
  } from "./analyzer.js";
22
+ import { readStepSlot } from "./step-slot.js";
23
+ import { navigateConcretePath } from "./manifest-path.js";
24
+ import {
25
+ isRefEntry,
26
+ resolveFieldEntries,
27
+ type ReferenceFieldMap,
28
+ } from "./reference-field-map.js";
22
29
 
23
30
  export interface StepInputIssue {
24
31
  path: string;
@@ -42,7 +49,8 @@ export interface StepInputIssue {
42
49
  * CEL leaves are replaced by schema-shaped placeholders first (`substituteCelFields`),
43
50
  * so an expression is never a false positive — only structural disagreement is
44
51
  * reported. Nothing is hardcoded about `Run.Sequence`: the invoke field comes
45
- * from `x-telo-step-context`, and the paired inputs field from whichever sibling
52
+ * from the step slot (the shared grammar, or the legacy `x-telo-step-context`),
53
+ * and the paired inputs field from whichever sibling
46
54
  * property carries `x-telo-topology-role: inputs`.
47
55
  */
48
56
  export function collectStepInputIssues(
@@ -64,9 +72,18 @@ export function collectStepInputIssues(
64
72
  const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
65
73
  const readingModule = (manifest.metadata as { module?: string } | undefined)?.module;
66
74
 
75
+ const ctx: CallCheckContext = {
76
+ manifest,
77
+ allManifests,
78
+ defs,
79
+ contractScope,
80
+ readingModule,
81
+ stepContext,
82
+ };
83
+
67
84
  for (const [fieldName, fieldSchema] of Object.entries(props)) {
68
- const stepCtx = fieldSchema["x-telo-step-context"] as Record<string, string> | undefined;
69
- if (!stepCtx?.invoke) continue;
85
+ const stepCtx = readStepSlot(fieldSchema);
86
+ if (!stepCtx) continue;
70
87
  const steps = manifest[fieldName];
71
88
  if (!Array.isArray(steps)) continue;
72
89
 
@@ -90,6 +107,127 @@ export function collectStepInputIssues(
90
107
  if (!invoke || typeof invoke !== "object") return;
91
108
  if (!values || typeof values !== "object" || Array.isArray(values)) return;
92
109
 
110
+ out.push(
111
+ ...checkCallSite(
112
+ {
113
+ inputsPath: `${stepPath}.${inputsField}`,
114
+ values: values as Record<string, any>,
115
+ invoke,
116
+ // Only a step declares a re-attempt policy, so only a step can carry
117
+ // the live-value-retried finding.
118
+ declaredRetryFor: (invokedManifest, invokedDef) =>
119
+ declaredRetry(step, stepItemSchema, invokedManifest, invokedDef),
120
+ },
121
+ ctx,
122
+ ),
123
+ );
124
+ });
125
+ }
126
+ return out;
127
+ }
128
+
129
+ /**
130
+ * Validate the argument map of every call this resource makes through a
131
+ * REFERENCE SLOT, as opposed to a step.
132
+ *
133
+ * A slot that transfers control names its argument slot on its own `x-telo-ref`
134
+ * (`inputs:`, a JSON Pointer relative to the object enclosing the slot). That
135
+ * annotation is the only thing tying an otherwise-open `inputs:` map to the
136
+ * resource it holds arguments for — an HTTP route's `handler:` + `inputs:` pair
137
+ * is exactly this shape, and nothing about it is a step.
138
+ *
139
+ * Discovery is driven by the annotation rather than by any kind's topology, so
140
+ * a composer that names its argument slot gets its call sites checked without
141
+ * the analyzer learning what a route is. It is the same check the step driver
142
+ * runs, because it is the same question.
143
+ */
144
+ export function collectRefInputIssues(
145
+ manifest: Record<string, any>,
146
+ fieldMap: ReferenceFieldMap | undefined,
147
+ allManifests: Record<string, any>[],
148
+ defs: DefinitionRegistry,
149
+ aliases: AliasResolver,
150
+ scopes: ModuleScopes,
151
+ ): StepInputIssue[] {
152
+ const out: StepInputIssue[] = [];
153
+ if (!fieldMap) return out;
154
+
155
+ const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
156
+ const ctx: CallCheckContext = {
157
+ manifest,
158
+ allManifests,
159
+ defs,
160
+ contractScope,
161
+ readingModule: (manifest.metadata as { module?: string } | undefined)?.module,
162
+ };
163
+
164
+ for (const [fieldPath, entry] of fieldMap) {
165
+ if (!isRefEntry(entry) || !entry.inputs) continue;
166
+ const pointer = pointerSegments(entry.inputs);
167
+ if (!pointer) continue;
168
+
169
+ for (const { value: invoke, path: slotPath } of resolveFieldEntries(manifest, fieldPath)) {
170
+ if (!invoke || typeof invoke !== "object" || Array.isArray(invoke)) continue;
171
+ // Relative to the object ENCLOSING the slot, which is the annotation's
172
+ // documented anchor.
173
+ const enclosing = slotPath.slice(0, Math.max(0, slotPath.lastIndexOf(".")));
174
+ const inputsPath = [enclosing, ...pointer].filter(Boolean).join(".");
175
+ const values = navigateConcretePath(manifest, inputsPath);
176
+ if (!values || typeof values !== "object" || Array.isArray(values)) continue;
177
+
178
+ out.push(
179
+ ...checkCallSite(
180
+ { inputsPath, values: values as Record<string, any>, invoke: invoke as Record<string, any> },
181
+ ctx,
182
+ ),
183
+ );
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+
189
+ /** A JSON Pointer naming a sibling FIELD path. An array index is not a field,
190
+ * so a pointer carrying one names nothing this can resolve. */
191
+ function pointerSegments(pointer: string): string[] | undefined {
192
+ if (!pointer.startsWith("/")) return undefined;
193
+ const segments = pointer
194
+ .slice(1)
195
+ .split("/")
196
+ .map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
197
+ return segments.every((s) => s.length > 0 && !/^\d+$/.test(s)) ? segments : undefined;
198
+ }
199
+
200
+ /** One call site: the arguments written, and the reference they are for. */
201
+ interface CallSite {
202
+ /** Concrete path of the argument map, for anchoring a diagnostic. */
203
+ inputsPath: string;
204
+ values: Record<string, any>;
205
+ invoke: Record<string, any>;
206
+ declaredRetryFor?(
207
+ invokedManifest: Record<string, any> | undefined,
208
+ invokedDef: ResourceDefinition | undefined,
209
+ ): string | undefined;
210
+ }
211
+
212
+ interface CallCheckContext {
213
+ manifest: Record<string, any>;
214
+ allManifests: Record<string, any>[];
215
+ defs: DefinitionRegistry;
216
+ contractScope: ReturnType<typeof analyzerContractScope>;
217
+ readingModule: string | undefined;
218
+ stepContext?: Record<string, any>;
219
+ }
220
+
221
+ /**
222
+ * The check itself, shared by both drivers: the arguments written at a call site
223
+ * against the contract the target declares.
224
+ */
225
+ function checkCallSite(site: CallSite, ctx: CallCheckContext): StepInputIssue[] {
226
+ const out: StepInputIssue[] = [];
227
+ const { manifest, allManifests, defs, contractScope, readingModule, stepContext } = ctx;
228
+ const { invoke, values } = site;
229
+ {
230
+ {
93
231
  const invokedKind = invoke.kind as string | undefined;
94
232
  const invokedName = invoke.name as string | undefined;
95
233
  const invokedManifest = invokedName
@@ -102,7 +240,7 @@ export function collectStepInputIssues(
102
240
  ? contractScope.resolveIn(invokedKind, readingModule)
103
241
  : undefined;
104
242
  const contract = resolveContract("inputType", invokedManifest, invokedDef, contractScope);
105
- if (!contract) return;
243
+ if (!contract) return out;
106
244
 
107
245
  // Findings AT a substituted path are about a placeholder, not about
108
246
  // anything the author wrote — a `pattern`-constrained string or a `oneOf`
@@ -161,10 +299,10 @@ export function collectStepInputIssues(
161
299
  // already declared: the value's liveness by its value type, and the
162
300
  // re-attempt by the retry policy. No kind is named.
163
301
  if (isLiveSlot(produced)) {
164
- const retry = declaredRetry(step, stepItemSchema, invokedManifest, invokedDef);
302
+ const retry = site.declaredRetryFor?.(invokedManifest, invokedDef);
165
303
  if (retry !== undefined) {
166
304
  out.push({
167
- path: `${stepPath}.${inputsField}.${inputName}`,
305
+ path: `${site.inputsPath}.${inputName}`,
168
306
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
169
307
  message:
170
308
  `'${inputName}' is a live value, which is consumed by reading and so exists ` +
@@ -182,7 +320,7 @@ export function collectStepInputIssues(
182
320
  );
183
321
  if (compatible) continue;
184
322
  out.push({
185
- path: `${stepPath}.${inputsField}.${inputName}`,
323
+ path: `${site.inputsPath}.${inputName}`,
186
324
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
187
325
  message: issues.join("; "),
188
326
  code: "CEL_TYPE_ARGUMENT_MISMATCH",
@@ -198,12 +336,12 @@ export function collectStepInputIssues(
198
336
  // container that should have held it, which does exist.
199
337
  const anchor = missingRequired(issue) ? containerOf(issue.path) : issue.path;
200
338
  out.push({
201
- path: anchor ? `${stepPath}.${inputsField}.${anchor}` : `${stepPath}.${inputsField}`,
339
+ path: anchor ? `${site.inputsPath}.${anchor}` : site.inputsPath,
202
340
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
203
341
  message: issue.message,
204
342
  });
205
343
  }
206
- });
344
+ }
207
345
  }
208
346
  return out;
209
347
  }
@@ -15,6 +15,7 @@ import {
15
15
  } from "./resolve-throws-union.js";
16
16
  import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
17
17
  import { extractAccessChains, validateChainAgainstSchema } from "./validate-cel-context.js";
18
+ import { isStepSlot } from "./step-slot.js";
18
19
 
19
20
  const SOURCE = "telo-analyzer";
20
21
  const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
@@ -457,9 +458,11 @@ function checkCelChainAgainstDataSchema(
457
458
  }
458
459
 
459
460
  /** Rule 8 extension: `inherit: true` only makes sense on a definition whose
460
- * schema contains at least one `x-telo-step-context` array — the annotation
461
- * that drives the resolver's generic step traversal. A definition with
462
- * `inherit: true` and no such array has no invocables to inherit from. */
461
+ * schema declares at least one STEP BODYan array whose items point at the
462
+ * shared grammar, or, for a module published before that fragment existed, one
463
+ * carrying the legacy `x-telo-step-context` annotation. That is what drives the
464
+ * resolver's generic step traversal; a definition with `inherit: true` and no
465
+ * such array has no invocables to inherit from. */
463
466
  function validateThrowsDeclarations(manifests: ResourceManifest[]): AnalysisDiagnostic[] {
464
467
  const diagnostics: AnalysisDiagnostic[] = [];
465
468
  for (const m of manifests) {
@@ -475,7 +478,11 @@ function validateThrowsDeclarations(manifests: ResourceManifest[]): AnalysisDiag
475
478
  severity: DiagnosticSeverity.Error,
476
479
  code: "INHERIT_WITHOUT_STEP_CONTEXT",
477
480
  source: SOURCE,
478
- message: `Telo.Definition '${name}' declares throws.inherit: true but its schema has no field annotated with x-telo-step-context. inherit is only meaningful on definitions that drive invocables via step arrays.`,
481
+ message:
482
+ `Telo.Definition '${name}' declares throws.inherit: true but its schema declares no step ` +
483
+ `body. inherit is only meaningful on a definition that drives invocables through steps — ` +
484
+ `give an array field 'items: { $ref: "telo://manifest#/$defs/Step" }' (the legacy ` +
485
+ `x-telo-step-context annotation is also recognised).`,
479
486
  data: { resource: { kind: m.kind, name }, filePath, path: "throws.inherit" },
480
487
  });
481
488
  }
@@ -486,7 +493,7 @@ function validateThrowsDeclarations(manifests: ResourceManifest[]): AnalysisDiag
486
493
 
487
494
  function schemaHasStepContext(schema: Record<string, any> | undefined): boolean {
488
495
  if (!schema || typeof schema !== "object") return false;
489
- if ("x-telo-step-context" in schema) return true;
496
+ if (isStepSlot(schema)) return true;
490
497
  const props = schema.properties;
491
498
  if (props && typeof props === "object") {
492
499
  for (const v of Object.values(props as Record<string, any>)) {
@@ -26,10 +26,11 @@
26
26
  *
27
27
  * Browser-safe: no Node built-ins.
28
28
  */
29
- import type { ResourceManifest } from "@telorun/sdk";
29
+ import { ZONE_ATTRIBUTES, zoneAttributeNames, type ResourceManifest } from "@telorun/sdk";
30
+ import { distance } from "./levenshtein.js";
30
31
 
31
32
  export interface ZoneSlotIssue {
32
- code: "ZONE_ANNOTATION_INVALID";
33
+ code: "ZONE_ANNOTATION_INVALID" | "ZONE_ATTRIBUTE_UNKNOWN" | "ZONE_ATTRIBUTE_INCOMPLETE";
33
34
  manifest: ResourceManifest;
34
35
  /** Schema path of the annotated slot. */
35
36
  path: string;
@@ -38,6 +39,7 @@ export interface ZoneSlotIssue {
38
39
 
39
40
  const PROVIDES = "x-telo-provides-zone";
40
41
  const REQUIRES = "x-telo-requires-zone";
42
+ const VIOLATES = "x-telo-violates-zone";
41
43
 
42
44
  /** A self-relative JSON Pointer, the only correlation-key spelling both halves
43
45
  * read identically. `""` (whole document) is meaningless as a key, so a
@@ -53,6 +55,107 @@ function describe(value: unknown): string {
53
55
  return typeof value;
54
56
  }
55
57
 
58
+ /** The closest declared attribute name within an edit-distance threshold, or
59
+ * undefined. Mirrors `suggestValueType`: case-sensitive, and silent on a tie,
60
+ * because a coin-flip suggestion is worse than none. */
61
+ function suggestAttribute(name: string): string | undefined {
62
+ if (!name) return undefined;
63
+ const threshold = Math.min(3, Math.floor(name.length / 3));
64
+ if (threshold < 1) return undefined;
65
+ let best: string | undefined;
66
+ let bestDist = threshold + 1;
67
+ let tied = false;
68
+ for (const candidate of ZONE_ATTRIBUTES.keys()) {
69
+ const d = distance(name, candidate);
70
+ if (d < bestDist) {
71
+ best = candidate;
72
+ bestDist = d;
73
+ tied = false;
74
+ } else if (d === bestDist) {
75
+ tied = true;
76
+ }
77
+ }
78
+ return !best || bestDist > threshold || tied ? undefined : best;
79
+ }
80
+
81
+ /**
82
+ * The attributes half of the object form.
83
+ *
84
+ * The composed `additionalProperties: false` schema would already reject an
85
+ * unknown name; the dedicated code earns its place by NAMING the valid ones and
86
+ * suggesting a spelling, instead of reporting a schema violation on a key the
87
+ * author believed was real — the same standing `X_TELO_TYPE_UNKNOWN` has over
88
+ * the value-type vocabulary it is modelled on.
89
+ */
90
+ function checkAttributes(
91
+ obj: Record<string, unknown>,
92
+ definition: ResourceManifest,
93
+ path: string,
94
+ issues: ZoneSlotIssue[],
95
+ annotation: string = PROVIDES,
96
+ ): void {
97
+ const declared = new Set<string>();
98
+
99
+ for (const [name, value] of Object.entries(obj)) {
100
+ if (name === "key") continue;
101
+ const entry = ZONE_ATTRIBUTES.get(name);
102
+ if (!entry) {
103
+ const suggestion = suggestAttribute(name);
104
+ issues.push({
105
+ code: "ZONE_ATTRIBUTE_UNKNOWN",
106
+ manifest: definition,
107
+ path,
108
+ message:
109
+ `${annotation} at '${path}' declares '${name}', which is not a zone attribute. ` +
110
+ (suggestion ? `Did you mean '${suggestion}'? ` : "") +
111
+ `The vocabulary is closed: ${zoneAttributeNames().join(", ")}. An attribute ` +
112
+ `states a property of everything executed inside this zone, and every reader ` +
113
+ `of one is core — a name outside the set would be read by nothing.`,
114
+ });
115
+ continue;
116
+ }
117
+ // Each value is the REASON, and it is required by being the value itself
118
+ // rather than a sibling of a boolean. That is also what makes a type check
119
+ // possible at all: there is no `true` to accept, so `atomic: true` is caught
120
+ // here rather than reading as a valid declaration with nothing to say.
121
+ if (typeof value !== "string" || !value) {
122
+ issues.push({
123
+ code: "ZONE_ANNOTATION_INVALID",
124
+ manifest: definition,
125
+ path,
126
+ message:
127
+ `${annotation} at '${path}' declares '${name}' as ${describe(value)}. Every zone ` +
128
+ `attribute's value is the author's REASON — a non-empty sentence, quoted verbatim ` +
129
+ `by the diagnostics that enforce it (${entry.description}).`,
130
+ });
131
+ continue;
132
+ }
133
+ declared.add(name);
134
+ }
135
+
136
+ // The completeness rule is about what a region PROMISES: declaring `atomic`
137
+ // without `noSuspend` is an incomplete promise. A VIOLATION is not a promise —
138
+ // breaking one guarantee implies nothing about breaking another — so the rule
139
+ // does not apply there.
140
+ if (annotation !== PROVIDES) return;
141
+ // `requires:` lives in the vocabulary entry rather than as a hardcoded pair of
142
+ // names here, so the completeness rule sits beside the thing it constrains.
143
+ for (const name of declared) {
144
+ for (const dependency of ZONE_ATTRIBUTES.get(name)!.requires) {
145
+ if (declared.has(dependency)) continue;
146
+ issues.push({
147
+ code: "ZONE_ATTRIBUTE_INCOMPLETE",
148
+ manifest: definition,
149
+ path,
150
+ message:
151
+ `${PROVIDES} at '${path}' declares '${name}' without '${dependency}', which it ` +
152
+ `requires. ${ZONE_ATTRIBUTES.get(dependency)!.description} Declare it with its own ` +
153
+ `reason — a generic message is exactly what the required reason exists to prevent.`,
154
+ });
155
+ }
156
+ }
157
+ }
158
+
56
159
  function checkProvides(
57
160
  raw: unknown,
58
161
  definition: ResourceManifest,
@@ -61,16 +164,38 @@ function checkProvides(
61
164
  ): void {
62
165
  if (raw === true) return;
63
166
  if (isPointer(raw)) return;
167
+
168
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
169
+ const obj = raw as Record<string, unknown>;
170
+ // `key` keeps the meaning it has in the scalar spelling; only its absence is
171
+ // legitimate here (an uncorrelated zone that still declares attributes).
172
+ if (obj.key !== undefined && !isPointer(obj.key)) {
173
+ issues.push({
174
+ code: "ZONE_ANNOTATION_INVALID",
175
+ manifest: definition,
176
+ path,
177
+ message:
178
+ `${PROVIDES} at '${path}' declares the correlation key ${describe(obj.key)}, which ` +
179
+ `is not a self-relative JSON Pointer. Write '/connection'. A bare field name is ` +
180
+ `read as a pointer by the runtime but skipped by the checker, so the two halves ` +
181
+ `would disagree about what this manifest means.`,
182
+ });
183
+ }
184
+ checkAttributes(obj, definition, path, issues);
185
+ return;
186
+ }
187
+
64
188
  issues.push({
65
189
  code: "ZONE_ANNOTATION_INVALID",
66
190
  manifest: definition,
67
191
  path,
68
192
  message:
69
193
  `${PROVIDES} at '${path}' is ${describe(raw)}. It takes 'true' (the zone is ` +
70
- `uncorrelated) or a self-relative JSON Pointer naming this kind's own field ` +
194
+ `uncorrelated), a self-relative JSON Pointer naming this kind's own field ` +
71
195
  `whose resolved reference the zone carries as its correlation payload ` +
72
- `(e.g. '/connection'). It never names the zone the zone a slot provides ` +
73
- `is always the declaring kind.`,
196
+ `(e.g. '/connection'), or an object carrying that pointer as 'key' beside the ` +
197
+ `zone attributes this region declares (${zoneAttributeNames().join(", ")}). It ` +
198
+ `never names the zone — the zone a slot provides is always the declaring kind.`,
74
199
  });
75
200
  }
76
201
 
@@ -129,17 +254,74 @@ function checkRequires(
129
254
  fail(`${REQUIRES} at '${path}' declares a non-string 'reason'.`);
130
255
  }
131
256
 
257
+ // `attributes` is the GUARANTEE half — what the satisfying zone must declare,
258
+ // as opposed to which kind it is. It is a list of NAMES with no reasons: the
259
+ // reason belongs to whoever makes the promise, and a requirer restating it
260
+ // would be a second sentence to keep in step with the one that is enforced.
261
+ if (obj.attributes !== undefined) {
262
+ if (!Array.isArray(obj.attributes) || obj.attributes.length === 0) {
263
+ fail(
264
+ `${REQUIRES} at '${path}' declares 'attributes' as ${describe(obj.attributes)}. It ` +
265
+ `takes a non-empty list of zone attribute names; omit it to require the kind alone.`,
266
+ );
267
+ } else {
268
+ for (const name of obj.attributes) {
269
+ if (typeof name === "string" && ZONE_ATTRIBUTES.has(name)) continue;
270
+ const suggestion = typeof name === "string" ? suggestAttribute(name) : undefined;
271
+ issues.push({
272
+ code: "ZONE_ATTRIBUTE_UNKNOWN",
273
+ manifest: definition,
274
+ path,
275
+ message:
276
+ `${REQUIRES} at '${path}' requires '${String(name)}', which is not a zone ` +
277
+ `attribute. ` +
278
+ (suggestion ? `Did you mean '${suggestion}'? ` : "") +
279
+ `The vocabulary is closed: ${zoneAttributeNames().join(", ")}.`,
280
+ });
281
+ }
282
+ }
283
+ }
284
+
132
285
  for (const key of Object.keys(obj)) {
133
- if (key === "zone" || key === "key" || key === "reason") {
286
+ if (key === "zone" || key === "key" || key === "reason" || key === "attributes") {
134
287
  continue;
135
288
  }
136
289
  fail(
137
290
  `${REQUIRES} at '${path}' declares an unknown property '${key}'. The object form takes ` +
138
- `'zone', 'key' and 'reason'.`,
291
+ `'zone', 'key', 'reason' and 'attributes'.`,
139
292
  );
140
293
  }
141
294
  }
142
295
 
296
+ /**
297
+ * The third annotation: what this kind cannot honour.
298
+ *
299
+ * Only the object form, because a violation without a reason is exactly the
300
+ * shape the vocabulary refuses everywhere else — the diagnostic that fires on it
301
+ * prints the region's promise and this resource's rebuttal side by side, and one
302
+ * of the two would otherwise be a generated sentence.
303
+ */
304
+ function checkViolates(
305
+ raw: unknown,
306
+ definition: ResourceManifest,
307
+ path: string,
308
+ issues: ZoneSlotIssue[],
309
+ ): void {
310
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
311
+ issues.push({
312
+ code: "ZONE_ANNOTATION_INVALID",
313
+ manifest: definition,
314
+ path,
315
+ message:
316
+ `${VIOLATES} at '${path}' is ${describe(raw)}. It takes a map of zone attribute ` +
317
+ `names to the reason this kind cannot honour each — ` +
318
+ `\`noSuspend: this waits for a delivery that may be days away\`.`,
319
+ });
320
+ return;
321
+ }
322
+ checkAttributes(raw as Record<string, unknown>, definition, path, issues, VIOLATES);
323
+ }
324
+
143
325
  /** Walk a definition schema, reporting every zone annotation it cannot read.
144
326
  * Pure-schema walk, so it needs a visited guard for cyclic `$defs`. */
145
327
  function walkSchema(
@@ -159,8 +341,10 @@ function walkSchema(
159
341
  const obj = node as Record<string, unknown>;
160
342
  if (obj[PROVIDES] !== undefined) checkProvides(obj[PROVIDES], definition, path, issues);
161
343
  if (obj[REQUIRES] !== undefined) checkRequires(obj[REQUIRES], definition, path, issues);
344
+ if (obj[VIOLATES] !== undefined) checkViolates(obj[VIOLATES], definition, path, issues);
162
345
  for (const [key, value] of Object.entries(obj)) {
163
- if (key === PROVIDES || key === REQUIRES || key === "examples" || key === "default") continue;
346
+ if (key === PROVIDES || key === REQUIRES || key === VIOLATES) continue;
347
+ if (key === "examples" || key === "default") continue;
164
348
  walkSchema(value, path ? `${path}.${key}` : key, visited, definition, issues);
165
349
  }
166
350
  }