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 +1 -1
- package/src/diff/diff-renderer.js +31 -14
- package/src/migration/partitive-relation-migrator.js +21 -1
- package/src/models/index.js +14 -0
- package/src/models/multiplicity.js +67 -17
- package/src/models/partitive-count.js +20 -0
- package/src/models/partitive-member.js +52 -43
- package/src/models/partitive-presence.js +18 -0
- package/src/models/partitive-relation.js +21 -7
- package/src/rdf/gloss-partitive-member.js +67 -0
- package/src/rdf/gloss-partitive-relation.js +18 -17
- package/src/rdf/index.d.ts +16 -1
- package/src/rdf/index.js +1 -0
- package/src/rdf/predicates.d.ts +4 -0
- package/src/rdf/predicates.js +2 -0
- package/src/validators/binary-has-part-redundancy-rule.js +5 -12
- package/src/validators/delimiting-coherence-rule.js +105 -0
- package/src/validators/index.js +4 -1
- package/src/validators/partitive-relation-coherence-rule.js +3 -21
- package/src/validators/ref-keys.js +31 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
217
|
-
? ` [${_pluralityLabel(r.plurality)}]`
|
|
218
|
-
: '';
|
|
219
|
-
return `${head} → {${partitives}}${completeness}${criterion}${plurality}`;
|
|
219
|
+
return `${head} → {${partitives}}${completeness}${criterion}`;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
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
|
-
|
|
57
|
+
presence: 'required',
|
|
58
|
+
count: 'exactly_one',
|
|
39
59
|
}));
|
|
40
60
|
|
|
41
61
|
out.completeness = _migrateEnumeration(v1Hash.enumeration);
|
package/src/models/index.js
CHANGED
|
@@ -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
|
|
1
|
+
// Multiplicity — ISO 704:2022 partitive multiplicity (derived view).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
//
|
|
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 {
|
|
21
|
+
import { PARTITIVE_PRESENCE } from './partitive-presence.js';
|
|
22
|
+
import { PARTITIVE_COUNT } from './partitive-count.js';
|
|
13
23
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
OPTIONAL:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4
|
+
// ISO 704:2022 presence + count + is_delimiting.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} from './
|
|
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.
|
|
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
|
|
41
|
-
|
|
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.
|
|
59
|
-
|
|
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
|
|
79
|
-
if (value == null) return
|
|
80
|
-
if (!
|
|
81
|
-
throw new Error(
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
93
|
-
'(
|
|
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
|
|
10
|
-
// gloss:criterion "..."@lang (one per language
|
|
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
|
|
13
|
-
//
|
|
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
|
|
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),
|
|
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') {
|
package/src/rdf/index.d.ts
CHANGED
|
@@ -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,
|
package/src/rdf/predicates.d.ts
CHANGED
|
@@ -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"
|
package/src/rdf/predicates.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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
|
+
}
|
package/src/validators/index.js
CHANGED
|
@@ -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 =
|
|
47
|
-
const critKey =
|
|
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
|
+
}
|