@domainlang/language 0.1.20 → 0.1.82

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 (110) hide show
  1. package/out/domain-lang-module.d.ts +0 -2
  2. package/out/domain-lang-module.js +3 -11
  3. package/out/domain-lang-module.js.map +1 -1
  4. package/out/generated/ast.d.ts +19 -8
  5. package/out/generated/ast.js +10 -1
  6. package/out/generated/ast.js.map +1 -1
  7. package/out/generated/grammar.d.ts +1 -1
  8. package/out/generated/grammar.js +123 -28
  9. package/out/generated/grammar.js.map +1 -1
  10. package/out/generated/module.d.ts +1 -1
  11. package/out/generated/module.js +1 -1
  12. package/out/index.d.ts +0 -3
  13. package/out/index.js +0 -5
  14. package/out/index.js.map +1 -1
  15. package/out/lsp/hover/domain-lang-hover.js +4 -0
  16. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  17. package/out/sdk/index.d.ts +1 -1
  18. package/out/sdk/loader-node.d.ts +3 -7
  19. package/out/sdk/loader-node.js +9 -24
  20. package/out/sdk/loader-node.js.map +1 -1
  21. package/out/sdk/types.d.ts +21 -0
  22. package/out/services/dependency-analyzer.d.ts +39 -3
  23. package/out/services/dependency-analyzer.js +47 -22
  24. package/out/services/dependency-analyzer.js.map +1 -1
  25. package/out/services/dependency-resolver.d.ts +45 -68
  26. package/out/services/dependency-resolver.js +43 -243
  27. package/out/services/dependency-resolver.js.map +1 -1
  28. package/out/services/git-url-resolver.browser.d.ts +12 -4
  29. package/out/services/git-url-resolver.browser.js +1 -5
  30. package/out/services/git-url-resolver.browser.js.map +1 -1
  31. package/out/services/git-url-resolver.d.ts +56 -22
  32. package/out/services/git-url-resolver.js +36 -70
  33. package/out/services/git-url-resolver.js.map +1 -1
  34. package/out/services/governance-validator.d.ts +37 -1
  35. package/out/services/governance-validator.js +10 -4
  36. package/out/services/governance-validator.js.map +1 -1
  37. package/out/services/import-resolver.d.ts +6 -65
  38. package/out/services/import-resolver.js +5 -223
  39. package/out/services/import-resolver.js.map +1 -1
  40. package/out/services/performance-optimizer.d.ts +1 -1
  41. package/out/services/workspace-manager.d.ts +10 -57
  42. package/out/services/workspace-manager.js +21 -187
  43. package/out/services/workspace-manager.js.map +1 -1
  44. package/out/syntaxes/domain-lang.monarch.js +1 -1
  45. package/out/syntaxes/domain-lang.monarch.js.map +1 -1
  46. package/out/utils/import-utils.d.ts +12 -4
  47. package/out/utils/import-utils.js +135 -35
  48. package/out/utils/import-utils.js.map +1 -1
  49. package/out/validation/constants.d.ts +0 -103
  50. package/out/validation/constants.js +1 -140
  51. package/out/validation/constants.js.map +1 -1
  52. package/out/validation/domain.js +1 -46
  53. package/out/validation/domain.js.map +1 -1
  54. package/out/validation/import.d.ts +22 -46
  55. package/out/validation/import.js +85 -187
  56. package/out/validation/import.js.map +1 -1
  57. package/out/validation/maps.js +6 -10
  58. package/out/validation/maps.js.map +1 -1
  59. package/out/validation/metadata.js +1 -5
  60. package/out/validation/metadata.js.map +1 -1
  61. package/package.json +6 -8
  62. package/src/domain-lang-module.ts +6 -18
  63. package/src/domain-lang.langium +12 -7
  64. package/src/generated/ast.ts +20 -7
  65. package/src/generated/grammar.ts +123 -28
  66. package/src/generated/module.ts +1 -1
  67. package/src/index.ts +0 -7
  68. package/src/lsp/hover/domain-lang-hover.ts +2 -0
  69. package/src/sdk/index.ts +2 -0
  70. package/src/sdk/loader-node.ts +9 -29
  71. package/src/sdk/types.ts +23 -0
  72. package/src/services/dependency-analyzer.ts +84 -24
  73. package/src/services/dependency-resolver.ts +84 -301
  74. package/src/services/git-url-resolver.browser.ts +14 -9
  75. package/src/services/git-url-resolver.ts +93 -86
  76. package/src/services/governance-validator.ts +47 -5
  77. package/src/services/import-resolver.ts +8 -270
  78. package/src/services/performance-optimizer.ts +1 -1
  79. package/src/services/workspace-manager.ts +46 -237
  80. package/src/syntaxes/domain-lang.monarch.ts +1 -1
  81. package/src/utils/import-utils.ts +160 -38
  82. package/src/validation/constants.ts +1 -181
  83. package/src/validation/domain.ts +1 -54
  84. package/src/validation/import.ts +104 -228
  85. package/src/validation/maps.ts +6 -10
  86. package/src/validation/metadata.ts +1 -5
  87. package/out/lsp/domain-lang-code-actions.d.ts +0 -55
  88. package/out/lsp/domain-lang-code-actions.js +0 -143
  89. package/out/lsp/domain-lang-code-actions.js.map +0 -1
  90. package/out/lsp/domain-lang-workspace-manager.d.ts +0 -21
  91. package/out/lsp/domain-lang-workspace-manager.js +0 -93
  92. package/out/lsp/domain-lang-workspace-manager.js.map +0 -1
  93. package/out/lsp/manifest-diagnostics.d.ts +0 -82
  94. package/out/lsp/manifest-diagnostics.js +0 -230
  95. package/out/lsp/manifest-diagnostics.js.map +0 -1
  96. package/out/services/semver.d.ts +0 -98
  97. package/out/services/semver.js +0 -195
  98. package/out/services/semver.js.map +0 -1
  99. package/out/services/types.d.ts +0 -340
  100. package/out/services/types.js +0 -46
  101. package/out/services/types.js.map +0 -1
  102. package/out/validation/manifest.d.ts +0 -144
  103. package/out/validation/manifest.js +0 -327
  104. package/out/validation/manifest.js.map +0 -1
  105. package/src/lsp/domain-lang-code-actions.ts +0 -189
  106. package/src/lsp/domain-lang-workspace-manager.ts +0 -104
  107. package/src/lsp/manifest-diagnostics.ts +0 -290
  108. package/src/services/semver.ts +0 -213
  109. package/src/services/types.ts +0 -415
  110. package/src/validation/manifest.ts +0 -439
