@glossarist/concept-browser 0.7.78 → 0.7.81

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 (57) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-edges.js +109 -39
  3. package/scripts/generate-data.mjs +63 -27
  4. package/src/__tests__/build-edges-redundancy.test.ts +101 -0
  5. package/src/__tests__/composables/use-concept-diff.test.ts +240 -0
  6. package/src/__tests__/composables/use-concept-edges-v2.test.ts +132 -0
  7. package/src/__tests__/composables/use-dataset-stats.test.ts +96 -0
  8. package/src/__tests__/config/page-types.test.ts +82 -0
  9. package/src/__tests__/model-bridge-bridges.test.ts +3 -2
  10. package/src/__tests__/model-bridge-citation.test.ts +1 -1
  11. package/src/__tests__/ontology-registry.test.ts +2 -2
  12. package/src/__tests__/partitive-relation-styling.test.ts +82 -0
  13. package/src/__tests__/partitive-relation.test.ts +285 -0
  14. package/src/adapters/GraphDataSource.ts +16 -6
  15. package/src/adapters/dataset-stats.ts +13 -0
  16. package/src/adapters/non-verbal/glossarist-augment.d.ts +155 -4
  17. package/src/adapters/types.ts +27 -17
  18. package/src/components/AppSidebar.vue +14 -57
  19. package/src/components/ConceptDetail.vue +29 -0
  20. package/src/components/ConceptDiffModal.vue +120 -0
  21. package/src/components/ConceptDiffView.vue +192 -0
  22. package/src/components/ConceptEditionRail.vue +61 -36
  23. package/src/components/PartitiveRelationList.vue +139 -0
  24. package/src/components/groups/DatasetGroupRenderer.vue +14 -4
  25. package/src/components/groups/DefaultGroupSidebar.vue +28 -11
  26. package/src/components/groups/LineageGroupSidebar.vue +17 -29
  27. package/src/composables/use-concept-diff.ts +231 -0
  28. package/src/composables/use-concept-edges.ts +66 -45
  29. package/src/composables/use-dataset-stats.ts +52 -0
  30. package/src/config/page-types.ts +67 -0
  31. package/src/config/types.ts +6 -1
  32. package/src/config/use-site-config.ts +13 -29
  33. package/src/data/taxonomies.json +24 -1
  34. package/src/i18n/locales/eng.yml +6 -0
  35. package/src/main.ts +1 -1
  36. package/src/style.css +1 -1
  37. package/src/utils/partitive-relation-styling.ts +62 -0
  38. package/src/views/DatasetView.vue +5 -3
  39. package/src/views/StatsView.vue +9 -22
  40. package/src/__tests__/hyperedge-styling.test.ts +0 -57
  41. package/src/__tests__/partitive-hyperedge.test.ts +0 -199
  42. package/src/components/PartitiveHyperedgeList.vue +0 -112
  43. package/src/islands/BibliographyIsland.vue +0 -26
  44. package/src/islands/ConceptDetailIsland.vue +0 -27
  45. package/src/islands/ConceptDiffIsland.vue +0 -214
  46. package/src/islands/ConceptIsland.vue +0 -102
  47. package/src/islands/GraphIsland.vue +0 -15
  48. package/src/islands/GroupRendererIsland.vue +0 -15
  49. package/src/islands/HeaderIsland.vue +0 -10
  50. package/src/islands/NonVerbalIsland.vue +0 -75
  51. package/src/islands/RelationSphereIsland.vue +0 -20
  52. package/src/islands/SearchIsland.vue +0 -23
  53. package/src/islands/SeriesCardIsland.vue +0 -12
  54. package/src/islands/ShortcutsIsland.vue +0 -13
  55. package/src/islands/SidebarIsland.vue +0 -15
  56. package/src/islands/SupersessionDiffIsland.vue +0 -267
  57. package/src/utils/hyperedge-styling.ts +0 -48
