claude-code-workflow 6.3.11 → 6.3.13

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.
Files changed (33) hide show
  1. package/.claude/CLAUDE.md +33 -33
  2. package/.claude/agents/issue-plan-agent.md +77 -5
  3. package/.claude/agents/issue-queue-agent.md +122 -18
  4. package/.claude/commands/issue/execute.md +53 -40
  5. package/.claude/commands/issue/new.md +113 -11
  6. package/.claude/commands/issue/plan.md +112 -37
  7. package/.claude/commands/issue/queue.md +28 -18
  8. package/.claude/skills/software-manual/scripts/assemble_docsify.py +584 -0
  9. package/.claude/skills/software-manual/templates/css/docsify-base.css +984 -0
  10. package/.claude/skills/software-manual/templates/docsify-shell.html +466 -0
  11. package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +141 -168
  12. package/.claude/workflows/cli-templates/schemas/solution-schema.json +3 -2
  13. package/.codex/prompts/issue-execute.md +3 -3
  14. package/.codex/prompts/issue-queue.md +3 -3
  15. package/ccw/dist/commands/issue.d.ts.map +1 -1
  16. package/ccw/dist/commands/issue.js +2 -1
  17. package/ccw/dist/commands/issue.js.map +1 -1
  18. package/ccw/src/commands/issue.ts +2 -1
  19. package/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +580 -467
  20. package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +532 -461
  21. package/ccw/src/templates/dashboard-js/components/notifications.js +774 -774
  22. package/ccw/src/templates/dashboard-js/i18n.js +4 -0
  23. package/ccw/src/templates/dashboard.html +10 -0
  24. package/ccw/src/tools/claude-cli-tools.ts +388 -388
  25. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  26. package/codex-lens/src/codexlens/config.py +19 -3
  27. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  28. package/codex-lens/src/codexlens/search/ranking.py +15 -4
  29. package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
  30. package/codex-lens/src/codexlens/semantic/vector_store.py +57 -47
  31. package/codex-lens/src/codexlens/storage/__pycache__/registry.cpython-313.pyc +0 -0
  32. package/codex-lens/src/codexlens/storage/registry.py +114 -101
  33. package/package.json +83 -83
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import logging
6
7
  import os
7
8
  from dataclasses import dataclass, field
8
9
  from functools import cached_property
@@ -18,6 +19,8 @@ WORKSPACE_DIR_NAME = ".codexlens"
18
19
  # Settings file name
19
20
  SETTINGS_FILE_NAME = "settings.json"
20
21
 
22
+ log = logging.getLogger(__name__)
23
+
21
24
 
22
25
  def _default_global_dir() -> Path:
23
26
  """Get global CodexLens data directory."""
@@ -200,7 +203,15 @@ class Config:
200
203
  # Load embedding settings
201
204
  embedding = settings.get("embedding", {})
202
205
  if "backend" in embedding:
203
- self.embedding_backend = embedding["backend"]
206
+ backend = embedding["backend"]
207
+ if backend in {"fastembed", "litellm"}:
208
+ self.embedding_backend = backend
209
+ else:
210
+ log.warning(
211
+ "Invalid embedding backend in %s: %r (expected 'fastembed' or 'litellm')",
212
+ self.settings_path,
213
+ backend,
214
+ )
204
215
  if "model" in embedding:
205
216
  self.embedding_model = embedding["model"]
206
217
  if "use_gpu" in embedding:
@@ -224,8 +235,13 @@ class Config:
224
235
  self.llm_timeout_ms = llm["timeout_ms"]
225
236
  if "batch_size" in llm:
226
237
  self.llm_batch_size = llm["batch_size"]
227
- except Exception:
228
- pass # Silently ignore errors
238
+ except Exception as exc:
239
+ log.warning(
240
+ "Failed to load settings from %s (%s): %s",
241
+ self.settings_path,
242
+ type(exc).__name__,
243
+ exc,
244
+ )
229
245
 
230
246
  @classmethod
231
247
  def load(cls) -> "Config":
@@ -22,12 +22,23 @@ class QueryIntent(str, Enum):
22
22
  MIXED = "mixed"
23
23
 
24
24
 
