scopecairn 0.15.0 → 0.16.1

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 CHANGED
@@ -58,10 +58,11 @@ No MCP server, no embeddings, no network calls, no telemetry.
58
58
  - **Multi-language** — TypeScript, JavaScript, Python, Java, Go, Rust,
59
59
  PHP, C#, C/C++, Ruby, HTML, Vue SFC (plus config/schema files as
60
60
  graph nodes).
61
- - **Multi-agent setup** — skills for Claude Code (project skill),
62
- Antigravity, Cursor, Windsurf, Copilot, Kiro, and an OpenCode
63
- slash command. ScopeCairn never touches your `AGENTS.md`/`CLAUDE.md` —
64
- those hold your repo's details, not tool instructions.
61
+ - **Multi-agent setup** — native rules & skills for Antigravity, Cursor,
62
+ Claude Code, Windsurf, GitHub Copilot, Kiro, OpenCode, and Qoder. ScopeCairn
63
+ merges safely into your project rules (Antigravity `AGENTS.md`, Copilot instructions)
64
+ via append-only managed blocks that keep your repo details 100% intact,
65
+ or writes native rules and skills.
65
66
  - **`GRAPH.md` knowledge file** — `.scopecairn/GRAPH.md` is a budgeted
66
67
  (±150 lines) frozen summary of the graph (modules, most-used symbols,
67
68
  PageRank hotspots, circular dependencies, routes, models, protected).
@@ -76,7 +77,7 @@ No MCP server, no embeddings, no network calls, no telemetry.
76
77
  click-for-details side panel (callers, callees, centrality scores).
77
78
  Nodes sized by symbols/PageRank, colored by Louvain community.
78
79
  Pan, zoom, hover inspect. Double-click to open, no server.
79
- - **Decision log** — `scopecairn decide "pakai X karena Y"` records an
80
+ - **Decision log** — `scopecairn decide "use SQLite graph, no embeddings"` records an
80
81
  architectural decision once; it is carried into every later
81
82
  `context` call, so the agent stops re-litigating settled choices.
82
83
  - **Definition of Done check** — `verify --dod` rates each DoD item and
@@ -129,38 +130,69 @@ it runs project setup itself.
129
130
 
130
131
  ```bash
131
132
  cd your-repo
132
- scopecairn init # index + Antigravity Skill + Workflow
133
+ scopecairn init # index + Antigravity Rule + AGENTS.md merge + Skill + Workflow
133
134
  ```
134
135
 
135
- Claude Code (project skill, committed with the repo):
136
+ Per-agent installers (instead of `init --agents …`):
137
+
138
+ You can list supported agents anytime with `scopecairn agents list`. Install or remove integrations individually using native paths:
136
139
 
137
140
  ```bash
141
+ # Antigravity IDE (.agents/rules/ + AGENTS.md merge + skills/ + workflows/)
142
+ scopecairn antigravity install
143
+
144
+ # Claude Code (.claude/skills/ + rule mirroring in .claude/rules/)
138
145
  scopecairn claude install
146
+
147
+ # Cursor (.cursor/rules/scopecairn.mdc with alwaysApply + knowledge skill)
148
+ scopecairn cursor install
149
+
150
+ # Windsurf (.windsurf/rules/scopecairn.md + knowledge skill)
151
+ scopecairn windsurf install
152
+
153
+ # GitHub Copilot (.github/copilot-instructions.md + knowledge skill)
154
+ scopecairn copilot install
155
+
156
+ # Kiro (.kiro/steering/scopecairn.md + knowledge skill)
157
+ scopecairn kiro install
158
+
159
+ # OpenCode (.opencode/commands/scopecairn.md + knowledge skill + permission snippet)
160
+ scopecairn opencode install
161
+
162
+ # Qoder (.qoder/rules/scopecairn.md + knowledge skill in .qoder/skills/)
163
+ scopecairn qoder install
164
+
165
+ # Batch installation during init
166
+ scopecairn init --agents all # install for all supported agents
167
+ scopecairn init --agents cursor,copilot # install for specific agents only
168
+
169
+ # Clean removal (preserves existing configs & custom instructions)
170
+ scopecairn <agent> uninstall
139
171
  ```
140
172
 
