@polycode-projects/the-mechanical-code-talker 2.0.2 → 2.2.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.
- package/README.md +10 -7
- package/ROADMAP.md +27 -4
- package/corpus/generated/ace-surface-variants.jsonl +1 -0
- package/corpus/generated/manifest.json +3 -3
- package/package.json +1 -1
- package/src/adapters/toml-config.mjs +0 -1
- package/src/domain/ask-vocab.mjs +3 -0
- package/src/domain/ask.mjs +7 -1
- package/src/domain/codegraph.mjs +4 -68
- package/src/domain/markdown-links.mjs +55 -0
- package/src/domain/real-word-collisions.json +1 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +9 -78
- package/src/adapters/embed.mjs +0 -169
- package/src/domain/vector.mjs +0 -12
package/src/adapters/embed.mjs
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
// embed.mjs — deterministic static-embedding lookup (model2vec potion-base-8M,
|
|
2
|
-
// 29,528 WordPiece subwords × 256 fp32 dims). Pure table lookup + float
|
|
3
|
-
// arithmetic, no ONNX runtime, no network after the one-time fetch — the same
|
|
4
|
-
// text always embeds to the same vector.
|
|
5
|
-
//
|
|
6
|
-
// The safetensors reader and WordPiece tokenizer below are hand-rolled: both
|
|
7
|
-
// formats are simple enough to parse directly, avoiding an ONNX/HF tokenizer
|
|
8
|
-
// dependency for it.
|
|
9
|
-
//
|
|
10
|
-
// Weights are gitignored (vendor/, fetched by scripts/fetch-embeddings.mjs)
|
|
11
|
-
// and never in the npm package; loadEmbedder() returns null when absent so
|
|
12
|
-
// CI/tests never require the download.
|
|
13
|
-
|
|
14
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
15
|
-
import { join, dirname } from "node:path";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
-
|
|
18
|
-
const MODEL_FILE = "model.safetensors";
|
|
19
|
-
const TOKENIZER_FILE = "tokenizer.json";
|
|
20
|
-
const CONFIG_FILE = "config.json";
|
|
21
|
-
|
|
22
|
-
/** Default artifact dir: $TMCT_EMBED_DIR, else <repo>/vendor/embeddings/potion-base-8M
|
|
23
|
-
* (gitignored via vendor/ — the location scripts/fetch-embeddings.mjs writes). */
|
|
24
|
-
export function defaultEmbeddingsDir() {
|
|
25
|
-
if (process.env.TMCT_EMBED_DIR) return process.env.TMCT_EMBED_DIR;
|
|
26
|
-
const here = dirname(fileURLToPath(import.meta.url)); // packages/tmct/src
|
|
27
|
-
return join(here, "..", "..", "..", "..", "vendor", "embeddings", "potion-base-8M");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ---- safetensors (hand-rolled: u64le header length + JSON header + raw tensors) -------
|
|
31
|
-
|
|
32
|
-
/** Read the single 2-D F32 embedding tensor from a safetensors file →
|
|
33
|
-
* { matrix: Float32Array (row-major), rows, dim }. model2vec exports exactly one
|
|
34
|
-
* tensor named "embeddings"; any lone 2-D F32 tensor is accepted for test fixtures. */
|
|
35
|
-
function readSafetensors(file) {
|
|
36
|
-
const buf = readFileSync(file);
|
|
37
|
-
const headerLen = Number(buf.readBigUInt64LE(0));
|
|
38
|
-
const header = JSON.parse(buf.subarray(8, 8 + headerLen).toString("utf8"));
|
|
39
|
-
const name = header.embeddings
|
|
40
|
-
? "embeddings"
|
|
41
|
-
: Object.keys(header).find((k) => k !== "__metadata__" && header[k]?.shape?.length === 2);
|
|
42
|
-
const t = name && header[name];
|
|
43
|
-
if (!t) throw new Error(`no 2-D tensor in ${file}`);
|
|
44
|
-
if (t.dtype !== "F32") throw new Error(`unsupported dtype ${t.dtype} in ${file} (only F32)`);
|
|
45
|
-
const [rows, dim] = t.shape;
|
|
46
|
-
const [start, end] = t.data_offsets;
|
|
47
|
-
const bytes = buf.subarray(8 + headerLen + start, 8 + headerLen + end);
|
|
48
|
-
if (bytes.byteLength !== rows * dim * 4) throw new Error(`tensor size mismatch in ${file}`);
|
|
49
|
-
// Copy into a fresh ArrayBuffer: the slice's byteOffset inside the file buffer is not
|
|
50
|
-
// guaranteed 4-byte aligned, and Float32Array requires alignment.
|
|
51
|
-
const matrix = new Float32Array(rows * dim);
|
|
52
|
-
new Uint8Array(matrix.buffer).set(bytes);
|
|
53
|
-
return { matrix, rows, dim };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ---- Bert-style WordPiece tokenizer (from tokenizer.json) -----------------------------
|
|
57
|
-
|
|
58
|
-
const isPunct = (ch) => {
|
|
59
|
-
const c = ch.codePointAt(0);
|
|
60
|
-
// ASCII punctuation ranges (Bert treats these as standalone tokens) + general unicode P/S.
|
|
61
|
-
return (c >= 33 && c <= 47) || (c >= 58 && c <= 64) || (c >= 91 && c <= 96) || (c >= 123 && c <= 126) ||
|
|
62
|
-
/[\p{P}\p{S}]/u.test(ch);
|
|
63
|
-
};
|
|
64
|
-
const isCjk = (ch) => {
|
|
65
|
-
const c = ch.codePointAt(0);
|
|
66
|
-
return (c >= 0x4e00 && c <= 0x9fff) || (c >= 0x3400 && c <= 0x4dbf) ||
|
|
67
|
-
(c >= 0xf900 && c <= 0xfaff) || (c >= 0x20000 && c <= 0x2ffff);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
function makeTokenizer(spec) {
|
|
71
|
-
if (spec?.model?.type !== "WordPiece") {
|
|
72
|
-
throw new Error(`unsupported tokenizer model "${spec?.model?.type}" (only WordPiece)`);
|
|
73
|
-
}
|
|
74
|
-
const vocab = spec.model.vocab; // token -> id
|
|
75
|
-
const contPrefix = spec.model.continuing_subword_prefix ?? "##";
|
|
76
|
-
const maxChars = spec.model.max_input_chars_per_word ?? 100;
|
|
77
|
-
const unkId = vocab[spec.model.unk_token] ?? null;
|
|
78
|
-
const lowercase = spec.normalizer?.lowercase !== false;
|
|
79
|
-
|
|
80
|
-
// BertNormalizer: clean control chars, pad CJK, lowercase (+ strip accents when lowercasing).
|
|
81
|
-
const normalize = (text) => {
|
|
82
|
-
let s = String(text).replace(/[\u0000\ufffd]/g, "").replace(/[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
|
|
83
|
-
s = [...s].map((ch) => (isCjk(ch) ? ` ${ch} ` : ch)).join("");
|
|
84
|
-
if (lowercase) s = s.toLowerCase().normalize("NFD").replace(/\p{Mn}/gu, "");
|
|
85
|
-
return s;
|
|
86
|
-
};
|
|
87
|
-
// BertPreTokenizer: split on whitespace; every punctuation char is its own word.
|
|
88
|
-
const preTokenize = (s) => {
|
|
89
|
-
const words = [];
|
|
90
|
-
for (const chunk of s.split(/\s+/)) {
|
|
91
|
-
if (!chunk) continue;
|
|
92
|
-
let cur = "";
|
|
93
|
-
for (const ch of chunk) {
|
|
94
|
-
if (isPunct(ch)) {
|
|
95
|
-
if (cur) { words.push(cur); cur = ""; }
|
|
96
|
-
words.push(ch);
|
|
97
|
-
} else cur += ch;
|
|
98
|
-
}
|
|
99
|
-
if (cur) words.push(cur);
|
|
100
|
-
}
|
|
101
|
-
return words;
|
|
102
|
-
};
|
|
103
|
-
// Greedy longest-match WordPiece per word; a word with no valid segmentation → [UNK].
|
|
104
|
-
const wordPiece = (word) => {
|
|
105
|
-
if (word.length > maxChars) return unkId == null ? [] : [unkId];
|
|
106
|
-
const ids = [];
|
|
107
|
-
let start = 0;
|
|
108
|
-
while (start < word.length) {
|
|
109
|
-
let end = word.length;
|
|
110
|
-
let id = null;
|
|
111
|
-
while (end > start) {
|
|
112
|
-
const piece = (start > 0 ? contPrefix : "") + word.slice(start, end);
|
|
113
|
-
if (vocab[piece] !== undefined) { id = vocab[piece]; break; }
|
|
114
|
-
end--;
|
|
115
|
-
}
|
|
116
|
-
if (id == null) return unkId == null ? [] : [unkId];
|
|
117
|
-
ids.push(id);
|
|
118
|
-
start = end;
|
|
119
|
-
}
|
|
120
|
-
return ids;
|
|
121
|
-
};
|
|
122
|
-
// No [CLS]/[SEP] template — model2vec pools content subwords only.
|
|
123
|
-
return (text) => preTokenize(normalize(text)).flatMap(wordPiece);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ---- embedder ---------------------------------------------------------------------------
|
|
127
|
-
|
|
128
|
-
const LOADED = new Map(); // dir -> embedder | null (per-process cache; weights load once)
|
|
129
|
-
|
|
130
|
-
/** Load tokenizer + embedding matrix from `dir` (default: defaultEmbeddingsDir()).
|
|
131
|
-
* Returns null — silently — when the artifacts are absent (the one-time
|
|
132
|
-
* `npm run refs:embeddings` fetch has not run): callers no-op, never fail.
|
|
133
|
-
* The returned embedder is { dim, dir, embed(text) → L2-normalised Float32Array }. */
|
|
134
|
-
export function loadEmbedder({ dir = defaultEmbeddingsDir() } = {}) {
|
|
135
|
-
if (LOADED.has(dir)) return LOADED.get(dir);
|
|
136
|
-
const modelFile = join(dir, MODEL_FILE);
|
|
137
|
-
const tokFile = join(dir, TOKENIZER_FILE);
|
|
138
|
-
if (!existsSync(modelFile) || !existsSync(tokFile)) {
|
|
139
|
-
LOADED.set(dir, null);
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
const { matrix, rows, dim } = readSafetensors(modelFile);
|
|
143
|
-
const tokenize = makeTokenizer(JSON.parse(readFileSync(tokFile, "utf8")));
|
|
144
|
-
let cfg = {};
|
|
145
|
-
try { cfg = JSON.parse(readFileSync(join(dir, CONFIG_FILE), "utf8")); } catch { /* optional */ }
|
|
146
|
-
const doNormalize = cfg.normalize !== false;
|
|
147
|
-
|
|
148
|
-
const embed = (text) => {
|
|
149
|
-
const ids = tokenize(text);
|
|
150
|
-
const v = new Float32Array(dim);
|
|
151
|
-
if (!ids.length) return v; // zero vector: cosine 0 against everything
|
|
152
|
-
for (const id of ids) {
|
|
153
|
-
if (id < 0 || id >= rows) continue;
|
|
154
|
-
const off = id * dim;
|
|
155
|
-
for (let j = 0; j < dim; j++) v[j] += matrix[off + j];
|
|
156
|
-
}
|
|
157
|
-
for (let j = 0; j < dim; j++) v[j] /= ids.length; // mean pool
|
|
158
|
-
if (doNormalize) {
|
|
159
|
-
let norm = 0;
|
|
160
|
-
for (let j = 0; j < dim; j++) norm += v[j] * v[j];
|
|
161
|
-
norm = Math.sqrt(norm);
|
|
162
|
-
if (norm > 0) for (let j = 0; j < dim; j++) v[j] /= norm;
|
|
163
|
-
}
|
|
164
|
-
return v;
|
|
165
|
-
};
|
|
166
|
-
const embedder = { dim, dir, embed };
|
|
167
|
-
LOADED.set(dir, embedder);
|
|
168
|
-
return embedder;
|
|
169
|
-
}
|
package/src/domain/vector.mjs
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// vector.mjs — pure vector arithmetic over embedding vectors. No model, no fs:
|
|
2
|
-
// the loader that reads weights off disk lives in src/adapters/embed.mjs.
|
|
3
|
-
|
|
4
|
-
/** Cosine similarity. Over L2-normalised vectors this is just the dot product, but the
|
|
5
|
-
* full form is kept so unnormalised test fixtures behave. 0 when either vector is zero. */
|
|
6
|
-
export function cosine(a, b) {
|
|
7
|
-
let dot = 0, na = 0, nb = 0;
|
|
8
|
-
const n = Math.min(a.length, b.length);
|
|
9
|
-
for (let i = 0; i < n; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; }
|
|
10
|
-
if (na === 0 || nb === 0) return 0;
|
|
11
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
12
|
-
}
|