glossarist 0.4.36 → 0.4.38
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.
- package/package.json +1 -1
- package/src/models/abstract-hyperedge.ts +164 -0
- package/src/models/bibliography-data.ts +64 -0
- package/src/models/{bibliography-entry.js → bibliography-entry.ts} +18 -4
- package/src/models/concept-system.ts +152 -0
- package/src/models/detailed-definition.ts +92 -0
- package/src/models/generic-hyperedge.ts +72 -0
- package/src/models/generic-member.ts +80 -0
- package/src/models/grammar-info.ts +78 -0
- package/src/models/{hyperedge-index.js → hyperedge-index.ts} +32 -18
- package/src/models/{hyperedge-member.js → hyperedge-member.ts} +38 -26
- package/src/models/{hyperedge-registry.js → hyperedge-registry.ts} +59 -29
- package/src/models/{locality.js → locality.ts} +16 -4
- package/src/models/localized-string.ts +32 -0
- package/src/models/partitive-hyperedge.ts +90 -0
- package/src/models/{partitive-member.js → partitive-member.ts} +18 -17
- package/src/models/{pronunciation.js → pronunciation.ts} +18 -4
- package/src/models/sequential-hyperedge.ts +70 -0
- package/src/models/{sequential-member.js → sequential-member.ts} +6 -3
- package/src/models/abstract-hyperedge.js +0 -119
- package/src/models/bibliography-data.js +0 -48
- package/src/models/concept-system.js +0 -117
- package/src/models/detailed-definition.js +0 -59
- package/src/models/generic-hyperedge.js +0 -55
- package/src/models/generic-member.js +0 -82
- package/src/models/grammar-info.js +0 -40
- package/src/models/localized-string.js +0 -19
- package/src/models/partitive-hyperedge.js +0 -91
- package/src/models/sequential-hyperedge.js +0 -56
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { GlossaristModel } from './base.js';
|
|
2
|
+
|
|
3
|
+
export type GrammarGender = 'm' | 'f' | 'n' | 'c' | string;
|
|
4
|
+
export type GrammarNumber = 'singular' | 'dual' | 'plural' | string;
|
|
5
|
+
export type GrammarPartOfSpeech =
|
|
6
|
+
| 'noun'
|
|
7
|
+
| 'verb'
|
|
8
|
+
| 'adj'
|
|
9
|
+
| 'adverb'
|
|
10
|
+
| 'preposition'
|
|
11
|
+
| 'participle'
|
|
12
|
+
| string;
|
|
13
|
+
|
|
14
|
+
export const GRAMMAR_GENDERS: ReadonlyArray<string> = Object.freeze([
|
|
15
|
+
'm', 'f', 'n', 'c',
|
|
16
|
+
]);
|
|
17
|
+
export const GRAMMAR_NUMBERS: ReadonlyArray<string> = Object.freeze([
|
|
18
|
+
'singular', 'dual', 'plural',
|
|
19
|
+
]);
|
|
20
|
+
export const GRAMMAR_PARTS_OF_SPEECH: ReadonlyArray<string> = Object.freeze([
|
|
21
|
+
'noun', 'verb', 'adj', 'adverb', 'preposition', 'participle',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export interface GrammarInfoJson {
|
|
25
|
+
gender?: GrammarGender | null;
|
|
26
|
+
number?: GrammarNumber | null;
|
|
27
|
+
part_of_speech?: GrammarPartOfSpeech | null;
|
|
28
|
+
partOfSpeech?: GrammarPartOfSpeech | null;
|
|
29
|
+
noun?: boolean;
|
|
30
|
+
verb?: boolean;
|
|
31
|
+
adj?: boolean;
|
|
32
|
+
adverb?: boolean;
|
|
33
|
+
preposition?: boolean;
|
|
34
|
+
participle?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class GrammarInfo extends GlossaristModel {
|
|
38
|
+
readonly gender: GrammarGender | null;
|
|
39
|
+
readonly number: GrammarNumber | null;
|
|
40
|
+
readonly partOfSpeech: GrammarPartOfSpeech | null;
|
|
41
|
+
readonly noun: boolean;
|
|
42
|
+
readonly verb: boolean;
|
|
43
|
+
readonly adj: boolean;
|
|
44
|
+
readonly adverb: boolean;
|
|
45
|
+
readonly preposition: boolean;
|
|
46
|
+
readonly participle: boolean;
|
|
47
|
+
|
|
48
|
+
constructor(data: GrammarInfoJson = {}) {
|
|
49
|
+
super();
|
|
50
|
+
this.gender = data.gender ?? null;
|
|
51
|
+
this.number = data.number ?? null;
|
|
52
|
+
this.partOfSpeech = data.part_of_speech ?? data.partOfSpeech ?? null;
|
|
53
|
+
this.noun = data.noun ?? false;
|
|
54
|
+
this.verb = data.verb ?? false;
|
|
55
|
+
this.adj = data.adj ?? false;
|
|
56
|
+
this.adverb = data.adverb ?? false;
|
|
57
|
+
this.preposition = data.preposition ?? false;
|
|
58
|
+
this.participle = data.participle ?? false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override toJSON(): GrammarInfoJson {
|
|
62
|
+
const obj: GrammarInfoJson = {};
|
|
63
|
+
if (this.gender != null) obj.gender = this.gender;
|
|
64
|
+
if (this.number != null) obj.number = this.number;
|
|
65
|
+
if (this.partOfSpeech != null) obj.part_of_speech = this.partOfSpeech;
|
|
66
|
+
if (this.noun) obj.noun = true;
|
|
67
|
+
if (this.verb) obj.verb = true;
|
|
68
|
+
if (this.adj) obj.adj = true;
|
|
69
|
+
if (this.adverb) obj.adverb = true;
|
|
70
|
+
if (this.preposition) obj.preposition = true;
|
|
71
|
+
if (this.participle) obj.participle = true;
|
|
72
|
+
return obj;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static override fromJSON(data: GrammarInfoJson): GrammarInfo {
|
|
76
|
+
return new GrammarInfo(data);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -15,11 +15,19 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Identity key matches HyperedgeRegistry / ConceptRef conventions:
|
|
17
17
|
// `${source}:${id}` (empty source or id is rejected)
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
|
|
19
|
+
import type { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
20
|
+
|
|
21
|
+
interface HyperedgeLike {
|
|
22
|
+
comprehensive?: { source?: string | null; id?: string | null } | null;
|
|
23
|
+
members?: ReadonlyArray<{ ref?: { source?: string | null; id?: string | null } | null }>;
|
|
24
|
+
}
|
|
20
25
|
|
|
21
26
|
export class HyperedgeIndex {
|
|
22
|
-
|
|
27
|
+
private readonly _byComprehensive: Map<string, HyperedgeLike[]>;
|
|
28
|
+
private readonly _byMember: Map<string, HyperedgeLike[]>;
|
|
29
|
+
|
|
30
|
+
constructor(hyperedges: ReadonlyArray<HyperedgeLike> = []) {
|
|
23
31
|
this._byComprehensive = new Map();
|
|
24
32
|
this._byMember = new Map();
|
|
25
33
|
|
|
@@ -35,45 +43,51 @@ export class HyperedgeIndex {
|
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
// Hyperedges where the given concept is the comprehensive (the "1" side).
|
|
38
|
-
forComprehensive(qualifiedId) {
|
|
46
|
+
forComprehensive(qualifiedId: string): HyperedgeLike[] {
|
|
39
47
|
return this._byComprehensive.get(qualifiedId) ?? [];
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
// Hyperedges where the given concept appears as a member (the "many" side).
|
|
43
|
-
forMember(qualifiedId) {
|
|
51
|
+
forMember(qualifiedId: string): HyperedgeLike[] {
|
|
44
52
|
return this._byMember.get(qualifiedId) ?? [];
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
// All concepts that appear as a comprehensive in any indexed hyperedge.
|
|
48
|
-
comprehensives() {
|
|
56
|
+
comprehensives(): string[] {
|
|
49
57
|
return [...this._byComprehensive.keys()];
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
// All concepts that appear as a member in any indexed hyperedge.
|
|
53
|
-
members() {
|
|
61
|
+
members(): string[] {
|
|
54
62
|
return [...this._byMember.keys()];
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
get size() {
|
|
65
|
+
get size(): number {
|
|
58
66
|
return this._byComprehensive.size;
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Build a HyperedgeIndex across multiple Concepts' relations arrays
|
|
72
|
+
* (a dataset-wide view). Each Concept contributes its own .relations.
|
|
73
|
+
* Helper for the most common pattern: index once at collection load,
|
|
74
|
+
* query many times.
|
|
75
|
+
*/
|
|
76
|
+
export function buildDatasetIndex(
|
|
77
|
+
concepts: ReadonlyArray<{ relations?: ReadonlyArray<AbstractHyperedge | HyperedgeLike> }> = [],
|
|
78
|
+
): HyperedgeIndex {
|
|
79
|
+
const all: HyperedgeLike[] = [];
|
|
68
80
|
for (const concept of concepts) {
|
|
69
81
|
for (const h of concept.relations ?? []) {
|
|
70
|
-
all.push(h);
|
|
82
|
+
all.push(h as HyperedgeLike);
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
return new HyperedgeIndex(all);
|
|
74
86
|
}
|
|
75
87
|
|
|
76
|
-
function _qualifiedId(
|
|
88
|
+
function _qualifiedId(
|
|
89
|
+
ref: { source?: string | null; id?: string | null } | null | undefined,
|
|
90
|
+
): string | null {
|
|
77
91
|
if (!ref) return null;
|
|
78
92
|
const source = ref.source ?? '';
|
|
79
93
|
const id = ref.id ?? '';
|
|
@@ -81,7 +95,7 @@ function _qualifiedId(ref) {
|
|
|
81
95
|
return `${source}:${id}`;
|
|
82
96
|
}
|
|
83
97
|
|
|
84
|
-
function _addToMapList(map, key, value) {
|
|
98
|
+
function _addToMapList<T>(map: Map<string, T[]>, key: string, value: T): void {
|
|
85
99
|
if (!map.has(key)) map.set(key, []);
|
|
86
|
-
map.get(key)
|
|
100
|
+
map.get(key)?.push(value);
|
|
87
101
|
}
|
|
@@ -19,21 +19,25 @@
|
|
|
19
19
|
|
|
20
20
|
import { GlossaristModel } from './base.js';
|
|
21
21
|
import { ConceptRef } from './concept-ref.js';
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} from './partitive-presence.js';
|
|
28
|
-
import {
|
|
29
|
-
PARTITIVE_COUNT_VALUES,
|
|
30
|
-
DEFAULT_COUNT,
|
|
31
|
-
isValidCount,
|
|
32
|
-
} from './partitive-count.js';
|
|
22
|
+
import type { ConceptRefJson } from './concept-ref.js';
|
|
23
|
+
import { PARTITIVE_PRESENCE_VALUES, DEFAULT_PRESENCE, isValidPresence } from './partitive-presence.js';
|
|
24
|
+
import type { PartitivePresence } from './partitive-presence.js';
|
|
25
|
+
import { PARTITIVE_COUNT_VALUES, DEFAULT_COUNT, isValidCount } from './partitive-count.js';
|
|
26
|
+
import type { PartitiveCount } from './partitive-count.js';
|
|
33
27
|
import { multiplicityFromPair } from './multiplicity.js';
|
|
34
28
|
|
|
35
|
-
export
|
|
36
|
-
|
|
29
|
+
export interface HyperedgeMemberJson {
|
|
30
|
+
ref: ConceptRefJson | ConceptRef;
|
|
31
|
+
presence?: PartitivePresence;
|
|
32
|
+
count?: PartitiveCount;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export abstract class HyperedgeMember extends GlossaristModel {
|
|
36
|
+
readonly ref: ConceptRef;
|
|
37
|
+
readonly presence: PartitivePresence;
|
|
38
|
+
readonly count: PartitiveCount;
|
|
39
|
+
|
|
40
|
+
constructor(data: HyperedgeMemberJson = { ref: {} }) {
|
|
37
41
|
super();
|
|
38
42
|
this.ref = _ensureRef(data.ref);
|
|
39
43
|
this.presence = _resolvePresence(data.presence);
|
|
@@ -42,32 +46,40 @@ export class HyperedgeMember extends GlossaristModel {
|
|
|
42
46
|
_assertValidPair(this.presence, this.count);
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
get isRequired() { return this.presence ===
|
|
46
|
-
get isOptional() { return this.presence ===
|
|
49
|
+
get isRequired(): boolean { return this.presence === 'required'; }
|
|
50
|
+
get isOptional(): boolean { return this.presence === 'optional'; }
|
|
47
51
|
|
|
48
|
-
identity() {
|
|
52
|
+
override identity(): string {
|
|
49
53
|
return HyperedgeMember.identityOf(this);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
toJSON() {
|
|
53
|
-
const obj = { ref: this.ref.toJSON() };
|
|
56
|
+
override toJSON(): HyperedgeMemberJson {
|
|
57
|
+
const obj: HyperedgeMemberJson = { ref: this.ref.toJSON() };
|
|
54
58
|
if (this.presence !== DEFAULT_PRESENCE) obj.presence = this.presence;
|
|
55
59
|
if (this.count !== DEFAULT_COUNT) obj.count = this.count;
|
|
56
60
|
return obj;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
static identityOf(
|
|
63
|
+
static identityOf(
|
|
64
|
+
value: { ref?: { source?: string | null; id?: string | null } | null } | null | undefined,
|
|
65
|
+
): string {
|
|
60
66
|
const v = value ?? {};
|
|
61
67
|
const ref = v.ref ?? {};
|
|
62
68
|
return `${ref.source ?? ''}:${ref.id ?? ''}`;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
static fromJSON(
|
|
66
|
-
|
|
71
|
+
static override fromJSON(_data: HyperedgeMemberJson): HyperedgeMember {
|
|
72
|
+
// Abstract base — subclasses MUST override. The base
|
|
73
|
+
// implementation is a typed marker that dispatches to the wrong
|
|
74
|
+
// class would surface as a runtime error rather than silently
|
|
75
|
+
// returning a mis-typed instance.
|
|
76
|
+
throw new Error(
|
|
77
|
+
'HyperedgeMember.fromJSON must be overridden by a subclass',
|
|
78
|
+
);
|
|
67
79
|
}
|
|
68
80
|
}
|
|
69
81
|
|
|
70
|
-
function _ensureRef(value) {
|
|
82
|
+
function _ensureRef(value: ConceptRefJson | ConceptRef | null | undefined): ConceptRef {
|
|
71
83
|
const ref = value instanceof ConceptRef ? value : new ConceptRef(value ?? {});
|
|
72
84
|
if (!ref.source && !ref.id && !ref.text) {
|
|
73
85
|
throw new Error(
|
|
@@ -78,7 +90,7 @@ function _ensureRef(value) {
|
|
|
78
90
|
return ref;
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
function _resolvePresence(value) {
|
|
93
|
+
function _resolvePresence(value: PartitivePresence | null | undefined): PartitivePresence {
|
|
82
94
|
const p = value ?? DEFAULT_PRESENCE;
|
|
83
95
|
if (!isValidPresence(p)) {
|
|
84
96
|
throw new Error(
|
|
@@ -89,7 +101,7 @@ function _resolvePresence(value) {
|
|
|
89
101
|
return p;
|
|
90
102
|
}
|
|
91
103
|
|
|
92
|
-
function _resolveCount(value) {
|
|
104
|
+
function _resolveCount(value: PartitiveCount | null | undefined): PartitiveCount {
|
|
93
105
|
const c = value ?? DEFAULT_COUNT;
|
|
94
106
|
if (!isValidCount(c)) {
|
|
95
107
|
throw new Error(
|
|
@@ -100,9 +112,9 @@ function _resolveCount(value) {
|
|
|
100
112
|
return c;
|
|
101
113
|
}
|
|
102
114
|
|
|
103
|
-
function _assertValidPair(presence, count) {
|
|
115
|
+
function _assertValidPair(presence: PartitivePresence, count: PartitiveCount): void {
|
|
104
116
|
// Delegate to the multiplicity SSOT. The canonical error string
|
|
105
|
-
// lives in one place: multiplicity.
|
|
117
|
+
// lives in one place: multiplicity.ts. Calling multiplicityFromPair
|
|
106
118
|
// throws on the invalid combination; we don't need to duplicate the
|
|
107
119
|
// error message here.
|
|
108
120
|
multiplicityFromPair(presence, count);
|
|
@@ -21,18 +21,34 @@
|
|
|
21
21
|
// The abstract base (AbstractHyperedge) has no wireKey and is skipped
|
|
22
22
|
// by register() — it cannot be instantiated through the registry.
|
|
23
23
|
|
|
24
|
+
import type { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Shape a registered hyperedge class must satisfy. Every leaf class
|
|
28
|
+
* in src/models/ matches this shape (declared as static fields).
|
|
29
|
+
*/
|
|
30
|
+
export interface HyperedgeClass {
|
|
31
|
+
readonly wireKey: string;
|
|
32
|
+
readonly typeTag: string;
|
|
33
|
+
readonly rdfType: string;
|
|
34
|
+
readonly memberClass: new (...args: never[]) => unknown;
|
|
35
|
+
readonly v1WireKeys?: ReadonlyArray<string>;
|
|
36
|
+
readonly kindLabel?: string;
|
|
37
|
+
readonly name: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
class HyperedgeRegistry {
|
|
25
|
-
static _byWireKey = new Map();
|
|
26
|
-
static _byTypeTag = new Map();
|
|
27
|
-
static _byRdfType = new Map();
|
|
41
|
+
static _byWireKey: Map<string, HyperedgeClass> = new Map();
|
|
42
|
+
static _byTypeTag: Map<string, HyperedgeClass> = new Map();
|
|
43
|
+
static _byRdfType: Map<string, HyperedgeClass> = new Map();
|
|
28
44
|
|
|
29
|
-
static register(cls) {
|
|
45
|
+
static register(cls: HyperedgeClass | undefined): boolean {
|
|
30
46
|
if (!cls || !cls.wireKey || !cls.typeTag || !cls.rdfType) return false;
|
|
31
47
|
if (this._byWireKey.has(cls.wireKey)) {
|
|
32
48
|
throw new Error(
|
|
33
49
|
`HyperedgeRegistry: duplicate wireKey '${cls.wireKey}' ` +
|
|
34
50
|
`(trying to register ${cls.name}; already registered to ` +
|
|
35
|
-
`${this._byWireKey.get(cls.wireKey)
|
|
51
|
+
`${this._byWireKey.get(cls.wireKey)?.name})`,
|
|
36
52
|
);
|
|
37
53
|
}
|
|
38
54
|
if (this._byTypeTag.has(cls.typeTag)) {
|
|
@@ -53,9 +69,11 @@ class HyperedgeRegistry {
|
|
|
53
69
|
return true;
|
|
54
70
|
}
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Test-only. Removes a class from all three indexes. Used by the OCP
|
|
74
|
+
* spec to clean up mock types it adds during a test run.
|
|
75
|
+
*/
|
|
76
|
+
static unregister(cls: HyperedgeClass | undefined): boolean {
|
|
59
77
|
if (!cls) return false;
|
|
60
78
|
const r1 = this._byWireKey.delete(cls.wireKey);
|
|
61
79
|
const r2 = this._byTypeTag.delete(cls.typeTag);
|
|
@@ -63,14 +81,20 @@ class HyperedgeRegistry {
|
|
|
63
81
|
return r1 || r2 || r3;
|
|
64
82
|
}
|
|
65
83
|
|
|
66
|
-
static forWireKey(key)
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
static forWireKey(key: string): HyperedgeClass | null {
|
|
85
|
+
return this._byWireKey.get(key) ?? null;
|
|
86
|
+
}
|
|
87
|
+
static forTypeTag(tag: string): HyperedgeClass | null {
|
|
88
|
+
return this._byTypeTag.get(tag) ?? null;
|
|
89
|
+
}
|
|
90
|
+
static forRdfType(type: string): HyperedgeClass | null {
|
|
91
|
+
return this._byRdfType.get(type) ?? null;
|
|
92
|
+
}
|
|
69
93
|
|
|
70
|
-
static allClasses() {
|
|
94
|
+
static allClasses(): HyperedgeClass[] {
|
|
71
95
|
// Dedup in case the same class is reachable via multiple indexes.
|
|
72
|
-
const seen = new Set();
|
|
73
|
-
const out = [];
|
|
96
|
+
const seen = new Set<HyperedgeClass>();
|
|
97
|
+
const out: HyperedgeClass[] = [];
|
|
74
98
|
for (const cls of this._byTypeTag.values()) {
|
|
75
99
|
if (!seen.has(cls)) {
|
|
76
100
|
seen.add(cls);
|
|
@@ -80,14 +104,16 @@ class HyperedgeRegistry {
|
|
|
80
104
|
return out;
|
|
81
105
|
}
|
|
82
106
|
|
|
83
|
-
static allWireKeys()
|
|
84
|
-
static allTypeTags()
|
|
107
|
+
static allWireKeys(): string[] { return [...this._byWireKey.keys()]; }
|
|
108
|
+
static allTypeTags(): string[] { return [...this._byTypeTag.keys()]; }
|
|
85
109
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Snapshot of every registered wire-shape identifier (v2 wireKey plus
|
|
112
|
+
* any v1 legacy keys). Used by the parser to reserve STRUCTURAL_KEYS
|
|
113
|
+
* and to know which top-level YAML keys to walk for hyperedge data.
|
|
114
|
+
*/
|
|
115
|
+
static allWireAndLegacyKeys(): string[] {
|
|
116
|
+
const out: string[] = [];
|
|
91
117
|
for (const cls of this.allClasses()) {
|
|
92
118
|
out.push(cls.wireKey);
|
|
93
119
|
for (const k of cls.v1WireKeys ?? []) out.push(k);
|
|
@@ -96,15 +122,19 @@ class HyperedgeRegistry {
|
|
|
96
122
|
}
|
|
97
123
|
}
|
|
98
124
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Helper: partition a unified hyperedge array by wire key. Used by
|
|
127
|
+
* Concept.toJSON, ConceptSerializer, and diff-patch to emit per-type
|
|
128
|
+
* wire keys without per-type branching in those files. Adding a new
|
|
129
|
+
* hyperedge type "just works" because the group is keyed by the
|
|
130
|
+
* class's static wireKey.
|
|
131
|
+
*/
|
|
132
|
+
export function groupHyperedgesByWireKey(
|
|
133
|
+
hyperedges: ReadonlyArray<AbstractHyperedge> | null | undefined,
|
|
134
|
+
): Record<string, AbstractHyperedge[]> {
|
|
135
|
+
const out: Record<string, AbstractHyperedge[]> = {};
|
|
106
136
|
for (const h of hyperedges ?? []) {
|
|
107
|
-
const key = h?.constructor?.wireKey;
|
|
137
|
+
const key = (h?.constructor as unknown as HyperedgeClass | undefined)?.wireKey;
|
|
108
138
|
if (!key) continue;
|
|
109
139
|
if (!out[key]) out[key] = [];
|
|
110
140
|
out[key].push(h);
|
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
import { GlossaristModel } from './base.js';
|
|
2
2
|
|
|
3
|
+
export interface LocalityJson {
|
|
4
|
+
type?: string | null;
|
|
5
|
+
reference_from?: string | null;
|
|
6
|
+
referenceFrom?: string | null;
|
|
7
|
+
reference_to?: string | null;
|
|
8
|
+
referenceTo?: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
export class Locality extends GlossaristModel {
|
|
4
|
-
|
|
12
|
+
readonly type: string | null;
|
|
13
|
+
readonly referenceFrom: string | null;
|
|
14
|
+
readonly referenceTo: string | null;
|
|
15
|
+
|
|
16
|
+
constructor(data: LocalityJson = {}) {
|
|
5
17
|
super();
|
|
6
18
|
this.type = data.type ?? null;
|
|
7
19
|
this.referenceFrom = data.reference_from ?? data.referenceFrom ?? null;
|
|
8
20
|
this.referenceTo = data.reference_to ?? data.referenceTo ?? null;
|
|
9
21
|
}
|
|
10
22
|
|
|
11
|
-
toJSON() {
|
|
12
|
-
const obj = {};
|
|
23
|
+
override toJSON(): LocalityJson {
|
|
24
|
+
const obj: LocalityJson = {};
|
|
13
25
|
if (this.type != null) obj.type = this.type;
|
|
14
26
|
if (this.referenceFrom != null) obj.reference_from = this.referenceFrom;
|
|
15
27
|
if (this.referenceTo != null) obj.reference_to = this.referenceTo;
|
|
16
28
|
return obj;
|
|
17
29
|
}
|
|
18
30
|
|
|
19
|
-
static fromJSON(data) {
|
|
31
|
+
static override fromJSON(data: LocalityJson): Locality {
|
|
20
32
|
return new Locality(data);
|
|
21
33
|
}
|
|
22
34
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const FALLBACK_LANG = 'eng';
|
|
2
|
+
|
|
3
|
+
export type LocalizedStringHash = Readonly<Record<string, string>>;
|
|
4
|
+
|
|
5
|
+
export function fetchLocalizedString(
|
|
6
|
+
hash: LocalizedStringHash | null | undefined,
|
|
7
|
+
lang: string,
|
|
8
|
+
fallback: string | null = FALLBACK_LANG,
|
|
9
|
+
): string | null {
|
|
10
|
+
if (hash == null || typeof hash !== 'object') return null;
|
|
11
|
+
const direct = hash[lang] ?? hash[String(lang)];
|
|
12
|
+
if (direct != null) return direct;
|
|
13
|
+
if (fallback != null && fallback !== lang) {
|
|
14
|
+
return hash[fallback] ?? hash[String(fallback)] ?? null;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function localizedStringIsEmpty(
|
|
20
|
+
hash: LocalizedStringHash | null | undefined,
|
|
21
|
+
): boolean {
|
|
22
|
+
return (
|
|
23
|
+
hash == null ||
|
|
24
|
+
(typeof hash === 'object' && Object.keys(hash).length === 0)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function localizedStringIsPresent(
|
|
29
|
+
hash: LocalizedStringHash | null | undefined,
|
|
30
|
+
): boolean {
|
|
31
|
+
return !localizedStringIsEmpty(hash);
|
|
32
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// PartitiveHyperedge — an ISO 704:2022 / ISO 1087-1 / ISO 12620
|
|
2
|
+
// partitive relation connecting a comprehensive concept
|
|
3
|
+
// (superordinate concept partitive) to two or more partitive
|
|
4
|
+
// concepts (subordinate concepts partitive) which fitted together
|
|
5
|
+
// constitute the comprehensive.
|
|
6
|
+
//
|
|
7
|
+
// Extends AbstractHyperedge; the leaf-specific concern is the
|
|
8
|
+
// PartitiveMember type for `members`. The wire-level field name
|
|
9
|
+
// stays `partitives` for v1/v2 backward compat.
|
|
10
|
+
|
|
11
|
+
import { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
12
|
+
import type { AbstractHyperedgeJson } from './abstract-hyperedge.js';
|
|
13
|
+
import { PartitiveMember } from './partitive-member.js';
|
|
14
|
+
import type { PartitiveMemberJson } from './partitive-member.js';
|
|
15
|
+
import { HyperedgeRegistry } from './hyperedge-registry.js';
|
|
16
|
+
|
|
17
|
+
export interface PartitiveHyperedgeJson extends AbstractHyperedgeJson {
|
|
18
|
+
members: ReadonlyArray<PartitiveMemberJson | PartitiveMember>;
|
|
19
|
+
/** PartitiveHyperedge wire alias of `members`. Both emitted for
|
|
20
|
+
* backward compat with consumers that read `partitives`. */
|
|
21
|
+
partitives?: ReadonlyArray<PartitiveMemberJson | PartitiveMember>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class PartitiveHyperedge extends AbstractHyperedge {
|
|
25
|
+
// Per-class metadata block — the OCP contract. External systems
|
|
26
|
+
// (parser, serializer, diff, RDF emitter, validators) read from
|
|
27
|
+
// these statics and the HyperedgeRegistry. Adding a new hyperedge
|
|
28
|
+
// type = declaring these fields on a new subclass + registering
|
|
29
|
+
// it. Nothing else in the codebase changes.
|
|
30
|
+
static wireKey = 'partitive_relations' as const;
|
|
31
|
+
static typeTag = 'partitive_relation' as const;
|
|
32
|
+
static rdfType = 'gloss:PartitiveRelation' as const;
|
|
33
|
+
static memberClass = PartitiveMember;
|
|
34
|
+
static v1WireKeys = ['partitive_hyperedges'];
|
|
35
|
+
static kindLabel = 'PART';
|
|
36
|
+
|
|
37
|
+
declare readonly members: ReadonlyArray<PartitiveMember>;
|
|
38
|
+
|
|
39
|
+
constructor(data: Partial<PartitiveHyperedgeJson> = {}) {
|
|
40
|
+
const memberSource = data.members ?? data.partitives;
|
|
41
|
+
const normalized: AbstractHyperedgeJson = {
|
|
42
|
+
comprehensive: data.comprehensive ?? {},
|
|
43
|
+
members: memberSource ?? [],
|
|
44
|
+
completeness: data.completeness ?? 'complete',
|
|
45
|
+
criterion: data.criterion,
|
|
46
|
+
sources: data.sources,
|
|
47
|
+
notes: data.notes,
|
|
48
|
+
status: data.status,
|
|
49
|
+
};
|
|
50
|
+
super(normalized);
|
|
51
|
+
this.members = (Array.isArray(memberSource) ? memberSource : []).map((m) =>
|
|
52
|
+
m instanceof PartitiveMember ? m : new PartitiveMember(m as PartitiveMemberJson),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get partitives(): ReadonlyArray<PartitiveMember> { return this.members; }
|
|
57
|
+
|
|
58
|
+
override toJSON(): PartitiveHyperedgeJson {
|
|
59
|
+
const base = super.toJSON() as Omit<PartitiveHyperedgeJson, 'partitives'>;
|
|
60
|
+
const { members, ...rest } = base;
|
|
61
|
+
return { ...rest, members: members as ReadonlyArray<PartitiveMemberJson | PartitiveMember>, partitives: members };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static identityOf(
|
|
65
|
+
value: {
|
|
66
|
+
comprehensive?: { source?: string | null; id?: string | null } | null;
|
|
67
|
+
partitives?: ReadonlyArray<{ ref?: { source?: string | null; id?: string | null } | null } | null>;
|
|
68
|
+
members?: ReadonlyArray<{ ref?: { source?: string | null; id?: string | null } | null } | null>;
|
|
69
|
+
} | null | undefined,
|
|
70
|
+
): string {
|
|
71
|
+
const v = value ?? {};
|
|
72
|
+
const c = v.comprehensive ?? {};
|
|
73
|
+
const source = v.partitives ?? v.members ?? [];
|
|
74
|
+
const partKeys = source
|
|
75
|
+
.map((p) => {
|
|
76
|
+
const r = p?.ref ?? (p as { source?: string; id?: string } | null) ?? {};
|
|
77
|
+
return `${r.source ?? ''}:${r.id ?? ''}`;
|
|
78
|
+
})
|
|
79
|
+
.sort();
|
|
80
|
+
return `${c.source ?? ''}:${c.id ?? ''}|${partKeys.join('|')}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static override fromJSON(data: PartitiveHyperedgeJson): PartitiveHyperedge {
|
|
84
|
+
return new PartitiveHyperedge(data);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default PartitiveHyperedge;
|
|
89
|
+
|
|
90
|
+
HyperedgeRegistry.register(PartitiveHyperedge);
|
|
@@ -13,36 +13,37 @@
|
|
|
13
13
|
// HyperedgeMember, because it is partitive-specific. GenericMember
|
|
14
14
|
// carries `delimitingCharacteristic` instead (the intension-difference
|
|
15
15
|
// value per §5.5.4.2.1, which always applies to every generic member).
|
|
16
|
-
//
|
|
17
|
-
// Matches concept-model/models/concepts/PartitiveMember.lutaml exactly:
|
|
18
|
-
// +ref — ConceptRef [1]
|
|
19
|
-
// +presence — PartitivePresence [0..1] (default: required)
|
|
20
|
-
// +count — PartitiveCount [0..1] (default: exactly_one)
|
|
21
|
-
// +is_delimiting — Boolean [0..1] (default: false)
|
|
22
|
-
//
|
|
23
|
-
// The combination (optional, at_least_one) is invalid: if a part is
|
|
24
|
-
// optional, the at-least-one constraint is vacuous and collapses to
|
|
25
|
-
// (optional, multiple). Rejected at construction by HyperedgeMember's
|
|
26
|
-
// delegate to multiplicityFromPair.
|
|
27
16
|
|
|
28
17
|
import { HyperedgeMember } from './hyperedge-member.js';
|
|
18
|
+
import type { HyperedgeMemberJson } from './hyperedge-member.js';
|
|
19
|
+
|
|
20
|
+
export interface PartitiveMemberJson extends HyperedgeMemberJson {
|
|
21
|
+
is_delimiting?: boolean;
|
|
22
|
+
}
|
|
29
23
|
|
|
30
24
|
export class PartitiveMember extends HyperedgeMember {
|
|
31
|
-
|
|
25
|
+
readonly is_delimiting: boolean;
|
|
26
|
+
|
|
27
|
+
constructor(data: PartitiveMemberJson = { ref: {} }) {
|
|
32
28
|
super(data);
|
|
33
29
|
this.is_delimiting = _resolveDelimiting(data.is_delimiting);
|
|
34
30
|
}
|
|
35
31
|
|
|
36
|
-
get isDelimiting() { return this.is_delimiting === true; }
|
|
32
|
+
get isDelimiting(): boolean { return this.is_delimiting === true; }
|
|
37
33
|
|
|
38
|
-
toJSON() {
|
|
34
|
+
override toJSON(): PartitiveMemberJson {
|
|
39
35
|
const base = super.toJSON();
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
const out: PartitiveMemberJson = { ...base };
|
|
37
|
+
if (this.is_delimiting) out.is_delimiting = true;
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static override fromJSON(data: PartitiveMemberJson): PartitiveMember {
|
|
42
|
+
return new PartitiveMember(data);
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
function _resolveDelimiting(value) {
|
|
46
|
+
function _resolveDelimiting(value: boolean | null | undefined): boolean {
|
|
46
47
|
if (value == null) return false;
|
|
47
48
|
if (typeof value !== 'boolean') {
|
|
48
49
|
throw new Error(
|