@seanhogg/builderforce-memory 2026.9.31 → 2026.9.32

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,395 @@
1
+ /**
2
+ * wordLexicon — a compact, zero-dependency lexicon + language guard used by the text
3
+ * coherence gate to catch the ONE failure mode the structural checks cannot see: an
4
+ * under-trained byte-BPE head emitting fluent-SHAPED prose made of INVENTED WORDS that
5
+ * balances its punctuation and never repeats itself ("…oredionisiing chats code related
6
+ * tot, bound reposea this inatic exie. The cainstiel.ts, ore…").
7
+ *
8
+ * Every structural signal (repetition, dominant-token collapse, replacement chars,
9
+ * orphaned delimiters) is blind to that text, and — measured on real samples —
10
+ * no dictionary-free feature separates it: long-token share, type-token ratio and
11
+ * character-cluster anomalies all score German/technical prose *worse* than the
12
+ * gibberish. So this module supplies the missing signal: **is this token a word?**
13
+ *
14
+ * Three deliberate design constraints, because a false positive here silently drops a
15
+ * legitimate reply:
16
+ *
17
+ * 1. **Language guard.** The lexicon is English-only, so it is consulted ONLY when the
18
+ * text is Latin-script AND English wins the function-word vote against es/fr/de/pt/it.
19
+ * A Spanish, French, German or Chinese reply is never judged by it. See
20
+ * {@link detectLatinLanguage}.
21
+ * 2. **Small lexicon + generous escapes.** ~2.5k high-frequency stems (plus suffix
22
+ * stripping) can't possibly cover domain jargon, so a token also counts as known
23
+ * when it REPEATS in the text or appears in the caller's context (the prompt/question).
24
+ * Real jargon echoes the question or recurs; invented gibberish words are each
25
+ * different — that asymmetry is the observed signature.
26
+ * 3. **Code is excluded, not judged.** Identifiers, paths, URLs, numbers and fenced
27
+ * blocks are stripped before scoring — they are legitimately non-words.
28
+ *
29
+ * Zero-dep and pure (no db/env/engine imports) so it can sit under `textCoherence`,
30
+ * which every surface imports.
31
+ */
32
+
33
+ /**
34
+ * High-frequency English stems. Deliberately a STEM list — {@link isKnownEnglishWord}
35
+ * strips regular inflections, so `merge` covers merged/merges/merging/merger(s).
36
+ * Written as space-separated groups purely for readability; deduped by the Set.
37
+ */
38
+ const ENGLISH_STEMS_RAW = [
39
+ // Function words / pronouns / determiners / prepositions / conjunctions.
40
+ `a an the this that these those there here it its it's they them their theirs he him his she her hers we us our ours you your yours i me my mine who whom whose which what when where why how
41
+ and or but nor so yet for as if then than because while although though unless until since whether either neither both each every all any some none no not none other another same such own
42
+ is am are was were be been being do does did done doing have has had having will would shall should can could may might must ought need dare used
43
+ in on at by to from of with without within into onto out off over under above below between among across through during before after around near behind beyond against about upon toward towards along beside besides despite per via versus
44
+ very too also just only even still already almost enough quite rather almost mostly always never often sometimes usually rarely again once twice more most less least much many few several little lot lots
45
+ here there now then today tonight tomorrow yesterday soon later early late ago
46
+ yes no maybe perhaps please thanks thank hello hi okay ok sure`,
47
+
48
+ // Core verbs.
49
+ `accept access add adjust admit adopt advise affect afford agree allow analyse analyze announce answer apply appear approve argue arise arrange arrive ask assign assume attach attempt attend avoid
50
+ base become begin believe belong break bring build buy calculate call cancel capture carry catch cause change charge check choose claim clean clear click close collect combine come comment commit compare complete compute concern confirm connect consider consist contain continue contribute control convert copy correct cost cover create cut
51
+ deal decide declare decrease define delete deliver depend describe design detect determine develop die differ disable discover discuss display divide document download draw drive drop
52
+ earn edit effect elect enable encourage end enforce engage enjoy ensure enter escape establish estimate evaluate examine exceed exchange exclude execute exist expand expect experience explain explore export express extend extract
53
+ face fail fall fetch fight fill filter find finish fit fix flag flow focus follow force forget form format forward free fund
54
+ gain gather generate get give go grant grow guess guide handle happen help hide hire hit hold hope host identify ignore illustrate imagine implement import improve include increase indicate influence inform initialise initialize insert inspect install instruct intend introduce invest invite involve issue
55
+ join judge keep kill know label last launch lead learn leave lend let level lie lift like limit link list listen live load locate lock log look lose love
56
+ maintain make manage map mark match matter mean measure meet mention merge migrate mind miss mix modify monitor move
57
+ name navigate need note notice notify observe obtain occur offer open operate order organise organize outline overwrite own
58
+ pack parse partner pass pause pay perform permit persist pick place plan play point poll post practise practice predict prefer prepare present preserve press prevent print process produce program promise promote propose protect prove provide publish pull purchase push put
59
+ qualify query queue quit quote raise rank reach react read realise realize receive recognise recognize recommend record recover reduce refer reflect refresh refuse register regret reject relate release rely remain remember remind remove rename render repair repeat replace reply report represent request require reset resolve respond rest restart restore restrict result resume retain retry return reveal review revert reward rewrite roll run
60
+ save say scale scan schedule score search secure seed seek seem select sell send separate serve set settle share ship show shut sign signal simplify sit skip sleep slow solve sort sound speak specify spend split spread stand start state stay step stick stop store stream stretch strike study submit succeed suggest supply support suppose surface survive suspend sustain switch sync
61
+ take talk target teach tell tend test thank think threaten throw tie touch trace track trade train transfer transform translate travel treat trigger trust try turn
62
+ understand undo unlock update upgrade upload use validate value verify view visit vote wait wake walk want warn wash watch wear win wish wonder work worry wrap write`,
63
+
64
+ // Core nouns.
65
+ `ability access account action activity address advantage advice age agency agent agreement aim air alert amount analysis answer api app application approach approval area argument array article aspect asset assistant attempt attention audience audit author authority availability average
66
+ background backup balance bar base baseline batch behaviour behavior benchmark benefit bill block board body book bottom boundary branch brand break budget bug build building business button
67
+ cache calendar call capability capacity card care career case cash category cause cell center centre chain chance change channel chapter character chart chat check child choice church city claim class client cloud code collection college colour color column combination command comment commit committee community company comparison competition component computer concept concern condition conference config configuration confidence connection consequence consideration content context contract contrast contribution control conversation copy core corner cost council count country couple course court cover credit crisis criteria culture currency current customer cycle
68
+ damage data database date day deal debt decade decision default defence defense degree delay demand department dependency deployment depth description design desk detail development device diagram dialog difference difficulty direction director discussion disk display distance distribution district document documentation dollar domain door draft drive driver drop duration duty
69
+ earth economy edge edition editor education effect effort element email emergency employee employer end energy engine engineer entry environment episode equipment error estate estimate evaluation evening event evidence example exception exchange exercise existence expense experience experiment expert explanation export expression extension
70
+ face fact factor failure family fault feature fee feed feedback field figure file film filter finance finding fix flag flow focus folder food foot force forecast form format formula forum foundation frame framework friend front function fund future
71
+ game gap gateway generation girl goal government grade graph group growth guide
72
+ habit half hand handler hardware head header health heart height help history hit hold home hook hope host hour house human hypothesis
73
+ icon idea identity image impact implementation import importance improvement incident income increase index indicator individual industry influence information infrastructure initiative input insight instance institution instruction integration intent interaction interest interface internet interval interview introduction investment issue item
74
+ job join journey judgement judgment jump
75
+ key keyword kind king kitchen knowledge
76
+ lab label lack land language latency law layer layout leader leadership league learning length lesson letter level library licence license life light limit line link list literature load loan location lock log logic login loop loss love
77
+ machine magazine mail main maintenance majority manager map margin mark market material matter meaning measure media meeting member memory message metadata method metric middle migration milestone military million mind minute mission mistake mode model module moment money monitor month morning mother motion mountain mouse movement movie music
78
+ name nation nature need network news night node noise note notice notification number
79
+ object objective observation offer office officer operation opinion opportunity option order organisation organization origin outcome output overhead overview owner
80
+ package page pain pair panel paper paragraph parameter parent park part participant partner party password patch path pattern payment peace people percent performance period permission person perspective phase phone photo phrase picture piece pipeline place plan plant platform player point policy politics pool population port portfolio position possibility post potential power practice practise prediction preference presence president pressure price principle priority privacy problem procedure process product profile program progress project promise proof property proposal protection protocol provider public purpose
81
+ quality quantity query question queue quota
82
+ race radio range rank rate ratio reaction reader reality reason record recovery reference region registry regression relation relationship release relevance reliability remainder report repository representation request requirement research resolution resource response responsibility rest restriction result retry return revenue review reward risk road role room root round route row rule run runtime
83
+ safety sale sample scale scenario schedule scheme school science scope score screen script search season seat second secret section sector security seed segment selection sense sentence sequence series server service session set setting shape share sheet shell shift ship shop side sign signal significance similarity site situation size skill sky sleep slot snapshot society software solution son song sound source space span speaker specification speech speed spirit sport spot spread stack staff stage standard star start state statement station statistic status step stock storage store story strategy stream street strength stress string structure student study style subject subset success summary supply support surface survey symbol syntax system
84
+ table tag target task tax team technique technology telephone television temperature template term test text theme theory thing thought thread threshold ticket time timeline timeout title today token tool top topic total town track trade traffic training transaction transfer transition tree trend trial trip trouble truth turn type
85
+ understanding union unit university update upgrade usage user utility
86
+ value variable variant variety vector vendor version video view viewer village vision visitor voice volume vote
87
+ wall war warning water wave way weather web website week weight west wheel while window winter woman word work worker workflow world worth writer writing
88
+ yard year youth zone`,
89
+
90
+ // Adjectives / adverbs / qualifiers.
91
+ `able absolute abstract active actual additional adequate advanced adverse affordable aggregate alternative ambiguous ancient annual anonymous apparent appropriate arbitrary asynchronous atomic automatic available average aware awful
92
+ bad basic beautiful best better big binary blank blue bold boring bottom brief bright broad broken busy
93
+ calm capable careful central certain cheap chief civil classic clean clear clever close closed cold collective comfortable commercial common competitive complete complex comprehensive concrete confident confusing conscious consistent constant convenient cool core correct costly creative critical crucial cultural current custom
94
+ daily dangerous dark dead deep default defensive deliberate dense dependent desirable detailed different difficult digital direct dirty disabled distinct distributed diverse domestic dominant double downstream dry due dynamic
95
+ eager early easy economic effective efficient elaborate electric elegant eligible embedded empty enabled entire environmental equal equivalent essential eventual evident exact excellent exceptional excited exclusive existing expensive experimental explicit extensive external extra extreme
96
+ fair false familiar famous fast fatal favourite favorite federal final financial fine firm first fit fixed flat flexible foreign formal former forward fragile free frequent fresh friendly full functional fundamental funny future fuzzy
97
+ general generic gentle genuine giant global golden good graceful gradual grand grateful great green gross
98
+ happy hard harmful healthy heavy helpful hidden high historical hollow honest horizontal hostile hot huge human hybrid
99
+ ideal identical idle illegal immediate immutable important impossible improper inactive inclusive incoherent incomplete inconsistent incorrect independent indirect individual industrial inevitable infinite informal initial inner innovative input insufficient intact integral intelligent intended interactive interesting interim intermediate internal international invalid inverse isolated
100
+ joint junior just
101
+ key kind known
102
+ large last late later latest lawful lazy leading legal legitimate lengthy level liable light likely limited linear liquid literal little live lively living local logical long loose lost loud low lower loyal
103
+ main major manual massive mature maximum mean meaningful measurable mechanical medical medium mental mere middle mild military minimal minimum minor missing mixed mobile modern modest modular monthly moral multiple mutual
104
+ narrow national native natural near nearby necessary negative net neutral new next nice noisy normal notable novel numerous
105
+ objective obvious occasional odd official old ongoing online only open operational opposite optimal optional ordinary organic original other outer outstanding overall
106
+ parallel partial particular passive past patient perfect periodic permanent persistent personal physical plain pleasant plural political poor popular positive possible potential powerful practical precise predictable preferred premium present previous primary prime principal prior private probable productive professional profitable progressive prominent proper proportional protective proud public pure
107
+ qualified quality quick quiet
108
+ random rapid rare raw ready real realistic reasonable recent redundant regional regular related relative relevant reliable remaining remote repeated representative required resilient responsible restricted retail reverse rich right rigid robust rough round routine royal rural
109
+ sad safe same satisfied scarce scientific secondary secret secure select selective senior sensible sensitive separate serious severe shallow shared sharp short sick significant silent similar simple single skilled slight slow small smart smooth social soft solid sophisticated sound sour southern spare spatial special specific stable standard static steady steep sticky still straight strange strategic strict strong structural stuck subsequent substantial subtle successful sudden sufficient suitable superior supportive supposed sure surprising suspicious sustainable sweet swift synthetic systematic
110
+ tall technical temporary terrible thick thin third thorough tight tiny tired top total tough toxic traditional transparent tremendous tricky trivial true typical
111
+ ugly ultimate unable unavailable uncertain unclear uncommon underlying undesirable unexpected unfair unfortunate uniform unique universal unknown unlikely unnecessary unrelated unsafe unstable unusual upcoming upper upstream urban urgent useful useless usual
112
+ vague valid valuable variable various vast verbal vertical viable visible visual vital vocal void volatile voluntary vulnerable
113
+ warm weak wealthy weekly weird western wet white whole wide widespread wild willing wise wooden worse worst worthy written wrong
114
+ yearly young zero`,
115
+
116
+ // Software / product / business vocabulary the assistant genuinely uses.
117
+ `agent agile algorithm allocation analytics annotation architecture archive argument artifact assertion assignment async attribute authentication authorization automation availability
118
+ backend backlog bandwidth binary boolean bootstrap bottleneck breakpoint broker browser buffer bundle
119
+ callback capacity certificate changelog checkpoint checksum classifier cluster codebase collaboration commit compiler compliance component composite compression concurrency connector console constant constraint container context contributor controller cookie coordinator corpus coverage credential cron cursor
120
+ daemon dashboard debug debugger decorator delegate dependency deploy deployment deprecation descriptor developer diff directory dispatch distribution docker domain driver duplicate
121
+ embedding encoder encryption endpoint entity enumeration environment escalation event exception executor exporter extension
122
+ fallback feature federation fetch fixture flag fork formatter forwarding fragment framework frontend function
123
+ garbage gateway generator governance gradient granularity graph grid guard
124
+ handler hash header heap heuristic hook hostname hypervisor
125
+ identifier idempotent image importer index inference ingestion inheritance initialiser initializer injection instance instrumentation integer integration interceptor interface interpreter invariant iteration iterator
126
+ json kernel keyword kubernetes
127
+ latency layer ledger lexicon lifecycle linter listener literal locale localisation localization locking logger lookup
128
+ manifest mapper marshalling matrix memoization merge metadata microservice middleware migration mock modality model modifier monolith mutation mutex
129
+ namespace neural nightly normalisation normalization notation nullable
130
+ observability observer offset onboarding operator optimisation optimization orchestration ordering overflow override
131
+ packet parameter parser partition payload permission persistence pipeline plugin pointer policy polling pooling portal predicate prefix preprocessing primitive priority procedure processor producer profiler prompt provider provisioning proxy publisher
132
+ quantisation quantization quota
133
+ race recall reconciliation recursion redirect refactor reference regex registry regression release rendering replica repository request resolver response retention retry rollback rollout router routing runbook runtime
134
+ sandbox scaffolding schema scheduler scope screenshot sdk secret selector semantic serialisation serialization serverless service session shard signature simulation singleton snapshot socket specification sprint stack stakeholder standup state stateless storage strategy stream subscription subsystem suite superset supervisor swagger symbol synchronisation synchronization
135
+ telemetry template tenant terminal test testing throughput throttle timestamp tokeniser tokenizer topology trace transaction transformer transpiler traversal trigger tuple typescript
136
+ unit upstream uptime usage utilisation utilization
137
+ validation validator variable vector versioning viewport virtualisation virtualization visualisation visualization
138
+ warehouse watcher webhook websocket widget worker workspace wrapper
139
+ yaml
140
+ ticket sprint roadmap milestone stakeholder retrospective onboarding offboarding compliance invoice payroll headcount revenue margin forecast pipeline quota churn retention acquisition conversion engagement adoption`,
141
+ ].join(' ');
142
+
143
+ /** The lexicon set — lowercase stems only. */
144
+ const ENGLISH_STEMS: ReadonlySet<string> = new Set(
145
+ ENGLISH_STEMS_RAW.split(/\s+/u).map((w) => w.trim().toLowerCase()).filter(Boolean),
146
+ );
147
+
148
+ /**
149
+ * Regular inflectional/derivational suffixes stripped before a lexicon lookup, longest
150
+ * first. Keeping the lexicon as STEMS (and stripping here) is what lets ~2.5k entries
151
+ * cover ordinary English text without a megabyte of word forms.
152
+ */
153
+ const SUFFIXES: readonly string[] = [
154
+ 'ationally', 'ability', 'ibility', 'ational', 'iveness', 'fulness', 'lessness',
155
+ 'ations', 'ments', 'nesses', 'ingly', 'ation', 'ition', 'ement', 'ments', 'ition',
156
+ 'ings', 'ness', 'ment', 'able', 'ible', 'ally', 'ical', 'ance', 'ence', 'sion', 'tion',
157
+ 'ised', 'ized', 'ises', 'izes', 'ising', 'izing', 'ise', 'ize',
158
+ 'ing', 'ers', 'est', 'ies', 'ied', 'ive', 'ous', 'ful', 'ity', 'ary', 'ory', 'ent', 'ant',
159
+ 'ed', 'er', 'es', 'ly', 'al', 'ic', 's', 'y',
160
+ ];
161
+
162
+ /** Undo consonant doubling ("commit" + "ing" → "committ" → "commit"). */
163
+ function undouble(stem: string): string {
164
+ const n = stem.length;
165
+ if (n >= 4 && stem[n - 1] === stem[n - 2] && !'aeiou'.includes(stem[n - 1] as string)) return stem.slice(0, -1);
166
+ return stem;
167
+ }
168
+
169
+ /** Does the lexicon know this stem, allowing the usual spelling repairs (drop-e, y→i)? */
170
+ function stemKnown(stem: string): boolean {
171
+ if (stem.length < 3) return false;
172
+ if (ENGLISH_STEMS.has(stem)) return true;
173
+ if (ENGLISH_STEMS.has(`${stem}e`)) return true; // us + e → use
174
+ const un = undouble(stem);
175
+ if (un !== stem && ENGLISH_STEMS.has(un)) return true; // committ → commit
176
+ if (stem.endsWith('i') && ENGLISH_STEMS.has(`${stem.slice(0, -1)}y`)) return true; // appli → apply
177
+ return false;
178
+ }
179
+
180
+ /**
181
+ * Is `word` a plausible English word — either present in the lexicon directly, or a
182
+ * regular inflection of a stem that is? Lowercase input; punctuation already stripped.
183
+ * Deliberately GENEROUS (it decides whether to *accuse* text of being gibberish).
184
+ */
185
+ export function isKnownEnglishWord(word: string): boolean {
186
+ const w = word.toLowerCase();
187
+ if (w.length < 3) return true; // too short to judge — never an accusation
188
+ if (ENGLISH_STEMS.has(w)) return true;
189
+ for (const suf of SUFFIXES) {
190
+ if (w.length > suf.length + 2 && w.endsWith(suf) && stemKnown(w.slice(0, -suf.length))) return true;
191
+ }
192
+ return false;
193
+ }
194
+
195
+ /**
196
+ * Function words for each supported Latin-script language, used ONLY to decide which
197
+ * language (if any) the text is in — never to score quality. Kept deliberately short:
198
+ * these are the highest-frequency, most distinctive markers of each language.
199
+ */
200
+ const FUNCTION_WORDS: Readonly<Record<string, readonly string[]>> = {
201
+ en: ['the', 'and', 'that', 'with', 'this', 'for', 'you', 'not', 'are', 'was', 'have', 'from', 'they', 'but', 'what', 'when', 'which', 'there', 'their', 'would', 'about', 'your', 'been', 'will', 'can', 'has', 'its', 'it', 'is', 'of', 'to', 'in', 'on', 'at', 'as', 'by', 'or', 'an', 'be', 'if'],
202
+ es: ['que', 'los', 'las', 'del', 'una', 'por', 'con', 'para', 'como', 'pero', 'este', 'esta', 'son', 'más', 'sus', 'sobre', 'todo', 'ser', 'hay', 'el', 'la', 'de', 'un', 'se', 'no', 'lo', 'al', 'es', 'y'],
203
+ fr: ['les', 'des', 'est', 'une', 'que', 'pour', 'dans', 'sur', 'pas', 'avec', 'sont', 'plus', 'vous', 'nous', 'cette', 'mais', 'aux', 'par', 'ils', 'ce', 'le', 'la', 'de', 'un', 'et', 'du', 'au', 'en', 'il'],
204
+ de: ['der', 'die', 'das', 'und', 'ist', 'nicht', 'mit', 'den', 'von', 'für', 'auf', 'ein', 'eine', 'sich', 'auch', 'dem', 'werden', 'wird', 'aber', 'oder', 'sie', 'wir', 'zu', 'im', 'es', 'als', 'bei', 'nach', 'sind'],
205
+ pt: ['que', 'não', 'uma', 'com', 'para', 'como', 'mais', 'dos', 'das', 'por', 'este', 'esta', 'são', 'ser', 'seu', 'sua', 'pelo', 'ao', 'os', 'as', 'de', 'do', 'da', 'em', 'um', 'no', 'na'],
206
+ it: ['che', 'per', 'con', 'del', 'della', 'una', 'sono', 'più', 'come', 'anche', 'nella', 'questo', 'questa', 'gli', 'nel', 'il', 'la', 'di', 'un', 'da', 'si', 'le', 'ed', 'ma'],
207
+ };
208
+
209
+ /** How many alphabetic characters are Latin-script (a–z plus the accented Latin range). */
210
+ function latinLetterShare(text: string): number {
211
+ let letters = 0;
212
+ let latin = 0;
213
+ for (const ch of text) {
214
+ if (!/\p{L}/u.test(ch)) continue;
215
+ letters++;
216
+ if (/[A-Za-zÀ-ɏ]/u.test(ch)) latin++;
217
+ }
218
+ return letters === 0 ? 0 : latin / letters;
219
+ }
220
+
221
+ /** The verdict of the language guard — which Latin language won, and how strongly. */
222
+ export interface LanguageVerdict {
223
+ /** Winning language code, or null when the text isn't Latin-script or no language wins. */
224
+ language: string | null;
225
+ /** Share of tokens that are function words of the winning language (0..1). */
226
+ share: number;
227
+ /** Share of tokens that are function words of ANY supported language (0..1). */
228
+ anyShare: number;
229
+ /** True when the text is predominantly Latin-script (so the guard even applies). */
230
+ latin: boolean;
231
+ /**
232
+ * True when English is the LEADING candidate — no other supported language scored
233
+ * strictly more function-word hits. This, not {@link language}, is what gates the
234
+ * English lexicon, because the two things it must separate are "a genuine Spanish
235
+ * reply" and "English-shaped gibberish", and only the former votes decisively for
236
+ * another language. Gibberish sprays short tokens that happen to be Spanish or
237
+ * French function words (`se`, `te`, `al`, `mis`), which is enough to TIE the vote
238
+ * or squeak past English — and treating that ambiguity as "not English, don't
239
+ * judge it" is exactly how invented-word output escapes the dictionary.
240
+ */
241
+ englishLeads: boolean;
242
+ }
243
+
244
+ /**
245
+ * How many DISTINCT function words a language needs before it wins outright. Raw hit
246
+ * counts are not enough: two occurrences of `se` plus one `al` outvoted English on a
247
+ * real gibberish sample, so the passage was measured against no lexicon at all.
248
+ * Genuine prose in any of these languages uses many different function words (the
249
+ * shortest real Spanish/French replies observed still clear 3), while gibberish
250
+ * repeats the same one or two accidental fragments.
251
+ */
252
+ const MIN_DISTINCT_FUNCTION_WORDS = 3;
253
+
254
+ /**
255
+ * Decide which supported Latin-script language a text is in, by function-word vote.
256
+ * Returns `language: null` for non-Latin scripts (CJK, Cyrillic, Arabic, …) and when
257
+ * no language clears the floor — in both cases the English lexicon must NOT be applied.
258
+ */
259
+ export function detectLatinLanguage(words: readonly string[], text: string): LanguageVerdict {
260
+ const latin = latinLetterShare(text) >= 0.6;
261
+ if (!latin || words.length === 0) return { language: null, share: 0, anyShare: 0, latin, englishLeads: false };
262
+
263
+ const counts = new Map<string, number>();
264
+ const distinct = new Map<string, number>();
265
+ const anyHit = new Set<number>();
266
+ for (const [lang, list] of Object.entries(FUNCTION_WORDS)) {
267
+ const set = new Set(list);
268
+ let hits = 0;
269
+ const seen = new Set<string>();
270
+ words.forEach((w, i) => {
271
+ if (set.has(w)) { hits++; seen.add(w); anyHit.add(i); }
272
+ });
273
+ counts.set(lang, hits);
274
+ distinct.set(lang, seen.size);
275
+ }
276
+ let best: string | null = null;
277
+ let bestHits = 0;
278
+ let runnerUp = 0;
279
+ for (const [lang, hits] of counts) {
280
+ if (hits > bestHits) { runnerUp = bestHits; bestHits = hits; best = lang; }
281
+ else if (hits > runnerUp) runnerUp = hits;
282
+ }
283
+ const share = bestHits / words.length;
284
+ const anyShare = anyHit.size / words.length;
285
+ // English leads unless some other language beat it on EVIDENCE — more hits AND enough
286
+ // different function words to be a language rather than one repeated fragment. A tie
287
+ // counts as a lead, and so does losing to a single token sprayed three times: see
288
+ // {@link LanguageVerdict.englishLeads}.
289
+ const englishHits = counts.get('en') ?? 0;
290
+ const englishLeads = [...counts].every(([lang, hits]) => (
291
+ lang === 'en'
292
+ || hits <= englishHits
293
+ || (distinct.get(lang) ?? 0) < MIN_DISTINCT_FUNCTION_WORDS
294
+ ));
295
+ // A win must be present (≥8% of tokens), unambiguous (strictly beats the runner-up)
296
+ // — "in/an/no/de" are shared across languages, so a tie decides nothing — and backed
297
+ // by enough DIFFERENT function words to be a language rather than a repeated fragment.
298
+ const decisive = !!best
299
+ && share >= 0.08
300
+ && bestHits > runnerUp
301
+ && (distinct.get(best) ?? 0) >= MIN_DISTINCT_FUNCTION_WORDS;
302
+ if (!decisive) return { language: null, share, anyShare, latin, englishLeads };
303
+ return { language: best, share, anyShare, latin, englishLeads };
304
+ }
305
+
306
+ /** Every supported language's function words in one set, built from the SAME table the
307
+ * language vote uses so the two can never disagree about what a function word is. */
308
+ const ANY_FUNCTION_WORD: ReadonlySet<string> = new Set(Object.values(FUNCTION_WORDS).flat());
309
+
310
+ /**
311
+ * Is this token a function word in ANY supported Latin-script language?
312
+ *
313
+ * Used to tell CONTENT words from grammar. Repetition is what function words are FOR
314
+ * ("the … the … the"), so counting them as evidence of a collapsed head is a category
315
+ * error — and the length-based proxy that stood in for this (any token of 3+ letters)
316
+ * quietly classified `the`, `and`, `for`, `was` and `not` as content.
317
+ */
318
+ export function isFunctionWord(word: string): boolean {
319
+ return ANY_FUNCTION_WORD.has(word.toLowerCase());
320
+ }
321
+
322
+ /** Tokens that are legitimately not words: identifiers, paths, urls, numbers, versions. */
323
+ const CODEISH = /[0-9_@#$/\\<>{}[\]|`~^*+=]|::|\.\w|--/u;
324
+
325
+ /**
326
+ * Is this raw token code rather than prose? Used to EXCLUDE tokens from word scoring —
327
+ * `projectEvermindRef`, `api/src/foo.ts`, `v2.1`, `https://…` are all legitimately
328
+ * absent from any dictionary.
329
+ */
330
+ export function isCodeishToken(raw: string): boolean {
331
+ if (CODEISH.test(raw)) return true;
332
+ // camelCase / PascalCase with an internal capital → an identifier, not a word.
333
+ if (/^[A-Za-z]+$/u.test(raw) && /[a-z][A-Z]/u.test(raw)) return true;
334
+ // ALLCAPS acronyms (SSM, HTTP, R2) — real, just not lexicon entries.
335
+ if (/^[A-Z]{2,}$/u.test(raw)) return true;
336
+ return false;
337
+ }
338
+
339
+ /** The measured outcome of {@link scoreEnglishWordiness}. */
340
+ export interface WordinessScore {
341
+ /** True when the scorer actually ran (English text, enough eligible tokens). */
342
+ scored: boolean;
343
+ /** Eligible content tokens considered. */
344
+ eligible: number;
345
+ /** How many of them matched nothing — not the lexicon, not the context, not a repeat. */
346
+ unknown: number;
347
+ /** unknown / eligible (0 when not scored). */
348
+ unknownShare: number;
349
+ }
350
+
351
+ /**
352
+ * Score what fraction of a text's CONTENT words are unrecognisable. Only meaningful for
353
+ * confidently-English text — the caller must consult {@link detectLatinLanguage} first.
354
+ *
355
+ * A token counts as KNOWN when any of these hold, in order of cost:
356
+ * - it is code-ish (identifier / path / acronym) → excluded from scoring entirely;
357
+ * - the lexicon recognises it (with inflection stripping);
358
+ * - it appears in `context` (the prompt/question) — domain jargon echoes the ask;
359
+ * - it occurs more than once in the text — real jargon recurs, invented words don't.
360
+ */
361
+ export function scoreEnglishWordiness(
362
+ words: readonly string[],
363
+ rawTokens: readonly string[],
364
+ context?: string,
365
+ ): WordinessScore {
366
+ const contextWords = new Set(
367
+ (context ?? '').toLowerCase().match(/\p{L}[\p{L}'-]*/gu)?.map((w) => w.replace(/[''-]/gu, '')) ?? [],
368
+ );
369
+ const freq = new Map<string, number>();
370
+ for (const w of words) freq.set(w, (freq.get(w) ?? 0) + 1);
371
+
372
+ let eligible = 0;
373
+ let unknown = 0;
374
+ words.forEach((w, i) => {
375
+ // Judge exactly the words the lexicon is willing to judge. {@link isKnownEnglishWord}
376
+ // already forgives everything under 3 characters outright, so excluding 3-letter
377
+ // tokens on top of that discarded a third of the tokens in a short reply and left
378
+ // the probe-length samples below the evidence floor — unscoreable, hence "coherent".
379
+ // Measured on real samples, including them RAISES the unknown share of gibberish and
380
+ // LOWERS it for genuine English (3-letter English words are almost all in the
381
+ // lexicon), so it widens the margin in both directions.
382
+ if (w.length < 3) return;
383
+ const raw = rawTokens[i] ?? w;
384
+ if (isCodeishToken(raw)) return;
385
+ eligible++;
386
+ if (isKnownEnglishWord(w)) return;
387
+ if (contextWords.has(w)) return;
388
+ if ((freq.get(w) ?? 0) > 1) return;
389
+ unknown++;
390
+ });
391
+
392
+ // Below this there isn't enough evidence for a verdict either way.
393
+ if (eligible < 10) return { scored: false, eligible, unknown, unknownShare: 0 };
394
+ return { scored: true, eligible, unknown, unknownShare: unknown / eligible };
395
+ }