scopecairn 0.12.1 → 0.14.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
@@ -76,6 +76,16 @@ No MCP server, no embeddings, no network calls, no telemetry.
76
76
  click-for-details side panel (callers, callees, centrality scores).
77
77
  Nodes sized by symbols/PageRank, colored by Louvain community.
78
78
  Pan, zoom, hover inspect. Double-click to open, no server.
79
+ - **Decision log** — `scopecairn decide "pakai X karena Y"` records an
80
+ architectural decision once; it is carried into every later
81
+ `context` call, so the agent stops re-litigating settled choices.
82
+ - **Definition of Done check** — `verify --dod` rates each DoD item and
83
+ is honest about what it cannot know: `pass` only for what the machine
84
+ verified (scope, index freshness), `warn`/`fail` where it has evidence,
85
+ `manual` for judgement calls. Never a fabricated "done".
86
+ - **Local usage report** — `status --usage` reads the local invocation
87
+ log: commands used, closed-loop rate (`scan` after a task), average
88
+ symbols per task. Local only, never leaves the machine.
79
89
  - **Graph export for external tools** — `scopecairn graph --export
80
90
  mermaid|graphml|dot|json` writes `.scopecairn/graph.<ext>` for PR
81
91
  diagrams (Mermaid), Gephi/Cytoscape (GraphML), Graphviz (DOT),
@@ -136,9 +146,10 @@ scopecairn scan # index repository (--rebuild, --cle
136
146
  scopecairn context "tambah fitur auth" # AI agent task context + scope
137
147
  scopecairn impact src/indexer.ts # blast radius & affected tests (--run-tests)
138
148
  scopecairn graph buildContext # inspect relations, path (--to), export (--export)
139
- scopecairn verify # pre-commit safety guard (--diff, --arch)
140
- scopecairn status # index statistics & health checks (--doctor, --html)
149
+ scopecairn verify --dod # pre-commit safety guard (--diff, --arch, --dod)
150
+ scopecairn status --usage # index statistics & health checks (--doctor, --html)
141
151
  scopecairn read symbol indexFileSymbols # symbol-level source excerpt
152
+ scopecairn decide "graf di SQLite, tanpa embedding" # catat keputusan arsitektur
142
153
  ```
143
154
 
144
155
  Per-agent installers (instead of `init --agents …`):
@@ -162,8 +173,8 @@ ScopeCairn organizes its capabilities into **6 core commands** aligned with code
162
173
  | `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`. |
163
174
  | `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). |
164
175
  | `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`). |
