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 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,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-H3D7FT4O.js");
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
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  emitPluginEvent
3
- } from "./chunk-AZ6SZH6P.js";
3
+ } from "./chunk-MLUJY3HI.js";
4
4
  export {
5
5
  emitPluginEvent
6
6
  };