@telorun/analyzer 0.27.0 → 0.28.1

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/README.md CHANGED
@@ -44,146 +44,7 @@ $ telo ./examples/hello-api
44
44
 
45
45
  ## Example manifest
46
46
 
47
- Here is an example Telo application that defines a simple HTTP API:
48
-
49
- ```yaml
50
- kind: Telo.Application
51
- metadata:
52
- name: feedback
53
- version: 1.0.0
54
- description: |
55
- A complete feedback collection REST API — no code, pure YAML.
56
- Persists entries to SQLite and serves them over HTTP.
57
- imports:
58
- Http: std/http-server@0.12.0
59
- Sql: std/sql@0.9.2
60
- targets:
61
- - !ref Migrations
62
- - !ref Server
63
- ---
64
- # SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
65
- kind: Sql.Connection
66
- metadata:
67
- name: Db
68
- driver: sqlite
69
- file: ./tmp/feedback.db
70
- ---
71
- # Migrations: applied automatically before the server starts
72
- kind: Sql.Migrations
73
- metadata:
74
- name: Migrations
75
- connection: !ref Db
76
- ---
77
- kind: Sql.Migration
78
- metadata:
79
- name: Migration_20260413_182154_CreateFeedback
80
- version: 20260413_182154_CreateFeedback
81
- sql: |
82
- CREATE TABLE IF NOT EXISTS feedback (
83
- id INTEGER PRIMARY KEY AUTOINCREMENT,
84
- text TEXT NOT NULL,
85
- source TEXT,
86
- score INTEGER NOT NULL DEFAULT 0,
87
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
88
- )
89
- ---
90
- kind: Http.Server
91
- metadata:
92
- name: Server
93
- baseUrl: http://localhost:8844
94
- port: 8844
95
- logger: true
96
- openapi:
97
- info:
98
- title: Feedback API
99
- version: 1.0.0
100
- mounts:
101
- - path: /v1
102
- mount: !ref FeedbackRoutes
103
- ---
104
- kind: Http.Api
105
- metadata:
106
- name: FeedbackRoutes
107
- routes:
108
- # POST /v1/feedback — insert a new entry, score derived from body length heuristic
109
- - request:
110
- path: /feedback
111
- method: POST
112
- schema:
113
- body:
114
- type: object
115
- properties:
116
- text:
117
- type: string
118
- minLength: 1
119
- source:
120
- type: string
121
- required: [ text ]
122
- handler:
123
- kind: Sql.Exec
124
- connection: !ref Db
125
- inputs:
126
- sql: "INSERT INTO feedback (text, source, score) VALUES (?, ?, ?)"
127
- bindings:
128
- - "${{ request.body.text }}"
129
- - "${{ request.body.source }}"
130
- - "${{ size(request.body.text) }}"
131
- response:
132
- - status: 201
133
- headers:
134
- Content-Type: application/json
135
- body:
136
- ok: true
137
- message: Feedback received
138
-
139
- # GET /v1/feedback — list all entries, newest first
140
- - request:
141
- path: /feedback
142
- method: GET
143
- handler:
144
- kind: Sql.Select
145
- connection: !ref Db
146
- from: feedback
147
- columns: [ id, text, source, score, created_at ]
148
- orderBy:
149
- - { column: created_at, direction: desc }
150
- response:
151
- - status: 200
152
- headers:
153
- Content-Type: application/json
154
- body: "${{ result.rows }}"
155
-
156
- # GET /v1/feedback/{id} — fetch a single entry
157
- - request:
158
- path: /feedback/{id}
159
- method: GET
160
- schema:
161
- params:
162
- type: object
163
- properties:
164
- id:
165
- type: integer
166
- required: [ id ]
167
- handler:
168
- kind: Sql.Select
169
- connection: !ref Db
170
- from: feedback
171
- columns: [ id, text, source, score, created_at ]
172
- where:
173
- - { column: id, op: "=", value: "${{ request.params.id }}" }
174
- response:
175
- - status: 200
176
- when: "size(result.rows) > 0"
177
- headers:
178
- Content-Type: application/json
179
- body: "${{ result.rows[0] }}"
180
- - status: 404
181
- headers:
182
- Content-Type: application/json
183
- body:
184
- ok: false
185
- message: Not found
186
- ```
47
+ See [examples/](./examples/) for a list of working applications.
187
48
 
188
49
  ## Status
189
50
 
package/dist/index.d.ts CHANGED
@@ -12,11 +12,14 @@ export { parseLoadedFile } from "./parse-loaded-file.js";
12
12
  export type { ParseOptions } from "./parse-loaded-file.js";
13
13
  export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
14
14
  export type { SyntheticImport } from "./inline-imports.js";
15
+ export { reconcileModuleVersions } from "./reconcile-module-versions.js";
16
+ export type { VersionReconciliation } from "./reconcile-module-versions.js";
15
17
  export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
16
18
  export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, } from "./position-metadata.js";
17
19
  export type { DocumentPosition } from "./position-metadata.js";
18
20
  export { HttpSource } from "./sources/http-source.js";
19
21
  export { RegistrySource } from "./sources/registry-source.js";
22
+ export { defaultSources } from "./sources/default-sources.js";
20
23
  export { withSyntheticPositions } from "./with-synthetic-positions.js";
21
24
  export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
22
25
  export type { AnalysisDiagnostic, AnalysisOptions, LoaderInitOptions, LoadOptions, ManifestSource, Position, PositionIndex, Range } from "./types.js";
@@ -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,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,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,EACH,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,gCAAgC,EAChC,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,GACpB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EACR,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,GACf,MAAM,uBAAuB,CAAC;AAC/B,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,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,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,EACH,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,gCAAgC,EAChC,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,GACpB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EACR,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,GACf,MAAM,uBAAuB,CAAC;AAC/B,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,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,YAAY,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAC5E,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,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
@@ -6,9 +6,11 @@ export { Loader } from "./manifest-loader.js";
6
6
  export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
7
7
  export { parseLoadedFile } from "./parse-loaded-file.js";
8
8
  export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
9
+ export { reconcileModuleVersions } from "./reconcile-module-versions.js";
9
10
  export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
10
11
  export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, } from "./position-metadata.js";
11
12
  export { HttpSource } from "./sources/http-source.js";
12
13
  export { RegistrySource } from "./sources/registry-source.js";
14
+ export { defaultSources } from "./sources/default-sources.js";
13
15
  export { withSyntheticPositions } from "./with-synthetic-positions.js";
14
16
  export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
@@ -1,7 +1,7 @@
1
1
  import type { ResourceManifest } from "@telorun/sdk";
2
2
  import type { Document } from "yaml";
3
3
  import type { DocumentPosition } from "./position-metadata.js";
4
- import type { Range } from "./types.js";
4
+ import type { AnalysisDiagnostic, Range } from "./types.js";
5
5
  /** One physical file's parsed result. Returned for the owner manifest, for
6
6
  * each `include:` partial, and for each external import target.
7
7
  *
@@ -65,8 +65,21 @@ export interface LoadedGraph {
65
65
  * its partials. */
66
66
  modules: Map<string, LoadedModule>;
67
67
  /** Per-Telo.Import resolution. Keyed by the resolved URL of the file the
68
- * Telo.Import was declared in, then by the import's PascalCase alias. */
68
+ * Telo.Import was declared in, then by the import's PascalCase alias.
69
+ * Version reconciliation repoints losing edges at their winner here, so a
70
+ * consumer walking these edges (`flattenForAnalyzer`) sees one version per
71
+ * module identity. */
69
72
  importEdges: Map<string, Map<string, ImportEdge>>;
