@telorun/analyzer 0.27.0 → 0.28.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/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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
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;