@oml/language 0.18.1 → 0.19.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.
@@ -11,7 +11,40 @@ import {
11
11
  getMemberName,
12
12
  getNamedElementName,
13
13
  } from './oml-utils.js';
14
- import type { EntityEquivalenceAxiom, ForwardRelation, Import, Member, OmlAstType, Ontology, PropertyEquivalenceAxiom, QuotedLiteral, RelationEntity, RelationInstance, ReverseRelation, Rule, Scalar, ScalarEquivalenceAxiom, TypeAssertion } from './generated/ast.js';
14
+ import type { EntityEquivalenceAxiom, ForwardRelation, Import, Member, OmlAstType, Ontology, PropertyEquivalenceAxiom, PropertyRangeRestrictionAxiom, PropertyValueAssertion, Quantity, QuantityProperty, QuotedLiteral, RelationEntity, RelationInstance, ReverseRelation, Rule, Scalar, ScalarEquivalenceAxiom, TypeAssertion, Unit } from './generated/ast.js';
15
+
16
+ type DimensionVector = { L: number; M: number; T: number; I: number; H: number; N: number; J: number };
17
+
18
+ function emptyDimension(): DimensionVector {
19
+ return { L: 0, M: 0, T: 0, I: 0, H: 0, N: 0, J: 0 };
20
+ }
21
+
22
+ function parseDimension(input: string): { vector?: DimensionVector; error?: string } {
23
+ const trimmed = (input ?? '').trim();
24
+ if (!trimmed) {
25
+ return { vector: emptyDimension() };
26
+ }
27
+ const tokens = trimmed.split(/\s+/);
28
+ const result = emptyDimension();
29
+ const seen = new Set<string>();
30
+ for (const tok of tokens) {
31
+ const m = /^([LMTIHNJ])(-?\d+)$/.exec(tok);
32
+ if (!m) {
33
+ return { error: `Invalid dimension token '${tok}'. Expected axis (L|M|T|I|H|N|J) followed by a signed integer.` };
34
+ }
35
+ const axis = m[1];
36
+ if (seen.has(axis)) {
37
+ return { error: `Dimension axis '${axis}' appears more than once.` };
38
+ }
39
+ seen.add(axis);
40
+ (result as any)[axis] = parseInt(m[2], 10);
41
+ }
42
+ return { vector: result };
43
+ }
44
+
45
+ function dimensionsEqual(a: DimensionVector, b: DimensionVector): boolean {
46
+ return a.L === b.L && a.M === b.M && a.T === b.T && a.I === b.I && a.H === b.H && a.N === b.N && a.J === b.J;
47
+ }
15
48
 
