glossarist 0.4.40 → 0.4.42
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/{canonical-json.js → canonical-json.ts} +8 -10
- package/src/diff/identity.ts +33 -0
- package/src/diff/{similarity.js → similarity.ts} +3 -7
- package/src/diff/text-diff.ts +167 -0
- package/src/models/concept.ts +499 -0
- package/src/models/localized-concept.ts +284 -0
- package/src/models/{relation-loader.js → relation-loader.ts} +50 -46
- package/src/diff/identity.js +0 -22
- package/src/diff/text-diff.js +0 -128
- package/src/models/concept.js +0 -420
- package/src/models/localized-concept.js +0 -187
- /package/src/models/{index.js → index.ts} +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { GlossaristModel } from './base.js';
|
|
2
|
+
import { Designation } from './designation.js';
|
|
3
|
+
import { DetailedDefinition } from './detailed-definition.js';
|
|
4
|
+
import { ConceptSource } from './concept-source.js';
|
|
5
|
+
import { ConceptDate } from './concept-date.js';
|
|
6
|
+
import { NonVerbRep } from './non-verb-rep.js';
|
|
7
|
+
import { RelatedConcept } from './related-concept.js';
|
|
8
|
+
|
|
9
|
+
function wrapAs<T>(
|
|
10
|
+
Cls: new (...args: never[]) => T,
|
|
11
|
+
): (item: unknown) => T {
|
|
12
|
+
return (item: unknown): T =>
|
|
13
|
+
item instanceof Cls ? item : new Cls(item as never);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const LOC_WIRE_NAMES: Readonly<Record<string, string>> = Object.freeze({
|
|
17
|
+
entryStatus: 'entry_status',
|
|
18
|
+
reviewType: 'review_type',
|
|
19
|
+
lineageSourceSimilarity: 'lineage_source_similarity',
|
|
20
|
+
reviewDate: 'review_date',
|
|
21
|
+
reviewDecisionDate: 'review_decision_date',
|
|
22
|
+
reviewDecisionEvent: 'review_decision_event',
|
|
23
|
+
reviewStatus: 'review_status',
|
|
24
|
+
reviewDecision: 'review_decision',
|
|
25
|
+
reviewDecisionNotes: 'review_decision_notes',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export interface LocalizedConceptJson {
|
|
29
|
+
language_code?: string | null;
|
|
30
|
+
languageCode?: string | null;
|
|
31
|
+
script?: string | null;
|
|
32
|
+
system?: string | null;
|
|
33
|
+
entry_status?: string | null;
|
|
34
|
+
entryStatus?: string | null;
|
|
35
|
+
classification?: string | null;
|
|
36
|
+
review_type?: string | null;
|
|
37
|
+
reviewType?: string | null;
|
|
38
|
+
domain?: string | null;
|
|
39
|
+
release?: string | null;
|
|
40
|
+
lineage_source_similarity?: string | null;
|
|
41
|
+
lineageSourceSimilarity?: string | null;
|
|
42
|
+
review_date?: string | null;
|
|
43
|
+
reviewDate?: string | null;
|
|
44
|
+
review_decision_date?: string | null;
|
|
45
|
+
reviewDecisionDate?: string | null;
|
|
46
|
+
review_decision_event?: string | null;
|
|
47
|
+
reviewDecisionEvent?: string | null;
|
|
48
|
+
review_status?: string | null;
|
|
49
|
+
reviewStatus?: string | null;
|
|
50
|
+
review_decision?: string | null;
|
|
51
|
+
reviewDecision?: string | null;
|
|
52
|
+
review_decision_notes?: string | null;
|
|
53
|
+
reviewDecisionNotes?: string | null;
|
|
54
|
+
terms?: ReadonlyArray<unknown>;
|
|
55
|
+
definition?: ReadonlyArray<unknown>;
|
|
56
|
+
sources?: ReadonlyArray<unknown>;
|
|
57
|
+
notes?: ReadonlyArray<unknown>;
|
|
58
|
+
annotations?: ReadonlyArray<unknown>;
|
|
59
|
+
examples?: ReadonlyArray<unknown>;
|
|
60
|
+
dates?: ReadonlyArray<unknown>;
|
|
61
|
+
non_verbal_rep?: ReadonlyArray<unknown>;
|
|
62
|
+
non_verb?: ReadonlyArray<unknown>;
|
|
63
|
+
related?: ReadonlyArray<unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type SlotName = 'definitions' | 'notes' | 'examples' | 'annotations';
|
|
67
|
+
|
|
68
|
+
export class LocalizedConcept extends GlossaristModel {
|
|
69
|
+
static get DIFF_FIELDS(): ReadonlyArray<string> {
|
|
70
|
+
return Object.freeze([
|
|
71
|
+
'entryStatus', 'classification', 'reviewType', 'domain', 'release',
|
|
72
|
+
'lineageSourceSimilarity', 'script', 'system',
|
|
73
|
+
'reviewDate', 'reviewDecisionDate', 'reviewDecisionEvent',
|
|
74
|
+
'reviewStatus', 'reviewDecision', 'reviewDecisionNotes',
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static wireNameFor(field: string): string {
|
|
79
|
+
return LOC_WIRE_NAMES[field] ?? field;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
readonly languageCode: string | null;
|
|
83
|
+
readonly script: string | null;
|
|
84
|
+
readonly system: string | null;
|
|
85
|
+
readonly entryStatus: string | null;
|
|
86
|
+
readonly classification: string | null;
|
|
87
|
+
readonly reviewType: string | null;
|
|
88
|
+
readonly domain: string | null;
|
|
89
|
+
readonly release: string | null;
|
|
90
|
+
readonly lineageSourceSimilarity: string | null;
|
|
91
|
+
readonly reviewDate: string | null;
|
|
92
|
+
readonly reviewDecisionDate: string | null;
|
|
93
|
+
readonly reviewDecisionEvent: string | null;
|
|
94
|
+
readonly reviewStatus: string | null;
|
|
95
|
+
readonly reviewDecision: string | null;
|
|
96
|
+
readonly reviewDecisionNotes: string | null;
|
|
97
|
+
|
|
98
|
+
private readonly _rawTerms: ReadonlyArray<unknown>;
|
|
99
|
+
private readonly _rawDefinition: ReadonlyArray<unknown>;
|
|
100
|
+
private readonly _rawSources: ReadonlyArray<unknown>;
|
|
101
|
+
private readonly _rawNotes: ReadonlyArray<unknown>;
|
|
102
|
+
private readonly _rawAnnotations: ReadonlyArray<unknown>;
|
|
103
|
+
private readonly _rawExamples: ReadonlyArray<unknown>;
|
|
104
|
+
private readonly _rawDates: ReadonlyArray<unknown>;
|
|
105
|
+
private readonly _rawNonVerbal: ReadonlyArray<unknown>;
|
|
106
|
+
private readonly _rawRelated: ReadonlyArray<unknown>;
|
|
107
|
+
private _terms: ReadonlyArray<Designation> | null = null;
|
|
108
|
+
private _definitions: ReadonlyArray<DetailedDefinition> | null = null;
|
|
109
|
+
private _sources: ReadonlyArray<ConceptSource> | null = null;
|
|
110
|
+
private _notes: ReadonlyArray<DetailedDefinition> | null = null;
|
|
111
|
+
private _annotations: ReadonlyArray<DetailedDefinition> | null = null;
|
|
112
|
+
private _examples: ReadonlyArray<DetailedDefinition> | null = null;
|
|
113
|
+
private _dates: ReadonlyArray<ConceptDate> | null = null;
|
|
114
|
+
private _nonVerbal: ReadonlyArray<NonVerbRep> | null = null;
|
|
115
|
+
private _related: ReadonlyArray<RelatedConcept> | null = null;
|
|
116
|
+
|
|
117
|
+
constructor(data: LocalizedConceptJson = {}) {
|
|
118
|
+
super();
|
|
119
|
+
this.languageCode = data.language_code ?? data.languageCode ?? null;
|
|
120
|
+
this.script = data.script ?? null;
|
|
121
|
+
this.system = data.system ?? null;
|
|
122
|
+
this.entryStatus = data.entry_status ?? data.entryStatus ?? null;
|
|
123
|
+
this.classification = data.classification ?? null;
|
|
124
|
+
this.reviewType = data.review_type ?? data.reviewType ?? null;
|
|
125
|
+
this.domain = data.domain ?? null;
|
|
126
|
+
this.release = data.release ?? null;
|
|
127
|
+
this.lineageSourceSimilarity =
|
|
128
|
+
data.lineage_source_similarity ?? data.lineageSourceSimilarity ?? null;
|
|
129
|
+
this.reviewDate = data.review_date ?? data.reviewDate ?? null;
|
|
130
|
+
this.reviewDecisionDate = data.review_decision_date ?? data.reviewDecisionDate ?? null;
|
|
131
|
+
this.reviewDecisionEvent = data.review_decision_event ?? data.reviewDecisionEvent ?? null;
|
|
132
|
+
this.reviewStatus = data.review_status ?? data.reviewStatus ?? null;
|
|
133
|
+
this.reviewDecision = data.review_decision ?? data.reviewDecision ?? null;
|
|
134
|
+
this.reviewDecisionNotes = data.review_decision_notes ?? data.reviewDecisionNotes ?? null;
|
|
135
|
+
|
|
136
|
+
this._rawTerms = data.terms ?? [];
|
|
137
|
+
this._rawDefinition = data.definition ?? [];
|
|
138
|
+
this._rawSources = data.sources ?? [];
|
|
139
|
+
this._rawNotes = data.notes ?? [];
|
|
140
|
+
this._rawAnnotations = data.annotations ?? [];
|
|
141
|
+
this._rawExamples = data.examples ?? [];
|
|
142
|
+
this._rawDates = data.dates ?? [];
|
|
143
|
+
this._rawNonVerbal = data.non_verbal_rep ?? data.non_verb ?? [];
|
|
144
|
+
this._rawRelated = data.related ?? [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get terms(): ReadonlyArray<Designation> {
|
|
148
|
+
return this._lazy<Designation>(
|
|
149
|
+
'_terms',
|
|
150
|
+
'_rawTerms',
|
|
151
|
+
(t) => Designation.fromData(t),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get definitions(): ReadonlyArray<DetailedDefinition> {
|
|
156
|
+
return this._lazy<DetailedDefinition>(
|
|
157
|
+
'_definitions',
|
|
158
|
+
'_rawDefinition',
|
|
159
|
+
wrapAs(DetailedDefinition),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get definition(): ReadonlyArray<DetailedDefinition> {
|
|
164
|
+
return this.definitions;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get sources(): ReadonlyArray<ConceptSource> {
|
|
168
|
+
return this._lazy<ConceptSource>(
|
|
169
|
+
'_sources',
|
|
170
|
+
'_rawSources',
|
|
171
|
+
wrapAs(ConceptSource),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get notes(): ReadonlyArray<DetailedDefinition> {
|
|
176
|
+
return this._lazy<DetailedDefinition>(
|
|
177
|
+
'_notes',
|
|
178
|
+
'_rawNotes',
|
|
179
|
+
wrapAs(DetailedDefinition),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
get annotations(): ReadonlyArray<DetailedDefinition> {
|
|
184
|
+
return this._lazy<DetailedDefinition>(
|
|
185
|
+
'_annotations',
|
|
186
|
+
'_rawAnnotations',
|
|
187
|
+
wrapAs(DetailedDefinition),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
get examples(): ReadonlyArray<DetailedDefinition> {
|
|
192
|
+
return this._lazy<DetailedDefinition>(
|
|
193
|
+
'_examples',
|
|
194
|
+
'_rawExamples',
|
|
195
|
+
wrapAs(DetailedDefinition),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
get dates(): ReadonlyArray<ConceptDate> {
|
|
200
|
+
return this._lazy<ConceptDate>(
|
|
201
|
+
'_dates',
|
|
202
|
+
'_rawDates',
|
|
203
|
+
wrapAs(ConceptDate),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
get nonVerbalRep(): ReadonlyArray<NonVerbRep> {
|
|
208
|
+
return this._lazy<NonVerbRep>(
|
|
209
|
+
'_nonVerbal',
|
|
210
|
+
'_rawNonVerbal',
|
|
211
|
+
wrapAs(NonVerbRep),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
get related(): ReadonlyArray<RelatedConcept> {
|
|
216
|
+
return this._lazy<RelatedConcept>(
|
|
217
|
+
'_related',
|
|
218
|
+
'_rawRelated',
|
|
219
|
+
wrapAs(RelatedConcept),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
get primaryDesignation(): string | null {
|
|
224
|
+
return this.terms[0]?.designation ?? null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
get primaryDefinition(): string | null {
|
|
228
|
+
return this.definitions[0]?.content ?? null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private *_textSlots(): Generator<[SlotName, ReadonlyArray<DetailedDefinition>]> {
|
|
232
|
+
yield ['definitions', this.definitions];
|
|
233
|
+
yield ['notes', this.notes];
|
|
234
|
+
yield ['examples', this.examples];
|
|
235
|
+
yield ['annotations', this.annotations];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
*walkTexts(basePath: string): Generator<{ text: string; source: string }> {
|
|
239
|
+
for (const [name, arr] of this._textSlots()) {
|
|
240
|
+
for (let i = 0; i < arr.length; i++) {
|
|
241
|
+
const item = arr[i];
|
|
242
|
+
if (!item) continue;
|
|
243
|
+
yield* item.walkTexts(`${basePath}.${name}[${i}]`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
override toJSON(): LocalizedConceptJson {
|
|
249
|
+
const obj: LocalizedConceptJson = {};
|
|
250
|
+
if (this.languageCode) obj.language_code = this.languageCode;
|
|
251
|
+
if (this.script) obj.script = this.script;
|
|
252
|
+
if (this.system) obj.system = this.system;
|
|
253
|
+
if (this.entryStatus) obj.entry_status = this.entryStatus;
|
|
254
|
+
if (this.classification) obj.classification = this.classification;
|
|
255
|
+
if (this.reviewType) obj.review_type = this.reviewType;
|
|
256
|
+
if (this.domain) obj.domain = this.domain;
|
|
257
|
+
if (this.release) obj.release = this.release;
|
|
258
|
+
if (this.lineageSourceSimilarity != null) {
|
|
259
|
+
obj.lineage_source_similarity = this.lineageSourceSimilarity;
|
|
260
|
+
}
|
|
261
|
+
if (this.reviewDate) obj.review_date = this.reviewDate;
|
|
262
|
+
if (this.reviewDecisionDate) obj.review_decision_date = this.reviewDecisionDate;
|
|
263
|
+
if (this.reviewDecisionEvent) obj.review_decision_event = this.reviewDecisionEvent;
|
|
264
|
+
if (this.reviewStatus) obj.review_status = this.reviewStatus;
|
|
265
|
+
if (this.reviewDecision) obj.review_decision = this.reviewDecision;
|
|
266
|
+
if (this.reviewDecisionNotes) obj.review_decision_notes = this.reviewDecisionNotes;
|
|
267
|
+
|
|
268
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'terms', '_terms', '_rawTerms');
|
|
269
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'definition', '_definitions', '_rawDefinition');
|
|
270
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'notes', '_notes', '_rawNotes');
|
|
271
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'annotations', '_annotations', '_rawAnnotations');
|
|
272
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'examples', '_examples', '_rawExamples');
|
|
273
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'sources', '_sources', '_rawSources');
|
|
274
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'dates', '_dates', '_rawDates');
|
|
275
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'non_verbal_rep', '_nonVerbal', '_rawNonVerbal');
|
|
276
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'related', '_related', '_rawRelated');
|
|
277
|
+
|
|
278
|
+
return obj;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
static override fromJSON(data: LocalizedConceptJson): LocalizedConcept {
|
|
282
|
+
return new LocalizedConcept(data);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -4,43 +4,52 @@
|
|
|
4
4
|
// dispatches each file to the appropriate class (PartitiveHyperedge,
|
|
5
5
|
// GenericHyperedge, future leaves) based on its `type` field, via
|
|
6
6
|
// HyperedgeRegistry.
|
|
7
|
-
//
|
|
8
|
-
// The wire-shape contract is owned by the JSON Schema at
|
|
9
|
-
// `schemas/v3/relation.yaml` (concept-model repo) — this loader
|
|
10
|
-
// produces the corresponding JS model instances. See
|
|
11
|
-
// docs/design/relations-as-files.md (concept-model).
|
|
12
|
-
//
|
|
13
|
-
// Adding a new hyperedge type means: declare the leaf class with the
|
|
14
|
-
// metadata block, register it. The loader picks it up automatically —
|
|
15
|
-
// no edits here.
|
|
16
7
|
|
|
17
8
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import type { Dirent } from 'node:fs';
|
|
18
10
|
import * as yaml from 'js-yaml';
|
|
19
11
|
import { HyperedgeRegistry } from './hyperedge-registry.js';
|
|
12
|
+
import type { HyperedgeClass } from './hyperedge-registry.js';
|
|
13
|
+
import type { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
15
|
+
interface HyperedgeDocument {
|
|
16
|
+
type: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface HyperedgeClassWithFromJson extends HyperedgeClass {
|
|
21
|
+
fromJSON(data: unknown): AbstractHyperedge;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Backward-compat: snapshot of registered classes keyed by typeTag.
|
|
26
|
+
* Prefer HyperedgeRegistry.forTypeTag() for new code — this snapshot
|
|
27
|
+
* is frozen at module load and won't pick up runtime registrations
|
|
28
|
+
* from tests (the OCP test in Phase 11 registers a mock type after
|
|
29
|
+
* import).
|
|
30
|
+
*/
|
|
31
|
+
export const TYPE_TO_CLASS = new Proxy(
|
|
32
|
+
{} as Record<string, HyperedgeClass>,
|
|
33
|
+
{
|
|
34
|
+
get(_target: unknown, prop: string | symbol): HyperedgeClass | null {
|
|
35
|
+
return typeof prop === 'string' ? HyperedgeRegistry.forTypeTag(prop) : null;
|
|
36
|
+
},
|
|
37
|
+
ownKeys(): string[] {
|
|
38
|
+
return HyperedgeRegistry.allTypeTags();
|
|
39
|
+
},
|
|
40
|
+
getOwnPropertyDescriptor(): PropertyDescriptor {
|
|
41
|
+
return { enumerable: true, configurable: true };
|
|
42
|
+
},
|
|
35
43
|
},
|
|
36
|
-
|
|
44
|
+
);
|
|
37
45
|
|
|
38
46
|
/**
|
|
39
47
|
* Thrown by `loadAll` when a relation file has an unknown `type`,
|
|
40
48
|
* malformed structure, or a class that fails to instantiate.
|
|
41
49
|
*/
|
|
42
50
|
export class RelationLoadError extends Error {
|
|
43
|
-
|
|
51
|
+
readonly path: string;
|
|
52
|
+
constructor(path: string, message: string) {
|
|
44
53
|
super(`relation-loader: ${path}: ${message}`);
|
|
45
54
|
this.path = path;
|
|
46
55
|
}
|
|
@@ -51,32 +60,27 @@ export class RelationLoadError extends Error {
|
|
|
51
60
|
* returning a Map keyed by comprehensive-id.
|
|
52
61
|
*
|
|
53
62
|
* Returns an empty Map if `relationsDir` doesn't exist or is empty.
|
|
54
|
-
*
|
|
55
|
-
* @param {string} relationsDir — path to a `relations/` directory
|
|
56
|
-
* containing `<comprehensive-id>/<slug>.yaml` files (per the
|
|
57
|
-
* schema in schemas/v3/relation.yaml).
|
|
58
|
-
* @returns {Map<string, Array<AbstractHyperedge>>} — for each
|
|
59
|
-
* comprehensive id, the list of relations that have it as their
|
|
60
|
-
* comprehensive.
|
|
61
63
|
*/
|
|
62
|
-
export function loadAll(
|
|
63
|
-
|
|
64
|
+
export function loadAll(
|
|
65
|
+
relationsDir: string | null | undefined,
|
|
66
|
+
): Map<string, AbstractHyperedge[]> {
|
|
67
|
+
const result = new Map<string, AbstractHyperedge[]>();
|
|
64
68
|
if (!relationsDir) return result;
|
|
65
69
|
if (!existsSync(relationsDir)) return result;
|
|
66
70
|
|
|
67
71
|
const entries = readdirSync(relationsDir, { withFileTypes: true });
|
|
68
|
-
for (const compEntry of entries) {
|
|
72
|
+
for (const compEntry of entries as Dirent[]) {
|
|
69
73
|
if (!compEntry.isDirectory()) continue;
|
|
70
74
|
const compId = compEntry.name;
|
|
71
75
|
const compDir = `${relationsDir}/${compId}`;
|
|
72
76
|
if (!result.has(compId)) result.set(compId, []);
|
|
73
77
|
const files = readdirSync(compDir)
|
|
74
|
-
.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
|
|
78
|
+
.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'))
|
|
75
79
|
.sort();
|
|
76
80
|
for (const file of files) {
|
|
77
81
|
const path = `${compDir}/${file}`;
|
|
78
82
|
const rel = loadOne(path);
|
|
79
|
-
if (rel) result.get(compId)
|
|
83
|
+
if (rel) result.get(compId)?.push(rel);
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
return result;
|
|
@@ -93,19 +97,19 @@ export const RelationLoader = Object.freeze({ loadAll, loadOne });
|
|
|
93
97
|
/**
|
|
94
98
|
* Load a single relation file. Returns null if the file is missing,
|
|
95
99
|
* unreadable, or doesn't have a recognized `type` field. Throws
|
|
96
|
-
* `RelationLoadError` on malformed content
|
|
97
|
-
* count, wrong comprehensive shape, YAML parse error).
|
|
100
|
+
* `RelationLoadError` on malformed content.
|
|
98
101
|
*/
|
|
99
|
-
export function loadOne(path) {
|
|
100
|
-
let doc;
|
|
102
|
+
export function loadOne(path: string): AbstractHyperedge | null {
|
|
103
|
+
let doc: unknown;
|
|
101
104
|
try {
|
|
102
105
|
doc = yaml.load(readFileSync(path, 'utf8'));
|
|
103
106
|
} catch (e) {
|
|
104
|
-
throw new RelationLoadError(path, `YAML parse failed: ${e.message}`);
|
|
107
|
+
throw new RelationLoadError(path, `YAML parse failed: ${(e as Error).message}`);
|
|
105
108
|
}
|
|
106
109
|
if (!doc || typeof doc !== 'object') return null;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
const d = doc as HyperedgeDocument;
|
|
111
|
+
if (!d.type) return null;
|
|
112
|
+
const Cls = HyperedgeRegistry.forTypeTag(d.type) as HyperedgeClassWithFromJson | null;
|
|
113
|
+
if (!Cls) return null;
|
|
114
|
+
return Cls.fromJSON(d);
|
|
111
115
|
}
|
package/src/diff/identity.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// Shared identity helper for the diff layer.
|
|
2
|
-
//
|
|
3
|
-
// Per invariant N1 (TODO.hyperedges-v2/00-master-plan.md), identity is a
|
|
4
|
-
// model concern. Models expose `identity()` returning a stable string.
|
|
5
|
-
// This helper dispatches: if the value is a model with `identity()`,
|
|
6
|
-
// call it; otherwise fall back to canonical JSON of `toJSON()`.
|
|
7
|
-
//
|
|
8
|
-
// Both `concept-diff.js` (compute side) and `diff-patch.js` (apply side)
|
|
9
|
-
// import this — closing the duplication that previously allowed the two
|
|
10
|
-
// sides to drift.
|
|
11
|
-
|
|
12
|
-
import { canonicalJson } from './canonical-json.js';
|
|
13
|
-
|
|
14
|
-
export function identityOf(item) {
|
|
15
|
-
if (item == null) return '';
|
|
16
|
-
if (typeof item.identity === 'function') return item.identity();
|
|
17
|
-
if (typeof item === 'string') return item;
|
|
18
|
-
if (typeof item === 'number') return String(item);
|
|
19
|
-
if (typeof item === 'boolean') return String(item);
|
|
20
|
-
if (typeof item.toJSON === 'function') return canonicalJson(item.toJSON());
|
|
21
|
-
return canonicalJson(item);
|
|
22
|
-
}
|
package/src/diff/text-diff.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { GlossaristModel } from '../models/base.js';
|
|
2
|
-
|
|
3
|
-
const HUNK_EQUAL = 'equal';
|
|
4
|
-
const HUNK_ADDED = 'added';
|
|
5
|
-
const HUNK_REMOVED = 'removed';
|
|
6
|
-
|
|
7
|
-
const TOKEN_RE = /(\s+)|(\S+)/g;
|
|
8
|
-
|
|
9
|
-
export class TextHunk extends GlossaristModel {
|
|
10
|
-
constructor(data = {}) {
|
|
11
|
-
super();
|
|
12
|
-
if (![HUNK_EQUAL, HUNK_ADDED, HUNK_REMOVED].includes(data.type)) {
|
|
13
|
-
throw new Error(`TextHunk type must be 'equal', 'added', or 'removed' (got ${data.type})`);
|
|
14
|
-
}
|
|
15
|
-
this.type = data.type;
|
|
16
|
-
this.text = data.text ?? '';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
toJSON() {
|
|
20
|
-
return { type: this.type, text: this.text };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
static fromJSON(data) {
|
|
24
|
-
return new TextHunk(data);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class TextDiff extends GlossaristModel {
|
|
29
|
-
constructor(data = {}) {
|
|
30
|
-
super();
|
|
31
|
-
this.oldText = data.oldText ?? data.old_text ?? '';
|
|
32
|
-
this.newText = data.newText ?? data.new_text ?? '';
|
|
33
|
-
this._hunks = (data.hunks ?? []).map(h => h instanceof TextHunk ? h : new TextHunk(h));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
get hunks() {
|
|
37
|
-
return this._hunks;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
get hasChanges() {
|
|
41
|
-
return this._hunks.some(h => h.type !== HUNK_EQUAL);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
get addedText() {
|
|
45
|
-
return this._hunks.filter(h => h.type === HUNK_ADDED).map(h => h.text).join('');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
get removedText() {
|
|
49
|
-
return this._hunks.filter(h => h.type === HUNK_REMOVED).map(h => h.text).join('');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
toJSON() {
|
|
53
|
-
return {
|
|
54
|
-
old_text: this.oldText,
|
|
55
|
-
new_text: this.newText,
|
|
56
|
-
hunks: this._hunks.map(h => h.toJSON()),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
static fromJSON(data) {
|
|
61
|
-
return new TextDiff(data);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function diffText(oldText, newText) {
|
|
66
|
-
const oldStr = String(oldText ?? '');
|
|
67
|
-
const newStr = String(newText ?? '');
|
|
68
|
-
const oldTokens = tokenize(oldStr);
|
|
69
|
-
const newTokens = tokenize(newStr);
|
|
70
|
-
const hunks = lcsHunks(oldTokens, newTokens);
|
|
71
|
-
return new TextDiff({ oldText: oldStr, newText: newStr, hunks });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function tokenize(text) {
|
|
75
|
-
if (text.length === 0) return [];
|
|
76
|
-
return text.match(TOKEN_RE) ?? [];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function lcsHunks(a, b) {
|
|
80
|
-
const n = a.length;
|
|
81
|
-
const m = b.length;
|
|
82
|
-
|
|
83
|
-
if (n === 0) return m === 0 ? [] : [new TextHunk({ type: HUNK_ADDED, text: b.join('') })];
|
|
84
|
-
if (m === 0) return [new TextHunk({ type: HUNK_REMOVED, text: a.join('') })];
|
|
85
|
-
|
|
86
|
-
const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
|
|
87
|
-
for (let i = 1; i <= n; i++) {
|
|
88
|
-
for (let j = 1; j <= m; j++) {
|
|
89
|
-
dp[i][j] = a[i - 1] === b[j - 1]
|
|
90
|
-
? dp[i - 1][j - 1] + 1
|
|
91
|
-
: Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const raw = [];
|
|
96
|
-
let i = n;
|
|
97
|
-
let j = m;
|
|
98
|
-
while (i > 0 && j > 0) {
|
|
99
|
-
if (a[i - 1] === b[j - 1]) {
|
|
100
|
-
raw.push({ type: HUNK_EQUAL, text: a[i - 1] });
|
|
101
|
-
i--; j--;
|
|
102
|
-
} else if (dp[i - 1][j] >= dp[i][j - 1]) {
|
|
103
|
-
raw.push({ type: HUNK_REMOVED, text: a[i - 1] });
|
|
104
|
-
i--;
|
|
105
|
-
} else {
|
|
106
|
-
raw.push({ type: HUNK_ADDED, text: b[j - 1] });
|
|
107
|
-
j--;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
while (i > 0) { raw.push({ type: HUNK_REMOVED, text: a[i - 1] }); i--; }
|
|
111
|
-
while (j > 0) { raw.push({ type: HUNK_ADDED, text: b[j - 1] }); j--; }
|
|
112
|
-
raw.reverse();
|
|
113
|
-
|
|
114
|
-
return coalesce(raw).map(h => new TextHunk(h));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function coalesce(raw) {
|
|
118
|
-
const out = [];
|
|
119
|
-
for (const r of raw) {
|
|
120
|
-
const last = out[out.length - 1];
|
|
121
|
-
if (last && last.type === r.type) {
|
|
122
|
-
last.text += r.text;
|
|
123
|
-
} else {
|
|
124
|
-
out.push({ type: r.type, text: r.text });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return out;
|
|
128
|
-
}
|