@telorun/analyzer 1.3.0 → 1.5.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 +15 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +49 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +85 -6
- 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 +1 -1
- package/src/analyzer.ts +50 -0
- package/src/definition-registry.ts +15 -0
- package/src/index.ts +1 -0
- package/src/validate-references.ts +80 -5
- package/src/with-synthetic-positions.ts +48 -0
package/dist/analyzer.d.ts
CHANGED
|
@@ -8,6 +8,21 @@ export interface StaticAnalyzerOptions {
|
|
|
8
8
|
export declare class StaticAnalyzer {
|
|
9
9
|
private readonly celEnv;
|
|
10
10
|
constructor(options?: StaticAnalyzerOptions);
|
|
11
|
+
/**
|
|
12
|
+
* Run static analysis over a flattened manifest list.
|
|
13
|
+
*
|
|
14
|
+
* **Contract**: every non-system manifest (anything outside `Telo.Definition`,
|
|
15
|
+
* `Telo.Abstract`) must carry `metadata.source` (non-empty string) and
|
|
16
|
+
* `metadata.sourceLine` (number). The dedup that backs
|
|
17
|
+
* `DUPLICATE_RESOURCE_NAME` reads those fields to tell a pipeline echo
|
|
18
|
+
* apart from a genuine collision, and downstream diagnostic positioning
|
|
19
|
+
* depends on them too. Real callers stamp positions already (the `Loader`,
|
|
20
|
+
* `flattenForAnalyzer`, the telo-editor's `emitDocsFor`, the VSCode
|
|
21
|
+
* extension). Programmatic callers — tests, ad-hoc scripts — should pass
|
|
22
|
+
* their inputs through `withSyntheticPositions(...)` before calling
|
|
23
|
+
* `analyze()`. A missing position throws a clear error rather than
|
|
24
|
+
* silently producing wrong diagnostics.
|
|
25
|
+
*/
|
|
11
26
|
analyze(manifests: ResourceManifest[], options?: AnalysisOptions, registry?: AnalysisRegistry): AnalysisDiagnostic[];
|
|
12
27
|
analyzeErrors(manifests: ResourceManifest[], options?: AnalysisOptions, registry?: AnalysisRegistry): AnalysisDiagnostic[];
|
|
13
28
|
normalize(manifests: ResourceManifest[], registry: AnalysisRegistry): ResourceManifest[];
|
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;
|
|
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;AAgB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAgf/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;IA8dvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CAAC,SAAS,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,EAAE;IAexF,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
|
@@ -7,6 +7,7 @@ import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kerne
|
|
|
7
7
|
import { computeSuggestKind } from "./kind-suggest.js";
|
|
8
8
|
import { isModuleKind } from "./module-kinds.js";
|
|
9
9
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
10
|
+
import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
|
|
10
11
|
import { resolveRefSentinels } from "./resolve-ref-sentinels.js";
|
|
11
12
|
import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
|
|
12
13
|
import { celTypeSatisfiesJsonSchema, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
|
|
@@ -17,6 +18,38 @@ import { validateProviderCoherence } from "./validate-provider-coherence.js";
|
|
|
17
18
|
import { validateReferences } from "./validate-references.js";
|
|
18
19
|
import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
19
20
|
const SELF_PREFIX = "Self.";
|
|
21
|
+
/**
|
|
22
|
+
* `StaticAnalyzer.analyze()` requires `metadata.source` (non-empty) and
|
|
23
|
+
* `metadata.sourceLine` (number) on every non-system manifest — see the
|
|
24
|
+
* JSDoc on `analyze()`. Production callers stamp these via the `Loader` /
|
|
25
|
+
* `flattenForAnalyzer` / `emitDocsFor` paths; programmatic callers (tests,
|
|
26
|
+
* scripts) should pre-process inputs with `withSyntheticPositions(...)`.
|
|
27
|
+
* Surfacing the violation here turns silent dedup misbehaviour into a
|
|
28
|
+
* loud, actionable error.
|
|
29
|
+
*/
|
|
30
|
+
function assertManifestPositions(manifests) {
|
|
31
|
+
for (let i = 0; i < manifests.length; i++) {
|
|
32
|
+
const m = manifests[i];
|
|
33
|
+
if (REF_VALIDATION_SKIP_KINDS.has(m.kind))
|
|
34
|
+
continue;
|
|
35
|
+
const meta = m.metadata;
|
|
36
|
+
const okSource = typeof meta?.source === "string" && meta.source.length > 0;
|
|
37
|
+
const okLine = typeof meta?.sourceLine === "number";
|
|
38
|
+
if (okSource && okLine)
|
|
39
|
+
continue;
|
|
40
|
+
const label = `${m.kind}/${m.metadata?.name ?? "(unnamed)"}`;
|
|
41
|
+
const missing = [
|
|
42
|
+
!okSource ? "metadata.source" : null,
|
|
43
|
+
!okLine ? "metadata.sourceLine" : null,
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(" and ");
|
|
47
|
+
throw new Error(`StaticAnalyzer.analyze(): manifest #${i} (${label}) is missing ${missing}. ` +
|
|
48
|
+
`Real callers stamp positions automatically; programmatic callers ` +
|
|
49
|
+
`(tests, ad-hoc scripts) should pass inputs through ` +
|
|
50
|
+
`\`withSyntheticPositions(manifests)\` before calling analyze().`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
20
53
|
/** Resolve an alias-prefixed kind value (e.g. `Self.Encoder` or `Ai.Model`)
|
|
21
54
|
* to its canonical form. `Self.<Name>` resolves to `<ownModule>.<Name>` —
|
|
22
55
|
* the magic alias for "this library's own module" — and other prefixes
|
|
@@ -376,7 +409,23 @@ export class StaticAnalyzer {
|
|
|
376
409
|
constructor(options = {}) {
|
|
377
410
|
this.celEnv = buildCelEnvironment(options.celHandlers);
|
|
378
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Run static analysis over a flattened manifest list.
|
|
414
|
+
*
|
|
415
|
+
* **Contract**: every non-system manifest (anything outside `Telo.Definition`,
|
|
416
|
+
* `Telo.Abstract`) must carry `metadata.source` (non-empty string) and
|
|
417
|
+
* `metadata.sourceLine` (number). The dedup that backs
|
|
418
|
+
* `DUPLICATE_RESOURCE_NAME` reads those fields to tell a pipeline echo
|
|
419
|
+
* apart from a genuine collision, and downstream diagnostic positioning
|
|
420
|
+
* depends on them too. Real callers stamp positions already (the `Loader`,
|
|
421
|
+
* `flattenForAnalyzer`, the telo-editor's `emitDocsFor`, the VSCode
|
|
422
|
+
* extension). Programmatic callers — tests, ad-hoc scripts — should pass
|
|
423
|
+
* their inputs through `withSyntheticPositions(...)` before calling
|
|
424
|
+
* `analyze()`. A missing position throws a clear error rather than
|
|
425
|
+
* silently producing wrong diagnostics.
|
|
426
|
+
*/
|
|
379
427
|
analyze(manifests, options, registry) {
|
|
428
|
+
assertManifestPositions(manifests);
|
|
380
429
|
const diagnostics = [];
|
|
381
430
|
// Use pre-seeded registries from the provided AnalysisRegistry, or create fresh ones.
|
|
382
431
|
// New aliases/definitions found in the manifests are accumulated into the provided instance
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definition-registry.d.ts","sourceRoot":"","sources":["../src/definition-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAGlC,+EAA+E;AAC/E,qBAAa,kBAAkB;;IAK7B;;sFAEkF;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IAEzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+B;IAC1D;6DACyD;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD;;0EAEsE;IACtE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAEhE,QAAQ,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI;IAiC9C,OAAO,CAAC,aAAa;IASrB;;;yFAGqF;IACrF,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"definition-registry.d.ts","sourceRoot":"","sources":["../src/definition-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAGlC,+EAA+E;AAC/E,qBAAa,kBAAkB;;IAK7B;;sFAEkF;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IAEzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+B;IAC1D;6DACyD;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD;;0EAEsE;IACtE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAEhE,QAAQ,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI;IAiC9C,OAAO,CAAC,aAAa;IASrB;;;yFAGqF;IACrF,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IA+B1E;4EACwE;IACxE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMnE;wFACoF;IACpF,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE;IAWtE,OAAO,CAAC,iBAAiB;IAczB;;;;;;;;4FAQwF;IACxF,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAUhD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIrD,+FAA+F;IAC/F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAIxD,gGAAgG;IAChG,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;KAAE,GACvD,iBAAiB,GAAG,SAAS;IAOhC;;;;;;;;qFAQiF;IACjF,2BAA2B,CACzB,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,aAAa,EACtB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC1C,iBAAiB,GAAG,SAAS;IAuBhC,OAAO,CAAC,uBAAuB;IA+B/B;;qEAEiE;IACjE,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAgBxD,KAAK,IAAI,MAAM,EAAE;CAGlB"}
|
|
@@ -70,6 +70,22 @@ export class DefinitionRegistry {
|
|
|
70
70
|
* @param namespace The module's metadata.namespace (e.g. "std"), or null for telo built-ins.
|
|
71
71
|
* @param moduleName The module's metadata.name (e.g. "pipeline", "http-server"). */
|
|
72
72
|
registerModuleIdentity(namespace, moduleName) {
|
|
73
|
+
// The "telo" identity is reserved for the Telo built-in module and gets
|
|
74
|
+
// populated automatically when a Telo.Abstract definition registers (see
|
|
75
|
+
// `register` below). A user app / library without a namespace must NOT
|
|
76
|
+
// claim it — silently overwriting the built-in entry breaks every
|
|
77
|
+
// x-telo-ref that resolves through "telo#…". Concretely, the
|
|
78
|
+
// `Http.Api.routes[].handler` slot in the http-server schema carries
|
|
79
|
+
// `x-telo-ref: "telo#Invocable"`. If the entry application is, say,
|
|
80
|
+
// `Telo.Application/HelloApi` (no namespace), this method previously
|
|
81
|
+
// overwrote `"telo" → "Telo"` with `"telo" → "HelloApi"`. The handler's
|
|
82
|
+
// ref then resolved to a nonexistent `HelloApi.Invocable`, the
|
|
83
|
+
// kind-mismatch check inside `validate-references.ts` short-circuited
|
|
84
|
+
// on partial context, and the analyzer reported zero issues for a
|
|
85
|
+
// manifest that explodes at runtime. Skip non-Telo no-namespace modules;
|
|
86
|
+
// they have no x-telo-ref identity to declare anyway.
|
|
87
|
+
if (!namespace && moduleName !== "Telo")
|
|
88
|
+
return;
|
|
73
89
|
const identity = namespace ? `${namespace}/${moduleName}` : "telo";
|
|
74
90
|
this.identityMap.set(identity, moduleName);
|
|
75
91
|
this.reverseIdentityMap.set(moduleName, identity);
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentL
|
|
|
12
12
|
export type { DocumentPosition } from "./position-metadata.js";
|
|
13
13
|
export { HttpSource } from "./sources/http-source.js";
|
|
14
14
|
export { RegistrySource } from "./sources/registry-source.js";
|
|
15
|
+
export { withSyntheticPositions } from "./with-synthetic-positions.js";
|
|
15
16
|
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
|
|
16
17
|
export type { AnalysisDiagnostic, AnalysisOptions, LoaderInitOptions, LoadOptions, ManifestSource, Position, PositionIndex, Range } from "./types.js";
|
|
17
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EACR,cAAc,EACd,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,EACZ,UAAU,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EACH,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,GACtB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EACR,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,aAAa,EACb,KAAK,EACR,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EACR,cAAc,EACd,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,EACZ,UAAU,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EACH,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,GACtB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EACR,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,aAAa,EACb,KAAK,EACR,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -8,4 +8,5 @@ export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.j
|
|
|
8
8
|
export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, } from "./position-metadata.js";
|
|
9
9
|
export { HttpSource } from "./sources/http-source.js";
|
|
10
10
|
export { RegistrySource } from "./sources/registry-source.js";
|
|
11
|
+
export { withSyntheticPositions } from "./with-synthetic-positions.js";
|
|
11
12
|
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
|
|
@@ -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;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,
|
|
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"}
|
|
@@ -60,14 +60,93 @@ export function validateReferences(resources, context) {
|
|
|
60
60
|
const aliasesByModule = context.aliasesByModule;
|
|
61
61
|
if (!aliases || !registry)
|
|
62
62
|
return diagnostics;
|
|
63
|
-
// Build outer resource lookup by name for resolution check
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
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();
|
|
67
99
|
for (const r of resources) {
|
|
68
|
-
if (r.metadata?.name
|
|
69
|
-
|
|
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]);
|
|
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
|
+
}
|
|
70
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]);
|
|
71
150
|
for (const r of resources) {
|
|
72
151
|
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind))
|
|
73
152
|
continue;
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
package/src/analyzer.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kerne
|
|
|
14
14
|
import { computeSuggestKind } from "./kind-suggest.js";
|
|
15
15
|
import { isModuleKind } from "./module-kinds.js";
|
|
16
16
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
17
|
+
import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
|
|
17
18
|
import { resolveRefSentinels } from "./resolve-ref-sentinels.js";
|
|
18
19
|
import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
|
|
19
20
|
import {
|
|
@@ -36,6 +37,39 @@ import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
|
36
37
|
|
|
37
38
|
const SELF_PREFIX = "Self.";
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* `StaticAnalyzer.analyze()` requires `metadata.source` (non-empty) and
|
|
42
|
+
* `metadata.sourceLine` (number) on every non-system manifest — see the
|
|
43
|
+
* JSDoc on `analyze()`. Production callers stamp these via the `Loader` /
|
|
44
|
+
* `flattenForAnalyzer` / `emitDocsFor` paths; programmatic callers (tests,
|
|
45
|
+
* scripts) should pre-process inputs with `withSyntheticPositions(...)`.
|
|
46
|
+
* Surfacing the violation here turns silent dedup misbehaviour into a
|
|
47
|
+
* loud, actionable error.
|
|
48
|
+
*/
|
|
49
|
+
function assertManifestPositions(manifests: ResourceManifest[]): void {
|
|
50
|
+
for (let i = 0; i < manifests.length; i++) {
|
|
51
|
+
const m = manifests[i];
|
|
52
|
+
if (REF_VALIDATION_SKIP_KINDS.has(m.kind)) continue;
|
|
53
|
+
const meta = m.metadata as { source?: string; sourceLine?: number } | undefined;
|
|
54
|
+
const okSource = typeof meta?.source === "string" && meta.source.length > 0;
|
|
55
|
+
const okLine = typeof meta?.sourceLine === "number";
|
|
56
|
+
if (okSource && okLine) continue;
|
|
57
|
+
const label = `${m.kind}/${m.metadata?.name ?? "(unnamed)"}`;
|
|
58
|
+
const missing = [
|
|
59
|
+
!okSource ? "metadata.source" : null,
|
|
60
|
+
!okLine ? "metadata.sourceLine" : null,
|
|
61
|
+
]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join(" and ");
|
|
64
|
+
throw new Error(
|
|
65
|
+
`StaticAnalyzer.analyze(): manifest #${i} (${label}) is missing ${missing}. ` +
|
|
66
|
+
`Real callers stamp positions automatically; programmatic callers ` +
|
|
67
|
+
`(tests, ad-hoc scripts) should pass inputs through ` +
|
|
68
|
+
`\`withSyntheticPositions(manifests)\` before calling analyze().`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
39
73
|
/** Resolve an alias-prefixed kind value (e.g. `Self.Encoder` or `Ai.Model`)
|
|
40
74
|
* to its canonical form. `Self.<Name>` resolves to `<ownModule>.<Name>` —
|
|
41
75
|
* the magic alias for "this library's own module" — and other prefixes
|
|
@@ -496,11 +530,27 @@ export class StaticAnalyzer {
|
|
|
496
530
|
this.celEnv = buildCelEnvironment(options.celHandlers);
|
|
497
531
|
}
|
|
498
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Run static analysis over a flattened manifest list.
|
|
535
|
+
*
|
|
536
|
+
* **Contract**: every non-system manifest (anything outside `Telo.Definition`,
|
|
537
|
+
* `Telo.Abstract`) must carry `metadata.source` (non-empty string) and
|
|
538
|
+
* `metadata.sourceLine` (number). The dedup that backs
|
|
539
|
+
* `DUPLICATE_RESOURCE_NAME` reads those fields to tell a pipeline echo
|
|
540
|
+
* apart from a genuine collision, and downstream diagnostic positioning
|
|
541
|
+
* depends on them too. Real callers stamp positions already (the `Loader`,
|
|
542
|
+
* `flattenForAnalyzer`, the telo-editor's `emitDocsFor`, the VSCode
|
|
543
|
+
* extension). Programmatic callers — tests, ad-hoc scripts — should pass
|
|
544
|
+
* their inputs through `withSyntheticPositions(...)` before calling
|
|
545
|
+
* `analyze()`. A missing position throws a clear error rather than
|
|
546
|
+
* silently producing wrong diagnostics.
|
|
547
|
+
*/
|
|
499
548
|
analyze(
|
|
500
549
|
manifests: ResourceManifest[],
|
|
501
550
|
options?: AnalysisOptions,
|
|
502
551
|
registry?: AnalysisRegistry,
|
|
503
552
|
): AnalysisDiagnostic[] {
|
|
553
|
+
assertManifestPositions(manifests);
|
|
504
554
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
505
555
|
|
|
506
556
|
// Use pre-seeded registries from the provided AnalysisRegistry, or create fresh ones.
|
|
@@ -80,6 +80,21 @@ export class DefinitionRegistry {
|
|
|
80
80
|
* @param namespace The module's metadata.namespace (e.g. "std"), or null for telo built-ins.
|
|
81
81
|
* @param moduleName The module's metadata.name (e.g. "pipeline", "http-server"). */
|
|
82
82
|
registerModuleIdentity(namespace: string | null, moduleName: string): void {
|
|
83
|
+
// The "telo" identity is reserved for the Telo built-in module and gets
|
|
84
|
+
// populated automatically when a Telo.Abstract definition registers (see
|
|
85
|
+
// `register` below). A user app / library without a namespace must NOT
|
|
86
|
+
// claim it — silently overwriting the built-in entry breaks every
|
|
87
|
+
// x-telo-ref that resolves through "telo#…". Concretely, the
|
|
88
|
+
// `Http.Api.routes[].handler` slot in the http-server schema carries
|
|
89
|
+
// `x-telo-ref: "telo#Invocable"`. If the entry application is, say,
|
|
90
|
+
// `Telo.Application/HelloApi` (no namespace), this method previously
|
|
91
|
+
// overwrote `"telo" → "Telo"` with `"telo" → "HelloApi"`. The handler's
|
|
92
|
+
// ref then resolved to a nonexistent `HelloApi.Invocable`, the
|
|
93
|
+
// kind-mismatch check inside `validate-references.ts` short-circuited
|
|
94
|
+
// on partial context, and the analyzer reported zero issues for a
|
|
95
|
+
// manifest that explodes at runtime. Skip non-Telo no-namespace modules;
|
|
96
|
+
// they have no x-telo-ref identity to declare anyway.
|
|
97
|
+
if (!namespace && moduleName !== "Telo") return;
|
|
83
98
|
const identity = namespace ? `${namespace}/${moduleName}` : "telo";
|
|
84
99
|
this.identityMap.set(identity, moduleName);
|
|
85
100
|
this.reverseIdentityMap.set(moduleName, identity);
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export {
|
|
|
24
24
|
export type { DocumentPosition } from "./position-metadata.js";
|
|
25
25
|
export { HttpSource } from "./sources/http-source.js";
|
|
26
26
|
export { RegistrySource } from "./sources/registry-source.js";
|
|
27
|
+
export { withSyntheticPositions } from "./with-synthetic-positions.js";
|
|
27
28
|
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
|
|
28
29
|
export type {
|
|
29
30
|
AnalysisDiagnostic,
|
|
@@ -72,13 +72,88 @@ export function validateReferences(
|
|
|
72
72
|
const aliasesByModule = context.aliasesByModule;
|
|
73
73
|
if (!aliases || !registry) return diagnostics;
|
|
74
74
|
|
|
75
|
-
// Build outer resource lookup by name for resolution check
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
75
|
+
// Build outer resource lookup by name for resolution check, collecting
|
|
76
|
+
// every entry per name so we can surface name collisions as diagnostics
|
|
77
|
+
// (the kernel's resource registry shares one namespace across all
|
|
78
|
+
// non-system kinds — e.g. `Telo.Application HelloApi` and `Http.Api
|
|
79
|
+
// HelloApi` collide at boot with `ERR_DUPLICATE_RESOURCE`. Catching it
|
|
80
|
+
// statically removes a class of "everything analyzes clean, then the
|
|
81
|
+
// kernel refuses to start" surprises.)
|
|
82
|
+
//
|
|
83
|
+
// Telo.Import is excluded from the duplicate check on top of the
|
|
84
|
+
// SYSTEM_KINDS skip: its `metadata.name` is an alias, not a resource
|
|
85
|
+
// identity (aliases live in a separate namespace from resources, and
|
|
86
|
+
// colliding aliases vs. resource names is benign — the alias is only
|
|
87
|
+
// ever read as a kind prefix).
|
|
88
|
+
// Group manifests by name to detect collisions. Two subtleties:
|
|
89
|
+
//
|
|
90
|
+
// 1. Some analyzer hosts emit the SAME physical document twice through
|
|
91
|
+
// their pipeline — e.g. the telo-editor's `toAnalysisManifests` walks
|
|
92
|
+
// each workspace module's documents independently, and a file
|
|
93
|
+
// reachable from two angles (entry module + `include:` partial)
|
|
94
|
+
// shows up twice. The fingerprint includes `sourceLine` so identical
|
|
95
|
+
// docs (same kind, name, source, AND source line) collapse to one,
|
|
96
|
+
// while two textually-separate documents in the same file (different
|
|
97
|
+
// source lines) keep separate fingerprints and trip the diagnostic.
|
|
98
|
+
// 2. The diagnostic carries a precomputed `range` pointing at the
|
|
99
|
+
// duplicate's source line — editor hosts that resolve diagnostic
|
|
100
|
+
// positions via a `${file}::${kind}::${name}` lookup would otherwise
|
|
101
|
+
// collide on duplicates (Map.set overwrites) and place the squiggle
|
|
102
|
+
// ambiguously. The explicit `range` short-circuits that lookup.
|
|
103
|
+
// Dedup pipeline echoes — the same physical document emitted twice
|
|
104
|
+
// through an analyzer host's pipeline. Keyed on (kind, name, source,
|
|
105
|
+
// sourceLine), so two textually-distinct docs in the same file (same
|
|
106
|
+
// source, different sourceLine) keep separate fingerprints and still
|
|
107
|
+
// trip the diagnostic. `analyze()` enforces that every non-system
|
|
108
|
+
// manifest carries both positional fields — no defensive guard needed.
|
|
109
|
+
const byNameAll = new Map<string, ResourceManifest[]>();
|
|
110
|
+
const seen = new Set<string>();
|
|
79
111
|
for (const r of resources) {
|
|
80
|
-
if (r.metadata?.name
|
|
112
|
+
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || r.kind === "Telo.Import") continue;
|
|
113
|
+
const name = r.metadata.name as string;
|
|
114
|
+
// `analyze()` guarantees both fields are present on non-system manifests.
|
|
115
|
+
const meta = r.metadata as unknown as { source: string; sourceLine: number };
|
|
116
|
+
const fingerprint = `${r.kind} ${name} ${meta.source} ${meta.sourceLine}`;
|
|
117
|
+
if (seen.has(fingerprint)) continue;
|
|
118
|
+
seen.add(fingerprint);
|
|
119
|
+
const existing = byNameAll.get(name);
|
|
120
|
+
if (existing) existing.push(r);
|
|
121
|
+
else byNameAll.set(name, [r]);
|
|
81
122
|
}
|
|
123
|
+
for (const [name, list] of byNameAll) {
|
|
124
|
+
if (list.length <= 1) continue;
|
|
125
|
+
const [first, ...rest] = list;
|
|
126
|
+
const firstLabel = `${first.kind}/${name}`;
|
|
127
|
+
for (const dup of rest) {
|
|
128
|
+
const dupMeta = dup.metadata as { source?: string; sourceLine?: number } | undefined;
|
|
129
|
+
const range =
|
|
130
|
+
typeof dupMeta?.sourceLine === "number"
|
|
131
|
+
? {
|
|
132
|
+
start: { line: dupMeta.sourceLine, character: 0 },
|
|
133
|
+
end: { line: dupMeta.sourceLine, character: Number.MAX_SAFE_INTEGER },
|
|
134
|
+
}
|
|
135
|
+
: undefined;
|
|
136
|
+
diagnostics.push({
|
|
137
|
+
severity: DiagnosticSeverity.Error,
|
|
138
|
+
code: "DUPLICATE_RESOURCE_NAME",
|
|
139
|
+
source: SOURCE,
|
|
140
|
+
message: `${dup.kind}/${name}: resource name collides with ${firstLabel} declared earlier (kernel runtime would fail with ERR_DUPLICATE_RESOURCE)`,
|
|
141
|
+
...(range ? { range } : {}),
|
|
142
|
+
data: {
|
|
143
|
+
resource: { kind: dup.kind, name },
|
|
144
|
+
filePath: dupMeta?.source,
|
|
145
|
+
path: "metadata.name",
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Single-resource map for the resolution / scope lookups below — when a
|
|
151
|
+
// collision exists, falling back to the first occurrence keeps the rest
|
|
152
|
+
// of the pass behaving the same as before the duplicate diagnostic was
|
|
153
|
+
// added (resolution still finds *something*; the duplicate diagnostic
|
|
154
|
+
// is what surfaces the underlying problem to the user).
|
|
155
|
+
const byName = new Map<string, ResourceManifest>();
|
|
156
|
+
for (const [name, list] of byNameAll) byName.set(name, list[0]);
|
|
82
157
|
|
|
83
158
|
for (const r of resources) {
|
|
84
159
|
if (!r.metadata?.name || !r.kind || SYSTEM_KINDS.has(r.kind)) continue;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stamp `metadata.source` and `metadata.sourceLine` on every non-system
|
|
6
|
+
* manifest that lacks them, returning a new array with cloned `metadata`
|
|
7
|
+
* objects for the affected entries.
|
|
8
|
+
*
|
|
9
|
+
* `StaticAnalyzer.analyze()` requires position info on every non-system
|
|
10
|
+
* manifest (the dedup that backs `DUPLICATE_RESOURCE_NAME` reads
|
|
11
|
+
* `(source, sourceLine)` to distinguish pipeline echoes from real
|
|
12
|
+
* collisions). Production callers — the `Loader`, `flattenForAnalyzer`,
|
|
13
|
+
* the telo-editor's `emitDocsFor`, the VSCode extension — all stamp
|
|
14
|
+
* positions already. This helper is the escape hatch for **programmatic
|
|
15
|
+
* callers** (tests, ad-hoc scripts) that construct `ResourceManifest`
|
|
16
|
+
* literals without going through a loader: it gives every otherwise-naked
|
|
17
|
+
* manifest a synthetic, deterministic position so the analyzer's
|
|
18
|
+
* invariant holds without each test having to spell positions out.
|
|
19
|
+
*
|
|
20
|
+
* The synthetic source defaults to `"<programmatic>"` — override via
|
|
21
|
+
* `source` when a stable, recognisable label helps diagnostic output.
|
|
22
|
+
* Each unstamped manifest gets a unique `sourceLine` (1-based array
|
|
23
|
+
* index) so two real duplicates supplied without positions retain
|
|
24
|
+
* distinct fingerprints and still trip `DUPLICATE_RESOURCE_NAME`.
|
|
25
|
+
*
|
|
26
|
+
* Manifests that already carry `metadata.source` and `metadata.sourceLine`
|
|
27
|
+
* pass through unchanged.
|
|
28
|
+
*/
|
|
29
|
+
export function withSyntheticPositions(
|
|
30
|
+
manifests: ResourceManifest[],
|
|
31
|
+
source: string = "<programmatic>",
|
|
32
|
+
): ResourceManifest[] {
|
|
33
|
+
return manifests.map((m, i) => {
|
|
34
|
+
if (REF_VALIDATION_SKIP_KINDS.has(m.kind)) return m;
|
|
35
|
+
const meta = m.metadata as { source?: string; sourceLine?: number } | undefined;
|
|
36
|
+
const hasSource = typeof meta?.source === "string" && meta.source.length > 0;
|
|
37
|
+
const hasLine = typeof meta?.sourceLine === "number";
|
|
38
|
+
if (hasSource && hasLine) return m;
|
|
39
|
+
return {
|
|
40
|
+
...m,
|
|
41
|
+
metadata: {
|
|
42
|
+
...m.metadata,
|
|
43
|
+
source: hasSource ? meta!.source : source,
|
|
44
|
+
sourceLine: hasLine ? meta!.sourceLine : i,
|
|
45
|
+
},
|
|
46
|
+
} as ResourceManifest;
|
|
47
|
+
});
|
|
48
|
+
}
|