glossarist 0.4.34 → 0.4.36
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 +15 -6
- package/src/models/base.ts +93 -0
- package/src/models/{completeness.js → completeness.ts} +3 -6
- package/src/models/concept-date.ts +48 -0
- package/src/models/concept-ref.ts +64 -0
- package/src/models/concept-source.ts +96 -0
- package/src/models/concept-system-type.ts +39 -0
- package/src/models/concept-system.js +117 -0
- package/src/models/concept-type.ts +33 -0
- package/src/models/{dataset-color.js → dataset-color.ts} +26 -17
- package/src/models/{definition-type.js → definition-type.ts} +8 -1
- package/src/models/enum.ts +60 -0
- package/src/models/equivalence-degree.ts +34 -0
- package/src/models/{multiplicity.js → multiplicity.ts} +58 -29
- package/src/models/{partitive-count.js → partitive-count.ts} +3 -1
- package/src/models/partitive-enumeration.ts +27 -0
- package/src/models/{partitive-presence.js → partitive-presence.ts} +3 -1
- package/src/models/{plurality-marker.js → plurality-marker.ts} +14 -7
- package/src/models/sequential-hyperedge.js +56 -0
- package/src/models/sequential-member.js +13 -0
- package/src/models/base.js +0 -32
- package/src/models/concept-date.js +0 -36
- package/src/models/concept-ref.js +0 -36
- package/src/models/concept-source.js +0 -52
- package/src/models/enum.js +0 -42
- package/src/models/partitive-enumeration.js +0 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glossarist",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.36",
|
|
4
4
|
"description": "JavaScript SDK for Glossarist GCR packages — read, write, validate, and manage terminology concepts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -65,14 +65,16 @@
|
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
67
|
"lint": "eslint src/ test/",
|
|
68
|
+
"typecheck": "tsc --noEmit",
|
|
69
|
+
"build": "tsc",
|
|
68
70
|
"sync:model": "node scripts/sync-concept-model.mjs",
|
|
69
71
|
"gen:predicates": "node scripts/gen-predicates.mjs",
|
|
70
72
|
"prebuild": "npm run gen:predicates",
|
|
71
73
|
"pretest": "node test/fixtures/build-fixtures.js",
|
|
72
|
-
"test": "find test -name '*.test.js' | sort | xargs node --test",
|
|
73
|
-
"test:verbose": "find test -name '*.test.js' | sort | xargs node --test --test-reporter spec",
|
|
74
|
-
"test:coverage": "find test -name '*.test.js' | sort | xargs node --test --experimental-test-coverage",
|
|
75
|
-
"prepublishOnly": "npm test"
|
|
74
|
+
"test": "find test \\( -name '*.test.js' -o -name '*.test.ts' \\) | sort | xargs node --import tsx --test",
|
|
75
|
+
"test:verbose": "find test \\( -name '*.test.js' -o -name '*.test.ts' \\) | sort | xargs node --import tsx --test --test-reporter spec",
|
|
76
|
+
"test:coverage": "find test \\( -name '*.test.js' -o -name '*.test.ts' \\) | sort | xargs node --import tsx --test --experimental-test-coverage",
|
|
77
|
+
"prepublishOnly": "npm run typecheck && npm test"
|
|
76
78
|
},
|
|
77
79
|
"keywords": [
|
|
78
80
|
"glossarist",
|
|
@@ -108,7 +110,14 @@
|
|
|
108
110
|
},
|
|
109
111
|
"devDependencies": {
|
|
110
112
|
"@eslint/js": "^10.0.1",
|
|
113
|
+
"@rdfjs/types": "^2.0.1",
|
|
114
|
+
"@types/node": "^26.1.2",
|
|
115
|
+
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
|
116
|
+
"@typescript-eslint/parser": "^8.65.0",
|
|
111
117
|
"eslint": "^10.3.0",
|
|
112
|
-
"globals": "^17.6.0"
|
|
118
|
+
"globals": "^17.6.0",
|
|
119
|
+
"tsx": "^4.23.1",
|
|
120
|
+
"typescript": "^6.0.3",
|
|
121
|
+
"typescript-eslint": "^8.65.0"
|
|
113
122
|
}
|
|
114
123
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GlossaristModel — abstract base for every domain model.
|
|
3
|
+
*
|
|
4
|
+
* Subclasses implement `toJSON()` (wire shape) and `identity()`
|
|
5
|
+
* (stable string key for diff/dedupe). `equals()` and `clone()`
|
|
6
|
+
* default to comparing via JSON serialization; subclasses may
|
|
7
|
+
* override for performance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export abstract class GlossaristModel {
|
|
11
|
+
/** Wire-shape serialization. Subclasses MUST override. */
|
|
12
|
+
toJSON(): object {
|
|
13
|
+
throw new Error(`${this.constructor.name} must implement toJSON()`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Stable identity string for diff, dedupe, registry keys. */
|
|
17
|
+
identity(): string {
|
|
18
|
+
throw new Error(`${this.constructor.name} must implement identity()`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Default: structural equality via JSON. Override for semantic equality. */
|
|
22
|
+
equals(other: this): boolean {
|
|
23
|
+
if (!(other instanceof this.constructor)) return false;
|
|
24
|
+
return JSON.stringify(this.toJSON()) === JSON.stringify(other.toJSON());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Deep clone via JSON round-trip. Subclasses with non-JSON-safe
|
|
28
|
+
* fields must override. */
|
|
29
|
+
clone(): this {
|
|
30
|
+
const Ctor = this.constructor as new (data: unknown) => this;
|
|
31
|
+
return new Ctor(JSON.parse(JSON.stringify(this.toJSON())));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Subclasses MUST override. Declared on the abstract base so callers
|
|
36
|
+
* who only have `typeof GlossaristModel` get a typed call site
|
|
37
|
+
* (rather than the static disappearing under `abstract`).
|
|
38
|
+
*/
|
|
39
|
+
static fromJSON(_data: unknown): GlossaristModel {
|
|
40
|
+
throw new Error(`${this.name} must implement fromJSON()`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Lazily wrap a raw array (plain hashes) into model instances.
|
|
45
|
+
* Used by Concept's lazy `figures`/`tables`/`formulas` getters so
|
|
46
|
+
* the wrap cost is only paid on first access.
|
|
47
|
+
*
|
|
48
|
+
* `cacheKey` and `rawKey` are private field names on `this`. The
|
|
49
|
+
* bracket access here is the only legitimate reflective pattern in
|
|
50
|
+
* the codebase (lazy field init). It is bracket-typed through a
|
|
51
|
+
* local alias rather than the public surface.
|
|
52
|
+
*
|
|
53
|
+
* Underscore-prefixed (not `protected`) so JS subclasses during the
|
|
54
|
+
* TS migration transition can call it without TS4094 on anonymous
|
|
55
|
+
* class expressions. Once all subclasses are .ts this can move to
|
|
56
|
+
* `protected`.
|
|
57
|
+
*/
|
|
58
|
+
_lazy<TItem>(
|
|
59
|
+
cacheKey: string,
|
|
60
|
+
rawKey: string,
|
|
61
|
+
wrapFn: (raw: unknown) => TItem,
|
|
62
|
+
): TItem[] {
|
|
63
|
+
const cache = this as unknown as Record<string, TItem[] | null | undefined>;
|
|
64
|
+
const raw = this as unknown as Record<string, unknown[] | undefined>;
|
|
65
|
+
if (cache[cacheKey] === null || cache[cacheKey] === undefined) {
|
|
66
|
+
const rawArr = raw[rawKey] ?? [];
|
|
67
|
+
cache[cacheKey] = rawArr.map((r) => wrapFn(r));
|
|
68
|
+
}
|
|
69
|
+
return cache[cacheKey] ?? [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Helper for `toJSON()`: serialize a lazy-or-raw array under
|
|
74
|
+
* `jsonKey` only when it has items. Model instances go through
|
|
75
|
+
* `toJSON()`; primitives pass through.
|
|
76
|
+
*/
|
|
77
|
+
_serialize(
|
|
78
|
+
obj: Record<string, unknown>,
|
|
79
|
+
jsonKey: string,
|
|
80
|
+
cacheKey: string,
|
|
81
|
+
rawKey: string,
|
|
82
|
+
): void {
|
|
83
|
+
const self = this as unknown as Record<string, unknown[] | null | undefined>;
|
|
84
|
+
const cached = self[cacheKey];
|
|
85
|
+
const raw = self[rawKey];
|
|
86
|
+
const items = cached ?? (Array.isArray(raw) && raw.length > 0 ? raw : []);
|
|
87
|
+
if (items.length > 0) {
|
|
88
|
+
obj[jsonKey] = items.map((i) =>
|
|
89
|
+
i instanceof GlossaristModel ? i.toJSON() : i,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -5,14 +5,11 @@
|
|
|
5
5
|
// ISO 704 depicts this as the rake's backline: a backline ending with
|
|
6
6
|
// a tooth is complete; a backline continuing without a tooth is
|
|
7
7
|
// partial.
|
|
8
|
-
//
|
|
9
|
-
// Renamed from the v1 `PartitiveEnumeration` (closed|open) per
|
|
10
|
-
// TODO.partitive-relation-v2 item 02. The semantic claim being
|
|
11
|
-
// expressed is completeness of the decomposition, not "enumeration"
|
|
12
|
-
// (which is a neutral-sounding word that doesn't convey the claim).
|
|
13
8
|
|
|
14
9
|
import { makeEnum } from './enum.js';
|
|
15
10
|
|
|
11
|
+
export type Completeness = 'complete' | 'partial';
|
|
12
|
+
|
|
16
13
|
export const COMPLETENESS = makeEnum('COMPLETENESS', {
|
|
17
14
|
COMPLETE: 'complete',
|
|
18
15
|
PARTIAL: 'partial',
|
|
@@ -21,4 +18,4 @@ export const COMPLETENESS = makeEnum('COMPLETENESS', {
|
|
|
21
18
|
export const COMPLETENESS_VALUES = COMPLETENESS.VALUES;
|
|
22
19
|
export const isValidCompleteness = COMPLETENESS.isValid;
|
|
23
20
|
|
|
24
|
-
export const DEFAULT_COMPLETENESS = COMPLETENESS.COMPLETE;
|
|
21
|
+
export const DEFAULT_COMPLETENESS: Completeness = COMPLETENESS.COMPLETE;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { GlossaristModel } from './base.js';
|
|
2
|
+
|
|
3
|
+
export type ConceptDateType = 'accepted' | 'amended' | 'retired' | string;
|
|
4
|
+
|
|
5
|
+
export const DATE_TYPES: ReadonlyArray<string> = Object.freeze([
|
|
6
|
+
'accepted',
|
|
7
|
+
'amended',
|
|
8
|
+
'retired',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export interface ConceptDateJson {
|
|
12
|
+
date?: string | null;
|
|
13
|
+
type?: ConceptDateType | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ConceptDate extends GlossaristModel {
|
|
17
|
+
readonly date: string | null;
|
|
18
|
+
readonly type: ConceptDateType | null;
|
|
19
|
+
|
|
20
|
+
constructor(data: ConceptDateJson = {}) {
|
|
21
|
+
super();
|
|
22
|
+
this.date = data.date ?? null;
|
|
23
|
+
this.type = data.type ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get parsedDate(): Date | null {
|
|
27
|
+
return this.date ? new Date(this.date) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static identityOf(value: { type?: ConceptDateType | null } | null | undefined): string {
|
|
31
|
+
return value?.type ?? '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override identity(): string {
|
|
35
|
+
return ConceptDate.identityOf(this);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override toJSON(): ConceptDateJson {
|
|
39
|
+
const obj: ConceptDateJson = {};
|
|
40
|
+
if (this.date != null) obj.date = this.date;
|
|
41
|
+
if (this.type != null) obj.type = this.type;
|
|
42
|
+
return obj;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static override fromJSON(data: ConceptDateJson): ConceptDate {
|
|
46
|
+
return new ConceptDate(data);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { GlossaristModel } from './base.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wire shape for ConceptRef. All three fields are optional on input
|
|
5
|
+
* (any one is enough to identify a concept; usually source+id pair).
|
|
6
|
+
* `text` is a free-form fallback for references that have no
|
|
7
|
+
* machine-readable source.
|
|
8
|
+
*/
|
|
9
|
+
export interface ConceptRefJson {
|
|
10
|
+
source?: string | null;
|
|
11
|
+
id?: string | null;
|
|
12
|
+
text?: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ConceptRef — a reference to a concept by (source, id) pair, or by
|
|
17
|
+
* free-form text when no machine-readable source exists.
|
|
18
|
+
*
|
|
19
|
+
* Identity: `${source ?? ''}|${id ?? ''}`. Two refs with the same
|
|
20
|
+
* (source, id) are equal regardless of `text`.
|
|
21
|
+
*/
|
|
22
|
+
export class ConceptRef extends GlossaristModel {
|
|
23
|
+
readonly source: string | null;
|
|
24
|
+
readonly id: string | null;
|
|
25
|
+
readonly text: string | null;
|
|
26
|
+
|
|
27
|
+
constructor(data: ConceptRefJson = {}) {
|
|
28
|
+
super();
|
|
29
|
+
this.source = data.source ?? null;
|
|
30
|
+
this.id = data.id ?? null;
|
|
31
|
+
this.text = data.text ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override toString(): string {
|
|
35
|
+
const parts: string[] = [];
|
|
36
|
+
if (this.source) parts.push(this.source);
|
|
37
|
+
if (this.id) parts.push(this.id);
|
|
38
|
+
const base = parts.join(' ');
|
|
39
|
+
if (this.text && base) return `${base} (${this.text})`;
|
|
40
|
+
if (this.text) return this.text;
|
|
41
|
+
return base;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override toJSON(): ConceptRefJson {
|
|
45
|
+
const obj: ConceptRefJson = {};
|
|
46
|
+
if (this.source != null) obj.source = this.source;
|
|
47
|
+
if (this.id != null) obj.id = this.id;
|
|
48
|
+
if (this.text != null) obj.text = this.text;
|
|
49
|
+
return obj;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override identity(): string {
|
|
53
|
+
return `${this.source ?? ''}|${this.id ?? ''}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static override fromJSON(data: ConceptRefJson): ConceptRef {
|
|
57
|
+
return new ConceptRef(data);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Convenience constructor for the common `(source, id)` form. */
|
|
61
|
+
static fromPair(source: string, id: string): ConceptRef {
|
|
62
|
+
return new ConceptRef({ source, id });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { GlossaristModel } from './base.js';
|
|
2
|
+
import { Citation } from './citation.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Local Citation wire-shape alias. Until `citation.ts` lands (Phase
|
|
6
|
+
* 2b), Citation is untyped JS; this alias gives the cross-references
|
|
7
|
+
* a typed shape without forcing the conversion order. When
|
|
8
|
+
* `citation.ts` exports its own `CitationJson`, replace this with
|
|
9
|
+
* `import type { CitationJson } from './citation.js'`.
|
|
10
|
+
*/
|
|
11
|
+
export interface CitationJson {
|
|
12
|
+
ref?: { source?: string | null; id?: string | null; text?: string | null } | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ConceptSourceJson {
|
|
16
|
+
id?: string | null;
|
|
17
|
+
status?: string | null;
|
|
18
|
+
type?: string | null;
|
|
19
|
+
origin?: CitationJson | Citation | null;
|
|
20
|
+
modification?: string | null;
|
|
21
|
+
sourcedFrom?: ReadonlyArray<CitationJson | Citation> | null;
|
|
22
|
+
sourced_from?: ReadonlyArray<CitationJson | Citation> | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ConceptSource extends GlossaristModel {
|
|
26
|
+
readonly id: string | null;
|
|
27
|
+
readonly status: string | null;
|
|
28
|
+
readonly type: string | null;
|
|
29
|
+
readonly origin: Citation | null;
|
|
30
|
+
readonly modification: string | null;
|
|
31
|
+
readonly sourcedFrom: ReadonlyArray<Citation>;
|
|
32
|
+
|
|
33
|
+
constructor(data: ConceptSourceJson = {}) {
|
|
34
|
+
super();
|
|
35
|
+
this.id = data.id ?? null;
|
|
36
|
+
this.status = data.status ?? null;
|
|
37
|
+
this.type = data.type ?? null;
|
|
38
|
+
this.origin = data.origin
|
|
39
|
+
? data.origin instanceof Citation
|
|
40
|
+
? data.origin
|
|
41
|
+
: new Citation(data.origin)
|
|
42
|
+
: null;
|
|
43
|
+
this.modification = data.modification ?? null;
|
|
44
|
+
const sourcedRaw = data.sourcedFrom ?? data.sourced_from ?? [];
|
|
45
|
+
this.sourcedFrom = sourcedRaw.map((c) =>
|
|
46
|
+
c instanceof Citation ? c : new Citation(c),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Backward-compat alias for callers using the snake_case JS field
|
|
52
|
+
* name. Prefer `.sourcedFrom` going forward; the wire name remains
|
|
53
|
+
* `sourced_from` (see toJSON).
|
|
54
|
+
*/
|
|
55
|
+
get sourced_from(): ReadonlyArray<Citation> {
|
|
56
|
+
return this.sourcedFrom;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override toJSON(): ConceptSourceJson {
|
|
60
|
+
const obj: ConceptSourceJson = {};
|
|
61
|
+
if (this.id != null) obj.id = this.id;
|
|
62
|
+
if (this.status != null) obj.status = this.status;
|
|
63
|
+
if (this.type != null) obj.type = this.type;
|
|
64
|
+
if (this.origin != null) obj.origin = this.origin.toJSON();
|
|
65
|
+
if (this.modification != null) obj.modification = this.modification;
|
|
66
|
+
if (this.sourcedFrom.length > 0) {
|
|
67
|
+
obj.sourced_from = this.sourcedFrom.map((c) => c.toJSON());
|
|
68
|
+
}
|
|
69
|
+
return obj;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static identityOf(
|
|
73
|
+
value: {
|
|
74
|
+
type?: string | null;
|
|
75
|
+
origin?: CitationJson | Citation | { ref?: { source?: string; id?: string } | null } | null;
|
|
76
|
+
} | null | undefined,
|
|
77
|
+
): string {
|
|
78
|
+
const v = value ?? {};
|
|
79
|
+
const origin = v.origin as CitationJson | Citation | { ref?: { source?: string; id?: string } | null } | null | undefined;
|
|
80
|
+
const ref =
|
|
81
|
+
origin instanceof Citation
|
|
82
|
+
? origin.ref
|
|
83
|
+
: origin && typeof origin === 'object'
|
|
84
|
+
? (origin as { ref?: { source?: string; id?: string } | null }).ref
|
|
85
|
+
: null;
|
|
86
|
+
return `${v.type ?? ''}|${ref?.source ?? ''}|${ref?.id ?? ''}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override identity(): string {
|
|
90
|
+
return ConceptSource.identityOf(this);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static override fromJSON(data: ConceptSourceJson): ConceptSource {
|
|
94
|
+
return new ConceptSource(data);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ConceptSystemType — ISO 12620 A.7.1 concept system type enum.
|
|
2
|
+
//
|
|
3
|
+
// Permissible instances per ISO 12620:
|
|
4
|
+
// generic — species are part of the extension of the genus
|
|
5
|
+
// partitive — based on whole-part relations
|
|
6
|
+
// sequential — based on spatial and temporal relations
|
|
7
|
+
// associative — based on thematic or pragmatic relations
|
|
8
|
+
// mixed — two or more of the above (the realistic case)
|
|
9
|
+
|
|
10
|
+
export type ConceptSystemType =
|
|
11
|
+
| 'generic'
|
|
12
|
+
| 'partitive'
|
|
13
|
+
| 'sequential'
|
|
14
|
+
| 'associative'
|
|
15
|
+
| 'mixed';
|
|
16
|
+
|
|
17
|
+
export const CONCEPT_SYSTEM_TYPE: Readonly<
|
|
18
|
+
Record<'GENERIC' | 'PARTITIVE' | 'SEQUENTIAL' | 'ASSOCIATIVE' | 'MIXED', ConceptSystemType>
|
|
19
|
+
> = Object.freeze({
|
|
20
|
+
GENERIC: 'generic',
|
|
21
|
+
PARTITIVE: 'partitive',
|
|
22
|
+
SEQUENTIAL: 'sequential',
|
|
23
|
+
ASSOCIATIVE: 'associative',
|
|
24
|
+
MIXED: 'mixed',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const CONCEPT_SYSTEM_TYPE_VALUES: ReadonlyArray<ConceptSystemType> =
|
|
28
|
+
Object.freeze(Object.values(CONCEPT_SYSTEM_TYPE));
|
|
29
|
+
|
|
30
|
+
export function isValidConceptSystemType(
|
|
31
|
+
value: unknown,
|
|
32
|
+
): value is ConceptSystemType {
|
|
33
|
+
return (
|
|
34
|
+
typeof value === 'string' &&
|
|
35
|
+
(CONCEPT_SYSTEM_TYPE_VALUES as readonly string[]).includes(value)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default CONCEPT_SYSTEM_TYPE;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// ConceptSystem — first-class concept system entity per ISO 12620 A.7.
|
|
2
|
+
//
|
|
3
|
+
// Promotes the previously-implicit concept system (domains + tags +
|
|
4
|
+
// relation graph) to an explicit first-class entity. Per-file at
|
|
5
|
+
// `concept-systems/<id>.yaml`.
|
|
6
|
+
//
|
|
7
|
+
// See docs/design/concept-systems.md (concept-model repo).
|
|
8
|
+
|
|
9
|
+
import { GlossaristModel } from './base.js';
|
|
10
|
+
import { ConceptRef } from './concept-ref.js';
|
|
11
|
+
import {
|
|
12
|
+
CONCEPT_SYSTEM_TYPE,
|
|
13
|
+
CONCEPT_SYSTEM_TYPE_VALUES,
|
|
14
|
+
isValidConceptSystemType,
|
|
15
|
+
} from './concept-system-type.js';
|
|
16
|
+
|
|
17
|
+
export class ConceptSystem extends GlossaristModel {
|
|
18
|
+
constructor(data = {}) {
|
|
19
|
+
super();
|
|
20
|
+
this.id = _ensureNonEmptyString(data.id, 'id');
|
|
21
|
+
this.name = _ensureName(data.name);
|
|
22
|
+
this.type = _resolveType(data.type);
|
|
23
|
+
this.domain = data.domain ?? null;
|
|
24
|
+
this.purpose = data.purpose ?? null;
|
|
25
|
+
this.members = _ensureMembers(data.members);
|
|
26
|
+
this.hyperedges = Array.isArray(data.hyperedges) ? [...data.hyperedges] : [];
|
|
27
|
+
this.rootConcepts = Array.isArray(data.rootConcepts)
|
|
28
|
+
? data.rootConcepts.map(c => c instanceof ConceptRef ? c : new ConceptRef(c))
|
|
29
|
+
: [];
|
|
30
|
+
this.sources = Array.isArray(data.sources) ? [...data.sources] : [];
|
|
31
|
+
this.status = data.status ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get isGeneric() { return this.type === CONCEPT_SYSTEM_TYPE.GENERIC; }
|
|
35
|
+
get isPartitive() { return this.type === CONCEPT_SYSTEM_TYPE.PARTITIVE; }
|
|
36
|
+
get isSequential() { return this.type === CONCEPT_SYSTEM_TYPE.SEQUENTIAL; }
|
|
37
|
+
get isAssociative() { return this.type === CONCEPT_SYSTEM_TYPE.ASSOCIATIVE; }
|
|
38
|
+
get isMixed() { return this.type === CONCEPT_SYSTEM_TYPE.MIXED; }
|
|
39
|
+
|
|
40
|
+
hasHyperedge(edgeId) {
|
|
41
|
+
return this.hyperedges.includes(edgeId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
hasMember(refOrUri) {
|
|
45
|
+
const uri = typeof refOrUri === 'string' ? refOrUri : _refUri(refOrUri);
|
|
46
|
+
return this.members.some(m => _refUri(m) === uri);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
identity() {
|
|
50
|
+
return this.id;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toJSON() {
|
|
54
|
+
const obj = {
|
|
55
|
+
id: this.id,
|
|
56
|
+
name: { ...this.name },
|
|
57
|
+
type: this.type,
|
|
58
|
+
members: this.members.map(m => m.toJSON()),
|
|
59
|
+
};
|
|
60
|
+
if (this.domain != null) obj.domain = this.domain;
|
|
61
|
+
if (this.purpose != null) obj.purpose = { ...this.purpose };
|
|
62
|
+
if (this.hyperedges.length > 0) obj.hyperedges = [...this.hyperedges];
|
|
63
|
+
if (this.rootConcepts.length > 0) obj.rootConcepts = this.rootConcepts.map(c => c.toJSON());
|
|
64
|
+
if (this.sources.length > 0) obj.sources = [...this.sources];
|
|
65
|
+
if (this.status != null) obj.status = this.status;
|
|
66
|
+
return obj;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static fromJSON(data) {
|
|
70
|
+
return new ConceptSystem(data);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _ensureNonEmptyString(value, fieldName) {
|
|
75
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
76
|
+
throw new Error(`ConceptSystem#${fieldName} must be a non-empty string`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _ensureName(value) {
|
|
82
|
+
if (value == null) return {};
|
|
83
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
84
|
+
throw new Error('ConceptSystem#name must be a localized object (e.g. { eng: "..." })');
|
|
85
|
+
}
|
|
86
|
+
return { ...value };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _resolveType(value) {
|
|
90
|
+
if (value == null) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'ConceptSystem#type is required; one of ' +
|
|
93
|
+
CONCEPT_SYSTEM_TYPE_VALUES.join(', '),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (!isValidConceptSystemType(value)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`ConceptSystem#type ${JSON.stringify(value)} invalid; ` +
|
|
99
|
+
`must be one of: ${CONCEPT_SYSTEM_TYPE_VALUES.join(', ')}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _ensureMembers(values) {
|
|
106
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
107
|
+
throw new Error('ConceptSystem#members must be a non-empty array');
|
|
108
|
+
}
|
|
109
|
+
return values.map(v => v instanceof ConceptRef ? v : new ConceptRef(v));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _refUri(ref) {
|
|
113
|
+
const r = ref?.ref ?? ref ?? {};
|
|
114
|
+
return `${r.source ?? ''}:${r.id ?? ''}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default ConceptSystem;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ConceptType — ISO 704:2022 §5.2-5.3 general vs individual concept.
|
|
2
|
+
//
|
|
3
|
+
// general — concept corresponds to a category of objects
|
|
4
|
+
// (designated by terms or symbols)
|
|
5
|
+
// individual — concept corresponds to a unique object
|
|
6
|
+
// (designated by proper names / appellations)
|
|
7
|
+
//
|
|
8
|
+
// Individual concepts occupy the bottom rung of a concept ladder
|
|
9
|
+
// and cannot be subdivided into more specific concepts, but CAN be
|
|
10
|
+
// subdivided into parts via a partitive relation.
|
|
11
|
+
|
|
12
|
+
export type ConceptType = 'general' | 'individual';
|
|
13
|
+
|
|
14
|
+
export const CONCEPT_TYPE: Readonly<Record<'GENERAL' | 'INDIVIDUAL', ConceptType>> =
|
|
15
|
+
Object.freeze({
|
|
16
|
+
GENERAL: 'general',
|
|
17
|
+
INDIVIDUAL: 'individual',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const CONCEPT_TYPE_VALUES: ReadonlyArray<ConceptType> = Object.freeze(
|
|
21
|
+
Object.values(CONCEPT_TYPE),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_CONCEPT_TYPE: ConceptType = CONCEPT_TYPE.GENERAL;
|
|
25
|
+
|
|
26
|
+
export function isValidConceptType(value: unknown): value is ConceptType {
|
|
27
|
+
return (
|
|
28
|
+
typeof value === 'string' &&
|
|
29
|
+
(CONCEPT_TYPE_VALUES as readonly string[]).includes(value)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default CONCEPT_TYPE;
|
|
@@ -5,27 +5,34 @@
|
|
|
5
5
|
// - an explicit { light, dark } pair
|
|
6
6
|
//
|
|
7
7
|
// Round-trips through Register.toJSON unchanged so the data lives in
|
|
8
|
-
// the GCR package. Consumers call
|
|
8
|
+
// the GCR package. Consumers call resolveColor(mode) to pick the
|
|
9
9
|
// right hex for the current UI mode.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
* @typedef {string} DatasetColorSpec
|
|
13
|
-
* @typedef {{ light: string, dark: string }} DatasetColorPair
|
|
14
|
-
* @typedef {DatasetColorSpec | DatasetColorPair} DatasetColor
|
|
15
|
-
*/
|
|
11
|
+
export type ColorMode = 'light' | 'dark';
|
|
16
12
|
|
|
17
|
-
export
|
|
13
|
+
export type DatasetColorSpec = string;
|
|
14
|
+
|
|
15
|
+
export interface DatasetColorPair {
|
|
16
|
+
light?: string;
|
|
17
|
+
dark?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type DatasetColor = DatasetColorSpec | DatasetColorPair;
|
|
21
|
+
|
|
22
|
+
export const COLOR_MODES: ReadonlyArray<ColorMode> = Object.freeze([
|
|
23
|
+
'light',
|
|
24
|
+
'dark',
|
|
25
|
+
]);
|
|
18
26
|
|
|
19
27
|
/**
|
|
20
28
|
* Returns the right hex for the given mode. Falls back to the single
|
|
21
29
|
* hex when the spec is a string. Returns null when the spec is null
|
|
22
30
|
* or when the requested mode is missing from a pair.
|
|
23
|
-
*
|
|
24
|
-
* @param {DatasetColor | null | undefined} color
|
|
25
|
-
* @param {'light' | 'dark'} mode
|
|
26
|
-
* @returns {string | null}
|
|
27
31
|
*/
|
|
28
|
-
export function resolveColor(
|
|
32
|
+
export function resolveColor(
|
|
33
|
+
color: DatasetColor | null | undefined,
|
|
34
|
+
mode: ColorMode,
|
|
35
|
+
): string | null {
|
|
29
36
|
if (color == null) return null;
|
|
30
37
|
if (typeof color === 'string') return color;
|
|
31
38
|
if (typeof color === 'object') {
|
|
@@ -37,10 +44,10 @@ export function resolveColor(color, mode) {
|
|
|
37
44
|
|
|
38
45
|
/**
|
|
39
46
|
* True when the spec declares an explicit {light, dark} object.
|
|
40
|
-
* Lets callers distinguish "single hex" from "per-mode pair" without
|
|
41
|
-
* repeating the typeof check.
|
|
42
47
|
*/
|
|
43
|
-
export function isColorPair(
|
|
48
|
+
export function isColorPair(
|
|
49
|
+
color: DatasetColor | null | undefined,
|
|
50
|
+
): color is DatasetColorPair {
|
|
44
51
|
return color != null && typeof color === 'object';
|
|
45
52
|
}
|
|
46
53
|
|
|
@@ -52,7 +59,9 @@ export function isColorPair(color) {
|
|
|
52
59
|
*
|
|
53
60
|
* Returns null when valid, or an error message describing the issue.
|
|
54
61
|
*/
|
|
55
|
-
export function validateColor(
|
|
62
|
+
export function validateColor(
|
|
63
|
+
color: DatasetColor | null | undefined,
|
|
64
|
+
): string | null {
|
|
56
65
|
if (color == null) return null;
|
|
57
66
|
if (typeof color === 'string') return hexError(color);
|
|
58
67
|
if (typeof color === 'object') {
|
|
@@ -73,7 +82,7 @@ export function validateColor(color) {
|
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
const HEX_RE = /^#[0-9a-fA-F]{3,8}$/;
|
|
76
|
-
function hexError(s) {
|
|
85
|
+
function hexError(s: unknown): string | null {
|
|
77
86
|
if (typeof s !== 'string') return `expected string, got ${typeof s}`;
|
|
78
87
|
return HEX_RE.test(s) ? null : `not a valid hex color: ${JSON.stringify(s)}`;
|
|
79
88
|
}
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import { makeEnum } from './enum.js';
|
|
9
9
|
|
|
10
|
+
export type DefinitionType =
|
|
11
|
+
| 'intensional'
|
|
12
|
+
| 'extensional'
|
|
13
|
+
| 'partitive'
|
|
14
|
+
| 'translated';
|
|
15
|
+
|
|
10
16
|
export const DEFINITION_TYPE = makeEnum('DEFINITION_TYPE', {
|
|
11
17
|
INTENSIONAL: 'intensional',
|
|
12
18
|
EXTENSIONAL: 'extensional',
|
|
@@ -14,4 +20,5 @@ export const DEFINITION_TYPE = makeEnum('DEFINITION_TYPE', {
|
|
|
14
20
|
TRANSLATED: 'translated',
|
|
15
21
|
});
|
|
16
22
|
|
|
17
|
-
export const DEFAULT_DEFINITION_TYPE =
|
|
23
|
+
export const DEFAULT_DEFINITION_TYPE: DefinitionType =
|
|
24
|
+
DEFINITION_TYPE.INTENSIONAL;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Enum factory — produces a frozen KEY/VALUES bundle, a frozen values
|
|
2
|
+
// array, and an isValid predicate from a single `makeEnum` call.
|
|
3
|
+
//
|
|
4
|
+
// Adding a new enum means one line:
|
|
5
|
+
//
|
|
6
|
+
// export const COLOR = makeEnum('COLOR', { RED: 'red', BLUE: 'blue' });
|
|
7
|
+
//
|
|
8
|
+
// The factory returns an object that exposes both the per-value
|
|
9
|
+
// constants (COLOR.RED, COLOR.BLUE) and the VALUES list + isValid
|
|
10
|
+
// predicate. The bundle is itself frozen.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Output of {@link makeEnum}. Captures both the original key→value
|
|
14
|
+
* pairs (so `MY_ENUM.MY_KEY` is the literal string value) and the
|
|
15
|
+
* helper properties (`name`, `VALUES`, `isValid`).
|
|
16
|
+
*/
|
|
17
|
+
export type EnumBundle<TValues extends Readonly<Record<string, string>>> = {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly VALUES: ReadonlyArray<TValues[keyof TValues]>;
|
|
20
|
+
readonly isValid: (value: unknown) => value is TValues[keyof TValues];
|
|
21
|
+
} & { readonly [K in keyof TValues]: TValues[K] };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build a frozen enum bundle.
|
|
25
|
+
*
|
|
26
|
+
* @param name Human-readable name used in error messages.
|
|
27
|
+
* @param values Object whose keys are the constant names and whose
|
|
28
|
+
* string values are the wire representations.
|
|
29
|
+
*/
|
|
30
|
+
export function makeEnum<const TValues extends Readonly<Record<string, string>>>(
|
|
31
|
+
name: string,
|
|
32
|
+
values: TValues,
|
|
33
|
+
): EnumBundle<TValues> {
|
|
34
|
+
if (typeof name !== 'string') {
|
|
35
|
+
throw new Error(`makeEnum: name must be a string, got ${typeof name}`);
|
|
36
|
+
}
|
|
37
|
+
if (typeof values !== 'object' || values === null) {
|
|
38
|
+
throw new Error(`makeEnum: values must be an object, got ${typeof values}`);
|
|
39
|
+
}
|
|
40
|
+
for (const [k, v] of Object.entries(values)) {
|
|
41
|
+
if (typeof v !== 'string') {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`makeEnum(${name}): value for ${k} must be a string, got ${typeof v}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const frozen = Object.freeze({ ...values });
|
|
49
|
+
const valuesArr = Object.freeze(Object.values(frozen)) as ReadonlyArray<
|
|
50
|
+
TValues[keyof TValues]
|
|
51
|
+
>;
|
|
52
|
+
const isValid = (v: unknown): v is TValues[keyof TValues] =>
|
|
53
|
+
typeof v === 'string' && (valuesArr as readonly string[]).includes(v);
|
|
54
|
+
|
|
55
|
+
return Object.assign(Object.create(null) as object, frozen, {
|
|
56
|
+
name,
|
|
57
|
+
VALUES: valuesArr,
|
|
58
|
+
isValid,
|
|
59
|
+
}) as EnumBundle<TValues>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// EquivalenceDegree — ISO 704:2022 §7.7.3 cross-language equivalence.
|
|
2
|
+
//
|
|
3
|
+
// full — equivalent intension across languages (default)
|
|
4
|
+
// partial — overlapping but not identical intension
|
|
5
|
+
// none — no corresponding concept in this language
|
|
6
|
+
// directional — monodirectional equivalence (see equivalence_note)
|
|
7
|
+
|
|
8
|
+
export type EquivalenceDegree =
|
|
9
|
+
| 'full'
|
|
10
|
+
| 'partial'
|
|
11
|
+
| 'none'
|
|
12
|
+
| 'directional';
|
|
13
|
+
|
|
14
|
+
export const EQUIVALENCE_DEGREE: Readonly<Record<Uppercase<EquivalenceDegree>, EquivalenceDegree>> = Object.freeze({
|
|
15
|
+
FULL: 'full',
|
|
16
|
+
PARTIAL: 'partial',
|
|
17
|
+
NONE: 'none',
|
|
18
|
+
DIRECTIONAL: 'directional',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const EQUIVALENCE_DEGREE_VALUES: ReadonlyArray<EquivalenceDegree> =
|
|
22
|
+
Object.freeze(Object.values(EQUIVALENCE_DEGREE));
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_EQUIVALENCE_DEGREE: EquivalenceDegree =
|
|
25
|
+
EQUIVALENCE_DEGREE.FULL;
|
|
26
|
+
|
|
27
|
+
export function isValidEquivalenceDegree(value: unknown): value is EquivalenceDegree {
|
|
28
|
+
return (
|
|
29
|
+
typeof value === 'string' &&
|
|
30
|
+
(EQUIVALENCE_DEGREE_VALUES as readonly string[]).includes(value)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default EQUIVALENCE_DEGREE;
|
|
@@ -18,34 +18,47 @@
|
|
|
18
18
|
// mapping. Both MULTIPLICITY constants and MULTIPLICITY_VALUES are
|
|
19
19
|
// derived from this single table; no duplicate string literals.
|
|
20
20
|
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
21
|
+
import type { PartitivePresence } from './partitive-presence.js';
|
|
22
|
+
import type { PartitiveCount } from './partitive-count.js';
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
export type MultiplicityName =
|
|
25
|
+
| 'compulsory'
|
|
26
|
+
| 'optional'
|
|
27
|
+
| 'compulsory_multiple'
|
|
28
|
+
| 'optional_multiple'
|
|
29
|
+
| 'compulsory_at_least_one';
|
|
30
|
+
|
|
31
|
+
const NAME_BY_PAIR: Readonly<Record<string, MultiplicityName>> = Object.freeze({
|
|
32
|
+
'required:exactly_one': 'compulsory',
|
|
33
|
+
'optional:exactly_one': 'optional',
|
|
34
|
+
'required:multiple': 'compulsory_multiple',
|
|
35
|
+
'optional:multiple': 'optional_multiple',
|
|
36
|
+
'required:at_least_one': 'compulsory_at_least_one',
|
|
30
37
|
});
|
|
31
38
|
|
|
32
|
-
const PAIR_BY_NAME = Object.freeze(
|
|
39
|
+
const PAIR_BY_NAME: Readonly<Record<MultiplicityName, string>> = Object.freeze(
|
|
33
40
|
Object.fromEntries(
|
|
34
41
|
Object.entries(NAME_BY_PAIR).map(([pair, name]) => [name, pair]),
|
|
35
|
-
),
|
|
42
|
+
) as Record<MultiplicityName, string>,
|
|
36
43
|
);
|
|
37
44
|
|
|
38
|
-
export const MULTIPLICITY =
|
|
39
|
-
Object.
|
|
40
|
-
Object.
|
|
41
|
-
|
|
42
|
-
)
|
|
45
|
+
export const MULTIPLICITY: Readonly<Record<Uppercase<MultiplicityName>, MultiplicityName>> =
|
|
46
|
+
Object.freeze(
|
|
47
|
+
Object.fromEntries(
|
|
48
|
+
Object.values(NAME_BY_PAIR).map((name) => [name.toUpperCase(), name]),
|
|
49
|
+
),
|
|
50
|
+
) as Readonly<Record<Uppercase<MultiplicityName>, MultiplicityName>>;
|
|
43
51
|
|
|
44
|
-
export const MULTIPLICITY_VALUES = Object.freeze(
|
|
52
|
+
export const MULTIPLICITY_VALUES: ReadonlyArray<MultiplicityName> = Object.freeze(
|
|
53
|
+
Object.values(NAME_BY_PAIR),
|
|
54
|
+
);
|
|
45
55
|
|
|
46
|
-
export const DEFAULT_MULTIPLICITY = MULTIPLICITY.COMPULSORY;
|
|
56
|
+
export const DEFAULT_MULTIPLICITY: MultiplicityName = MULTIPLICITY.COMPULSORY;
|
|
47
57
|
|
|
48
|
-
export function multiplicityFromPair(
|
|
58
|
+
export function multiplicityFromPair(
|
|
59
|
+
presence: PartitivePresence,
|
|
60
|
+
count: PartitiveCount,
|
|
61
|
+
): MultiplicityName {
|
|
49
62
|
const name = NAME_BY_PAIR[`${presence}:${count}`];
|
|
50
63
|
if (name == null) {
|
|
51
64
|
throw invalidCombinationError(presence, count);
|
|
@@ -53,28 +66,43 @@ export function multiplicityFromPair(presence, count) {
|
|
|
53
66
|
return name;
|
|
54
67
|
}
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Canonical error for the invalid (presence, count) combination.
|
|
71
|
+
* Single source of truth — hyperedge-member.ts delegates to this so
|
|
72
|
+
* the error string doesn't drift across the two files.
|
|
73
|
+
*/
|
|
74
|
+
export function invalidCombinationError(
|
|
75
|
+
presence: unknown,
|
|
76
|
+
count: unknown,
|
|
77
|
+
): Error {
|
|
61
78
|
return new Error(
|
|
62
79
|
`Invalid multiplicity combination: presence=${presence}, count=${count}. ` +
|
|
63
80
|
`(optional + at_least_one collapses to optional + multiple — use count=multiple.)`,
|
|
64
81
|
);
|
|
65
82
|
}
|
|
66
83
|
|
|
67
|
-
export function pairFromMultiplicity(
|
|
84
|
+
export function pairFromMultiplicity(
|
|
85
|
+
name: MultiplicityName,
|
|
86
|
+
): { presence: PartitivePresence; count: PartitiveCount } {
|
|
68
87
|
const pair = PAIR_BY_NAME[name];
|
|
69
88
|
if (pair == null) {
|
|
70
89
|
throw new Error(`Unknown multiplicity: ${JSON.stringify(name)}`);
|
|
71
90
|
}
|
|
72
|
-
const [presence, count] = pair.split(':');
|
|
91
|
+
const [presence, count] = pair.split(':') as [PartitivePresence, PartitiveCount];
|
|
73
92
|
return { presence, count };
|
|
74
93
|
}
|
|
75
94
|
|
|
76
|
-
export function isValidMultiplicity(name) {
|
|
77
|
-
return
|
|
95
|
+
export function isValidMultiplicity(name: unknown): name is MultiplicityName {
|
|
96
|
+
return (
|
|
97
|
+
typeof name === 'string' &&
|
|
98
|
+
Object.prototype.hasOwnProperty.call(PAIR_BY_NAME, name)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface MemberWithMultiplicity {
|
|
103
|
+
presence?: PartitivePresence;
|
|
104
|
+
count?: PartitiveCount;
|
|
105
|
+
multiplicity?: string;
|
|
78
106
|
}
|
|
79
107
|
|
|
80
108
|
/**
|
|
@@ -93,7 +121,9 @@ export function isValidMultiplicity(name) {
|
|
|
93
121
|
* rather than silently returning 'compulsory' (which would mask the
|
|
94
122
|
* data corruption).
|
|
95
123
|
*/
|
|
96
|
-
export function resolveMultiplicity(
|
|
124
|
+
export function resolveMultiplicity(
|
|
125
|
+
member: MemberWithMultiplicity | null | undefined,
|
|
126
|
+
): MultiplicityName | string {
|
|
97
127
|
if (member?.presence && member?.count) {
|
|
98
128
|
return multiplicityFromPair(member.presence, member.count);
|
|
99
129
|
}
|
|
@@ -102,4 +132,3 @@ export function resolveMultiplicity(member) {
|
|
|
102
132
|
}
|
|
103
133
|
return DEFAULT_MULTIPLICITY;
|
|
104
134
|
}
|
|
105
|
-
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { makeEnum } from './enum.js';
|
|
11
11
|
|
|
12
|
+
export type PartitiveCount = 'exactly_one' | 'at_least_one' | 'multiple';
|
|
13
|
+
|
|
12
14
|
export const PARTITIVE_COUNT = makeEnum('PARTITIVE_COUNT', {
|
|
13
15
|
EXACTLY_ONE: 'exactly_one',
|
|
14
16
|
AT_LEAST_ONE: 'at_least_one',
|
|
@@ -16,5 +18,5 @@ export const PARTITIVE_COUNT = makeEnum('PARTITIVE_COUNT', {
|
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
export const PARTITIVE_COUNT_VALUES = PARTITIVE_COUNT.VALUES;
|
|
19
|
-
export const DEFAULT_COUNT = PARTITIVE_COUNT.EXACTLY_ONE;
|
|
21
|
+
export const DEFAULT_COUNT: PartitiveCount = PARTITIVE_COUNT.EXACTLY_ONE;
|
|
20
22
|
export const isValidCount = PARTITIVE_COUNT.isValid;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// PartitiveEnumeration — allowed values for PartitiveHyperedge#enumeration.
|
|
2
|
+
//
|
|
3
|
+
// closed — the encoded parts are ALL the parts of the comprehensive.
|
|
4
|
+
// Adding or removing a part would change the relationship.
|
|
5
|
+
// open — the encoded parts are SOME of the parts; others may exist
|
|
6
|
+
// but are not encoded.
|
|
7
|
+
|
|
8
|
+
export type PartitiveEnumeration = 'closed' | 'open';
|
|
9
|
+
|
|
10
|
+
export const PARTITIVE_ENUMERATION: Readonly<
|
|
11
|
+
Record<'CLOSED' | 'OPEN', PartitiveEnumeration>
|
|
12
|
+
> = Object.freeze({
|
|
13
|
+
CLOSED: 'closed',
|
|
14
|
+
OPEN: 'open',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const PARTITIVE_ENUMERATION_VALUES: ReadonlyArray<PartitiveEnumeration> =
|
|
18
|
+
Object.freeze([PARTITIVE_ENUMERATION.CLOSED, PARTITIVE_ENUMERATION.OPEN]);
|
|
19
|
+
|
|
20
|
+
export function isValidPartitiveEnumeration(
|
|
21
|
+
value: unknown,
|
|
22
|
+
): value is PartitiveEnumeration {
|
|
23
|
+
return (
|
|
24
|
+
typeof value === 'string' &&
|
|
25
|
+
(PARTITIVE_ENUMERATION_VALUES as readonly string[]).includes(value)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
import { makeEnum } from './enum.js';
|
|
10
10
|
|
|
11
|
+
export type PartitivePresence = 'required' | 'optional';
|
|
12
|
+
|
|
11
13
|
export const PARTITIVE_PRESENCE = makeEnum('PARTITIVE_PRESENCE', {
|
|
12
14
|
REQUIRED: 'required',
|
|
13
15
|
OPTIONAL: 'optional',
|
|
14
16
|
});
|
|
15
17
|
|
|
16
18
|
export const PARTITIVE_PRESENCE_VALUES = PARTITIVE_PRESENCE.VALUES;
|
|
17
|
-
export const DEFAULT_PRESENCE = PARTITIVE_PRESENCE.REQUIRED;
|
|
19
|
+
export const DEFAULT_PRESENCE: PartitivePresence = PARTITIVE_PRESENCE.REQUIRED;
|
|
18
20
|
export const isValidPresence = PARTITIVE_PRESENCE.isValid;
|
|
@@ -9,16 +9,23 @@
|
|
|
9
9
|
// Orthogonal to PartitiveEnumeration — both may be set on the same
|
|
10
10
|
// hyperedge.
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export type PluralityMarker = 'double' | 'dashed';
|
|
13
|
+
|
|
14
|
+
export const PLURALITY_MARKER: Readonly<
|
|
15
|
+
Record<'DOUBLE' | 'DASHED', PluralityMarker>
|
|
16
|
+
> = Object.freeze({
|
|
13
17
|
DOUBLE: 'double',
|
|
14
18
|
DASHED: 'dashed',
|
|
15
19
|
});
|
|
16
20
|
|
|
17
|
-
export const PLURALITY_MARKER_VALUES =
|
|
18
|
-
PLURALITY_MARKER.DOUBLE,
|
|
19
|
-
PLURALITY_MARKER.DASHED,
|
|
20
|
-
]);
|
|
21
|
+
export const PLURALITY_MARKER_VALUES: ReadonlyArray<PluralityMarker> =
|
|
22
|
+
Object.freeze([PLURALITY_MARKER.DOUBLE, PLURALITY_MARKER.DASHED]);
|
|
21
23
|
|
|
22
|
-
export function isValidPluralityMarker(
|
|
23
|
-
|
|
24
|
+
export function isValidPluralityMarker(
|
|
25
|
+
value: unknown,
|
|
26
|
+
): value is PluralityMarker {
|
|
27
|
+
return (
|
|
28
|
+
typeof value === 'string' &&
|
|
29
|
+
(PLURALITY_MARKER_VALUES as readonly string[]).includes(value)
|
|
30
|
+
);
|
|
24
31
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SequentialHyperedge — an ISO 12620 A.6.3 / ISO 704:2022 §5.5.5
|
|
2
|
+
// sequential relation connecting a comprehensive concept to two or
|
|
3
|
+
// more subordinate concepts in an ORDERED sequence (temporal,
|
|
4
|
+
// spatial, causal, developmental).
|
|
5
|
+
//
|
|
6
|
+
// Mirror of PartitiveHyperedge and GenericHyperedge. Member array
|
|
7
|
+
// order IS significant — reversing the array reverses the sequence.
|
|
8
|
+
// Use cases: manufacturing process steps, biological life cycles,
|
|
9
|
+
// software release lifecycles, supply chain stages, geological eras.
|
|
10
|
+
//
|
|
11
|
+
// See docs/design/abstract-nary-relation.md (concept-model repo).
|
|
12
|
+
|
|
13
|
+
import { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
14
|
+
import { SequentialMember } from './sequential-member.js';
|
|
15
|
+
import { HyperedgeRegistry } from './hyperedge-registry.js';
|
|
16
|
+
|
|
17
|
+
export class SequentialHyperedge extends AbstractHyperedge {
|
|
18
|
+
// Per-class metadata block — the OCP contract.
|
|
19
|
+
static wireKey = 'sequential_relations';
|
|
20
|
+
static typeTag = 'sequential_relation';
|
|
21
|
+
static rdfType = 'gloss:SequentialRelation';
|
|
22
|
+
static memberClass = SequentialMember;
|
|
23
|
+
static v1WireKeys = [];
|
|
24
|
+
static kindLabel = 'SEQ';
|
|
25
|
+
|
|
26
|
+
constructor(data = {}) {
|
|
27
|
+
super(data);
|
|
28
|
+
this.members = data?.members == null
|
|
29
|
+
? this.members
|
|
30
|
+
: (Array.isArray(data.members)
|
|
31
|
+
? data.members.map(m =>
|
|
32
|
+
m instanceof SequentialMember ? m : new SequentialMember(m))
|
|
33
|
+
: this.members);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static identityOf(value) {
|
|
37
|
+
const v = value ?? {};
|
|
38
|
+
const c = v.comprehensive ?? {};
|
|
39
|
+
// For sequential, member order IS significant — do NOT sort.
|
|
40
|
+
const memberKeys = Array.isArray(v.members)
|
|
41
|
+
? v.members.map(m => {
|
|
42
|
+
const r = m?.ref ?? m ?? {};
|
|
43
|
+
return `${r.source ?? ''}:${r.id ?? ''}`;
|
|
44
|
+
})
|
|
45
|
+
: [];
|
|
46
|
+
return `${c.source ?? ''}:${c.id ?? ''}|${memberKeys.join('|')}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static fromJSON(data) {
|
|
50
|
+
return new SequentialHyperedge(data);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default SequentialHyperedge;
|
|
55
|
+
|
|
56
|
+
HyperedgeRegistry.register(SequentialHyperedge);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SequentialMember — one member of a SequentialHyperedge. Inherits
|
|
2
|
+
// the MECE shape from HyperedgeMember. Distinct leaf class for type
|
|
3
|
+
// safety. Member array order IS significant for sequential hyperedges.
|
|
4
|
+
//
|
|
5
|
+
// See docs/design/abstract-nary-relation.md (concept-model repo).
|
|
6
|
+
|
|
7
|
+
import { HyperedgeMember } from './hyperedge-member.js';
|
|
8
|
+
|
|
9
|
+
export class SequentialMember extends HyperedgeMember {
|
|
10
|
+
// Inherits all attributes and validations from HyperedgeMember.
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default SequentialMember;
|
package/src/models/base.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export class GlossaristModel {
|
|
2
|
-
toJSON() {
|
|
3
|
-
throw new Error(`${this.constructor.name} must implement toJSON()`);
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
static fromJSON() {
|
|
7
|
-
throw new Error(`${this.name} must implement fromJSON()`);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
equals(other) {
|
|
11
|
-
if (!(other instanceof this.constructor)) return false;
|
|
12
|
-
return JSON.stringify(this.toJSON()) === JSON.stringify(other.toJSON());
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
clone() {
|
|
16
|
-
return this.constructor.fromJSON(JSON.parse(JSON.stringify(this.toJSON())));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
_lazy(cacheKey, rawKey, wrapFn) {
|
|
20
|
-
if (this[cacheKey] === null) {
|
|
21
|
-
this[cacheKey] = this[rawKey].map(wrapFn);
|
|
22
|
-
}
|
|
23
|
-
return this[cacheKey];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
_serialize(obj, jsonKey, cacheKey, rawKey) {
|
|
27
|
-
const items = this[cacheKey] ?? (this[rawKey].length > 0 ? this[rawKey] : []);
|
|
28
|
-
if (items.length > 0) {
|
|
29
|
-
obj[jsonKey] = items.map(i => (i instanceof GlossaristModel) ? i.toJSON() : i);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { GlossaristModel } from './base.js';
|
|
2
|
-
|
|
3
|
-
export const DATE_TYPES = Object.freeze([
|
|
4
|
-
'accepted', 'amended', 'retired',
|
|
5
|
-
]);
|
|
6
|
-
|
|
7
|
-
export class ConceptDate extends GlossaristModel {
|
|
8
|
-
constructor(data = {}) {
|
|
9
|
-
super();
|
|
10
|
-
this.date = data.date ?? null;
|
|
11
|
-
this.type = data.type ?? null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
get parsedDate() {
|
|
15
|
-
return this.date ? new Date(this.date) : null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
static identityOf(value) {
|
|
19
|
-
return value?.type ?? '';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
identity() {
|
|
23
|
-
return ConceptDate.identityOf(this);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
toJSON() {
|
|
27
|
-
const obj = {};
|
|
28
|
-
if (this.date != null) obj.date = this.date;
|
|
29
|
-
if (this.type != null) obj.type = this.type;
|
|
30
|
-
return obj;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
static fromJSON(data) {
|
|
34
|
-
return new ConceptDate(data);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { GlossaristModel } from './base.js';
|
|
2
|
-
|
|
3
|
-
export class ConceptRef extends GlossaristModel {
|
|
4
|
-
constructor(data = {}) {
|
|
5
|
-
super();
|
|
6
|
-
this.source = data.source ?? null;
|
|
7
|
-
this.id = data.id ?? null;
|
|
8
|
-
this.text = data.text ?? null;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
toString() {
|
|
12
|
-
const parts = [];
|
|
13
|
-
if (this.source) parts.push(this.source);
|
|
14
|
-
if (this.id) parts.push(this.id);
|
|
15
|
-
const base = parts.join(' ');
|
|
16
|
-
if (this.text && base) return `${base} (${this.text})`;
|
|
17
|
-
if (this.text) return this.text;
|
|
18
|
-
return base;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
toJSON() {
|
|
22
|
-
const obj = {};
|
|
23
|
-
if (this.source != null) obj.source = this.source;
|
|
24
|
-
if (this.id != null) obj.id = this.id;
|
|
25
|
-
if (this.text != null) obj.text = this.text;
|
|
26
|
-
return obj;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
identity() {
|
|
30
|
-
return `${this.source ?? ''}|${this.id ?? ''}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
static fromJSON(data) {
|
|
34
|
-
return new ConceptRef(data);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { GlossaristModel } from './base.js';
|
|
2
|
-
import { Citation } from './citation.js';
|
|
3
|
-
|
|
4
|
-
export class ConceptSource extends GlossaristModel {
|
|
5
|
-
constructor(data = {}) {
|
|
6
|
-
super();
|
|
7
|
-
this.id = data.id ?? null;
|
|
8
|
-
this.status = data.status ?? null;
|
|
9
|
-
this.type = data.type ?? null;
|
|
10
|
-
this.origin = data.origin
|
|
11
|
-
? (data.origin instanceof Citation ? data.origin : new Citation(data.origin))
|
|
12
|
-
: null;
|
|
13
|
-
this.modification = data.modification ?? null;
|
|
14
|
-
this.sourcedFrom = (data.sourcedFrom ?? data.sourced_from ?? []).map(
|
|
15
|
-
c => c instanceof Citation ? c : new Citation(c)
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Backward-compat alias for callers using the snake_case JS field name.
|
|
20
|
-
// Prefer `.sourcedFrom` going forward; the wire name remains
|
|
21
|
-
// `sourced_from` (see toJSON).
|
|
22
|
-
get sourced_from() {
|
|
23
|
-
return this.sourcedFrom;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
toJSON() {
|
|
27
|
-
const obj = {};
|
|
28
|
-
if (this.id != null) obj.id = this.id;
|
|
29
|
-
if (this.status != null) obj.status = this.status;
|
|
30
|
-
if (this.type != null) obj.type = this.type;
|
|
31
|
-
if (this.origin != null) obj.origin = this.origin.toJSON();
|
|
32
|
-
if (this.modification != null) obj.modification = this.modification;
|
|
33
|
-
if (this.sourcedFrom.length > 0) {
|
|
34
|
-
obj.sourced_from = this.sourcedFrom.map(c => c.toJSON());
|
|
35
|
-
}
|
|
36
|
-
return obj;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
static identityOf(value) {
|
|
40
|
-
const v = value ?? {};
|
|
41
|
-
const ref = v?.origin?.ref ?? (v?.origin instanceof Citation ? v.origin.ref : null);
|
|
42
|
-
return `${v.type ?? ''}|${ref?.source ?? ''}|${ref?.id ?? ''}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
identity() {
|
|
46
|
-
return ConceptSource.identityOf(this);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
static fromJSON(data) {
|
|
50
|
-
return new ConceptSource(data);
|
|
51
|
-
}
|
|
52
|
-
}
|
package/src/models/enum.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// Enum factory — produces a frozen KEY/VALUES bundle, a frozen values
|
|
2
|
-
// array, and an isValid* predicate from a single `makeEnum` call.
|
|
3
|
-
//
|
|
4
|
-
// Adding a new enum means one line:
|
|
5
|
-
//
|
|
6
|
-
// export const COLOR = makeEnum('COLOR', { RED: 'red', BLUE: 'blue' });
|
|
7
|
-
//
|
|
8
|
-
// The factory returns:
|
|
9
|
-
// {
|
|
10
|
-
// CLOSED: 'closed', // exposed by destructuring
|
|
11
|
-
// OPEN: 'open',
|
|
12
|
-
// VALUES: ['closed', 'open'], // frozen array
|
|
13
|
-
// isValid: (v) => boolean, // predicate
|
|
14
|
-
// }
|
|
15
|
-
//
|
|
16
|
-
// We attach the bundle via Object.assign on a frozen object so the
|
|
17
|
-
// bundle is itself frozen and exposes both the per-value constants
|
|
18
|
-
// (CLOSED, OPEN, ...) and the VALUES list + isValid predicate.
|
|
19
|
-
|
|
20
|
-
export function makeEnum(name, values) {
|
|
21
|
-
if (typeof name !== 'string') {
|
|
22
|
-
throw new Error(`makeEnum: name must be a string, got ${typeof name}`);
|
|
23
|
-
}
|
|
24
|
-
if (typeof values !== 'object' || values === null) {
|
|
25
|
-
throw new Error(`makeEnum: values must be an object, got ${typeof values}`);
|
|
26
|
-
}
|
|
27
|
-
for (const [k, v] of Object.entries(values)) {
|
|
28
|
-
if (typeof v !== 'string') {
|
|
29
|
-
throw new Error(`makeEnum(${name}): value for ${k} must be a string, got ${typeof v}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const frozen = Object.freeze({ ...values });
|
|
34
|
-
const valuesArr = Object.freeze(Object.values(frozen));
|
|
35
|
-
const isValid = (v) => valuesArr.includes(v);
|
|
36
|
-
|
|
37
|
-
return Object.assign(Object.create(null), frozen, {
|
|
38
|
-
name,
|
|
39
|
-
VALUES: valuesArr,
|
|
40
|
-
isValid,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// PartitiveEnumeration — allowed values for PartitiveHyperedge#enumeration.
|
|
2
|
-
//
|
|
3
|
-
// closed — the encoded parts are ALL the parts of the comprehensive.
|
|
4
|
-
// Adding or removing a part would change the relationship.
|
|
5
|
-
// open — the encoded parts are SOME of the parts; others may exist
|
|
6
|
-
// but are not encoded.
|
|
7
|
-
//
|
|
8
|
-
// See concept-model/TODO.hyperedge/00-design-overview.md for the design
|
|
9
|
-
// rationale and the open/closed distinction.
|
|
10
|
-
|
|
11
|
-
export const PARTITIVE_ENUMERATION = Object.freeze({
|
|
12
|
-
CLOSED: 'closed',
|
|
13
|
-
OPEN: 'open',
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
export const PARTITIVE_ENUMERATION_VALUES = Object.freeze([
|
|
17
|
-
PARTITIVE_ENUMERATION.CLOSED,
|
|
18
|
-
PARTITIVE_ENUMERATION.OPEN,
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
export function isValidPartitiveEnumeration(value) {
|
|
22
|
-
return PARTITIVE_ENUMERATION_VALUES.includes(value);
|
|
23
|
-
}
|