glossarist 0.4.24 → 0.4.26

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glossarist",
3
- "version": "0.4.24",
3
+ "version": "0.4.26",
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",
@@ -205,7 +205,10 @@ function relationLabel(r) {
205
205
  const partitives = Array.isArray(r.partitives)
206
206
  ? r.partitives.map(m => {
207
207
  const ref = m?.ref ?? m ?? {};
208
- const tail = m?.certainty === 'possible' ? '?' : '';
208
+ let tail = '';
209
+ const mult = m?.multiplicity;
210
+ if (mult && mult !== 'compulsory') tail = ` (${mult})`;
211
+ if (m?.is_delimiting === true) tail += ' ⊘';
209
212
  return `${ref.id ?? ref.source ?? ref.text ?? '?'}${tail}`;
210
213
  }).join(', ')
211
214
  : '';
@@ -213,22 +216,36 @@ function relationLabel(r) {
213
216
  const criterion = r.criterion
214
217
  ? ` / ${Object.values(r.criterion)[0] ?? ''}`
215
218
  : '';
216
- const plurality = r.plurality
217
- ? ` [${_pluralityLabel(r.plurality)}]`
218
- : '';
219
- return `${head} → {${partitives}}${completeness}${criterion}${plurality}`;
219
+ return `${head} → {${partitives}}${completeness}${criterion}`;
220
220
  }
221
221
 
222
- function _pluralityLabel(p) {
223
- if (!p) return '';
224
- const flags = [];
225
- if (p.is_shared) flags.push('shared');
226
- if (p.is_uncertain) flags.push('uncertain');
227
- if (p.shared_type) {
228
- const t = p.shared_type;
229
- flags.push(`type=${t.id ?? t.source ?? '?'}`);
222
+ // Aggregate multiplicity + delimiting stats across a concept's
223
+ // PartitiveRelations. Used by dataset-level summaries, not by the
224
+ // per-relation diff label.
225
+ //
226
+ // v4: presence and count are independent dimensions. Stats include
227
+ // byPresence and byCount breakdowns so tools can query each axis
228
+ // independently ("show me all optional parts" or "show me all
229
+ // multiple-count parts").
230
+ export function multiplicityStats(relations) {
231
+ const byMultiplicity = {};
232
+ const byPresence = {};
233
+ const byCount = {};
234
+ let delimitingCount = 0;
235
+ let total = 0;
236
+ for (const rel of relations ?? []) {
237
+ for (const m of rel?.partitives ?? []) {
238
+ total += 1;
239
+ const mult = m?.multiplicity ?? 'compulsory';
240
+ byMultiplicity[mult] = (byMultiplicity[mult] ?? 0) + 1;
241
+ const p = m?.presence ?? 'required';
242
+ const c = m?.count ?? 'exactly_one';
243
+ byPresence[p] = (byPresence[p] ?? 0) + 1;
244
+ byCount[c] = (byCount[c] ?? 0) + 1;
245
+ if (m?.is_delimiting === true) delimitingCount += 1;
246
+ }
230
247
  }
231
- return flags.join(',');
248
+ return { total, byMultiplicity, byPresence, byCount, delimiting: delimitingCount };
232
249
  }
233
250
 
234
251
  function itemLabel(item) {
@@ -21,6 +21,25 @@
21
21
  // A v1 hyperedge with exactly 1 part cannot migrate to v2 and is
22
22
  // returned with a `migrationWarning` field set so callers can decide
23
23
  // whether to surface it (the parser drops these with a warning).
24
+ //
25
+ // ─── Migration decision table (v1 markers → v2 multiplicity) ──────
26
+ //
27
+ // v1 markers encoded per-relation plurality claims. v2 multiplicity
28
+ // is per-member. The mapping is lossy; reviewers must set per-member
29
+ // multiplicity individually after migration.
30
+ //
31
+ // | v1 markers | Likely multiplicity for each partitive | Notes |
32
+ // |-------------------------|------------------------------------------------|--------------------------------------------------------|
33
+ // | (none) | `compulsory` | Default; reviewer confirms |
34
+ // | `[double]` | `compulsory_multiple` or `compulsory_at_least_one` | Reviewer distinguishes "all required" from "≥1 required" |
35
+ // | `[dashed]` | `optional` or `optional_multiple` | Reviewer distinguishes single vs multiple |
36
+ // | `[double, dashed]` | `optional_multiple` or `compulsory_at_least_one` | Plurality claim; reviewer distinguishes uncertain vs required |
37
+ // | (single-part v1 edge) | (cannot migrate — needs ≥2 partitives) | Convert to a binary `has_part` edge |
38
+ //
39
+ // `is_delimiting` is NEVER set by migration. ISO 704:2022 says
40
+ // delimiting depends on the concept system and coordinate concepts,
41
+ // which migration cannot infer. Reviewer sets it based on domain
42
+ // knowledge (see DelimitingCoherenceRule for cross-relation checks).
24
43
 
25
44
  export function migrateHyperedgeToRelation(v1Hash) {
26
45
  if (v1Hash == null) return null;
@@ -35,7 +54,8 @@ export function migrateHyperedgeToRelation(v1Hash) {
35
54
  const parts = Array.isArray(v1Hash.parts) ? v1Hash.parts : [];
36
55
  out.partitives = parts.map(ref => ({
37
56
  ref: { ...ref },
38
- multiplicity: 'compulsory',
57
+ presence: 'required',
58
+ count: 'exactly_one',
39
59
  }));
40
60
 
41
61
  out.completeness = _migrateEnumeration(v1Hash.enumeration);
@@ -38,6 +38,18 @@ export {
38
38
  // backward-compat input. New code should use v2 exclusively.
39
39
  export { PartitiveRelation } from './partitive-relation.js';
40
40
  export { PartitiveMember } from './partitive-member.js';
41
+ export {
42
+ PARTITIVE_PRESENCE,
43
+ PARTITIVE_PRESENCE_VALUES,
44
+ DEFAULT_PRESENCE,
45
+ isValidPresence,
46
+ } from './partitive-presence.js';
47
+ export {
48
+ PARTITIVE_COUNT,
49
+ PARTITIVE_COUNT_VALUES,
50
+ DEFAULT_COUNT,
51
+ isValidCount,
52
+ } from './partitive-count.js';
41
53
  export {
42
54
  COMPLETENESS,
43
55
  COMPLETENESS_VALUES,
@@ -49,6 +61,8 @@ export {
49
61
  MULTIPLICITY_VALUES,
50
62
  DEFAULT_MULTIPLICITY,
51
63
  isValidMultiplicity,
64
+ multiplicityFromPair,
65
+ pairFromMultiplicity,
52
66
  } from './multiplicity.js';
53
67
 
54
68
  // v1 → v2 migration helper. Pure function; idempotent.
@@ -1,24 +1,74 @@
1
- // Multiplicity — ISO 704:2022 partitive member multiplicity.
1
+ // Multiplicity — ISO 704:2022 partitive multiplicity (derived view).
2
2
  //
3
- // Encodes the diagram line notation as structured data:
4
- // compulsory — 1 solid line (must exist in every instance)
5
- // optional — 1 dashed line (exists in some instances only)
6
- // compulsory_multiple — 2 solid lines (multiple must exist)
7
- // optional_multiple — 2 dashed lines (multiple may exist)
8
- // compulsory_at_least_one — 1 solid + 1 dashed line (at least one must exist)
3
+ // ISO 704:2022 multiplicity is the cross product of two orthogonal
4
+ // dimensions (PartitivePresence × PartitiveCount). The 5 valid
5
+ // combinations map to the 5 ISO 704 names:
9
6
  //
10
- // Defaults to `compulsory` when omitted.
7
+ // required × exactly_one → compulsory
8
+ // optional × exactly_one → optional
9
+ // required × multiple → compulsory_multiple
10
+ // optional × multiple → optional_multiple
11
+ // required × at_least_one → compulsory_at_least_one
12
+ //
13
+ // (optional × at_least_one is REDUNDANT with optional × multiple —
14
+ // "may exist, ≥1 if it does" = "may exist in any number" — and is
15
+ // explicitly rejected at construction with a clear error message.)
16
+ //
17
+ // This module is the SSOT for the (presence, count) → ISO name
18
+ // mapping. Both MULTIPLICITY constants and MULTIPLICITY_VALUES are
19
+ // derived from this single table; no duplicate string literals.
11
20
 
12
- import { makeEnum } from './enum.js';
21
+ import { PARTITIVE_PRESENCE } from './partitive-presence.js';
22
+ import { PARTITIVE_COUNT } from './partitive-count.js';
13
23
 
14
- export const MULTIPLICITY = makeEnum('MULTIPLICITY', {
15
- COMPULSORY: 'compulsory',
16
- OPTIONAL: 'optional',
17
- COMPULSORY_MULTIPLE: 'compulsory_multiple',
18
- OPTIONAL_MULTIPLE: 'optional_multiple',
19
- COMPULSORY_AT_LEAST_ONE: 'compulsory_at_least_one',
24
+ const NAME_BY_PAIR = Object.freeze({
25
+ [`${PARTITIVE_PRESENCE.REQUIRED}:${PARTITIVE_COUNT.EXACTLY_ONE}`]: 'compulsory',
26
+ [`${PARTITIVE_PRESENCE.OPTIONAL}:${PARTITIVE_COUNT.EXACTLY_ONE}`]: 'optional',
27
+ [`${PARTITIVE_PRESENCE.REQUIRED}:${PARTITIVE_COUNT.MULTIPLE}`]: 'compulsory_multiple',
28
+ [`${PARTITIVE_PRESENCE.OPTIONAL}:${PARTITIVE_COUNT.MULTIPLE}`]: 'optional_multiple',
29
+ [`${PARTITIVE_PRESENCE.REQUIRED}:${PARTITIVE_COUNT.AT_LEAST_ONE}`]: 'compulsory_at_least_one',
20
30
  });
21
31
 
22
- export const MULTIPLICITY_VALUES = MULTIPLICITY.VALUES;
32
+ const PAIR_BY_NAME = Object.freeze(
33
+ Object.fromEntries(
34
+ Object.entries(NAME_BY_PAIR).map(([pair, name]) => [name, pair]),
35
+ ),
36
+ );
37
+
38
+ export const MULTIPLICITY = Object.freeze(
39
+ Object.fromEntries(
40
+ Object.values(NAME_BY_PAIR).map(name => [name.toUpperCase(), name]),
41
+ ),
42
+ );
43
+
44
+ export const MULTIPLICITY_VALUES = Object.freeze(Object.values(NAME_BY_PAIR));
45
+
23
46
  export const DEFAULT_MULTIPLICITY = MULTIPLICITY.COMPULSORY;
24
- export const isValidMultiplicity = MULTIPLICITY.isValid;
47
+
48
+ export function multiplicityFromPair(presence, count) {
49
+ const name = NAME_BY_PAIR[`${presence}:${count}`];
50
+ if (name == null) {
51
+ throw invalidCombinationError(presence, count);
52
+ }
53
+ return name;
54
+ }
55
+
56
+ export function pairFromMultiplicity(name) {
57
+ const pair = PAIR_BY_NAME[name];
58
+ if (pair == null) {
59
+ throw new Error(`Unknown multiplicity: ${JSON.stringify(name)}`);
60
+ }
61
+ const [presence, count] = pair.split(':');
62
+ return { presence, count };
63
+ }
64
+
65
+ export function isValidMultiplicity(name) {
66
+ return Object.prototype.hasOwnProperty.call(PAIR_BY_NAME, name);
67
+ }
68
+
69
+ function invalidCombinationError(presence, count) {
70
+ return new Error(
71
+ `Invalid multiplicity combination: presence=${presence}, count=${count}. ` +
72
+ `(optional + at_least_one collapses to optional + multiple — use count=multiple.)`,
73
+ );
74
+ }
@@ -0,0 +1,20 @@
1
+ // PartitiveCount — ISO 704:2022 partitive member count.
2
+ //
3
+ // count (line count):
4
+ // exactly_one — 1 line (exactly one instance)
5
+ // at_least_one — 1 solid + 1 dashed (at least one instance)
6
+ // multiple — 2 lines (multiple instances)
7
+ //
8
+ // Default: exactly_one.
9
+
10
+ import { makeEnum } from './enum.js';
11
+
12
+ export const PARTITIVE_COUNT = makeEnum('PARTITIVE_COUNT', {
13
+ EXACTLY_ONE: 'exactly_one',
14
+ AT_LEAST_ONE: 'at_least_one',
15
+ MULTIPLE: 'multiple',
16
+ });
17
+
18
+ export const PARTITIVE_COUNT_VALUES = PARTITIVE_COUNT.VALUES;
19
+ export const DEFAULT_COUNT = PARTITIVE_COUNT.EXACTLY_ONE;
20
+ export const isValidCount = PARTITIVE_COUNT.isValid;
@@ -1,29 +1,35 @@
1
1
  // PartitiveMember — one member of a PartitiveRelation.
2
2
  //
3
3
  // Carries a ConceptRef (the subordinate concept partitive) plus
4
- // ISO 704:2022 multiplicity and delimiting metadata.
4
+ // ISO 704:2022 presence + count + is_delimiting.
5
5
  //
6
- // ISO 704:2022 partitive member notation:
6
+ // Matches concept-model/models/concepts/PartitiveMember.lutaml exactly:
7
+ // +ref — ConceptRef [1]
8
+ // +presence — PartitivePresence [0..1] (default: required)
9
+ // +count — PartitiveCount [0..1] (default: exactly_one)
10
+ // +is_delimiting — Boolean [0..1] (default: false)
7
11
  //
8
- // multiplicity (diagram line notation):
9
- // compulsory — 1 solid line (must exist in every instance)
10
- // optional — 1 dashed line (exists in some instances only)
11
- // compulsory_multiple — 2 solid lines (multiple must exist)
12
- // optional_multiple — 2 dashed lines (multiple may exist)
13
- // compulsory_at_least_one — 1 solid + 1 dashed line (≥1 must exist)
12
+ // The combination (optional, at_least_one) is invalid: if a part is
13
+ // optional, the at-least-one constraint is vacuous and collapses to
14
+ // (optional, multiple). Rejected at construction.
14
15
  //
15
- // is_delimiting (orthogonal, bold 3x-width line in diagram):
16
- // A delimiting part behaves like a delimiting characteristic:
17
- // it distinguishes the comprehensive from coordinate concepts.
16
+ // ISO 704 "name" strings (compulsory, optional, etc.) are NOT part of
17
+ // the model — they're a display concern handled by multiplicityFromPair
18
+ // in the renderer/viewer layer. The model is purely presence + count.
18
19
 
19
20
  import { GlossaristModel } from './base.js';
20
21
  import { ConceptRef } from './concept-ref.js';
21
22
  import {
22
- MULTIPLICITY,
23
- DEFAULT_MULTIPLICITY,
24
- MULTIPLICITY_VALUES,
25
- isValidMultiplicity,
26
- } from './multiplicity.js';
23
+ PARTITIVE_PRESENCE,
24
+ DEFAULT_PRESENCE,
25
+ PARTITIVE_PRESENCE_VALUES,
26
+ isValidPresence,
27
+ } from './partitive-presence.js';
28
+ import {
29
+ DEFAULT_COUNT,
30
+ PARTITIVE_COUNT_VALUES,
31
+ isValidCount,
32
+ } from './partitive-count.js';
27
33
 
28
34
  export class PartitiveMember extends GlossaristModel {
29
35
  constructor(data = {}) {
@@ -31,23 +37,17 @@ export class PartitiveMember extends GlossaristModel {
31
37
  this.ref = data.ref instanceof ConceptRef
32
38
  ? data.ref
33
39
  : new ConceptRef(data.ref ?? {});
34
- this.multiplicity = _resolveMultiplicity(data.multiplicity);
40
+ this.presence = _resolvePresence(data.presence);
41
+ this.count = _resolveCount(data.count);
35
42
  this.is_delimiting = data.is_delimiting === true;
36
43
 
37
44
  _assertNonEmptyRef(this.ref);
45
+ _assertValidCombination(this.presence, this.count);
38
46
  }
39
47
 
40
- get isCompulsory() {
41
- return this.multiplicity === MULTIPLICITY.COMPULSORY;
42
- }
43
-
44
- get isOptional() {
45
- return this.multiplicity === MULTIPLICITY.OPTIONAL;
46
- }
47
-
48
- get isDelimiting() {
49
- return this.is_delimiting === true;
50
- }
48
+ get isRequired() { return this.presence === PARTITIVE_PRESENCE.REQUIRED; }
49
+ get isOptional() { return this.presence === PARTITIVE_PRESENCE.OPTIONAL; }
50
+ get isDelimiting() { return this.is_delimiting === true; }
51
51
 
52
52
  identity() {
53
53
  return PartitiveMember.identityOf(this);
@@ -55,12 +55,9 @@ export class PartitiveMember extends GlossaristModel {
55
55
 
56
56
  toJSON() {
57
57
  const obj = { ref: this.ref.toJSON() };
58
- if (this.multiplicity !== DEFAULT_MULTIPLICITY) {
59
- obj.multiplicity = this.multiplicity;
60
- }
61
- if (this.is_delimiting === true) {
62
- obj.is_delimiting = true;
63
- }
58
+ if (this.presence !== DEFAULT_PRESENCE) obj.presence = this.presence;
59
+ if (this.count !== DEFAULT_COUNT) obj.count = this.count;
60
+ if (this.is_delimiting === true) obj.is_delimiting = true;
64
61
  return obj;
65
62
  }
66
63
 
@@ -75,22 +72,34 @@ export class PartitiveMember extends GlossaristModel {
75
72
  }
76
73
  }
77
74
 
78
- function _resolveMultiplicity(value) {
79
- if (value == null) return DEFAULT_MULTIPLICITY;
80
- if (!isValidMultiplicity(value)) {
81
- throw new Error(
82
- `invalid multiplicity ${JSON.stringify(value)}; ` +
83
- `must be one of ${MULTIPLICITY_VALUES.join(', ')}`,
84
- );
75
+ function _resolvePresence(value) {
76
+ if (value == null) return DEFAULT_PRESENCE;
77
+ if (!isValidPresence(value)) {
78
+ throw new Error(`invalid presence ${JSON.stringify(value)}; must be one of ${PARTITIVE_PRESENCE_VALUES.join(', ')}`);
79
+ }
80
+ return value;
81
+ }
82
+
83
+ function _resolveCount(value) {
84
+ if (value == null) return DEFAULT_COUNT;
85
+ if (!isValidCount(value)) {
86
+ throw new Error(`invalid count ${JSON.stringify(value)}; must be one of ${PARTITIVE_COUNT_VALUES.join(', ')}`);
85
87
  }
86
88
  return value;
87
89
  }
88
90
 
89
91
  function _assertNonEmptyRef(ref) {
90
92
  if (!ref.source && !ref.id && !ref.text) {
93
+ throw new Error('PartitiveMember#ref must be a non-empty ConceptRef (source, id, or text required)');
94
+ }
95
+ }
96
+
97
+ function _assertValidCombination(presence, count) {
98
+ if (presence === 'optional' && count === 'at_least_one') {
91
99
  throw new Error(
92
- 'PartitiveMember#ref must be a non-empty ConceptRef ' +
93
- '(source, id, or text required)',
100
+ 'PartitiveMember presence=optional + count=at_least_one is invalid — ' +
101
+ 'it collapses to optional + multiple (zero or more). ' +
102
+ 'Use presence: optional, count: multiple instead.',
94
103
  );
95
104
  }
96
105
  }
@@ -0,0 +1,18 @@
1
+ // PartitivePresence — ISO 704:2022 partitive member presence.
2
+ //
3
+ // presence (line style):
4
+ // required — solid line (must exist in every instance)
5
+ // optional — dashed line (may exist; exists in some instances)
6
+ //
7
+ // Default: required.
8
+
9
+ import { makeEnum } from './enum.js';
10
+
11
+ export const PARTITIVE_PRESENCE = makeEnum('PARTITIVE_PRESENCE', {
12
+ REQUIRED: 'required',
13
+ OPTIONAL: 'optional',
14
+ });
15
+
16
+ export const PARTITIVE_PRESENCE_VALUES = PARTITIVE_PRESENCE.VALUES;
17
+ export const DEFAULT_PRESENCE = PARTITIVE_PRESENCE.REQUIRED;
18
+ export const isValidPresence = PARTITIVE_PRESENCE.isValid;
@@ -1,19 +1,33 @@
1
- // PartitiveRelation — an ISO 704 / ISO 1087-1 / ISO 12620
1
+ // PartitiveRelation — an ISO 704:2022 / ISO 1087-1 / ISO 12620
2
2
  // partitive relation connecting a comprehensive concept
3
3
  // (superordinate concept partitive) to two or more partitive
4
4
  // concepts (subordinate concepts partitive) which fitted together
5
5
  // constitute the comprehensive.
6
6
  //
7
+ // ISO 704:2022 rake diagram semantics:
8
+ //
9
+ // comprehensive concept
10
+ // ─────────────────────
11
+ // │ partitive (one or more) with per-member multiplicity + optional delimiting
12
+ // ─────────────────────
13
+ //
14
+ // All partitives within one relation are coordinate concepts
15
+ // (ISO 12620): they share the comprehensive AND share the
16
+ // criterion of subdivision.
17
+ //
18
+ // "Delimiting" parts (bold/3x-width line in source diagrams) behave
19
+ // like delimiting characteristics — they distinguish the comprehensive
20
+ // from coordinate concepts. Whether a part is delimiting depends on
21
+ // the concept system, the coordinate concepts, the inheritance
22
+ // principle, and the criterion of subdivision used
23
+ // (ISO 704:2022 §5.5.4.2.1).
24
+ //
7
25
  // Field renames (v1 → v2):
8
26
  // parts [1..*] → partitives [2..*] (as PartitiveMember)
9
27
  // enumeration: closed|open → completeness: complete|partial
28
+ // markers: [double, dashed] → (per-member multiplicity + is_delimiting)
10
29
  // content: LocalizedString → (dropped — structural edges carry no prose)
11
- //
12
- // Per-member metadata (ISO 704:2022):
13
- // multiplicity (5 values) + is_delimiting (boolean)
14
- //
15
- // New field:
16
- // criterion: LocalizedString [0..1] (ISO 12620 coordinate-concept coherence)
30
+ // plural-positive is_shared / is_uncertain → multiplicity (ISO 704:2022)
17
31
  //
18
32
  // Distinguished from binary `has_part` / `is_part_of` edges, which
19
33
  // express pairwise part-of assertions without completeness,
@@ -0,0 +1,67 @@
1
+ // PartitiveMember → RDF quads.
2
+ //
3
+ // Emits per-member predicates (presence, count, is_delimiting,
4
+ // derived multiplicity) onto the member's concept URI. The member URI
5
+ // is constructed by the caller (typically `gloss-partitive-relation.js`).
6
+ //
7
+ // Extracted from `gloss-partitive-relation.js` per
8
+ // TODO.partitive-relation-v3/04 so future emitters (a standalone
9
+ // PartitiveMember listing, a stats aggregator) can reuse this
10
+ // emission without modifying the relation emitter (OCP).
11
+ //
12
+ // v4 decomposition: ISO 704:2022 multiplicity is the cross product of
13
+ // two orthogonal dimensions (presence × count). Per the JSON-LD
14
+ // context (`"@type": "@id"`), all three are emitted as URI references
15
+ // into the corresponding SKOS taxonomy namespaces — none as literals.
16
+ //
17
+ // <member-uri> gloss:presence <.../presence/required|optional>
18
+ // <member-uri> gloss:count <.../count/exactly_one|at_least_one|multiple>
19
+ // <member-uri> gloss:multiplicity <.../multiplicity/{ISO name}> (derived, v3 compat)
20
+ // <member-uri> gloss:isDelimiting "true" (xsd:boolean)
21
+
22
+ import { PRED } from './predicates.js';
23
+ import { namedNode, literal, quad } from './terms.js';
24
+
25
+ // SKOS taxonomy namespaces (concept-model will ship these taxonomies).
26
+ // Until then, the URIs are forward-compatible and resolve once the
27
+ // taxonomies land.
28
+ export const PRESENCE_NS = 'https://www.glossarist.org/ontologies/presence/';
29
+ export const COUNT_NS = 'https://www.glossarist.org/ontologies/count/';
30
+ export const MULTIPLICITY_NS = 'https://www.glossarist.org/ontologies/multiplicity/';
31
+
32
+ export function* partitiveMemberToQuads(member, { memberUri }) {
33
+ if (member?.presence) {
34
+ yield quad(
35
+ namedNode(memberUri),
36
+ namedNode(PRED.gloss.presence),
37
+ namedNode(PRESENCE_NS + member.presence),
38
+ );
39
+ }
40
+ if (member?.count) {
41
+ yield quad(
42
+ namedNode(memberUri),
43
+ namedNode(PRED.gloss.count),
44
+ namedNode(COUNT_NS + member.count),
45
+ );
46
+ }
47
+ // Derived multiplicity URI for v3 RDF consumer backward compat.
48
+ // multiplicity is computed from (presence, count) via the model's
49
+ // accessor; fall back to the raw field for raw JSON input.
50
+ const mult = typeof member?.multiplicity === 'string'
51
+ ? member.multiplicity
52
+ : null;
53
+ if (mult) {
54
+ yield quad(
55
+ namedNode(memberUri),
56
+ namedNode(PRED.gloss.multiplicity),
57
+ namedNode(MULTIPLICITY_NS + mult),
58
+ );
59
+ }
60
+ if (member?.is_delimiting === true) {
61
+ yield quad(
62
+ namedNode(memberUri),
63
+ namedNode(PRED.gloss.isDelimiting),
64
+ literal('true', 'xsd:boolean'),
65
+ );
66
+ }
67
+ }
@@ -6,22 +6,31 @@
6
6
  // rdf:type gloss:PartitiveRelation
7
7
  // gloss:comprehensive <base>/concept/<comprehensive-id> (named node)
8
8
  // gloss:hasPartitive <base>/concept/<partitive-id> (named node, one per member)
9
- // gloss:completeness "complete"|"partial" (literal)
10
- // gloss:criterion "..."@lang (one per language, when present)
9
+ // gloss:completeness <.../completeness/complete|partial> (named node, SKOS)
10
+ // gloss:criterion "..."@lang (literal, one per language)
11
11
  //
12
- // Per-member multiplicity + is_delimiting are emitted as part of
13
- // each member's named-node object via the partitiveMemberToQuads
14
- // helper (separate file).
12
+ // Per-member multiplicity + is_delimiting are emitted by the
13
+ // `partitiveMemberToQuads` helper in `gloss-partitive-member.js`.
15
14
  //
16
15
  // The carrying concept links via `gloss:hasPartitiveRelation`.
17
16
  //
18
- // Replaces gloss-hyperedge.js (the v1 emitter) per TODO.partitive-relation-v2 item 01.
17
+ // Replaces gloss-hyperedge.js (the v1 emitter) per
18
+ // TODO.partitive-relation-v2 item 01.
19
19
  // Cross-repo alignment with glossarist-ruby's Rdf::GlossPartitiveRelation.
20
+ //
21
+ // JSON-LD `@type: @id` invariant: `multiplicity` and `completeness`
22
+ // are URI references into SKOS taxonomies (per
23
+ // `data/concept-model/glossarist.context.jsonld`), not literals.
24
+ // `criterion` is `@container: @language` (one literal per language).
25
+ // `isDelimiting` is `xsd:boolean` literal.
20
26
 
21
27
  import { PRED } from './predicates.js';
22
28
  import { WELL_KNOWN } from './prefixes.js';
29
+ import { partitiveMemberToQuads } from './gloss-partitive-member.js';
23
30
  import { namedNode, literal, quad } from './terms.js';
24
31
 
32
+ const COMPLETENESS_NS = 'https://www.glossarist.org/ontologies/completeness/';
33
+
25
34
  export function* partitiveRelationToQuads(relation, { parentUri, index }) {
26
35
  const subjectUri = partitiveRelationSubjectUri(parentUri, relation, index);
27
36
  const s = namedNode(subjectUri);
@@ -40,20 +49,12 @@ export function* partitiveRelationToQuads(relation, { parentUri, index }) {
40
49
  if (!ref || (!ref.source && !ref.id)) continue;
41
50
  const memberUri = conceptRefUri(ref, parentUri);
42
51
  yield quad(s, namedNode(PRED.gloss.hasPartitive), namedNode(memberUri));
43
-
44
- // Per-member multiplicity (ISO 704:2022) and is_delimiting.
45
- if (member?.multiplicity) {
46
- yield quad(namedNode(memberUri), namedNode(PRED.gloss.multiplicity),
47
- literal(member.multiplicity));
48
- }
49
- if (member?.is_delimiting === true) {
50
- yield quad(namedNode(memberUri), namedNode(PRED.gloss.isDelimiting),
51
- literal('true', 'xsd:boolean'));
52
- }
52
+ yield* partitiveMemberToQuads(member, { memberUri });
53
53
  }
54
54
 
55
55
  if (relation.completeness) {
56
- yield quad(s, namedNode(PRED.gloss.completeness), literal(relation.completeness));
56
+ yield quad(s, namedNode(PRED.gloss.completeness),
57
+ namedNode(COMPLETENESS_NS + relation.completeness));
57
58
  }
58
59
 
59
60
  if (relation.criterion && typeof relation.criterion === 'object') {
@@ -11,7 +11,7 @@ import type { Designation } from '../models/index';
11
11
  import type { DetailedDefinition } from '../models/index';
12
12
  import type { ConceptSource } from '../models/index';
13
13
  import type { NonVerbRep, Figure, Table, Formula } from '../models/index';
14
- import type { PartitiveHyperedge, PartitiveRelation } from '../models/index';
14
+ import type { PartitiveHyperedge, PartitiveRelation, PartitiveMember } from '../models/index';
15
15
 
16
16
  export { PRED, PREFIXES } from './predicates';
17
17
  export type PredicateMap = typeof PRED;
@@ -114,6 +114,21 @@ export declare function partitiveRelationSubjectUri(
114
114
  index: number,
115
115
  ): string;
116
116
 
117
+ // ── PartitiveMember emitter (per-member multiplicity + is_delimiting) ──
118
+
119
+ export interface PartitiveMemberEmitOptions {
120
+ memberUri: string;
121
+ }
122
+
123
+ export declare const MULTIPLICITY_NS: string;
124
+ export declare const PRESENCE_NS: string;
125
+ export declare const COUNT_NS: string;
126
+
127
+ export declare function partitiveMemberToQuads(
128
+ member: PartitiveMember,
129
+ options: PartitiveMemberEmitOptions,
130
+ ): Generator<Quad, void, unknown>;
131
+
117
132
  // ── Non-verbal representation emitters ─────────────────────────────────
118
133
 
119
134
  export interface NonVerbalRepEmitOptions {
package/src/rdf/index.js CHANGED
@@ -27,6 +27,7 @@ export {
27
27
  export { detailedDefinitionToQuads } from './gloss-detailed-definition.js';
28
28
  export { conceptSourceToQuads } from './gloss-source.js';
29
29
  export { hyperedgeToQuads, hyperedgeSubjectUri } from './gloss-hyperedge.js';
30
+ export { partitiveMemberToQuads, MULTIPLICITY_NS, PRESENCE_NS, COUNT_NS } from './gloss-partitive-member.js';
30
31
  export { partitiveRelationToQuads, partitiveRelationSubjectUri } from './gloss-partitive-relation.js';
31
32
  export {
32
33
  nonVerbalRepToQuads,
@@ -110,6 +110,8 @@ export declare const PRED = {
110
110
  "hasPartitiveRelation": "https://www.glossarist.org/ontologies/hasPartitiveRelation",
111
111
  "hasPartitive": "https://www.glossarist.org/ontologies/hasPartitive",
112
112
  "completeness": "https://www.glossarist.org/ontologies/completeness",
113
+ "presence": "https://www.glossarist.org/ontologies/presence",
114
+ "count": "https://www.glossarist.org/ontologies/count",
113
115
  "multiplicity": "https://www.glossarist.org/ontologies/multiplicity",
114
116
  "isDelimiting": "https://www.glossarist.org/ontologies/isDelimiting",
115
117
  "criterion": "https://www.glossarist.org/ontologies/criterion",
@@ -392,6 +394,8 @@ export type Predicate =
392
394
  | "https://www.glossarist.org/ontologies/hasPartitiveRelation"
393
395
  | "https://www.glossarist.org/ontologies/hasPartitive"
394
396
  | "https://www.glossarist.org/ontologies/completeness"
397
+ | "https://www.glossarist.org/ontologies/presence"
398
+ | "https://www.glossarist.org/ontologies/count"
395
399
  | "https://www.glossarist.org/ontologies/multiplicity"
396
400
  | "https://www.glossarist.org/ontologies/isDelimiting"
397
401
  | "https://www.glossarist.org/ontologies/criterion"
@@ -110,6 +110,8 @@ export const PRED = {
110
110
  "hasPartitiveRelation": "https://www.glossarist.org/ontologies/hasPartitiveRelation",
111
111
  "hasPartitive": "https://www.glossarist.org/ontologies/hasPartitive",
112
112
  "completeness": "https://www.glossarist.org/ontologies/completeness",
113
+ "presence": "https://www.glossarist.org/ontologies/presence",
114
+ "count": "https://www.glossarist.org/ontologies/count",
113
115
  "multiplicity": "https://www.glossarist.org/ontologies/multiplicity",
114
116
  "isDelimiting": "https://www.glossarist.org/ontologies/isDelimiting",
115
117
  "criterion": "https://www.glossarist.org/ontologies/criterion",
@@ -12,6 +12,7 @@
12
12
  // Severity: warning. Never blocks save.
13
13
 
14
14
  import { ValidationRule } from './validation-rule.js';
15
+ import { refKey } from './ref-keys.js';
15
16
 
16
17
  const BINARY_HAS_PART_TYPES = new Set(['has_part', 'is_part_of']);
17
18
 
@@ -24,11 +25,11 @@ export class BinaryHasPartRedundancyRule extends ValidationRule {
24
25
  // Collect partitive refs from PartitiveRelations.
25
26
  const relationTargets = new Set();
26
27
  for (const rel of relations) {
27
- const compKey = _refKey(rel?.comprehensive);
28
+ const compKey = refKey(rel?.comprehensive);
28
29
  const members = rel?.partitives ?? rel?.parts ?? [];
29
30
  for (const member of members) {
30
31
  const ref = member?.ref ?? member;
31
- const key = _refKey(ref);
32
+ const key = refKey(ref);
32
33
  if (key && key !== compKey) relationTargets.add(key);
33
34
  }
34
35
  }
@@ -38,7 +39,7 @@ export class BinaryHasPartRedundancyRule extends ValidationRule {
38
39
  const related = concept.relatedConcepts ?? [];
39
40
  for (const rc of related) {
40
41
  if (!BINARY_HAS_PART_TYPES.has(rc.type)) continue;
41
- const key = _refKey(rc?.ref);
42
+ const key = refKey(rc?.ref);
42
43
  if (key) binaryTargets.add(key);
43
44
  }
44
45
 
@@ -59,16 +60,8 @@ export class BinaryHasPartRedundancyRule extends ValidationRule {
59
60
  `${path}relatedConcepts`,
60
61
  `${binaryTargets.size} binary has_part edges on this concept; ` +
61
62
  `consider converting to a PartitiveRelation (which can carry ` +
62
- `completeness, plurality, and criterion metadata)`,
63
+ `completeness, multiplicity, and criterion metadata)`,
63
64
  );
64
65
  }
65
66
  }
66
67
  }
67
-
68
- function _refKey(ref) {
69
- if (!ref) return null;
70
- const source = ref.source ?? '';
71
- const id = ref.id ?? '';
72
- if (!source && !id) return null;
73
- return `${source}:${id}`;
74
- }
@@ -0,0 +1,105 @@
1
+ // Validates delimiting coherence across a concept's PartitiveRelations
2
+ // per ISO 704:2022.
3
+ //
4
+ // ISO 704:2022 (§5.5.4.2.1, rake diagram semantics):
5
+ // "Whether a part is delimiting depends on the concept system, the
6
+ // coordinate concepts, the inheritance principle, and the criterion
7
+ // of subdivision used."
8
+ //
9
+ // This rule surfaces three categories of issues that the model
10
+ // constructor cannot catch (they're cross-relation, not per-instance):
11
+ //
12
+ // 1. WARNING: A PartitiveRelation where every partitive is
13
+ // is_delimiting=true overuses delimiting. Typically only some
14
+ // parts distinguish the comprehensive from coordinate concepts.
15
+ // 2. ERROR: Two PartitiveRelations on the same concept with the
16
+ // same criterion but different is_delimiting for the same
17
+ // partitive ref — inconsistent delimiting claim about the same
18
+ // part. ISO 704 requires delimiting claims about a given part to
19
+ // be consistent across decompositions sharing a criterion.
20
+ // 3. WARNING: A partitive marked is_delimiting=true on a relation
21
+ // with no criterion set. Delimiting is meaningful only relative
22
+ // to a subdivision criterion.
23
+ //
24
+ // Per TODO.partitive-relation-v3/06.
25
+
26
+ import { ValidationRule } from './validation-rule.js';
27
+ import { refKey, criterionKey } from './ref-keys.js';
28
+
29
+ export class DelimitingCoherenceRule extends ValidationRule {
30
+ constructor() { super('delimiting-coherence'); }
31
+
32
+ validate(concept, path, result) {
33
+ const relations = concept.partitiveRelations ?? concept.partitiveHyperedges ?? [];
34
+ if (relations.length === 0) return;
35
+
36
+ _checkOveruse(relations, path, result);
37
+ _checkInconsistentAcrossRelations(relations, path, result, this);
38
+ _checkDelimitingWithoutCriterion(relations, path, result);
39
+ }
40
+ }
41
+
42
+ function _checkOveruse(relations, path, result) {
43
+ for (let i = 0; i < relations.length; i++) {
44
+ const rel = relations[i];
45
+ const members = rel?.partitives ?? [];
46
+ if (members.length < 2) continue;
47
+ const delimitingCount = members.filter(m => m?.is_delimiting === true).length;
48
+ if (delimitingCount === members.length) {
49
+ result.addWarning(
50
+ `${path}partitiveRelations[${i}]`,
51
+ `every partitive is marked is_delimiting=true; typically only ` +
52
+ `some parts distinguish the comprehensive from coordinate concepts`,
53
+ );
54
+ }
55
+ }
56
+ }
57
+
58
+ function _checkInconsistentAcrossRelations(relations, path, result, rule) {
59
+ const byCriterion = new Map();
60
+ for (let i = 0; i < relations.length; i++) {
61
+ const rel = relations[i];
62
+ const critKey = criterionKey(rel?.criterion);
63
+ if (critKey == null) continue;
64
+ if (!byCriterion.has(critKey)) byCriterion.set(critKey, []);
65
+ byCriterion.get(critKey).push({ rel, i });
66
+ }
67
+
68
+ for (const [, group] of byCriterion) {
69
+ const refDelimiting = new Map();
70
+ for (const { rel, i } of group) {
71
+ for (const m of rel?.partitives ?? []) {
72
+ const key = refKey(m?.ref);
73
+ if (key == null) continue;
74
+ const delimiting = m?.is_delimiting === true;
75
+ const existing = refDelimiting.get(key);
76
+ if (existing != null && existing.delimiting !== delimiting) {
77
+ rule.addIssue(result,
78
+ `${path}partitiveRelations[${i}].partitives`,
79
+ `inconsistent is_delimiting for ref (${key}) across ` +
80
+ `relations ${existing.relIdx} and ${i} with the same ` +
81
+ `criterion; ISO 704 requires delimiting claims about the ` +
82
+ `same part to be consistent`,
83
+ );
84
+ } else if (existing == null) {
85
+ refDelimiting.set(key, { delimiting, relIdx: i });
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ function _checkDelimitingWithoutCriterion(relations, path, result) {
93
+ for (let i = 0; i < relations.length; i++) {
94
+ const rel = relations[i];
95
+ if (rel?.criterion) continue;
96
+ const anyDelimiting = (rel?.partitives ?? []).some(m => m?.is_delimiting === true);
97
+ if (anyDelimiting) {
98
+ result.addWarning(
99
+ `${path}partitiveRelations[${i}].criterion`,
100
+ `relation has is_delimiting=true on partitives but no criterion; ` +
101
+ `delimiting is meaningful only relative to a subdivision criterion`,
102
+ );
103
+ }
104
+ }
105
+ }
@@ -23,6 +23,7 @@ export { SourcedFromLocalityRule } from './sourced-from-locality-rule.js';
23
23
  export { PartitiveRelationCoherenceRule } from './partitive-relation-coherence-rule.js';
24
24
  export { ExternalConceptShapeRule } from './external-concept-shape-rule.js';
25
25
  export { BinaryHasPartRedundancyRule } from './binary-has-part-redundancy-rule.js';
26
+ export { DelimitingCoherenceRule } from './delimiting-coherence-rule.js';
26
27
 
27
28
  import { ConceptValidator, LanguageCodeRule, DesignationTypeRule, EntryStatusRule } from './concept-validator.js';
28
29
  import { RegisterValidator } from './register-validator.js';
@@ -45,6 +46,7 @@ import { SourcedFromLocalityRule } from './sourced-from-locality-rule.js';
45
46
  import { PartitiveRelationCoherenceRule } from './partitive-relation-coherence-rule.js';
46
47
  import { ExternalConceptShapeRule } from './external-concept-shape-rule.js';
47
48
  import { BinaryHasPartRedundancyRule } from './binary-has-part-redundancy-rule.js';
49
+ import { DelimitingCoherenceRule } from './delimiting-coherence-rule.js';
48
50
 
49
51
  const _default = new ConceptValidator()
50
52
  .addRule(new LanguageCodeRule())
@@ -65,7 +67,8 @@ const _default = new ConceptValidator()
65
67
  .addRule(new SourcedFromLocalityRule())
66
68
  .addRule(new PartitiveRelationCoherenceRule())
67
69
  .addRule(new ExternalConceptShapeRule())
68
- .addRule(new BinaryHasPartRedundancyRule());
70
+ .addRule(new BinaryHasPartRedundancyRule())
71
+ .addRule(new DelimitingCoherenceRule());
69
72
 
70
73
  export function validateConcept(concept) {
71
74
  return _default.validate(concept);
@@ -17,6 +17,7 @@
17
17
  // validators enforce semantics.
18
18
 
19
19
  import { ValidationRule } from './validation-rule.js';
20
+ import { refKey, criterionKey } from './ref-keys.js';
20
21
 
21
22
  export class PartitiveRelationCoherenceRule extends ValidationRule {
22
23
  constructor() { super('partitive-relation-coherence'); }
@@ -43,8 +44,8 @@ export class PartitiveRelationCoherenceRule extends ValidationRule {
43
44
  const seen = new Map();
44
45
  for (let i = 0; i < relations.length; i++) {
45
46
  const rel = relations[i];
46
- const compKey = _conceptRefKey(rel?.comprehensive);
47
- const critKey = _criterionKey(rel?.criterion);
47
+ const compKey = refKey(rel?.comprehensive);
48
+ const critKey = criterionKey(rel?.criterion);
48
49
  if (compKey == null || critKey == null) continue;
49
50
  const key = `${compKey}|${critKey}`;
50
51
  if (seen.has(key)) {
@@ -77,22 +78,3 @@ export class PartitiveRelationCoherenceRule extends ValidationRule {
77
78
  // at construction by PartitiveMember itself.)
78
79
  }
79
80
  }
80
-
81
- function _conceptRefKey(ref) {
82
- if (!ref) return null;
83
- const source = ref.source ?? '';
84
- const id = ref.id ?? '';
85
- if (!source && !id) return null;
86
- return `${source}:${id}`;
87
- }
88
-
89
- function _criterionKey(criterion) {
90
- if (!criterion || typeof criterion !== 'object') return null;
91
- // Sort values for stable comparison regardless of language key order.
92
- const values = Object.values(criterion)
93
- .filter(v => typeof v === 'string')
94
- .map(s => s.trim())
95
- .filter(s => s.length > 0)
96
- .sort();
97
- return values.length > 0 ? values.join('|') : null;
98
- }
@@ -0,0 +1,31 @@
1
+ // Shared identity-key helpers for cross-relation validators.
2
+ //
3
+ // Extracted from PartitiveRelationCoherenceRule and
4
+ // BinaryHasPartRedundancyRule per TODO.partitive-relation-v3/06.
5
+ // Eliminates drift: both rules now use the same notion of "same ref"
6
+ // and "same criterion" instead of each defining its own _refKey and
7
+ // _criterionKey with subtly different behaviors.
8
+
9
+ export function refKey(ref) {
10
+ if (!ref) return null;
11
+ const source = ref.source ?? '';
12
+ const id = ref.id ?? '';
13
+ if (!source && !id) return null;
14
+ return `${source}:${id}`;
15
+ }
16
+
17
+ // Hashable key for a criterion (lang-keyed hash). Returns null when
18
+ // no criterion is set OR when the criterion contains no string values.
19
+ // Values are sorted so two criteria with the same strings in different
20
+ // language-key order produce the same key (ISO 12620 coordinate-
21
+ // concept coherence cares about the criterion's content, not the
22
+ // language label order).
23
+ export function criterionKey(criterion) {
24
+ if (!criterion || typeof criterion !== 'object') return null;
25
+ const values = Object.values(criterion)
26
+ .filter(v => typeof v === 'string')
27
+ .map(s => s.trim())
28
+ .filter(s => s.length > 0)
29
+ .sort();
30
+ return values.length > 0 ? values.join('|') : null;
31
+ }