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
daemon/service.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Continuum - Daemon Service & Workspace Observer
|
|
3
|
+
========================================================
|
|
4
|
+
Milestone 7 - Phase 18: Continuum Daemon & CLI.
|
|
5
|
+
Implements the continuous background monitoring service that synchronizes
|
|
6
|
+
workspace changes into persistent Canonical State.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field, asdict
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any, Dict, Optional
|
|
17
|
+
|
|
18
|
+
from core.serializer import CanonicalStateSerializer
|
|
19
|
+
from core.state_models import CanonicalProjectState
|
|
20
|
+
from storage.manager import ContinuumStorageManager
|
|
21
|
+
from storage.recovery import StateRecoveryManager
|
|
22
|
+
from watcher.detector import WorkspaceChangeDetector
|
|
23
|
+
from watcher.updater import IncrementalStateUpdater
|
|
24
|
+
from graph.manager import StateGraphManager
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class DaemonStatus:
|
|
29
|
+
is_running: bool
|
|
30
|
+
pid: Optional[int] = None
|
|
31
|
+
workspace_root: str = ""
|
|
32
|
+
started_at: Optional[str] = None
|
|
33
|
+
last_tick_at: Optional[str] = None
|
|
34
|
+
cycles_completed: int = 0
|
|
35
|
+
total_changes_processed: int = 0
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
38
|
+
return asdict(self)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ContinuumDaemon:
|
|
42
|
+
"""
|
|
43
|
+
Background daemon service monitoring workspace filesystem events,
|
|
44
|
+
applying incremental state updates, and persisting to .continuum/.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
workspace_root: str,
|
|
50
|
+
poll_interval_seconds: float = 1.0
|
|
51
|
+
):
|
|
52
|
+
self.workspace_root = Path(workspace_root).resolve()
|
|
53
|
+
self.poll_interval = poll_interval_seconds
|
|
54
|
+
self._storage = ContinuumStorageManager(str(self.workspace_root))
|
|
55
|
+
self._recovery = StateRecoveryManager(self._storage)
|
|
56
|
+
self._detector = WorkspaceChangeDetector(str(self.workspace_root))
|
|
57
|
+
self._updater = IncrementalStateUpdater(str(self.workspace_root))
|
|
58
|
+
|
|
59
|
+
self._pid_file = self._storage.continuum_dir / "daemon.pid"
|
|
60
|
+
self._status_file = self._storage.continuum_dir / "daemon.json"
|
|
61
|
+
self._stop_event = threading.Event()
|
|
62
|
+
self._thread: Optional[threading.Thread] = None
|
|
63
|
+
|
|
64
|
+
self._cycles = 0
|
|
65
|
+
self._total_changes = 0
|
|
66
|
+
self._started_at: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
def start(self, run_in_background: bool = False) -> DaemonStatus:
|
|
69
|
+
"""Starts daemon monitoring loop."""
|
|
70
|
+
self._storage.initialize_storage()
|
|
71
|
+
self._detector.initialize_baseline()
|
|
72
|
+
self._stop_event.clear()
|
|
73
|
+
self._started_at = datetime.now(timezone.utc).isoformat()
|
|
74
|
+
|
|
75
|
+
# Write PID file
|
|
76
|
+
self._pid_file.write_text(str(os.getpid()), encoding="utf-8")
|
|
77
|
+
|
|
78
|
+
if run_in_background:
|
|
79
|
+
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
|
80
|
+
self._thread.start()
|
|
81
|
+
else:
|
|
82
|
+
self._run_loop()
|
|
83
|
+
|
|
84
|
+
return self.get_status()
|
|
85
|
+
|
|
86
|
+
def stop(self) -> DaemonStatus:
|
|
87
|
+
"""Stops the daemon gracefully."""
|
|
88
|
+
self._stop_event.set()
|
|
89
|
+
if self._thread and self._thread.is_alive():
|
|
90
|
+
self._thread.join(timeout=3.0)
|
|
91
|
+
|
|
92
|
+
if self._pid_file.exists():
|
|
93
|
+
try:
|
|
94
|
+
self._pid_file.unlink()
|
|
95
|
+
except OSError:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
return self.get_status()
|
|
99
|
+
|
|
100
|
+
def run_once(self) -> Dict[str, Any]:
|
|
101
|
+
"""
|
|
102
|
+
Executes a single check and synchronization cycle.
|
|
103
|
+
"""
|
|
104
|
+
self._storage.initialize_storage()
|
|
105
|
+
|
|
106
|
+
# Load or initialize state
|
|
107
|
+
if self._storage.state_exists():
|
|
108
|
+
try:
|
|
109
|
+
state = self._storage.load_state(validate=True)
|
|
110
|
+
except Exception:
|
|
111
|
+
rec_res = self._recovery.recover_corrupted_state()
|
|
112
|
+
if rec_res.success:
|
|
113
|
+
state = self._storage.load_state(validate=True)
|
|
114
|
+
else:
|
|
115
|
+
state = CanonicalProjectState(project_id="proj_auto_init")
|
|
116
|
+
else:
|
|
117
|
+
state = CanonicalProjectState(project_id="proj_auto_init")
|
|
118
|
+
|
|
119
|
+
# Detect changes
|
|
120
|
+
changes = self._detector.detect_changes()
|
|
121
|
+
result_info = {"changes_detected": len(changes), "updated": False}
|
|
122
|
+
|
|
123
|
+
if changes:
|
|
124
|
+
graph_manager = StateGraphManager(state)
|
|
125
|
+
update_res = self._updater.apply_changes(state, changes, graph_manager)
|
|
126
|
+
self._storage.save_state(state, create_history_snapshot=True)
|
|
127
|
+
self._total_changes += len(changes)
|
|
128
|
+
result_info["updated"] = True
|
|
129
|
+
result_info["details"] = update_res.to_dict()
|
|
130
|
+
|
|
131
|
+
self._cycles += 1
|
|
132
|
+
self._write_status_file()
|
|
133
|
+
return result_info
|
|
134
|
+
|
|
135
|
+
def get_status(self) -> DaemonStatus:
|
|
136
|
+
"""Returns current daemon operational status."""
|
|
137
|
+
is_running = not self._stop_event.is_set() and (self._thread.is_alive() if self._thread else False)
|
|
138
|
+
pid = None
|
|
139
|
+
if self._pid_file.exists():
|
|
140
|
+
try:
|
|
141
|
+
pid = int(self._pid_file.read_text().strip())
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
return DaemonStatus(
|
|
146
|
+
is_running=is_running,
|
|
147
|
+
pid=pid,
|
|
148
|
+
workspace_root=str(self.workspace_root),
|
|
149
|
+
started_at=self._started_at,
|
|
150
|
+
last_tick_at=datetime.now(timezone.utc).isoformat(),
|
|
151
|
+
cycles_completed=self._cycles,
|
|
152
|
+
total_changes_processed=self._total_changes
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _run_loop(self) -> None:
|
|
156
|
+
"""Internal worker loop."""
|
|
157
|
+
while not self._stop_event.is_set():
|
|
158
|
+
try:
|
|
159
|
+
self.run_once()
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
time.sleep(self.poll_interval)
|
|
163
|
+
|
|
164
|
+
def _write_status_file(self) -> None:
|
|
165
|
+
"""Persists daemon heartbeat to .continuum/daemon.json."""
|
|
166
|
+
status = self.get_status()
|
|
167
|
+
try:
|
|
168
|
+
self._status_file.write_text(json.dumps(status.to_dict(), indent=2), encoding="utf-8")
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
extractors/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Continuum - Extractors Subsystem Package Initialization
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from extractors.base import BaseEvidenceExtractor, DEFAULT_IGNORE_DIRS, CONFIG_FILE_NAMES, DOC_EXTENSIONS
|
|
6
|
+
from extractors.workspace_extractor import WorkspaceEvidenceExtractor
|
|
7
|
+
from extractors.config_extractor import ConfigEvidenceExtractor, LOCKFILE_NAMES
|
|
8
|
+
from extractors.git_extractor import GitEvidenceExtractor
|
|
9
|
+
from extractors.verification_extractor import VerificationEvidenceExtractor
|
|
10
|
+
from extractors.conversation_extractor import ConversationEvidenceExtractor
|
|
11
|
+
from extractors.conversation.models import ConversationTranscript, ConversationTurn, TurnRole
|
|
12
|
+
from extractors.conversation.analyzers import TranscriptAnalyzer, TranscriptAnalysisResult
|
|
13
|
+
from extractors.verification.runners import VerificationRunner, TestOutputParser, ExecutionResult, ParsedTestSummary
|
|
14
|
+
from extractors.config.secret_sanitizer import SecretSanitizer
|
|
15
|
+
from extractors.config.parsers import ManifestParser
|
|
16
|
+
from extractors.parsers.base import ILanguageParser, ParserResult, CommentMarker, ImportStatement, ParseError
|
|
17
|
+
from extractors.parsers.comment_parser import CommentMarkerParser
|
|
18
|
+
from extractors.parsers.python_parser import PythonParser
|
|
19
|
+
from extractors.parsers.js_ts_parser import JavaScriptTypeScriptParser
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"BaseEvidenceExtractor",
|
|
23
|
+
"WorkspaceEvidenceExtractor",
|
|
24
|
+
"ConfigEvidenceExtractor",
|
|
25
|
+
"GitEvidenceExtractor",
|
|
26
|
+
"VerificationEvidenceExtractor",
|
|
27
|
+
"ConversationEvidenceExtractor",
|
|
28
|
+
"ConversationTranscript",
|
|
29
|
+
"ConversationTurn",
|
|
30
|
+
"TurnRole",
|
|
31
|
+
"TranscriptAnalyzer",
|
|
32
|
+
"TranscriptAnalysisResult",
|
|
33
|
+
"VerificationRunner",
|
|
34
|
+
"TestOutputParser",
|
|
35
|
+
"ExecutionResult",
|
|
36
|
+
"ParsedTestSummary",
|
|
37
|
+
"SecretSanitizer",
|
|
38
|
+
"ManifestParser",
|
|
39
|
+
"DEFAULT_IGNORE_DIRS",
|
|
40
|
+
"CONFIG_FILE_NAMES",
|
|
41
|
+
"DOC_EXTENSIONS",
|
|
42
|
+
"LOCKFILE_NAMES",
|
|
43
|
+
"ILanguageParser",
|
|
44
|
+
"ParserResult",
|
|
45
|
+
"CommentMarker",
|
|
46
|
+
"ImportStatement",
|
|
47
|
+
"ParseError",
|
|
48
|
+
"CommentMarkerParser",
|
|
49
|
+
"PythonParser",
|
|
50
|
+
"JavaScriptTypeScriptParser",
|
|
51
|
+
]
|
extractors/base.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Continuum - Base Evidence Extractor & Classification
|
|
3
|
+
============================================================
|
|
4
|
+
Defines the BaseEvidenceExtractor and workspace classification logic.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional, Set
|
|
10
|
+
|
|
11
|
+
from core.enums import EvidenceType, EvidenceLevel
|
|
12
|
+
from core.evidence import Evidence, EvidenceProvenance
|
|
13
|
+
from core.interfaces import IEvidenceExtractor
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Standard directories to ignore during workspace scanning
|
|
17
|
+
DEFAULT_IGNORE_DIRS: Set[str] = {
|
|
18
|
+
".git",
|
|
19
|
+
".hg",
|
|
20
|
+
".svn",
|
|
21
|
+
"node_modules",
|
|
22
|
+
"__pycache__",
|
|
23
|
+
".pytest_cache",
|
|
24
|
+
".venv",
|
|
25
|
+
"venv",
|
|
26
|
+
"env",
|
|
27
|
+
".idea",
|
|
28
|
+
".vscode",
|
|
29
|
+
"dist",
|
|
30
|
+
"build",
|
|
31
|
+
".egg-info",
|
|
32
|
+
"target",
|
|
33
|
+
".dart_tool",
|
|
34
|
+
"coverage",
|
|
35
|
+
".continuum"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Standard configuration filenames
|
|
39
|
+
CONFIG_FILE_NAMES: Set[str] = {
|
|
40
|
+
"package.json",
|
|
41
|
+
"pyproject.toml",
|
|
42
|
+
"setup.py",
|
|
43
|
+
"setup.cfg",
|
|
44
|
+
"Cargo.toml",
|
|
45
|
+
"go.mod",
|
|
46
|
+
"pom.xml",
|
|
47
|
+
"build.gradle",
|
|
48
|
+
"pubspec.yaml",
|
|
49
|
+
"Dockerfile",
|
|
50
|
+
"docker-compose.yml",
|
|
51
|
+
"docker-compose.yaml",
|
|
52
|
+
"Makefile",
|
|
53
|
+
"CMakeLists.txt",
|
|
54
|
+
"tsconfig.json"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Documentation extensions
|
|
58
|
+
DOC_EXTENSIONS: Set[str] = {
|
|
59
|
+
".md",
|
|
60
|
+
".markdown",
|
|
61
|
+
".rst",
|
|
62
|
+
".txt",
|
|
63
|
+
".adoc"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class BaseEvidenceExtractor(ABC):
|
|
68
|
+
"""
|
|
69
|
+
Abstract base class providing standard helper methods for all evidence extractors.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def extractor_name(self) -> str:
|
|
75
|
+
"""Unique identifier for this extractor."""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def supported_evidence_types(self) -> List[EvidenceType]:
|
|
81
|
+
"""Evidence types this extractor produces."""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def can_extract(self, target_path_or_input: str) -> bool:
|
|
86
|
+
"""Determines if this extractor is applicable to the target."""
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def extract(self, target_path_or_input: str, context: Optional[Dict[str, Any]] = None) -> List[Evidence]:
|
|
91
|
+
"""Harvests verifiable evidence from the target."""
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
def create_evidence(
|
|
95
|
+
self,
|
|
96
|
+
evidence_type: EvidenceType,
|
|
97
|
+
summary: str,
|
|
98
|
+
raw_payload: Dict[str, Any],
|
|
99
|
+
source_uri: str,
|
|
100
|
+
locator: str,
|
|
101
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
102
|
+
) -> Evidence:
|
|
103
|
+
"""Helper to create standardized, verified Evidence records with provenance."""
|
|
104
|
+
level = EvidenceLevel.get_level_for_type(evidence_type)
|
|
105
|
+
prov = EvidenceProvenance(
|
|
106
|
+
extractor_name=self.extractor_name,
|
|
107
|
+
source_uri=source_uri,
|
|
108
|
+
locator=locator
|
|
109
|
+
)
|
|
110
|
+
return Evidence(
|
|
111
|
+
type=evidence_type,
|
|
112
|
+
level=level,
|
|
113
|
+
summary=summary,
|
|
114
|
+
raw_payload=raw_payload,
|
|
115
|
+
provenance=prov,
|
|
116
|
+
metadata=metadata or {}
|
|
117
|
+
)
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Continuum - Configuration and Manifest Parsers
|
|
3
|
+
======================================================
|
|
4
|
+
Parsers for package.json, pyproject.toml, Cargo.toml, go.mod,
|
|
5
|
+
pubspec.yaml, Dockerfile, docker-compose.yml, and environment templates.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from core.state_models import ManifestInfo
|
|
13
|
+
from extractors.config.secret_sanitizer import SecretSanitizer
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import tomllib # Python 3.11+ standard library
|
|
17
|
+
except ImportError:
|
|
18
|
+
try:
|
|
19
|
+
import tomli as tomllib # Python 3.10 with tomli
|
|
20
|
+
except ImportError:
|
|
21
|
+
tomllib = None # Fallback handled via regex
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ManifestParser:
|
|
25
|
+
"""
|
|
26
|
+
Parses various software package manifests and project configuration files.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def parse_package_json(file_path: str, content: str) -> ManifestInfo:
|
|
31
|
+
try:
|
|
32
|
+
data = json.loads(content)
|
|
33
|
+
except Exception:
|
|
34
|
+
return ManifestInfo(manifest_type="package.json", file_path=file_path)
|
|
35
|
+
|
|
36
|
+
dependencies = {str(k): str(v) for k, v in data.get("dependencies", {}).items()}
|
|
37
|
+
dev_dependencies = {str(k): str(v) for k, v in data.get("devDependencies", {}).items()}
|
|
38
|
+
scripts = {str(k): str(v) for k, v in data.get("scripts", {}).items()}
|
|
39
|
+
|
|
40
|
+
return ManifestInfo(
|
|
41
|
+
manifest_type="package.json",
|
|
42
|
+
file_path=file_path,
|
|
43
|
+
project_name=data.get("name"),
|
|
44
|
+
version=data.get("version"),
|
|
45
|
+
dependencies=SecretSanitizer.sanitize_dict(dependencies),
|
|
46
|
+
dev_dependencies=SecretSanitizer.sanitize_dict(dev_dependencies),
|
|
47
|
+
scripts=scripts
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def parse_pyproject_toml(file_path: str, content: str) -> ManifestInfo:
|
|
52
|
+
project_name = None
|
|
53
|
+
version = None
|
|
54
|
+
dependencies: Dict[str, str] = {}
|
|
55
|
+
dev_dependencies: Dict[str, str] = {}
|
|
56
|
+
scripts: Dict[str, str] = {}
|
|
57
|
+
|
|
58
|
+
if tomllib:
|
|
59
|
+
try:
|
|
60
|
+
data = tomllib.loads(content)
|
|
61
|
+
proj = data.get("project", {})
|
|
62
|
+
project_name = proj.get("name")
|
|
63
|
+
version = proj.get("version")
|
|
64
|
+
|
|
65
|
+
# Project dependencies
|
|
66
|
+
for dep in proj.get("dependencies", []):
|
|
67
|
+
parts = re.split(r"[><=~^! ]+", dep, maxsplit=1)
|
|
68
|
+
name = parts[0].strip()
|
|
69
|
+
ver = dep[len(name):].strip() if len(parts) > 1 else "*"
|
|
70
|
+
dependencies[name] = ver or "*"
|
|
71
|
+
|
|
72
|
+
# Optional/dev dependencies
|
|
73
|
+
for group, deps in proj.get("optional-dependencies", {}).items():
|
|
74
|
+
for dep in deps:
|
|
75
|
+
parts = re.split(r"[><=~^! ]+", dep, maxsplit=1)
|
|
76
|
+
name = parts[0].strip()
|
|
77
|
+
ver = dep[len(name):].strip() if len(parts) > 1 else "*"
|
|
78
|
+
dev_dependencies[f"{group}:{name}"] = ver or "*"
|
|
79
|
+
|
|
80
|
+
# Scripts
|
|
81
|
+
scripts = {str(k): str(v) for k, v in proj.get("scripts", {}).items()}
|
|
82
|
+
|
|
83
|
+
# Poetry fallback if present
|
|
84
|
+
poetry = data.get("tool", {}).get("poetry", {})
|
|
85
|
+
if poetry:
|
|
86
|
+
project_name = project_name or poetry.get("name")
|
|
87
|
+
version = version or poetry.get("version")
|
|
88
|
+
for k, v in poetry.get("dependencies", {}).items():
|
|
89
|
+
if k != "python":
|
|
90
|
+
dependencies[k] = str(v)
|
|
91
|
+
for k, v in poetry.get("group", {}).get("dev", {}).get("dependencies", {}).items():
|
|
92
|
+
dev_dependencies[k] = str(v)
|
|
93
|
+
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
else:
|
|
97
|
+
# Pure Python fallback parser for Python 3.10 without tomli
|
|
98
|
+
name_match = re.search(r'name\s*=\s*["\']([^"\']+)["\']', content)
|
|
99
|
+
if name_match:
|
|
100
|
+
project_name = name_match.group(1)
|
|
101
|
+
ver_match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
|
102
|
+
if ver_match:
|
|
103
|
+
version = ver_match.group(1)
|
|
104
|
+
deps_match = re.search(r'dependencies\s*=\s*\[(.*?)\]', content, re.DOTALL)
|
|
105
|
+
if deps_match:
|
|
106
|
+
for dep in re.findall(r'["\']([^"\']+)["\']', deps_match.group(1)):
|
|
107
|
+
parts = re.split(r"[><=~^! ]+", dep, maxsplit=1)
|
|
108
|
+
name = parts[0].strip()
|
|
109
|
+
ver = dep[len(name):].strip() if len(parts) > 1 else "*"
|
|
110
|
+
dependencies[name] = ver or "*"
|
|
111
|
+
|
|
112
|
+
return ManifestInfo(
|
|
113
|
+
manifest_type="pyproject.toml",
|
|
114
|
+
file_path=file_path,
|
|
115
|
+
project_name=project_name,
|
|
116
|
+
version=version,
|
|
117
|
+
dependencies=dependencies,
|
|
118
|
+
dev_dependencies=dev_dependencies,
|
|
119
|
+
scripts=scripts
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def parse_cargo_toml(file_path: str, content: str) -> ManifestInfo:
|
|
124
|
+
project_name = None
|
|
125
|
+
version = None
|
|
126
|
+
dependencies: Dict[str, str] = {}
|
|
127
|
+
dev_dependencies: Dict[str, str] = {}
|
|
128
|
+
|
|
129
|
+
if tomllib:
|
|
130
|
+
try:
|
|
131
|
+
data = tomllib.loads(content)
|
|
132
|
+
pkg = data.get("package", {})
|
|
133
|
+
project_name = pkg.get("name")
|
|
134
|
+
version = pkg.get("version")
|
|
135
|
+
|
|
136
|
+
for k, v in data.get("dependencies", {}).items():
|
|
137
|
+
dependencies[k] = str(v.get("version", "*")) if isinstance(v, dict) else str(v)
|
|
138
|
+
|
|
139
|
+
for k, v in data.get("dev-dependencies", {}).items():
|
|
140
|
+
dev_dependencies[k] = str(v.get("version", "*")) if isinstance(v, dict) else str(v)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
else:
|
|
144
|
+
# Pure Python fallback parser for Python 3.10 without tomli
|
|
145
|
+
name_match = re.search(r'name\s*=\s*["\']([^"\']+)["\']', content)
|
|
146
|
+
if name_match:
|
|
147
|
+
project_name = name_match.group(1)
|
|
148
|
+
ver_match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
|
149
|
+
if ver_match:
|
|
150
|
+
version = ver_match.group(1)
|
|
151
|
+
in_deps = False
|
|
152
|
+
for line in content.splitlines():
|
|
153
|
+
stripped = line.strip()
|
|
154
|
+
if stripped.startswith("["):
|
|
155
|
+
in_deps = (stripped == "[dependencies]")
|
|
156
|
+
continue
|
|
157
|
+
if in_deps and "=" in stripped:
|
|
158
|
+
k, v = stripped.split("=", 1)
|
|
159
|
+
k = k.strip()
|
|
160
|
+
v_match = re.search(r'["\']([^"\']+)["\']', v)
|
|
161
|
+
dependencies[k] = v_match.group(1) if v_match else "*"
|
|
162
|
+
|
|
163
|
+
return ManifestInfo(
|
|
164
|
+
manifest_type="Cargo.toml",
|
|
165
|
+
file_path=file_path,
|
|
166
|
+
project_name=project_name,
|
|
167
|
+
version=version,
|
|
168
|
+
dependencies=dependencies,
|
|
169
|
+
dev_dependencies=dev_dependencies,
|
|
170
|
+
scripts={}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def parse_go_mod(file_path: str, content: str) -> ManifestInfo:
|
|
175
|
+
module_name = None
|
|
176
|
+
go_version = None
|
|
177
|
+
dependencies: Dict[str, str] = {}
|
|
178
|
+
|
|
179
|
+
lines = content.splitlines()
|
|
180
|
+
for line in lines:
|
|
181
|
+
line = line.strip()
|
|
182
|
+
if line.startswith("module "):
|
|
183
|
+
module_name = line.replace("module ", "").strip()
|
|
184
|
+
elif line.startswith("go "):
|
|
185
|
+
go_version = line.replace("go ", "").strip()
|
|
186
|
+
elif not line.startswith("//") and not line.startswith("require") and not line.startswith(")") and not line.startswith("("):
|
|
187
|
+
parts = line.split()
|
|
188
|
+
if len(parts) >= 2:
|
|
189
|
+
dependencies[parts[0]] = parts[1]
|
|
190
|
+
|
|
191
|
+
scripts = {"go_version": go_version} if go_version else {}
|
|
192
|
+
|
|
193
|
+
return ManifestInfo(
|
|
194
|
+
manifest_type="go.mod",
|
|
195
|
+
file_path=file_path,
|
|
196
|
+
project_name=module_name,
|
|
197
|
+
version=go_version,
|
|
198
|
+
dependencies=dependencies,
|
|
199
|
+
dev_dependencies={},
|
|
200
|
+
scripts=scripts
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def parse_pubspec_yaml(file_path: str, content: str) -> ManifestInfo:
|
|
205
|
+
project_name = None
|
|
206
|
+
version = None
|
|
207
|
+
dependencies: Dict[str, str] = {}
|
|
208
|
+
dev_dependencies: Dict[str, str] = {}
|
|
209
|
+
|
|
210
|
+
current_section = None
|
|
211
|
+
for line in content.splitlines():
|
|
212
|
+
stripped = line.strip()
|
|
213
|
+
if not stripped or stripped.startswith("#"):
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
if not line.startswith(" ") and ":" in stripped:
|
|
217
|
+
key, val = stripped.split(":", 1)
|
|
218
|
+
key = key.strip()
|
|
219
|
+
val = val.strip()
|
|
220
|
+
if key == "name":
|
|
221
|
+
project_name = val
|
|
222
|
+
elif key == "version":
|
|
223
|
+
version = val
|
|
224
|
+
elif key in {"dependencies", "dev_dependencies"}:
|
|
225
|
+
current_section = key
|
|
226
|
+
else:
|
|
227
|
+
current_section = None
|
|
228
|
+
elif current_section and line.startswith(" ") and ":" in stripped:
|
|
229
|
+
key, val = stripped.split(":", 1)
|
|
230
|
+
k = key.strip()
|
|
231
|
+
v = val.strip() or "*"
|
|
232
|
+
if current_section == "dependencies" and k != "flutter":
|
|
233
|
+
dependencies[k] = v
|
|
234
|
+
elif current_section == "dev_dependencies":
|
|
235
|
+
dev_dependencies[k] = v
|
|
236
|
+
|
|
237
|
+
return ManifestInfo(
|
|
238
|
+
manifest_type="pubspec.yaml",
|
|
239
|
+
file_path=file_path,
|
|
240
|
+
project_name=project_name,
|
|
241
|
+
version=version,
|
|
242
|
+
dependencies=dependencies,
|
|
243
|
+
dev_dependencies=dev_dependencies,
|
|
244
|
+
scripts={}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def parse_dockerfile(file_path: str, content: str) -> Dict[str, Any]:
|
|
249
|
+
base_images = []
|
|
250
|
+
exposed_ports = []
|
|
251
|
+
entrypoints = []
|
|
252
|
+
cmd = None
|
|
253
|
+
|
|
254
|
+
for line in content.splitlines():
|
|
255
|
+
stripped = line.strip()
|
|
256
|
+
if stripped.startswith("FROM "):
|
|
257
|
+
base_images.append(stripped.replace("FROM ", "").strip())
|
|
258
|
+
elif stripped.startswith("EXPOSE "):
|
|
259
|
+
ports = stripped.replace("EXPOSE ", "").split()
|
|
260
|
+
exposed_ports.extend(ports)
|
|
261
|
+
elif stripped.startswith("ENTRYPOINT "):
|
|
262
|
+
entrypoints.append(stripped.replace("ENTRYPOINT ", "").strip())
|
|
263
|
+
elif stripped.startswith("CMD "):
|
|
264
|
+
cmd = stripped.replace("CMD ", "").strip()
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
"file_path": file_path,
|
|
268
|
+
"base_images": base_images,
|
|
269
|
+
"exposed_ports": exposed_ports,
|
|
270
|
+
"entrypoint": entrypoints[0] if entrypoints else None,
|
|
271
|
+
"cmd": cmd
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
def parse_env_template(file_path: str, content: str) -> Dict[str, Any]:
|
|
276
|
+
"""Extracts expected environment variable keys while redacting all values."""
|
|
277
|
+
variables: List[str] = []
|
|
278
|
+
for line in content.splitlines():
|
|
279
|
+
stripped = line.strip()
|
|
280
|
+
if stripped and not stripped.startswith("#") and "=" in stripped:
|
|
281
|
+
key, _ = stripped.split("=", 1)
|
|
282
|
+
variables.append(key.strip())
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
"file_path": file_path,
|
|
286
|
+
"variable_keys": variables,
|
|
287
|
+
"sanitized": True
|
|
288
|
+
}
|