continuum-toolkit 1.0.0__py3-none-any.whl

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 (72) hide show
  1. cli/__init__.py +12 -0
  2. cli/main.py +415 -0
  3. confidence/__init__.py +13 -0
  4. confidence/calculator.py +308 -0
  5. confidence/models.py +40 -0
  6. context/__init__.py +17 -0
  7. context/models.py +108 -0
  8. context/pruner.py +228 -0
  9. context/selector.py +193 -0
  10. continuum_toolkit-1.0.0.dist-info/METADATA +511 -0
  11. continuum_toolkit-1.0.0.dist-info/RECORD +72 -0
  12. continuum_toolkit-1.0.0.dist-info/WHEEL +5 -0
  13. continuum_toolkit-1.0.0.dist-info/entry_points.txt +2 -0
  14. continuum_toolkit-1.0.0.dist-info/licenses/LICENSE +21 -0
  15. continuum_toolkit-1.0.0.dist-info/top_level.txt +13 -0
  16. contradictions/__init__.py +19 -0
  17. contradictions/detector.py +442 -0
  18. contradictions/models.py +75 -0
  19. core/__init__.py +97 -0
  20. core/enums.py +130 -0
  21. core/evidence.py +117 -0
  22. core/interfaces.py +209 -0
  23. core/schema.py +391 -0
  24. core/serializer.py +84 -0
  25. core/state_models.py +530 -0
  26. daemon/__init__.py +12 -0
  27. daemon/service.py +170 -0
  28. extractors/__init__.py +51 -0
  29. extractors/base.py +117 -0
  30. extractors/config/parsers.py +288 -0
  31. extractors/config/secret_sanitizer.py +91 -0
  32. extractors/config_extractor.py +172 -0
  33. extractors/conversation/analyzers.py +193 -0
  34. extractors/conversation/models.py +148 -0
  35. extractors/conversation_extractor.py +166 -0
  36. extractors/git_extractor.py +305 -0
  37. extractors/parsers/base.py +91 -0
  38. extractors/parsers/comment_parser.py +51 -0
  39. extractors/parsers/js_ts_parser.py +171 -0
  40. extractors/parsers/python_parser.py +180 -0
  41. extractors/verification/runners.py +278 -0
  42. extractors/verification_extractor.py +263 -0
  43. extractors/workspace_extractor.py +221 -0
  44. graph/__init__.py +24 -0
  45. graph/diff.py +109 -0
  46. graph/manager.py +473 -0
  47. graph/models.py +62 -0
  48. graph/propagator.py +194 -0
  49. graph/query.py +86 -0
  50. graph/snapshot.py +85 -0
  51. handoff/__init__.py +28 -0
  52. handoff/adapters/__init__.py +45 -0
  53. handoff/adapters/base.py +90 -0
  54. handoff/adapters/claude_adapter.py +176 -0
  55. handoff/adapters/codex_gpt_adapter.py +151 -0
  56. handoff/adapters/gemini_adapter.py +151 -0
  57. handoff/adapters/local_model_adapter.py +130 -0
  58. handoff/models.py +88 -0
  59. handoff/packager.py +288 -0
  60. pipeline/__init__.py +9 -0
  61. pipeline/orchestrator.py +260 -0
  62. resolution/__init__.py +15 -0
  63. resolution/resolver.py +311 -0
  64. storage/__init__.py +18 -0
  65. storage/hooks.py +125 -0
  66. storage/manager.py +127 -0
  67. storage/models.py +39 -0
  68. storage/recovery.py +98 -0
  69. watcher/__init__.py +17 -0
  70. watcher/detector.py +136 -0
  71. watcher/models.py +62 -0
  72. watcher/updater.py +163 -0
