Files
zeekay 237a080a80 brain: land fortemi parity sweep + 7 pure-CPU algorithm modules
Studied fortemi/fortemi @ v2026.5.0 and produced PARITY.md — every
algorithm Fortémi ships has a row mapping to a Hanzo destination
(this repo, a sibling hanzo/* repo, or a tracked planned module).

Landed under packages/memory/, all backed by 21 passing tests in
algorithms.test.ts:

- fusion.ts    RRF (k=20 BEIR), RSF, adaptive k, adaptive weights
- rerank.ts    Maximal Marginal Relevance (Carbonell & Goldstein 1998)
- dedup.ts     chunk-aware deduplication (best per chain)
- script.ts    Unicode script detection — Latin/CJK/emoji/Cyrillic/
               Arabic/Hebrew/Greek/Devanagari
- embed.ts     embedding model registry, asymmetric E5 prefixes,
               Matryoshka truncation, coarse-dim picker
- temporal.ts  UUIDv7 floor/ceiling + named ranges
- captions.ts  WebVTT / SRT / RTTM renderers

Exposed from @hanzo/bot-memory v1.4.3.

Remaining gaps tracked in PARITY.md: FTS feature flags, federated search,
runtime config hot-swap, capabilities/task selector, eval harness, SKOS,
FAIR export, SNN/PFNET/Louvain graph maintenance, LLM-typed graph links,
extraction registry — plus sibling-repo wire-ups (search/vector/crypto/
age/replicate/insights/agents).
2026-05-11 10:44:01 -07:00

118 lines
3.4 KiB
TypeScript

/**
* Unicode script detection.
*
* Single O(n) pass over the input. Routes FTS strategy:
* - Latin → stemming + BM25
* - CJK → bigram/trigram character matching
* - Emoji/Sym → trigram substring matching
* - RTL → basic tokenization (Arabic / Hebrew)
*/
export type Script = "latin" | "cjk" | "emoji" | "cyrillic" | "arabic" | "hebrew" | "greek" | "devanagari" | "other";
export interface ScriptReport {
primary: Script;
/** Fraction of classified characters per script. */
fractions: Record<Script, number>;
/** Whether any CJK was seen (cheap shortcut). */
hasCjk: boolean;
/** Whether any emoji or pictographic was seen. */
hasEmoji: boolean;
}
export function detectScript(text: string): ScriptReport {
const counts: Record<Script, number> = {
latin: 0, cjk: 0, emoji: 0, cyrillic: 0, arabic: 0, hebrew: 0, greek: 0, devanagari: 0, other: 0,
};
let total = 0;
for (const ch of text) {
const cp = ch.codePointAt(0)!;
if (cp < 0x0030 || (cp >= 0x0030 && cp <= 0x0039)) {
// digits — don't count toward script signal
continue;
}
const s = classify(cp);
if (s === null) continue;
counts[s] += 1;
total += 1;
}
const fractions: Record<Script, number> = {
latin: 0, cjk: 0, emoji: 0, cyrillic: 0, arabic: 0, hebrew: 0, greek: 0, devanagari: 0, other: 0,
};
if (total > 0) {
for (const k of Object.keys(counts) as Script[]) {
fractions[k] = counts[k] / total;
}
}
let primary: Script = "other";
let max = 0;
for (const k of Object.keys(counts) as Script[]) {
if (counts[k] > max) {
max = counts[k];
primary = k;
}
}
return {
primary,
fractions,
hasCjk: counts.cjk > 0,
hasEmoji: counts.emoji > 0,
};
}
export function hasCjk(text: string): boolean {
for (const ch of text) {
const cp = ch.codePointAt(0)!;
if (isCjk(cp)) return true;
}
return false;
}
export function hasEmoji(text: string): boolean {
for (const ch of text) {
const cp = ch.codePointAt(0)!;
if (isEmoji(cp)) return true;
}
return false;
}
function classify(cp: number): Script | null {
if (isCjk(cp)) return "cjk";
if (isEmoji(cp)) return "emoji";
if ((cp >= 0x0041 && cp <= 0x005a) || (cp >= 0x0061 && cp <= 0x007a)) return "latin";
if (cp >= 0x00c0 && cp <= 0x024f) return "latin"; // Latin-1 supplement + extended
if (cp >= 0x0370 && cp <= 0x03ff) return "greek";
if (cp >= 0x0400 && cp <= 0x04ff) return "cyrillic";
if (cp >= 0x0590 && cp <= 0x05ff) return "hebrew";
if (cp >= 0x0600 && cp <= 0x06ff) return "arabic";
if (cp >= 0x0900 && cp <= 0x097f) return "devanagari";
if (cp <= 0x002f || (cp >= 0x003a && cp <= 0x0040) || (cp >= 0x005b && cp <= 0x0060) || (cp >= 0x007b && cp <= 0x007e)) {
return null; // ASCII punctuation/whitespace — uninformative
}
return "other";
}
function isCjk(cp: number): boolean {
return (
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK Ext A
(cp >= 0x20000 && cp <= 0x2a6df) || // CJK Ext B
(cp >= 0x3040 && cp <= 0x309f) || // Hiragana
(cp >= 0x30a0 && cp <= 0x30ff) || // Katakana
(cp >= 0xac00 && cp <= 0xd7af) // Hangul Syllables
);
}
function isEmoji(cp: number): boolean {
return (
(cp >= 0x1f300 && cp <= 0x1faff) ||
(cp >= 0x2600 && cp <= 0x27bf) ||
(cp >= 0x1f000 && cp <= 0x1f0ff) ||
cp === 0x2705 || cp === 0x2728 || cp === 0x274c || cp === 0x2049
);
}