diffwiki-core 0.5.0 → 0.6.0-rc.202607240049.586dcb6
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/dist/bm25.cjs +85 -0
- package/dist/bm25.d.cts +48 -0
- package/dist/bm25.d.ts +48 -0
- package/dist/bm25.js +8 -0
- package/dist/chunk-FTUF6IXS.js +60 -0
- package/dist/{chunk-AZ6SZH6P.js → chunk-MLUJY3HI.js} +24 -1
- package/dist/{events-H3D7FT4O.js → events-UCJWB4WP.js} +1 -1
- package/dist/index.cjs +904 -396
- package/dist/index.d.cts +284 -12
- package/dist/index.d.ts +284 -12
- package/dist/index.js +747 -341
- package/package.json +6 -1
package/dist/bm25.cjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/bm25.ts
|
|
21
|
+
var bm25_exports = {};
|
|
22
|
+
__export(bm25_exports, {
|
|
23
|
+
rankDocs: () => rankDocs,
|
|
24
|
+
tokenize: () => tokenize
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(bm25_exports);
|
|
27
|
+
var K1 = 1.5;
|
|
28
|
+
var B = 0.75;
|
|
29
|
+
var FIELD_WEIGHTS = { title: 3, tags: 2, collection: 1.5, body: 1 };
|
|
30
|
+
function tokenize(text) {
|
|
31
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
|
|
32
|
+
}
|
|
33
|
+
function termFreqs(tokens) {
|
|
34
|
+
const freqs = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const t of tokens) freqs.set(t, (freqs.get(t) ?? 0) + 1);
|
|
36
|
+
return freqs;
|
|
37
|
+
}
|
|
38
|
+
function bm25FieldScore(queryTerms, fieldTokens, avgFieldLen, idf) {
|
|
39
|
+
const tf = termFreqs(fieldTokens);
|
|
40
|
+
const dl = fieldTokens.length;
|
|
41
|
+
const avgDl = avgFieldLen || 1;
|
|
42
|
+
let score = 0;
|
|
43
|
+
for (const term of queryTerms) {
|
|
44
|
+
const freq = tf.get(term) ?? 0;
|
|
45
|
+
if (freq === 0) continue;
|
|
46
|
+
const termIdf = idf.get(term) ?? 0;
|
|
47
|
+
const numerator = freq * (K1 + 1);
|
|
48
|
+
const denominator = freq + K1 * (1 - B + B * (dl / avgDl));
|
|
49
|
+
score += termIdf * (numerator / denominator);
|
|
50
|
+
}
|
|
51
|
+
return score;
|
|
52
|
+
}
|
|
53
|
+
function rankDocs(term, docs) {
|
|
54
|
+
const queryTerms = tokenize(term);
|
|
55
|
+
if (queryTerms.length === 0) return docs.map((d) => ({ ...d }));
|
|
56
|
+
const N = docs.length;
|
|
57
|
+
const docTokenSets = docs.map(
|
|
58
|
+
(d) => /* @__PURE__ */ new Set([...d.fields.title, ...d.fields.tags, ...d.fields.collection, ...d.fields.body])
|
|
59
|
+
);
|
|
60
|
+
const idf = /* @__PURE__ */ new Map();
|
|
61
|
+
for (const qt of queryTerms) {
|
|
62
|
+
let docFreq = 0;
|
|
63
|
+
for (const set of docTokenSets) if (set.has(qt)) docFreq++;
|
|
64
|
+
idf.set(qt, Math.log((N - docFreq + 0.5) / (docFreq + 0.5) + 1));
|
|
65
|
+
}
|
|
66
|
+
const avg = (sel) => docs.reduce((s, d) => s + sel(d), 0) / (N || 1);
|
|
67
|
+
const avgLens = {
|
|
68
|
+
title: avg((d) => d.fields.title.length),
|
|
69
|
+
tags: avg((d) => d.fields.tags.length),
|
|
70
|
+
collection: avg((d) => d.fields.collection.length),
|
|
71
|
+
body: avg((d) => d.fields.body.length)
|
|
72
|
+
};
|
|
73
|
+
const scored = [];
|
|
74
|
+
for (const d of docs) {
|
|
75
|
+
const total = bm25FieldScore(queryTerms, d.fields.title, avgLens.title, idf) * FIELD_WEIGHTS.title + bm25FieldScore(queryTerms, d.fields.tags, avgLens.tags, idf) * FIELD_WEIGHTS.tags + bm25FieldScore(queryTerms, d.fields.collection, avgLens.collection, idf) * FIELD_WEIGHTS.collection + bm25FieldScore(queryTerms, d.fields.body, avgLens.body, idf) * FIELD_WEIGHTS.body;
|
|
76
|
+
if (total > 0) scored.push({ ...d, score: total });
|
|
77
|
+
}
|
|
78
|
+
scored.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
79
|
+
return scored;
|
|
80
|
+
}
|
|
81
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
82
|
+
0 && (module.exports = {
|
|
83
|
+
rankDocs,
|
|
84
|
+
tokenize
|
|
85
|
+
});
|
package/dist/bm25.d.cts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, runtime-agnostic BM25 ranking.
|
|
3
|
+
*
|
|
4
|
+
* This module has ZERO Node/filesystem imports, so the exact same ranking runs on
|
|
5
|
+
* the server (native search over files) and in the browser (the static site, over
|
|
6
|
+
* a pre-built index). It is exposed browser-side via the `diffwiki-core/bm25`
|
|
7
|
+
* subpath export so the client bundle never pulls in Node-only core code.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
/** Tokenize a string: lowercase, split on non-alphanumeric, drop empties. */
|
|
12
|
+
declare function tokenize(text: string): string[];
|
|
13
|
+
/** Pre-tokenized, weighted fields of a rankable document. */
|
|
14
|
+
interface RankableFields {
|
|
15
|
+
title: string[];
|
|
16
|
+
tags: string[];
|
|
17
|
+
collection: string[];
|
|
18
|
+
body: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A document in the serialisable search index: routing metadata (for linking to
|
|
22
|
+
* the article) plus the pre-tokenized fields BM25 ranks. The static exporter
|
|
23
|
+
* writes `SearchDoc[]` to `search-index.json`; the browser ranks it with
|
|
24
|
+
* {@link rankDocs}.
|
|
25
|
+
*/
|
|
26
|
+
interface SearchDoc {
|
|
27
|
+
collection: string;
|
|
28
|
+
slug: string;
|
|
29
|
+
title: string;
|
|
30
|
+
tags: string[];
|
|
31
|
+
fields: RankableFields;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Rank pre-tokenized docs against a query with BM25 over the four weighted fields
|
|
35
|
+
* (title ×3, tags ×2, collection ×1.5, body ×1). Pure — no I/O.
|
|
36
|
+
*
|
|
37
|
+
* An empty/blank query returns every doc unranked (browse-all), preserving the
|
|
38
|
+
* native search's behaviour. Otherwise only matching docs (score > 0) are
|
|
39
|
+
* returned, highest score first. Each returned doc keeps its original shape plus
|
|
40
|
+
* a `score`.
|
|
41
|
+
*/
|
|
42
|
+
declare function rankDocs<T extends {
|
|
43
|
+
fields: RankableFields;
|
|
44
|
+
}>(term: string, docs: T[]): Array<T & {
|
|
45
|
+
score?: number;
|
|
46
|
+
}>;
|
|
47
|
+
|
|
48
|
+
export { type RankableFields, type SearchDoc, rankDocs, tokenize };
|
package/dist/bm25.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, runtime-agnostic BM25 ranking.
|
|
3
|
+
*
|
|
4
|
+
* This module has ZERO Node/filesystem imports, so the exact same ranking runs on
|
|
5
|
+
* the server (native search over files) and in the browser (the static site, over
|
|
6
|
+
* a pre-built index). It is exposed browser-side via the `diffwiki-core/bm25`
|
|
7
|
+
* subpath export so the client bundle never pulls in Node-only core code.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
/** Tokenize a string: lowercase, split on non-alphanumeric, drop empties. */
|
|
12
|
+
declare function tokenize(text: string): string[];
|
|
13
|
+
/** Pre-tokenized, weighted fields of a rankable document. */
|
|
14
|
+
interface RankableFields {
|
|
15
|
+
title: string[];
|
|
16
|
+
tags: string[];
|
|
17
|
+
collection: string[];
|
|
18
|
+
body: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A document in the serialisable search index: routing metadata (for linking to
|
|
22
|
+
* the article) plus the pre-tokenized fields BM25 ranks. The static exporter
|
|
23
|
+
* writes `SearchDoc[]` to `search-index.json`; the browser ranks it with
|
|
24
|
+
* {@link rankDocs}.
|
|
25
|
+
*/
|
|
26
|
+
interface SearchDoc {
|
|
27
|
+
collection: string;
|
|
28
|
+
slug: string;
|
|
29
|
+
title: string;
|
|
30
|
+
tags: string[];
|
|
31
|
+
fields: RankableFields;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Rank pre-tokenized docs against a query with BM25 over the four weighted fields
|
|
35
|
+
* (title ×3, tags ×2, collection ×1.5, body ×1). Pure — no I/O.
|
|
36
|
+
*
|
|
37
|
+
* An empty/blank query returns every doc unranked (browse-all), preserving the
|
|
38
|
+
* native search's behaviour. Otherwise only matching docs (score > 0) are
|
|
39
|
+
* returned, highest score first. Each returned doc keeps its original shape plus
|
|
40
|
+
* a `score`.
|
|
41
|
+
*/
|
|
42
|
+
declare function rankDocs<T extends {
|
|
43
|
+
fields: RankableFields;
|
|
44
|
+
}>(term: string, docs: T[]): Array<T & {
|
|
45
|
+
score?: number;
|
|
46
|
+
}>;
|
|
47
|
+
|
|
48
|
+
export { type RankableFields, type SearchDoc, rankDocs, tokenize };
|
package/dist/bm25.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/bm25.ts
|
|
2
|
+
var K1 = 1.5;
|
|
3
|
+
var B = 0.75;
|
|
4
|
+
var FIELD_WEIGHTS = { title: 3, tags: 2, collection: 1.5, body: 1 };
|
|
5
|
+
function tokenize(text) {
|
|
6
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
|
|
7
|
+
}
|
|
8
|
+
function termFreqs(tokens) {
|
|
9
|
+
const freqs = /* @__PURE__ */ new Map();
|
|
10
|
+
for (const t of tokens) freqs.set(t, (freqs.get(t) ?? 0) + 1);
|
|
11
|
+
return freqs;
|
|
12
|
+
}
|
|
13
|
+
function bm25FieldScore(queryTerms, fieldTokens, avgFieldLen, idf) {
|
|
14
|
+
const tf = termFreqs(fieldTokens);
|
|
15
|
+
const dl = fieldTokens.length;
|
|
16
|
+
const avgDl = avgFieldLen || 1;
|
|
17
|
+
let score = 0;
|
|
18
|
+
for (const term of queryTerms) {
|
|
19
|
+
const freq = tf.get(term) ?? 0;
|
|
20
|
+
if (freq === 0) continue;
|
|
21
|
+
const termIdf = idf.get(term) ?? 0;
|
|
22
|
+
const numerator = freq * (K1 + 1);
|
|
23
|
+
const denominator = freq + K1 * (1 - B + B * (dl / avgDl));
|
|
24
|
+
score += termIdf * (numerator / denominator);
|
|
25
|
+
}
|
|
26
|
+
return score;
|
|
27
|
+
}
|
|
28
|
+
function rankDocs(term, docs) {
|
|
29
|
+
const queryTerms = tokenize(term);
|
|
30
|
+
if (queryTerms.length === 0) return docs.map((d) => ({ ...d }));
|
|
31
|
+
const N = docs.length;
|
|
32
|
+
const docTokenSets = docs.map(
|
|
33
|
+
(d) => /* @__PURE__ */ new Set([...d.fields.title, ...d.fields.tags, ...d.fields.collection, ...d.fields.body])
|
|
34
|
+
);
|
|
35
|
+
const idf = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const qt of queryTerms) {
|
|
37
|
+
let docFreq = 0;
|
|
38
|
+
for (const set of docTokenSets) if (set.has(qt)) docFreq++;
|
|
39
|
+
idf.set(qt, Math.log((N - docFreq + 0.5) / (docFreq + 0.5) + 1));
|
|
40
|
+
}
|
|
41
|
+
const avg = (sel) => docs.reduce((s, d) => s + sel(d), 0) / (N || 1);
|
|
42
|
+
const avgLens = {
|
|
43
|
+
title: avg((d) => d.fields.title.length),
|
|
44
|
+
tags: avg((d) => d.fields.tags.length),
|
|
45
|
+
collection: avg((d) => d.fields.collection.length),
|
|
46
|
+
body: avg((d) => d.fields.body.length)
|
|
47
|
+
};
|
|
48
|
+
const scored = [];
|
|
49
|
+
for (const d of docs) {
|
|
50
|
+
const total = bm25FieldScore(queryTerms, d.fields.title, avgLens.title, idf) * FIELD_WEIGHTS.title + bm25FieldScore(queryTerms, d.fields.tags, avgLens.tags, idf) * FIELD_WEIGHTS.tags + bm25FieldScore(queryTerms, d.fields.collection, avgLens.collection, idf) * FIELD_WEIGHTS.collection + bm25FieldScore(queryTerms, d.fields.body, avgLens.body, idf) * FIELD_WEIGHTS.body;
|
|
51
|
+
if (total > 0) scored.push({ ...d, score: total });
|
|
52
|
+
}
|
|
53
|
+
scored.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
54
|
+
return scored;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
tokenize,
|
|
59
|
+
rankDocs
|
|
60
|
+
};
|
|
@@ -96,11 +96,27 @@ var InvalidTargetError = class extends DiffwikiError {
|
|
|
96
96
|
};
|
|
97
97
|
var PluginError = class extends DiffwikiError {
|
|
98
98
|
};
|
|
99
|
+
var WorktreeNotAllowedError = class extends DiffwikiError {
|
|
100
|
+
constructor(root) {
|
|
101
|
+
super(`refusing to add a linked worktree \u2014 add the source repository instead (${root})`);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var CollectionAlreadyAddedError = class extends DiffwikiError {
|
|
105
|
+
constructor(name) {
|
|
106
|
+
super(`this repository is already added as collection "${name}"`);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var InvalidExportSelectionError = class extends DiffwikiError {
|
|
110
|
+
constructor(problems) {
|
|
111
|
+
super(`export aborted \u2014 ${problems.length} selection problem(s):
|
|
112
|
+
${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
99
115
|
|
|
100
116
|
// src/collections.ts
|
|
101
117
|
async function fireHook(event, collection) {
|
|
102
118
|
try {
|
|
103
|
-
const { emitPluginEvent: emitPluginEvent2 } = await import("./events-
|
|
119
|
+
const { emitPluginEvent: emitPluginEvent2 } = await import("./events-UCJWB4WP.js");
|
|
104
120
|
await emitPluginEvent2(event, collection);
|
|
105
121
|
} catch {
|
|
106
122
|
}
|
|
@@ -111,6 +127,9 @@ async function listCollections() {
|
|
|
111
127
|
async function findCollection(name) {
|
|
112
128
|
return (await readRegistry()).collections.find((c) => c.name === name);
|
|
113
129
|
}
|
|
130
|
+
async function findCollectionById(id) {
|
|
131
|
+
return (await readRegistry()).collections.find((c) => c.id === id);
|
|
132
|
+
}
|
|
114
133
|
async function createCollection(name) {
|
|
115
134
|
if (await findCollection(name)) throw new CollectionExistsError(name);
|
|
116
135
|
const dir = collectionDir(name);
|
|
@@ -452,6 +471,9 @@ export {
|
|
|
452
471
|
ArticleNotFoundError,
|
|
453
472
|
InvalidTargetError,
|
|
454
473
|
PluginError,
|
|
474
|
+
WorktreeNotAllowedError,
|
|
475
|
+
CollectionAlreadyAddedError,
|
|
476
|
+
InvalidExportSelectionError,
|
|
455
477
|
resolveHome,
|
|
456
478
|
registryPath,
|
|
457
479
|
configPath,
|
|
@@ -479,6 +501,7 @@ export {
|
|
|
479
501
|
emitPluginEvent,
|
|
480
502
|
listCollections,
|
|
481
503
|
findCollection,
|
|
504
|
+
findCollectionById,
|
|
482
505
|
createCollection,
|
|
483
506
|
removeCollection
|
|
484
507
|
};
|