scopecairn 0.8.0 → 0.10.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 +22 -5
- package/dist/chunk-2BIPX6YJ.js +326 -0
- package/dist/chunk-G5RA43OH.js +348 -0
- package/dist/chunk-OPA4TMAT.js +24 -0
- package/dist/chunk-PR3BGBXP.js +217 -0
- package/dist/chunk-PYBIR5FP.js +250 -0
- package/dist/cli.js +3390 -1000
- package/dist/contextBuilder-7COQIG6Z.js +10 -0
- package/dist/extractor-665DZW2D.js +20 -0
- package/dist/scope-RA4EDS4A.js +9 -0
- package/dist/search-OLYYFEQV.js +28 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ No MCP server, no embeddings, no network calls, no telemetry.
|
|
|
36
36
|
- **Auto-refresh** — `context` and `impact` re-index changed files first;
|
|
37
37
|
run `scan` after editing to close the loop.
|
|
38
38
|
- **Framework adapters** — Next.js (App + Pages Router routes),
|
|
39
|
-
Prisma models,
|
|
39
|
+
Prisma models, Drizzle tables/queries, Express, FastAPI, NestJS (controllers, routes, injectables), Django (urls, models), SQLAlchemy, and Vue SFC detected automatically.
|
|
40
40
|
- **Multi-language** — TypeScript, JavaScript, Python, Java, Go, Rust,
|
|
41
41
|
PHP, C#, C/C++, Ruby, HTML, Vue SFC (plus config/schema files as
|
|
42
42
|
graph nodes).
|
|
@@ -149,7 +149,8 @@ scopecairn agents list
|
|
|
149
149
|
| `init [path]` | Initial indexing + Skill, Workflow, detected agent skills (`--agents …\|all`; never writes `AGENTS.md`) |
|
|
150
150
|
| `scan` / `rebuild` / `clean` | Manual index management (`scan` incremental; `rebuild` from scratch; `clean` removes `.scopecairn/`) |
|
|
151
151
|
| `status` / `doctor` | Index status (incl. adapters, invocations) and health checks |
|
|
152
|
-
| `context "<task>"` | Main entry point: context + scope (`--escalate`, `--no-refresh`, `--mode NORMAL|FAST|SAFE|AUDIT`, `--max-tokens N`); a path task returns an orientation map |
|
|
152
|
+
| `context "<task>"` | Main entry point: context + scope (`--escalate`, `--no-refresh`, `--mode NORMAL|FAST|SAFE|AUDIT`, `--max-tokens N`, `--no-history`, `--with-diff`); a path task returns an orientation map |
|
|
153
|
+
| `history "<task>" [--limit N] [--json] [--with-diff]` | Analog commit lama paling mirip (BM25 + recency half-life 180 hari + co-change); co-change set ikut ke Optional di `context` |
|
|
153
154
|
| `impact <path\|symbol>` | Change impact: direct, indirect, tests (via TESTS edges + static CALLS backward reachability), UI, queries, routes. Edge evidence tagged EXTRACTED/INFERRED/AMBIGUOUS |
|
|
154
155
|
| `graph <symbol>` | Symbol relations (+ PageRank, community, cycle status) |
|
|
155
156
|
| `path <from> <to>` | Shortest multi-hop call/dependency path between two symbols |
|
|
@@ -157,14 +158,18 @@ scopecairn agents list
|
|
|
157
158
|
| `read symbol <name>` | Symbol-level source excerpt (not whole files) |
|
|
158
159
|
| `<agent> install` | Per-agent setup: `antigravity`, `claude`, `gemini`, `cursor`, `windsurf`, `copilot`, `kiro` (each also `uninstall`; `agents list` to see all) |
|
|
159
160
|
| `dashboard` | Generate local HTML dashboard at `.scopecairn/dashboard.html` |
|
|
160
|
-
| `test-select <target
|
|
161
|
+
| `test-select <target> [--run]` | List affected test files via graph dependency traversal & runner detection (`--run` executes runner) |
|
|
162
|
+
| `verify [--task <t>] [--strict]` | Post-edit guard: check modified protected files, task scope creep, and circular dependency regressions |
|
|
163
|
+
| `diff` | Structural symbol-level diff of working tree vs HEAD with breaking impact alerts |
|
|
164
|
+
| `arch-check [--init]` | Check architectural layer rules & disallowed import boundaries (`--init` scaffolds `.scopecairn/arch.json`) |
|
|
165
|
+
| `watch [--debounce <ms>]` | Daemon mode: auto-refresh index in background on file saves |
|
|
161
166
|
| `doctor --verbose` | Also surface adapter/graph errors from `.scopecairn/last-run.log` to stderr |
|
|
162
167
|
| `benchmark [--tune]` | Retrieval recall, irrelevant ratio, context reduction + weight calibration |
|
|
163
168
|
| `update [--check]` | Update ScopeCairn to the latest npm release (`--check` only checks) |
|
|
164
169
|
|
|
165
|
-
Agent commands (`context`, `impact`, `read`, `graph`, `status`, `doctor`)
|
|
170
|
+
Agent commands (`context`, `impact`, `read`, `graph`, `status`, `doctor`, `verify`, `diff`, `test-select`, `arch-check`)
|
|
166
171
|
are read-only toward source and only write to `.scopecairn/` — safe to
|
|
167
|
-
allowlist. `init`, `scan`, and `
|
|
172
|
+
allowlist. `init`, `scan`, `rebuild`, and `watch` are manual/user-side commands.
|
|
168
173
|
|
|
169
174
|
## Framework adapters
|
|
170
175
|
|
|
@@ -178,6 +183,8 @@ No configuration needed.
|
|
|
178
183
|
| `drizzle` | `drizzle*` paths/config, `db/` schemas | `model` symbols from `pgTable`/`sqliteTable`/`mysqlTable`; `QUERIES` from `db.query.*` relational and `db.select/insert/update/delete` builder calls |
|
|
179
184
|
| `express` | `package.json` deps | Route symbols from `app/router.METHOD(path)`, handler edges |
|
|
180
185
|
| `fastapi` | `.py` routes, requirements/pyproject | Route symbols from `@app.METHOD("/path")` decorators |
|
|
186
|
+
| `nestjs` | `*.controller.ts`, `*.service.ts`, `nest-cli.json` | Route symbols from `@Controller` and `@Get/@Post/...` decorators, `ROUTES_TO` handler edges, `@Injectable()` service providers |
|
|
187
|
+
| `django` | `manage.py`, `urls.py`, `models.py` | Route symbols from `path()`/`re_path()` in `urls.py`, `ROUTES_TO` handler edges, `model` symbols from `models.Model` |
|
|
181
188
|
| `sqlalchemy` | requirements/pyproject | `model` symbols from `class X(Base)`/`__tablename__`; `QUERIES` from `session.query/select` |
|
|
182
189
|
| `vue` | `.vue` SFCs, `package.json` deps | `component` symbols per SFC; `IMPORTS` edges from used components in template |
|
|
183
190
|
|
|
@@ -199,6 +206,16 @@ prompt → scopecairn context (refresh at start) → agent edits code
|
|
|
199
206
|
|
|
200
207
|
Measure retrieval quality anytime: `scopecairn benchmark [--tune]`.
|
|
201
208
|
|
|
209
|
+
## History privacy
|
|
210
|
+
|
|
211
|
+
`scan` mengindeks pesan commit git lokal ke tabel `commits`/`commit_files` di
|
|
212
|
+
`.scopecairn/scopecairn.db` (dilewati commit >40 file, lockfile, dan generated).
|
|
213
|
+
Isi commit **tidak** masuk `GRAPH.md`/`GRAPH.html`/`export` (sering di-commit dan
|
|
214
|
+
dibagikan) — riwayat hanya hidup di database lokal dan output `context`/`history`.
|
|
215
|
+
Pesan commit bisa memuat info sensitif; karena tidak keluar dari mesin, risikonya
|
|
216
|
+
kecil. Repo tanpa git atau clone dangkal tetap didukung (riwayat parsial/absen,
|
|
217
|
+
dilaporkan `doctor`).
|
|
218
|
+
|
|
202
219
|
## License
|
|
203
220
|
|
|
204
221
|
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
logError
|
|
4
|
+
} from "./chunk-OPA4TMAT.js";
|
|
5
|
+
|
|
6
|
+
// src/retrieval/tokenize.ts
|
|
7
|
+
var STOP = /* @__PURE__ */ new Set([
|
|
8
|
+
"the",
|
|
9
|
+
"a",
|
|
10
|
+
"an",
|
|
11
|
+
"of",
|
|
12
|
+
"to",
|
|
13
|
+
"in",
|
|
14
|
+
"for",
|
|
15
|
+
"on",
|
|
16
|
+
"with",
|
|
17
|
+
"and",
|
|
18
|
+
"or",
|
|
19
|
+
"src",
|
|
20
|
+
"lib",
|
|
21
|
+
"app",
|
|
22
|
+
"test",
|
|
23
|
+
"tests",
|
|
24
|
+
"spec"
|
|
25
|
+
]);
|
|
26
|
+
function stemVariants(t) {
|
|
27
|
+
const out = /* @__PURE__ */ new Set([t]);
|
|
28
|
+
if (t.endsWith("ies") && t.length > 4) out.add(t.slice(0, -3) + "y");
|
|
29
|
+
else if (t.endsWith("es") && t.length > 4 && /(s|x|z|ch|sh)es$/.test(t)) out.add(t.slice(0, -2));
|
|
30
|
+
else if (t.endsWith("s") && t.length > 3 && !t.endsWith("ss")) out.add(t.slice(0, -1));
|
|
31
|
+
if (t.endsWith("ing") && t.length > 5) {
|
|
32
|
+
const base = t.slice(0, -3);
|
|
33
|
+
out.add(base);
|
|
34
|
+
out.add(base + "e");
|
|
35
|
+
}
|
|
36
|
+
if (t.endsWith("ation") && t.length > 7) {
|
|
37
|
+
const base = t.slice(0, -5);
|
|
38
|
+
out.add(base);
|
|
39
|
+
}
|
|
40
|
+
return [...out];
|
|
41
|
+
}
|
|
42
|
+
var EXPLICIT_STEM = /* @__PURE__ */ new Map([
|
|
43
|
+
["configuration", "config"],
|
|
44
|
+
["configurations", "config"],
|
|
45
|
+
["configured", "config"],
|
|
46
|
+
["ranking", "rank"],
|
|
47
|
+
["ranked", "rank"],
|
|
48
|
+
["weights", "weight"],
|
|
49
|
+
["dependencies", "dependency"],
|
|
50
|
+
["dependency", "depend"],
|
|
51
|
+
["extraction", "extract"],
|
|
52
|
+
["extracted", "extract"]
|
|
53
|
+
]);
|
|
54
|
+
function tokenizeIdentifier(raw) {
|
|
55
|
+
const parts = raw.replace(/[\\/]/g, " ").replace(/[.:\-_"']/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/g).filter((t) => t.length >= 2 && !STOP.has(t));
|
|
56
|
+
const out = /* @__PURE__ */ new Set();
|
|
57
|
+
for (const t of parts) {
|
|
58
|
+
out.add(t);
|
|
59
|
+
for (const v of stemVariants(t)) {
|
|
60
|
+
if (v.length >= 2 && !STOP.has(v)) out.add(v);
|
|
61
|
+
}
|
|
62
|
+
const e = EXPLICIT_STEM.get(t);
|
|
63
|
+
if (e && !STOP.has(e)) out.add(e);
|
|
64
|
+
}
|
|
65
|
+
return [...out];
|
|
66
|
+
}
|
|
67
|
+
function tokenizeTask(text) {
|
|
68
|
+
return tokenizeIdentifier(text);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/retrieval/history/index.ts
|
|
72
|
+
function stripConventionalPrefix(subject) {
|
|
73
|
+
return subject.replace(/^[a-z]+(\([^)]*\))?(!)?:\s*/i, "").replace(/^\[\w+\]\s*/, "").trim();
|
|
74
|
+
}
|
|
75
|
+
function isPoorSubject(subject) {
|
|
76
|
+
const clean = stripConventionalPrefix(subject);
|
|
77
|
+
if (/^merge\s+(pr|pull request)\s*#?\d*/i.test(clean)) return true;
|
|
78
|
+
if (/^(update|updates|wip|tmp|test|fix|patch|changes?)$/i.test(clean)) return true;
|
|
79
|
+
return tokenizeIdentifier(clean).length < 2;
|
|
80
|
+
}
|
|
81
|
+
function commitTokens(subject, body, paths) {
|
|
82
|
+
const out = /* @__PURE__ */ new Set();
|
|
83
|
+
const cleanSubject = stripConventionalPrefix(subject);
|
|
84
|
+
for (const t of tokenizeIdentifier(cleanSubject)) out.add(t);
|
|
85
|
+
if (!isPoorSubject(subject)) {
|
|
86
|
+
for (const t of tokenizeIdentifier(body.slice(0, 2e3))) out.add(t);
|
|
87
|
+
}
|
|
88
|
+
for (const p of paths) {
|
|
89
|
+
for (const t of tokenizeIdentifier(p)) out.add(t);
|
|
90
|
+
}
|
|
91
|
+
return [...out];
|
|
92
|
+
}
|
|
93
|
+
function syncCommitFts(db, shas) {
|
|
94
|
+
let n = 0;
|
|
95
|
+
try {
|
|
96
|
+
const del = db.prepare(`DELETE FROM commit_fts WHERE sha = ?`);
|
|
97
|
+
const ins = db.prepare(`INSERT INTO commit_fts(sha, subject, body, paths) VALUES (?, ?, ?, ?)`);
|
|
98
|
+
const getCommit = db.prepare(`SELECT subject, body FROM commits WHERE sha = ?`);
|
|
99
|
+
const getPaths = db.prepare(`SELECT path FROM commit_files WHERE sha = ?`);
|
|
100
|
+
for (const sha of shas) {
|
|
101
|
+
const c = getCommit.get(sha);
|
|
102
|
+
if (!c) continue;
|
|
103
|
+
const paths = getPaths.all(sha).map((r) => r.path);
|
|
104
|
+
del.run(sha);
|
|
105
|
+
ins.run(sha, c.subject, c.body.slice(0, 4e3), paths.join(" "));
|
|
106
|
+
n++;
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
logError("history", err);
|
|
110
|
+
}
|
|
111
|
+
return n;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/retrieval/history/search.ts
|
|
115
|
+
var HISTORY_HALF_LIFE_DAYS = 180;
|
|
116
|
+
var HISTORY_MIN_SCORE = 0.05;
|
|
117
|
+
var HISTORY_MAX_FILES = 40;
|
|
118
|
+
var BM25_LIMIT = 20;
|
|
119
|
+
function recencyScore(ts, nowSec, halfLifeDays) {
|
|
120
|
+
if (ts <= 0) return 0.5;
|
|
121
|
+
const ageDays = Math.max(0, (nowSec - ts) / 86400);
|
|
122
|
+
return Math.pow(0.5, ageDays / Math.max(1, halfLifeDays));
|
|
123
|
+
}
|
|
124
|
+
function sizePenalty(nFiles) {
|
|
125
|
+
return 1 / Math.log(2 + Math.max(0, nFiles));
|
|
126
|
+
}
|
|
127
|
+
function jaccard(a, b) {
|
|
128
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
129
|
+
let inter = 0;
|
|
130
|
+
for (const x of a) if (b.has(x)) inter++;
|
|
131
|
+
const union = a.size + b.size - inter;
|
|
132
|
+
return union === 0 ? 0 : inter / union;
|
|
133
|
+
}
|
|
134
|
+
function resolveCurrentPath(db, path) {
|
|
135
|
+
let cur = path;
|
|
136
|
+
try {
|
|
137
|
+
const stmt = db.prepare(`SELECT path FROM commit_files WHERE old_path = ? ORDER BY rowid DESC LIMIT 1`);
|
|
138
|
+
for (let i = 0; i < 5; i++) {
|
|
139
|
+
const row = stmt.get(cur);
|
|
140
|
+
if (!row || row.path === cur) break;
|
|
141
|
+
cur = row.path;
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
return cur;
|
|
146
|
+
}
|
|
147
|
+
function candidates(db, tokens, asOfTs, excludeShas) {
|
|
148
|
+
const excluded = new Set(excludeShas ?? []);
|
|
149
|
+
if (tokens.length === 0) return [];
|
|
150
|
+
const q = tokens.map((t) => `"${t.replace(/"/g, "")}"`).join(" OR ");
|
|
151
|
+
try {
|
|
152
|
+
const rows = db.prepare(
|
|
153
|
+
`SELECT sha, bm25(commit_fts) AS rank FROM commit_fts
|
|
154
|
+
WHERE commit_fts MATCH ? ORDER BY rank LIMIT ${BM25_LIMIT}`
|
|
155
|
+
).all(q);
|
|
156
|
+
if (rows.length === 0) return [];
|
|
157
|
+
const filt = rows.filter((r) => !excluded.has(r.sha));
|
|
158
|
+
if (filt.length === 0) return [];
|
|
159
|
+
const worst = Math.min(...filt.map((r) => r.rank));
|
|
160
|
+
const range = Math.max(1e-9, 0 - worst + 1);
|
|
161
|
+
void asOfTs;
|
|
162
|
+
return filt.map((r) => ({
|
|
163
|
+
sha: r.sha,
|
|
164
|
+
bm25norm: Math.max(0, Math.min(1, (0 - r.rank + 0.5) / range))
|
|
165
|
+
}));
|
|
166
|
+
} catch (err) {
|
|
167
|
+
logError("history", err);
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
let rows = db.prepare(`SELECT sha, subject, ts FROM commits`).all();
|
|
171
|
+
if (asOfTs !== void 0) rows = rows.filter((r) => r.ts <= asOfTs);
|
|
172
|
+
rows = rows.filter((r) => !excluded.has(r.sha));
|
|
173
|
+
const tset = new Set(tokens);
|
|
174
|
+
const scored = rows.map((r) => {
|
|
175
|
+
let paths = [];
|
|
176
|
+
try {
|
|
177
|
+
paths = db.prepare(`SELECT path FROM commit_files WHERE sha = ?`).all(r.sha).map((x) => x.path);
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
const doc = new Set(commitTokens(r.subject, "", paths));
|
|
181
|
+
let hit = 0;
|
|
182
|
+
for (const t of tset) if (doc.has(t)) hit++;
|
|
183
|
+
return { sha: r.sha, bm25norm: tset.size === 0 ? 0 : hit / tset.size };
|
|
184
|
+
}).filter((r) => r.bm25norm > 0);
|
|
185
|
+
scored.sort((a, b) => b.bm25norm - a.bm25norm || (a.sha < b.sha ? -1 : 1));
|
|
186
|
+
return scored.slice(0, BM25_LIMIT);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
logError("history", err);
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function historySearch(db, task, opts = {}) {
|
|
193
|
+
const seedFiles = new Set(opts.seedFiles ?? []);
|
|
194
|
+
const limit = Math.max(1, Math.min(10, opts.limit ?? 3));
|
|
195
|
+
const halfLife = opts.halfLifeDays ?? HISTORY_HALF_LIFE_DAYS;
|
|
196
|
+
const minScore = opts.minScore ?? HISTORY_MIN_SCORE;
|
|
197
|
+
const nowSec = opts.nowSec ?? Math.floor(Date.now() / 1e3);
|
|
198
|
+
const tokens = tokenizeTask(task);
|
|
199
|
+
if (tokens.length === 0) return [];
|
|
200
|
+
let cands;
|
|
201
|
+
try {
|
|
202
|
+
cands = candidates(db, tokens, opts.asOfTs, opts.excludeShas);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
logError("history", err);
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
if (cands.length === 0) return [];
|
|
208
|
+
const out = [];
|
|
209
|
+
try {
|
|
210
|
+
const getCommit = db.prepare(`SELECT ts, subject FROM commits WHERE sha = ?`);
|
|
211
|
+
const getFiles = db.prepare(`SELECT path FROM commit_files WHERE sha = ?`);
|
|
212
|
+
const fileExists = db.prepare(`SELECT 1 AS ok FROM files WHERE path = ? LIMIT 1`);
|
|
213
|
+
for (const c of cands) {
|
|
214
|
+
const commit = getCommit.get(c.sha);
|
|
215
|
+
if (!commit) continue;
|
|
216
|
+
if (opts.asOfTs !== void 0 && commit.ts > opts.asOfTs) continue;
|
|
217
|
+
const rawFiles = getFiles.all(c.sha).map((r) => r.path);
|
|
218
|
+
if (rawFiles.length > HISTORY_MAX_FILES) continue;
|
|
219
|
+
const live = [];
|
|
220
|
+
for (const p of rawFiles) {
|
|
221
|
+
const cur = resolveCurrentPath(db, p);
|
|
222
|
+
try {
|
|
223
|
+
const hit = fileExists.get(cur);
|
|
224
|
+
if (hit) {
|
|
225
|
+
if (!live.includes(cur)) live.push(cur);
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (live.length === 0) continue;
|
|
231
|
+
const rec = recencyScore(commit.ts, nowSec, halfLife);
|
|
232
|
+
const pen = sizePenalty(rawFiles.length);
|
|
233
|
+
const ov = jaccard(new Set(live), seedFiles);
|
|
234
|
+
const score = c.bm25norm * rec * pen * (1 + ov);
|
|
235
|
+
if (score < minScore) continue;
|
|
236
|
+
const d = new Date(commit.ts * 1e3);
|
|
237
|
+
out.push({
|
|
238
|
+
sha: c.sha,
|
|
239
|
+
shortSha: c.sha.slice(0, 7),
|
|
240
|
+
ts: commit.ts,
|
|
241
|
+
date: Number.isNaN(d.getTime()) ? "-" : d.toISOString().slice(0, 10),
|
|
242
|
+
subject: commit.subject,
|
|
243
|
+
files: [...live].sort(),
|
|
244
|
+
score,
|
|
245
|
+
sortTs: commit.ts
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
logError("history", err);
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
out.sort((a, b) => b.score - a.score || b.sortTs - a.sortTs || (a.sha < b.sha ? -1 : a.sha > b.sha ? 1 : 0));
|
|
253
|
+
return out.slice(0, limit).map(({ sortTs: _s, ...rest }) => rest);
|
|
254
|
+
}
|
|
255
|
+
function coChangeSet(analogs) {
|
|
256
|
+
const counts = /* @__PURE__ */ new Map();
|
|
257
|
+
for (const a of analogs) {
|
|
258
|
+
for (const f of new Set(a.files)) counts.set(f, (counts.get(f) ?? 0) + 1);
|
|
259
|
+
}
|
|
260
|
+
return [...counts.entries()].filter(([, n]) => n >= 2).map(([path, count]) => ({ path, count })).sort((a, b) => b.count - a.count || (a.path < b.path ? -1 : 1));
|
|
261
|
+
}
|
|
262
|
+
function groupHistoryFiles(files) {
|
|
263
|
+
const source = [];
|
|
264
|
+
const test = [];
|
|
265
|
+
const config = [];
|
|
266
|
+
const docs = [];
|
|
267
|
+
for (const f of files) {
|
|
268
|
+
const base = f.split("/").pop() ?? f;
|
|
269
|
+
const lower = f.toLowerCase();
|
|
270
|
+
if (f.includes(".test.") || f.includes(".spec.") || f.includes("__tests__") || base.startsWith("test_") || base.endsWith("_test.py") || lower.startsWith("tests/")) {
|
|
271
|
+
test.push(f);
|
|
272
|
+
} else if (/\.(json|ya?ml|toml|ini|cfg)$/.test(lower) || ["package.json", "dockerfile", "makefile"].includes(base.toLowerCase())) {
|
|
273
|
+
config.push(f);
|
|
274
|
+
} else if (/\.mdx?$/.test(lower) || lower.startsWith("docs/")) {
|
|
275
|
+
docs.push(f);
|
|
276
|
+
} else {
|
|
277
|
+
source.push(f);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { source, test, config, docs };
|
|
281
|
+
}
|
|
282
|
+
function leaveOneOut(db, shas, opts = {}) {
|
|
283
|
+
const out = [];
|
|
284
|
+
try {
|
|
285
|
+
const getCommit = db.prepare(`SELECT ts, subject FROM commits WHERE sha = ?`);
|
|
286
|
+
const getFiles = db.prepare(`SELECT path FROM commit_files WHERE sha = ?`);
|
|
287
|
+
for (const sha of shas) {
|
|
288
|
+
const commit = getCommit.get(sha);
|
|
289
|
+
if (!commit) continue;
|
|
290
|
+
const expected = getFiles.all(sha).map((r) => r.path);
|
|
291
|
+
const seeds = opts.seedFilesOf ? opts.seedFilesOf(sha) : [];
|
|
292
|
+
const analogs = historySearch(db, commit.subject, {
|
|
293
|
+
seedFiles: seeds,
|
|
294
|
+
limit: opts.limit ?? 3,
|
|
295
|
+
asOfTs: commit.ts - 1,
|
|
296
|
+
excludeShas: [sha],
|
|
297
|
+
nowSec: opts.nowSec
|
|
298
|
+
});
|
|
299
|
+
const recalledFiles = /* @__PURE__ */ new Set();
|
|
300
|
+
for (const a of analogs) for (const f of a.files) recalledFiles.add(f);
|
|
301
|
+
for (const c of coChangeSet(analogs)) recalledFiles.add(c.path);
|
|
302
|
+
const recalled = expected.filter((f) => recalledFiles.has(f));
|
|
303
|
+
out.push({ sha, subject: commit.subject, recalled, expected, recall: expected.length === 0 ? 1 : recalled.length / expected.length });
|
|
304
|
+
}
|
|
305
|
+
} catch (err) {
|
|
306
|
+
logError("history", err);
|
|
307
|
+
}
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export {
|
|
312
|
+
tokenizeIdentifier,
|
|
313
|
+
tokenizeTask,
|
|
314
|
+
syncCommitFts,
|
|
315
|
+
HISTORY_HALF_LIFE_DAYS,
|
|
316
|
+
HISTORY_MIN_SCORE,
|
|
317
|
+
HISTORY_MAX_FILES,
|
|
318
|
+
recencyScore,
|
|
319
|
+
sizePenalty,
|
|
320
|
+
jaccard,
|
|
321
|
+
resolveCurrentPath,
|
|
322
|
+
historySearch,
|
|
323
|
+
coChangeSet,
|
|
324
|
+
groupHistoryFiles,
|
|
325
|
+
leaveOneOut
|
|
326
|
+
};
|