@telorun/analyzer 0.14.0 → 0.15.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/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +6 -8
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +6 -1
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +8 -4
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +3 -11
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +25 -22
- package/package.json +5 -4
- package/src/alias-resolver.ts +9 -0
- package/src/analyzer.ts +30 -5
- package/src/builtins.ts +6 -0
- package/src/flatten-for-analyzer.ts +20 -0
- package/src/resolve-ref-sentinels.ts +89 -44
- package/src/validate-references.ts +103 -8
- package/dist/adapters/http-adapter.d.ts +0 -10
- package/dist/adapters/http-adapter.d.ts.map +0 -1
- package/dist/adapters/http-adapter.js +0 -18
- package/dist/adapters/node-adapter.d.ts +0 -17
- package/dist/adapters/node-adapter.d.ts.map +0 -1
- package/dist/adapters/node-adapter.js +0 -71
- package/dist/adapters/registry-adapter.d.ts +0 -15
- package/dist/adapters/registry-adapter.d.ts.map +0 -1
- package/dist/adapters/registry-adapter.js +0 -53
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAuhB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAuhB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAimBvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CACP,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,EAI1B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,GACtC,gBAAgB,EAAE;IAqBrB,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
|
package/dist/analyzer.js
CHANGED
|
@@ -696,14 +696,12 @@ export class StaticAnalyzer {
|
|
|
696
696
|
if (m.kind === "Telo.Abstract") {
|
|
697
697
|
continue;
|
|
698
698
|
}
|
|
699
|
-
// Forwarded
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
const mModule = m.metadata?.module;
|
|
706
|
-
if (typeof mModule === "string" && !rootModules.has(mModule)) {
|
|
699
|
+
// Forwarded exports (flagged by flattenForAnalyzer) are an imported library's exported
|
|
700
|
+
// instances, already validated in their own module's standalone analysis; their
|
|
701
|
+
// `kind`/CEL are authored in that module's scope (e.g. `Self.X` → that module, not the
|
|
702
|
+
// consumer). Re-validating against the consumer's scope yields false UNDEFINED_KIND /
|
|
703
|
+
// scope-mismatch errors, so skip — they participate here only as resolution targets.
|
|
704
|
+
if (m.metadata?.forwardedExport === true) {
|
|
707
705
|
continue;
|
|
708
706
|
}
|
|
709
707
|
const resource = { kind: m.kind, name: m.metadata?.name };
|
package/dist/builtins.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EA4Z/C,CAAC"}
|
package/dist/builtins.js
CHANGED
|
@@ -396,7 +396,12 @@ export const KERNEL_BUILTINS = [
|
|
|
396
396
|
type: "object",
|
|
397
397
|
properties: {
|
|
398
398
|
kinds: { type: "array", items: { type: "string" } },
|
|
399
|
-
|
|
399
|
+
// `variables` / `secrets` are reserved on the resources.<Alias> value-flow
|
|
400
|
+
// surface, so a library may not export instances under those names.
|
|
401
|
+
resources: {
|
|
402
|
+
type: "array",
|
|
403
|
+
items: { type: "string", not: { enum: ["variables", "secrets"] } },
|
|
404
|
+
},
|
|
400
405
|
},
|
|
401
406
|
additionalProperties: true,
|
|
402
407
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flatten-for-analyzer.d.ts","sourceRoot":"","sources":["../src/flatten-for-analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAc,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAG/E;;;;;;;;;;;;;;;;;;6EAkB6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,gBAAgB,EAAE,
|
|
1
|
+
{"version":3,"file":"flatten-for-analyzer.d.ts","sourceRoot":"","sources":["../src/flatten-for-analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAc,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAG/E;;;;;;;;;;;;;;;;;;6EAkB6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,gBAAgB,EAAE,CA6EzE;AAED;;;;;6BAK6B;AAC7B,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAEzE"}
|
|
@@ -52,10 +52,14 @@ export function flattenForAnalyzer(graph) {
|
|
|
52
52
|
// Forward instances listed in the library's `exports.resources` so the
|
|
53
53
|
// consumer's analyzer can resolve, gate, kind-check, and draw topology edges
|
|
54
54
|
// for cross-module `!ref Alias.name`. The gate IS this forwarding — only
|
|
55
|
-
// exported names cross the boundary. `metadata.
|
|
56
|
-
//
|
|
57
|
-
// as
|
|
58
|
-
|
|
55
|
+
// exported names cross the boundary. `metadata.forwardedExport` marks them as
|
|
56
|
+
// cross-module resolution targets only (keyed by `metadata.module`), so ref
|
|
57
|
+
// resolution / validation treats them as targets, never as local sources to
|
|
58
|
+
// re-validate or walk.
|
|
59
|
+
result.push({
|
|
60
|
+
...m,
|
|
61
|
+
metadata: { ...m.metadata, forwardedExport: true },
|
|
62
|
+
});
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-ref-sentinels.d.ts","sourceRoot":"","sources":["../src/resolve-ref-sentinels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"resolve-ref-sentinels.d.ts","sourceRoot":"","sources":["../src/resolve-ref-sentinels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAQnE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,EAK5C,kBAAkB,GAAE,gBAAgB,EAAO,GAC1C,IAAI,CAwEN"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isRefSentinel } from "@telorun/templating";
|
|
2
|
-
import { isModuleKind } from "./module-kinds.js";
|
|
3
2
|
import { isRefEntry } from "./reference-field-map.js";
|
|
4
3
|
import { REF_RESOLUTION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
5
4
|
/**
|
|
@@ -28,17 +27,10 @@ export function resolveRefSentinels(resources, registry, aliases, aliasesByModul
|
|
|
28
27
|
// pass — which loads the entry module only — can still resolve `!ref Alias.name` against
|
|
29
28
|
// imported libraries' exported instances.
|
|
30
29
|
crossModuleTargets = []) {
|
|
31
|
-
const rootModules = new Set();
|
|
32
|
-
for (const r of resources) {
|
|
33
|
-
if (isModuleKind(r.kind) && typeof r.metadata?.name === "string") {
|
|
34
|
-
rootModules.add(r.metadata.name);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
30
|
const moduleOf = (r) => r.metadata?.module;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
31
|
+
// Forwarded exports are flagged by flattenForAnalyzer (`metadata.forwardedExport`); they're
|
|
32
|
+
// cross-module resolution targets only — never walked as local ref sources here.
|
|
33
|
+
const isForeign = (r) => r.metadata?.forwardedExport === true;
|
|
42
34
|
// Local resources resolve a bare / `Self.`-qualified name; forwarded foreign exports
|
|
43
35
|
// resolve an `Alias.`-qualified name keyed by (module, name).
|
|
44
36
|
const byName = new Map();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA4C/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAgftB"}
|
|
@@ -4,7 +4,6 @@ import { isInlineResource, resolveFieldEntries, resolveFieldValues } from "./ref
|
|
|
4
4
|
import { navigateJsonPointer } from "./schema-compat.js";
|
|
5
5
|
import { REF_VALIDATION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
6
6
|
import { DiagnosticSeverity } from "./types.js";
|
|
7
|
-
import { isModuleKind } from "./module-kinds.js";
|
|
8
7
|
const SOURCE = "telo-analyzer";
|
|
9
8
|
/**
|
|
10
9
|
* Checks whether `kind` satisfies the ref constraint in `entry`.
|
|
@@ -99,33 +98,37 @@ export function validateReferences(resources, context) {
|
|
|
99
98
|
// Forwarded foreign exports (an imported library's exported instances, carrying a
|
|
100
99
|
// metadata.module that isn't a root module) are resolution TARGETS only: excluded from
|
|
101
100
|
// duplicate detection and local name resolution, and never walked as ref sources.
|
|
102
|
-
const rootModules = new Set();
|
|
103
|
-
for (const r of resources) {
|
|
104
|
-
if (isModuleKind(r.kind) && typeof r.metadata?.name === "string") {
|
|
105
|
-
rootModules.add(r.metadata.name);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
101
|
const moduleOf = (r) => r.metadata?.module;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
102
|
+
// Forwarded exports are flagged by flattenForAnalyzer (`metadata.forwardedExport`); they're
|
|
103
|
+
// cross-module resolution targets only — excluded from duplicate detection and local name
|
|
104
|
+
// resolution, and never walked as ref sources.
|
|
105
|
+
const isForeign = (r) => r.metadata?.forwardedExport === true;
|
|
106
|
+
// Forwarded exported instances keyed `${module}\0${name}` — the lookup that resolves
|
|
107
|
+
// whether a cross-module `!ref Alias.name` names a real exported instance.
|
|
113
108
|
const byModuleName = new Map();
|
|
109
|
+
/** Modules whose import subtree was actually loaded in this analysis. A resolved
|
|
110
|
+
* `Telo.Import` carries `resolvedModuleName` (stamped only once the edge — and thus the
|
|
111
|
+
* imported module — resolved); forwarded exports carry `metadata.module`. Either marks
|
|
112
|
+
* the module loaded independent of how many instances it exports, so a loaded library
|
|
113
|
+
* that exports nothing still reports invalid cross-module refs, while partial single-file
|
|
114
|
+
* analysis (neither present) is skipped to avoid false `UNRESOLVED_REFERENCE`. */
|
|
115
|
+
const loadedModules = new Set();
|
|
114
116
|
for (const r of resources) {
|
|
117
|
+
if (r.kind === "Telo.Import") {
|
|
118
|
+
const m = r.metadata?.resolvedModuleName;
|
|
119
|
+
if (typeof m === "string")
|
|
120
|
+
loadedModules.add(m);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
115
123
|
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || !isForeign(r))
|
|
116
124
|
continue;
|
|
117
|
-
|
|
125
|
+
const m = moduleOf(r);
|
|
126
|
+
if (!m)
|
|
127
|
+
continue;
|
|
128
|
+
byModuleName.set(`${m}\0${r.metadata.name}`, r);
|
|
129
|
+
loadedModules.add(m);
|
|
118
130
|
}
|
|
119
|
-
|
|
120
|
-
* False in partial single-file analysis (the import subtree wasn't loaded) — used to
|
|
121
|
-
* skip cross-module resolution checks rather than emit false `UNRESOLVED_REFERENCE`. */
|
|
122
|
-
const moduleLoaded = (module) => {
|
|
123
|
-
const prefix = `${module}\0`;
|
|
124
|
-
for (const key of byModuleName.keys())
|
|
125
|
-
if (key.startsWith(prefix))
|
|
126
|
-
return true;
|
|
127
|
-
return false;
|
|
128
|
-
};
|
|
131
|
+
const moduleLoaded = (module) => loadedModules.has(module);
|
|
129
132
|
const localResources = resources.filter((r) => !isForeign(r));
|
|
130
133
|
const byNameAll = new Map();
|
|
131
134
|
const seen = new Set();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -42,15 +42,16 @@
|
|
|
42
42
|
"ajv-formats": "^3.0.1",
|
|
43
43
|
"jsonpath-plus": "^10.3.0",
|
|
44
44
|
"yaml": "^2.8.3",
|
|
45
|
-
"@telorun/templating": "0.4.
|
|
45
|
+
"@telorun/templating": "0.4.1"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"typescript": "^5.0.0",
|
|
50
|
-
"vitest": "^2.1.8"
|
|
50
|
+
"vitest": "^2.1.8",
|
|
51
|
+
"@telorun/sdk": "0.16.0"
|
|
51
52
|
},
|
|
52
53
|
"peerDependencies": {
|
|
53
|
-
"@telorun/sdk": "
|
|
54
|
+
"@telorun/sdk": "*"
|
|
54
55
|
},
|
|
55
56
|
"scripts": {
|
|
56
57
|
"build": "tsc -p tsconfig.lib.json",
|
package/src/alias-resolver.ts
CHANGED
|
@@ -11,6 +11,15 @@ export class AliasResolver {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** Real module name an alias points at (e.g. "Console" → "console"), or undefined.
|
|
15
|
+
* Used to resolve an alias-qualified instance reference "Console.writeLine" to the
|
|
16
|
+
* forwarded resource declared in that module. The `exports.resources` gate is enforced
|
|
17
|
+
* upstream by `flattenForAnalyzer` (only exported instances are forwarded), so a name
|
|
18
|
+
* that isn't exported simply won't be found. */
|
|
19
|
+
moduleForAlias(alias: string): string | undefined {
|
|
20
|
+
return this.importAliases.get(alias);
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
/** Resolves "Http.Api" → "http-server.Api". Returns undefined if alias is unknown. */
|
|
15
24
|
resolveKind(kind: string): string | undefined {
|
|
16
25
|
if (!kind) {
|
package/src/analyzer.ts
CHANGED
|
@@ -633,16 +633,19 @@ export class StaticAnalyzer {
|
|
|
633
633
|
// through the same alias machinery as user-declared Telo.Imports —
|
|
634
634
|
// honours the library's `exports.kinds` list, no special cases.
|
|
635
635
|
if (moduleName) {
|
|
636
|
-
|
|
636
|
+
// `Self` resolves the library's own kinds UNGATED — a library may reference
|
|
637
|
+
// its own kinds regardless of `exports.kinds`, which gates importers, not
|
|
638
|
+
// internal use. This is what lets a library declare an instance of a kind it
|
|
639
|
+
// does not export (e.g. console's `writeLine`) to enforce a singleton.
|
|
637
640
|
if (rootModules.has(moduleName)) {
|
|
638
|
-
aliases.registerImport("Self", moduleName,
|
|
641
|
+
aliases.registerImport("Self", moduleName, []);
|
|
639
642
|
} else {
|
|
640
643
|
let libResolver = aliasesByModule.get(moduleName);
|
|
641
644
|
if (!libResolver) {
|
|
642
645
|
libResolver = new AliasResolver();
|
|
643
646
|
aliasesByModule.set(moduleName, libResolver);
|
|
644
647
|
}
|
|
645
|
-
libResolver.registerImport("Self", moduleName,
|
|
648
|
+
libResolver.registerImport("Self", moduleName, []);
|
|
646
649
|
}
|
|
647
650
|
}
|
|
648
651
|
}
|
|
@@ -825,6 +828,15 @@ export class StaticAnalyzer {
|
|
|
825
828
|
continue;
|
|
826
829
|
}
|
|
827
830
|
|
|
831
|
+
// Forwarded exports (flagged by flattenForAnalyzer) are an imported library's exported
|
|
832
|
+
// instances, already validated in their own module's standalone analysis; their
|
|
833
|
+
// `kind`/CEL are authored in that module's scope (e.g. `Self.X` → that module, not the
|
|
834
|
+
// consumer). Re-validating against the consumer's scope yields false UNDEFINED_KIND /
|
|
835
|
+
// scope-mismatch errors, so skip — they participate here only as resolution targets.
|
|
836
|
+
if ((m.metadata as { forwardedExport?: boolean } | undefined)?.forwardedExport === true) {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
|
|
828
840
|
const resource = { kind: m.kind, name: m.metadata?.name as string };
|
|
829
841
|
|
|
830
842
|
// Resolve kind through alias if needed; direct lookup takes priority so that
|
|
@@ -1196,7 +1208,14 @@ export class StaticAnalyzer {
|
|
|
1196
1208
|
);
|
|
1197
1209
|
}
|
|
1198
1210
|
|
|
1199
|
-
normalize(
|
|
1211
|
+
normalize(
|
|
1212
|
+
manifests: ResourceManifest[],
|
|
1213
|
+
registry: AnalysisRegistry,
|
|
1214
|
+
// Forwarded foreign exports used only as cross-module resolution targets (see
|
|
1215
|
+
// resolveRefSentinels). The kernel passes its analyzer-flattened set so the
|
|
1216
|
+
// entry-only runtime pass can still resolve `!ref Alias.name`.
|
|
1217
|
+
crossModuleTargets?: ResourceManifest[],
|
|
1218
|
+
): ResourceManifest[] {
|
|
1200
1219
|
const ctx = registry._context();
|
|
1201
1220
|
const normalized = normalizeInlineResources(
|
|
1202
1221
|
manifests,
|
|
@@ -1207,7 +1226,13 @@ export class StaticAnalyzer {
|
|
|
1207
1226
|
// Resolve !ref sentinels after normalize so both the original and
|
|
1208
1227
|
// inline-extracted manifests get their refs canonicalized to
|
|
1209
1228
|
// {kind, name} for the kernel that consumes this output.
|
|
1210
|
-
resolveRefSentinels(
|
|
1229
|
+
resolveRefSentinels(
|
|
1230
|
+
normalized,
|
|
1231
|
+
ctx.definitions!,
|
|
1232
|
+
ctx.aliases,
|
|
1233
|
+
ctx.aliasesByModule,
|
|
1234
|
+
crossModuleTargets ?? [],
|
|
1235
|
+
);
|
|
1211
1236
|
return normalized;
|
|
1212
1237
|
}
|
|
1213
1238
|
|
package/src/builtins.ts
CHANGED
|
@@ -398,6 +398,12 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
398
398
|
type: "object",
|
|
399
399
|
properties: {
|
|
400
400
|
kinds: { type: "array", items: { type: "string" } },
|
|
401
|
+
// `variables` / `secrets` are reserved on the resources.<Alias> value-flow
|
|
402
|
+
// surface, so a library may not export instances under those names.
|
|
403
|
+
resources: {
|
|
404
|
+
type: "array",
|
|
405
|
+
items: { type: "string", not: { enum: ["variables", "secrets"] } },
|
|
406
|
+
},
|
|
401
407
|
},
|
|
402
408
|
additionalProperties: true,
|
|
403
409
|
},
|
|
@@ -44,6 +44,10 @@ export function flattenForAnalyzer(graph: LoadedGraph): ResourceManifest[] {
|
|
|
44
44
|
if (!targetModule) continue;
|
|
45
45
|
|
|
46
46
|
const stamped = collectModuleManifests(targetModule);
|
|
47
|
+
const libDoc = stamped.find((m) => isModuleKind(m.kind));
|
|
48
|
+
const exportedResources = new Set<string>(
|
|
49
|
+
(libDoc as { exports?: { resources?: string[] } } | undefined)?.exports?.resources ?? [],
|
|
50
|
+
);
|
|
47
51
|
for (const m of stamped) {
|
|
48
52
|
if (
|
|
49
53
|
m.kind === "Telo.Definition" ||
|
|
@@ -51,6 +55,22 @@ export function flattenForAnalyzer(graph: LoadedGraph): ResourceManifest[] {
|
|
|
51
55
|
m.kind === "Telo.Import"
|
|
52
56
|
) {
|
|
53
57
|
result.push(m);
|
|
58
|
+
} else if (
|
|
59
|
+
!isModuleKind(m.kind) &&
|
|
60
|
+
typeof m.metadata?.name === "string" &&
|
|
61
|
+
exportedResources.has(m.metadata.name as string)
|
|
62
|
+
) {
|
|
63
|
+
// Forward instances listed in the library's `exports.resources` so the
|
|
64
|
+
// consumer's analyzer can resolve, gate, kind-check, and draw topology edges
|
|
65
|
+
// for cross-module `!ref Alias.name`. The gate IS this forwarding — only
|
|
66
|
+
// exported names cross the boundary. `metadata.forwardedExport` marks them as
|
|
67
|
+
// cross-module resolution targets only (keyed by `metadata.module`), so ref
|
|
68
|
+
// resolution / validation treats them as targets, never as local sources to
|
|
69
|
+
// re-validate or walk.
|
|
70
|
+
result.push({
|
|
71
|
+
...m,
|
|
72
|
+
metadata: { ...m.metadata, forwardedExport: true } as ResourceManifest["metadata"],
|
|
73
|
+
});
|
|
54
74
|
}
|
|
55
75
|
}
|
|
56
76
|
}
|
|
@@ -5,43 +5,100 @@ import type { DefinitionRegistry } from "./definition-registry.js";
|
|
|
5
5
|
import { isRefEntry } from "./reference-field-map.js";
|
|
6
6
|
import { REF_RESOLUTION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
7
7
|
|
|
8
|
+
/** Resolved ref shape written in place of a `!ref` sentinel. `alias` is set only for
|
|
9
|
+
* cross-module references (resolved into an imported library's exported instance). */
|
|
10
|
+
type ResolvedRef = { kind: string; name: string; alias?: string };
|
|
11
|
+
|
|
8
12
|
/**
|
|
9
13
|
* Walks every `x-telo-ref` slot in every non-system resource and rewrites
|
|
10
|
-
* `!ref <name>` sentinels in-place to `{kind
|
|
14
|
+
* `!ref <name>` sentinels in-place to `{kind, name}` (local) or
|
|
15
|
+
* `{kind, name, alias}` (cross-module).
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* Reference grammar — the tag's source string is split on the FIRST dot:
|
|
18
|
+
* - `!ref writeLine` → local resource `writeLine`
|
|
19
|
+
* - `!ref Self.writeLine` → local resource `writeLine` (explicit self-qualifier)
|
|
20
|
+
* - `!ref Console.writeLine` → instance `writeLine` exported by the import aliased
|
|
21
|
+
* `Console`, resolved against the forwarded foreign set
|
|
17
22
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
23
|
+
* Aliases are PascalCase identifiers without dots and resource names carry no dots
|
|
24
|
+
* (enforced as a hard diagnostic), so the first-dot split is unambiguous. When the
|
|
25
|
+
* name doesn't resolve, the sentinel is left in place so `validateReferences` emits the
|
|
26
|
+
* `UNRESOLVED_REFERENCE` diagnostic with full context.
|
|
22
27
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* to the path-encoding rules of `resolveFieldEntries` — any new path
|
|
27
|
-
* marker would silently break this writer. Descending directly avoids
|
|
28
|
-
* that coupling.
|
|
28
|
+
* Forwarded foreign resources (an imported library's exported instances, carrying a
|
|
29
|
+
* `metadata.module` that isn't a root module) are resolution TARGETS only — they are not
|
|
30
|
+
* re-walked as sources here, since their own ref slots belong to their own module scope.
|
|
29
31
|
*/
|
|
30
32
|
export function resolveRefSentinels(
|
|
31
33
|
resources: ResourceManifest[],
|
|
32
34
|
registry: DefinitionRegistry,
|
|
33
35
|
aliases?: AliasResolver,
|
|
34
36
|
aliasesByModule?: Map<string, AliasResolver>,
|
|
37
|
+
// Extra foreign resources used only as cross-module resolution TARGETS (not mutated, not
|
|
38
|
+
// walked as sources). The kernel passes the analyzer-flattened set here so the runtime
|
|
39
|
+
// pass — which loads the entry module only — can still resolve `!ref Alias.name` against
|
|
40
|
+
// imported libraries' exported instances.
|
|
41
|
+
crossModuleTargets: ResourceManifest[] = [],
|
|
35
42
|
): void {
|
|
43
|
+
const moduleOf = (r: ResourceManifest): string | undefined =>
|
|
44
|
+
(r.metadata as { module?: string } | undefined)?.module;
|
|
45
|
+
// Forwarded exports are flagged by flattenForAnalyzer (`metadata.forwardedExport`); they're
|
|
46
|
+
// cross-module resolution targets only — never walked as local ref sources here.
|
|
47
|
+
const isForeign = (r: ResourceManifest): boolean =>
|
|
48
|
+
(r.metadata as { forwardedExport?: boolean } | undefined)?.forwardedExport === true;
|
|
49
|
+
|
|
50
|
+
// Local resources resolve a bare / `Self.`-qualified name; forwarded foreign exports
|
|
51
|
+
// resolve an `Alias.`-qualified name keyed by (module, name).
|
|
36
52
|
const byName = new Map<string, ResourceManifest>();
|
|
53
|
+
const byModuleName = new Map<string, ResourceManifest>();
|
|
37
54
|
for (const r of resources) {
|
|
38
|
-
if (r.metadata?.name
|
|
39
|
-
|
|
55
|
+
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind)) continue;
|
|
56
|
+
const name = r.metadata.name as string;
|
|
57
|
+
if (isForeign(r)) {
|
|
58
|
+
byModuleName.set(`${moduleOf(r)}\0${name}`, r);
|
|
59
|
+
} else {
|
|
60
|
+
byName.set(name, r);
|
|
40
61
|
}
|
|
41
62
|
}
|
|
63
|
+
for (const r of crossModuleTargets) {
|
|
64
|
+
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || !isForeign(r)) continue;
|
|
65
|
+
byModuleName.set(`${moduleOf(r)}\0${r.metadata.name as string}`, r);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const resolveTarget = (source: string): ResolvedRef | undefined => {
|
|
69
|
+
const dot = source.indexOf(".");
|
|
70
|
+
if (dot === -1) {
|
|
71
|
+
const t = byName.get(source);
|
|
72
|
+
return t ? { kind: t.kind as string, name: source } : undefined;
|
|
73
|
+
}
|
|
74
|
+
const alias = source.slice(0, dot);
|
|
75
|
+
const name = source.slice(dot + 1);
|
|
76
|
+
if (alias === "Self") {
|
|
77
|
+
const t = byName.get(name);
|
|
78
|
+
return t ? { kind: t.kind as string, name } : undefined;
|
|
79
|
+
}
|
|
80
|
+
const module = aliases?.moduleForAlias(alias);
|
|
81
|
+
if (module) {
|
|
82
|
+
const t = byModuleName.get(`${module}\0${name}`);
|
|
83
|
+
if (t) {
|
|
84
|
+
// The foreign instance's `kind` is authored in ITS module's scope (e.g.
|
|
85
|
+
// `Self.WriteLine`); canonicalize to a scope-independent `<module>.<Kind>` for the
|
|
86
|
+
// consumer's kind check. `Self.` maps to the owning module directly — the forwarded
|
|
87
|
+
// library's Library doc (hence its `Self` alias) isn't in the consumer's manifest
|
|
88
|
+
// set — while other alias prefixes resolve via that module's forwarded import scope.
|
|
89
|
+
const rawKind = t.kind as string;
|
|
90
|
+
const foreignKind = rawKind.startsWith("Self.")
|
|
91
|
+
? `${module}.${rawKind.slice("Self.".length)}`
|
|
92
|
+
: aliasesByModule?.get(module)?.resolveKind(rawKind) ?? rawKind;
|
|
93
|
+
return { kind: foreignKind, name, alias };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
};
|
|
42
98
|
|
|
43
99
|
for (const r of resources) {
|
|
44
100
|
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind)) continue;
|
|
101
|
+
if (isForeign(r)) continue;
|
|
45
102
|
|
|
46
103
|
const fieldMap =
|
|
47
104
|
aliases && aliasesByModule
|
|
@@ -51,45 +108,33 @@ export function resolveRefSentinels(
|
|
|
51
108
|
|
|
52
109
|
for (const [fieldPath, entry] of fieldMap) {
|
|
53
110
|
if (!isRefEntry(entry)) continue;
|
|
54
|
-
|
|
111
|
+
descend(r as Record<string, unknown>, fieldPath.split("."), resolveTarget);
|
|
55
112
|
}
|
|
56
113
|
}
|
|
57
114
|
}
|
|
58
115
|
|
|
59
|
-
/** Walks `obj` along `fieldPath` (dot notation with `[]` for arrays and
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* via `byName`. Mutates the parent container in place; no string-path
|
|
63
|
-
* round-trip. */
|
|
64
|
-
function replaceSentinelsAtPath(
|
|
65
|
-
obj: Record<string, unknown>,
|
|
66
|
-
fieldPath: string,
|
|
67
|
-
byName: Map<string, ResourceManifest>,
|
|
68
|
-
): void {
|
|
69
|
-
const parts = fieldPath.split(".");
|
|
70
|
-
descend(obj, parts, byName);
|
|
71
|
-
}
|
|
72
|
-
|
|
116
|
+
/** Walks `obj` along `fieldPath` parts (dot notation with `[]` for arrays and `{}` for
|
|
117
|
+
* additionalProperties-typed maps) and replaces any `!ref` sentinel at the terminal slot
|
|
118
|
+
* with its resolved `{kind, name, alias?}`. Mutates the parent container in place. */
|
|
73
119
|
function descend(
|
|
74
120
|
obj: unknown,
|
|
75
121
|
parts: string[],
|
|
76
|
-
|
|
122
|
+
resolve: (source: string) => ResolvedRef | undefined,
|
|
77
123
|
): void {
|
|
78
124
|
if (obj == null || typeof obj !== "object" || parts.length === 0) return;
|
|
79
125
|
const [head, ...rest] = parts;
|
|
80
126
|
|
|
81
|
-
// Map iteration: descend into every value of the current object.
|
|
82
127
|
if (head === "{}") {
|
|
83
128
|
const container = obj as Record<string, unknown>;
|
|
84
129
|
for (const key of Object.keys(container)) {
|
|
85
130
|
const child = container[key];
|
|
86
131
|
if (rest.length === 0) {
|
|
87
132
|
if (isRefSentinel(child)) {
|
|
88
|
-
const target =
|
|
89
|
-
if (target) container[key] =
|
|
133
|
+
const target = resolve(child.source);
|
|
134
|
+
if (target) container[key] = target;
|
|
90
135
|
}
|
|
91
136
|
} else {
|
|
92
|
-
descend(child, rest,
|
|
137
|
+
descend(child, rest, resolve);
|
|
93
138
|
}
|
|
94
139
|
}
|
|
95
140
|
return;
|
|
@@ -107,21 +152,21 @@ function descend(
|
|
|
107
152
|
if (rest.length === 0) {
|
|
108
153
|
const elem = val[i];
|
|
109
154
|
if (isRefSentinel(elem)) {
|
|
110
|
-
const target =
|
|
111
|
-
if (target) val[i] =
|
|
155
|
+
const target = resolve(elem.source);
|
|
156
|
+
if (target) val[i] = target;
|
|
112
157
|
}
|
|
113
158
|
} else {
|
|
114
|
-
descend(val[i], rest,
|
|
159
|
+
descend(val[i], rest, resolve);
|
|
115
160
|
}
|
|
116
161
|
}
|
|
117
162
|
} else {
|
|
118
163
|
if (rest.length === 0) {
|
|
119
164
|
if (isRefSentinel(val)) {
|
|
120
|
-
const target =
|
|
121
|
-
if (target) container[key] =
|
|
165
|
+
const target = resolve(val.source);
|
|
166
|
+
if (target) container[key] = target;
|
|
122
167
|
}
|
|
123
168
|
} else {
|
|
124
|
-
descend(val, rest,
|
|
169
|
+
descend(val, rest, resolve);
|
|
125
170
|
}
|
|
126
171
|
}
|
|
127
172
|
}
|
|
@@ -107,10 +107,46 @@ export function validateReferences(
|
|
|
107
107
|
// source, different sourceLine) keep separate fingerprints and still
|
|
108
108
|
// trip the diagnostic. `analyze()` enforces that every non-system
|
|
109
109
|
// manifest carries both positional fields — no defensive guard needed.
|
|
110
|
+
// Forwarded foreign exports (an imported library's exported instances, carrying a
|
|
111
|
+
// metadata.module that isn't a root module) are resolution TARGETS only: excluded from
|
|
112
|
+
// duplicate detection and local name resolution, and never walked as ref sources.
|
|
113
|
+
const moduleOf = (r: ResourceManifest): string | undefined =>
|
|
114
|
+
(r.metadata as { module?: string } | undefined)?.module;
|
|
115
|
+
// Forwarded exports are flagged by flattenForAnalyzer (`metadata.forwardedExport`); they're
|
|
116
|
+
// cross-module resolution targets only — excluded from duplicate detection and local name
|
|
117
|
+
// resolution, and never walked as ref sources.
|
|
118
|
+
const isForeign = (r: ResourceManifest): boolean =>
|
|
119
|
+
(r.metadata as { forwardedExport?: boolean } | undefined)?.forwardedExport === true;
|
|
120
|
+
// Forwarded exported instances keyed `${module}\0${name}` — the lookup that resolves
|
|
121
|
+
// whether a cross-module `!ref Alias.name` names a real exported instance.
|
|
122
|
+
const byModuleName = new Map<string, ResourceManifest>();
|
|
123
|
+
/** Modules whose import subtree was actually loaded in this analysis. A resolved
|
|
124
|
+
* `Telo.Import` carries `resolvedModuleName` (stamped only once the edge — and thus the
|
|
125
|
+
* imported module — resolved); forwarded exports carry `metadata.module`. Either marks
|
|
126
|
+
* the module loaded independent of how many instances it exports, so a loaded library
|
|
127
|
+
* that exports nothing still reports invalid cross-module refs, while partial single-file
|
|
128
|
+
* analysis (neither present) is skipped to avoid false `UNRESOLVED_REFERENCE`. */
|
|
129
|
+
const loadedModules = new Set<string>();
|
|
130
|
+
for (const r of resources) {
|
|
131
|
+
if (r.kind === "Telo.Import") {
|
|
132
|
+
const m = (r.metadata as { resolvedModuleName?: unknown } | undefined)?.resolvedModuleName;
|
|
133
|
+
if (typeof m === "string") loadedModules.add(m);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || !isForeign(r)) continue;
|
|
137
|
+
const m = moduleOf(r);
|
|
138
|
+
if (!m) continue;
|
|
139
|
+
byModuleName.set(`${m}\0${r.metadata.name as string}`, r);
|
|
140
|
+
loadedModules.add(m);
|
|
141
|
+
}
|
|
142
|
+
const moduleLoaded = (module: string): boolean => loadedModules.has(module);
|
|
143
|
+
const localResources = resources.filter((r) => !isForeign(r));
|
|
144
|
+
|
|
110
145
|
const byNameAll = new Map<string, ResourceManifest[]>();
|
|
111
146
|
const seen = new Set<string>();
|
|
112
147
|
for (const r of resources) {
|
|
113
|
-
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || r.kind === "Telo.Import")
|
|
148
|
+
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || r.kind === "Telo.Import" || isForeign(r))
|
|
149
|
+
continue;
|
|
114
150
|
const name = r.metadata.name as string;
|
|
115
151
|
// `analyze()` guarantees both fields are present on non-system manifests.
|
|
116
152
|
const meta = r.metadata as unknown as { source: string; sourceLine: number };
|
|
@@ -148,6 +184,33 @@ export function validateReferences(
|
|
|
148
184
|
});
|
|
149
185
|
}
|
|
150
186
|
}
|
|
187
|
+
// A resource name must contain no dot. The `!ref` resolver splits the tag's source on
|
|
188
|
+
// the first dot to separate an import alias from the resource name, so a dotted name
|
|
189
|
+
// would mis-resolve into a cross-module lookup. This is the load-bearing invariant of
|
|
190
|
+
// the reference grammar, so it is enforced here rather than left to the (unenforced)
|
|
191
|
+
// casing convention.
|
|
192
|
+
for (const [name, list] of byNameAll) {
|
|
193
|
+
if (!name.includes(".")) continue;
|
|
194
|
+
for (const r of list) {
|
|
195
|
+
const m = r.metadata as { source?: string; sourceLine?: number } | undefined;
|
|
196
|
+
const range =
|
|
197
|
+
typeof m?.sourceLine === "number"
|
|
198
|
+
? {
|
|
199
|
+
start: { line: m.sourceLine, character: 0 },
|
|
200
|
+
end: { line: m.sourceLine, character: Number.MAX_SAFE_INTEGER },
|
|
201
|
+
}
|
|
202
|
+
: undefined;
|
|
203
|
+
diagnostics.push({
|
|
204
|
+
severity: DiagnosticSeverity.Error,
|
|
205
|
+
code: "INVALID_RESOURCE_NAME",
|
|
206
|
+
source: SOURCE,
|
|
207
|
+
message: `${r.kind}/${name}: resource name must not contain '.' — in a '!ref' the '.' separates an import alias from the resource name`,
|
|
208
|
+
...(range ? { range } : {}),
|
|
209
|
+
data: { resource: { kind: r.kind, name }, filePath: m?.source, path: "metadata.name" },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
151
214
|
// Single-resource map for the resolution / scope lookups below — when a
|
|
152
215
|
// collision exists, falling back to the first occurrence keeps the rest
|
|
153
216
|
// of the pass behaving the same as before the duplicate diagnostic was
|
|
@@ -161,7 +224,7 @@ export function validateReferences(
|
|
|
161
224
|
// enclosure (`inScope`) and the scope manifests visible to it — so this
|
|
162
225
|
// handler only validates, it does not re-walk.
|
|
163
226
|
visitManifest(
|
|
164
|
-
|
|
227
|
+
localResources,
|
|
165
228
|
registry,
|
|
166
229
|
{
|
|
167
230
|
onRef: (e) => {
|
|
@@ -178,14 +241,37 @@ export function validateReferences(
|
|
|
178
241
|
// string/inline ambiguity at the source.
|
|
179
242
|
if (isRefSentinel(val)) {
|
|
180
243
|
const refName = val.source;
|
|
244
|
+
const dot = refName.indexOf(".");
|
|
245
|
+
const aliasPrefix = dot > 0 ? refName.slice(0, dot) : undefined;
|
|
246
|
+
|
|
247
|
+
// Cross-module sentinel left unresolved by Phase 2.5 — it qualifies an import
|
|
248
|
+
// alias. If that module's exports are loaded in this analysis, the miss is real
|
|
249
|
+
// (name not in exports.resources, or a typo); if not (partial single-file
|
|
250
|
+
// analysis), skip rather than emit a false UNRESOLVED_REFERENCE.
|
|
251
|
+
if (aliasPrefix && aliasPrefix !== "Self" && aliases.hasAlias(aliasPrefix)) {
|
|
252
|
+
const module = aliases.moduleForAlias(aliasPrefix);
|
|
253
|
+
if (module && !moduleLoaded(module)) return;
|
|
254
|
+
diagnostics.push({
|
|
255
|
+
severity: DiagnosticSeverity.Error,
|
|
256
|
+
code: "UNRESOLVED_REFERENCE",
|
|
257
|
+
source: SOURCE,
|
|
258
|
+
message: `${resourceLabel}: reference at '${concretePath}' → '${refName}' is not exported by module '${module ?? aliasPrefix}' (add it to exports.resources)`,
|
|
259
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Local reference (bare name or explicit `Self.`-qualified).
|
|
265
|
+
const localName = aliasPrefix === "Self" ? refName.slice(dot + 1) : refName;
|
|
181
266
|
const target =
|
|
182
|
-
byName.get(
|
|
267
|
+
byName.get(localName) ??
|
|
268
|
+
visibleScopeManifests.find((m) => m.metadata?.name === localName);
|
|
183
269
|
if (!target) {
|
|
184
270
|
diagnostics.push({
|
|
185
271
|
severity: DiagnosticSeverity.Error,
|
|
186
272
|
code: "UNRESOLVED_REFERENCE",
|
|
187
273
|
source: SOURCE,
|
|
188
|
-
message: `${resourceLabel}: reference at '${concretePath}' → resource '${
|
|
274
|
+
message: `${resourceLabel}: reference at '${concretePath}' → resource '${localName}' not found`,
|
|
189
275
|
data: { resource: resourceData, filePath, path: concretePath },
|
|
190
276
|
});
|
|
191
277
|
return;
|
|
@@ -280,9 +366,18 @@ export function validateReferences(
|
|
|
280
366
|
}
|
|
281
367
|
|
|
282
368
|
// 3. Resolution check — resource with this name must exist.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
369
|
+
let exists: boolean;
|
|
370
|
+
if (typeof refVal.alias === "string" && refVal.alias !== "Self") {
|
|
371
|
+
// Cross-module ref resolved by Phase 2.5. Validate against the forwarded
|
|
372
|
+
// exports when loaded; in partial context (module not loaded) assume resolvable.
|
|
373
|
+
const module = aliases.moduleForAlias(refVal.alias);
|
|
374
|
+
exists =
|
|
375
|
+
!module || !moduleLoaded(module) || byModuleName.has(`${module}\0${refVal.name}`);
|
|
376
|
+
} else {
|
|
377
|
+
exists =
|
|
378
|
+
byName.has(refVal.name) ||
|
|
379
|
+
visibleScopeManifests.some((m) => m.metadata?.name === refVal.name);
|
|
380
|
+
}
|
|
286
381
|
if (!exists) {
|
|
287
382
|
diagnostics.push({
|
|
288
383
|
severity: DiagnosticSeverity.Error,
|
|
@@ -303,7 +398,7 @@ export function validateReferences(
|
|
|
303
398
|
// validate the field value against the resulting sub-schema. Driven off the base map
|
|
304
399
|
// (un-expanded) so each schema-from slot is seen as its own site.
|
|
305
400
|
visitManifest(
|
|
306
|
-
|
|
401
|
+
localResources,
|
|
307
402
|
registry,
|
|
308
403
|
{
|
|
309
404
|
onSchemaFrom: (e) => {
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { type ManifestAdapter } from "../types.js";
|
|
2
|
-
export declare class HttpAdapter implements ManifestAdapter {
|
|
3
|
-
supports(url: string): boolean;
|
|
4
|
-
read(url: string): Promise<{
|
|
5
|
-
text: string;
|
|
6
|
-
source: string;
|
|
7
|
-
}>;
|
|
8
|
-
resolveRelative(base: string, relative: string): string;
|
|
9
|
-
}
|
|
10
|
-
//# sourceMappingURL=http-adapter.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"http-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/http-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9E,qBAAa,WAAY,YAAW,eAAe;IACjD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAWlE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;CAIxD"}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_MANIFEST_FILENAME } from "../types.js";
|
|
2
|
-
export class HttpAdapter {
|
|
3
|
-
supports(url) {
|
|
4
|
-
return url.startsWith("http://") || url.startsWith("https://");
|
|
5
|
-
}
|
|
6
|
-
async read(url) {
|
|
7
|
-
const fetchUrl = url.includes(".yaml") ? url : `${url}/${DEFAULT_MANIFEST_FILENAME}`;
|
|
8
|
-
const response = await fetch(fetchUrl);
|
|
9
|
-
if (!response.ok) {
|
|
10
|
-
throw new Error(`Failed to fetch manifest from ${fetchUrl}: ${response.status} ${response.statusText}`);
|
|
11
|
-
}
|
|
12
|
-
return { text: await response.text(), source: fetchUrl };
|
|
13
|
-
}
|
|
14
|
-
resolveRelative(base, relative) {
|
|
15
|
-
const baseDir = base.endsWith("/") ? base : base.slice(0, base.lastIndexOf("/") + 1);
|
|
16
|
-
return new URL(relative, baseDir).href;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type ManifestAdapter } from "../types.js";
|
|
2
|
-
/** Node.js fs-based ManifestAdapter for local files. Not browser-compatible. */
|
|
3
|
-
export declare class NodeAdapter implements ManifestAdapter {
|
|
4
|
-
private readonly cwd;
|
|
5
|
-
constructor(cwd?: string);
|
|
6
|
-
supports(url: string): boolean;
|
|
7
|
-
read(url: string): Promise<{
|
|
8
|
-
text: string;
|
|
9
|
-
source: string;
|
|
10
|
-
}>;
|
|
11
|
-
resolveRelative(base: string, relative: string): string;
|
|
12
|
-
expandGlob(base: string, patterns: string[]): Promise<string[]>;
|
|
13
|
-
resolveOwnerOf(fileUrl: string): Promise<string | null>;
|
|
14
|
-
}
|
|
15
|
-
/** @deprecated Use `new NodeAdapter(cwd)` instead */
|
|
16
|
-
export declare function createNodeAdapter(cwd?: string): ManifestAdapter;
|
|
17
|
-
//# sourceMappingURL=node-adapter.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"node-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/node-adapter.ts"],"names":[],"mappings":"AAIA,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAM9E,gFAAgF;AAChF,qBAAa,WAAY,YAAW,eAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,GAAE,MAAsB;IAExD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAUxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IASlE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAKjD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAc/D,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAoB9D;AAED,qDAAqD;AACrD,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAsB,GAAG,eAAe,CAE9E"}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs/promises";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
import { minimatch } from "minimatch";
|
|
5
|
-
import { DEFAULT_MANIFEST_FILENAME } from "../types.js";
|
|
6
|
-
function toFilePath(url) {
|
|
7
|
-
return url.startsWith("file://") ? fileURLToPath(url) : url;
|
|
8
|
-
}
|
|
9
|
-
/** Node.js fs-based ManifestAdapter for local files. Not browser-compatible. */
|
|
10
|
-
export class NodeAdapter {
|
|
11
|
-
cwd;
|
|
12
|
-
constructor(cwd = process.cwd()) {
|
|
13
|
-
this.cwd = cwd;
|
|
14
|
-
}
|
|
15
|
-
supports(url) {
|
|
16
|
-
return (url.startsWith("file://") ||
|
|
17
|
-
url.startsWith("/") ||
|
|
18
|
-
url.startsWith("./") ||
|
|
19
|
-
url.startsWith("../") ||
|
|
20
|
-
(!url.includes("://") && !url.includes("@")));
|
|
21
|
-
}
|
|
22
|
-
async read(url) {
|
|
23
|
-
const filePath = toFilePath(url);
|
|
24
|
-
const stat = await fs.stat(filePath).catch(() => null);
|
|
25
|
-
const resolvedPath = stat?.isDirectory() ? path.join(filePath, DEFAULT_MANIFEST_FILENAME) : filePath;
|
|
26
|
-
const text = await fs.readFile(resolvedPath, "utf8");
|
|
27
|
-
return { text, source: resolvedPath };
|
|
28
|
-
}
|
|
29
|
-
resolveRelative(base, relative) {
|
|
30
|
-
const baseDir = path.dirname(path.resolve(this.cwd, toFilePath(base)));
|
|
31
|
-
return path.resolve(baseDir, relative);
|
|
32
|
-
}
|
|
33
|
-
async expandGlob(base, patterns) {
|
|
34
|
-
const baseDir = path.dirname(path.resolve(this.cwd, toFilePath(base)));
|
|
35
|
-
const entries = await fs.readdir(baseDir, { recursive: true, encoding: "utf8" });
|
|
36
|
-
const normalizedPatterns = patterns.map((p) => p.replace(/\\/g, "/").replace(/^\.\//, ""));
|
|
37
|
-
const matched = [];
|
|
38
|
-
for (const entry of entries) {
|
|
39
|
-
const normalized = entry.replace(/\\/g, "/");
|
|
40
|
-
if (normalizedPatterns.some((p) => minimatch(normalized, p))) {
|
|
41
|
-
matched.push(path.resolve(baseDir, entry));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return matched.sort();
|
|
45
|
-
}
|
|
46
|
-
async resolveOwnerOf(fileUrl) {
|
|
47
|
-
const resolved = path.resolve(this.cwd, toFilePath(fileUrl));
|
|
48
|
-
let dir = path.dirname(resolved);
|
|
49
|
-
while (true) {
|
|
50
|
-
const candidate = path.join(dir, DEFAULT_MANIFEST_FILENAME);
|
|
51
|
-
if (candidate !== resolved) {
|
|
52
|
-
try {
|
|
53
|
-
await fs.access(candidate);
|
|
54
|
-
return candidate;
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
// telo.yaml not found at this level
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const parent = path.dirname(dir);
|
|
61
|
-
if (parent === dir)
|
|
62
|
-
break;
|
|
63
|
-
dir = parent;
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/** @deprecated Use `new NodeAdapter(cwd)` instead */
|
|
69
|
-
export function createNodeAdapter(cwd = process.cwd()) {
|
|
70
|
-
return new NodeAdapter(cwd);
|
|
71
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { type ManifestAdapter } from "../types.js";
|
|
2
|
-
export declare class RegistryAdapter implements ManifestAdapter {
|
|
3
|
-
private registryUrl;
|
|
4
|
-
constructor(registryUrl?: string);
|
|
5
|
-
supports(url: string): boolean;
|
|
6
|
-
read(moduleRef: string): Promise<{
|
|
7
|
-
text: string;
|
|
8
|
-
source: string;
|
|
9
|
-
}>;
|
|
10
|
-
resolveRelative(base: string, relative: string): string;
|
|
11
|
-
private toRegistryModuleBase;
|
|
12
|
-
private toRegistryUrl;
|
|
13
|
-
private parseModuleRef;
|
|
14
|
-
}
|
|
15
|
-
//# sourceMappingURL=registry-adapter.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"registry-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/registry-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAI9E,qBAAa,eAAgB,YAAW,eAAe;IACzC,OAAO,CAAC,WAAW;gBAAX,WAAW,SAAuB;IAEtD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAWxB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAWxE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAMvD,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,cAAc;CAmBvB"}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_MANIFEST_FILENAME } from "../types.js";
|
|
2
|
-
const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
|
|
3
|
-
export class RegistryAdapter {
|
|
4
|
-
registryUrl;
|
|
5
|
-
constructor(registryUrl = DEFAULT_REGISTRY_URL) {
|
|
6
|
-
this.registryUrl = registryUrl;
|
|
7
|
-
}
|
|
8
|
-
supports(url) {
|
|
9
|
-
return (!url.startsWith("http://") &&
|
|
10
|
-
!url.startsWith("https://") &&
|
|
11
|
-
!url.startsWith("/") &&
|
|
12
|
-
!url.startsWith(".") &&
|
|
13
|
-
url.includes("@") &&
|
|
14
|
-
url.includes("/"));
|
|
15
|
-
}
|
|
16
|
-
async read(moduleRef) {
|
|
17
|
-
const fetchUrl = this.toRegistryUrl(moduleRef);
|
|
18
|
-
const response = await fetch(fetchUrl);
|
|
19
|
-
if (!response.ok) {
|
|
20
|
-
throw new Error(`Failed to fetch manifest ${moduleRef}: ${response.status} ${response.statusText}`);
|
|
21
|
-
}
|
|
22
|
-
return { text: await response.text(), source: fetchUrl };
|
|
23
|
-
}
|
|
24
|
-
resolveRelative(base, relative) {
|
|
25
|
-
const baseUrl = this.supports(base) ? this.toRegistryModuleBase(base) : base;
|
|
26
|
-
const baseWithSlash = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
27
|
-
return new URL(relative, baseWithSlash).href;
|
|
28
|
-
}
|
|
29
|
-
toRegistryModuleBase(moduleRef) {
|
|
30
|
-
const parsed = this.parseModuleRef(moduleRef);
|
|
31
|
-
const normalizedBase = this.registryUrl.replace(/\/+$/, "");
|
|
32
|
-
return `${normalizedBase}/${parsed.modulePath}/${parsed.version}`;
|
|
33
|
-
}
|
|
34
|
-
toRegistryUrl(moduleRef) {
|
|
35
|
-
return `${this.toRegistryModuleBase(moduleRef)}/${DEFAULT_MANIFEST_FILENAME}`;
|
|
36
|
-
}
|
|
37
|
-
parseModuleRef(moduleRef) {
|
|
38
|
-
const atIdx = moduleRef.lastIndexOf("@");
|
|
39
|
-
if (atIdx <= 0 || atIdx === moduleRef.length - 1) {
|
|
40
|
-
throw new Error(`Invalid module reference '${moduleRef}', expected namespace/name@version`);
|
|
41
|
-
}
|
|
42
|
-
const modulePath = moduleRef.slice(0, atIdx);
|
|
43
|
-
if (!modulePath.includes("/")) {
|
|
44
|
-
throw new Error(`Invalid module reference '${moduleRef}', expected namespace/name@version`);
|
|
45
|
-
}
|
|
46
|
-
const rawVersion = moduleRef.slice(atIdx + 1);
|
|
47
|
-
const version = rawVersion.startsWith("v") ? rawVersion.substring(1) : rawVersion;
|
|
48
|
-
if (!version) {
|
|
49
|
-
throw new Error(`Invalid module reference '${moduleRef}', expected namespace/name@version`);
|
|
50
|
-
}
|
|
51
|
-
return { modulePath, version };
|
|
52
|
-
}
|
|
53
|
-
}
|