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.
- package/migrations/0004_symbols.sql +18 -0
- package/migrations/0005_references.sql +19 -0
- package/package.json +1 -1
- package/src/cli.ts +2 -0
- package/src/constants.ts +154 -19
- package/src/db/queries.ts +349 -34
- package/src/db/types.ts +60 -0
- package/src/mcp/server.ts +16 -1
- package/src/mcp/tools/claim.ts +209 -40
- package/src/mcp/tools/lsp.ts +705 -0
|
@@ -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
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. **
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
40
|
+
4. **For significant changes**: Call \`collab_claim\` before starting work
|
|
43
41
|
|
|
44
|
-
5. **When done
|
|
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):
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
-
|
|
72
|
-
- Use
|
|
73
|
-
-
|
|
74
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
+
}
|