@domainlang/language 0.7.0 → 0.8.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.
Files changed (83) hide show
  1. package/out/domain-lang-module.d.ts +2 -0
  2. package/out/domain-lang-module.js +21 -2
  3. package/out/domain-lang-module.js.map +1 -1
  4. package/out/lsp/domain-lang-completion.d.ts +142 -1
  5. package/out/lsp/domain-lang-completion.js +620 -22
  6. package/out/lsp/domain-lang-completion.js.map +1 -1
  7. package/out/lsp/domain-lang-document-symbol-provider.d.ts +79 -0
  8. package/out/lsp/domain-lang-document-symbol-provider.js +210 -0
  9. package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -0
  10. package/out/lsp/domain-lang-index-manager.d.ts +34 -5
  11. package/out/lsp/domain-lang-index-manager.js +66 -27
  12. package/out/lsp/domain-lang-index-manager.js.map +1 -1
  13. package/out/lsp/domain-lang-node-kind-provider.d.ts +27 -0
  14. package/out/lsp/domain-lang-node-kind-provider.js +87 -0
  15. package/out/lsp/domain-lang-node-kind-provider.js.map +1 -0
  16. package/out/lsp/domain-lang-scope-provider.d.ts +53 -20
  17. package/out/lsp/domain-lang-scope-provider.js +119 -44
  18. package/out/lsp/domain-lang-scope-provider.js.map +1 -1
  19. package/out/lsp/domain-lang-workspace-manager.d.ts +23 -2
  20. package/out/lsp/domain-lang-workspace-manager.js +51 -6
  21. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  22. package/out/lsp/hover/domain-lang-hover.d.ts +16 -6
  23. package/out/lsp/hover/domain-lang-hover.js +160 -134
  24. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  25. package/out/lsp/hover/hover-builders.d.ts +57 -0
  26. package/out/lsp/hover/hover-builders.js +171 -0
  27. package/out/lsp/hover/hover-builders.js.map +1 -0
  28. package/out/main.js +2 -1
  29. package/out/main.js.map +1 -1
  30. package/out/sdk/index.d.ts +2 -1
  31. package/out/sdk/index.js +1 -1
  32. package/out/sdk/index.js.map +1 -1
  33. package/out/sdk/loader-node.js +1 -1
  34. package/out/sdk/loader-node.js.map +1 -1
  35. package/out/sdk/loader.d.ts +55 -2
  36. package/out/sdk/loader.js +87 -28
  37. package/out/sdk/loader.js.map +1 -1
  38. package/out/sdk/query.js +14 -11
  39. package/out/sdk/query.js.map +1 -1
  40. package/out/services/package-boundary-detector.d.ts +101 -0
  41. package/out/services/package-boundary-detector.js +211 -0
  42. package/out/services/package-boundary-detector.js.map +1 -0
  43. package/out/services/performance-optimizer.js +6 -2
  44. package/out/services/performance-optimizer.js.map +1 -1
  45. package/out/services/types.d.ts +24 -0
  46. package/out/services/types.js.map +1 -1
  47. package/out/services/workspace-manager.d.ts +73 -6
  48. package/out/services/workspace-manager.js +210 -57
  49. package/out/services/workspace-manager.js.map +1 -1
  50. package/out/utils/import-utils.d.ts +9 -6
  51. package/out/utils/import-utils.js +26 -15
  52. package/out/utils/import-utils.js.map +1 -1
  53. package/out/validation/constants.d.ts +7 -0
  54. package/out/validation/constants.js +21 -3
  55. package/out/validation/constants.js.map +1 -1
  56. package/out/validation/import.d.ts +11 -1
  57. package/out/validation/import.js +42 -14
  58. package/out/validation/import.js.map +1 -1
  59. package/out/validation/maps.js +50 -1
  60. package/out/validation/maps.js.map +1 -1
  61. package/package.json +5 -5
  62. package/src/domain-lang-module.ts +24 -3
  63. package/src/lsp/domain-lang-completion.ts +736 -27
  64. package/src/lsp/domain-lang-document-symbol-provider.ts +254 -0
  65. package/src/lsp/domain-lang-index-manager.ts +79 -27
  66. package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
  67. package/src/lsp/domain-lang-scope-provider.ts +171 -55
  68. package/src/lsp/domain-lang-workspace-manager.ts +64 -6
  69. package/src/lsp/hover/domain-lang-hover.ts +189 -131
  70. package/src/lsp/hover/hover-builders.ts +208 -0
  71. package/src/main.ts +3 -1
  72. package/src/sdk/index.ts +2 -1
  73. package/src/sdk/loader-node.ts +2 -1
  74. package/src/sdk/loader.ts +125 -34
  75. package/src/sdk/query.ts +15 -11
  76. package/src/services/package-boundary-detector.ts +238 -0
  77. package/src/services/performance-optimizer.ts +6 -2
  78. package/src/services/types.ts +25 -0
  79. package/src/services/workspace-manager.ts +259 -62
  80. package/src/utils/import-utils.ts +27 -15
  81. package/src/validation/constants.ts +23 -6
  82. package/src/validation/import.ts +49 -14
  83. package/src/validation/maps.ts +59 -2
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Package Boundary Detector
3
+ *
4
+ * Determines package boundaries for import scoping.
5
+ * Per ADR-003, package boundaries are defined by:
6
+ * - External packages: Files within .dlang/packages/ sharing the same model.yaml
7
+ * - Local files: Each file is its own boundary (non-transitive)
8
+ *
9
+ * Used by DomainLangScopeProvider to enable transitive imports within
10
+ * package boundaries while keeping local file imports non-transitive.
11
+ */
12
+
13
+ import * as path from 'node:path';
14
+ import * as fs from 'node:fs/promises';
15
+ import { URI } from 'langium';
16
+
17
+ /**
18
+ * Detects and caches package boundaries for efficient scope resolution.
19
+ */
20
+ export class PackageBoundaryDetector {
21
+ /**
22
+ * Cache mapping document URI to its package root path.
23
+ * - External packages: path to directory containing model.yaml
24
+ * - Local files: null (no package boundary)
25
+ */
26
+ private readonly packageRootCache = new Map<string, string | null>();
27
+
28
+ /**
29
+ * Determines if a document is part of an external package.
30
+ *
31
+ * External packages are stored in .dlang/packages/owner/repo/commit/
32
+ *
33
+ * @param documentUri - The URI of the document to check
34
+ * @returns true if document is in an external package
35
+ */
36
+ isExternalPackage(documentUri: URI | string): boolean {
37
+ const fsPath = this.toFsPath(documentUri);
38
+ const normalized = fsPath.split(path.sep);
39
+
40
+ // Check if path contains .dlang/packages/
41
+ const dlangIndex = normalized.indexOf('.dlang');
42
+ if (dlangIndex === -1) {
43
+ return false;
44
+ }
45
+
46
+ return dlangIndex + 1 < normalized.length &&
47
+ normalized[dlangIndex + 1] === 'packages';
48
+ }
49
+
50
+ /**
51
+ * Gets the package root for a document.
52
+ *
53
+ * For external packages (.dlang/packages/), walks up from the document
54
+ * to find the nearest model.yaml file within the package structure.
55
+ *
56
+ * For local files, returns null (no package boundary).
57
+ *
58
+ * @param documentUri - The URI of the document
59
+ * @returns Absolute path to package root, or null if not in a package
60
+ */
61
+ async getPackageRoot(documentUri: URI | string): Promise<string | null> {
62
+ const uriString = documentUri.toString();
63
+
64
+ // Check cache first
65
+ if (this.packageRootCache.has(uriString)) {
66
+ return this.packageRootCache.get(uriString) ?? null;
67
+ }
68
+
69
+ // If not an external package, it has no package boundary
70
+ if (!this.isExternalPackage(documentUri)) {
71
+ this.packageRootCache.set(uriString, null);
72
+ return null;
73
+ }
74
+
75
+ const fsPath = this.toFsPath(documentUri);
76
+ const packageRoot = await this.findPackageRootForExternal(fsPath);
77
+ this.packageRootCache.set(uriString, packageRoot);
78
+ return packageRoot;
79
+ }
80
+
81
+ /**
82
+ * Checks if two documents are in the same package (synchronous heuristic).
83
+ *
84
+ * This is a fast, synchronous check that compares package commit directories
85
+ * without filesystem access. Documents are in the same package if:
86
+ * - Both are in .dlang/packages/ AND
87
+ * - They share the same owner/repo/commit path
88
+ *
89
+ * This is used by the scope provider which needs synchronous access.
90
+ *
91
+ * Structure: .dlang/packages/owner/repo/commit/...
92
+ *
93
+ * @param doc1Uri - URI of first document
94
+ * @param doc2Uri - URI of second document
95
+ * @returns true if both are in the same package commit directory
96
+ */
97
+ areInSamePackageSync(doc1Uri: URI | string, doc2Uri: URI | string): boolean {
98
+ // Both must be external packages
99
+ if (!this.isExternalPackage(doc1Uri) || !this.isExternalPackage(doc2Uri)) {
100
+ return false;
101
+ }
102
+
103
+ const path1 = this.toFsPath(doc1Uri);
104
+ const path2 = this.toFsPath(doc2Uri);
105
+
106
+ const root1 = this.getPackageCommitDirectory(path1);
107
+ const root2 = this.getPackageCommitDirectory(path2);
108
+
109
+ return root1 !== null && root1 === root2;
110
+ }
111
+
112
+ /**
113
+ * Gets the package commit directory (owner/repo/commit) from a path.
114
+ *
115
+ * @param fsPath - Filesystem path
116
+ * @returns Commit directory path or null
117
+ */
118
+ private getPackageCommitDirectory(fsPath: string): string | null {
119
+ const normalized = fsPath.split(path.sep);
120
+ const dlangIndex = normalized.indexOf('.dlang');
121
+
122
+ if (dlangIndex === -1) {
123
+ return null;
124
+ }
125
+
126
+ const packagesIndex = dlangIndex + 1;
127
+ if (packagesIndex >= normalized.length || normalized[packagesIndex] !== 'packages') {
128
+ return null;
129
+ }
130
+
131
+ // Commit directory is at: .dlang/packages/owner/repo/commit
132
+ const commitIndex = packagesIndex + 3;
133
+ if (commitIndex >= normalized.length) {
134
+ return null;
135
+ }
136
+
137
+ // Return the path up to and including the commit directory
138
+ return normalized.slice(0, commitIndex + 1).join(path.sep);
139
+ }
140
+
141
+ /**
142
+ * Checks if two documents are in the same package.
143
+ *
144
+ * Documents are in the same package if:
145
+ * - Both are external packages AND
146
+ * - They share the same package root (model.yaml location)
147
+ *
148
+ * Local files are never in the same package (each is isolated).
149
+ *
150
+ * @param doc1Uri - URI of first document
151
+ * @param doc2Uri - URI of second document
152
+ * @returns true if both documents are in the same package
153
+ */
154
+ async areInSamePackage(doc1Uri: URI | string, doc2Uri: URI | string): Promise<boolean> {
155
+ const root1 = await this.getPackageRoot(doc1Uri);
156
+ const root2 = await this.getPackageRoot(doc2Uri);
157
+
158
+ // If either is not in a package, they can't be in the same package
159
+ if (!root1 || !root2) {
160
+ return false;
161
+ }
162
+
163
+ return root1 === root2;
164
+ }
165
+
166
+ /**
167
+ * Finds the package root for an external package by walking up to find model.yaml.
168
+ *
169
+ * External packages have structure: .dlang/packages/owner/repo/commit/...
170
+ * The model.yaml should be at the commit level or just below it.
171
+ *
172
+ * @param fsPath - Filesystem path of the document
173
+ * @returns Path to directory containing model.yaml, or null
174
+ */
175
+ private async findPackageRootForExternal(fsPath: string): Promise<string | null> {
176
+ const normalized = fsPath.split(path.sep);
177
+ const dlangIndex = normalized.indexOf('.dlang');
178
+
179
+ if (dlangIndex === -1) {
180
+ return null;
181
+ }
182
+
183
+ // Find the packages directory
184
+ const packagesIndex = dlangIndex + 1;
185
+ if (packagesIndex >= normalized.length || normalized[packagesIndex] !== 'packages') {
186
+ return null;
187
+ }
188
+
189
+ // Start from the commit directory level
190
+ // Structure: .dlang/packages/owner/repo/commit/
191
+ const commitIndex = packagesIndex + 3;
192
+ if (commitIndex >= normalized.length) {
193
+ return null;
194
+ }
195
+
196
+ // Walk up from the document path to the commit directory
197
+ let currentPath = path.dirname(fsPath);
198
+ const commitPath = normalized.slice(0, commitIndex + 1).join(path.sep);
199
+
200
+ // Search upward for model.yaml, but don't go above the commit directory
201
+ while (currentPath.length >= commitPath.length) {
202
+ const manifestPath = path.join(currentPath, 'model.yaml');
203
+ try {
204
+ await fs.access(manifestPath);
205
+ return currentPath;
206
+ } catch {
207
+ // model.yaml not found at this level, continue upward
208
+ }
209
+
210
+ const parent = path.dirname(currentPath);
211
+ if (parent === currentPath) {
212
+ // Reached filesystem root without finding model.yaml
213
+ break;
214
+ }
215
+ currentPath = parent;
216
+ }
217
+
218
+ return null;
219
+ }
220
+
221
+ /**
222
+ * Converts a URI to a filesystem path.
223
+ */
224
+ private toFsPath(uri: URI | string): string {
225
+ if (typeof uri === 'string') {
226
+ uri = URI.parse(uri);
227
+ }
228
+ return uri.fsPath;
229
+ }
230
+
231
+ /**
232
+ * Clears the package root cache.
233
+ * Call this when packages are installed/removed.
234
+ */
235
+ clearCache(): void {
236
+ this.packageRootCache.clear();
237
+ }
238
+ }
@@ -76,7 +76,8 @@ export class PerformanceOptimizer {
76
76
 
77
77
  try {
78
78
  const content = await fs.readFile(manifestPath, 'utf-8');
79
- const manifest = JSON.parse(content);
79
+ const { parse } = await import('yaml');
80
+ const manifest: unknown = parse(content);
80
81
 
81
82
  this.manifestCache.set(cacheKey, {
82
83
  value: manifest,
@@ -127,7 +128,10 @@ export class PerformanceOptimizer {
127
128
  const stat = await fs.stat(lockPath);
128
129
  const cached = this.lockFileCache.get(workspaceRoot);
129
130
 
130
- if (cached && stat.mtimeMs > cached.timestamp) {
131
+ // Floor mtimeMs to integer precision to match Date.now()
132
+ // some filesystems (e.g. APFS) report sub-millisecond mtime,
133
+ // which can exceed the integer timestamp from Date.now().
134
+ if (cached && Math.floor(stat.mtimeMs) > cached.timestamp) {
131
135
  stale.push(workspaceRoot);
132
136
  }
133
137
  } catch {
@@ -42,6 +42,31 @@
42
42
  // Core Building Blocks
43
43
  // ============================================================================
44
44
 
45
+ /**
46
+ * Information about an import statement tracked during indexing.
47
+ *
48
+ * Used by IndexManager to track both the import's resolved location
49
+ * and its alias (if any) for scope resolution.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * // import "larsbaunwall/ddd-types" as ddd
54
+ * const info: ImportInfo = {
55
+ * specifier: "larsbaunwall/ddd-types",
56
+ * alias: "ddd",
57
+ * resolvedUri: "file:///.dlang/packages/larsbaunwall/ddd-types/abc123/index.dlang"
58
+ * };
59
+ * ```
60
+ */
61
+ export interface ImportInfo {
62
+ /** The import specifier as written in source (e.g., "./file.dlang", "owner/repo") */
63
+ readonly specifier: string;
64
+ /** Optional alias from 'as' clause (e.g., "ddd" in 'import "pkg" as ddd') */
65
+ readonly alias?: string;
66
+ /** Resolved absolute URI of the imported document */
67
+ readonly resolvedUri: string;
68
+ }
69
+
45
70
  /**
46
71
  * Type of git reference for version pinning.
47
72
  *