@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.
- package/dist/analysis-registry.d.ts +20 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +36 -3
- package/dist/analyzer.d.ts +3 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +188 -26
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +32 -12
- package/dist/call-graph.d.ts +189 -0
- package/dist/call-graph.d.ts.map +1 -0
- package/dist/call-graph.js +617 -0
- package/dist/dependency-graph.d.ts +17 -7
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +36 -65
- package/dist/flatten-for-analyzer.d.ts +8 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +32 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/manifest-navigation.d.ts +32 -0
- package/dist/manifest-navigation.d.ts.map +1 -0
- package/dist/manifest-navigation.js +91 -0
- package/dist/manifest-visitor.js +1 -1
- package/dist/ref-slot.d.ts +125 -0
- package/dist/ref-slot.d.ts.map +1 -0
- package/dist/ref-slot.js +226 -0
- package/dist/reference-field-map.d.ts +15 -1
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +29 -35
- package/dist/resolve-schema-ref-kinds.d.ts +4 -0
- package/dist/resolve-schema-ref-kinds.d.ts.map +1 -1
- package/dist/resolve-schema-ref-kinds.js +31 -8
- package/dist/resolve-zone-requirements.d.ts +110 -0
- package/dist/resolve-zone-requirements.d.ts.map +1 -0
- package/dist/resolve-zone-requirements.js +541 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-observed-state.d.ts +14 -13
- package/dist/validate-observed-state.d.ts.map +1 -1
- package/dist/validate-observed-state.js +21 -88
- package/dist/validate-ref-slots.d.ts +48 -0
- package/dist/validate-ref-slots.d.ts.map +1 -0
- package/dist/validate-ref-slots.js +219 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +8 -1
- package/dist/validate-zone-slots.d.ts +39 -0
- package/dist/validate-zone-slots.d.ts.map +1 -0
- package/dist/validate-zone-slots.js +114 -0
- package/dist/zone-module-documents.d.ts +27 -0
- package/dist/zone-module-documents.d.ts.map +1 -0
- package/dist/zone-module-documents.js +1 -0
- package/dist/zone-slot.d.ts +61 -0
- package/dist/zone-slot.d.ts.map +1 -0
- package/dist/zone-slot.js +91 -0
- package/package.json +3 -3
- package/src/analysis-registry.ts +36 -2
- package/src/analyzer.ts +206 -24
- package/src/builtins.ts +32 -12
- package/src/call-graph.ts +827 -0
- package/src/dependency-graph.ts +34 -68
- package/src/flatten-for-analyzer.ts +32 -0
- package/src/index.ts +47 -0
- package/src/manifest-navigation.ts +91 -0
- package/src/manifest-visitor.ts +1 -1
- package/src/ref-slot.ts +273 -0
- package/src/reference-field-map.ts +39 -36
- package/src/resolve-schema-ref-kinds.ts +34 -7
- package/src/resolve-zone-requirements.ts +781 -0
- package/src/types.ts +8 -0
- package/src/validate-observed-state.ts +26 -92
- package/src/validate-ref-slots.ts +293 -0
- package/src/validate-references.ts +8 -1
- package/src/validate-zone-slots.ts +175 -0
- package/src/zone-module-documents.ts +27 -0
- 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,
|
|
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
|
|
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
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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(
|
|
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
|
|
93
|
-
|
|
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;
|
|
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
|
-
|
|
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 {};
|