@domainlang/language 0.1.82 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +18 -18
  2. package/out/domain-lang-module.d.ts +2 -0
  3. package/out/domain-lang-module.js +11 -3
  4. package/out/domain-lang-module.js.map +1 -1
  5. package/out/generated/ast.d.ts +8 -19
  6. package/out/generated/ast.js +1 -10
  7. package/out/generated/ast.js.map +1 -1
  8. package/out/generated/grammar.d.ts +1 -1
  9. package/out/generated/grammar.js +28 -123
  10. package/out/generated/grammar.js.map +1 -1
  11. package/out/generated/module.d.ts +1 -1
  12. package/out/generated/module.js +1 -1
  13. package/out/index.d.ts +3 -0
  14. package/out/index.js +5 -0
  15. package/out/index.js.map +1 -1
  16. package/out/lsp/domain-lang-code-actions.d.ts +55 -0
  17. package/out/lsp/domain-lang-code-actions.js +143 -0
  18. package/out/lsp/domain-lang-code-actions.js.map +1 -0
  19. package/out/lsp/domain-lang-workspace-manager.d.ts +21 -0
  20. package/out/lsp/domain-lang-workspace-manager.js +93 -0
  21. package/out/lsp/domain-lang-workspace-manager.js.map +1 -0
  22. package/out/lsp/hover/domain-lang-hover.js +0 -4
  23. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  24. package/out/lsp/manifest-diagnostics.d.ts +82 -0
  25. package/out/lsp/manifest-diagnostics.js +230 -0
  26. package/out/lsp/manifest-diagnostics.js.map +1 -0
  27. package/out/sdk/index.d.ts +1 -1
  28. package/out/sdk/loader-node.d.ts +7 -3
  29. package/out/sdk/loader-node.js +24 -9
  30. package/out/sdk/loader-node.js.map +1 -1
  31. package/out/sdk/types.d.ts +0 -21
  32. package/out/services/dependency-analyzer.d.ts +3 -39
  33. package/out/services/dependency-analyzer.js +22 -47
  34. package/out/services/dependency-analyzer.js.map +1 -1
  35. package/out/services/dependency-resolver.d.ts +68 -45
  36. package/out/services/dependency-resolver.js +243 -43
  37. package/out/services/dependency-resolver.js.map +1 -1
  38. package/out/services/git-url-resolver.browser.d.ts +4 -12
  39. package/out/services/git-url-resolver.browser.js +5 -1
  40. package/out/services/git-url-resolver.browser.js.map +1 -1
  41. package/out/services/git-url-resolver.d.ts +22 -56
  42. package/out/services/git-url-resolver.js +70 -36
  43. package/out/services/git-url-resolver.js.map +1 -1
  44. package/out/services/governance-validator.d.ts +1 -37
  45. package/out/services/governance-validator.js +4 -10
  46. package/out/services/governance-validator.js.map +1 -1
  47. package/out/services/import-resolver.d.ts +65 -6
  48. package/out/services/import-resolver.js +223 -5
  49. package/out/services/import-resolver.js.map +1 -1
  50. package/out/services/performance-optimizer.d.ts +1 -1
  51. package/out/services/semver.d.ts +98 -0
  52. package/out/services/semver.js +195 -0
  53. package/out/services/semver.js.map +1 -0
  54. package/out/services/types.d.ts +340 -0
  55. package/out/services/types.js +46 -0
  56. package/out/services/types.js.map +1 -0
  57. package/out/services/workspace-manager.d.ts +57 -10
  58. package/out/services/workspace-manager.js +187 -21
  59. package/out/services/workspace-manager.js.map +1 -1
  60. package/out/syntaxes/domain-lang.monarch.js +1 -1
  61. package/out/syntaxes/domain-lang.monarch.js.map +1 -1
  62. package/out/utils/import-utils.d.ts +4 -12
  63. package/out/utils/import-utils.js +35 -135
  64. package/out/utils/import-utils.js.map +1 -1
  65. package/out/validation/constants.d.ts +103 -0
  66. package/out/validation/constants.js +141 -2
  67. package/out/validation/constants.js.map +1 -1
  68. package/out/validation/domain.js +46 -1
  69. package/out/validation/domain.js.map +1 -1
  70. package/out/validation/import.d.ts +46 -22
  71. package/out/validation/import.js +187 -85
  72. package/out/validation/import.js.map +1 -1
  73. package/out/validation/manifest.d.ts +144 -0
  74. package/out/validation/manifest.js +327 -0
  75. package/out/validation/manifest.js.map +1 -0
  76. package/out/validation/maps.js +10 -6
  77. package/out/validation/maps.js.map +1 -1
  78. package/out/validation/metadata.js +5 -1
  79. package/out/validation/metadata.js.map +1 -1
  80. package/package.json +8 -6
  81. package/src/domain-lang-module.ts +18 -6
  82. package/src/domain-lang.langium +7 -12
  83. package/src/generated/ast.ts +7 -20
  84. package/src/generated/grammar.ts +28 -123
  85. package/src/generated/module.ts +1 -1
  86. package/src/index.ts +7 -0
  87. package/src/lsp/domain-lang-code-actions.ts +189 -0
  88. package/src/lsp/domain-lang-workspace-manager.ts +104 -0
  89. package/src/lsp/hover/domain-lang-hover.ts +0 -2
  90. package/src/lsp/manifest-diagnostics.ts +290 -0
  91. package/src/sdk/index.ts +0 -2
  92. package/src/sdk/loader-node.ts +29 -9
  93. package/src/sdk/types.ts +0 -23
  94. package/src/services/dependency-analyzer.ts +24 -84
  95. package/src/services/dependency-resolver.ts +301 -84
  96. package/src/services/git-url-resolver.browser.ts +9 -14
  97. package/src/services/git-url-resolver.ts +86 -93
  98. package/src/services/governance-validator.ts +5 -47
  99. package/src/services/import-resolver.ts +270 -8
  100. package/src/services/performance-optimizer.ts +1 -1
  101. package/src/services/semver.ts +213 -0
  102. package/src/services/types.ts +415 -0
  103. package/src/services/workspace-manager.ts +237 -46
  104. package/src/utils/import-utils.ts +38 -160
  105. package/src/validation/constants.ts +182 -2
  106. package/src/validation/domain.ts +54 -1
  107. package/src/validation/import.ts +228 -104
  108. package/src/validation/manifest.ts +439 -0
  109. package/src/validation/maps.ts +10 -6
  110. package/src/validation/metadata.ts +5 -1
  111. package/src/syntaxes/domain-lang.monarch.ts +0 -29