173
+ | Agent | Native Files Written / Merged | Execution Behavior |
174
+ | :--- | :--- | :--- |
175
+ | **Antigravity** | `.agents/rules/scopecairn.md`<br>`AGENTS.md` (smart merge)<br>`.agents/skills/scopecairn/SKILL.md`<br>`.agents/skills/scopecairn-knowledge/SKILL.md`<br>`.agents/workflows/scopecairn.md` | Auto-loaded into `<user_rules>` on every turn via `AGENTS.md` and rule files; skills available on demand. |
176
+ | **Cursor** | `.cursor/rules/scopecairn.mdc`<br>`.cursor/skills/scopecairn-knowledge/SKILL.md` | `alwaysApply: true` auto-injects protocol into every chat & agent session. |
177
+ | **Claude Code** | `.claude/skills/scopecairn/SKILL.md`<br>`.claude/rules/scopecairn.md`<br>`.claude/skills/scopecairn-knowledge/SKILL.md` | Standing rules loaded automatically; `allowed-tools: Bash` provides pre-approved terminal execution. |
178
+ | **Windsurf** | `.windsurf/rules/scopecairn.md`<br>`.windsurf/skills/scopecairn-knowledge/SKILL.md` | Cascade loads modern `.windsurf/rules/` automatically. |
179
+ | **GitHub Copilot** | `.github/copilot-instructions.md`<br>`.github/skills/scopecairn-knowledge/SKILL.md` | Safely merges managed block into repository custom instructions. |
180
+ | **Kiro** | `.kiro/steering/scopecairn.md`<br>`.kiro/skills/scopecairn-knowledge/SKILL.md` | Default `always` steering inclusion injects context into all prompts. |
181
+ | **OpenCode** | `.opencode/commands/scopecairn.md`<br>`.opencode/skills/scopecairn-knowledge/SKILL.md` | Registers `/scopecairn` slash command with `$ARGUMENTS` and prints permission guide. |
182
+ | **Qoder** | `.qoder/rules/scopecairn.md`<br>`.qoder/skills/scopecairn-knowledge/SKILL.md` | Rules without loading frontmatter are always active, so the protocol is injected into every Qoder IDE and CLI session; `AGENTS.md` is still honoured as a compatibility source. |
183
+
141
184
  After setup, your agent calls ScopeCairn automatically on every coding task.
142
185
  To query manually (replace `src/indexer.ts` or `buildContext` with files/symbols from your codebase):
143
186
 
144
187
  ```bash
145
188
  scopecairn scan # index repository (--rebuild, --clean, --watch)
146
- scopecairn context "tambah fitur auth" # AI agent task context + scope
189
+ scopecairn context "add auth feature" # AI agent task context + scope
147
190
  scopecairn impact src/indexer.ts # blast radius & affected tests (--run-tests)
148
191
  scopecairn graph buildContext # inspect relations, path (--to), export (--export)
149
192
  scopecairn verify --dod # pre-commit safety guard (--diff, --arch, --dod)
150
193
  scopecairn status --usage # index statistics & health checks (--doctor, --html)
151
194
  scopecairn read symbol indexFileSymbols # symbol-level source excerpt
152
- scopecairn decide "graf di SQLite, tanpa embedding" # catat keputusan arsitektur
153
- ```
154
-
155
- Per-agent installers (instead of `init --agents …`):
156
-
157
- ```bash
158
- scopecairn antigravity install # Skill + Workflow + allowlist guide
159
- scopecairn claude install # project skill .claude/skills (also: cursor, windsurf, copilot)
160
- scopecairn kiro install # steering .kiro/steering + knowledge skill .kiro/skills/scopecairn-knowledge
161
- scopecairn opencode install # /scopecairn slash command + permission snippet
162
- scopecairn claude uninstall # clean removal, user content preserved
163
- scopecairn agents list
195
+ scopecairn decide "graph in SQLite, no embeddings" # record architectural decision
164
196
  ```
165
197
 
166
198
  ## 6 Core Commands
@@ -173,18 +205,32 @@ ScopeCairn organizes its capabilities into **6 core commands** aligned with code
173
205
  | `context "<task>"` | **AI Agent Context & Scope:** Main agent entry point with auto-refresh, task classification, relevant context, task scope, and Task Guard. Options: `--escalate`, `--mode NORMAL\|FAST\|SAFE\|AUDIT`, `--max-tokens N`, `--no-history`, `--with-diff`, `--json`. |
174
206
  | `impact <target>` | **Blast Radius & Test Selection:** Change impact analysis across direct, indirect, tests, UI components, and API clients. Options: `--run-tests` (run affected tests directly), `--tests` (list affected tests only), `--repos [list]` (cross-repo blast radius). |
175
207
  | `graph [symbol]` | **Knowledge Graph & Navigation:** Symbol relations, PageRank centrality, community clusters, and cycle status. Options: `--to <target>` (shortest multi-hop call path), `--export <fmt>` (`mermaid\|graphml\|dot\|json`). |
