glossarist 0.4.35 → 0.4.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/package.json +15 -6
  2. package/src/models/abstract-hyperedge.ts +164 -0
  3. package/src/models/base.ts +93 -0
  4. package/src/models/{completeness.js → completeness.ts} +3 -6
  5. package/src/models/concept-date.ts +48 -0
  6. package/src/models/concept-ref.ts +64 -0
  7. package/src/models/concept-source.ts +96 -0
  8. package/src/models/concept-system-type.ts +39 -0
  9. package/src/models/concept-system.ts +152 -0
  10. package/src/models/concept-type.ts +33 -0
  11. package/src/models/{dataset-color.js → dataset-color.ts} +26 -17
  12. package/src/models/{definition-type.js → definition-type.ts} +8 -1
  13. package/src/models/enum.ts +60 -0
  14. package/src/models/equivalence-degree.ts +34 -0
  15. package/src/models/generic-hyperedge.ts +72 -0
  16. package/src/models/generic-member.ts +80 -0
  17. package/src/models/{hyperedge-index.js → hyperedge-index.ts} +32 -18
  18. package/src/models/{hyperedge-member.js → hyperedge-member.ts} +38 -26
  19. package/src/models/{hyperedge-registry.js → hyperedge-registry.ts} +59 -29
  20. package/src/models/{multiplicity.js → multiplicity.ts} +58 -29
  21. package/src/models/{partitive-count.js → partitive-count.ts} +3 -1
  22. package/src/models/partitive-enumeration.ts +27 -0
  23. package/src/models/partitive-hyperedge.ts +90 -0
  24. package/src/models/{partitive-member.js → partitive-member.ts} +18 -17
  25. package/src/models/{partitive-presence.js → partitive-presence.ts} +3 -1
  26. package/src/models/{plurality-marker.js → plurality-marker.ts} +14 -7
  27. package/src/models/sequential-hyperedge.ts +70 -0
  28. package/src/models/{sequential-member.js → sequential-member.ts} +6 -3
  29. package/src/models/abstract-hyperedge.js +0 -119
  30. package/src/models/base.js +0 -32
  31. package/src/models/concept-date.js +0 -36
  32. package/src/models/concept-ref.js +0 -36
  33. package/src/models/concept-source.js +0 -52
  34. package/src/models/concept-system-type.js +0 -26
  35. package/src/models/concept-system.js +0 -117
  36. package/src/models/concept-type.js +0 -25
  37. package/src/models/enum.js +0 -42
  38. package/src/models/equivalence-degree.js +0 -25
  39. package/src/models/generic-hyperedge.js +0 -55
  40. package/src/models/generic-member.js +0 -82
  41. package/src/models/partitive-enumeration.js +0 -23
  42. package/src/models/partitive-hyperedge.js +0 -91
  43. package/src/models/sequential-hyperedge.js +0 -56
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glossarist",
3
- "version": "0.4.35",
3
+ "version": "0.4.37",
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,164 @@
1
+ // AbstractHyperedge — shared base for directed 1→N hyperedges
2
+ // (PartitiveHyperedge, GenericHyperedge, SequentialHyperedge, future
3
+ // AssociativeHyperedge, etc.).
4
+ //
5
+ // Carries the common shape:
6
+ // - comprehensive: ConceptRef
7
+ // - members: 2+ MECE dimensioned concept refs (≥2 per ISO 704)
8
+ // - completeness: complete | partial
9
+ // - criterion: localized subdivision criterion (ISO 12620)
10
+ // - sources / notes / status: administrative metadata
11
+ //
12
+ // See docs/design/abstract-hyperedge.md (concept-model repo).
13
+
14
+ import { GlossaristModel } from './base.js';
15
+ import { ConceptRef } from './concept-ref.js';
16
+ import type { ConceptRefJson } from './concept-ref.js';
17
+ import { DEFAULT_COMPLETENESS, isValidCompleteness } from './completeness.js';
18
+ import type { Completeness } from './completeness.js';
19
+ import type { HyperedgeMember } from './hyperedge-member.js';
20
+ import type { LocalizedString } from './generic-member.js';
21
+
22
+ export interface AbstractHyperedgeJson {
23
+ comprehensive: ConceptRefJson;
24
+ members: ReadonlyArray<unknown>;
25
+ completeness: Completeness;
26
+ criterion?: LocalizedString;
27
+ sources?: ReadonlyArray<unknown>;
28
+ notes?: Record<string, unknown> | null;
29
+ status?: string | null;
30
+ /** PartitiveHyperedge wire alias (only present on PartitiveHyperedge JSON). */
31
+ partitives?: ReadonlyArray<unknown>;
32
+ }
33
+
34
+ export type CriterionInput = string | LocalizedString | null | undefined;
35
+
36
+ export abstract class AbstractHyperedge extends GlossaristModel {
37
+ readonly comprehensive: ConceptRef;
38
+ readonly members: ReadonlyArray<HyperedgeMember>;
39
+ readonly completeness: Completeness;
40
+ readonly criterion: LocalizedString | null;
41
+ readonly sources: ReadonlyArray<unknown>;
42
+ readonly notes: Record<string, unknown> | null;
43
+ readonly status: string | null;
44
+
45
+ constructor(
46
+ data: AbstractHyperedgeJson = {
47
+ comprehensive: {},
48
+ members: [],
49
+ completeness: 'complete',
50
+ },
51
+ ) {
52
+ super();
53
+ this.comprehensive = _ensureComprehensive(data.comprehensive);
54
+ this.members = _ensureMembers(
55
+ data.members,
56
+ this.constructor.name,
57
+ ) as ReadonlyArray<HyperedgeMember>;
58
+ this.completeness = _resolveCompleteness(data.completeness);
59
+ this.criterion = _normalizeCriterion(data.criterion);
60
+ this.sources = Array.isArray(data.sources) ? [...data.sources] : [];
61
+ this.notes = (data.notes as Record<string, unknown> | null) ?? null;
62
+ this.status = data.status ?? null;
63
+
64
+ _assertNoSelfLoop(this.comprehensive, this.members);
65
+ }
66
+
67
+ get isComplete(): boolean { return this.completeness === 'complete'; }
68
+ get isPartial(): boolean { return this.completeness === 'partial'; }
69
+ get isCoordinate(): boolean { return this.members.length >= 2; }
70
+
71
+ hasCriterion(): boolean { return this.criterion != null; }
72
+
73
+ override identity(): string {
74
+ const Ctor = this.constructor as unknown as { identityOf: (v: unknown) => string };
75
+ return Ctor.identityOf(this);
76
+ }
77
+
78
+ override toJSON(): AbstractHyperedgeJson {
79
+ const obj: AbstractHyperedgeJson = {
80
+ comprehensive: this.comprehensive.toJSON(),
81
+ members: this.members.map((m) => m.toJSON()),
82
+ completeness: this.completeness,
83
+ };
84
+ if (this.criterion != null) obj.criterion = { ...this.criterion };
85
+ if (this.sources.length > 0) obj.sources = [...this.sources];
86
+ if (this.notes != null) obj.notes = { ...this.notes };
87
+ if (this.status != null) obj.status = this.status;
88
+ return obj;
89
+ }
90
+ }
91
+
92
+ function _ensureComprehensive(
93
+ value: ConceptRefJson | ConceptRef | null | undefined,
94
+ ): ConceptRef {
95
+ const ref = value instanceof ConceptRef ? value : new ConceptRef(value ?? {});
96
+ if (!ref.source && !ref.id && !ref.text) {
97
+ throw new Error(
98
+ 'AbstractHyperedge.comprehensive must be a non-empty ConceptReference ' +
99
+ '(source, id, or text required)',
100
+ );
101
+ }
102
+ return ref;
103
+ }
104
+
105
+ function _ensureMembers(
106
+ values: ReadonlyArray<unknown> | null | undefined,
107
+ typeName: string,
108
+ ): ReadonlyArray<unknown> {
109
+ if (!Array.isArray(values)) {
110
+ throw new Error(
111
+ `${typeName}.members must be an array (got ${values === null ? 'null' : typeof values})`,
112
+ );
113
+ }
114
+ if (values.length < 2) {
115
+ throw new Error(
116
+ `${typeName} requires ≥2 partitives (ISO 704); got ${values.length}`,
117
+ );
118
+ }
119
+ return values;
120
+ }
121
+
122
+ function _resolveCompleteness(
123
+ value: Completeness | null | undefined,
124
+ ): Completeness {
125
+ const c = value ?? DEFAULT_COMPLETENESS;
126
+ if (!isValidCompleteness(c)) {
127
+ throw new Error(
128
+ `completeness has invalid value ${JSON.stringify(value)}`,
129
+ );
130
+ }
131
+ return c;
132
+ }
133
+
134
+ function _normalizeCriterion(value: CriterionInput): LocalizedString | null {
135
+ if (value == null) return null;
136
+ if (typeof value === 'string') {
137
+ return value.length > 0 ? { default: value } : null;
138
+ }
139
+ if (typeof value === 'object') {
140
+ const entries = Object.entries(value).filter(
141
+ ([, v]) => typeof v === 'string',
142
+ );
143
+ return entries.length > 0
144
+ ? (Object.fromEntries(entries) as LocalizedString)
145
+ : null;
146
+ }
147
+ return null;
148
+ }
149
+
150
+ function _assertNoSelfLoop(
151
+ comprehensive: ConceptRef,
152
+ members: ReadonlyArray<{ ref?: { source?: string | null; id?: string | null } }>,
153
+ ): void {
154
+ const cKey = `${comprehensive.source ?? ''}:${comprehensive.id ?? ''}`;
155
+ for (const m of members) {
156
+ const ref = m.ref;
157
+ const pKey = `${ref?.source ?? ''}:${ref?.id ?? ''}`;
158
+ if (pKey === cKey && (ref?.source || ref?.id)) {
159
+ throw new Error(
160
+ 'Relation.members cannot include the comprehensive (no self-loops)',
161
+ );
162
+ }
163
+ }
164
+ }
@@ -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;