@@ -13,11 +13,63 @@
13
13
 
14
14
  import type { CodeDescription } from 'vscode-languageserver-types';
15
15
 
16
+ // ============================================================================
17
+ // Issue Codes for Code Actions
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Diagnostic codes used to identify validation issues.
22
+ * Code actions match on these codes to provide quick fixes.
23
+ *
24
+ * Naming convention: CATEGORY_SPECIFIC_ISSUE
25
+ */
26
+ export const IssueCodes = {
27
+ // Import & Dependency Issues
28
+ ImportMissingUri: 'import-missing-uri',
29
+ ImportRequiresManifest: 'import-requires-manifest',
30
+ ImportNotInManifest: 'import-not-in-manifest',
31
+ ImportNotInstalled: 'import-not-installed',
32
+ ImportConflictingSourcePath: 'import-conflicting-source-path',
33
+ ImportMissingSourceOrPath: 'import-missing-source-or-path',
34
+ ImportMissingRef: 'import-missing-ref',
35
+ ImportAbsolutePath: 'import-absolute-path',
36
+ ImportEscapesWorkspace: 'import-escapes-workspace',
37
+
38
+ // Domain Issues
39
+ DomainNoVision: 'domain-no-vision',
40
+ DomainCircularHierarchy: 'domain-circular-hierarchy',
41
+
42
+ // Bounded Context Issues
43
+ BoundedContextNoDescription: 'bounded-context-no-description',
44
+ BoundedContextNoDomain: 'bounded-context-no-domain',
45
+ BoundedContextClassificationConflict: 'bounded-context-classification-conflict',
46
+ BoundedContextTeamConflict: 'bounded-context-team-conflict',
47
+
48
+ // Integration Pattern Issues
49
+ SharedKernelNotBidirectional: 'shared-kernel-not-bidirectional',
50
+ AclOnWrongSide: 'acl-on-wrong-side',
51
+ ConformistOnWrongSide: 'conformist-on-wrong-side',
52
+ TooManyPatterns: 'too-many-patterns',
53
+
54
+ // Context/Domain Map Issues
55
+ ContextMapNoContexts: 'context-map-no-contexts',
56
+ ContextMapNoRelationships: 'context-map-no-relationships',
57
+ DomainMapNoDomains: 'domain-map-no-domains',
58
+
59
+ // Metadata Issues
60
+ MetadataMissingName: 'metadata-missing-name',
61
+
62
+ // General Issues
63
+ DuplicateElement: 'duplicate-element'
64
+ } as const;
65
+
66
+ export type IssueCode = typeof IssueCodes[keyof typeof IssueCodes];
67
+
16
68
  // ============================================================================