176
- | `verify` | **Pre-Commit Safety Guard:** Working tree check against protected files, task scope creep, and circular dependency regressions. Options: `--diff` (structural symbol-level diff vs HEAD), `--arch` (architectural layer rules), `--dod` (Definition of Done: verified / warned / manual, never a claimed pass), `--strict`, `--task <t>`. |
208
+ | `verify` | **Pre-Commit Safety & Testing Gate:** Working tree check against protected files, task scope creep, circular dependency regressions, and test gate. Options: `--diff` (structural symbol-level diff vs HEAD), `--arch` (architectural layer rules), `--dod` (Definition of Done: verified / warned / manual, never a claimed pass), `--test-gate` (detect weakened tests, enforce changed-line coverage default 80% for Vitest/Jest, optional `--mutation` via Stryker), `--strict`, `--task <t>`. |
177
209
  | `status` | **Health & Diagnostics:** Index statistics and metadata. Options: `--doctor` (integration and adapter health check), `--usage` (local usage & compliance report), `--verbose`, `--html` (generate local HTML dashboard). |
178
210
 
179
211
  ### Helper & Setup Commands
180
212
 
181
213
  | Command | Description |
182
214
  | ------- | ----------- |
183
- | `init [path]` | Initial indexing + Skill, Workflow, detected agent skills (`--agents …\|all`). |
215
+ | `init [path]` | Initial indexing + Skill, Workflow, detected agent skills (`--agents …\|all`, `--hooks` to install `.git/hooks/pre-commit` test & verification gate). |
216
+ | `reuse "<query>"` | Find existing similar functions or classes from the graph (name tokens, signature, doc comment, call-graph neighbors) before creating new ones. |
217
+ | `dead` | Report symbols and files with no incoming edges (`high`/`medium`/`low` confidence, excluding entrypoints, tests, framework routes, and Protected paths). |
218
+ | `session <save\|load>` | Atomically save or load working session state in `.scopecairn/session.md` (max 4,000 chars, rejects secrets, reports commits & files changed since last save). |
184
219
  | `read symbol <name>` | Symbol-level source excerpt (not whole files). |
185
220
  | `decide "…"` | Record an architectural decision into `.scopecairn/decisions.md` (no argument: list them). Read back by `context`. |
186
221
  | `benchmark [--tune]` | Retrieval recall, irrelevant ratio, context reduction + weight tuning (dev-only, hidden from `--help`). |
187
- | `<agent> install` | Agent installers: `antigravity`, `claude`, `cursor`, `windsurf`, `copilot`, `kiro`, `opencode`. |
222
+ | `<agent> install` | Agent installers: `antigravity`, `claude`, `cursor`, `windsurf`, `copilot`, `kiro`, `opencode`, `qoder`. |
223
+
224
+ ### Testing Gate & Pre-Commit Hook Limitations (`init --hooks`)
225
+
226
+ Running `scopecairn init --hooks` installs a managed `.git/hooks/pre-commit` guard that executes `scopecairn verify --no-strict --test-gate`:
227
+ - **Weakened Test Detection:** Blocks diffs in test files that delete assertions (`expect`/`assert`), add `.skip`/`.todo`/`xit`, or loosen strict matchers (`toBe`/`toEqual` → `toBeDefined`/`toBeTruthy` or tautological `expect(true).toBe(true)`).
228
+ - **Changed-Line Coverage (Vitest & Jest):** Evaluates coverage on modified/added executable source lines against a configurable threshold (`coverage_threshold: 80` in `.scopecairn/config.yml`, or `--coverage-threshold <n>`).
229
+ - **Optional Mutation Testing (Stryker):** Supports `--mutation` (or `mutation_enabled: true` in `.scopecairn/config.yml`) to validate mutation score on changed files via Stryker (`--testRunner vitest|jest`). Disabled by default because mutation testing is slow.
230
+ - **Limitation (`--no-verify` bypass):** Because an AI agent or developer can bypass local git hooks using `git commit --no-verify`, always run the same check in your CI pipeline:
231
+ ```bash
232
+ scopecairn verify --test-gate --base origin/main
233
+ ```
188
234
 
189
235
  > [!NOTE]
190
236
  > **Backward Compatibility:** All previous standalone commands (`rebuild`, `clean`, `watch`, `doctor`, `dashboard`, `test-select`, `path`, `export`, `diff`, `arch-check`, `history`) are preserved as hidden aliases mapped directly into these 6 core commands — they still work but no longer appear in `--help`. `update` is deprecated (use `npm install -g scopecairn@latest`).
@@ -228,15 +274,60 @@ prompt → scopecairn context (refresh at start) → agent edits code
228
274
 
