imprnt 0.1.3 → 0.1.4-edge.74
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 +6 -6
- package/dist/cli.js +174 -14
- package/dist/imp.js +174 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,14 +123,10 @@ asking ("add the anti-slop plugin"), each a separate `imprnt-plugin-*` package:
|
|
|
123
123
|
|
|
124
124
|
| Package | What it gives your assistant |
|
|
125
125
|
|---------|------------------------------|
|
|
126
|
-
| `imprnt-plugin-character` | A voice and standards to write in. "Scribe" is the default you copy and personalize. |
|
|
127
126
|
| `imprnt-plugin-anti-slop` | Rules that keep its prose from reading like AI. |
|
|
128
|
-
| `imprnt-plugin-
|
|
129
|
-
| `imprnt-plugin-kleinanzeigen` | A watcher for your Kleinanzeigen inbox: regex triage of buyer messages, drafts, a phone digest. You press send. |
|
|
130
|
-
| `imprnt-plugin-session-host` | A warm browser holding your logged-in sessions, providing the authed-session capability. You enroll each site once. Not yet on npm: install from a repo checkout with `imprnt plugin add session-host --from <dir>`. |
|
|
131
|
-
| `imprnt-plugin-timemachine` | Snapshots your work before each change so you can recover what the agent breaks. |
|
|
127
|
+
| `imprnt-plugin-character` | A voice and standards to write in. "Scribe" is the default you copy and personalize. |
|
|
132
128
|
| `imprnt-plugin-statusline` | A customizable status line: model, branch, context, cost, rate limits, clock. |
|
|
133
|
-
| `imprnt-plugin-
|
|
129
|
+
| `imprnt-plugin-timemachine` | Snapshots your work before each change so you can recover what the agent breaks. |
|
|
134
130
|
|
|
135
131
|
Adding one copies it into your project and wires it into `CLAUDE.local.md`, the per-machine file
|
|
136
132
|
your assistant loads each session. A fresh setup loads zero plugins until you add them. The full
|
|
@@ -180,3 +176,7 @@ only, never needed by people who use it through their assistant). Clone, `bun in
|
|
|
180
176
|
## License
|
|
181
177
|
|
|
182
178
|
MIT (c) 2026 Aleksandr Bogdanov
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
<img src=".github/mark.svg" height="15" alt=""> built by [bogdanov.wtf](https://bogdanov.wtf)
|
package/dist/cli.js
CHANGED
|
@@ -1879,6 +1879,28 @@ var init_tags = __esm(() => {
|
|
|
1879
1879
|
var exports_recall = {};
|
|
1880
1880
|
import { lstatSync as lstatSync2, readdirSync as readdirSync6, readFileSync as readFileSync11 } from "node:fs";
|
|
1881
1881
|
import { basename as basename4, join as join12, relative as relative4 } from "node:path";
|
|
1882
|
+
function stemWord(w) {
|
|
1883
|
+
if (w.length <= 3)
|
|
1884
|
+
return w;
|
|
1885
|
+
if (/[a-z]$/.test(w) === false)
|
|
1886
|
+
return w;
|
|
1887
|
+
if (w.endsWith("sses"))
|
|
1888
|
+
return w.slice(0, -2);
|
|
1889
|
+
if (w.endsWith("ies") && w.length > 4)
|
|
1890
|
+
return w.slice(0, -3) + "y";
|
|
1891
|
+
if (w.endsWith("ss") || w.endsWith("us") || w.endsWith("is")) {} else if (w.endsWith("s") && !w.endsWith("ous"))
|
|
1892
|
+
w = w.slice(0, -1);
|
|
1893
|
+
if (w.length > 4 && w.endsWith("ing")) {
|
|
1894
|
+
const base = w.slice(0, -3);
|
|
1895
|
+
if (base.length >= 3)
|
|
1896
|
+
w = /([bdfglmnprt])\1$/.test(base) ? base.slice(0, -1) : base;
|
|
1897
|
+
} else if (w.length > 3 && w.endsWith("ed")) {
|
|
1898
|
+
const base = w.slice(0, -2);
|
|
1899
|
+
if (base.length >= 3)
|
|
1900
|
+
w = /([bdfglmnprt])\1$/.test(base) ? base.slice(0, -1) : base;
|
|
1901
|
+
}
|
|
1902
|
+
return w;
|
|
1903
|
+
}
|
|
1882
1904
|
function walk2(dir2) {
|
|
1883
1905
|
const out = [];
|
|
1884
1906
|
for (const entry of readdirSync6(dir2)) {
|
|
@@ -1902,7 +1924,53 @@ function walk2(dir2) {
|
|
|
1902
1924
|
}
|
|
1903
1925
|
return out;
|
|
1904
1926
|
}
|
|
1905
|
-
|
|
1927
|
+
function bestPassages(absPath, n) {
|
|
1928
|
+
let raw = "";
|
|
1929
|
+
try {
|
|
1930
|
+
raw = stripBom(readFileSync11(absPath, "utf8"));
|
|
1931
|
+
} catch {
|
|
1932
|
+
return [];
|
|
1933
|
+
}
|
|
1934
|
+
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1935
|
+
const body2 = fmMatch ? raw.slice(fmMatch.index + fmMatch[0].length) : raw;
|
|
1936
|
+
const paras = body2.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
1937
|
+
if (paras.length <= n)
|
|
1938
|
+
return paras;
|
|
1939
|
+
const scoredParas = paras.map((p) => {
|
|
1940
|
+
const tf = new Map;
|
|
1941
|
+
for (const t of tokenize(p))
|
|
1942
|
+
tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
1943
|
+
let plen = 0;
|
|
1944
|
+
for (const c of tf.values())
|
|
1945
|
+
plen += c;
|
|
1946
|
+
let sc = 0;
|
|
1947
|
+
const used = new Set;
|
|
1948
|
+
for (const group of scoringGroups) {
|
|
1949
|
+
let best = 0, bv = "";
|
|
1950
|
+
for (const v of group) {
|
|
1951
|
+
if (used.has(v))
|
|
1952
|
+
continue;
|
|
1953
|
+
const x = bm25Term(tf.get(v) ?? 0, plen, variantIdf.get(v) ?? 0);
|
|
1954
|
+
if (x > best) {
|
|
1955
|
+
best = x;
|
|
1956
|
+
bv = v;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
if (best > 0) {
|
|
1960
|
+
sc += best;
|
|
1961
|
+
used.add(bv);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
return { p, sc };
|
|
1965
|
+
});
|
|
1966
|
+
const keep = new Set(scoredParas.slice().sort((a, b) => b.sc - a.sc).slice(0, n).filter((x) => x.sc > 0).map((x) => x.p));
|
|
1967
|
+
const out = paras.filter((p) => keep.has(p));
|
|
1968
|
+
return out.length ? out : paras.slice(0, n);
|
|
1969
|
+
}
|
|
1970
|
+
var args2, vault2, limit = 15, gap = 0, passages = 0, proximity = false, coverage = false, stem = false, positional2, query, vocab, MAX_SYNONYM_NGRAM, STOPWORDS, tokenize = (text2) => {
|
|
1971
|
+
const raw = text2.normalize("NFC").toLowerCase().replace(/['’]/gu, "").split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
1972
|
+
return stem ? raw.map(stemWord) : raw;
|
|
1973
|
+
}, phraseSynonymTokens = (q) => {
|
|
1906
1974
|
const out = [];
|
|
1907
1975
|
const words = q.normalize("NFC").toLowerCase().split(/[\s-]+/).map((w) => w.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "")).filter(Boolean);
|
|
1908
1976
|
for (let n = Math.min(words.length, MAX_SYNONYM_NGRAM);n >= 2; n--) {
|
|
@@ -1916,14 +1984,14 @@ var args2, vault2, limit = 15, positional2, query, vocab, MAX_SYNONYM_NGRAM, STO
|
|
|
1916
1984
|
}
|
|
1917
1985
|
}
|
|
1918
1986
|
return out;
|
|
1919
|
-
}, rawTerms, contentTerms, baseTerms, queryTerms, CONTROL2, files, TITLE_BOOST = 3, TAG_BOOST = 2, SUMMARY_BOOST = 1, BODY_BOOST = 1, docs, df, N, avgdl, K1 = 1.5, B = 0.75, idf = (term) => {
|
|
1987
|
+
}, rawTerms, contentTerms, baseTerms, queryTerms, queryVariantSet, CONTROL2, files, TITLE_BOOST = 3, TAG_BOOST = 2, SUMMARY_BOOST = 1, BODY_BOOST = 1, docs, df, N, avgdl, K1 = 1.5, B = 0.75, PROX_WINDOW = 25, PROX_WEIGHT = 0.35, idf = (term) => {
|
|
1920
1988
|
const n = df.get(term) ?? 0;
|
|
1921
1989
|
return Math.max(0, Math.log(1 + (N - n + 0.5) / (n + 0.5)));
|
|
1922
1990
|
}, bm25Term = (tf, dl, termIdf) => {
|
|
1923
1991
|
if (tf <= 0)
|
|
1924
1992
|
return 0;
|
|
1925
1993
|
return termIdf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * (dl / avgdl)));
|
|
1926
|
-
}, variantIdf, hits, scoringGroups, shown, expanded;
|
|
1994
|
+
}, variantIdf, hits, scoringGroups, cut, shown, expanded;
|
|
1927
1995
|
var init_recall = __esm(() => {
|
|
1928
1996
|
init_tags();
|
|
1929
1997
|
init_moc();
|
|
@@ -1950,12 +2018,42 @@ var init_recall = __esm(() => {
|
|
|
1950
2018
|
process.exit(1);
|
|
1951
2019
|
}
|
|
1952
2020
|
limit = n;
|
|
2021
|
+
} else if (args2[i] === "--proximity") {
|
|
2022
|
+
proximity = true;
|
|
2023
|
+
} else if (args2[i] === "--coverage") {
|
|
2024
|
+
coverage = true;
|
|
2025
|
+
} else if (args2[i] === "--stem") {
|
|
2026
|
+
stem = true;
|
|
2027
|
+
} else if (args2[i] === "--passages") {
|
|
2028
|
+
const tok = args2[++i];
|
|
2029
|
+
if (tok === undefined || !/^[0-9]+$/.test(tok)) {
|
|
2030
|
+
console.error("--passages must be a positive integer");
|
|
2031
|
+
process.exit(1);
|
|
2032
|
+
}
|
|
2033
|
+
const n = parseInt(tok, 10);
|
|
2034
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
2035
|
+
console.error("--passages must be a positive integer");
|
|
2036
|
+
process.exit(1);
|
|
2037
|
+
}
|
|
2038
|
+
passages = n;
|
|
2039
|
+
} else if (args2[i] === "--gap") {
|
|
2040
|
+
const tok = args2[++i];
|
|
2041
|
+
if (tok === undefined || !/^0?\.[0-9]+$/.test(tok)) {
|
|
2042
|
+
console.error("--gap must be a ratio between 0 and 1 (e.g. 0.25)");
|
|
2043
|
+
process.exit(1);
|
|
2044
|
+
}
|
|
2045
|
+
const g = parseFloat(tok);
|
|
2046
|
+
if (!Number.isFinite(g) || g <= 0 || g >= 1) {
|
|
2047
|
+
console.error("--gap must be a ratio between 0 and 1 (e.g. 0.25)");
|
|
2048
|
+
process.exit(1);
|
|
2049
|
+
}
|
|
2050
|
+
gap = g;
|
|
1953
2051
|
} else
|
|
1954
2052
|
positional2.push(args2[i]);
|
|
1955
2053
|
}
|
|
1956
2054
|
query = positional2.join(" ").trim();
|
|
1957
2055
|
if (!query) {
|
|
1958
|
-
console.error('usage: imprnt recall "<query>" [--vault DIR] [--limit N]');
|
|
2056
|
+
console.error('usage: imprnt recall "<query>" [--vault DIR] [--limit N] [--gap R] [--passages N] [--proximity] [--coverage] [--stem]');
|
|
1959
2057
|
process.exit(1);
|
|
1960
2058
|
}
|
|
1961
2059
|
vocab = loadTags(vault2);
|
|
@@ -2063,6 +2161,7 @@ var init_recall = __esm(() => {
|
|
|
2063
2161
|
contentTerms = rawTerms.filter((w) => !STOPWORDS.has(w));
|
|
2064
2162
|
baseTerms = contentTerms.length ? contentTerms : rawTerms;
|
|
2065
2163
|
queryTerms = baseTerms.map((w) => [...new Set([w, ...tokenize(normalize(vocab, w))])]);
|
|
2164
|
+
queryVariantSet = new Set(queryTerms.flat());
|
|
2066
2165
|
for (const t of phraseSynonymTokens(query)) {
|
|
2067
2166
|
if (!STOPWORDS.has(t))
|
|
2068
2167
|
queryTerms.push([t]);
|
|
@@ -2099,11 +2198,26 @@ var init_recall = __esm(() => {
|
|
|
2099
2198
|
add(tokenize(aliases), TITLE_BOOST);
|
|
2100
2199
|
add(tags.flatMap(tokenize), TAG_BOOST);
|
|
2101
2200
|
add(tokenize(summary), SUMMARY_BOOST);
|
|
2102
|
-
|
|
2201
|
+
const bodyTokens = tokenize(body2);
|
|
2202
|
+
add(bodyTokens, BODY_BOOST);
|
|
2203
|
+
let pos;
|
|
2204
|
+
if (proximity) {
|
|
2205
|
+
pos = new Map;
|
|
2206
|
+
for (let bi = 0;bi < bodyTokens.length; bi++) {
|
|
2207
|
+
const t = bodyTokens[bi];
|
|
2208
|
+
if (!queryVariantSet.has(t))
|
|
2209
|
+
continue;
|
|
2210
|
+
const arr = pos.get(t);
|
|
2211
|
+
if (arr)
|
|
2212
|
+
arr.push(bi);
|
|
2213
|
+
else
|
|
2214
|
+
pos.set(t, [bi]);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2103
2217
|
let len = 0;
|
|
2104
2218
|
for (const c of tf.values())
|
|
2105
2219
|
len += c;
|
|
2106
|
-
docs.push({ path, tf, len });
|
|
2220
|
+
docs.push({ path, tf, len, pos });
|
|
2107
2221
|
for (const term of tf.keys())
|
|
2108
2222
|
df.set(term, (df.get(term) ?? 0) + 1);
|
|
2109
2223
|
}
|
|
@@ -2136,23 +2250,69 @@ var init_recall = __esm(() => {
|
|
|
2136
2250
|
scored.add(bestVariant);
|
|
2137
2251
|
}
|
|
2138
2252
|
}
|
|
2139
|
-
if (score > 0)
|
|
2253
|
+
if (score > 0) {
|
|
2254
|
+
if (coverage && scoringGroups.length > 1) {
|
|
2255
|
+
score *= 0.5 + 0.5 * (scored.size / scoringGroups.length);
|
|
2256
|
+
}
|
|
2257
|
+
if (proximity && d.pos && scored.size > 1) {
|
|
2258
|
+
const present = [...scored].filter((t) => d.pos.has(t));
|
|
2259
|
+
let bonus = 0;
|
|
2260
|
+
for (let x = 0;x < present.length; x++) {
|
|
2261
|
+
for (let y = x + 1;y < present.length; y++) {
|
|
2262
|
+
const a = d.pos.get(present[x]), b = d.pos.get(present[y]);
|
|
2263
|
+
let best = Infinity;
|
|
2264
|
+
for (let ia = 0, ib = 0;ia < a.length && ib < b.length; ) {
|
|
2265
|
+
const dist = Math.abs(a[ia] - b[ib]);
|
|
2266
|
+
if (dist < best)
|
|
2267
|
+
best = dist;
|
|
2268
|
+
if (a[ia] < b[ib])
|
|
2269
|
+
ia++;
|
|
2270
|
+
else
|
|
2271
|
+
ib++;
|
|
2272
|
+
}
|
|
2273
|
+
if (best < PROX_WINDOW) {
|
|
2274
|
+
const w = Math.min(variantIdf.get(present[x]) ?? 0, variantIdf.get(present[y]) ?? 0);
|
|
2275
|
+
bonus += PROX_WEIGHT * w * (1 - best / PROX_WINDOW);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
score += bonus;
|
|
2280
|
+
}
|
|
2140
2281
|
hits.push({ path: relative4(vault2, d.path), score: Math.round(score * 100) / 100 });
|
|
2282
|
+
}
|
|
2141
2283
|
}
|
|
2142
2284
|
hits.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
2143
2285
|
if (!hits.length) {
|
|
2144
2286
|
console.log(`no matches for "${query}" in ${vault2}`);
|
|
2145
2287
|
process.exit(0);
|
|
2146
2288
|
}
|
|
2147
|
-
|
|
2289
|
+
cut = Math.min(limit, hits.length);
|
|
2290
|
+
if (gap > 0) {
|
|
2291
|
+
for (let i = 1;i < cut; i++) {
|
|
2292
|
+
if (hits[i].score < gap * hits[i - 1].score) {
|
|
2293
|
+
cut = i;
|
|
2294
|
+
break;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
shown = hits.slice(0, cut);
|
|
2148
2299
|
expanded = queryTerms.map((g) => g.join("|")).join(" ");
|
|
2149
|
-
console.log(`recall "${query}" [${expanded}]
|
|
2300
|
+
console.log(`recall "${query}" [${expanded}] - ${hits.length} match(es)${hits.length > shown.length ? `, showing top ${shown.length}` : ""}, BM25-ranked:
|
|
2150
2301
|
`);
|
|
2151
|
-
for (const h of shown)
|
|
2302
|
+
for (const h of shown) {
|
|
2152
2303
|
console.log(` [${h.score.toFixed(2)}] ${h.path}`);
|
|
2304
|
+
if (passages > 0) {
|
|
2305
|
+
for (const p of bestPassages(join12(vault2, h.path), passages)) {
|
|
2306
|
+
for (const line of p.split(`
|
|
2307
|
+
`))
|
|
2308
|
+
console.log(` ${line}`);
|
|
2309
|
+
console.log("");
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2153
2313
|
if (hits.length > shown.length) {
|
|
2154
2314
|
console.log(`
|
|
2155
|
-
|
|
2315
|
+
... ${hits.length - shown.length} lower-ranked hit(s) hidden. Raise with --limit if needed. Usually you do not.`);
|
|
2156
2316
|
}
|
|
2157
2317
|
});
|
|
2158
2318
|
|
|
@@ -2261,11 +2421,11 @@ var init_snapshot = __esm(() => {
|
|
|
2261
2421
|
continue;
|
|
2262
2422
|
}
|
|
2263
2423
|
const ext = extname2(rel);
|
|
2264
|
-
const
|
|
2265
|
-
let relD = `${
|
|
2424
|
+
const stem2 = rel.slice(0, rel.length - ext.length);
|
|
2425
|
+
let relD = `${stem2}-${hash2.slice(0, 8)}${ext}`;
|
|
2266
2426
|
let n = 2;
|
|
2267
2427
|
while (existsSync11(join13(destRoot, relD)) && Buffer.compare(readFileSync12(join13(destRoot, relD)), srcBytes2) !== 0) {
|
|
2268
|
-
relD = `${
|
|
2428
|
+
relD = `${stem2}-${hash2.slice(0, 8)}-${n}${ext}`;
|
|
2269
2429
|
n++;
|
|
2270
2430
|
}
|
|
2271
2431
|
rawPath2 = join13(destRoot, relD);
|
package/dist/imp.js
CHANGED
|
@@ -1879,6 +1879,28 @@ var init_tags = __esm(() => {
|
|
|
1879
1879
|
var exports_recall = {};
|
|
1880
1880
|
import { lstatSync as lstatSync2, readdirSync as readdirSync6, readFileSync as readFileSync11 } from "node:fs";
|
|
1881
1881
|
import { basename as basename4, join as join12, relative as relative4 } from "node:path";
|
|
1882
|
+
function stemWord(w) {
|
|
1883
|
+
if (w.length <= 3)
|
|
1884
|
+
return w;
|
|
1885
|
+
if (/[a-z]$/.test(w) === false)
|
|
1886
|
+
return w;
|
|
1887
|
+
if (w.endsWith("sses"))
|
|
1888
|
+
return w.slice(0, -2);
|
|
1889
|
+
if (w.endsWith("ies") && w.length > 4)
|
|
1890
|
+
return w.slice(0, -3) + "y";
|
|
1891
|
+
if (w.endsWith("ss") || w.endsWith("us") || w.endsWith("is")) {} else if (w.endsWith("s") && !w.endsWith("ous"))
|
|
1892
|
+
w = w.slice(0, -1);
|
|
1893
|
+
if (w.length > 4 && w.endsWith("ing")) {
|
|
1894
|
+
const base = w.slice(0, -3);
|
|
1895
|
+
if (base.length >= 3)
|
|
1896
|
+
w = /([bdfglmnprt])\1$/.test(base) ? base.slice(0, -1) : base;
|
|
1897
|
+
} else if (w.length > 3 && w.endsWith("ed")) {
|
|
1898
|
+
const base = w.slice(0, -2);
|
|
1899
|
+
if (base.length >= 3)
|
|
1900
|
+
w = /([bdfglmnprt])\1$/.test(base) ? base.slice(0, -1) : base;
|
|
1901
|
+
}
|
|
1902
|
+
return w;
|
|
1903
|
+
}
|
|
1882
1904
|
function walk2(dir2) {
|
|
1883
1905
|
const out = [];
|
|
1884
1906
|
for (const entry of readdirSync6(dir2)) {
|
|
@@ -1902,7 +1924,53 @@ function walk2(dir2) {
|
|
|
1902
1924
|
}
|
|
1903
1925
|
return out;
|
|
1904
1926
|
}
|
|
1905
|
-
|
|
1927
|
+
function bestPassages(absPath, n) {
|
|
1928
|
+
let raw = "";
|
|
1929
|
+
try {
|
|
1930
|
+
raw = stripBom(readFileSync11(absPath, "utf8"));
|
|
1931
|
+
} catch {
|
|
1932
|
+
return [];
|
|
1933
|
+
}
|
|
1934
|
+
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1935
|
+
const body2 = fmMatch ? raw.slice(fmMatch.index + fmMatch[0].length) : raw;
|
|
1936
|
+
const paras = body2.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
1937
|
+
if (paras.length <= n)
|
|
1938
|
+
return paras;
|
|
1939
|
+
const scoredParas = paras.map((p) => {
|
|
1940
|
+
const tf = new Map;
|
|
1941
|
+
for (const t of tokenize(p))
|
|
1942
|
+
tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
1943
|
+
let plen = 0;
|
|
1944
|
+
for (const c of tf.values())
|
|
1945
|
+
plen += c;
|
|
1946
|
+
let sc = 0;
|
|
1947
|
+
const used = new Set;
|
|
1948
|
+
for (const group of scoringGroups) {
|
|
1949
|
+
let best = 0, bv = "";
|
|
1950
|
+
for (const v of group) {
|
|
1951
|
+
if (used.has(v))
|
|
1952
|
+
continue;
|
|
1953
|
+
const x = bm25Term(tf.get(v) ?? 0, plen, variantIdf.get(v) ?? 0);
|
|
1954
|
+
if (x > best) {
|
|
1955
|
+
best = x;
|
|
1956
|
+
bv = v;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
if (best > 0) {
|
|
1960
|
+
sc += best;
|
|
1961
|
+
used.add(bv);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
return { p, sc };
|
|
1965
|
+
});
|
|
1966
|
+
const keep = new Set(scoredParas.slice().sort((a, b) => b.sc - a.sc).slice(0, n).filter((x) => x.sc > 0).map((x) => x.p));
|
|
1967
|
+
const out = paras.filter((p) => keep.has(p));
|
|
1968
|
+
return out.length ? out : paras.slice(0, n);
|
|
1969
|
+
}
|
|
1970
|
+
var args2, vault2, limit = 15, gap = 0, passages = 0, proximity = false, coverage = false, stem = false, positional2, query, vocab, MAX_SYNONYM_NGRAM, STOPWORDS, tokenize = (text2) => {
|
|
1971
|
+
const raw = text2.normalize("NFC").toLowerCase().replace(/['’]/gu, "").split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
1972
|
+
return stem ? raw.map(stemWord) : raw;
|
|
1973
|
+
}, phraseSynonymTokens = (q) => {
|
|
1906
1974
|
const out = [];
|
|
1907
1975
|
const words = q.normalize("NFC").toLowerCase().split(/[\s-]+/).map((w) => w.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "")).filter(Boolean);
|
|
1908
1976
|
for (let n = Math.min(words.length, MAX_SYNONYM_NGRAM);n >= 2; n--) {
|
|
@@ -1916,14 +1984,14 @@ var args2, vault2, limit = 15, positional2, query, vocab, MAX_SYNONYM_NGRAM, STO
|
|
|
1916
1984
|
}
|
|
1917
1985
|
}
|
|
1918
1986
|
return out;
|
|
1919
|
-
}, rawTerms, contentTerms, baseTerms, queryTerms, CONTROL2, files, TITLE_BOOST = 3, TAG_BOOST = 2, SUMMARY_BOOST = 1, BODY_BOOST = 1, docs, df, N, avgdl, K1 = 1.5, B = 0.75, idf = (term) => {
|
|
1987
|
+
}, rawTerms, contentTerms, baseTerms, queryTerms, queryVariantSet, CONTROL2, files, TITLE_BOOST = 3, TAG_BOOST = 2, SUMMARY_BOOST = 1, BODY_BOOST = 1, docs, df, N, avgdl, K1 = 1.5, B = 0.75, PROX_WINDOW = 25, PROX_WEIGHT = 0.35, idf = (term) => {
|
|
1920
1988
|
const n = df.get(term) ?? 0;
|
|
1921
1989
|
return Math.max(0, Math.log(1 + (N - n + 0.5) / (n + 0.5)));
|
|
1922
1990
|
}, bm25Term = (tf, dl, termIdf) => {
|
|
1923
1991
|
if (tf <= 0)
|
|
1924
1992
|
return 0;
|
|
1925
1993
|
return termIdf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * (dl / avgdl)));
|
|
1926
|
-
}, variantIdf, hits, scoringGroups, shown, expanded;
|
|
1994
|
+
}, variantIdf, hits, scoringGroups, cut, shown, expanded;
|
|
1927
1995
|
var init_recall = __esm(() => {
|
|
1928
1996
|
init_tags();
|
|
1929
1997
|
init_moc();
|
|
@@ -1950,12 +2018,42 @@ var init_recall = __esm(() => {
|
|
|
1950
2018
|
process.exit(1);
|
|
1951
2019
|
}
|
|
1952
2020
|
limit = n;
|
|
2021
|
+
} else if (args2[i] === "--proximity") {
|
|
2022
|
+
proximity = true;
|
|
2023
|
+
} else if (args2[i] === "--coverage") {
|
|
2024
|
+
coverage = true;
|
|
2025
|
+
} else if (args2[i] === "--stem") {
|
|
2026
|
+
stem = true;
|
|
2027
|
+
} else if (args2[i] === "--passages") {
|
|
2028
|
+
const tok = args2[++i];
|
|
2029
|
+
if (tok === undefined || !/^[0-9]+$/.test(tok)) {
|
|
2030
|
+
console.error("--passages must be a positive integer");
|
|
2031
|
+
process.exit(1);
|
|
2032
|
+
}
|
|
2033
|
+
const n = parseInt(tok, 10);
|
|
2034
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
2035
|
+
console.error("--passages must be a positive integer");
|
|
2036
|
+
process.exit(1);
|
|
2037
|
+
}
|
|
2038
|
+
passages = n;
|
|
2039
|
+
} else if (args2[i] === "--gap") {
|
|
2040
|
+
const tok = args2[++i];
|
|
2041
|
+
if (tok === undefined || !/^0?\.[0-9]+$/.test(tok)) {
|
|
2042
|
+
console.error("--gap must be a ratio between 0 and 1 (e.g. 0.25)");
|
|
2043
|
+
process.exit(1);
|
|
2044
|
+
}
|
|
2045
|
+
const g = parseFloat(tok);
|
|
2046
|
+
if (!Number.isFinite(g) || g <= 0 || g >= 1) {
|
|
2047
|
+
console.error("--gap must be a ratio between 0 and 1 (e.g. 0.25)");
|
|
2048
|
+
process.exit(1);
|
|
2049
|
+
}
|
|
2050
|
+
gap = g;
|
|
1953
2051
|
} else
|
|
1954
2052
|
positional2.push(args2[i]);
|
|
1955
2053
|
}
|
|
1956
2054
|
query = positional2.join(" ").trim();
|
|
1957
2055
|
if (!query) {
|
|
1958
|
-
console.error('usage: imprnt recall "<query>" [--vault DIR] [--limit N]');
|
|
2056
|
+
console.error('usage: imprnt recall "<query>" [--vault DIR] [--limit N] [--gap R] [--passages N] [--proximity] [--coverage] [--stem]');
|
|
1959
2057
|
process.exit(1);
|
|
1960
2058
|
}
|
|
1961
2059
|
vocab = loadTags(vault2);
|
|
@@ -2063,6 +2161,7 @@ var init_recall = __esm(() => {
|
|
|
2063
2161
|
contentTerms = rawTerms.filter((w) => !STOPWORDS.has(w));
|
|
2064
2162
|
baseTerms = contentTerms.length ? contentTerms : rawTerms;
|
|
2065
2163
|
queryTerms = baseTerms.map((w) => [...new Set([w, ...tokenize(normalize(vocab, w))])]);
|
|
2164
|
+
queryVariantSet = new Set(queryTerms.flat());
|
|
2066
2165
|
for (const t of phraseSynonymTokens(query)) {
|
|
2067
2166
|
if (!STOPWORDS.has(t))
|
|
2068
2167
|
queryTerms.push([t]);
|
|
@@ -2099,11 +2198,26 @@ var init_recall = __esm(() => {
|
|
|
2099
2198
|
add(tokenize(aliases), TITLE_BOOST);
|
|
2100
2199
|
add(tags.flatMap(tokenize), TAG_BOOST);
|
|
2101
2200
|
add(tokenize(summary), SUMMARY_BOOST);
|
|
2102
|
-
|
|
2201
|
+
const bodyTokens = tokenize(body2);
|
|
2202
|
+
add(bodyTokens, BODY_BOOST);
|
|
2203
|
+
let pos;
|
|
2204
|
+
if (proximity) {
|
|
2205
|
+
pos = new Map;
|
|
2206
|
+
for (let bi = 0;bi < bodyTokens.length; bi++) {
|
|
2207
|
+
const t = bodyTokens[bi];
|
|
2208
|
+
if (!queryVariantSet.has(t))
|
|
2209
|
+
continue;
|
|
2210
|
+
const arr = pos.get(t);
|
|
2211
|
+
if (arr)
|
|
2212
|
+
arr.push(bi);
|
|
2213
|
+
else
|
|
2214
|
+
pos.set(t, [bi]);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2103
2217
|
let len = 0;
|
|
2104
2218
|
for (const c of tf.values())
|
|
2105
2219
|
len += c;
|
|
2106
|
-
docs.push({ path, tf, len });
|
|
2220
|
+
docs.push({ path, tf, len, pos });
|
|
2107
2221
|
for (const term of tf.keys())
|
|
2108
2222
|
df.set(term, (df.get(term) ?? 0) + 1);
|
|
2109
2223
|
}
|
|
@@ -2136,23 +2250,69 @@ var init_recall = __esm(() => {
|
|
|
2136
2250
|
scored.add(bestVariant);
|
|
2137
2251
|
}
|
|
2138
2252
|
}
|
|
2139
|
-
if (score > 0)
|
|
2253
|
+
if (score > 0) {
|
|
2254
|
+
if (coverage && scoringGroups.length > 1) {
|
|
2255
|
+
score *= 0.5 + 0.5 * (scored.size / scoringGroups.length);
|
|
2256
|
+
}
|
|
2257
|
+
if (proximity && d.pos && scored.size > 1) {
|
|
2258
|
+
const present = [...scored].filter((t) => d.pos.has(t));
|
|
2259
|
+
let bonus = 0;
|
|
2260
|
+
for (let x = 0;x < present.length; x++) {
|
|
2261
|
+
for (let y = x + 1;y < present.length; y++) {
|
|
2262
|
+
const a = d.pos.get(present[x]), b = d.pos.get(present[y]);
|
|
2263
|
+
let best = Infinity;
|
|
2264
|
+
for (let ia = 0, ib = 0;ia < a.length && ib < b.length; ) {
|
|
2265
|
+
const dist = Math.abs(a[ia] - b[ib]);
|
|
2266
|
+
if (dist < best)
|
|
2267
|
+
best = dist;
|
|
2268
|
+
if (a[ia] < b[ib])
|
|
2269
|
+
ia++;
|
|
2270
|
+
else
|
|
2271
|
+
ib++;
|
|
2272
|
+
}
|
|
2273
|
+
if (best < PROX_WINDOW) {
|
|
2274
|
+
const w = Math.min(variantIdf.get(present[x]) ?? 0, variantIdf.get(present[y]) ?? 0);
|
|
2275
|
+
bonus += PROX_WEIGHT * w * (1 - best / PROX_WINDOW);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
score += bonus;
|
|
2280
|
+
}
|
|
2140
2281
|
hits.push({ path: relative4(vault2, d.path), score: Math.round(score * 100) / 100 });
|
|
2282
|
+
}
|
|
2141
2283
|
}
|
|
2142
2284
|
hits.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
2143
2285
|
if (!hits.length) {
|
|
2144
2286
|
console.log(`no matches for "${query}" in ${vault2}`);
|
|
2145
2287
|
process.exit(0);
|
|
2146
2288
|
}
|
|
2147
|
-
|
|
2289
|
+
cut = Math.min(limit, hits.length);
|
|
2290
|
+
if (gap > 0) {
|
|
2291
|
+
for (let i = 1;i < cut; i++) {
|
|
2292
|
+
if (hits[i].score < gap * hits[i - 1].score) {
|
|
2293
|
+
cut = i;
|
|
2294
|
+
break;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
shown = hits.slice(0, cut);
|
|
2148
2299
|
expanded = queryTerms.map((g) => g.join("|")).join(" ");
|
|
2149
|
-
console.log(`recall "${query}" [${expanded}]
|
|
2300
|
+
console.log(`recall "${query}" [${expanded}] - ${hits.length} match(es)${hits.length > shown.length ? `, showing top ${shown.length}` : ""}, BM25-ranked:
|
|
2150
2301
|
`);
|
|
2151
|
-
for (const h of shown)
|
|
2302
|
+
for (const h of shown) {
|
|
2152
2303
|
console.log(` [${h.score.toFixed(2)}] ${h.path}`);
|
|
2304
|
+
if (passages > 0) {
|
|
2305
|
+
for (const p of bestPassages(join12(vault2, h.path), passages)) {
|
|
2306
|
+
for (const line of p.split(`
|
|
2307
|
+
`))
|
|
2308
|
+
console.log(` ${line}`);
|
|
2309
|
+
console.log("");
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2153
2313
|
if (hits.length > shown.length) {
|
|
2154
2314
|
console.log(`
|
|
2155
|
-
|
|
2315
|
+
... ${hits.length - shown.length} lower-ranked hit(s) hidden. Raise with --limit if needed. Usually you do not.`);
|
|
2156
2316
|
}
|
|
2157
2317
|
});
|
|
2158
2318
|
|
|
@@ -2261,11 +2421,11 @@ var init_snapshot = __esm(() => {
|
|
|
2261
2421
|
continue;
|
|
2262
2422
|
}
|
|
2263
2423
|
const ext = extname2(rel);
|
|
2264
|
-
const
|
|
2265
|
-
let relD = `${
|
|
2424
|
+
const stem2 = rel.slice(0, rel.length - ext.length);
|
|
2425
|
+
let relD = `${stem2}-${hash2.slice(0, 8)}${ext}`;
|
|
2266
2426
|
let n = 2;
|
|
2267
2427
|
while (existsSync11(join13(destRoot, relD)) && Buffer.compare(readFileSync12(join13(destRoot, relD)), srcBytes2) !== 0) {
|
|
2268
|
-
relD = `${
|
|
2428
|
+
relD = `${stem2}-${hash2.slice(0, 8)}-${n}${ext}`;
|
|
2269
2429
|
n++;
|
|
2270
2430
|
}
|
|
2271
2431
|
rawPath2 = join13(destRoot, relD);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imprnt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4-edge.74",
|
|
4
4
|
"stable": true,
|
|
5
5
|
"description": "Deterministic-first, plain-markdown knowledge vault. Code does the bulk transform. The LLM only touches the irreducibly-semantic 20%. Native grep plus BM25 retrieval, zero MCP on the vault.",
|
|
6
6
|
"type": "module",
|