glossarist 0.4.19 → 0.4.22
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/data/concept-model/shapes/glossarist.shacl.ttl +151 -1
- package/package.json +1 -1
- package/src/concept-diff.js +13 -0
- package/src/concept-parser.js +52 -2
- package/src/concept-serializer.js +11 -0
- package/src/diff/canonical-json.js +31 -0
- package/src/diff/change.js +28 -0
- package/src/diff/collection-diff.js +57 -25
- package/src/diff/concept-diff.js +100 -60
- package/src/diff/diff-patch.js +49 -48
- package/src/diff/diff-renderer.js +34 -0
- package/src/diff/identity.js +22 -0
- package/src/diff/index.d.ts +64 -1
- package/src/diff/index.js +2 -0
- package/src/diff/similarity.js +24 -0
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/migration/partitive-relation-migrator.js +99 -0
- package/src/models/completeness.js +24 -0
- package/src/models/concept-date.js +8 -0
- package/src/models/concept-ref.js +4 -0
- package/src/models/concept-source.js +21 -2
- package/src/models/concept.js +55 -0
- package/src/models/designation.js +15 -0
- package/src/models/detailed-definition.js +11 -0
- package/src/models/enum.js +42 -0
- package/src/models/hyperedge-colors.js +71 -0
- package/src/models/index.d.ts +61 -0
- package/src/models/index.js +38 -0
- package/src/models/localized-concept.js +29 -0
- package/src/models/multiplicity.js +24 -0
- package/src/models/partitive-enumeration.js +23 -0
- package/src/models/partitive-hyperedge.js +175 -0
- package/src/models/partitive-member.js +96 -0
- package/src/models/partitive-relation.js +149 -0
- package/src/models/plurality-marker.js +24 -0
- package/src/models/related-concept.js +5 -0
- package/src/models/relation-categories.js +6 -0
- package/src/rdf/gloss-concept.js +26 -0
- package/src/rdf/gloss-hyperedge.js +90 -0
- package/src/rdf/gloss-partitive-relation.js +88 -0
- package/src/rdf/gloss-source.js +1 -1
- package/src/rdf/index.d.ts +37 -0
- package/src/rdf/index.js +2 -0
- package/src/rdf/predicates.d.ts +46 -0
- package/src/rdf/predicates.js +23 -0
- package/src/reference-resolver.js +49 -0
- package/src/validators/binary-has-part-redundancy-rule.js +74 -0
- package/src/validators/external-concept-shape-rule.js +60 -0
- package/src/validators/index.js +19 -1
- package/src/validators/partitive-drift-rule.js +51 -0
- package/src/validators/partitive-hyperedge-rule.js +75 -0
- package/src/validators/partitive-relation-coherence-rule.js +98 -0
- package/src/validators/sourced-from-locality-rule.js +53 -0
package/src/rdf/gloss-concept.js
CHANGED
|
@@ -7,6 +7,8 @@ import { PRED } from './predicates.js';
|
|
|
7
7
|
import { WELL_KNOWN } from './prefixes.js';
|
|
8
8
|
import { localizedConceptToQuads } from './gloss-localized-concept.js';
|
|
9
9
|
import { conceptSourceToQuads } from './gloss-source.js';
|
|
10
|
+
import { partitiveRelationToQuads, partitiveRelationSubjectUri } from './gloss-partitive-relation.js';
|
|
11
|
+
import { hyperedgeToQuads, hyperedgeSubjectUri } from './gloss-hyperedge.js';
|
|
10
12
|
import { namedNode, literal, quad } from './terms.js';
|
|
11
13
|
|
|
12
14
|
// Kind tag → URI segment. Each NonVerbalReference subtype has a
|
|
@@ -69,4 +71,28 @@ export function* conceptToQuads(concept, options) {
|
|
|
69
71
|
yield* conceptSourceToQuads(source, { subjectUri, index: srcIndex });
|
|
70
72
|
srcIndex += 1;
|
|
71
73
|
}
|
|
74
|
+
|
|
75
|
+
// v2 canonical: concept.partitiveRelations. v1 alias
|
|
76
|
+
// (concept.partitiveHyperedges) points at the same array — both
|
|
77
|
+
// work here.
|
|
78
|
+
const relations = concept.partitiveRelations ?? concept.partitiveHyperedges ?? [];
|
|
79
|
+
let relIdx = 0;
|
|
80
|
+
for (const relation of relations) {
|
|
81
|
+
// Skip v1 PartitiveHyperedge instances — they have their own
|
|
82
|
+
// emitter (gloss-hyperedge.js) and would double-emit. The parser
|
|
83
|
+
// always produces v2 PartitiveRelation instances; only direct
|
|
84
|
+
// callers using the v1 model would see this branch.
|
|
85
|
+
const isV2 = !relation.enumeration && !relation.markers && !relation.parts
|
|
86
|
+
|| (relation.completeness != null || relation.partitives != null);
|
|
87
|
+
if (!isV2) {
|
|
88
|
+
const heSubject = hyperedgeSubjectUri(subjectUri, relation, relIdx);
|
|
89
|
+
yield quad(s, namedNode(PRED.gloss.hasPartitiveHyperedge), namedNode(heSubject));
|
|
90
|
+
yield* hyperedgeToQuads(relation, { parentUri: subjectUri, index: relIdx });
|
|
91
|
+
} else {
|
|
92
|
+
const relSubject = partitiveRelationSubjectUri(subjectUri, relation, relIdx);
|
|
93
|
+
yield quad(s, namedNode(PRED.gloss.hasPartitiveRelation), namedNode(relSubject));
|
|
94
|
+
yield* partitiveRelationToQuads(relation, { parentUri: subjectUri, index: relIdx });
|
|
95
|
+
}
|
|
96
|
+
relIdx += 1;
|
|
97
|
+
}
|
|
72
98
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// PartitiveHyperedge → RDF quads.
|
|
2
|
+
//
|
|
3
|
+
// Shape (per concept-model ontology, v3.1.0):
|
|
4
|
+
// <base>/hyperedge/<carrying-id>/<comprehensive-id>
|
|
5
|
+
// rdf:type gloss:PartitiveHyperedge
|
|
6
|
+
// gloss:comprehensive <base>/concept/<comprehensive-id>
|
|
7
|
+
// gloss:hasPart <base>/concept/<part-id> (one per part)
|
|
8
|
+
// gloss:enumeration "closed"|"open" (literal)
|
|
9
|
+
// gloss:hasPluralityMarker "double"|"dashed" (literal, one per marker)
|
|
10
|
+
// gloss:hyperedgeContent "..." (literal, omitted if absent)
|
|
11
|
+
//
|
|
12
|
+
// The carrying concept links to this hyperedge via `gloss:hasPartitiveHyperedge`
|
|
13
|
+
// (emitted by gloss-concept.js#conceptToQuads).
|
|
14
|
+
//
|
|
15
|
+
// CROSS-REPO STATUS (validated 2026-07-22):
|
|
16
|
+
// - Subject URI scheme: matches Ruby (`hyperedge/<c>/<comp>`).
|
|
17
|
+
// - `comprehensive`, `enumeration`, `hasPluralityMarker`, `hyperedgeContent`:
|
|
18
|
+
// predicates + object types match Ruby.
|
|
19
|
+
// - `hasPart`: JS follows the concept-model ontology; Ruby emits
|
|
20
|
+
// `gloss:part` instead. This is a Ruby bug — tracked as a cross-repo
|
|
21
|
+
// follow-up. Once Ruby aligns with the ontology, output will be
|
|
22
|
+
// byte-equivalent (modulo serialization order).
|
|
23
|
+
|
|
24
|
+
import { PRED } from './predicates.js';
|
|
25
|
+
import { WELL_KNOWN } from './prefixes.js';
|
|
26
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
27
|
+
|
|
28
|
+
export function* hyperedgeToQuads(hyperedge, { parentUri, index }) {
|
|
29
|
+
const heSubject = hyperedgeSubjectUri(parentUri, hyperedge, index);
|
|
30
|
+
const s = namedNode(heSubject);
|
|
31
|
+
|
|
32
|
+
yield quad(s, namedNode(WELL_KNOWN.rdfType), namedNode(PRED.gloss.PartitiveHyperedge));
|
|
33
|
+
|
|
34
|
+
const comp = hyperedge.comprehensive;
|
|
35
|
+
if (comp && (comp.source || comp.id)) {
|
|
36
|
+
yield quad(s, namedNode(PRED.gloss.comprehensive),
|
|
37
|
+
namedNode(conceptRefUri(comp, parentUri)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const parts = Array.isArray(hyperedge.parts) ? hyperedge.parts : [];
|
|
41
|
+
for (const p of parts) {
|
|
42
|
+
if (p && (p.source || p.id)) {
|
|
43
|
+
yield quad(s, namedNode(PRED.gloss.hasPart),
|
|
44
|
+
namedNode(conceptRefUri(p, parentUri)));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (hyperedge.enumeration) {
|
|
49
|
+
yield quad(s, namedNode(PRED.gloss.enumeration), literal(hyperedge.enumeration));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const marker of Array.isArray(hyperedge.markers) ? hyperedge.markers : []) {
|
|
53
|
+
yield quad(s, namedNode(PRED.gloss.hasPluralityMarker), literal(marker));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (hyperedge.content) {
|
|
57
|
+
const content = hyperedge.content;
|
|
58
|
+
if (typeof content === 'string') {
|
|
59
|
+
yield quad(s, namedNode(PRED.gloss.hyperedgeContent), literal(content));
|
|
60
|
+
} else if (typeof content === 'object') {
|
|
61
|
+
for (const [lang, text] of Object.entries(content)) {
|
|
62
|
+
if (typeof text !== 'string') continue;
|
|
63
|
+
yield quad(s, namedNode(PRED.gloss.hyperedgeContent), literal(text, lang));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Subject URI scheme: `<base>/hyperedge/<carrying-id>/<comprehensive-id>`.
|
|
70
|
+
// Matches Ruby's `subject { |h| "hyperedge/#{h.identifier}/#{h.comprehensive_id}" }`.
|
|
71
|
+
// Falls back to an index suffix if comprehensive has no id, so two
|
|
72
|
+
// hyperedges on the same concept never collide.
|
|
73
|
+
export function hyperedgeSubjectUri(parentUri, hyperedge, index) {
|
|
74
|
+
const base = parentUri.replace(/\/concept\/[^/]+$/, '');
|
|
75
|
+
const carrierId = parentUri.split('/concept/')[1] ?? `carrier-${index}`;
|
|
76
|
+
const compId = hyperedge?.comprehensive?.id ?? `comp-${index}`;
|
|
77
|
+
return `${base}/hyperedge/${carrierId}/${compId}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ConceptRef URI: `<base>/concept/<id>`. Matches Ruby's
|
|
81
|
+
// `concept_to_gloss_transform.rb#hyperedge_concept_uri`. Source is
|
|
82
|
+
// dropped from the URI; it stays on the ConceptRef object if needed.
|
|
83
|
+
function conceptRefUri(ref, parentUri) {
|
|
84
|
+
const id = ref?.id;
|
|
85
|
+
if (id) {
|
|
86
|
+
const base = parentUri.split('/concept/')[0];
|
|
87
|
+
return `${base}/concept/${id}`;
|
|
88
|
+
}
|
|
89
|
+
return `urn:glossarist:${ref?.source ?? 'unknown'}:`;
|
|
90
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// PartitiveRelation → RDF quads.
|
|
2
|
+
//
|
|
3
|
+
// Shape (per concept-model ontology, v3.2.0):
|
|
4
|
+
//
|
|
5
|
+
// <base>/partitive-relation/<carrying-id>/<comprehensive-id>
|
|
6
|
+
// rdf:type gloss:PartitiveRelation
|
|
7
|
+
// gloss:comprehensive <base>/concept/<comprehensive-id> (named node)
|
|
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)
|
|
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).
|
|
15
|
+
//
|
|
16
|
+
// The carrying concept links via `gloss:hasPartitiveRelation`.
|
|
17
|
+
//
|
|
18
|
+
// Replaces gloss-hyperedge.js (the v1 emitter) per TODO.partitive-relation-v2 item 01.
|
|
19
|
+
// Cross-repo alignment with glossarist-ruby's Rdf::GlossPartitiveRelation.
|
|
20
|
+
|
|
21
|
+
import { PRED } from './predicates.js';
|
|
22
|
+
import { WELL_KNOWN } from './prefixes.js';
|
|
23
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
24
|
+
|
|
25
|
+
export function* partitiveRelationToQuads(relation, { parentUri, index }) {
|
|
26
|
+
const subjectUri = partitiveRelationSubjectUri(parentUri, relation, index);
|
|
27
|
+
const s = namedNode(subjectUri);
|
|
28
|
+
|
|
29
|
+
yield quad(s, namedNode(WELL_KNOWN.rdfType), namedNode(PRED.gloss.PartitiveRelation));
|
|
30
|
+
|
|
31
|
+
const comp = relation.comprehensive;
|
|
32
|
+
if (comp && (comp.source || comp.id)) {
|
|
33
|
+
yield quad(s, namedNode(PRED.gloss.comprehensive),
|
|
34
|
+
namedNode(conceptRefUri(comp, parentUri)));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const members = Array.isArray(relation.partitives) ? relation.partitives : [];
|
|
38
|
+
for (const member of members) {
|
|
39
|
+
const ref = member?.ref ?? member;
|
|
40
|
+
if (!ref || (!ref.source && !ref.id)) continue;
|
|
41
|
+
const memberUri = conceptRefUri(ref, parentUri);
|
|
42
|
+
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
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (relation.completeness) {
|
|
56
|
+
yield quad(s, namedNode(PRED.gloss.completeness), literal(relation.completeness));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (relation.criterion && typeof relation.criterion === 'object') {
|
|
60
|
+
for (const [lang, text] of Object.entries(relation.criterion)) {
|
|
61
|
+
if (typeof text !== 'string') continue;
|
|
62
|
+
yield quad(s, namedNode(PRED.gloss.criterion), literal(text, lang));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Subject URI scheme: `<base>/partitive-relation/<carrying-id>/<comprehensive-id>`.
|
|
68
|
+
// Matches the Ruby emitter's scheme. Falls back to an index suffix if
|
|
69
|
+
// the comprehensive has no id, so two relations on the same concept
|
|
70
|
+
// never collide.
|
|
71
|
+
export function partitiveRelationSubjectUri(parentUri, relation, index) {
|
|
72
|
+
const base = parentUri.replace(/\/concept\/[^/]+$/, '');
|
|
73
|
+
const carrierId = parentUri.split('/concept/')[1] ?? `carrier-${index}`;
|
|
74
|
+
const compId = relation?.comprehensive?.id ?? `comp-${index}`;
|
|
75
|
+
return `${base}/partitive-relation/${carrierId}/${compId}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ConceptRef URI resolution: `<base>/concept/<id>` for any ref with an
|
|
79
|
+
// id. Source is dropped (matches Ruby). Falls back to a stable urn:/
|
|
80
|
+
// form for refs with only a source (rare).
|
|
81
|
+
function conceptRefUri(ref, parentUri) {
|
|
82
|
+
const id = ref?.id;
|
|
83
|
+
if (id) {
|
|
84
|
+
const base = parentUri.split('/concept/')[0];
|
|
85
|
+
return `${base}/concept/${id}`;
|
|
86
|
+
}
|
|
87
|
+
return `urn:glossarist:${ref?.source ?? 'unknown'}:`;
|
|
88
|
+
}
|
package/src/rdf/gloss-source.js
CHANGED
|
@@ -30,7 +30,7 @@ export function* conceptSourceToQuads(source, { subjectUri, index }) {
|
|
|
30
30
|
yield* citationToQuads(source.origin, { subjectUri: srcSubject, predicate: PRED.gloss.sourceOrigin, bnodeKey: 'origin' });
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const sourcedFrom = source.sourced_from ?? [];
|
|
33
|
+
const sourcedFrom = source.sourcedFrom ?? source.sourced_from ?? [];
|
|
34
34
|
for (let i = 0; i < sourcedFrom.length; i++) {
|
|
35
35
|
yield* citationToQuads(sourcedFrom[i], { subjectUri: srcSubject, predicate: PRED.gloss.sourcedFrom, bnodeKey: 'sourced_from', index: i });
|
|
36
36
|
}
|
package/src/rdf/index.d.ts
CHANGED
|
@@ -11,6 +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
15
|
|
|
15
16
|
export { PRED, PREFIXES } from './predicates';
|
|
16
17
|
export type PredicateMap = typeof PRED;
|
|
@@ -77,6 +78,42 @@ export interface ConceptSourceEmitOptions {
|
|
|
77
78
|
|
|
78
79
|
export declare function conceptSourceToQuads(source: ConceptSource, options: ConceptSourceEmitOptions): Generator<Quad, void, unknown>;
|
|
79
80
|
|
|
81
|
+
// ── Hyperedge emitter ─────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
export interface HyperedgeEmitOptions {
|
|
84
|
+
parentUri: string;
|
|
85
|
+
index: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export declare function hyperedgeToQuads(
|
|
89
|
+
hyperedge: PartitiveHyperedge,
|
|
90
|
+
options: HyperedgeEmitOptions,
|
|
91
|
+
): Generator<Quad, void, unknown>;
|
|
92
|
+
|
|
93
|
+
export declare function hyperedgeSubjectUri(
|
|
94
|
+
parentUri: string,
|
|
95
|
+
hyperedge: PartitiveHyperedge,
|
|
96
|
+
index: number,
|
|
97
|
+
): string;
|
|
98
|
+
|
|
99
|
+
// ── PartitiveRelation emitter (v2) ────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export interface PartitiveRelationEmitOptions {
|
|
102
|
+
parentUri: string;
|
|
103
|
+
index: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export declare function partitiveRelationToQuads(
|
|
107
|
+
relation: PartitiveRelation,
|
|
108
|
+
options: PartitiveRelationEmitOptions,
|
|
109
|
+
): Generator<Quad, void, unknown>;
|
|
110
|
+
|
|
111
|
+
export declare function partitiveRelationSubjectUri(
|
|
112
|
+
parentUri: string,
|
|
113
|
+
relation: PartitiveRelation,
|
|
114
|
+
index: number,
|
|
115
|
+
): string;
|
|
116
|
+
|
|
80
117
|
// ── Non-verbal representation emitters ─────────────────────────────────
|
|
81
118
|
|
|
82
119
|
export interface NonVerbalRepEmitOptions {
|
package/src/rdf/index.js
CHANGED
|
@@ -26,6 +26,8 @@ export {
|
|
|
26
26
|
} from './gloss-designation.js';
|
|
27
27
|
export { detailedDefinitionToQuads } from './gloss-detailed-definition.js';
|
|
28
28
|
export { conceptSourceToQuads } from './gloss-source.js';
|
|
29
|
+
export { hyperedgeToQuads, hyperedgeSubjectUri } from './gloss-hyperedge.js';
|
|
30
|
+
export { partitiveRelationToQuads, partitiveRelationSubjectUri } from './gloss-partitive-relation.js';
|
|
29
31
|
export {
|
|
30
32
|
nonVerbalRepToQuads,
|
|
31
33
|
nonVerbalEntityToQuads,
|
package/src/rdf/predicates.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ export declare const PRED = {
|
|
|
23
23
|
"ConceptRef": "https://www.glossarist.org/ontologies/ConceptRef",
|
|
24
24
|
"Reference": "https://www.glossarist.org/ontologies/Reference",
|
|
25
25
|
"RelatedConcept": "https://www.glossarist.org/ontologies/RelatedConcept",
|
|
26
|
+
"PartitiveHyperedge": "https://www.glossarist.org/ontologies/PartitiveHyperedge",
|
|
27
|
+
"PartitiveEnumeration": "https://www.glossarist.org/ontologies/PartitiveEnumeration",
|
|
28
|
+
"PluralityMarker": "https://www.glossarist.org/ontologies/PluralityMarker",
|
|
26
29
|
"DesignationRelationship": "https://www.glossarist.org/ontologies/DesignationRelationship",
|
|
27
30
|
"ConceptDate": "https://www.glossarist.org/ontologies/ConceptDate",
|
|
28
31
|
"NonVerbalRepresentation": "https://www.glossarist.org/ontologies/NonVerbalRepresentation",
|
|
@@ -42,6 +45,7 @@ export declare const PRED = {
|
|
|
42
45
|
"isLocalizationOf": "https://www.glossarist.org/ontologies/isLocalizationOf",
|
|
43
46
|
"hasStatus": "https://www.glossarist.org/ontologies/hasStatus",
|
|
44
47
|
"hasDomain": "https://www.glossarist.org/ontologies/hasDomain",
|
|
48
|
+
"tag": "https://www.glossarist.org/ontologies/tag",
|
|
45
49
|
"hasRelatedConcept": "https://www.glossarist.org/ontologies/hasRelatedConcept",
|
|
46
50
|
"hasDate": "https://www.glossarist.org/ontologies/hasDate",
|
|
47
51
|
"hasSource": "https://www.glossarist.org/ontologies/hasSource",
|
|
@@ -93,7 +97,26 @@ export declare const PRED = {
|
|
|
93
97
|
"relationshipType": "https://www.glossarist.org/ontologies/relationshipType",
|
|
94
98
|
"relationshipContent": "https://www.glossarist.org/ontologies/relationshipContent",
|
|
95
99
|
"relationshipRef": "https://www.glossarist.org/ontologies/relationshipRef",
|
|
100
|
+
"relatedConceptBroader": "https://www.glossarist.org/ontologies/relatedConceptBroader",
|
|
101
|
+
"relatedConceptNarrower": "https://www.glossarist.org/ontologies/relatedConceptNarrower",
|
|
102
|
+
"hasPartitiveHyperedge": "https://www.glossarist.org/ontologies/hasPartitiveHyperedge",
|
|
103
|
+
"comprehensive": "https://www.glossarist.org/ontologies/comprehensive",
|
|
104
|
+
"hasPart": "https://www.glossarist.org/ontologies/hasPart",
|
|
105
|
+
"enumeration": "https://www.glossarist.org/ontologies/enumeration",
|
|
106
|
+
"hasPluralityMarker": "https://www.glossarist.org/ontologies/hasPluralityMarker",
|
|
107
|
+
"hyperedgeContent": "https://www.glossarist.org/ontologies/hyperedgeContent",
|
|
108
|
+
"PartitiveRelation": "https://www.glossarist.org/ontologies/PartitiveRelation",
|
|
109
|
+
"PartitiveMember": "https://www.glossarist.org/ontologies/PartitiveMember",
|
|
110
|
+
"hasPartitiveRelation": "https://www.glossarist.org/ontologies/hasPartitiveRelation",
|
|
111
|
+
"hasPartitive": "https://www.glossarist.org/ontologies/hasPartitive",
|
|
112
|
+
"completeness": "https://www.glossarist.org/ontologies/completeness",
|
|
113
|
+
"multiplicity": "https://www.glossarist.org/ontologies/multiplicity",
|
|
114
|
+
"isDelimiting": "https://www.glossarist.org/ontologies/isDelimiting",
|
|
115
|
+
"criterion": "https://www.glossarist.org/ontologies/criterion",
|
|
116
|
+
"provides": "https://www.glossarist.org/ontologies/provides",
|
|
117
|
+
"providedBy": "https://www.glossarist.org/ontologies/providedBy",
|
|
96
118
|
"refType": "https://www.glossarist.org/ontologies/refType",
|
|
119
|
+
"source": "https://www.glossarist.org/ontologies/source",
|
|
97
120
|
"urn": "https://www.glossarist.org/ontologies/urn",
|
|
98
121
|
"term": "https://www.glossarist.org/ontologies/term",
|
|
99
122
|
"refId": "https://www.glossarist.org/ontologies/refId",
|
|
@@ -282,6 +305,9 @@ export type Predicate =
|
|
|
282
305
|
| "https://www.glossarist.org/ontologies/ConceptRef"
|
|
283
306
|
| "https://www.glossarist.org/ontologies/Reference"
|
|
284
307
|
| "https://www.glossarist.org/ontologies/RelatedConcept"
|
|
308
|
+
| "https://www.glossarist.org/ontologies/PartitiveHyperedge"
|
|
309
|
+
| "https://www.glossarist.org/ontologies/PartitiveEnumeration"
|
|
310
|
+
| "https://www.glossarist.org/ontologies/PluralityMarker"
|
|
285
311
|
| "https://www.glossarist.org/ontologies/DesignationRelationship"
|
|
286
312
|
| "https://www.glossarist.org/ontologies/ConceptDate"
|
|
287
313
|
| "https://www.glossarist.org/ontologies/NonVerbalRepresentation"
|
|
@@ -301,6 +327,7 @@ export type Predicate =
|
|
|
301
327
|
| "https://www.glossarist.org/ontologies/isLocalizationOf"
|
|
302
328
|
| "https://www.glossarist.org/ontologies/hasStatus"
|
|
303
329
|
| "https://www.glossarist.org/ontologies/hasDomain"
|
|
330
|
+
| "https://www.glossarist.org/ontologies/tag"
|
|
304
331
|
| "https://www.glossarist.org/ontologies/hasRelatedConcept"
|
|
305
332
|
| "https://www.glossarist.org/ontologies/hasDate"
|
|
306
333
|
| "https://www.glossarist.org/ontologies/hasSource"
|
|
@@ -352,7 +379,26 @@ export type Predicate =
|
|
|
352
379
|
| "https://www.glossarist.org/ontologies/relationshipType"
|
|
353
380
|
| "https://www.glossarist.org/ontologies/relationshipContent"
|
|
354
381
|
| "https://www.glossarist.org/ontologies/relationshipRef"
|
|
382
|
+
| "https://www.glossarist.org/ontologies/relatedConceptBroader"
|
|
383
|
+
| "https://www.glossarist.org/ontologies/relatedConceptNarrower"
|
|
384
|
+
| "https://www.glossarist.org/ontologies/hasPartitiveHyperedge"
|
|
385
|
+
| "https://www.glossarist.org/ontologies/comprehensive"
|
|
386
|
+
| "https://www.glossarist.org/ontologies/hasPart"
|
|
387
|
+
| "https://www.glossarist.org/ontologies/enumeration"
|
|
388
|
+
| "https://www.glossarist.org/ontologies/hasPluralityMarker"
|
|
389
|
+
| "https://www.glossarist.org/ontologies/hyperedgeContent"
|
|
390
|
+
| "https://www.glossarist.org/ontologies/PartitiveRelation"
|
|
391
|
+
| "https://www.glossarist.org/ontologies/PartitiveMember"
|
|
392
|
+
| "https://www.glossarist.org/ontologies/hasPartitiveRelation"
|
|
393
|
+
| "https://www.glossarist.org/ontologies/hasPartitive"
|
|
394
|
+
| "https://www.glossarist.org/ontologies/completeness"
|
|
395
|
+
| "https://www.glossarist.org/ontologies/multiplicity"
|
|
396
|
+
| "https://www.glossarist.org/ontologies/isDelimiting"
|
|
397
|
+
| "https://www.glossarist.org/ontologies/criterion"
|
|
398
|
+
| "https://www.glossarist.org/ontologies/provides"
|
|
399
|
+
| "https://www.glossarist.org/ontologies/providedBy"
|
|
355
400
|
| "https://www.glossarist.org/ontologies/refType"
|
|
401
|
+
| "https://www.glossarist.org/ontologies/source"
|
|
356
402
|
| "https://www.glossarist.org/ontologies/urn"
|
|
357
403
|
| "https://www.glossarist.org/ontologies/term"
|
|
358
404
|
| "https://www.glossarist.org/ontologies/refId"
|
package/src/rdf/predicates.js
CHANGED
|
@@ -23,6 +23,9 @@ export const PRED = {
|
|
|
23
23
|
"ConceptRef": "https://www.glossarist.org/ontologies/ConceptRef",
|
|
24
24
|
"Reference": "https://www.glossarist.org/ontologies/Reference",
|
|
25
25
|
"RelatedConcept": "https://www.glossarist.org/ontologies/RelatedConcept",
|
|
26
|
+
"PartitiveHyperedge": "https://www.glossarist.org/ontologies/PartitiveHyperedge",
|
|
27
|
+
"PartitiveEnumeration": "https://www.glossarist.org/ontologies/PartitiveEnumeration",
|
|
28
|
+
"PluralityMarker": "https://www.glossarist.org/ontologies/PluralityMarker",
|
|
26
29
|
"DesignationRelationship": "https://www.glossarist.org/ontologies/DesignationRelationship",
|
|
27
30
|
"ConceptDate": "https://www.glossarist.org/ontologies/ConceptDate",
|
|
28
31
|
"NonVerbalRepresentation": "https://www.glossarist.org/ontologies/NonVerbalRepresentation",
|
|
@@ -42,6 +45,7 @@ export const PRED = {
|
|
|
42
45
|
"isLocalizationOf": "https://www.glossarist.org/ontologies/isLocalizationOf",
|
|
43
46
|
"hasStatus": "https://www.glossarist.org/ontologies/hasStatus",
|
|
44
47
|
"hasDomain": "https://www.glossarist.org/ontologies/hasDomain",
|
|
48
|
+
"tag": "https://www.glossarist.org/ontologies/tag",
|
|
45
49
|
"hasRelatedConcept": "https://www.glossarist.org/ontologies/hasRelatedConcept",
|
|
46
50
|
"hasDate": "https://www.glossarist.org/ontologies/hasDate",
|
|
47
51
|
"hasSource": "https://www.glossarist.org/ontologies/hasSource",
|
|
@@ -93,7 +97,26 @@ export const PRED = {
|
|
|
93
97
|
"relationshipType": "https://www.glossarist.org/ontologies/relationshipType",
|
|
94
98
|
"relationshipContent": "https://www.glossarist.org/ontologies/relationshipContent",
|
|
95
99
|
"relationshipRef": "https://www.glossarist.org/ontologies/relationshipRef",
|
|
100
|
+
"relatedConceptBroader": "https://www.glossarist.org/ontologies/relatedConceptBroader",
|
|
101
|
+
"relatedConceptNarrower": "https://www.glossarist.org/ontologies/relatedConceptNarrower",
|
|
102
|
+
"hasPartitiveHyperedge": "https://www.glossarist.org/ontologies/hasPartitiveHyperedge",
|
|
103
|
+
"comprehensive": "https://www.glossarist.org/ontologies/comprehensive",
|
|
104
|
+
"hasPart": "https://www.glossarist.org/ontologies/hasPart",
|
|
105
|
+
"enumeration": "https://www.glossarist.org/ontologies/enumeration",
|
|
106
|
+
"hasPluralityMarker": "https://www.glossarist.org/ontologies/hasPluralityMarker",
|
|
107
|
+
"hyperedgeContent": "https://www.glossarist.org/ontologies/hyperedgeContent",
|
|
108
|
+
"PartitiveRelation": "https://www.glossarist.org/ontologies/PartitiveRelation",
|
|
109
|
+
"PartitiveMember": "https://www.glossarist.org/ontologies/PartitiveMember",
|
|
110
|
+
"hasPartitiveRelation": "https://www.glossarist.org/ontologies/hasPartitiveRelation",
|
|
111
|
+
"hasPartitive": "https://www.glossarist.org/ontologies/hasPartitive",
|
|
112
|
+
"completeness": "https://www.glossarist.org/ontologies/completeness",
|
|
113
|
+
"multiplicity": "https://www.glossarist.org/ontologies/multiplicity",
|
|
114
|
+
"isDelimiting": "https://www.glossarist.org/ontologies/isDelimiting",
|
|
115
|
+
"criterion": "https://www.glossarist.org/ontologies/criterion",
|
|
116
|
+
"provides": "https://www.glossarist.org/ontologies/provides",
|
|
117
|
+
"providedBy": "https://www.glossarist.org/ontologies/providedBy",
|
|
96
118
|
"refType": "https://www.glossarist.org/ontologies/refType",
|
|
119
|
+
"source": "https://www.glossarist.org/ontologies/source",
|
|
97
120
|
"urn": "https://www.glossarist.org/ontologies/urn",
|
|
98
121
|
"term": "https://www.glossarist.org/ontologies/term",
|
|
99
122
|
"refId": "https://www.glossarist.org/ontologies/refId",
|
|
@@ -38,6 +38,17 @@ function refTarget(rc) {
|
|
|
38
38
|
return '';
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Extract a concept-ref target from a hyperedge comprehensive/part
|
|
42
|
+
// ConceptRef. Returns the id (preferred) or source. Returns '' if
|
|
43
|
+
// the ref is empty (the constructor already rejects empty refs; this
|
|
44
|
+
// is a defensive guard for data that bypassed the constructor).
|
|
45
|
+
function hyperedgeRefTarget(ref) {
|
|
46
|
+
if (!ref) return '';
|
|
47
|
+
if (ref.id) return ref.id;
|
|
48
|
+
if (ref.source) return ref.source;
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
export function resolveBibliographyRecord(citationRef, registry) {
|
|
42
53
|
if (!citationRef?.source || !citationRef?.id) return null;
|
|
43
54
|
const bioColl = registry[`bibliography:${citationRef.source}`]?.concepts;
|
|
@@ -73,6 +84,44 @@ export class ReferenceResolver {
|
|
|
73
84
|
}
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
// v2 model: concept.partitiveRelations is canonical; v1 alias
|
|
88
|
+
// concept.partitiveHyperedges points at the same array. Each
|
|
89
|
+
// relation has a comprehensive ConceptRef and 2+ partitive members
|
|
90
|
+
// each carrying a ConceptRef.
|
|
91
|
+
const relations = concept.partitiveRelations ?? concept.partitiveHyperedges ?? [];
|
|
92
|
+
for (let i = 0; i < relations.length; i++) {
|
|
93
|
+
const rel = relations[i];
|
|
94
|
+
const relPath = `partitiveRelations[${i}]`;
|
|
95
|
+
|
|
96
|
+
const compTarget = hyperedgeRefTarget(rel.comprehensive);
|
|
97
|
+
if (compTarget) {
|
|
98
|
+
refs.push(new Reference(
|
|
99
|
+
'concept',
|
|
100
|
+
compTarget,
|
|
101
|
+
'partitive_relation',
|
|
102
|
+
`${relPath}.comprehensive`,
|
|
103
|
+
{ lookupKey: { id: compTarget } },
|
|
104
|
+
));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// v2 shape: rel.partitives is [{ ref, certainty }, ...].
|
|
108
|
+
// v1 shape: rel.parts is [ref, ...]. Accept both.
|
|
109
|
+
const members = rel.partitives ?? rel.parts ?? [];
|
|
110
|
+
for (let j = 0; j < members.length; j++) {
|
|
111
|
+
const m = members[j];
|
|
112
|
+
const memberRef = m?.ref ?? m;
|
|
113
|
+
const pTarget = hyperedgeRefTarget(memberRef);
|
|
114
|
+
if (!pTarget) continue;
|
|
115
|
+
refs.push(new Reference(
|
|
116
|
+
'concept',
|
|
117
|
+
pTarget,
|
|
118
|
+
'partitive_relation',
|
|
119
|
+
`${relPath}.partitives[${j}]`,
|
|
120
|
+
{ lookupKey: { id: pTarget } },
|
|
121
|
+
));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
76
125
|
for (const ref of concept.figures) {
|
|
77
126
|
refs.push(new Reference('figure', ref.display ?? ref.entityId, 'structural', 'figures', {
|
|
78
127
|
lookupKey: { entityType: 'figure', entityId: ref.entityId },
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Warns when binary `has_part` / `is_part_of` edges duplicate
|
|
2
|
+
// PartitiveRelation members, or when a concept has many binary
|
|
3
|
+
// has_part edges that should be consolidated into a PartitiveRelation.
|
|
4
|
+
//
|
|
5
|
+
// Per TODO.partitive-relation-v2 item 14, binary edges and
|
|
6
|
+
// PartitiveRelations coexist by design. The asymmetry is pragmatic:
|
|
7
|
+
// binary edges are compact for single pairwise facts; relations
|
|
8
|
+
// carry completeness/plurality/criterion metadata. The redundancy
|
|
9
|
+
// case (same fact encoded both ways) is harmless data-wise but
|
|
10
|
+
// wasteful and should be flagged for migration.
|
|
11
|
+
//
|
|
12
|
+
// Severity: warning. Never blocks save.
|
|
13
|
+
|
|
14
|
+
import { ValidationRule } from './validation-rule.js';
|
|
15
|
+
|
|
16
|
+
const BINARY_HAS_PART_TYPES = new Set(['has_part', 'is_part_of']);
|
|
17
|
+
|
|
18
|
+
export class BinaryHasPartRedundancyRule extends ValidationRule {
|
|
19
|
+
constructor() { super('binary-has-part-redundancy', 'warning'); }
|
|
20
|
+
|
|
21
|
+
validate(concept, path, result) {
|
|
22
|
+
const relations = concept.partitiveRelations ?? concept.partitiveHyperedges ?? [];
|
|
23
|
+
|
|
24
|
+
// Collect partitive refs from PartitiveRelations.
|
|
25
|
+
const relationTargets = new Set();
|
|
26
|
+
for (const rel of relations) {
|
|
27
|
+
const compKey = _refKey(rel?.comprehensive);
|
|
28
|
+
const members = rel?.partitives ?? rel?.parts ?? [];
|
|
29
|
+
for (const member of members) {
|
|
30
|
+
const ref = member?.ref ?? member;
|
|
31
|
+
const key = _refKey(ref);
|
|
32
|
+
if (key && key !== compKey) relationTargets.add(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Collect binary has_part refs.
|
|
37
|
+
const binaryTargets = new Set();
|
|
38
|
+
const related = concept.relatedConcepts ?? [];
|
|
39
|
+
for (const rc of related) {
|
|
40
|
+
if (!BINARY_HAS_PART_TYPES.has(rc.type)) continue;
|
|
41
|
+
const key = _refKey(rc?.ref);
|
|
42
|
+
if (key) binaryTargets.add(key);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Redundancy: refs in both.
|
|
46
|
+
for (const target of relationTargets) {
|
|
47
|
+
if (binaryTargets.has(target)) {
|
|
48
|
+
this.addIssue(result,
|
|
49
|
+
`${path}relatedConcepts`,
|
|
50
|
+
`binary has_part edge for (${target}) is redundant — already in a ` +
|
|
51
|
+
`PartitiveRelation member; pick one encoding to avoid divergence`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Cluster: 3+ binary has_part edges suggests PartitiveRelation territory.
|
|
57
|
+
if (binaryTargets.size >= 3) {
|
|
58
|
+
this.addIssue(result,
|
|
59
|
+
`${path}relatedConcepts`,
|
|
60
|
+
`${binaryTargets.size} binary has_part edges on this concept; ` +
|
|
61
|
+
`consider converting to a PartitiveRelation (which can carry ` +
|
|
62
|
+
`completeness, plurality, and criterion metadata)`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
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,60 @@
|
|
|
1
|
+
// Validates ExternalConcept shape (status: external).
|
|
2
|
+
//
|
|
3
|
+
// Per TODO.partitive-relation-v2 item 06, an ExternalConcept is a
|
|
4
|
+
// ManagedConcept with status: external. External concepts:
|
|
5
|
+
// - MUST have at least one designation (the name by which they're referenced)
|
|
6
|
+
// - MUST NOT have a definition (contradicts status: external)
|
|
7
|
+
// - MUST NOT have sources (we don't define them)
|
|
8
|
+
//
|
|
9
|
+
// Severity: error. An external concept with a definition or sources
|
|
10
|
+
// contradicts the semantic role of "external" and should be re-typed
|
|
11
|
+
// to a real concept status (valid, draft, etc.).
|
|
12
|
+
|
|
13
|
+
import { ValidationRule } from './validation-rule.js';
|
|
14
|
+
|
|
15
|
+
export class ExternalConceptShapeRule extends ValidationRule {
|
|
16
|
+
constructor() { super('external-concept-shape', 'error'); }
|
|
17
|
+
|
|
18
|
+
validate(concept, path, result) {
|
|
19
|
+
if (concept.status !== 'external') return;
|
|
20
|
+
|
|
21
|
+
// Every concept — even an external one — needs at least one
|
|
22
|
+
// designation (the name by which it is referenced).
|
|
23
|
+
let designationCount = 0;
|
|
24
|
+
for (const lang of concept.languages ?? []) {
|
|
25
|
+
const lc = concept.localization?.(lang);
|
|
26
|
+
if (!lc) continue;
|
|
27
|
+
designationCount += (lc.terms ?? []).length;
|
|
28
|
+
}
|
|
29
|
+
if (designationCount === 0) {
|
|
30
|
+
this.addIssue(result,
|
|
31
|
+
`${path}localizations.*.terms`,
|
|
32
|
+
'ExternalConcept has no designations; every concept (including external) ' +
|
|
33
|
+
'needs at least one designation (the name by which it is referenced)',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// External concepts must NOT have a definition or sources.
|
|
38
|
+
for (const lang of concept.languages ?? []) {
|
|
39
|
+
const lc = concept.localization?.(lang);
|
|
40
|
+
if (!lc) continue;
|
|
41
|
+
|
|
42
|
+
if ((lc.definitions ?? []).length > 0) {
|
|
43
|
+
this.addIssue(result,
|
|
44
|
+
`${path}localizations.${lang}.definitions`,
|
|
45
|
+
`ExternalConcept has a definition in language '${lang}'; ` +
|
|
46
|
+
`external concepts are referenced but not defined here ` +
|
|
47
|
+
`(move the definition to the providing dataset and add a provided_by edge)`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ((lc.sources ?? []).length > 0) {
|
|
52
|
+
this.addIssue(result,
|
|
53
|
+
`${path}localizations.${lang}.sources`,
|
|
54
|
+
`ExternalConcept has sources in language '${lang}'; ` +
|
|
55
|
+
`external concepts should not carry sources (sources belong on the providing concept)`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|