glossarist 0.4.31 → 0.4.33
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/concept-parser.js +95 -27
- package/src/concept-reader.js +59 -3
- package/src/concept-serializer.js +21 -10
- package/src/diff/concept-diff.js +70 -18
- package/src/diff/diff-patch.js +37 -13
- package/src/diff/diff-renderer.js +16 -4
- package/src/index.js +2 -1
- package/src/migration/partitive-relation-migrator.js +3 -3
- package/src/models/{abstract-nary-relation.js → abstract-hyperedge.js} +4 -4
- package/src/models/completeness.js +1 -1
- package/src/models/concept.js +172 -30
- package/src/models/{generic-relation.js → generic-hyperedge.js} +18 -6
- package/src/models/generic-member.js +5 -5
- package/src/models/hyperedge-index.js +87 -0
- package/src/models/{concept-system-member.js → hyperedge-member.js} +12 -24
- package/src/models/hyperedge-registry.js +115 -0
- package/src/models/index.d.ts +16 -16
- package/src/models/index.js +13 -10
- package/src/models/multiplicity.js +11 -6
- package/src/models/partitive-hyperedge.js +69 -153
- package/src/models/partitive-member.js +6 -6
- package/src/models/relation-loader.js +27 -23
- package/src/rdf/gloss-concept.js +36 -20
- package/src/rdf/gloss-generic-hyperedge.js +92 -0
- package/src/rdf/gloss-partitive-relation.js +3 -3
- package/src/rdf/index.d.ts +4 -4
- package/src/rdf/index.js +0 -1
- package/src/reference-resolver.js +14 -12
- package/src/validators/binary-has-part-redundancy-rule.js +6 -6
- package/src/validators/delimiting-coherence-rule.js +2 -2
- package/src/validators/hyperedge-coherence-rule.js +98 -0
- package/src/validators/index.js +3 -9
- package/src/validators/partitive-relation-coherence-rule.js +6 -79
- package/src/models/partitive-relation.js +0 -76
- package/src/rdf/gloss-hyperedge.js +0 -90
- package/src/validators/partitive-drift-rule.js +0 -51
- package/src/validators/partitive-hyperedge-rule.js +0 -75
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// GenericHyperedge → RDF quads.
|
|
2
|
+
//
|
|
3
|
+
// Shape (mirrors gloss-partitive-relation.js, swapping partitive
|
|
4
|
+
// predicates for generic predicates per the concept-model ontology
|
|
5
|
+
// extension that introduces gloss:GenericRelation):
|
|
6
|
+
//
|
|
7
|
+
// <base>/generic-relation/<carrying-id>/<comprehensive-id>
|
|
8
|
+
// rdf:type gloss:GenericRelation
|
|
9
|
+
// gloss:comprehensive <base>/concept/<comprehensive-id> (named node)
|
|
10
|
+
// gloss:hasGeneric <base>/concept/<member-id> (named node, one per member)
|
|
11
|
+
// gloss:completeness <.../completeness/complete|partial> (named node, SKOS)
|
|
12
|
+
// gloss:criterion "..."@lang (literal, one per language)
|
|
13
|
+
//
|
|
14
|
+
// Per-member multiplicity + is_delimiting reuse the same shape as
|
|
15
|
+
// partitive members (HyperedgeMember is the shared base). Emitted via
|
|
16
|
+
// `partitiveMemberToQuads` — the per-member shape is type-blind.
|
|
17
|
+
//
|
|
18
|
+
// The carrying concept links via `gloss:hasGenericRelation`.
|
|
19
|
+
//
|
|
20
|
+
// Per TODO Phase 6e. When the concept-model ontology publishes the
|
|
21
|
+
// generic-relation predicates, this file is the single place to update
|
|
22
|
+
// the predicate strings; nothing else in the RDF layer changes.
|
|
23
|
+
|
|
24
|
+
import { PRED } from './predicates.js';
|
|
25
|
+
import { WELL_KNOWN } from './prefixes.js';
|
|
26
|
+
import { partitiveMemberToQuads } from './gloss-partitive-member.js';
|
|
27
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
28
|
+
|
|
29
|
+
const COMPLETENESS_NS = 'https://www.glossarist.org/ontologies/completeness/';
|
|
30
|
+
|
|
31
|
+
// Predicate IRIs. concept-model ontology v3.2.0 does not yet publish
|
|
32
|
+
// GenericRelation predicates; we mint them under the same gloss:
|
|
33
|
+
// namespace following the PartitiveRelation naming convention. Update
|
|
34
|
+
// to match concept-model once finalized.
|
|
35
|
+
const HAS_GENERIC_RELATION = 'https://www.glossarist.org/ontologies/hasGenericRelation';
|
|
36
|
+
const HAS_GENERIC = 'https://www.glossarist.org/ontologies/hasGeneric';
|
|
37
|
+
const GENERIC_RELATION_TYPE = 'https://www.glossarist.org/ontologies/GenericRelation';
|
|
38
|
+
|
|
39
|
+
export function* genericHyperedgeToQuads(relation, { parentUri, index }) {
|
|
40
|
+
const subjectUri = genericHyperedgeSubjectUri(parentUri, relation, index);
|
|
41
|
+
const s = namedNode(subjectUri);
|
|
42
|
+
|
|
43
|
+
yield quad(s, namedNode(WELL_KNOWN.rdfType), namedNode(GENERIC_RELATION_TYPE));
|
|
44
|
+
|
|
45
|
+
const comp = relation.comprehensive;
|
|
46
|
+
if (comp && (comp.source || comp.id)) {
|
|
47
|
+
yield quad(s, namedNode(PRED.gloss.comprehensive),
|
|
48
|
+
namedNode(conceptRefUri(comp, parentUri)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const members = Array.isArray(relation.members) ? relation.members : [];
|
|
52
|
+
for (const member of members) {
|
|
53
|
+
const ref = member?.ref ?? member;
|
|
54
|
+
if (!ref || (!ref.source && !ref.id)) continue;
|
|
55
|
+
const memberUri = conceptRefUri(ref, parentUri);
|
|
56
|
+
yield quad(s, namedNode(HAS_GENERIC), namedNode(memberUri));
|
|
57
|
+
yield* partitiveMemberToQuads(member, { memberUri });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (relation.completeness) {
|
|
61
|
+
yield quad(s, namedNode(PRED.gloss.completeness),
|
|
62
|
+
namedNode(COMPLETENESS_NS + relation.completeness));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (relation.criterion && typeof relation.criterion === 'object') {
|
|
66
|
+
for (const [lang, text] of Object.entries(relation.criterion)) {
|
|
67
|
+
if (typeof text !== 'string') continue;
|
|
68
|
+
yield quad(s, namedNode(PRED.gloss.criterion), literal(text, lang));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Subject URI scheme: `<base>/generic-relation/<carrying-id>/<comprehensive-id>`.
|
|
74
|
+
// Mirrors the partitive scheme so consumers can predict either path.
|
|
75
|
+
export function genericHyperedgeSubjectUri(parentUri, relation, index) {
|
|
76
|
+
const base = parentUri.replace(/\/concept\/[^/]+$/, '');
|
|
77
|
+
const carrierId = parentUri.split('/concept/')[1] ?? `carrier-${index}`;
|
|
78
|
+
const compId = relation?.comprehensive?.id ?? `comp-${index}`;
|
|
79
|
+
return `${base}/generic-relation/${carrierId}/${compId}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Exported for gloss-concept.js to use when linking the carrying concept.
|
|
83
|
+
export const GENERIC_HYPEREDGE_LINK_PREDICATE = HAS_GENERIC_RELATION;
|
|
84
|
+
|
|
85
|
+
function conceptRefUri(ref, parentUri) {
|
|
86
|
+
const id = ref?.id;
|
|
87
|
+
if (id) {
|
|
88
|
+
const base = parentUri.split('/concept/')[0];
|
|
89
|
+
return `${base}/concept/${id}`;
|
|
90
|
+
}
|
|
91
|
+
return `urn:glossarist:${ref?.source ?? 'unknown'}:`;
|
|
92
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// PartitiveHyperedge → RDF quads.
|
|
2
2
|
//
|
|
3
3
|
// Shape (per concept-model ontology, v3.2.0):
|
|
4
4
|
//
|
|
5
5
|
// <base>/partitive-relation/<carrying-id>/<comprehensive-id>
|
|
6
|
-
// rdf:type gloss:
|
|
6
|
+
// rdf:type gloss:PartitiveHyperedge
|
|
7
7
|
// gloss:comprehensive <base>/concept/<comprehensive-id> (named node)
|
|
8
8
|
// gloss:hasPartitive <base>/concept/<partitive-id> (named node, one per member)
|
|
9
9
|
// gloss:completeness <.../completeness/complete|partial> (named node, SKOS)
|
|
@@ -35,7 +35,7 @@ export function* partitiveRelationToQuads(relation, { parentUri, index }) {
|
|
|
35
35
|
const subjectUri = partitiveRelationSubjectUri(parentUri, relation, index);
|
|
36
36
|
const s = namedNode(subjectUri);
|
|
37
37
|
|
|
38
|
-
yield quad(s, namedNode(WELL_KNOWN.rdfType), namedNode(PRED.gloss.
|
|
38
|
+
yield quad(s, namedNode(WELL_KNOWN.rdfType), namedNode(PRED.gloss.PartitiveHyperedge));
|
|
39
39
|
|
|
40
40
|
const comp = relation.comprehensive;
|
|
41
41
|
if (comp && (comp.source || comp.id)) {
|
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,
|
|
14
|
+
import type { PartitiveHyperedge, PartitiveHyperedge, PartitiveMember } from '../models/index';
|
|
15
15
|
|
|
16
16
|
export { PRED, PREFIXES } from './predicates';
|
|
17
17
|
export type PredicateMap = typeof PRED;
|
|
@@ -96,7 +96,7 @@ export declare function hyperedgeSubjectUri(
|
|
|
96
96
|
index: number,
|
|
97
97
|
): string;
|
|
98
98
|
|
|
99
|
-
// ──
|
|
99
|
+
// ── PartitiveHyperedge emitter (v2) ────────────────────────────────────
|
|
100
100
|
|
|
101
101
|
export interface PartitiveRelationEmitOptions {
|
|
102
102
|
parentUri: string;
|
|
@@ -104,13 +104,13 @@ export interface PartitiveRelationEmitOptions {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export declare function partitiveRelationToQuads(
|
|
107
|
-
relation:
|
|
107
|
+
relation: PartitiveHyperedge,
|
|
108
108
|
options: PartitiveRelationEmitOptions,
|
|
109
109
|
): Generator<Quad, void, unknown>;
|
|
110
110
|
|
|
111
111
|
export declare function partitiveRelationSubjectUri(
|
|
112
112
|
parentUri: string,
|
|
113
|
-
relation:
|
|
113
|
+
relation: PartitiveHyperedge,
|
|
114
114
|
index: number,
|
|
115
115
|
): string;
|
|
116
116
|
|
package/src/rdf/index.js
CHANGED
|
@@ -26,7 +26,6 @@ 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
29
|
export { partitiveMemberToQuads, MULTIPLICITY_NS, PRESENCE_NS, COUNT_NS } from './gloss-partitive-member.js';
|
|
31
30
|
export { partitiveRelationToQuads, partitiveRelationSubjectUri } from './gloss-partitive-relation.js';
|
|
32
31
|
export {
|
|
@@ -84,29 +84,31 @@ export class ReferenceResolver {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
//
|
|
88
|
-
// concept.
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
const relations = concept.
|
|
87
|
+
// All hyperedges (Partitive + Generic + future types) live in
|
|
88
|
+
// concept.relations. Each has a comprehensive ConceptRef and 2+
|
|
89
|
+
// members each carrying a ConceptRef. The reference type is the
|
|
90
|
+
// relation's typeTag (partitive_relation, generic_relation, …).
|
|
91
|
+
const relations = concept.relations ?? [];
|
|
92
92
|
for (let i = 0; i < relations.length; i++) {
|
|
93
93
|
const rel = relations[i];
|
|
94
|
-
const
|
|
94
|
+
const relType = rel?.constructor?.typeTag ?? 'hyperedge';
|
|
95
|
+
const relPath = `relations[${i}]`;
|
|
95
96
|
|
|
96
97
|
const compTarget = hyperedgeRefTarget(rel.comprehensive);
|
|
97
98
|
if (compTarget) {
|
|
98
99
|
refs.push(new Reference(
|
|
99
100
|
'concept',
|
|
100
101
|
compTarget,
|
|
101
|
-
|
|
102
|
+
relType,
|
|
102
103
|
`${relPath}.comprehensive`,
|
|
103
104
|
{ lookupKey: { id: compTarget } },
|
|
104
105
|
));
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
108
|
+
// Member shape: members is [{ ref, presence, count, is_delimiting }, ...]
|
|
109
|
+
// (HyperedgeMember / PartitiveMember / GenericMember). The wire
|
|
110
|
+
// alias .partitives is also handled for backward compat with v2 wire consumers.
|
|
111
|
+
const members = rel.members ?? rel.partitives ?? rel.parts ?? [];
|
|
110
112
|
for (let j = 0; j < members.length; j++) {
|
|
111
113
|
const m = members[j];
|
|
112
114
|
const memberRef = m?.ref ?? m;
|
|
@@ -115,8 +117,8 @@ export class ReferenceResolver {
|
|
|
115
117
|
refs.push(new Reference(
|
|
116
118
|
'concept',
|
|
117
119
|
pTarget,
|
|
118
|
-
|
|
119
|
-
`${relPath}.
|
|
120
|
+
relType,
|
|
121
|
+
`${relPath}.members[${j}]`,
|
|
120
122
|
{ lookupKey: { id: pTarget } },
|
|
121
123
|
));
|
|
122
124
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Warns when binary `has_part` / `is_part_of` edges duplicate
|
|
2
|
-
//
|
|
3
|
-
// has_part edges that should be consolidated into a
|
|
2
|
+
// PartitiveHyperedge members, or when a concept has many binary
|
|
3
|
+
// has_part edges that should be consolidated into a PartitiveHyperedge.
|
|
4
4
|
//
|
|
5
5
|
// Per TODO.partitive-relation-v2 item 14, binary edges and
|
|
6
6
|
// PartitiveRelations coexist by design. The asymmetry is pragmatic:
|
|
@@ -20,7 +20,7 @@ export class BinaryHasPartRedundancyRule extends ValidationRule {
|
|
|
20
20
|
constructor() { super('binary-has-part-redundancy', 'warning'); }
|
|
21
21
|
|
|
22
22
|
validate(concept, path, result) {
|
|
23
|
-
const relations = concept.
|
|
23
|
+
const relations = concept.relations ?? [];
|
|
24
24
|
|
|
25
25
|
// Collect partitive refs from PartitiveRelations.
|
|
26
26
|
const relationTargets = new Set();
|
|
@@ -49,17 +49,17 @@ export class BinaryHasPartRedundancyRule extends ValidationRule {
|
|
|
49
49
|
this.addIssue(result,
|
|
50
50
|
`${path}relatedConcepts`,
|
|
51
51
|
`binary has_part edge for (${target}) is redundant — already in a ` +
|
|
52
|
-
`
|
|
52
|
+
`PartitiveHyperedge member; pick one encoding to avoid divergence`,
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// Cluster: 3+ binary has_part edges suggests
|
|
57
|
+
// Cluster: 3+ binary has_part edges suggests PartitiveHyperedge territory.
|
|
58
58
|
if (binaryTargets.size >= 3) {
|
|
59
59
|
this.addIssue(result,
|
|
60
60
|
`${path}relatedConcepts`,
|
|
61
61
|
`${binaryTargets.size} binary has_part edges on this concept; ` +
|
|
62
|
-
`consider converting to a
|
|
62
|
+
`consider converting to a PartitiveHyperedge (which can carry ` +
|
|
63
63
|
`completeness, multiplicity, and criterion metadata)`,
|
|
64
64
|
);
|
|
65
65
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// This rule surfaces three categories of issues that the model
|
|
10
10
|
// constructor cannot catch (they're cross-relation, not per-instance):
|
|
11
11
|
//
|
|
12
|
-
// 1. WARNING: A
|
|
12
|
+
// 1. WARNING: A PartitiveHyperedge where every partitive is
|
|
13
13
|
// is_delimiting=true overuses delimiting. Typically only some
|
|
14
14
|
// parts distinguish the comprehensive from coordinate concepts.
|
|
15
15
|
// 2. ERROR: Two PartitiveRelations on the same concept with the
|
|
@@ -30,7 +30,7 @@ export class DelimitingCoherenceRule extends ValidationRule {
|
|
|
30
30
|
constructor() { super('delimiting-coherence'); }
|
|
31
31
|
|
|
32
32
|
validate(concept, path, result) {
|
|
33
|
-
const relations = concept.
|
|
33
|
+
const relations = concept.relations ?? [];
|
|
34
34
|
if (relations.length === 0) return;
|
|
35
35
|
|
|
36
36
|
_checkOveruse(relations, path, result);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Validates hyperedge coherence per ISO 704 / ISO 12620.
|
|
2
|
+
//
|
|
3
|
+
// Applies to every AbstractHyperedge subclass (PartitiveHyperedge,
|
|
4
|
+
// GenericHyperedge, future TemporalHyperedge / AssociativeHyperedge /
|
|
5
|
+
// SequentialHyperedge). The ISO 704 / ISO 12620 invariants are
|
|
6
|
+
// type-blind:
|
|
7
|
+
//
|
|
8
|
+
// 1. ERROR: each hyperedge has ≥2 members (ISO 704 "two or more")
|
|
9
|
+
// 2. ERROR: no two hyperedges of the same type on the same concept
|
|
10
|
+
// share the same comprehensive + same criterion (duplicate
|
|
11
|
+
// decomposition — violates ISO 12620 coordinate-concept coherence)
|
|
12
|
+
// 3. WARNING: a concept with multiple hyperedges of the same type
|
|
13
|
+
// where any lacks criterion (cannot verify distinctness from
|
|
14
|
+
// siblings)
|
|
15
|
+
//
|
|
16
|
+
// Lives in the validator framework, not the model constructor, per
|
|
17
|
+
// invariant I8 (TODO.hyperedges-v2/00). Constructors enforce type-shape;
|
|
18
|
+
// validators enforce semantics.
|
|
19
|
+
//
|
|
20
|
+
// Per TODO Phase 7: rule renamed from PartitiveRelationCoherenceRule
|
|
21
|
+
// to HyperedgeCoherenceRule. The rule applies to all hyperedges; the
|
|
22
|
+
// duplicate check is scoped per type so a Partitive and a Generic with
|
|
23
|
+
// the same comprehensive+criterion don't false-positive.
|
|
24
|
+
|
|
25
|
+
import { ValidationRule } from './validation-rule.js';
|
|
26
|
+
import { refKey, criterionKey } from './ref-keys.js';
|
|
27
|
+
import { AbstractHyperedge } from '../models/abstract-hyperedge.js';
|
|
28
|
+
|
|
29
|
+
export class HyperedgeCoherenceRule extends ValidationRule {
|
|
30
|
+
constructor() { super('hyperedge-coherence'); }
|
|
31
|
+
|
|
32
|
+
validate(concept, path, result) {
|
|
33
|
+
const hyperedges = (concept.relations ?? []).filter(r => r instanceof AbstractHyperedge);
|
|
34
|
+
if (hyperedges.length === 0) return;
|
|
35
|
+
|
|
36
|
+
// Check 1: ≥2 members per hyperedge. (The constructor also
|
|
37
|
+
// rejects this, but a warning here surfaces it in the result
|
|
38
|
+
// bundle for CI visibility.)
|
|
39
|
+
for (let i = 0; i < hyperedges.length; i++) {
|
|
40
|
+
const memberCount = (hyperedges[i]?.members ?? []).length;
|
|
41
|
+
if (memberCount < 2) {
|
|
42
|
+
this.addIssue(result,
|
|
43
|
+
`${path}relations[${i}].members`,
|
|
44
|
+
`hyperedge ${i + 1} has ${memberCount} member(s); ` +
|
|
45
|
+
`ISO 704 requires ≥2 (use a binary edge for pairwise facts)`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check 2: no duplicate (type + comprehensive + criterion) on the same concept.
|
|
51
|
+
// Scope by type so a Partitive and a Generic with the same comp+criterion
|
|
52
|
+
// don't false-positive — they're different decompositions.
|
|
53
|
+
const seen = new Map();
|
|
54
|
+
for (let i = 0; i < hyperedges.length; i++) {
|
|
55
|
+
const rel = hyperedges[i];
|
|
56
|
+
const typeTag = rel?.constructor?.typeTag ?? '';
|
|
57
|
+
const compKey = refKey(rel?.comprehensive);
|
|
58
|
+
const critKey = criterionKey(rel?.criterion);
|
|
59
|
+
if (compKey == null || critKey == null) continue;
|
|
60
|
+
const key = `${typeTag}|${compKey}|${critKey}`;
|
|
61
|
+
if (seen.has(key)) {
|
|
62
|
+
this.addIssue(result,
|
|
63
|
+
`${path}relations[${i}]`,
|
|
64
|
+
`duplicate ${rel.constructor.name} (same comprehensive + same criterion ` +
|
|
65
|
+
`as relation ${seen.get(key) + 1}); violates ISO 12620 coordinate-concept coherence`,
|
|
66
|
+
);
|
|
67
|
+
} else {
|
|
68
|
+
seen.set(key, i);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check 3: criterion documentation (warning) when concept has multiple
|
|
73
|
+
// hyperedges of the same type. Same-type-scope rationale as Check 2.
|
|
74
|
+
const byType = new Map();
|
|
75
|
+
for (const rel of hyperedges) {
|
|
76
|
+
const typeTag = rel?.constructor?.typeTag ?? '';
|
|
77
|
+
if (!byType.has(typeTag)) byType.set(typeTag, []);
|
|
78
|
+
byType.get(typeTag).push(rel);
|
|
79
|
+
}
|
|
80
|
+
for (const [typeTag, rels] of byType) {
|
|
81
|
+
if (rels.length <= 1) continue;
|
|
82
|
+
for (const rel of rels) {
|
|
83
|
+
if (!rel.criterion) {
|
|
84
|
+
const idx = hyperedges.indexOf(rel);
|
|
85
|
+
result.addWarning(
|
|
86
|
+
`${path}relations[${idx}].criterion`,
|
|
87
|
+
`concept has multiple ${typeTag} hyperedges but this one ` +
|
|
88
|
+
`has no criterion; cannot verify distinctness from siblings`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Backward-compat alias. Existing imports of PartitiveRelationCoherenceRule
|
|
97
|
+
// continue to work; the rule is now type-blind.
|
|
98
|
+
export const PartitiveRelationCoherenceRule = HyperedgeCoherenceRule;
|
package/src/validators/index.js
CHANGED
|
@@ -17,10 +17,8 @@ export {
|
|
|
17
17
|
NonVerbalRefIntegrityRule,
|
|
18
18
|
OrphanedImagesRule,
|
|
19
19
|
} from './v3-rules.js';
|
|
20
|
-
export { PartitiveHyperedgeShapeRule } from './partitive-hyperedge-rule.js';
|
|
21
|
-
export { PartitiveDriftRule } from './partitive-drift-rule.js';
|
|
22
20
|
export { SourcedFromLocalityRule } from './sourced-from-locality-rule.js';
|
|
23
|
-
export { PartitiveRelationCoherenceRule } from './
|
|
21
|
+
export { PartitiveRelationCoherenceRule, HyperedgeCoherenceRule } from './hyperedge-coherence-rule.js';
|
|
24
22
|
export { ExternalConceptShapeRule } from './external-concept-shape-rule.js';
|
|
25
23
|
export { BinaryHasPartRedundancyRule } from './binary-has-part-redundancy-rule.js';
|
|
26
24
|
export { DelimitingCoherenceRule } from './delimiting-coherence-rule.js';
|
|
@@ -40,10 +38,8 @@ import {
|
|
|
40
38
|
CiteRefIntegrityRule,
|
|
41
39
|
NonVerbalRefIntegrityRule,
|
|
42
40
|
} from './v3-rules.js';
|
|
43
|
-
import { PartitiveHyperedgeShapeRule } from './partitive-hyperedge-rule.js';
|
|
44
|
-
import { PartitiveDriftRule } from './partitive-drift-rule.js';
|
|
45
41
|
import { SourcedFromLocalityRule } from './sourced-from-locality-rule.js';
|
|
46
|
-
import {
|
|
42
|
+
import { HyperedgeCoherenceRule } from './hyperedge-coherence-rule.js';
|
|
47
43
|
import { ExternalConceptShapeRule } from './external-concept-shape-rule.js';
|
|
48
44
|
import { BinaryHasPartRedundancyRule } from './binary-has-part-redundancy-rule.js';
|
|
49
45
|
import { DelimitingCoherenceRule } from './delimiting-coherence-rule.js';
|
|
@@ -62,10 +58,8 @@ const _default = new ConceptValidator()
|
|
|
62
58
|
.addRule(new RelationshipTypeRule())
|
|
63
59
|
.addRule(new CiteRefIntegrityRule())
|
|
64
60
|
.addRule(new NonVerbalRefIntegrityRule())
|
|
65
|
-
.addRule(new PartitiveHyperedgeShapeRule())
|
|
66
|
-
.addRule(new PartitiveDriftRule())
|
|
67
61
|
.addRule(new SourcedFromLocalityRule())
|
|
68
|
-
.addRule(new
|
|
62
|
+
.addRule(new HyperedgeCoherenceRule())
|
|
69
63
|
.addRule(new ExternalConceptShapeRule())
|
|
70
64
|
.addRule(new BinaryHasPartRedundancyRule())
|
|
71
65
|
.addRule(new DelimitingCoherenceRule());
|
|
@@ -1,80 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// comprehensive + same criterion (duplicate decomposition —
|
|
7
|
-
// violates ISO 12620 coordinate-concept coherence)
|
|
8
|
-
// 3. WARNING: a concept with multiple relations where any relation
|
|
9
|
-
// lacks criterion (cannot verify distinctness from siblings)
|
|
10
|
-
// 4. WARNING: a TypeSharedPlurality with is_uncertain=true but
|
|
11
|
-
// is_shared=false (semantically odd — broken line qualifies
|
|
12
|
-
// the close-set double line; without `double`, what's being
|
|
13
|
-
// qualified?)
|
|
14
|
-
//
|
|
15
|
-
// Lives in the validator framework, not the model constructor, per
|
|
16
|
-
// invariant I8 (TODO.hyperedges-v2/00). Constructors enforce type-shape;
|
|
17
|
-
// validators enforce semantics.
|
|
1
|
+
// Deprecated. This rule has been generalized and renamed to
|
|
2
|
+
// HyperedgeCoherenceRule (see ./hyperedge-coherence-rule.js). The
|
|
3
|
+
// new rule is type-blind — it validates every AbstractHyperedge
|
|
4
|
+
// subclass, not just PartitiveHyperedge. This file remains as a
|
|
5
|
+
// re-export shim for existing imports.
|
|
18
6
|
|
|
19
|
-
|
|
20
|
-
import { refKey, criterionKey } from './ref-keys.js';
|
|
21
|
-
|
|
22
|
-
export class PartitiveRelationCoherenceRule extends ValidationRule {
|
|
23
|
-
constructor() { super('partitive-relation-coherence'); }
|
|
24
|
-
|
|
25
|
-
validate(concept, path, result) {
|
|
26
|
-
const relations = concept.partitiveRelations ?? concept.partitiveHyperedges ?? [];
|
|
27
|
-
if (relations.length === 0) return;
|
|
28
|
-
|
|
29
|
-
// Check 1: ≥2 partitives per relation. (The constructor also
|
|
30
|
-
// rejects this, but a warning here surfaces it in the result
|
|
31
|
-
// bundle for CI visibility.)
|
|
32
|
-
for (let i = 0; i < relations.length; i++) {
|
|
33
|
-
const partitiveCount = (relations[i]?.partitives ?? []).length;
|
|
34
|
-
if (partitiveCount < 2) {
|
|
35
|
-
this.addIssue(result,
|
|
36
|
-
`${path}partitiveRelations[${i}].partitives`,
|
|
37
|
-
`PartitiveRelation ${i + 1} has ${partitiveCount} partitive(s); ` +
|
|
38
|
-
`ISO 704 requires ≥2 (use a binary has_part edge for pairwise facts)`,
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Check 2: no duplicate (comprehensive + criterion) on the same concept.
|
|
44
|
-
const seen = new Map();
|
|
45
|
-
for (let i = 0; i < relations.length; i++) {
|
|
46
|
-
const rel = relations[i];
|
|
47
|
-
const compKey = refKey(rel?.comprehensive);
|
|
48
|
-
const critKey = criterionKey(rel?.criterion);
|
|
49
|
-
if (compKey == null || critKey == null) continue;
|
|
50
|
-
const key = `${compKey}|${critKey}`;
|
|
51
|
-
if (seen.has(key)) {
|
|
52
|
-
this.addIssue(result,
|
|
53
|
-
`${path}partitiveRelations[${i}]`,
|
|
54
|
-
`duplicate PartitiveRelation (same comprehensive + same criterion ` +
|
|
55
|
-
`as relation ${seen.get(key) + 1}); violates ISO 12620 coordinate-concept coherence`,
|
|
56
|
-
);
|
|
57
|
-
} else {
|
|
58
|
-
seen.set(key, i);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Check 3: criterion documentation (warning) when concept has multiple relations.
|
|
63
|
-
if (relations.length > 1) {
|
|
64
|
-
for (let i = 0; i < relations.length; i++) {
|
|
65
|
-
if (!relations[i]?.criterion) {
|
|
66
|
-
result.addWarning(
|
|
67
|
-
`${path}partitiveRelations[${i}].criterion`,
|
|
68
|
-
`concept has multiple PartitiveRelations but relation ${i + 1} ` +
|
|
69
|
-
`has no criterion; cannot verify distinctness from siblings`,
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// (Check 4 — plurality coherence — removed. The prior plurality
|
|
76
|
-
// block was based on a misreading of ISO 704; the corrected model
|
|
77
|
-
// uses per-member multiplicity + is_delimiting, which are validated
|
|
78
|
-
// at construction by PartitiveMember itself.)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
7
|
+
export { HyperedgeCoherenceRule, PartitiveRelationCoherenceRule } from './hyperedge-coherence-rule.js';
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// PartitiveRelation — an ISO 704:2022 / ISO 1087-1 / ISO 12620
|
|
2
|
-
// partitive relation connecting a comprehensive concept
|
|
3
|
-
// (superordinate concept partitive) to two or more partitive
|
|
4
|
-
// concepts (subordinate concepts partitive) which fitted together
|
|
5
|
-
// constitute the comprehensive.
|
|
6
|
-
//
|
|
7
|
-
// Extends AbstractNaryRelation; the leaf-specific concern is the
|
|
8
|
-
// PartitiveMember type for `members`. The wire-level field name
|
|
9
|
-
// stays `partitives` for v1/v2 backward compat.
|
|
10
|
-
//
|
|
11
|
-
// ISO 704:2022 rake diagram semantics:
|
|
12
|
-
//
|
|
13
|
-
// comprehensive concept
|
|
14
|
-
// ─────────────────────
|
|
15
|
-
// │ partitive (one or more) with per-member multiplicity + optional delimiting
|
|
16
|
-
// ─────────────────────
|
|
17
|
-
//
|
|
18
|
-
// All partitives within one relation are coordinate concepts
|
|
19
|
-
// (ISO 12620): they share the comprehensive AND share the
|
|
20
|
-
// criterion of subdivision.
|
|
21
|
-
//
|
|
22
|
-
// "Delimiting" parts (bold/3x-width line in source diagrams) behave
|
|
23
|
-
// like delimiting characteristics — they distinguish the comprehensive
|
|
24
|
-
// from coordinate concepts. Whether a part is delimiting depends on
|
|
25
|
-
// the concept system, the coordinate concepts, the inheritance
|
|
26
|
-
// principle, and the criterion of subdivision used
|
|
27
|
-
// (ISO 704:2022 §5.5.4.2.1).
|
|
28
|
-
//
|
|
29
|
-
// Distinguished from binary `has_part` / `is_part_of` edges, which
|
|
30
|
-
// express pairwise part-of assertions without completeness,
|
|
31
|
-
// multiplicity, or criterion claims. A single partitive should be
|
|
32
|
-
// expressed as a binary edge, not a PartitiveRelation (ISO requires
|
|
33
|
-
// "two or more").
|
|
34
|
-
|
|
35
|
-
import { AbstractNaryRelation } from './abstract-nary-relation.js';
|
|
36
|
-
import { PartitiveMember } from './partitive-member.js';
|
|
37
|
-
|
|
38
|
-
export class PartitiveRelation extends AbstractNaryRelation {
|
|
39
|
-
constructor(data = {}) {
|
|
40
|
-
const members = data?.members ?? data?.partitives;
|
|
41
|
-
super({ ...data, members });
|
|
42
|
-
this.members = members == null
|
|
43
|
-
? this.members
|
|
44
|
-
: (Array.isArray(members)
|
|
45
|
-
? members.map(m =>
|
|
46
|
-
m instanceof PartitiveMember ? m : new PartitiveMember(m))
|
|
47
|
-
: this.members);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
get partitives() { return this.members; }
|
|
51
|
-
|
|
52
|
-
toJSON() {
|
|
53
|
-
const base = super.toJSON();
|
|
54
|
-
const { members, ...rest } = base;
|
|
55
|
-
return { ...rest, partitives: members };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
static identityOf(value) {
|
|
59
|
-
const v = value ?? {};
|
|
60
|
-
const c = v.comprehensive ?? {};
|
|
61
|
-
const partKeys = Array.isArray(v.partitives ?? v.members)
|
|
62
|
-
? (v.partitives ?? v.members).map(p => {
|
|
63
|
-
const r = p?.ref ?? p ?? {};
|
|
64
|
-
return `${r.source ?? ''}:${r.id ?? ''}`;
|
|
65
|
-
}).sort()
|
|
66
|
-
: [];
|
|
67
|
-
return `${c.source ?? ''}:${c.id ?? ''}|${partKeys.join('|')}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
static fromJSON(data) {
|
|
71
|
-
const normalized = { ...(data ?? {}), members: data?.partitives ?? data?.members };
|
|
72
|
-
return new PartitiveRelation(normalized);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export default PartitiveRelation;
|
|
@@ -1,90 +0,0 @@
|
|
|
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
|
-
}
|