switchman-dev 0.1.2 → 0.1.4
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 +95 -205
- package/examples/README.md +18 -2
- package/examples/walkthrough.sh +12 -16
- package/package.json +3 -3
- package/src/cli/index.js +2517 -331
- package/src/core/ci.js +114 -0
- package/src/core/db.js +1669 -28
- package/src/core/detector.js +109 -7
- package/src/core/enforcement.js +966 -0
- package/src/core/git.js +108 -5
- package/src/core/ignore.js +49 -0
- package/src/core/mcp.js +76 -0
- package/src/core/merge-gate.js +305 -0
- package/src/core/monitor.js +39 -0
- package/src/core/outcome.js +190 -0
- package/src/core/pipeline.js +1113 -0
- package/src/core/planner.js +508 -0
- package/src/core/policy.js +49 -0
- package/src/core/queue.js +225 -0
- package/src/core/semantic.js +311 -0
- package/src/mcp/server.js +321 -1
package/src/core/detector.js
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
* Scans all registered worktrees for file-level and branch-level conflicts
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { listWorktrees } from './db.js';
|
|
6
|
+
import { listScopeReservations, listWorktrees } from './db.js';
|
|
7
7
|
import {
|
|
8
8
|
listGitWorktrees,
|
|
9
9
|
getWorktreeChangedFiles,
|
|
10
10
|
checkMergeConflicts,
|
|
11
11
|
} from './git.js';
|
|
12
|
+
import { filterIgnoredPaths } from './ignore.js';
|
|
13
|
+
import { evaluateRepoCompliance } from './enforcement.js';
|
|
14
|
+
import { buildSemanticIndexForPath, detectSemanticConflicts } from './semantic.js';
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Scan all worktrees for conflicts.
|
|
@@ -20,12 +23,22 @@ export async function scanAllWorktrees(db, repoRoot) {
|
|
|
20
23
|
|
|
21
24
|
// Build a unified list, merging db metadata with git reality
|
|
22
25
|
const worktrees = mergeWorktreeInfo(dbWorktrees, gitWorktrees);
|
|
26
|
+
const enforcement = evaluateRepoCompliance(db, repoRoot, worktrees);
|
|
23
27
|
|
|
24
28
|
if (worktrees.length < 2) {
|
|
25
29
|
return {
|
|
26
30
|
worktrees,
|
|
31
|
+
fileMap: {},
|
|
27
32
|
conflicts: [],
|
|
28
33
|
fileConflicts: [],
|
|
34
|
+
ownershipConflicts: detectOwnershipOverlaps(db),
|
|
35
|
+
semanticConflicts: [],
|
|
36
|
+
unclaimedChanges: enforcement.unclaimedChanges,
|
|
37
|
+
worktreeCompliance: enforcement.worktreeCompliance,
|
|
38
|
+
complianceSummary: enforcement.complianceSummary,
|
|
39
|
+
deniedWrites: enforcement.deniedWrites,
|
|
40
|
+
commitGateFailures: enforcement.commitGateFailures,
|
|
41
|
+
scannedAt: new Date().toISOString(),
|
|
29
42
|
summary: 'Less than 2 worktrees. Nothing to compare.',
|
|
30
43
|
};
|
|
31
44
|
}
|
|
@@ -40,6 +53,13 @@ export async function scanAllWorktrees(db, repoRoot) {
|
|
|
40
53
|
|
|
41
54
|
// Step 2: Detect file-level overlaps (fast, always available)
|
|
42
55
|
const fileConflicts = detectFileOverlaps(fileMap, worktrees);
|
|
56
|
+
const ownershipConflicts = detectOwnershipOverlaps(db);
|
|
57
|
+
const semanticIndexes = worktrees.map((wt) => ({
|
|
58
|
+
worktree: wt.name,
|
|
59
|
+
branch: wt.branch || 'unknown',
|
|
60
|
+
objects: buildSemanticIndexForPath(wt.path, fileMap[wt.name] || []).objects,
|
|
61
|
+
}));
|
|
62
|
+
const semanticConflicts = detectSemanticConflicts(semanticIndexes);
|
|
43
63
|
|
|
44
64
|
// Step 3: Detect branch-level merge conflicts (slower, uses git merge-tree)
|
|
45
65
|
const branchConflicts = [];
|
|
@@ -48,27 +68,34 @@ export async function scanAllWorktrees(db, repoRoot) {
|
|
|
48
68
|
for (const [wtA, wtB] of pairs) {
|
|
49
69
|
if (!wtA.branch || !wtB.branch) continue;
|
|
50
70
|
const result = checkMergeConflicts(repoRoot, wtA.branch, wtB.branch);
|
|
51
|
-
|
|
71
|
+
const conflictingFiles = filterIgnoredPaths(result.conflictingFiles || []);
|
|
72
|
+
if (result.hasConflicts && conflictingFiles.length > 0) {
|
|
52
73
|
branchConflicts.push({
|
|
53
74
|
type: result.isOverlapOnly ? 'file_overlap' : 'merge_conflict',
|
|
54
75
|
worktreeA: wtA.name,
|
|
55
76
|
worktreeB: wtB.name,
|
|
56
77
|
branchA: wtA.branch,
|
|
57
78
|
branchB: wtB.branch,
|
|
58
|
-
conflictingFiles
|
|
79
|
+
conflictingFiles,
|
|
59
80
|
details: result.details,
|
|
60
81
|
});
|
|
61
82
|
}
|
|
62
83
|
}
|
|
63
84
|
|
|
64
85
|
const allConflicts = [...branchConflicts];
|
|
65
|
-
|
|
66
86
|
return {
|
|
67
87
|
worktrees,
|
|
68
88
|
fileMap,
|
|
69
89
|
conflicts: allConflicts,
|
|
70
90
|
fileConflicts,
|
|
71
|
-
|
|
91
|
+
ownershipConflicts,
|
|
92
|
+
semanticConflicts,
|
|
93
|
+
unclaimedChanges: enforcement.unclaimedChanges,
|
|
94
|
+
worktreeCompliance: enforcement.worktreeCompliance,
|
|
95
|
+
complianceSummary: enforcement.complianceSummary,
|
|
96
|
+
deniedWrites: enforcement.deniedWrites,
|
|
97
|
+
commitGateFailures: enforcement.commitGateFailures,
|
|
98
|
+
summary: buildSummary(worktrees, allConflicts, fileConflicts, ownershipConflicts, semanticConflicts, enforcement.unclaimedChanges),
|
|
72
99
|
scannedAt: new Date().toISOString(),
|
|
73
100
|
};
|
|
74
101
|
}
|
|
@@ -96,6 +123,70 @@ function detectFileOverlaps(fileMap, worktrees) {
|
|
|
96
123
|
return conflicts;
|
|
97
124
|
}
|
|
98
125
|
|
|
126
|
+
function normalizeScopeRoot(pattern) {
|
|
127
|
+
return String(pattern || '')
|
|
128
|
+
.replace(/\\/g, '/')
|
|
129
|
+
.replace(/\/\*\*$/, '')
|
|
130
|
+
.replace(/\/\*$/, '')
|
|
131
|
+
.replace(/\/+$/, '');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function scopeRootsOverlap(leftPattern, rightPattern) {
|
|
135
|
+
const left = normalizeScopeRoot(leftPattern);
|
|
136
|
+
const right = normalizeScopeRoot(rightPattern);
|
|
137
|
+
if (!left || !right) return false;
|
|
138
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function detectOwnershipOverlaps(db) {
|
|
142
|
+
const reservations = listScopeReservations(db);
|
|
143
|
+
const conflicts = [];
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < reservations.length; i++) {
|
|
146
|
+
for (let j = i + 1; j < reservations.length; j++) {
|
|
147
|
+
const left = reservations[i];
|
|
148
|
+
const right = reservations[j];
|
|
149
|
+
if (left.lease_id === right.lease_id) continue;
|
|
150
|
+
if (left.worktree === right.worktree) continue;
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
left.ownership_level === 'subsystem' &&
|
|
154
|
+
right.ownership_level === 'subsystem' &&
|
|
155
|
+
left.subsystem_tag &&
|
|
156
|
+
left.subsystem_tag === right.subsystem_tag
|
|
157
|
+
) {
|
|
158
|
+
conflicts.push({
|
|
159
|
+
type: 'subsystem_overlap',
|
|
160
|
+
worktreeA: left.worktree,
|
|
161
|
+
worktreeB: right.worktree,
|
|
162
|
+
leaseA: left.lease_id,
|
|
163
|
+
leaseB: right.lease_id,
|
|
164
|
+
subsystemTag: left.subsystem_tag,
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
left.ownership_level === 'path_scope' &&
|
|
171
|
+
right.ownership_level === 'path_scope' &&
|
|
172
|
+
scopeRootsOverlap(left.scope_pattern, right.scope_pattern)
|
|
173
|
+
) {
|
|
174
|
+
conflicts.push({
|
|
175
|
+
type: 'scope_overlap',
|
|
176
|
+
worktreeA: left.worktree,
|
|
177
|
+
worktreeB: right.worktree,
|
|
178
|
+
leaseA: left.lease_id,
|
|
179
|
+
leaseB: right.lease_id,
|
|
180
|
+
scopeA: left.scope_pattern,
|
|
181
|
+
scopeB: right.scope_pattern,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return conflicts;
|
|
188
|
+
}
|
|
189
|
+
|
|
99
190
|
/**
|
|
100
191
|
* Check if claiming a set of files from a worktree would conflict with current activity
|
|
101
192
|
*/
|
|
@@ -135,6 +226,8 @@ function mergeWorktreeInfo(dbWorktrees, gitWorktrees) {
|
|
|
135
226
|
const dbMatch = dbWorktrees.find(d => d.path === gw.path || d.name === gw.name);
|
|
136
227
|
return {
|
|
137
228
|
...gw,
|
|
229
|
+
name: dbMatch?.name || gw.name,
|
|
230
|
+
branch: dbMatch?.branch || gw.branch,
|
|
138
231
|
agent: dbMatch?.agent || null,
|
|
139
232
|
registeredInDb: !!dbMatch,
|
|
140
233
|
};
|
|
@@ -153,11 +246,11 @@ function getPairs(arr) {
|
|
|
153
246
|
return pairs;
|
|
154
247
|
}
|
|
155
248
|
|
|
156
|
-
function buildSummary(worktrees, conflicts, fileConflicts) {
|
|
249
|
+
function buildSummary(worktrees, conflicts, fileConflicts, ownershipConflicts, semanticConflicts, unclaimedChanges) {
|
|
157
250
|
const lines = [];
|
|
158
251
|
lines.push(`Scanned ${worktrees.length} worktree(s)`);
|
|
159
252
|
|
|
160
|
-
if (conflicts.length === 0 && fileConflicts.length === 0) {
|
|
253
|
+
if (conflicts.length === 0 && fileConflicts.length === 0 && ownershipConflicts.length === 0 && semanticConflicts.length === 0) {
|
|
161
254
|
lines.push('✓ No conflicts detected');
|
|
162
255
|
} else {
|
|
163
256
|
if (conflicts.length > 0) {
|
|
@@ -166,6 +259,15 @@ function buildSummary(worktrees, conflicts, fileConflicts) {
|
|
|
166
259
|
if (fileConflicts.length > 0) {
|
|
167
260
|
lines.push(`⚠ ${fileConflicts.length} file(s) being edited in multiple worktrees`);
|
|
168
261
|
}
|
|
262
|
+
if (ownershipConflicts.length > 0) {
|
|
263
|
+
lines.push(`⚠ ${ownershipConflicts.length} ownership boundary overlap(s) detected`);
|
|
264
|
+
}
|
|
265
|
+
if (semanticConflicts.length > 0) {
|
|
266
|
+
lines.push(`⚠ ${semanticConflicts.length} semantic overlap(s) detected`);
|
|
267
|
+
}
|
|
268
|
+
if (unclaimedChanges.length > 0) {
|
|
269
|
+
lines.push(`⚠ ${unclaimedChanges.length} worktree(s) have unclaimed changed files`);
|
|
270
|
+
}
|
|
169
271
|
}
|
|
170
272
|
|
|
171
273
|
return lines.join('\n');
|