session-collab-mcp 0.4.7 → 0.5.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.
@@ -0,0 +1,18 @@
1
+ -- Symbol-level claim tracking for fine-grained conflict detection
2
+ -- Allows claiming specific functions/classes instead of entire files
3
+
4
+ CREATE TABLE IF NOT EXISTS claim_symbols (
5
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ claim_id TEXT NOT NULL,
7
+ file_path TEXT NOT NULL,
8
+ symbol_name TEXT NOT NULL,
9
+ symbol_type TEXT DEFAULT 'function' CHECK (symbol_type IN ('function', 'class', 'method', 'variable', 'block', 'other')),
10
+ created_at TEXT DEFAULT (datetime('now')),
11
+ FOREIGN KEY (claim_id) REFERENCES claims(id) ON DELETE CASCADE,
12
+ UNIQUE(claim_id, file_path, symbol_name)
13
+ );
14
+
15
+ CREATE INDEX IF NOT EXISTS idx_claim_symbols_path ON claim_symbols(file_path);
16
+ CREATE INDEX IF NOT EXISTS idx_claim_symbols_name ON claim_symbols(symbol_name);
17
+ CREATE INDEX IF NOT EXISTS idx_claim_symbols_claim ON claim_symbols(claim_id);
18
+ CREATE INDEX IF NOT EXISTS idx_claim_symbols_lookup ON claim_symbols(file_path, symbol_name);
@@ -0,0 +1,19 @@
1
+ -- Symbol reference tracking for impact analysis
2
+ -- Stores which symbols are referenced by which files
3
+
4
+ CREATE TABLE IF NOT EXISTS symbol_references (
5
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ source_file TEXT NOT NULL,
7
+ source_symbol TEXT NOT NULL,
8
+ ref_file TEXT NOT NULL,
9
+ ref_line INTEGER,
10
+ ref_context TEXT,
11
+ session_id TEXT NOT NULL,
12
+ created_at TEXT DEFAULT (datetime('now')),
13
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
14
+ UNIQUE(source_file, source_symbol, ref_file, ref_line)
15
+ );
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_symbol_refs_source ON symbol_references(source_file, source_symbol);
18
+ CREATE INDEX IF NOT EXISTS idx_symbol_refs_ref_file ON symbol_references(ref_file);
19
+ CREATE INDEX IF NOT EXISTS idx_symbol_refs_session ON symbol_references(session_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-collab-mcp",
3
- "version": "0.4.7",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for Claude Code session collaboration - prevents conflicts between parallel sessions",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -19,6 +19,8 @@ function loadMigrations(): string[] {
19
19
  readFileSync(join(migrationsDir, '0001_init.sql'), 'utf-8'),
20
20
  readFileSync(join(migrationsDir, '0002_auth.sql'), 'utf-8'),
21
21
  readFileSync(join(migrationsDir, '0003_config.sql'), 'utf-8'),
22
+ readFileSync(join(migrationsDir, '0004_symbols.sql'), 'utf-8'),
23
+ readFileSync(join(migrationsDir, '0005_references.sql'), 'utf-8'),
22
24
  ];
23
25
  }
24
26
 
package/src/constants.ts CHANGED
@@ -32,44 +32,179 @@ This MCP server coordinates multiple Claude Code sessions working on the same co
32
32
 
