@domainlang/language 0.1.82 → 0.4.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.
Files changed (111) hide show
  1. package/README.md +18 -18
  2. package/out/domain-lang-module.d.ts +2 -0
  3. package/out/domain-lang-module.js +11 -3
  4. package/out/domain-lang-module.js.map +1 -1
  5. package/out/generated/ast.d.ts +8 -19
  6. package/out/generated/ast.js +1 -10
  7. package/out/generated/ast.js.map +1 -1
  8. package/out/generated/grammar.d.ts +1 -1
  9. package/out/generated/grammar.js +28 -123
  10. package/out/generated/grammar.js.map +1 -1
  11. package/out/generated/module.d.ts +1 -1
  12. package/out/generated/module.js +1 -1
  13. package/out/index.d.ts +3 -0
  14. package/out/index.js +5 -0
  15. package/out/index.js.map +1 -1
  16. package/out/lsp/domain-lang-code-actions.d.ts +55 -0
  17. package/out/lsp/domain-lang-code-actions.js +143 -0
  18. package/out/lsp/domain-lang-code-actions.js.map +1 -0
  19. package/out/lsp/domain-lang-workspace-manager.d.ts +21 -0
  20. package/out/lsp/domain-lang-workspace-manager.js +93 -0
  21. package/out/lsp/domain-lang-workspace-manager.js.map +1 -0
  22. package/out/lsp/hover/domain-lang-hover.js +0 -4
  23. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  24. package/out/lsp/manifest-diagnostics.d.ts +82 -0
  25. package/out/lsp/manifest-diagnostics.js +230 -0
  26. package/out/lsp/manifest-diagnostics.js.map +1 -0
  27. package/out/sdk/index.d.ts +1 -1
  28. package/out/sdk/loader-node.d.ts +7 -3
  29. package/out/sdk/loader-node.js +24 -9
  30. package/out/sdk/loader-node.js.map +1 -1
  31. package/out/sdk/types.d.ts +0 -21
  32. package/out/services/dependency-analyzer.d.ts +3 -39
  33. package/out/services/dependency-analyzer.js +22 -47
  34. package/out/services/dependency-analyzer.js.map +1 -1
  35. package/out/services/dependency-resolver.d.ts +68 -45
  36. package/out/services/dependency-resolver.js +243 -43
  37. package/out/services/dependency-resolver.js.map +1 -1
  38. package/out/services/git-url-resolver.browser.d.ts +4 -12
  39. package/out/services/git-url-resolver.browser.js +5 -1
  40. package/out/services/git-url-resolver.browser.js.map +1 -1
  41. package/out/services/git-url-resolver.d.ts +22 -56
  42. package/out/services/git-url-resolver.js +70 -36
  43. package/out/services/git-url-resolver.js.map +1 -1
  44. package/out/services/governance-validator.d.ts +1 -37
  45. package/out/services/governance-validator.js +4 -10
  46. package/out/services/governance-validator.js.map +1 -1
  47. package/out/services/import-resolver.d.ts +65 -6
  48. package/out/services/import-resolver.js +223 -5
  49. package/out/services/import-resolver.js.map +1 -1
  50. package/out/services/performance-optimizer.d.ts +1 -1
  51. package/out/services/semver.d.ts +98 -0
  52. package/out/services/semver.js +195 -0
  53. package/out/services/semver.js.map +1 -0
  54. package/out/services/types.d.ts +340 -0
  55. package/out/services/types.js +46 -0
  56. package/out/services/types.js.map +1 -0
  57. package/out/services/workspace-manager.d.ts +57 -10
  58. package/out/services/workspace-manager.js +187 -21
  59. package/out/services/workspace-manager.js.map +1 -1
  60. package/out/syntaxes/domain-lang.monarch.js +1 -1
  61. package/out/syntaxes/domain-lang.monarch.js.map +1 -1
  62. package/out/utils/import-utils.d.ts +4 -12
  63. package/out/utils/import-utils.js +35 -135
  64. package/out/utils/import-utils.js.map +1 -1
  65. package/out/validation/constants.d.ts +103 -0
  66. package/out/validation/constants.js +141 -2
  67. package/out/validation/constants.js.map +1 -1
  68. package/out/validation/domain.js +46 -1
  69. package/out/validation/domain.js.map +1 -1
  70. package/out/validation/import.d.ts +46 -22
  71. package/out/validation/import.js +187 -85
  72. package/out/validation/import.js.map +1 -1
  73. package/out/validation/manifest.d.ts +144 -0
  74. package/out/validation/manifest.js +327 -0
  75. package/out/validation/manifest.js.map +1 -0
  76. package/out/validation/maps.js +10 -6
  77. package/out/validation/maps.js.map +1 -1
  78. package/out/validation/metadata.js +5 -1
  79. package/out/validation/metadata.js.map +1 -1
  80. package/package.json +8 -6
  81. package/src/domain-lang-module.ts +18 -6
  82. package/src/domain-lang.langium +7 -12
  83. package/src/generated/ast.ts +7 -20
  84. package/src/generated/grammar.ts +28 -123
  85. package/src/generated/module.ts +1 -1
  86. package/src/index.ts +7 -0
  87. package/src/lsp/domain-lang-code-actions.ts +189 -0
  88. package/src/lsp/domain-lang-workspace-manager.ts +104 -0
  89. package/src/lsp/hover/domain-lang-hover.ts +0 -2
  90. package/src/lsp/manifest-diagnostics.ts +290 -0
  91. package/src/sdk/index.ts +0 -2
  92. package/src/sdk/loader-node.ts +29 -9
  93. package/src/sdk/types.ts +0 -23
  94. package/src/services/dependency-analyzer.ts +24 -84
  95. package/src/services/dependency-resolver.ts +301 -84
  96. package/src/services/git-url-resolver.browser.ts +9 -14
  97. package/src/services/git-url-resolver.ts +86 -93
  98. package/src/services/governance-validator.ts +5 -47
  99. package/src/services/import-resolver.ts +270 -8
  100. package/src/services/performance-optimizer.ts +1 -1
  101. package/src/services/semver.ts +213 -0
  102. package/src/services/types.ts +415 -0
  103. package/src/services/workspace-manager.ts +237 -46
  104. package/src/utils/import-utils.ts +38 -160
  105. package/src/validation/constants.ts +182 -2
  106. package/src/validation/domain.ts +54 -1
  107. package/src/validation/import.ts +228 -104
  108. package/src/validation/manifest.ts +439 -0
  109. package/src/validation/maps.ts +10 -6
  110. package/src/validation/metadata.ts +5 -1
  111. package/src/syntaxes/domain-lang.monarch.ts +0 -29