storage/recovery.py ADDED
@@ -0,0 +1,98 @@
1
+ """
2
+ Project Continuum - State Corruption Detection & Safe Auto-Recovery
3
+ ===================================================================
4
+ Milestone 7 - Phase 17: Git Hooks & Persistent State Management.
5
+ Validates active state JSON integrity, detects schema or byte-level corruption,
6
+ and safely restores from the most recent valid history snapshot.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Optional, Tuple
11
+
12
+ from core.schema import validate_canonical_state_dict
13
+ from core.serializer import CanonicalStateSerializer, StateSerializationError, StateValidationError
14
+ from core.state_models import CanonicalProjectState
15
+ from storage.manager import ContinuumStorageManager
16
+ from storage.models import RecoveryResult
17
+
18
+
19
+ class StateRecoveryManager:
20
+ """
21
+ Guarantees state recoverability:
22
+ 1. Audits active state for corruption.
23
+ 2. Recovers cleanly from the latest uncorrupted history snapshot if damaged.
24
+ """
25
+
26
+ def __init__(self, storage_manager: ContinuumStorageManager):
27
+ self._storage = storage_manager
28
+
29
+ def verify_active_state_integrity(self) -> Tuple[bool, Optional[str]]:
30
+ """
31
+ Verifies if active state file is valid and compliant with schema.
32
+ Returns (is_valid, error_reason).
33
+ """
34
+ if not self._storage.state_exists():
35
+ return False, "Active state file does not exist."
36
+
37
+ try:
38
+ state = self._storage.load_state(validate=True)
39
+ return True, None
40
+ except (StateSerializationError, StateValidationError, Exception) as e:
41
+ return False, str(e)
42
+
43
+ def recover_corrupted_state(self) -> RecoveryResult:
44
+ """
45
+ Attempts to recover from state corruption by finding the most recent
46
+ valid snapshot in .continuum/history/ and restoring it to state.json.
47
+ """
48
+ is_valid, error_reason = self.verify_active_state_integrity()
49
+ if is_valid:
50
+ return RecoveryResult(
51
+ success=True,
52
+ recovered_from=str(self._storage.active_state_file),
53
+ state_restored=False,
54
+ details={"status": "Active state is healthy; no recovery needed."}
55
+ )
56
+
57
+ # Active state is corrupted: inspect history snapshots
58
+ snapshots = self._storage.list_history_snapshots()
59
+ if not snapshots:
60
+ # Check backup directory
61
+ backup_file = self._storage.backup_dir / "state.json.bak"
62
+ if backup_file.exists():
63
+ try:
64
+ state = CanonicalStateSerializer.load_from_file(backup_file, validate=True)
65
+ self._storage.save_state(state, create_history_snapshot=False)
66
+ return RecoveryResult(
67
+ success=True,
68
+ recovered_from=str(backup_file),
69
+ state_restored=True,
70
+ details={"restored_from": "backup"}
71
+ )
72
+ except Exception:
73
+ pass
74
+
75
+ return RecoveryResult(
76
+ success=False,
77
+ error_message=f"Active state corrupted ({error_reason}), and no valid history snapshots exist."
78
+ )
79
+
80
+ # Search snapshots for the newest valid one
81
+ for snapshot_path in snapshots:
82
+ try:
83
+ state = CanonicalStateSerializer.load_from_file(snapshot_path, validate=True)
84
+ # Found valid snapshot! Restore as active state
85
+ self._storage.save_state(state, create_history_snapshot=False)
86
+ return RecoveryResult(
87
+ success=True,
88
+ recovered_from=str(snapshot_path),
89
+ state_restored=True,
90
+ details={"snapshot_name": snapshot_path.name, "corrupted_reason": error_reason}
91
+ )
92
+ except Exception:
93
+ continue
94
+
95
+ return RecoveryResult(
96
+ success=False,
97
+ error_message=f"All {len(snapshots)} history snapshots were tested and none were valid."
98
+ )
watcher/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """
2
+ Project Continuum - Watcher Package Exports
3
+ ===========================================
4
+ Milestone 7 - Phase 16: Filesystem Observation & Incremental State Updates.
5
+ """
6
+
7
+ from watcher.models import ChangeType, FileChangeEvent, IncrementalUpdateResult
8
+ from watcher.detector import WorkspaceChangeDetector
9
+ from watcher.updater import IncrementalStateUpdater
10
+
11
+ __all__ = [
12
+ "ChangeType",
13
+ "FileChangeEvent",
14
+ "IncrementalUpdateResult",
15
+ "WorkspaceChangeDetector",
16
+ "IncrementalStateUpdater",
17
+ ]
watcher/detector.py ADDED
@@ -0,0 +1,136 @@
1
+ """
2
+ Project Continuum - Workspace Change Detector
3
+ ==============================================
4
+ Milestone 7 - Phase 16: Filesystem Observation & Incremental State Updates.
5
+ Computes filesystem delta sets (creations, modifications, deletions) against
6
+ a cached baseline snapshot with built-in ignore filtering.
7
+ """
8
+
9
+ import hashlib
10
+ import os
11
+ from pathlib import Path
12
+ import time
13
+ from typing import Dict, List, Optional, Set, Tuple
14
+
15
+ from extractors.base import DEFAULT_IGNORE_DIRS
16
+ from watcher.models import ChangeType, FileChangeEvent
17
+
18
+
19
+ class WorkspaceChangeDetector:
20
+ """
21
+ Tracks workspace file timestamps and SHA-256 hashes to detect modifications,
22
+ creations, and deletions with high efficiency.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ workspace_root: str,
28
+ ignore_dirs: Optional[Set[str]] = None,
29
+ max_file_size_mb: float = 10.0
30
+ ):
31
+ self.workspace_root = Path(workspace_root).resolve()
32
+ self.ignore_dirs = ignore_dirs or DEFAULT_IGNORE_DIRS
33
+ self.max_file_size_bytes = int(max_file_size_mb * 1024 * 1024)
34
+ self._baseline_snapshot: Dict[str, Tuple[float, int, str]] = {} # rel_path -> (mtime, size, hash)
35
+
36
+ def compute_current_snapshot(self) -> Dict[str, Tuple[float, int, str]]:
37
+ """
38
+ Walks the workspace and constructs a map of rel_path -> (mtime, size, sha256).
39
+ """
40
+ snapshot: Dict[str, Tuple[float, int, str]] = {}
41
+ if not self.workspace_root.exists():
42
+ return snapshot
43
+
44
+ for root, dirs, files in os.walk(self.workspace_root):
45
+ # Prune ignored directories
46
+ dirs[:] = [d for d in dirs if d not in self.ignore_dirs and not d.startswith(".")]
47
+
48
+ for file in files:
49
+ full_path = Path(root) / file
50
+ try:
51
+ stat = full_path.stat()
52
+ if stat.st_size > self.max_file_size_bytes:
53
+ continue
54
+
55
+ rel_path = str(full_path.relative_to(self.workspace_root)).replace("\\", "/")
56
+ mtime = stat.st_mtime
57
+ size = stat.st_size
58
+
59
+ # Hash only if size or mtime differs from cached snapshot or on first pass
60
+ cached = self._baseline_snapshot.get(rel_path)
61
+ if cached and cached[0] == mtime and cached[1] == size:
62
+ f_hash = cached[2]
63
+ else:
64
+ f_hash = self._hash_file(full_path)
65
+
66
+ snapshot[rel_path] = (mtime, size, f_hash)
67
+ except (OSError, PermissionError):
68
+ continue
69
+
70
+ return snapshot
71
+
72
+ def initialize_baseline(self) -> int:
73
+ """
74
+ Takes initial baseline snapshot of the workspace.
75
+ Returns number of tracked files.
76
+ """
77
+ self._baseline_snapshot = self.compute_current_snapshot()
78
+ return len(self._baseline_snapshot)
79
+
80
+ def detect_changes(self) -> List[FileChangeEvent]:
81
+ """
82
+ Compares current filesystem state against baseline snapshot and returns
83
+ the list of FileChangeEvents, updating the baseline in-place.
84
+ """
85
+ current_snapshot = self.compute_current_snapshot()
86
+ changes: List[FileChangeEvent] = []
87
+
88
+ # 1. Check for creations and modifications
89
+ for rel_path, (mtime, size, f_hash) in current_snapshot.items():
90
+ if rel_path not in self._baseline_snapshot:
91
+ changes.append(
92
+ FileChangeEvent(
93
+ file_path=rel_path,
94
+ change_type=ChangeType.CREATED,
95
+ file_hash=f_hash,
96
+ file_size_bytes=size
97
+ )
98
+ )
99
+ else:
100
+ base_mtime, base_size, base_hash = self._baseline_snapshot[rel_path]
101
+ if f_hash != base_hash:
102
+ changes.append(
103
+ FileChangeEvent(
104
+ file_path=rel_path,
105
+ change_type=ChangeType.MODIFIED,
106
+ file_hash=f_hash,
107
+ file_size_bytes=size
108
+ )
109
+ )
110
+
111
+ # 2. Check for deletions
112
+ for rel_path, (base_mtime, base_size, base_hash) in self._baseline_snapshot.items():
113
+ if rel_path not in current_snapshot:
114
+ changes.append(
115
+ FileChangeEvent(
116
+ file_path=rel_path,
117
+ change_type=ChangeType.DELETED,
118
+ file_hash=base_hash,
119
+ file_size_bytes=base_size
120
+ )
121
+ )
122
+
123
+ # Update baseline to current snapshot
124
+ self._baseline_snapshot = current_snapshot
125
+ return changes
126
+
127
+ def _hash_file(self, path: Path) -> str:
128
+ """Calculates SHA-256 hash of file content."""
129
+ hasher = hashlib.sha256()
130
+ try:
131
+ with open(path, "rb") as f:
132
+ while chunk := f.read(65536):
133
+ hasher.update(chunk)
134
+ return hasher.hexdigest()
135
+ except (OSError, PermissionError):
136
+ return ""
watcher/models.py ADDED
@@ -0,0 +1,62 @@
1
+ """
2
+ Project Continuum - Filesystem Observation & Incremental State Models
3
+ ======================================================================
4
+ Milestone 7 - Phase 16: Filesystem Observation & Incremental State Updates.
5
+ Defines change event structures and incremental update result containers.
6
+ """
7
+
8
+ from dataclasses import dataclass, field, asdict
9
+ from datetime import datetime, timezone
10
+ from enum import Enum
11
+ from typing import Any, Dict, List, Optional, Set
12
+
13
+
14
+ class ChangeType(str, Enum):
15
+ """Types of filesystem change events."""
16
+ CREATED = "CREATED"
17
+ MODIFIED = "MODIFIED"
18
+ DELETED = "DELETED"
19
+
20
+
21
+ @dataclass
22
+ class FileChangeEvent:
23
+ """Represents a discrete filesystem modification event."""
24
+ file_path: str
25
+ change_type: ChangeType
26
+ timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
27
+ file_hash: Optional[str] = None
28
+ file_size_bytes: int = 0
29
+
30
+ def to_dict(self) -> Dict[str, Any]:
31
+ d = asdict(self)
32
+ d["change_type"] = self.change_type.value
33
+ return d
34
+
35
+
36
+ @dataclass
37
+ class IncrementalUpdateResult:
38
+ """
39
+ Summary of an incremental state update cycle.
40
+ """
41
+ workspace_root: str
42
+ changes_processed: List[FileChangeEvent] = field(default_factory=list)
43
+ re_extracted_symbols_count: int = 0
44
+ removed_symbols_count: int = 0
45
+ invalidated_node_ids: List[str] = field(default_factory=list)
46
+ propagated_stale_node_ids: List[str] = field(default_factory=list)
47
+ full_scan_fallback_triggered: bool = False
48
+ duration_ms: float = 0.0
49
+ updated_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
50
+
51
+ def to_dict(self) -> Dict[str, Any]:
52
+ return {
53
+ "workspace_root": self.workspace_root,
54
+ "changes_processed": [c.to_dict() for c in self.changes_processed],
55
+ "re_extracted_symbols_count": self.re_extracted_symbols_count,
56
+ "removed_symbols_count": self.removed_symbols_count,
57
+ "invalidated_node_ids": self.invalidated_node_ids,
58
+ "propagated_stale_node_ids": self.propagated_stale_node_ids,
59
+ "full_scan_fallback_triggered": self.full_scan_fallback_triggered,
60
+ "duration_ms": self.duration_ms,
61
+ "updated_at": self.updated_at
62
+ }
watcher/updater.py ADDED
@@ -0,0 +1,163 @@
1
+ """
2
+ Project Continuum - Incremental State Updater
3
+ ==============================================
4
+ Milestone 7 - Phase 16: Filesystem Observation & Incremental State Updates.
5
+ Applies detected workspace changes to CanonicalProjectState and the Canonical State Graph,
6
+ re-extracting only affected evidence and propagating invalidations.
7
+ """
8
+
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ import time
12
+ from typing import Any, Dict, List, Optional, Set
13
+
14
+ from core.enums import NodeType, RelationType, Status
15
+ from core.state_models import (
16
+ AstSymbol,
17
+ CanonicalProjectState,
18
+ GraphNode,
19
+ ProjectState,
20
+ )
21
+ from extractors.parsers.python_parser import PythonParser
22
+ from extractors.parsers.js_ts_parser import JavaScriptTypeScriptParser
23
+ from extractors.parsers.comment_parser import CommentMarkerParser
24
+ from graph.manager import StateGraphManager
25
+ from watcher.models import ChangeType, FileChangeEvent, IncrementalUpdateResult
26
+
27
+
28
+ class IncrementalStateUpdater:
29
+ """
30
+ Applies fine-grained file changes to a CanonicalProjectState, updating
31
+ symbols, files, and propagating invalidations through the StateGraph.
32
+ """
33
+
34
+ MAX_CHANGES_BEFORE_FALLBACK_FULL_SCAN = 50
35
+
36
+ def __init__(self, workspace_root: str):
37
+ self.workspace_root = Path(workspace_root).resolve()
38
+ self._python_parser = PythonParser()
39
+ self._js_ts_parser = JavaScriptTypeScriptParser()
40
+
41
+ def apply_changes(
42
+ self,
43
+ canonical_state: CanonicalProjectState,
44
+ changes: List[FileChangeEvent],
45
+ graph_manager: Optional[StateGraphManager] = None
46
+ ) -> IncrementalUpdateResult:
47
+ """
48
+ Updates canonical_state in-place based on the list of file change events.
49
+ """
50
+ start_time = time.time()
51
+ p_state = canonical_state.project_state
52
+
53
+ # Fallback to full scan if too many simultaneous modifications occur
54
+ if len(changes) > self.MAX_CHANGES_BEFORE_FALLBACK_FULL_SCAN:
55
+ duration = (time.time() - start_time) * 1000.0
56
+ return IncrementalUpdateResult(
57
+ workspace_root=str(self.workspace_root),
58
+ changes_processed=changes,
59
+ full_scan_fallback_triggered=True,
60
+ duration_ms=duration
61
+ )
62
+
63
+ re_extracted_symbols = 0
64
+ removed_symbols = 0
65
+ invalidated_nodes: Set[str] = set()
66
+ propagated_stale_nodes: Set[str] = set()
67
+
68
+ for change in changes:
69
+ rel_path = change.file_path.replace("\\", "/")
70
+ full_path = self.workspace_root / rel_path
71
+
72
+ if change.change_type == ChangeType.DELETED:
73
+ # 1. Remove file from files list
74
+ if rel_path in p_state.files:
75
+ p_state.files.remove(rel_path)
76
+
77
+ # 2. Remove all symbols belonging to this file
78
+ old_sym_count = len(p_state.symbols)
79
+ p_state.symbols = [s for s in p_state.symbols if s.file_path != rel_path]
80
+ removed_symbols += (old_sym_count - len(p_state.symbols))
81
+
82
+ # 3. Find associated graph node and mark STALE
83
+ node_id = self._find_node_for_file(canonical_state, rel_path)
84
+ if node_id:
85
+ invalidated_nodes.add(node_id)
86
+
87
+ elif change.change_type in (ChangeType.CREATED, ChangeType.MODIFIED):
88
+ # 1. Ensure file is in files list
89
+ if rel_path not in p_state.files:
90
+ p_state.files.append(rel_path)
91
+
92
+ # 2. Remove previous symbols for this file
93
+ old_sym_count = len(p_state.symbols)
94
+ p_state.symbols = [s for s in p_state.symbols if s.file_path != rel_path]
95
+ removed_symbols += (old_sym_count - len(p_state.symbols))
96
+
97
+ # 3. Re-extract symbols from file
98
+ new_symbols = self._extract_file_symbols(full_path, rel_path)
99
+ p_state.symbols.extend(new_symbols)
100
+ re_extracted_symbols += len(new_symbols)
101
+
102
+ # 4. Find associated graph node
103
+ node_id = self._find_node_for_file(canonical_state, rel_path)
104
+ if node_id:
105
+ invalidated_nodes.add(node_id)
106
+
107
+ # Invalidate graph nodes and propagate STALE downstream if graph_manager provided
108
+ if graph_manager:
109
+ for n_id in invalidated_nodes:
110
+ if n_id in canonical_state.graph_nodes:
111
+ node = canonical_state.graph_nodes[n_id]
112
+ node.status = Status.STALE
113
+ affected = graph_manager.propagate_invalidation(n_id)
114
+ propagated_stale_nodes.update(affected)
115
+
116
+ canonical_state.updated_at = datetime.now(timezone.utc).isoformat()
117
+ p_state.last_scanned_at = datetime.now(timezone.utc).isoformat()
118
+
119
+ duration = (time.time() - start_time) * 1000.0
120
+ return IncrementalUpdateResult(
121
+ workspace_root=str(self.workspace_root),
122
+ changes_processed=changes,
123
+ re_extracted_symbols_count=re_extracted_symbols,
124
+ removed_symbols_count=removed_symbols,
125
+ invalidated_node_ids=sorted(list(invalidated_nodes)),
126
+ propagated_stale_node_ids=sorted(list(propagated_stale_nodes)),
127
+ full_scan_fallback_triggered=False,
128
+ duration_ms=duration
129
+ )
130
+
131
+ def _find_node_for_file(self, state: CanonicalProjectState, rel_path: str) -> Optional[str]:
132
+ """Finds a graph node that directly references the given file."""
133
+ file_stem = Path(rel_path).stem.lower()
134
+ for node_id, node in state.graph_nodes.items():
135
+ if node_id.lower() == file_stem or node.name.lower() == file_stem:
136
+ return node_id
137
+ if node.metadata.get("file_path") == rel_path:
138
+ return node_id
139
+ return None
140
+
141
+ def _extract_file_symbols(self, full_path: Path, rel_path: str) -> List[AstSymbol]:
142
+ """Extracts AST symbols for a single file."""
143
+ if not full_path.exists():
144
+ return []
145
+
146
+ ext = full_path.suffix.lower()
147
+ try:
148
+ content = full_path.read_text(encoding="utf-8", errors="replace")
149
+ except Exception:
150
+ return []
151
+
152
+ symbols: List[AstSymbol] = []
153
+ if ext == ".py":
154
+ res = self._python_parser.parse_source(str(full_path), content)
155
+ symbols.extend(res.symbols)
156
+ elif ext in (".js", ".ts", ".jsx", ".tsx"):
157
+ res = self._js_ts_parser.parse_source(str(full_path), content)
158
+ symbols.extend(res.symbols)
159
+
160
+ # Standardize relative paths
161
+ for s in symbols:
162
+ s.file_path = rel_path
163
+ return symbols