165
- | `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), `--strict`, `--task <t>`. |
166
- | `status` | **Health & Diagnostics:** Index statistics and metadata. Options: `--doctor` (integration and adapter health check), `--verbose`, `--html` (generate local HTML dashboard). |
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>`. |
177
+ | `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). |
167
178
 
168
179
  ### Helper & Setup Commands
169
180
 
@@ -171,6 +182,7 @@ ScopeCairn organizes its capabilities into **6 core commands** aligned with code
171
182
  | ------- | ----------- |
172
183
  | `init [path]` | Initial indexing + Skill, Workflow, detected agent skills (`--agents …\|all`). |
173
184
  | `read symbol <name>` | Symbol-level source excerpt (not whole files). |
185
+ | `decide "…"` | Record an architectural decision into `.scopecairn/decisions.md` (no argument: list them). Read back by `context`. |
174
186
  | `benchmark [--tune]` | Retrieval recall, irrelevant ratio, context reduction + weight tuning (dev-only, hidden from `--help`). |
175
187
  | `<agent> install` | Agent installers: `antigravity`, `claude`, `cursor`, `windsurf`, `copilot`, `kiro`, `opencode`. |
176
188
 
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isProtected
4
+ } from "./chunk-JQBYQ4EF.js";
5
+
6
+ // src/retrieval/classify.ts
7
+ var FANIN_THRESHOLD = 5;
8
+ function moduleOf(file) {
9
+ const i = file.indexOf("/");
10
+ return i === -1 ? "." : file.slice(0, i);
11
+ }
12
+ function classify(ranked, faninOf, threshold = 0.15, protectedPatterns) {
13
+ const relevant = ranked.filter((r) => r.score >= threshold);
14
+ const files = [...new Set(relevant.map((r) => r.file))];
15
+ const modules = [...new Set(files.map(moduleOf))];
16
+ const reasons = [];
17
+ const DEFINITIONAL = /* @__PURE__ */ new Set([
18
+ "function",
19
+ "class",
20
+ "method",
21
+ "component",
22
+ "interface",
23
+ "type"
24
+ ]);
25
+ const highFanin = relevant.filter((r) => DEFINITIONAL.has(r.type) && faninOf(r.id) >= FANIN_THRESHOLD).map((r) => r.name);
26
+ const protectedHits = files.filter(
27
+ (f) => protectedPatterns ? isProtected(f, protectedPatterns) : isProtected(f)
28
+ );
29
+ let complexity = "SIMPLE";
30
+ if (files.length >= 3) {
31
+ complexity = "COMPLEX";
32
+ reasons.push(`estimasi ${files.length} file (\u22653)`);
33
+ }
34
+ if (modules.length > 1) {
35
+ complexity = "COMPLEX";
36
+ reasons.push(`lintas modul (${modules.join(", ")})`);
37
+ }
38
+ if (highFanin.length > 0) {
39
+ complexity = "COMPLEX";
40
+ reasons.push(`simbol dipakai banyak tempat: ${highFanin.slice(0, 3).join(", ")}`);
41
+ }
42
+ if (protectedHits.length > 0) {
43
+ complexity = "COMPLEX";
44
+ reasons.push(`menyentuh area sensitif: ${protectedHits.slice(0, 3).join(", ")}`);
45
+ }
46
+ if (complexity === "SIMPLE") reasons.push(`estimasi ${files.length} file, 1 modul`);
47
+ return {
48
+ complexity,
49
+ estimatedFiles: files.length,
50
+ modules,
51
+ reasons,
52
+ highFanin,
53
+ protectedHits
54
+ };
55
+ }
56
+
57
+ export {
58
+ classify
59
+ };
@@ -2,71 +2,10 @@
2
2
  import {
3
3
  logError
4
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
- }
5
+ import {
6
+ tokenizeIdentifier,
7
+ tokenizeTask
8
+ } from "./chunk-UVKHDHBU.js";
70
9
 
71
10
  // src/retrieval/history/index.ts
72
11
  function stripConventionalPrefix(subject) {
@@ -309,8 +248,6 @@ function leaveOneOut(db, shas, opts = {}) {
309
248
  }
310
249
 
311
250
  export {
312
- tokenizeIdentifier,
313
- tokenizeTask,
314
251
  syncCommitFts,
315
252
  HISTORY_HALF_LIFE_DAYS,
316
253
  HISTORY_MIN_SCORE,
@@ -1,17 +1,53 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- computeScope,
3
+ demoteHeadings
4
+ } from "./chunk-ZV4BSIQM.js";
5
+ import {
4
6
  isProtected,
5
- loadProtectedPatterns,
6
- scopeBlock
7
- } from "./chunk-G5RA43OH.js";
7
+ loadProtectedPatterns
8
+ } from "./chunk-JQBYQ4EF.js";
8
9
  import {
9
10
  coChangeSet,
10
11
  groupHistoryFiles
11
- } from "./chunk-2BIPX6YJ.js";
12
- import {
13
- demoteHeadings
14
- } from "./chunk-ZV4BSIQM.js";
12
+ } from "./chunk-GQFBQRDD.js";
13
+
14
+ // src/scope/scope.ts
15
+ function computeScope(highFiles, mediumFiles, testFiles, patterns, historyOptional) {
16
+ const required = [];
17
+ const optional = [];
18
+ const protectedFiles = [];
19
+ const seen = /* @__PURE__ */ new Set();
20
+ const place = (f) => {
21
+ if (seen.has(f)) return;
22
+ seen.add(f);
23
+ if (isProtected(f, patterns)) protectedFiles.push(f);
24
+ else if (highFiles.includes(f)) required.push(f);
25
+ else optional.push(f);
26
+ };
27
+ for (const f of highFiles) place(f);
28
+ for (const f of mediumFiles) place(f);
29
+ for (const t of testFiles) {
30
+ if (seen.has(t)) continue;
31
+ seen.add(t);
32
+ if (isProtected(t, patterns)) protectedFiles.push(t);
33
+ else optional.push(t);
34
+ }
35
+ for (const h of historyOptional ?? []) {
36
+ if (seen.has(h)) continue;
37
+ seen.add(h);
38
+ if (isProtected(h, patterns)) protectedFiles.push(h);
39
+ else optional.push(h);
40
+ }
41
+ return { required, optional, protected: protectedFiles };
42
+ }
43
+ function scopeBlock(scope) {
44
+ const fmt = (xs) => xs.length > 0 ? xs.join(", ") : "-";
45
+ return `## Scope
46
+ Required: ${fmt(scope.required)}
47
+ Optional: ${fmt(scope.optional)}
48
+ Protected: ${fmt(scope.protected)}
49
+ `;
50
+ }
15
51
 
16
52
  // src/scope/guard.ts
17
53
  var TASK_GUARD_RULES = [
@@ -48,6 +84,12 @@ function buildContext(db, task, result, cls, opts) {
48
84
  if (files.length >= 3) break;
49
85
  }
50
86
  const cand = files.length > 0 ? files.map((f) => `\`${f}\``).join(", ") : "pencarian langsung (tidak ada simbol cocok di index)";