@@ -9,56 +9,33 @@
9
9
  * 2. Download all direct dependencies
10
10
  * 3. Parse each dependency's model.yaml
11
11
  * 4. Recursively discover transitive dependencies
12
- * 5. Resolve version constraints (simple: use latest satisfying version)
12
+ * 5. Resolve version constraints using "Latest Wins" strategy
13
13
  * 6. Generate lock file with pinned commit hashes
14
+ *
15
+ * Resolution Strategy ("Latest Wins"):
16
+ * - SemVer tags (same major): Pick highest compatible version
17
+ * - Same branch: No conflict, resolve once
18
+ * - Commit pins: Error (explicit pins are intentional)
19
+ * - Major version mismatch: Error
20
+ * - Tag vs Branch: Error (incompatible intent)
14
21
  */
15
22
 
16
23
  import path from 'node:path';
17
24
  import fs from 'node:fs/promises';
18
25
  import YAML from 'yaml';
19
- import { GitUrlParser, GitUrlResolver, PackageMetadata, LockFile, LockedDependency } from './git-url-resolver.js';
20
-
21
- /**
22
- * Dependency graph node representing a package and its dependencies.
23
- */
24
- export interface DependencyNode {
25
- /** Package identifier (owner/repo) */
26
- packageKey: string;
27
- /** Version constraint from parent (e.g., "^1.0.0") */
28
- versionConstraint: string;
29
- /** Resolved version */
30
- resolvedVersion?: string;
31
- /** Resolved commit hash */
32
- commitHash?: string;
33
- /** Full git URL */
34
- repoUrl?: string;
35
- /** Direct dependencies of this package */
36
- dependencies: Record<string, string>;
37
- /** Parent packages that depend on this one */
38
- dependents: string[];
39
- }
26
+ import { GitUrlParser, GitUrlResolver } from './git-url-resolver.js';
27
+ import { parseSemVer, pickLatestSemVer, detectRefType } from './semver.js';
28
+ import type { SemVer, ResolvingPackage, LockFile, LockedDependency, DependencyGraph } from './types.js';
40
29
 