73
+ /** Version-reconciliation redirects: a losing module's canonical source URL →
74
+ * the winning version's canonical source URL. The runtime consults this when
75
+ * it independently re-resolves an import (the analyzer already sees repointed
76
+ * `importEdges`). Empty when no module identity appeared at two sources. */
77
+ overrides: Map<string, string>;
78
+ /** Diagnostics produced while reconciling module versions — one per import
79
+ * edge redirected to a different version (warning for a same-major hoist,
80
+ * error for a major mismatch). Surfaced alongside `analyze()` diagnostics by
81
+ * every consumer (CLI, editor, VS Code). */
82
+ versionDiagnostics: AnalysisDiagnostic[];
70
83
  /** Surface-level errors that did not abort the graph load (e.g. an import
71
84
  * whose target failed to fetch). */
72
85
  errors: GraphLoadError[];
@@ -1 +1 @@
1
- {"version":3,"file":"loaded-types.d.ts","sourceRoot":"","sources":["../src/loaded-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC;;;;4EAI4E;AAC5E,MAAM,WAAW,UAAU;IACzB;+DAC2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf;gEAC4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,6EAA6E;IAC7E,SAAS,EAAE,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC1C,0EAA0E;IAC1E,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED;mCACmC;AACnC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,UAAU,CAAC;IAClB;oEACgE;IAChE,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED;;;;;2EAK2E;AAC3E,MAAM,WAAW,UAAU;IACzB,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB;6EACyE;IACzE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,oEAAoE;IACpE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;0BAC0B;AAC1B,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;IACpB;;wBAEoB;IACpB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACnC;8EAC0E;IAC1E,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IAClD;yCACqC;IACrC,MAAM,EAAE,cAAc,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,KAAK,CAAC;CACd"}
1
+ {"version":3,"file":"loaded-types.d.ts","sourceRoot":"","sources":["../src/loaded-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;4EAI4E;AAC5E,MAAM,WAAW,UAAU;IACzB;+DAC2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf;gEAC4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,6EAA6E;IAC7E,SAAS,EAAE,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC1C,0EAA0E;IAC1E,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED;mCACmC;AACnC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,UAAU,CAAC;IAClB;oEACgE;IAChE,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED;;;;;2EAK2E;AAC3E,MAAM,WAAW,UAAU;IACzB,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB;6EACyE;IACzE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,oEAAoE;IACpE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;0BAC0B;AAC1B,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;IACpB;;wBAEoB;IACpB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACnC;;;;2BAIuB;IACvB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IAClD;;;iFAG6E;IAC7E,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;;;iDAG6C;IAC7C,kBAAkB,EAAE,kBAAkB,EAAE,CAAC;IACzC;yCACqC;IACrC,MAAM,EAAE,cAAc,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,KAAK,CAAC;CACd"}
@@ -15,7 +15,12 @@ export declare class Loader {
15
15
  private readonly urlToSource;
16
16
  protected sources: ManifestSource[];
17
17
  private readonly celEnv;
18
- constructor(extraSourcesOrOptions?: ManifestSource[] | LoaderInitOptions);
18
+ /** Sources are resolved in order — the first whose `supports(url)` matches
19
+ * wins. The caller (composition root) decides which concrete sources exist
20
+ * and supplies them; `defaultSources()` bundles the browser-safe built-ins
21
+ * (HTTP + registry) for the common case. `register()` prepends a source at
22
+ * runtime. */
23
+ constructor(sources?: ManifestSource[], options?: LoaderInitOptions);
19
24
  register(source: ManifestSource): this;
20
25
  private pick;
21
26
  resolveEntryPoint(url: string): Promise<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAGV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAiBpB,qBAAa,MAAM;IACjB;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAE3D;;;;;8BAK0B;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAEzD,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,qBAAqB,GAAE,cAAc,EAAE,GAAG,iBAAsB;IAmB5E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUrD;;;;2CAIuC;IACvC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAM7C;;;+BAG2B;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAyCvE;;;;gEAI4D;IAC5D,OAAO,CAAC,oBAAoB;IAa5B;gFAC4E;IAC5E,OAAO,CAAC,cAAc;IAQtB;;wEAEoE;IAC9D,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAsB3E;;;qCAGiC;IAC3B,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAwG9E;;;;;;;;sBAQkB;IAClB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAOlE,OAAO,CAAC,6BAA6B;IAarC,OAAO,CAAC,mCAAmC;IAc3C,OAAO,CAAC,2BAA2B;YAkCrB,eAAe;IAmB7B;;;0CAGsC;IAChC,gBAAgB,CACpB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CA0B5D"}
1
+ {"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAGV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAiBpB,qBAAa,MAAM;IACjB;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAE3D;;;;;8BAK0B;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAEzD,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IAErC;;;;mBAIe;gBACH,OAAO,GAAE,cAAc,EAAO,EAAE,OAAO,GAAE,iBAAsB;IAK3E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUrD;;;;2CAIuC;IACvC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAM7C;;;+BAG2B;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAyCvE;;;;gEAI4D;IAC5D,OAAO,CAAC,oBAAoB;IAa5B;gFAC4E;IAC5E,OAAO,CAAC,cAAc;IAQtB;;wEAEoE;IAC9D,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAsB3E;;;qCAGiC;IAC3B,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAqH9E;;;;;;;;sBAQkB;IAClB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAOlE,OAAO,CAAC,6BAA6B;IAarC,OAAO,CAAC,mCAAmC;IAc3C,OAAO,CAAC,2BAA2B;YAkCrB,eAAe;IAmB7B;;;0CAGsC;IAChC,gBAAgB,CACpB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CA0B5D"}
@@ -1,9 +1,8 @@
1
- import { HttpSource } from "./sources/http-source.js";
2
- import { RegistrySource } from "./sources/registry-source.js";
3
1
  import { buildCelEnvironment } from "./cel-environment.js";
4
2
  import { desugarLoadedFile } from "./inline-imports.js";
5
3
  import { isModuleKind } from "./module-kinds.js";
6
4
  import { parseLoadedFile } from "./parse-loaded-file.js";
5
+ import { reconcileModuleVersions } from "./reconcile-module-versions.js";
7
6
  import { DEFAULT_MANIFEST_FILENAME, } from "./types.js";
8
7
  const SYSTEM_KINDS = new Set([
9
8
  "Telo.Application",
@@ -33,20 +32,13 @@ export class Loader {
33
32
  urlToSource = new Map();
34
33
  sources;
35
34
  celEnv;
36
- constructor(extraSourcesOrOptions = []) {
37
- const options = Array.isArray(extraSourcesOrOptions)
38
- ? { extraSources: extraSourcesOrOptions }
39
- : extraSourcesOrOptions;
40
- const includeHttpSource = options.includeHttpSource ?? true;
41
- const includeRegistrySource = options.includeRegistrySource ?? true;
42
- this.sources = [];
43
- if (includeHttpSource)
44
- this.sources.push(new HttpSource());
45
- if (includeRegistrySource)
46
- this.sources.push(new RegistrySource(options.registryUrl));
47
- if (options.extraSources?.length) {
48
- this.sources.unshift(...options.extraSources);
49
- }
35
+ /** Sources are resolved in order — the first whose `supports(url)` matches
36
+ * wins. The caller (composition root) decides which concrete sources exist
37
+ * and supplies them; `defaultSources()` bundles the browser-safe built-ins
38
+ * (HTTP + registry) for the common case. `register()` prepends a source at
39
+ * runtime. */
40
+ constructor(sources = [], options = {}) {
41
+ this.sources = [...sources];
50
42
  this.celEnv = buildCelEnvironment(options.celHandlers);
51
43
  }
52
44
  register(source) {
@@ -265,7 +257,19 @@ export class Loader {
265
257
  importEdges.set(file.source, aliases);
266
258
  }
267
259
  }
268
- return { rootSource, entry, modules, importEdges, errors };
260
+ // Collapse multiple versions of the same module identity onto one version
261
+ // before any consumer walks the edges: repoints losing `importEdges` in
262
+ // place and yields the runtime override map + hoist/conflict diagnostics.
263
+ const { overrides, diagnostics } = reconcileModuleVersions(modules, importEdges);
264
+ return {
265
+ rootSource,
266
+ entry,
267
+ modules,
268
+ importEdges,
269
+ overrides,
270
+ versionDiagnostics: diagnostics,
271
+ errors,
272
+ };
269
273
  }
270
274
  /** Resolve an `import` URL against the file it appears in. Relative /
271
275
  * absolute-path forms run through the owning `ManifestSource`'s
@@ -0,0 +1,25 @@
1
+ import type { ImportEdge, LoadedModule } from "./loaded-types.js";
2
+ import { type AnalysisDiagnostic } from "./types.js";
3
+ /** Outcome of reconciling a module name that appears at more than one resolved
4
+ * source in a single import graph. The `overrides` map redirects each losing
5
+ * canonical URL to the winner's canonical URL — consulted by the runtime when
6
+ * it independently re-resolves an import (the analyzer side is handled by
7
+ * repointing `importEdges` in place). */
8
+ export interface VersionReconciliation {
9
+ /** Loser canonical source URL → winner canonical source URL. */
10
+ overrides: Map<string, string>;
11
+ /** One diagnostic per import edge that pointed at a non-winner: a warning for
12
+ * a same-major hoist, an error for an incompatible major mismatch. */
13
+ diagnostics: AnalysisDiagnostic[];
14
+ }
15
+ /**
16
+ * Reconcile a loaded import graph so each module identity (`namespace/name`)
17
+ * resolves to a single version. Within a shared major the highest version wins
18
+ * (a non-lossy hoist, given Telo's additive-only pre-1.0 policy); a major
19
+ * mismatch is a hard conflict. Mutates `importEdges` in place — every edge that
20
+ * pointed at a losing source is repointed at the winner — so `flattenForAnalyzer`
21
+ * walks a deduplicated graph and the runtime collision (two definitions of the
22
+ * same kind) cannot occur. Pure and browser-safe: no I/O, no Node built-ins.
23
+ */
24
+ export declare function reconcileModuleVersions(modules: Map<string, LoadedModule>, importEdges: Map<string, Map<string, ImportEdge>>): VersionReconciliation;
25
+ //# sourceMappingURL=reconcile-module-versions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile-module-versions.d.ts","sourceRoot":"","sources":["../src/reconcile-module-versions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAElE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;0CAI0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;2EACuE;IACvE,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC;AA8KD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAClC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,GAChD,qBAAqB,CAiDvB"}
@@ -0,0 +1,215 @@
1
+ import { isModuleKind } from "./module-kinds.js";
2
+ import { DiagnosticSeverity } from "./types.js";
3
+ const SOURCE = "telo-analyzer";
4
+ /** Parse `X.Y.Z`, `vX.Y.Z`, or `X.Y.Z-pre.1`. Returns `null` for anything that
5
+ * isn't a plain three-part numeric core — an unparseable version forces the
6
+ * group onto the conflict path (we never silently hoist across a version we
7
+ * can't reason about). Pure: no dependency on the `semver` package, so the
8
+ * analyzer stays browser-safe and dependency-free. */
9
+ function parseVersion(raw) {
10
+ if (typeof raw !== "string")
11
+ return null;
12
+ const v = raw.startsWith("v") ? raw.slice(1) : raw;
13
+ const [core, ...preParts] = v.split("-");
14
+ const pre = preParts.length > 0 ? preParts.join("-") : null;
15
+ const segments = core.split(".");
16
+ if (segments.length !== 3)
17
+ return null;
18
+ const [major, minor, patch] = segments.map((s) => {
19
+ if (!/^\d+$/.test(s))
20
+ return NaN;
21
+ return Number(s);
22
+ });
23
+ if ([major, minor, patch].some((n) => Number.isNaN(n)))
24
+ return null;
25
+ return { major, minor, patch, pre: pre === null ? null : pre.split(".") };
26
+ }
27
+ /** SemVer precedence: numeric core, then a release outranks a prerelease, then
28
+ * prerelease identifiers compared field-by-field (numeric < non-numeric per
29
+ * spec, shorter set loses when all shared fields are equal). */
30
+ function compareVersions(a, b) {
31
+ if (a.major !== b.major)
32
+ return a.major - b.major;
33
+ if (a.minor !== b.minor)
34
+ return a.minor - b.minor;
35
+ if (a.patch !== b.patch)
36
+ return a.patch - b.patch;
37
+ if (a.pre === null && b.pre === null)
38
+ return 0;
39
+ if (a.pre === null)
40
+ return 1;
41
+ if (b.pre === null)
42
+ return -1;
43
+ const len = Math.max(a.pre.length, b.pre.length);
44
+ for (let i = 0; i < len; i++) {
45
+ const ai = a.pre[i];
46
+ const bi = b.pre[i];
47
+ if (ai === undefined)
48
+ return -1;
49
+ if (bi === undefined)
50
+ return 1;
51
+ const an = /^\d+$/.test(ai);
52
+ const bn = /^\d+$/.test(bi);
53
+ if (an && bn) {
54
+ const d = Number(ai) - Number(bi);
55
+ if (d !== 0)
56
+ return d;
57
+ }
58
+ else if (an !== bn) {
59
+ return an ? -1 : 1;
60
+ }
61
+ else if (ai !== bi) {
62
+ return ai < bi ? -1 : 1;
63
+ }
64
+ }
65
+ return 0;
66
+ }
67
+ /** Read a loaded module's `namespace/name` identity, version, and raw owner
68
+ * text. Returns `null` for modules without a namespace: only a registry
69
+ * identity (`<namespace>/<name>`) is a stable cross-import key. Two namespace-
70
+ * less local libraries that merely share a `metadata.name` are distinct modules
71
+ * reached via distinct source URLs — reconciling them would drop one and break
72
+ * its kinds; the same local file reached via two paths is already collapsed by
73
+ * canonical-source dedup, so there is nothing left to reconcile here. */
74
+ function moduleIdentityOf(mod) {
75
+ const doc = mod.owner.manifests.find((m) => m && isModuleKind(m.kind));
76
+ if (!doc)
77
+ return null;
78
+ const meta = doc.metadata;
79
+ const name = meta?.name;
80
+ if (typeof name !== "string" || name.length === 0)
81
+ return null;
82
+ if (typeof meta.namespace !== "string" || meta.namespace.length === 0)
83
+ return null;
84
+ const version = typeof meta.version === "string" ? meta.version : "";
85
+ return {
86
+ source: mod.owner.source,
87
+ identity: `${meta.namespace}/${name}`,
88
+ version,
89
+ parsed: parseVersion(version),
90
+ text: mod.owner.text,
91
+ };
92
+ }
93
+ /** Pick the winning member of a same-identity group and classify it. The winner
94
+ * is the highest version (deterministic tiebreak on source URL for equal
95
+ * versions / same-version-different-source). A major disagreement — or any
96
+ * unparseable version — marks the group a conflict; we still pick a winner so
97
+ * the rest of analysis proceeds against a single version instead of cascading
98
+ * duplicate-kind errors. */
99
+ function resolveGroup(members) {
100
+ const majors = new Set();
101
+ for (const m of members)
102
+ majors.add(m.parsed ? m.parsed.major : null);
103
+ const conflict = majors.has(null) || majors.size > 1;
104
+ const winner = members.reduce((best, cur) => {
105
+ if (!cur.parsed)
106
+ return best;
107
+ if (!best.parsed)
108
+ return cur;
109
+ const cmp = compareVersions(cur.parsed, best.parsed);
110
+ if (cmp > 0)
111
+ return cur;
112
+ if (cmp === 0 && cur.source < best.source)
113
+ return cur;
114
+ return best;
115
+ }, members[0]);
116
+ return { winner, conflict };
117
+ }
118
+ /** The diagnostic for a redirected edge, or `null` when the redirect is a
119
+ * silent dedupe (the same version resolved from two sources with identical
120
+ * content — no decision was made, so nothing to report). */
121
+ function hoistDiagnostic(identity, importerSource, alias, loser, winner, conflict) {
122
+ const data = { filePath: importerSource, path: `imports.${alias}` };
123
+ if (conflict) {
124
+ return {
125
+ severity: DiagnosticSeverity.Error,
126
+ code: "MODULE_VERSION_CONFLICT",
127
+ source: SOURCE,
128
+ message: `Module '${identity}' is imported at incompatible major versions: ` +
129
+ `${loser.version || "<unknown>"} here and ${winner.version} elsewhere in the same graph. ` +
130
+ `Major versions can carry breaking changes and cannot be reconciled automatically — ` +
131
+ `align every importer on one major.`,
132
+ data,
133
+ };
134
+ }
135
+ if (loser.version === winner.version) {
136
+ // Same version, two sources. Identical content is a no-op dedupe; differing
137
+ // content means one is masquerading as the other (e.g. a local checkout vs
138
+ // the published version) — worth surfacing.
139
+ if (loser.text === winner.text)
140
+ return null;
141
+ return {
142
+ severity: DiagnosticSeverity.Warning,
143
+ code: "MODULE_VERSION_HOISTED",
144
+ source: SOURCE,
145
+ message: `Module '${identity}@${winner.version}' is imported from two sources whose contents ` +
146
+ `differ ('${loser.source}' and '${winner.source}'). Using '${winner.source}' for every ` +
147
+ `importer — pin a single source to remove the ambiguity.`,
148
+ data,
149
+ };
150
+ }
151
+ return {
152
+ severity: DiagnosticSeverity.Warning,
153
+ code: "MODULE_VERSION_HOISTED",
154
+ source: SOURCE,
155
+ message: `Module '${identity}@${loser.version || "<unknown>"}' was hoisted to '${winner.version}' ` +
156
+ `because the same module is imported at the higher version elsewhere in the graph. ` +
157
+ `Pre-1.0 versions are additive, so the higher version is used for every importer.`,
158
+ data,
159
+ };
160
+ }
161
+ /**
162
+ * Reconcile a loaded import graph so each module identity (`namespace/name`)
163
+ * resolves to a single version. Within a shared major the highest version wins
164
+ * (a non-lossy hoist, given Telo's additive-only pre-1.0 policy); a major
165
+ * mismatch is a hard conflict. Mutates `importEdges` in place — every edge that
166
+ * pointed at a losing source is repointed at the winner — so `flattenForAnalyzer`
167
+ * walks a deduplicated graph and the runtime collision (two definitions of the
168
+ * same kind) cannot occur. Pure and browser-safe: no I/O, no Node built-ins.
169
+ */
170
+ export function reconcileModuleVersions(modules, importEdges) {
171
+ const overrides = new Map();
172
+ const diagnostics = [];
173
+ const groups = new Map();
174
+ const infoBySource = new Map();
175
+ for (const mod of modules.values()) {
176
+ const info = moduleIdentityOf(mod);
177
+ if (!info)
178
+ continue;
179
+ infoBySource.set(info.source, info);
180
+ const list = groups.get(info.identity);
181
+ if (list)
182
+ list.push(info);
183
+ else
184
+ groups.set(info.identity, [info]);
185
+ }
186
+ const conflictByIdentity = new Map();
187
+ for (const [identity, members] of groups) {
188
+ if (members.length < 2)
189
+ continue;
190
+ const { winner, conflict } = resolveGroup(members);
191
+ conflictByIdentity.set(identity, conflict);
192
+ for (const member of members) {
193
+ if (member.source !== winner.source)
194
+ overrides.set(member.source, winner.source);
195
+ }
196
+ }
197
+ if (overrides.size === 0)
198
+ return { overrides, diagnostics };
199
+ for (const [importerSource, aliasMap] of importEdges) {
200
+ for (const [alias, edge] of aliasMap) {
201
+ const winnerSource = overrides.get(edge.targetSource);
202
+ if (!winnerSource)
203
+ continue;
204
+ const loser = infoBySource.get(edge.targetSource);
205
+ const winner = infoBySource.get(winnerSource);
206
+ if (loser && winner) {
207
+ const diag = hoistDiagnostic(loser.identity, importerSource, alias, loser, winner, conflictByIdentity.get(loser.identity) ?? false);
208
+ if (diag)
209
+ diagnostics.push(diag);
210
+ }
211
+ edge.targetSource = winnerSource;
212
+ }
213
+ }
214
+ return { overrides, diagnostics };
215
+ }
@@ -0,0 +1,8 @@
1
+ import type { ManifestSource } from "../types.js";
2
+ /** The browser-safe built-in sources, in resolution order: HTTP fetch then
3
+ * registry. Node-specific sources (local filesystem) are supplied by the
4
+ * consuming package and passed alongside these into the `Loader` constructor.
5
+ * Callers that only want a subset (e.g. the editor, which brings its own
6
+ * registry adapters) construct the individual sources directly. */
7
+ export declare function defaultSources(registryUrl?: string): ManifestSource[];
8
+ //# sourceMappingURL=default-sources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-sources.d.ts","sourceRoot":"","sources":["../../src/sources/default-sources.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIlD;;;;oEAIoE;AACpE,wBAAgB,cAAc,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAErE"}
@@ -0,0 +1,10 @@
1
+ import { HttpSource } from "./http-source.js";
2
+ import { RegistrySource } from "./registry-source.js";
3
+ /** The browser-safe built-in sources, in resolution order: HTTP fetch then
4
+ * registry. Node-specific sources (local filesystem) are supplied by the
5
+ * consuming package and passed alongside these into the `Loader` constructor.
6
+ * Callers that only want a subset (e.g. the editor, which brings its own
7
+ * registry adapters) construct the individual sources directly. */
8
+ export function defaultSources(registryUrl) {
9
+ return [new HttpSource(), new RegistrySource(registryUrl)];
10
+ }
package/dist/types.d.ts CHANGED
@@ -68,14 +68,6 @@ export interface LoadOptions {
68
68
  desugarImports?: boolean;
69
69
  }
70
70
  export interface LoaderInitOptions {
71
- /** Sources inserted with highest priority before built-ins. */
72
- extraSources?: ManifestSource[];
73
- /** Include built-in HttpSource. Defaults to true. */
74
- includeHttpSource?: boolean;
75
- /** Include built-in RegistrySource. Defaults to true. */
76
- includeRegistrySource?: boolean;
77
- /** Base URL used by built-in RegistrySource when enabled. */
78
- registryUrl?: string;
79
71
  /** Handlers for CEL stdlib functions (e.g. `sha256`). Analyzer-only callers may
80
72
  * omit this and get throwing stubs; runtime callers (kernel) must supply real impls. */
81
73
  celHandlers?: import("./cel-environment.js").CelHandlers;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;qEAEiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;qEAEiE;IACjE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;mEAO+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,YAAY,CAAC,EAAE,cAAc,EAAE,CAAC;IAChC,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,yDAAyD;IACzD,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;6FACyF;IACzF,WAAW,CAAC,EAAE,OAAO,sBAAsB,EAAE,WAAW,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;;;sDAUkD;IAClD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;IACpE;;;;+EAI2E;IAC3E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC,CAAC;CAC5E"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;qEAEiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;qEAEiE;IACjE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;mEAO+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC;6FACyF;IACzF,WAAW,CAAC,EAAE,OAAO,sBAAsB,EAAE,WAAW,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;;;sDAUkD;IAClD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;IACpE;;;;+EAI2E;IAC3E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC,CAAC;CAC5E"}
@@ -20,7 +20,7 @@ export interface ContextResolveOpts {
20
20
  * - Object with `kind` + `schema`: inline type definition → return the `schema`
21
21
  * - Object with `type` or `properties`: raw JSON Schema, return as-is
22
22
  */
23
- export declare function resolveTypeFieldToSchema(value: unknown, allManifests: Record<string, any>[]): Record<string, any> | undefined;
23
+ export declare function resolveTypeFieldToSchema(value: unknown, allManifests: Record<string, any>[], ancestry?: ReadonlySet<string>): Record<string, any> | undefined;
24
24
  /**
25
25
  * Returns true when a CEL expression path (from walkCelExpressions, e.g. "routes[0].inputs.q")
26
26
  * falls within the scope of a context (e.g. "$.routes[*].inputs").
@@ -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,CAkCjC;AA6DD;;;;;;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,CAgHrB;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;AAWD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,SAAM,GACT,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAAE,CAAC,CAGvD"}
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;AAGtF,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,EACnC,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA0CjC;AAuFD;;;;;;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,CAgHrB;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;AAWD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,SAAM,GACT,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAAE,CAAC,CAGvD"}
@@ -1,27 +1,33 @@
1
1
  export { extractAccessChains, validateChainAgainstSchema } from "@telorun/templating";
2
+ import { mergeTypeSchemas } from "@telorun/sdk";
2
3
  /**
3
4
  * Resolve a type field value (string name, inline type, or raw schema) to a JSON Schema.
4
5
  * - String: look up the named type in allManifests (Type.JsonSchema resources)
5
6
  * - Object with `kind` + `schema`: inline type definition → return the `schema`
6
7
  * - Object with `type` or `properties`: raw JSON Schema, return as-is
7
8
  */
8
- export function resolveTypeFieldToSchema(value, allManifests) {
9
+ export function resolveTypeFieldToSchema(value, allManifests, ancestry = new Set()) {
9
10
  if (!value)
10
11
  return undefined;
11
12
  if (typeof value === "string") {
13
+ // Cycle guard: a type already on the resolution path can't extend back into it.
14
+ if (ancestry.has(value))
15
+ return undefined;
12
16
  // Named type reference — find a Telo.Type resource by name
13
17
  const typeManifest = allManifests.find((m) => m.metadata?.name === value &&
14
18
  typeof m.kind === "string" &&
15
19
  /\bType\b/.test(m.kind) &&
16
20
  typeof m.schema === "object" &&
17
21
  m.schema !== null);
18
- return typeManifest?.schema;
22
+ if (!typeManifest)
23
+ return undefined;
24
+ return applyExtends(typeManifest.schema, typeManifest.extends, allManifests, new Set(ancestry).add(value));
19
25
  }
20
26
  if (typeof value === "object" && value !== null) {
21
27
  const obj = value;
22
28
  // Inline type resource: { kind: "Type.JsonSchema", schema: {...} }
23
29
  if (obj.schema && typeof obj.schema === "object") {
24
- return obj.schema;
30
+ return applyExtends(obj.schema, obj.extends, allManifests, ancestry);
25
31
  }
26
32
  // Raw JSON Schema (has type or properties)
27
33
  if (obj.type || obj.properties) {
@@ -30,11 +36,34 @@ export function resolveTypeFieldToSchema(value, allManifests) {
30
36
  // Named type reference resolved from a `!ref` → { kind, name } — resolve the
31
37
  // named Telo.Type the same way as the bare-string form.
32
38
  if (typeof obj.name === "string") {
33
- return resolveTypeFieldToSchema(obj.name, allManifests);
39
+ return resolveTypeFieldToSchema(obj.name, allManifests, ancestry);
34
40
  }
35
41
  }
36
42
  return undefined;
37
43
  }
44
+ /**
45
+ * Fold a `Type.JsonSchema`'s `extends` parents into its own schema, matching the
46
+ * runtime `type` controller exactly — both call the shared `mergeTypeSchemas`, so
47
+ * static analysis and runtime validation can never disagree on a type's effective
48
+ * shape. Without this the analyzer would see only a child type's own properties
49
+ * and reject valid access to an inherited field with a false `CEL_UNKNOWN_FIELD`.
50
+ * `ancestry` carries the resolution path for cycle detection (siblings share it
51
+ * unmutated, so diamond inheritance still re-includes a shared grandparent).
52
+ */
53
+ function applyExtends(ownSchema, extendsField, allManifests, ancestry) {
54
+ if (!extendsField)
55
+ return ownSchema;
56
+ const parents = Array.isArray(extendsField) ? extendsField : [extendsField];
57
+ const resolved = [];
58
+ for (const parent of parents) {
59
+ const parentSchema = resolveTypeFieldToSchema(parent, allManifests, ancestry);
60
+ if (parentSchema)
61
+ resolved.push(parentSchema);
62
+ }
63
+ if (resolved.length === 0)
64
+ return ownSchema;
65
+ return mergeTypeSchemas([...resolved, ownSchema]);
66
+ }
38
67
  /** Pull the raw expression source from a CEL field value — a compiled value
39
68
  * (`{ source }`), or a string (`!cel "x"` or `"${{ x }}"`). Strips a lone
40
69
  * `${{ }}` wrapper. Returns null when no source is recoverable. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.27.0",
3
+ "version": "0.28.1",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -48,7 +48,7 @@
48
48
  "@types/node": "^20.0.0",
49
49
  "typescript": "^5.0.0",
50
50
  "vitest": "^2.1.8",
51
- "@telorun/sdk": "0.36.0"
51
+ "@telorun/sdk": "0.38.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@telorun/sdk": "*"
package/src/index.ts CHANGED
@@ -39,6 +39,8 @@ export { parseLoadedFile } from "./parse-loaded-file.js";
39
39
  export type { ParseOptions } from "./parse-loaded-file.js";
40
40
  export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
41
41
  export type { SyntheticImport } from "./inline-imports.js";
42
+ export { reconcileModuleVersions } from "./reconcile-module-versions.js";
43
+ export type { VersionReconciliation } from "./reconcile-module-versions.js";
42
44
  export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
43
45
  export {
44
46
  buildDocumentPositions,
@@ -49,6 +51,7 @@ export {
49
51
  export type { DocumentPosition } from "./position-metadata.js";
50
52
  export { HttpSource } from "./sources/http-source.js";
51
53
  export { RegistrySource } from "./sources/registry-source.js";
54
+ export { defaultSources } from "./sources/default-sources.js";
52
55
  export { withSyntheticPositions } from "./with-synthetic-positions.js";
53
56
  export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
54
57
  export type {
@@ -1,7 +1,7 @@
1
1
  import type { ResourceManifest } from "@telorun/sdk";
2
2
  import type { Document } from "yaml";
3
3
  import type { DocumentPosition } from "./position-metadata.js";
4
- import type { Range } from "./types.js";
4
+ import type { AnalysisDiagnostic, Range } from "./types.js";
5
5
 
6
6
  /** One physical file's parsed result. Returned for the owner manifest, for
7
7
  * each `include:` partial, and for each external import target.
@@ -70,8 +70,21 @@ export interface LoadedGraph {
70
70
  * its partials. */
71
71
  modules: Map<string, LoadedModule>;
72
72
  /** Per-Telo.Import resolution. Keyed by the resolved URL of the file the
73
- * Telo.Import was declared in, then by the import's PascalCase alias. */
73
+ * Telo.Import was declared in, then by the import's PascalCase alias.
74
+ * Version reconciliation repoints losing edges at their winner here, so a
75
+ * consumer walking these edges (`flattenForAnalyzer`) sees one version per
76
+ * module identity. */
74
77
  importEdges: Map<string, Map<string, ImportEdge>>;
78
+ /** Version-reconciliation redirects: a losing module's canonical source URL →
79
+ * the winning version's canonical source URL. The runtime consults this when
80
+ * it independently re-resolves an import (the analyzer already sees repointed
81
+ * `importEdges`). Empty when no module identity appeared at two sources. */
82
+ overrides: Map<string, string>;
83
+ /** Diagnostics produced while reconciling module versions — one per import
84
+ * edge redirected to a different version (warning for a same-major hoist,
85
+ * error for a major mismatch). Surfaced alongside `analyze()` diagnostics by
86
+ * every consumer (CLI, editor, VS Code). */
87
+ versionDiagnostics: AnalysisDiagnostic[];
75
88
  /** Surface-level errors that did not abort the graph load (e.g. an import
76
89
  * whose target failed to fetch). */
77
90
  errors: GraphLoadError[];
@@ -1,7 +1,5 @@
1
1
  import type { Environment } from "@marcbachmann/cel-js";
2
2
  import type { ResourceManifest } from "@telorun/sdk";
3
- import { HttpSource } from "./sources/http-source.js";
4
- import { RegistrySource } from "./sources/registry-source.js";
5
3
  import { buildCelEnvironment } from "./cel-environment.js";
6
4
  import type {
7
5
  GraphLoadError,
@@ -13,6 +11,7 @@ import type {
13
11
  import { desugarLoadedFile } from "./inline-imports.js";
14
12
  import { isModuleKind } from "./module-kinds.js";
15
13
  import { parseLoadedFile } from "./parse-loaded-file.js";
14
+ import { reconcileModuleVersions } from "./reconcile-module-versions.js";
16
15
  import {
17
16
  DEFAULT_MANIFEST_FILENAME,
18
17
  type LoadOptions,
@@ -53,22 +52,13 @@ export class Loader {
53
52
  protected sources: ManifestSource[];
54
53
  private readonly celEnv: Environment;
55
54
 
56
- constructor(extraSourcesOrOptions: ManifestSource[] | LoaderInitOptions = []) {
57
- const options: LoaderInitOptions = Array.isArray(extraSourcesOrOptions)
58
- ? { extraSources: extraSourcesOrOptions }
59
- : extraSourcesOrOptions;
60
-
61
- const includeHttpSource = options.includeHttpSource ?? true;
62
- const includeRegistrySource = options.includeRegistrySource ?? true;
63
-
64
- this.sources = [];
65
- if (includeHttpSource) this.sources.push(new HttpSource());
66
- if (includeRegistrySource) this.sources.push(new RegistrySource(options.registryUrl));
67
-
68
- if (options.extraSources?.length) {
69
- this.sources.unshift(...options.extraSources);
70
- }
71
-
55
+ /** Sources are resolved in order — the first whose `supports(url)` matches
56
+ * wins. The caller (composition root) decides which concrete sources exist
57
+ * and supplies them; `defaultSources()` bundles the browser-safe built-ins
58
+ * (HTTP + registry) for the common case. `register()` prepends a source at
59
+ * runtime. */
60
+ constructor(sources: ManifestSource[] = [], options: LoaderInitOptions = {}) {
61
+ this.sources = [...sources];
72
62
  this.celEnv = buildCelEnvironment(options.celHandlers);
73
63
  }
74
64
 
@@ -307,7 +297,20 @@ export class Loader {
307
297
  }
308
298
  }
309
299
 
310
- return { rootSource, entry, modules, importEdges, errors };
300
+ // Collapse multiple versions of the same module identity onto one version
301
+ // before any consumer walks the edges: repoints losing `importEdges` in
302
+ // place and yields the runtime override map + hoist/conflict diagnostics.
303
+ const { overrides, diagnostics } = reconcileModuleVersions(modules, importEdges);
304
+
305
+ return {
306
+ rootSource,
307
+ entry,
308
+ modules,
309
+ importEdges,
310
+ overrides,
311
+ versionDiagnostics: diagnostics,
312
+ errors,
313
+ };
311
314
  }
312
315
 
313
316
  /** Resolve an `import` URL against the file it appears in. Relative /
@@ -0,0 +1,253 @@
1
+ import type { ImportEdge, LoadedModule } from "./loaded-types.js";
2
+ import { isModuleKind } from "./module-kinds.js";
3
+ import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
4
+
5
+ const SOURCE = "telo-analyzer";
6
+
7
+ /** Outcome of reconciling a module name that appears at more than one resolved
8
+ * source in a single import graph. The `overrides` map redirects each losing
9
+ * canonical URL to the winner's canonical URL — consulted by the runtime when
10
+ * it independently re-resolves an import (the analyzer side is handled by
11
+ * repointing `importEdges` in place). */
12
+ export interface VersionReconciliation {
13
+ /** Loser canonical source URL → winner canonical source URL. */
14
+ overrides: Map<string, string>;
15
+ /** One diagnostic per import edge that pointed at a non-winner: a warning for
16
+ * a same-major hoist, an error for an incompatible major mismatch. */
17
+ diagnostics: AnalysisDiagnostic[];
18
+ }
19
+
20
+ interface ParsedVersion {
21
+ major: number;
22
+ minor: number;
23
+ patch: number;
24
+ /** Dot-separated prerelease identifiers, or `null` for a release version. */
25
+ pre: string[] | null;
26
+ }
27
+
28
+ interface ModuleIdentity {
29
+ source: string;
30
+ identity: string;
31
+ version: string;
32
+ parsed: ParsedVersion | null;
33
+ text: string;
34
+ }
35
+
36
+ /** Parse `X.Y.Z`, `vX.Y.Z`, or `X.Y.Z-pre.1`. Returns `null` for anything that
37
+ * isn't a plain three-part numeric core — an unparseable version forces the
38
+ * group onto the conflict path (we never silently hoist across a version we
39
+ * can't reason about). Pure: no dependency on the `semver` package, so the
40
+ * analyzer stays browser-safe and dependency-free. */
41
+ function parseVersion(raw: string | undefined): ParsedVersion | null {
42
+ if (typeof raw !== "string") return null;
43
+ const v = raw.startsWith("v") ? raw.slice(1) : raw;
44
+ const [core, ...preParts] = v.split("-");
45
+ const pre = preParts.length > 0 ? preParts.join("-") : null;
46
+ const segments = core.split(".");
47
+ if (segments.length !== 3) return null;
48
+ const [major, minor, patch] = segments.map((s) => {
49
+ if (!/^\d+$/.test(s)) return NaN;
50
+ return Number(s);
51
+ });
52
+ if ([major, minor, patch].some((n) => Number.isNaN(n))) return null;
53
+ return { major, minor, patch, pre: pre === null ? null : pre.split(".") };
54
+ }
55
+
56
+ /** SemVer precedence: numeric core, then a release outranks a prerelease, then
57
+ * prerelease identifiers compared field-by-field (numeric < non-numeric per
58
+ * spec, shorter set loses when all shared fields are equal). */
59
+ function compareVersions(a: ParsedVersion, b: ParsedVersion): number {
60
+ if (a.major !== b.major) return a.major - b.major;
61
+ if (a.minor !== b.minor) return a.minor - b.minor;
62
+ if (a.patch !== b.patch) return a.patch - b.patch;
63
+ if (a.pre === null && b.pre === null) return 0;
64
+ if (a.pre === null) return 1;
65
+ if (b.pre === null) return -1;
66
+ const len = Math.max(a.pre.length, b.pre.length);
67
+ for (let i = 0; i < len; i++) {
68
+ const ai = a.pre[i];
69
+ const bi = b.pre[i];
70
+ if (ai === undefined) return -1;
71
+ if (bi === undefined) return 1;
72
+ const an = /^\d+$/.test(ai);
73
+ const bn = /^\d+$/.test(bi);
74
+ if (an && bn) {
75
+ const d = Number(ai) - Number(bi);
76
+ if (d !== 0) return d;
77
+ } else if (an !== bn) {
78
+ return an ? -1 : 1;
79
+ } else if (ai !== bi) {
80
+ return ai < bi ? -1 : 1;
81
+ }
82
+ }
83
+ return 0;
84
+ }
85
+
86
+ /** Read a loaded module's `namespace/name` identity, version, and raw owner
87
+ * text. Returns `null` for modules without a namespace: only a registry
88
+ * identity (`<namespace>/<name>`) is a stable cross-import key. Two namespace-
89
+ * less local libraries that merely share a `metadata.name` are distinct modules
90
+ * reached via distinct source URLs — reconciling them would drop one and break
91
+ * its kinds; the same local file reached via two paths is already collapsed by
92
+ * canonical-source dedup, so there is nothing left to reconcile here. */
93
+ function moduleIdentityOf(mod: LoadedModule): ModuleIdentity | null {
94
+ const doc = mod.owner.manifests.find((m) => m && isModuleKind(m.kind));
95
+ if (!doc) return null;
96
+ const meta = doc.metadata as { name?: string; namespace?: string | null; version?: string };
97
+ const name = meta?.name;
98
+ if (typeof name !== "string" || name.length === 0) return null;
99
+ if (typeof meta.namespace !== "string" || meta.namespace.length === 0) return null;
100
+ const version = typeof meta.version === "string" ? meta.version : "";
101
+ return {
102
+ source: mod.owner.source,
103
+ identity: `${meta.namespace}/${name}`,
104
+ version,
105
+ parsed: parseVersion(version),
106
+ text: mod.owner.text,
107
+ };
108
+ }
109
+
110
+ interface GroupResolution {
111
+ winner: ModuleIdentity;
112
+ /** True when members disagree on major version (or a version is unparseable). */
113
+ conflict: boolean;
114
+ }
115
+
116
+ /** Pick the winning member of a same-identity group and classify it. The winner
117
+ * is the highest version (deterministic tiebreak on source URL for equal
118
+ * versions / same-version-different-source). A major disagreement — or any
119
+ * unparseable version — marks the group a conflict; we still pick a winner so
120
+ * the rest of analysis proceeds against a single version instead of cascading
121
+ * duplicate-kind errors. */
122
+ function resolveGroup(members: ModuleIdentity[]): GroupResolution {
123
+ const majors = new Set<number | null>();
124
+ for (const m of members) majors.add(m.parsed ? m.parsed.major : null);
125
+ const conflict = majors.has(null) || majors.size > 1;
126
+
127
+ const winner = members.reduce((best, cur) => {
128
+ if (!cur.parsed) return best;
129
+ if (!best.parsed) return cur;
130
+ const cmp = compareVersions(cur.parsed, best.parsed);
131
+ if (cmp > 0) return cur;
132
+ if (cmp === 0 && cur.source < best.source) return cur;
133
+ return best;
134
+ }, members[0]);
135
+
136
+ return { winner, conflict };
137
+ }
138
+
139
+ /** The diagnostic for a redirected edge, or `null` when the redirect is a
140
+ * silent dedupe (the same version resolved from two sources with identical
141
+ * content — no decision was made, so nothing to report). */
142
+ function hoistDiagnostic(
143
+ identity: string,
144
+ importerSource: string,
145
+ alias: string,
146
+ loser: ModuleIdentity,
147
+ winner: ModuleIdentity,
148
+ conflict: boolean,
149
+ ): AnalysisDiagnostic | null {
150
+ const data = { filePath: importerSource, path: `imports.${alias}` };
151
+ if (conflict) {
152
+ return {
153
+ severity: DiagnosticSeverity.Error,
154
+ code: "MODULE_VERSION_CONFLICT",
155
+ source: SOURCE,
156
+ message:
157
+ `Module '${identity}' is imported at incompatible major versions: ` +
158
+ `${loser.version || "<unknown>"} here and ${winner.version} elsewhere in the same graph. ` +
159
+ `Major versions can carry breaking changes and cannot be reconciled automatically — ` +
160
+ `align every importer on one major.`,
161
+ data,
162
+ };
163
+ }
164
+ if (loser.version === winner.version) {
165
+ // Same version, two sources. Identical content is a no-op dedupe; differing
166
+ // content means one is masquerading as the other (e.g. a local checkout vs
167
+ // the published version) — worth surfacing.
168
+ if (loser.text === winner.text) return null;
169
+ return {
170
+ severity: DiagnosticSeverity.Warning,
171
+ code: "MODULE_VERSION_HOISTED",
172
+ source: SOURCE,
173
+ message:
174
+ `Module '${identity}@${winner.version}' is imported from two sources whose contents ` +
175
+ `differ ('${loser.source}' and '${winner.source}'). Using '${winner.source}' for every ` +
176
+ `importer — pin a single source to remove the ambiguity.`,
177
+ data,
178
+ };
179
+ }
180
+ return {
181
+ severity: DiagnosticSeverity.Warning,
182
+ code: "MODULE_VERSION_HOISTED",
183
+ source: SOURCE,
184
+ message:
185
+ `Module '${identity}@${loser.version || "<unknown>"}' was hoisted to '${winner.version}' ` +
186
+ `because the same module is imported at the higher version elsewhere in the graph. ` +
187
+ `Pre-1.0 versions are additive, so the higher version is used for every importer.`,
188
+ data,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Reconcile a loaded import graph so each module identity (`namespace/name`)
194
+ * resolves to a single version. Within a shared major the highest version wins
195
+ * (a non-lossy hoist, given Telo's additive-only pre-1.0 policy); a major
196
+ * mismatch is a hard conflict. Mutates `importEdges` in place — every edge that
197
+ * pointed at a losing source is repointed at the winner — so `flattenForAnalyzer`
198
+ * walks a deduplicated graph and the runtime collision (two definitions of the
199
+ * same kind) cannot occur. Pure and browser-safe: no I/O, no Node built-ins.
200
+ */
201
+ export function reconcileModuleVersions(
202
+ modules: Map<string, LoadedModule>,
203
+ importEdges: Map<string, Map<string, ImportEdge>>,
204
+ ): VersionReconciliation {
205
+ const overrides = new Map<string, string>();
206
+ const diagnostics: AnalysisDiagnostic[] = [];
207
+
208
+ const groups = new Map<string, ModuleIdentity[]>();
209
+ const infoBySource = new Map<string, ModuleIdentity>();
210
+ for (const mod of modules.values()) {
211
+ const info = moduleIdentityOf(mod);
212
+ if (!info) continue;
213
+ infoBySource.set(info.source, info);
214
+ const list = groups.get(info.identity);
215
+ if (list) list.push(info);
216
+ else groups.set(info.identity, [info]);
217
+ }
218
+
219
+ const conflictByIdentity = new Map<string, boolean>();
220
+ for (const [identity, members] of groups) {
221
+ if (members.length < 2) continue;
222
+ const { winner, conflict } = resolveGroup(members);
223
+ conflictByIdentity.set(identity, conflict);
224
+ for (const member of members) {
225
+ if (member.source !== winner.source) overrides.set(member.source, winner.source);
226
+ }
227
+ }
228
+
229
+ if (overrides.size === 0) return { overrides, diagnostics };
230
+
231
+ for (const [importerSource, aliasMap] of importEdges) {
232
+ for (const [alias, edge] of aliasMap) {
233
+ const winnerSource = overrides.get(edge.targetSource);
234
+ if (!winnerSource) continue;
235
+ const loser = infoBySource.get(edge.targetSource);
236
+ const winner = infoBySource.get(winnerSource);
237
+ if (loser && winner) {
238
+ const diag = hoistDiagnostic(
239
+ loser.identity,
240
+ importerSource,
241
+ alias,
242
+ loser,
243
+ winner,
244
+ conflictByIdentity.get(loser.identity) ?? false,
245
+ );
246
+ if (diag) diagnostics.push(diag);
247
+ }
248
+ edge.targetSource = winnerSource;
249
+ }
250
+ }
251
+
252
+ return { overrides, diagnostics };
253
+ }
@@ -0,0 +1,12 @@
1
+ import type { ManifestSource } from "../types.js";
2
+ import { HttpSource } from "./http-source.js";
3
+ import { RegistrySource } from "./registry-source.js";
4
+
5
+ /** The browser-safe built-in sources, in resolution order: HTTP fetch then
6
+ * registry. Node-specific sources (local filesystem) are supplied by the
7
+ * consuming package and passed alongside these into the `Loader` constructor.
8
+ * Callers that only want a subset (e.g. the editor, which brings its own
9
+ * registry adapters) construct the individual sources directly. */
10
+ export function defaultSources(registryUrl?: string): ManifestSource[] {
11
+ return [new HttpSource(), new RegistrySource(registryUrl)];
12
+ }
package/src/types.ts CHANGED
@@ -75,14 +75,6 @@ export interface LoadOptions {
75
75
  }
76
76
 
77
77
  export interface LoaderInitOptions {
78
- /** Sources inserted with highest priority before built-ins. */
79
- extraSources?: ManifestSource[];
80
- /** Include built-in HttpSource. Defaults to true. */
81
- includeHttpSource?: boolean;
82
- /** Include built-in RegistrySource. Defaults to true. */
83
- includeRegistrySource?: boolean;
84
- /** Base URL used by built-in RegistrySource when enabled. */
85
- registryUrl?: string;
86
78
  /** Handlers for CEL stdlib functions (e.g. `sha256`). Analyzer-only callers may
87
79
  * omit this and get throwing stubs; runtime callers (kernel) must supply real impls. */
88
80
  celHandlers?: import("./cel-environment.js").CelHandlers;
@@ -1,4 +1,5 @@
1
1
  export { extractAccessChains, validateChainAgainstSchema } from "@telorun/templating";
2
+ import { mergeTypeSchemas } from "@telorun/sdk";
2
3
 
3
4
  export interface ContextResolveOpts {
4
5
  /** When provided, used to resolve `x-telo-context-from-root` annotations against the
@@ -25,10 +26,13 @@ export interface ContextResolveOpts {
25
26
  export function resolveTypeFieldToSchema(
26
27
  value: unknown,
27
28
  allManifests: Record<string, any>[],
29
+ ancestry: ReadonlySet<string> = new Set(),
28
30
  ): Record<string, any> | undefined {
29
31
  if (!value) return undefined;
30
32
 
31
33
  if (typeof value === "string") {
34
+ // Cycle guard: a type already on the resolution path can't extend back into it.
35
+ if (ancestry.has(value)) return undefined;
32
36
  // Named type reference — find a Telo.Type resource by name
33
37
  const typeManifest = allManifests.find(
34
38
  (m) =>
@@ -38,14 +42,20 @@ export function resolveTypeFieldToSchema(
38
42
  typeof m.schema === "object" &&
39
43
  m.schema !== null,
40
44
  );
41
- return typeManifest?.schema as Record<string, any> | undefined;
45
+ if (!typeManifest) return undefined;
46
+ return applyExtends(
47
+ typeManifest.schema as Record<string, any>,
48
+ typeManifest.extends,
49
+ allManifests,
50
+ new Set(ancestry).add(value),
51
+ );
42
52
  }
43
53
 
44
54
  if (typeof value === "object" && value !== null) {
45
55
  const obj = value as Record<string, any>;
46
56
  // Inline type resource: { kind: "Type.JsonSchema", schema: {...} }
47
57
  if (obj.schema && typeof obj.schema === "object") {
48
- return obj.schema as Record<string, any>;
58
+ return applyExtends(obj.schema as Record<string, any>, obj.extends, allManifests, ancestry);
49
59
  }
50
60
  // Raw JSON Schema (has type or properties)
51
61
  if (obj.type || obj.properties) {
@@ -54,13 +64,39 @@ export function resolveTypeFieldToSchema(
54
64
  // Named type reference resolved from a `!ref` → { kind, name } — resolve the
55
65
  // named Telo.Type the same way as the bare-string form.
56
66
  if (typeof obj.name === "string") {
57
- return resolveTypeFieldToSchema(obj.name, allManifests);
67
+ return resolveTypeFieldToSchema(obj.name, allManifests, ancestry);
58
68
  }
59
69
  }
60
70
 
61
71
  return undefined;
62
72
  }
63
73
 
74
+ /**
75
+ * Fold a `Type.JsonSchema`'s `extends` parents into its own schema, matching the
76
+ * runtime `type` controller exactly — both call the shared `mergeTypeSchemas`, so
77
+ * static analysis and runtime validation can never disagree on a type's effective
78
+ * shape. Without this the analyzer would see only a child type's own properties
79
+ * and reject valid access to an inherited field with a false `CEL_UNKNOWN_FIELD`.
80
+ * `ancestry` carries the resolution path for cycle detection (siblings share it
81
+ * unmutated, so diamond inheritance still re-includes a shared grandparent).
82
+ */
83
+ function applyExtends(
84
+ ownSchema: Record<string, any>,
85
+ extendsField: unknown,
86
+ allManifests: Record<string, any>[],
87
+ ancestry: ReadonlySet<string>,
88
+ ): Record<string, any> {
89
+ if (!extendsField) return ownSchema;
90
+ const parents = Array.isArray(extendsField) ? extendsField : [extendsField];
91
+ const resolved: Record<string, any>[] = [];
92
+ for (const parent of parents) {
93
+ const parentSchema = resolveTypeFieldToSchema(parent, allManifests, ancestry);
94
+ if (parentSchema) resolved.push(parentSchema);
95
+ }
96
+ if (resolved.length === 0) return ownSchema;
97
+ return mergeTypeSchemas([...resolved, ownSchema]) as Record<string, any>;
98
+ }
99
+
64
100
  /** Pull the raw expression source from a CEL field value — a compiled value
65
101
  * (`{ source }`), or a string (`!cel "x"` or `"${{ x }}"`). Strips a lone
66
102
  * `${{ }}` wrapper. Returns null when no source is recoverable. */