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.
- cli/__init__.py +12 -0
- cli/main.py +415 -0
- confidence/__init__.py +13 -0
- confidence/calculator.py +308 -0
- confidence/models.py +40 -0
- context/__init__.py +17 -0
- context/models.py +108 -0
- context/pruner.py +228 -0
- context/selector.py +193 -0
- continuum_toolkit-1.0.0.dist-info/METADATA +511 -0
- continuum_toolkit-1.0.0.dist-info/RECORD +72 -0
- continuum_toolkit-1.0.0.dist-info/WHEEL +5 -0
- continuum_toolkit-1.0.0.dist-info/entry_points.txt +2 -0
- continuum_toolkit-1.0.0.dist-info/licenses/LICENSE +21 -0
- continuum_toolkit-1.0.0.dist-info/top_level.txt +13 -0
- contradictions/__init__.py +19 -0
- contradictions/detector.py +442 -0
- contradictions/models.py +75 -0
- core/__init__.py +97 -0
- core/enums.py +130 -0
- core/evidence.py +117 -0
- core/interfaces.py +209 -0
- core/schema.py +391 -0
- core/serializer.py +84 -0
- core/state_models.py +530 -0
- daemon/__init__.py +12 -0
- daemon/service.py +170 -0
- extractors/__init__.py +51 -0
- extractors/base.py +117 -0
- extractors/config/parsers.py +288 -0
- extractors/config/secret_sanitizer.py +91 -0
- extractors/config_extractor.py +172 -0
- extractors/conversation/analyzers.py +193 -0
- extractors/conversation/models.py +148 -0
- extractors/conversation_extractor.py +166 -0
- extractors/git_extractor.py +305 -0
- extractors/parsers/base.py +91 -0
- extractors/parsers/comment_parser.py +51 -0
- extractors/parsers/js_ts_parser.py +171 -0
- extractors/parsers/python_parser.py +180 -0
- extractors/verification/runners.py +278 -0
- extractors/verification_extractor.py +263 -0
- extractors/workspace_extractor.py +221 -0
- graph/__init__.py +24 -0
- graph/diff.py +109 -0
- graph/manager.py +473 -0
- graph/models.py +62 -0
- graph/propagator.py +194 -0
- graph/query.py +86 -0
- graph/snapshot.py +85 -0
- handoff/__init__.py +28 -0
- handoff/adapters/__init__.py +45 -0
- handoff/adapters/base.py +90 -0
- handoff/adapters/claude_adapter.py +176 -0
- handoff/adapters/codex_gpt_adapter.py +151 -0
- handoff/adapters/gemini_adapter.py +151 -0
- handoff/adapters/local_model_adapter.py +130 -0
- handoff/models.py +88 -0
- handoff/packager.py +288 -0
- pipeline/__init__.py +9 -0
- pipeline/orchestrator.py +260 -0
- resolution/__init__.py +15 -0
- resolution/resolver.py +311 -0
- storage/__init__.py +18 -0
- storage/hooks.py +125 -0
- storage/manager.py +127 -0
- storage/models.py +39 -0
- storage/recovery.py +98 -0
- watcher/__init__.py +17 -0
- watcher/detector.py +136 -0
- watcher/models.py +62 -0
- watcher/updater.py +163 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Continuum - Test, Build & Verification Evidence Extractor
|
|
3
|
+
=================================================================
|
|
4
|
+
Milestone 2 - Phase 4: Test, Build & Verification Evidence Collection.
|
|
5
|
+
Discovers available test frameworks, explicitly executes configured verification
|
|
6
|
+
commands, records exit codes, durations, parsed outcomes, and registers
|
|
7
|
+
highest-priority Level 1 canonical Evidence.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
12
|
+
|
|
13
|
+
from core.enums import EvidenceType, EvidenceLevel, Status
|
|
14
|
+
from core.evidence import Evidence
|
|
15
|
+
from core.state_models import (
|
|
16
|
+
CanonicalProjectState,
|
|
17
|
+
ManifestInfo,
|
|
18
|
+
ProjectState,
|
|
19
|
+
TestResult,
|
|
20
|
+
)
|
|
21
|
+
from extractors.base import BaseEvidenceExtractor
|
|
22
|
+
from extractors.verification.runners import (
|
|
23
|
+
ExecutionResult,
|
|
24
|
+
ParsedTestSummary,
|
|
25
|
+
TestOutputParser,
|
|
26
|
+
VerificationRunner,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class VerificationEvidenceExtractor(BaseEvidenceExtractor):
|
|
31
|
+
"""
|
|
32
|
+
Extractor responsible for Phase 4: Verification, Test, and Build Evidence.
|
|
33
|
+
Adheres strictly to the rule: Never run scripts automatically; only discover
|
|
34
|
+
configurations or run when explicitly requested.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def extractor_name(self) -> str:
|
|
39
|
+
return "VerificationEvidenceExtractor"
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def supported_evidence_types(self) -> List[EvidenceType]:
|
|
43
|
+
return [
|
|
44
|
+
EvidenceType.TEST_RUN,
|
|
45
|
+
EvidenceType.BUILD_LOG,
|
|
46
|
+
EvidenceType.RUNTIME_LOG,
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
def can_extract(self, target_path_or_input: str) -> bool:
|
|
50
|
+
path = Path(target_path_or_input)
|
|
51
|
+
return path.exists()
|
|
52
|
+
|
|
53
|
+
def discover_verification_configs(self, root_path: Path, manifests: Optional[List[ManifestInfo]] = None) -> List[Dict[str, Any]]:
|
|
54
|
+
"""
|
|
55
|
+
Scans project directory and manifests for available test and build commands
|
|
56
|
+
WITHOUT automatically executing any of them.
|
|
57
|
+
"""
|
|
58
|
+
discovered: List[Dict[str, Any]] = []
|
|
59
|
+
|
|
60
|
+
# 1. Check Python tools
|
|
61
|
+
if (root_path / "pyproject.toml").exists() or (root_path / "pytest.ini").exists() or (root_path / "tests").exists():
|
|
62
|
+
discovered.append({
|
|
63
|
+
"framework": "pytest",
|
|
64
|
+
"type": "test",
|
|
65
|
+
"recommended_command": "pytest",
|
|
66
|
+
"config_file": "pytest.ini" if (root_path / "pytest.ini").exists() else "pyproject.toml"
|
|
67
|
+
})
|
|
68
|
+
discovered.append({
|
|
69
|
+
"framework": "unittest",
|
|
70
|
+
"type": "test",
|
|
71
|
+
"recommended_command": "py -m unittest discover",
|
|
72
|
+
"config_file": None
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# 2. Check JavaScript / TypeScript (package.json scripts)
|
|
76
|
+
pkg_json = root_path / "package.json"
|
|
77
|
+
if pkg_json.exists():
|
|
78
|
+
try:
|
|
79
|
+
import json
|
|
80
|
+
data = json.loads(pkg_json.read_text(encoding="utf-8"))
|
|
81
|
+
scripts = data.get("scripts", {})
|
|
82
|
+
if "test" in scripts:
|
|
83
|
+
discovered.append({
|
|
84
|
+
"framework": "npm_test",
|
|
85
|
+
"type": "test",
|
|
86
|
+
"recommended_command": "npm test",
|
|
87
|
+
"script_body": scripts["test"]
|
|
88
|
+
})
|
|
89
|
+
if "build" in scripts:
|
|
90
|
+
discovered.append({
|
|
91
|
+
"framework": "npm_build",
|
|
92
|
+
"type": "build",
|
|
93
|
+
"recommended_command": "npm run build",
|
|
94
|
+
"script_body": scripts["build"]
|
|
95
|
+
})
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# 3. Check Rust
|
|
100
|
+
if (root_path / "Cargo.toml").exists():
|
|
101
|
+
discovered.append({
|
|
102
|
+
"framework": "cargo_test",
|
|
103
|
+
"type": "test",
|
|
104
|
+
"recommended_command": "cargo test",
|
|
105
|
+
"config_file": "Cargo.toml"
|
|
106
|
+
})
|
|
107
|
+
discovered.append({
|
|
108
|
+
"framework": "cargo_build",
|
|
109
|
+
"type": "build",
|
|
110
|
+
"recommended_command": "cargo build",
|
|
111
|
+
"config_file": "Cargo.toml"
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
# 4. Check Go
|
|
115
|
+
if (root_path / "go.mod").exists():
|
|
116
|
+
discovered.append({
|
|
117
|
+
"framework": "go_test",
|
|
118
|
+
"type": "test",
|
|
119
|
+
"recommended_command": "go test ./...",
|
|
120
|
+
"config_file": "go.mod"
|
|
121
|
+
})
|
|
122
|
+
discovered.append({
|
|
123
|
+
"framework": "go_build",
|
|
124
|
+
"type": "build",
|
|
125
|
+
"recommended_command": "go build ./...",
|
|
126
|
+
"config_file": "go.mod"
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return discovered
|
|
130
|
+
|
|
131
|
+
def run_verification(
|
|
132
|
+
self,
|
|
133
|
+
root_path: Path | str,
|
|
134
|
+
command: str | List[str],
|
|
135
|
+
suite_name: str = "default_suite",
|
|
136
|
+
is_build: bool = False,
|
|
137
|
+
timeout_seconds: int = 60
|
|
138
|
+
) -> Tuple[ExecutionResult, ParsedTestSummary, Evidence]:
|
|
139
|
+
"""
|
|
140
|
+
Explicitly executes a verification command and produces Level 1 canonical Evidence.
|
|
141
|
+
"""
|
|
142
|
+
root = Path(root_path).resolve()
|
|
143
|
+
exec_result = VerificationRunner.run_command(command, cwd=root, timeout_seconds=timeout_seconds)
|
|
144
|
+
|
|
145
|
+
parsed_summary = TestOutputParser.parse(
|
|
146
|
+
stdout=exec_result.stdout,
|
|
147
|
+
stderr=exec_result.stderr,
|
|
148
|
+
exit_code=exec_result.exit_code,
|
|
149
|
+
duration_ms=exec_result.duration_ms
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
status = Status.VERIFIED if exec_result.is_success else Status.FAILED
|
|
153
|
+
evidence_type = EvidenceType.BUILD_LOG if is_build else EvidenceType.TEST_RUN
|
|
154
|
+
|
|
155
|
+
summary_text = (
|
|
156
|
+
f"{'Build' if is_build else 'Test'} suite '{suite_name}' {status.value} (exit code {exec_result.exit_code}) "
|
|
157
|
+
f"in {exec_result.duration_ms:.2f}ms. "
|
|
158
|
+
f"Results: {parsed_summary.passed} passed, {parsed_summary.failed} failed, {parsed_summary.errors} errors."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
raw_payload = {
|
|
162
|
+
"suite_name": suite_name,
|
|
163
|
+
"is_build": is_build,
|
|
164
|
+
"command": exec_result.command,
|
|
165
|
+
"status": status.value,
|
|
166
|
+
"exit_code": exec_result.exit_code,
|
|
167
|
+
"duration_ms": exec_result.duration_ms,
|
|
168
|
+
"timed_out": exec_result.timed_out,
|
|
169
|
+
"error": exec_result.error,
|
|
170
|
+
"summary": {
|
|
171
|
+
"framework": parsed_summary.framework,
|
|
172
|
+
"total": parsed_summary.total,
|
|
173
|
+
"passed": parsed_summary.passed,
|
|
174
|
+
"failed": parsed_summary.failed,
|
|
175
|
+
"errors": parsed_summary.errors,
|
|
176
|
+
"skipped": parsed_summary.skipped,
|
|
177
|
+
},
|
|
178
|
+
"failure_details": parsed_summary.failure_details,
|
|
179
|
+
"stdout_snippet": exec_result.stdout[-1500:] if exec_result.stdout else "",
|
|
180
|
+
"stderr_snippet": exec_result.stderr[-1500:] if exec_result.stderr else ""
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
evidence = self.create_evidence(
|
|
184
|
+
evidence_type=evidence_type,
|
|
185
|
+
summary=summary_text,
|
|
186
|
+
raw_payload=raw_payload,
|
|
187
|
+
source_uri=str(root),
|
|
188
|
+
locator=f"{'build' if is_build else 'test'}:{suite_name}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return exec_result, parsed_summary, evidence
|
|
192
|
+
|
|
193
|
+
def extract(self, target_path_or_input: str, context: Optional[Dict[str, Any]] = None) -> List[Evidence]:
|
|
194
|
+
"""
|
|
195
|
+
Standard extract method: If context contains 'explicit_commands', executes them.
|
|
196
|
+
Otherwise, extracts discovery metadata only.
|
|
197
|
+
"""
|
|
198
|
+
root_path = Path(target_path_or_input).resolve()
|
|
199
|
+
evidence_list: List[Evidence] = []
|
|
200
|
+
|
|
201
|
+
if context and "commands" in context:
|
|
202
|
+
for cmd_info in context["commands"]:
|
|
203
|
+
cmd = cmd_info.get("command")
|
|
204
|
+
suite = cmd_info.get("suite", "verification")
|
|
205
|
+
is_build = cmd_info.get("is_build", False)
|
|
206
|
+
if cmd:
|
|
207
|
+
_, _, ev = self.run_verification(root_path, cmd, suite_name=suite, is_build=is_build)
|
|
208
|
+
evidence_list.append(ev)
|
|
209
|
+
else:
|
|
210
|
+
# Only record discovery of available configurations
|
|
211
|
+
configs = self.discover_verification_configs(root_path)
|
|
212
|
+
evidence_list.append(self.create_evidence(
|
|
213
|
+
evidence_type=EvidenceType.TEST_RUN,
|
|
214
|
+
summary=f"Discovered {len(configs)} available verification and build configurations (auto-execution disabled)",
|
|
215
|
+
raw_payload={"discovered_configs": configs},
|
|
216
|
+
source_uri=str(root_path),
|
|
217
|
+
locator="verification:discovery"
|
|
218
|
+
))
|
|
219
|
+
|
|
220
|
+
return evidence_list
|
|
221
|
+
|
|
222
|
+
def populate_project_state(
|
|
223
|
+
self,
|
|
224
|
+
root_dir: str,
|
|
225
|
+
canonical_state: CanonicalProjectState,
|
|
226
|
+
evidence_list: Optional[List[Evidence]] = None
|
|
227
|
+
) -> ProjectState:
|
|
228
|
+
"""
|
|
229
|
+
Registers verification evidence and updates ProjectState.test_results and build_status.
|
|
230
|
+
"""
|
|
231
|
+
root_path = Path(root_dir).resolve()
|
|
232
|
+
ev_list = evidence_list or self.extract(str(root_path))
|
|
233
|
+
|
|
234
|
+
test_results: List[TestResult] = []
|
|
235
|
+
build_status = canonical_state.project_state.build_status
|
|
236
|
+
|
|
237
|
+
for ev in ev_list:
|
|
238
|
+
canonical_state.add_evidence(ev)
|
|
239
|
+
|
|
240
|
+
if ev.type == EvidenceType.TEST_RUN and "exit_code" in ev.raw_payload:
|
|
241
|
+
suite = ev.raw_payload.get("suite_name", "test_suite")
|
|
242
|
+
status = Status(ev.raw_payload.get("status", Status.FAILED.value))
|
|
243
|
+
tr = TestResult(
|
|
244
|
+
test_id=f"tr_{ev.id}",
|
|
245
|
+
name=suite,
|
|
246
|
+
suite=suite,
|
|
247
|
+
status=status,
|
|
248
|
+
exit_code=ev.raw_payload.get("exit_code", 1),
|
|
249
|
+
duration_ms=ev.raw_payload.get("duration_ms", 0.0),
|
|
250
|
+
output_snippet=ev.raw_payload.get("stdout_snippet", ""),
|
|
251
|
+
error_message=ev.raw_payload.get("error") or "\n".join(ev.raw_payload.get("failure_details", [])),
|
|
252
|
+
evidence_id=ev.id
|
|
253
|
+
)
|
|
254
|
+
test_results.append(tr)
|
|
255
|
+
elif ev.type == EvidenceType.BUILD_LOG and "exit_code" in ev.raw_payload:
|
|
256
|
+
build_status = Status(ev.raw_payload.get("status", Status.FAILED.value))
|
|
257
|
+
|
|
258
|
+
ps = canonical_state.project_state
|
|
259
|
+
ps.test_results.extend(test_results)
|
|
260
|
+
ps.build_status = build_status
|
|
261
|
+
ps.evidence_ids.extend([ev.id for ev in ev_list])
|
|
262
|
+
|
|
263
|
+
return ps
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Continuum - Workspace & Source Code Evidence Extractor
|
|
3
|
+
==============================================================
|
|
4
|
+
Milestone 2 - Phase 1: Physical Workspace & Source Code Inspection.
|
|
5
|
+
Scans the project directory, classifies files, detects programming languages,
|
|
6
|
+
runs language parsers to extract AST symbols and TODO markers, handles malformed
|
|
7
|
+
files gracefully, and produces canonical Evidence records.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional, Set
|
|
13
|
+
|
|
14
|
+
from core.enums import EvidenceType, Status
|
|
15
|
+
from core.evidence import Evidence
|
|
16
|
+
from core.state_models import (
|
|
17
|
+
AstSymbol,
|
|
18
|
+
CanonicalProjectState,
|
|
19
|
+
ProjectState,
|
|
20
|
+
)
|
|
21
|
+
from extractors.base import (
|
|
22
|
+
CONFIG_FILE_NAMES,
|
|
23
|
+
DEFAULT_IGNORE_DIRS,
|
|
24
|
+
DOC_EXTENSIONS,
|
|
25
|
+
BaseEvidenceExtractor,
|
|
26
|
+
)
|
|
27
|
+
from extractors.parsers.base import ILanguageParser, ParserResult
|
|
28
|
+
from extractors.parsers.js_ts_parser import JavaScriptTypeScriptParser
|
|
29
|
+
from extractors.parsers.python_parser import PythonParser
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WorkspaceEvidenceExtractor(BaseEvidenceExtractor):
|
|
33
|
+
"""
|
|
34
|
+
Primary extractor for Phase 1.
|
|
35
|
+
Performs physical filesystem scanning, language detection, and symbol parsing.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, custom_parsers: Optional[List[ILanguageParser]] = None):
|
|
39
|
+
# Register default language parsers
|
|
40
|
+
self.parsers: Dict[str, ILanguageParser] = {}
|
|
41
|
+
default_parsers = [PythonParser(), JavaScriptTypeScriptParser()]
|
|
42
|
+
if custom_parsers:
|
|
43
|
+
default_parsers.extend(custom_parsers)
|
|
44
|
+
|
|
45
|
+
for parser in default_parsers:
|
|
46
|
+
for ext in parser.supported_extensions:
|
|
47
|
+
self.parsers[ext.lower()] = parser
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def extractor_name(self) -> str:
|
|
51
|
+
return "WorkspaceEvidenceExtractor"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def supported_evidence_types(self) -> List[EvidenceType]:
|
|
55
|
+
return [
|
|
56
|
+
EvidenceType.SOURCE_CODE,
|
|
57
|
+
EvidenceType.AST_SYMBOL,
|
|
58
|
+
EvidenceType.DOCUMENTATION,
|
|
59
|
+
EvidenceType.CONFIG_FILE
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
def can_extract(self, target_path_or_input: str) -> bool:
|
|
63
|
+
path = Path(target_path_or_input)
|
|
64
|
+
return path.exists() and path.is_dir()
|
|
65
|
+
|
|
66
|
+
def extract(self, target_path_or_input: str, context: Optional[Dict[str, Any]] = None) -> List[Evidence]:
|
|
67
|
+
"""
|
|
68
|
+
Scans workspace directory, parses files, and returns a list of canonical Evidence records.
|
|
69
|
+
"""
|
|
70
|
+
root_path = Path(target_path_or_input).resolve()
|
|
71
|
+
evidence_list: List[Evidence] = []
|
|
72
|
+
|
|
73
|
+
scanned_files, lang_counts = self._scan_directory(root_path)
|
|
74
|
+
|
|
75
|
+
# 1. Workspace Structure Evidence
|
|
76
|
+
structure_payload = {
|
|
77
|
+
"root_path": str(root_path),
|
|
78
|
+
"total_files": len(scanned_files),
|
|
79
|
+
"languages": dict(lang_counts),
|
|
80
|
+
"file_list": [str(p.relative_to(root_path).as_posix()) for p in scanned_files]
|
|
81
|
+
}
|
|
82
|
+
evidence_list.append(self.create_evidence(
|
|
83
|
+
evidence_type=EvidenceType.SOURCE_CODE,
|
|
84
|
+
summary=f"Workspace structure scanned: {len(scanned_files)} files across {len(lang_counts)} languages",
|
|
85
|
+
raw_payload=structure_payload,
|
|
86
|
+
source_uri=str(root_path),
|
|
87
|
+
locator="workspace:root"
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
# 2. Parse Source Code Files & Generate Symbol Evidence
|
|
91
|
+
for file_path in scanned_files:
|
|
92
|
+
ext = file_path.suffix.lower()
|
|
93
|
+
rel_path = file_path.relative_to(root_path).as_posix()
|
|
94
|
+
|
|
95
|
+
if ext in self.parsers:
|
|
96
|
+
parser = self.parsers[ext]
|
|
97
|
+
try:
|
|
98
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
parse_result = parser.parse_source(rel_path, content)
|
|
103
|
+
|
|
104
|
+
# Record AST Symbol evidence
|
|
105
|
+
if parse_result.symbols:
|
|
106
|
+
evidence_list.append(self.create_evidence(
|
|
107
|
+
evidence_type=EvidenceType.AST_SYMBOL,
|
|
108
|
+
summary=f"Extracted {len(parse_result.symbols)} symbols from {rel_path}",
|
|
109
|
+
raw_payload={
|
|
110
|
+
"file_path": rel_path,
|
|
111
|
+
"language": parse_result.language,
|
|
112
|
+
"symbols": [s.to_dict() for s in parse_result.symbols],
|
|
113
|
+
"imports": [i.to_dict() for i in parse_result.imports],
|
|
114
|
+
"exported_symbols": parse_result.exported_symbols,
|
|
115
|
+
"markers": [m.to_dict() for m in parse_result.markers],
|
|
116
|
+
"errors": [e.to_dict() for e in parse_result.errors]
|
|
117
|
+
},
|
|
118
|
+
source_uri=rel_path,
|
|
119
|
+
locator=f"file:{rel_path}"
|
|
120
|
+
))
|
|
121
|
+
# Record Documentation Evidence
|
|
122
|
+
elif ext in DOC_EXTENSIONS:
|
|
123
|
+
try:
|
|
124
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
125
|
+
evidence_list.append(self.create_evidence(
|
|
126
|
+
evidence_type=EvidenceType.DOCUMENTATION,
|
|
127
|
+
summary=f"Documentation file {rel_path} ({len(content.splitlines())} lines)",
|
|
128
|
+
raw_payload={
|
|
129
|
+
"file_path": rel_path,
|
|
130
|
+
"content": content
|
|
131
|
+
},
|
|
132
|
+
source_uri=rel_path,
|
|
133
|
+
locator=f"doc:{rel_path}"
|
|
134
|
+
))
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
return evidence_list
|
|
139
|
+
|
|
140
|
+
def populate_project_state(self, root_dir: str, canonical_state: CanonicalProjectState) -> ProjectState:
|
|
141
|
+
"""
|
|
142
|
+
High-level convenience method: scans workspace, updates `canonical_state.project_state`,
|
|
143
|
+
and registers all harvested evidence into `canonical_state.evidence_pool`.
|
|
144
|
+
"""
|
|
145
|
+
root_path = Path(root_dir).resolve()
|
|
146
|
+
evidence_list = self.extract(str(root_path))
|
|
147
|
+
|
|
148
|
+
all_symbols: List[AstSymbol] = []
|
|
149
|
+
all_markers: List[Dict[str, Any]] = []
|
|
150
|
+
scanned_files: List[str] = []
|
|
151
|
+
lang_counter = Counter()
|
|
152
|
+
|
|
153
|
+
for ev in evidence_list:
|
|
154
|
+
canonical_state.add_evidence(ev)
|
|
155
|
+
|
|
156
|
+
if ev.type == EvidenceType.SOURCE_CODE:
|
|
157
|
+
scanned_files = ev.raw_payload.get("file_list", [])
|
|
158
|
+
for lang, count in ev.raw_payload.get("languages", {}).items():
|
|
159
|
+
lang_counter[lang] += count
|
|
160
|
+
elif ev.type == EvidenceType.AST_SYMBOL:
|
|
161
|
+
for s_dict in ev.raw_payload.get("symbols", []):
|
|
162
|
+
sym = AstSymbol.from_dict(s_dict)
|
|
163
|
+
sym.evidence_id = ev.id
|
|
164
|
+
all_symbols.append(sym)
|
|
165
|
+
all_markers.extend(ev.raw_payload.get("markers", []))
|
|
166
|
+
|
|
167
|
+
# Update ProjectState
|
|
168
|
+
ps = canonical_state.project_state
|
|
169
|
+
ps.root_path = str(root_path)
|
|
170
|
+
ps.files = scanned_files
|
|
171
|
+
ps.detected_languages = [lang for lang, _ in lang_counter.most_common()]
|
|
172
|
+
ps.symbols = all_symbols
|
|
173
|
+
ps.todo_markers = all_markers
|
|
174
|
+
ps.evidence_ids = [ev.id for ev in evidence_list]
|
|
175
|
+
|
|
176
|
+
return ps
|
|
177
|
+
|
|
178
|
+
def _scan_directory(self, root_path: Path) -> tuple[List[Path], Counter]:
|
|
179
|
+
"""Recursively traverses directory ignoring standard irrelevant folders."""
|
|
180
|
+
scanned_files: List[Path] = []
|
|
181
|
+
lang_counter = Counter()
|
|
182
|
+
|
|
183
|
+
for path in sorted(root_path.rglob("*")):
|
|
184
|
+
if path.is_file():
|
|
185
|
+
# Check if any parent part is in ignored list
|
|
186
|
+
parts = set(path.relative_to(root_path).parts)
|
|
187
|
+
if any(ignored in parts for ignored in DEFAULT_IGNORE_DIRS):
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
scanned_files.append(path)
|
|
191
|
+
ext = path.suffix.lower()
|
|
192
|
+
lang = self._classify_extension(ext, path.name)
|
|
193
|
+
if lang:
|
|
194
|
+
lang_counter[lang] += 1
|
|
195
|
+
|
|
196
|
+
return scanned_files, lang_counter
|
|
197
|
+
|
|
198
|
+
def _classify_extension(self, ext: str, filename: str) -> Optional[str]:
|
|
199
|
+
if filename in CONFIG_FILE_NAMES:
|
|
200
|
+
return "Configuration"
|
|
201
|
+
if ext in {".py", ".pyw"}:
|
|
202
|
+
return "Python"
|
|
203
|
+
if ext in {".js", ".jsx", ".mjs", ".cjs"}:
|
|
204
|
+
return "JavaScript"
|
|
205
|
+
if ext in {".ts", ".tsx"}:
|
|
206
|
+
return "TypeScript"
|
|
207
|
+
if ext in {".go"}:
|
|
208
|
+
return "Go"
|
|
209
|
+
if ext in {".rs"}:
|
|
210
|
+
return "Rust"
|
|
211
|
+
if ext in {".java"}:
|
|
212
|
+
return "Java"
|
|
213
|
+
if ext in {".c", ".h"}:
|
|
214
|
+
return "C"
|
|
215
|
+
if ext in {".cpp", ".hpp", ".cc"}:
|
|
216
|
+
return "C++"
|
|
217
|
+
if ext in {".dart"}:
|
|
218
|
+
return "Dart"
|
|
219
|
+
if ext in DOC_EXTENSIONS:
|
|
220
|
+
return "Documentation"
|
|
221
|
+
return None
|
graph/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Continuum - State Graph Subsystem Package
|
|
3
|
+
=================================================
|
|
4
|
+
Milestone 4 - Phases 9, 10 & 11.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from graph.models import GraphStats, GraphValidationResult, PropagationResult
|
|
8
|
+
from graph.snapshot import GraphSnapshot
|
|
9
|
+
from graph.diff import GraphDiff, NodeDelta
|
|
10
|
+
from graph.query import GraphQueryEngine
|
|
11
|
+
from graph.propagator import DependencyPropagator
|
|
12
|
+
from graph.manager import StateGraphManager
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"GraphStats",
|
|
16
|
+
"GraphValidationResult",
|
|
17
|
+
"PropagationResult",
|
|
18
|
+
"GraphSnapshot",
|
|
19
|
+
"GraphDiff",
|
|
20
|
+
"NodeDelta",
|
|
21
|
+
"GraphQueryEngine",
|
|
22
|
+
"DependencyPropagator",
|
|
23
|
+
"StateGraphManager",
|
|
24
|
+
]
|
graph/diff.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Continuum - Graph Comparison & Semantic Diff Engine
|
|
3
|
+
===========================================================
|
|
4
|
+
Milestone 4 - Phase 11: Graph Persistence, Querying & Visualization.
|
|
5
|
+
Compares two graph snapshots or StateGraphManager instances to compute fine-grained diffs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field, asdict
|
|
9
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
10
|
+
|
|
11
|
+
from graph.snapshot import GraphSnapshot
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class NodeDelta:
|
|
16
|
+
"""Detailed attribute changes for a modified graph node."""
|
|
17
|
+
old_status: Optional[str] = None
|
|
18
|
+
new_status: Optional[str] = None
|
|
19
|
+
old_confidence: Optional[float] = None
|
|
20
|
+
new_confidence: Optional[float] = None
|
|
21
|
+
old_evidence_count: int = 0
|
|
22
|
+
new_evidence_count: int = 0
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
25
|
+
return asdict(self)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class GraphDiff:
|
|
30
|
+
"""
|
|
31
|
+
Semantic difference between two graph snapshots (Baseline -> Target).
|
|
32
|
+
"""
|
|
33
|
+
baseline_id: str
|
|
34
|
+
target_id: str
|
|
35
|
+
added_nodes: List[str] = field(default_factory=list)
|
|
36
|
+
removed_nodes: List[str] = field(default_factory=list)
|
|
37
|
+
modified_nodes: Dict[str, NodeDelta] = field(default_factory=dict)
|
|
38
|
+
added_edges: List[Dict[str, str]] = field(default_factory=list)
|
|
39
|
+
removed_edges: List[Dict[str, str]] = field(default_factory=list)
|
|
40
|
+
has_changes: bool = False
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
43
|
+
return {
|
|
44
|
+
"baseline_id": self.baseline_id,
|
|
45
|
+
"target_id": self.target_id,
|
|
46
|
+
"has_changes": self.has_changes,
|
|
47
|
+
"added_nodes": self.added_nodes,
|
|
48
|
+
"removed_nodes": self.removed_nodes,
|
|
49
|
+
"modified_nodes": {k: v.to_dict() for k, v in self.modified_nodes.items()},
|
|
50
|
+
"added_edges": self.added_edges,
|
|
51
|
+
"removed_edges": self.removed_edges,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def compare(cls, snap_a: GraphSnapshot, snap_b: GraphSnapshot) -> "GraphDiff":
|
|
56
|
+
"""
|
|
57
|
+
Computes the semantic delta from snap_a to snap_b.
|
|
58
|
+
"""
|
|
59
|
+
nodes_a = {n["id"]: n for n in snap_a.nodes}
|
|
60
|
+
nodes_b = {n["id"]: n for n in snap_b.nodes}
|
|
61
|
+
|
|
62
|
+
ids_a = set(nodes_a.keys())
|
|
63
|
+
ids_b = set(nodes_b.keys())
|
|
64
|
+
|
|
65
|
+
added_node_ids = sorted(list(ids_b - ids_a))
|
|
66
|
+
removed_node_ids = sorted(list(ids_a - ids_b))
|
|
67
|
+
common_node_ids = ids_a & ids_b
|
|
68
|
+
|
|
69
|
+
modified_nodes: Dict[str, NodeDelta] = {}
|
|
70
|
+
for nid in sorted(list(common_node_ids)):
|
|
71
|
+
na = nodes_a[nid]
|
|
72
|
+
nb = nodes_b[nid]
|
|
73
|
+
|
|
74
|
+
stat_changed = na.get("status") != nb.get("status")
|
|
75
|
+
conf_changed = na.get("confidence_score") != nb.get("confidence_score")
|
|
76
|
+
ev_changed = len(na.get("evidence_ids", [])) != len(nb.get("evidence_ids", []))
|
|
77
|
+
|
|
78
|
+
if stat_changed or conf_changed or ev_changed:
|
|
79
|
+
modified_nodes[nid] = NodeDelta(
|
|
80
|
+
old_status=na.get("status"),
|
|
81
|
+
new_status=nb.get("status"),
|
|
82
|
+
old_confidence=na.get("confidence_score"),
|
|
83
|
+
new_confidence=nb.get("confidence_score"),
|
|
84
|
+
old_evidence_count=len(na.get("evidence_ids", [])),
|
|
85
|
+
new_evidence_count=len(nb.get("evidence_ids", []))
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Compare edges
|
|
89
|
+
edges_a = {(e["source_id"], e["target_id"], e["relation"]) for e in snap_a.edges}
|
|
90
|
+
edges_b = {(e["source_id"], e["target_id"], e["relation"]) for e in snap_b.edges}
|
|
91
|
+
|
|
92
|
+
added_edges_raw = edges_b - edges_a
|
|
93
|
+
removed_edges_raw = edges_a - edges_b
|
|
94
|
+
|
|
95
|
+
added_edges = [{"source_id": s, "target_id": t, "relation": r} for s, t, r in sorted(list(added_edges_raw))]
|
|
96
|
+
removed_edges = [{"source_id": s, "target_id": t, "relation": r} for s, t, r in sorted(list(removed_edges_raw))]
|
|
97
|
+
|
|
98
|
+
has_changes = bool(added_node_ids or removed_node_ids or modified_nodes or added_edges or removed_edges)
|
|
99
|
+
|
|
100
|
+
return cls(
|
|
101
|
+
baseline_id=snap_a.snapshot_id,
|
|
102
|
+
target_id=snap_b.snapshot_id,
|
|
103
|
+
added_nodes=added_node_ids,
|
|
104
|
+
removed_nodes=removed_node_ids,
|
|
105
|
+
modified_nodes=modified_nodes,
|
|
106
|
+
added_edges=added_edges,
|
|
107
|
+
removed_edges=removed_edges,
|
|
108
|
+
has_changes=has_changes
|
|
109
|
+
)
|