229
275
  Measure retrieval quality anytime: `scopecairn benchmark [--tune]`.
230
276
 
277
+ ## Ranking 5+2 sinyal
278
+
279
+ Retrieval (`src/retrieval/retrieve.ts`) menggabungkan 5 sinyal klasik + 2 sinyal graph:
280
+
281
+ | Sinyal | Bobot default (Fase 6) | Makna |
282
+ | ------ | ---------------------- | ----- |
283
+ | `wSeed` (seed) | 0.5 | Token-match FTS5 terhadap task + glossary. |
284
+ | `wProximity` | 0.25 | Dekat di graph dari seed (BFS `maxDepth`, `decayPerHop`). |
285
+ | `wCentrality` | 0.1 | `0.5*PageRank + 0.5*fan-in` (cache `node_metrics`/`fanin`). |
286
+ | `wRecency` | 0.1 | File sering berubah baru-baru ini (sinyal git). |
287
+ | `wCochange` | 0.1 | File yang sering berubah bersama file seed (riwayat). |
288
+ | `wPpr` | 0.0 | Personalized PageRank proxy (opsional, 0 = nonaktif). |
289
+ | `wPr` | 0.0 | PageRank global mandiri (opsional, 0 = nonaktif). |
290
+
291
+ + `verified` (closed-loop): file yang terbukti berubah saat `verify` lulus mendapat bonus `+0.35` proporsional.
292
+
293
+ Bobot dapat dioverride di `.scopecairn/config.yml` (`w_seed:`, `w_proximity:`, …) atau via `retrieve(db, root, task, { weights: { ... } })` saat benchmark/tuning.
294
+
295
+ Contoh pakai:
296
+
297
+ ```bash
298
+ # Breakdown skor per simbol (wSeed*seed, prox, cent, recency, cochange, verified)
299
+ scopecairn context "add login" --explain
300
+
301
+ # Format output ringkas untuk agent hemat token
302
+ scopecairn context "add login" --format brief # 1 baris/simbol
303
+ scopecairn context "add login" --format table # markdown table
304
+ scopecairn context "add login" --format tree # grup per folder
305
+ scopecairn context "add login" --format mermaid # graph TD antar simbol teratas
306
+
307
+ # PR risk agregat (protected/cycle/fan-in) vs base, + opsi SARIF untuk GitHub
308
+ scopecairn verify --pr-risk --base origin/main
309
+ scopecairn verify --pr-risk --sarif # SARIF 2.1.0 JSON
310
+
311
+ # Dependensi manifest dan kandidat unused (npm/python/go/rust)
312
+ scopecairn deps
313
+ scopecairn deps --json
314
+
315
+ # Deteksi LSP opsional saat scan (best-effort, tak wajib)
316
+ scopecairn scan --lsp # melaporkan tsserver/gopls/pyright di PATH
317
+
318
+ # Kalibrasi ulang bobot (grid search 36 kombinasi wSeed/wProximity/wCentrality)
319
+ scopecairn benchmark --tune
320
+
321
+ # CI gate (tulis .github/workflows/scopecairn.yml)
322
+ scopecairn init --ci
323
+ ```
324
+
231
325
  ## History privacy
232
326
 
233
- `scan` mengindeks pesan commit git lokal ke tabel `commits`/`commit_files` di
234
- `.scopecairn/scopecairn.db` (dilewati commit >40 file, lockfile, dan generated).
235
- Isi commit **tidak** masuk `GRAPH.md`/`GRAPH.html`/`graph --export` (sering di-commit dan
236
- dibagikan) — riwayat hanya hidup di database lokal dan output `context`.
237
- Pesan commit bisa memuat info sensitif; karena tidak keluar dari mesin, risikonya
238
- kecil. Repo tanpa git atau clone dangkal tetap didukung (riwayat parsial/absen,
239
- dilaporkan `status --doctor`).
327
+ `scan` indexes local git commit messages into the `commits`/`commit_files` tables in
328
+ `.scopecairn/scopecairn.db` (skipping commits with >40 files, lockfiles, and generated files).
329
+ Commit contents are **never** included in `GRAPH.md`, `GRAPH.html`, or `graph --export` (which are often committed and shared) — history lives exclusively in your local database and in `context` output.
330
+ Commit messages may contain sensitive project information; because they never leave your local machine, risk is minimized. Repositories without git or shallow clones are fully supported (partial or absent history is reported by `status --doctor`).
240
331
 
241
332
  ## License
242
333
 
