cortexm 0.3.0__py3-none-any.whl

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.
Files changed (120) hide show
  1. context_m.py +17 -0
  2. cortexm/__init__.py +45 -0
  3. cortexm/accel.py +403 -0
  4. cortexm/api/__init__.py +0 -0
  5. cortexm/api/chaos.py +118 -0
  6. cortexm/api/memory.py +635 -0
  7. cortexm/bench/__init__.py +0 -0
  8. cortexm/bench/abilities.py +311 -0
  9. cortexm/bench/baselines.py +89 -0
  10. cortexm/bench/beam_loader.py +317 -0
  11. cortexm/bench/generator.py +376 -0
  12. cortexm/bench/harness.py +211 -0
  13. cortexm/bench/messy.py +218 -0
  14. cortexm/bench/micro.py +251 -0
  15. cortexm/bench/ood.py +443 -0
  16. cortexm/bench/run.py +137 -0
  17. cortexm/bridge/__init__.py +0 -0
  18. cortexm/bridge/dates.py +178 -0
  19. cortexm/bridge/decoders.py +204 -0
  20. cortexm/bridge/enrich.py +255 -0
  21. cortexm/bridge/extractor.py +316 -0
  22. cortexm/bridge/fallback.py +332 -0
  23. cortexm/bridge/onnx_runtime.py +158 -0
  24. cortexm/bridge/patterns.py +760 -0
  25. cortexm/bridge/ppr.py +104 -0
  26. cortexm/bridge/prefilter.py +188 -0
  27. cortexm/bridge/query_extract.py +420 -0
  28. cortexm/bridge/reader.py +1174 -0
  29. cortexm/bridge/rerank.py +204 -0
  30. cortexm/bridge/writer.py +492 -0
  31. cortexm/cli.py +295 -0
  32. cortexm/cognition/__init__.py +53 -0
  33. cortexm/cognition/abstraction.py +192 -0
  34. cortexm/cognition/analogy.py +159 -0
  35. cortexm/cognition/engine.py +204 -0
  36. cortexm/cognition/gaps.py +365 -0
  37. cortexm/cognition/scanner.py +204 -0
  38. cortexm/config.py +375 -0
  39. cortexm/cortexm.py +8 -0
  40. cortexm/enterprise/__init__.py +0 -0
  41. cortexm/enterprise/audit.py +178 -0
  42. cortexm/enterprise/governance.py +239 -0
  43. cortexm/errors.py +35 -0
  44. cortexm/features/__init__.py +0 -0
  45. cortexm/features/git.py +204 -0
  46. cortexm/features/prefetch.py +88 -0
  47. cortexm/features/zk.py +105 -0
  48. cortexm/federation/__init__.py +39 -0
  49. cortexm/federation/crdt.py +275 -0
  50. cortexm/federation/fabric.py +109 -0
  51. cortexm/federation/hlc.py +80 -0
  52. cortexm/federation/node.py +145 -0
  53. cortexm/federation/schema_report.py +73 -0
  54. cortexm/federation/transport.py +164 -0
  55. cortexm/index/__init__.py +19 -0
  56. cortexm/index/nsg.py +386 -0
  57. cortexm/mcp/__init__.py +0 -0
  58. cortexm/mcp/server.py +985 -0
  59. cortexm/metrics.py +62 -0
  60. cortexm/migrate/__init__.py +0 -0
  61. cortexm/migrate/importers.py +192 -0
  62. cortexm/provenance/__init__.py +78 -0
  63. cortexm/provenance/agent.py +214 -0
  64. cortexm/provenance/cose.py +201 -0
  65. cortexm/provenance/scitt.py +258 -0
  66. cortexm/provenance/vc.py +250 -0
  67. cortexm/security/__init__.py +0 -0
  68. cortexm/security/crypto.py +162 -0
  69. cortexm/security/hashes.py +140 -0
  70. cortexm/security/injection.py +149 -0
  71. cortexm/security/mind.py +154 -0
  72. cortexm/security/pii.py +265 -0
  73. cortexm/security/rbac.py +169 -0
  74. cortexm/security/sandbox.py +131 -0
  75. cortexm/security/zk_hamming.py +142 -0
  76. cortexm/security/zk_sql.py +485 -0
  77. cortexm/server/__init__.py +0 -0
  78. cortexm/server/metrics.py +88 -0
  79. cortexm/server/rest.py +936 -0
  80. cortexm/server/sparql.py +984 -0
  81. cortexm/text/__init__.py +0 -0
  82. cortexm/text/dissim.py +252 -0
  83. cortexm/text/embedder.py +155 -0
  84. cortexm/text/fuzzy.py +218 -0
  85. cortexm/text/idiolect.py +253 -0
  86. cortexm/text/labse.py +374 -0
  87. cortexm/text/tokenizer.py +79 -0
  88. cortexm/trace/__init__.py +0 -0
  89. cortexm/trace/blob_arena.py +277 -0
  90. cortexm/trace/consolidate.py +337 -0
  91. cortexm/trace/contradictions.py +69 -0
  92. cortexm/trace/dedup.py +114 -0
  93. cortexm/trace/edges.py +214 -0
  94. cortexm/trace/fact.py +121 -0
  95. cortexm/trace/fade.py +245 -0
  96. cortexm/trace/lifecycle.py +112 -0
  97. cortexm/trace/rebuild.py +173 -0
  98. cortexm/trace/rules.py +171 -0
  99. cortexm/trace/store.py +680 -0
  100. cortexm/trace/structural.py +183 -0
  101. cortexm/trace/tmt.py +335 -0
  102. cortexm/util.py +148 -0
  103. cortexm/vsa/__init__.py +0 -0
  104. cortexm/vsa/attribution.py +149 -0
  105. cortexm/vsa/cleanup.py +161 -0
  106. cortexm/vsa/codecs.py +397 -0
  107. cortexm/vsa/hologram_overlay.py +139 -0
  108. cortexm/vsa/index.py +163 -0
  109. cortexm/vsa/ops.py +149 -0
  110. cortexm/vsa/palace.py +446 -0
  111. cortexm/vsa/role_vectors.py +236 -0
  112. cortexm/vsa/slb.py +78 -0
  113. cortexm/vsa/tlsh_trie.py +137 -0
  114. cortexm/vsa/working_memory.py +249 -0
  115. cortexm-0.3.0.dist-info/METADATA +482 -0
  116. cortexm-0.3.0.dist-info/RECORD +120 -0
  117. cortexm-0.3.0.dist-info/WHEEL +5 -0
  118. cortexm-0.3.0.dist-info/entry_points.txt +2 -0
  119. cortexm-0.3.0.dist-info/licenses/LICENSE +190 -0
  120. cortexm-0.3.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,253 @@