87
+ const simpleScope = computeScope(
88
+ files,
89
+ [],
90
+ [],
91
+ loadProtectedPatterns(opts?.repoRoot ?? ".")
92
+ );
51
93
  return {
52
94
  markdown: `# Task Context
53
95
 
@@ -63,7 +105,8 @@ Task sederhana. Gunakan pencarian langsung. Kandidat: ${cand}
63
105
  medium: [],
64
106
  tests: [],
65
107
  dependencies: [],
66
- hotspots: []
108
+ hotspots: [],
109
+ scope: simpleScope
67
110
  };
68
111
  }
69
112
  const ranked = result.ranked;
@@ -97,15 +140,14 @@ Task sederhana. Gunakan pencarian langsung. Kandidat: ${cand}
97
140
  const ph = highFiles.map(() => "?").join(",");
98
141
  try {
99
142
  const rows = db.prepare(
100
- `SELECT DISTINCT f2.path AS t
143
+ `SELECT DISTINCT f1.path AS t
101
144
  FROM relationships r
102
145
  JOIN symbols s1 ON s1.id = r.source_id
103
146
  JOIN symbols s2 ON s2.id = r.target_id
104
147
  JOIN files f1 ON f1.id = s1.file_id
105
148
  JOIN files f2 ON f2.id = s2.file_id
106
- WHERE r.relationship_type = 'TESTS'
107
- AND (f1.path IN (${ph}) OR f2.path IN (${ph}))`
108
- ).all(...highFiles, ...highFiles);
149
+ WHERE r.relationship_type = 'TESTS' AND f2.path IN (${ph})`
150
+ ).all(...highFiles);
109
151
  for (const r of rows) testSet.add(r.t);
110
152
  } catch {
111
153
  }
@@ -270,7 +312,8 @@ ${tests.map((t) => `- ${t}`).join("\n") || "-"}
270
312
  medium: medFiles,
271
313
  tests,
272
314
  dependencies: deps,
273
- hotspots
315
+ hotspots,
316
+ scope
274
317
  };
275
318
  }
276
319
  const md = `# Task Context
@@ -301,7 +344,15 @@ ${tests.map((t) => `- ${t}`).join("\n") || "-"}
301
344
 
