@jestek-dev/scripture-engine 0.7.1 → 0.14.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jesse Freeman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @jestek-dev/scripture-engine
2
+
3
+ Pure, deterministic Scripture retrieval and ranking core. Zero I/O, zero
4
+ runtime AI, zero dependencies. Every result carries typed reasons and the
5
+ identities that make it reproducible:
6
+ `(engineVersion, corpusFingerprint, layerFingerprint, query)` yields identical
7
+ ordering on every platform.
8
+
9
+ The engine reads a prebuilt SQLite artifact (`content.db`) through the one
10
+ seam it knows, `ContentQueryPort` — supply it with any SQLite binding
11
+ (`node:sqlite`, OP-SQLite, expo-sqlite):
12
+
13
+ ```ts
14
+ import { createEngine } from '@jestek-dev/scripture-engine';
15
+ import { DatabaseSync } from 'node:sqlite';
16
+
17
+ const db = new DatabaseSync('content.db', { readOnly: true });
18
+ const engine = await createEngine({
19
+ async execute(sql, params = []) { return { rows: db.prepare(sql).all(...params) }; },
20
+ async close() { db.close(); },
21
+ });
22
+
23
+ const result = await engine.research('hearing and doing');
24
+ ```
25
+
26
+ The artifact ships separately as a
27
+ [GitHub Release asset](https://github.com/jestek-dev/scripture-search-engine/releases),
28
+ alongside a reviewed descriptor. Verify `content.db` against the descriptor's
29
+ `databaseSha256` before opening it.
30
+
31
+ Full documentation, architecture, data provenance and the admission gauntlet:
32
+ [jestek-dev/scripture-search-engine](https://github.com/jestek-dev/scripture-search-engine).
33
+
34
+ MIT — the code, not the corpora it is built from; those carry their own terms,
35
+ recorded per source in the repository's `docs/ATTRIBUTIONS.md`.
@@ -8,7 +8,7 @@
8
8
  * tie-breaks — MUST bump this in the same commit. Gate G2 fails a PR whose
9
9
  * ordering changed without a bump, so this is enforced, not merely asked for.
10
10
  */
11
- export declare const ENGINE_VERSION = "0.7.1";
11
+ export declare const ENGINE_VERSION = "0.14.0";
12
12
  /**
13
13
  * Bumped independently of ENGINE_VERSION when the tokenizer changes, because
14
14
  * a tokenizer change invalidates precomputed corpus term profiles: the
@@ -8,7 +8,53 @@
8
8
  * tie-breaks — MUST bump this in the same commit. Gate G2 fails a PR whose
9
9
  * ordering changed without a bump, so this is enforced, not merely asked for.
10
10
  */
11
- export const ENGINE_VERSION = '0.7.1';
11
+ // 0.9.0: query-coverage scaling, IDF-thin bare-concept cue demotion, and
12
+ // one-significant-word phrase fallback suppression. These alter ordering and reasons.
13
+ // 0.10.0: ranking fixes, staged on one branch and landed as one squash. Stage 1
14
+ // (sole-evidence floor): a result whose only evidence is translation_variant is
15
+ // capped at 6 points — below any honest text match — so a bag-of-stems hint can
16
+ // accompany but never hold #1 alone. Later 0.10.0 stages ride this same bump.
17
+ // 0.11.0 (QR-4): reference-grammar extension. Space-separated chapter/verse
18
+ // resolves ("John 3 16"); un-resolving books earn a cited did-you-mean
19
+ // suggestion (suggestion only, never auto-resolve); bare-number dead ends fall
20
+ // through to discovery; the verse-1 range-label defect is fixed ("John 3:1-5"
21
+ // no longer labeled "John 3"). Kind flips and label changes are
22
+ // results-visible, so this bumps in the same commit as the behavior.
23
+ // 0.12.0 (QR-5): deterministic cited spelling correction, with artifact
24
+ // schema v7 (spelling_terms/spelling_deletes precomputed by the pipeline;
25
+ // curated_aliases shipped empty for QR-6). research() discovery substitutes
26
+ // the unique in-policy correction for tokens in NO vocabulary — bounded
27
+ // integer Damerau under the ONE edit-policy table — and every substitution is
28
+ // cited (token chips + the additive `corrections` list). Substituted tokens
29
+ // change which verses surface and how they are explained, so this bumps in
30
+ // the same commit as the behavior and the schema.
31
+ // 0.13.0 (QR-6): curated phrase/hymn aliases (no schema bump — the
32
+ // curated_aliases table shipped EMPTY in v7 and the engine probes
33
+ // presence-and-rows). research() discovery matches the TYPED query's
34
+ // normalizedPhrase (stopwords kept) against curated_aliases by whole-string
35
+ // EQUALITY — never containment — and surfaces the target concept's own
36
+ // curated anchors (or the named verse range) under the existing
37
+ // concept_anchor family with a full attribution chip
38
+ // (`Hymn: "<title>" → Theme: <label>`). New evidence on alias-keyed queries
39
+ // is results-visible, so this bumps in the same commit as the behavior;
40
+ // non-alias queries are untouched by construction (equality matching).
41
+ // Rollback: rebuild the artifact without alias rows (the probe reverts
42
+ // behavior); an engine revert is a new version.
43
+ // 0.14.0 (CO-3 PR 2): passage-level grouping over the schema-v8 pericope
44
+ // tiling. discover() merges ungoverned results that are consecutive in rank,
45
+ // verseId-consecutive, and members of ONE derived pericope into a single
46
+ // passage row; the existing curated-anchor collapse is unchanged in its
47
+ // merge rule but both mechanisms now EXPLAIN the merge — merged rows carry
48
+ // additive `verses[]` (per-member evidence, uncollapsed) and a typed
49
+ // `grouping` naming the section span and its source (anchor sources for
50
+ // anchor runs, 'openbible-sections' + the summed boundary vote for pericope
51
+ // runs; grouping is not a Reason and contributes zero points). Authority
52
+ // order is fixed: an anchor-claimed verse never joins a pericope run.
53
+ // Merged pericope rows change which rows the default page shows, so this
54
+ // bumps in the same commit as the behavior. Rollback: rebuild the artifact
55
+ // without pericope rows (the presence-and-rows probe reverts the pericope
56
+ // path); an engine revert is a new version.
57
+ export const ENGINE_VERSION = '0.14.0';
12
58
  /**
13
59
  * Bumped independently of ENGINE_VERSION when the tokenizer changes, because
14
60
  * a tokenizer change invalidates precomputed corpus term profiles: the
@@ -6,7 +6,7 @@
6
6
  * lives in `ranking/`, so the ranker stays pure and unit-testable without a
7
7
  * database.
8
8
  */
9
- import { type ReferenceResolver, type ResolvedBook, type ResolvedReference } from '../reference/reference.js';
9
+ import { type BookAliasEntry, type ReferenceResolver, type ResolvedBook, type ResolvedReference } from '../reference/reference.js';
10
10
  import type { ContentQueryPort, ScripturePassage, ScriptureVerse } from '../types.js';
11
11
  export declare const MAX_PHRASE_LENGTH = 500;
12
12
  export declare const MAX_CANDIDATES = 200;
@@ -41,6 +41,32 @@ export interface TokenMatch extends ScriptureVerse {
41
41
  /** Distinct significant tokens in this verse — the precision denominator. */
42
42
  readonly distinctTokenCount: number;
43
43
  }
44
+ /**
45
+ * One derived pericope (schema v8, CO-3 PR 1). `boundaryVotes` is the
46
+ * summed boundary vote at `startVerseId` — a countable structural fact
47
+ * (how many of the 20 surveyed translations start a section there), never
48
+ * a relevance score.
49
+ */
50
+ export interface PericopeRow {
51
+ readonly startVerseId: number;
52
+ readonly endVerseId: number;
53
+ readonly boundaryVotes: number;
54
+ readonly sourceId: string;
55
+ }
56
+ /**
57
+ * One mined cross-reference phrase triple (schema v9, B3 Phase A). The
58
+ * `normalizedPhrase` is the tokenizer-normalized key of the TSK entry
59
+ * fragment under which the reference was printed — a lookup key, never
60
+ * displayed prose. Structural lineage only: the row says a curated source
61
+ * printed this link under that phrase; it scores nothing by itself.
62
+ */
63
+ export interface CrossReferencePhraseRow {
64
+ readonly fromVerseId: number;
65
+ readonly normalizedPhrase: string;
66
+ readonly toStartVerseId: number;
67
+ readonly toEndVerseId: number;
68
+ readonly sourceId: string;
69
+ }
44
70
  export interface CorpusMeta {
45
71
  readonly schemaVersion: string;
46
72
  readonly tokenizerVersion: string;
@@ -58,6 +84,15 @@ export interface CorpusMeta {
58
84
  export declare class CorpusRepository implements ReferenceResolver {
59
85
  private readonly database;
60
86
  constructor(database: ContentQueryPort);
87
+ /**
88
+ * The alias vocabulary for the reference did-you-mean (0.11.0/QR-4),
89
+ * fetched through the port once per repository instance and cached: ~270
90
+ * rows that cannot change under a running engine (the artifact is
91
+ * immutable), so re-reading them per query would be waste, and caching
92
+ * keeps the engine's no-I/O covenant intact — the ONE read still goes
93
+ * through ContentQueryPort.
94
+ */
95
+ private bookAliasCache;
61
96
  close(): Promise<void>;
62
97
  readMeta(): Promise<CorpusMeta>;
63
98
  /** Total indexed verses per translation — the N in IDF. */
@@ -65,7 +100,8 @@ export declare class CorpusRepository implements ReferenceResolver {
65
100
  resolveBookAlias(aliasKey: string): Promise<ResolvedBook | null>;
66
101
  getChapterVerseCount(bookId: number, chapter: number): Promise<number | null>;
67
102
  verseExists(bookId: number, chapter: number, verse: number): Promise<boolean>;
68
- resolveReference(input: string): Promise<import("../index.js").ReferenceResolutionAttempt>;
103
+ listBookAliases(): Promise<readonly BookAliasEntry[]>;
104
+ resolveReference(input: string): Promise<import("../internal.js").ReferenceResolutionAttempt>;
69
105
  loadPassage(resolved: ResolvedReference): Promise<ScripturePassage>;
70
106
  /**
71
107
  * Exact phrase via FTS5, ranked by bm25.
@@ -89,6 +125,79 @@ export declare class CorpusRepository implements ReferenceResolver {
89
125
  */
90
126
  searchTokens(tokens: readonly string[], documentCount: number, limit?: number): Promise<readonly TokenMatch[]>;
91
127
  tokenDocumentCounts(tokens: readonly string[]): Promise<ReadonlyMap<string, number>>;
128
+ /**
129
+ * Whether this artifact carries the precomputed spelling index
130
+ * (schema v7, 0.12.0/QR-5). Presence-probed like the other optional
131
+ * layers: a v6 artifact simply has no tables, the probe returns false, and
132
+ * the engine gracefully does not correct — behaving exactly as the
133
+ * pre-spelling engine did. That probe IS the rollback story: rebuild the
134
+ * artifact without the tables and behavior reverts with no engine change.
135
+ */
136
+ hasSpellingIndex(): Promise<boolean>;
137
+ /**
138
+ * Whether this artifact carries the derived pericope tiling (schema v8,
139
+ * CO-3 PR 1). Presence-and-rows probed like the other optional layers: a
140
+ * v7 artifact has no table, an emptied table disables the (future)
141
+ * grouping step silently, and behavior reverts to pre-pericope output
142
+ * with no engine change — the probe IS the rollback story.
143
+ */
144
+ hasPericopes(): Promise<boolean>;
145
+ /**
146
+ * The pericopes containing any of the given verse ids, batched as ONE
147
+ * bounded query over the ranked window (G11): the window's min..max verse
148
+ * span overlaps few pericopes, and the caller maps verses to rows. Rows
149
+ * come back ordered by start verse for platform-stable iteration.
150
+ *
151
+ * NO CALL SITES in discover() yet (CO-3 PR 1 capability): the grouping
152
+ * behavior that consumes this lands with the PR 2 ENGINE_VERSION bump.
153
+ * boundaryVotes is the summed boundary vote at the pericope's start verse
154
+ * — the countable fact the artifact stores, so a future explanation and
155
+ * the shipped data cannot disagree.
156
+ */
157
+ pericopesContaining(verseIds: readonly number[]): Promise<readonly PericopeRow[]>;
158
+ /**
159
+ * Whether this artifact carries the mined TSK cross-reference phrase keys
160
+ * (schema v9, B3 Phase A). Presence-and-rows probed like pericopes: a v8
161
+ * artifact has no table, an emptied table reads false, and (future)
162
+ * phrase-labeled behavior reverts to plain cross_references output with no
163
+ * engine change — the probe IS the rollback story.
164
+ */
165
+ hasCrossReferencePhrases(): Promise<boolean>;
166
+ /**
167
+ * The cross-reference phrase triples whose FROM verse is one of the given
168
+ * verse ids, batched as ONE bounded query over the window's min..max span
169
+ * (G11) and filtered back to the asked-for verses. Rows come back ordered
170
+ * by (from, phrase, start, end, source) for platform-stable iteration —
171
+ * sorted HERE, in engine code, by UTF-16 code units (the same comparison
172
+ * buildConceptLayer's fingerprint feed uses), never by the port's SQL
173
+ * collation: SQLite's BINARY collation compares UTF-8 bytes, which
174
+ * disagrees with JS on some non-ASCII strings, and the ordering contract
175
+ * must not depend on which side compares.
176
+ *
177
+ * NO CALL SITES in discover() yet (B3 Phase A capability): the labeling
178
+ * and off-phrase-discount behavior that consumes this lands with the
179
+ * Phase B ENGINE_VERSION bump behind J26/J55.
180
+ */
181
+ crossReferencePhrasesFor(verseIds: readonly number[]): Promise<readonly CrossReferencePhraseRow[]>;
182
+ /**
183
+ * Which of the given tokens exist in the artifact's spelling vocabulary
184
+ * (corpus tokens ∪ book aliases ∪ lexicon tokens ∪ translation tokens ∪
185
+ * Layer B verse terms).
186
+ * This is the OOV gate's second half: a token with corpus df 0 that is
187
+ * still a known name or curated word is IN vocabulary and never corrected.
188
+ */
189
+ spellingTermsPresent(tokens: readonly string[]): Promise<ReadonlySet<string>>;
190
+ /**
191
+ * Dictionary terms whose precomputed delete variants intersect the given
192
+ * keys — the SymSpell candidate lookup (0.12.0/QR-5). Proposes only: every
193
+ * candidate is re-verified with the bounded Damerau DP before it may win
194
+ * (see intents/spelling.ts). ORDER BY term for a platform-stable row order,
195
+ * though the picker is proven row-order independent anyway.
196
+ */
197
+ spellingCandidates(deleteKeys: readonly string[]): Promise<readonly {
198
+ term: string;
199
+ documentCount: number;
200
+ }[]>;
92
201
  }
93
202
  /**
94
203
  * Finds the LONGEST verbatim fragment of the query present in the corpus.
@@ -121,6 +230,18 @@ export interface ConceptAnchorRow extends ScriptureVerse {
121
230
  readonly sourceId: string;
122
231
  readonly weight: number;
123
232
  readonly locator: string | null;
233
+ /**
234
+ * The curated anchor's OWN span, carried through so results can be presented
235
+ * as the passage a human named rather than as N separate verses.
236
+ *
237
+ * A ranged anchor emits one candidate per verse, and authoritative results
238
+ * are exempt from group diversification by design — so `communion` returned
239
+ * 1 Cor 11:23, :24, :25 and :26 as four identical-scoring results occupying
240
+ * the whole top of the list. The span is the natural unit here because a
241
+ * human chose it; nothing has to be inferred.
242
+ */
243
+ readonly anchorStartVerseId: number;
244
+ readonly anchorEndVerseId: number;
124
245
  }
125
246
  /** A verse reached by a curated cross-reference edge. */
126
247
  export interface CrossReferenceRow extends ScriptureVerse {
@@ -128,6 +249,26 @@ export interface CrossReferenceRow extends ScriptureVerse {
128
249
  readonly sourceId: string;
129
250
  readonly votes: number;
130
251
  }
252
+ /**
253
+ * One curated phrase/hymn alias (0.13.0/QR-6): a whole-query key mapping to
254
+ * exactly one of a curated concept or an explicit verse range (the schema's
255
+ * XOR CHECK). `title` and `locator` surface verbatim in the explanation chip
256
+ * — the attribution IS the product here (covenant 6: the engine reports that
257
+ * a named source connects this phrase to this target; it adjudicates
258
+ * nothing).
259
+ */
260
+ export interface CuratedAliasRow {
261
+ readonly id: number;
262
+ readonly title: string;
263
+ readonly conceptId: string | null;
264
+ /** Label of the target concept; null exactly when conceptId is null. */
265
+ readonly conceptLabel: string | null;
266
+ readonly startVerseId: number | null;
267
+ readonly endVerseId: number | null;
268
+ readonly sourceId: string;
269
+ readonly weight: number;
270
+ readonly locator: string | null;
271
+ }
131
272
  /**
132
273
  * Concept lookup and anchor expansion — Layer A at query time.
133
274
  *
@@ -181,6 +322,26 @@ export declare class ConceptRepository {
181
322
  * it is a long way from the passage saying it. G6 caps it so no volume of
182
323
  * homiletical vocabulary can outrank a curated anchor or a verbatim quote.
183
324
  */
325
+ /**
326
+ * Verses whose CROSS-TRANSLATION vocabulary matches the query.
327
+ *
328
+ * This is what lets someone search in the translation they learned a verse
329
+ * in. The stems here appear in some English translation of the verse but not
330
+ * in the one shipped, so a query using that wording reaches the verse
331
+ * anyway. See pipeline/src/schema.ts for what is and is not stored.
332
+ *
333
+ * Ranked by how MANY query stems a verse accounts for, then by verse id. No
334
+ * IDF weighting: these stems are already the residue after the shipped
335
+ * wording is subtracted, so a stem appearing here is by construction
336
+ * something the shipped text does not say.
337
+ */
338
+ searchTranslationTokens(tokens: readonly string[], limit?: number): Promise<readonly (ScriptureVerse & {
339
+ matchedTokens: readonly string[];
340
+ })[]>;
341
+ /** How many verses carry each stem — the df for weighting alternate wording. */
342
+ translationTokenDocumentCounts(tokens: readonly string[]): Promise<ReadonlyMap<string, number>>;
343
+ /** Whether the artifact carries cross-translation vocabulary at all. */
344
+ hasTranslationTokens(): Promise<boolean>;
184
345
  searchPassageTerms(terms: readonly string[], limit?: number): Promise<readonly (ScriptureVerse & {
185
346
  matchedTerms: readonly string[];
186
347
  pmiSum: number;
@@ -189,5 +350,31 @@ export declare class ConceptRepository {
189
350
  locator: string;
190
351
  })[]>;
191
352
  hasPassageTerms(): Promise<boolean>;
353
+ /**
354
+ * Whether this artifact carries any curated phrase/hymn aliases
355
+ * (0.13.0/QR-6). Presence-AND-ROWS probed, deliberately stricter than the
356
+ * other layer probes: schema v7 ships the table EMPTY (QR-5), and an
357
+ * engine that ran the alias step against an empty table would pay a query
358
+ * per research() call for nothing — and, more importantly, the rollback
359
+ * story is "rebuild without alias rows", which must restore pre-QR-6
360
+ * behavior exactly. No table, or an empty one, and 0.13.0 behaves as
361
+ * 0.12.0 did.
362
+ */
363
+ hasCuratedAliases(): Promise<boolean>;
364
+ /**
365
+ * The curated aliases whose whole-query key equals the given normalized
366
+ * phrase. EQUALITY, never containment — the line that keeps a curated
367
+ * phrase table from becoming a hidden second ranking system; brittleness
368
+ * to extra words is accepted BY DESIGN. `normalized_raw` is UNIQUE, so
369
+ * this returns at most one row; it is typed as a list so the caller does
370
+ * not encode that schema fact.
371
+ */
372
+ matchAliases(normalizedQuery: string): Promise<readonly CuratedAliasRow[]>;
373
+ /**
374
+ * Verses of an explicit alias verse range (the XOR's other arm). A range
375
+ * absent from this corpus returns no rows — the alias then contributes
376
+ * nothing, honestly, rather than being guessed at.
377
+ */
378
+ aliasRangeVerses(startVerseId: number, endVerseId: number): Promise<readonly ScriptureVerse[]>;
192
379
  hasConceptLayer(): Promise<boolean>;
193
380
  }