@telorun/analyzer 0.53.0 → 0.55.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 (76) hide show
  1. package/dist/analysis-registry.d.ts +20 -0
  2. package/dist/analysis-registry.d.ts.map +1 -1
  3. package/dist/analysis-registry.js +36 -3
  4. package/dist/analyzer.d.ts +3 -2
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/analyzer.js +188 -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 +14 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +7 -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-observed-state.d.ts +14 -13
  40. package/dist/validate-observed-state.d.ts.map +1 -1
  41. package/dist/validate-observed-state.js +21 -88
  42. package/dist/validate-ref-slots.d.ts +48 -0
  43. package/dist/validate-ref-slots.d.ts.map +1 -0
  44. package/dist/validate-ref-slots.js +219 -0
  45. package/dist/validate-references.d.ts.map +1 -1
  46. package/dist/validate-references.js +8 -1
  47. package/dist/validate-zone-slots.d.ts +39 -0
  48. package/dist/validate-zone-slots.d.ts.map +1 -0
  49. package/dist/validate-zone-slots.js +114 -0
  50. package/dist/zone-module-documents.d.ts +27 -0
  51. package/dist/zone-module-documents.d.ts.map +1 -0
  52. package/dist/zone-module-documents.js +1 -0
  53. package/dist/zone-slot.d.ts +61 -0
  54. package/dist/zone-slot.d.ts.map +1 -0
  55. package/dist/zone-slot.js +91 -0
  56. package/package.json +3 -3
  57. package/src/analysis-registry.ts +36 -2
  58. package/src/analyzer.ts +206 -24
  59. package/src/builtins.ts +32 -12
  60. package/src/call-graph.ts +827 -0
  61. package/src/dependency-graph.ts +34 -68
  62. package/src/flatten-for-analyzer.ts +32 -0
  63. package/src/index.ts +47 -0
  64. package/src/manifest-navigation.ts +91 -0
  65. package/src/manifest-visitor.ts +1 -1
  66. package/src/ref-slot.ts +273 -0
  67. package/src/reference-field-map.ts +39 -36
  68. package/src/resolve-schema-ref-kinds.ts +34 -7
  69. package/src/resolve-zone-requirements.ts +781 -0
  70. package/src/types.ts +8 -0
  71. package/src/validate-observed-state.ts +26 -92
  72. package/src/validate-ref-slots.ts +293 -0
  73. package/src/validate-references.ts +8 -1
  74. package/src/validate-zone-slots.ts +175 -0
  75. package/src/zone-module-documents.ts +27 -0
  76. package/src/zone-slot.ts +116 -0
@@ -1,16 +1,8 @@
1
1
  import { OBSERVED_STATE_KEY } from "@telorun/sdk";
2
2
  import { effectiveStatusSchema } from "./extends-resolution.js";
3
3
  import { parseExportEntry } from "./flatten-for-analyzer.js";