25
- def normalize_weights(weights: Dict[str, float]) -> Dict[str, float]:
25
+ def normalize_weights(weights: Dict[str, float | None]) -> Dict[str, float | None]:
26
26
  """Normalize weights to sum to 1.0 (best-effort)."""
27
27
  total = sum(float(v) for v in weights.values() if v is not None)
28
- if not math.isfinite(total) or total <= 0:
29
- return {k: float(v) for k, v in weights.items()}
30
- return {k: float(v) / total for k, v in weights.items()}
28
+
29
+ # NaN total: do not attempt to normalize (division would propagate NaNs).
30
+ if math.isnan(total):
31
+ return dict(weights)
32
+
33
+ # Infinite total: do not attempt to normalize (division yields 0 or NaN).
34
+ if not math.isfinite(total):
35
+ return dict(weights)
36
+
37
+ # Zero/negative total: do not attempt to normalize (invalid denominator).
38
+ if total <= 0:
39
+ return dict(weights)
40
+
41
+ return {k: (float(v) / total if v is not None else None) for k, v in weights.items()}
31
42
 
32
43
 
33
44
  def detect_query_intent(query: str) -> QueryIntent:
@@ -16,43 +16,53 @@ import threading
16
16
  from pathlib import Path
17
17
  from typing import Any, Dict, List, Optional, Tuple
18
18
 
19
- from codexlens.entities import SearchResult, SemanticChunk
20
- from codexlens.errors import StorageError
21
-
22
- from . import SEMANTIC_AVAILABLE
23
-
24
- if SEMANTIC_AVAILABLE:
25
- import numpy as np
26
-
27
- # Try to import ANN index (optional hnswlib dependency)
28
- try:
29
- from codexlens.semantic.ann_index import ANNIndex, HNSWLIB_AVAILABLE
19
+ from codexlens.entities import SearchResult, SemanticChunk
20
+ from codexlens.errors import StorageError
21
+
22
+ try:
23
+ import numpy as np
24
+ NUMPY_AVAILABLE = True
25
+ except ImportError:
26
+ np = None # type: ignore[assignment]
27
+ NUMPY_AVAILABLE = False
28
+
29
+ # Try to import ANN index (optional hnswlib dependency)
30
+ try:
31
+ from codexlens.semantic.ann_index import ANNIndex, HNSWLIB_AVAILABLE
30
32
  except ImportError:
31
33
  HNSWLIB_AVAILABLE = False
32
34
  ANNIndex = None
33
35
 
