@telorun/analyzer 0.8.1 → 0.10.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 +5 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +7 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +5 -5
- package/dist/definition-registry.d.ts +13 -1
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +58 -2
- package/dist/dependency-graph.d.ts +1 -1
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +8 -2
- package/dist/flatten-for-analyzer.d.ts +30 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -0
- package/dist/flatten-for-analyzer.js +119 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/loaded-types.d.ts +81 -0
- package/dist/loaded-types.d.ts.map +1 -0
- package/dist/loaded-types.js +1 -0
- package/dist/manifest-loader.d.ts +30 -9
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +197 -417
- package/dist/normalize-inline-resources.d.ts +1 -1
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +7 -2
- package/dist/parse-loaded-file.d.ts +12 -0
- package/dist/parse-loaded-file.d.ts.map +1 -0
- package/dist/parse-loaded-file.js +50 -0
- package/dist/position-metadata.d.ts +27 -0
- package/dist/position-metadata.d.ts.map +1 -0
- package/dist/position-metadata.js +88 -0
- package/dist/reference-field-map.d.ts +5 -0
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +9 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +68 -1
- package/package.json +3 -3
- package/src/analysis-registry.ts +11 -2
- package/src/analyzer.ts +17 -5
- package/src/definition-registry.ts +76 -3
- package/src/dependency-graph.ts +9 -1
- package/src/flatten-for-analyzer.ts +134 -0
- package/src/index.ts +18 -0
- package/src/loaded-types.ts +86 -0
- package/src/manifest-loader.ts +230 -459
- package/src/normalize-inline-resources.ts +8 -1
- package/src/parse-loaded-file.ts +70 -0
- package/src/position-metadata.ts +106 -0
- package/src/reference-field-map.ts +13 -0
- package/src/types.ts +6 -0
- package/src/validate-references.ts +74 -1
|
@@ -18,5 +18,5 @@ import type { AliasResolver } from "./alias-resolver.js";
|
|
|
18
18
|
* Returns a new array containing the original manifests (mutated in-place) plus all
|
|
19
19
|
* extracted manifests. The original array is not mutated.
|
|
20
20
|
*/
|
|
21
|
-
export declare function normalizeInlineResources(resources: ResourceManifest[], registry: DefinitionRegistry, aliases?: AliasResolver): ResourceManifest[];
|
|
21
|
+
export declare function normalizeInlineResources(resources: ResourceManifest[], registry: DefinitionRegistry, aliases?: AliasResolver, aliasesByModule?: Map<string, AliasResolver>): ResourceManifest[];
|
|
22
22
|
//# sourceMappingURL=normalize-inline-resources.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalize-inline-resources.d.ts","sourceRoot":"","sources":["../src/normalize-inline-resources.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAczD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"normalize-inline-resources.d.ts","sourceRoot":"","sources":["../src/normalize-inline-resources.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAczD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC3C,gBAAgB,EAAE,CAwDpB"}
|
|
@@ -26,14 +26,19 @@ function sanitizeName(raw) {
|
|
|
26
26
|
* Returns a new array containing the original manifests (mutated in-place) plus all
|
|
27
27
|
* extracted manifests. The original array is not mutated.
|
|
28
28
|
*/
|
|
29
|
-
export function normalizeInlineResources(resources, registry, aliases) {
|
|
29
|
+
export function normalizeInlineResources(resources, registry, aliases, aliasesByModule) {
|
|
30
30
|
const result = [...resources];
|
|
31
31
|
// Queue: all non-system resources with a name. Extracted resources are appended.
|
|
32
32
|
const queue = resources.filter((r) => typeof r.metadata?.name === "string" && !!r.kind && !SYSTEM_KINDS.has(r.kind));
|
|
33
33
|
let i = 0;
|
|
34
34
|
while (i < queue.length) {
|
|
35
35
|
const resource = queue[i++];
|
|
36
|
-
|
|
36
|
+
// When aliasesByModule is available, use the expanded map so inline refs
|
|
37
|
+
// hidden behind an x-telo-schema-from indirection (e.g. an encoder inside
|
|
38
|
+
// an HttpDispatch.Outcomes/$defs/Returns sub-schema) reach extraction.
|
|
39
|
+
const fieldMap = aliases && aliasesByModule
|
|
40
|
+
? registry.expandedFieldMapForResource(resource, aliases, aliasesByModule)
|
|
41
|
+
: registry.getFieldMapForKind(resource.kind, aliases);
|
|
37
42
|
if (!fieldMap)
|
|
38
43
|
continue;
|
|
39
44
|
const parentName = resource.metadata.name;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Environment } from "@marcbachmann/cel-js";
|
|
2
|
+
import type { LoadedFile } from "./loaded-types.js";
|
|
3
|
+
export interface ParseOptions {
|
|
4
|
+
/** When true, runs `precompileDoc` per document and stamps compiled CEL
|
|
5
|
+
* on the manifests — same flag `LoadOptions.compile` carries today. */
|
|
6
|
+
compile?: boolean;
|
|
7
|
+
/** CEL environment for precompile. Defaults to `buildCelEnvironment()`. */
|
|
8
|
+
celEnv?: Environment;
|
|
9
|
+
}
|
|
10
|
+
/** Pure: text in, structured load result out. No I/O, no caches. */
|
|
11
|
+
export declare function parseLoadedFile(source: string, requestedUrl: string, text: string, options?: ParseOptions): LoadedFile;
|
|
12
|
+
//# sourceMappingURL=parse-loaded-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-loaded-file.d.ts","sourceRoot":"","sources":["../src/parse-loaded-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAKxD,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,mBAAmB,CAAC;AAIhE,MAAM,WAAW,YAAY;IAC3B;4EACwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,oEAAoE;AACpE,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,YAAY,GACrB,UAAU,CA8CZ"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defaultCustomTags } from "@telorun/templating";
|
|
2
|
+
import { parseAllDocuments } from "yaml";
|
|
3
|
+
import { buildCelEnvironment } from "./cel-environment.js";
|
|
4
|
+
import { buildDocumentPositions } from "./position-metadata.js";
|
|
5
|
+
import { precompileDoc } from "./precompile.js";
|
|
6
|
+
/** Pure: text in, structured load result out. No I/O, no caches. */
|
|
7
|
+
export function parseLoadedFile(source, requestedUrl, text, options) {
|
|
8
|
+
const documents = parseAllDocuments(text, { customTags: defaultCustomTags() });
|
|
9
|
+
const positions = buildDocumentPositions(text, documents);
|
|
10
|
+
const parseErrors = [];
|
|
11
|
+
documents.forEach((doc, documentIndex) => {
|
|
12
|
+
for (const err of doc.errors) {
|
|
13
|
+
parseErrors.push({
|
|
14
|
+
documentIndex,
|
|
15
|
+
message: err.message,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const manifests = [];
|
|
20
|
+
let env;
|
|
21
|
+
for (const doc of documents) {
|
|
22
|
+
const raw = doc.toJSON();
|
|
23
|
+
if (raw === null || raw === undefined) {
|
|
24
|
+
manifests.push(null);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (options?.compile) {
|
|
28
|
+
env ??= options.celEnv ?? buildCelEnvironment();
|
|
29
|
+
try {
|
|
30
|
+
const compiled = precompileDoc(raw, env);
|
|
31
|
+
manifests.push(compiled);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new Error(`Failed to compile manifest in ${source}: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
manifests.push(raw);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
source,
|
|
43
|
+
requestedUrl,
|
|
44
|
+
text,
|
|
45
|
+
documents,
|
|
46
|
+
manifests,
|
|
47
|
+
positions,
|
|
48
|
+
parseErrors,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Document } from "yaml";
|
|
2
|
+
import type { PositionIndex } from "./types.js";
|
|
3
|
+
/** Single source of truth for "given the source text of a multi-document YAML
|
|
4
|
+
* file, where does each document start, and what is the byte→(line,char)
|
|
5
|
+
* table for the file." Both the analyzer's `Loader` and editor frontends
|
|
6
|
+
* feed the same parsed `yaml.Document[]` through this so diagnostics
|
|
7
|
+
* resolved against `positionIndex` / `sourceLine` line up identically
|
|
8
|
+
* across hosts. */
|
|
9
|
+
/** Per-document position metadata used by `normalizeDiagnostic`'s fallback chain. */
|
|
10
|
+
export interface DocumentPosition {
|
|
11
|
+
sourceLine: number;
|
|
12
|
+
positionIndex: PositionIndex;
|
|
13
|
+
}
|
|
14
|
+
/** Builds DocumentPosition entries aligned to `parsedDocs[i]`. */
|
|
15
|
+
export declare function buildDocumentPositions(text: string, parsedDocs: Document[]): DocumentPosition[];
|
|
16
|
+
/** Line numbers (0-indexed) where each YAML document in a multi-doc file
|
|
17
|
+
* starts. The first document is always at line 0; subsequent entries point
|
|
18
|
+
* to the line after each `---` directive. */
|
|
19
|
+
export declare function documentLineOffsets(text: string): number[];
|
|
20
|
+
/** Byte-offset → start-of-line lookup table. Index `i` is the byte offset of
|
|
21
|
+
* the first character on line `i`. Used with `offsetToPosition` to turn a
|
|
22
|
+
* yaml-AST node range into Range coordinates. */
|
|
23
|
+
export declare function buildLineOffsets(text: string): number[];
|
|
24
|
+
/** Walks the YAML AST and records source ranges for every field value, keyed
|
|
25
|
+
* by dotted path (e.g. "kind", "config.handler", "config.routes[0].path"). */
|
|
26
|
+
export declare function buildPositionIndex(doc: Document, lineOffsets: number[]): PositionIndex;
|
|
27
|
+
//# sourceMappingURL=position-metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"position-metadata.d.ts","sourceRoot":"","sources":["../src/position-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,QAAQ,EAAE,MAAM,MAAM,CAAC;AACrE,OAAO,KAAK,EAAY,aAAa,EAAE,MAAM,YAAY,CAAC;AAE1D;;;;;oBAKoB;AAEpB,qFAAqF;AACrF,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,kEAAkE;AAClE,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,QAAQ,EAAE,GACrB,gBAAgB,EAAE,CAOpB;AAED;;8CAE8C;AAC9C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ1D;AAED;;kDAEkD;AAClD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAMvD;AAaD;+EAC+E;AAC/E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,aAAa,CAuCtF"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { isMap, isPair, isScalar, isSeq } from "yaml";
|
|
2
|
+
/** Builds DocumentPosition entries aligned to `parsedDocs[i]`. */
|
|
3
|
+
export function buildDocumentPositions(text, parsedDocs) {
|
|
4
|
+
const docOffsets = documentLineOffsets(text);
|
|
5
|
+
const lineOffsets = buildLineOffsets(text);
|
|
6
|
+
return parsedDocs.map((doc, i) => ({
|
|
7
|
+
sourceLine: docOffsets[i] ?? 0,
|
|
8
|
+
positionIndex: buildPositionIndex(doc, lineOffsets),
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
/** Line numbers (0-indexed) where each YAML document in a multi-doc file
|
|
12
|
+
* starts. The first document is always at line 0; subsequent entries point
|
|
13
|
+
* to the line after each `---` directive. */
|
|
14
|
+
export function documentLineOffsets(text) {
|
|
15
|
+
const offsets = [0];
|
|
16
|
+
const lines = text.split("\n");
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
const t = lines[i].trimEnd();
|
|
19
|
+
if (t === "---" || t.startsWith("--- "))
|
|
20
|
+
offsets.push(i + 1);
|
|
21
|
+
}
|
|
22
|
+
return offsets;
|
|
23
|
+
}
|
|
24
|
+
/** Byte-offset → start-of-line lookup table. Index `i` is the byte offset of
|
|
25
|
+
* the first character on line `i`. Used with `offsetToPosition` to turn a
|
|
26
|
+
* yaml-AST node range into Range coordinates. */
|
|
27
|
+
export function buildLineOffsets(text) {
|
|
28
|
+
const offsets = [0];
|
|
29
|
+
for (let i = 0; i < text.length; i++) {
|
|
30
|
+
if (text[i] === "\n")
|
|
31
|
+
offsets.push(i + 1);
|
|
32
|
+
}
|
|
33
|
+
return offsets;
|
|
34
|
+
}
|
|
35
|
+
function offsetToPosition(offset, lineOffsets) {
|
|
36
|
+
let lo = 0;
|
|
37
|
+
let hi = lineOffsets.length - 1;
|
|
38
|
+
while (lo < hi) {
|
|
39
|
+
const mid = (lo + hi + 1) >> 1;
|
|
40
|
+
if (lineOffsets[mid] <= offset)
|
|
41
|
+
lo = mid;
|
|
42
|
+
else
|
|
43
|
+
hi = mid - 1;
|
|
44
|
+
}
|
|
45
|
+
return { line: lo, character: offset - lineOffsets[lo] };
|
|
46
|
+
}
|
|
47
|
+
/** Walks the YAML AST and records source ranges for every field value, keyed
|
|
48
|
+
* by dotted path (e.g. "kind", "config.handler", "config.routes[0].path"). */
|
|
49
|
+
export function buildPositionIndex(doc, lineOffsets) {
|
|
50
|
+
const index = new Map();
|
|
51
|
+
function recordNode(node, path) {
|
|
52
|
+
if (!node || !node.range)
|
|
53
|
+
return;
|
|
54
|
+
const [start, , end] = node.range;
|
|
55
|
+
index.set(path, {
|
|
56
|
+
start: offsetToPosition(start, lineOffsets),
|
|
57
|
+
end: offsetToPosition(end, lineOffsets),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function walk(node, path) {
|
|
61
|
+
if (isMap(node)) {
|
|
62
|
+
for (const pair of node.items) {
|
|
63
|
+
if (!isPair(pair))
|
|
64
|
+
continue;
|
|
65
|
+
const key = isScalar(pair.key) ? String(pair.key.value) : null;
|
|
66
|
+
if (key == null)
|
|
67
|
+
continue;
|
|
68
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
69
|
+
if (pair.value != null) {
|
|
70
|
+
recordNode(pair.value, childPath);
|
|
71
|
+
walk(pair.value, childPath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (isSeq(node)) {
|
|
76
|
+
for (let i = 0; i < node.items.length; i++) {
|
|
77
|
+
const item = node.items[i];
|
|
78
|
+
const childPath = `${path}[${i}]`;
|
|
79
|
+
recordNode(item, childPath);
|
|
80
|
+
walk(item, childPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (doc.contents) {
|
|
85
|
+
walk(doc.contents, "");
|
|
86
|
+
}
|
|
87
|
+
return index;
|
|
88
|
+
}
|
|
@@ -61,4 +61,9 @@ export declare function resolveFieldValues(obj: unknown, path: string): unknown[
|
|
|
61
61
|
* - A node with `properties` → recurse into each property
|
|
62
62
|
*/
|
|
63
63
|
export declare function buildReferenceFieldMap(schema: Record<string, any>): ReferenceFieldMap;
|
|
64
|
+
/** Traverses an arbitrary JSON Schema starting at the given path prefix. Used to
|
|
65
|
+
* expand x-telo-schema-from sub-schemas into nested ref/scope entries so Phase 2
|
|
66
|
+
* inline normalization and Phase 5 injection see slots that the local field map
|
|
67
|
+
* hid behind the schema-from indirection. */
|
|
68
|
+
export declare function buildFieldMapAtPath(schema: Record<string, any>, pathPrefix: string): ReferenceFieldMap;
|
|
64
69
|
//# sourceMappingURL=reference-field-map.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reference-field-map.d.ts","sourceRoot":"","sources":["../src/reference-field-map.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B;sDACkD;IAClD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,0FAA0F;IAC1F,OAAO,EAAE,OAAO,CAAC;IACjB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B;2CACuC;IACvC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED;8CAC8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC;;qFAEiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAEnF;0FAC0F;AAC1F,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAE3D,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,aAAa,CAEvE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,eAAe,CAE3E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,oBAAoB,CAErF;AAED,oGAAoG;AACpG,eAAO,MAAM,cAAc,aAAwC,CAAC;AAEpE;;;;;;;;;2EAS2E;AAC3E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAItE;AAED;;;;;kEAKkE;AAClE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CA6BxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,iBAAiB,CAQrF"}
|
|
1
|
+
{"version":3,"file":"reference-field-map.d.ts","sourceRoot":"","sources":["../src/reference-field-map.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B;sDACkD;IAClD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,0FAA0F;IAC1F,OAAO,EAAE,OAAO,CAAC;IACjB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B;2CACuC;IACvC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED;8CAC8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC;;qFAEiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAEnF;0FAC0F;AAC1F,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAE3D,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,aAAa,CAEvE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,eAAe,CAE3E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,oBAAoB,CAErF;AAED,oGAAoG;AACpG,eAAO,MAAM,cAAc,aAAwC,CAAC;AAEpE;;;;;;;;;2EAS2E;AAC3E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAItE;AAED;;;;;kEAKkE;AAClE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CA6BxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,iBAAiB,CAQrF;AAiBD;;;8CAG8C;AAC9C,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,UAAU,EAAE,MAAM,GACjB,iBAAiB,CAInB"}
|
|
@@ -101,6 +101,15 @@ function collectRefs(node) {
|
|
|
101
101
|
}
|
|
102
102
|
return refs;
|
|
103
103
|
}
|
|
104
|
+
/** Traverses an arbitrary JSON Schema starting at the given path prefix. Used to
|
|
105
|
+
* expand x-telo-schema-from sub-schemas into nested ref/scope entries so Phase 2
|
|
106
|
+
* inline normalization and Phase 5 injection see slots that the local field map
|
|
107
|
+
* hid behind the schema-from indirection. */
|
|
108
|
+
export function buildFieldMapAtPath(schema, pathPrefix) {
|
|
109
|
+
const map = new Map();
|
|
110
|
+
traverseNode(schema, pathPrefix, map);
|
|
111
|
+
return map;
|
|
112
|
+
}
|
|
104
113
|
function traverseNode(node, path, map) {
|
|
105
114
|
// Scope slot — record and stop; do not recurse into scope contents
|
|
106
115
|
if ("x-telo-scope" in node) {
|
package/dist/types.d.ts
CHANGED
|
@@ -83,5 +83,11 @@ export interface AnalysisOptions {
|
|
|
83
83
|
export interface AnalysisContext {
|
|
84
84
|
aliases?: import("./alias-resolver.js").AliasResolver;
|
|
85
85
|
definitions?: import("./definition-registry.js").DefinitionRegistry;
|
|
86
|
+
/** Per-library alias resolvers keyed by the library's module name. Populated by
|
|
87
|
+
* the analyzer when imports are forwarded from inside imported libraries.
|
|
88
|
+
* Validators that resolve schema-side annotations (e.g. x-telo-schema-from
|
|
89
|
+
* pointing at an imported kind) consult the kind owner's scope here, since
|
|
90
|
+
* the consumer's aliases will not contain a library's private imports. */
|
|
91
|
+
aliasesByModule?: Map<string, import("./alias-resolver.js").AliasResolver>;
|
|
86
92
|
}
|
|
87
93
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;qEAEiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;qEAEiE;IACjE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,YAAY,CAAC,EAAE,cAAc,EAAE,CAAC;IAChC,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,yDAAyD;IACzD,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;6FACyF;IACzF,WAAW,CAAC,EAAE,OAAO,sBAAsB,EAAE,WAAW,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;qEAEiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;qEAEiE;IACjE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,YAAY,CAAC,EAAE,cAAc,EAAE,CAAC;IAChC,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,yDAAyD;IACzD,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;6FACyF;IACzF,WAAW,CAAC,EAAE,OAAO,sBAAsB,EAAE,WAAW,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;IACpE;;;;+EAI2E;IAC3E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC,CAAC;CAC5E"}
|
|
@@ -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;AAGrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAkD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,
|
|
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;AAGrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAkD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CA2UtB"}
|
|
@@ -61,6 +61,7 @@ export function validateReferences(resources, context) {
|
|
|
61
61
|
const diagnostics = [];
|
|
62
62
|
const aliases = context.aliases;
|
|
63
63
|
const registry = context.definitions;
|
|
64
|
+
const aliasesByModule = context.aliasesByModule;
|
|
64
65
|
if (!aliases || !registry)
|
|
65
66
|
return diagnostics;
|
|
66
67
|
// Build outer resource lookup by name for resolution check.
|
|
@@ -74,7 +75,12 @@ export function validateReferences(resources, context) {
|
|
|
74
75
|
for (const r of resources) {
|
|
75
76
|
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind))
|
|
76
77
|
continue;
|
|
77
|
-
|
|
78
|
+
// Use the expanded map so refs nested behind x-telo-schema-from get the
|
|
79
|
+
// same kind-check / unresolved-name validation as locally-declared refs.
|
|
80
|
+
// Falls back to the base map when aliasesByModule isn't supplied.
|
|
81
|
+
const fieldMap = aliasesByModule
|
|
82
|
+
? registry.expandedFieldMapForResource(r, aliases, aliasesByModule)
|
|
83
|
+
: registry.getFieldMapForKind(r.kind, aliases);
|
|
78
84
|
if (!fieldMap)
|
|
79
85
|
continue;
|
|
80
86
|
const resourceLabel = `${r.kind}/${r.metadata.name}`;
|
|
@@ -228,6 +234,67 @@ export function validateReferences(resources, context) {
|
|
|
228
234
|
}
|
|
229
235
|
const anchorName = expr.slice(0, slashIdx);
|
|
230
236
|
const jsonPointer = "/" + expr.slice(slashIdx + 1);
|
|
237
|
+
// Aliased absolute kind path — first segment carries a dot, e.g.
|
|
238
|
+
// "HttpDispatch.Outcomes/$defs/Returns". Resolves the alias through the
|
|
239
|
+
// *kind owner's* scope (not the consumer's), navigates the JSON Pointer
|
|
240
|
+
// into the resolved definition's schema, and validates each field value.
|
|
241
|
+
//
|
|
242
|
+
// Relative anchors are property names that cannot contain a dot
|
|
243
|
+
// (CEL-style identifiers), so a dot in anchorName is unambiguous.
|
|
244
|
+
if (!isAbsolute && anchorName.includes(".")) {
|
|
245
|
+
const resolvedResourceKind = aliases.resolveKind(r.kind) ?? r.kind;
|
|
246
|
+
const resourceDef = registry.resolve(r.kind) ?? registry.resolve(resolvedResourceKind);
|
|
247
|
+
const owningModule = resourceDef?.metadata?.module;
|
|
248
|
+
const ownerScope = (owningModule ? aliasesByModule?.get(owningModule) : undefined) ?? aliases;
|
|
249
|
+
const targetKind = ownerScope.resolveKind(anchorName);
|
|
250
|
+
if (!targetKind) {
|
|
251
|
+
diagnostics.push({
|
|
252
|
+
severity: DiagnosticSeverity.Error,
|
|
253
|
+
code: "SCHEMA_FROM_MISSING_PATH",
|
|
254
|
+
source: SOURCE,
|
|
255
|
+
message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → cannot resolve alias '${anchorName}'`,
|
|
256
|
+
data: { resource: resourceData, filePath, path: fieldPath },
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const targetDef = registry.resolve(targetKind);
|
|
261
|
+
if (!targetDef?.schema) {
|
|
262
|
+
diagnostics.push({
|
|
263
|
+
severity: DiagnosticSeverity.Error,
|
|
264
|
+
code: "SCHEMA_FROM_MISSING_PATH",
|
|
265
|
+
source: SOURCE,
|
|
266
|
+
message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → kind '${targetKind}' has no schema`,
|
|
267
|
+
data: { resource: resourceData, filePath, path: fieldPath },
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const subSchema = navigateJsonPointer(targetDef.schema, jsonPointer);
|
|
272
|
+
if (subSchema === undefined) {
|
|
273
|
+
diagnostics.push({
|
|
274
|
+
severity: DiagnosticSeverity.Error,
|
|
275
|
+
code: "SCHEMA_FROM_MISSING_PATH",
|
|
276
|
+
source: SOURCE,
|
|
277
|
+
message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → kind '${targetKind}' has no schema path '${jsonPointer}'`,
|
|
278
|
+
data: { resource: resourceData, filePath, path: fieldPath },
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
for (const fieldValue of resolveFieldValues(r, fieldPath)) {
|
|
283
|
+
if (fieldValue == null)
|
|
284
|
+
continue;
|
|
285
|
+
const issues = registry.validateWithRefs(fieldValue, subSchema);
|
|
286
|
+
for (const issue of issues) {
|
|
287
|
+
diagnostics.push({
|
|
288
|
+
severity: DiagnosticSeverity.Error,
|
|
289
|
+
code: "DEPENDENT_SCHEMA_MISMATCH",
|
|
290
|
+
source: SOURCE,
|
|
291
|
+
message: `${resourceLabel}: '${fieldPath}' does not match schema from '${anchorName}${jsonPointer}': ${issue}`,
|
|
292
|
+
data: { resource: resourceData, filePath, path: fieldPath },
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
231
298
|
// Derive the anchor path in the resource config.
|
|
232
299
|
let anchorPath;
|
|
233
300
|
if (isAbsolute) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"ajv-formats": "^3.0.1",
|
|
42
42
|
"jsonpath-plus": "^10.3.0",
|
|
43
43
|
"yaml": "^2.8.3",
|
|
44
|
-
"@telorun/sdk": "0.
|
|
45
|
-
"@telorun/templating": "0.2.
|
|
44
|
+
"@telorun/sdk": "0.10.0",
|
|
45
|
+
"@telorun/templating": "0.2.2"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^20.0.0",
|
package/src/analysis-registry.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { AnalysisContext } from "./types.js";
|
|
|
14
14
|
export class AnalysisRegistry {
|
|
15
15
|
private readonly defs = new DefinitionRegistry();
|
|
16
16
|
private readonly aliases = new AliasResolver();
|
|
17
|
+
private readonly aliasesByModule = new Map<string, AliasResolver>();
|
|
17
18
|
|
|
18
19
|
registerDefinition(def: ResourceDefinition): void {
|
|
19
20
|
this.defs.register(def);
|
|
@@ -34,13 +35,21 @@ export class AnalysisRegistry {
|
|
|
34
35
|
/**
|
|
35
36
|
* Iterates a resource's reference and scope fields as declared by its definition.
|
|
36
37
|
* Calls onRef for each plain reference field and onScope for each scope field.
|
|
38
|
+
*
|
|
39
|
+
* Uses the expanded field map so x-telo-schema-from entries contribute their
|
|
40
|
+
* nested ref/scope slots — Phase 5 injection sees encoders that live inside a
|
|
41
|
+
* sub-schema (e.g. Server.notFoundHandler.returns[].content[mime].encoder).
|
|
37
42
|
*/
|
|
38
43
|
iterateFieldEntries(
|
|
39
44
|
resource: ResourceManifest,
|
|
40
45
|
onRef: (fieldPath: string) => void,
|
|
41
46
|
onScope: (fieldPath: string) => void,
|
|
42
47
|
): void {
|
|
43
|
-
const fieldMap = this.defs.
|
|
48
|
+
const fieldMap = this.defs.expandedFieldMapForResource(
|
|
49
|
+
resource,
|
|
50
|
+
this.aliases,
|
|
51
|
+
this.aliasesByModule,
|
|
52
|
+
);
|
|
44
53
|
if (!fieldMap) return;
|
|
45
54
|
for (const [fieldPath, entry] of fieldMap) {
|
|
46
55
|
if (isScopeEntry(entry)) {
|
|
@@ -94,6 +103,6 @@ export class AnalysisRegistry {
|
|
|
94
103
|
|
|
95
104
|
/** @internal Bridge for StaticAnalyzer — do not use outside the analyzer package. */
|
|
96
105
|
_context(): AnalysisContext {
|
|
97
|
-
return { aliases: this.aliases, definitions: this.defs };
|
|
106
|
+
return { aliases: this.aliases, definitions: this.defs, aliasesByModule: this.aliasesByModule };
|
|
98
107
|
}
|
|
99
108
|
}
|
package/src/analyzer.ts
CHANGED
|
@@ -433,7 +433,7 @@ export class StaticAnalyzer {
|
|
|
433
433
|
rootModules.add(m.metadata.name as string);
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
|
-
const aliasesByModule = new Map<string, AliasResolver>();
|
|
436
|
+
const aliasesByModule = ctx?.aliasesByModule ?? new Map<string, AliasResolver>();
|
|
437
437
|
for (const m of manifests) {
|
|
438
438
|
if (isModuleKind(m.kind)) {
|
|
439
439
|
const namespace = ((m.metadata as any).namespace as string | undefined) ?? null;
|
|
@@ -525,7 +525,7 @@ export class StaticAnalyzer {
|
|
|
525
525
|
}
|
|
526
526
|
|
|
527
527
|
// Phase 2: extract inline resources from x-telo-ref slots into first-class manifests
|
|
528
|
-
const allManifests = normalizeInlineResources(manifests, defs, aliases);
|
|
528
|
+
const allManifests = normalizeInlineResources(manifests, defs, aliases, aliasesByModule);
|
|
529
529
|
|
|
530
530
|
// Build a name→manifest map for looking up referenced resources
|
|
531
531
|
const byName = new Map<string, ResourceManifest>();
|
|
@@ -715,7 +715,9 @@ export class StaticAnalyzer {
|
|
|
715
715
|
}
|
|
716
716
|
|
|
717
717
|
// Validate resource references (Phase 3)
|
|
718
|
-
diagnostics.push(
|
|
718
|
+
diagnostics.push(
|
|
719
|
+
...validateReferences(allManifests, { aliases, definitions: defs, aliasesByModule }),
|
|
720
|
+
);
|
|
719
721
|
|
|
720
722
|
// Validate `extends` fields and flag legacy `capability: <UserAbstract>` overload.
|
|
721
723
|
diagnostics.push(...validateExtends(allManifests, defs, aliases));
|
|
@@ -738,7 +740,12 @@ export class StaticAnalyzer {
|
|
|
738
740
|
|
|
739
741
|
normalize(manifests: ResourceManifest[], registry: AnalysisRegistry): ResourceManifest[] {
|
|
740
742
|
const ctx = registry._context();
|
|
741
|
-
return normalizeInlineResources(
|
|
743
|
+
return normalizeInlineResources(
|
|
744
|
+
manifests,
|
|
745
|
+
ctx.definitions!,
|
|
746
|
+
ctx.aliases,
|
|
747
|
+
ctx.aliasesByModule,
|
|
748
|
+
);
|
|
742
749
|
}
|
|
743
750
|
|
|
744
751
|
prepare(
|
|
@@ -751,7 +758,12 @@ export class StaticAnalyzer {
|
|
|
751
758
|
if (errors.length > 0) {
|
|
752
759
|
return { diagnostics: errors, order: null, cycleError: null };
|
|
753
760
|
}
|
|
754
|
-
const graph = buildDependencyGraph(
|
|
761
|
+
const graph = buildDependencyGraph(
|
|
762
|
+
manifests,
|
|
763
|
+
ctx.definitions!,
|
|
764
|
+
ctx.aliases,
|
|
765
|
+
ctx.aliasesByModule,
|
|
766
|
+
);
|
|
755
767
|
if (graph.cycle) {
|
|
756
768
|
return { diagnostics: [], order: null, cycleError: formatCycle(graph.cycle) };
|
|
757
769
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import type { ResourceDefinition } from "@telorun/sdk";
|
|
1
|
+
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
2
3
|
import { KERNEL_BUILTINS } from "./builtins.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
4
|
+
import {
|
|
5
|
+
buildFieldMapAtPath,
|
|
6
|
+
buildReferenceFieldMap,
|
|
7
|
+
isSchemaFromEntry,
|
|
8
|
+
type ReferenceFieldMap,
|
|
9
|
+
} from "./reference-field-map.js";
|
|
10
|
+
import { createAjv, formatSingleError, navigateJsonPointer } from "./schema-compat.js";
|
|
5
11
|
|
|
6
12
|
/** Pure kind → ResourceDefinition map. No controller loading, no lifecycle. */
|
|
7
13
|
export class DefinitionRegistry {
|
|
@@ -163,6 +169,73 @@ export class DefinitionRegistry {
|
|
|
163
169
|
return resolved ? this.getFieldMap(resolved) : undefined;
|
|
164
170
|
}
|
|
165
171
|
|
|
172
|
+
/** Returns the field map for `resource.kind` with x-telo-schema-from entries replaced
|
|
173
|
+
* by their nested ref/scope slots — so Phase 2 inline normalization and Phase 5
|
|
174
|
+
* injection see encoders nested behind a schema-from indirection (e.g.
|
|
175
|
+
* http-server `Server.notFoundHandler.returns[].content[mime].encoder`).
|
|
176
|
+
*
|
|
177
|
+
* Only static absolute schema-from paths with a dotted alias anchor are expanded
|
|
178
|
+
* (e.g. "HttpDispatch.Outcomes/$defs/Returns"). Relative and unqualified absolute
|
|
179
|
+
* anchors depend on a sibling property at runtime and stay unexpanded; the
|
|
180
|
+
* analyzer's reference validation phase already flags the cases that matter. */
|
|
181
|
+
expandedFieldMapForResource(
|
|
182
|
+
resource: ResourceManifest,
|
|
183
|
+
aliases: AliasResolver,
|
|
184
|
+
aliasesByModule: Map<string, AliasResolver>,
|
|
185
|
+
): ReferenceFieldMap | undefined {
|
|
186
|
+
const baseMap = this.getFieldMapForKind(resource.kind, aliases);
|
|
187
|
+
if (!baseMap) return undefined;
|
|
188
|
+
|
|
189
|
+
const resolvedKind = aliases.resolveKind(resource.kind) ?? resource.kind;
|
|
190
|
+
const def = this.resolve(resource.kind) ?? this.resolve(resolvedKind);
|
|
191
|
+
const ownerModule = (def?.metadata as { module?: string } | undefined)?.module;
|
|
192
|
+
const ownerScope =
|
|
193
|
+
(ownerModule ? aliasesByModule.get(ownerModule) : undefined) ?? aliases;
|
|
194
|
+
|
|
195
|
+
const expanded: ReferenceFieldMap = new Map();
|
|
196
|
+
for (const [path, entry] of baseMap) {
|
|
197
|
+
if (!isSchemaFromEntry(entry)) {
|
|
198
|
+
expanded.set(path, entry);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const sub = this.resolveSchemaFromSubMap(entry.schemaFrom, path, ownerScope);
|
|
202
|
+
if (!sub) continue;
|
|
203
|
+
for (const [subPath, subEntry] of sub) expanded.set(subPath, subEntry);
|
|
204
|
+
}
|
|
205
|
+
return expanded;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private resolveSchemaFromSubMap(
|
|
209
|
+
schemaFrom: string,
|
|
210
|
+
fieldPath: string,
|
|
211
|
+
ownerScope: AliasResolver,
|
|
212
|
+
): ReferenceFieldMap | null {
|
|
213
|
+
const isAbsolute = schemaFrom.startsWith("/");
|
|
214
|
+
const expr = isAbsolute ? schemaFrom.slice(1) : schemaFrom;
|
|
215
|
+
const slashIdx = expr.indexOf("/");
|
|
216
|
+
if (slashIdx === -1) return null;
|
|
217
|
+
const anchorName = expr.slice(0, slashIdx);
|
|
218
|
+
const jsonPointer = "/" + expr.slice(slashIdx + 1);
|
|
219
|
+
|
|
220
|
+
// Static form: absolute path whose anchor is a dotted alias (e.g.
|
|
221
|
+
// "HttpDispatch.Outcomes/$defs/Returns"). Polymorphic forms — relative
|
|
222
|
+
// anchors or single-segment absolute anchors — only resolve once we know a
|
|
223
|
+
// sibling property's value, which is per-resource.
|
|
224
|
+
if (!anchorName.includes(".")) return null;
|
|
225
|
+
|
|
226
|
+
const targetKind = ownerScope.resolveKind(anchorName);
|
|
227
|
+
if (!targetKind) return null;
|
|
228
|
+
const targetDef = this.resolve(targetKind);
|
|
229
|
+
if (!targetDef?.schema) return null;
|
|
230
|
+
const subSchema = navigateJsonPointer(
|
|
231
|
+
targetDef.schema as Record<string, unknown>,
|
|
232
|
+
jsonPointer,
|
|
233
|
+
);
|
|
234
|
+
if (!subSchema || typeof subSchema !== "object") return null;
|
|
235
|
+
|
|
236
|
+
return buildFieldMapAtPath(subSchema as Record<string, any>, fieldPath);
|
|
237
|
+
}
|
|
238
|
+
|
|
166
239
|
/** Returns all definitions that transitively extend the given abstract kind.
|
|
167
240
|
* Follows the capability chain to any depth (equivalent to instanceof in OOP).
|
|
168
241
|
* Definitions are included regardless of registration order. */
|
package/src/dependency-graph.ts
CHANGED
|
@@ -45,6 +45,7 @@ export function buildDependencyGraph(
|
|
|
45
45
|
resources: ResourceManifest[],
|
|
46
46
|
registry: DefinitionRegistry,
|
|
47
47
|
aliases?: AliasResolver,
|
|
48
|
+
aliasesByModule?: Map<string, AliasResolver>,
|
|
48
49
|
): DependencyGraph {
|
|
49
50
|
// --- Build node set ---
|
|
50
51
|
const nodes = new Map<string, ResourceNode>();
|
|
@@ -62,7 +63,14 @@ export function buildDependencyGraph(
|
|
|
62
63
|
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind)) continue;
|
|
63
64
|
|
|
64
65
|
const sourceKey = nodeKey(r.kind, r.metadata.name as string);
|
|
65
|
-
|
|
66
|
+
// Use the expanded map so refs nested behind x-telo-schema-from contribute
|
|
67
|
+
// edges to the DAG. Without these, a parent (e.g. Http.Server) can init
|
|
68
|
+
// before its extracted encoder and Phase 5 injection fires against a
|
|
69
|
+
// not-yet-created dependency.
|
|
70
|
+
const fieldMap =
|
|
71
|
+
aliases && aliasesByModule
|
|
72
|
+
? registry.expandedFieldMapForResource(r, aliases, aliasesByModule)
|
|
73
|
+
: registry.getFieldMapForKind(r.kind, aliases);
|
|
66
74
|
if (!fieldMap) continue;
|
|
67
75
|
|
|
68
76
|
// Collect names of resources declared inside scope fields — these are initialized
|