33
33
  2. **Before editing any file**: Call \`collab_check\` with the file path to verify no conflicts
34
34
 
35
- 3. **If conflicts detected** (DEFAULT: coordinate):
36
- - Show warning to user with options:
37
- a) **Coordinate** (default): Send message to other session via \`collab_message_send\`, wait for response
38
- b) **Bypass**: Proceed anyway (warn about potential conflicts)
39
- c) **Request release**: Ask owner to release if claim seems stale
40
- - NEVER auto-release another session's claim without explicit user permission
35
+ 3. **Follow the \`recommendation\` from collab_check automatically**:
36
+ - \`proceed_all\`: All files/symbols safe. Edit them without asking user.
37
+ - \`proceed_safe_only\`: Some content blocked. Edit ONLY safe files/symbols, skip blocked. No need to ask user.
38
+ - \`abort\`: All content blocked. Inform user and suggest coordination.
41
39
 
42
- 4. **For significant changes**: Call \`collab_claim\` before starting work on files
40
+ 4. **For significant changes**: Call \`collab_claim\` before starting work
43
41
 
44
- 5. **When done with files**: Call \`collab_release\` with YOUR session_id to free them
42
+ 5. **When done**: Call \`collab_release\` with YOUR session_id to free them
45
43
 
46
44
  6. **On conversation end**: Call \`collab_session_end\` to clean up
47
45
 
46
+ ## Symbol-Level Claims (Fine-Grained)
47
+
48
+ Use symbol-level claims when modifying specific functions/classes, allowing other sessions to work on different parts of the same file.
49
+
50
+ **Claim specific symbols:**
51
+ \`\`\`json
52
+ {
53
+ "symbols": [
54
+ { "file": "src/auth.ts", "symbols": ["validateToken", "refreshToken"] }
55
+ ],
56
+ "intent": "Refactoring token validation"
57
+ }
58
+ \`\`\`
59
+
60
+ **Check specific symbols:**
61
+ \`\`\`json
62
+ {
63
+ "files": ["src/auth.ts"],
64
+ "symbols": [
65
+ { "file": "src/auth.ts", "symbols": ["validateToken"] }
66
+ ]
67
+ }
68
+ \`\`\`
69
+
70
+ **Conflict levels:**
71
+ - \`file\`: Whole file is claimed (no symbols specified)
72
+ - \`symbol\`: Only specific functions/classes are claimed
73
+
74
+ **Example scenario:**
75
+ - Session A claims \`validateToken\` in auth.ts
76
+ - Session B wants to modify \`refreshToken\` in auth.ts
77
+ - → No conflict! Session B can proceed.
78
+
79
+ ## Auto-Decision Rules
80
+
81
+ When \`collab_check\` returns:
82
+ - \`can_edit: true\` → Proceed with safe content automatically
83
+ - \`can_edit: false\` → Stop and inform user about blocked content
84
+
85
+ For symbol-level checks, use \`symbol_status.safe\` and \`symbol_status.blocked\`.
86
+
48
87
  ## Permission Rules
49
88
 
50
89
  - You can ONLY release claims that belong to YOUR session
51
90
  - To release another session's claim, you must ask the user and they must explicitly confirm
52
91
  - Use \`force=true\` in \`collab_release\` only after user explicitly confirms
53
- - When user chooses "coordinate", send a message first and suggest waiting
54
92
 
55
93
  ## Conflict Handling Modes
56
94
 
57
95
  Configure your session behavior with \`collab_config\`:
58
96
 
59
97
  - **"strict"**: Always ask user, never bypass or auto-release
60
- - **"smart"** (default): Ask user, but suggest auto-release for stale claims (>2hr old)
98
+ - **"smart"** (default): Auto-proceed with safe content, ask for blocked
61
99
  - **"bypass"**: Proceed despite conflicts (just warn, don't block)
62
100
 
63
- Config options:
64
- - \`mode\`: strict | smart | bypass
65
- - \`allow_release_others\`: Allow releasing other sessions' claims (default: false)
66
- - \`auto_release_stale\`: Auto-release stale claims (default: false)
67
- - \`stale_threshold_hours\`: Hours before claim is stale (default: 2)
101
+ ## LSP Integration (Advanced)
102
+
103
+ For precise symbol validation and impact analysis, use LSP tools:
104
+
105
+ ### Workflow with LSP
106
+
107
+ 1. **Get symbols from LSP**: Use \`LSP.documentSymbol\` to get actual symbols in a file
108
+ 2. **Validate before claiming**: Use \`collab_validate_symbols\` to verify symbol names
109
+ 3. **Analyze conflicts with context**: Use \`collab_analyze_symbols\` for enhanced conflict detection
110
+
111
+ ### collab_validate_symbols
112
+
113
+ Validate symbol names exist before claiming:
114
+
115
+ \`\`\`
116
+ 1. Claude: LSP.documentSymbol("src/auth.ts")
117
+ 2. Claude: collab_validate_symbols({
118
+ file: "src/auth.ts",
119
+ symbols: ["validateToken", "refreshTokne"], // typo!
120
+ lsp_symbols: [/* LSP output */]
121
+ })
122
+ 3. Response: { invalid_symbols: ["refreshTokne"], suggestions: { "refreshTokne": ["refreshToken"] } }
123
+ \`\`\`
124
+
125
+ ### collab_analyze_symbols
126
+
127
+ Enhanced conflict detection with LSP data:
128
+
129
+ \`\`\`
130
+ 1. Claude: LSP.documentSymbol("src/auth.ts")
131
+ 2. Claude: LSP.findReferences("validateToken")
132
+ 3. Claude: collab_analyze_symbols({
133
+ session_id: "...",
134
+ files: [{ file: "src/auth.ts", symbols: [/* LSP symbols */] }],
135
+ references: [{ symbol: "validateToken", file: "src/auth.ts", references: [...] }]
136
+ })
137
+ 4. Response: {
138
+ can_edit: true,
139
+ recommendation: "proceed_safe_only",
140
+ symbols: [
141
+ { name: "validateToken", conflict_status: "blocked", impact: { references_count: 5, affected_files: [...] } },
142
+ { name: "refreshToken", conflict_status: "safe" }
143
+ ]
144
+ }
145
+ \`\`\`
146
+
147
+ ### Benefits of LSP Integration
148
+
149
+ - **Accurate symbol names**: No typos in claims
150
+ - **Impact awareness**: Know which files will be affected by changes
151
+ - **Smart prioritization**: Focus on low-impact changes first
152
+
153
+ ## Reference Tracking & Impact Analysis
154
+
155
+ Store and query symbol references for smart conflict detection:
156
+
157
+ ### collab_store_references
158
+
159
+ Persist LSP reference data for future impact queries:
160
+
161
+ \`\`\`
162
+ 1. Claude: LSP.findReferences("validateToken")
163
+ 2. Claude: collab_store_references({
164
+ session_id: "...",
165
+ references: [{
166
+ source_file: "src/auth.ts",
167
+ source_symbol: "validateToken",
168
+ references: [
169
+ { file: "src/api/users.ts", line: 15 },
170
+ { file: "src/api/orders.ts", line: 23 }
171
+ ]
172
+ }]
173
+ })
174
+ \`\`\`
175
+
176
+ ### collab_impact_analysis
177
+
178
+ Check if modifying a symbol would affect files claimed by others:
179
+
180
+ \`\`\`
181
+ Claude: collab_impact_analysis({
182
+ session_id: "...",
183
+ file: "src/auth.ts",
184
+ symbol: "validateToken"
185
+ })
186
+
187
+ Response: {
188
+ risk_level: "high",
189
+ reference_count: 3,
190
+ affected_files: ["src/api/users.ts", "src/api/orders.ts"],
191
+ affected_claims: [{ session_name: "other-session", intent: "..." }],
192
+ message: "HIGH RISK: 1 active claim on referencing files"
193
+ }
194
+ \`\`\`
195
+
196
+ ### Risk Levels
197
+
198
+ - **high**: Other sessions have claims on files that reference this symbol
199
+ - **medium**: Many references (>10) but no active claims conflict
200
+ - **low**: Few references, no conflicts
68
201
 
69
202
  ## Best Practices
70
203
 
71
- - Claim files early, release when done
72
- - Use descriptive intents when claiming (e.g., "Refactoring auth module")
73
- - Check for messages periodically with \`collab_message_list\`
74
- - Record architectural decisions with \`collab_decision_add\`
204
+ - **Prefer symbol-level claims** for focused changes (single function/class)
205
+ - **Use file-level claims** for large refactors affecting many symbols
206
+ - **Use LSP validation** when unsure about symbol names
207
+ - **Check references** before modifying widely-used symbols
208
+ - Claim early, release when done
209
+ - Use descriptive intents (e.g., "Refactoring validateToken for JWT support")
75
210
  `.trim();