302
345
  ` + historyBlock + cycleWarn + scopeBlock(scope) + `
303
346
  ` + guardBlock(extraDoD.length > 0 ? extraDoD : void 0);
304
- return { markdown: md, high: highFiles, medium: medFiles, tests, dependencies: deps, hotspots };
347
+ return {
348
+ markdown: md,
349
+ high: highFiles,
350
+ medium: medFiles,
351
+ tests,
352
+ dependencies: deps,
353
+ hotspots,
354
+ scope
355
+ };
305
356
  }
306
357
  function resolveEffectiveMode(mode, cls, knowledge) {
307
358
  return mode === "NORMAL" && cls.complexity === "SIMPLE" && cls.estimatedFiles <= 1 && !knowledge ? "FAST" : mode;
@@ -339,6 +390,7 @@ function buildContextPayload(input) {
339
390
  }
340
391
 
341
392
  export {
393
+ DEFINITION_OF_DONE,
342
394
  buildContext,
343
395
  resolveEffectiveMode,
344
396
  buildContextPayload
@@ -17,7 +17,7 @@ function loadDatabaseSync() {
17
17
  }
18
18
  var DATA_DIR_NAME = ".scopecairn";
19
19
  var DB_FILE_NAME = "scopecairn.db";
20
- var SCHEMA_VERSION = 7;
20
+ var SCHEMA_VERSION = 8;
21
21
  function dataDir(repoRoot) {
22
22
  return path.join(repoRoot, DATA_DIR_NAME);
23
23
  }
@@ -30,6 +30,9 @@ function openDb(repoRoot) {
30
30
  const db = new DatabaseSync(dbPath(repoRoot));
31
31
  db.exec(`
32
32
  PRAGMA journal_mode = WAL;
33
+ PRAGMA synchronous = NORMAL;
34
+ PRAGMA temp_store = MEMORY;
35
+ PRAGMA cache_size = -64000;
33
36
  PRAGMA foreign_keys = ON;