@@ -3,11 +3,13 @@ import {
3
3
  PROJECT_RULES_MAX_CHARS,
4
4
  demoteHeadings,
5
5
  findAgentsMd,
6
- loadProjectRules
7
- } from "./chunk-ZV4BSIQM.js";
6
+ loadProjectRules,
7
+ stripManagedBlock
8
+ } from "./chunk-T3CVC4TV.js";
8
9
  export {
9
10
  PROJECT_RULES_MAX_CHARS,
10
11
  demoteHeadings,
11
12
  findAgentsMd,
12
- loadProjectRules
13
+ loadProjectRules,
14
+ stripManagedBlock
13
15
  };
@@ -86,8 +86,8 @@ function computeBetweenness(db) {
86
86
  sigma.set(s, 1);
87
87
  dist.set(s, 0);
88
88
  const queue = [s];
89
- while (queue.length > 0) {
90
- const v = queue.shift();
89
+ for (let qi = 0; qi < queue.length; qi++) {
90
+ const v = queue[qi];
91
91
  stack.push(v);
92
92
  for (const w of adj.get(v) ?? []) {
93
93
  if ((dist.get(w) ?? -1) < 0) {
@@ -1,15 +1,26 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ coChangeSet,
4
+ groupHistoryFiles
5
+ } from "./chunk-GERWAENA.js";
6
+ import {
7
+ logError
8
+ } from "./chunk-OPA4TMAT.js";
2
9
  import {
3
10
  demoteHeadings
4
- } from "./chunk-ZV4BSIQM.js";
11
+ } from "./chunk-T3CVC4TV.js";
12
+ import {
13
+ isTestPath,
14
+ selectHigh
15
+ } from "./chunk-RTLKS5IS.js";
5
16
  import {
6
17
  isProtected,
7
18
  loadProtectedPatterns
8
- } from "./chunk-JQBYQ4EF.js";
9
- import {
10
- coChangeSet,
11
- groupHistoryFiles
12
- } from "./chunk-GQFBQRDD.js";
19
+ } from "./chunk-DADX4SRG.js";
20
+
21
+ // src/retrieval/contextBuilder.ts
22
+ import fs from "fs";
23
+ import path from "path";
13
24
 
14
25
  // src/scope/scope.ts
15
26
  function computeScope(highFiles, mediumFiles, testFiles, patterns, historyOptional) {
@@ -110,8 +121,9 @@ Task sederhana. Gunakan pencarian langsung. Kandidat: ${cand}
110
121
  };
111
122
  }
112
123
  const ranked = result.ranked;
113
- const hi = ranked.filter((r) => r.score >= 0.3).slice(0, 10);
114
- const med = ranked.filter((r) => r.score >= 0.15 && r.score < 0.3).slice(0, 10);
124
+ const hi = selectHigh(ranked);
125
+ const hiSet = new Set(hi);
126
+ const med = ranked.filter((r) => r.score >= 0.15 && !hiSet.has(r)).slice(0, 10);
115
127
  const highFiles = [...new Set(hi.map((r) => r.file))];
116
128
  const medFiles = [...new Set(med.map((r) => r.file))].filter((f) => !highFiles.includes(f));
117
129
  const traceSeed = highFiles.length > 0 ? highFiles : opts?.knowledge ? medFiles : [];
@@ -131,10 +143,11 @@ Task sederhana. Gunakan pencarian langsung. Kandidat: ${cand}
131
143
  LIMIT 10`
132
144
  ).all(...traceSeed, ...traceSeed);
133
145
  for (const r of rows) deps.push(`${r.a} \u2192 ${r.b}`);
134
- } catch {
146
+ } catch (err) {
147
+ logError("contextBuilder:deps", err, opts?.repoRoot);
135
148
  }
136
149
  }
137
- const isTestFileLike = (p) => p.includes(".test.") || p.includes(".spec.") || p.includes("__tests__") || (p.split("/").pop() ?? "").startsWith("test_");
150
+ const isTestFileLike = isTestPath;
138
151
  const testSet = /* @__PURE__ */ new Set();
139
152
  if (highFiles.length > 0) {
140
153
  const ph = highFiles.map(() => "?").join(",");
@@ -149,7 +162,8 @@ Task sederhana. Gunakan pencarian langsung. Kandidat: ${cand}
149
162
  WHERE r.relationship_type = 'TESTS' AND f2.path IN (${ph})`
150
163
  ).all(...highFiles);
151
164
  for (const r of rows) testSet.add(r.t);
152
- } catch {
165
+ } catch (err) {
166
+ logError("contextBuilder:tests", err, opts?.repoRoot);
153
167
  }