17
69
  // Documentation Link Utilities
18
70
  // ============================================================================
19
71
 
20
- const REPO_BASE = 'https://github.com/larsbaunwall/DomainLang/blob/main';
72
+ const REPO_BASE = 'https://github.com/DomainLang/DomainLang/blob/main';
21
73
  const DOCS_BASE = `${REPO_BASE}/dsl/domain-lang/docs`;
22
74
 
23
75
  /**
@@ -49,6 +101,13 @@ export const ValidationMessages = {
49
101
  DOMAIN_NO_VISION: (name: string) =>
50
102
  `Domain '${name}' is missing a vision statement.`,
51
103
 
104
+ /**
105
+ * Error message when a circular domain hierarchy is detected.
106
+ * @param cycle - Array of domain names forming the cycle
107
+ */
108
+ DOMAIN_CIRCULAR_HIERARCHY: (cycle: string[]) =>
109
+ `Circular domain hierarchy detected: ${cycle.join(' → ')}.`,
110
+
52
111
  /**
53
112
  * Warning message when a bounded context lacks a description.
54
113
  * @param name - The name of the bounded context
@@ -120,5 +179,126 @@ export const ValidationMessages = {
120
179
  * Suggests possible syntax confusion.
121
180
  */
122
181
  TOO_MANY_PATTERNS: (count: number, side: 'left' | 'right') =>
