glossarist 0.4.23 → 0.4.25
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 +20 -14
- package/src/migration/partitive-relation-migrator.js +19 -0
- package/src/models/partitive-member.js +36 -6
- package/src/models/partitive-relation.js +21 -7
- package/src/rdf/gloss-partitive-member.js +39 -0
- package/src/rdf/gloss-partitive-relation.js +18 -17
- package/src/rdf/index.d.ts +14 -1
- package/src/rdf/index.js +1 -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,25 @@ 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
|
+
export function multiplicityStats(relations) {
|
|
226
|
+
const byMultiplicity = {};
|
|
227
|
+
let delimitingCount = 0;
|
|
228
|
+
let total = 0;
|
|
229
|
+
for (const rel of relations ?? []) {
|
|
230
|
+
for (const m of rel?.partitives ?? []) {
|
|
231
|
+
total += 1;
|
|
232
|
+
const k = m?.multiplicity ?? 'compulsory';
|
|
233
|
+
byMultiplicity[k] = (byMultiplicity[k] ?? 0) + 1;
|
|
234
|
+
if (m?.is_delimiting === true) delimitingCount += 1;
|
|
235
|
+
}
|
|
230
236
|
}
|
|
231
|
-
return
|
|
237
|
+
return { total, byMultiplicity, delimiting: delimitingCount };
|
|
232
238
|
}
|
|
233
239
|
|
|
234
240
|
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;
|
|
@@ -6,15 +6,19 @@
|
|
|
6
6
|
// ISO 704:2022 partitive member notation:
|
|
7
7
|
//
|
|
8
8
|
// multiplicity (diagram line notation):
|
|
9
|
-
// compulsory
|
|
10
|
-
// optional
|
|
11
|
-
// compulsory_multiple
|
|
12
|
-
// optional_multiple
|
|
13
|
-
// compulsory_at_least_one
|
|
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 (≥1 must exist)
|
|
14
14
|
//
|
|
15
15
|
// is_delimiting (orthogonal, bold 3x-width line in diagram):
|
|
16
16
|
// A delimiting part behaves like a delimiting characteristic:
|
|
17
17
|
// it distinguishes the comprehensive from coordinate concepts.
|
|
18
|
+
//
|
|
19
|
+
// Whether a part is delimiting depends on the concept system, the
|
|
20
|
+
// coordinate concepts, the inheritance principle, and the criterion
|
|
21
|
+
// of subdivision used (ISO 704:2022 §5.5.4.2.1).
|
|
18
22
|
|
|
19
23
|
import { GlossaristModel } from './base.js';
|
|
20
24
|
import { ConceptRef } from './concept-ref.js';
|
|
@@ -32,11 +36,14 @@ export class PartitiveMember extends GlossaristModel {
|
|
|
32
36
|
? data.ref
|
|
33
37
|
: new ConceptRef(data.ref ?? {});
|
|
34
38
|
this.multiplicity = _resolveMultiplicity(data.multiplicity);
|
|
35
|
-
this.is_delimiting = data.is_delimiting
|
|
39
|
+
this.is_delimiting = _resolveBoolean(data.is_delimiting, 'is_delimiting');
|
|
36
40
|
|
|
37
41
|
_assertNonEmptyRef(this.ref);
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
// Per-value multiplicity predicates. Each new enum value gets one
|
|
45
|
+
// line — OCP-friendly: the predicates grow with the enum without
|
|
46
|
+
// touching unrelated code. (TODO.partitive-relation-v3/05.)
|
|
40
47
|
get isCompulsory() {
|
|
41
48
|
return this.multiplicity === MULTIPLICITY.COMPULSORY;
|
|
42
49
|
}
|
|
@@ -45,6 +52,18 @@ export class PartitiveMember extends GlossaristModel {
|
|
|
45
52
|
return this.multiplicity === MULTIPLICITY.OPTIONAL;
|
|
46
53
|
}
|
|
47
54
|
|
|
55
|
+
get isCompulsoryMultiple() {
|
|
56
|
+
return this.multiplicity === MULTIPLICITY.COMPULSORY_MULTIPLE;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get isOptionalMultiple() {
|
|
60
|
+
return this.multiplicity === MULTIPLICITY.OPTIONAL_MULTIPLE;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get isCompulsoryAtLeastOne() {
|
|
64
|
+
return this.multiplicity === MULTIPLICITY.COMPULSORY_AT_LEAST_ONE;
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
get isDelimiting() {
|
|
49
68
|
return this.is_delimiting === true;
|
|
50
69
|
}
|
|
@@ -86,6 +105,17 @@ function _resolveMultiplicity(value) {
|
|
|
86
105
|
return value;
|
|
87
106
|
}
|
|
88
107
|
|
|
108
|
+
// Boolean coercion with strict type check. Rejects strings, numbers,
|
|
109
|
+
// objects (silent coercion was the v3 bug TODO.partitive-relation-v3/02).
|
|
110
|
+
function _resolveBoolean(value, fieldName) {
|
|
111
|
+
if (value == null) return false;
|
|
112
|
+
if (typeof value === 'boolean') return value;
|
|
113
|
+
throw new Error(
|
|
114
|
+
`PartitiveMember.${fieldName} must be a boolean; got ${typeof value} ` +
|
|
115
|
+
`${JSON.stringify(value)}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
function _assertNonEmptyRef(ref) {
|
|
90
120
|
if (!ref.source && !ref.id && !ref.text) {
|
|
91
121
|
throw new Error(
|
|
@@ -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,39 @@
|
|
|
1
|
+
// PartitiveMember → RDF quads.
|
|
2
|
+
//
|
|
3
|
+
// Emits per-member predicates (multiplicity, is_delimiting) onto the
|
|
4
|
+
// member's concept URI. The member URI is constructed by the caller
|
|
5
|
+
// (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
|
+
import { PRED } from './predicates.js';
|
|
13
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
14
|
+
|
|
15
|
+
// SKOS taxonomy namespace for multiplicity values. concept-model will
|
|
16
|
+
// ship the corresponding taxonomy; until then, the URIs are forward-
|
|
17
|
+
// compatible and resolve once the taxonomy lands.
|
|
18
|
+
//
|
|
19
|
+
// Per the JSON-LD context (`"@type": "@id"`), multiplicity objects
|
|
20
|
+
// MUST be URI references, not string literals. Emitted shape:
|
|
21
|
+
// <member-uri> gloss:multiplicity <.../multiplicity/compulsory>
|
|
22
|
+
export const MULTIPLICITY_NS = 'https://www.glossarist.org/ontologies/multiplicity/';
|
|
23
|
+
|
|
24
|
+
export function* partitiveMemberToQuads(member, { memberUri }) {
|
|
25
|
+
if (member?.multiplicity) {
|
|
26
|
+
yield quad(
|
|
27
|
+
namedNode(memberUri),
|
|
28
|
+
namedNode(PRED.gloss.multiplicity),
|
|
29
|
+
namedNode(MULTIPLICITY_NS + member.multiplicity),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (member?.is_delimiting === true) {
|
|
33
|
+
yield quad(
|
|
34
|
+
namedNode(memberUri),
|
|
35
|
+
namedNode(PRED.gloss.isDelimiting),
|
|
36
|
+
literal('true', 'xsd:boolean'),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -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,19 @@ 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
|
+
|
|
125
|
+
export declare function partitiveMemberToQuads(
|
|
126
|
+
member: PartitiveMember,
|
|
127
|
+
options: PartitiveMemberEmitOptions,
|
|
128
|
+
): Generator<Quad, void, unknown>;
|
|
129
|
+
|
|
117
130
|
// ── Non-verbal representation emitters ─────────────────────────────────
|
|
118
131
|
|
|
119
132
|
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 } from './gloss-partitive-member.js';
|
|
30
31
|
export { partitiveRelationToQuads, partitiveRelationSubjectUri } from './gloss-partitive-relation.js';
|
|
31
32
|
export {
|
|
32
33
|
nonVerbalRepToQuads,
|
|
@@ -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
|
+
}
|