34
-
35
- logger = logging.getLogger(__name__)
36
-
37
-
38
- def _cosine_similarity(a: List[float], b: List[float]) -> float:
39
- """Compute cosine similarity between two vectors."""
40
- if not SEMANTIC_AVAILABLE:
41
- raise ImportError("numpy required for vector operations")
42
-
43
- a_arr = np.array(a)
44
- b_arr = np.array(b)
45
-
46
- norm_a = np.linalg.norm(a_arr)
47
- norm_b = np.linalg.norm(b_arr)
48
-
49
- if norm_a == 0 or norm_b == 0:
50
- return 0.0
51
-
52
- return float(np.dot(a_arr, b_arr) / (norm_a * norm_b))
53
-
54
-
55
- class VectorStore:
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ # Epsilon used to guard against floating point precision edge cases (e.g., near-zero norms).
40
+ EPSILON = 1e-10
41
+
42
+
43
+ def _cosine_similarity(a: List[float], b: List[float]) -> float:
44
+ """Compute cosine similarity between two vectors."""
45
+ if not NUMPY_AVAILABLE:
46
+ raise ImportError("numpy required for vector operations")
47
+
48
+ a_arr = np.array(a)
49
+ b_arr = np.array(b)
50
+
51
+ norm_a = np.linalg.norm(a_arr)
52
+ norm_b = np.linalg.norm(b_arr)
53
+
54
+ # Use epsilon tolerance to avoid division by (near-)zero due to floating point precision.
55
+ if norm_a < EPSILON or norm_b < EPSILON:
56
+ return 0.0
57
+
58
+ denom = norm_a * norm_b
59
+ if denom < EPSILON:
60
+ return 0.0
61
+
62
+ return float(np.dot(a_arr, b_arr) / denom)
63
+
64
+
65
+ class VectorStore:
56
66
  """SQLite-based vector storage with HNSW-accelerated similarity search.
57
67
 
58
68
  Performance optimizations:
@@ -67,12 +77,12 @@ class VectorStore:
67
77
  # Default embedding dimension (used when creating new index)
68
78
  DEFAULT_DIM = 768
69
79
 
70
- def __init__(self, db_path: str | Path) -> None:
71
- if not SEMANTIC_AVAILABLE:
72
- raise ImportError(
73
- "Semantic search dependencies not available. "
74
- "Install with: pip install codexlens[semantic]"
75
- )
80
+ def __init__(self, db_path: str | Path) -> None:
81
+ if not NUMPY_AVAILABLE:
82
+ raise ImportError(
83
+ "Semantic search dependencies not available. "
84
+ "Install with: pip install codexlens[semantic]"
85
+ )
76
86
 
77
87
  self.db_path = Path(db_path)
78
88
  self.db_path.parent.mkdir(parents=True, exist_ok=True)
@@ -299,14 +309,14 @@ class VectorStore:
299
309
  ]
300
310
  self._embedding_matrix = np.vstack(embeddings)
301
311
 
302
- # Pre-compute norms for faster similarity calculation
303
- self._embedding_norms = np.linalg.norm(
304
- self._embedding_matrix, axis=1, keepdims=True
305
- )
306
- # Avoid division by zero
307
- self._embedding_norms = np.where(
308
- self._embedding_norms == 0, 1e-10, self._embedding_norms
309
- )
312
+ # Pre-compute norms for faster similarity calculation
313
+ self._embedding_norms = np.linalg.norm(
314
+ self._embedding_matrix, axis=1, keepdims=True
315
+ )
316
+ # Avoid division by zero
317
+ self._embedding_norms = np.where(
318
+ self._embedding_norms == 0, EPSILON, self._embedding_norms
319
+ )
310
320
 
311
321
  return True
312
322
 
@@ -1,12 +1,13 @@
1
1
  """Global project registry for CodexLens - SQLite storage."""
2
2
 
3
- from __future__ import annotations
4
-
5
- import sqlite3
6
- import threading
7
- import time
8
- from dataclasses import dataclass
9
- from pathlib import Path
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ import sqlite3
7
+ import threading
8
+ import time
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
10
11
  from typing import Any, Dict, List, Optional
11
12
 
12
13
  from codexlens.errors import StorageError
@@ -39,7 +40,7 @@ class DirMapping:
39
40
  last_updated: float
40
41
 
41
42
 
42
- class RegistryStore:
43
+ class RegistryStore:
43
44
  """Global project registry - SQLite storage.
44
45
 
45
46
  Manages indexed projects and directory-to-index path mappings.
@@ -106,8 +107,8 @@ class RegistryStore:
106
107
  conn = self._get_connection()
107
108
  self._create_schema(conn)
108
109
 
109
- def _create_schema(self, conn: sqlite3.Connection) -> None:
110
- """Create database schema."""
110
+ def _create_schema(self, conn: sqlite3.Connection) -> None:
111
+ """Create database schema."""
111
112
  try:
112
113
  conn.execute(
113
114
  """
@@ -150,12 +151,23 @@ class RegistryStore:
150
151
  )
151
152
 
152
153
  conn.commit()
153
- except sqlite3.DatabaseError as exc:
154
- raise StorageError(f"Failed to initialize registry schema: {exc}") from exc
155
-
156
- # === Project Operations ===
157
-
158
- def register_project(self, source_root: Path, index_root: Path) -> ProjectInfo:
154
+ except sqlite3.DatabaseError as exc:
155
+ raise StorageError(f"Failed to initialize registry schema: {exc}") from exc
156
+
157
+ def _normalize_path_for_comparison(self, path: Path) -> str:
158
+ """Normalize paths for comparisons and storage.
159
+
160
+ Windows paths are treated as case-insensitive, so normalize to lowercase.
161
+ Unix platforms preserve case sensitivity.
162
+ """
163
+ path_str = str(path)
164
+ if platform.system() == "Windows":
165
+ return path_str.lower()
166
+ return path_str
167
+
168
+ # === Project Operations ===
169
+
170
+ def register_project(self, source_root: Path, index_root: Path) -> ProjectInfo:
159
171
  """Register a new project or update existing one.
160
172
 
161
173
  Args:
@@ -164,12 +176,12 @@ class RegistryStore:
164
176
 
165
177
  Returns:
166
178
  ProjectInfo for the registered project
167
- """
168
- with self._lock:
169
- conn = self._get_connection()
170
- source_root_str = str(source_root.resolve())
171
- index_root_str = str(index_root.resolve())
172
- now = time.time()
179
+ """
180
+ with self._lock:
181
+ conn = self._get_connection()
182
+ source_root_str = self._normalize_path_for_comparison(source_root.resolve())
183
+ index_root_str = str(index_root.resolve())
184
+ now = time.time()
173
185
 
174
186
  conn.execute(
175
187
  """
@@ -194,7 +206,7 @@ class RegistryStore:
194
206
 
195
207
  return self._row_to_project_info(row)
196
208
 
197
- def unregister_project(self, source_root: Path) -> bool:
209
+ def unregister_project(self, source_root: Path) -> bool:
198
210
  """Remove a project registration (cascades to directory mappings).
199
211
 
200
212
  Args:
@@ -202,10 +214,10 @@ class RegistryStore:
202
214
 
203
215
  Returns:
204
216
  True if project was removed, False if not found
205
- """
206
- with self._lock:
207
- conn = self._get_connection()
208
- source_root_str = str(source_root.resolve())
217
+ """
218
+ with self._lock:
219
+ conn = self._get_connection()
220
+ source_root_str = self._normalize_path_for_comparison(source_root.resolve())
209
221
 
210
222
  row = conn.execute(
211
223
  "SELECT id FROM projects WHERE source_root=?", (source_root_str,)
@@ -218,7 +230,7 @@ class RegistryStore:
218
230
  conn.commit()
219
231
  return True
220
232
 
221
- def get_project(self, source_root: Path) -> Optional[ProjectInfo]:
233
+ def get_project(self, source_root: Path) -> Optional[ProjectInfo]:
222
234
  """Get project information by source root.
223
235
 
224
236
  Args:
@@ -226,10 +238,10 @@ class RegistryStore:
226
238
 
227
239
  Returns:
228
240
  ProjectInfo if found, None otherwise
229
- """
230
- with self._lock:
231
- conn = self._get_connection()
232
- source_root_str = str(source_root.resolve())
241
+ """
242
+ with self._lock:
243
+ conn = self._get_connection()
244
+ source_root_str = self._normalize_path_for_comparison(source_root.resolve())
233
245
 
234
246
  row = conn.execute(
235
247
  "SELECT * FROM projects WHERE source_root=?", (source_root_str,)
@@ -279,19 +291,19 @@ class RegistryStore:
279
291
 
280
292
  return [self._row_to_project_info(row) for row in rows]
281
293
 
282
- def update_project_stats(
283
- self, source_root: Path, total_files: int, total_dirs: int
284
- ) -> None:
294
+ def update_project_stats(
295
+ self, source_root: Path, total_files: int, total_dirs: int
296
+ ) -> None:
285
297
  """Update project statistics.
286
298
 
287
299
  Args:
288
300
  source_root: Source code root directory
289
301
  total_files: Total number of indexed files
290
302
  total_dirs: Total number of indexed directories
291
- """
292
- with self._lock:
293
- conn = self._get_connection()
294
- source_root_str = str(source_root.resolve())
303
+ """
304
+ with self._lock:
305
+ conn = self._get_connection()
306
+ source_root_str = self._normalize_path_for_comparison(source_root.resolve())
295
307
 
296
308
  conn.execute(
297
309
  """
@@ -303,16 +315,16 @@ class RegistryStore:
303
315
  )
304
316
  conn.commit()
305
317
 
306
- def set_project_status(self, source_root: Path, status: str) -> None:
318
+ def set_project_status(self, source_root: Path, status: str) -> None:
307
319
  """Set project status.
308
320
 
309
321
  Args:
310
322
  source_root: Source code root directory
311
323
  status: Status string ('active', 'stale', 'removed')
312
- """
313
- with self._lock:
314
- conn = self._get_connection()
315
- source_root_str = str(source_root.resolve())
324
+ """
325
+ with self._lock:
326
+ conn = self._get_connection()
327
+ source_root_str = self._normalize_path_for_comparison(source_root.resolve())
316
328
 
317
329
  conn.execute(
318
330
  "UPDATE projects SET status=? WHERE source_root=?",
@@ -322,7 +334,7 @@ class RegistryStore:
322
334
 
323
335
  # === Directory Mapping Operations ===
324
336
 
325
- def register_dir(
337
+ def register_dir(
326
338
  self,
327
339
  project_id: int,
328
340
  source_path: Path,
@@ -341,12 +353,12 @@ class RegistryStore:
341
353
 
342
354
  Returns:
343
355
  DirMapping for the registered directory
344
- """
345
- with self._lock:
346
- conn = self._get_connection()
347
- source_path_str = str(source_path.resolve())
348
- index_path_str = str(index_path.resolve())
349
- now = time.time()
356
+ """
357
+ with self._lock:
358
+ conn = self._get_connection()
359
+ source_path_str = self._normalize_path_for_comparison(source_path.resolve())
360
+ index_path_str = str(index_path.resolve())
361
+ now = time.time()
350
362
 
351
363
  conn.execute(
352
364
  """
@@ -374,7 +386,7 @@ class RegistryStore:
374
386
 
375
387
  return self._row_to_dir_mapping(row)
376
388
 
377
- def unregister_dir(self, source_path: Path) -> bool:
389
+ def unregister_dir(self, source_path: Path) -> bool:
378
390
  """Remove a directory mapping.
379
391
 
380
392
  Args:
@@ -382,10 +394,10 @@ class RegistryStore:
382
394
 
383
395
  Returns:
384
396
  True if directory was removed, False if not found
385
- """
386
- with self._lock:
387
- conn = self._get_connection()
388
- source_path_str = str(source_path.resolve())
397
+ """
398
+ with self._lock:
399
+ conn = self._get_connection()
400
+ source_path_str = self._normalize_path_for_comparison(source_path.resolve())
389
401
 
390
402
  row = conn.execute(
391
403
  "SELECT id FROM dir_mapping WHERE source_path=?", (source_path_str,)
@@ -398,7 +410,7 @@ class RegistryStore:
398
410
  conn.commit()
399
411
  return True
400
412
 
401
- def find_index_path(self, source_path: Path) -> Optional[Path]:
413
+ def find_index_path(self, source_path: Path) -> Optional[Path]:
402
414
  """Find index path for a source directory (exact match).
403
415
 
404
416
  Args:
@@ -406,10 +418,10 @@ class RegistryStore:
406
418
 
407
419
  Returns:
408
420
  Index path if found, None otherwise
409
- """
410
- with self._lock:
411
- conn = self._get_connection()
412
- source_path_str = str(source_path.resolve())
421
+ """
422
+ with self._lock:
423
+ conn = self._get_connection()
424
+ source_path_str = self._normalize_path_for_comparison(source_path.resolve())
413
425
 
414
426
  row = conn.execute(
415
427
  "SELECT index_path FROM dir_mapping WHERE source_path=?",
@@ -418,7 +430,7 @@ class RegistryStore:
418
430
 
419
431
  return Path(row["index_path"]) if row else None
420
432
 
421
- def find_nearest_index(self, source_path: Path) -> Optional[DirMapping]:
433
+ def find_nearest_index(self, source_path: Path) -> Optional[DirMapping]:
422
434
  """Find nearest indexed ancestor directory.
423
435
 
424
436
  Searches for the closest parent directory that has an index.
@@ -437,15 +449,15 @@ class RegistryStore:
437
449
  conn = self._get_connection()
438
450
  source_path_resolved = source_path.resolve()
439
451
 
440
- # Build list of all parent paths from deepest to shallowest
441
- paths_to_check = []
442
- current = source_path_resolved
443
- while True:
444
- paths_to_check.append(str(current))
445
- parent = current.parent
446
- if parent == current: # Reached filesystem root
447
- break
448
- current = parent
452
+ # Build list of all parent paths from deepest to shallowest
453
+ paths_to_check = []
454
+ current = source_path_resolved
455
+ while True:
456
+ paths_to_check.append(self._normalize_path_for_comparison(current))
457
+ parent = current.parent
458
+ if parent == current: # Reached filesystem root
459
+ break
460
+ current = parent
449
461
 
450
462
  if not paths_to_check:
451
463
  return None
@@ -462,7 +474,7 @@ class RegistryStore:
462
474
  row = conn.execute(query, paths_to_check).fetchone()
463
475
  return self._row_to_dir_mapping(row) if row else None
464
476
 
465
- def find_by_source_path(self, source_path: str) -> Optional[Dict[str, str]]:
477
+ def find_by_source_path(self, source_path: str) -> Optional[Dict[str, str]]:
466
478
  """Find project by source path (exact or nearest match).
467
479
 
468
480
  Searches for a project whose source_root matches or contains
@@ -473,15 +485,16 @@ class RegistryStore:
473
485
 
474
486
  Returns:
475
487
  Dict with project info including 'index_root', or None if not found
476
- """
477
- with self._lock:
478
- conn = self._get_connection()
479
- source_path_resolved = str(Path(source_path).resolve())
480
-
481
- # First try exact match on projects table
482
- row = conn.execute(
483
- "SELECT * FROM projects WHERE source_root=?", (source_path_resolved,)
484
- ).fetchone()
488
+ """
489
+ with self._lock:
490
+ conn = self._get_connection()
491
+ resolved_path = Path(source_path).resolve()
492
+ source_path_resolved = self._normalize_path_for_comparison(resolved_path)
493
+
494
+ # First try exact match on projects table
495
+ row = conn.execute(
496
+ "SELECT * FROM projects WHERE source_root=?", (source_path_resolved,)
497
+ ).fetchone()
485
498
 
486
499
  if row:
487
500
  return {
@@ -491,16 +504,16 @@ class RegistryStore:
491
504
  "status": row["status"] or "active",
492
505
  }
493
506
 
494
- # Try finding project that contains this path
495
- # Build list of all parent paths
496
- paths_to_check = []
497
- current = Path(source_path_resolved)
498
- while True:
499
- paths_to_check.append(str(current))
500
- parent = current.parent
501
- if parent == current:
502
- break
503
- current = parent
507
+ # Try finding project that contains this path
508
+ # Build list of all parent paths
509
+ paths_to_check = []
510
+ current = resolved_path
511
+ while True:
512
+ paths_to_check.append(self._normalize_path_for_comparison(current))
513
+ parent = current.parent
514
+ if parent == current:
515
+ break
516
+ current = parent
504
517
 
505
518
  if paths_to_check:
506
519
  placeholders = ','.join('?' * len(paths_to_check))
@@ -541,7 +554,7 @@ class RegistryStore:
541
554
 
542
555
  return [self._row_to_dir_mapping(row) for row in rows]
543
556
 
544
- def get_subdirs(self, source_path: Path) -> List[DirMapping]:
557
+ def get_subdirs(self, source_path: Path) -> List[DirMapping]:
545
558
  """Get direct subdirectory mappings.
546
559
 
547
560
  Args:
@@ -549,10 +562,10 @@ class RegistryStore:
549
562
 
550
563
  Returns:
551
564
  List of DirMapping objects for direct children
552
- """
553
- with self._lock:
554
- conn = self._get_connection()
555
- source_path_str = str(source_path.resolve())
565
+ """
566
+ with self._lock:
567
+ conn = self._get_connection()
568
+ source_path_str = self._normalize_path_for_comparison(source_path.resolve())
556
569
 
557
570
  # First get the parent's depth
558
571
  parent_row = conn.execute(
@@ -578,16 +591,16 @@ class RegistryStore:
578
591
 
579
592
  return [self._row_to_dir_mapping(row) for row in rows]
580
593
 
581
- def update_dir_stats(self, source_path: Path, files_count: int) -> None:
594
+ def update_dir_stats(self, source_path: Path, files_count: int) -> None:
582
595
  """Update directory statistics.
583
596
 
584
597
  Args:
585
598
  source_path: Source directory path
586
599
  files_count: Number of files in directory
587
- """
588
- with self._lock:
589
- conn = self._get_connection()
590
- source_path_str = str(source_path.resolve())
600
+ """
601
+ with self._lock:
602
+ conn = self._get_connection()
603
+ source_path_str = self._normalize_path_for_comparison(source_path.resolve())
591
604
 
592
605
  conn.execute(
593
606
  """