switchman-dev 0.1.2 → 0.1.3

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.
@@ -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
- if (result.hasConflicts) {
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: result.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
- summary: buildSummary(worktrees, allConflicts, fileConflicts),
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');