123
- `Too many integration patterns (${count}) on ${side} side. Typically use 1-2 patterns per side.`
182
+ `Too many integration patterns (${count}) on ${side} side. Typically use 1-2 patterns per side.`,
183
+
184
+ // ========================================================================
185
+ // Import & Dependency Validation (PRS-010 Phase 8)
186
+ // ========================================================================
187
+
188
+ /**
189
+ * Error when import statement has no URI.
190
+ */
191
+ IMPORT_MISSING_URI: () =>
192
+ `Import statement must have a URI. Use: import "package" or import "./local-path"`,
193
+
194
+ /**
195
+ * Error when external dependency requires model.yaml but none exists.
196
+ * @param specifier - The import specifier (e.g., "core", "domainlang/patterns")
197
+ */
198
+ IMPORT_REQUIRES_MANIFEST: (specifier: string) =>
199
+ `External dependency '${specifier}' requires model.yaml.\n` +
200
+ `Hint: Create model.yaml and add the dependency:\n` +
201
+ ` dependencies:\n` +
202
+ ` ${specifier}:\n` +
203
+ ` ref: v1.0.0`,
204
+
205
+ /**
206
+ * Error when import specifier not found in manifest dependencies.
207
+ * @param alias - The dependency alias/specifier
208
+ */
209
+ IMPORT_NOT_IN_MANIFEST: (alias: string) =>
210
+ `Import '${alias}' not found in model.yaml dependencies.\n` +
211
+ `Hint: Run 'dlang add ${alias} <source>@<ref>' to add it, or manually add to model.yaml:\n` +
212
+ ` dependencies:\n` +
213
+ ` ${alias}:\n` +
214
+ ` ref: v1.0.0`,
215
+
216
+ /**
217
+ * Error when dependency not installed (no lock file entry).
218
+ * @param alias - The dependency alias
219
+ */
220
+ IMPORT_NOT_INSTALLED: (alias: string) =>
221
+ `Dependency '${alias}' not installed.\n` +
222
+ `Hint: Run 'dlang install' to fetch dependencies and generate model.lock.`,
223
+
224
+ /**
225
+ * Error when dependency has conflicting source and path definitions.
226
+ * @param alias - The dependency alias
227
+ */
228
+ IMPORT_CONFLICTING_SOURCE_PATH: (alias: string) =>
229
+ `Dependency '${alias}' cannot define both 'source' and 'path' in model.yaml.\n` +
230
+ `Hint: Use 'source' for git-based packages or 'path' for local workspace packages.`,
231
+
232
+ /**
233
+ * Error when dependency is missing both source and path.
234
+ * @param alias - The dependency alias
235
+ */
236
+ IMPORT_MISSING_SOURCE_OR_PATH: (alias: string) =>
237
+ `Dependency '${alias}' must define either 'source' or 'path' in model.yaml.\n` +
238
+ `Hint: Add 'source: owner/repo' for git packages, or 'path: ./local/path' for local packages.`,
239
+
240
+ /**
241
+ * Error when git dependency is missing ref (tag, branch, or commit).
242
+ * @param alias - The dependency alias
243
+ */
244
+ IMPORT_MISSING_REF: (alias: string) =>
245
+ `Dependency '${alias}' must specify a git ref in model.yaml.\n` +
246
+ `Hint: Add a git ref: 'ref: v1.0.0' (tag), 'ref: main' (branch), or a commit SHA.`,
247
+
248
+ /**
249
+ * Error when local path uses absolute path.
250
+ * @param alias - The dependency alias
251
+ * @param absolutePath - The absolute path that was specified
252
+ */
253
+ IMPORT_ABSOLUTE_PATH: (alias: string, absolutePath: string) =>
254
+ `Local path dependency '${alias}' cannot use absolute path '${absolutePath}'.\n` +
255
+ `Hint: Use a relative path from the workspace root, e.g., 'path: ./packages/shared'.`,
256
+
257
+ /**
258
+ * Error when local path escapes workspace boundary.
259
+ * @param alias - The dependency alias
260
+ */
261
+ IMPORT_ESCAPES_WORKSPACE: (alias: string) =>
262
+ `Local path dependency '${alias}' escapes workspace boundary.\n` +
263
+ `Hint: Local dependencies must be within the workspace. Consider moving the dependency or using a git-based source.`,
264
+
265
+ // ========================================================================
266
+ // Context Map & Domain Map Validation
267
+ // ========================================================================
268
+
269
+ /**
270
+ * Warning when context map contains no bounded contexts.
271
+ * @param name - The context map name
272
+ */
273
+ CONTEXT_MAP_NO_CONTEXTS: (name: string) =>
274
+ `Context Map '${name}' contains no bounded contexts.\n` +
275
+ `Hint: Use 'contains ContextA, ContextB' to specify which contexts are in the map.`,
276
+
277
+ /**
278
+ * Info when context map has multiple contexts but no relationships.
279
+ * @param name - The context map name
280
+ * @param count - Number of contexts
281
+ */
282
+ CONTEXT_MAP_NO_RELATIONSHIPS: (name: string, count: number) =>
283
+ `Context Map '${name}' contains ${count} contexts but no documented relationships.\n` +
284
+ `Hint: Add relationships to show how contexts integrate (e.g., '[OHS] A -> [CF] B').`,
285
+
286
+ /**
287
+ * Warning when domain map contains no domains.
288
+ * @param name - The domain map name
289
+ */
290
+ DOMAIN_MAP_NO_DOMAINS: (name: string) =>
291
+ `Domain Map '${name}' contains no domains.\n` +
292
+ `Hint: Use 'contains DomainA, DomainB' to specify which domains are in the map.`,
293
+
294
+ // ========================================================================
295
+ // Metadata Validation
296
+ // ========================================================================
297
+
298
+ /**
299
+ * Error when metadata is missing a name.
300
+ */
301
+ METADATA_MISSING_NAME: () =>
302
+ `Metadata must have a name.\n` +
303
+ `Hint: Define metadata with: Metadata MyMetadata { ... }`
124
304
  } as const;
@@ -21,4 +21,57 @@ function validateDomainHasVision(
21
21
  }
22
22
  }
23
23
 
