@jestek-dev/scripture-engine 0.7.0 → 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.
@@ -174,6 +174,21 @@ export function normalizeToken(raw) {
174
174
  * set form used for overlap scoring and concept-lexicon matching.
175
175
  */
176
176
  export function significantWords(text) {
177
+ return significantWordsWithSurface(text).map((entry) => entry.token);
178
+ }
179
+ /**
180
+ * The same significant-token set, each token paired with the SURFACE form it
181
+ * was normalized from — the first raw word (lowercased, punctuation-stripped)
182
+ * that produced it. Added for the spelling-correction citation (0.12.0/QR-5):
183
+ * a correction chip must cite what the user actually typed ("beleived"),
184
+ * never the stem the tokenizer made of it ("beleiv").
185
+ *
186
+ * This is a PAIRING, not a second tokenizer: `significantWords` delegates
187
+ * here, the token stream is byte-identical to what it always was (invariance-
188
+ * tested), and there is still no options parameter. TOKENIZER_VERSION stays
189
+ * 1.0.0.
190
+ */
191
+ export function significantWordsWithSurface(text) {
177
192
  const seen = new Set();
178
193
  const result = [];
179
194
  for (const raw of rawWords(text)) {
@@ -181,10 +196,31 @@ export function significantWords(text) {
181
196
  if (token === null || seen.has(token))
182
197
  continue;
183
198
  seen.add(token);
184
- result.push(token);
199
+ result.push({ token, surface: raw });
185
200
  }
186
201
  return result;
187
202
  }
203
+ /**
204
+ * Whole-query phrase normalization for the curated alias table
205
+ * (0.13.0/QR-6): lowercase, apostrophes removed, punctuation folded to
206
+ * spaces, whitespace collapsed — and NOTHING else. Stopwords are KEPT and no
207
+ * stemming, archaic folding, or lemma lookup applies, because the phrases
208
+ * this key serves are exactly the stopword-heavy lines the token pipeline
209
+ * cannot represent ("it is well with my soul" -> `well soul`). Matching is
210
+ * whole-string EQUALITY against curated_aliases.normalized_raw, never
211
+ * containment, so the minimal normalization is the safety property: the less
212
+ * this folds, the less an alias can accidentally swallow.
213
+ *
214
+ * This is an ADDITIVE surface over the same `rawWords` core every other
215
+ * tokenizer output uses — not a second tokenizer, and there is still no
216
+ * options parameter. The token stream is untouched (invariance-tested) and
217
+ * TOKENIZER_VERSION stays 1.0.0. The pipeline's alias importer imports THIS
218
+ * function (never a mirror), so build-side keys and query-side keys cannot
219
+ * drift.
220
+ */
221
+ export function normalizedPhrase(text) {
222
+ return rawWords(text).join(' ');
223
+ }
188
224
  /**
189
225
  * Positional token stream: every significant occurrence, with the word index
190
226
  * it came from. Proximity scoring (intent 3) needs positions, which the
package/dist/types.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  * none of which the engine knows or cares about.
10
10
  */
11
11
  import type { Reason } from './reasons/types.js';
12
+ import type { ReferenceSuggestion } from './reference/reference.js';
12
13
  export type ContentScalar = string | number | boolean | null | ArrayBuffer | ArrayBufferView;
13
14
  export interface ContentQueryResult {
14
15
  readonly rows: readonly Readonly<Record<string, ContentScalar>>[];
@@ -56,12 +57,94 @@ export interface ResultIdentity {
56
57
  */
57
58
  readonly layerFingerprint: string;
58
59
  }
60
+ /**
61
+ * One member verse of a grouped (merged) result, its evidence uncollapsed
62
+ * (0.14.0/CO-3 PR 2). The merged row's reasons are strongest-per-label across
63
+ * the group; this is where a consumer finds what EACH verse contributed —
64
+ * "every verse's own evidence still visible" is part of the grouping design,
65
+ * not an optional nicety.
66
+ */
67
+ export interface GroupedVerse {
68
+ readonly targetId: string;
69
+ readonly reference: string;
70
+ readonly excerpt: string;
71
+ readonly score: number;
72
+ readonly reasons: readonly Reason[];
73
+ }
74
+ /**
75
+ * Why a merged result's verses travel together (0.14.0/CO-3 PR 2). A typed
76
+ * field, deliberately NOT a `Reason`: reasons must correspond to scoring
77
+ * evidence, and grouping contributes ZERO points — the merged row's score is
78
+ * the max of its members, never a sum. The grouping cites a countable fact
79
+ * and names its source; nothing theological is adjudicated (covenant 6).
80
+ */
81
+ export interface ResultGrouping {
82
+ /**
83
+ * The full span of the grouping unit — the curated anchor span, the alias
84
+ * verse range, or the derived pericope. The result row's own `reference`
85
+ * spans only the verses that actually surfaced (the hits), which may be a
86
+ * subset of this section.
87
+ */
88
+ readonly section: {
89
+ /** Label of the full section span, e.g. "Psalms 136:1-26". */
90
+ readonly reference: string;
91
+ readonly startVerseId: number;
92
+ readonly endVerseId: number;
93
+ };
94
+ readonly provenance: {
95
+ /**
96
+ * Manifest source id the grouping stands on: the anchor's own source(s)
97
+ * for an anchor-span merge ('+'-joined ascending when several agree,
98
+ * e.g. 'editorial'), 'hymn-aliases' for an alias verse range, or
99
+ * 'openbible-sections' for the pericope path. Authority order is fixed:
100
+ * an anchor span is checked first, so pericope provenance never usurps
101
+ * anchor provenance.
102
+ */
103
+ readonly sourceId: string;
104
+ /** Human-facing attribution, e.g. "OpenBible section boundaries (CC BY)". */
105
+ readonly label: string;
106
+ /**
107
+ * Pericope groups only: the summed boundary vote at the section's start
108
+ * verse — the exact number the artifact stores (how many of the 20
109
+ * surveyed translations start a section there). A countable structural
110
+ * fact, never a relevance score; the explanation and the shipped data
111
+ * cannot disagree because this is read from the same row.
112
+ */
113
+ readonly boundaryVotes?: number;
114
+ };
115
+ }
59
116
  export interface DiscoveryResult {
60
117
  readonly targetId: string;
61
118
  readonly reference: string;
62
119
  readonly excerpt: string;
63
120
  readonly score: number;
64
121
  readonly reasons: readonly Reason[];
122
+ /**
123
+ * Additive (0.14.0/CO-3 PR 2): present exactly when this row is a merged
124
+ * passage-level result — the member verses in canonical order, each with
125
+ * its own uncollapsed evidence. Absent on single-verse rows.
126
+ */
127
+ readonly verses?: readonly GroupedVerse[];
128
+ /**
129
+ * Additive (0.14.0/CO-3 PR 2): present exactly when `verses` is — why the
130
+ * members travel together, citing the source that drew the boundary.
131
+ */
132
+ readonly grouping?: ResultGrouping;
133
+ }
134
+ /**
135
+ * One cited spelling correction (0.12.0/QR-5). Corrections are never silent:
136
+ * every substituted token is reported with the SURFACE form the user typed
137
+ * (never the stem the tokenizer made of it), the vocabulary term substituted,
138
+ * and the verified integer Damerau distance that justifies it — the same
139
+ * citation the token chips render as `(corrected from "<typed>")`.
140
+ */
141
+ export interface SpellingCorrection {
142
+ /** What the user typed (lowercased surface form), e.g. "beleived". */
143
+ readonly typed: string;
144
+ /** The vocabulary term substituted, e.g. "believ". */
145
+ readonly corrected: string;
146
+ /** Verified integer Damerau–Levenshtein distance — the citation. */
147
+ readonly distance: number;
65
148
  }
66
149
  export type ResearchOutcome = {
67
150
  readonly kind: 'reference';
@@ -69,10 +152,27 @@ export type ResearchOutcome = {
69
152
  } | {
70
153
  readonly kind: 'invalid-reference';
71
154
  readonly query: string;
155
+ /**
156
+ * Additive (0.11.0/QR-4): a cited did-you-mean on the dead end — the
157
+ * unique in-policy near-miss book, the validated reference it implies,
158
+ * and the edit distance that justifies the guess. Suggestion only:
159
+ * the engine NEVER silently opens a guessed passage; consumers render
160
+ * it as a question ("did you mean Philippians 4:13?").
161
+ */
162
+ readonly suggestion?: ReferenceSuggestion;
72
163
  } | {
73
164
  readonly kind: 'discovery';
74
165
  readonly query: string;
75
166
  readonly results: readonly DiscoveryResult[];
167
+ /**
168
+ * Additive (0.12.0/QR-5): present iff `research()` substituted
169
+ * corrections for out-of-vocabulary tokens — the machine-readable
170
+ * citation consumers render (J32). Absent means nothing was corrected;
171
+ * a word in ANY vocabulary is never rewritten (the OOV gate).
172
+ * `forSong()` never corrects, so its discovery outcome never carries
173
+ * this field.
174
+ */
175
+ readonly corrections?: readonly SpellingCorrection[];
76
176
  };
77
177
  export type ResearchResult = ResearchOutcome & ResultIdentity;
78
178
  /** Concept-resolution output for `engine.themes()`. */
@@ -90,6 +190,8 @@ export type PassageResult = ({
90
190
  } & ResultIdentity) | ({
91
191
  readonly kind: 'invalid-reference';
92
192
  readonly query: string;
193
+ /** Additive (0.11.0/QR-4): see ResearchOutcome's invalid-reference. */
194
+ readonly suggestion?: ReferenceSuggestion;
93
195
  } & ResultIdentity);
94
196
  /**
95
197
  * `engine.related()` — what a curated source connects to a passage.
@@ -107,6 +209,8 @@ export type RelatedResult = ({
107
209
  } & ResultIdentity) | ({
108
210
  readonly kind: 'invalid-reference';
109
211
  readonly query: string;
212
+ /** Additive (0.11.0/QR-4): see ResearchOutcome's invalid-reference. */
213
+ readonly suggestion?: ReferenceSuggestion;
110
214
  } & ResultIdentity);
111
215
  /**
112
216
  * Multi-field input for `engine.forSong()`.
package/package.json CHANGED
@@ -1,14 +1,28 @@
1
1
  {
2
2
  "name": "@jestek-dev/scripture-engine",
3
- "version": "0.7.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "description": "Pure deterministic Scripture retrieval and ranking core. Zero I/O, zero runtime AI.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/jestek-dev/scripture-search-engine.git",
10
+ "directory": "engine"
11
+ },
12
+ "homepage": "https://github.com/jestek-dev/scripture-search-engine#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/jestek-dev/scripture-search-engine/issues"
15
+ },
6
16
  "main": "dist/index.js",
7
17
  "types": "dist/index.d.ts",
8
18
  "exports": {
9
19
  ".": {
10
20
  "types": "./dist/index.d.ts",
11
21
  "default": "./dist/index.js"
22
+ },
23
+ "./internal": {
24
+ "types": "./dist/internal.d.ts",
25
+ "default": "./dist/internal.js"
12
26
  }
13
27
  },
14
28
  "files": [
@@ -21,6 +35,7 @@
21
35
  "test": "vitest run"
22
36
  },
23
37
  "devDependencies": {
38
+ "fast-check": "4.9.0",
24
39
  "typescript": "5.9.3",
25
40
  "vitest": "4.1.10"
26
41
  }