glossarist 0.4.42 → 0.4.44
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/{compiled-format.js → compiled-format.ts} +1 -0
- package/src/{concept-collection.js → concept-collection.ts} +1 -0
- package/src/{concept-diff.js → concept-diff.ts} +1 -0
- package/src/{concept-parser.js → concept-parser.ts} +1 -0
- package/src/{concept-reader.js → concept-reader.ts} +1 -0
- package/src/{concept-serializer.js → concept-serializer.ts} +1 -0
- package/src/{concept-writer.js → concept-writer.ts} +1 -0
- package/src/{dataset-asset.js → dataset-asset.ts} +1 -0
- package/src/diff/change.ts +175 -0
- package/src/diff/{collection-diff.js → collection-diff.ts} +1 -0
- package/src/diff/{concept-diff.js → concept-diff.ts} +1 -0
- package/src/diff/{diff-patch.js → diff-patch.ts} +1 -0
- package/src/diff/{diff-renderer.js → diff-renderer.ts} +1 -0
- package/src/diff/{index.js → index.ts} +1 -0
- package/src/diff/list-diff.ts +231 -0
- package/src/{entity-directory.js → entity-directory.ts} +1 -0
- package/src/errors.ts +24 -0
- package/src/{gcr-reader.js → gcr-reader.ts} +1 -0
- package/src/{gcr-writer.js → gcr-writer.ts} +1 -0
- package/src/{index.js → index.ts} +1 -0
- package/src/{managed-concept-collection.js → managed-concept-collection.ts} +1 -0
- package/src/migration/hyperedge-migrator.ts +104 -0
- package/src/models/concept.ts +1 -1
- package/src/rdf/{agents-emitter.js → agents-emitter.ts} +1 -0
- package/src/rdf/{bibliography-emitter.js → bibliography-emitter.ts} +1 -0
- package/src/rdf/{bnode-id.js → bnode-id.ts} +1 -0
- package/src/rdf/{build-activity-emitter.js → build-activity-emitter.ts} +1 -0
- package/src/rdf/{curie.js → curie.ts} +1 -0
- package/src/rdf/{dataset-emitter.js → dataset-emitter.ts} +1 -0
- package/src/rdf/{deterministic-id.js → deterministic-id.ts} +1 -0
- package/src/rdf/{document-writer.js → document-writer.ts} +1 -0
- package/src/rdf/{gloss-concept.js → gloss-concept.ts} +1 -0
- package/src/rdf/{gloss-designation.js → gloss-designation.ts} +1 -0
- package/src/rdf/{gloss-detailed-definition.js → gloss-detailed-definition.ts} +1 -0
- package/src/rdf/{gloss-generic-hyperedge.js → gloss-generic-hyperedge.ts} +1 -0
- package/src/rdf/{gloss-localized-concept.js → gloss-localized-concept.ts} +1 -0
- package/src/rdf/{gloss-non-verbal-rep.js → gloss-non-verbal-rep.ts} +1 -0
- package/src/rdf/{gloss-partitive-member.js → gloss-partitive-member.ts} +1 -0
- package/src/rdf/{gloss-partitive-relation.js → gloss-partitive-relation.ts} +1 -0
- package/src/rdf/{gloss-source.js → gloss-source.ts} +1 -0
- package/src/rdf/{group-emitter.js → group-emitter.ts} +1 -0
- package/src/rdf/{image-variant-emitter.js → image-variant-emitter.ts} +1 -0
- package/src/rdf/{index.js → index.ts} +1 -0
- package/src/rdf/{normalize-enum.js → normalize-enum.ts} +1 -0
- package/src/rdf/{prefixes.js → prefixes.ts} +1 -0
- package/src/rdf/{provenance-emitter.js → provenance-emitter.ts} +1 -0
- package/src/rdf/{sections-builder.js → sections-builder.ts} +1 -0
- package/src/rdf/{shacl-entry.js → shacl-entry.ts} +1 -0
- package/src/rdf/{shacl.js → shacl.ts} +1 -0
- package/src/rdf/{terms.js → terms.ts} +1 -0
- package/src/rdf/{version-emitter.js → version-emitter.ts} +1 -0
- package/src/rdf/{vocabulary-emitter.js → vocabulary-emitter.ts} +1 -0
- package/src/rdf/{write-turtle-sync.js → write-turtle-sync.ts} +1 -0
- package/src/{reference-mention.js → reference-mention.ts} +1 -0
- package/src/{reference-resolver.js → reference-resolver.ts} +1 -0
- package/src/{render-classification.js → render-classification.ts} +1 -0
- package/src/{sort.js → sort.ts} +1 -4
- package/src/transforms/concept-to-gloss.transform.ts +81 -0
- package/src/{uuid.js → uuid.ts} +11 -7
- package/src/{v1-reader.js → v1-reader.ts} +1 -0
- package/src/validators/{asset-index.js → asset-index.ts} +1 -0
- package/src/validators/{binary-has-part-redundancy-rule.js → binary-has-part-redundancy-rule.ts} +1 -0
- package/src/validators/{concept-validator.js → concept-validator.ts} +1 -0
- package/src/validators/{delimiting-coherence-rule.js → delimiting-coherence-rule.ts} +1 -0
- package/src/validators/{external-concept-shape-rule.js → external-concept-shape-rule.ts} +1 -0
- package/src/validators/{gcr-validator.js → gcr-validator.ts} +1 -0
- package/src/validators/{hyperedge-coherence-rule.js → hyperedge-coherence-rule.ts} +1 -0
- package/src/validators/{index.js → index.ts} +1 -0
- package/src/validators/{partitive-relation-coherence-rule.js → partitive-relation-coherence-rule.ts} +1 -0
- package/src/validators/{ref-keys.js → ref-keys.ts} +1 -0
- package/src/validators/{register-validator.js → register-validator.ts} +1 -0
- package/src/validators/{relationship-type-rule.js → relationship-type-rule.ts} +1 -0
- package/src/validators/{sourced-from-locality-rule.js → sourced-from-locality-rule.ts} +1 -0
- package/src/validators/{v3-rules.js → v3-rules.ts} +1 -0
- package/src/validators/{validation-error.js → validation-error.ts} +1 -0
- package/src/validators/{validation-result.js → validation-result.ts} +1 -0
- package/src/validators/{validation-rule.js → validation-rule.ts} +1 -0
- package/src/diff/change.js +0 -139
- package/src/diff/list-diff.js +0 -227
- package/src/errors.js +0 -42
- package/src/migration/hyperedge-migrator.js +0 -119
- package/src/transforms/concept-to-gloss.transform.js +0 -77
- /package/src/rdf/{predicates.js → predicates.ts} +0 -0
- /package/src/transforms/{index.js → index.ts} +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { GlossaristModel } from '../models/base.js';
|
|
2
|
+
import { TextDiff } from './text-diff.js';
|
|
3
|
+
import type { TextDiffJson } from './text-diff.js';
|
|
4
|
+
|
|
5
|
+
export const CHANGE_ADDED = 'added';
|
|
6
|
+
export const CHANGE_REMOVED = 'removed';
|
|
7
|
+
export const CHANGE_CHANGED = 'changed';
|
|
8
|
+
export const CHANGE_MATCHED = 'matched';
|
|
9
|
+
|
|
10
|
+
export type ChangeType = 'added' | 'removed' | 'changed' | 'matched';
|
|
11
|
+
|
|
12
|
+
export interface ChangeJson {
|
|
13
|
+
type: ChangeType;
|
|
14
|
+
path?: string | null;
|
|
15
|
+
value?: unknown;
|
|
16
|
+
old_value?: unknown;
|
|
17
|
+
oldValue?: unknown;
|
|
18
|
+
new_value?: unknown;
|
|
19
|
+
newValue?: unknown;
|
|
20
|
+
text_diff?: TextDiffJson;
|
|
21
|
+
textDiff?: TextDiffJson;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function serializeValue(v: unknown): unknown {
|
|
25
|
+
if (v == null) return null;
|
|
26
|
+
if (v instanceof GlossaristModel) return v.toJSON();
|
|
27
|
+
if (Array.isArray(v)) return v.map(serializeValue);
|
|
28
|
+
if (typeof v === 'object') {
|
|
29
|
+
const out: Record<string, unknown> = {};
|
|
30
|
+
for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
|
|
31
|
+
out[k] = serializeValue(val);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
return v;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class Change extends GlossaristModel {
|
|
39
|
+
readonly path: string | null;
|
|
40
|
+
|
|
41
|
+
constructor(data: Pick<ChangeJson, 'path'> = {}) {
|
|
42
|
+
super();
|
|
43
|
+
this.path = data.path ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get type(): ChangeType {
|
|
47
|
+
throw new Error(`${this.constructor.name} must override type getter`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static override fromJSON(data: ChangeJson): Change {
|
|
51
|
+
return deserializeChange(data);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class Added extends Change {
|
|
56
|
+
readonly value: unknown;
|
|
57
|
+
|
|
58
|
+
constructor(data: Pick<ChangeJson, 'path' | 'value'> = {}) {
|
|
59
|
+
super(data);
|
|
60
|
+
this.value = data.value ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override get type(): ChangeType { return CHANGE_ADDED; }
|
|
64
|
+
|
|
65
|
+
override toJSON(): ChangeJson {
|
|
66
|
+
const obj: ChangeJson = { type: CHANGE_ADDED };
|
|
67
|
+
if (this.path != null) obj.path = this.path;
|
|
68
|
+
obj.value = serializeValue(this.value);
|
|
69
|
+
return obj;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static override fromJSON(data: ChangeJson): Added {
|
|
73
|
+
return new Added(data as Pick<ChangeJson, 'path' | 'value'>);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class Removed extends Change {
|
|
78
|
+
readonly value: unknown;
|
|
79
|
+
|
|
80
|
+
constructor(data: Pick<ChangeJson, 'path' | 'value'> = {}) {
|
|
81
|
+
super(data);
|
|
82
|
+
this.value = data.value ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
override get type(): ChangeType { return CHANGE_REMOVED; }
|
|
86
|
+
|
|
87
|
+
override toJSON(): ChangeJson {
|
|
88
|
+
const obj: ChangeJson = { type: CHANGE_REMOVED };
|
|
89
|
+
if (this.path != null) obj.path = this.path;
|
|
90
|
+
obj.value = serializeValue(this.value);
|
|
91
|
+
return obj;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static override fromJSON(data: ChangeJson): Removed {
|
|
95
|
+
return new Removed(data as Pick<ChangeJson, 'path' | 'value'>);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ChangedConstructorData {
|
|
100
|
+
path?: string | null;
|
|
101
|
+
oldValue?: unknown;
|
|
102
|
+
old_value?: unknown;
|
|
103
|
+
newValue?: unknown;
|
|
104
|
+
new_value?: unknown;
|
|
105
|
+
textDiff?: TextDiff | TextDiffJson;
|
|
106
|
+
text_diff?: TextDiffJson;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class Changed extends Change {
|
|
110
|
+
readonly oldValue: unknown;
|
|
111
|
+
readonly newValue: unknown;
|
|
112
|
+
private readonly _textDiff: TextDiff | null;
|
|
113
|
+
|
|
114
|
+
constructor(data: ChangedConstructorData = {}) {
|
|
115
|
+
super(data as Pick<ChangeJson, 'path'>);
|
|
116
|
+
this.oldValue = data.oldValue ?? data.old_value ?? null;
|
|
117
|
+
this.newValue = data.newValue ?? data.new_value ?? null;
|
|
118
|
+
const td = data.textDiff ?? data.text_diff;
|
|
119
|
+
this._textDiff = td instanceof TextDiff ? td : td ? TextDiff.fromJSON(td) : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
override get type(): ChangeType { return CHANGE_CHANGED; }
|
|
123
|
+
|
|
124
|
+
get textDiff(): TextDiff | null {
|
|
125
|
+
return this._textDiff;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
override toJSON(): ChangeJson {
|
|
129
|
+
const obj: ChangeJson = { type: CHANGE_CHANGED };
|
|
130
|
+
if (this.path != null) obj.path = this.path;
|
|
131
|
+
obj.old_value = serializeValue(this.oldValue);
|
|
132
|
+
obj.new_value = serializeValue(this.newValue);
|
|
133
|
+
if (this._textDiff) obj.text_diff = this._textDiff.toJSON() as TextDiffJson;
|
|
134
|
+
return obj;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static override fromJSON(data: ChangeJson): Changed {
|
|
138
|
+
return new Changed(data as Required<Pick<ChangeJson, never>>);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Matched is for set-membership records that exist in both sides of a
|
|
143
|
+
// comparison but have no directionality — used by ConceptCollectionDiff.
|
|
144
|
+
export class Matched extends Change {
|
|
145
|
+
readonly value: unknown;
|
|
146
|
+
|
|
147
|
+
constructor(data: Pick<ChangeJson, 'path' | 'value'> = {}) {
|
|
148
|
+
super(data);
|
|
149
|
+
this.value = data.value ?? null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
override get type(): ChangeType { return CHANGE_MATCHED; }
|
|
153
|
+
|
|
154
|
+
override toJSON(): ChangeJson {
|
|
155
|
+
const obj: ChangeJson = { type: CHANGE_MATCHED };
|
|
156
|
+
if (this.path != null) obj.path = this.path;
|
|
157
|
+
obj.value = serializeValue(this.value);
|
|
158
|
+
return obj;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static override fromJSON(data: ChangeJson): Matched {
|
|
162
|
+
return new Matched(data as Pick<ChangeJson, 'path' | 'value'>);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function deserializeChange(data: ChangeJson): Change {
|
|
167
|
+
switch (data?.type) {
|
|
168
|
+
case CHANGE_ADDED: return Added.fromJSON(data);
|
|
169
|
+
case CHANGE_REMOVED: return Removed.fromJSON(data);
|
|
170
|
+
case CHANGE_CHANGED: return Changed.fromJSON(data);
|
|
171
|
+
case CHANGE_MATCHED: return Matched.fromJSON(data);
|
|
172
|
+
default:
|
|
173
|
+
throw new Error(`Unknown change type: ${(data as { type?: string })?.type}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck — TEMPORARY bridge during TS migration. TODO(Phase 2e): remove this directive and type all parameters.
|
|
1
2
|
import { GlossaristModel } from '../models/base.js';
|
|
2
3
|
import { Added, Removed, Matched, deserializeChange } from './change.js';
|
|
3
4
|
import { ConceptDiff, DiffStats, diffConcepts } from './concept-diff.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck — TEMPORARY bridge during TS migration. TODO(Phase 2e): remove this directive and type all parameters.
|
|
1
2
|
import { GlossaristModel } from '../models/base.js';
|
|
2
3
|
import { PartitiveHyperedge } from '../models/partitive-hyperedge.js';
|
|
3
4
|
import { GenericHyperedge } from '../models/generic-hyperedge.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck — TEMPORARY bridge during TS migration. TODO(Phase 2e): remove this directive and type all parameters.
|
|
1
2
|
import { Concept } from '../models/concept.js';
|
|
2
3
|
import { ConceptSource } from '../models/concept-source.js';
|
|
3
4
|
import { ConceptDate } from '../models/concept-date.js';
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { GlossaristModel } from '../models/base.js';
|
|
2
|
+
import { Added, Removed, Changed, deserializeChange } from './change.js';
|
|
3
|
+
import type { ChangeJson, ChangedConstructorData } from './change.js';
|
|
4
|
+
import { diffText } from './text-diff.js';
|
|
5
|
+
import { identityOf } from './identity.js';
|
|
6
|
+
|
|
7
|
+
export interface ListDiffJson {
|
|
8
|
+
added?: ReadonlyArray<ChangeJson | Added>;
|
|
9
|
+
removed?: ReadonlyArray<ChangeJson | Removed>;
|
|
10
|
+
changed?: ReadonlyArray<ChangeJson | Changed>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ListDiffOptions {
|
|
14
|
+
identityKey?: (item: unknown) => string;
|
|
15
|
+
textKey?: ((item: unknown) => string | null) | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ListDiff extends GlossaristModel {
|
|
19
|
+
private readonly _added: ReadonlyArray<Added>;
|
|
20
|
+
private readonly _removed: ReadonlyArray<Removed>;
|
|
21
|
+
private readonly _changed: ReadonlyArray<Changed>;
|
|
22
|
+
|
|
23
|
+
constructor(data: ListDiffJson = {}) {
|
|
24
|
+
super();
|
|
25
|
+
this._added = (data.added ?? []).map((c) =>
|
|
26
|
+
c instanceof Added ? c : Added.fromJSON(c),
|
|
27
|
+
) as ReadonlyArray<Added>;
|
|
28
|
+
this._removed = (data.removed ?? []).map((c) =>
|
|
29
|
+
c instanceof Removed ? c : Removed.fromJSON(c),
|
|
30
|
+
) as ReadonlyArray<Removed>;
|
|
31
|
+
this._changed = (data.changed ?? []).map((c) => {
|
|
32
|
+
if (c instanceof Changed) return c;
|
|
33
|
+
return deserializeChange(c) as Changed;
|
|
34
|
+
}) as ReadonlyArray<Changed>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get added(): ReadonlyArray<Added> { return this._added; }
|
|
38
|
+
get removed(): ReadonlyArray<Removed> { return this._removed; }
|
|
39
|
+
get changed(): ReadonlyArray<Changed> { return this._changed; }
|
|
40
|
+
|
|
41
|
+
get hasChanges(): boolean {
|
|
42
|
+
return this._added.length > 0 || this._removed.length > 0 || this._changed.length > 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get count(): number {
|
|
46
|
+
return this._added.length + this._removed.length + this._changed.length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
*entries(): Generator<Added | Removed | Changed> {
|
|
50
|
+
yield* this._added;
|
|
51
|
+
yield* this._removed;
|
|
52
|
+
yield* this._changed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override toJSON(): ListDiffJson {
|
|
56
|
+
return {
|
|
57
|
+
added: this._added.map((c) => c.toJSON()),
|
|
58
|
+
removed: this._removed.map((c) => c.toJSON()),
|
|
59
|
+
changed: this._changed.map((c) => c.toJSON()),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static override fromJSON(data: ListDiffJson): ListDiff {
|
|
64
|
+
return new ListDiff(data);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Diff two ordered lists using LCS alignment.
|
|
70
|
+
*/
|
|
71
|
+
export function diffList(
|
|
72
|
+
oldList: ReadonlyArray<unknown> = [],
|
|
73
|
+
newList: ReadonlyArray<unknown> = [],
|
|
74
|
+
options: ListDiffOptions = {},
|
|
75
|
+
): ListDiff {
|
|
76
|
+
const identityKey = options.identityKey ?? defaultIdentityKey;
|
|
77
|
+
const textKey = options.textKey ?? null;
|
|
78
|
+
|
|
79
|
+
const oldArr = [...oldList];
|
|
80
|
+
const newArr = [...newList];
|
|
81
|
+
|
|
82
|
+
const oldKeys = oldArr.map(identityKey);
|
|
83
|
+
const newKeys = newArr.map(identityKey);
|
|
84
|
+
|
|
85
|
+
const dp = lcsTable(oldKeys, newKeys);
|
|
86
|
+
const ops = backtrackOps(dp, oldKeys, newKeys);
|
|
87
|
+
|
|
88
|
+
const added: Added[] = [];
|
|
89
|
+
const removed: Removed[] = [];
|
|
90
|
+
const changed: Changed[] = [];
|
|
91
|
+
|
|
92
|
+
for (let k = 0; k < ops.length; k++) {
|
|
93
|
+
const op = ops[k]!;
|
|
94
|
+
if (op.type === 'equal') continue;
|
|
95
|
+
|
|
96
|
+
if (op.type === 'remove' && k + 1 < ops.length && ops[k + 1]!.type === 'add') {
|
|
97
|
+
const next = ops[k + 1]!;
|
|
98
|
+
changed.push(buildChanged(oldArr[op.i]!, newArr[next.j]!, textKey));
|
|
99
|
+
k++;
|
|
100
|
+
} else if (op.type === 'add' && k + 1 < ops.length && ops[k + 1]!.type === 'remove') {
|
|
101
|
+
const next = ops[k + 1]!;
|
|
102
|
+
changed.push(buildChanged(oldArr[next.i]!, newArr[op.j]!, textKey));
|
|
103
|
+
k++;
|
|
104
|
+
} else if (op.type === 'remove') {
|
|
105
|
+
removed.push(new Removed({ value: oldArr[op.i] }));
|
|
106
|
+
} else if (op.type === 'add') {
|
|
107
|
+
added.push(new Added({ value: newArr[op.j] }));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return new ListDiff({ added, removed, changed });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Diff two unordered collections by identity key (set-based).
|
|
116
|
+
*/
|
|
117
|
+
export function diffSet(
|
|
118
|
+
oldList: ReadonlyArray<unknown> = [],
|
|
119
|
+
newList: ReadonlyArray<unknown> = [],
|
|
120
|
+
options: ListDiffOptions = {},
|
|
121
|
+
): ListDiff {
|
|
122
|
+
const identityKey = options.identityKey ?? defaultIdentityKey;
|
|
123
|
+
const textKey = options.textKey ?? null;
|
|
124
|
+
|
|
125
|
+
const oldMap = new Map<string, unknown>();
|
|
126
|
+
for (const item of oldList) {
|
|
127
|
+
oldMap.set(identityKey(item), item);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const seenKeys = new Set<string>();
|
|
131
|
+
const added: Added[] = [];
|
|
132
|
+
const changed: Changed[] = [];
|
|
133
|
+
|
|
134
|
+
for (const item of newList) {
|
|
135
|
+
const key = identityKey(item);
|
|
136
|
+
seenKeys.add(key);
|
|
137
|
+
if (oldMap.has(key)) {
|
|
138
|
+
const oldItem = oldMap.get(key);
|
|
139
|
+
if (oldItem !== undefined && !itemsEqual(oldItem, item)) {
|
|
140
|
+
changed.push(buildChanged(oldItem, item, textKey));
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
added.push(new Added({ value: item }));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const removed: Removed[] = [];
|
|
148
|
+
for (const item of oldList) {
|
|
149
|
+
if (!seenKeys.has(identityKey(item))) {
|
|
150
|
+
removed.push(new Removed({ value: item }));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return new ListDiff({ added, removed, changed });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildChanged(
|
|
158
|
+
oldItem: unknown,
|
|
159
|
+
newItem: unknown,
|
|
160
|
+
textKey: ((item: unknown) => string | null) | null,
|
|
161
|
+
): Changed {
|
|
162
|
+
const opts: ChangedConstructorData = {
|
|
163
|
+
oldValue: oldItem,
|
|
164
|
+
newValue: newItem,
|
|
165
|
+
};
|
|
166
|
+
if (textKey) {
|
|
167
|
+
const oldText = textKey(oldItem);
|
|
168
|
+
const newText = textKey(newItem);
|
|
169
|
+
if (oldText != null || newText != null) {
|
|
170
|
+
opts.textDiff = diffText(oldText ?? '', newText ?? '');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return new Changed(opts);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function itemsEqual(a: unknown, b: unknown): boolean {
|
|
177
|
+
if (a === b) return true;
|
|
178
|
+
if (a instanceof GlossaristModel && b instanceof GlossaristModel) {
|
|
179
|
+
return a.equals(b as never);
|
|
180
|
+
}
|
|
181
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function defaultIdentityKey(item: unknown): string {
|
|
185
|
+
return identityOf(item);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type Op = { type: 'equal' | 'add' | 'remove'; i: number; j: number };
|
|
189
|
+
|
|
190
|
+
function lcsTable(aKeys: string[], bKeys: string[]): Uint32Array[] {
|
|
191
|
+
const m = aKeys.length;
|
|
192
|
+
const n = bKeys.length;
|
|
193
|
+
const dp = Array.from({ length: m + 1 }, () => new Uint32Array(n + 1));
|
|
194
|
+
for (let i = 1; i <= m; i++) {
|
|
195
|
+
for (let j = 1; j <= n; j++) {
|
|
196
|
+
dp[i]![j]! =
|
|
197
|
+
aKeys[i - 1] === bKeys[j - 1]
|
|
198
|
+
? dp[i - 1]![j - 1]! + 1
|
|
199
|
+
: Math.max(dp[i - 1]![j]!, dp[i]![j - 1]!);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return dp;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function backtrackOps(dp: Uint32Array[], aKeys: string[], bKeys: string[]): Op[] {
|
|
206
|
+
const ops: Op[] = [];
|
|
207
|
+
let i = aKeys.length;
|
|
208
|
+
let j = bKeys.length;
|
|
209
|
+
while (i > 0 && j > 0) {
|
|
210
|
+
if (aKeys[i - 1] === bKeys[j - 1]) {
|
|
211
|
+
ops.push({ type: 'equal', i: i - 1, j: j - 1 });
|
|
212
|
+
i--; j--;
|
|
213
|
+
} else if ((dp[i - 1]![j] ?? 0) >= (dp[i]![j - 1] ?? 0)) {
|
|
214
|
+
ops.push({ type: 'remove', i: i - 1, j });
|
|
215
|
+
i--;
|
|
216
|
+
} else {
|
|
217
|
+
ops.push({ type: 'add', i, j: j - 1 });
|
|
218
|
+
j--;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
while (i > 0) {
|
|
222
|
+
ops.push({ type: 'remove', i: i - 1, j });
|
|
223
|
+
i--;
|
|
224
|
+
}
|
|
225
|
+
while (j > 0) {
|
|
226
|
+
ops.push({ type: 'add', i, j: j - 1 });
|
|
227
|
+
j--;
|
|
228
|
+
}
|
|
229
|
+
ops.reverse();
|
|
230
|
+
return ops;
|
|
231
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Base error for all glossarist errors. */
|
|
2
|
+
export class GlossaristError extends Error {
|
|
3
|
+
constructor(message: string, options?: { cause?: Error }) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = 'GlossaristError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Thrown when a function receives invalid input (null, undefined, wrong type). */
|
|
10
|
+
export class InvalidInputError extends GlossaristError {
|
|
11
|
+
constructor(what: string, expected?: string) {
|
|
12
|
+
const msg = expected ? `${what} (expected ${expected})` : what;
|
|
13
|
+
super(msg);
|
|
14
|
+
this.name = 'InvalidInputError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Thrown when YAML content cannot be parsed. */
|
|
19
|
+
export class YamlParseError extends GlossaristError {
|
|
20
|
+
constructor(context: string, cause: Error) {
|
|
21
|
+
super(`Failed to parse YAML for ${context}: ${cause.message}`, { cause });
|
|
22
|
+
this.name = 'YamlParseError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// v1 → v2 PartitiveHyperedge migration.
|
|
2
|
+
//
|
|
3
|
+
// Pure function: takes a v1 PartitiveHyperedge hash, returns a v2
|
|
4
|
+
// PartitiveHyperedge hash. Idempotent.
|
|
5
|
+
|
|
6
|
+
export interface V1HyperedgeHash {
|
|
7
|
+
comprehensive?: { source?: string; id?: string; text?: string };
|
|
8
|
+
parts?: ReadonlyArray<{ source?: string; id?: string }>;
|
|
9
|
+
enumeration?: string;
|
|
10
|
+
content?: string | null;
|
|
11
|
+
markers?: ReadonlyArray<string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface V2HyperedgeHash {
|
|
15
|
+
comprehensive: { source?: string; id?: string };
|
|
16
|
+
partitives: ReadonlyArray<{
|
|
17
|
+
ref: { source?: string; id?: string };
|
|
18
|
+
presence: string;
|
|
19
|
+
count: string;
|
|
20
|
+
}>;
|
|
21
|
+
completeness: string;
|
|
22
|
+
migrationWarning?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function migrateHyperedgeToRelation(
|
|
26
|
+
v1Hash: V1HyperedgeHash | ReadonlyArray<V1HyperedgeHash> | null,
|
|
27
|
+
): V2HyperedgeHash | ReadonlyArray<V2HyperedgeHash | null> | null {
|
|
28
|
+
if (v1Hash == null) return null;
|
|
29
|
+
if (Array.isArray(v1Hash)) {
|
|
30
|
+
return v1Hash.map(migrateHyperedgeToRelation) as ReadonlyArray<V2HyperedgeHash | null>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const h = v1Hash as V1HyperedgeHash;
|
|
34
|
+
const out: Partial<V2HyperedgeHash> = {
|
|
35
|
+
comprehensive: { ...(h.comprehensive ?? {}) },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const parts = Array.isArray(h.parts) ? h.parts : [];
|
|
39
|
+
out.partitives = parts.map((ref) => ({
|
|
40
|
+
ref: { ...ref },
|
|
41
|
+
presence: 'required',
|
|
42
|
+
count: 'exactly_one',
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
out.completeness = _migrateEnumeration(h.enumeration);
|
|
46
|
+
|
|
47
|
+
if (parts.length < 2) {
|
|
48
|
+
out.migrationWarning =
|
|
49
|
+
`v1 PartitiveHyperedge had ${parts.length} part(s); v2 requires ≥2. ` +
|
|
50
|
+
`Either add more partitives or convert to a binary has_part edge.`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (h.content != null) {
|
|
54
|
+
out.migrationWarning = (out.migrationWarning ?? '') +
|
|
55
|
+
` v1 'content' field was dropped (v2 PartitiveHyperedge carries no prose; ` +
|
|
56
|
+
`move valuable text to the comprehensive concept's notes).`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (Array.isArray(h.markers) && h.markers.length > 0) {
|
|
60
|
+
out.migrationWarning = (out.migrationWarning ?? '') +
|
|
61
|
+
` v1 'markers' field (${h.markers.join(', ')}) was dropped; ` +
|
|
62
|
+
`v2 uses per-member 'multiplicity' + 'is_delimiting'. Reviewer ` +
|
|
63
|
+
`should set these per partitive based on ISO 704:2022 notation.`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return out as V2HyperedgeHash;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function downgradeRelationToHyperedge(
|
|
70
|
+
v2Hash: Partial<V2HyperedgeHash> | ReadonlyArray<Partial<V2HyperedgeHash>> | null,
|
|
71
|
+
): unknown {
|
|
72
|
+
if (v2Hash == null) return null;
|
|
73
|
+
if (Array.isArray(v2Hash)) {
|
|
74
|
+
return v2Hash.map(downgradeRelationToHyperedge);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const v2 = v2Hash as V2HyperedgeHash;
|
|
78
|
+
const out: {
|
|
79
|
+
comprehensive: { source?: string; id?: string };
|
|
80
|
+
parts: Array<{ source?: string; id?: string }>;
|
|
81
|
+
enumeration: string;
|
|
82
|
+
} = {
|
|
83
|
+
comprehensive: { ...(v2.comprehensive ?? {}) },
|
|
84
|
+
parts: [],
|
|
85
|
+
enumeration: '',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
out.parts = (v2.partitives ?? []).map((m) => ({ ...(m.ref ?? {}) }));
|
|
89
|
+
out.enumeration = _downgradeCompleteness(v2.completeness);
|
|
90
|
+
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _migrateEnumeration(value: string | undefined): string {
|
|
95
|
+
if (value === 'open') return 'partial';
|
|
96
|
+
if (value === 'closed') return 'complete';
|
|
97
|
+
if (value == null) return 'complete';
|
|
98
|
+
return 'complete';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _downgradeCompleteness(value: string | undefined): string {
|
|
102
|
+
if (value === 'partial') return 'open';
|
|
103
|
+
return 'closed';
|
|
104
|
+
}
|
package/src/models/concept.ts
CHANGED
|
@@ -467,7 +467,7 @@ function _migrateV1Hash(
|
|
|
467
467
|
hash: Record<string, unknown>,
|
|
468
468
|
): Record<string, unknown> | null {
|
|
469
469
|
if (cls === PartitiveHyperedge && v1Key === 'partitive_hyperedges') {
|
|
470
|
-
return migrateHyperedgeToRelation(hash) as Record<string, unknown>;
|
|
470
|
+
return migrateHyperedgeToRelation(hash) as unknown as Record<string, unknown>;
|
|
471
471
|
}
|
|
472
472
|
return null;
|
|
473
473
|
}
|