4
- import { moduleScopedDefResolver } from "./alias-resolver.js";
5
- import { buildReferenceFieldMap, isRefEntry, isScopeEntry, resolveFieldValues, } from "./reference-field-map.js";
6
- /** The kernel capabilities whose `run()` the kernel dispatches. A ref slot that
7
- * accepts one of them is a slot that can start a resource — `targets:` on an
8
- * Application or a `Run.Sequence`, and a step's `invoke:` (whose schema accepts
9
- * `Telo.Runnable` alongside `Telo.Invocable`, and which the kernel dispatches
10
- * through `run()` when the target has no `invoke()`). Keyed on the declared
11
- * capability, never on a field name or a kind, so any composer that accepts a
12
- * runnable participates without the analyzer knowing about it. */
13
- const RUN_DISPATCH_CONTRACTS = new Set(["Telo.Runnable", "Telo.Service"]);
4
+ import { moduleScopedDefResolver, } from "./alias-resolver.js";
5
+ import { buildReferenceFieldMap, isScopeEntry, resolveFieldValues, } from "./reference-field-map.js";
14
6
  const SYSTEM_KINDS = new Set([
15
7
  "Telo.Definition",
16
8
  "Telo.Abstract",
@@ -77,89 +69,30 @@ export function observedStateRead(chain) {
77
69
  return undefined;
78
70
  }
79
71
  /**
80
- * The names of every resource some slot can start: referenced from a ref slot
81
- * that accepts a `Telo.Runnable` / `Telo.Service`, or named as a step's
82
- * `invoke:` target. A resource in none of them can never `run()`, so it can
83
- * never report observed state.
72
+ * The names of every resource some slot can start.
84
73
  *
85
- * Deliberately an over-approximation a name reachable through any of these
86
- * routes counts as runnable because the cost of a false "can never run" is a
87
- * valid manifest rejected, while the cost of a miss is only that the reader
88
- * finds out at runtime instead, with a message that names the same fix.
74
+ * One question, one answer: a resource is run-reachable when a control-
75
+ * transferring edge reaches it in the typed reference graph. `call`, `detached`,
76
+ * `trigger.inbound` and `trigger.consumer` all mean control arrives; `schema` and
77
+ * `dependency` mean it never does.
78
+ *
79
+ * This replaced two independent over-approximations that had to agree by
80
+ * coincidence: a field-map scan keeping slots whose *constraint capability*
81
+ * looked runnable, plus an untyped whole-manifest scan for the declared step
82
+ * invoke key at any depth. Both were guesses at the question `use` now answers —
83
+ * and the first was wrong in the direction that rejects valid manifests, since a
84
+ * slot constrained to `Telo.Invocable` can still be dispatched through `run()`.
89
85
  */
90
- export function collectRunReachableNames(manifests, defs, aliases) {
86
+ export function collectRunReachableNames(graph) {
87
+ // By NAME, not by resolved node: a `with:`-scoped resource is started by its
88
+ // sequence's `targets:` while never being a top-level node, so resolving first
89
+ // would report it as unstartable. The graph is passed in, never built here —
90
+ // one build per analysis, shared with every other graph consumer.
91
91
  const names = new Set();
92
- const resolve = (kind) => defs.resolve(aliases?.resolveKind(kind) ?? kind) ?? defs.resolve(kind);
93
- for (const manifest of manifests) {
94
- const def = resolve(manifest.kind);
95
- const schema = def?.schema;
96
- if (!schema)
97
- continue;
98
- for (const [path, entry] of buildReferenceFieldMap(schema)) {
99
- if (!isRefEntry(entry))
100
- continue;
101
- if (!entry.refs.some((ref) => RUN_DISPATCH_CONTRACTS.has(ref)))
102
- continue;
103
- for (const value of resolveFieldValues(manifest, path))
104
- collectRefName(value, names);
105
- }
106
- // Step arrays nest through `if` / `while` / `switch` / `try`, and the step
107
- // `invoke:` slot sits behind a local `$ref` the field map does not follow.
108
- // Match the declared invoke key at any depth instead of re-deriving the
109
- // nesting rules — over-approximating in the safe direction.
110
- const invokeKey = stepInvokeKey(schema);
111
- if (invokeKey)
112
- collectKeyedRefs(manifest, invokeKey, names);
113
- }
92
+ for (const edge of graph.controlEdges())
93
+ names.add(edge.toName);
114
94
  return names;
115
95
  }
116
- /** The property name a kind's `x-telo-step-context` declares as its dispatch
117
- * slot (`invoke`), or undefined when the kind has no step array. */
118
- function stepInvokeKey(schema) {
119
- for (const fieldSchema of Object.values((schema.properties ?? {}))) {
120
- const stepCtx = fieldSchema?.["x-telo-step-context"];
121
- if (stepCtx?.invoke)
122
- return stepCtx.invoke;
123
- }
124
- return undefined;
125
- }
126
- /** Collect ref names at every `key` property anywhere in `node`. */
127
- function collectKeyedRefs(node, key, out) {
128
- if (Array.isArray(node)) {
129
- for (const item of node)
130
- collectKeyedRefs(item, key, out);
131
- return;
132
- }
133
- if (node === null || typeof node !== "object")
134
- return;
135
- for (const [k, value] of Object.entries(node)) {
136
- if (k === key)
137
- collectRefName(value, out);
138
- collectKeyedRefs(value, key, out);
139
- }
140
- }
141
- /** Record the resource name a slot value points at — a resolved `{kind, name}`
142
- * ref, an unresolved `!ref` sentinel, or a `{ ref }` / `{ invoke }` wrapper. */
143
- function collectRefName(value, out) {
144
- if (value === null || typeof value !== "object")
145
- return;
146
- if (Array.isArray(value)) {
147
- for (const item of value)
148
- collectRefName(item, out);
149
- return;
150
- }
151
- const v = value;
152
- if (typeof v.name === "string")
153
- out.add(v.name);
154
- if (typeof v.source === "string") {
155
- const dot = v.source.lastIndexOf(".");
156
- out.add(dot >= 0 ? v.source.slice(dot + 1) : v.source);
157
- }
158
- for (const wrapper of ["ref", "invoke"]) {
159
- if (v[wrapper] !== undefined)
160
- collectRefName(v[wrapper], out);
161
- }
162
- }
163
96
  /**
164
97
  * Index every resource a CEL `resources.…` read can name: the module's own
165
98
  * top-level resources, the ones declared inside `x-telo-scope` slots (a
@@ -0,0 +1,48 @@
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 { type CallGraph } from "./call-graph.js";
32
+ import type { DefinitionRegistry } from "./definition-registry.js";
33
+ export interface RefSlotIssue {
34
+ code: "X_TELO_REF_INVALID_USE" | "X_TELO_REF_MISSING_USE" | "X_TELO_REF_MISSING_KIND" | "X_TELO_REF_USE_CONFLICT" | "X_TELO_REF_DYNAMIC_SELECTOR";
35
+ /** The definition (schema issues) or resource (selector issues) at fault. */
36
+ manifest: ResourceManifest;
37
+ /** Schema path of the slot (schema issues) or concrete value path of the
38
+ * selector's site (dynamic-selector issues). */
39
+ path: string;
40
+ message: string;
41
+ }
42
+ /** Schema-level checks over one definition/abstract manifest. */
43
+ export declare function validateRefSlotDeclarations(definition: ResourceManifest): RefSlotIssue[];
44
+ /** Manifest-level check: a `use` case map whose selector is written in CEL.
45
+ * Reads the built graph's `unresolvedReason`, so the detection lives once, in
46
+ * `resolveUseAtSite`, and this pass cannot disagree with what consumers saw. */
47
+ export declare function validateDynamicSelectors(allManifests: ResourceManifest[], registry: DefinitionRegistry, aliases?: AliasResolver, aliasesByModule?: Map<string, AliasResolver>, graph?: CallGraph): RefSlotIssue[];
48
+ //# sourceMappingURL=validate-ref-slots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-ref-slots.d.ts","sourceRoot":"","sources":["../src/validate-ref-slots.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAGnE,MAAM,WAAW,YAAY;IAC3B,IAAI,EACA,wBAAwB,GACxB,wBAAwB,GACxB,yBAAyB,GACzB,yBAAyB,GACzB,6BAA6B,CAAC;IAClC,6EAA6E;IAC7E,QAAQ,EAAE,gBAAgB,CAAC;IAC3B;qDACiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAuJD,iEAAiE;AACjE,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,gBAAgB,GAAG,YAAY,EAAE,CA2CxF;AAED;;iFAEiF;AACjF,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,gBAAgB,EAAE,EAChC,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,EAC5C,KAAK,CAAC,EAAE,SAAS,GAChB,YAAY,EAAE,CA0BhB"}
@@ -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 {};