@@ -1,274 +1,24 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { URI, type LangiumDocument } from 'langium';
4
1
  import { WorkspaceManager } from './workspace-manager.js';
2
+ import { URI } from 'langium';
5
3
  import type { DomainLangServices } from '../domain-lang-module.js';
6
- import type { LockFile } from './types.js';
4
+ import type { LockFile } from './git-url-resolver.js';
7
5
 
8
6
  /**
9
- * ImportResolver resolves import statements using manifest-centric rules (PRS-010).
10
- *
11
- * Import Types (PRS-010):
12
- * - Local relative: ./path, ../path → Directory-first resolution
13
- * - Path aliases: @/path, @alias/path → Configurable in model.yaml paths section
14
- * - External: owner/package → Manifest dependencies
15
- *
16
- * Directory-First Resolution:
17
- * - ./types → ./types/index.dlang → ./types.dlang
18
- * - Module entry defaults to index.dlang (no model.yaml required)
7
+ * ImportResolver resolves import statements using WorkspaceManager and GitUrlResolver.
19
8
  */
20
9
  export class ImportResolver {
21
- private readonly workspaceManager: WorkspaceManager;
10
+ private workspaceManager: WorkspaceManager;
22
11
 
23
12
  constructor(services: DomainLangServices) {
24
13
  this.workspaceManager = services.imports.WorkspaceManager;
25
14
  }
26
15
 
27
16
  /**
28
- * Resolve an import specifier relative to a Langium document.
17
+ * Resolve an import URL to a file URI using the workspace's GitUrlResolver.
29
18
  */
30
- async resolveForDocument(document: LangiumDocument, specifier: string): Promise<URI> {
31
- const baseDir = path.dirname(document.uri.fsPath);
32
- return this.resolveFrom(baseDir, specifier);
33
- }
34
-
35
- /**
36
- * Resolve an import specifier from a base directory (non-LSP contexts).
37
- */
38
- async resolveFrom(baseDir: string, specifier: string): Promise<URI> {
39
- await this.workspaceManager.initialize(baseDir);
40
-
41
- // Local relative paths (./path or ../path) - directory-first resolution
42
- if (specifier.startsWith('./') || specifier.startsWith('../')) {
43
- const resolved = path.resolve(baseDir, specifier);
44
- return this.resolveLocalPath(resolved, specifier);
45
- }
46
-
47
- // Path aliases (@/path or @alias/path)
48
- if (specifier.startsWith('@')) {
49
- return this.resolvePathAlias(specifier);
50
- }
51
-
52
- // External dependency via manifest (owner/package format)
53
- return this.resolveExternalDependency(specifier);
54
- }
55
-
56
- /**
57
- * Resolves a path alias import.
58
- *
59
- * @param specifier - Import specifier starting with @ (e.g., "@/lib", "@shared/types")
60
- */
61
- private async resolvePathAlias(specifier: string): Promise<URI> {
62
- const aliases = await this.workspaceManager.getPathAliases();
63
- const root = this.workspaceManager.getWorkspaceRoot();
64
-
65
- // Find matching alias
66
- const aliasMatch = this.findMatchingAlias(specifier, aliases);
67
-
68
- if (aliasMatch) {
69
- const { alias: _alias, targetPath, remainder } = aliasMatch;
70
- const manifestPath = await this.workspaceManager.getManifestPath();
71
- const manifestDir = manifestPath ? path.dirname(manifestPath) : root;
72
- const resolvedBase = path.resolve(manifestDir, targetPath);
73
- const resolved = remainder ? path.join(resolvedBase, remainder) : resolvedBase;
74
- return this.resolveLocalPath(resolved, specifier);
75
- }
76
-
77
- // Default: @/ maps to workspace root (implicit)
78
- if (specifier.startsWith('@/')) {
79
- const relativePath = specifier.slice(2);
80
- const resolved = path.join(root, relativePath);
81
- return this.resolveLocalPath(resolved, specifier);
82
- }
83
-
84
- throw new Error(
85
- `Unknown path alias '${specifier.split('/')[0]}' in import '${specifier}'.\n` +
86
- `Hint: Define it in model.yaml paths section:\n` +
87
- ` paths:\n` +
88
- ` "${specifier.split('/')[0]}": "./some/path"`
89
- );
90
- }
91
-
92
- /**
93
- * Finds the longest matching alias for a specifier.
94
- */
95
- private findMatchingAlias(
96
- specifier: string,
97
- aliases: Record<string, string> | undefined
98
- ): { alias: string; targetPath: string; remainder: string } | undefined {
99
- if (!aliases) {
100
- return undefined;
101
- }
102
-
103
- // Sort by length descending to match most specific alias first
104
- const sortedAliases = Object.entries(aliases)
105
- .sort(([a], [b]) => b.length - a.length);
106
-
107
- for (const [alias, targetPath] of sortedAliases) {
108
- // Exact match
109
- if (specifier === alias) {
110
- return { alias, targetPath, remainder: '' };
111
- }
112
- // Prefix match (alias + /)
113
- if (specifier.startsWith(`${alias}/`)) {
114
- return { alias, targetPath, remainder: specifier.slice(alias.length + 1) };
115
- }
116
- }
117
-
118
- return undefined;
119
- }
120
-
121
- /**
122
- * Resolves an external dependency via manifest.
123
- *
124
- * NEW FORMAT (PRS-010): Import specifier is owner/package format.
125
- */
126
- private async resolveExternalDependency(specifier: string): Promise<URI> {
127
- const manifest = await this.workspaceManager.getManifest();
128
- if (!manifest) {
129
- throw new Error(
130
- `External dependency '${specifier}' requires model.yaml.\n` +
131
- `Hint: Create model.yaml and add the dependency:\n` +
132
- ` dependencies:\n` +
133
- ` ${specifier}:\n` +
134
- ` ref: v1.0.0`
135
- );
136
- }
137
-
138
- const lock = await this.workspaceManager.getLockFile();
139
- if (!lock) {
140
- throw new Error(
141
- `Dependency '${specifier}' not installed.\n` +
142
- `Hint: Run 'dlang install' to fetch dependencies and generate model.lock.`
143
- );
144
- }
145
-
146
- const mapped = await this.workspaceManager.resolveDependencyImport(specifier);
147
- if (!mapped) {
148
- throw new Error(
149
- `Dependency '${specifier}' not found in model.yaml.\n` +
150
- `Hint: Add it to your dependencies:\n` +
151
- ` dependencies:\n` +
152
- ` ${specifier}:\n` +
153
- ` ref: v1.0.0`
154
- );
155
- }
156
-
157
- const git = await this.workspaceManager.getGitResolver();
158
- return git.resolve(mapped, { allowNetwork: false });
159
- }
160
-
161
- /**
162
- * Resolves a local path using directory-first resolution.
163
- *
164
- * Per PRS-010 (updated design):
165
- * - If path ends with .dlang → direct file import
166
- * - If no extension → directory-first:
167
- * 1. Try ./path/index.dlang (module default, no model.yaml required)
168
- * 2. Try ./path.dlang (file fallback)
169
- */
170
- private async resolveLocalPath(resolved: string, original: string): Promise<URI> {
171
- const ext = path.extname(resolved);
172
-
173
- if (ext === '.dlang') {
174
- // Direct file import
175
- await assertFileExists(resolved, original);
176
- return URI.file(resolved);
177
- }
178
-
179
- if (ext && ext !== '.dlang') {
180
- throw new Error(
181
- `Invalid file extension '${ext}' in import '${original}'.\n` +
182
- `Hint: DomainLang files must use the .dlang extension.`
183
- );
184
- }
185
-
186
- // No extension → directory-first resolution
187
- return this.resolveDirectoryFirst(resolved, original);
188
- }
189
-
190
- /**
191
- * Directory-first resolution: ./types → ./types/index.dlang → ./types.dlang
192
- *
193
- * Module entry defaults to index.dlang without requiring model.yaml.
194
- * If the directory has model.yaml with custom entry, use that.
195
- */
196
- private async resolveDirectoryFirst(resolved: string, original: string): Promise<URI> {
197
- // Step 1: Check if directory exists with index.dlang (or custom entry)
198
- const isDirectory = await this.isDirectory(resolved);
199
- if (isDirectory) {
200
- // Check for model.yaml to get custom entry point
201
- const moduleManifestPath = path.join(resolved, 'model.yaml');
202
- const entryPoint = await this.readModuleEntry(moduleManifestPath);
203
- const entryFile = path.join(resolved, entryPoint);
204
-
205
- if (await this.fileExists(entryFile)) {
206
- return URI.file(entryFile);
207
- }
208
-
209
- // Directory exists but no entry file
210
- throw new Error(
211
- `Module '${original}' is missing its entry file.\n` +
212
- `Expected: ${resolved}/${entryPoint}\n` +
213
- `Hint: Create '${entryPoint}' in the module directory, or specify a custom entry in model.yaml:\n` +
214
- ` model:\n` +
215
- ` entry: main.dlang`
216
- );
217
- }
218
-
219
- // Step 2: Try .dlang file fallback
220
- const fileWithExt = `${resolved}.dlang`;
221
- if (await this.fileExists(fileWithExt)) {
222
- return URI.file(fileWithExt);
223
- }
224
-
225
- // Neither directory nor file found
226
- throw new Error(
227
- `Cannot resolve import '${original}'.\n` +
228
- `Tried:\n` +
229
- ` • ${resolved}/index.dlang (directory module)\n` +
230
- ` • ${resolved}.dlang (file)\n` +
231
- `Hint: Check that the path is correct and the file exists.`
232
- );
233
- }
234
-
235
- /**
236
- * Reads the entry point from a module's model.yaml.
237
- * Defaults to index.dlang if no manifest or no entry specified.
238
- */
239
- private async readModuleEntry(manifestPath: string): Promise<string> {
240
- try {
241
- const content = await fs.readFile(manifestPath, 'utf-8');
242
- const YAML = await import('yaml');
243
- const manifest = YAML.parse(content) as { model?: { entry?: string } };
244
- return manifest?.model?.entry ?? 'index.dlang';
245
- } catch {
246
- return 'index.dlang';
247
- }
248
- }
249
-
250
- /**
251
- * Checks if a path is a directory.
252
- */
253
- private async isDirectory(targetPath: string): Promise<boolean> {
254
- try {
255
- const stat = await fs.stat(targetPath);
256
- return stat.isDirectory();
257
- } catch {
258
- return false;
259
- }
260
- }
261
-
262
- /**
263
- * Checks if a file exists.
264
- */
265
- private async fileExists(filePath: string): Promise<boolean> {
266
- try {
267
- await fs.access(filePath);
268
- return true;
269
- } catch {
270
- return false;
271
- }
19
+ async resolveImport(importUrl: string): Promise<URI> {
20
+ const gitResolver = await this.workspaceManager.getGitResolver();
21
+ return gitResolver.resolve(importUrl);
272
22
  }
273
23
 
274
24
  /**
@@ -278,15 +28,3 @@ export class ImportResolver {
278
28
  return this.workspaceManager.getLockFile();
279
29
  }
280
30
  }
281
-
282
- async function assertFileExists(filePath: string, original: string): Promise<void> {
283
- try {
284
- await fs.access(filePath);
285
- } catch {
286
- throw new Error(
287
- `Import file not found: '${original}'.\\n` +
288
- `Resolved path: ${filePath}\\n` +
289
- `Hint: Check that the file exists and the path is correct.`
290
- );
291
- }
292
- }
@@ -8,7 +8,7 @@
8
8
  * - Stale cache detection
9
9
  */
10
10
 
11
- import type { LockFile } from './types.js';
11
+ import type { LockFile } from './git-url-resolver.js';
12
12
  import path from 'node:path';
13
13
  import fs from 'node:fs/promises';
14
14
 
@@ -4,15 +4,7 @@ import YAML from 'yaml';
4
4
  import { DependencyResolver } from './dependency-resolver.js';
5
5
  import { GitUrlResolver } from './git-url-resolver.js';
6
6
  import { getGlobalOptimizer } from './performance-optimizer.js';
7
- import type {
8
- LockFile,
9
- LockedDependency,
10
- ModelManifest,
11
- DependencySpec,
12
- ExtendedDependencySpec,
13
- PathAliases,
14
- WorkspaceManagerOptions
15
- } from './types.js';
7
+ import type { LockFile, LockedDependency } from './git-url-resolver.js';
16
8
 
17
9
  const DEFAULT_MANIFEST_FILES = [
18
10
  'model.yaml'
@@ -24,6 +16,27 @@ const DEFAULT_LOCK_FILES = [
24
16
 
25
17
  const JSON_SPACE = 2;
26
18
 
19
+ export interface WorkspaceManagerOptions {
20
+ readonly autoResolve?: boolean;
21
+ readonly manifestFiles?: readonly string[];
22
+ readonly lockFiles?: readonly string[];
23
+ }
24
+
25
+ interface ManifestDependency {
26
+ readonly source?: string;
27
+ readonly version?: string;
28
+ readonly description?: string;
29
+ }
30
+
31
+ interface ModelManifest {
32
+ readonly model?: {
33
+ readonly name?: string;
34
+ readonly version?: string;
35
+ readonly entry?: string;
36
+ };
37
+ readonly dependencies?: Record<string, ManifestDependency>;
38
+ }
39
+
27
40
  interface ManifestCache {
28
41
  readonly manifest: ModelManifest;
29
42
  readonly path: string;
@@ -95,15 +108,6 @@ export class WorkspaceManager {
95
108
  return undefined;
96
109
  }
97
110
 
98
- /**
99
- * Returns the parsed manifest when present, otherwise undefined.
100
- * Uses cached contents when unchanged on disk.
101
- */
102
- async getManifest(): Promise<ModelManifest | undefined> {
103
- await this.ensureInitialized();
104
- return this.loadManifest();
105
- }
106
-
107
111
  /**
108
112
  * Returns the cached lock file or triggers resolution when missing.
109
113
  */
@@ -118,21 +122,12 @@ export class WorkspaceManager {
118
122
  if (cached) {
119
123
  this.lockFile = cached;
120
124
  } else {
121
- if (this.options.allowNetwork === false) {
122
- throw new Error(
123
- 'Lock file (model.lock) not found and network access is disabled.\n' +
124
- 'Hint: Run \'dlang install\' to generate the lock file.'
125
- );
126
- }
127
125
  await this.generateLockFile();
128
126
  }
129
127
  }
130
128
 
131
129
  if (!this.lockFile) {
132
- throw new Error(
133
- 'Unable to resolve workspace lock file.\n' +
134
- 'Hint: Ensure model.yaml exists and run \'dlang install\' to generate model.lock.'
135
- );
130
+ throw new Error('Unable to resolve workspace lock file.');
136
131
  }
137
132
 
138
133
  return this.lockFile;
@@ -156,41 +151,6 @@ export class WorkspaceManager {
156
151
  return this.lockFile;
157
152
  }
158
153
 
159
- /**
160
- * Invalidates all cached data (manifest and lock file).
161
- * Call this when config files change externally (e.g., from CLI commands).
162
- *
163
- * After invalidation, the next call to getManifest() or getLockFile()
164
- * will re-read from disk.
165
- */
166
- invalidateCache(): void {
167
- this.manifestCache = undefined;
168
- this.lockFile = undefined;
169
- // Re-apply undefined to git resolver to clear its lock file
170
- if (this.gitResolver) {
171
- this.gitResolver.setLockFile(undefined);
172
- }
173
- }
174
-
175
- /**
176
- * Invalidates only the manifest cache.
177
- * Call this when model.yaml changes.
178
- */
179
- invalidateManifestCache(): void {
180
- this.manifestCache = undefined;
181
- }
182
-
183
- /**
184
- * Invalidates only the lock file cache.
185
- * Call this when model.lock changes.
186
- */
187
- invalidateLockCache(): void {
188
- this.lockFile = undefined;
189
- if (this.gitResolver) {
190
- this.gitResolver.setLockFile(undefined);
191
- }
192
- }
193
-
194
154
  /**
195
155
  * Provides the shared git URL resolver configured with the current lock file.
196
156
  */
@@ -215,44 +175,12 @@ export class WorkspaceManager {
215
175
  }
216
176
 
217
177
  /**
218
- * Returns the path aliases from the manifest, if present.
178
+ * Resolves a manifest dependency alias to its git import string.
179
+ *
180
+ * @param aliasPath - Alias from import statement (may include subpaths)
181
+ * @returns Resolved git import string or undefined when alias is unknown
219
182
  */
220
- async getPathAliases(): Promise<PathAliases | undefined> {
221
- const manifest = await this.getManifest();
222
- return manifest?.paths;
223
- }
224
-
225
- /**
226
- * Normalizes a dependency entry to its extended form.
227
- * Handles both short form (string version) and extended form (object).
228
- *
229
- * In the new format, the key IS the owner/package, so source is derived from key
230
- * ONLY for git dependencies (not for path-based local dependencies).
231
- */
232
- private normalizeDependency(key: string, dep: DependencySpec): ExtendedDependencySpec {
233
- if (typeof dep === 'string') {
234
- // Short form: "owner/package": "v1.0.0" or "main"
235
- // Key is the source (owner/package format)
236
- return { source: key, ref: dep };
237
- }
238
- // Extended form:
239
- // - If has source: use as-is
240
- // - If has path: it's a local dep, don't set source
241
- // - If neither: derive source from key (owner/package becomes source)
242
- if (dep.source || dep.path) {
243
- return dep;
244
- }
245
- return { ...dep, source: key };
246
- }
247
-
248
- /**
249
- * Resolves a manifest dependency to its git import string.
250
- *
251
- * NEW FORMAT (PRS-010): Dependencies are keyed by owner/package directly
252
- * @param specifier - Import specifier (owner/package format, may include subpaths)
253
- * @returns Resolved git import string or undefined when not found
254
- */
255
- async resolveDependencyImport(specifier: string): Promise<string | undefined> {
183
+ async resolveDependencyImport(aliasPath: string): Promise<string | undefined> {
256
184
  await this.ensureInitialized();
257
185
  const manifest = await this.loadManifest();
258
186
  const dependencies = manifest?.dependencies;
@@ -261,28 +189,18 @@ export class WorkspaceManager {
261
189
  return undefined;
262
190
  }
263
191
 
264
- // NEW: Dependencies are keyed by owner/package (e.g., "domainlang/core")
265
- // Import specifier is also owner/package, potentially with subpath
266
- for (const [key, dep] of Object.entries(dependencies)) {
267
- const normalized = this.normalizeDependency(key, dep);
268
-
269
- // Skip path-based dependencies (handled by path aliases)
270
- if (normalized.path) {
271
- continue;
272
- }
273
-
274
- if (!normalized.source) {
192
+ for (const [alias, dep] of Object.entries(dependencies)) {
193
+ if (!dep?.source) {
275
194
  continue;
276
195
  }
277
196
 
278
- // Match if specifier equals key or starts with key/
279
- if (specifier === key || specifier.startsWith(`${key}/`)) {
280
- const suffix = specifier.slice(key.length);
281
- const ref = normalized.ref ?? '';
282
- const refSegment = ref
283
- ? (ref.startsWith('@') ? ref : `@${ref}`)
197
+ if (aliasPath === alias || aliasPath.startsWith(`${alias}/`)) {
198
+ const suffix = aliasPath.slice(alias.length);
199
+ const version = dep.version ?? '';
200
+ const versionSegment = version
201
+ ? (version.startsWith('@') ? version : `@${version}`)
284
202
  : '';
285
- return `${normalized.source}${refSegment}${suffix}`;
203
+ return `${dep.source}${versionSegment}${suffix}`;
286
204
  }
287
205
  }
288
206
 
@@ -290,15 +208,16 @@ export class WorkspaceManager {
290
208
  }
291
209
 
292
210
  private async performInitialization(startPath: string): Promise<void> {
293
- this.workspaceRoot = await this.findWorkspaceRoot(startPath) ?? path.resolve(startPath);
211
+ this.workspaceRoot = await this.findWorkspaceRoot(startPath);
212
+ if (!this.workspaceRoot) {
213
+ throw new Error('Workspace root (directory with model.yaml) not found.');
214
+ }
294
215
 
295
- // Per PRS-010: Project-local cache at .dlang/packages/ (like node_modules)
296
- const cacheDir = path.join(this.workspaceRoot, '.dlang', 'packages');
297
- this.gitResolver = new GitUrlResolver(cacheDir);
216
+ this.gitResolver = new GitUrlResolver();
298
217
  const loaded = await this.loadLockFileFromDisk();
299
218
  this.applyLockFile(loaded);
300
219
 
301
- if (!this.lockFile && this.options.autoResolve !== false && this.options.allowNetwork !== false) {
220
+ if (!this.lockFile && this.options.autoResolve !== false) {
302
221
  await this.generateLockFile();
303
222
  }
304
223
  }
@@ -336,8 +255,8 @@ export class WorkspaceManager {
336
255
  }
337
256
 
338
257
  const lockFile = await resolver.resolveDependencies();
339
- this.lockFile = lockFile;
340
- this.gitResolver.setLockFile(lockFile);
258
+ this.lockFile = lockFile;
259
+ this.gitResolver.setLockFile(lockFile);
341
260
 
342
261
  // Write JSON lock file
343
262
  await this.writeJsonLockFile(lockFile);
@@ -414,10 +333,6 @@ export class WorkspaceManager {
414
333
 
415
334
  const content = await fs.readFile(manifestPath, 'utf-8');
416
335
  const manifest = (YAML.parse(content) ?? {}) as ModelManifest;
417
-
418
- // Validate manifest structure
419
- this.validateManifest(manifest, manifestPath);
420
-
421
336
  this.manifestCache = {
422
337
  manifest,
423
338
  path: manifestPath,
@@ -433,111 +348,6 @@ export class WorkspaceManager {
433
348
  }
434
349
  }
435
350
 
436
- /**
437
- * Validates manifest structure and dependency configurations.
438
- * Throws detailed errors for invalid manifests.
439
- *
440
- * Supports both new format (owner/package: version) and extended format.
441
- */
442
- private validateManifest(manifest: ModelManifest, manifestPath: string): void {
443
- // Validate path aliases
444
- if (manifest.paths) {
445
- this.validatePathAliases(manifest.paths, manifestPath);
446
- }
447
-
448
- if (!manifest.dependencies) {
449
- return; // No dependencies to validate
450
- }
451
-
452
- for (const [key, dep] of Object.entries(manifest.dependencies)) {
453
- const normalized = this.normalizeDependency(key, dep);
454
-
455
- // Validate mutually exclusive source and path
456
- if (normalized.source && normalized.path) {
457
- throw new Error(
458
- `Invalid dependency '${key}' in ${manifestPath}:\n` +
459
- `Cannot specify both 'source' and 'path'.\n` +
460
- `Hint: Use 'source' for git dependencies or 'path' for local workspace dependencies.`
461
- );
462
- }
463
-
464
- // For string format, source is always derived from key (valid)
465
- // For extended format without source or path, error
466
- if (typeof dep !== 'string' && !normalized.source && !normalized.path) {
467
- throw new Error(
468
- `Invalid dependency '${key}' in ${manifestPath}:\n` +
469
- `Must specify either 'source' or 'path'.\n` +
470
- `Hint: Add 'source: owner/repo' for git dependencies, or 'path: ./local/path' for local packages.`
471
- );
472
- }
473
-
474
- // Validate path is relative and within workspace
475
- if (normalized.path) {
476
- this.validateLocalPath(normalized.path, key, manifestPath);
477
- }
478
-
479
- // Validate source has ref when specified
480
- if (normalized.source && !normalized.ref) {
481
- throw new Error(
482
- `Invalid dependency '${key}' in ${manifestPath}:\n` +
483
- `Git dependencies must specify a 'ref' (git reference).\n` +
484
- `Hint: Add 'ref: v1.0.0' (tag), 'ref: main' (branch), or a commit SHA.`
485
- );
486
- }
487
- }
488
- }
489
-
490
- /**
491
- * Validates path aliases for security and correctness.
492
- */
493
- private validatePathAliases(paths: PathAliases, manifestPath: string): void {
494
- for (const [alias, targetPath] of Object.entries(paths)) {
495
- // Validate alias starts with @
496
- if (!alias.startsWith('@')) {
497
- throw new Error(
498
- `Invalid path alias '${alias}' in ${manifestPath}:\n` +
499
- `Path aliases must start with '@'.\n` +
500
- `Hint: Rename to '@${alias}' in your model.yaml paths section.`
501
- );
502
- }
503
-
504
- // Validate target path doesn't escape workspace
505
- this.validateLocalPath(targetPath, alias, manifestPath);
506
- }
507
- }
508
-
509
- /**
510
- * Validates local path dependencies for security.
511
- * Ensures paths don't escape workspace boundary.
512
- */
513
- private validateLocalPath(localPath: string, alias: string, manifestPath: string): void {
514
- // Reject absolute paths
515
- if (path.isAbsolute(localPath)) {
516
- throw new Error(
517
- `Invalid local path '${alias}' in ${manifestPath}:\n` +
518
- `Cannot use absolute path '${localPath}'.\n` +
519
- `Hint: Use relative paths (e.g., './lib', '../shared') for local dependencies.`
520
- );
521
- }
522
-
523
- // Resolve path relative to manifest directory
524
- const manifestDir = path.dirname(manifestPath);
525
- const resolvedPath = path.resolve(manifestDir, localPath);
526
- const workspaceRoot = this.workspaceRoot || manifestDir;
527
-
528
- // Check if resolved path is within workspace
529
- const relativePath = path.relative(workspaceRoot, resolvedPath);
530
- if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
531
- throw new Error(
532
- `Invalid local path '${alias}' in ${manifestPath}:\n` +
533
- `Path '${localPath}' resolves outside workspace boundary.\n` +
534
- `Resolved: ${resolvedPath}\n` +
535
- `Workspace: ${workspaceRoot}\n` +
536
- `Hint: Local dependencies must be within the workspace. Consider moving the dependency or using a git-based source.`
537
- );
538
- }
539
- }
540
-
541
351
  private parseJsonLockFile(content: string): LockFile {
542
352
  const parsed = JSON.parse(content) as Partial<LockFile> & {
543
353
  dependencies?: Record<string, Partial<LockedDependency>>;
@@ -547,12 +357,11 @@ export class WorkspaceManager {
547
357
  const dependencies: Record<string, LockedDependency> = {};
548
358
 
549
359
  for (const [key, value] of Object.entries(parsed.dependencies ?? {})) {
550
- if (!value || typeof value.ref !== 'string' || typeof value.resolved !== 'string' || typeof value.commit !== 'string') {
360
+ if (!value || typeof value.version !== 'string' || typeof value.resolved !== 'string' || typeof value.commit !== 'string') {
551
361
  continue;
552
362
  }
553
363
  dependencies[key] = {
554
- ref: value.ref,
555
- refType: value.refType ?? 'commit', // Default to commit for backwards compatibility
364
+ version: value.version,
556
365
  resolved: value.resolved,
557
366
  commit: value.commit,
558
367
  integrity: value.integrity,