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,442 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Continuum - Contradiction Detection Engine
|
|
3
|
+
==================================================
|
|
4
|
+
Milestone 3 - Phase 7: Automated Discrepancy Identification.
|
|
5
|
+
Detects mismatches between:
|
|
6
|
+
1. Conversational claims vs Missing physical AST/file implementations (HIGH).
|
|
7
|
+
2. Conversational claims vs Failing automated test results (HIGH).
|
|
8
|
+
3. Documentation references vs Missing code symbols/types (MEDIUM).
|
|
9
|
+
4. Manifest dependencies vs Missing lockfiles/imports (MEDIUM).
|
|
10
|
+
5. Conversational claims of clean state vs Dirty Git working trees (LOW/MEDIUM).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
import re
|
|
15
|
+
from typing import Any, Dict, List, Optional, Set
|
|
16
|
+
import uuid
|
|
17
|
+
|
|
18
|
+
from core.enums import EvidenceLevel, EvidenceType, Status
|
|
19
|
+
from core.evidence import Evidence
|
|
20
|
+
from core.interfaces import IContradictionDetector
|
|
21
|
+
from core.state_models import (
|
|
22
|
+
AgentClaim,
|
|
23
|
+
AstSymbol,
|
|
24
|
+
CanonicalProjectState,
|
|
25
|
+
ContradictionRecord,
|
|
26
|
+
ConversationalState,
|
|
27
|
+
GitState,
|
|
28
|
+
ProjectState,
|
|
29
|
+
TestResult,
|
|
30
|
+
)
|
|
31
|
+
from contradictions.models import ContradictionSeverity, ContradictionType, DiscrepancyLedger
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ContradictionDetector(IContradictionDetector):
|
|
35
|
+
"""
|
|
36
|
+
Identifies and records discrepancies across ProjectState, ConversationalState,
|
|
37
|
+
and the global Evidence pool.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
COMPLETION_PATTERNS = [
|
|
41
|
+
re.compile(r"\b(completed?|implemented?|finished?|done|created|built|ready)\b", re.IGNORECASE),
|
|
42
|
+
re.compile(r"\b(working|passes?|passed|passing|verified)\b", re.IGNORECASE),
|
|
43
|
+
re.compile(r"\b(all tests (pass|passed|are passing))\b", re.IGNORECASE),
|
|
44
|
+
re.compile(r"\b(100%|completely|fully implemented)\b", re.IGNORECASE),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
DOC_SYMBOL_PATTERN = re.compile(r"`([A-Za-z_][A-Za-z0-9_]{2,})`|\bclass\s+([A-Za-z_][A-Za-z0-9_]+)\b|\bdef\s+([A-Za-z_][A-Za-z0-9_]+)\b")
|
|
48
|
+
|
|
49
|
+
def detect_contradictions(
|
|
50
|
+
self,
|
|
51
|
+
project_state: ProjectState,
|
|
52
|
+
conversational_state: ConversationalState,
|
|
53
|
+
evidence_pool: Dict[str, Evidence]
|
|
54
|
+
) -> List[ContradictionRecord]:
|
|
55
|
+
"""
|
|
56
|
+
Analyzes all state dimensions and returns structured ContradictionRecord items.
|
|
57
|
+
Implements IContradictionDetector Protocol.
|
|
58
|
+
"""
|
|
59
|
+
contradictions: List[ContradictionRecord] = []
|
|
60
|
+
|
|
61
|
+
# 1. Detect claims of completion vs missing implementation (AST / Files)
|
|
62
|
+
claim_impl_contradictions = self._detect_claims_vs_missing_implementation(
|
|
63
|
+
conversational_state.agent_claims,
|
|
64
|
+
project_state.symbols,
|
|
65
|
+
project_state.files,
|
|
66
|
+
evidence_pool
|
|
67
|
+
)
|
|
68
|
+
contradictions.extend(claim_impl_contradictions)
|
|
69
|
+
|
|
70
|
+
# 2. Detect claims of passing tests vs failing test results
|
|
71
|
+
claim_test_contradictions = self._detect_claims_vs_failing_tests(
|
|
72
|
+
conversational_state.agent_claims,
|
|
73
|
+
project_state.test_results,
|
|
74
|
+
evidence_pool
|
|
75
|
+
)
|
|
76
|
+
contradictions.extend(claim_test_contradictions)
|
|
77
|
+
|
|
78
|
+
# 3. Detect documentation referencing non-existent code symbols
|
|
79
|
+
doc_symbol_contradictions = self._detect_documentation_vs_missing_symbols(
|
|
80
|
+
evidence_pool,
|
|
81
|
+
project_state.symbols
|
|
82
|
+
)
|
|
83
|
+
contradictions.extend(doc_symbol_contradictions)
|
|
84
|
+
|
|
85
|
+
# 4. Detect claims of clean repository vs dirty Git state
|
|
86
|
+
git_contradictions = self._detect_stale_git_state(
|
|
87
|
+
conversational_state.agent_claims,
|
|
88
|
+
project_state.git_state,
|
|
89
|
+
evidence_pool
|
|
90
|
+
)
|
|
91
|
+
contradictions.extend(git_contradictions)
|
|
92
|
+
|
|
93
|
+
# 5. Detect misleading commit messages claiming non-existent implementations
|
|
94
|
+
commit_contradictions = self._detect_misleading_commits(
|
|
95
|
+
project_state.git_state,
|
|
96
|
+
project_state.symbols,
|
|
97
|
+
project_state.files
|
|
98
|
+
)
|
|
99
|
+
contradictions.extend(commit_contradictions)
|
|
100
|
+
|
|
101
|
+
return contradictions
|
|
102
|
+
|
|
103
|
+
def _detect_claims_vs_missing_implementation(
|
|
104
|
+
self,
|
|
105
|
+
claims: List[AgentClaim],
|
|
106
|
+
symbols: List[AstSymbol],
|
|
107
|
+
files: List[str],
|
|
108
|
+
evidence_pool: Dict[str, Evidence]
|
|
109
|
+
) -> List[ContradictionRecord]:
|
|
110
|
+
"""
|
|
111
|
+
Detects claims asserting a component, service, or function is complete/implemented
|
|
112
|
+
when no matching AST symbol or file exists in the physical workspace.
|
|
113
|
+
"""
|
|
114
|
+
records: List[ContradictionRecord] = []
|
|
115
|
+
symbol_names = {s.name.lower() for s in symbols}
|
|
116
|
+
file_names = {f.replace("\\", "/").lower() for f in files}
|
|
117
|
+
|
|
118
|
+
# If claim is specifically about test pass/fail, avoid duplicate missing-impl contradiction
|
|
119
|
+
test_assertion_pattern = re.compile(r"\b(tests? (pass|passed|passing)|all tests pass)\b", re.IGNORECASE)
|
|
120
|
+
|
|
121
|
+
for claim in claims:
|
|
122
|
+
target = claim.target_component or ""
|
|
123
|
+
claim_text = claim.claim_text.strip()
|
|
124
|
+
|
|
125
|
+
# If claim is strictly a test execution claim, let _detect_claims_vs_failing_tests handle it
|
|
126
|
+
if test_assertion_pattern.search(claim_text) and not any(k in claim_text.lower() for k in ["implemented", "complete", "finished", "created", "built"]):
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
is_completion_claim = (
|
|
130
|
+
claim.claimed_status == Status.VERIFIED
|
|
131
|
+
or any(p.search(claim_text) for p in self.COMPLETION_PATTERNS)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if not is_completion_claim:
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Extract possible component identifiers from claim_text or target_component
|
|
138
|
+
candidates = self._extract_identifier_candidates(target, claim_text)
|
|
139
|
+
if not candidates:
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
# Check if any candidate exists in AST symbols or file paths
|
|
143
|
+
has_match = False
|
|
144
|
+
for cand in candidates:
|
|
145
|
+
cand_lower = cand.lower()
|
|
146
|
+
if cand_lower in symbol_names or any(cand_lower in s for s in symbol_names):
|
|
147
|
+
has_match = True
|
|
148
|
+
break
|
|
149
|
+
if any(cand_lower in f for f in file_names):
|
|
150
|
+
has_match = True
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
if not has_match:
|
|
154
|
+
record = ContradictionRecord(
|
|
155
|
+
id=f"contra_{uuid.uuid4().hex[:8]}",
|
|
156
|
+
severity=ContradictionSeverity.HIGH.value,
|
|
157
|
+
claim_id=claim.id,
|
|
158
|
+
claim_text=claim_text,
|
|
159
|
+
physical_evidence_id=None,
|
|
160
|
+
explanation=(
|
|
161
|
+
f"Agent claimed '{claim_text}' (target: '{target or candidates[0]}'), "
|
|
162
|
+
f"but no matching AST symbols ({', '.join(candidates[:3])}) or source files "
|
|
163
|
+
f"exist in the workspace."
|
|
164
|
+
),
|
|
165
|
+
detected_at=datetime.now(timezone.utc).isoformat(),
|
|
166
|
+
resolved=False
|
|
167
|
+
)
|
|
168
|
+
records.append(record)
|
|
169
|
+
|
|
170
|
+
return records
|
|
171
|
+
|
|
172
|
+
def _detect_claims_vs_failing_tests(
|
|
173
|
+
self,
|
|
174
|
+
claims: List[AgentClaim],
|
|
175
|
+
test_results: List[TestResult],
|
|
176
|
+
evidence_pool: Dict[str, Evidence]
|
|
177
|
+
) -> List[ContradictionRecord]:
|
|
178
|
+
"""
|
|
179
|
+
Detects claims asserting tests pass or features are verified when actual
|
|
180
|
+
recorded test executions failed (exit code != 0).
|
|
181
|
+
"""
|
|
182
|
+
records: List[ContradictionRecord] = []
|
|
183
|
+
failing_tests = [t for t in test_results if t.status == Status.FAILED or t.exit_code != 0]
|
|
184
|
+
|
|
185
|
+
# Also check evidence pool for Level 1 test runs
|
|
186
|
+
failing_test_ev = [
|
|
187
|
+
ev for ev in evidence_pool.values()
|
|
188
|
+
if ev.type == EvidenceType.TEST_RUN and ev.raw_payload.get("exit_code", 0) != 0
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
if not failing_tests and not failing_test_ev:
|
|
192
|
+
return records
|
|
193
|
+
|
|
194
|
+
test_pass_regex = re.compile(r"\b(tests? (pass|passed|passing|green|succeeded)|all tests|100% pass|verified)\b", re.IGNORECASE)
|
|
195
|
+
|
|
196
|
+
for claim in claims:
|
|
197
|
+
claim_text = claim.claim_text
|
|
198
|
+
target = (claim.target_component or "").lower()
|
|
199
|
+
|
|
200
|
+
claims_passing = (
|
|
201
|
+
bool(test_pass_regex.search(claim_text))
|
|
202
|
+
or claim.claimed_status == Status.VERIFIED
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if not claims_passing:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# Identify matching failing test
|
|
209
|
+
matching_failing_test = None
|
|
210
|
+
for ft in failing_tests:
|
|
211
|
+
if not target or target in ft.name.lower() or target in ft.suite.lower() or "test" in claim_text.lower():
|
|
212
|
+
matching_failing_test = ft
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
matching_ev = None
|
|
216
|
+
for ev in failing_test_ev:
|
|
217
|
+
if not target or target in ev.summary.lower() or target in str(ev.raw_payload).lower() or "test" in claim_text.lower():
|
|
218
|
+
matching_ev = ev
|
|
219
|
+
break
|
|
220
|
+
|
|
221
|
+
if matching_failing_test or matching_ev:
|
|
222
|
+
ev_id = matching_failing_test.evidence_id if matching_failing_test else (matching_ev.id if matching_ev else None)
|
|
223
|
+
fail_detail = (
|
|
224
|
+
f"test '{matching_failing_test.name}' failed with exit code {matching_failing_test.exit_code}"
|
|
225
|
+
if matching_failing_test
|
|
226
|
+
else f"test suite '{matching_ev.summary}' failed (exit code {matching_ev.raw_payload.get('exit_code')})"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
record = ContradictionRecord(
|
|
230
|
+
id=f"contra_{uuid.uuid4().hex[:8]}",
|
|
231
|
+
severity=ContradictionSeverity.HIGH.value,
|
|
232
|
+
claim_id=claim.id,
|
|
233
|
+
claim_text=claim_text,
|
|
234
|
+
physical_evidence_id=ev_id,
|
|
235
|
+
explanation=(
|
|
236
|
+
f"Agent claimed '{claim_text}', but physical verification recorded a failure: {fail_detail}."
|
|
237
|
+
),
|
|
238
|
+
detected_at=datetime.now(timezone.utc).isoformat(),
|
|
239
|
+
resolved=False
|
|
240
|
+
)
|
|
241
|
+
records.append(record)
|
|
242
|
+
|
|
243
|
+
return records
|
|
244
|
+
|
|
245
|
+
def _detect_documentation_vs_missing_symbols(
|
|
246
|
+
self,
|
|
247
|
+
evidence_pool: Dict[str, Evidence],
|
|
248
|
+
symbols: List[AstSymbol]
|
|
249
|
+
) -> List[ContradictionRecord]:
|
|
250
|
+
"""
|
|
251
|
+
Detects documentation evidence (markdown, comments) referencing classes or functions
|
|
252
|
+
that do not exist anywhere in the extracted AST symbol table.
|
|
253
|
+
"""
|
|
254
|
+
records: List[ContradictionRecord] = []
|
|
255
|
+
symbol_names = {s.name for s in symbols}
|
|
256
|
+
doc_evidences = [
|
|
257
|
+
ev for ev in evidence_pool.values()
|
|
258
|
+
if ev.level == EvidenceLevel.LEVEL_4_DOCUMENTATION or ev.type == EvidenceType.DOCUMENTATION
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
# Standard built-in names to ignore
|
|
262
|
+
BUILTIN_IGNORE = {
|
|
263
|
+
"str", "int", "float", "bool", "dict", "list", "set", "tuple", "None", "Optional",
|
|
264
|
+
"Any", "List", "Dict", "Set", "Tuple", "Union", "Protocol", "Enum", "True", "False",
|
|
265
|
+
"self", "cls", "return", "import", "from", "class", "def", "async", "await",
|
|
266
|
+
"JSON", "HTTP", "REST", "API", "URL", "UUID", "SHA", "README", "TODO", "FIXME",
|
|
267
|
+
"GET", "POST", "PUT", "DELETE", "UTF-8", "ASCII", "Linux", "Windows", "MacOS"
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for ev in doc_evidences:
|
|
271
|
+
content = str(ev.raw_payload.get("content", "")) or ev.summary
|
|
272
|
+
referenced_identifiers: Set[str] = set()
|
|
273
|
+
|
|
274
|
+
for match in self.DOC_SYMBOL_PATTERN.finditer(content):
|
|
275
|
+
for group in match.groups():
|
|
276
|
+
if group and len(group) > 3 and group not in BUILTIN_IGNORE:
|
|
277
|
+
referenced_identifiers.add(group)
|
|
278
|
+
|
|
279
|
+
missing_symbols = [
|
|
280
|
+
ident for ident in referenced_identifiers
|
|
281
|
+
if ident not in symbol_names and not any(ident.lower() == s.lower() for s in symbol_names)
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
# Filter out likely non-code nouns (keep CamelCase and snake_case with underscores)
|
|
285
|
+
suspicious_missing = [
|
|
286
|
+
m for m in missing_symbols
|
|
287
|
+
if ("_" in m or (m[0].isupper() and any(c.islower() for c in m[1:])))
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
if suspicious_missing:
|
|
291
|
+
record = ContradictionRecord(
|
|
292
|
+
id=f"contra_{uuid.uuid4().hex[:8]}",
|
|
293
|
+
severity=ContradictionSeverity.MEDIUM.value,
|
|
294
|
+
claim_id=None,
|
|
295
|
+
claim_text=f"Documentation in '{ev.summary}'",
|
|
296
|
+
physical_evidence_id=ev.id,
|
|
297
|
+
explanation=(
|
|
298
|
+
f"Documentation ({ev.summary}) references code symbol(s) '{', '.join(suspicious_missing[:3])}' "
|
|
299
|
+
f"which are absent from physical AST symbols."
|
|
300
|
+
),
|
|
301
|
+
detected_at=datetime.now(timezone.utc).isoformat(),
|
|
302
|
+
resolved=False
|
|
303
|
+
)
|
|
304
|
+
records.append(record)
|
|
305
|
+
|
|
306
|
+
return records
|
|
307
|
+
|
|
308
|
+
def _detect_stale_git_state(
|
|
309
|
+
self,
|
|
310
|
+
claims: List[AgentClaim],
|
|
311
|
+
git_state: GitState,
|
|
312
|
+
evidence_pool: Dict[str, Evidence]
|
|
313
|
+
) -> List[ContradictionRecord]:
|
|
314
|
+
"""
|
|
315
|
+
Detects claims asserting the workspace is committed, clean, or ready for release
|
|
316
|
+
when Git state is dirty or has untracked / uncommitted changes.
|
|
317
|
+
"""
|
|
318
|
+
records: List[ContradictionRecord] = []
|
|
319
|
+
if not git_state.is_repo or not git_state.is_dirty:
|
|
320
|
+
return records
|
|
321
|
+
|
|
322
|
+
clean_claim_regex = re.compile(r"\b(committed|all changes committed|clean working tree|ready to ship|pushed)\b", re.IGNORECASE)
|
|
323
|
+
|
|
324
|
+
for claim in claims:
|
|
325
|
+
if clean_claim_regex.search(claim.claim_text):
|
|
326
|
+
dirty_summary = f"{len(git_state.staged_files)} staged, {len(git_state.unstaged_files)} unstaged, {len(git_state.untracked_files)} untracked files"
|
|
327
|
+
record = ContradictionRecord(
|
|
328
|
+
id=f"contra_{uuid.uuid4().hex[:8]}",
|
|
329
|
+
severity=ContradictionSeverity.MEDIUM.value,
|
|
330
|
+
claim_id=claim.id,
|
|
331
|
+
claim_text=claim.claim_text,
|
|
332
|
+
physical_evidence_id=git_state.evidence_ids[0] if git_state.evidence_ids else None,
|
|
333
|
+
explanation=(
|
|
334
|
+
f"Agent claimed '{claim.claim_text}', but physical Git repository is DIRTY ({dirty_summary})."
|
|
335
|
+
),
|
|
336
|
+
detected_at=datetime.now(timezone.utc).isoformat(),
|
|
337
|
+
resolved=False
|
|
338
|
+
)
|
|
339
|
+
records.append(record)
|
|
340
|
+
|
|
341
|
+
return records
|
|
342
|
+
|
|
343
|
+
def _detect_misleading_commits(
|
|
344
|
+
self,
|
|
345
|
+
git_state: GitState,
|
|
346
|
+
symbols: List[AstSymbol],
|
|
347
|
+
files: List[str]
|
|
348
|
+
) -> List[ContradictionRecord]:
|
|
349
|
+
"""
|
|
350
|
+
Detects commits with messages claiming to have added/implemented components
|
|
351
|
+
that do not actually exist in the physical AST or file system.
|
|
352
|
+
"""
|
|
353
|
+
records: List[ContradictionRecord] = []
|
|
354
|
+
if not git_state.is_repo or not git_state.recent_commits:
|
|
355
|
+
return records
|
|
356
|
+
|
|
357
|
+
symbol_names = {s.name.lower() for s in symbols}
|
|
358
|
+
file_names = {f.replace("\\", "/").lower() for f in files}
|
|
359
|
+
|
|
360
|
+
for commit in git_state.recent_commits:
|
|
361
|
+
msg = commit.get("message", "") or commit.get("summary", "")
|
|
362
|
+
if not msg:
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
candidates = self._extract_identifier_candidates("", msg)
|
|
366
|
+
if not candidates:
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
# Check if this commit claims creation/implementation of a specific component
|
|
370
|
+
if any(p.search(msg) for p in self.COMPLETION_PATTERNS) or msg.lower().startswith(("feat:", "add ", "implement ")):
|
|
371
|
+
for cand in candidates:
|
|
372
|
+
cand_lower = cand.lower()
|
|
373
|
+
if len(cand) < 4 or cand_lower in {"test", "tests", "code", "file", "feat", "docs", "fix"}:
|
|
374
|
+
continue
|
|
375
|
+
has_match = (
|
|
376
|
+
cand_lower in symbol_names
|
|
377
|
+
or any(cand_lower in s for s in symbol_names)
|
|
378
|
+
or any(cand_lower in f for f in file_names)
|
|
379
|
+
)
|
|
380
|
+
if not has_match:
|
|
381
|
+
records.append(ContradictionRecord(
|
|
382
|
+
id=f"contra_{uuid.uuid4().hex[:8]}",
|
|
383
|
+
severity=ContradictionSeverity.MEDIUM.value,
|
|
384
|
+
claim_id=None,
|
|
385
|
+
claim_text=f"Git commit '{commit.get('hash', 'head')[:7]}': {msg}",
|
|
386
|
+
physical_evidence_id=git_state.evidence_ids[0] if git_state.evidence_ids else None,
|
|
387
|
+
explanation=(
|
|
388
|
+
f"Git commit message '{msg}' implies implementation of '{cand}', "
|
|
389
|
+
f"but no matching AST symbol or source file exists in the repository."
|
|
390
|
+
),
|
|
391
|
+
detected_at=datetime.now(timezone.utc).isoformat(),
|
|
392
|
+
resolved=False
|
|
393
|
+
))
|
|
394
|
+
|
|
395
|
+
return records
|
|
396
|
+
|
|
397
|
+
def _extract_identifier_candidates(self, target: str, text: str) -> List[str]:
|
|
398
|
+
"""Extracts potential class, function, or module names from claim text."""
|
|
399
|
+
candidates = []
|
|
400
|
+
if target:
|
|
401
|
+
candidates.append(target)
|
|
402
|
+
|
|
403
|
+
# Look for words in quotes or CamelCase or snake_case
|
|
404
|
+
quoted = re.findall(r"['\"`]([A-Za-z0-9_\-\.]+)['\"`]", text)
|
|
405
|
+
candidates.extend(quoted)
|
|
406
|
+
|
|
407
|
+
# Look for explicit CamelCase identifiers (e.g. KafkaEventStreamingProducer)
|
|
408
|
+
camel_cases = re.findall(r"\b([A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)+)\b", text)
|
|
409
|
+
candidates.extend(camel_cases)
|
|
410
|
+
|
|
411
|
+
# Look for targets following feat:, add, implement, create
|
|
412
|
+
verb_match = re.findall(r"\b(?:implement|add|create|feat:)\s+([A-Za-z0-9_]+)\b", text, re.IGNORECASE)
|
|
413
|
+
candidates.extend(verb_match)
|
|
414
|
+
|
|
415
|
+
# Look for tokens before keywords like "service", "handler", "manager", "parser", "client", "controller"
|
|
416
|
+
comp_match = re.findall(r"\b([A-Za-z0-9_]+(?:\s+[A-Za-z0-9_]+)?\s+(?:service|handler|manager|parser|client|controller|engine|module|endpoint|function|producer|consumer))\b", text, re.IGNORECASE)
|
|
417
|
+
for m in comp_match:
|
|
418
|
+
cleaned = m.replace(" ", "_").lower()
|
|
419
|
+
candidates.append(cleaned)
|
|
420
|
+
candidates.append(m)
|
|
421
|
+
|
|
422
|
+
# Strip duplicates while preserving order
|
|
423
|
+
unique: List[str] = []
|
|
424
|
+
for c in candidates:
|
|
425
|
+
if c and c not in unique:
|
|
426
|
+
unique.append(c)
|
|
427
|
+
|
|
428
|
+
return unique
|
|
429
|
+
|
|
430
|
+
def audit_canonical_state(self, canonical_state: CanonicalProjectState) -> DiscrepancyLedger:
|
|
431
|
+
"""
|
|
432
|
+
Executes full contradiction audit on a CanonicalProjectState,
|
|
433
|
+
populates its discrepancies ledger, and returns the DiscrepancyLedger.
|
|
434
|
+
"""
|
|
435
|
+
detected = self.detect_contradictions(
|
|
436
|
+
project_state=canonical_state.project_state,
|
|
437
|
+
conversational_state=canonical_state.conversational_state,
|
|
438
|
+
evidence_pool=canonical_state.evidence_pool
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
canonical_state.contradictions = detected
|
|
442
|
+
return DiscrepancyLedger(records=detected)
|
contradictions/models.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Continuum - Contradiction & Discrepancy Models
|
|
3
|
+
======================================================
|
|
4
|
+
Milestone 3 - Phase 7: Contradiction Detection Engine.
|
|
5
|
+
Defines contradiction categories, discrepancy severities, and the discrepancy ledger.
|
|
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
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
from core.state_models import ContradictionRecord
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ContradictionType(str, Enum):
|
|
18
|
+
"""Categories of discrepancies identified across Continuum's state boundaries."""
|
|
19
|
+
CLAIM_VS_MISSING_IMPLEMENTATION = "claim_vs_missing_implementation"
|
|
20
|
+
CLAIM_VS_FAILING_TEST = "claim_vs_failing_test"
|
|
21
|
+
DOC_VS_MISSING_SYMBOL = "doc_vs_missing_symbol"
|
|
22
|
+
INCOMPLETE_DEPENDENCY = "incomplete_dependency"
|
|
23
|
+
STALE_PROJECT_INFO = "stale_project_info"
|
|
24
|
+
UNVERIFIED_CLAIM = "unverified_claim"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ContradictionSeverity(str, Enum):
|
|
28
|
+
"""Severity levels for detected contradictions."""
|
|
29
|
+
HIGH = "HIGH" # Direct falsehood (e.g. claiming tests pass when they fail)
|
|
30
|
+
MEDIUM = "MEDIUM" # Significant drift (e.g. docs reference non-existent symbols)
|
|
31
|
+
LOW = "LOW" # Minor divergence (e.g. uncommitted changes after claiming done)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class DiscrepancyLedger:
|
|
36
|
+
"""
|
|
37
|
+
Maintains an audit ledger of all unresolved and resolved discrepancies.
|
|
38
|
+
Ensures complete transparency for developers and subsequent AI agents.
|
|
39
|
+
"""
|
|
40
|
+
records: List[ContradictionRecord] = field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
def add_record(self, record: ContradictionRecord) -> None:
|
|
43
|
+
"""Appends a new contradiction record if not already present."""
|
|
44
|
+
if not any(r.id == record.id for r in self.records):
|
|
45
|
+
self.records.append(record)
|
|
46
|
+
|
|
47
|
+
def get_unresolved(self) -> List[ContradictionRecord]:
|
|
48
|
+
"""Returns all currently unresolved contradictions."""
|
|
49
|
+
return [r for r in self.records if not r.resolved]
|
|
50
|
+
|
|
51
|
+
def get_by_severity(self, severity: str) -> List[ContradictionRecord]:
|
|
52
|
+
"""Filters contradictions by severity (HIGH, MEDIUM, LOW)."""
|
|
53
|
+
return [r for r in self.records if r.severity == severity]
|
|
54
|
+
|
|
55
|
+
def resolve_record(self, record_id: str) -> bool:
|
|
56
|
+
"""Marks a contradiction as resolved."""
|
|
57
|
+
for r in self.records:
|
|
58
|
+
if r.id == record_id:
|
|
59
|
+
r.resolved = True
|
|
60
|
+
return True
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
64
|
+
return {
|
|
65
|
+
"unresolved_count": len(self.get_unresolved()),
|
|
66
|
+
"high_severity_count": len(self.get_by_severity("HIGH")),
|
|
67
|
+
"medium_severity_count": len(self.get_by_severity("MEDIUM")),
|
|
68
|
+
"low_severity_count": len(self.get_by_severity("LOW")),
|
|
69
|
+
"records": [r.to_dict() for r in self.records]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_dict(cls, data: Dict[str, Any]) -> "DiscrepancyLedger":
|
|
74
|
+
records = [ContradictionRecord.from_dict(r) for r in data.get("records", [])]
|
|
75
|
+
return cls(records=records)
|
core/__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Continuum - Core Package Facade
|
|
3
|
+
======================================
|
|
4
|
+
Exposes primary models, enums, interfaces, and serializers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from core.enums import (
|
|
8
|
+
Status,
|
|
9
|
+
EvidenceType,
|
|
10
|
+
EvidenceLevel,
|
|
11
|
+
NodeType,
|
|
12
|
+
RelationType,
|
|
13
|
+
TargetModel,
|
|
14
|
+
)
|
|
15
|
+
from core.evidence import (
|
|
16
|
+
Evidence,
|
|
17
|
+
EvidenceProvenance,
|
|
18
|
+
)
|
|
19
|
+
from core.state_models import (
|
|
20
|
+
ProjectState,
|
|
21
|
+
ConversationalState,
|
|
22
|
+
AgentExecutionState,
|
|
23
|
+
CanonicalProjectState,
|
|
24
|
+
AstSymbol,
|
|
25
|
+
TestResult,
|
|
26
|
+
GitState,
|
|
27
|
+
ManifestInfo,
|
|
28
|
+
Requirement,
|
|
29
|
+
ArchitecturalDecision,
|
|
30
|
+
AgentClaim,
|
|
31
|
+
UnresolvedQuestion,
|
|
32
|
+
TacticalTask,
|
|
33
|
+
NextActionRecommendation,
|
|
34
|
+
ContradictionRecord,
|
|
35
|
+
GraphNode,
|
|
36
|
+
GraphEdge,
|
|
37
|
+
)
|
|
38
|
+
from core.interfaces import (
|
|
39
|
+
IEvidenceExtractor,
|
|
40
|
+
IEvidenceResolver,
|
|
41
|
+
IContradictionDetector,
|
|
42
|
+
IConfidenceCalculator,
|
|
43
|
+
IStateGraphManager,
|
|
44
|
+
IContextSelector,
|
|
45
|
+
IModelAdapter,
|
|
46
|
+
)
|
|
47
|
+
from core.schema import (
|
|
48
|
+
CANONICAL_STATE_SCHEMA_VERSION,
|
|
49
|
+
get_canonical_project_state_schema,
|
|
50
|
+
validate_canonical_state_dict,
|
|
51
|
+
)
|
|
52
|
+
from core.serializer import (
|
|
53
|
+
CanonicalStateSerializer,
|
|
54
|
+
StateSerializationError,
|
|
55
|
+
StateValidationError,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
"Status",
|
|
60
|
+
"EvidenceType",
|
|
61
|
+
"EvidenceLevel",
|
|
62
|
+
"NodeType",
|
|
63
|
+
"RelationType",
|
|
64
|
+
"TargetModel",
|
|
65
|
+
"Evidence",
|
|
66
|
+
"EvidenceProvenance",
|
|
67
|
+
"ProjectState",
|
|
68
|
+
"ConversationalState",
|
|
69
|
+
"AgentExecutionState",
|
|
70
|
+
"CanonicalProjectState",
|
|
71
|
+
"AstSymbol",
|
|
72
|
+
"TestResult",
|
|
73
|
+
"GitState",
|
|
74
|
+
"ManifestInfo",
|
|
75
|
+
"Requirement",
|
|
76
|
+
"ArchitecturalDecision",
|
|
77
|
+
"AgentClaim",
|
|
78
|
+
"UnresolvedQuestion",
|
|
79
|
+
"TacticalTask",
|
|
80
|
+
"NextActionRecommendation",
|
|
81
|
+
"ContradictionRecord",
|
|
82
|
+
"GraphNode",
|
|
83
|
+
"GraphEdge",
|
|
84
|
+
"IEvidenceExtractor",
|
|
85
|
+
"IEvidenceResolver",
|
|
86
|
+
"IContradictionDetector",
|
|
87
|
+
"IConfidenceCalculator",
|
|
88
|
+
"IStateGraphManager",
|
|
89
|
+
"IContextSelector",
|
|
90
|
+
"IModelAdapter",
|
|
91
|
+
"CANONICAL_STATE_SCHEMA_VERSION",
|
|
92
|
+
"get_canonical_project_state_schema",
|
|
93
|
+
"validate_canonical_state_dict",
|
|
94
|
+
"CanonicalStateSerializer",
|
|
95
|
+
"StateSerializationError",
|
|
96
|
+
"StateValidationError",
|
|
97
|
+
]
|