@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,219 @@
1
+ import { buildCallGraph } from "./call-graph.js";
2
+ import { isRefUse, REF_USES } from "./ref-slot.js";
3
+ const VALID_USES = REF_USES.join(", ");
4
+ /** Raw `use` tokens carried by one annotation value: scalar, list, and every
5
+ * case of a case map. Returned unfiltered so a typo is visible. */
6
+ function rawUseTokens(use) {
7
+ if (use === undefined)
8
+ return [];
9
+ if (Array.isArray(use))
10
+ return use;
11
+ if (use && typeof use === "object") {
12
+ const cases = use.cases;
13
+ if (!cases || typeof cases !== "object")
14
+ return [];
15
+ return Object.values(cases).flatMap((v) => Array.isArray(v) ? v : [v]);
16
+ }
17
+ return [use];
18
+ }
19
+ /** The declared fixed uses of one annotation (scalar/list form only), for the
20
+ * branch-disagreement check. */
21
+ function declaredUses(use) {
22
+ if (isRefUse(use))
23
+ return [use];
24
+ if (Array.isArray(use))
25
+ return use.filter(isRefUse);
26
+ return [];
27
+ }
28
+ function checkAnnotation(annotation, manifest, path, issues) {
29
+ if (typeof annotation === "string" || annotation === undefined)
30
+ return undefined;
31
+ if (!annotation || typeof annotation !== "object" || Array.isArray(annotation))
32
+ return undefined;
33
+ const obj = annotation;
34
+ const kind = obj.kind;
35
+ const hasKind = (typeof kind === "string" && kind.length > 0) ||
36
+ (Array.isArray(kind) && kind.some((k) => typeof k === "string" && k.length > 0));
37
+ if (!hasKind) {
38
+ issues.push({
39
+ code: "X_TELO_REF_MISSING_KIND",
40
+ manifest,
41
+ path,
42
+ message: `x-telo-ref at '${path}' declares no 'kind'. The structured form is ` +
43
+ `'{ kind: <Alias>.<Kind> | [<kinds>], use: <use> }' — without a kind the slot ` +
44
+ `constrains nothing and the editor has nothing to pick against.`,
45
+ });
46
+ }
47
+ const use = obj.use;
48
+ const isCaseMap = !!use && typeof use === "object" && !Array.isArray(use) && "by" in use;
49
+ if (use === undefined) {
50
+ issues.push({
51
+ code: "X_TELO_REF_MISSING_USE",
52
+ manifest,
53
+ path,
54
+ message: `x-telo-ref at '${path}' declares no 'use'. The structured form must say what the ` +
55
+ `declaring resource does with the target — one of: ${VALID_USES} — or a ` +
56
+ `'{ by, cases }' map when a sibling config field selects the mode. Only the legacy ` +
57
+ `bare-string spelling ('x-telo-ref: <Kind>') may omit it.`,
58
+ });
59
+ }
60
+ else {
61
+ for (const token of rawUseTokens(use)) {
62
+ if (isRefUse(token))
63
+ continue;
64
+ issues.push({
65
+ code: "X_TELO_REF_INVALID_USE",
66
+ manifest,
67
+ path,
68
+ message: `x-telo-ref at '${path}' declares unrecognized use '${String(token)}'. ` +
69
+ `Valid uses: ${VALID_USES}. An unrecognized token would silently degrade the slot ` +
70
+ `to the legacy no-use reading.`,
71
+ });
72
+ }
73
+ if (isCaseMap) {
74
+ const by = use.by;
75
+ if (typeof by !== "string" || !by.startsWith("/")) {
76
+ issues.push({
77
+ code: "X_TELO_REF_INVALID_USE",
78
+ manifest,
79
+ path,
80
+ message: `x-telo-ref at '${path}' has a 'use' case map whose 'by' is not a JSON Pointer. ` +
81
+ `'by' names a sibling field of the object enclosing the slot, e.g. '/detach'.`,
82
+ });
83
+ }
84
+ }
85
+ }
86
+ return declaredUses(use);
87
+ }
88
+ /** True when a node is a reference slot: it carries `x-telo-ref` directly or on
89
+ * an `anyOf`/`oneOf` branch. */
90
+ function carriesRefAnnotation(obj) {
91
+ if (obj["x-telo-ref"] !== undefined)
92
+ return true;
93
+ for (const key of ["anyOf", "oneOf"]) {
94
+ const branches = obj[key];
95
+ if (!Array.isArray(branches))
96
+ continue;
97
+ if (branches.some((b) => b && typeof b === "object" && b["x-telo-ref"] !== undefined)) {
98
+ return true;
99
+ }
100
+ }
101
+ return false;
102
+ }
103
+ /** Walk a definition schema, invoking `onSlot` for every node that carries an
104
+ * `x-telo-ref` (directly or on an `anyOf`/`oneOf` branch — the SLOT is the
105
+ * node holding the branches, so a branch is never reported twice). Pure-schema
106
+ * walk, so it needs — and has — a visited guard for cyclic `$defs`. */
107
+ function walkSchema(node, path, visited, claimedBranches, onSlot) {
108
+ if (!node || typeof node !== "object")
109
+ return;
110
+ if (visited.has(node))
111
+ return;
112
+ visited.add(node);
113
+ if (Array.isArray(node)) {
114
+ node.forEach((item, i) => walkSchema(item, `${path}[${i}]`, visited, claimedBranches, onSlot));
115
+ return;
116
+ }
117
+ const obj = node;
118
+ if (carriesRefAnnotation(obj) && !claimedBranches.has(obj)) {
119
+ onSlot(obj, path);
120
+ for (const key of ["anyOf", "oneOf"]) {
121
+ const branches = obj[key];
122
+ if (!Array.isArray(branches))
123
+ continue;
124
+ for (const branch of branches) {
125
+ if (branch && typeof branch === "object")
126
+ claimedBranches.add(branch);
127
+ }
128
+ }
129
+ }
130
+ for (const [key, value] of Object.entries(obj)) {
131
+ if (key === "x-telo-ref" || key === "examples" || key === "default")
132
+ continue;
133
+ walkSchema(value, path ? `${path}.${key}` : key, visited, claimedBranches, onSlot);
134
+ }
135
+ }
136
+ /** Schema-level checks over one definition/abstract manifest. */
137
+ export function validateRefSlotDeclarations(definition) {
138
+ const issues = [];
139
+ const schema = definition.schema;
140
+ if (!schema || typeof schema !== "object")
141
+ return issues;
142
+ walkSchema(schema, "schema", new Set(), new Set(), (node, path) => {
143
+ const branchUses = [];
144
+ const own = checkAnnotation(node["x-telo-ref"], definition, path, issues);
145
+ if (own)
146
+ branchUses.push(own);
147
+ for (const key of ["anyOf", "oneOf"]) {
148
+ const branches = node[key];
149
+ if (!Array.isArray(branches))
150
+ continue;
151
+ branches.forEach((branch, i) => {
152
+ if (!branch || typeof branch !== "object")
153
+ return;
154
+ const declared = checkAnnotation(branch["x-telo-ref"], definition, `${path}.${key}[${i}]`, issues);
155
+ if (declared)
156
+ branchUses.push(declared);
157
+ });
158
+ }
159
+ const nonEmpty = branchUses.filter((uses) => uses.length > 0);
160
+ if (nonEmpty.length > 1) {
161
+ const first = [...nonEmpty[0]].sort().join(",");
162
+ const disagrees = nonEmpty.some((uses) => [...uses].sort().join(",") !== first);
163
+ if (disagrees) {
164
+ issues.push({
165
+ code: "X_TELO_REF_USE_CONFLICT",
166
+ manifest: definition,
167
+ path,
168
+ message: `x-telo-ref branches at '${path}' declare disagreeing uses ` +
169
+ `(${nonEmpty.map((u) => u.join("|")).join(" vs ")}). 'use' is a property of the ` +
170
+ `slot, never of a branch — declare several acceptable kinds as one ` +
171
+ `'kind: [<kinds>]' list with one 'use'.`,
172
+ });
173
+ }
174
+ }
175
+ });
176
+ return issues;
177
+ }
178
+ /** Manifest-level check: a `use` case map whose selector is written in CEL.
179
+ * Reads the built graph's `unresolvedReason`, so the detection lives once, in
180
+ * `resolveUseAtSite`, and this pass cannot disagree with what consumers saw. */
181
+ export function validateDynamicSelectors(allManifests, registry, aliases, aliasesByModule, graph) {
182
+ const issues = [];
183
+ const callGraph = graph ?? buildCallGraph(allManifests, registry, { aliases, aliasesByModule });
184
+ for (const edge of callGraph.edges) {
185
+ if (edge.unresolvedReason !== "dynamic")
186
+ continue;
187
+ const from = callGraph.nodes.get(edge.from);
188
+ const owner = from?.type === "step" ? callGraph.nodes.get(from.owner) : from;
189
+ if (!owner || owner.type !== "resource")
190
+ continue;
191
+ // Anchor at the SELECTOR — the field the author must change — not at the
192
+ // ref slot several lines away. Derivable: the slot's enclosing path plus
193
+ // the pointer's segments.
194
+ const selectorPath = selectorPathOf(edge.path, edge.unresolved?.by ?? "");
195
+ issues.push({
196
+ code: "X_TELO_REF_DYNAMIC_SELECTOR",
197
+ manifest: owner.manifest,
198
+ path: selectorPath,
199
+ message: `The mode selector at '${selectorPath}' is a CEL expression, so which 'use' holds ` +
200
+ `for the reference at '${edge.path}' cannot be resolved statically. The selector must ` +
201
+ `be a literal or take its schema default — a call graph known only at runtime is not ` +
202
+ `statically analyzable. Write the mode as a literal, or split the wiring into one ` +
203
+ `resource per mode.`,
204
+ });
205
+ }
206
+ return issues;
207
+ }
208
+ /** Concrete path of a case-map selector: the slot's enclosing path joined with
209
+ * the pointer's segments (`steps[0].invoke` + `/detach` → `steps[0].detach`). */
210
+ function selectorPathOf(slotPath, pointer) {
211
+ const lastDot = slotPath.lastIndexOf(".");
212
+ const enclosing = lastDot < 0 ? "" : slotPath.slice(0, lastDot);
213
+ const segments = pointer
214
+ .replace(/^\//, "")
215
+ .split("/")
216
+ .map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"))
217
+ .join(".");
218
+ return enclosing ? `${enclosing}.${segments}` : segments;
219
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAgD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAmetB"}
1
+ {"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAuD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAmetB"}
@@ -35,7 +35,14 @@ function checkKind(kind, entry, registry, aliases) {
35
35
  if (targetDef.kind === "Telo.Abstract") {
36
36
  if (subtypes.length === 0)
37
37
  return []; // partial context — no implementations loaded yet
38
- const options = [...subtypeKinds].join(", ");
38
+ // Suggest only what an author can actually wire: with abstract-extends-
39
+ // abstract real (Telo.Executable over Invocable/Runnable), the transitive
40
+ // subtype list contains abstracts, which are non-instantiable and would
41
+ // read as fixes that cannot work.
42
+ const concrete = subtypes
43
+ .filter((d) => d.kind !== "Telo.Abstract")
44
+ .map((d) => `${d.metadata.module}.${d.metadata.name}`);
45
+ const options = (concrete.length > 0 ? concrete : [...subtypeKinds]).join(", ");
39
46
  errors.push(`'${kind}' does not implement '${targetKind}' (known implementations: ${options})`);
40
47
  }
41
48
  else {
@@ -0,0 +1,39 @@
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
+ export interface ZoneSlotIssue {
31
+ code: "ZONE_ANNOTATION_INVALID";
32
+ manifest: ResourceManifest;
33
+ /** Schema path of the annotated slot. */
34
+ path: string;
35
+ message: string;
36
+ }
37
+ /** Schema-level zone-annotation checks over one definition/abstract manifest. */
38
+ export declare function validateZoneSlotDeclarations(definition: ResourceManifest): ZoneSlotIssue[];
39
+ //# sourceMappingURL=validate-zone-slots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-zone-slots.d.ts","sourceRoot":"","sources":["../src/validate-zone-slots.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,yBAAyB,CAAC;IAChC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAmID,iFAAiF;AACjF,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,gBAAgB,GAAG,aAAa,EAAE,CAM1F"}
@@ -0,0 +1,114 @@
1
+ const PROVIDES = "x-telo-provides-zone";
2
+ const REQUIRES = "x-telo-requires-zone";
3
+ /** A self-relative JSON Pointer, the only correlation-key spelling both halves
4
+ * read identically. `""` (whole document) is meaningless as a key, so a
5
+ * pointer must name at least one segment. */
6
+ function isPointer(value) {
7
+ return typeof value === "string" && value.startsWith("/") && value.length > 1;
8
+ }
9
+ function describe(value) {
10
+ if (typeof value === "string")
11
+ return `'${value}'`;
12
+ if (Array.isArray(value))
13
+ return `a list`;
14
+ if (value === null)
15
+ return "null";
16
+ return typeof value;
17
+ }
18
+ function checkProvides(raw, definition, path, issues) {
19
+ if (raw === true)
20
+ return;
21
+ if (isPointer(raw))
22
+ return;
23
+ issues.push({
24
+ code: "ZONE_ANNOTATION_INVALID",
25
+ manifest: definition,
26
+ path,
27
+ message: `${PROVIDES} at '${path}' is ${describe(raw)}. It takes 'true' (the zone is ` +
28
+ `uncorrelated) or a self-relative JSON Pointer naming this kind's own field ` +
29
+ `whose resolved reference the zone carries as its correlation payload ` +
30
+ `(e.g. '/connection'). It never names the zone — the zone a slot provides ` +
31
+ `is always the declaring kind.`,
32
+ });
33
+ }
34
+ function checkRequires(raw, definition, path, issues) {
35
+ const fail = (message) => {
36
+ issues.push({ code: "ZONE_ANNOTATION_INVALID", manifest: definition, path, message });
37
+ };
38
+ // Bare-string form: the zone kind, uncorrelated.
39
+ if (typeof raw === "string") {
40
+ if (!raw)
41
+ fail(`${REQUIRES} at '${path}' is an empty string; name the providing kind.`);
42
+ return;
43
+ }
44
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
45
+ fail(`${REQUIRES} at '${path}' is ${describe(raw)}. It takes an alias-qualified kind name ` +
46
+ `(e.g. 'Self.Transaction') or an object with 'zone', an optional 'key' and an ` +
47
+ `optional 'reason'.`);
48
+ return;
49
+ }
50
+ const obj = raw;
51
+ if (typeof obj.zone !== "string" || !obj.zone) {
52
+ fail(`${REQUIRES} at '${path}' declares no 'zone'. Name the providing kind with the same ` +
53
+ `alias-qualified grammar 'extends' and 'x-telo-ref' use — '<Alias>.<Kind>', ` +
54
+ `'Self.<Kind>', or 'Telo.<Kind>'. Without it the requirement is silently ` +
55
+ `unenforced, and the resource throws at dispatch instead.`);
56
+ }
57
+ if (obj.key !== undefined) {
58
+ const pointers = Array.isArray(obj.key) ? obj.key : [obj.key];
59
+ if (Array.isArray(obj.key) && obj.key.length === 0) {
60
+ fail(`${REQUIRES} at '${path}' declares an empty 'key' list; omit 'key' instead.`);
61
+ }
62
+ for (const pointer of pointers) {
63
+ if (isPointer(pointer))
64
+ continue;
65
+ fail(`${REQUIRES} at '${path}' declares the correlation key ${describe(pointer)}, which is ` +
66
+ `not a self-relative JSON Pointer. Write '/connection' (or a list of pointers tried ` +
67
+ `in order, first hit winning). A bare field name is read as a pointer by the runtime ` +
68
+ `but skipped by the checker, so the two halves would disagree about what this ` +
69
+ `manifest means.`);
70
+ }
71
+ }
72
+ if (obj.reason !== undefined && typeof obj.reason !== "string") {
73
+ fail(`${REQUIRES} at '${path}' declares a non-string 'reason'.`);
74
+ }
75
+ for (const key of Object.keys(obj)) {
76
+ if (key === "zone" || key === "key" || key === "reason") {
77
+ continue;
78
+ }
79
+ fail(`${REQUIRES} at '${path}' declares an unknown property '${key}'. The object form takes ` +
80
+ `'zone', 'key' and 'reason'.`);
81
+ }
82
+ }
83
+ /** Walk a definition schema, reporting every zone annotation it cannot read.
84
+ * Pure-schema walk, so it needs a visited guard for cyclic `$defs`. */
85
+ function walkSchema(node, path, visited, definition, issues) {
86
+ if (!node || typeof node !== "object")
87
+ return;
88
+ if (visited.has(node))
89
+ return;
90
+ visited.add(node);
91
+ if (Array.isArray(node)) {
92
+ node.forEach((item, i) => walkSchema(item, `${path}[${i}]`, visited, definition, issues));
93
+ return;
94
+ }
95
+ const obj = node;
96
+ if (obj[PROVIDES] !== undefined)
97
+ checkProvides(obj[PROVIDES], definition, path, issues);
98
+ if (obj[REQUIRES] !== undefined)
99
+ checkRequires(obj[REQUIRES], definition, path, issues);
100
+ for (const [key, value] of Object.entries(obj)) {
101
+ if (key === PROVIDES || key === REQUIRES || key === "examples" || key === "default")
102
+ continue;
103
+ walkSchema(value, path ? `${path}.${key}` : key, visited, definition, issues);
104
+ }
105
+ }
106
+ /** Schema-level zone-annotation checks over one definition/abstract manifest. */
107
+ export function validateZoneSlotDeclarations(definition) {
108
+ const issues = [];
109
+ const schema = definition.schema;
110
+ if (!schema || typeof schema !== "object")
111
+ return issues;
112
+ walkSchema(schema, "schema", new Set(), definition, issues);
113
+ return issues;
114
+ }
@@ -0,0 +1,27 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+ /**
3
+ * One imported library's FULL document set, for the zone stage's per-library
4
+ * export derivation — what the flattened analysis view no longer holds, since
5
+ * it forwards only each library's export surface and never its internal
6
+ * dispatch chain.
7
+ *
8
+ * Plain data in a module of its own, deliberately. It is produced by the
9
+ * loading side (`collectZoneModuleDocuments`), named in `AnalysisOptions`, and
10
+ * consumed by the projection; putting it in any of the three would make the
11
+ * other two import that one, and `types.ts` ↔ the projection is a genuine
12
+ * cycle. A leaf module with no imports of its own breaks it without an inline
13
+ * `import(...)` type expression standing in for the dependency nobody wanted.
14
+ */
15
+ export interface ZoneModuleDocuments {
16
+ /** The library's module name (its `Telo.Library` doc's `metadata.name`). */
17
+ module: string;
18
+ /** Stable source identity of the library's owner file — the cache key. */
19
+ sourceId: string;
20
+ /** Owner + partial manifests, stamped with `metadata.source` / `.module`. */
21
+ manifests: ResourceManifest[];
22
+ /** Precomputed content signature; derived from the documents when absent. */
23
+ signature?: string;
24
+ /** The library's declared `exports.resources` entries (bare names). */
25
+ exportedNames: readonly string[];
26
+ }
27
+ //# sourceMappingURL=zone-module-documents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zone-module-documents.d.ts","sourceRoot":"","sources":["../src/zone-module-documents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,mBAAmB;IAClC,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
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
+ /** A body slot that establishes the declaring kind's zone when dispatched
21
+ * through. The zone's identity is always the declaring kind — the annotation
22
+ * never names one, so provision-on-behalf-of is unrepresentable. */
23
+ export interface ProvidesZoneSlot {
24
+ /** Self-relative JSON pointer to the declaring kind's own field whose resolved
25
+ * reference the zone carries as its correlation payload. Absent =
26
+ * uncorrelated (`true`). */
27
+ key?: string;
28
+ }
29
+ /** A field declaring that its resource must be reached through a zone. */
30
+ export interface RequiresZoneSlot {
31
+ /** The providing kind, alias-qualified as authored (`Self.Transaction`,
32
+ * `<Alias>.<Kind>`) — canonical `<module>.<Kind>` once
33
+ * `resolveSchemaRefKinds` has rewritten it in the declaring scope. */
34
+ zone: string;
35
+ /** Ordered self-relative JSON pointers tried in order, first hit winning; a
36
+ * pointer may traverse a `!ref` into the referenced resource's own field.
37
+ * Empty = uncorrelated. */
38
+ key: string[];
39
+ /** The runtime consequence, quoted after the path in diagnostics. */
40
+ reason?: string;
41
+ }
42
+ /** Reads a schema node's provides-zone declaration, or undefined when it has
43
+ * none or the value is malformed (`validate-zone-slots` reports those). */
44
+ export declare function readProvidesZone(node: Record<string, any> | undefined): ProvidesZoneSlot | undefined;
45
+ /** True when the node carries `x-telo-provides-zone` in any shape, valid or not
46
+ * — the recognition test validation needs before it judges the value. */
47
+ export declare function hasProvidesZone(node: Record<string, any> | undefined): boolean;
48
+ /** Reads a schema node's requires-zone declaration, or undefined when it has
49
+ * none or the value is malformed (`validate-zone-slots` reports those). */
50
+ export declare function readRequiresZone(node: Record<string, any> | undefined): RequiresZoneSlot | undefined;
51
+ /** True when the node carries `x-telo-requires-zone` in any shape. */
52
+ export declare function hasRequiresZone(node: Record<string, any> | undefined): boolean;
53
+ /**
54
+ * Rewrites the requires-zone kind name in place, in whichever shape it is
55
+ * written — the write-side twin of {@link readRequiresZone}, mirroring
56
+ * `rewriteRefSlotKinds` so `resolveSchemaRefKinds` canonicalizes both
57
+ * annotations in one walk. `map` returns the replacement or `undefined` to
58
+ * leave the authored name untouched (idempotence + quotable diagnostics).
59
+ */
60
+ export declare function rewriteRequiresZoneKind(annotationHolder: Record<string, any>, map: (kind: string) => string | undefined): void;
61
+ //# sourceMappingURL=zone-slot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zone-slot.d.ts","sourceRoot":"","sources":["../src/zone-slot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH;;qEAEqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B;;iCAE6B;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B;;2EAEuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb;;gCAE4B;IAC5B,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAYD;4EAC4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,gBAAgB,GAAG,SAAS,CAKpG;AAED;0EAC0E;AAC1E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,OAAO,CAE9E;AAED;4EAC4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,gBAAgB,GAAG,SAAS,CAWpG;AAED,sEAAsE;AACtE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,OAAO,CAE9E;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACrC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACxC,IAAI,CAaN"}
@@ -0,0 +1,91 @@
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
+ const PROVIDES = "x-telo-provides-zone";
21
+ const REQUIRES = "x-telo-requires-zone";
22
+ /** A self-relative JSON Pointer — the only correlation-key spelling the
23
+ * analyzer and the kernel read identically. Applied to BOTH the scalar and the
24
+ * list form: the kernel's walk splits on `/` and drops empty segments, so a
25
+ * bare `connection` would resolve there while the checker skipped it, and the
26
+ * two halves would disagree about what the manifest means. `validate-zone-slots`
27
+ * reports what this rejects. */
28
+ function isPointer(value) {
29
+ return typeof value === "string" && value.startsWith("/") && value.length > 1;
30
+ }
31
+ /** Reads a schema node's provides-zone declaration, or undefined when it has
32
+ * none or the value is malformed (`validate-zone-slots` reports those). */
33
+ export function readProvidesZone(node) {
34
+ const raw = node?.[PROVIDES];
35
+ if (raw === true)
36
+ return {};
37
+ if (isPointer(raw))
38
+ return { key: raw };
39
+ return undefined;
40
+ }
41
+ /** True when the node carries `x-telo-provides-zone` in any shape, valid or not
42
+ * — the recognition test validation needs before it judges the value. */
43
+ export function hasProvidesZone(node) {
44
+ return node?.[PROVIDES] !== undefined;
45
+ }
46
+ /** Reads a schema node's requires-zone declaration, or undefined when it has
47
+ * none or the value is malformed (`validate-zone-slots` reports those). */
48
+ export function readRequiresZone(node) {
49
+ const raw = node?.[REQUIRES];
50
+ if (typeof raw === "string" && raw)
51
+ return { zone: raw, key: [] };
52
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
53
+ return undefined;
54
+ const obj = raw;
55
+ if (typeof obj.zone !== "string" || !obj.zone)
56
+ return undefined;
57
+ // One filter for both spellings — see `isPointer`.
58
+ const key = (Array.isArray(obj.key) ? obj.key : [obj.key]).filter(isPointer);
59
+ const slot = { zone: obj.zone, key };
60
+ if (typeof obj.reason === "string")
61
+ slot.reason = obj.reason;
62
+ return slot;
63
+ }
64
+ /** True when the node carries `x-telo-requires-zone` in any shape. */
65
+ export function hasRequiresZone(node) {
66
+ return node?.[REQUIRES] !== undefined;
67
+ }
68
+ /**
69
+ * Rewrites the requires-zone kind name in place, in whichever shape it is
70
+ * written — the write-side twin of {@link readRequiresZone}, mirroring
71
+ * `rewriteRefSlotKinds` so `resolveSchemaRefKinds` canonicalizes both
72
+ * annotations in one walk. `map` returns the replacement or `undefined` to
73
+ * leave the authored name untouched (idempotence + quotable diagnostics).
74
+ */
75
+ export function rewriteRequiresZoneKind(annotationHolder, map) {
76
+ const raw = annotationHolder[REQUIRES];
77
+ if (typeof raw === "string") {
78
+ const next = map(raw);
79
+ if (next !== undefined)
80
+ annotationHolder[REQUIRES] = next;
81
+ return;
82
+ }
83
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
84
+ return;
85
+ const obj = raw;
86
+ if (typeof obj.zone === "string") {
87
+ const next = map(obj.zone);
88
+ if (next !== undefined)
89
+ obj.zone = next;
90
+ }
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.52.0",
3
+ "version": "0.54.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -42,13 +42,13 @@
42
42
  "ajv-formats": "^3.0.1",
43
43
  "jsonpath-plus": "^10.3.0",
44
44
  "yaml": "^2.8.3",
45
- "@telorun/templating": "0.11.1"
45
+ "@telorun/templating": "0.12.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^20.0.0",
49
49
  "typescript": "^5.0.0",
50
50
  "vitest": "^2.1.8",
51
- "@telorun/sdk": "0.65.0"
51
+ "@telorun/sdk": "0.67.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@telorun/sdk": "*"
@@ -177,12 +177,30 @@ export class AnalysisRegistry {
177
177
  private capabilitiesForRefs(refs: string[]): string[] {
178
178
  const out: string[] = [];
179
179
  for (const ref of refs) {
180
- const cap = this.capabilityForRef(ref);
181
- if (cap && !out.includes(cap)) out.push(cap);
180
+ for (const cap of this.leafCapabilitiesForRef(ref)) {
181
+ if (!out.includes(cap)) out.push(cap);
182
+ }
182
183
  }
183
184
  return out;
184
185
  }
185
186
 
187
+ /** Like {@link capabilityForRef}, but a capability GROUP — an abstract with no
188
+ * `capability` of its own that other capability abstracts extend, i.e.
189
+ * `Telo.Executable` over Invocable and Runnable — expands to its leaves.
190
+ * Classification consumers (the editor's port flavor) match against leaf
191
+ * capabilities, so without expansion every `Telo.Executable` slot would fall
192
+ * out of both classification sets and silently stop rendering — and so would
193
+ * slots constrained to the next abstract-of-abstracts. */
194
+ private leafCapabilitiesForRef(xTeloRef: string): string[] {
195
+ const cap = this.capabilityForRef(xTeloRef);
196
+ if (!cap) return [];
197
+ const leaves = this.defs
198
+ .getByExtends(cap)
199
+ .filter((d) => d.kind === "Telo.Abstract" && !d.capability)
200
+ .map((d) => `${(d.metadata as { module?: string }).module}.${d.metadata.name}`);
201
+ return leaves.length > 0 ? leaves : [cap];
202
+ }
203
+
186
204
  /**
187
205
  * Walks a manifest's annotation sites (refs, scopes, schema-from, CEL) via
188
206
  * the shared manifest visitor, bound to this registry's definitions and