@jestek-dev/scripture-engine 0.7.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.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The engine version participates in the reproducibility contract:
3
+ *
4
+ * (engineVersion, corpusFingerprint, layerFingerprint, query)
5
+ * -> identical ordering
6
+ *
7
+ * Any change that can alter ordering — weights, caps, tokenizer rules,
8
+ * tie-breaks — MUST bump this in the same commit. Gate G2 fails a PR whose
9
+ * ordering changed without a bump, so this is enforced, not merely asked for.
10
+ */
11
+ export declare const ENGINE_VERSION = "0.7.0";
12
+ /**
13
+ * Bumped independently of ENGINE_VERSION when the tokenizer changes, because
14
+ * a tokenizer change invalidates precomputed corpus term profiles: the
15
+ * pipeline and the runtime must tokenize identically or scoring silently
16
+ * compares mismatched vocabularies. The pipeline stamps this into the
17
+ * artifact and the engine refuses an artifact built with a different value.
18
+ */
19
+ export declare const TOKENIZER_VERSION = "1.0.0";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The engine version participates in the reproducibility contract:
3
+ *
4
+ * (engineVersion, corpusFingerprint, layerFingerprint, query)
5
+ * -> identical ordering
6
+ *
7
+ * Any change that can alter ordering — weights, caps, tokenizer rules,
8
+ * tie-breaks — MUST bump this in the same commit. Gate G2 fails a PR whose
9
+ * ordering changed without a bump, so this is enforced, not merely asked for.
10
+ */
11
+ export const ENGINE_VERSION = '0.7.0';
12
+ /**
13
+ * Bumped independently of ENGINE_VERSION when the tokenizer changes, because
14
+ * a tokenizer change invalidates precomputed corpus term profiles: the
15
+ * pipeline and the runtime must tokenize identically or scoring silently
16
+ * compares mismatched vocabularies. The pipeline stamps this into the
17
+ * artifact and the engine refuses an artifact built with a different value.
18
+ */
19
+ export const TOKENIZER_VERSION = '1.0.0';
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Corpus access. The only module that knows SQL.
3
+ *
4
+ * Ported from Maskil's `ScriptureRepository` and extended with the token
5
+ * postings this engine adds. Everything here returns plain data; scoring
6
+ * lives in `ranking/`, so the ranker stays pure and unit-testable without a
7
+ * database.
8
+ */
9
+ import { type ReferenceResolver, type ResolvedBook, type ResolvedReference } from '../reference/reference.js';
10
+ import type { ContentQueryPort, ScripturePassage, ScriptureVerse } from '../types.js';
11
+ export declare const MAX_PHRASE_LENGTH = 500;
12
+ export declare const MAX_CANDIDATES = 200;
13
+ /** One verse that matched a phrase query, with its bm25 relevance. */
14
+ export interface PhraseMatch extends ScriptureVerse {
15
+ /** SQLite bm25: more negative is more relevant. Normalized by the caller. */
16
+ readonly bm25: number;
17
+ }
18
+ /** The longest verbatim fragment of a query that occurs in the corpus. */
19
+ export interface PhraseFragmentResult {
20
+ /** The fragment that matched, as the user wrote it. */
21
+ readonly fragment: string;
22
+ /** Words in the fragment. */
23
+ readonly fragmentWords: number;
24
+ /** Words in the whole query — the denominator for partial-match strength. */
25
+ readonly queryWords: number;
26
+ readonly matches: readonly PhraseMatch[];
27
+ }
28
+ /** One verse that matched at least one query token. */
29
+ export interface TokenMatch extends ScriptureVerse {
30
+ /** Distinct query tokens present in this verse. */
31
+ readonly matchedTokens: readonly string[];
32
+ /** Sum of IDF over matched tokens — rare words contribute more. */
33
+ readonly idfSum: number;
34
+ /**
35
+ * Smallest window (in raw word positions) containing all matched tokens.
36
+ * Null when only one token matched, since a single point has no span.
37
+ */
38
+ readonly minSpan: number | null;
39
+ /** Total significant tokens in this verse. */
40
+ readonly tokenCount: number;
41
+ /** Distinct significant tokens in this verse — the precision denominator. */
42
+ readonly distinctTokenCount: number;
43
+ }
44
+ export interface CorpusMeta {
45
+ readonly schemaVersion: string;
46
+ readonly tokenizerVersion: string;
47
+ readonly corpusFingerprint: string;
48
+ readonly verseCount: number;
49
+ /** Mean significant-token length per verse — the avgdl of length normalization. */
50
+ readonly avgVerseTokens: number;
51
+ /**
52
+ * Identity of the curated layers. Empty string when an artifact has no
53
+ * concept layer, which is a legitimate state (a v1 lexical-only artifact),
54
+ * not a missing value.
55
+ */
56
+ readonly layerFingerprint: string;
57
+ }
58
+ export declare class CorpusRepository implements ReferenceResolver {
59
+ private readonly database;
60
+ constructor(database: ContentQueryPort);
61
+ close(): Promise<void>;
62
+ readMeta(): Promise<CorpusMeta>;
63
+ /** Total indexed verses per translation — the N in IDF. */
64
+ documentCount(): Promise<number>;
65
+ resolveBookAlias(aliasKey: string): Promise<ResolvedBook | null>;
66
+ getChapterVerseCount(bookId: number, chapter: number): Promise<number | null>;
67
+ verseExists(bookId: number, chapter: number, verse: number): Promise<boolean>;
68
+ resolveReference(input: string): Promise<import("../index.js").ReferenceResolutionAttempt>;
69
+ loadPassage(resolved: ResolvedReference): Promise<ScripturePassage>;
70
+ /**
71
+ * Exact phrase via FTS5, ranked by bm25.
72
+ *
73
+ * Runs against the RAW verse text, not our token stream, because "exact"
74
+ * must mean exact — a user asking for a verbatim phrase does not want
75
+ * stemming or archaic folding applied behind their back. The token intent
76
+ * is where fuzziness is allowed, and it says so in its reason label.
77
+ */
78
+ searchPhrase(phrase: string, limit?: number): Promise<readonly PhraseMatch[]>;
79
+ /**
80
+ * Token search over precomputed postings.
81
+ *
82
+ * This is the intent that makes theme-ish queries work before any concept
83
+ * layer exists: query tokens are folded by the same tokenizer the corpus
84
+ * was indexed with, so "hearing and doing" reaches "heareth ... doeth".
85
+ *
86
+ * IDF is computed from stored document counts, which is why keeping common
87
+ * words as tokens is safe — "do" earns a near-zero weight automatically
88
+ * instead of needing a stopword list to predict its unimportance.
89
+ */
90
+ searchTokens(tokens: readonly string[], documentCount: number, limit?: number): Promise<readonly TokenMatch[]>;
91
+ tokenDocumentCounts(tokens: readonly string[]): Promise<ReadonlyMap<string, number>>;
92
+ }
93
+ /**
94
+ * Finds the LONGEST verbatim fragment of the query present in the corpus.
95
+ *
96
+ * Users paraphrase. "be doers of the word not hearers only" is nobody's
97
+ * translation verbatim, but "doers of the word" is exactly James 1:22 — and
98
+ * a search that only tries the whole query throws that away, leaving the
99
+ * decision to weaker signals.
100
+ *
101
+ * Longest-first with early exit: the first fragment length that matches wins,
102
+ * so we never pay for shorter, vaguer fragments once a strong one is found.
103
+ * Strength is computed by the caller as fragmentWords / queryWords, which
104
+ * makes partial verbatim evidence proportional to how much of the question it
105
+ * actually answers — a full verbatim match still earns full authority.
106
+ */
107
+ export declare function searchLongestFragment(repository: CorpusRepository, query: string, limit?: number): Promise<PhraseFragmentResult | null>;
108
+ /** A concept whose lexicon matched the query. */
109
+ export interface ConceptMatchRow {
110
+ readonly conceptId: string;
111
+ readonly label: string;
112
+ /** The author's original phrase that matched — shown, not the normalized form. */
113
+ readonly matchedPhrase: string;
114
+ /** Tokens in the matched phrase; longer phrases are more specific evidence. */
115
+ readonly matchedTokenCount: number;
116
+ }
117
+ /** A verse named by a concept, with the source that named it. */
118
+ export interface ConceptAnchorRow extends ScriptureVerse {
119
+ readonly conceptId: string;
120
+ readonly conceptLabel: string;
121
+ readonly sourceId: string;
122
+ readonly weight: number;
123
+ readonly locator: string | null;
124
+ }
125
+ /** A verse reached by a curated cross-reference edge. */
126
+ export interface CrossReferenceRow extends ScriptureVerse {
127
+ readonly fromVerseId: number;
128
+ readonly sourceId: string;
129
+ readonly votes: number;
130
+ }
131
+ /**
132
+ * Concept lookup and anchor expansion — Layer A at query time.
133
+ *
134
+ * Kept in the repository (not the intents module) because it is SQL; the
135
+ * scoring decisions live in `intents/concept.ts` so they stay pure.
136
+ */
137
+ export declare class ConceptRepository {
138
+ private readonly database;
139
+ constructor(database: ContentQueryPort);
140
+ /**
141
+ * Concepts whose lexicon phrase is fully contained in the query's tokens.
142
+ *
143
+ * Containment, not similarity: "hearing and doing" (tokens hear, do) fires
144
+ * the concept because every token of the lexicon phrase is present. A
145
+ * fuzzy threshold here would be a second, hidden ranking system competing
146
+ * with the real one — the lexicon is curated precisely so matching can be
147
+ * exact and explainable.
148
+ */
149
+ matchConcepts(queryTokens: readonly string[]): Promise<readonly ConceptMatchRow[]>;
150
+ /** Verses anchored by the given concepts. */
151
+ anchorVerses(conceptIds: readonly string[]): Promise<readonly ConceptAnchorRow[]>;
152
+ /**
153
+ * The reverse of anchorVerses: which curated concepts name THIS passage?
154
+ *
155
+ * Powers `related()`, whose contract is "what did a human connect to this
156
+ * text", not "what resembles it". An anchor overlapping the passage at all
157
+ * counts, because a concept anchored to James 1:22-25 is about James 1:23
158
+ * even though it does not name that verse alone.
159
+ */
160
+ conceptsAnchoring(startVerseId: number, endVerseId: number): Promise<readonly {
161
+ conceptId: string;
162
+ label: string;
163
+ }[]>;
164
+ /** Concepts one hop away in the curated graph. */
165
+ relatedConcepts(conceptIds: readonly string[]): Promise<readonly string[]>;
166
+ /**
167
+ * Verses reached by cross-reference from the given seed verses.
168
+ *
169
+ * Bounded per seed: an unbounded expansion would let one well-connected
170
+ * verse flood the candidate set, which is precision erosion by another
171
+ * name.
172
+ */
173
+ expandCrossReferences(fromVerseIds: readonly number[], perSeedLimit?: number): Promise<readonly CrossReferenceRow[]>;
174
+ /** Highest observed vote count, used to normalize vote-derived strength. */
175
+ maxCrossReferenceVotes(): Promise<number>;
176
+ /**
177
+ * Verses whose pericope profile contains query terms (Layer B).
178
+ *
179
+ * Weak evidence by design and by budget: a preacher using a word while
180
+ * expounding a passage is real signal about what the passage is ABOUT, but
181
+ * it is a long way from the passage saying it. G6 caps it so no volume of
182
+ * homiletical vocabulary can outrank a curated anchor or a verbatim quote.
183
+ */
184
+ searchPassageTerms(terms: readonly string[], limit?: number): Promise<readonly (ScriptureVerse & {
185
+ matchedTerms: readonly string[];
186
+ pmiSum: number;
187
+ sourceIds: string;
188
+ minSpanVerses: number;
189
+ locator: string;
190
+ })[]>;
191
+ hasPassageTerms(): Promise<boolean>;
192
+ hasConceptLayer(): Promise<boolean>;
193
+ }