24
- export const domainChecks = [validateDomainHasVision];
24
+ /**
25
+ * Validates that a domain hierarchy does not contain circular references.
26
+ *
27
+ * The `Domain A in B` syntax expresses subdomain containment.
28
+ * Circular containment (A in B, B in C, C in A) is semantically invalid
29
+ * because it violates the fundamental concept of domain decomposition.
30
+ *
31
+ * @param domain - The domain to validate
32
+ * @param accept - The validation acceptor for reporting issues
33
+ */
34
+ function validateNoCyclicDomainHierarchy(
35
+ domain: Domain,
36
+ accept: ValidationAcceptor
37
+ ): void {
38
+ // Only check if this domain has a parent
39
+ if (!domain.parent?.ref) {
40
+ return;
41
+ }
42
+
43
+ const visited = new Set<Domain>();
44
+ const path: string[] = [domain.name];
45
+ let current: Domain | undefined = domain.parent.ref;
46
+
47
+ while (current) {
48
+ // Check if we've encountered this domain before (cycle detected)
49
+ if (visited.has(current)) {
50
+ // We found a cycle - report it
51
+ path.push(current.name);
52
+ accept('error', ValidationMessages.DOMAIN_CIRCULAR_HIERARCHY(path), {
53
+ node: domain,
54
+ property: 'parent',
55
+ codeDescription: buildCodeDescription('language.md', 'domain-hierarchy')
56
+ });
57
+ return;
58
+ }
59
+
60
+ // Check if we've looped back to the starting domain
61
+ if (current === domain) {
62
+ path.push(domain.name);
63
+ accept('error', ValidationMessages.DOMAIN_CIRCULAR_HIERARCHY(path), {
64
+ node: domain,
65
+ property: 'parent',
66
+ codeDescription: buildCodeDescription('language.md', 'domain-hierarchy')
67
+ });
68
+ return;
69
+ }
70
+
71
+ visited.add(current);
72
+ path.push(current.name);
73
+ current = current.parent?.ref;
74
+ }
75
+ }
76
+
77
+ export const domainChecks = [validateDomainHasVision, validateNoCyclicDomainHierarchy];
@@ -1,171 +1,295 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
1
3
  import type { ValidationAcceptor, ValidationChecks } from 'langium';
2
- import type { DomainLangAstType, ImportStatement, Model } from '../generated/ast.js';
3
- import { resolveImportPath } from '../utils/import-utils.js';
4
+ import { Cancellation } from 'langium';
5
+ import type { DomainLangAstType, ImportStatement } from '../generated/ast.js';
4
6
  import type { DomainLangServices } from '../domain-lang-module.js';
5
7
  import type { LangiumDocument } from 'langium';
8
+ import type { WorkspaceManager } from '../services/workspace-manager.js';
9
+ import type { ExtendedDependencySpec, ModelManifest, LockFile } from '../services/types.js';
10
+ import { ValidationMessages, buildCodeDescription, IssueCodes } from './constants.js';
6
11
 
7
12
  /**
8
13
  * Validates import statements in DomainLang.
9
- *
14
+ *
15
+ * Uses async validators (Langium 4.x supports MaybePromise<void>) to leverage
16
+ * the shared WorkspaceManager service with its cached manifest/lock file reading.
17
+ *
10
18
  * Checks:
11
- * - Import paths are resolvable
12
- * - Named imports exist in target document
13
- * - Import aliases don't conflict with local names
19
+ * - External imports require manifest + alias
20
+ * - Local path dependencies stay inside workspace
21
+ * - Lock file exists for external dependencies
14
22
  */