34
37
  CREATE TABLE IF NOT EXISTS files (
35
38
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -85,6 +88,17 @@ function openDb(repoRoot) {
85
88
  reason TEXT NOT NULL DEFAULT ''
86
89
  );
87
90
  CREATE INDEX IF NOT EXISTS idx_task_context_task ON task_context(task_id);
91
+ -- Scope yang DIRENDER ke agent (Required/Optional/Protected), disimpan
92
+ -- per task. Tanpa ini verify --task harus menghitung ulang scope dari
93
+ -- task string, dan hasilnya bisa berbeda dari yang benar-benar dilihat
94
+ -- agent (co-change history, --escalate, task SIMPLE) \u2192 false positive.
95
+ CREATE TABLE IF NOT EXISTS task_scope (
96
+ task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
97
+ path TEXT NOT NULL,
98
+ tier TEXT NOT NULL,
99
+ PRIMARY KEY (task_id, path)
100
+ );
101
+ CREATE INDEX IF NOT EXISTS idx_task_scope_task ON task_scope(task_id);
88
102
  -- Fase 5: invocation log untuk compliance rate (AI-5, \xA718).
89
103
  CREATE TABLE IF NOT EXISTS invocations (
90
104
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -138,6 +152,15 @@ function openDb(repoRoot) {
138
152
  );
139
153
  CREATE INDEX IF NOT EXISTS idx_commit_files_sha ON commit_files(sha);
140
154
  CREATE INDEX IF NOT EXISTS idx_commit_files_path ON commit_files(path);
155
+ -- Cache fan-in (jumlah relasi masuk per simbol). Dipakai ranking
156
+ -- retrieval: tanpa cache, retrieve() menghitung ulang
157
+ -- GROUP BY target_id di seluruh tabel relationships pada SETIAP
158
+ -- pemanggilan \u2014 O(E) penuh, dan itu bottleneck utama di repo besar.
159
+ -- Dipopulate ulang di akhir scanRepository (satu lintasan GROUP BY).
160
+ CREATE TABLE IF NOT EXISTS fanin (
161
+ node_id INTEGER PRIMARY KEY,
162
+ n INTEGER NOT NULL
163
+ );
141
164
  CREATE VIRTUAL TABLE IF NOT EXISTS commit_fts USING fts5(
142
165
  sha UNINDEXED, subject, body, paths
143
166
  );
@@ -157,10 +180,17 @@ function openDb(repoRoot) {
157
180
  `UPDATE relationships SET evidence = 'INFERRED'
158
181
  WHERE relationship_type IN ('QUERIES','ROUTES_TO','TESTS') AND evidence = 'EXTRACTED'`
159
182
  );
160
- db.exec(
161
- `UPDATE relationships SET evidence = 'AMBIGUOUS'
162
- WHERE relationship_type = 'CALLS' AND confidence < 1.0 AND evidence = 'EXTRACTED'`
163
- );
183
+ const CALLS_BACKFILL_KEY = "migration_calls_evidence_v1";
184
+ const callsBackfilled = db.prepare(`SELECT value FROM meta WHERE key = ?`).get(CALLS_BACKFILL_KEY);
185
+ if (!callsBackfilled) {
186
+ db.exec(
187
+ `UPDATE relationships SET evidence = 'AMBIGUOUS'
188
+ WHERE relationship_type = 'CALLS' AND confidence < 1.0 AND evidence = 'EXTRACTED'`
189
+ );
190
+ db.prepare(`INSERT INTO meta(key, value) VALUES (?, '1') ON CONFLICT(key) DO NOTHING`).run(
191
+ CALLS_BACKFILL_KEY
192
+ );
193
+ }
164
194
  return db;
165
195
  }
166
196
  function hashContent(buf) {
@@ -295,54 +325,17 @@ function ensureProtectedFile(repoRoot) {
295
325
  }
296
326
  }
297
327
 
298
- // src/scope/scope.ts
299
- function computeScope(highFiles, mediumFiles, testFiles, patterns, historyOptional) {
300
- const required = [];
301
- const optional = [];
302
- const protectedFiles = [];
303
- const seen = /* @__PURE__ */ new Set();
304
- const place = (f) => {
305
- if (seen.has(f)) return;
306
- seen.add(f);
307
- if (isProtected(f, patterns)) protectedFiles.push(f);
308
- else if (highFiles.includes(f)) required.push(f);
309
- else optional.push(f);
310
- };
311
- for (const f of highFiles) place(f);
312
- for (const f of mediumFiles) place(f);
313
- for (const t of testFiles) {
314
- if (seen.has(t)) continue;
315
- seen.add(t);
316
- if (isProtected(t, patterns)) protectedFiles.push(t);
317
- else optional.push(t);
318
- }
319
- for (const h of historyOptional ?? []) {
320
- if (seen.has(h)) continue;
321
- seen.add(h);
322
- if (isProtected(h, patterns)) protectedFiles.push(h);
323
- else optional.push(h);
324
- }
325
- return { required, optional, protected: protectedFiles };
326
- }
327
- function scopeBlock(scope) {
328
- const fmt = (xs) => xs.length > 0 ? xs.join(", ") : "-";
329
- return `## Scope
330
- Required: ${fmt(scope.required)}
331
- Optional: ${fmt(scope.optional)}
332
- Protected: ${fmt(scope.protected)}
333
- `;
334
- }
335
-
336
328
  export {
337
329
  dataDir,
338
330
  dbPath,
339
331
  openDb,
340
332
  hashContent,
333
+ DEFAULT_PROTECTED_PATTERNS,
341
334
  matchPattern,
342
335
  loadProtectedPatterns,
343
336
  isProtected,
337
+ SECRET_PATTERNS,
338
+ isSecretFile,
344
339
  isIndexableMetaFile,
345
- ensureProtectedFile,
346
- computeScope,
347
- scopeBlock
340
+ ensureProtectedFile
348
341
  };