package/src/db/queries.ts CHANGED
@@ -14,6 +14,8 @@ import type {
14
14
  TodoItem,
15
15
  SessionProgress,
16
16
  SessionConfig,
17
+ SymbolClaim,
18
+ SymbolType,
17
19
  } from './types';
18
20
 
19
21
  // Helper to generate UUID v4
@@ -262,8 +264,9 @@ export async function createClaim(
262
264
  files: string[];
263
265
  intent: string;
264
266
  scope?: ClaimScope;
267
+ symbols?: SymbolClaim[];
265
268
  }
266
- ): Promise<{ claim: Claim; files: string[] }> {
269
+ ): Promise<{ claim: Claim; files: string[]; symbols?: SymbolClaim[] }> {
267
270
  const id = generateId();
268
271
  const now = new Date().toISOString();
269
272
  const scope = params.scope ?? 'medium';
@@ -283,7 +286,23 @@ export async function createClaim(
283
286
  .bind(id, filePath, isPattern);
284
287
  });
285
288
 
286
- await db.batch([claimStatement, ...fileStatements]);
289
+ // Add symbol claims if provided
290
+ const symbolStatements: ReturnType<typeof db.prepare>[] = [];
291
+ if (params.symbols && params.symbols.length > 0) {
292
+ for (const symbolClaim of params.symbols) {
293
+ for (const symbolName of symbolClaim.symbols) {
294
+ symbolStatements.push(
295
+ db
296
+ .prepare(
297
+ 'INSERT INTO claim_symbols (claim_id, file_path, symbol_name, symbol_type, created_at) VALUES (?, ?, ?, ?, ?)'
298
+ )
299
+ .bind(id, symbolClaim.file, symbolName, symbolClaim.symbol_type ?? 'function', now)
300
+ );
301
+ }
302
+ }
303
+ }
304
+
305
+ await db.batch([claimStatement, ...fileStatements, ...symbolStatements]);
287
306
 
