@telorun/analyzer 0.52.0 → 0.54.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 (80) hide show
  1. package/dist/analysis-registry.d.ts +8 -0
  2. package/dist/analysis-registry.d.ts.map +1 -1
  3. package/dist/analysis-registry.js +21 -3
  4. package/dist/analyzer.d.ts +3 -2
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/analyzer.js +193 -26
  7. package/dist/builtins.d.ts.map +1 -1
  8. package/dist/builtins.js +32 -12
  9. package/dist/call-graph.d.ts +189 -0
  10. package/dist/call-graph.d.ts.map +1 -0
  11. package/dist/call-graph.js +617 -0
  12. package/dist/dependency-graph.d.ts +17 -7
  13. package/dist/dependency-graph.d.ts.map +1 -1
  14. package/dist/dependency-graph.js +36 -65
  15. package/dist/flatten-for-analyzer.d.ts +8 -0
  16. package/dist/flatten-for-analyzer.d.ts.map +1 -1
  17. package/dist/flatten-for-analyzer.js +32 -0
  18. package/dist/index.d.ts +15 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +11 -1
  21. package/dist/manifest-navigation.d.ts +32 -0
  22. package/dist/manifest-navigation.d.ts.map +1 -0
  23. package/dist/manifest-navigation.js +91 -0
  24. package/dist/manifest-visitor.js +1 -1
  25. package/dist/ref-slot.d.ts +125 -0
  26. package/dist/ref-slot.d.ts.map +1 -0
  27. package/dist/ref-slot.js +226 -0
  28. package/dist/reference-field-map.d.ts +15 -1
  29. package/dist/reference-field-map.d.ts.map +1 -1
  30. package/dist/reference-field-map.js +29 -35
  31. package/dist/resolve-schema-ref-kinds.d.ts +4 -0
  32. package/dist/resolve-schema-ref-kinds.d.ts.map +1 -1
  33. package/dist/resolve-schema-ref-kinds.js +31 -8
  34. package/dist/resolve-zone-requirements.d.ts +110 -0
  35. package/dist/resolve-zone-requirements.d.ts.map +1 -0
  36. package/dist/resolve-zone-requirements.js +541 -0
  37. package/dist/types.d.ts +8 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/validate-module-metadata.d.ts +38 -0
  40. package/dist/validate-module-metadata.d.ts.map +1 -0
  41. package/dist/validate-module-metadata.js +256 -0
  42. package/dist/validate-observed-state.d.ts +14 -13
  43. package/dist/validate-observed-state.d.ts.map +1 -1
  44. package/dist/validate-observed-state.js +21 -88
  45. package/dist/validate-ref-slots.d.ts +48 -0
  46. package/dist/validate-ref-slots.d.ts.map +1 -0
  47. package/dist/validate-ref-slots.js +219 -0
  48. package/dist/validate-references.d.ts.map +1 -1
  49. package/dist/validate-references.js +8 -1
  50. package/dist/validate-zone-slots.d.ts +39 -0
  51. package/dist/validate-zone-slots.d.ts.map +1 -0
  52. package/dist/validate-zone-slots.js +114 -0
  53. package/dist/zone-module-documents.d.ts +27 -0
  54. package/dist/zone-module-documents.d.ts.map +1 -0
  55. package/dist/zone-module-documents.js +1 -0
  56. package/dist/zone-slot.d.ts +61 -0
  57. package/dist/zone-slot.d.ts.map +1 -0
  58. package/dist/zone-slot.js +91 -0
  59. package/package.json +3 -3
  60. package/src/analysis-registry.ts +20 -2
  61. package/src/analyzer.ts +211 -24
  62. package/src/builtins.ts +32 -12
  63. package/src/call-graph.ts +827 -0
  64. package/src/dependency-graph.ts +34 -68
  65. package/src/flatten-for-analyzer.ts +32 -0
  66. package/src/index.ts +51 -0
  67. package/src/manifest-navigation.ts +91 -0
  68. package/src/manifest-visitor.ts +1 -1
  69. package/src/ref-slot.ts +273 -0
  70. package/src/reference-field-map.ts +39 -36
  71. package/src/resolve-schema-ref-kinds.ts +34 -7
  72. package/src/resolve-zone-requirements.ts +781 -0
  73. package/src/types.ts +8 -0
  74. package/src/validate-module-metadata.ts +335 -0
  75. package/src/validate-observed-state.ts +26 -92
  76. package/src/validate-ref-slots.ts +293 -0
  77. package/src/validate-references.ts +8 -1
  78. package/src/validate-zone-slots.ts +175 -0
  79. package/src/zone-module-documents.ts +27 -0
  80. package/src/zone-slot.ts +116 -0
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Static validation of the `x-telo-ref` annotation itself — the strict half of
3
+ * the accessor split. `readRefSlot` is deliberately lenient (it normalizes
4
+ * whatever it can read, because every surface must keep working mid-migration);
5
+ * this pass reads the RAW annotation and reports what leniency would otherwise
6
+ * silently absorb:
7
+ *
8
+ * - an unrecognized `use` token — a typo like `use: cal` would degrade to the
9
+ * legacy no-use reading, indistinguishable from a slot that never answered;
10
+ * - a structured annotation with no `kind` — the editor would recognise the
11
+ * slot but have nothing to pick against;
12
+ * - a structured annotation with no `use` — the structured form is the
13
+ * declaration that answers the question; omitting it is only legal in the
14
+ * legacy bare-string spelling;
15
+ * - `anyOf` branches whose declared uses disagree — a state with no meaning,
16
+ * since `use` is a property of the slot, never of a branch;
17
+ * - a `use` case map whose selector is written in CEL — a call graph known
18
+ * only at runtime is not statically analyzable, which is the property the
19
+ * typed reference graph exists to protect. There is deliberately no
20
+ * fallback: no single value is conservative for every consumer.
21
+ *
22
+ * Scoping follows `X_TELO_REF_UNRESOLVED`: schema issues are reported only for
23
+ * definitions in the entry's own modules, and the dynamic-selector issue only
24
+ * for manifests in them — a published dependency's slot is not the consumer's
25
+ * to fix.
26
+ *
27
+ * Browser-safe: no Node built-ins.
28
+ */
29
+ import type { ResourceManifest } from "@telorun/sdk";
30
+ import type { AliasResolver } from "./alias-resolver.js";
31
+ import { buildCallGraph, type CallGraph } from "./call-graph.js";
32
+ import type { DefinitionRegistry } from "./definition-registry.js";
33
+ import { isRefUse, REF_USES, type RefUse } from "./ref-slot.js";
34
+
35
+ export interface RefSlotIssue {
36
+ code:
37
+ | "X_TELO_REF_INVALID_USE"
38
+ | "X_TELO_REF_MISSING_USE"
39
+ | "X_TELO_REF_MISSING_KIND"
40
+ | "X_TELO_REF_USE_CONFLICT"
41
+ | "X_TELO_REF_DYNAMIC_SELECTOR";
42
+ /** The definition (schema issues) or resource (selector issues) at fault. */
43
+ manifest: ResourceManifest;
44
+ /** Schema path of the slot (schema issues) or concrete value path of the
45
+ * selector's site (dynamic-selector issues). */
46
+ path: string;
47
+ message: string;
48
+ }
49
+
50
+ const VALID_USES = REF_USES.join(", ");
51
+
52
+ /** Raw `use` tokens carried by one annotation value: scalar, list, and every
53
+ * case of a case map. Returned unfiltered so a typo is visible. */
54
+ function rawUseTokens(use: unknown): unknown[] {
55
+ if (use === undefined) return [];
56
+ if (Array.isArray(use)) return use;
57
+ if (use && typeof use === "object") {
58
+ const cases = (use as Record<string, unknown>).cases;
59
+ if (!cases || typeof cases !== "object") return [];
60
+ return Object.values(cases as Record<string, unknown>).flatMap((v) =>
61
+ Array.isArray(v) ? v : [v],
62
+ );
63
+ }
64
+ return [use];
65
+ }
66
+
67
+ /** The declared fixed uses of one annotation (scalar/list form only), for the
68
+ * branch-disagreement check. */
69
+ function declaredUses(use: unknown): RefUse[] {
70
+ if (isRefUse(use)) return [use];
71
+ if (Array.isArray(use)) return use.filter(isRefUse);
72
+ return [];
73
+ }
74
+
75
+ function checkAnnotation(
76
+ annotation: unknown,
77
+ manifest: ResourceManifest,
78
+ path: string,
79
+ issues: RefSlotIssue[],
80
+ ): RefUse[] | undefined {
81
+ if (typeof annotation === "string" || annotation === undefined) return undefined;
82
+ if (!annotation || typeof annotation !== "object" || Array.isArray(annotation)) return undefined;
83
+ const obj = annotation as Record<string, unknown>;
84
+
85
+ const kind = obj.kind;
86
+ const hasKind =
87
+ (typeof kind === "string" && kind.length > 0) ||
88
+ (Array.isArray(kind) && kind.some((k) => typeof k === "string" && k.length > 0));
89
+ if (!hasKind) {
90
+ issues.push({
91
+ code: "X_TELO_REF_MISSING_KIND",
92
+ manifest,
93
+ path,
94
+ message:
95
+ `x-telo-ref at '${path}' declares no 'kind'. The structured form is ` +
96
+ `'{ kind: <Alias>.<Kind> | [<kinds>], use: <use> }' — without a kind the slot ` +
97
+ `constrains nothing and the editor has nothing to pick against.`,
98
+ });
99
+ }
100
+
101
+ const use = obj.use;
102
+ const isCaseMap =
103
+ !!use && typeof use === "object" && !Array.isArray(use) && "by" in (use as object);
104
+ if (use === undefined) {
105
+ issues.push({
106
+ code: "X_TELO_REF_MISSING_USE",
107
+ manifest,
108
+ path,
109
+ message:
110
+ `x-telo-ref at '${path}' declares no 'use'. The structured form must say what the ` +
111
+ `declaring resource does with the target — one of: ${VALID_USES} — or a ` +
112
+ `'{ by, cases }' map when a sibling config field selects the mode. Only the legacy ` +
113
+ `bare-string spelling ('x-telo-ref: <Kind>') may omit it.`,
114
+ });
115
+ } else {
116
+ for (const token of rawUseTokens(use)) {
117
+ if (isRefUse(token)) continue;
118
+ issues.push({
119
+ code: "X_TELO_REF_INVALID_USE",
120
+ manifest,
121
+ path,
122
+ message:
123
+ `x-telo-ref at '${path}' declares unrecognized use '${String(token)}'. ` +
124
+ `Valid uses: ${VALID_USES}. An unrecognized token would silently degrade the slot ` +
125
+ `to the legacy no-use reading.`,
126
+ });
127
+ }
128
+ if (isCaseMap) {
129
+ const by = (use as Record<string, unknown>).by;
130
+ if (typeof by !== "string" || !by.startsWith("/")) {
131
+ issues.push({
132
+ code: "X_TELO_REF_INVALID_USE",
133
+ manifest,
134
+ path,
135
+ message:
136
+ `x-telo-ref at '${path}' has a 'use' case map whose 'by' is not a JSON Pointer. ` +
137
+ `'by' names a sibling field of the object enclosing the slot, e.g. '/detach'.`,
138
+ });
139
+ }
140
+ }
141
+ }
142
+
143
+ return declaredUses(use);
144
+ }
145
+
146
+ /** True when a node is a reference slot: it carries `x-telo-ref` directly or on
147
+ * an `anyOf`/`oneOf` branch. */
148
+ function carriesRefAnnotation(obj: Record<string, unknown>): boolean {
149
+ if (obj["x-telo-ref"] !== undefined) return true;
150
+ for (const key of ["anyOf", "oneOf"] as const) {
151
+ const branches = obj[key];
152
+ if (!Array.isArray(branches)) continue;
153
+ if (
154
+ branches.some(
155
+ (b) => b && typeof b === "object" && (b as Record<string, unknown>)["x-telo-ref"] !== undefined,
156
+ )
157
+ ) {
158
+ return true;
159
+ }
160
+ }
161
+ return false;
162
+ }
163
+
164
+ /** Walk a definition schema, invoking `onSlot` for every node that carries an
165
+ * `x-telo-ref` (directly or on an `anyOf`/`oneOf` branch — the SLOT is the
166
+ * node holding the branches, so a branch is never reported twice). Pure-schema
167
+ * walk, so it needs — and has — a visited guard for cyclic `$defs`. */
168
+ function walkSchema(
169
+ node: unknown,
170
+ path: string,
171
+ visited: Set<object>,
172
+ claimedBranches: Set<object>,
173
+ onSlot: (node: Record<string, unknown>, path: string) => void,
174
+ ): void {
175
+ if (!node || typeof node !== "object") return;
176
+ if (visited.has(node as object)) return;
177
+ visited.add(node as object);
178
+ if (Array.isArray(node)) {
179
+ node.forEach((item, i) => walkSchema(item, `${path}[${i}]`, visited, claimedBranches, onSlot));
180
+ return;
181
+ }
182
+ const obj = node as Record<string, unknown>;
183
+ if (carriesRefAnnotation(obj) && !claimedBranches.has(obj)) {
184
+ onSlot(obj, path);
185
+ for (const key of ["anyOf", "oneOf"] as const) {
186
+ const branches = obj[key];
187
+ if (!Array.isArray(branches)) continue;
188
+ for (const branch of branches) {
189
+ if (branch && typeof branch === "object") claimedBranches.add(branch as object);
190
+ }
191
+ }
192
+ }
193
+ for (const [key, value] of Object.entries(obj)) {
194
+ if (key === "x-telo-ref" || key === "examples" || key === "default") continue;
195
+ walkSchema(value, path ? `${path}.${key}` : key, visited, claimedBranches, onSlot);
196
+ }
197
+ }
198
+
199
+ /** Schema-level checks over one definition/abstract manifest. */
200
+ export function validateRefSlotDeclarations(definition: ResourceManifest): RefSlotIssue[] {
201
+ const issues: RefSlotIssue[] = [];
202
+ const schema = (definition as Record<string, unknown>).schema;
203
+ if (!schema || typeof schema !== "object") return issues;
204
+
205
+ walkSchema(schema, "schema", new Set(), new Set(), (node, path) => {
206
+ const branchUses: RefUse[][] = [];
207
+ const own = checkAnnotation(node["x-telo-ref"], definition, path, issues);
208
+ if (own) branchUses.push(own);
209
+ for (const key of ["anyOf", "oneOf"] as const) {
210
+ const branches = node[key];
211
+ if (!Array.isArray(branches)) continue;
212
+ branches.forEach((branch, i) => {
213
+ if (!branch || typeof branch !== "object") return;
214
+ const declared = checkAnnotation(
215
+ (branch as Record<string, unknown>)["x-telo-ref"],
216
+ definition,
217
+ `${path}.${key}[${i}]`,
218
+ issues,
219
+ );
220
+ if (declared) branchUses.push(declared);
221
+ });
222
+ }
223
+ const nonEmpty = branchUses.filter((uses) => uses.length > 0);
224
+ if (nonEmpty.length > 1) {
225
+ const first = [...nonEmpty[0]].sort().join(",");
226
+ const disagrees = nonEmpty.some((uses) => [...uses].sort().join(",") !== first);
227
+ if (disagrees) {
228
+ issues.push({
229
+ code: "X_TELO_REF_USE_CONFLICT",
230
+ manifest: definition,
231
+ path,
232
+ message:
233
+ `x-telo-ref branches at '${path}' declare disagreeing uses ` +
234
+ `(${nonEmpty.map((u) => u.join("|")).join(" vs ")}). 'use' is a property of the ` +
235
+ `slot, never of a branch — declare several acceptable kinds as one ` +
236
+ `'kind: [<kinds>]' list with one 'use'.`,
237
+ });
238
+ }
239
+ }
240
+ });
241
+
242
+ return issues;
243
+ }
244
+
245
+ /** Manifest-level check: a `use` case map whose selector is written in CEL.
246
+ * Reads the built graph's `unresolvedReason`, so the detection lives once, in
247
+ * `resolveUseAtSite`, and this pass cannot disagree with what consumers saw. */
248
+ export function validateDynamicSelectors(
249
+ allManifests: ResourceManifest[],
250
+ registry: DefinitionRegistry,
251
+ aliases?: AliasResolver,
252
+ aliasesByModule?: Map<string, AliasResolver>,
253
+ graph?: CallGraph,
254
+ ): RefSlotIssue[] {
255
+ const issues: RefSlotIssue[] = [];
256
+ const callGraph = graph ?? buildCallGraph(allManifests, registry, { aliases, aliasesByModule });
257
+ for (const edge of callGraph.edges) {
258
+ if (edge.unresolvedReason !== "dynamic") continue;
259
+ const from = callGraph.nodes.get(edge.from);
260
+ const owner =
261
+ from?.type === "step" ? callGraph.nodes.get(from.owner) : from;
262
+ if (!owner || owner.type !== "resource") continue;
263
+ // Anchor at the SELECTOR — the field the author must change — not at the
264
+ // ref slot several lines away. Derivable: the slot's enclosing path plus
265
+ // the pointer's segments.
266
+ const selectorPath = selectorPathOf(edge.path, edge.unresolved?.by ?? "");
267
+ issues.push({
268
+ code: "X_TELO_REF_DYNAMIC_SELECTOR",
269
+ manifest: owner.manifest,
270
+ path: selectorPath,
271
+ message:
272
+ `The mode selector at '${selectorPath}' is a CEL expression, so which 'use' holds ` +
273
+ `for the reference at '${edge.path}' cannot be resolved statically. The selector must ` +
274
+ `be a literal or take its schema default — a call graph known only at runtime is not ` +
275
+ `statically analyzable. Write the mode as a literal, or split the wiring into one ` +
276
+ `resource per mode.`,
277
+ });
278
+ }
279
+ return issues;
280
+ }
281
+
282
+ /** Concrete path of a case-map selector: the slot's enclosing path joined with
283
+ * the pointer's segments (`steps[0].invoke` + `/detach` → `steps[0].detach`). */
284
+ function selectorPathOf(slotPath: string, pointer: string): string {
285
+ const lastDot = slotPath.lastIndexOf(".");
286
+ const enclosing = lastDot < 0 ? "" : slotPath.slice(0, lastDot);
287
+ const segments = pointer
288
+ .replace(/^\//, "")
289
+ .split("/")
290
+ .map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"))
291
+ .join(".");
292
+ return enclosing ? `${enclosing}.${segments}` : segments;
293
+ }
@@ -40,7 +40,14 @@ function checkKind(
40
40
  if (subtypeKinds.has(resolved)) return [];
41
41
  if (targetDef.kind === "Telo.Abstract") {
42
42
  if (subtypes.length === 0) return []; // partial context — no implementations loaded yet
43
- const options = [...subtypeKinds].join(", ");
43
+ // Suggest only what an author can actually wire: with abstract-extends-
44
+ // abstract real (Telo.Executable over Invocable/Runnable), the transitive
45
+ // subtype list contains abstracts, which are non-instantiable and would
46
+ // read as fixes that cannot work.
47
+ const concrete = subtypes
48
+ .filter((d) => d.kind !== "Telo.Abstract")
49
+ .map((d) => `${d.metadata.module}.${d.metadata.name}`);
50
+ const options = (concrete.length > 0 ? concrete : [...subtypeKinds]).join(", ");
44
51
  errors.push(
45
52
  `'${kind}' does not implement '${targetKind}' (known implementations: ${options})`,
46
53
  );
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Static validation of the two execution-zone annotations themselves — the
3
+ * strict half of the accessor split, mirroring `validate-ref-slots.ts`.
4
+ *
5
+ * `readProvidesZone` / `readRequiresZone` are deliberately lenient: they return
6
+ * `undefined` for anything they cannot read. Without this pass that leniency is
7
+ * silent in the worst possible direction, because the two annotations fail in
8
+ * OPPOSITE ways:
9
+ *
10
+ * - an unreadable **requires** annotation drops the requirement entirely, so a
11
+ * safety constraint the author wrote is never enforced — and the resource
12
+ * then throws `ERR_ZONE_REQUIRED` / `ERR_ZONE_ANNOTATION_MISSING` at
13
+ * dispatch. That is exactly the silent-non-enforcement `ZONE_PROVIDER_UNRESOLVED`
14
+ * exists to prevent, reached by a different route.
15
+ * - an unreadable **provides** annotation drops the discharge, so the pass
16
+ * reports `ZONE_REQUIREMENT_UNSATISFIED` on manifests that are correct.
17
+ *
18
+ * A third shape is worse than either: a `key` the analyzer skips but the kernel
19
+ * accepts (a pointer with no leading `/` — the kernel's walk splits on `/` and
20
+ * drops empty segments, so it resolves) makes the two halves disagree about what
21
+ * the manifest MEANS, which is the one outcome neither severity can express.
22
+ *
23
+ * Scoping follows `X_TELO_REF_UNRESOLVED`: reported only for definitions in the
24
+ * entry's own modules — a published dependency's slot is not the consumer's to
25
+ * fix.
26
+ *
27
+ * Browser-safe: no Node built-ins.
28
+ */
29
+ import type { ResourceManifest } from "@telorun/sdk";
30
+
31
+ export interface ZoneSlotIssue {
32
+ code: "ZONE_ANNOTATION_INVALID";
33
+ manifest: ResourceManifest;
34
+ /** Schema path of the annotated slot. */
35
+ path: string;
36
+ message: string;
37
+ }
38
+
39
+ const PROVIDES = "x-telo-provides-zone";
40
+ const REQUIRES = "x-telo-requires-zone";
41
+
42
+ /** A self-relative JSON Pointer, the only correlation-key spelling both halves
43
+ * read identically. `""` (whole document) is meaningless as a key, so a
44
+ * pointer must name at least one segment. */
45
+ function isPointer(value: unknown): value is string {
46
+ return typeof value === "string" && value.startsWith("/") && value.length > 1;
47
+ }
48
+
49
+ function describe(value: unknown): string {
50
+ if (typeof value === "string") return `'${value}'`;
51
+ if (Array.isArray(value)) return `a list`;
52
+ if (value === null) return "null";
53
+ return typeof value;
54
+ }
55
+
56
+ function checkProvides(
57
+ raw: unknown,
58
+ definition: ResourceManifest,
59
+ path: string,
60
+ issues: ZoneSlotIssue[],
61
+ ): void {
62
+ if (raw === true) return;
63
+ if (isPointer(raw)) return;
64
+ issues.push({
65
+ code: "ZONE_ANNOTATION_INVALID",
66
+ manifest: definition,
67
+ path,
68
+ message:
69
+ `${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 ` +
71
+ `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.`,
74
+ });
75
+ }
76
+
77
+ function checkRequires(
78
+ raw: unknown,
79
+ definition: ResourceManifest,
80
+ path: string,
81
+ issues: ZoneSlotIssue[],
82
+ ): void {
83
+ const fail = (message: string): void => {
84
+ issues.push({ code: "ZONE_ANNOTATION_INVALID", manifest: definition, path, message });
85
+ };
86
+
87
+ // Bare-string form: the zone kind, uncorrelated.
88
+ if (typeof raw === "string") {
89
+ if (!raw) fail(`${REQUIRES} at '${path}' is an empty string; name the providing kind.`);
90
+ return;
91
+ }
92
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
93
+ fail(
94
+ `${REQUIRES} at '${path}' is ${describe(raw)}. It takes an alias-qualified kind name ` +
95
+ `(e.g. 'Self.Transaction') or an object with 'zone', an optional 'key' and an ` +
96
+ `optional 'reason'.`,
97
+ );
98
+ return;
99
+ }
100
+
101
+ const obj = raw as Record<string, unknown>;
102
+ if (typeof obj.zone !== "string" || !obj.zone) {
103
+ fail(
104
+ `${REQUIRES} at '${path}' declares no 'zone'. Name the providing kind with the same ` +
105
+ `alias-qualified grammar 'extends' and 'x-telo-ref' use — '<Alias>.<Kind>', ` +
106
+ `'Self.<Kind>', or 'Telo.<Kind>'. Without it the requirement is silently ` +
107
+ `unenforced, and the resource throws at dispatch instead.`,
108
+ );
109
+ }
110
+
111
+ if (obj.key !== undefined) {
112
+ const pointers = Array.isArray(obj.key) ? obj.key : [obj.key];
113
+ if (Array.isArray(obj.key) && obj.key.length === 0) {
114
+ fail(`${REQUIRES} at '${path}' declares an empty 'key' list; omit 'key' instead.`);
115
+ }
116
+ for (const pointer of pointers) {
117
+ if (isPointer(pointer)) continue;
118
+ fail(
119
+ `${REQUIRES} at '${path}' declares the correlation key ${describe(pointer)}, which is ` +
120
+ `not a self-relative JSON Pointer. Write '/connection' (or a list of pointers tried ` +
121
+ `in order, first hit winning). A bare field name is read as a pointer by the runtime ` +
122
+ `but skipped by the checker, so the two halves would disagree about what this ` +
123
+ `manifest means.`,
124
+ );
125
+ }
126
+ }
127
+
128
+ if (obj.reason !== undefined && typeof obj.reason !== "string") {
129
+ fail(`${REQUIRES} at '${path}' declares a non-string 'reason'.`);
130
+ }
131
+
132
+ for (const key of Object.keys(obj)) {
133
+ if (key === "zone" || key === "key" || key === "reason") {
134
+ continue;
135
+ }
136
+ fail(
137
+ `${REQUIRES} at '${path}' declares an unknown property '${key}'. The object form takes ` +
138
+ `'zone', 'key' and 'reason'.`,
139
+ );
140
+ }
141
+ }
142
+
143
+ /** Walk a definition schema, reporting every zone annotation it cannot read.
144
+ * Pure-schema walk, so it needs a visited guard for cyclic `$defs`. */
145
+ function walkSchema(
146
+ node: unknown,
147
+ path: string,
148
+ visited: Set<object>,
149
+ definition: ResourceManifest,
150
+ issues: ZoneSlotIssue[],
151
+ ): void {
152
+ if (!node || typeof node !== "object") return;
153
+ if (visited.has(node as object)) return;
154
+ visited.add(node as object);
155
+ if (Array.isArray(node)) {
156
+ node.forEach((item, i) => walkSchema(item, `${path}[${i}]`, visited, definition, issues));
157
+ return;
158
+ }
159
+ const obj = node as Record<string, unknown>;
160
+ if (obj[PROVIDES] !== undefined) checkProvides(obj[PROVIDES], definition, path, issues);
161
+ if (obj[REQUIRES] !== undefined) checkRequires(obj[REQUIRES], definition, path, issues);
162
+ for (const [key, value] of Object.entries(obj)) {
163
+ if (key === PROVIDES || key === REQUIRES || key === "examples" || key === "default") continue;
164
+ walkSchema(value, path ? `${path}.${key}` : key, visited, definition, issues);
165
+ }
166
+ }
167
+
168
+ /** Schema-level zone-annotation checks over one definition/abstract manifest. */
169
+ export function validateZoneSlotDeclarations(definition: ResourceManifest): ZoneSlotIssue[] {
170
+ const issues: ZoneSlotIssue[] = [];
171
+ const schema = (definition as Record<string, unknown>).schema;
172
+ if (!schema || typeof schema !== "object") return issues;
173
+ walkSchema(schema, "schema", new Set(), definition, issues);
174
+ return issues;
175
+ }
@@ -0,0 +1,27 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+
3
+ /**
4
+ * One imported library's FULL document set, for the zone stage's per-library
5
+ * export derivation — what the flattened analysis view no longer holds, since
6
+ * it forwards only each library's export surface and never its internal
7
+ * dispatch chain.
8
+ *
9
+ * Plain data in a module of its own, deliberately. It is produced by the
10
+ * loading side (`collectZoneModuleDocuments`), named in `AnalysisOptions`, and
11
+ * consumed by the projection; putting it in any of the three would make the
12
+ * other two import that one, and `types.ts` ↔ the projection is a genuine
13
+ * cycle. A leaf module with no imports of its own breaks it without an inline
14
+ * `import(...)` type expression standing in for the dependency nobody wanted.
15
+ */
16
+ export interface ZoneModuleDocuments {
17
+ /** The library's module name (its `Telo.Library` doc's `metadata.name`). */
18
+ module: string;
19
+ /** Stable source identity of the library's owner file — the cache key. */
20
+ sourceId: string;
21
+ /** Owner + partial manifests, stamped with `metadata.source` / `.module`. */
22
+ manifests: ResourceManifest[];
23
+ /** Precomputed content signature; derived from the documents when absent. */
24
+ signature?: string;
25
+ /** The library's declared `exports.resources` entries (bare names). */
26
+ exportedNames: readonly string[];
27
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The single reader of the two execution-zone annotations —
3
+ * `x-telo-provides-zone` and `x-telo-requires-zone` (see
4
+ * `kernel/specs/execution-zones.md`). The analyzer's zone projection, the
5
+ * kernel's `withZone` / `requireZone`, and any editor surface all recognise a
6
+ * zone slot here and nowhere else, the same one-accessor rule `ref-slot.ts`
7
+ * established for `x-telo-ref`. Browser-safe: no Node built-ins.
8
+ *
9
+ * Accepted shapes:
10
+ *
11
+ * x-telo-provides-zone: true # uncorrelated — the zone is the kind
12
+ * x-telo-provides-zone: /connection # correlation-key pointer (own field)
13
+ *
14
+ * x-telo-requires-zone: Self.Transaction # uncorrelated string form
15
+ * x-telo-requires-zone: # object form
16
+ * zone: Self.Transaction
17
+ * key: [/connection, /transaction/connection] # ordered, first hit wins
18
+ * reason: the statement would execute outside any transaction
19
+ */
20
+
21
+ const PROVIDES = "x-telo-provides-zone";
22
+ const REQUIRES = "x-telo-requires-zone";
23
+
24
+ /** A body slot that establishes the declaring kind's zone when dispatched
25
+ * through. The zone's identity is always the declaring kind — the annotation
26
+ * never names one, so provision-on-behalf-of is unrepresentable. */
27
+ export interface ProvidesZoneSlot {
28
+ /** Self-relative JSON pointer to the declaring kind's own field whose resolved
29
+ * reference the zone carries as its correlation payload. Absent =
30
+ * uncorrelated (`true`). */
31
+ key?: string;
32
+ }
33
+
34
+ /** A field declaring that its resource must be reached through a zone. */
35
+ export interface RequiresZoneSlot {
36
+ /** The providing kind, alias-qualified as authored (`Self.Transaction`,
37
+ * `<Alias>.<Kind>`) — canonical `<module>.<Kind>` once
38
+ * `resolveSchemaRefKinds` has rewritten it in the declaring scope. */
39
+ zone: string;
40
+ /** Ordered self-relative JSON pointers tried in order, first hit winning; a
41
+ * pointer may traverse a `!ref` into the referenced resource's own field.
42
+ * Empty = uncorrelated. */
43
+ key: string[];
44
+ /** The runtime consequence, quoted after the path in diagnostics. */
45
+ reason?: string;
46
+ }
47
+
48
+ /** A self-relative JSON Pointer — the only correlation-key spelling the
49
+ * analyzer and the kernel read identically. Applied to BOTH the scalar and the
50
+ * list form: the kernel's walk splits on `/` and drops empty segments, so a
51
+ * bare `connection` would resolve there while the checker skipped it, and the
52
+ * two halves would disagree about what the manifest means. `validate-zone-slots`
53
+ * reports what this rejects. */
54
+ function isPointer(value: unknown): value is string {
55
+ return typeof value === "string" && value.startsWith("/") && value.length > 1;
56
+ }
57
+
58
+ /** Reads a schema node's provides-zone declaration, or undefined when it has
59
+ * none or the value is malformed (`validate-zone-slots` reports those). */
60
+ export function readProvidesZone(node: Record<string, any> | undefined): ProvidesZoneSlot | undefined {
61
+ const raw = node?.[PROVIDES];
62
+ if (raw === true) return {};
63
+ if (isPointer(raw)) return { key: raw };
64
+ return undefined;
65
+ }
66
+
67
+ /** True when the node carries `x-telo-provides-zone` in any shape, valid or not
68
+ * — the recognition test validation needs before it judges the value. */
69
+ export function hasProvidesZone(node: Record<string, any> | undefined): boolean {
70
+ return node?.[PROVIDES] !== undefined;
71
+ }
72
+
73
+ /** Reads a schema node's requires-zone declaration, or undefined when it has
74
+ * none or the value is malformed (`validate-zone-slots` reports those). */
75
+ export function readRequiresZone(node: Record<string, any> | undefined): RequiresZoneSlot | undefined {
76
+ const raw = node?.[REQUIRES];
77
+ if (typeof raw === "string" && raw) return { zone: raw, key: [] };
78
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
79
+ const obj = raw as Record<string, unknown>;
80
+ if (typeof obj.zone !== "string" || !obj.zone) return undefined;
81
+ // One filter for both spellings — see `isPointer`.
82
+ const key = (Array.isArray(obj.key) ? obj.key : [obj.key]).filter(isPointer);
83
+ const slot: RequiresZoneSlot = { zone: obj.zone, key };
84
+ if (typeof obj.reason === "string") slot.reason = obj.reason;
85
+ return slot;
86
+ }
87
+
88
+ /** True when the node carries `x-telo-requires-zone` in any shape. */
89
+ export function hasRequiresZone(node: Record<string, any> | undefined): boolean {
90
+ return node?.[REQUIRES] !== undefined;
91
+ }
92
+
93
+ /**
94
+ * Rewrites the requires-zone kind name in place, in whichever shape it is
95
+ * written — the write-side twin of {@link readRequiresZone}, mirroring
96
+ * `rewriteRefSlotKinds` so `resolveSchemaRefKinds` canonicalizes both
97
+ * annotations in one walk. `map` returns the replacement or `undefined` to
98
+ * leave the authored name untouched (idempotence + quotable diagnostics).
99
+ */
100
+ export function rewriteRequiresZoneKind(
101
+ annotationHolder: Record<string, any>,
102
+ map: (kind: string) => string | undefined,
103
+ ): void {
104
+ const raw = annotationHolder[REQUIRES];
105
+ if (typeof raw === "string") {
106
+ const next = map(raw);
107
+ if (next !== undefined) annotationHolder[REQUIRES] = next;
108
+ return;
109
+ }
110
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
111
+ const obj = raw as Record<string, unknown>;
112
+ if (typeof obj.zone === "string") {
113
+ const next = map(obj.zone);
114
+ if (next !== undefined) obj.zone = next;
115
+ }
116
+ }