@domainlang/language 0.1.20

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 (212) hide show
  1. package/README.md +163 -0
  2. package/out/ast-augmentation.d.ts +6 -0
  3. package/out/ast-augmentation.js +2 -0
  4. package/out/ast-augmentation.js.map +1 -0
  5. package/out/domain-lang-module.d.ts +57 -0
  6. package/out/domain-lang-module.js +67 -0
  7. package/out/domain-lang-module.js.map +1 -0
  8. package/out/generated/ast.d.ts +759 -0
  9. package/out/generated/ast.js +556 -0
  10. package/out/generated/ast.js.map +1 -0
  11. package/out/generated/grammar.d.ts +6 -0
  12. package/out/generated/grammar.js +2407 -0
  13. package/out/generated/grammar.js.map +1 -0
  14. package/out/generated/module.d.ts +13 -0
  15. package/out/generated/module.js +21 -0
  16. package/out/generated/module.js.map +1 -0
  17. package/out/index.d.ts +16 -0
  18. package/out/index.js +22 -0
  19. package/out/index.js.map +1 -0
  20. package/out/lsp/domain-lang-code-actions.d.ts +55 -0
  21. package/out/lsp/domain-lang-code-actions.js +143 -0
  22. package/out/lsp/domain-lang-code-actions.js.map +1 -0
  23. package/out/lsp/domain-lang-completion.d.ts +37 -0
  24. package/out/lsp/domain-lang-completion.js +452 -0
  25. package/out/lsp/domain-lang-completion.js.map +1 -0
  26. package/out/lsp/domain-lang-formatter.d.ts +15 -0
  27. package/out/lsp/domain-lang-formatter.js +43 -0
  28. package/out/lsp/domain-lang-formatter.js.map +1 -0
  29. package/out/lsp/domain-lang-naming.d.ts +34 -0
  30. package/out/lsp/domain-lang-naming.js +49 -0
  31. package/out/lsp/domain-lang-naming.js.map +1 -0
  32. package/out/lsp/domain-lang-scope.d.ts +59 -0
  33. package/out/lsp/domain-lang-scope.js +102 -0
  34. package/out/lsp/domain-lang-scope.js.map +1 -0
  35. package/out/lsp/domain-lang-workspace-manager.d.ts +21 -0
  36. package/out/lsp/domain-lang-workspace-manager.js +93 -0
  37. package/out/lsp/domain-lang-workspace-manager.js.map +1 -0
  38. package/out/lsp/hover/ddd-pattern-explanations.d.ts +50 -0
  39. package/out/lsp/hover/ddd-pattern-explanations.js +196 -0
  40. package/out/lsp/hover/ddd-pattern-explanations.js.map +1 -0
  41. package/out/lsp/hover/domain-lang-hover.d.ts +19 -0
  42. package/out/lsp/hover/domain-lang-hover.js +302 -0
  43. package/out/lsp/hover/domain-lang-hover.js.map +1 -0
  44. package/out/lsp/hover/domain-lang-keywords.d.ts +13 -0
  45. package/out/lsp/hover/domain-lang-keywords.js +47 -0
  46. package/out/lsp/hover/domain-lang-keywords.js.map +1 -0
  47. package/out/lsp/manifest-diagnostics.d.ts +82 -0
  48. package/out/lsp/manifest-diagnostics.js +230 -0
  49. package/out/lsp/manifest-diagnostics.js.map +1 -0
  50. package/out/main-browser.d.ts +1 -0
  51. package/out/main-browser.js +11 -0
  52. package/out/main-browser.js.map +1 -0
  53. package/out/main.d.ts +1 -0
  54. package/out/main.js +74 -0
  55. package/out/main.js.map +1 -0
  56. package/out/sdk/ast-augmentation.d.ts +136 -0
  57. package/out/sdk/ast-augmentation.js +62 -0
  58. package/out/sdk/ast-augmentation.js.map +1 -0
  59. package/out/sdk/index.d.ts +94 -0
  60. package/out/sdk/index.js +97 -0
  61. package/out/sdk/index.js.map +1 -0
  62. package/out/sdk/indexes.d.ts +16 -0
  63. package/out/sdk/indexes.js +97 -0
  64. package/out/sdk/indexes.js.map +1 -0
  65. package/out/sdk/loader-node.d.ts +51 -0
  66. package/out/sdk/loader-node.js +119 -0
  67. package/out/sdk/loader-node.js.map +1 -0
  68. package/out/sdk/loader.d.ts +49 -0
  69. package/out/sdk/loader.js +85 -0
  70. package/out/sdk/loader.js.map +1 -0
  71. package/out/sdk/patterns.d.ts +93 -0
  72. package/out/sdk/patterns.js +123 -0
  73. package/out/sdk/patterns.js.map +1 -0
  74. package/out/sdk/query.d.ts +90 -0
  75. package/out/sdk/query.js +679 -0
  76. package/out/sdk/query.js.map +1 -0
  77. package/out/sdk/resolution.d.ts +52 -0
  78. package/out/sdk/resolution.js +68 -0
  79. package/out/sdk/resolution.js.map +1 -0
  80. package/out/sdk/types.d.ts +280 -0
  81. package/out/sdk/types.js +8 -0
  82. package/out/sdk/types.js.map +1 -0
  83. package/out/services/dependency-analyzer.d.ts +58 -0
  84. package/out/services/dependency-analyzer.js +254 -0
  85. package/out/services/dependency-analyzer.js.map +1 -0
  86. package/out/services/dependency-resolver.d.ts +146 -0
  87. package/out/services/dependency-resolver.js +452 -0
  88. package/out/services/dependency-resolver.js.map +1 -0
  89. package/out/services/git-url-resolver.browser.d.ts +10 -0
  90. package/out/services/git-url-resolver.browser.js +19 -0
  91. package/out/services/git-url-resolver.browser.js.map +1 -0
  92. package/out/services/git-url-resolver.d.ts +158 -0
  93. package/out/services/git-url-resolver.js +416 -0
  94. package/out/services/git-url-resolver.js.map +1 -0
  95. package/out/services/governance-validator.d.ts +44 -0
  96. package/out/services/governance-validator.js +153 -0
  97. package/out/services/governance-validator.js.map +1 -0
  98. package/out/services/import-resolver.d.ts +77 -0
  99. package/out/services/import-resolver.js +240 -0
  100. package/out/services/import-resolver.js.map +1 -0
  101. package/out/services/performance-optimizer.d.ts +60 -0
  102. package/out/services/performance-optimizer.js +140 -0
  103. package/out/services/performance-optimizer.js.map +1 -0
  104. package/out/services/relationship-inference.d.ts +11 -0
  105. package/out/services/relationship-inference.js +98 -0
  106. package/out/services/relationship-inference.js.map +1 -0
  107. package/out/services/semver.d.ts +98 -0
  108. package/out/services/semver.js +195 -0
  109. package/out/services/semver.js.map +1 -0
  110. package/out/services/types.d.ts +340 -0
  111. package/out/services/types.js +46 -0
  112. package/out/services/types.js.map +1 -0
  113. package/out/services/workspace-manager.d.ts +123 -0
  114. package/out/services/workspace-manager.js +489 -0
  115. package/out/services/workspace-manager.js.map +1 -0
  116. package/out/syntaxes/domain-lang.monarch.d.ts +76 -0
  117. package/out/syntaxes/domain-lang.monarch.js +29 -0
  118. package/out/syntaxes/domain-lang.monarch.js.map +1 -0
  119. package/out/utils/import-utils.d.ts +49 -0
  120. package/out/utils/import-utils.js +128 -0
  121. package/out/utils/import-utils.js.map +1 -0
  122. package/out/validation/bounded-context.d.ts +11 -0
  123. package/out/validation/bounded-context.js +79 -0
  124. package/out/validation/bounded-context.js.map +1 -0
  125. package/out/validation/classification.d.ts +3 -0
  126. package/out/validation/classification.js +3 -0
  127. package/out/validation/classification.js.map +1 -0
  128. package/out/validation/constants.d.ts +180 -0
  129. package/out/validation/constants.js +235 -0
  130. package/out/validation/constants.js.map +1 -0
  131. package/out/validation/domain-lang-validator.d.ts +2 -0
  132. package/out/validation/domain-lang-validator.js +27 -0
  133. package/out/validation/domain-lang-validator.js.map +1 -0
  134. package/out/validation/domain.d.ts +11 -0
  135. package/out/validation/domain.js +63 -0
  136. package/out/validation/domain.js.map +1 -0
  137. package/out/validation/import.d.ts +68 -0
  138. package/out/validation/import.js +237 -0
  139. package/out/validation/import.js.map +1 -0
  140. package/out/validation/manifest.d.ts +144 -0
  141. package/out/validation/manifest.js +327 -0
  142. package/out/validation/manifest.js.map +1 -0
  143. package/out/validation/maps.d.ts +21 -0
  144. package/out/validation/maps.js +60 -0
  145. package/out/validation/maps.js.map +1 -0
  146. package/out/validation/metadata.d.ts +7 -0
  147. package/out/validation/metadata.js +16 -0
  148. package/out/validation/metadata.js.map +1 -0
  149. package/out/validation/model.d.ts +12 -0
  150. package/out/validation/model.js +29 -0
  151. package/out/validation/model.js.map +1 -0
  152. package/out/validation/relationships.d.ts +12 -0
  153. package/out/validation/relationships.js +94 -0
  154. package/out/validation/relationships.js.map +1 -0
  155. package/out/validation/shared.d.ts +6 -0
  156. package/out/validation/shared.js +12 -0
  157. package/out/validation/shared.js.map +1 -0
  158. package/package.json +110 -0
  159. package/src/ast-augmentation.ts +5 -0
  160. package/src/domain-lang-module.ts +112 -0
  161. package/src/domain-lang.langium +351 -0
  162. package/src/generated/ast.ts +986 -0
  163. package/src/generated/grammar.ts +2409 -0
  164. package/src/generated/module.ts +25 -0
  165. package/src/index.ts +24 -0
  166. package/src/lsp/domain-lang-code-actions.ts +189 -0
  167. package/src/lsp/domain-lang-completion.ts +514 -0
  168. package/src/lsp/domain-lang-formatter.ts +51 -0
  169. package/src/lsp/domain-lang-naming.ts +56 -0
  170. package/src/lsp/domain-lang-scope.ts +137 -0
  171. package/src/lsp/domain-lang-workspace-manager.ts +104 -0
  172. package/src/lsp/hover/ddd-pattern-explanations.ts +237 -0
  173. package/src/lsp/hover/domain-lang-hover.ts +338 -0
  174. package/src/lsp/hover/domain-lang-keywords.ts +50 -0
  175. package/src/lsp/manifest-diagnostics.ts +290 -0
  176. package/src/main-browser.ts +15 -0
  177. package/src/main.ts +85 -0
  178. package/src/sdk/README.md +297 -0
  179. package/src/sdk/ast-augmentation.ts +157 -0
  180. package/src/sdk/index.ts +126 -0
  181. package/src/sdk/indexes.ts +155 -0
  182. package/src/sdk/loader-node.ts +146 -0
  183. package/src/sdk/loader.ts +99 -0
  184. package/src/sdk/patterns.ts +147 -0
  185. package/src/sdk/query.ts +802 -0
  186. package/src/sdk/resolution.ts +78 -0
  187. package/src/sdk/types.ts +323 -0
  188. package/src/services/dependency-analyzer.ts +321 -0
  189. package/src/services/dependency-resolver.ts +551 -0
  190. package/src/services/git-url-resolver.browser.ts +26 -0
  191. package/src/services/git-url-resolver.ts +517 -0
  192. package/src/services/governance-validator.ts +177 -0
  193. package/src/services/import-resolver.ts +292 -0
  194. package/src/services/performance-optimizer.ts +170 -0
  195. package/src/services/relationship-inference.ts +121 -0
  196. package/src/services/semver.ts +213 -0
  197. package/src/services/types.ts +415 -0
  198. package/src/services/workspace-manager.ts +607 -0
  199. package/src/syntaxes/domain-lang.monarch.ts +29 -0
  200. package/src/utils/import-utils.ts +152 -0
  201. package/src/validation/bounded-context.ts +99 -0
  202. package/src/validation/classification.ts +5 -0
  203. package/src/validation/constants.ts +304 -0
  204. package/src/validation/domain-lang-validator.ts +33 -0
  205. package/src/validation/domain.ts +77 -0
  206. package/src/validation/import.ts +295 -0
  207. package/src/validation/manifest.ts +439 -0
  208. package/src/validation/maps.ts +76 -0
  209. package/src/validation/metadata.ts +18 -0
  210. package/src/validation/model.ts +37 -0
  211. package/src/validation/relationships.ts +154 -0
  212. package/src/validation/shared.ts +14 -0
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Dependency Resolution Service
3
+ *
4
+ * Discovers and resolves transitive dependencies for DomainLang packages.
5
+ * Generates lock files for reproducible builds.
6
+ *
7
+ * Algorithm:
8
+ * 1. Parse root model.yaml
9
+ * 2. Download all direct dependencies
10
+ * 3. Parse each dependency's model.yaml
11
+ * 4. Recursively discover transitive dependencies
12
+ * 5. Resolve version constraints using "Latest Wins" strategy
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)
21
+ */
22
+
23
+ import path from 'node:path';
24
+ import fs from 'node:fs/promises';
25
+ import YAML from 'yaml';
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';
29
+
30
+ export class DependencyResolver {
31
+ private gitResolver: GitUrlResolver;
32
+ private workspaceRoot: string;
33
+
34
+ constructor(workspaceRoot: string, gitResolver?: GitUrlResolver) {
35
+ this.workspaceRoot = workspaceRoot;
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);
39
+ }
40
+
41
+ /**
42
+ * Resolves all dependencies for a workspace.
43
+ *
44
+ * Process:
45
+ * 1. Load root model.yaml
46
+ * 2. Build dependency graph (discover transitive deps)
47
+ * 3. Resolve version constraints
48
+ * 4. Generate lock file
49
+ * 5. Download all dependencies to cache
50
+ *
51
+ * @returns Generated lock file
52
+ */
53
+ async resolveDependencies(): Promise<LockFile> {
54
+ // Load root package config
55
+ const rootConfig = await this.loadPackageConfig(this.workspaceRoot);
56
+
57
+ if (!rootConfig.dependencies || Object.keys(rootConfig.dependencies).length === 0) {
58
+ // No dependencies
59
+ return { version: '1', dependencies: {} };
60
+ }
61
+
62
+ // Build dependency graph
63
+ const graph = await this.buildDependencyGraph(rootConfig);
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
+
72
+ // Resolve version constraints
73
+ await this.resolveVersions(graph);
74
+
75
+ // Generate and return lock file (caller writes to disk)
76
+ return this.generateLockFile(graph);
77
+ }
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
+
116
+ /**
117
+ * Builds the complete dependency graph by recursively discovering transitive dependencies.
118
+ */
119
+ private async buildDependencyGraph(rootConfig: ResolvingPackage): Promise<DependencyGraph> {
120
+ const graph: DependencyGraph = {
121
+ nodes: {},
122
+ root: rootConfig.name || 'root',
123
+ };
124
+
125
+ // Process root dependencies
126
+ const queue: Array<{ packageKey: string; refConstraint: string; parent: string }> = [];
127
+
128
+ for (const [depName, refConstraint] of Object.entries(rootConfig.dependencies || {})) {
129
+ queue.push({
130
+ packageKey: depName,
131
+ refConstraint,
132
+ parent: graph.root
133
+ });
134
+ }
135
+
136
+ // BFS to discover all transitive dependencies
137
+ const visited = new Set<string>();
138
+
139
+ while (queue.length > 0) {
140
+ const entry = queue.shift();
141
+ if (!entry) break;
142
+ const { packageKey, refConstraint, parent } = entry;
143
+
144
+ // Skip if already processed
145
+ if (visited.has(packageKey)) {
146
+ // Update dependents list and record constraint
147
+ const existing = graph.nodes[packageKey];
148
+ if (!existing.dependents.includes(parent)) {
149
+ existing.dependents.push(parent);
150
+ }
151
+ if (!existing.constraints) existing.constraints = new Set<string>();
152
+ existing.constraints.add(refConstraint);
153
+ continue;
154
+ }
155
+ visited.add(packageKey);
156
+
157
+ // Parse package identifier
158
+ const gitInfo = GitUrlParser.parse(packageKey);
159
+
160
+ // Download package to get its model.yaml
161
+ const packageUri = await this.gitResolver.resolve(packageKey);
162
+ const packageDir = path.dirname(packageUri.fsPath);
163
+
164
+ // Load package config
165
+ const packageConfig = await this.loadPackageConfig(packageDir);
166
+
167
+ // Add to graph
168
+ graph.nodes[packageKey] = {
169
+ packageKey,
170
+ refConstraint,
171
+ constraints: new Set<string>([refConstraint]),
172
+ repoUrl: gitInfo.repoUrl,
173
+ dependencies: packageConfig.dependencies || {},
174
+ dependents: [parent],
175
+ };
176
+
177
+ // Queue transitive dependencies
178
+ for (const [transDepName, transRefConstraint] of Object.entries(packageConfig.dependencies || {})) {
179
+ queue.push({
180
+ packageKey: transDepName,
181
+ refConstraint: transRefConstraint,
182
+ parent: packageKey,
183
+ });
184
+ }
185
+ }
186
+
187
+ return graph;
188
+ }
189
+
190
+ /**
191
+ * Resolves ref constraints to specific commits.
192
+ *
193
+ * Simple algorithm: Use the ref specified in the constraint.
194
+ * Detects refType (tag, branch, or commit) based on format.
195
+ */
196
+ private async resolveVersions(graph: DependencyGraph): Promise<void> {
197
+ for (const [packageKey, node] of Object.entries(graph.nodes)) {
198
+ // Parse package to get repo info
199
+ const gitInfo = GitUrlParser.parse(packageKey);
200
+
201
+ // Extract ref from constraint
202
+ const ref = this.extractRefFromConstraint(node.refConstraint);
203
+
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);
209
+
210
+ node.resolvedRef = ref;
211
+ node.refType = refType;
212
+ node.commitHash = commitHash;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Extracts a ref from a constraint string.
218
+ *
219
+ * Examples:
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)
225
+ * - "owner/repo@1.0.0" → "1.0.0"
226
+ */
227
+ private extractRefFromConstraint(constraint: string): string {
228
+ // Remove semver operators (legacy support)
229
+ let ref = constraint.replace(/^[\^~>=<]/, '');
230
+
231
+ // Extract ref from full import URL if present
232
+ if (ref.includes('@')) {
233
+ ref = ref.split('@')[1];
234
+ }
235
+
236
+ return ref || 'main';
237
+ }
238
+
239
+ /**
240
+ * Resolves a version (tag/branch) to a commit hash using git ls-remote.
241
+ */
242
+ private async resolveCommitHash(repoUrl: string, version: string): Promise<string> {
243
+ // This is a placeholder - the actual implementation is in GitUrlResolver
244
+ // We need to extract it or call the resolver
245
+ const gitInfo = GitUrlParser.parse(`${repoUrl}@${version}`);
246
+ const uri = await this.gitResolver.resolve(gitInfo.original);
247
+
248
+ // Extract commit hash from cache path
249
+ // Per PRS-010: Project-local cache at .dlang/packages/{owner}/{repo}/{commit}/
250
+ const pathParts = uri.fsPath.split(path.sep);
251
+ const commitHashIndex = pathParts.length - 2; // Second to last segment
252
+ return pathParts[commitHashIndex];
253
+ }
254
+
255
+ /**
256
+ * Generates a lock file from the resolved dependency graph.
257
+ */
258
+ private generateLockFile(graph: DependencyGraph): LockFile {
259
+ const dependencies: Record<string, LockedDependency> = {};
260
+
261
+ for (const [packageKey, node] of Object.entries(graph.nodes)) {
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
+ );
267
+ }
268
+
269
+ dependencies[packageKey] = {
270
+ ref: node.resolvedRef,
271
+ refType: node.refType ?? 'commit',
272
+ resolved: node.repoUrl || '',
273
+ commit: node.commitHash,
274
+ // Future: Calculate integrity hash
275
+ };
276
+ }
277
+
278
+ return {
279
+ version: '1',
280
+ dependencies,
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Loads and parses a package's model.yaml file.
286
+ */
287
+ private async loadPackageConfig(packageDir: string): Promise<ResolvingPackage> {
288
+ const yamlPath = path.join(packageDir, 'model.yaml');
289
+
290
+ try {
291
+ const yamlContent = await fs.readFile(yamlPath, 'utf-8');
292
+ return this.parseYaml(yamlContent);
293
+ } catch {
294
+ // No model.yaml found
295
+ return {};
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Parses model.yaml content.
301
+ *
302
+ * Expected structure:
303
+ * model:
304
+ * name: package-name
305
+ * version: 1.0.0
306
+ * entry: index.dlang
307
+ *
308
+ * dependencies:
309
+ * package-name:
310
+ * source: owner/repo
311
+ * ref: v1.0.0
312
+ */
313
+ private parseYaml(content: string): ResolvingPackage {
314
+ const parsed = YAML.parse(content) as {
315
+ model?: {
316
+ name?: string;
317
+ version?: string;
318
+ entry?: string;
319
+ };
320
+ dependencies?: Record<string, { source?: string; ref?: string }>;
321
+ overrides?: Record<string, string>;
322
+ };
323
+
324
+ const config: ResolvingPackage = {};
325
+
326
+ if (parsed.model) {
327
+ config.name = parsed.model.name;
328
+ config.version = parsed.model.version;
329
+ config.entry = parsed.model.entry;
330
+ }
331
+
332
+ if (parsed.dependencies) {
333
+ config.dependencies = {};
334
+ for (const [, depInfo] of Object.entries(parsed.dependencies)) {
335
+ if (depInfo.source) {
336
+ const refConstraint = depInfo.ref || 'main';
337
+ // Store as "source@ref" for consistency with import resolution
338
+ config.dependencies[depInfo.source] = refConstraint;
339
+ }
340
+ }
341
+ }
342
+
343
+ // Parse overrides section for explicit ref control
344
+ if (parsed.overrides) {
345
+ config.overrides = parsed.overrides;
346
+ }
347
+
348
+ return config;
349
+ }
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
+
530
+ /**
531
+ * Loads an existing lock file from disk.
532
+ */
533
+ static async loadLockFile(workspaceRoot: string): Promise<LockFile | undefined> {
534
+ const lockPath = path.join(workspaceRoot, 'model.lock');
535
+
536
+ try {
537
+ const content = await fs.readFile(lockPath, 'utf-8');
538
+ return JSON.parse(content) as LockFile;
539
+ } catch {
540
+ // No lock file
541
+ return undefined;
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Parses a lock file from JSON format.
547
+ */
548
+ static parseLockFile(content: string): LockFile {
549
+ return JSON.parse(content) as LockFile;
550
+ }
551
+ }
@@ -0,0 +1,26 @@
1
+ // Browser stub for GitUrlResolver
2
+ // Git operations are not available in the browser environment
3
+
4
+ import type { GitImportInfo } from './types.js';
5
+
6
+ // Re-export the type for API consistency
7
+ export type { GitImportInfo } from './types.js';
8
+
9
+ export class GitUrlResolver {
10
+ constructor() {
11
+ throw new Error('GitUrlResolver is not available in the browser.');
12
+ }
13
+ }
14
+
15
+ export const GitUrlParser = {
16
+ parse(_importStr: string): GitImportInfo {
17
+ throw new Error('GitUrlParser is not available in the browser.');
18
+ },
19
+ isGitUrl(_importStr: string): boolean {
20
+ throw new Error('GitUrlParser is not available in the browser.');
21
+ }
22
+ };
23
+
24
+ export function loadLockFile(): void {
25
+ throw new Error('loadLockFile is not available in the browser.');
26
+ }