154
168
  try {
155
169
  const symRows = db.prepare(
@@ -190,14 +204,45 @@ Task sederhana. Gunakan pencarian langsung. Kandidat: ${cand}
190
204
  }
191
205
  }
192
206
  }
193
- } catch {
207
+ } catch (err) {
208
+ logError("contextBuilder:callers", err, opts?.repoRoot);
194
209
  }
195
210
  }
196
211
  for (const r of ranked) {
197
212
  if (isTestFileLike(r.file)) testSet.add(r.file);
198
213
  }
199
214
  const tests = [...testSet].slice(0, 10);
200
- const symLine = (r) => `- ${r.name} (${r.type}) \u2014 \`${r.file}\` [${r.score.toFixed(2)} ${r.reason}]`;
215
+ const callerStmt = (() => {
216
+ try {
217
+ return db.prepare(
218
+ `SELECT DISTINCT s.name AS name, f.path AS file, s.start_line AS startLine
219
+ FROM relationships r
220
+ JOIN symbols s ON s.id = r.source_id
221
+ JOIN files f ON f.id = s.file_id
222
+ WHERE r.target_id = ? AND r.relationship_type IN ('CALLS','IMPORTS') AND s.type != 'file' AND s.id != ?
223
+ LIMIT 3`
224
+ );
225
+ } catch {
226
+ return null;
227
+ }
228
+ })();
229
+ const symLine = (r) => {
230
+ const loc = r.startLine && r.startLine > 0 ? `${r.file}:${r.startLine}-${r.endLine ?? r.startLine}` : r.file;
231
+ const sig = r.signature ? ` \`${r.signature.replace(/\s+/g, " ").trim().slice(0, 90)}\`` : "";
232
+ let callersLine = "";
233
+ if (callerStmt) {
234
+ try {
235
+ const cRows = callerStmt.all(r.id, r.id);
236
+ if (cRows.length > 0) {
237
+ const formatted = cRows.map((c) => `${c.name} (\`${c.file}${c.startLine && c.startLine > 0 ? `:${c.startLine}` : ""}\`)`).join(", ");
238
+ callersLine = `
239
+ \u21B3 called-by: ${formatted}`;
240
+ }
241
+ } catch {
242
+ }
243
+ }
244
+ return `- ${r.name} (${r.type}) \u2014 \`${loc}\`${sig} [${r.score.toFixed(2)} ${r.reason}]${callersLine}`;
245
+ };
201
246
  const hotspotLine = (h) => `- ${h.name} (${h.type}) \u2014 \`${h.file}\` [pr=${h.pagerank.toFixed(4)}]`;
202
247
  let hotspots = [];
203
248
  let cycleWarn = "";
@@ -230,13 +275,29 @@ Task sederhana. Gunakan pencarian langsung. Kandidat: ${cand}
230
275
  `;
231
276
  }
232
277
  }
233
- } catch {
278
+ } catch (err) {
279
+ logError("contextBuilder:metrics", err, opts?.repoRoot);
280
+ }
281
+ const verifiedAnalogs = result.verifiedAnalogs ?? [];
282
+ const verifiedFiles = [...new Set(verifiedAnalogs.flatMap((v) => v.files))];
283
+ for (const vf of verifiedFiles) {
284
+ if (!highFiles.includes(vf)) highFiles.push(vf);
234
285
  }
235
286
  const historyAnalogs = opts?.history?.analogs ?? [];
236
287
  const cochange = coChangeSet(historyAnalogs);
237
288
  const patterns = loadProtectedPatterns(opts?.repoRoot ?? ".");
238
289
  const historyOptional = cochange.map((c) => c.path).filter((f) => !isProtected(f, patterns));
239
290
  const scope = computeScope(highFiles, medFiles, tests, patterns, historyOptional);
291
+ let verifiedBlock = "";
292
+ if (verifiedAnalogs.length > 0) {
293
+ const lines = [`## Analogi task terverifikasi`, ``];
294
+ for (const v of verifiedAnalogs) {
295
+ lines.push(`- "${v.description}" (${v.verifiedAt.slice(0, 10)})`);
296
+ lines.push(` file: ${v.files.map((f) => `\`${f}\``).join(", ")}`);
297
+ }
298
+ lines.push(``);
299
+ verifiedBlock = lines.join("\n") + "\n";
300
+ }
240
301
  let historyBlock = "";
241
302
  if (historyAnalogs.length > 0) {
242
303
  const lines = [`## Analogi riwayat`, ``];
@@ -305,7 +366,7 @@ ${kSyms.slice(0, 8).map(symLine).join("\n") || "-"}
305
366
  ## Related Tests
306
367
  ${tests.map((t) => `- ${t}`).join("\n") || "-"}
