@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.
- package/dist/analysis-registry.d.ts +8 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +21 -3
- package/dist/analyzer.d.ts +3 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +193 -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 +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -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-module-metadata.d.ts +38 -0
- package/dist/validate-module-metadata.d.ts.map +1 -0
- package/dist/validate-module-metadata.js +256 -0
- 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 +20 -2
- package/src/analyzer.ts +211 -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 +51 -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-module-metadata.ts +335 -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
package/src/dependency-graph.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
-
import { isRefSentinel } from "@telorun/templating";
|
|
3
2
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
|
+
import { buildCallGraph, projectToPairs, type ResourceGraphNode } from "./call-graph.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
|
-
import { visitManifest } from "./manifest-visitor.js";
|
|
6
|
-
import { DEPENDENCY_GRAPH_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
7
5
|
|
|
8
6
|
export interface ResourceNode {
|
|
9
7
|
kind: string;
|
|
@@ -22,16 +20,26 @@ export interface DependencyGraph {
|
|
|
22
20
|
const nodeKey = (kind: string, name: string) => `${kind}\0${name}`;
|
|
23
21
|
|
|
24
22
|
/**
|
|
25
|
-
* Builds a directed acyclic graph (DAG) of
|
|
23
|
+
* Builds a directed acyclic graph (DAG) of boot-time resource dependencies and
|
|
26
24
|
* returns either a topological initialization order or the cycle path.
|
|
27
25
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* (scoped resources are initialized on demand at runtime, not at boot).
|
|
31
|
-
* - x-telo-scope fields themselves are excluded from the graph.
|
|
26
|
+
* A projection of the typed reference graph, not a second walk of the manifest.
|
|
27
|
+
* What this consumer keeps of the full graph:
|
|
32
28
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
29
|
+
* - **Injection sites only.** A site the reference field map reaches is a
|
|
30
|
+
* Phase-5 injection site, so its target must be constructed first — including
|
|
31
|
+
* `Telo.Application`'s inline `targets[].invoke`, which is step-declared but
|
|
32
|
+
* injected. A step slot behind a local `$ref` and a value-tree-discovered ref
|
|
33
|
+
* resolve at dispatch, so their targets need only exist by the time the step
|
|
34
|
+
* runs. All can be `use: call` — the difference is the site, never the node
|
|
35
|
+
* kind or the use.
|
|
36
|
+
* - **Every use but `schema`.** A `Telo.Type` slot names a shape; no runtime
|
|
37
|
+
* instance is constructed, so there is nothing to order against.
|
|
38
|
+
* - **No edge into the source's own scope.** A scoped resource is created when
|
|
39
|
+
* the scope opens, not at boot.
|
|
40
|
+
* - **Pairs, not parallel edges.** This is the one consumer for which two slots
|
|
41
|
+
* naming the same target genuinely mean the same thing, so it collapses the
|
|
42
|
+
* multigraph itself instead of the graph erasing the distinction for everyone.
|
|
35
43
|
*/
|
|
36
44
|
export function buildDependencyGraph(
|
|
37
45
|
resources: ResourceManifest[],
|
|
@@ -39,67 +47,25 @@ export function buildDependencyGraph(
|
|
|
39
47
|
aliases?: AliasResolver,
|
|
40
48
|
aliasesByModule?: Map<string, AliasResolver>,
|
|
41
49
|
): DependencyGraph {
|
|
42
|
-
|
|
50
|
+
const graph = buildCallGraph(resources, registry, { aliases, aliasesByModule });
|
|
51
|
+
|
|
43
52
|
const nodes = new Map<string, ResourceNode>();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind)) continue;
|
|
51
|
-
const node = { kind: r.kind, name: r.metadata.name as string };
|
|
52
|
-
nodes.set(nodeKey(node.kind, node.name), node);
|
|
53
|
-
nodesByName.set(node.name, node);
|
|
53
|
+
for (const node of graph.nodes.values()) {
|
|
54
|
+
if (node.type !== "resource") continue;
|
|
55
|
+
const resource = node as ResourceGraphNode;
|
|
56
|
+
// A scope-declared resource is created when its scope opens, never at boot.
|
|
57
|
+
if (resource.scoped) continue;
|
|
58
|
+
nodes.set(resource.id, { kind: resource.kind, name: resource.name });
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// before that resource's RefSites, so this is set before any edge is added).
|
|
65
|
-
let scopedNames = new Set<string>();
|
|
66
|
-
|
|
67
|
-
// Expanded map so refs nested behind x-telo-schema-from contribute edges to
|
|
68
|
-
// the DAG. Without these, a parent (e.g. Http.Server) can init before its
|
|
69
|
-
// extracted encoder and Phase 5 injection fires against a not-yet-created
|
|
70
|
-
// dependency.
|
|
71
|
-
visitManifest(
|
|
72
|
-
resources,
|
|
73
|
-
registry,
|
|
74
|
-
{
|
|
75
|
-
onScope: (e) => {
|
|
76
|
-
scopedNames = e.enclosedNames;
|
|
77
|
-
},
|
|
78
|
-
onRef: (e) => {
|
|
79
|
-
const sourceKey = nodeKey(e.source.kind, e.source.metadata!.name as string);
|
|
80
|
-
const val = e.value;
|
|
81
|
-
|
|
82
|
-
// `!ref <name>` sentinel — look up the target's kind from the name
|
|
83
|
-
// (resources are unique by name) so the edge carries the concrete kind,
|
|
84
|
-
// matching the {kind, name} edge shape below.
|
|
85
|
-
if (isRefSentinel(val)) {
|
|
86
|
-
const refName = val.source;
|
|
87
|
-
if (scopedNames.has(refName)) return;
|
|
88
|
-
const node = nodesByName.get(refName);
|
|
89
|
-
if (node) deps.get(sourceKey)!.add(nodeKey(node.kind, node.name));
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (typeof val !== "object") return;
|
|
94
|
-
const ref = val as Record<string, unknown>;
|
|
95
|
-
if (!ref.kind || !ref.name) return;
|
|
96
|
-
if (scopedNames.has(ref.name as string)) return;
|
|
97
|
-
const targetKey = nodeKey(ref.kind as string, ref.name as string);
|
|
98
|
-
if (nodes.has(targetKey)) deps.get(sourceKey)!.add(targetKey);
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
{ aliases, aliasesByModule, skipKinds: SYSTEM_KINDS, expand: true },
|
|
102
|
-
);
|
|
61
|
+
const deps = projectToPairs(graph, {
|
|
62
|
+
keepUse: (use) => use.length === 0 || use.some((u) => u !== "schema"),
|
|
63
|
+
});
|
|
64
|
+
for (const edge of graph.edges) {
|
|
65
|
+
if (!edge.scoped || !edge.to) continue;
|
|
66
|
+
deps.get(edge.from)?.delete(edge.to);
|
|
67
|
+
}
|
|
68
|
+
for (const key of nodes.keys()) if (!deps.has(key)) deps.set(key, new Set());
|
|
103
69
|
|
|
104
70
|
// --- Kahn's topological sort ---
|
|
105
71
|
// in-degree[X] = number of X's dependencies (size of deps[X])
|
|
@@ -2,6 +2,7 @@ import type { ResourceManifest } from "@telorun/sdk";
|
|
|
2
2
|
import type { LoadedGraph, LoadedModule } from "./loaded-types.js";
|
|
3
3
|
import type { LoadedFile } from "./loaded-types.js";
|
|
4
4
|
import { isModuleKind } from "./module-kinds.js";
|
|
5
|
+
import type { ZoneModuleDocuments } from "./zone-module-documents.js";
|
|
5
6
|
|
|
6
7
|
/** One parsed `exports.resources` / `exports.kinds` entry. `name` is the exported
|
|
7
8
|
* instance name or kind suffix (the part after the dot, or the whole entry); `alias`
|
|
@@ -380,6 +381,37 @@ function forwardReExports(graph: LoadedGraph, result: ResourceManifest[]): void
|
|
|
380
381
|
stampExportedKinds(imports, declaredKinds);
|
|
381
382
|
}
|
|
382
383
|
|
|
384
|
+
/** Collect every imported library's FULL document set for the zone stage's
|
|
385
|
+
* per-library export derivation (`AnalysisOptions.moduleDocuments`) — the
|
|
386
|
+
* flat list above deliberately drops library internals, and the zone
|
|
387
|
+
* projection needs the internal dispatch chain to derive an export's open
|
|
388
|
+
* requirements. Only libraries that actually export instances are included:
|
|
389
|
+
* a library exporting nothing has no export contract to derive. */
|
|
390
|
+
export function collectZoneModuleDocuments(graph: LoadedGraph): ZoneModuleDocuments[] {
|
|
391
|
+
const out: ZoneModuleDocuments[] = [];
|
|
392
|
+
for (const [source, mod] of graph.modules) {
|
|
393
|
+
if (source === graph.rootSource) continue;
|
|
394
|
+
const libDoc = mod.owner.manifests.find((m) => m && isModuleKind(m.kind)) as
|
|
395
|
+
| (ResourceManifest & { exports?: { resources?: unknown[] } })
|
|
396
|
+
| undefined;
|
|
397
|
+
const moduleName = libDoc?.metadata?.name as string | undefined;
|
|
398
|
+
if (!libDoc || !moduleName) continue;
|
|
399
|
+
const exportedNames: string[] = [];
|
|
400
|
+
for (const entry of libDoc.exports?.resources ?? []) {
|
|
401
|
+
if (typeof entry !== "string") continue;
|
|
402
|
+
exportedNames.push(parseExportEntry(entry).name);
|
|
403
|
+
}
|
|
404
|
+
if (exportedNames.length === 0) continue;
|
|
405
|
+
out.push({
|
|
406
|
+
module: moduleName,
|
|
407
|
+
sourceId: mod.owner.source,
|
|
408
|
+
manifests: collectModuleManifests(mod),
|
|
409
|
+
exportedNames,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
|
|
383
415
|
/** Project a LoadedModule (owner + partials) to a flat ResourceManifest[]
|
|
384
416
|
* with `metadata.module` stamped on non-module docs. The kernel's runtime
|
|
385
417
|
* entry load uses this to convert a `Loader.loadModule` result into the
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type {
|
|
|
11
11
|
ParseError,
|
|
12
12
|
} from "./loaded-types.js";
|
|
13
13
|
export {
|
|
14
|
+
collectZoneModuleDocuments,
|
|
14
15
|
flattenForAnalyzer,
|
|
15
16
|
flattenLoadedModule,
|
|
16
17
|
forwardReExportManifests,
|
|
@@ -73,8 +74,54 @@ export {
|
|
|
73
74
|
RedactionPathError,
|
|
74
75
|
} from "./redaction-path.js";
|
|
75
76
|
export type { RedactionSegment } from "./redaction-path.js";
|
|
77
|
+
export { buildCallGraph, projectToPairs, resourceId } from "./call-graph.js";
|
|
78
|
+
export type {
|
|
79
|
+
BuildCallGraphOptions,
|
|
80
|
+
CallGraph,
|
|
81
|
+
CallGraphEdge,
|
|
82
|
+
CallGraphNode,
|
|
83
|
+
ResourceGraphNode,
|
|
84
|
+
StepGraphNode,
|
|
85
|
+
} from "./call-graph.js";
|
|
76
86
|
export { buildReferenceFieldMap, isRefEntry, isScopeEntry } from "./reference-field-map.js";
|
|
77
87
|
export type { ReferenceFieldMap, RefFieldEntry } from "./reference-field-map.js";
|
|
88
|
+
export {
|
|
89
|
+
hasDeclaredUse,
|
|
90
|
+
isRefSlot,
|
|
91
|
+
isRefUse,
|
|
92
|
+
possibleUses,
|
|
93
|
+
readRefSlot,
|
|
94
|
+
REF_USES,
|
|
95
|
+
refSlotAnnotation,
|
|
96
|
+
rewriteRefSlotKinds,
|
|
97
|
+
transfersControl,
|
|
98
|
+
} from "./ref-slot.js";
|
|
99
|
+
export type { RefSlot, RefUse, RefUseCases } from "./ref-slot.js";
|
|
100
|
+
export {
|
|
101
|
+
hasProvidesZone,
|
|
102
|
+
hasRequiresZone,
|
|
103
|
+
readProvidesZone,
|
|
104
|
+
readRequiresZone,
|
|
105
|
+
rewriteRequiresZoneKind,
|
|
106
|
+
} from "./zone-slot.js";
|
|
107
|
+
export type { ProvidesZoneSlot, RequiresZoneSlot } from "./zone-slot.js";
|
|
108
|
+
export {
|
|
109
|
+
deriveLibraryExportRequirements,
|
|
110
|
+
projectZoneRequirements,
|
|
111
|
+
runZoneAnalysis,
|
|
112
|
+
zoneDocumentsSignature,
|
|
113
|
+
} from "./resolve-zone-requirements.js";
|
|
114
|
+
export type {
|
|
115
|
+
ZoneExportCache,
|
|
116
|
+
ZoneExportCacheEntry,
|
|
117
|
+
ZoneExportRequirements,
|
|
118
|
+
ZoneRequirementSpec,
|
|
119
|
+
} from "./resolve-zone-requirements.js";
|
|
120
|
+
export type { ZoneModuleDocuments } from "./zone-module-documents.js";
|
|
121
|
+
export { validateZoneSlotDeclarations } from "./validate-zone-slots.js";
|
|
122
|
+
export type { ZoneSlotIssue } from "./validate-zone-slots.js";
|
|
123
|
+
export { validateDynamicSelectors, validateRefSlotDeclarations } from "./validate-ref-slots.js";
|
|
124
|
+
export type { RefSlotIssue } from "./validate-ref-slots.js";
|
|
78
125
|
export { visitManifest } from "./manifest-visitor.js";
|
|
79
126
|
export type {
|
|
80
127
|
CelSiteEvent,
|
|
@@ -167,6 +214,10 @@ export {
|
|
|
167
214
|
} from "./artifact-layer-index.js";
|
|
168
215
|
export type { ArtifactLayer } from "./artifact-layer-index.js";
|
|
169
216
|
export { validateModuleArtifact } from "./validate-module-artifact.js";
|
|
217
|
+
// Warnings everywhere, fatal at `telo publish` — descriptive metadata has no
|
|
218
|
+
// runtime failure mode, so it must not stop a manifest running, but it is the
|
|
219
|
+
// module's public face the moment it is published.
|
|
220
|
+
export { PUBLISH_BLOCKING_CODES } from "./validate-module-metadata.js";
|
|
170
221
|
export { withSyntheticPositions } from "./with-synthetic-positions.js";
|
|
171
222
|
export { documentToAst, parseToAst } from "./yaml-ast.js";
|
|
172
223
|
export type { AstDocument, AstMap, AstNode, AstPair, AstScalar, AstSeq } from "./yaml-ast.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigating a manifest value and a definition schema by path — the primitives
|
|
3
|
+
* the call graph and the zone projection both need.
|
|
4
|
+
*
|
|
5
|
+
* They live here rather than in either consumer because they encode *rules*,
|
|
6
|
+
* not conveniences: which object a JSON Pointer anchors at, and when a path
|
|
7
|
+
* refuses to resolve. Two copies of a rule drift, and the drift is invisible —
|
|
8
|
+
* a second `enclosingOf` that returned the resource root where the first
|
|
9
|
+
* returned `undefined` would silently anchor a correlation pointer at the wrong
|
|
10
|
+
* object rather than failing.
|
|
11
|
+
*
|
|
12
|
+
* Browser-safe: no Node built-ins.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The object ENCLOSING a concrete site — its path with the last segment
|
|
17
|
+
* dropped. `routes[2].handler` → the value at `routes[2]`.
|
|
18
|
+
*
|
|
19
|
+
* A slot that IS an array element (`targets[0]`) has no enclosing object: its
|
|
20
|
+
* siblings are other elements, and a pointer must not cross an array boundary —
|
|
21
|
+
* returning the root here would silently do exactly that. The bracket test runs
|
|
22
|
+
* BEFORE the no-dot early return, so a top-level array path refuses too.
|
|
23
|
+
*/
|
|
24
|
+
export function enclosingOf(root: unknown, concretePath: string): unknown {
|
|
25
|
+
const lastDot = concretePath.lastIndexOf(".");
|
|
26
|
+
const lastSegment = concretePath.slice(lastDot + 1);
|
|
27
|
+
if (lastSegment.includes("[")) return undefined;
|
|
28
|
+
if (lastDot < 0) return root;
|
|
29
|
+
return navigateConcrete(root, concretePath.slice(0, lastDot));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Navigate a concrete dotted path (`routes[2].handler`, `content.a/b.encoder`). */
|
|
33
|
+
export function navigateConcrete(root: unknown, path: string): unknown {
|
|
34
|
+
let current: unknown = root;
|
|
35
|
+
for (const rawSegment of path.split(".")) {
|
|
36
|
+
if (current == null || typeof current !== "object") return undefined;
|
|
37
|
+
let segment = rawSegment;
|
|
38
|
+
const indices: number[] = [];
|
|
39
|
+
const bracket = segment.indexOf("[");
|
|
40
|
+
if (bracket >= 0) {
|
|
41
|
+
for (const m of segment.slice(bracket).matchAll(/\[(\d+)\]/g)) {
|
|
42
|
+
indices.push(Number(m[1]));
|
|
43
|
+
}
|
|
44
|
+
segment = segment.slice(0, bracket);
|
|
45
|
+
}
|
|
46
|
+
if (segment) current = (current as Record<string, unknown>)[segment];
|
|
47
|
+
for (const index of indices) {
|
|
48
|
+
if (!Array.isArray(current)) return undefined;
|
|
49
|
+
current = current[index];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return current;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Resolve a local `#/$defs/<name>` ref against the root schema; any other
|
|
56
|
+
* schema (including one with no `$ref`) is returned unchanged. */
|
|
57
|
+
export function resolveLocalRef(
|
|
58
|
+
schema: Record<string, any> | undefined,
|
|
59
|
+
root: Record<string, any>,
|
|
60
|
+
): Record<string, any> | undefined {
|
|
61
|
+
if (!schema) return undefined;
|
|
62
|
+
const ref = schema.$ref;
|
|
63
|
+
if (typeof ref === "string" && ref.startsWith("#/$defs/")) {
|
|
64
|
+
const resolved = root.$defs?.[ref.slice("#/$defs/".length)];
|
|
65
|
+
if (resolved && typeof resolved === "object") return resolved as Record<string, any>;
|
|
66
|
+
}
|
|
67
|
+
return schema;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Property schemas of a possibly variant-bearing object schema — `properties`
|
|
71
|
+
* plus every `oneOf` / `anyOf` / `allOf` branch's. */
|
|
72
|
+
export function propertySchemas(
|
|
73
|
+
schema: Record<string, any>,
|
|
74
|
+
): Array<[string, Record<string, any>]> {
|
|
75
|
+
const out: Array<[string, Record<string, any>]> = [];
|
|
76
|
+
if (schema.properties && typeof schema.properties === "object") {
|
|
77
|
+
for (const [k, v] of Object.entries(schema.properties)) out.push([k, v as Record<string, any>]);
|
|
78
|
+
}
|
|
79
|
+
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
80
|
+
const variants = schema[key];
|
|
81
|
+
if (!Array.isArray(variants)) continue;
|
|
82
|
+
for (const variant of variants) {
|
|
83
|
+
if (variant?.properties && typeof variant.properties === "object") {
|
|
84
|
+
for (const [k, v] of Object.entries(variant.properties)) {
|
|
85
|
+
out.push([k, v as Record<string, any>]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
package/src/manifest-visitor.ts
CHANGED
|
@@ -146,7 +146,7 @@ export interface VisitOptions {
|
|
|
146
146
|
|
|
147
147
|
/** Synthetic entry for a value-tree-discovered ref — these carry no declared
|
|
148
148
|
* x-telo-ref constraint. */
|
|
149
|
-
const NESTED_REF_ENTRY: RefFieldEntry = { refs: [], isArray: false };
|
|
149
|
+
const NESTED_REF_ENTRY: RefFieldEntry = { refs: [], uses: [], isArray: false };
|
|
150
150
|
|
|
151
151
|
/** Scans a value tree for ref-shaped values, emitting each with its concrete
|
|
152
152
|
* path. Recognizes `!ref <name>` sentinels and named `{kind, name}` reference
|
package/src/ref-slot.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single reader of the `x-telo-ref` annotation.
|
|
3
|
+
*
|
|
4
|
+
* A reference slot is *recognised* here and nowhere else. Four surfaces ask what
|
|
5
|
+
* a slot accepts — the analyzer's reference checks, the kernel's Phase-5
|
|
6
|
+
* injection, the GUI editor's reference picker, and `ide-support`'s completions
|
|
7
|
+
* / hover / go-to-definition — and before this module each recognised the
|
|
8
|
+
* annotation by pattern-matching its shape (`typeof node["x-telo-ref"] ===
|
|
9
|
+
* "string"`, plus a hand-rolled `anyOf` peel in three of them, and no peel at all
|
|
10
|
+
* in hover). That is why the shape could not change without silently turning
|
|
11
|
+
* every ref slot in the GUI into a free-text field.
|
|
12
|
+
*
|
|
13
|
+
* Browser-safe by construction: no Node built-ins, so the editor and
|
|
14
|
+
* `ide-support` import it directly and the kernel re-imports it rather than
|
|
15
|
+
* carrying a second reader — the split already used for the invocation-contract
|
|
16
|
+
* resolver, the eval-path matcher and the redaction path parser.
|
|
17
|
+
*
|
|
18
|
+
* Two annotation shapes are accepted during the migration:
|
|
19
|
+
*
|
|
20
|
+
* x-telo-ref: Telo.Invocable # bare string, no declared use
|
|
21
|
+
* x-telo-ref: # structured
|
|
22
|
+
* kind: [Telo.Invocable, Telo.Runnable]
|
|
23
|
+
* use: call
|
|
24
|
+
* inputs: /inputs
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** How control reaches a slot's target, relative to the declaring resource's own
|
|
28
|
+
* invocation. The one primitive fact every consumer derives its own semantics
|
|
29
|
+
* from — see `plans/typed-reference-graph.md`. */
|
|
30
|
+
export type RefUse =
|
|
31
|
+
/** Names a shape; no runtime instance exists. No edge of any kind. */
|
|
32
|
+
| "schema"
|
|
33
|
+
/** Held and read; control never transfers. Init-order edge only. */
|
|
34
|
+
| "dependency"
|
|
35
|
+
/** Control transfers during my invocation and returns to me. */
|
|
36
|
+
| "call"
|
|
37
|
+
/** Control transfers during my invocation through the kernel's detach
|
|
38
|
+
* primitive; I do not await it. */
|
|
39
|
+
| "detached"
|
|
40
|
+
/** I register the target; control reaches it after my invocation, driven by a
|
|
41
|
+
* request or a timer, and the runtime guarantees a fresh ambient context. */
|
|
42
|
+
| "trigger.inbound"
|
|
43
|
+
/** I register the target; control reaches it when someone drains a value I
|
|
44
|
+
* returned, so no guarantee holds either way. */
|
|
45
|
+
| "trigger.consumer";
|
|
46
|
+
|
|
47
|
+
export const REF_USES: readonly RefUse[] = [
|
|
48
|
+
"schema",
|
|
49
|
+
"dependency",
|
|
50
|
+
"call",
|
|
51
|
+
"detached",
|
|
52
|
+
"trigger.inbound",
|
|
53
|
+
"trigger.consumer",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export function isRefUse(value: unknown): value is RefUse {
|
|
57
|
+
return typeof value === "string" && (REF_USES as readonly string[]).includes(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True for a use that transfers control to the target at some point. Everything
|
|
61
|
+
* but `schema` and `dependency`. */
|
|
62
|
+
export function transfersControl(use: RefUse): boolean {
|
|
63
|
+
return use !== "schema" && use !== "dependency";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A slot whose use is selected by a sibling config field. The selector must be
|
|
67
|
+
* statically resolvable — a literal or a schema default — because a call graph
|
|
68
|
+
* known only at runtime is not statically analyzable. */
|
|
69
|
+
export interface RefUseCases {
|
|
70
|
+
/** JSON Pointer relative to the object ENCLOSING the annotated slot: the
|
|
71
|
+
* resource root for a resource-level slot, the array item for a slot inside
|
|
72
|
+
* one. No pointer can reach across an array boundary. */
|
|
73
|
+
by: string;
|
|
74
|
+
/** Selector value (stringified as written in YAML) → the uses that hold. */
|
|
75
|
+
cases: Record<string, RefUse[]>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A reference slot's declaration, normalized from every accepted shape. */
|
|
79
|
+
export interface RefSlot {
|
|
80
|
+
/** Accepted kinds, unioned across a `kind:` list and across `anyOf` branches.
|
|
81
|
+
* Canonical `<module>.<Kind>` keys once `resolveSchemaRefKinds` has run;
|
|
82
|
+
* alias-qualified as authored before that, or the legacy `<ns>/<mod>#<Kind>`
|
|
83
|
+
* identity form for an already-published module. */
|
|
84
|
+
kinds: string[];
|
|
85
|
+
/** The relations the declaring resource has with the target. A set, because a
|
|
86
|
+
* slot may dispatch its target more than one way within a single invocation
|
|
87
|
+
* (`Cache.View` calls inline on a miss and detached on a background
|
|
88
|
+
* revalidation). Empty when the slot declares no use — the bare-string form,
|
|
89
|
+
* or a slot that defers to {@link RefSlot.useCases}. */
|
|
90
|
+
uses: RefUse[];
|
|
91
|
+
/** Present when the use is chosen by configuration rather than fixed. */
|
|
92
|
+
useCases?: RefUseCases;
|
|
93
|
+
/** JSON Pointer (same anchoring as {@link RefUseCases.by}) naming the field
|
|
94
|
+
* that carries this call's arguments. Replaces
|
|
95
|
+
* `x-telo-topology-role: inputs`. */
|
|
96
|
+
inputs?: string;
|
|
97
|
+
/** `x-telo-inline: true` on the slot or any `anyOf` branch — accepts an inline
|
|
98
|
+
* `{kind, ...config}` definition, not only a `!ref`. */
|
|
99
|
+
inline: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Every use a slot can take, flattening a case map. The conservative reading
|
|
103
|
+
* for a consumer that does not resolve the selector. */
|
|
104
|
+
export function possibleUses(slot: RefSlot): RefUse[] {
|
|
105
|
+
if (!slot.useCases) return slot.uses;
|
|
106
|
+
const out = new Set<RefUse>(slot.uses);
|
|
107
|
+
for (const uses of Object.values(slot.useCases.cases)) {
|
|
108
|
+
for (const use of uses) out.add(use);
|
|
109
|
+
}
|
|
110
|
+
return [...out];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** True when the slot states what the declaring resource does with the target.
|
|
114
|
+
* False only for the legacy bare-string form. */
|
|
115
|
+
export function hasDeclaredUse(slot: RefSlot): boolean {
|
|
116
|
+
return slot.uses.length > 0 || slot.useCases !== undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeUses(raw: unknown): RefUse[] {
|
|
120
|
+
if (isRefUse(raw)) return [raw];
|
|
121
|
+
if (Array.isArray(raw)) return raw.filter(isRefUse);
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readUseCases(raw: unknown): RefUseCases | undefined {
|
|
126
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
127
|
+
const obj = raw as Record<string, unknown>;
|
|
128
|
+
if (typeof obj.by !== "string") return undefined;
|
|
129
|
+
const rawCases = obj.cases;
|
|
130
|
+
if (!rawCases || typeof rawCases !== "object" || Array.isArray(rawCases)) return undefined;
|
|
131
|
+
const cases: Record<string, RefUse[]> = {};
|
|
132
|
+
for (const [key, value] of Object.entries(rawCases as Record<string, unknown>)) {
|
|
133
|
+
cases[key] = normalizeUses(value);
|
|
134
|
+
}
|
|
135
|
+
return { by: obj.by, cases };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Kind names declared by one annotation value, in either shape. */
|
|
139
|
+
function readKinds(annotation: unknown): string[] {
|
|
140
|
+
if (typeof annotation === "string") return annotation ? [annotation] : [];
|
|
141
|
+
if (!annotation || typeof annotation !== "object" || Array.isArray(annotation)) return [];
|
|
142
|
+
const kind = (annotation as Record<string, unknown>).kind;
|
|
143
|
+
if (typeof kind === "string") return kind ? [kind] : [];
|
|
144
|
+
if (Array.isArray(kind)) return kind.filter((k): k is string => typeof k === "string" && !!k);
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The nodes carrying an `x-telo-ref` for this slot: the node itself plus any
|
|
149
|
+
* `anyOf` / `oneOf` branch. The multi-kind shape the `kind:` list replaces.
|
|
150
|
+
*
|
|
151
|
+
* `oneOf` is peeled because the editor's picker always did and the analyzer
|
|
152
|
+
* never did — unifying on the wider reading is the point of a single accessor.
|
|
153
|
+
* The no-op claim is scoped to THIS repo: no schema here puts an `x-telo-ref`
|
|
154
|
+
* under `oneOf`, so for the standard library this cannot add a field-map entry
|
|
155
|
+
* (and hence a Phase-5 injection site) that did not exist before. An
|
|
156
|
+
* already-published third-party module that does use that spelling gains an
|
|
157
|
+
* injection site on upgrade — behavior its author most plausibly intended (the
|
|
158
|
+
* editor already treated the slot as a reference), but a change of behavior
|
|
159
|
+
* nonetheless, and part of this release's contract. */
|
|
160
|
+
function annotationNodes(node: Record<string, any> | undefined): Record<string, any>[] {
|
|
161
|
+
if (!node || typeof node !== "object") return [];
|
|
162
|
+
const out: Record<string, any>[] = [];
|
|
163
|
+
if (node["x-telo-ref"] !== undefined) out.push(node);
|
|
164
|
+
for (const key of ["anyOf", "oneOf"] as const) {
|
|
165
|
+
const branches = node[key];
|
|
166
|
+
if (!Array.isArray(branches)) continue;
|
|
167
|
+
for (const branch of branches) {
|
|
168
|
+
if (branch && typeof branch === "object" && branch["x-telo-ref"] !== undefined) {
|
|
169
|
+
out.push(branch);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Reads a schema node as a reference slot, or `undefined` when it declares none.
|
|
178
|
+
*
|
|
179
|
+
* Unions the accepted kinds across the `kind:` list and across `anyOf` branches,
|
|
180
|
+
* which is the model the analyzer has always had internally — `RefFieldEntry`
|
|
181
|
+
* has carried a flat `refs: string[]` since before the structured form existed.
|
|
182
|
+
* A `use` declared on more than one branch is taken once; a *disagreement*
|
|
183
|
+
* between branches is left visible in the returned set rather than silently
|
|
184
|
+
* resolved here, so `validate-ref-slots.ts` reports it against the authored
|
|
185
|
+
* node (`X_TELO_REF_USE_CONFLICT`) — the same split that keeps this reader
|
|
186
|
+
* lenient about unrecognized `use` tokens while that pass rejects them.
|
|
187
|
+
*/
|
|
188
|
+
export function readRefSlot(node: Record<string, any> | undefined): RefSlot | undefined {
|
|
189
|
+
const nodes = annotationNodes(node);
|
|
190
|
+
if (nodes.length === 0) return undefined;
|
|
191
|
+
|
|
192
|
+
const kinds: string[] = [];
|
|
193
|
+
const uses = new Set<RefUse>();
|
|
194
|
+
let useCases: RefUseCases | undefined;
|
|
195
|
+
let inputs: string | undefined;
|
|
196
|
+
|
|
197
|
+
for (const carrier of nodes) {
|
|
198
|
+
const annotation = carrier["x-telo-ref"];
|
|
199
|
+
for (const kind of readKinds(annotation)) {
|
|
200
|
+
if (!kinds.includes(kind)) kinds.push(kind);
|
|
201
|
+
}
|
|
202
|
+
if (!annotation || typeof annotation !== "object" || Array.isArray(annotation)) continue;
|
|
203
|
+
const obj = annotation as Record<string, unknown>;
|
|
204
|
+
for (const use of normalizeUses(obj.use)) uses.add(use);
|
|
205
|
+
useCases ??= readUseCases(obj.use);
|
|
206
|
+
if (typeof obj.inputs === "string") inputs ??= obj.inputs;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const slot: RefSlot = {
|
|
210
|
+
kinds,
|
|
211
|
+
uses: [...uses],
|
|
212
|
+
inline: node?.["x-telo-inline"] === true || nodes.some((n) => n["x-telo-inline"] === true),
|
|
213
|
+
};
|
|
214
|
+
if (useCases) slot.useCases = useCases;
|
|
215
|
+
if (inputs !== undefined) slot.inputs = inputs;
|
|
216
|
+
return slot;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** True when the node declares a reference slot in any accepted shape. The
|
|
220
|
+
* recognition test every surface used to spell for itself. */
|
|
221
|
+
export function isRefSlot(node: Record<string, any> | undefined): boolean {
|
|
222
|
+
return annotationNodes(node).length > 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Re-emits a slot as a canonical structured annotation value.
|
|
227
|
+
*
|
|
228
|
+
* For a consumer that has to *synthesize* a schema node and keep its reference
|
|
229
|
+
* slot intact — `ide-support` merges `anyOf` branches into one node for
|
|
230
|
+
* completion, and the merged node would otherwise carry no constraint at all,
|
|
231
|
+
* because the branches that held it are gone.
|
|
232
|
+
*/
|
|
233
|
+
export function refSlotAnnotation(slot: RefSlot): Record<string, unknown> {
|
|
234
|
+
const annotation: Record<string, unknown> = {
|
|
235
|
+
kind: slot.kinds.length === 1 ? slot.kinds[0] : slot.kinds,
|
|
236
|
+
};
|
|
237
|
+
if (slot.useCases) annotation.use = slot.useCases;
|
|
238
|
+
else if (slot.uses.length === 1) annotation.use = slot.uses[0];
|
|
239
|
+
else if (slot.uses.length > 1) annotation.use = slot.uses;
|
|
240
|
+
if (slot.inputs !== undefined) annotation.inputs = slot.inputs;
|
|
241
|
+
return annotation;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Rewrites every kind name an annotation declares, in place, in whichever shape
|
|
246
|
+
* it is written. `map` returns the replacement, or `undefined` to leave the name
|
|
247
|
+
* untouched (which is what keeps `resolveSchemaRefKinds` idempotent and lets it
|
|
248
|
+
* report an unresolvable constraint while quoting what the author wrote).
|
|
249
|
+
*
|
|
250
|
+
* Lives here rather than in the caller so that adding an annotation shape is a
|
|
251
|
+
* one-file change on the write side as well as the read side.
|
|
252
|
+
*/
|
|
253
|
+
export function rewriteRefSlotKinds(
|
|
254
|
+
annotationHolder: Record<string, any>,
|
|
255
|
+
map: (kind: string) => string | undefined,
|
|
256
|
+
): void {
|
|
257
|
+
const annotation = annotationHolder["x-telo-ref"];
|
|
258
|
+
if (typeof annotation === "string") {
|
|
259
|
+
const next = map(annotation);
|
|
260
|
+
if (next !== undefined) annotationHolder["x-telo-ref"] = next;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (!annotation || typeof annotation !== "object" || Array.isArray(annotation)) return;
|
|
264
|
+
const obj = annotation as Record<string, unknown>;
|
|
265
|
+
if (typeof obj.kind === "string") {
|
|
266
|
+
const next = map(obj.kind);
|
|
267
|
+
if (next !== undefined) obj.kind = next;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (Array.isArray(obj.kind)) {
|
|
271
|
+
obj.kind = obj.kind.map((k) => (typeof k === "string" ? (map(k) ?? k) : k));
|
|
272
|
+
}
|
|
273
|
+
}
|