glossarist 0.4.41 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glossarist",
3
- "version": "0.4.41",
3
+ "version": "0.4.42",
4
4
  "description": "JavaScript SDK for Glossarist GCR packages — read, write, validate, and manage terminology concepts",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -3,26 +3,24 @@
3
3
  // Sorts object keys recursively so two semantically-equal values produce
4
4
  // identical strings regardless of insertion order. Arrays preserve order
5
5
  // (they're ordered in domain semantics). Undefined values are dropped.
6
- //
7
- // Used by the diff layer to compare items whose `toJSON()` may emit keys
8
- // in different orders depending on conditional inclusion (e.g.,
9
- // PartitiveHyperedge#toJSON omits `markers` when empty).
10
6
 
11
- export function canonicalJson(value) {
12
- return JSON.stringify(toCanonical(value));
7
+ type JsonInput = null | undefined | string | number | boolean | JsonInput[] | { [k: string]: JsonInput };
8
+
9
+ export function canonicalJson(value: JsonInput | unknown): string {
10
+ return JSON.stringify(toCanonical(value as JsonInput));
13
11
  }
14
12
 
15
- export function deepEqualCanonical(a, b) {
13
+ export function deepEqualCanonical(a: JsonInput | unknown, b: JsonInput | unknown): boolean {
16
14
  return canonicalJson(a) === canonicalJson(b);
17
15
  }
18
16
 
19
- function toCanonical(value) {
17
+ function toCanonical(value: JsonInput): unknown {
20
18
  if (value == null) return null;
21
19
  if (Array.isArray(value)) return value.map(toCanonical);
22
20
  if (typeof value === 'object') {
23
- const out = {};
21
+ const out: Record<string, unknown> = {};
24
22
  for (const k of Object.keys(value).sort()) {
25
- const v = toCanonical(value[k]);
23
+ const v = toCanonical((value as Record<string, JsonInput>)[k] as JsonInput);
26
24
  if (v !== undefined) out[k] = v;
27
25
  }
28
26
  return out;
@@ -0,0 +1,33 @@
1
+ // Shared identity helper for the diff layer.
2
+ //
3
+ // Per invariant N1, identity is a model concern. Models expose
4
+ // `identity()` returning a stable string. This helper dispatches: if
5
+ // the value is a model with `identity()`, call it; otherwise fall
6
+ // back to canonical JSON of `toJSON()`.
7
+
8
+ import { canonicalJson } from './canonical-json.js';
9
+
10
+ interface Identifiable {
11
+ identity(): string;
12
+ }
13
+ interface HasToJSON {
14
+ toJSON(): unknown;
15
+ }
16
+
17
+ function hasIdentity(v: unknown): v is Identifiable {
18
+ return v != null && typeof v === 'object' && typeof (v as { identity?: unknown }).identity === 'function';
19
+ }
20
+
21
+ function hasToJSON(v: unknown): v is HasToJSON {
22
+ return v != null && typeof v === 'object' && typeof (v as { toJSON?: unknown }).toJSON === 'function';
23
+ }
24
+
25
+ export function identityOf(item: unknown): string {
26
+ if (item == null) return '';
27
+ if (hasIdentity(item)) return item.identity();
28
+ if (typeof item === 'string') return item;
29
+ if (typeof item === 'number') return String(item);
30
+ if (typeof item === 'boolean') return String(item);
31
+ if (hasToJSON(item)) return canonicalJson(item.toJSON());
32
+ return canonicalJson(item);
33
+ }
@@ -2,22 +2,18 @@
2
2
  //
3
3
  // `computeSimilarity(changeCount, totalItems)` returns a value in
4
4
  // [0, 1] rounded to SIMILARITY_PRECISION_DECIMALS decimal places. The
5
- // rounding matches what glossarist-ruby's diff layer produces, so
6
- // similarity scores are stable across repos.
7
- //
8
- // `averageSimilarity(values)` averages an array of per-item similarity
9
- // scores (used by ConceptCollectionDiff to roll up matched concepts).
5
+ // rounding matches what glossarist-ruby's diff layer produces.
10
6
 
11
7
  export const SIMILARITY_PRECISION_DECIMALS = 4;
12
8
  const _SCALE = 10 ** SIMILARITY_PRECISION_DECIMALS;
13
9
 
14
- export function computeSimilarity(changeCount, totalItems) {
10
+ export function computeSimilarity(changeCount: number, totalItems: number): number {
15
11
  if (totalItems <= 0) return 1.0;
16
12
  const ratio = Math.min(changeCount / totalItems, 1);
17
13
  return Math.round((1 - ratio) * _SCALE) / _SCALE;
18
14
  }
19
15
 
20
- export function averageSimilarity(values) {
16
+ export function averageSimilarity(values: ReadonlyArray<number>): number {
21
17
  if (values.length === 0) return 1.0;
22
18
  const sum = values.reduce((a, b) => a + b, 0);
23
19
  return Math.round((sum / values.length) * _SCALE) / _SCALE;
@@ -0,0 +1,167 @@
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
+ export type TextHunkType = typeof HUNK_EQUAL | typeof HUNK_ADDED | typeof HUNK_REMOVED;
8
+
9
+ const TOKEN_RE = /(\s+)|(\S+)/g;
10
+
11
+ export interface TextHunkJson {
12
+ type: TextHunkType;
13
+ text: string;
14
+ }
15
+
16
+ export class TextHunk extends GlossaristModel {
17
+ readonly type: TextHunkType;
18
+ readonly text: string;
19
+
20
+ constructor(data: TextHunkJson = { type: HUNK_EQUAL, text: '' }) {
21
+ super();
22
+ if (![HUNK_EQUAL, HUNK_ADDED, HUNK_REMOVED].includes(data.type)) {
23
+ throw new Error(`TextHunk type must be 'equal', 'added', or 'removed' (got ${data.type})`);
24
+ }
25
+ this.type = data.type;
26
+ this.text = data.text ?? '';
27
+ }
28
+
29
+ override toJSON(): TextHunkJson {
30
+ return { type: this.type, text: this.text };
31
+ }
32
+
33
+ static override fromJSON(data: TextHunkJson): TextHunk {
34
+ return new TextHunk(data);
35
+ }
36
+ }
37
+
38
+ export interface TextDiffJson {
39
+ oldText?: string;
40
+ old_text?: string;
41
+ newText?: string;
42
+ new_text?: string;
43
+ hunks?: ReadonlyArray<TextHunkJson | TextHunk>;
44
+ }
45
+
46
+ export class TextDiff extends GlossaristModel {
47
+ readonly oldText: string;
48
+ readonly newText: string;
49
+ private readonly _hunks: ReadonlyArray<TextHunk>;
50
+
51
+ constructor(data: TextDiffJson = {}) {
52
+ super();
53
+ this.oldText = data.oldText ?? data.old_text ?? '';
54
+ this.newText = data.newText ?? data.new_text ?? '';
55
+ this._hunks = (data.hunks ?? []).map((h) =>
56
+ h instanceof TextHunk ? h : new TextHunk(h),
57
+ );
58
+ }
59
+
60
+ get hunks(): ReadonlyArray<TextHunk> {
61
+ return this._hunks;
62
+ }
63
+
64
+ get hasChanges(): boolean {
65
+ return this._hunks.some((h) => h.type !== HUNK_EQUAL);
66
+ }
67
+
68
+ get addedText(): string {
69
+ return this._hunks
70
+ .filter((h) => h.type === HUNK_ADDED)
71
+ .map((h) => h.text)
72
+ .join('');
73
+ }
74
+
75
+ get removedText(): string {
76
+ return this._hunks
77
+ .filter((h) => h.type === HUNK_REMOVED)
78
+ .map((h) => h.text)
79
+ .join('');
80
+ }
81
+
82
+ override toJSON(): TextDiffJson & { old_text: string; new_text: string; hunks: ReadonlyArray<TextHunkJson> } {
83
+ return {
84
+ old_text: this.oldText,
85
+ new_text: this.newText,
86
+ hunks: this._hunks.map((h) => h.toJSON()),
87
+ };
88
+ }
89
+
90
+ static override fromJSON(data: TextDiffJson): TextDiff {
91
+ return new TextDiff(data);
92
+ }
93
+ }
94
+
95
+ export function diffText(oldText: string | null | undefined, newText: string | null | undefined): TextDiff {
96
+ const oldStr = String(oldText ?? '');
97
+ const newStr = String(newText ?? '');
98
+ const oldTokens = tokenize(oldStr);
99
+ const newTokens = tokenize(newStr);
100
+ const hunks = lcsHunks(oldTokens, newTokens);
101
+ return new TextDiff({ oldText: oldStr, newText: newStr, hunks });
102
+ }
103
+
104
+ function tokenize(text: string): string[] {
105
+ if (text.length === 0) return [];
106
+ return text.match(TOKEN_RE) ?? [];
107
+ }
108
+
109
+ function lcsHunks(a: string[], b: string[]): TextHunk[] {
110
+ const n = a.length;
111
+ const m = b.length;
112
+
113
+ if (n === 0) return m === 0 ? [] : [new TextHunk({ type: HUNK_ADDED, text: b.join('') })];
114
+ if (m === 0) return [new TextHunk({ type: HUNK_REMOVED, text: a.join('') })];
115
+
116
+ const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
117
+ for (let i = 1; i <= n; i++) {
118
+ for (let j = 1; j <= m; j++) {
119
+ dp[i]![j]! =
120
+ a[i - 1] === b[j - 1]
121
+ ? dp[i - 1]![j - 1]! + 1
122
+ : Math.max(dp[i - 1]![j]!, dp[i]![j - 1]!);
123
+ }
124
+ }
125
+
126
+ const raw: Array<{ type: TextHunkType; text: string }> = [];
127
+ let i = n;
128
+ let j = m;
129
+ while (i > 0 && j > 0) {
130
+ if (a[i - 1] === b[j - 1]) {
131
+ raw.push({ type: HUNK_EQUAL, text: a[i - 1]! });
132
+ i--; j--;
133
+ } else if ((dp[i - 1]![j] ?? 0) >= (dp[i]![j - 1] ?? 0)) {
134
+ raw.push({ type: HUNK_REMOVED, text: a[i - 1]! });
135
+ i--;
136
+ } else {
137
+ raw.push({ type: HUNK_ADDED, text: b[j - 1]! });
138
+ j--;
139
+ }
140
+ }
141
+ while (i > 0) {
142
+ raw.push({ type: HUNK_REMOVED, text: a[i - 1]! });
143
+ i--;
144
+ }
145
+ while (j > 0) {
146
+ raw.push({ type: HUNK_ADDED, text: b[j - 1]! });
147
+ j--;
148
+ }
149
+ raw.reverse();
150
+
151
+ return coalesce(raw).map((h) => new TextHunk(h));
152
+ }
153
+
154
+ function coalesce(
155
+ raw: ReadonlyArray<{ type: TextHunkType; text: string }>,
156
+ ): Array<{ type: TextHunkType; text: string }> {
157
+ const out: Array<{ type: TextHunkType; text: string }> = [];
158
+ for (const r of raw) {
159
+ const last = out[out.length - 1];
160
+ if (last && last.type === r.type) {
161
+ last.text += r.text;
162
+ } else {
163
+ out.push({ type: r.type, text: r.text });
164
+ }
165
+ }
166
+ return out;
167
+ }
@@ -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
- }
@@ -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
- }
File without changes