307
368
 
308
- ` + historyBlock + cycleWarn;
369
+ ` + verifiedBlock + historyBlock + cycleWarn;
309
370
  return {
310
371
  markdown: mdKnowledge,
311
372
  high: highFiles,
@@ -316,6 +377,39 @@ ${tests.map((t) => `- ${t}`).join("\n") || "-"}
316
377
  scope
317
378
  };
318
379
  }
380
+ let excerptsBlock = "";
381
+ if (opts?.withBodies !== false && opts?.repoRoot) {
382
+ const excerptLines = [];
383
+ const maxSpan = opts?.withBodies === true ? 40 : 25;
384
+ const candidates = hi.filter(
385
+ (r) => r.type !== "file" && (opts?.withBodies === true || r.type !== "variable") && r.startLine && r.startLine > 0
386
+ ).slice(0, 2);
387
+ for (const r of candidates) {
388
+ const start = r.startLine;
389
+ const rawEnd = r.endLine && r.endLine >= start ? r.endLine : start;
390
+ const span = rawEnd - start + 1;
391
+ if (opts?.withBodies !== true && (span <= 1 || span > maxSpan)) continue;
392
+ try {
393
+ const abs = path.join(opts.repoRoot, r.file);
394
+ const fileLines = fs.readFileSync(abs, "utf8").replace(/\r\n/g, "\n").split("\n");
395
+ const end = Math.min(fileLines.length, opts?.withBodies === true && rawEnd === start ? start + 24 : Math.min(rawEnd, start + maxSpan - 1));
396
+ const body = fileLines.slice(start - 1, end).join("\n").trimEnd();
397
+ if (body) {
398
+ excerptLines.push(`### ${r.name} (\`${r.file}:${start}-${end}\`)
399
+ \`\`\`
400
+ ${body}
401
+ \`\`\``);
402
+ }
403
+ } catch {
404
+ }
405
+ }
406
+ if (excerptLines.length > 0) {
407
+ excerptsBlock = `## Key Excerpts
408
+ ${excerptLines.join("\n\n")}
409
+
410
+ `;
411
+ }
412
+ }
319
413
  const md = `# Task Context
320
414
 
321
415
  ## Task
@@ -333,7 +427,7 @@ ${medFiles.map((f) => `- ${f}`).join("\n") || "-"}
333
427
  ## Key Symbols
334
428
  ${hi.slice(0, 8).map(symLine).join("\n") || "-"}
335
429
 
336
- ` + (hotspots.length > 0 ? `## Critical hotspots (PageRank)
430
+ ` + excerptsBlock + (hotspots.length > 0 ? `## Critical hotspots (PageRank)
337
431
  ${hotspots.map(hotspotLine).join("\n")}
338
432
 
339
433
  ` : ``) + `## Dependencies
@@ -342,8 +436,8 @@ ${deps.map((d) => `- ${d}`).join("\n") || "-"}
342
436
  ## Related Tests
343
437
  ${tests.map((t) => `- ${t}`).join("\n") || "-"}
344
438
 
345
- ` + historyBlock + cycleWarn + scopeBlock(scope) + `
346
- ` + guardBlock(extraDoD.length > 0 ? extraDoD : void 0);
439
+ ` + verifiedBlock + historyBlock + cycleWarn + scopeBlock(scope) + (opts?.withGuard ? `
440
+ ` + guardBlock(extraDoD.length > 0 ? extraDoD : void 0) : ``);
347
441
  return {
348
442
  markdown: md,
349
443
  high: highFiles,
@@ -55,6 +55,7 @@ function openDb(repoRoot) {
55
55
  CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id);
56
56
  CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
57
57
  CREATE INDEX IF NOT EXISTS idx_symbols_type ON symbols(type);
58
+ CREATE INDEX IF NOT EXISTS idx_symbols_file_name_type ON symbols(file_id, name, type);
58
59
  CREATE TABLE IF NOT EXISTS relationships (
59
60
  id INTEGER PRIMARY KEY AUTOINCREMENT,
60
61
  source_id INTEGER NOT NULL REFERENCES symbols(id) ON DELETE CASCADE,
@@ -66,6 +67,7 @@ function openDb(repoRoot) {
66
67
  CREATE INDEX IF NOT EXISTS idx_rel_source ON relationships(source_id);
67
68
  CREATE INDEX IF NOT EXISTS idx_rel_target ON relationships(target_id);
68
69
  CREATE INDEX IF NOT EXISTS idx_rel_type ON relationships(relationship_type);
70
+ CREATE INDEX IF NOT EXISTS idx_rel_source_type ON relationships(source_id, relationship_type);
69
71
  CREATE TABLE IF NOT EXISTS meta (
70
72
  key TEXT PRIMARY KEY,
71
73
  value TEXT NOT NULL
@@ -164,6 +166,17 @@ function openDb(repoRoot) {
164
166
  CREATE VIRTUAL TABLE IF NOT EXISTS commit_fts USING fts5(
165
167
  sha UNINDEXED, subject, body, paths
166
168
  );
169
+ -- Closed-loop learning: pasangan task -> file yang terbukti berubah saat verify lulus
170
+ CREATE TABLE IF NOT EXISTS verified_tasks (
171
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
172
+ task_id INTEGER REFERENCES tasks(id) ON DELETE SET NULL,
173
+ description TEXT NOT NULL UNIQUE,
174
+ files_json TEXT NOT NULL,
175
+ verified_at TEXT NOT NULL DEFAULT (datetime('now'))
176
+ );
177
+ CREATE VIRTUAL TABLE IF NOT EXISTS verified_tasks_fts USING fts5(
178
+ task_id UNINDEXED, description, files
179
+ );
167
180
  `);