41
- /**
42
- * Dependency graph for the entire workspace.
43
- */
44
- export interface DependencyGraph {
45
- /** All discovered packages, keyed by owner/repo */
46
- nodes: Record<string, DependencyNode>;
47
- /** Root package name */
48
- root: string;
49
- }
50
-
51
- /**
52
- * Resolves dependencies and generates lock files.
53
- */
54
- export type { LockFile, PackageMetadata } from './git-url-resolver.js';
55
30
  export class DependencyResolver {
56
31
  private gitResolver: GitUrlResolver;
57
32
  private workspaceRoot: string;
58
33
 
59
34
  constructor(workspaceRoot: string, gitResolver?: GitUrlResolver) {
60
35
  this.workspaceRoot = workspaceRoot;
61
- this.gitResolver = gitResolver || new GitUrlResolver();
36
+ // Per PRS-010: Project-local cache at .dlang/packages/
37
+ const cacheDir = path.join(workspaceRoot, '.dlang', 'packages');
38
+ this.gitResolver = gitResolver || new GitUrlResolver(cacheDir);
62
39
  }
63
40
 
64
41
  /**
@@ -85,6 +62,13 @@ export class DependencyResolver {
85
62
  // Build dependency graph
86
63
  const graph = await this.buildDependencyGraph(rootConfig);
87
64
 
65
+ // Apply overrides before conflict detection
66
+ this.applyOverrides(graph, rootConfig.overrides);
67
+
68
+ // Detect version conflicts and package-level cycles before resolving
69
+ this.detectVersionConflicts(graph);
70
+ this.detectPackageCycles(graph);
71
+
88
72
  // Resolve version constraints
89
73
  await this.resolveVersions(graph);
90
74
 
@@ -92,22 +76,59 @@ export class DependencyResolver {
92
76
  return this.generateLockFile(graph);
93
77
  }
94
78
 
79
+ /**
80
+ * Applies ref overrides from model.yaml to resolve conflicts explicitly.
81
+ *
82
+ * Overrides take precedence over all other constraints.
83
+ *
84
+ * @example
85
+ * ```yaml
86
+ * overrides:
87
+ * domainlang/core: v2.0.0
88
+ * ```
89
+ */
90
+ private applyOverrides(graph: DependencyGraph, overrides?: Record<string, string>): void {
91
+ if (!overrides) return;
92
+
93
+ for (const [pkg, overrideRef] of Object.entries(overrides)) {
94
+ const node = graph.nodes[pkg];
95
+ if (node) {
96
+ // Override replaces all constraints with a single definitive ref
97
+ node.constraints = new Set([overrideRef]);
98
+ node.refConstraint = overrideRef;
99
+
100
+ // Track that this was an override for messaging
101
+ this.overrideMessages.push(`Override applied: ${pkg}@${overrideRef}`);
102
+ }
103
+ }
104
+ }
105
+
106
+ /** Override messages for CLI output */
107
+ private overrideMessages: string[] = [];
108
+
109
+ /**
110
+ * Returns any override messages from the last dependency resolution.
111
+ */
112
+ getOverrideMessages(): string[] {
113
+ return this.overrideMessages;
114
+ }
115
+
95
116
  /**
96
117
  * Builds the complete dependency graph by recursively discovering transitive dependencies.
97
118
  */
98
- private async buildDependencyGraph(rootConfig: PackageMetadata): Promise<DependencyGraph> {
119
+ private async buildDependencyGraph(rootConfig: ResolvingPackage): Promise<DependencyGraph> {
99
120
  const graph: DependencyGraph = {
100
121
  nodes: {},
101
122
  root: rootConfig.name || 'root',
102
123
  };
103
124
 
104
125
  // Process root dependencies
105
- const queue: Array<{ packageKey: string; versionConstraint: string; parent: string }> = [];
126
+ const queue: Array<{ packageKey: string; refConstraint: string; parent: string }> = [];
106
127
 
107
- for (const [depName, versionConstraint] of Object.entries(rootConfig.dependencies || {})) {
128
+ for (const [depName, refConstraint] of Object.entries(rootConfig.dependencies || {})) {
108
129
  queue.push({
109
130
  packageKey: depName,
110
- versionConstraint,
131
+ refConstraint,
111
132
  parent: graph.root
112
133
  });
113
134
  }
@@ -118,14 +139,17 @@ export class DependencyResolver {
118
139
  while (queue.length > 0) {
119
140
  const entry = queue.shift();
120
141
  if (!entry) break;
121
- const { packageKey, versionConstraint, parent } = entry;
142
+ const { packageKey, refConstraint, parent } = entry;
122
143
 
123
144
  // Skip if already processed
124
145
  if (visited.has(packageKey)) {
125
- // Update dependents list
126
- if (!graph.nodes[packageKey].dependents.includes(parent)) {
127
- graph.nodes[packageKey].dependents.push(parent);
146
+ // Update dependents list and record constraint
147
+ const existing = graph.nodes[packageKey];
148
+ if (!existing.dependents.includes(parent)) {
149
+ existing.dependents.push(parent);
128
150
  }
151
+ if (!existing.constraints) existing.constraints = new Set<string>();
152
+ existing.constraints.add(refConstraint);
129
153
  continue;
130
154
  }
131
155
  visited.add(packageKey);
@@ -133,7 +157,7 @@ export class DependencyResolver {
133
157
  // Parse package identifier
134
158
  const gitInfo = GitUrlParser.parse(packageKey);
135
159
 
136
- // Download package to get its dlang.toml
160
+ // Download package to get its model.yaml
137
161
  const packageUri = await this.gitResolver.resolve(packageKey);
138
162
  const packageDir = path.dirname(packageUri.fsPath);
139
163
 
@@ -143,17 +167,18 @@ export class DependencyResolver {
143
167
  // Add to graph
144
168
  graph.nodes[packageKey] = {
145
169
  packageKey,
146
- versionConstraint,
170
+ refConstraint,
171
+ constraints: new Set<string>([refConstraint]),
147
172
  repoUrl: gitInfo.repoUrl,
148
173
  dependencies: packageConfig.dependencies || {},
149
174
  dependents: [parent],
150
175
  };
151
176
 
152
177
  // Queue transitive dependencies
153
- for (const [transDepName, transVersionConstraint] of Object.entries(packageConfig.dependencies || {})) {
178
+ for (const [transDepName, transRefConstraint] of Object.entries(packageConfig.dependencies || {})) {
154
179
  queue.push({
155
180
  packageKey: transDepName,
156
- versionConstraint: transVersionConstraint,
181
+ refConstraint: transRefConstraint,
157
182
  parent: packageKey,
158
183
  });
159
184
  }
@@ -163,49 +188,52 @@ export class DependencyResolver {
163
188
  }
164
189
 
165
190
  /**
166
- * Resolves version constraints to specific versions.
191
+ * Resolves ref constraints to specific commits.
167
192
  *
168
- * Simple algorithm: Use the latest version that satisfies all constraints.
169
- * Future: Implement proper semantic versioning resolution.
193
+ * Simple algorithm: Use the ref specified in the constraint.
194
+ * Detects refType (tag, branch, or commit) based on format.
170
195
  */
171
196
  private async resolveVersions(graph: DependencyGraph): Promise<void> {
172
197
  for (const [packageKey, node] of Object.entries(graph.nodes)) {
173
198
  // Parse package to get repo info
174
199
  const gitInfo = GitUrlParser.parse(packageKey);
175
200
 
176
- // For now, resolve to the version specified in the constraint
177
- // Future: Implement proper semver range resolution
178
- const version = this.extractVersionFromConstraint(node.versionConstraint);
201
+ // Extract ref from constraint
202
+ const ref = this.extractRefFromConstraint(node.refConstraint);
179
203
 
180
- // Resolve version to commit hash
181
- const commitHash = await this.resolveCommitHash(gitInfo.repoUrl, version);
204
+ // Detect ref type based on format
205
+ const refType = detectRefType(ref);
206
+
207
+ // Resolve ref to commit hash
208
+ const commitHash = await this.resolveCommitHash(gitInfo.repoUrl, ref);
182
209
 
183
- node.resolvedVersion = version;
210
+ node.resolvedRef = ref;
211
+ node.refType = refType;
184
212
  node.commitHash = commitHash;
185
213
  }
186
214
  }
187
215
 
188
216
  /**
189
- * Extracts a version from a constraint string.
217
+ * Extracts a ref from a constraint string.
190
218
  *
191
219
  * Examples:
192
- * - "^1.0.0" → "1.0.0"
193
- * - "~2.3.0" → "2.3.0"
194
- * - "1.5.0" → "1.5.0"
220
+ * - "^1.0.0" → "1.0.0" (treated as tag)
221
+ * - "~2.3.0" → "2.3.0" (treated as tag)
222
+ * - "1.5.0" → "1.5.0" (treated as tag)
223
+ * - "main" → "main" (treated as branch)
224
+ * - "abc123def" → "abc123def" (treated as commit)
195
225
  * - "owner/repo@1.0.0" → "1.0.0"
196
- *
197
- * Future: Implement proper semver range parsing and resolution.
198
226
  */
199
- private extractVersionFromConstraint(constraint: string): string {
200
- // Remove semver operators
201
- let version = constraint.replace(/^[\^~>=<]/, '');
227
+ private extractRefFromConstraint(constraint: string): string {
228
+ // Remove semver operators (legacy support)
229
+ let ref = constraint.replace(/^[\^~>=<]/, '');
202
230
 
203
- // Extract version from full import URL if present
204
- if (version.includes('@')) {
205
- version = version.split('@')[1];
231
+ // Extract ref from full import URL if present
232
+ if (ref.includes('@')) {
233
+ ref = ref.split('@')[1];
206
234
  }
207
235
 
208
- return version || 'main';
236
+ return ref || 'main';
209
237
  }
210
238
 
211
239
  /**
@@ -218,7 +246,7 @@ export class DependencyResolver {
218
246
  const uri = await this.gitResolver.resolve(gitInfo.original);
219
247
 
220
248
  // Extract commit hash from cache path
221
- // Cache format: ~/.dlang/cache/{platform}/{owner}/{repo}/{commit-hash}/
249
+ // Per PRS-010: Project-local cache at .dlang/packages/{owner}/{repo}/{commit}/
222
250
  const pathParts = uri.fsPath.split(path.sep);
223
251
  const commitHashIndex = pathParts.length - 2; // Second to last segment
224
252
  return pathParts[commitHashIndex];
@@ -231,12 +259,16 @@ export class DependencyResolver {
231
259
  const dependencies: Record<string, LockedDependency> = {};
232
260
 
233
261
  for (const [packageKey, node] of Object.entries(graph.nodes)) {
234
- if (!node.resolvedVersion || !node.commitHash) {
235
- throw new Error(`Failed to resolve version for ${packageKey}`);
262
+ if (!node.resolvedRef || !node.commitHash) {
263
+ throw new Error(
264
+ `Failed to resolve ref for '${packageKey}'.\n` +
265
+ `Hint: Check that the package exists and the ref is valid.`
266
+ );
236
267
  }
237
268
 
238
269
  dependencies[packageKey] = {
239
- version: node.resolvedVersion,
270
+ ref: node.resolvedRef,
271
+ refType: node.refType ?? 'commit',
240
272
  resolved: node.repoUrl || '',
241
273
  commit: node.commitHash,
242
274
  // Future: Calculate integrity hash
@@ -252,7 +284,7 @@ export class DependencyResolver {
252
284
  /**
253
285
  * Loads and parses a package's model.yaml file.
254
286
  */
255
- private async loadPackageConfig(packageDir: string): Promise<PackageMetadata> {
287
+ private async loadPackageConfig(packageDir: string): Promise<ResolvingPackage> {
256
288
  const yamlPath = path.join(packageDir, 'model.yaml');
257
289
 
258
290
  try {
@@ -276,40 +308,225 @@ export class DependencyResolver {
276
308
  * dependencies:
277
309
  * package-name:
278
310
  * source: owner/repo
279
- * version: ^1.0.0
311
+ * ref: v1.0.0
280
312
  */
281
- private parseYaml(content: string): PackageMetadata {
313
+ private parseYaml(content: string): ResolvingPackage {
282
314
  const parsed = YAML.parse(content) as {
283
315
  model?: {
284
316
  name?: string;
285
317
  version?: string;
286
318
  entry?: string;
287
319
  };
288
- dependencies?: Record<string, { source?: string; version?: string }>;
320
+ dependencies?: Record<string, { source?: string; ref?: string }>;
321
+ overrides?: Record<string, string>;
289
322
  };
290
323
 
291
- const config: PackageMetadata = {};
324
+ const config: ResolvingPackage = {};
292
325
 
293
326
  if (parsed.model) {
294
327
  config.name = parsed.model.name;
295
328
  config.version = parsed.model.version;
296
- config.main = parsed.model.entry;
329
+ config.entry = parsed.model.entry;
297
330
  }
298
331
 
299
332
  if (parsed.dependencies) {
300
333
  config.dependencies = {};
301
334
  for (const [, depInfo] of Object.entries(parsed.dependencies)) {
302
335
  if (depInfo.source) {
303
- const versionConstraint = depInfo.version || 'main';
304
- // Store as "source@version" for consistency with import resolution
305
- config.dependencies[depInfo.source] = versionConstraint;
336
+ const refConstraint = depInfo.ref || 'main';
337
+ // Store as "source@ref" for consistency with import resolution
338
+ config.dependencies[depInfo.source] = refConstraint;
306
339
  }
307
340
  }
308
341
  }
309
342
 
343
+ // Parse overrides section for explicit ref control
344
+ if (parsed.overrides) {
345
+ config.overrides = parsed.overrides;
346
+ }
347
+
310
348
  return config;
311
349
  }
312
350
 
351
+ /**
352
+ * Detects ref conflicts and applies "Latest Wins" resolution strategy.
353
+ *
354
+ * Resolution Rules:
355
+ * - SemVer tags (same major): Pick highest version automatically
356
+ * - Same branch refs: No conflict, use single resolution
357
+ * - Commit SHA conflicts: Error (explicit pins are intentional)
358
+ * - Major version mismatch: Error (breaking change)
359
+ * - Tag vs Branch: Error (incompatible intent)
360
+ *
361
+ * Modifies graph nodes in-place to set the resolved constraint.
362
+ * Throws an error only for unresolvable conflicts.
363
+ */
364
+ private detectVersionConflicts(graph: DependencyGraph): void {
365
+ const resolutionMessages: string[] = [];
366
+
367
+ for (const [pkg, node] of Object.entries(graph.nodes)) {
368
+ const constraints = node.constraints ?? new Set<string>([node.refConstraint]);
369
+
370
+ if (constraints.size <= 1) continue; // No conflict
371
+
372
+ const refs = Array.from(constraints);
373
+ const refTypes = refs.map(ref => ({
374
+ ref,
375
+ type: detectRefType(ref),
376
+ semver: parseSemVer(ref),
377
+ }));
378
+
379
+ // Check for mixed types (tag vs branch vs commit)
380
+ const types = new Set(refTypes.map(r => r.type));
381
+
382
+ // Case 1: All commits - must be exact match
383
+ if (types.size === 1 && types.has('commit')) {
384
+ this.throwConflictError(pkg, refs, node.dependents,
385
+ 'Explicit commit pins cannot be automatically resolved.\n' +
386
+ 'Add an override in model.yaml:\n\n' +
387
+ ' overrides:\n' +
388
+ ` ${pkg}: ${refs[0]}`
389
+ );
390
+ }
391
+
392
+ // Case 2: Mixed types (tag vs branch or tag vs commit)
393
+ if (types.size > 1) {
394
+ this.throwConflictError(pkg, refs, node.dependents,
395
+ 'Cannot mix ref types (tags, branches, commits).\n' +
396
+ 'Add an override in model.yaml to specify which to use:\n\n' +
397
+ ' overrides:\n' +
398
+ ` ${pkg}: <ref>`
399
+ );
400
+ }
401
+
402
+ // Case 3: All branches - must be same branch
403
+ if (types.size === 1 && types.has('branch')) {
404
+ const uniqueBranches = new Set(refs);
405
+ if (uniqueBranches.size > 1) {
406
+ this.throwConflictError(pkg, refs, node.dependents,
407
+ 'Different branch refs cannot be automatically resolved.\n' +
408
+ 'Add an override in model.yaml:\n\n' +
409
+ ' overrides:\n' +
410
+ ` ${pkg}: ${refs[0]}`
411
+ );
412
+ }
413
+ // Same branch - no conflict, continue
414
+ continue;
415
+ }
416
+
417
+ // Case 4: All SemVer tags - apply "Latest Wins"
418
+ const semvers = refTypes
419
+ .filter((r): r is typeof r & { semver: SemVer } => r.semver !== undefined)
420
+ .map(r => r.semver);
421
+
422
+ if (semvers.length !== refs.length) {
423
+ // Some refs don't parse as SemVer - can't auto-resolve
424
+ this.throwConflictError(pkg, refs, node.dependents,
425
+ 'Not all refs are valid SemVer tags.\n' +
426
+ 'Add an override in model.yaml:\n\n' +
427
+ ' overrides:\n' +
428
+ ` ${pkg}: <ref>`
429
+ );
430
+ }
431
+
432
+ // Check major version compatibility
433
+ const majors = new Set(semvers.map(s => s.major));
434
+ if (majors.size > 1) {
435
+ const majorList = Array.from(majors).sort().join(' vs ');
436
+ this.throwConflictError(pkg, refs, node.dependents,
437
+ `Major version mismatch (v${majorList}). This may indicate breaking changes.\n` +
438
+ 'Add an override in model.yaml if you want to force a version:\n\n' +
439
+ ' overrides:\n' +
440
+ ` ${pkg}: ${refs[refs.length - 1]}`
441
+ );
442
+ }
443
+
444
+ // All same major version - pick latest (Latest Wins!)
445
+ const latest = pickLatestSemVer(refs);
446
+ if (latest && latest !== node.refConstraint) {
447
+ // Update the node to use the resolved ref
448
+ node.refConstraint = latest;
449
+
450
+ // Log the resolution for user feedback
451
+ const otherRefs = refs.filter(r => r !== latest).join(', ');
452
+ resolutionMessages.push(
453
+ `Resolved ${pkg}: using ${latest} (satisfies ${otherRefs})`
454
+ );
455
+ }
456
+ }
457
+
458
+ // Store resolution messages for later output
459
+ if (resolutionMessages.length > 0) {
460
+ this.resolutionMessages = resolutionMessages;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Throws a formatted conflict error with actionable hints.
466
+ */
467
+ private throwConflictError(
468
+ pkg: string,
469
+ refs: string[],
470
+ dependents: string[],
471
+ hint: string
472
+ ): never {
473
+ const depLines = dependents.map((d, i) =>
474
+ ` └─ ${d} requires ${pkg}@${refs[i] || refs[0]}`
475
+ ).join('\n');
476
+
477
+ throw new Error(
478
+ `Dependency ref conflict for '${pkg}'\n` +
479
+ depLines + '\n\n' +
480
+ hint
481
+ );
482
+ }
483
+
484
+ /** Resolution messages from "Latest Wins" auto-resolution */
485
+ private resolutionMessages: string[] = [];
486
+
487
+ /**
488
+ * Returns any resolution messages from the last dependency resolution.
489
+ * Useful for CLI output to inform users about auto-resolved conflicts.
490
+ */
491
+ getResolutionMessages(): string[] {
492
+ return this.resolutionMessages;
493
+ }
494
+
495
+ /**
496
+ * Detects package-level cycles in the dependency graph and throws a clear error.
497
+ */
498
+ private detectPackageCycles(graph: DependencyGraph): void {
499
+ const GRAY = 1, BLACK = 2;
500
+ const color: Record<string, number> = {};
501
+ const stack: string[] = [];
502
+
503
+ const visit = (pkg: string): void => {
504
+ color[pkg] = GRAY;
505
+ stack.push(pkg);
506
+ const deps = Object.keys(graph.nodes[pkg]?.dependencies ?? {});
507
+ for (const dep of deps) {
508
+ if (!graph.nodes[dep]) continue; // Unknown dep will resolve later
509
+ if (color[dep] === GRAY) {
510
+ // Found a back edge: cycle
511
+ const cycleStart = stack.indexOf(dep);
512
+ const cyclePath = [...stack.slice(cycleStart), dep].join(' → ');
513
+ throw new Error(
514
+ `Cyclic package dependency detected:\n` +
515
+ ` ${cyclePath}\n\n` +
516
+ `Hint: Extract shared types into a separate package that both can depend on.`
517
+ );
518
+ }
519
+ if (color[dep] !== BLACK) visit(dep);
520
+ }
521
+ stack.pop();
522
+ color[pkg] = BLACK;
523
+ };
524
+
525
+ for (const pkg of Object.keys(graph.nodes)) {
526
+ if (!color[pkg]) visit(pkg);
527
+ }
528
+ }
529
+
313
530
  /**
314
531
  * Loads an existing lock file from disk.
315
532
  */
@@ -1,14 +1,10 @@
1
1
  // Browser stub for GitUrlResolver
2
+ // Git operations are not available in the browser environment
2
3
 
3
- export interface GitImportInfo {
4
- original: string;
5
- platform: 'github' | 'gitlab' | 'bitbucket' | 'generic';
6
- owner: string;
7
- repo: string;
8
- version: string;
9
- repoUrl: string;
10
- entryPoint: string;
11
- }
4
+ import type { GitImportInfo } from './types.js';
5
+
6
+ // Re-export the type for API consistency
7
+ export type { GitImportInfo } from './types.js';
12
8
 
13
9
  export class GitUrlResolver {
14
10
  constructor() {
@@ -16,9 +12,11 @@ export class GitUrlResolver {
16
12
  }
17
13
  }
18
14
 
19
-
20
15
  export const GitUrlParser = {
21
- parse() {
16
+ parse(_importStr: string): GitImportInfo {
17
+ throw new Error('GitUrlParser is not available in the browser.');
18
+ },
19
+ isGitUrl(_importStr: string): boolean {
22
20
  throw new Error('GitUrlParser is not available in the browser.');
23
21
  }
24
22
  };
@@ -26,6 +24,3 @@ export const GitUrlParser = {
26
24
  export function loadLockFile(): void {
27
25
  throw new Error('loadLockFile is not available in the browser.');
28
26
  }
29
-
30
- export type LockFile = unknown;
31
- export type LockedDependency = unknown;