@telorun/analyzer 0.11.0 → 0.12.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/LICENSE +2 -2
- package/README.md +3 -3
- package/dist/adapters/http-adapter.d.ts +10 -0
- package/dist/adapters/http-adapter.d.ts.map +1 -0
- package/dist/adapters/http-adapter.js +18 -0
- package/dist/adapters/node-adapter.d.ts +17 -0
- package/dist/adapters/node-adapter.d.ts.map +1 -0
- package/dist/adapters/node-adapter.js +71 -0
- package/dist/adapters/registry-adapter.d.ts +15 -0
- package/dist/adapters/registry-adapter.d.ts.map +1 -0
- package/dist/adapters/registry-adapter.js +53 -0
- package/dist/analysis-registry.d.ts +7 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +38 -0
- package/dist/analyzer.d.ts +15 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +114 -10
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +58 -1
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +16 -0
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +27 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +9 -11
- package/dist/manifest-loader.d.ts +23 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +66 -3
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +26 -14
- package/dist/position-metadata.d.ts +11 -2
- package/dist/position-metadata.d.ts.map +1 -1
- package/dist/position-metadata.js +18 -3
- package/dist/precompile.d.ts.map +1 -1
- package/dist/precompile.js +9 -1
- package/dist/reference-field-map.d.ts +21 -4
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +93 -25
- package/dist/residual-schema.d.ts +23 -0
- package/dist/residual-schema.d.ts.map +1 -0
- package/dist/residual-schema.js +45 -0
- package/dist/resolve-ref-sentinels.d.ts +27 -0
- package/dist/resolve-ref-sentinels.d.ts.map +1 -0
- package/dist/resolve-ref-sentinels.js +114 -0
- package/dist/rewrite-synthetic-origins.d.ts +10 -0
- package/dist/rewrite-synthetic-origins.d.ts.map +1 -0
- package/dist/rewrite-synthetic-origins.js +55 -0
- package/dist/schema-compat.d.ts +7 -1
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +19 -2
- package/dist/system-kinds.d.ts +25 -0
- package/dist/system-kinds.d.ts.map +1 -0
- package/dist/system-kinds.js +34 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-cel-context.d.ts +5 -0
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +27 -15
- package/dist/validate-provider-coherence.d.ts +23 -0
- package/dist/validate-provider-coherence.d.ts.map +1 -0
- package/dist/validate-provider-coherence.js +148 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +141 -36
- package/dist/with-synthetic-positions.d.ts +28 -0
- package/dist/with-synthetic-positions.d.ts.map +1 -0
- package/dist/with-synthetic-positions.js +45 -0
- package/package.json +7 -4
- package/src/analysis-registry.ts +37 -0
- package/src/analyzer.ts +118 -12
- package/src/builtins.ts +58 -1
- package/src/definition-registry.ts +15 -0
- package/src/dependency-graph.ts +27 -14
- package/src/index.ts +2 -0
- package/src/kernel-globals.ts +9 -11
- package/src/manifest-loader.ts +69 -4
- package/src/normalize-inline-resources.ts +48 -13
- package/src/position-metadata.ts +18 -3
- package/src/precompile.ts +8 -1
- package/src/reference-field-map.ts +129 -24
- package/src/residual-schema.ts +49 -0
- package/src/resolve-ref-sentinels.ts +127 -0
- package/src/rewrite-synthetic-origins.ts +75 -0
- package/src/schema-compat.ts +19 -2
- package/src/system-kinds.ts +37 -0
- package/src/types.ts +12 -0
- package/src/validate-cel-context.ts +28 -15
- package/src/validate-provider-coherence.ts +166 -0
- package/src/validate-references.ts +138 -35
- package/src/with-synthetic-positions.ts +48 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEtF,MAAM,WAAW,kBAAkB;IACjC;mEAC+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC;;kDAE8C;IAC9C,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;KACxD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KAC/C,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAClC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA6BjC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED
|
|
1
|
+
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEtF,MAAM,WAAW,kBAAkB;IACjC;mEAC+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC;;kDAE8C;IAC9C,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;KACxD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KAC/C,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAClC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA6BjC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACjC,IAAI,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAChD,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAqGrB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAQrB"}
|
|
@@ -94,6 +94,11 @@ export function pathMatchesScope(exprPath, scope) {
|
|
|
94
94
|
* `manifestRoot.provide.kind` as a kind name, looks up the kind's Telo.Definition,
|
|
95
95
|
* and returns the `outputType` schema.
|
|
96
96
|
*
|
|
97
|
+
* Accepts either a single string or an array of strings. With an array, paths
|
|
98
|
+
* are tried in order and the first one that resolves to a usable schema wins —
|
|
99
|
+
* used by `result:` to find its dispatch target under whichever entry-point
|
|
100
|
+
* field (`provide:` or `invoke:`) the definition declares.
|
|
101
|
+
*
|
|
97
102
|
* - `x-telo-context-ref-from`: existing form — reads `{kind, name}` object from
|
|
98
103
|
* `manifestItem.<path>`, looks up the named manifest, returns its `<subpath>` field.
|
|
99
104
|
*
|
|
@@ -122,30 +127,37 @@ export function resolveContextAnnotations(schema, manifestItem, opts) {
|
|
|
122
127
|
};
|
|
123
128
|
}
|
|
124
129
|
const fromRoot = schema["x-telo-context-from-root"];
|
|
125
|
-
const
|
|
126
|
-
|
|
130
|
+
const fromRefKindRaw = schema["x-telo-context-from-ref-kind"];
|
|
131
|
+
const fromRefKinds = fromRefKindRaw == null
|
|
132
|
+
? []
|
|
133
|
+
: Array.isArray(fromRefKindRaw)
|
|
134
|
+
? fromRefKindRaw
|
|
135
|
+
: [fromRefKindRaw];
|
|
136
|
+
if (fromRoot || fromRefKinds.length > 0) {
|
|
127
137
|
if (fromRoot) {
|
|
128
138
|
const resolved = navigatePath(manifestRoot, fromRoot.split("/"));
|
|
129
139
|
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
130
140
|
return resolved;
|
|
131
141
|
}
|
|
132
142
|
}
|
|
133
|
-
if (
|
|
134
|
-
const
|
|
135
|
-
|
|
143
|
+
if (defs) {
|
|
144
|
+
for (const fromRefKind of fromRefKinds) {
|
|
145
|
+
const hashIdx = fromRefKind.indexOf("#");
|
|
146
|
+
if (hashIdx <= 0)
|
|
147
|
+
continue;
|
|
136
148
|
const refPath = fromRefKind.slice(0, hashIdx);
|
|
137
149
|
const field = fromRefKind.slice(hashIdx + 1);
|
|
138
150
|
const kindValue = navigatePath(manifestRoot, refPath.split("/"));
|
|
139
|
-
if (typeof kindValue
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
if (typeof kindValue !== "string" || kindValue.length === 0)
|
|
152
|
+
continue;
|
|
153
|
+
const canonical = aliases?.resolveKind(kindValue) ?? kindValue;
|
|
154
|
+
const def = defs.resolve(canonical);
|
|
155
|
+
const typeField = def
|
|
156
|
+
? def[field]
|
|
157
|
+
: undefined;
|
|
158
|
+
const resolved = resolveTypeFieldToSchema(typeField, allManifests ?? []);
|
|
159
|
+
if (resolved && typeof resolved === "object") {
|
|
160
|
+
return resolved;
|
|
149
161
|
}
|
|
150
162
|
}
|
|
151
163
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
|
+
import { type AnalysisDiagnostic } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Validates coherence rules for `Telo.Definition` documents that use the `provide:`
|
|
7
|
+
* template target, plus the implementation-presence rule on `Telo.Provider`
|
|
8
|
+
* definitions.
|
|
9
|
+
*
|
|
10
|
+
* Diagnostics:
|
|
11
|
+
* - PROVIDE_ON_NON_PROVIDER: `provide:` declared on a definition whose
|
|
12
|
+
* `capability` is not `Telo.Provider`.
|
|
13
|
+
* - PROVIDE_DISPATCHER_CONFLICT: `provide:` co-exists with `invoke:` or `run:`
|
|
14
|
+
* on the same definition.
|
|
15
|
+
* - PROVIDE_TARGET_UNKNOWN: `provide.name` does not resolve to an entry in
|
|
16
|
+
* `resources:`.
|
|
17
|
+
* - PROVIDE_TARGET_NOT_INVOCABLE: `provide.name` resolves to a resource whose
|
|
18
|
+
* kind is registered but not a `Telo.Invocable`.
|
|
19
|
+
* - PROVIDER_MISSING_IMPLEMENTATION: definition with `capability: Telo.Provider`
|
|
20
|
+
* declares neither `controllers:` (TS-backed) nor `provide:` (template-backed).
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateProviderCoherence(manifests: ResourceManifest[], registry: DefinitionRegistry, aliases: AliasResolver): AnalysisDiagnostic[];
|
|
23
|
+
//# sourceMappingURL=validate-provider-coherence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-provider-coherence.d.ts","sourceRoot":"","sources":["../src/validate-provider-coherence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,GACrB,kBAAkB,EAAE,CAyItB"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { DiagnosticSeverity } from "./types.js";
|
|
2
|
+
const SOURCE = "telo-analyzer";
|
|
3
|
+
/**
|
|
4
|
+
* Validates coherence rules for `Telo.Definition` documents that use the `provide:`
|
|
5
|
+
* template target, plus the implementation-presence rule on `Telo.Provider`
|
|
6
|
+
* definitions.
|
|
7
|
+
*
|
|
8
|
+
* Diagnostics:
|
|
9
|
+
* - PROVIDE_ON_NON_PROVIDER: `provide:` declared on a definition whose
|
|
10
|
+
* `capability` is not `Telo.Provider`.
|
|
11
|
+
* - PROVIDE_DISPATCHER_CONFLICT: `provide:` co-exists with `invoke:` or `run:`
|
|
12
|
+
* on the same definition.
|
|
13
|
+
* - PROVIDE_TARGET_UNKNOWN: `provide.name` does not resolve to an entry in
|
|
14
|
+
* `resources:`.
|
|
15
|
+
* - PROVIDE_TARGET_NOT_INVOCABLE: `provide.name` resolves to a resource whose
|
|
16
|
+
* kind is registered but not a `Telo.Invocable`.
|
|
17
|
+
* - PROVIDER_MISSING_IMPLEMENTATION: definition with `capability: Telo.Provider`
|
|
18
|
+
* declares neither `controllers:` (TS-backed) nor `provide:` (template-backed).
|
|
19
|
+
*/
|
|
20
|
+
export function validateProviderCoherence(manifests, registry, aliases) {
|
|
21
|
+
const diagnostics = [];
|
|
22
|
+
const importedModules = new Set();
|
|
23
|
+
for (const m of manifests) {
|
|
24
|
+
if (m.kind !== "Telo.Import")
|
|
25
|
+
continue;
|
|
26
|
+
const resolved = m.metadata
|
|
27
|
+
?.resolvedModuleName;
|
|
28
|
+
if (resolved)
|
|
29
|
+
importedModules.add(resolved);
|
|
30
|
+
}
|
|
31
|
+
for (const m of manifests) {
|
|
32
|
+
if (m.kind !== "Telo.Definition")
|
|
33
|
+
continue;
|
|
34
|
+
const name = m.metadata?.name;
|
|
35
|
+
if (!name)
|
|
36
|
+
continue;
|
|
37
|
+
const ownModule = m.metadata?.module;
|
|
38
|
+
if (ownModule && importedModules.has(ownModule))
|
|
39
|
+
continue;
|
|
40
|
+
const filePath = m.metadata?.source;
|
|
41
|
+
const resource = { kind: m.kind, name };
|
|
42
|
+
const label = `${m.kind}/${name}`;
|
|
43
|
+
const md = m;
|
|
44
|
+
const capability = typeof md.capability === "string" ? md.capability : undefined;
|
|
45
|
+
const provide = md.provide;
|
|
46
|
+
const invoke = md.invoke;
|
|
47
|
+
const run = md.run;
|
|
48
|
+
const controllers = md.controllers;
|
|
49
|
+
const resources = md.resources;
|
|
50
|
+
const hasProvide = provide !== undefined && provide !== null;
|
|
51
|
+
const hasInvoke = invoke !== undefined && invoke !== null;
|
|
52
|
+
const hasRun = run !== undefined && run !== null;
|
|
53
|
+
const hasControllers = Array.isArray(controllers) && controllers.length > 0;
|
|
54
|
+
if (hasProvide && capability !== "Telo.Provider") {
|
|
55
|
+
diagnostics.push({
|
|
56
|
+
severity: DiagnosticSeverity.Error,
|
|
57
|
+
code: "PROVIDE_ON_NON_PROVIDER",
|
|
58
|
+
source: SOURCE,
|
|
59
|
+
message: `${label}: 'provide:' is only valid on definitions with 'capability: Telo.Provider' ` +
|
|
60
|
+
`(found '${capability ?? "<unset>"}'). Use 'invoke:' or 'run:' for other capabilities.`,
|
|
61
|
+
data: { resource, filePath, path: "provide" },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (hasProvide && (hasInvoke || hasRun)) {
|
|
65
|
+
const conflict = hasInvoke ? "invoke" : "run";
|
|
66
|
+
diagnostics.push({
|
|
67
|
+
severity: DiagnosticSeverity.Error,
|
|
68
|
+
code: "PROVIDE_DISPATCHER_CONFLICT",
|
|
69
|
+
source: SOURCE,
|
|
70
|
+
message: `${label}: 'provide:' cannot co-exist with '${conflict}:'. ` +
|
|
71
|
+
`A definition declares exactly one dispatch entry-point.`,
|
|
72
|
+
data: { resource, filePath, path: "provide" },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (hasProvide && typeof provide === "object" && !Array.isArray(provide)) {
|
|
76
|
+
const provideObj = provide;
|
|
77
|
+
const providedName = typeof provideObj.name === "string" ? provideObj.name : undefined;
|
|
78
|
+
const providedKind = typeof provideObj.kind === "string" ? provideObj.kind : undefined;
|
|
79
|
+
if (providedName && Array.isArray(resources)) {
|
|
80
|
+
const match = resources.find((r) => {
|
|
81
|
+
const meta = r?.metadata;
|
|
82
|
+
return typeof meta?.name === "string" && meta.name === providedName;
|
|
83
|
+
});
|
|
84
|
+
if (!match) {
|
|
85
|
+
diagnostics.push({
|
|
86
|
+
severity: DiagnosticSeverity.Error,
|
|
87
|
+
code: "PROVIDE_TARGET_UNKNOWN",
|
|
88
|
+
source: SOURCE,
|
|
89
|
+
message: `${label}: 'provide.name: ${providedName}' does not match any entry's ` +
|
|
90
|
+
`metadata.name in 'resources:'.`,
|
|
91
|
+
data: { resource, filePath, path: "provide.name" },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else if (typeof match.kind === "string") {
|
|
95
|
+
// `provide.kind` is the type contract the analyzer uses to type
|
|
96
|
+
// `result` CEL against the target's `outputType`. The runtime
|
|
97
|
+
// dispatches on `provide.name` and ignores `provide.kind`, so a
|
|
98
|
+
// mismatch silently degrades `result` typing to an open schema
|
|
99
|
+
// (and at runtime quietly invokes the actually-matched resource).
|
|
100
|
+
// Flag the divergence so result-typing never lies.
|
|
101
|
+
if (providedKind) {
|
|
102
|
+
const providedCanonical = aliases.resolveKind(providedKind) ?? providedKind;
|
|
103
|
+
const matchCanonical = aliases.resolveKind(match.kind) ?? match.kind;
|
|
104
|
+
if (providedCanonical !== matchCanonical) {
|
|
105
|
+
diagnostics.push({
|
|
106
|
+
severity: DiagnosticSeverity.Error,
|
|
107
|
+
code: "PROVIDE_KIND_MISMATCH",
|
|
108
|
+
source: SOURCE,
|
|
109
|
+
message: `${label}: 'provide.kind: ${providedKind}' disagrees with the matched ` +
|
|
110
|
+
`'resources:' entry's kind '${match.kind}' (matched by metadata.name ` +
|
|
111
|
+
`'${providedName}'). The runtime dispatches by name, so 'provide.kind' ` +
|
|
112
|
+
`is decorative — but the analyzer types 'result:' against it, and a ` +
|
|
113
|
+
`mismatch silently turns off that typing.`,
|
|
114
|
+
data: { resource, filePath, path: "provide.kind" },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const resolvedKind = aliases.resolveKind(match.kind) ?? match.kind;
|
|
119
|
+
const targetDef = registry.resolve(resolvedKind) ?? registry.resolve(match.kind);
|
|
120
|
+
if (targetDef && targetDef.kind === "Telo.Definition") {
|
|
121
|
+
const targetCap = targetDef.capability;
|
|
122
|
+
if (typeof targetCap === "string" && targetCap !== "Telo.Invocable") {
|
|
123
|
+
diagnostics.push({
|
|
124
|
+
severity: DiagnosticSeverity.Error,
|
|
125
|
+
code: "PROVIDE_TARGET_NOT_INVOCABLE",
|
|
126
|
+
source: SOURCE,
|
|
127
|
+
message: `${label}: 'provide.name: ${providedName}' resolves to a ${match.kind} ` +
|
|
128
|
+
`(capability '${targetCap}'); 'provide:' requires a Telo.Invocable target.`,
|
|
129
|
+
data: { resource, filePath, path: "provide.name" },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (capability === "Telo.Provider" && !hasControllers && !hasProvide) {
|
|
137
|
+
diagnostics.push({
|
|
138
|
+
severity: DiagnosticSeverity.Error,
|
|
139
|
+
code: "PROVIDER_MISSING_IMPLEMENTATION",
|
|
140
|
+
source: SOURCE,
|
|
141
|
+
message: `${label}: 'capability: Telo.Provider' requires either 'controllers:' ` +
|
|
142
|
+
`(TS-backed) or 'provide:' (template-backed) to declare an implementation.`,
|
|
143
|
+
data: { resource, filePath, path: "capability" },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return diagnostics;
|
|
148
|
+
}
|
|
@@ -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;
|
|
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;AAKrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA4C/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAsbtB"}
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
2
|
+
import { isRefEntry, isScopeEntry, isSchemaFromEntry, isInlineResource, resolveFieldEntries, resolveFieldValues } from "./reference-field-map.js";
|
|
2
3
|
import { navigateJsonPointer } from "./schema-compat.js";
|
|
4
|
+
import { REF_VALIDATION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
3
5
|
import { DiagnosticSeverity } from "./types.js";
|
|
4
6
|
const SOURCE = "telo-analyzer";
|
|
5
|
-
/** Kinds skipped by reference validation. Telo.Application and Telo.Library
|
|
6
|
-
* are intentionally not here: Application has `targets` with x-telo-ref that
|
|
7
|
-
* must be validated, and Library has no ref-bearing fields so flows through
|
|
8
|
-
* harmlessly. Telo.Import is also not here for the same reason — its
|
|
9
|
-
* `source` field isn't x-telo-ref, so nothing gets checked. */
|
|
10
|
-
const SYSTEM_KINDS = new Set(["Telo.Definition", "Telo.Abstract"]);
|
|
11
7
|
/**
|
|
12
8
|
* Checks whether `kind` satisfies the ref constraint in `entry`.
|
|
13
9
|
* Returns an empty array when valid, or mismatch error strings when not.
|
|
@@ -64,14 +60,93 @@ export function validateReferences(resources, context) {
|
|
|
64
60
|
const aliasesByModule = context.aliasesByModule;
|
|
65
61
|
if (!aliases || !registry)
|
|
66
62
|
return diagnostics;
|
|
67
|
-
// Build outer resource lookup by name for resolution check
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
63
|
+
// Build outer resource lookup by name for resolution check, collecting
|
|
64
|
+
// every entry per name so we can surface name collisions as diagnostics
|
|
65
|
+
// (the kernel's resource registry shares one namespace across all
|
|
66
|
+
// non-system kinds — e.g. `Telo.Application HelloApi` and `Http.Api
|
|
67
|
+
// HelloApi` collide at boot with `ERR_DUPLICATE_RESOURCE`. Catching it
|
|
68
|
+
// statically removes a class of "everything analyzes clean, then the
|
|
69
|
+
// kernel refuses to start" surprises.)
|
|
70
|
+
//
|
|
71
|
+
// Telo.Import is excluded from the duplicate check on top of the
|
|
72
|
+
// SYSTEM_KINDS skip: its `metadata.name` is an alias, not a resource
|
|
73
|
+
// identity (aliases live in a separate namespace from resources, and
|
|
74
|
+
// colliding aliases vs. resource names is benign — the alias is only
|
|
75
|
+
// ever read as a kind prefix).
|
|
76
|
+
// Group manifests by name to detect collisions. Two subtleties:
|
|
77
|
+
//
|
|
78
|
+
// 1. Some analyzer hosts emit the SAME physical document twice through
|
|
79
|
+
// their pipeline — e.g. the telo-editor's `toAnalysisManifests` walks
|
|
80
|
+
// each workspace module's documents independently, and a file
|
|
81
|
+
// reachable from two angles (entry module + `include:` partial)
|
|
82
|
+
// shows up twice. The fingerprint includes `sourceLine` so identical
|
|
83
|
+
// docs (same kind, name, source, AND source line) collapse to one,
|
|
84
|
+
// while two textually-separate documents in the same file (different
|
|
85
|
+
// source lines) keep separate fingerprints and trip the diagnostic.
|
|
86
|
+
// 2. The diagnostic carries a precomputed `range` pointing at the
|
|
87
|
+
// duplicate's source line — editor hosts that resolve diagnostic
|
|
88
|
+
// positions via a `${file}::${kind}::${name}` lookup would otherwise
|
|
89
|
+
// collide on duplicates (Map.set overwrites) and place the squiggle
|
|
90
|
+
// ambiguously. The explicit `range` short-circuits that lookup.
|
|
91
|
+
// Dedup pipeline echoes — the same physical document emitted twice
|
|
92
|
+
// through an analyzer host's pipeline. Keyed on (kind, name, source,
|
|
93
|
+
// sourceLine), so two textually-distinct docs in the same file (same
|
|
94
|
+
// source, different sourceLine) keep separate fingerprints and still
|
|
95
|
+
// trip the diagnostic. `analyze()` enforces that every non-system
|
|
96
|
+
// manifest carries both positional fields — no defensive guard needed.
|
|
97
|
+
const byNameAll = new Map();
|
|
98
|
+
const seen = new Set();
|
|
71
99
|
for (const r of resources) {
|
|
72
|
-
if (r.metadata?.name
|
|
73
|
-
|
|
100
|
+
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || r.kind === "Telo.Import")
|
|
101
|
+
continue;
|
|
102
|
+
const name = r.metadata.name;
|
|
103
|
+
// `analyze()` guarantees both fields are present on non-system manifests.
|
|
104
|
+
const meta = r.metadata;
|
|
105
|
+
const fingerprint = `${r.kind} ${name} ${meta.source} ${meta.sourceLine}`;
|
|
106
|
+
if (seen.has(fingerprint))
|
|
107
|
+
continue;
|
|
108
|
+
seen.add(fingerprint);
|
|
109
|
+
const existing = byNameAll.get(name);
|
|
110
|
+
if (existing)
|
|
111
|
+
existing.push(r);
|
|
112
|
+
else
|
|
113
|
+
byNameAll.set(name, [r]);
|
|
74
114
|
}
|
|
115
|
+
for (const [name, list] of byNameAll) {
|
|
116
|
+
if (list.length <= 1)
|
|
117
|
+
continue;
|
|
118
|
+
const [first, ...rest] = list;
|
|
119
|
+
const firstLabel = `${first.kind}/${name}`;
|
|
120
|
+
for (const dup of rest) {
|
|
121
|
+
const dupMeta = dup.metadata;
|
|
122
|
+
const range = typeof dupMeta?.sourceLine === "number"
|
|
123
|
+
? {
|
|
124
|
+
start: { line: dupMeta.sourceLine, character: 0 },
|
|
125
|
+
end: { line: dupMeta.sourceLine, character: Number.MAX_SAFE_INTEGER },
|
|
126
|
+
}
|
|
127
|
+
: undefined;
|
|
128
|
+
diagnostics.push({
|
|
129
|
+
severity: DiagnosticSeverity.Error,
|
|
130
|
+
code: "DUPLICATE_RESOURCE_NAME",
|
|
131
|
+
source: SOURCE,
|
|
132
|
+
message: `${dup.kind}/${name}: resource name collides with ${firstLabel} declared earlier (kernel runtime would fail with ERR_DUPLICATE_RESOURCE)`,
|
|
133
|
+
...(range ? { range } : {}),
|
|
134
|
+
data: {
|
|
135
|
+
resource: { kind: dup.kind, name },
|
|
136
|
+
filePath: dupMeta?.source,
|
|
137
|
+
path: "metadata.name",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Single-resource map for the resolution / scope lookups below — when a
|
|
143
|
+
// collision exists, falling back to the first occurrence keeps the rest
|
|
144
|
+
// of the pass behaving the same as before the duplicate diagnostic was
|
|
145
|
+
// added (resolution still finds *something*; the duplicate diagnostic
|
|
146
|
+
// is what surfaces the underlying problem to the user).
|
|
147
|
+
const byName = new Map();
|
|
148
|
+
for (const [name, list] of byNameAll)
|
|
149
|
+
byName.set(name, list[0]);
|
|
75
150
|
for (const r of resources) {
|
|
76
151
|
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind))
|
|
77
152
|
continue;
|
|
@@ -119,9 +194,39 @@ export function validateReferences(resources, context) {
|
|
|
119
194
|
}
|
|
120
195
|
}
|
|
121
196
|
}
|
|
122
|
-
for (const val of
|
|
197
|
+
for (const { value: val, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
|
|
123
198
|
if (!val)
|
|
124
199
|
continue;
|
|
200
|
+
// `!ref <name>` sentinel — bare resource name marked at parse time as a
|
|
201
|
+
// reference. Look it up against the slot's x-telo-ref constraint exactly
|
|
202
|
+
// like the legacy bare-string path; the only difference is the value's
|
|
203
|
+
// shape (a TaggedSentinel rather than a raw string), which removed the
|
|
204
|
+
// string/inline ambiguity at the source.
|
|
205
|
+
if (isRefSentinel(val)) {
|
|
206
|
+
const refName = val.source;
|
|
207
|
+
const target = byName.get(refName) ?? visibleScopeManifests.find((m) => m.metadata?.name === refName);
|
|
208
|
+
if (!target) {
|
|
209
|
+
diagnostics.push({
|
|
210
|
+
severity: DiagnosticSeverity.Error,
|
|
211
|
+
code: "UNRESOLVED_REFERENCE",
|
|
212
|
+
source: SOURCE,
|
|
213
|
+
message: `${resourceLabel}: reference at '${concretePath}' → resource '${refName}' not found`,
|
|
214
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const kindErrors = checkKind(target.kind, entry, registry, aliases);
|
|
219
|
+
if (kindErrors.length > 0) {
|
|
220
|
+
diagnostics.push({
|
|
221
|
+
severity: DiagnosticSeverity.Error,
|
|
222
|
+
code: "REFERENCE_KIND_MISMATCH",
|
|
223
|
+
source: SOURCE,
|
|
224
|
+
message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
|
|
225
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
125
230
|
// Name-only reference (plain string) — look up by name to validate.
|
|
126
231
|
// Qualified references use "Kind.Name" format (e.g. "Http.Api.PaymentApi");
|
|
127
232
|
// extract the resource name from the last dot segment.
|
|
@@ -142,8 +247,8 @@ export function validateReferences(resources, context) {
|
|
|
142
247
|
severity: DiagnosticSeverity.Error,
|
|
143
248
|
code: "UNRESOLVED_REFERENCE",
|
|
144
249
|
source: SOURCE,
|
|
145
|
-
message: `${resourceLabel}: reference at '${
|
|
146
|
-
data: { resource: resourceData, filePath, path:
|
|
250
|
+
message: `${resourceLabel}: reference at '${concretePath}' → resource '${val}' not found`,
|
|
251
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
147
252
|
});
|
|
148
253
|
continue;
|
|
149
254
|
}
|
|
@@ -153,8 +258,8 @@ export function validateReferences(resources, context) {
|
|
|
153
258
|
severity: DiagnosticSeverity.Error,
|
|
154
259
|
code: "REFERENCE_KIND_MISMATCH",
|
|
155
260
|
source: SOURCE,
|
|
156
|
-
message: `${resourceLabel}: reference at '${
|
|
157
|
-
data: { resource: resourceData, filePath, path:
|
|
261
|
+
message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
|
|
262
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
158
263
|
});
|
|
159
264
|
}
|
|
160
265
|
continue;
|
|
@@ -171,8 +276,8 @@ export function validateReferences(resources, context) {
|
|
|
171
276
|
severity: DiagnosticSeverity.Error,
|
|
172
277
|
code: "INVALID_REFERENCE",
|
|
173
278
|
source: SOURCE,
|
|
174
|
-
message: `${resourceLabel}: reference at '${
|
|
175
|
-
data: { resource: resourceData, filePath, path:
|
|
279
|
+
message: `${resourceLabel}: reference at '${concretePath}' must have string 'kind' and 'name' fields`,
|
|
280
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
176
281
|
});
|
|
177
282
|
continue;
|
|
178
283
|
}
|
|
@@ -183,8 +288,8 @@ export function validateReferences(resources, context) {
|
|
|
183
288
|
severity: DiagnosticSeverity.Error,
|
|
184
289
|
code: "REFERENCE_KIND_MISMATCH",
|
|
185
290
|
source: SOURCE,
|
|
186
|
-
message: `${resourceLabel}: reference at '${
|
|
187
|
-
data: { resource: resourceData, filePath, path:
|
|
291
|
+
message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
|
|
292
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
188
293
|
});
|
|
189
294
|
}
|
|
190
295
|
// 3. Resolution check — resource with this name must exist.
|
|
@@ -195,8 +300,8 @@ export function validateReferences(resources, context) {
|
|
|
195
300
|
severity: DiagnosticSeverity.Error,
|
|
196
301
|
code: "UNRESOLVED_REFERENCE",
|
|
197
302
|
source: SOURCE,
|
|
198
|
-
message: `${resourceLabel}: reference at '${
|
|
199
|
-
data: { resource: resourceData, filePath, path:
|
|
303
|
+
message: `${resourceLabel}: reference at '${concretePath}' → resource '${refVal.name}' not found`,
|
|
304
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
200
305
|
});
|
|
201
306
|
}
|
|
202
307
|
}
|
|
@@ -279,7 +384,7 @@ export function validateReferences(resources, context) {
|
|
|
279
384
|
});
|
|
280
385
|
continue;
|
|
281
386
|
}
|
|
282
|
-
for (const fieldValue of
|
|
387
|
+
for (const { value: fieldValue, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
|
|
283
388
|
if (fieldValue == null)
|
|
284
389
|
continue;
|
|
285
390
|
const issues = registry.validateWithRefs(fieldValue, subSchema);
|
|
@@ -288,8 +393,8 @@ export function validateReferences(resources, context) {
|
|
|
288
393
|
severity: DiagnosticSeverity.Error,
|
|
289
394
|
code: "DEPENDENT_SCHEMA_MISMATCH",
|
|
290
395
|
source: SOURCE,
|
|
291
|
-
message: `${resourceLabel}: '${
|
|
292
|
-
data: { resource: resourceData, filePath, path:
|
|
396
|
+
message: `${resourceLabel}: '${concretePath}' does not match schema from '${anchorName}${jsonPointer}': ${issue}`,
|
|
397
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
293
398
|
});
|
|
294
399
|
}
|
|
295
400
|
}
|
|
@@ -309,9 +414,9 @@ export function validateReferences(resources, context) {
|
|
|
309
414
|
const anchorValues = resolveFieldValues(r, anchorPath);
|
|
310
415
|
if (anchorValues.length === 0)
|
|
311
416
|
continue; // anchor field not set — nothing to validate
|
|
312
|
-
const
|
|
313
|
-
for (let i = 0; i <
|
|
314
|
-
const fieldValue =
|
|
417
|
+
const fieldEntries = resolveFieldEntries(r, fieldPath);
|
|
418
|
+
for (let i = 0; i < fieldEntries.length; i++) {
|
|
419
|
+
const { value: fieldValue, path: concretePath } = fieldEntries[i];
|
|
315
420
|
if (fieldValue == null)
|
|
316
421
|
continue;
|
|
317
422
|
// For absolute paths, the single anchor applies to all field values.
|
|
@@ -328,8 +433,8 @@ export function validateReferences(resources, context) {
|
|
|
328
433
|
severity: DiagnosticSeverity.Error,
|
|
329
434
|
code: "SCHEMA_FROM_MISSING_PATH",
|
|
330
435
|
source: SOURCE,
|
|
331
|
-
message: `${resourceLabel}: x-telo-schema-from at '${
|
|
332
|
-
data: { resource: resourceData, filePath, path:
|
|
436
|
+
message: `${resourceLabel}: x-telo-schema-from at '${concretePath}' → kind '${refVal.kind}' has no schema`,
|
|
437
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
333
438
|
});
|
|
334
439
|
continue;
|
|
335
440
|
}
|
|
@@ -339,8 +444,8 @@ export function validateReferences(resources, context) {
|
|
|
339
444
|
severity: DiagnosticSeverity.Error,
|
|
340
445
|
code: "SCHEMA_FROM_MISSING_PATH",
|
|
341
446
|
source: SOURCE,
|
|
342
|
-
message: `${resourceLabel}: x-telo-schema-from at '${
|
|
343
|
-
data: { resource: resourceData, filePath, path:
|
|
447
|
+
message: `${resourceLabel}: x-telo-schema-from at '${concretePath}' → kind '${refVal.kind}' has no schema path '${jsonPointer}'`,
|
|
448
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
344
449
|
});
|
|
345
450
|
continue;
|
|
346
451
|
}
|
|
@@ -350,8 +455,8 @@ export function validateReferences(resources, context) {
|
|
|
350
455
|
severity: DiagnosticSeverity.Error,
|
|
351
456
|
code: "DEPENDENT_SCHEMA_MISMATCH",
|
|
352
457
|
source: SOURCE,
|
|
353
|
-
message: `${resourceLabel}: '${
|
|
354
|
-
data: { resource: resourceData, filePath, path:
|
|
458
|
+
message: `${resourceLabel}: '${concretePath}' does not match schema from '${refVal.kind}${jsonPointer}': ${issue}`,
|
|
459
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
355
460
|
});
|
|
356
461
|
}
|
|
357
462
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Stamp `metadata.source` and `metadata.sourceLine` on every non-system
|
|
4
|
+
* manifest that lacks them, returning a new array with cloned `metadata`
|
|
5
|
+
* objects for the affected entries.
|
|
6
|
+
*
|
|
7
|
+
* `StaticAnalyzer.analyze()` requires position info on every non-system
|
|
8
|
+
* manifest (the dedup that backs `DUPLICATE_RESOURCE_NAME` reads
|
|
9
|
+
* `(source, sourceLine)` to distinguish pipeline echoes from real
|
|
10
|
+
* collisions). Production callers — the `Loader`, `flattenForAnalyzer`,
|
|
11
|
+
* the telo-editor's `emitDocsFor`, the VSCode extension — all stamp
|
|
12
|
+
* positions already. This helper is the escape hatch for **programmatic
|
|
13
|
+
* callers** (tests, ad-hoc scripts) that construct `ResourceManifest`
|
|
14
|
+
* literals without going through a loader: it gives every otherwise-naked
|
|
15
|
+
* manifest a synthetic, deterministic position so the analyzer's
|
|
16
|
+
* invariant holds without each test having to spell positions out.
|
|
17
|
+
*
|
|
18
|
+
* The synthetic source defaults to `"<programmatic>"` — override via
|
|
19
|
+
* `source` when a stable, recognisable label helps diagnostic output.
|
|
20
|
+
* Each unstamped manifest gets a unique `sourceLine` (1-based array
|
|
21
|
+
* index) so two real duplicates supplied without positions retain
|
|
22
|
+
* distinct fingerprints and still trip `DUPLICATE_RESOURCE_NAME`.
|
|
23
|
+
*
|
|
24
|
+
* Manifests that already carry `metadata.source` and `metadata.sourceLine`
|
|
25
|
+
* pass through unchanged.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withSyntheticPositions(manifests: ResourceManifest[], source?: string): ResourceManifest[];
|
|
28
|
+
//# sourceMappingURL=with-synthetic-positions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-synthetic-positions.d.ts","sourceRoot":"","sources":["../src/with-synthetic-positions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,MAAM,GAAE,MAAyB,GAChC,gBAAgB,EAAE,CAgBpB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
|
|
2
|
+
/**
|
|
3
|
+
* Stamp `metadata.source` and `metadata.sourceLine` on every non-system
|
|
4
|
+
* manifest that lacks them, returning a new array with cloned `metadata`
|
|
5
|
+
* objects for the affected entries.
|
|
6
|
+
*
|
|
7
|
+
* `StaticAnalyzer.analyze()` requires position info on every non-system
|
|
8
|
+
* manifest (the dedup that backs `DUPLICATE_RESOURCE_NAME` reads
|
|
9
|
+
* `(source, sourceLine)` to distinguish pipeline echoes from real
|
|
10
|
+
* collisions). Production callers — the `Loader`, `flattenForAnalyzer`,
|
|
11
|
+
* the telo-editor's `emitDocsFor`, the VSCode extension — all stamp
|
|
12
|
+
* positions already. This helper is the escape hatch for **programmatic
|
|
13
|
+
* callers** (tests, ad-hoc scripts) that construct `ResourceManifest`
|
|
14
|
+
* literals without going through a loader: it gives every otherwise-naked
|
|
15
|
+
* manifest a synthetic, deterministic position so the analyzer's
|
|
16
|
+
* invariant holds without each test having to spell positions out.
|
|
17
|
+
*
|
|
18
|
+
* The synthetic source defaults to `"<programmatic>"` — override via
|
|
19
|
+
* `source` when a stable, recognisable label helps diagnostic output.
|
|
20
|
+
* Each unstamped manifest gets a unique `sourceLine` (1-based array
|
|
21
|
+
* index) so two real duplicates supplied without positions retain
|
|
22
|
+
* distinct fingerprints and still trip `DUPLICATE_RESOURCE_NAME`.
|
|
23
|
+
*
|
|
24
|
+
* Manifests that already carry `metadata.source` and `metadata.sourceLine`
|
|
25
|
+
* pass through unchanged.
|
|
26
|
+
*/
|
|
27
|
+
export function withSyntheticPositions(manifests, source = "<programmatic>") {
|
|
28
|
+
return manifests.map((m, i) => {
|
|
29
|
+
if (REF_VALIDATION_SKIP_KINDS.has(m.kind))
|
|
30
|
+
return m;
|
|
31
|
+
const meta = m.metadata;
|
|
32
|
+
const hasSource = typeof meta?.source === "string" && meta.source.length > 0;
|
|
33
|
+
const hasLine = typeof meta?.sourceLine === "number";
|
|
34
|
+
if (hasSource && hasLine)
|
|
35
|
+
return m;
|
|
36
|
+
return {
|
|
37
|
+
...m,
|
|
38
|
+
metadata: {
|
|
39
|
+
...m.metadata,
|
|
40
|
+
source: hasSource ? meta.source : source,
|
|
41
|
+
sourceLine: hasLine ? meta.sourceLine : i,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|