@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,292 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { URI, type LangiumDocument } from 'langium';
4
+ import { WorkspaceManager } from './workspace-manager.js';
5
+ import type { DomainLangServices } from '../domain-lang-module.js';
6
+ import type { LockFile } from './types.js';
7
+
8
+ /**
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)
19
+ */
20
+ export class ImportResolver {
21
+ private readonly workspaceManager: WorkspaceManager;
22
+
23
+ constructor(services: DomainLangServices) {
24
+ this.workspaceManager = services.imports.WorkspaceManager;
25
+ }
26
+
27
+ /**
28
+ * Resolve an import specifier relative to a Langium document.
29
+ */
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
+ }
272
+ }
273
+
274
+ /**
275
+ * Get the current lock file (if loaded).
276
+ */
277
+ async getLockFile(): Promise<LockFile | undefined> {
278
+ return this.workspaceManager.getLockFile();
279
+ }
280
+ }
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
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Performance Optimization Service
3
+ *
4
+ * Provides caching and optimization strategies for dependency resolution:
5
+ * - In-memory caching of frequently accessed lock files
6
+ * - Parallel dependency downloads
7
+ * - Cache warming strategies
8
+ * - Stale cache detection
9
+ */
10
+
11
+ import type { LockFile } from './types.js';
12
+ import path from 'node:path';
13
+ import fs from 'node:fs/promises';
14
+
15
+ /**
16
+ * Cache entry with timestamp for TTL management.
17
+ */
18
+ interface CacheEntry<T> {
19
+ value: T;
20
+ timestamp: number;
21
+ }
22
+
23
+ /**
24
+ * Performance optimizer with in-memory caching.
25
+ */
26
+ export class PerformanceOptimizer {
27
+ private lockFileCache = new Map<string, CacheEntry<LockFile>>();
28
+ private manifestCache = new Map<string, CacheEntry<unknown>>();
29
+ private readonly cacheTTL: number;
30
+
31
+ constructor(options: { cacheTTL?: number } = {}) {
32
+ // Default TTL: 5 minutes
33
+ this.cacheTTL = options.cacheTTL ?? 5 * 60 * 1000;
34
+ }
35
+
36
+ /**
37
+ * Gets a lock file from cache or loads it from disk.
38
+ */
39
+ async getCachedLockFile(workspaceRoot: string): Promise<LockFile | undefined> {
40
+ const cacheKey = this.normalizePath(workspaceRoot);
41
+ const cached = this.lockFileCache.get(cacheKey);
42
+
43
+ // Check if cache is still valid
44
+ if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
45
+ return cached.value;
46
+ }
47
+
48
+ // Load from disk
49
+ const lockPath = path.join(workspaceRoot, 'model.lock');
50
+ try {
51
+ const content = await fs.readFile(lockPath, 'utf-8');
52
+ const lockFile = JSON.parse(content) as LockFile;
53
+
54
+ // Cache it
55
+ this.lockFileCache.set(cacheKey, {
56
+ value: lockFile,
57
+ timestamp: Date.now(),
58
+ });
59
+
60
+ return lockFile;
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Gets a manifest file from cache or loads it from disk.
68
+ */
69
+ async getCachedManifest(manifestPath: string): Promise<unknown | undefined> {
70
+ const cacheKey = this.normalizePath(manifestPath);
71
+ const cached = this.manifestCache.get(cacheKey);
72
+
73
+ if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
74
+ return cached.value;
75
+ }
76
+
77
+ try {
78
+ const content = await fs.readFile(manifestPath, 'utf-8');
79
+ const manifest = JSON.parse(content);
80
+
81
+ this.manifestCache.set(cacheKey, {
82
+ value: manifest,
83
+ timestamp: Date.now(),
84
+ });
85
+
86
+ return manifest;
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Invalidates cache for a specific workspace.
94
+ */
95
+ invalidateCache(workspaceRoot: string): void {
96
+ const cacheKey = this.normalizePath(workspaceRoot);
97
+ this.lockFileCache.delete(cacheKey);
98
+ }
99
+
100
+ /**
101
+ * Clears all caches.
102
+ */
103
+ clearAllCaches(): void {
104
+ this.lockFileCache.clear();
105
+ this.manifestCache.clear();
106
+ }
107
+
108
+ /**
109
+ * Gets cache statistics.
110
+ */
111
+ getCacheStats(): { lockFiles: number; manifests: number } {
112
+ return {
113
+ lockFiles: this.lockFileCache.size,
114
+ manifests: this.manifestCache.size,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Detects if cached files are stale compared to disk.
120
+ */
121
+ async detectStaleCaches(): Promise<string[]> {
122
+ const stale: string[] = [];
123
+
124
+ for (const [workspaceRoot] of this.lockFileCache) {
125
+ const lockPath = path.join(workspaceRoot, 'model.lock');
126
+ try {
127
+ const stat = await fs.stat(lockPath);
128
+ const cached = this.lockFileCache.get(workspaceRoot);
129
+
130
+ if (cached && stat.mtimeMs > cached.timestamp) {
131
+ stale.push(workspaceRoot);
132
+ }
133
+ } catch {
134
+ // File doesn't exist anymore
135
+ stale.push(workspaceRoot);
136
+ }
137
+ }
138
+
139
+ return stale;
140
+ }
141
+
142
+ /**
143
+ * Normalizes a file path for cache keys.
144
+ */
145
+ private normalizePath(filePath: string): string {
146
+ return path.resolve(filePath);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Global singleton performance optimizer.
152
+ */
153
+ let globalOptimizer: PerformanceOptimizer | undefined;
154
+
155
+ /**
156
+ * Gets the global performance optimizer instance.
157
+ */
158
+ export function getGlobalOptimizer(): PerformanceOptimizer {
159
+ if (!globalOptimizer) {
160
+ globalOptimizer = new PerformanceOptimizer();
161
+ }
162
+ return globalOptimizer;
163
+ }
164
+
165
+ /**
166
+ * Resets the global optimizer (useful for testing).
167
+ */
168
+ export function resetGlobalOptimizer(): void {
169
+ globalOptimizer = undefined;
170
+ }
@@ -0,0 +1,121 @@
1
+ import type {
2
+ Model,
3
+ Relationship,
4
+ StructureElement,
5
+ BoundedContext,
6
+ ContextMap
7
+ } from '../generated/ast.js';
8
+ import {
9
+ isBoundedContext,
10
+ isContextMap,
11
+ isNamespaceDeclaration
12
+ } from '../generated/ast.js';
13
+
14
+ /**
15
+ * Enriches relationships in the model by inferring relationship types
16
+ * from roles and arrow directions.
17
+ *
18
+ * This service walks the entire model structure and applies inference
19
+ * rules to relationships that don't have an explicit type.
20
+ *
21
+ * @param model - The root model to process
22
+ */
23
+ export function setInferredRelationshipTypes(model: Model): void {
24
+ walkStructureElements(model.children);
25
+ }
26
+
27
+ /**
28
+ * Recursively walks structure elements to find and enrich relationships.
29
+ *
30
+ * @param elements - Array of structure elements to process
31
+ * @param containerBc - Optional container bounded context (for nested contexts)
32
+ */
33
+ function walkStructureElements(
34
+ elements: StructureElement[] = [],
35
+ containerBc?: BoundedContext
36
+ ): void {
37
+ for (const element of elements) {
38
+ if (isNamespaceDeclaration(element)) {
39
+ walkStructureElements(element.children, containerBc);
40
+ } else if (isBoundedContext(element)) {
41
+ processContextRelationships(element);
42
+ } else if (isContextMap(element)) {
43
+ processMapRelationships(element);
44
+ }
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Processes relationships within a bounded context.
50
+ *
51
+ * @param context - The bounded context to process
52
+ */
53
+ function processContextRelationships(context: BoundedContext): void {
54
+ for (const rel of context.relationships) {
55
+ enrichRelationship(rel);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Processes relationships within a context map.
61
+ *
62
+ * @param map - The context map to process
63
+ */
64
+ function processMapRelationships(map: ContextMap): void {
65
+ if (map.relationships) {
66
+ for (const rel of map.relationships) {
67
+ enrichRelationship(rel);
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Enriches a single relationship by inferring its type if not explicitly set.
74
+ *
75
+ * @param rel - The relationship to enrich
76
+ */
77
+ function enrichRelationship(rel: Relationship): void {
78
+ if (!rel.type) {
79
+ rel.inferredType = inferRelationshipType(rel);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Infers relationship type from arrow direction and roles.
85
+ *
86
+ * Inference rules:
87
+ * - `><` → SeparateWays
88
+ * - `<->` with P roles → Partnership
89
+ * - `<->` with SK roles → SharedKernel
90
+ * - `->` or `<-` → UpstreamDownstream
91
+ *
92
+ * @param relationship - The relationship to analyze
93
+ * @returns The inferred type or undefined if no rule matches
94
+ */
95
+ function inferRelationshipType(relationship: Relationship): string | undefined {
96
+ const leftPatterns = (relationship.leftPatterns ?? []).map((r: string) => r.toUpperCase());
97
+ const rightPatterns = (relationship.rightPatterns ?? []).map((r: string) => r.toUpperCase());
98
+
99
+ if (relationship.arrow === '><') {
100
+ return 'SeparateWays';
101
+ }
102
+
103
+ if (relationship.arrow === '<->') {
104
+ const noPatterns = leftPatterns.length === 0 && rightPatterns.length === 0;
105
+ const bothPartners = leftPatterns.includes('P') && rightPatterns.includes('P');
106
+
107
+ if (noPatterns || bothPartners) {
108
+ return 'Partnership';
109
+ }
110
+
111
+ if (leftPatterns.includes('SK') && rightPatterns.includes('SK')) {
112
+ return 'SharedKernel';
113
+ }
114
+ }
115
+
116
+ if (relationship.arrow === '->' || relationship.arrow === '<-') {
117
+ return 'UpstreamDownstream';
118
+ }
119
+
120
+ return undefined;
121
+ }