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
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
+ }