16
49
  interface OntologyValidationContext {
17
50
  version: number;
@@ -60,7 +93,7 @@ export function registerValidationChecks(services: OmlServices) {
60
93
  InstanceEnumerationAxiom: [],
61
94
  LiteralEnumerationAxiom: [],
62
95
  PropertyRestrictionAxiom: [],
63
- PropertyRangeRestrictionAxiom: [],
96
+ PropertyRangeRestrictionAxiom: [validator.checkInvalidQuantityPropertyRangeRestriction],
64
97
  PropertyCardinalityRestrictionAxiom: [],
65
98
  PropertyValueRestrictionAxiom: [],
66
99
  PropertySelfRestrictionAxiom: [],
@@ -75,7 +108,7 @@ export function registerValidationChecks(services: OmlServices) {
75
108
  // Assertion types
76
109
  Assertion: [],
77
110
  TypeAssertion: [validator.checkInvalidInstanceType],
78
- PropertyValueAssertion: [],
111
+ PropertyValueAssertion: [validator.checkInvalidPropertyValueAssertion],
79
112
  // Literal types
80
113
  Literal: [],
81
114
  IntegerLiteral: [],
@@ -122,6 +155,10 @@ export function registerValidationChecks(services: OmlServices) {
122
155
  Aspect: [],
123
156
  Concept: [],
124
157
  RelationEntity: [validator.checkInvalidRelationEntity],
158
+ // Quantity types
159
+ Quantity: [validator.checkQuantityDimensionFormat],
160
+ Unit: [validator.checkUnitKindsDimensionalConsistency, validator.checkUnitSymbolUniqueness],
161
+ QuantityProperty: [validator.checkQuantityPropertyCardinality],
125
162
  };
126
163
  registry.register(checks, validator);
127
164
  }
@@ -200,7 +237,7 @@ export class OmlValidator {
200
237
 
201
238
  const context = this.getOntologyValidationContext(ontology);
202
239
  if (!context?.usedPrefixes.has(prefix)) {
203
- accept('warning', `Could not find a reference to prefix '${ownedImport.prefix}'`, {
240
+ accept('warning', `Prefix '${ownedImport.prefix}' is declared but not used`, {
204
241
  node: ownedImport
205
242
  });
206
243
  }
@@ -533,7 +570,182 @@ export class OmlValidator {
533
570
  }
534
571
  }
535
572
 
536
- private getPropertyKind(property: any): 'annotation property' | 'scalar property' | 'relation' | undefined {
573
+ checkInvalidPropertyValueAssertion(assertion: PropertyValueAssertion, accept: ValidationAcceptor): void {
574
+ const property = assertion.property?.ref as any;
575
+ if (!property) {
576
+ return;
577
+ }
578
+
579
+ const propertyKind = this.getPropertyKind(property);
580
+ const literals = assertion.literalValues ?? [];
581
+
582
+ if (propertyKind === 'scalar property') {
583
+ const hasObjectValues = (assertion.referencedValues?.length ?? 0) > 0
584
+ || (assertion.containedValues?.length ?? 0) > 0;
585
+ if (hasObjectValues) {
586
+ accept('error', `Scalar property ${this.getAbbreviatedIri(property)} cannot have instance values`, {
587
+ node: assertion,
588
+ property: (assertion.referencedValues?.length ?? 0) > 0 ? 'referencedValues' : 'containedValues'
589
+ });
590
+ }
591
+ for (let i = 0; i < literals.length; i++) {
592
+ if (this.literalUnit(literals[i])) {
593
+ accept('error', `Unit suffix is only allowed on quantity property values`, {
594
+ node: assertion,
595
+ property: 'literalValues',
596
+ index: i
597
+ });
598
+ }
599
+ }
600
+ return;
601
+ }
602
+
603
+ if (propertyKind === 'relation') {
604
+ if (literals.length > 0) {
605
+ accept('error', `Relation ${this.getAbbreviatedIri(property)} cannot have literal values`, {
606
+ node: assertion,
607
+ property: 'literalValues'
608
+ });
609
+ }
610
+ return;
611
+ }
612
+
613
+ if (propertyKind === 'quantity property') {
614
+ const hasObjectValues = (assertion.referencedValues?.length ?? 0) > 0
615
+ || (assertion.containedValues?.length ?? 0) > 0;
616
+ if (hasObjectValues) {
617
+ accept('error', `Quantity property ${this.getAbbreviatedIri(property)} cannot have instance values`, {
618
+ node: assertion,
619
+ property: (assertion.referencedValues?.length ?? 0) > 0 ? 'referencedValues' : 'containedValues'
620
+ });
621
+ }
622
+ const propKinds = new Set<any>((((property as any).kind ?? []) as any[])
623
+ .map((r: any) => r?.ref)
624
+ .filter(Boolean));
625
+ for (let i = 0; i < literals.length; i++) {
626
+ const lit: any = literals[i];
627
+ if (!this.isNumericLiteral(lit)) {
628
+ accept('error', `Quantity property ${this.getAbbreviatedIri(property)} requires a numeric literal`, {
629
+ node: assertion,
630
+ property: 'literalValues',
631
+ index: i
632
+ });
633
+ continue;
634
+ }
635
+ const unit = lit?.unit?.ref;
636
+ if (!unit) {
637
+ if (lit?.unit?.$refText) {
638
+ // Reference present but unresolved; let the linker handle it.
639
+ continue;
640
+ }
641
+ accept('error', `Quantity property ${this.getAbbreviatedIri(property)} requires a unit suffix (e.g., '5^^unit:Metre')`, {
642
+ node: assertion,
643
+ property: 'literalValues',
644
+ index: i
645
+ });
646
+ continue;
647
+ }
648
+ if (propKinds.size === 0) continue;
649
+ const unitKinds: any[] = ((unit.kinds ?? []) as any[])
650
+ .map((r: any) => r?.ref)
651
+ .filter(Boolean);
652
+ if (unitKinds.length === 0) continue;
653
+ const overlap = unitKinds.some((k: any) => propKinds.has(k));
654
+ if (!overlap) {
655
+ accept('error', `Unit ${this.getAbbreviatedIri(unit)} is not applicable to any quantity kind of property ${this.getAbbreviatedIri(property)}`, {
656
+ node: assertion,
657
+ property: 'literalValues',
658
+ index: i
659
+ });
660
+ }
661
+ }
662
+ }
663
+ }
664
+
665
+ checkQuantityDimensionFormat(quantity: Quantity, accept: ValidationAcceptor): void {
666
+ const dim = quantity.dimension;
667
+ if (!dim) return;
668
+ const parsed = parseDimension(dim);
669
+ if (parsed.error) {
670
+ accept('error', parsed.error, {
671
+ node: quantity,
672
+ property: 'dimension'
673
+ });
674
+ }
675
+ }
676
+
677
+ checkUnitKindsDimensionalConsistency(unit: Unit, accept: ValidationAcceptor): void {
678
+ const kindRefs = ((unit.kinds ?? []) as any[]);
679
+ const kinds = kindRefs.map((r: any) => r?.ref).filter(Boolean) as any[];
680
+ if (kinds.length <= 1) return;
681
+ const vectors: DimensionVector[] = [];
682
+ for (const k of kinds) {
683
+ const dim = typeof k?.dimension === 'string' ? k.dimension : '';
684
+ if (!dim) continue;
685
+ const parsed = parseDimension(dim);
686
+ if (parsed.vector) vectors.push(parsed.vector);
687
+ }
688
+ if (vectors.length <= 1) return;
689
+ const ref = vectors[0];
690
+ for (let i = 1; i < vectors.length; i++) {
691
+ if (!dimensionsEqual(ref, vectors[i])) {
692
+ accept('error', `Unit ${this.getAbbreviatedIri(unit)} declares quantity kinds with inconsistent dimensions`, {
693
+ node: unit,
694
+ property: 'kinds'
695
+ });
696
+ return;
697
+ }
698
+ }
699
+ }
700
+
701
+ checkUnitSymbolUniqueness(unit: Unit, accept: ValidationAcceptor): void {
702
+ const ontology: any = findOwningOntologyNode(unit);
703
+ if (!ontology || ontology.$type !== 'Vocabulary') return;
704
+ const symbols = ((unit as any).symbol ?? []) as string[];
705
+ if (symbols.length === 0) return;
706
+ if (symbols.length > 1) {
707
+ accept('error', `Unit ${this.getAbbreviatedIri(unit)} declares more than one symbol; only one is allowed`, {
708
+ node: unit,
709
+ property: 'symbol' as any,
710
+ index: 1
711
+ });
712
+ }
713
+ const statements: any[] = ontology.ownedStatements ?? [];
714
+ const otherUnits = statements.filter((s: any) => s && s !== unit && s.$type === 'Unit');
715
+ for (let i = 0; i < symbols.length; i++) {
716
+ const sym = symbols[i];
717
+ const conflict = otherUnits.find((u: any) => ((u.symbol ?? []) as string[]).includes(sym));
718
+ if (conflict) {
719
+ accept('error', `Unit symbol "${sym}" is already used by unit ${this.getAbbreviatedIri(conflict)}`, {
720
+ node: unit,
721
+ property: 'symbol' as any,
722
+ index: i
723
+ });
724
+ }
725
+ }
726
+ }
727
+
728
+ checkQuantityPropertyCardinality(prop: QuantityProperty, accept: ValidationAcceptor): void {
729
+ const kinds = (((prop as any).kind ?? []) as any[]);
730
+ if (kinds.length > 1) {
731
+ accept('error', `Quantity property ${this.getAbbreviatedIri(prop)} declares more than one quantity; only one is allowed`, {
732
+ node: prop,
733
+ property: 'kind' as any,
734
+ index: 1
735
+ });
736
+ }
737
+ }
738
+
739
+ checkInvalidQuantityPropertyRangeRestriction(axiom: PropertyRangeRestrictionAxiom, accept: ValidationAcceptor): void {
740
+ const prop = axiom.property?.ref as any;
741
+ if (prop?.$type === 'QuantityProperty') {
742
+ accept('warning', `Range restriction on quantity property ${this.getAbbreviatedIri(prop)} is not meaningful; use a value restriction instead.`, {
743
+ node: axiom
744
+ });
745
+ }
746
+ }
747
+
748
+ private getPropertyKind(property: any): 'annotation property' | 'scalar property' | 'relation' | 'quantity property' | undefined {
537
749
  const type = property?.$type;
538
750
  if (type === 'AnnotationProperty') {
539
751
  return 'annotation property';
@@ -544,9 +756,24 @@ export class OmlValidator {
544
756
  if (type === 'UnreifiedRelation' || type === 'ForwardRelation' || type === 'ReverseRelation') {
545
757
  return 'relation';
546
758
  }
759
+ if (type === 'QuantityProperty') {
760
+ return 'quantity property';
761
+ }
547
762
  return undefined;
548
763
  }
549
764
 
765
+ private isNumericLiteral(node: any): boolean {
766
+ const t = node?.$type;
767
+ return t === 'IntegerLiteral' || t === 'DecimalLiteral' || t === 'DoubleLiteral';
768
+ }
769
+
770
+ private literalUnit(node: any): any {
771
+ if (!this.isNumericLiteral(node)) return undefined;
772
+ const ref = node?.unit;
773
+ if (!ref) return undefined;
774
+ return ref?.ref ?? (ref?.$refText ? ref : undefined);
775
+ }
776
+
550
777
  private isStandardScalar(scalar: any): boolean {
551
778
  const ontology = findOwningOntologyNode(scalar);
552
779
  const namespace = typeof ontology?.namespace === 'string' ? ontology.namespace : '';
@@ -148,7 +148,9 @@ DescriptionMember:
148
148
  VocabularyStatement:
149
149
  Rule |
150
150
  BuiltIn |
151
- SpecializableTerm;
151
+ SpecializableTerm |
152
+ Quantity |
153
+ Unit;
152
154
 
153
155
  DescriptionStatement:
154
156
  NamedInstance;
@@ -160,7 +162,9 @@ DescriptionStatement:
160
162
  Term:
161
163
  SpecializableTerm |
162
164
  RelationBase |
163
- Property;
165
+ Property |
166
+ Quantity |
167
+ Unit;
164
168
 
165
169
  Rule:
166
170
  ownedAnnotations+=Annotation*
@@ -200,7 +204,8 @@ RelationBase:
200
204
  SpecializableProperty:
201
205
  AnnotationProperty |
202
206
  ScalarProperty |
203
- UnreifiedRelation;
207
+ UnreifiedRelation |
208
+ QuantityProperty;
204
209
 
205
210
  ////////////////////////////////////////
206
211
  // Types
@@ -283,7 +288,8 @@ AnnotationProperty:
283
288
 
284
289
  SemanticProperty:
285
290
  ScalarProperty |
286
- Relation;
291
+ Relation |
292
+ QuantityProperty;
287
293
 
288
294
  fragment PropertySpecialization:
289
295
  '<' ownedSpecializations+=SpecializationAxiom (',' ownedSpecializations+=SpecializationAxiom)*;
@@ -335,6 +341,32 @@ UnreifiedRelation:
335
341
  (transitive?='transitive'))
336
342
  ']')? (PropertySpecialization)? (PropertyEquivalence)?;
337
343
 
344
+ ////////////////////////////////////////
345
+ // Quantities and Units
346
+ ////////////////////////////////////////
347
+
348
+ Quantity:
349
+ ownedAnnotations+=Annotation*
350
+ ('quantity' name=ID | 'ref' 'quantity' ref=[Quantity:Ref]) ('['
351
+ ('dimension' dimension=STRING)?
352
+ ']')?;
353
+
354
+ Unit:
355
+ ownedAnnotations+=Annotation*
356
+ ('unit' name=ID | 'ref' 'unit' ref=[Unit:Ref]) ('['
357
+ (('measures' kinds+=[Quantity:Ref] (',' kinds+=[Quantity:Ref])*) |
358
+ ('symbol' symbol+=STRING) |
359
+ ('multiplier' multiplier+=Decimal))*
360
+ ']')?;
361
+
362
+ QuantityProperty:
363
+ ownedAnnotations+=Annotation*
364
+ ('quantity' 'property' name=ID | 'ref' 'quantity' 'property' ref=[QuantityProperty:Ref]) ('['
365
+ (('domain' domains+=[Entity:Ref] (',' domains+=[Entity:Ref])*) |
366
+ ('quantity' kind+=[Quantity:Ref]))*
367
+ (functional?='functional')?
368
+ ']')? (PropertySpecialization)? (PropertyEquivalence)?;
369
+
338
370
  ////////////////////////////////////////
339
371
  // Description Members and Instances
340
372
  ////////////////////////////////////////
@@ -501,13 +533,13 @@ DifferentFromPredicate:
501
533
  ////////////////////////////////////////
502
534
 
503
535
  IntegerLiteral:
504
- value=Integer;
536
+ value=Integer ('^^' unit=[Unit:Ref])?;
505
537
 
506
538
  DecimalLiteral:
507
- value=Decimal;
539
+ value=Decimal ('^^' unit=[Unit:Ref])?;
508
540
 
509
541
  DoubleLiteral:
510
- value=Double;
542
+ value=Double ('^^' unit=[Unit:Ref])?;
511
543
 
512
544
  BooleanLiteral:
513
545
  value=Boolean;