15
23
  export class ImportValidator {
16
- private readonly documents: DomainLangServices['shared']['workspace']['LangiumDocuments'];
24
+ private readonly workspaceManager: WorkspaceManager;
17
25
 
18
26
  constructor(services: DomainLangServices) {
19
- this.documents = services.shared.workspace.LangiumDocuments;
27
+ this.workspaceManager = services.imports.WorkspaceManager;
20
28
  }
21
29
 
22
30
  /**
23
- * Validates that an import path is resolvable.
31
+ * Validates an import statement asynchronously.
32
+ *
33
+ * Langium validators can return MaybePromise<void>, enabling async operations
34
+ * like reading manifests via the shared, cached WorkspaceManager.
24
35
  */
25
36
  async checkImportPath(
26
37
  imp: ImportStatement,
27
38
  accept: ValidationAcceptor,
28
- document: LangiumDocument
39
+ document: LangiumDocument,
40
+ _cancelToken: Cancellation.CancellationToken
29
41
  ): Promise<void> {
30
42
  if (!imp.uri) {
31
- accept('error', 'Import statement must have a URI', {
43
+ accept('error', ValidationMessages.IMPORT_MISSING_URI(), {
32
44
  node: imp,
33
- keyword: 'import'
45
+ keyword: 'import',
46
+ codeDescription: buildCodeDescription('language.md', 'imports'),
47
+ data: { code: IssueCodes.ImportMissingUri }
34
48
  });
35
49
  return;
36
50
  }
37
51
 
38
- try {
39
- await resolveImportPath(document, imp.uri);
40
- } catch (error) {
41
- const message = error instanceof Error ? error.message : String(error);
42
- accept('error', `Cannot resolve import: ${message}`, {
52
+ if (!this.isExternalImport(imp.uri)) {
53
+ return;
54
+ }
55
+
56
+ // Initialize workspace manager from document location
57
+ const docDir = path.dirname(document.uri.fsPath);
58
+ await this.workspaceManager.initialize(docDir);
59
+
60
+ const manifest = await this.workspaceManager.getManifest();
61
+ if (!manifest) {
62
+ accept('error', ValidationMessages.IMPORT_REQUIRES_MANIFEST(imp.uri), {
43
63
  node: imp,
44
- property: 'uri'
64
+ property: 'uri',
65
+ codeDescription: buildCodeDescription('language.md', 'imports'),
66
+ data: { code: IssueCodes.ImportRequiresManifest, specifier: imp.uri }
45
67
  });
68
+ return;
46
69
  }
47
- }
48
70
 
49
- /**
50
- * Validates that named imports exist in the target document.
51
- */
52
- async checkNamedImports(
53
- imp: ImportStatement,
54
- accept: ValidationAcceptor,
55
- document: LangiumDocument
56
- ): Promise<void> {
57
- // Only check if we have named imports
58
- if (!imp.symbols || imp.symbols.length === 0) {
71
+ const alias = imp.uri.split('/')[0];
72
+ const dependency = this.getDependency(manifest, alias);
73
+
74
+ if (!dependency) {
75
+ accept('error', ValidationMessages.IMPORT_NOT_IN_MANIFEST(alias), {
76
+ node: imp,
77
+ property: 'uri',
78
+ codeDescription: buildCodeDescription('language.md', 'imports'),
79
+ data: { code: IssueCodes.ImportNotInManifest, alias }
80
+ });
59
81
  return;
60
82
  }
61
83
 
62
- if (!imp.uri) {
63
- return; // Already reported by checkImportPath
64
- }
84
+ this.validateDependencyConfig(dependency, alias, accept, imp);
65
85
 
66
- try {
67
- // Resolve the target document
68
- const targetUri = await resolveImportPath(document, imp.uri);
69
- const targetDoc = await this.documents.getOrCreateDocument(targetUri);
70
-
71
- if (!targetDoc.parseResult?.value) {
72
- accept('error', `Cannot load imported document: ${imp.uri}`, {
86
+ // External source dependencies require lock file and cached packages
87
+ if (dependency.source) {
88
+ const lockFile = await this.workspaceManager.getLockFile();
89
+ if (!lockFile) {
90
+ accept('error', ValidationMessages.IMPORT_NOT_INSTALLED(alias), {
73
91
  node: imp,
74
- property: 'uri'
92
+ property: 'uri',
93
+ codeDescription: buildCodeDescription('language.md', 'imports'),
94
+ data: { code: IssueCodes.ImportNotInstalled, alias }
75
95
  });
76
96
  return;
77
97
  }
78
98
 
79
- // Get all exported symbols from target document
80
- const targetModel = targetDoc.parseResult.value as Model;
81
- const exportedSymbols = this.getExportedSymbols(targetModel);
82
-
83
- // Check each imported symbol
84
- for (const symbol of imp.symbols) {
85
- if (!exportedSymbols.has(symbol)) {
86
- accept('error',
87
- `Symbol '${symbol}' not found in ${imp.uri}`,
88
- {
89
- node: imp,
90
- property: 'symbols'
91
- }
92
- );
93
- }
94
- }
95
- } catch {
96
- // Import path error already reported by checkImportPath
97
- return;
99
+ await this.validateCachedPackage(dependency, alias, lockFile, accept, imp);
98
100
  }
99
101
  }
100
102
 
101
103
  /**
102
- * Gets all exportable symbols from a model.
103
- *
104
- * In DomainLang, top-level declarations are implicitly exported:
105
- * - Domains
106
- * - BoundedContexts
107
- * - Classifications
108
- * - Groups
104
+ * Determines if an import URI is external (requires manifest).
105
+ *
106
+ * Per PRS-010:
107
+ * - Local relative: ./path, ../path
108
+ * - Path aliases: @/path, @alias/path (resolved via manifest paths section)
109
+ * - External: owner/package (requires manifest dependencies)
109
110
  */
110
- private getExportedSymbols(model: Model): Set<string> {
111
- const symbols = new Set<string>();
112
-
113
- // Iterate through all structure elements
114
- for (const element of model.children ?? []) {
115
- // Check if element has a name and add it
116
- if ('name' in element && typeof element.name === 'string') {
117
- symbols.add(element.name);
118
- }
111
+ private isExternalImport(uri: string): boolean {
112
+ if (uri.startsWith('./') || uri.startsWith('../')) {
113
+ return false;
114
+ }
115
+ if (uri.startsWith('@')) {
116
+ return false;
117
+ }
118
+ return true;
119
+ }
120
+
121
+ /**
122
+ * Gets the normalized dependency configuration for an alias.
123
+ */
124
+ private getDependency(manifest: ModelManifest, alias: string): ExtendedDependencySpec | undefined {
125
+ const dep = manifest.dependencies?.[alias];
126
+ if (!dep) {
127
+ return undefined;
128
+ }
129
+
130
+ if (typeof dep === 'string') {
131
+ return { source: alias, ref: dep };
132
+ }
133
+
134
+ if (!dep.source && !dep.path) {
135
+ return { ...dep, source: alias };
119
136
  }
120
137
 
121
- return symbols;
138
+ return dep;
122
139
  }
123
140
 
124
141
  /**
125
- * Checks for unused imports.
126
- *
127
- * This is a warning, not an error, to avoid being too strict.
142
+ * Validates dependency configuration.
128
143
  */
129
- checkUnusedImports(
130
- imp: ImportStatement,
131
- _accept: ValidationAcceptor,
132
- _model: Model
144
+ private validateDependencyConfig(
145
+ dependency: ExtendedDependencySpec,
146
+ alias: string,
147
+ accept: ValidationAcceptor,
148
+ imp: ImportStatement
149
+ ): void {
150
+ if (dependency.source && dependency.path) {
151
+ accept('error', ValidationMessages.IMPORT_CONFLICTING_SOURCE_PATH(alias), {
152
+ node: imp,
153
+ property: 'uri',
154
+ codeDescription: buildCodeDescription('language.md', 'imports'),
155
+ data: { code: IssueCodes.ImportConflictingSourcePath, alias }
156
+ });
157
+ return;
158
+ }
159
+
160
+ if (!dependency.source && !dependency.path) {
161
+ accept('error', ValidationMessages.IMPORT_MISSING_SOURCE_OR_PATH(alias), {
162
+ node: imp,
163
+ property: 'uri',
164
+ codeDescription: buildCodeDescription('language.md', 'imports'),
165
+ data: { code: IssueCodes.ImportMissingSourceOrPath, alias }
166
+ });
167
+ return;
168
+ }
169
+
170
+ if (dependency.source && !dependency.ref) {
171
+ accept('error', ValidationMessages.IMPORT_MISSING_REF(alias), {
172
+ node: imp,
173
+ property: 'uri',
174
+ codeDescription: buildCodeDescription('language.md', 'imports'),
175
+ data: { code: IssueCodes.ImportMissingRef, alias }
176
+ });
177
+ }
178
+
179
+ if (dependency.path) {
180
+ this.validateLocalPathDependency(dependency.path, alias, accept, imp);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Validates local path dependencies stay within workspace.
186
+ */
187
+ private validateLocalPathDependency(
188
+ dependencyPath: string,
189
+ alias: string,
190
+ accept: ValidationAcceptor,
191
+ imp: ImportStatement
133
192
  ): void {
134
- // Skip check for wildcard imports (no named imports)
135
- if (!imp.symbols || imp.symbols.length === 0) {
193
+ if (path.isAbsolute(dependencyPath)) {
194
+ accept('error', ValidationMessages.IMPORT_ABSOLUTE_PATH(alias, dependencyPath), {
195
+ node: imp,
196
+ property: 'uri',
197
+ codeDescription: buildCodeDescription('language.md', 'imports'),
198
+ data: { code: IssueCodes.ImportAbsolutePath, alias, path: dependencyPath }
199
+ });
200
+ return;
201
+ }
202
+
203
+ const workspaceRoot = this.workspaceManager.getWorkspaceRoot();
204
+ const resolvedPath = path.resolve(workspaceRoot, dependencyPath);
205
+ const relativeToWorkspace = path.relative(workspaceRoot, resolvedPath);
206
+
207
+ if (relativeToWorkspace.startsWith('..') || path.isAbsolute(relativeToWorkspace)) {
208
+ accept('error', ValidationMessages.IMPORT_ESCAPES_WORKSPACE(alias), {
209
+ node: imp,
210
+ property: 'uri',
211
+ codeDescription: buildCodeDescription('language.md', 'imports'),
212
+ data: { code: IssueCodes.ImportEscapesWorkspace, alias }
213
+ });
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Validates that external dependency is in lock file and cached.
219
+ */
220
+ private async validateCachedPackage(
221
+ dependency: ExtendedDependencySpec,
222
+ alias: string,
223
+ lockFile: LockFile,
224
+ accept: ValidationAcceptor,
225
+ imp: ImportStatement
226
+ ): Promise<void> {
227
+ // Source is guaranteed to exist when this method is called (see caller)
228
+ const packageKey = dependency.source ?? alias;
229
+ const lockedDep = lockFile.dependencies[packageKey];
230
+
231
+ if (!lockedDep) {
232
+ accept('error', ValidationMessages.IMPORT_NOT_INSTALLED(alias), {
233
+ node: imp,
234
+ property: 'uri',
235
+ codeDescription: buildCodeDescription('language.md', 'imports'),
236
+ data: { code: IssueCodes.ImportNotInstalled, alias }
237
+ });
136
238
  return;
137
239
  }
138
240
 
139
- // For now, just a placeholder - would require tracking symbol usage
140
- // across the entire document, which is complex
141
- // TODO: Implement symbol usage tracking
241
+ const workspaceRoot = this.workspaceManager.getWorkspaceRoot();
242
+ const cacheDir = this.getCacheDirectory(workspaceRoot, packageKey, lockedDep.commit);
243
+
244
+ const cacheExists = await this.directoryExists(cacheDir);
245
+ if (!cacheExists) {
246
+ accept('error', ValidationMessages.IMPORT_NOT_INSTALLED(alias), {
247
+ node: imp,
248
+ property: 'uri',
249
+ codeDescription: buildCodeDescription('language.md', 'imports'),
250
+ data: { code: IssueCodes.ImportNotInstalled, alias }
251
+ });
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Gets the cache directory for a dependency.
257
+ * Per PRS-010: Project-local cache at .dlang/packages/{owner}/{repo}/{commit}/
258
+ */
259
+ private getCacheDirectory(workspaceRoot: string, source: string, commitHash: string): string {
260
+ const [owner, repo] = source.split('/');
261
+ return path.join(workspaceRoot, '.dlang', 'packages', owner, repo, commitHash);
262
+ }
263
+
264
+ /**
265
+ * Checks if a directory exists (async).
266
+ */
267
+ private async directoryExists(dirPath: string): Promise<boolean> {
268
+ try {
269
+ const stat = await fs.stat(dirPath);
270
+ return stat.isDirectory();
271
+ } catch {
272
+ return false;
273
+ }
142
274
  }
143
275
  }
144
276
 
145
277
  /**
146
278
  * Creates validation checks for import statements.
279
+ *
280
+ * Returns async validators that leverage the shared WorkspaceManager
281
+ * for cached manifest/lock file reading.
147
282
  */
148
- export function createImportChecks(_services: DomainLangServices): ValidationChecks<DomainLangAstType> {
283
+ export function createImportChecks(services: DomainLangServices): ValidationChecks<DomainLangAstType> {
284
+ const validator = new ImportValidator(services);
149
285
 
150
286
  return {
151
- ImportStatement: (imp, accept) => {
287
+ // Langium 4.x supports async validators via MaybePromise<void>
288
+ ImportStatement: async (imp, accept, cancelToken) => {
152
289
  const document = imp.$document;
153
290
  if (!document) return;
154
291
 
155
- // Note: Langium's validation is synchronous, so async checks won't
156
- // execute during document validation. These checks will run during
157
- // the build phase when documents are fully loaded.
158
-
159
- // For now, just do basic syntax validation
160
- if (!imp.uri) {
161
- accept('error', 'Import statement must have a URI', {
162
- node: imp,
163
- keyword: 'import'
164
- });
165
- }
166
-
167
- // TODO: Implement async validation in a separate build phase
168
- // This would require using DocumentBuilder.onBuildPhase() or similar
292
+ await validator.checkImportPath(imp, accept, document, cancelToken);
169
293
  }
170
294
  };
171
295
  }