@oml/language 0.12.0 → 0.14.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 (41) hide show
  1. package/out/oml/index.d.ts +3 -1
  2. package/out/oml/index.js +19 -1
  3. package/out/oml/index.js.map +1 -1
  4. package/out/oml/oml-candidates.d.ts +3 -3
  5. package/out/oml/oml-candidates.js +1 -1
  6. package/out/oml/oml-candidates.js.map +1 -1
  7. package/out/oml/oml-diagram.d.ts +17 -0
  8. package/out/oml/oml-diagram.js +1549 -0
  9. package/out/oml/oml-diagram.js.map +1 -0
  10. package/out/oml/oml-document.d.ts +5 -0
  11. package/out/oml/oml-document.js +12 -1
  12. package/out/oml/oml-document.js.map +1 -1
  13. package/out/oml/oml-index.d.ts +2 -1
  14. package/out/oml/oml-index.js +90 -9
  15. package/out/oml/oml-index.js.map +1 -1
  16. package/out/oml/oml-scope.js +4 -3
  17. package/out/oml/oml-scope.js.map +1 -1
  18. package/out/oml/oml-search.d.ts +24 -0
  19. package/out/oml/oml-search.js +95 -0
  20. package/out/oml/oml-search.js.map +1 -0
  21. package/out/oml/{oml-edit.d.ts → oml-update.d.ts} +2 -0
  22. package/out/oml/{oml-edit.js → oml-update.js} +270 -67
  23. package/out/oml/oml-update.js.map +1 -0
  24. package/out/oml/oml-utils.d.ts +1 -1
  25. package/out/oml/oml-utils.js +3 -4
  26. package/out/oml/oml-utils.js.map +1 -1
  27. package/out/oml/oml-validator.d.ts +1 -0
  28. package/out/oml/oml-validator.js +18 -3
  29. package/out/oml/oml-validator.js.map +1 -1
  30. package/package.json +4 -2
  31. package/src/oml/index.ts +32 -1
  32. package/src/oml/oml-candidates.ts +4 -4
  33. package/src/oml/oml-diagram.ts +1708 -0
  34. package/src/oml/oml-document.ts +13 -1
  35. package/src/oml/oml-index.ts +87 -9
  36. package/src/oml/oml-scope.ts +4 -3
  37. package/src/oml/oml-search.ts +132 -0
  38. package/src/oml/{oml-edit.ts → oml-update.ts} +317 -66
  39. package/src/oml/oml-utils.ts +3 -4
  40. package/src/oml/oml-validator.ts +18 -3
  41. package/out/oml/oml-edit.js.map +0 -1
@@ -32,10 +32,11 @@ type ConnectionLike = {
32
32
  };
33
33
  };
34
34
 
35
- const OmlEditRequestType = new RequestType<OmlEditRequest, OmlEditResponse, void>('oml/edit');
35
+ const OmlEditRequestType = new RequestType<OmlEditRequest, OmlEditResponse, void>('oml/update');
36
36
  const RDF_TYPE_IRI = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
37
37
  const OML_HAS_SOURCE_IRI = 'http://opencaesar.io/oml#hasSource';
38
38
  const OML_HAS_TARGET_IRI = 'http://opencaesar.io/oml#hasTarget';
39
+ const XSD_STRING_IRI = 'http://www.w3.org/2001/XMLSchema#string';
39
40
 
40
41
  type OmlEditOperation =
41
42
  | { kind: 'createInstance'; ontologyIri: string; instanceName: string }
@@ -53,6 +54,7 @@ type OmlEditOperation =
53
54
 
54
55
  export type OmlEditRequest = {
55
56
  operations: OmlEditOperation[];
57
+ referencingUri?: string;
56
58
  };
57
59
 
