@sid-ai/sid-sdk 0.1.0
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/LICENSE +21 -0
- package/README.md +155 -0
- package/THIRD_PARTY_NOTICES.md +7017 -0
- package/dist/cjs/cache.d.ts +47 -0
- package/dist/cjs/cache.js +120 -0
- package/dist/cjs/id-stream.d.ts +25 -0
- package/dist/cjs/id-stream.js +76 -0
- package/dist/cjs/index.d.ts +10 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/intervals.d.ts +4 -0
- package/dist/cjs/intervals.js +50 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/ranges.d.ts +17 -0
- package/dist/cjs/ranges.js +52 -0
- package/dist/cjs/rendering.d.ts +26 -0
- package/dist/cjs/rendering.js +147 -0
- package/dist/cjs/snippet.d.ts +10 -0
- package/dist/cjs/snippet.js +36 -0
- package/dist/esm/index.js +1 -0
- package/dist/wasm/sid_snippet.cjs +174 -0
- package/dist/wasm/sid_snippet_bg.wasm +0 -0
- package/package.json +48 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type CharacterRange, type RangeMode } from "./ranges";
|
|
2
|
+
import { DocumentView, DocumentViewWithSnippet, type Document } from "./rendering";
|
|
3
|
+
import { type Language } from "./snippet";
|
|
4
|
+
export declare const SNIPPET_SIZE_DEFAULT = 50;
|
|
5
|
+
export declare const MIN_SEEN_OVERLAP_DEFAULT = 100;
|
|
6
|
+
export interface DocumentCacheOptions {
|
|
7
|
+
language?: Language;
|
|
8
|
+
rangeMode?: RangeMode;
|
|
9
|
+
}
|
|
10
|
+
export interface ApplySnippetOptions {
|
|
11
|
+
snippetField: string;
|
|
12
|
+
query: string;
|
|
13
|
+
snippetSize?: number;
|
|
14
|
+
minSeenOverlap?: number;
|
|
15
|
+
displayFields?: readonly string[];
|
|
16
|
+
language?: Language;
|
|
17
|
+
}
|
|
18
|
+
export interface SingleSpanOptions {
|
|
19
|
+
snippetField?: string;
|
|
20
|
+
snippetDisplaySpan?: CharacterRange;
|
|
21
|
+
displayFields?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
export declare class DocumentCache<T extends Document = Document> {
|
|
24
|
+
readonly language: Language;
|
|
25
|
+
readonly rangeMode: RangeMode;
|
|
26
|
+
readonly seenLedger: Map<string, CharacterRange[]>;
|
|
27
|
+
private family;
|
|
28
|
+
constructor({ language, rangeMode }?: DocumentCacheOptions);
|
|
29
|
+
addDocument(dataId: string, document: T): string;
|
|
30
|
+
contains(dataId: string): boolean;
|
|
31
|
+
containsModelFacingId(modelId: string): boolean;
|
|
32
|
+
toDataId(modelId: string): string;
|
|
33
|
+
toModelFacingId(dataId: string): string;
|
|
34
|
+
getDocument(dataId: string): T;
|
|
35
|
+
getDocumentFromModelFacingId(modelId: string): T;
|
|
36
|
+
private content;
|
|
37
|
+
resolveCharRange(dataId: string, snippetField: string, range: CharacterRange): CharacterRange;
|
|
38
|
+
/** Override in a subclass to select a different span. Returned spans are strictly validated. */
|
|
39
|
+
snippetFn(query: string, content: string, snippetSize: number, language: Language): CharacterRange;
|
|
40
|
+
applySnippet(dataId: string, { snippetField, query, snippetSize, minSeenOverlap, displayFields, language }: ApplySnippetOptions): DocumentViewWithSnippet<T>;
|
|
41
|
+
getSingleSpanDocumentView(dataId: string, options: SingleSpanOptions & {
|
|
42
|
+
snippetField: string;
|
|
43
|
+
}): DocumentViewWithSnippet<T>;
|
|
44
|
+
getSingleSpanDocumentView(dataId: string, options?: SingleSpanOptions): DocumentView<T>;
|
|
45
|
+
updateSeen(view: DocumentView): void;
|
|
46
|
+
fork(n: number): DocumentCache<T>[];
|
|
47
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DocumentCache = exports.MIN_SEEN_OVERLAP_DEFAULT = exports.SNIPPET_SIZE_DEFAULT = void 0;
|
|
4
|
+
const id_stream_1 = require("./id-stream");
|
|
5
|
+
const intervals_1 = require("./intervals");
|
|
6
|
+
const ranges_1 = require("./ranges");
|
|
7
|
+
const rendering_1 = require("./rendering");
|
|
8
|
+
const snippet_1 = require("./snippet");
|
|
9
|
+
exports.SNIPPET_SIZE_DEFAULT = 50;
|
|
10
|
+
exports.MIN_SEEN_OVERLAP_DEFAULT = 100;
|
|
11
|
+
class DocumentCache {
|
|
12
|
+
language;
|
|
13
|
+
rangeMode;
|
|
14
|
+
seenLedger = new Map();
|
|
15
|
+
family;
|
|
16
|
+
constructor({ language = "english", rangeMode = "lenient" } = {}) {
|
|
17
|
+
this.language = (0, snippet_1.validateLanguage)(language);
|
|
18
|
+
this.rangeMode = (0, ranges_1.validateRangeMode)(rangeMode);
|
|
19
|
+
this.family = { documents: new Map(), toData: new Map(), toModel: new Map(), ids: new id_stream_1.IdStream() };
|
|
20
|
+
}
|
|
21
|
+
addDocument(dataId, document) {
|
|
22
|
+
if (typeof dataId !== "string")
|
|
23
|
+
throw new TypeError("dataId must be a string");
|
|
24
|
+
const existing = this.family.toModel.get(dataId);
|
|
25
|
+
if (existing !== undefined) {
|
|
26
|
+
if (!this.seenLedger.has(dataId))
|
|
27
|
+
this.seenLedger.set(dataId, []);
|
|
28
|
+
return existing;
|
|
29
|
+
}
|
|
30
|
+
if (document === null || typeof document !== "object" || Array.isArray(document))
|
|
31
|
+
throw new TypeError("document must be a record");
|
|
32
|
+
const stored = structuredClone(document);
|
|
33
|
+
const modelId = this.family.ids.mint();
|
|
34
|
+
this.family.documents.set(dataId, stored);
|
|
35
|
+
this.family.toModel.set(dataId, modelId);
|
|
36
|
+
this.family.toData.set(modelId, dataId);
|
|
37
|
+
this.seenLedger.set(dataId, []);
|
|
38
|
+
return modelId;
|
|
39
|
+
}
|
|
40
|
+
contains(dataId) { return this.family.documents.has(dataId); }
|
|
41
|
+
containsModelFacingId(modelId) { return this.family.toData.has(modelId); }
|
|
42
|
+
toDataId(modelId) {
|
|
43
|
+
const id = this.family.toData.get(modelId);
|
|
44
|
+
if (id === undefined)
|
|
45
|
+
throw new Error(`modelFacingId ${JSON.stringify(modelId)} not found in cache`);
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
48
|
+
toModelFacingId(dataId) {
|
|
49
|
+
const id = this.family.toModel.get(dataId);
|
|
50
|
+
if (id === undefined)
|
|
51
|
+
throw new Error(`dataId ${JSON.stringify(dataId)} not found in cache`);
|
|
52
|
+
return id;
|
|
53
|
+
}
|
|
54
|
+
getDocument(dataId) {
|
|
55
|
+
const doc = this.family.documents.get(dataId);
|
|
56
|
+
if (doc === undefined)
|
|
57
|
+
throw new Error(`dataId ${JSON.stringify(dataId)} not found in the document cache`);
|
|
58
|
+
return doc;
|
|
59
|
+
}
|
|
60
|
+
getDocumentFromModelFacingId(modelId) { return this.getDocument(this.toDataId(modelId)); }
|
|
61
|
+
content(dataId, snippetField) {
|
|
62
|
+
const value = (0, rendering_1.field)(this.getDocument(dataId), snippetField);
|
|
63
|
+
if (typeof value !== "string" || !value)
|
|
64
|
+
throw new TypeError(`snippet field ${JSON.stringify(snippetField)} must be a non-empty string`);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
resolveCharRange(dataId, snippetField, range) {
|
|
68
|
+
return (0, ranges_1.resolveRange)(range, new ranges_1.CodePointText(this.content(dataId, snippetField)).length, this.rangeMode, dataId);
|
|
69
|
+
}
|
|
70
|
+
/** Override in a subclass to select a different span. Returned spans are strictly validated. */
|
|
71
|
+
snippetFn(query, content, snippetSize, language) {
|
|
72
|
+
return (0, snippet_1.bm25SnippetWithStride)(query, content, { windowSize: snippetSize, stride: Math.max(1, Math.floor(snippetSize / 5)), language });
|
|
73
|
+
}
|
|
74
|
+
applySnippet(dataId, { snippetField, query, snippetSize = exports.SNIPPET_SIZE_DEFAULT, minSeenOverlap = exports.MIN_SEEN_OVERLAP_DEFAULT, displayFields, language = this.language }) {
|
|
75
|
+
const document = this.getDocument(dataId), content = this.content(dataId, snippetField);
|
|
76
|
+
const fields = displayFields ?? Object.keys(document);
|
|
77
|
+
if (!fields.includes(snippetField))
|
|
78
|
+
throw new RangeError(`snippet field ${JSON.stringify(snippetField)} must be in displayFields`);
|
|
79
|
+
(0, ranges_1.positiveInteger)(snippetSize, "snippetSize");
|
|
80
|
+
if (!Number.isSafeInteger(minSeenOverlap) || minSeenOverlap < 0)
|
|
81
|
+
throw new RangeError("minSeenOverlap must be a nonnegative safe integer");
|
|
82
|
+
const span = (0, ranges_1.resolveRange)(this.snippetFn(query, content, snippetSize, (0, snippet_1.validateLanguage)(language)), new ranges_1.CodePointText(content).length, "strict", dataId);
|
|
83
|
+
const seen = this.seenLedger.get(dataId) ?? [];
|
|
84
|
+
const overlap = (0, intervals_1.overlaps)(span, seen);
|
|
85
|
+
const fullySeen = overlap.length === 1 && overlap[0][0] === span[0] && overlap[0][1] === span[1];
|
|
86
|
+
const [shown, masked] = fullySeen ? [[], [span]] : (0, intervals_1.planSegments)(span, seen, minSeenOverlap);
|
|
87
|
+
return new rendering_1.DocumentViewWithSnippet(dataId, this.toModelFacingId(dataId), document, snippetField, masked, shown, fields);
|
|
88
|
+
}
|
|
89
|
+
getSingleSpanDocumentView(dataId, { snippetField, snippetDisplaySpan, displayFields } = {}) {
|
|
90
|
+
const document = this.getDocument(dataId), modelId = this.toModelFacingId(dataId);
|
|
91
|
+
if (snippetField === undefined) {
|
|
92
|
+
if (displayFields === undefined)
|
|
93
|
+
throw new TypeError("displayFields is required when snippetField is omitted");
|
|
94
|
+
return new rendering_1.DocumentView(dataId, modelId, document, displayFields);
|
|
95
|
+
}
|
|
96
|
+
const content = this.content(dataId, snippetField);
|
|
97
|
+
const span = snippetDisplaySpan === undefined ? [0, new ranges_1.CodePointText(content).length]
|
|
98
|
+
: this.resolveCharRange(dataId, snippetField, snippetDisplaySpan);
|
|
99
|
+
return new rendering_1.DocumentViewWithSnippet(dataId, modelId, document, snippetField, [], [span], displayFields ?? Object.keys(document));
|
|
100
|
+
}
|
|
101
|
+
updateSeen(view) {
|
|
102
|
+
this.getDocument(view.dataId);
|
|
103
|
+
let seen = this.seenLedger.get(view.dataId) ?? [];
|
|
104
|
+
for (const span of view.snippetDisplaySpans)
|
|
105
|
+
seen = (0, intervals_1.insertInterval)(seen, span);
|
|
106
|
+
this.seenLedger.set(view.dataId, seen);
|
|
107
|
+
}
|
|
108
|
+
fork(n) {
|
|
109
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
110
|
+
throw new RangeError("fork count must be a nonnegative safe integer");
|
|
111
|
+
return Array.from({ length: n }, () => {
|
|
112
|
+
const child = new DocumentCache({ language: this.language, rangeMode: this.rangeMode });
|
|
113
|
+
child.family = this.family;
|
|
114
|
+
for (const [id, spans] of this.seenLedger)
|
|
115
|
+
child.seenLedger.set(id, spans.map(([a, b]) => [a, b]));
|
|
116
|
+
return child;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
exports.DocumentCache = DocumentCache;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface IdStreamOptions {
|
|
2
|
+
alphabet?: string;
|
|
3
|
+
length?: number;
|
|
4
|
+
seed?: number | bigint;
|
|
5
|
+
}
|
|
6
|
+
export declare class IdSpaceExhausted extends Error {
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class IdStream implements IterableIterator<string> {
|
|
10
|
+
readonly alphabet: string;
|
|
11
|
+
readonly length: number;
|
|
12
|
+
readonly space: bigint;
|
|
13
|
+
private readonly chars;
|
|
14
|
+
private readonly key;
|
|
15
|
+
private readonly halfBits;
|
|
16
|
+
private counter;
|
|
17
|
+
constructor({ alphabet, length, seed }?: IdStreamOptions);
|
|
18
|
+
get minted(): bigint;
|
|
19
|
+
get remaining(): bigint;
|
|
20
|
+
[Symbol.iterator](): IterableIterator<string>;
|
|
21
|
+
next(): IteratorResult<string, never>;
|
|
22
|
+
mint(): string;
|
|
23
|
+
at(index: number | bigint): string;
|
|
24
|
+
private permute;
|
|
25
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IdStream = exports.IdSpaceExhausted = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const ranges_1 = require("./ranges");
|
|
6
|
+
class IdSpaceExhausted extends Error {
|
|
7
|
+
name = "IdSpaceExhausted";
|
|
8
|
+
}
|
|
9
|
+
exports.IdSpaceExhausted = IdSpaceExhausted;
|
|
10
|
+
const MASK = (1n << 64n) - 1n;
|
|
11
|
+
function mix(value, key, round) {
|
|
12
|
+
let x = (value + key + round * 0x9e3779b97f4a7c15n) & MASK;
|
|
13
|
+
x ^= x >> 30n;
|
|
14
|
+
x = (x * 0xbf58476d1ce4e5b9n) & MASK;
|
|
15
|
+
x ^= x >> 27n;
|
|
16
|
+
x = (x * 0x94d049bb133111ebn) & MASK;
|
|
17
|
+
return x ^ (x >> 31n);
|
|
18
|
+
}
|
|
19
|
+
class IdStream {
|
|
20
|
+
alphabet;
|
|
21
|
+
length;
|
|
22
|
+
space;
|
|
23
|
+
chars;
|
|
24
|
+
key;
|
|
25
|
+
halfBits;
|
|
26
|
+
counter = 0n;
|
|
27
|
+
constructor({ alphabet = "abcdefghijklmnopqrstuvwxyz", length = 5, seed } = {}) {
|
|
28
|
+
this.length = (0, ranges_1.positiveInteger)(length, "length");
|
|
29
|
+
if (typeof alphabet !== "string")
|
|
30
|
+
throw new TypeError("alphabet must be a string");
|
|
31
|
+
this.alphabet = alphabet;
|
|
32
|
+
this.chars = Array.from(alphabet);
|
|
33
|
+
if (this.chars.length === 0 || new Set(this.chars).size !== this.chars.length || /[#:]/.test(alphabet)) {
|
|
34
|
+
throw new RangeError("alphabet must be nonempty, unique, and exclude # and :");
|
|
35
|
+
}
|
|
36
|
+
if (seed !== undefined && typeof seed !== "bigint" && !Number.isSafeInteger(seed)) {
|
|
37
|
+
throw new RangeError("seed must be a safe integer or bigint");
|
|
38
|
+
}
|
|
39
|
+
this.space = BigInt(this.chars.length) ** BigInt(length);
|
|
40
|
+
this.key = seed === undefined ? (0, node_crypto_1.randomBytes)(8).readBigUInt64LE() : mix(BigInt(seed), 0n, 0n);
|
|
41
|
+
this.halfBits = BigInt(Math.max(1, Math.ceil((this.space - 1n).toString(2).length / 2)));
|
|
42
|
+
}
|
|
43
|
+
get minted() { return this.counter; }
|
|
44
|
+
get remaining() { return this.space - this.counter; }
|
|
45
|
+
[Symbol.iterator]() { return this; }
|
|
46
|
+
next() { return { value: this.mint(), done: false }; }
|
|
47
|
+
mint() {
|
|
48
|
+
if (this.counter >= this.space)
|
|
49
|
+
throw new IdSpaceExhausted(`all ${this.space} ids of this stream are in use`);
|
|
50
|
+
return this.at(this.counter++);
|
|
51
|
+
}
|
|
52
|
+
at(index) {
|
|
53
|
+
if (typeof index !== "bigint" && !Number.isSafeInteger(index))
|
|
54
|
+
throw new RangeError("index must be a safe integer or bigint");
|
|
55
|
+
let x = BigInt(index);
|
|
56
|
+
if (x < 0n || x >= this.space)
|
|
57
|
+
throw new RangeError("index outside id space");
|
|
58
|
+
do {
|
|
59
|
+
x = this.permute(x);
|
|
60
|
+
} while (x >= this.space);
|
|
61
|
+
const base = BigInt(this.chars.length), result = [];
|
|
62
|
+
for (let i = 0; i < this.length; i++) {
|
|
63
|
+
result.push(this.chars[Number(x % base)]);
|
|
64
|
+
x /= base;
|
|
65
|
+
}
|
|
66
|
+
return result.reverse().join("");
|
|
67
|
+
}
|
|
68
|
+
permute(x) {
|
|
69
|
+
const mask = (1n << this.halfBits) - 1n;
|
|
70
|
+
let lo = x >> this.halfBits, hi = x & mask;
|
|
71
|
+
for (let round = 0n; round < 4n; round++)
|
|
72
|
+
[lo, hi] = [hi, lo ^ (mix(hi, this.key, round) & mask)];
|
|
73
|
+
return (lo << this.halfBits) | hi;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.IdStream = IdStream;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { DocumentCache, SNIPPET_SIZE_DEFAULT, MIN_SEEN_OVERLAP_DEFAULT } from "./cache";
|
|
2
|
+
export type { DocumentCacheOptions, ApplySnippetOptions, SingleSpanOptions } from "./cache";
|
|
3
|
+
export { DocumentView, DocumentViewWithSnippet, parseRenderedModelFacingId, renderMarkdownTable } from "./rendering";
|
|
4
|
+
export type { Document, RenderParts } from "./rendering";
|
|
5
|
+
export { InvalidCharacterRange } from "./ranges";
|
|
6
|
+
export type { CharacterRange, RangeMode } from "./ranges";
|
|
7
|
+
export { IdStream, IdSpaceExhausted } from "./id-stream";
|
|
8
|
+
export type { IdStreamOptions } from "./id-stream";
|
|
9
|
+
export { bm25SnippetWithStride, SUPPORTED_LANGUAGES } from "./snippet";
|
|
10
|
+
export type { Language, SnippetOptions } from "./snippet";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SUPPORTED_LANGUAGES = exports.bm25SnippetWithStride = exports.IdSpaceExhausted = exports.IdStream = exports.InvalidCharacterRange = exports.renderMarkdownTable = exports.parseRenderedModelFacingId = exports.DocumentViewWithSnippet = exports.DocumentView = exports.MIN_SEEN_OVERLAP_DEFAULT = exports.SNIPPET_SIZE_DEFAULT = exports.DocumentCache = void 0;
|
|
4
|
+
var cache_1 = require("./cache");
|
|
5
|
+
Object.defineProperty(exports, "DocumentCache", { enumerable: true, get: function () { return cache_1.DocumentCache; } });
|
|
6
|
+
Object.defineProperty(exports, "SNIPPET_SIZE_DEFAULT", { enumerable: true, get: function () { return cache_1.SNIPPET_SIZE_DEFAULT; } });
|
|
7
|
+
Object.defineProperty(exports, "MIN_SEEN_OVERLAP_DEFAULT", { enumerable: true, get: function () { return cache_1.MIN_SEEN_OVERLAP_DEFAULT; } });
|
|
8
|
+
var rendering_1 = require("./rendering");
|
|
9
|
+
Object.defineProperty(exports, "DocumentView", { enumerable: true, get: function () { return rendering_1.DocumentView; } });
|
|
10
|
+
Object.defineProperty(exports, "DocumentViewWithSnippet", { enumerable: true, get: function () { return rendering_1.DocumentViewWithSnippet; } });
|
|
11
|
+
Object.defineProperty(exports, "parseRenderedModelFacingId", { enumerable: true, get: function () { return rendering_1.parseRenderedModelFacingId; } });
|
|
12
|
+
Object.defineProperty(exports, "renderMarkdownTable", { enumerable: true, get: function () { return rendering_1.renderMarkdownTable; } });
|
|
13
|
+
var ranges_1 = require("./ranges");
|
|
14
|
+
Object.defineProperty(exports, "InvalidCharacterRange", { enumerable: true, get: function () { return ranges_1.InvalidCharacterRange; } });
|
|
15
|
+
var id_stream_1 = require("./id-stream");
|
|
16
|
+
Object.defineProperty(exports, "IdStream", { enumerable: true, get: function () { return id_stream_1.IdStream; } });
|
|
17
|
+
Object.defineProperty(exports, "IdSpaceExhausted", { enumerable: true, get: function () { return id_stream_1.IdSpaceExhausted; } });
|
|
18
|
+
var snippet_1 = require("./snippet");
|
|
19
|
+
Object.defineProperty(exports, "bm25SnippetWithStride", { enumerable: true, get: function () { return snippet_1.bm25SnippetWithStride; } });
|
|
20
|
+
Object.defineProperty(exports, "SUPPORTED_LANGUAGES", { enumerable: true, get: function () { return snippet_1.SUPPORTED_LANGUAGES; } });
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CharacterRange } from "./ranges";
|
|
2
|
+
export declare function insertInterval(intervals: readonly CharacterRange[], range: CharacterRange): CharacterRange[];
|
|
3
|
+
export declare function overlaps(span: CharacterRange, intervals: readonly CharacterRange[]): CharacterRange[];
|
|
4
|
+
export declare function planSegments(span: CharacterRange, seen: readonly CharacterRange[], minimum: number): [CharacterRange[], CharacterRange[]];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.insertInterval = insertInterval;
|
|
4
|
+
exports.overlaps = overlaps;
|
|
5
|
+
exports.planSegments = planSegments;
|
|
6
|
+
function insertInterval(intervals, range) {
|
|
7
|
+
let [a, b] = range;
|
|
8
|
+
if (a >= b)
|
|
9
|
+
throw new RangeError("cannot record an empty interval");
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const [x, y] of intervals) {
|
|
12
|
+
if (y < a || x > b)
|
|
13
|
+
out.push([x, y]);
|
|
14
|
+
else {
|
|
15
|
+
a = Math.min(a, x);
|
|
16
|
+
b = Math.max(b, y);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
out.push([a, b]);
|
|
20
|
+
return out.sort((left, right) => left[0] - right[0]);
|
|
21
|
+
}
|
|
22
|
+
function overlaps(span, intervals) {
|
|
23
|
+
return intervals.flatMap(([x, y]) => {
|
|
24
|
+
const a = Math.max(span[0], x), b = Math.min(span[1], y);
|
|
25
|
+
return a < b ? [[a, b]] : [];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function planSegments(span, seen, minimum) {
|
|
29
|
+
const display = [], masked = [];
|
|
30
|
+
function append(a, b) {
|
|
31
|
+
const last = display.at(-1);
|
|
32
|
+
if (last?.[1] === a)
|
|
33
|
+
display[display.length - 1] = [last[0], b];
|
|
34
|
+
else
|
|
35
|
+
display.push([a, b]);
|
|
36
|
+
}
|
|
37
|
+
let cursor = span[0];
|
|
38
|
+
for (const [x, y] of overlaps(span, seen)) {
|
|
39
|
+
if (x > cursor)
|
|
40
|
+
append(cursor, x);
|
|
41
|
+
if (y - x >= minimum)
|
|
42
|
+
masked.push([x, y]);
|
|
43
|
+
else
|
|
44
|
+
append(x, y);
|
|
45
|
+
cursor = y;
|
|
46
|
+
}
|
|
47
|
+
if (cursor < span[1])
|
|
48
|
+
append(cursor, span[1]);
|
|
49
|
+
return [display, masked];
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Half-open Unicode code-point offsets, matching Python string indices. */
|
|
2
|
+
export type CharacterRange = readonly [number, number];
|
|
3
|
+
export type RangeMode = "lenient" | "strict";
|
|
4
|
+
export declare class InvalidCharacterRange extends RangeError {
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function validateRangeMode(mode: RangeMode): RangeMode;
|
|
8
|
+
export declare function resolveRange(range: CharacterRange, length: number, mode: RangeMode, id: string): CharacterRange;
|
|
9
|
+
/** Build once per render; boundaries also preserve astral Unicode characters. */
|
|
10
|
+
export declare class CodePointText {
|
|
11
|
+
readonly text: string;
|
|
12
|
+
private readonly boundaries;
|
|
13
|
+
constructor(text: string);
|
|
14
|
+
get length(): number;
|
|
15
|
+
slice(start: number, end: number): string;
|
|
16
|
+
}
|
|
17
|
+
export declare function positiveInteger(value: number, name: string): number;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CodePointText = exports.InvalidCharacterRange = void 0;
|
|
4
|
+
exports.validateRangeMode = validateRangeMode;
|
|
5
|
+
exports.resolveRange = resolveRange;
|
|
6
|
+
exports.positiveInteger = positiveInteger;
|
|
7
|
+
class InvalidCharacterRange extends RangeError {
|
|
8
|
+
name = "InvalidCharacterRange";
|
|
9
|
+
}
|
|
10
|
+
exports.InvalidCharacterRange = InvalidCharacterRange;
|
|
11
|
+
function validateRangeMode(mode) {
|
|
12
|
+
if (mode !== "lenient" && mode !== "strict") {
|
|
13
|
+
throw new RangeError(`unsupported range mode ${String(mode)}; supported values: lenient, strict`);
|
|
14
|
+
}
|
|
15
|
+
return mode;
|
|
16
|
+
}
|
|
17
|
+
function resolveRange(range, length, mode, id) {
|
|
18
|
+
const prefix = `invalid character range for document ${JSON.stringify(id)} (${length} characters): `;
|
|
19
|
+
if (!Array.isArray(range) || range.length !== 2 || !range.every(Number.isSafeInteger)) {
|
|
20
|
+
throw new InvalidCharacterRange(prefix + "expected a (start, end) pair of safe integers");
|
|
21
|
+
}
|
|
22
|
+
const [start, end] = range;
|
|
23
|
+
if (start >= end)
|
|
24
|
+
throw new InvalidCharacterRange(prefix + `start ${start} must be less than end ${end}`);
|
|
25
|
+
if (end <= 0 || start >= length)
|
|
26
|
+
throw new InvalidCharacterRange(prefix + "the requested range does not overlap the document");
|
|
27
|
+
if (mode === "strict" && (start < 0 || end > length)) {
|
|
28
|
+
throw new InvalidCharacterRange(prefix + "range exceeds the document boundaries");
|
|
29
|
+
}
|
|
30
|
+
return [Math.max(0, start), Math.min(end, length)];
|
|
31
|
+
}
|
|
32
|
+
/** Build once per render; boundaries also preserve astral Unicode characters. */
|
|
33
|
+
class CodePointText {
|
|
34
|
+
text;
|
|
35
|
+
boundaries = [0];
|
|
36
|
+
constructor(text) {
|
|
37
|
+
this.text = text;
|
|
38
|
+
let offset = 0;
|
|
39
|
+
for (const char of text) {
|
|
40
|
+
offset += char.length;
|
|
41
|
+
this.boundaries.push(offset);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
get length() { return this.boundaries.length - 1; }
|
|
45
|
+
slice(start, end) { return this.text.slice(this.boundaries[start], this.boundaries[end]); }
|
|
46
|
+
}
|
|
47
|
+
exports.CodePointText = CodePointText;
|
|
48
|
+
function positiveInteger(value, name) {
|
|
49
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
50
|
+
throw new RangeError(`${name} must be a positive safe integer`);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CharacterRange } from "./ranges";
|
|
2
|
+
export type Document = Record<string, unknown>;
|
|
3
|
+
export declare function field(document: Document, key: string): unknown;
|
|
4
|
+
export declare function parseRenderedModelFacingId(reference: string): [string, CharacterRange | null];
|
|
5
|
+
export interface RenderParts {
|
|
6
|
+
id: string;
|
|
7
|
+
attributes: Map<string, unknown>;
|
|
8
|
+
body: string | null;
|
|
9
|
+
}
|
|
10
|
+
export declare class DocumentView<T extends Document = Document> {
|
|
11
|
+
readonly dataId: string;
|
|
12
|
+
readonly modelFacingId: string;
|
|
13
|
+
readonly document: T;
|
|
14
|
+
readonly displayFields: readonly string[];
|
|
15
|
+
readonly snippetDisplaySpans: CharacterRange[];
|
|
16
|
+
constructor(dataId: string, modelFacingId: string, document: T, displayFields: readonly string[]);
|
|
17
|
+
renderParts(displayFields?: readonly string[]): RenderParts;
|
|
18
|
+
renderXml(): string;
|
|
19
|
+
}
|
|
20
|
+
export declare class DocumentViewWithSnippet<T extends Document = Document> extends DocumentView<T> {
|
|
21
|
+
readonly snippetField: string;
|
|
22
|
+
readonly snippetSeenSpans: CharacterRange[];
|
|
23
|
+
constructor(dataId: string, modelFacingId: string, document: T, snippetField: string, snippetSeenSpans: CharacterRange[], snippetDisplaySpans: CharacterRange[], displayFields: readonly string[]);
|
|
24
|
+
renderParts(displayFields?: readonly string[]): RenderParts;
|
|
25
|
+
}
|
|
26
|
+
export declare function renderMarkdownTable(views: readonly DocumentView[], displayFields?: readonly string[]): string;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DocumentViewWithSnippet = exports.DocumentView = void 0;
|
|
4
|
+
exports.field = field;
|
|
5
|
+
exports.parseRenderedModelFacingId = parseRenderedModelFacingId;
|
|
6
|
+
exports.renderMarkdownTable = renderMarkdownTable;
|
|
7
|
+
const ranges_1 = require("./ranges");
|
|
8
|
+
function field(document, key) {
|
|
9
|
+
if (!Object.hasOwn(document, key))
|
|
10
|
+
throw new Error(`document field ${JSON.stringify(key)} not found`);
|
|
11
|
+
return document[key];
|
|
12
|
+
}
|
|
13
|
+
function scalar(value) {
|
|
14
|
+
if (value === true)
|
|
15
|
+
return "True";
|
|
16
|
+
if (value === false)
|
|
17
|
+
return "False";
|
|
18
|
+
if (value === null)
|
|
19
|
+
return "None";
|
|
20
|
+
return String(value);
|
|
21
|
+
}
|
|
22
|
+
function stringify(value) { return Array.isArray(value) ? value.map(scalar).join(", ") : scalar(value); }
|
|
23
|
+
function escape(value) { return stringify(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); }
|
|
24
|
+
function truthy(value) {
|
|
25
|
+
if (!value)
|
|
26
|
+
return false;
|
|
27
|
+
if (Array.isArray(value))
|
|
28
|
+
return value.length > 0;
|
|
29
|
+
if (typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype)
|
|
30
|
+
return Object.keys(value).length > 0;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
function decimal(value) {
|
|
34
|
+
// Python accepts Unicode decimal digits; normalize each Nd block to ASCII.
|
|
35
|
+
let digits = "";
|
|
36
|
+
for (const char of value) {
|
|
37
|
+
if (char === "-") {
|
|
38
|
+
digits += char;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
let first = char.codePointAt(0);
|
|
42
|
+
while (first > 0 && /\p{Nd}/u.test(String.fromCodePoint(first - 1)))
|
|
43
|
+
first--;
|
|
44
|
+
digits += String((char.codePointAt(0) - first) % 10);
|
|
45
|
+
}
|
|
46
|
+
const result = Number(digits);
|
|
47
|
+
if (!Number.isSafeInteger(result))
|
|
48
|
+
throw new RangeError("character offsets must be safe integers");
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function parseRenderedModelFacingId(reference) {
|
|
52
|
+
if (typeof reference !== "string" || !reference)
|
|
53
|
+
throw new TypeError("invalid document reference: expected a non-empty string");
|
|
54
|
+
const hash = reference.indexOf("#");
|
|
55
|
+
if (hash < 0)
|
|
56
|
+
return [reference, null];
|
|
57
|
+
if (hash === 0)
|
|
58
|
+
throw new RangeError("invalid document reference: missing document id before '#'");
|
|
59
|
+
const parts = reference.slice(hash + 1).split(":").map(part => part.trim());
|
|
60
|
+
if (parts.length !== 2 || !parts.every(part => /^-?\p{Nd}+$/u.test(part)))
|
|
61
|
+
throw new RangeError("invalid document reference: expected '<doc_id>#<start>:<end>' with decimal integer character offsets");
|
|
62
|
+
const start = decimal(parts[0]), end = decimal(parts[1]);
|
|
63
|
+
if (start >= end)
|
|
64
|
+
throw new RangeError("invalid document reference: start must be < end");
|
|
65
|
+
return [reference.slice(0, hash), [start, end]];
|
|
66
|
+
}
|
|
67
|
+
class DocumentView {
|
|
68
|
+
dataId;
|
|
69
|
+
modelFacingId;
|
|
70
|
+
document;
|
|
71
|
+
displayFields;
|
|
72
|
+
snippetDisplaySpans = [];
|
|
73
|
+
constructor(dataId, modelFacingId, document, displayFields) {
|
|
74
|
+
this.dataId = dataId;
|
|
75
|
+
this.modelFacingId = modelFacingId;
|
|
76
|
+
this.document = document;
|
|
77
|
+
this.displayFields = displayFields;
|
|
78
|
+
}
|
|
79
|
+
renderParts(displayFields = this.displayFields) {
|
|
80
|
+
return { id: this.modelFacingId, attributes: new Map(displayFields.map(key => [key, field(this.document, key)])), body: null };
|
|
81
|
+
}
|
|
82
|
+
renderXml() {
|
|
83
|
+
const { id, attributes, body } = this.renderParts();
|
|
84
|
+
const parts = [`id="${escape(id)}"`];
|
|
85
|
+
for (const [key, value] of attributes) {
|
|
86
|
+
if (truthy(value))
|
|
87
|
+
parts.push(typeof value === "boolean" || (typeof value === "number" && Number.isInteger(value))
|
|
88
|
+
? `${key}=${scalar(value)}` : `${key}="${escape(value)}"`);
|
|
89
|
+
}
|
|
90
|
+
const inner = body === null ? "" : `\n${escape(body)}\n`;
|
|
91
|
+
return `<doc ${parts.join(" ")}>${inner}</doc>`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.DocumentView = DocumentView;
|
|
95
|
+
class DocumentViewWithSnippet extends DocumentView {
|
|
96
|
+
snippetField;
|
|
97
|
+
snippetSeenSpans;
|
|
98
|
+
constructor(dataId, modelFacingId, document, snippetField, snippetSeenSpans, snippetDisplaySpans, displayFields) {
|
|
99
|
+
super(dataId, modelFacingId, document, displayFields);
|
|
100
|
+
this.snippetField = snippetField;
|
|
101
|
+
this.snippetSeenSpans = snippetSeenSpans;
|
|
102
|
+
this.snippetDisplaySpans.push(...snippetDisplaySpans);
|
|
103
|
+
}
|
|
104
|
+
renderParts(displayFields = this.displayFields) {
|
|
105
|
+
const content = field(this.document, this.snippetField);
|
|
106
|
+
if (typeof content !== "string" || !content)
|
|
107
|
+
throw new TypeError("snippet field must be a non-empty string");
|
|
108
|
+
const text = new ranges_1.CodePointText(content);
|
|
109
|
+
const segments = [
|
|
110
|
+
...this.snippetDisplaySpans.map(([a, b]) => ({ a, b, seen: false })),
|
|
111
|
+
...this.snippetSeenSpans.map(([a, b]) => ({ a, b, seen: true })),
|
|
112
|
+
].sort((left, right) => left.a - right.a);
|
|
113
|
+
if (!segments.length)
|
|
114
|
+
throw new RangeError("snippet view requires at least one span");
|
|
115
|
+
const a = segments[0].a, b = segments.at(-1).b;
|
|
116
|
+
const fullySeen = this.snippetDisplaySpans.length === 0;
|
|
117
|
+
const body = (a > 0 && !fullySeen ? "... " : "")
|
|
118
|
+
+ segments.map(segment => segment.seen ? `[seen: "#${segment.a}:${segment.b}"]` : text.slice(segment.a, segment.b)).join("")
|
|
119
|
+
+ (b < text.length && !fullySeen ? " ..." : "");
|
|
120
|
+
const id = this.modelFacingId + (!fullySeen && (a !== 0 || b !== text.length) ? `#${a}:${b}` : "");
|
|
121
|
+
const attributes = new Map([["doc_length", text.length]]);
|
|
122
|
+
for (const key of displayFields)
|
|
123
|
+
if (key !== this.snippetField)
|
|
124
|
+
attributes.set(key, field(this.document, key));
|
|
125
|
+
return { id, attributes, body };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
exports.DocumentViewWithSnippet = DocumentViewWithSnippet;
|
|
129
|
+
function renderMarkdownTable(views, displayFields) {
|
|
130
|
+
if (views.length === 0)
|
|
131
|
+
return "";
|
|
132
|
+
const fields = displayFields ?? views[0].displayFields;
|
|
133
|
+
const rows = views.map(view => {
|
|
134
|
+
const { id, attributes, body } = view.renderParts(fields);
|
|
135
|
+
const cells = new Map([["id", id], ...attributes]);
|
|
136
|
+
if (body !== null && view instanceof DocumentViewWithSnippet)
|
|
137
|
+
cells.set(view.snippetField, body);
|
|
138
|
+
return cells;
|
|
139
|
+
});
|
|
140
|
+
const columns = ["id", ...Array.from(rows[0].keys()).filter(key => key !== "id" && !fields.includes(key)), ...fields];
|
|
141
|
+
const cell = (value) => stringify(value).replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
142
|
+
return [
|
|
143
|
+
`| ${columns.join(" | ")} |`,
|
|
144
|
+
`|${columns.map(key => "-".repeat(Array.from(key).length + 2)).join("|")}|`,
|
|
145
|
+
...rows.map(row => `| ${columns.map(key => cell(row.has(key) ? row.get(key) : "")).join(" | ")} |`),
|
|
146
|
+
].join("\n");
|
|
147
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CharacterRange } from "./ranges";
|
|
2
|
+
export declare const SUPPORTED_LANGUAGES: readonly ["danish", "dutch", "english", "finnish", "french", "german", "generic", "hungarian", "italian", "norwegian", "portuguese", "russian", "spanish", "swedish"];
|
|
3
|
+
export type Language = typeof SUPPORTED_LANGUAGES[number];
|
|
4
|
+
export interface SnippetOptions {
|
|
5
|
+
windowSize?: number;
|
|
6
|
+
stride?: number;
|
|
7
|
+
language?: Language;
|
|
8
|
+
}
|
|
9
|
+
export declare function validateLanguage(language: Language): Language;
|
|
10
|
+
export declare function bm25SnippetWithStride(query: string, content: string, { windowSize, stride, language }?: SnippetOptions): CharacterRange;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SUPPORTED_LANGUAGES = void 0;
|
|
4
|
+
exports.validateLanguage = validateLanguage;
|
|
5
|
+
exports.bm25SnippetWithStride = bm25SnippetWithStride;
|
|
6
|
+
const ranges_1 = require("./ranges");
|
|
7
|
+
exports.SUPPORTED_LANGUAGES = ["danish", "dutch", "english", "finnish", "french", "german", "generic", "hungarian", "italian", "norwegian", "portuguese", "russian", "spanish", "swedish"];
|
|
8
|
+
function validateLanguage(language) {
|
|
9
|
+
if (!exports.SUPPORTED_LANGUAGES.includes(language))
|
|
10
|
+
throw new RangeError(`unsupported language ${String(language)}; supported values: ${exports.SUPPORTED_LANGUAGES.join(", ")}`);
|
|
11
|
+
return language;
|
|
12
|
+
}
|
|
13
|
+
let engine;
|
|
14
|
+
function bm25SnippetWithStride(query, content, { windowSize = 50, stride = 10, language = "english" } = {}) {
|
|
15
|
+
if (typeof query !== "string" || typeof content !== "string")
|
|
16
|
+
throw new TypeError("query and content must be strings");
|
|
17
|
+
// wasm-bindgen encodes lone surrogates as U+FFFD; reject instead of silently changing source text.
|
|
18
|
+
if (!isWellFormed(query) || !isWellFormed(content))
|
|
19
|
+
throw new TypeError("query and content must contain well-formed Unicode");
|
|
20
|
+
(0, ranges_1.positiveInteger)(windowSize, "windowSize");
|
|
21
|
+
(0, ranges_1.positiveInteger)(stride, "stride");
|
|
22
|
+
if (windowSize > 0xffffffff || stride > 0xffffffff)
|
|
23
|
+
throw new RangeError("windowSize and stride must fit unsigned 32-bit integers");
|
|
24
|
+
validateLanguage(language);
|
|
25
|
+
engine ??= require("../wasm/sid_snippet.cjs");
|
|
26
|
+
const result = engine.bm25SnippetWithStride(query, content, windowSize, stride, language);
|
|
27
|
+
return [result[0], result[1]];
|
|
28
|
+
}
|
|
29
|
+
function isWellFormed(value) {
|
|
30
|
+
for (const char of value) {
|
|
31
|
+
const cp = char.codePointAt(0);
|
|
32
|
+
if (cp >= 0xd800 && cp <= 0xdfff)
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DocumentCache, SNIPPET_SIZE_DEFAULT, MIN_SEEN_OVERLAP_DEFAULT, DocumentView, DocumentViewWithSnippet, parseRenderedModelFacingId, renderMarkdownTable, InvalidCharacterRange, IdStream, IdSpaceExhausted, bm25SnippetWithStride, SUPPORTED_LANGUAGES } from "../cjs/index.js";
|