288
307
  return {
289
308
  claim: {
@@ -297,6 +316,7 @@ export async function createClaim(
297
316
  completed_summary: null,
298
317
  },
299
318
  files: params.files,
319
+ symbols: params.symbols,
300
320
  };
301
321
  }
302
322
 
@@ -384,47 +404,187 @@ export async function listClaims(
384
404
  export async function checkConflicts(
385
405
  db: DatabaseAdapter,
386
406
  files: string[],
387
- excludeSessionId?: string
407
+ excludeSessionId?: string,
408
+ symbols?: SymbolClaim[]
388
409
  ): Promise<ConflictInfo[]> {
389
410
  const conflicts: ConflictInfo[] = [];
390
411
 
391
- for (const filePath of files) {
392
- // Check for exact matches or pattern overlaps
393
- let query = `
394
- SELECT
395
- c.id as claim_id,
396
- c.session_id,
397
- s.name as session_name,
398
- cf.file_path,
399
- c.intent,
400
- c.scope,
401
- c.created_at
402
- FROM claim_files cf
403
- JOIN claims c ON cf.claim_id = c.id
404
- JOIN sessions s ON c.session_id = s.id
405
- WHERE c.status = 'active'
406
- AND s.status = 'active'
407
- AND (cf.file_path = ? OR (cf.is_pattern = 1 AND ? GLOB cf.file_path))
408
- `;
409
- const bindings: string[] = [filePath, filePath];
410
-
411
- if (excludeSessionId) {
412
- query += ' AND c.session_id != ?';
413
- bindings.push(excludeSessionId);
412
+ // Build a map of file -> symbols for quick lookup
413
+ const symbolsByFile = new Map<string, Set<string>>();
414
+ if (symbols && symbols.length > 0) {
415
+ for (const sc of symbols) {
416
+ const existing = symbolsByFile.get(sc.file) ?? new Set();
417
+ for (const sym of sc.symbols) {
418
+ existing.add(sym);
419
+ }
420
+ symbolsByFile.set(sc.file, existing);
414
421
  }
422
+ }
415
423
 
416
- const result = await db
417
- .prepare(query)
418
- .bind(...bindings)
419
- .all<ConflictInfo>();
420
-
421
- conflicts.push(...result.results);
424
+ for (const filePath of files) {
425
+ const requestedSymbols = symbolsByFile.get(filePath);
426
+
427
+ // First check if there are symbol-level claims for this file
428
+ if (requestedSymbols && requestedSymbols.size > 0) {
429
+ // Symbol-level conflict check
430
+ let symbolQuery = `
431
+ SELECT
432
+ c.id as claim_id,
433
+ c.session_id,
434
+ s.name as session_name,
435
+ cs.file_path,
436
+ c.intent,
437
+ c.scope,
438
+ c.created_at,
439
+ cs.symbol_name,
440
+ cs.symbol_type
441
+ FROM claim_symbols cs
442
+ JOIN claims c ON cs.claim_id = c.id
443
+ JOIN sessions s ON c.session_id = s.id
444
+ WHERE c.status = 'active'
445
+ AND s.status = 'active'
446
+ AND cs.file_path = ?
447
+ AND cs.symbol_name IN (${Array.from(requestedSymbols).map(() => '?').join(',')})
448
+ `;
449
+ const symbolBindings: string[] = [filePath, ...Array.from(requestedSymbols)];
450
+
451
+ if (excludeSessionId) {
452
+ symbolQuery += ' AND c.session_id != ?';
453
+ symbolBindings.push(excludeSessionId);
454
+ }
455
+
456
+ const symbolResult = await db
457
+ .prepare(symbolQuery)
458
+ .bind(...symbolBindings)
459
+ .all<ConflictInfo & { symbol_name: string; symbol_type: SymbolType }>();
460
+
461
+ for (const r of symbolResult.results) {
462
+ conflicts.push({
463
+ ...r,
464
+ conflict_level: 'symbol',
465
+ });
466
+ }
467
+
468
+ // Also check if there's a file-level claim (no symbols = whole file claimed)
469
+ let fileClaimQuery = `
470
+ SELECT
471
+ c.id as claim_id,
472
+ c.session_id,
473
+ s.name as session_name,
474
+ cf.file_path,
475
+ c.intent,
476
+ c.scope,
477
+ c.created_at
478
+ FROM claim_files cf
479
+ JOIN claims c ON cf.claim_id = c.id
480
+ JOIN sessions s ON c.session_id = s.id
481
+ WHERE c.status = 'active'
482
+ AND s.status = 'active'
483
+ AND (cf.file_path = ? OR (cf.is_pattern = 1 AND ? GLOB cf.file_path))
484
+ AND NOT EXISTS (
485
+ SELECT 1 FROM claim_symbols cs WHERE cs.claim_id = c.id AND cs.file_path = cf.file_path
486
+ )
487
+ `;
488
+ const fileClaimBindings: string[] = [filePath, filePath];
489
+
490
+ if (excludeSessionId) {
491
+ fileClaimQuery += ' AND c.session_id != ?';
492
+ fileClaimBindings.push(excludeSessionId);
493
+ }
494
+
495
+ const fileClaimResult = await db
496
+ .prepare(fileClaimQuery)
497
+ .bind(...fileClaimBindings)
498
+ .all<Omit<ConflictInfo, 'conflict_level'>>();
499
+
500
+ for (const r of fileClaimResult.results) {
501
+ conflicts.push({
502
+ ...r,
503
+ conflict_level: 'file',
504
+ });
505
+ }
506
+ } else {
507
+ // No symbols specified - check both file-level and symbol-level claims
508
+ // File-level claims (whole file)
509
+ let fileQuery = `
510
+ SELECT
511
+ c.id as claim_id,
512
+ c.session_id,
513
+ s.name as session_name,
514
+ cf.file_path,
515
+ c.intent,
516
+ c.scope,
517
+ c.created_at
518
+ FROM claim_files cf
519
+ JOIN claims c ON cf.claim_id = c.id
520
+ JOIN sessions s ON c.session_id = s.id
521
+ WHERE c.status = 'active'
522
+ AND s.status = 'active'
523
+ AND (cf.file_path = ? OR (cf.is_pattern = 1 AND ? GLOB cf.file_path))
524
+ `;
525
+ const fileBindings: string[] = [filePath, filePath];
526
+
527
+ if (excludeSessionId) {
528
+ fileQuery += ' AND c.session_id != ?';
529
+ fileBindings.push(excludeSessionId);
530
+ }
531
+
532
+ const fileResult = await db
533
+ .prepare(fileQuery)
534
+ .bind(...fileBindings)
535
+ .all<Omit<ConflictInfo, 'conflict_level'>>();
536
+
537
+ for (const r of fileResult.results) {
538
+ conflicts.push({
539
+ ...r,
540
+ conflict_level: 'file',
541
+ });
542
+ }
543
+
544
+ // Symbol-level claims on this file
545
+ let symbolOnlyQuery = `
546
+ SELECT DISTINCT
547
+ c.id as claim_id,
548
+ c.session_id,
549
+ s.name as session_name,
550
+ cs.file_path,
551
+ c.intent,
552
+ c.scope,
553
+ c.created_at,
554
+ cs.symbol_name,
555
+ cs.symbol_type
556
+ FROM claim_symbols cs
557
+ JOIN claims c ON cs.claim_id = c.id
558
+ JOIN sessions s ON c.session_id = s.id
559
+ WHERE c.status = 'active'
560
+ AND s.status = 'active'
561
+ AND cs.file_path = ?
562
+ `;
563
+ const symbolOnlyBindings: string[] = [filePath];
564
+
565
+ if (excludeSessionId) {
566
+ symbolOnlyQuery += ' AND c.session_id != ?';
567
+ symbolOnlyBindings.push(excludeSessionId);
568
+ }
569
+
570
+ const symbolOnlyResult = await db
571
+ .prepare(symbolOnlyQuery)
572
+ .bind(...symbolOnlyBindings)
573
+ .all<ConflictInfo & { symbol_name: string; symbol_type: SymbolType }>();
574
+
575
+ for (const r of symbolOnlyResult.results) {
576
+ conflicts.push({
577
+ ...r,
578
+ conflict_level: 'symbol',
579
+ });
580
+ }
581
+ }
422
582
  }
423
583
 
424
- // Deduplicate by claim_id + file_path
584
+ // Deduplicate by claim_id + file_path + symbol_name
425
585
  const seen = new Set<string>();
426
586
  return conflicts.filter((c) => {
427
- const key = `${c.claim_id}:${c.file_path}`;
587
+ const key = `${c.claim_id}:${c.file_path}:${c.symbol_name ?? ''}`;
428
588
  if (seen.has(key)) return false;
429
589
  seen.add(key);
430
590
  return true;
@@ -575,3 +735,158 @@ export async function listDecisions(
575
735
 
576
736
  return result.results;
577
737
  }
738
+
739
+ // ============ Reference Queries ============
740
+
741
+ import type { ReferenceInput, SymbolReference, ImpactInfo } from './types';
742
+
743
+ export async function storeReferences(
744
+ db: DatabaseAdapter,
745
+ sessionId: string,
746
+ references: ReferenceInput[]
747
+ ): Promise<{ stored: number; skipped: number }> {
748
+ let stored = 0;
749
+ let skipped = 0;
750
+ const now = new Date().toISOString();
751
+
752
+ for (const ref of references) {
753
+ for (const r of ref.references) {
754
+ try {
755
+ await db
756
+ .prepare(
757
+ `INSERT OR IGNORE INTO symbol_references
758
+ (source_file, source_symbol, ref_file, ref_line, ref_context, session_id, created_at)
759
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
760
+ )
761
+ .bind(ref.source_file, ref.source_symbol, r.file, r.line, r.context ?? null, sessionId, now)
762
+ .run();
763
+ stored++;
764
+ } catch {
765
+ skipped++;
766
+ }
767
+ }
768
+ }
769
+
770
+ return { stored, skipped };
771
+ }
772
+
773
+ export async function getReferencesForSymbol(
774
+ db: DatabaseAdapter,
775
+ sourceFile: string,
776
+ sourceSymbol: string
777
+ ): Promise<SymbolReference[]> {
778
+ const result = await db
779
+ .prepare(
780
+ `SELECT * FROM symbol_references
781
+ WHERE source_file = ? AND source_symbol = ?
782
+ ORDER BY ref_file, ref_line`
783
+ )
784
+ .bind(sourceFile, sourceSymbol)
785
+ .all<SymbolReference>();
786
+
787
+ return result.results;
788
+ }
789
+
790
+ export async function getReferencesToFile(
791
+ db: DatabaseAdapter,
792
+ filePath: string
793
+ ): Promise<SymbolReference[]> {
794
+ const result = await db
795
+ .prepare(
796
+ `SELECT * FROM symbol_references
797
+ WHERE ref_file = ?
798
+ ORDER BY source_file, source_symbol`
799
+ )
800
+ .bind(filePath)
801
+ .all<SymbolReference>();
802
+
803
+ return result.results;
804
+ }
805
+
806
+ export async function analyzeClaimImpact(
807
+ db: DatabaseAdapter,
808
+ sourceFile: string,
809
+ sourceSymbol: string,
810
+ excludeSessionId?: string
811
+ ): Promise<ImpactInfo> {
812
+ // Get all references to this symbol
813
+ const refs = await getReferencesForSymbol(db, sourceFile, sourceSymbol);
814
+
815
+ // Get unique files that reference this symbol
816
+ const affectedFiles = [...new Set(refs.map((r) => r.ref_file))];
817
+
818
+ // Check if any of these files have active claims
819
+ const affectedClaims: ImpactInfo['affected_claims'] = [];
820
+
821
+ if (affectedFiles.length > 0) {
822
+ const placeholders = affectedFiles.map(() => '?').join(',');
823
+ let query = `
824
+ SELECT DISTINCT
825
+ c.id as claim_id,
826
+ s.name as session_name,
827
+ c.intent,
828
+ cf.file_path
829
+ FROM claim_files cf
830
+ JOIN claims c ON cf.claim_id = c.id
831
+ JOIN sessions s ON c.session_id = s.id
832
+ WHERE c.status = 'active'
833
+ AND s.status = 'active'
834
+ AND cf.file_path IN (${placeholders})
835
+ `;
836
+ const bindings: string[] = [...affectedFiles];
837
+
838
+ if (excludeSessionId) {
839
+ query += ' AND c.session_id != ?';
840
+ bindings.push(excludeSessionId);
841
+ }
842
+
843
+ const claimResults = await db
844
+ .prepare(query)
845
+ .bind(...bindings)
846
+ .all<{ claim_id: string; session_name: string | null; intent: string; file_path: string }>();
847
+
848
+ // Group by claim
849
+ const claimMap = new Map<string, { session_name: string | null; intent: string; files: string[] }>();
850
+ for (const r of claimResults.results) {
851
+ const existing = claimMap.get(r.claim_id);
852
+ if (existing) {
853
+ existing.files.push(r.file_path);
854
+ } else {
855
+ claimMap.set(r.claim_id, {
856
+ session_name: r.session_name,
857
+ intent: r.intent,
858
+ files: [r.file_path],
859
+ });
860
+ }
861
+ }
862
+
863
+ for (const [claimId, data] of claimMap) {
864
+ affectedClaims.push({
865
+ claim_id: claimId,
866
+ session_name: data.session_name,
867
+ intent: data.intent,
868
+ affected_symbols: data.files,
869
+ });
870
+ }
871
+ }
872
+
873
+ return {
874
+ symbol: sourceSymbol,
875
+ file: sourceFile,
876
+ affected_claims: affectedClaims,
877
+ reference_count: refs.length,
878
+ affected_files: affectedFiles,
879
+ };
880
+ }
881
+
882
+ export async function clearSessionReferences(
883
+ db: DatabaseAdapter,
884
+ sessionId: string
885
+ ): Promise<number> {
886
+ const result = await db
887
+ .prepare('DELETE FROM symbol_references WHERE session_id = ?')
888
+ .bind(sessionId)
889
+ .run();
890
+
891
+ return result.meta.changes;
892
+ }