168
181
  db.prepare(
169
182
  `INSERT INTO meta(key, value) VALUES ('schema_version', ?)
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  tokenizeIdentifier,
7
7
  tokenizeTask
8
- } from "./chunk-UVKHDHBU.js";
8
+ } from "./chunk-M4PQW6PF.js";
9
9
 
10
10
  // src/retrieval/history/index.ts
11
11
  function stripConventionalPrefix(subject) {
@@ -34,6 +34,10 @@ function stemVariants(t) {
34
34
  const base = t.slice(0, -5);
35
35
  out.add(base);
36
36
  }
37
+ if ((t.endsWith("er") || t.endsWith("ed")) && t.length > 5) {
38
+ out.add(t.slice(0, -1));
39
+ out.add(t.slice(0, -2));
40
+ }
37
41
  return [...out];
38
42
  }
39
43
  var EXPLICIT_STEM = /* @__PURE__ */ new Map([
@@ -48,6 +52,51 @@ var EXPLICIT_STEM = /* @__PURE__ */ new Map([
48
52
  ["extraction", "extract"],
49
53
  ["extracted", "extract"]
50
54
  ]);
55
+ var DOMAIN_SYNONYMS = /* @__PURE__ */ new Map([
56
+ // Kupon / diskon
57
+ ["kupon", "voucher"],
58
+ ["voucher", "kupon"],
59
+ ["coupon", "voucher"],
60
+ ["promo", "voucher"],
61
+ ["diskon", "discount"],
62
+ ["discount", "diskon"],
63
+ // Penagihan
64
+ ["invoice", "billing"],
65
+ ["billing", "invoice"],
66
+ ["tagihan", "invoice"],
67
+ ["pembayaran", "payment"],
68
+ ["payment", "pembayaran"],
69
+ ["pelunasan", "payment"],
70
+ // Pengguna / akun
71
+ ["pengguna", "user"],
72
+ ["akun", "account"],
73
+ ["account", "akun"],
74
+ // Keranjang / pesanan
75
+ ["keranjang", "cart"],
76
+ ["cart", "keranjang"],
77
+ ["troli", "cart"],
78
+ ["pesanan", "order"],
79
+ ["order", "pesanan"],
80
+ ["checkout", "order"],
81
+ // Pengiriman / stok
82
+ ["pengiriman", "shipping"],
83
+ ["shipping", "pengiriman"],
84
+ ["ongkir", "shipping"],
85
+ ["stok", "stock"],
86
+ ["stock", "stok"],
87
+ ["inventaris", "stock"],
88
+ // Produk / harga
89
+ ["harga", "price"],
90
+ ["price", "harga"],
91
+ ["produk", "product"],
92
+ ["product", "produk"],
93
+ // Auth umum
94
+ ["masuk", "login"],
95
+ ["login", "masuk"],
96
+ ["daftar", "signup"],
97
+ ["logout", "signout"],
98
+ ["signout", "logout"]
99
+ ]);
51
100
  function tokenizeIdentifier(raw) {
52
101
  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));
53
102
  const out = /* @__PURE__ */ new Set();
@@ -58,6 +107,8 @@ function tokenizeIdentifier(raw) {
58
107
  }
59
108
  const e = EXPLICIT_STEM.get(t);
60
109
  if (e && !STOP.has(e)) out.add(e);
110
+ const d = DOMAIN_SYNONYMS.get(t);
111
+ if (d && !STOP.has(d)) out.add(d);
61
112
  }
62
113
  return [...out];
63
114
  }