diffwiki-core 0.4.0 → 0.5.0-rc.202607230329.fb70592

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 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
+ });
@@ -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,8 @@
1
+ import {
2
+ rankDocs,
3
+ tokenize
4
+ } from "./chunk-FTUF6IXS.js";
5
+ export {
6
+ rankDocs,
7
+ tokenize
8
+ };
@@ -0,0 +1,145 @@
1
+ // src/plugins/sdk.ts
2
+ import { execFile } from "child_process";
3
+ import { promisify } from "util";
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { dirname, join } from "path";
6
+ import { fileURLToPath } from "url";
7
+ var execFileAsync = promisify(execFile);
8
+ async function runProcess(file, args, opts) {
9
+ const { stdout, stderr } = await execFileAsync(file, args, {
10
+ env: opts?.env,
11
+ timeout: opts?.timeout,
12
+ // Large: qmd emits multi-MB JSON search payloads that overflow the default 1 MB.
13
+ maxBuffer: 50 * 1024 * 1024
14
+ });
15
+ return { stdout, stderr };
16
+ }
17
+ async function runProcessJson(file, args, opts) {
18
+ const { stdout } = await runProcess(file, args, opts);
19
+ try {
20
+ return JSON.parse(stdout);
21
+ } catch {
22
+ throw new Error(`${file} ${args.join(" ")} did not return valid JSON: ${stdout.slice(0, 200)}`);
23
+ }
24
+ }
25
+ async function commandExists(name) {
26
+ try {
27
+ await execFileAsync(name, ["--version"]);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+ function resolveNpmBin(pkg, command, fromUrl) {
34
+ try {
35
+ let dir = dirname(fileURLToPath(fromUrl));
36
+ for (; ; ) {
37
+ const pkgJson = join(dir, "node_modules", pkg, "package.json");
38
+ if (existsSync(pkgJson)) {
39
+ const raw = JSON.parse(readFileSync(pkgJson, "utf8"));
40
+ const rel = typeof raw.bin === "string" ? raw.bin : raw.bin?.[command];
41
+ if (!rel) return null;
42
+ const binPath = join(dirname(pkgJson), rel);
43
+ return existsSync(binPath) ? binPath : null;
44
+ }
45
+ const parent = dirname(dir);
46
+ if (parent === dir) return null;
47
+ dir = parent;
48
+ }
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ async function resolveDependency(dep, fromUrl) {
54
+ if (dep.npm) {
55
+ const local = resolveNpmBin(dep.npm, dep.command, fromUrl);
56
+ if (local) return { dependency: dep, command: [local], source: "npm" };
57
+ }
58
+ if (await commandExists(dep.command)) {
59
+ return { dependency: dep, command: [dep.command], source: "path" };
60
+ }
61
+ return { dependency: dep, command: null, source: "missing" };
62
+ }
63
+ function definePlugin(def) {
64
+ const { name, kind, version, capabilities, dependencies, handlers } = def;
65
+ const describeResult = {
66
+ name,
67
+ kind,
68
+ version,
69
+ capabilities: {
70
+ searchTypes: capabilities.searchTypes,
71
+ collectionFilter: capabilities.collectionFilter,
72
+ events: capabilities.events
73
+ },
74
+ dependencies: (dependencies ?? []).map((d) => ({ command: d.command, npm: d.npm, hint: d.hint }))
75
+ };
76
+ async function dispatch(op, params, log) {
77
+ switch (op) {
78
+ case "describe":
79
+ return describeResult;
80
+ case "search":
81
+ return handlers.search(params, log);
82
+ case "index":
83
+ if (handlers.index) return handlers.index(params, log);
84
+ return { indexed: 0 };
85
+ case "setup":
86
+ if (handlers.setup) return handlers.setup(params, log);
87
+ return { ready: true };
88
+ case "event":
89
+ if (handlers.onEvent) return handlers.onEvent(params, log);
90
+ return { accepted: true };
91
+ case "health":
92
+ if (handlers.health) return handlers.health(params, log);
93
+ return { ready: true, diagnostics: [] };
94
+ default:
95
+ throw new Error(`unknown op: ${op}`);
96
+ }
97
+ }
98
+ function run() {
99
+ const log = (level, message) => {
100
+ process.stderr.write(JSON.stringify({ log: { level, message } }) + "\n");
101
+ };
102
+ let buffer = "";
103
+ process.stdin.setEncoding("utf8");
104
+ process.stdin.on("data", (chunk) => {
105
+ buffer += chunk;
106
+ });
107
+ process.stdin.on("end", () => {
108
+ const line = buffer.trim();
109
+ if (!line) {
110
+ process.stderr.write(`${name}: no input received on stdin
111
+ `);
112
+ process.exit(1);
113
+ return;
114
+ }
115
+ let req;
116
+ try {
117
+ req = JSON.parse(line);
118
+ } catch {
119
+ process.stderr.write(`${name}: malformed JSON on stdin: ${line.slice(0, 200)}
120
+ `);
121
+ process.exit(1);
122
+ return;
123
+ }
124
+ dispatch(req.op, req.params ?? {}, log).then((result) => {
125
+ const response = { ok: true, result };
126
+ process.stdout.write(JSON.stringify(response) + "\n");
127
+ process.exit(0);
128
+ }).catch((err) => {
129
+ const message = err instanceof Error ? err.message : String(err);
130
+ const response = { ok: false, error: { message } };
131
+ process.stdout.write(JSON.stringify(response) + "\n");
132
+ process.exit(1);
133
+ });
134
+ });
135
+ }
136
+ return { definition: def, run };
137
+ }
138
+
139
+ export {
140
+ runProcess,
141
+ runProcessJson,
142
+ commandExists,
143
+ resolveDependency,
144
+ definePlugin
145
+ };
@@ -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
+ };