@@ -0,0 +1,285 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { PartitiveRelationWire } from '../adapters/types';
3
+
4
+ // Re-implements the extractor under test so we can test it in isolation
5
+ // without spinning up the full build-edges.js pipeline. The production
6
+ // extractor in scripts/build-edges.js uses the same logic.
7
+ function extractPartitiveRelations(
8
+ concept: { '@id': string; 'gl:partitiveRelations'?: any[] },
9
+ registerId: string,
10
+ uriBase: string,
11
+ urnMap: Map<string, string>,
12
+ ): PartitiveRelationWire[] {
13
+ const relations: PartitiveRelationWire[] = [];
14
+ const sourceUri = concept['@id'];
15
+
16
+ const resolveConceptUri = (ref: any): string | null => {
17
+ const source = ref?.['gl:source'] || ref?.source;
18
+ const id = ref?.['gl:id'] || ref?.id;
19
+ if (!source || !id) return null;
20
+ const reg = urnMap.get(source) || source;
21
+ return `${uriBase}/${reg}/concept/${id}`;
22
+ };
23
+
24
+ const resolvePlurality = (plurality: any) => {
25
+ if (!plurality) return null;
26
+ const isShared = plurality['gl:isShared'] ?? plurality.is_shared;
27
+ if (typeof isShared !== 'boolean') return null;
28
+ const isUncertain = plurality['gl:isUncertain'] ?? plurality.is_uncertain ?? false;
29
+ const sharedTypeRef = plurality['gl:sharedType'] ?? plurality.shared_type;
30
+ let sharedType = null;
31
+ if (sharedTypeRef) {
32
+ const s = sharedTypeRef['gl:source'] ?? sharedTypeRef.source;
33
+ const i = sharedTypeRef['gl:id'] ?? sharedTypeRef.id;
34
+ if (s && i) sharedType = `${s}:${i}`;
35
+ }
36
+ return { isShared, isUncertain, sharedType };
37
+ };
38
+
39
+ for (const rel of concept['gl:partitiveRelations'] || []) {
40
+ const comprehensive = resolveConceptUri(rel['gl:comprehensive']);
41
+ if (!comprehensive) continue;
42
+ const partitives = (rel['gl:hasPartitive'] || [])
43
+ .map((member: any) => {
44
+ const ref = member['gl:ref'] || member;
45
+ const uri = resolveConceptUri(ref);
46
+ if (!uri || uri === sourceUri) return null;
47
+ const certainty = member['gl:certainty'] || 'confirmed';
48
+ if (certainty !== 'confirmed' && certainty !== 'possible') {
49
+ throw new Error(
50
+ `Invalid partitive member certainty: "${certainty}". Allowed: confirmed, possible`,
51
+ );
52
+ }
53
+ return { uri, certainty };
54
+ })
55
+ .filter((p: any): p is { uri: string; certainty: 'confirmed' | 'possible' } => p !== null);
56
+ if (partitives.length === 0) continue;
57
+ const completeness = rel['gl:completeness'] || 'complete';
58
+ if (completeness !== 'complete' && completeness !== 'partial') {
59
+ throw new Error(
60
+ `Invalid partitive relation completeness: "${completeness}". Allowed: complete, partial`,
61
+ );
62
+ }
63
+ relations.push({
64
+ source: sourceUri,
65
+ comprehensive,
66
+ partitives,
67
+ completeness,
68
+ plurality: resolvePlurality(rel['gl:hasPlurality']) as PartitiveRelationWire['plurality'],
69
+ criterion: rel['gl:criterion'],
70
+ register: registerId,
71
+ });
72
+ }
73
+ return relations;
74
+ }
75
+
76
+ describe('extractPartitiveRelations', () => {
77
+ const uriBase = 'https://example.org';
78
+ const urnMap = new Map([['urn:vim:pub:v:2:2012', 'vim-2012']]);
79
+ const registerId = 'vim-2012';
80
+ const sourceUri = `${uriBase}/${registerId}/concept/2.9`;
81
+
82
+ it('extracts a complete relation with two confirmed members', () => {
83
+ const concept = {
84
+ '@id': sourceUri,
85
+ 'gl:partitiveRelations': [
86
+ {
87
+ 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' },
88
+ 'gl:hasPartitive': [
89
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.10' } },
90
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.26' } },
91
+ ],
92
+ 'gl:completeness': 'complete',
93
+ 'gl:criterion': { eng: 'measurement result composition' },
94
+ },
95
+ ],
96
+ };
97
+
98
+ const result = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
99
+ expect(result).toHaveLength(1);
100
+ expect(result[0].comprehensive).toBe(`${uriBase}/${registerId}/concept/2.9`);
101
+ expect(result[0].partitives).toEqual([
102
+ { uri: `${uriBase}/${registerId}/concept/2.10`, certainty: 'confirmed' },
103
+ { uri: `${uriBase}/${registerId}/concept/2.26`, certainty: 'confirmed' },
104
+ ]);
105
+ expect(result[0].completeness).toBe('complete');
106
+ expect(result[0].criterion).toEqual({ eng: 'measurement result composition' });
107
+ });
108
+
109
+ it('defaults completeness to complete when omitted', () => {
110
+ const concept = {
111
+ '@id': sourceUri,
112
+ 'gl:partitiveRelations': [
113
+ {
114
+ 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' },
115
+ 'gl:hasPartitive': [
116
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.10' } },
117
+ ],
118
+ },
119
+ ],
120
+ };
121
+ const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
122
+ expect(rel.completeness).toBe('complete');
123
+ });
124
+
125
+ it('preserves partial completeness', () => {
126
+ const concept = {
127
+ '@id': sourceUri,
128
+ 'gl:partitiveRelations': [
129
+ {
130
+ 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
131
+ 'gl:hasPartitive': [
132
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
133
+ ],
134
+ 'gl:completeness': 'partial',
135
+ },
136
+ ],
137
+ };
138
+ const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
139
+ expect(rel.completeness).toBe('partial');
140
+ });
141
+
142
+ it('resolves plurality block (isShared + sharedType)', () => {
143
+ const concept = {
144
+ '@id': sourceUri,
145
+ 'gl:partitiveRelations': [
146
+ {
147
+ 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
148
+ 'gl:hasPartitive': [
149
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
150
+ ],
151
+ 'gl:hasPlurality': {
152
+ 'gl:isShared': true,
153
+ 'gl:isUncertain': false,
154
+ 'gl:sharedType': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.99' },
155
+ },
156
+ },
157
+ ],
158
+ };
159
+ const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
160
+ expect(rel.plurality).toEqual({
161
+ isShared: true,
162
+ isUncertain: false,
163
+ sharedType: 'urn:vim:pub:v:2:2012:1.99',
164
+ });
165
+ });
166
+
167
+ it('returns null plurality when isShared missing', () => {
168
+ const concept = {
169
+ '@id': sourceUri,
170
+ 'gl:partitiveRelations': [
171
+ {
172
+ 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
173
+ 'gl:hasPartitive': [
174
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
175
+ ],
176
+ 'gl:hasPlurality': { 'gl:isUncertain': true },
177
+ },
178
+ ],
179
+ };
180
+ const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
181
+ expect(rel.plurality).toBeNull();
182
+ });
183
+
184
+ it('preserves per-member certainty', () => {
185
+ const concept = {
186
+ '@id': sourceUri,
187
+ 'gl:partitiveRelations': [
188
+ {
189
+ 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
190
+ 'gl:hasPartitive': [
191
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
192
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.5' }, 'gl:certainty': 'possible' },
193
+ ],
194
+ },
195
+ ],
196
+ };
197
+ const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
198
+ expect(rel.partitives).toEqual([
199
+ { uri: `${uriBase}/${registerId}/concept/1.4`, certainty: 'confirmed' },
200
+ { uri: `${uriBase}/${registerId}/concept/1.5`, certainty: 'possible' },
201
+ ]);
202
+ });
203
+
204
+ it('throws on invalid completeness', () => {
205
+ const concept = {
206
+ '@id': sourceUri,
207
+ 'gl:partitiveRelations': [
208
+ {
209
+ 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
210
+ 'gl:hasPartitive': [
211
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
212
+ ],
213
+ 'gl:completeness': 'closed',
214
+ },
215
+ ],
216
+ };
217
+ expect(() => extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toThrow(/Invalid.*completeness.*closed/);
218
+ });
219
+
220
+ it('throws on invalid member certainty', () => {
221
+ const concept = {
222
+ '@id': sourceUri,
223
+ 'gl:partitiveRelations': [
224
+ {
225
+ 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
226
+ 'gl:hasPartitive': [
227
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' }, 'gl:certainty': 'maybe' },
228
+ ],
229
+ },
230
+ ],
231
+ };
232
+ expect(() => extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toThrow(/Invalid.*certainty.*maybe/);
233
+ });
234
+
235
+ it('skips a relation whose comprehensive cannot be resolved', () => {
236
+ const concept = {
237
+ '@id': sourceUri,
238
+ 'gl:partitiveRelations': [
239
+ {
240
+ 'gl:comprehensive': { 'gl:id': '1.3' },
241
+ 'gl:hasPartitive': [
242
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
243
+ ],
244
+ },
245
+ ],
246
+ };
247
+ expect(extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toHaveLength(0);
248
+ });
249
+
250
+ it('skips a relation with no resolvable partitives', () => {
251
+ const concept = {
252
+ '@id': sourceUri,
253
+ 'gl:partitiveRelations': [
254
+ {
255
+ 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' },
256
+ 'gl:hasPartitive': [{ 'gl:ref': { 'gl:id': '1.4' } }],
257
+ },
258
+ ],
259
+ };
260
+ expect(extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toHaveLength(0);
261
+ });
262
+
263
+ it('excludes the source concept from partitives (self-loop guard)', () => {
264
+ const concept = {
265
+ '@id': sourceUri,
266
+ 'gl:partitiveRelations': [
267
+ {
268
+ 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' },
269
+ 'gl:hasPartitive': [
270
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.10' } },
271
+ { 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' } },
272
+ ],
273
+ },
274
+ ],
275
+ };
276
+ const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
277
+ expect(rel.partitives).toHaveLength(1);
278
+ expect(rel.partitives[0].uri).toBe(`${uriBase}/${registerId}/concept/2.10`);
279
+ });
280
+
281
+ it('returns empty array when concept has no partitiveRelations', () => {
282
+ const concept = { '@id': sourceUri };
283
+ expect(extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toEqual([]);
284
+ });
285
+ });
@@ -1,4 +1,4 @@
1
- import type { GraphEdge, GraphNode, PartitiveHyperedge, SectionNode } from './types';
1
+ import type { GraphEdge, GraphNode, PartitiveRelationWire, SectionNode } from './types';
2
2
  import type { Concept, RelatedConcept } from 'glossarist';
3
3
  import type { DatasetAdapter } from './DatasetAdapter';
4
4
  import { UriRouter } from './UriRouter';
@@ -30,6 +30,16 @@ function resolveRefTarget(rc: RelatedConcept, uriBase: string, registerId: strin
30
30
  return ref.source || '';
31
31
  }
32
32
 
33
+ function contentLabel(content: unknown): string | undefined {
34
+ if (content == null) return undefined;
35
+ if (typeof content === 'string') return content;
36
+ if (typeof content === 'object' && content !== null) {
37
+ const obj = content as Record<string, string>;
38
+ return obj.default ?? obj.eng ?? Object.values(obj)[0];
39
+ }
40
+ return undefined;
41
+ }
42
+
33
43
  export class GraphDataSource {
34
44
  constructor(private adapter: DatasetAdapter) {}
35
45
 
@@ -58,11 +68,11 @@ export class GraphDataSource {
58
68
  return data.edges ?? [];
59
69
  }
60
70
 
61
- async loadHyperedges(): Promise<PartitiveHyperedge[]> {
62
- const resp = await fetch(`${this.baseUrl}/hyperedges.json`);
71
+ async loadPartitiveRelations(): Promise<PartitiveRelationWire[]> {
72
+ const resp = await fetch(`${this.baseUrl}/partitive_relations.json`);
63
73
  if (!resp.ok) return [];
64
74
  const data = await resp.json();
65
- return data.hyperedges ?? [];
75
+ return data.relations ?? data.hyperedges ?? [];
66
76
  }
67
77
 
68
78
  async loadGraphNodes(): Promise<{ uriPrefix: string; nodes: [string, Record<string, string>, string][] }> {
@@ -90,7 +100,7 @@ export class GraphDataSource {
90
100
  source: sourceUri,
91
101
  target,
92
102
  type: rc.type || 'references',
93
- label: rc.content || undefined,
103
+ label: contentLabel(rc.content),
94
104
  register: parsed?.registerId ?? this.registerId,
95
105
  });
96
106
  }
@@ -107,7 +117,7 @@ export class GraphDataSource {
107
117
  source: sourceUri,
108
118
  target,
109
119
  type: rc.type || 'references',
110
- label: rc.content || undefined,
120
+ label: contentLabel(rc.content),
111
121
  register: parsed?.registerId ?? this.registerId,
112
122
  lang,
113
123
  });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Dataset-level stats shape. The data pipeline emits this as
3
+ * `public/data/{registerId}/stats.json`. Consumers fetch via HTTP.
4
+ *
5
+ * Kept in adapters/types.ts (the SSOT for wire shapes) so all views
6
+ * that need stats share the same definition.
7
+ */
8
+ export interface DatasetStats {
9
+ sourceCount: number;
10
+ sources: Array<{ ref: string; types: string[]; conceptCount: number }>;
11
+ relationshipCount: number;
12
+ relationshipTypes: Record<string, number>;
13
+ }
@@ -10,14 +10,15 @@
10
10
  // This file declares the runtime shape so consumer code can be type-checked.
11
11
  // DELETE this file when upstream ships proper declarations — tracked by
12
12
  // PR glossarist/glossarist-js#31 (targets v0.4.3+).
13
+ //
14
+ // v2 PartitiveRelation block (below) tracks glossarist 0.4.20 — the runtime
15
+ // exports PartitiveRelation/PartitiveMember/TypeSharedPlurality from
16
+ // src/models/index.js, but the d.ts has not been updated. Remove the v2
17
+ // block when upstream PRs the d.ts changes.
13
18
 
14
19
  import type { ConceptSource, Citation, GlossaristModel } from 'glossarist';
15
20
 
16
21
  declare module 'glossarist' {
17
- interface ConceptSource {
18
- sourced_from?: Citation[];
19
- }
20
-
21
22
  class RegistrableModel extends GlossaristModel {
22
23
  static register(type: string, cls: typeof RegistrableModel): void;
23
24
  static fromData(data: Record<string, unknown>): RegistrableModel;
@@ -134,4 +135,154 @@ declare module 'glossarist' {
134
135
  static fromYAML(yamlString: string): BibliographyData;
135
136
  static fromJSON(data: Record<string, unknown>): BibliographyData;
136
137
  }
138
+
139
+ // ── v2 PartitiveRelation (glossarist 0.4.20) ────────────────────────────
140
+ // Runtime exports these from src/models/index.js; d.ts is stale.
141
+
142
+ type Completeness = 'complete' | 'partial';
143
+ const COMPLETENESS: { readonly COMPLETE: 'complete'; readonly PARTIAL: 'partial' };
144
+ const COMPLETENESS_VALUES: readonly Completeness[];
145
+ const DEFAULT_COMPLETENESS: Completeness;
146
+ function isValidCompleteness(value: unknown): value is Completeness;
147
+
148
+ type MemberCertainty = 'confirmed' | 'possible';
149
+ const MEMBER_CERTAINTY: { readonly CONFIRMED: 'confirmed'; readonly POSSIBLE: 'possible' };
150
+ const MEMBER_CERTAINTY_VALUES: readonly MemberCertainty[];
151
+ const DEFAULT_MEMBER_CERTAINTY: MemberCertainty;
152
+ function isValidMemberCertainty(value: unknown): value is MemberCertainty;
153
+
154
+ class TypeSharedPlurality extends GlossaristModel {
155
+ constructor(data?: {
156
+ isShared?: boolean;
157
+ is_shared?: boolean;
158
+ isUncertain?: boolean;
159
+ is_uncertain?: boolean;
160
+ sharedType?: ConceptRef;
161
+ shared_type?: ConceptRef;
162
+ });
163
+ readonly isShared: boolean;
164
+ readonly isUncertain: boolean;
165
+ readonly sharedType: ConceptRef | null;
166
+ hasSharedType(): boolean;
167
+ toJSON(): { is_shared: boolean; is_uncertain?: boolean; shared_type?: ReturnType<ConceptRef['toJSON']> };
168
+ static fromJSON(data: Record<string, unknown>): TypeSharedPlurality;
169
+ }
170
+
171
+ class PartitiveMember extends GlossaristModel {
172
+ constructor(data?: { ref?: ConceptRef; certainty?: MemberCertainty });
173
+ readonly ref: ConceptRef;
174
+ readonly certainty: MemberCertainty;
175
+ readonly isConfirmed: boolean;
176
+ readonly isPossible: boolean;
177
+ toJSON(): { ref: ReturnType<ConceptRef['toJSON']>; certainty?: MemberCertainty };
178
+ static fromJSON(data: Record<string, unknown>): PartitiveMember;
179
+ static identityOf(value: unknown): string;
180
+ }
181
+
182
+ class PartitiveRelation extends GlossaristModel {
183
+ constructor(data?: {
184
+ comprehensive?: ConceptRef;
185
+ partitives?: PartitiveMember[];
186
+ completeness?: Completeness;
187
+ plurality?: TypeSharedPlurality;
188
+ criterion?: Record<string, string> | string;
189
+ });
190
+ readonly comprehensive: ConceptRef;
191
+ readonly partitives: PartitiveMember[];
192
+ readonly completeness: Completeness;
193
+ readonly plurality: TypeSharedPlurality | null;
194
+ readonly criterion: Record<string, string> | null;
195
+ readonly isComplete: boolean;
196
+ readonly isPartial: boolean;
197
+ readonly isCoordinate: boolean;
198
+ hasPlurality(): boolean;
199
+ hasCriterion(): boolean;
200
+ toJSON(): {
201
+ comprehensive: ReturnType<ConceptRef['toJSON']>;
202
+ partitives: ReturnType<PartitiveMember['toJSON']>[];
203
+ completeness: Completeness;
204
+ plurality?: ReturnType<TypeSharedPlurality['toJSON']>;
205
+ criterion?: Record<string, string>;
206
+ };
207
+ static fromJSON(data: Record<string, unknown>): PartitiveRelation;
208
+ static identityOf(value: unknown): string;
209
+ }
210
+
211
+ interface Concept {
212
+ readonly partitiveRelations: PartitiveRelation[];
213
+ }
214
+ }
215
+
216
+ // Re-declare the same exports under the 'glossarist/models' subpath so
217
+ // deep-import callers can use them. Upstream's d.ts is stale on this
218
+ // subpath too.
219
+ declare module 'glossarist/models' {
220
+ export type Completeness = 'complete' | 'partial';
221
+ export const COMPLETENESS: { readonly COMPLETE: 'complete'; readonly PARTIAL: 'partial' };
222
+ export const COMPLETENESS_VALUES: readonly Completeness[];
223
+ export const DEFAULT_COMPLETENESS: Completeness;
224
+ export function isValidCompleteness(value: unknown): value is Completeness;
225
+
226
+ export type MemberCertainty = 'confirmed' | 'possible';
227
+ export const MEMBER_CERTAINTY: { readonly CONFIRMED: 'confirmed'; readonly POSSIBLE: 'possible' };
228
+ export const MEMBER_CERTAINTY_VALUES: readonly MemberCertainty[];
229
+ export const DEFAULT_MEMBER_CERTAINTY: MemberCertainty;
230
+ export function isValidMemberCertainty(value: unknown): value is MemberCertainty;
231
+
232
+ export class TypeSharedPlurality extends GlossaristModel {
233
+ constructor(data?: {
234
+ isShared?: boolean;
235
+ is_shared?: boolean;
236
+ isUncertain?: boolean;
237
+ is_uncertain?: boolean;
238
+ sharedType?: ConceptRef;
239
+ shared_type?: ConceptRef;
240
+ });
241
+ readonly isShared: boolean;
242
+ readonly isUncertain: boolean;
243
+ readonly sharedType: ConceptRef | null;
244
+ hasSharedType(): boolean;
245
+ toJSON(): { is_shared: boolean; is_uncertain?: boolean; shared_type?: ReturnType<ConceptRef['toJSON']> };
246
+ static fromJSON(data: Record<string, unknown>): TypeSharedPlurality;
247
+ }
248
+
249
+ export class PartitiveMember extends GlossaristModel {
250
+ constructor(data?: { ref?: ConceptRef; certainty?: MemberCertainty });
251
+ readonly ref: ConceptRef;
252
+ readonly certainty: MemberCertainty;
253
+ readonly isConfirmed: boolean;
254
+ readonly isPossible: boolean;
255
+ toJSON(): { ref: ReturnType<ConceptRef['toJSON']>; certainty?: MemberCertainty };
256
+ static fromJSON(data: Record<string, unknown>): PartitiveMember;
257
+ static identityOf(value: unknown): string;
258
+ }
259
+
260
+ export class PartitiveRelation extends GlossaristModel {
261
+ constructor(data?: {
262
+ comprehensive?: ConceptRef;
263
+ partitives?: PartitiveMember[];
264
+ completeness?: Completeness;
265
+ plurality?: TypeSharedPlurality;
266
+ criterion?: Record<string, string> | string;
267
+ });
268
+ readonly comprehensive: ConceptRef;
269
+ readonly partitives: PartitiveMember[];
270
+ readonly completeness: Completeness;
271
+ readonly plurality: TypeSharedPlurality | null;
272
+ readonly criterion: Record<string, string> | null;
273
+ readonly isComplete: boolean;
274
+ readonly isPartial: boolean;
275
+ readonly isCoordinate: boolean;
276
+ hasPlurality(): boolean;
277
+ hasCriterion(): boolean;
278
+ toJSON(): {
279
+ comprehensive: ReturnType<ConceptRef['toJSON']>;
280
+ partitives: ReturnType<PartitiveMember['toJSON']>[];
281
+ completeness: Completeness;
282
+ plurality?: ReturnType<TypeSharedPlurality['toJSON']>;
283
+ criterion?: Record<string, string>;
284
+ };
285
+ static fromJSON(data: Record<string, unknown>): PartitiveRelation;
286
+ static identityOf(value: unknown): string;
287
+ }
137
288
  }
@@ -149,31 +149,41 @@ export interface GraphEdge {
149
149
  }
150
150
 
151
151
  /**
152
- * One-to-many partitive decomposition with optional plurality markers
153
- * and enumeration completeness (open vs closed).
152
+ * Wire-shape projection of a PartitiveRelation — used by:
153
+ * - build-edges.js output (partitive_relations.json)
154
+ * - GraphDataSource (loads the wire file for tools/future consumers)
155
+ * - use-concept-edges composable (projects the glossarist-js model
156
+ * into this shape for display, resolving ConceptRef → URI)
154
157
  *
155
- * Independent of {@link GraphEdge} (which stays binary). The UI renders
156
- * hyperedges specially (rake visualisation, enumeration/marker badges).
158
+ * The glossarist-js `PartitiveRelation` class is the model SSOT
159
+ * (ConceptRef-based). This interface is the *resolved-for-display*
160
+ * shape — keep the two concerns separate.
157
161
  *
158
- * See concept-model/TODO.hyperedge/00-design-overview.md.
162
+ * Renamed from `PartitiveRelation` to avoid collision with the
163
+ * upstream model class. v2 shape per concept-model
164
+ * TODO.partitive-relation-v2.
159
165
  */
160
- export interface PartitiveHyperedge {
161
- /** URI of the concept that owns this hyperedge (the source). */
166
+ export interface PartitiveRelationWire {
162
167
  source: string;
163
- /** URI of the comprehensive (whole) concept — usually equals `source`. */
164
168
  comprehensive: string;
165
- /** URIs of the partitive concepts. At least one. */
166
- parts: string[];
167
- /** 'closed' (all parts encoded) or 'open' (partial). Defaults to 'closed'. */
168
- enumeration: 'closed' | 'open';
169
- /** Diagram notation flags. Empty array if none. */
170
- markers: ('double' | 'dashed')[];
171
- /** Optional human-readable label/note. */
172
- label?: string;
173
- /** Owning dataset id. */
169
+ partitives: PartitiveMemberWire[];
170
+ completeness: 'complete' | 'partial';
171
+ plurality: TypeSharedPluralityWire | null;
172
+ criterion?: Record<string, string>;
174
173
  register: string;
175
174
  }
176
175
 
176
+ export interface PartitiveMemberWire {
177
+ uri: string;
178
+ certainty: 'confirmed' | 'possible';
179
+ }
180
+
181
+ export interface TypeSharedPluralityWire {
182
+ isShared: boolean;
183
+ isUncertain: boolean;
184
+ sharedType?: string | null;
185
+ }
186
+
177
187
  export interface GraphNode {
178
188
  uri: string;
179
189
  register: string;