58
60
  export type OmlEditError = {
@@ -65,36 +67,47 @@ export type OmlEditResponse = {
65
67
  errors?: OmlEditError[];
66
68
  };
67
69
 
68
- export function registerOmlEditRequests(connection: ConnectionLike, shared: any): void {
69
- connection.onRequest(OmlEditRequestType, async (params: OmlEditRequest): Promise<OmlEditResponse> => {
70
- const operations = Array.isArray(params?.operations) ? params.operations : [];
71
- const contexts = new Map<string, OntologyContext>();
72
- const changedOntologyIris = new Set<string>();
73
- for (let i = 0; i < operations.length; i += 1) {
74
- const operation = operations[i];
75
- try {
76
- const context = await resolveOntologyContext(shared, operation.ontologyIri, contexts);
77
- const changed = await executeOperation(shared, context, operation, contexts);
78
- for (const ontologyIri of changed) {
79
- changedOntologyIris.add(ontologyIri);
80
- }
81
- } catch (error) {
82
- await restoreContextsFromDocuments(shared, contexts);
83
- const message = error instanceof Error ? error.message : String(error);
84
- const details = error instanceof Error && error.stack
85
- ? `${message}\n${error.stack}`
86
- : message;
87
- connection.console?.error(`[oml] OmlEditRequest failed at operation ${i} (${operation.kind}): ${details}`);
88
- return {
89
- errors: [{
90
- operationIndex: i,
91
- message
92
- }]
93
- };
70
+ export async function applyOmlUpdate(
71
+ shared: any,
72
+ params: OmlEditRequest,
73
+ logError?: (message: string) => void,
74
+ ): Promise<OmlEditResponse> {
75
+ const operations = Array.isArray(params?.operations) ? params.operations : [];
76
+ const referencingUri = typeof params?.referencingUri === 'string'
77
+ ? params.referencingUri.trim()
78
+ : undefined;
79
+ const contexts = new Map<string, OntologyContext>();
80
+ const changedOntologyIris = new Set<string>();
81
+ for (let i = 0; i < operations.length; i += 1) {
82
+ const operation = operations[i];
83
+ try {
84
+ const context = await resolveOntologyContext(shared, operation.ontologyIri, contexts, referencingUri);
85
+ const changed = await executeOperation(shared, context, operation, contexts);
86
+ for (const ontologyIri of changed) {
87
+ changedOntologyIris.add(ontologyIri);
94
88
  }
89
+ } catch (error) {
90
+ await restoreContextsFromDocuments(shared, contexts);
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ const details = error instanceof Error && error.stack
93
+ ? `${message}\n${error.stack}`
94
+ : message;
95
+ logError?.(`[oml] OmlEditRequest failed at operation ${i} (${operation.kind}): ${details}`);
96
+ return {
97
+ errors: [{
98
+ operationIndex: i,
99
+ message
100
+ }]
101
+ };
95
102
  }
96
- const edit = buildWorkspaceEdit(contexts, changedOntologyIris);
97
- return edit ? { edit } : {};
103
+ }
104
+ const edit = buildWorkspaceEdit(contexts, changedOntologyIris);
105
+ return edit ? { edit } : {};
106
+ }
107
+
108
+ export function registerOmlEditRequests(connection: ConnectionLike, shared: any): void {
109
+ connection.onRequest(OmlEditRequestType, async (params: OmlEditRequest): Promise<OmlEditResponse> => {
110
+ return await applyOmlUpdate(shared, params, (message) => connection.console?.error(message));
98
111
  });
99
112
  }
100
113
 
@@ -105,14 +118,19 @@ type OntologyContext = {
105
118
  document: any;
106
119
  };
107
120
 
108
- async function resolveOntologyContext(shared: any, ontologyIri: string, cache: Map<string, OntologyContext>): Promise<OntologyContext> {
121
+ async function resolveOntologyContext(
122
+ shared: any,
123
+ ontologyIri: string,
124
+ cache: Map<string, OntologyContext>,
125
+ referencingUri?: string
126
+ ): Promise<OntologyContext> {
109
127
  const normalizedOntologyIri = normalizeIri(ontologyIri);
110
128
  const cached = cache.get(normalizedOntologyIri);
111
129
  if (cached) {
112
130
  return cached;
113
131
  }
114
132
  const index = getOntologyModelIndex(shared);
115
- const modelUri = index.resolveModelUri(normalizedOntologyIri);
133
+ const modelUri = index.resolveModelUri(normalizedOntologyIri, referencingUri);
116
134
  if (!modelUri) {
117
135
  throw new Error(`Unable to resolve ontology '${ontologyIri}'.`);
118
136
  }
@@ -121,12 +139,7 @@ async function resolveOntologyContext(shared: any, ontologyIri: string, cache: M
121
139
  const builder: any = shared.workspace.DocumentBuilder;
122
140
  const document = langiumDocuments.getDocument(uri)
123
141
  ?? await langiumDocuments.getOrCreateDocument(uri);
124
- // Only build if not yet Linked. Forcing validation:true when the workspace
125
- // is already Validated causes a document-state conflict in Langium on Windows.
126
- const docState: number | undefined = document?.state;
127
- if (docState === undefined || docState < DocumentState.Linked) {
128
- await builder.build([document], { validation: false });
129
- }
142
+ await ensureDocumentBuiltForEdit(builder, document);
130
143
  const ontology = document?.parseResult?.value;
131
144
  if (!ontology || !isOntology(ontology)) {
132
145
  throw new Error(`Resolved document '${modelUri}' is not an ontology.`);
@@ -290,10 +303,7 @@ async function resolveOntologyContextByModelUri(
290
303
  const builder: any = shared.workspace.DocumentBuilder;
291
304
  const document = langiumDocuments.getDocument(uri)
292
305
  ?? await langiumDocuments.getOrCreateDocument(uri);
293
- const docState2: number | undefined = document?.state;
294
- if (docState2 === undefined || docState2 < DocumentState.Linked) {
295
- await builder.build([document], { validation: false });
296
- }
306
+ await ensureDocumentBuiltForEdit(builder, document);
297
307
  const ontology = document?.parseResult?.value;
298
308
  if (!ontology || !isOntology(ontology)) {
299
309
  throw new Error(`Resolved document '${modelUri}' is not an ontology.`);
@@ -365,7 +375,9 @@ async function addAssertion(shared: any, ontology: any, subjectIri: string, pred
365
375
  }
366
376
 
367
377
  subject.ownedPropertyValues ??= [];
368
- let propertyAssertion = subject.ownedPropertyValues.find((assertion: any) => matchesPredicateRef(ontology, assertion?.property, normalizedPredicate));
378
+ const matchingAssertions = subject.ownedPropertyValues
379
+ .filter((assertion: any) => matchesPredicateRef(ontology, assertion?.property, normalizedPredicate));
380
+ let propertyAssertion = matchingAssertions[0];
369
381
  if (!propertyAssertion) {
370
382
  await ensureReferenceImport(shared, ontology, normalizedPredicate);
371
383
  propertyAssertion = {
@@ -376,6 +388,8 @@ async function addAssertion(shared: any, ontology: any, subjectIri: string, pred
376
388
  containedValues: []
377
389
  };
378
390
  pushAstArrayChild(subject, 'ownedPropertyValues', propertyAssertion);
391
+ } else if (matchingAssertions.length > 1) {
392
+ normalizePropertyAssertionGroup(subject, propertyAssertion, matchingAssertions.slice(1));
379
393
  }
380
394
  if (isIriLike(objectValue)) {
381
395
  await ensureReferenceImport(shared, ontology, normalizeIri(asRequiredIri(objectValue, 'assertion object')));
@@ -383,6 +397,7 @@ async function addAssertion(shared: any, ontology: any, subjectIri: string, pred
383
397
  await ensureTypedLiteralDatatypeImport(shared, ontology, objectValue);
384
398
  }
385
399
  appendAssertionValue(ontology, propertyAssertion, objectValue);
400
+ normalizeAllPropertyAssertions(subject, ontology);
386
401
  return true;
387
402
  }
388
403
 
@@ -515,19 +530,27 @@ async function addAnnotation(shared: any, ontology: any, subjectIri: string, pre
515
530
  const normalizedPredicate = normalizeIri(predicateIri);
516
531
  await ensureReferenceImport(shared, ontology, normalizedPredicate);
517
532
  subject.ownedAnnotations ??= [];
518
- const annotation: any = {
519
- $type: 'Annotation',
520
- property: asRef(ontology, normalizedPredicate),
521
- literalValues: [],
522
- referencedValues: []
523
- };
533
+ const matchingAnnotations = subject.ownedAnnotations
534
+ .filter((entry: any) => matchesPredicateRef(ontology, entry?.property, normalizedPredicate));
535
+ let annotation = matchingAnnotations[0];
536
+ if (!annotation) {
537
+ annotation = {
538
+ $type: 'Annotation',
539
+ property: asRef(ontology, normalizedPredicate),
540
+ literalValues: [],
541
+ referencedValues: []
542
+ };
543
+ pushAstArrayChild(subject, 'ownedAnnotations', annotation);
544
+ } else if (matchingAnnotations.length > 1) {
545
+ normalizeAnnotationGroup(subject, annotation, matchingAnnotations.slice(1));
546
+ }
524
547
  if (isIriLike(objectValue)) {
525
548
  await ensureReferenceImport(shared, ontology, normalizeIri(asRequiredIri(objectValue, 'annotation object')));
526
549
  } else {
527
550
  await ensureTypedLiteralDatatypeImport(shared, ontology, objectValue);
528
551
  }
529
552
  appendAnnotationValue(ontology, annotation, objectValue);
530
- pushAstArrayChild(subject, 'ownedAnnotations', annotation);
553
+ normalizeAllAnnotations(subject, ontology);
531
554
  return true;
532
555
  }
533
556
 
@@ -936,7 +959,7 @@ function removeAssertionValue(ontology: any, assertion: any, objectValue: unknow
936
959
  assertion.referencedValues = (assertion.referencedValues ?? []).filter((ref: any) => !matchesPredicateRef(ontology, ref, normalizedIri));
937
960
  return;
938
961
  }
939
- const expectedLiteral = asLiteral(ontology, objectValue);
962
+ const expectedLiteral = toLiteralRemovalMatchSpec(ontology, objectValue);
940
963
  assertion.literalValues = (assertion.literalValues ?? []).filter((literal: any) => !matchesLiteralForRemoval(ontology, literal, expectedLiteral));
941
964
  }
942
965
 
@@ -957,7 +980,7 @@ function removeAnnotationValue(ontology: any, annotation: any, objectValue: unkn
957
980
  annotation.referencedValues = (annotation.referencedValues ?? []).filter((ref: any) => !matchesPredicateRef(ontology, ref, normalizedIri));
958
981
  return;
959
982
  }
960
- const expectedLiteral = asLiteral(ontology, objectValue);
983
+ const expectedLiteral = toLiteralRemovalMatchSpec(ontology, objectValue);
961
984
  annotation.literalValues = (annotation.literalValues ?? []).filter((literal: any) => !matchesLiteralForRemoval(ontology, literal, expectedLiteral));
962
985
  }
963
986
 
@@ -976,15 +999,44 @@ function literalTypeIri(ontology: any, literal: any): string {
976
999
  return normalizeIri(expandRefTextToIri(ontology, refText) ?? refText);
977
1000
  }
978
1001
 
979
- function matchesLiteralForRemoval(ontology: any, existingLiteral: any, expectedLiteral: any): boolean {
980
- if (lexicalLiteralValue(existingLiteral) !== lexicalLiteralValue(expectedLiteral)) {
1002
+ type LiteralRemovalMatchSpec = {
1003
+ lexical: string;
1004
+ languageTag?: string;
1005
+ typeIri?: string;
1006
+ };
1007
+
1008
+ function toLiteralRemovalMatchSpec(ontology: any, objectValue: unknown): LiteralRemovalMatchSpec {
1009
+ if (isTypedQuotedLiteralTransport(objectValue)) {
1010
+ return {
1011
+ lexical: String(objectValue.value ?? ''),
1012
+ languageTag: literalLanguageTagFromTransport(objectValue),
1013
+ typeIri: literalDatatypeIriFromTransport(ontology, objectValue),
1014
+ };
1015
+ }
1016
+ const expectedLiteral = asLiteral(ontology, objectValue);
1017
+ return {
1018
+ lexical: lexicalLiteralValue(expectedLiteral),
1019
+ languageTag: literalLanguageTag(expectedLiteral),
1020
+ typeIri: literalTypeIri(ontology, expectedLiteral),
1021
+ };
1022
+ }
1023
+
1024
+ function matchesLiteralForRemoval(ontology: any, existingLiteral: any, expected: LiteralRemovalMatchSpec): boolean {
1025
+ if (lexicalLiteralValue(existingLiteral) !== expected.lexical) {
1026
+ return false;
1027
+ }
1028
+ const existingLanguage = literalLanguageTag(existingLiteral);
1029
+ if (existingLanguage && expected.languageTag && normalizeLanguageTag(existingLanguage) !== normalizeLanguageTag(expected.languageTag)) {
981
1030
  return false;
982
1031
  }
983
- const expectedType = literalTypeIri(ontology, expectedLiteral);
1032
+ const expectedType = expected.typeIri ?? '';
984
1033
  if (!expectedType) {
985
1034
  return true;
986
1035
  }
987
1036
  const existingType = literalTypeIri(ontology, existingLiteral);
1037
+ if (!existingType) {
1038
+ return true;
1039
+ }
988
1040
  return isSameIriTarget(existingType, expectedType);
989
1041
  }
990
1042
 
@@ -1175,7 +1227,7 @@ async function findOntologyByNamespace(shared: any, namespace: string): Promise<
1175
1227
  const builder: any = shared.workspace.DocumentBuilder;
1176
1228
  const document = langiumDocuments.getDocument(uri)
1177
1229
  ?? await langiumDocuments.getOrCreateDocument(uri);
1178
- await builder.build([document], { validation: false });
1230
+ await ensureDocumentBuiltForEdit(builder, document);
1179
1231
  const ontology = document?.parseResult?.value;
1180
1232
  return ontology && isOntology(ontology) ? ontology : undefined;
1181
1233
  }
@@ -1332,13 +1384,19 @@ function pickImportPrefix(namespace: string, usedPrefixes: ReadonlySet<string>):
1332
1384
  function asLiteral(ontology: any, value: unknown): any {
1333
1385
  if (isTypedQuotedLiteralTransport(value)) {
1334
1386
  const literalValue = String(value.value ?? '');
1335
- const datatypeIri = typeof value.datatypeIri === 'string' ? value.datatypeIri.trim() : '';
1336
- const datatypeRefText = typeof value.datatypeRefText === 'string' ? value.datatypeRefText.trim() : '';
1337
- return datatypeIri
1338
- ? { $type: 'QuotedLiteral', value: literalValue, type: { $refText: toRefText(ontology, normalizeIri(datatypeIri)) } }
1339
- : datatypeRefText
1340
- ? { $type: 'QuotedLiteral', value: literalValue, type: { $refText: datatypeRefText } }
1341
- : { $type: 'QuotedLiteral', value: literalValue };
1387
+ const langTag = literalLanguageTagFromTransport(value);
1388
+ const datatypeIri = literalDatatypeIriFromTransport(ontology, value);
1389
+ const literal: Record<string, unknown> = {
1390
+ $type: 'QuotedLiteral',
1391
+ value: literalValue,
1392
+ };
1393
+ if (langTag) {
1394
+ literal.langTag = langTag;
1395
+ }
1396
+ if (datatypeIri && !isXsdStringDatatypeIri(ontology, datatypeIri)) {
1397
+ literal.type = { $refText: toRefText(ontology, datatypeIri) };
1398
+ }
1399
+ return literal;
1342
1400
  }
1343
1401
  if (typeof value === 'boolean') {
1344
1402
  return { $type: 'BooleanLiteral', value };
@@ -1356,14 +1414,24 @@ async function ensureTypedLiteralDatatypeImport(shared: any, ontology: any, valu
1356
1414
  if (!isTypedQuotedLiteralTransport(value)) {
1357
1415
  return;
1358
1416
  }
1359
- const datatypeIri = typeof value.datatypeIri === 'string' ? normalizeIri(value.datatypeIri) : '';
1360
- if (!datatypeIri) {
1417
+ const datatypeIri = literalDatatypeIriFromTransport(ontology, value);
1418
+ if (!datatypeIri || isXsdStringDatatypeIri(ontology, datatypeIri)) {
1361
1419
  return;
1362
1420
  }
1363
1421
  await ensureReferenceImport(shared, ontology, datatypeIri);
1364
1422
  }
1365
1423
 
1366
- function isTypedQuotedLiteralTransport(value: unknown): value is { $type?: unknown; value?: unknown; datatypeIri?: unknown; datatypeRefText?: unknown } {
1424
+ function isTypedQuotedLiteralTransport(value: unknown): value is {
1425
+ $type?: unknown;
1426
+ value?: unknown;
1427
+ langTag?: unknown;
1428
+ language?: unknown;
1429
+ datatypeIri?: unknown;
1430
+ datatypeRefText?: unknown;
1431
+ typeIri?: unknown;
1432
+ typeRefText?: unknown;
1433
+ type?: unknown;
1434
+ } {
1367
1435
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
1368
1436
  return false;
1369
1437
  }
@@ -1371,7 +1439,71 @@ function isTypedQuotedLiteralTransport(value: unknown): value is { $type?: unkno
1371
1439
  if (record.$type === 'QuotedLiteral') {
1372
1440
  return true;
1373
1441
  }
1374
- return (typeof record.datatypeIri === 'string' || typeof record.datatypeRefText === 'string') && 'value' in record;
1442
+ return (
1443
+ typeof record.datatypeIri === 'string'
1444
+ || typeof record.datatypeRefText === 'string'
1445
+ || typeof record.typeIri === 'string'
1446
+ || typeof record.typeRefText === 'string'
1447
+ || typeof record.type === 'string'
1448
+ ) && 'value' in record;
1449
+ }
1450
+
1451
+ function literalLanguageTag(literal: any): string {
1452
+ if (!literal || typeof literal !== 'object') {
1453
+ return '';
1454
+ }
1455
+ return typeof literal.langTag === 'string' ? literal.langTag.trim() : '';
1456
+ }
1457
+
1458
+ function literalLanguageTagFromTransport(value: {
1459
+ langTag?: unknown;
1460
+ language?: unknown;
1461
+ }): string {
1462
+ if (typeof value.langTag === 'string') {
1463
+ return value.langTag.trim();
1464
+ }
1465
+ if (typeof value.language === 'string') {
1466
+ return value.language.trim();
1467
+ }
1468
+ return '';
1469
+ }
1470
+
1471
+ function normalizeLanguageTag(value: string): string {
1472
+ return value.trim().toLowerCase();
1473
+ }
1474
+
1475
+ function literalDatatypeIriFromTransport(ontology: any, value: {
1476
+ datatypeIri?: unknown;
1477
+ datatypeRefText?: unknown;
1478
+ typeIri?: unknown;
1479
+ typeRefText?: unknown;
1480
+ type?: unknown;
1481
+ }): string {
1482
+ const directIri = typeof value.datatypeIri === 'string'
1483
+ ? value.datatypeIri.trim()
1484
+ : typeof value.typeIri === 'string'
1485
+ ? value.typeIri.trim()
1486
+ : '';
1487
+ if (directIri) {
1488
+ const expanded = expandRefTextToIri(ontology, directIri);
1489
+ return normalizeIri(expanded ?? directIri);
1490
+ }
1491
+ const refText = typeof value.datatypeRefText === 'string'
1492
+ ? value.datatypeRefText.trim()
1493
+ : typeof value.typeRefText === 'string'
1494
+ ? value.typeRefText.trim()
1495
+ : typeof value.type === 'string'
1496
+ ? value.type.trim()
1497
+ : '';
1498
+ if (!refText) {
1499
+ return '';
1500
+ }
1501
+ return normalizeIri(expandRefTextToIri(ontology, refText) ?? refText);
1502
+ }
1503
+
1504
+ function isXsdStringDatatypeIri(ontology: any, value: string): boolean {
1505
+ const normalized = normalizeIri(expandRefTextToIri(ontology, value) ?? value);
1506
+ return normalized === XSD_STRING_IRI;
1375
1507
  }
1376
1508
 
1377
1509
  function attachAstNode<T extends Record<string, any>>(node: T, parent: any, containerProperty: string): T {
@@ -1413,6 +1545,113 @@ function normalizeIri(value: string): string {
1413
1545
  return String(value ?? '').trim().replace(/^<|>$/g, '');
1414
1546
  }
1415
1547
 
1548
+ function normalizePropertyAssertionGroup(subject: any, target: any, duplicates: any[]): void {
1549
+ if (!Array.isArray(subject?.ownedPropertyValues) || !target || duplicates.length === 0) {
1550
+ return;
1551
+ }
1552
+ target.literalValues ??= [];
1553
+ target.referencedValues ??= [];
1554
+ target.containedValues ??= [];
1555
+ for (const duplicate of duplicates) {
1556
+ for (const literal of duplicate?.literalValues ?? []) {
1557
+ attachAstNode(literal, target, 'literalValues');
1558
+ target.literalValues.push(literal);
1559
+ }
1560
+ for (const ref of duplicate?.referencedValues ?? []) {
1561
+ target.referencedValues.push(ref);
1562
+ }
1563
+ for (const contained of duplicate?.containedValues ?? []) {
1564
+ attachAstNode(contained, target, 'containedValues');
1565
+ target.containedValues.push(contained);
1566
+ }
1567
+ }
1568
+ const duplicateSet = new Set(duplicates);
1569
+ subject.ownedPropertyValues = subject.ownedPropertyValues.filter((entry: any) => !duplicateSet.has(entry));
1570
+ }
1571
+
1572
+ function normalizeAnnotationGroup(subject: any, target: any, duplicates: any[]): void {
1573
+ if (!Array.isArray(subject?.ownedAnnotations) || !target || duplicates.length === 0) {
1574
+ return;
1575
+ }
1576
+ target.literalValues ??= [];
1577
+ target.referencedValues ??= [];
1578
+ for (const duplicate of duplicates) {
1579
+ for (const literal of duplicate?.literalValues ?? []) {
1580
+ attachAstNode(literal, target, 'literalValues');
1581
+ target.literalValues.push(literal);
1582
+ }
1583
+ for (const ref of duplicate?.referencedValues ?? []) {
1584
+ target.referencedValues.push(ref);
1585
+ }
1586
+ }
1587
+ const duplicateSet = new Set(duplicates);
1588
+ subject.ownedAnnotations = subject.ownedAnnotations.filter((entry: any) => !duplicateSet.has(entry));
1589
+ }
1590
+
1591
+ function normalizeAllPropertyAssertions(subject: any, ontology: any): void {
1592
+ if (!Array.isArray(subject?.ownedPropertyValues) || subject.ownedPropertyValues.length < 2) {
1593
+ return;
1594
+ }
1595
+ const groups = new Map<string, { target: any; duplicates: any[] }>();
1596
+ for (const assertion of subject.ownedPropertyValues) {
1597
+ const key = propertyRefMergeKey(ontology, assertion?.property);
1598
+ if (!key) {
1599
+ continue;
1600
+ }
1601
+ const group = groups.get(key);
1602
+ if (!group) {
1603
+ groups.set(key, { target: assertion, duplicates: [] });
1604
+ continue;
1605
+ }
1606
+ group.duplicates.push(assertion);
1607
+ }
1608
+ for (const { target, duplicates } of groups.values()) {
1609
+ if (duplicates.length > 0) {
1610
+ normalizePropertyAssertionGroup(subject, target, duplicates);
1611
+ }
1612
+ }
1613
+ }
1614
+
1615
+ function normalizeAllAnnotations(subject: any, ontology: any): void {
1616
+ if (!Array.isArray(subject?.ownedAnnotations) || subject.ownedAnnotations.length < 2) {
1617
+ return;
1618
+ }
1619
+ const groups = new Map<string, { target: any; duplicates: any[] }>();
1620
+ for (const annotation of subject.ownedAnnotations) {
1621
+ const key = propertyRefMergeKey(ontology, annotation?.property);
1622
+ if (!key) {
1623
+ continue;
1624
+ }
1625
+ const group = groups.get(key);
1626
+ if (!group) {
1627
+ groups.set(key, { target: annotation, duplicates: [] });
1628
+ continue;
1629
+ }
1630
+ group.duplicates.push(annotation);
1631
+ }
1632
+ for (const { target, duplicates } of groups.values()) {
1633
+ if (duplicates.length > 0) {
1634
+ normalizeAnnotationGroup(subject, target, duplicates);
1635
+ }
1636
+ }
1637
+ }
1638
+
1639
+ function propertyRefMergeKey(ontology: any, ref: any): string | undefined {
1640
+ const refText = getRefText(ref);
1641
+ if (refText) {
1642
+ const expanded = expandRefTextToIri(ontology, refText);
1643
+ return expanded
1644
+ ? `iri:${normalizeIri(expanded)}`
1645
+ : `ref:${normalizeIri(refText)}`;
1646
+ }
1647
+ const resolved = ref?.ref ?? ref?._ref;
1648
+ const resolvedIri = resolved ? getIriForNode(resolved) : undefined;
1649
+ if (resolvedIri) {
1650
+ return `iri:${normalizeIri(resolvedIri)}`;
1651
+ }
1652
+ return undefined;
1653
+ }
1654
+
1416
1655
  function isSourcePredicate(predicateIri: string): boolean {
1417
1656
  return normalizeIri(predicateIri) === OML_HAS_SOURCE_IRI;
1418
1657
  }
@@ -1467,3 +1706,15 @@ async function restoreContextsFromDocuments(shared: any, contexts: Map<string, O
1467
1706
  }
1468
1707
  }
1469
1708
  }
1709
+
1710
+ async function ensureDocumentBuiltForEdit(builder: any, document: any): Promise<void> {
1711
+ const docState: number | undefined = document?.state;
1712
+ if (docState !== undefined && docState >= DocumentState.Linked) {
1713
+ return;
1714
+ }
1715
+ const workspaceState: number | undefined = builder?.currentState;
1716
+ const validation = workspaceState !== undefined && workspaceState >= DocumentState.Validated
1717
+ ? { categories: ['built-in', 'fast'] as const }
1718
+ : false;
1719
+ await builder.build([document], { validation });
1720
+ }
@@ -160,16 +160,15 @@ export const getWorkspaceSnapshot = (documents: LangiumDocuments): string =>
160
160
  .sort()
161
161
  .join('|');
162
162
 
163
- const GITHUB_COPILOT_TRANSIENT_DOCUMENT_SCHEMES = new Set([
163
+ const OML_LS_IGNORED_DOCUMENT_SCHEMES = new Set([
164
164
  'chat-editing-text-model',
165
165
  'chat-editing-snapshot-text-model',
166
166
  'vscode-chat-code-block',
167
- 'git',
168
167
  ]);
169
168
 
170
- export function isTransientEditorDocumentUri(uri: string): boolean {
169
+ export function isIgnoredByOmlLsDocumentUri(uri: string): boolean {
171
170
  try {
172
- return GITHUB_COPILOT_TRANSIENT_DOCUMENT_SCHEMES.has(URI.parse(uri).scheme);
171
+ return OML_LS_IGNORED_DOCUMENT_SCHEMES.has(URI.parse(uri).scheme);
173
172
  } catch {
174
173
  return false;
175
174
  }
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
 
3
- import { AstUtils } from 'langium';
3
+ import { AstUtils, URI } from 'langium';
4
4
  import type { ValidationAcceptor, ValidationChecks } from 'langium';
5
5
  import type { OmlServices } from './oml-module.js';
6
6
  import { getOntologyModelIndex } from './oml-index.js';
@@ -270,8 +270,15 @@ export class OmlValidator {
270
270
  const namespace = rawNamespace.replace(/^<|>$/g, '');
271
271
  const iri = namespace.replace(/[#/]?$/, '');
272
272
  const docUri = ontology?.$document?.uri?.toString?.() ?? '';
273
+ const currentScheme = this.getUriScheme(docUri);
273
274
  const duplicateModelUris = getOntologyModelIndex(this.services.shared).getDuplicateWorkspaceModelUris(iri);
274
- const conflictingModelUris = duplicateModelUris.filter((modelUri) => modelUri !== docUri);
275
+ const conflictingModelUris = duplicateModelUris.filter((modelUri) => {
276
+ if (modelUri === docUri) {
277
+ return false;
278
+ }
279
+ const otherScheme = this.getUriScheme(modelUri);
280
+ return !currentScheme || !otherScheme || currentScheme === otherScheme;
281
+ });
275
282
  if (conflictingModelUris.length > 0) {
276
283
  accept('error', `Ontology IRI '${iri}' is also declared by '${conflictingModelUris[0]}'`, {
277
284
  node: ontology,
@@ -309,6 +316,14 @@ export class OmlValidator {
309
316
  }
310
317
  }
311
318
 
319
+ private getUriScheme(value: string): string | undefined {
320
+ try {
321
+ return URI.parse(value).scheme;
322
+ } catch {
323
+ return undefined;
324
+ }
325
+ }
326
+
312
327
  checkQuotedLiteralNonStandardType(literal: QuotedLiteral, accept: ValidationAcceptor): void {
313
328
  const scalar = literal.type?.ref as any;
314
329
  if (!scalar) {
@@ -700,7 +715,7 @@ export class OmlValidator {
700
715
  // Mask quoted literals and comments so prefix scanning only sees code tokens.
701
716
  const stripped = text.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\*[\s\S]*?\*\/|\/\/[^\n\r]*/g, ' ');
702
717
  const usedPrefixes = new Set<string>();
703
- const pattern = /\b([A-Za-z_][A-Za-z0-9_-]*):\S/g;
718
+ const pattern = /(?<![A-Za-z0-9$._~%-])([A-Za-z0-9._~%-][A-Za-z0-9$._~%-]*):\S/g;
704
719
  let match: RegExpExecArray | null;
705
720
  while ((match = pattern.exec(stripped)) !== null) {
706
721
  usedPrefixes.add(match[1]);