1
+ """Per-user idiolect normalization via embedding neighborhoods.
2
+
3
+ Self-supervised: builds a per-user idiolect centroid (EMA of recent
4
+ chunk embeddings) and an in-vocabulary codebook. When a noisy token
5
+ appears, looks up k nearest in-vocab neighbors weighted by idiolect
6
+ consistency, returns the canonical form if similarity >= threshold.
7
+
8
+ arxiv research: Göker 2018 (Turkish social media), TERUN 2020 (Roman
9
+ Hindi), Rocca & Weston 2022 (per-user transformers). The embedding-
10
+ neighborhood method is fully unsupervised.
11
+
12
+ Pure numpy. No trained model. Plays well with μ=0 — the
13
+ HashingEmbedder is deterministic.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections import OrderedDict
19
+ from typing import Iterable
20
+
21
+ import numpy as np
22
+
23
+ from cortexm.text.tokenizer import STOPWORDS, words
24
+
25
+
26
+ class PerUserIdiolectNormalizer:
27
+ """Per-user idiolect-aware normalization.
28
+
29
+ Maintains:
30
+ * in-vocab codebook: canonical_token → embedding
31
+ * per-user centroid: EMA of recent chunk embeddings
32
+
33
+ On normalize(user, text):
34
+ for each token, if in-vocab → leave alone; else look up k-NN in
35
+ vocab weighted by (0.7 * direct_sim + 0.3 * user_centroid_sim),
36
+ replace if best >= threshold.
37
+ """
38
+
39
+ def __init__(self, embedder, vocab_cap: int = 50_000,
40
+ win: int = 256, decay: float = 0.92,
41
+ min_count: int = 2, threshold: float = 0.78,
42
+ k: int = 5) -> None:
43
+ self.embedder = embedder
44
+ self.vocab_cap = vocab_cap
45
+ self.win = win
46
+ self.decay = decay
47
+ self.min_count = min_count # min user co-occurrences before promotion
48
+ self.threshold = threshold
49
+ self.k = k
50
+ # canonical vocab: token → (embedding, count)
51
+ self._vocab: "OrderedDict[str, tuple[np.ndarray, int]]" = OrderedDict()
52
+ # user_id → (centroid_emb, raw_count)
53
+ self._users: dict[str, tuple[np.ndarray, int]] = {}
54
+ # (user_id, slang_token, canonical_token) → co-occurrence count
55
+ self._co_counts: dict[tuple, int] = {}
56
+ # built-in text-speak escape hatch — a small curated map of
57
+ # common short-forms that the embedding-kNN path can't recover
58
+ # (because "u" / "ur" / "@" / "2" / "4" have very different
59
+ # char n-gram signatures from their canonical forms). This is
60
+ # reasonable because text-speak is a well-documented cross-user
61
+ # idiolect; the normalizer uses it as a pre-learned baseline
62
+ # before self-supervising per-user slang not in this map.
63
+ # Public so callers can extend or override per domain.
64
+ self.text_speak_map: dict[str, str] = {
65
+ "u": "you", "ur": "your", "u r": "you are",
66
+ "2": "to", "4": "for",
67
+ "b4": "before", "tmr": "tomorrow", "defo": "definitely",
68
+ "prolly": "probably", "kinda": "kind of", "sorta": "sort of",
69
+ "gimme": "give me", "lemme": "let me",
70
+ "wanna": "want to", "gonna": "going to",
71
+ "gotta": "got to", "outta": "out of",
72
+ "bc": "because", "dk": "don't know", "idk": "i don't know",
73
+ "rn": "right now", "w/": "with", "w/o": "without",
74
+ "ppl": "people", "thx": "thanks", "k": "okay",
75
+ "rly": "really", "tho": "though",
76
+ "@": "at", "&": "and", "b/c": "because",
77
+ "y": "why", "r": "are", "n": "and",
78
+ "im": "i'm", "ive": "i've", "ill": "i'll",
79
+ "wouldnt": "wouldn't", "shouldnt": "shouldn't",
80
+ "couldnt": "couldn't", "dont": "don't",
81
+ "cant": "can't", "wont": "won't", "isnt": "isn't",
82
+ "wasnt": "wasn't", "didnt": "didn't",
83
+ "hasnt": "hasn't", "havent": "haven't",
84
+ }
85
+
86
+ # ------------------------------------------------------- observation
87
+ def observe(self, user_id: str, text: str) -> None:
88
+ """Update the user's idiolect centroid and add tokens to vocab."""
89
+ if not text or not text.strip():
90
+ return
91
+ v = self.embedder.embed(text)
92
+ u = self._users.get(user_id)
93
+ if u is None:
94
+ self._users[user_id] = (v, 1)
95
+ else:
96
+ old, n = u
97
+ new = self.decay * old + (1.0 - self.decay) * v
98
+ nn = float(np.linalg.norm(new))
99
+ self._users[user_id] = (new / nn if nn > 0 else new, n + 1)
100
+ # add canonical tokens to vocab
101
+ for tok in words(text):
102
+ if tok in STOPWORDS or len(tok) < 2:
103
+ continue
104
+ if tok in self._vocab:
105
+ emb, cnt = self._vocab[tok]
106
+ self._vocab[tok] = (emb, cnt + 1)
107
+ elif len(self._vocab) < self.vocab_cap:
108
+ self._vocab[tok] = (self.embedder.embed(tok), 1)
109
+ # LRU eviction
110
+ if len(self._vocab) > self.vocab_cap:
111
+ # drop least-recently-promoted entries
112
+ while len(self._vocab) > self.vocab_cap:
113
+ self._vocab.popitem(last=False)
114
+
115
+ def observe_pair(self, user_id: str, slang: str, canonical: str) -> None:
116
+ """Promote a slang→canonical mapping after multiple confirmations."""
117
+ key = (user_id, slang.lower(), canonical.lower())
118
+ self._co_counts[key] = self._co_counts.get(key, 0) + 1
119
+
120
+ # ------------------------------------------------------- normalization
121
+ def normalize_token(self, user_id: str, token: str) -> str:
122
+ """Return canonical form of token for this user.
123
+
124
+ Case-preserving: if the token is in vocab (case-insensitive),
125
+ return the ORIGINAL token unchanged. Only normalize tokens
126
+ that are genuinely OOV (no case-insensitive match).
127
+ """
128
+ if not token or token in STOPWORDS:
129
+ return token
130
+ # text-speak escape hatch — checked FIRST because the embedding
131
+ # kNN path can't recover "u"/"@"/"2" → canonical (their char
132
+ # n-gram signatures are too different). The map is curated and
133
+ # public so callers can extend per domain.
134
+ token_lower = token.lower()
135
+ if token_lower in self.text_speak_map:
136
+ canon = self.text_speak_map[token_lower]
137
+ if token[:1].isupper():
138
+ canon = canon[:1].upper() + canon[1:]
139
+ return canon
140
+ # case-insensitive vocab check — preserves the original token
141
+ # when it's already a canonical form (just with different case)
142
+ if token_lower in self._vocab:
143
+ return token # already canonical, just preserve case
144
+ # check if user has a promoted mapping
145
+ canonical = self._find_promoted(user_id, token)
146
+ if canonical is not None:
147
+ return canonical
148
+ # embedding k-NN over vocab
149
+ if len(self._vocab) < 5:
150
+ return token
151
+ q = self.embedder.embed(token)
152
+ ids = list(self._vocab.keys())
153
+ embs = np.stack([self._vocab[i][0] for i in ids])
154
+ sims = embs @ q # (N,)
155
+ # idiolect bias
156
+ u = self._users.get(user_id)
157
+ if u is not None:
158
+ u_centroid = u[0]
159
+ u_sims = embs @ u_centroid
160
+ sims = 0.7 * sims + 0.3 * u_sims
161
+ order = np.argsort(-sims)[: self.k]
162
+ best_idx = int(order[0])
163
+ if sims[best_idx] < self.threshold:
164
+ return token # OOV — preserve original
165
+ return ids[best_idx] # might be lowercased canonical
166
+
167
+ def _find_promoted(self, user_id: str, token: str) -> str | None:
168
+ """Return promoted canonical if (user, slang) co-occurred >= min_count."""
169
+ token_l = token.lower()
170
+ for (u, slang, canonical), cnt in self._co_counts.items():
171
+ if u == user_id and slang == token_l and cnt >= self.min_count:
172
+ return canonical
173
+ return None
174
+
175
+ def normalize(self, user_id: str, text: str) -> str:
176
+ """Normalize all tokens in text per-user.
177
+
178
+ Preserves whitespace (including newlines) so downstream regex
179
+ patterns that use ^ to anchor line starts still work — the
180
+ text-speak map collapses multi-char tokens (u→you, 2→to) but
181
+ shouldn't collapse newlines into spaces.
182
+ """
183
+ if not text:
184
+ return text
185
+ import re as _re
186
+ out = []
187
+ # use re.split with capture to preserve whitespace
188
+ # tokens: runs of non-whitespace
189
+ for piece in _re.split(r"(\s+)", text):
190
+ if not piece:
191
+ continue
192
+ if piece.isspace():
193
+ out.append(piece)
194
+ continue
195
+ tok = piece
196
+ # CRITICAL: check the text-speak map for the FULL token
197
+ # (with punctuation) BEFORE stripping — otherwise "@" gets
198
+ # stripped to "" and never reaches the map. This is what
199
+ # let "I work @ Microsoft" through un-normalized.
200
+ tok_lower = tok.lower()
201
+ if tok_lower in self.text_speak_map:
202
+ canon = self.text_speak_map[tok_lower]
203
+ if tok[:1].isupper():
204
+ canon = canon[:1].upper() + canon[1:]
205
+ out.append(canon)
206
+ continue
207
+ # preserve simple punctuation
208
+ prefix = ""
209
+ core = tok
210
+ while core and not core[0].isalnum():
211
+ prefix += core[0]
212
+ core = core[1:]
213
+ suffix = ""
214
+ while core and not core[-1].isalnum():
215
+ suffix = core[-1] + suffix
216
+ core = core[:-1]
217
+ if core:
218
+ canon = self.normalize_token(user_id, core)
219
+ out.append(prefix + canon + suffix)
220
+ else:
221
+ out.append(tok)
222
+ return "".join(out)
223
+
224
+ # ------------------------------------------------------- admin
225
+ def stats(self) -> dict:
226
+ return {
227
+ "vocab_size": len(self._vocab),
228
+ "users": len(self._users),
229
+ "promoted_mappings": sum(1 for v in self._co_counts.values()
230
+ if v >= self.min_count),
231
+ "vocab_cap": self.vocab_cap,
232
+ "threshold": self.threshold,
233
+ }
234
+
235
+ def save_state(self) -> dict:
236
+ """Serialize for federation (deterministic)."""
237
+ return {
238
+ "vocab": [(k, v[0].tolist(), v[1]) for k, v in self._vocab.items()],
239
+ "users": {k: [v[0].tolist(), v[1]] for k, v in self._users.items()},
240
+ "co_counts": dict(self._co_counts),
241
+ }
242
+
243
+ def load_state(self, state: dict) -> None:
244
+ self._vocab.clear()
245
+ for k, emb, cnt in state.get("vocab", []):
246
+ self._vocab[k] = (np.asarray(emb, dtype=np.float32), cnt)
247
+ self._users = {k: (np.asarray(v[0], dtype=np.float32), v[1])
248
+ for k, v in state.get("users", {}).items()}
249
+ self._co_counts = {tuple(k) if isinstance(k, list) else k: v
250
+ for k, v in state.get("co_counts", {}).items()}
251
+
252
+
253
+ __all__ = ["PerUserIdiolectNormalizer"]
cortexm/text/labse.py ADDED
@@ -0,0 +1,374 @@
1
+ """LaBSE-inspired polyglot hashing encoder.
2
+
3
+ The non-English ingest problem: docs/BENCHMARKS.md Tier-1 shows
4
+ non-English extraction recall = 0.000 ± 0.000 because the pattern
5
+ extractor + HashingEmbedder is English-regex based. LaBSE solves this
6
+ but requires a 3GB model download (violates μ=0 + no-GPU rules).
7
+
8
+ This module implements a LaBSE-inspired polyglot encoder using Unicode
9
+ codepoint n-grams instead of WordPiece tokens. The trick: LaBSE's
10
+ multilingual power comes from training on 109 languages' subword
11
+ structure — we approximate that by treating any Unicode n-gram as a
12
+ valid feature and hashing it into 768-dim space via the existing
13
+ ``h64()`` (BLAKE2b).
14
+
15
+ Algorithm (pure numpy + stdlib unicodedata, no model, no GPU):
16
+ 1. Script-aware tokenization: split on Unicode script boundaries AND
17
+ whitespace (handles CJK, Devanagari, Arabic, Cyrillic, Thai, etc.).
18
+ CJK ideographs become one-char tokens (Chinese has no whitespace
19
+ word boundaries — each ideograph is the morphological unit).
20
+ Combining marks (Mn/Mc/Me) stick to the preceding letter so
21
+ Devanagari vowel signs, Arabic diacritics, and Latin combining
22
+ accents stay with their host letter.
23
+ 2. 3-5 char n-grams per token, padded with "^" and "$" (catches
24
+ morphology — like LaBSE's WordPiece captures subword structure).
25
+ Short tokens (padded length < n) fall back to emitting the whole
26
+ padded form as a single feature for that n-gram size, so
27
+ single-char CJK tokens still contribute at every n-gram size.
28
+ 3. Per-token structural features ("TOK", "TOK:short" / "TOK:long"):
29
+ language-agnostic features that give two sentences of similar
30
+ shape a small positive cosine bias — the LaBSE-equivalent of
31
+ "same sentence shape" alignment, without breaking μ=0. This is
32
+ what makes "Alice works at Google" and "爱丽丝在谷歌工作" land
33
+ at cos ≈ 0.23 instead of ≈ 0.03 (pure random hashing).
34
+ 4. ``h64()`` -> ``dims``-dim bucket + sign flip (bit 63).
35
+ 5. Sum + L2-normalize. Empty/whitespace-only input returns a zero
36
+ vector (no crash, downstream callers handle the zero-norm case).
37
+
38
+ Bit-identical across runs: ``h64`` uses BLAKE2b (not Python's
39
+ randomized ``hash()``), feature iteration order is fixed by the
40
+ deterministic tokenizer, and the float32 accumulation
41
+ ``vec[idx] += sign * w`` is single-threaded and order-stable.
42
+
43
+ References:
44
+ * Feng et al., "Language-Agnostic BERT Sentence Embedding", ACL 2022.
45
+ * Weinberger et al., "Feature Hashing", ICML 2009.
46
+ """
47
+ from __future__ import annotations
48
+
49
+ import math
50
+ import unicodedata
51
+
52
+ import numpy as np
53
+
54
+ from cortexm.util import h64
55
+
56
+
57
+ # ------------------------------------------------------------------- script
58
+ # Per-char script cache. _script_of is called per character — the cache
59
+ # short-circuits the ~20 sequential codepoint-range checks for repeat
60
+ # chars (ASCII letters in English, common Han ideographs in Chinese).
61
+ # Pure perf optimization — disabling it leaves output bit-identical.
62
+ # Bounded to 65k entries (covers the BMP).
63
+ _SCRIPT_CACHE: dict[str, str] = {}
64
+ _SCRIPT_CACHE_LIMIT = 65_536
65
+
66
+
67
+ def _script_of_uncached(cp: int) -> str:
68
+ """Codepoint-range script lookup (no cache)."""
69
+ # CJK Unified Ideographs + Ext A + Compatibility ideographs
70
+ if 0x3400 <= cp <= 0x4DBF or 0x4E00 <= cp <= 0x9FFF or 0xF900 <= cp <= 0xFAFF:
71
+ return "Han"
72
+ # Kana (Hiragana, Katakana, Katakana Phonetic Extensions)
73
+ if 0x3040 <= cp <= 0x309F:
74
+ return "Hira"
75
+ if 0x30A0 <= cp <= 0x30FF or 0x31F0 <= cp <= 0x31FF:
76
+ return "Kana"
77
+ # Hangul: Jamo, Compatibility Jamo, Syllables
78
+ if 0x1100 <= cp <= 0x11FF or 0x3130 <= cp <= 0x318F or 0xAC00 <= cp <= 0xD7AF:
79
+ return "Hang"
80
+ # Greek (and Coptic polytonic)
81
+ if 0x0370 <= cp <= 0x03FF or 0x1F00 <= cp <= 0x1FFF:
82
+ return "Grek"
83
+ # Cyrillic (+ supplement)
84
+ if 0x0400 <= cp <= 0x04FF or 0x0500 <= cp <= 0x052F:
85
+ return "Cyrl"
86
+ # Hebrew
87
+ if 0x0590 <= cp <= 0x05FF:
88
+ return "Hebr"
89
+ # Arabic (incl. Supplement and Extended-A)
90
+ if 0x0600 <= cp <= 0x06FF or 0x0750 <= cp <= 0x077F or 0x08A0 <= cp <= 0x08FF:
91
+ return "Arab"
92
+ # Indic scripts — Devanagari, Bengali, Gurmukhi, Gujarati, Oriya,
93
+ # Tamil, Telugu, Kannada, Malayalam, Sinhala. All use whitespace
94
+ # word boundaries, but combining marks (vowel signs, nuktas) must
95
+ # stick to the host letter — handled by the tokenizer.
96
+ if 0x0900 <= cp <= 0x097F:
97
+ return "Deva"
98
+ if 0x0980 <= cp <= 0x09FF:
99
+ return "Beng"
100
+ if 0x0A00 <= cp <= 0x0A7F:
101
+ return "Guru"
102
+ if 0x0A80 <= cp <= 0x0AFF:
103
+ return "Gujr"
104
+ if 0x0B00 <= cp <= 0x0B7F:
105
+ return "Orya"
106
+ if 0x0B80 <= cp <= 0x0BFF:
107
+ return "Taml"
108
+ if 0x0C00 <= cp <= 0x0C7F:
109
+ return "Telu"
110
+ if 0x0C80 <= cp <= 0x0CFF:
111
+ return "Knda"
112
+ if 0x0D00 <= cp <= 0x0D7F:
113
+ return "Mlym"
114
+ if 0x0D80 <= cp <= 0x0DFF:
115
+ return "Sinh"
116
+ # Thai, Lao (no whitespace word boundaries — but we tokenize by
117
+ # script run so each contiguous Thai/Lao run becomes one token,
118
+ # and char n-grams still capture the subword structure).
119
+ if 0x0E00 <= cp <= 0x0E7F:
120
+ return "Thai"
121
+ if 0x0E80 <= cp <= 0x0EFF:
122
+ return "Laoo"
123
+ # Default: Latin (covers ASCII + Latin-1 + Latin Extended + Latin-1
124
+ # Supplement + diacritics not explicitly mapped above).
125
+ return "Latn"
126
+
127
+
128
+ def _script_of(ch: str) -> str:
129
+ """Approximate Unicode script tag for a character, by codepoint range.
130
+
131
+ Used for script-boundary tokenization. CJK ideographs return
132
+ ``"Han"`` so the tokenizer can split per character (Chinese/Japanese
133
+ kanji have no whitespace word boundaries — each ideograph is its
134
+ own morphological unit). Coverage is intentionally NOT a complete
135
+ Unicode script table — it covers the scripts that have native
136
+ speakers in the Tier-1 benchmark + the major scripts that don't
137
+ use ASCII whitespace between words. Latin is the default fallback
138
+ (covers ASCII + Latin-1 + Latin Extended + diacritics not listed
139
+ explicitly).
140
+ """
141
+ hit = _SCRIPT_CACHE.get(ch)
142
+ if hit is not None:
143
+ return hit
144
+ out = _script_of_uncached(ord(ch))
145
+ if len(_SCRIPT_CACHE) < _SCRIPT_CACHE_LIMIT:
146
+ _SCRIPT_CACHE[ch] = out
147
+ return out
148
+
149
+
150
+ class PolyglotEncoder:
151
+ """LaBSE-inspired multilingual 768-dim encoder.
152
+
153
+ Produces a ``dims``-dim L2-normalized float32 vector from any
154
+ Unicode text. Pure numpy + stdlib unicodedata. No model download,
155
+ no GPU. Bit-identical across runs (BLAKE2b hashing + fixed feature
156
+ iteration order + single-threaded float32 accumulation).
157
+ """
158
+
159
+ def __init__(self, dims: int = 768,
160
+ ngram_sizes: tuple[int, ...] = (3, 4, 5),
161
+ seed: int = 0) -> None:
162
+ self.dims = int(dims)
163
+ self.ngram_sizes = tuple(int(n) for n in ngram_sizes)
164
+ self.seed = int(seed) & 0xFFFFFFFFFFFFFFFF
165
+ # Bounded feature cache: (feature_str -> (bucket_idx, sign)).
166
+ # Most real-world corpora have heavy n-gram repetition ("the",
167
+ # "^th", "the$", etc.) — caching the hash output lifts
168
+ # throughput ~4× (measured 10k -> 27k sent/sec on the bench
169
+ # corpus). Bounded to 1M entries (~150MB peak) so a runaway
170
+ # corpus can't OOM. Cache is purely a perf optimization —
171
+ # removing it leaves the algorithm bit-identical.
172
+ self._feat_cache: dict[str, tuple[int, int]] = {}
173
+ # Precompute the constant structural-feature hashes once at
174
+ # init — they're used per-token, so this saves a dict lookup
175
+ # (cache hit) per token. Output is identical to the cache-hit
176
+ # path; we just skip the lookup overhead.
177
+ self._tok_idx, self._tok_sign = self._hash("TOK")
178
+ self._tok_short_idx, self._tok_short_sign = self._hash("TOK:short")
179
+ self._tok_long_idx, self._tok_long_sign = self._hash("TOK:long")
180
+
181
+ # -- tokenization ------------------------------------------------------
182
+
183
+ def _tokenize(self, text: str) -> list[str]:
184
+ """Script-aware tokenization.
185
+
186
+ Splits on whitespace, punctuation/symbols, control chars, and
187
+ Unicode script transitions. CJK ideographs become one-char
188
+ tokens (no whitespace word boundaries in Chinese; each
189
+ ideograph is its own morphological unit). Combining marks
190
+ (Mn/Mc/Me) stick to the preceding letter (preserves Devanagari
191
+ vowel signs, Arabic diacritics, Latin combining accents).
192
+ """
193
+ tokens: list[str] = []
194
+ current: list[str] = []
195
+ prev_script: str | None = None
196
+ _script_of_local = _script_of # local-binding perf micro-opt
197
+
198
+ for ch in text:
199
+ cp = ord(ch)
200
+ # Fast path for ASCII — covers ~95% of English text and
201
+ # ~36% of the mixed-language bench corpus. Skips the
202
+ # unicodedata.category() call entirely for ASCII.
203
+ if cp < 128:
204
+ # ASCII letter or digit → keep in token (Latin script).
205
+ # ASCII whitespace / punctuation / control → break.
206
+ # Use a quick range check: 'A'-'Z', 'a'-'z', '0'-'9'.
207
+ if (65 <= cp <= 90) or (97 <= cp <= 122) or (48 <= cp <= 57):
208
+ script = "Latn"
209
+ else:
210
+ if current:
211
+ tokens.append("".join(current))
212
+ current = []
213
+ prev_script = None
214
+ continue
215
+ else:
216
+ # Slow path: non-ASCII char — needs unicodedata lookup.
217
+ cat = unicodedata.category(ch)
218
+
219
+ # Whitespace, separators, control, punctuation, symbols → break
220
+ if ch.isspace() or cat.startswith(("Z", "C", "P", "S")):
221
+ if current:
222
+ tokens.append("".join(current))
223
+ current = []
224
+ prev_script = None
225
+ continue
226
+
227
+ # Marks (Mn/Mc/Me) stick to the previous letter.
228
+ if cat.startswith("M"):
229
+ if current:
230
+ current.append(ch)
231
+ continue
232
+
233
+ script = _script_of_local(ch)
234
+
235
+ # CJK ideographs: each char is its own token.
236
+ if script == "Han":
237
+ if current:
238
+ tokens.append("".join(current))
239
+ current = []
240
+ tokens.append(ch)
241
+ prev_script = None
242
+ continue
243
+
244
+ # Script transition (e.g. Katakana → Latin, Cyrillic → Latin)
245
+ # breaks the token. This handles mixed-script strings like
246
+ # "アリスはGoogleで働いています" (Kana+Latin+Kana+Han+Kana)
247
+ # by splitting at every script boundary.
248
+ if prev_script is not None and script != prev_script:
249
+ tokens.append("".join(current))
250
+ current = []
251
+
252
+ current.append(ch)
253
+ prev_script = script
254
+
255
+ if current:
256
+ tokens.append("".join(current))
257
+ return tokens
258
+
259
+ # -- feature extraction -----------------------------------------------
260
+
261
+ def _char_features(self, token: str) -> list[str]:
262
+ """Char n-grams of the configured sizes, with start/end padding.
263
+
264
+ Each token is padded as ``^<token>$`` so the embedding captures
265
+ word-boundary information (the "^ali" prefix vs the "ice$"
266
+ suffix of "Alice" are different features — like LaBSE's
267
+ WordPiece ``<w>`` markers).
268
+
269
+ Short tokens (padded length < n) fall back to using the whole
270
+ padded form as a single feature for that n-gram size. This
271
+ means single-char CJK tokens still emit one feature per n-gram
272
+ size (so they contribute to the embedding, not get dropped).
273
+ """
274
+ padded = f"^{token}$"
275
+ feats: list[str] = []
276
+ for n in self.ngram_sizes:
277
+ if len(padded) < n:
278
+ # Short token fallback — emit the whole padded form
279
+ # once for this n-gram size.
280
+ feats.append(f"n{n}:{padded}")
281
+ continue
282
+ for i in range(len(padded) - n + 1):
283
+ feats.append(f"n{n}:{padded[i:i + n]}")
284
+ return feats
285
+
286
+ # -- hashing ----------------------------------------------------------
287
+
288
+ def _hash(self, feature: str) -> tuple[int, int]:
289
+ """Return (bucket_index, sign) for a feature string.
290
+
291
+ Uses ``h64`` from ``cortexm.util`` (BLAKE2b keyed by the
292
+ encoder seed). Bit 63 of the hash is the sign bit (Weinberger
293
+ et al. 2009 signed feature hashing — unbiased estimator of
294
+ inner product under hash collisions). Cached for perf; the
295
+ cache is purely a perf optimization — disabling it leaves
296
+ the algorithm output bit-identical.
297
+ """
298
+ hit = self._feat_cache.get(feature)
299
+ if hit is not None:
300
+ return hit
301
+ h = h64(feature, self.seed)
302
+ out = (h % self.dims, 1 if (h >> 63) & 1 else -1)
303
+ if len(self._feat_cache) < 1_000_000:
304
+ self._feat_cache[feature] = out
305
+ return out
306
+
307
+ # -- API --------------------------------------------------------------
308
+
309
+ def encode(self, text: str) -> np.ndarray:
310
+ """Encode any Unicode text to a dims-dim float32 L2-normalized vector.
311
+
312
+ Empty or whitespace-only input returns a zero vector (no crash).
313
+ Downstream callers should treat a zero vector as "no signal"
314
+ and skip indexing / fall back to a different encoder.
315
+ """
316
+ vec = np.zeros(self.dims, dtype=np.float32)
317
+ if not text:
318
+ return vec
319
+ tokens = self._tokenize(text)
320
+ if not tokens:
321
+ return vec
322
+
323
+ for tok in tokens:
324
+ # Char n-grams (weight 0.5 each) — captures morphology
325
+ # like LaBSE's WordPiece subword embeddings.
326
+ for feat in self._char_features(tok):
327
+ idx, sign = self._hash(feat)
328
+ vec[idx] += sign * 0.5
329
+
330
+ # Per-token structural features (weight 0.3 + 0.2). These
331
+ # are language-agnostic: every token in any script
332
+ # contributes the same set of structural features. This
333
+ # gives two sentences of similar shape a small positive
334
+ # cosine bias — the LaBSE-equivalent of "same sentence
335
+ # shape" alignment. Without this, the encoder degenerates
336
+ # to pure random projection and cross-language cos sims
337
+ # land at ~0.03 (random hashing noise floor).
338
+ tl = len(tok)
339
+ vec[self._tok_idx] += self._tok_sign * 0.3
340
+ if tl <= 4:
341
+ vec[self._tok_short_idx] += self._tok_short_sign * 0.2
342
+ else:
343
+ vec[self._tok_long_idx] += self._tok_long_sign * 0.2
344
+
345
+ # L2-normalize. Use a plain float32 reduction (vec*vec).sum()
346
+ # rather than np.linalg.norm — the latter may route through
347
+ # BLAS which can introduce ULP drift across runs when threads
348
+ # are unpinned. For a 768-dim float32 reduction, numpy's
349
+ # internal pairwise sum is deterministic in-process.
350
+ ss = float((vec * vec).sum())
351
+ if ss > 0.0:
352
+ n = math.sqrt(ss)
353
+ vec /= n
354
+ return vec
355
+
356
+ def encode_batch(self, texts: list[str]) -> np.ndarray:
357
+ """Encode a batch of texts. Returns shape (len(texts), dims).
358
+
359
+ Empty input list returns a (0, dims) array — no crash.
360
+ """
361
+ if not texts:
362
+ return np.zeros((0, self.dims), dtype=np.float32)
363
+ return np.stack([self.encode(t) for t in texts])
364
+
365
+
366
+ def encode(text: str, dims: int = 768) -> np.ndarray:
367
+ """Drop-in replacement for ``HashingEmbedder.embed`` (single text).
368
+
369
+ Recomputes a fresh ``PolyglotEncoder`` on every call — convenient
370
+ for one-off use, slower than reusing an encoder instance. For
371
+ batch use, instantiate ``PolyglotEncoder`` once and call
372
+ ``encode_batch``.
373
+ """
374
+ return PolyglotEncoder(dims).encode(text)