chronolith-omega 3.2.0__tar.gz

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 (40) hide show
  1. chronolith_omega-3.2.0/LICENSE +22 -0
  2. chronolith_omega-3.2.0/PKG-INFO +56 -0
  3. chronolith_omega-3.2.0/README.md +37 -0
  4. chronolith_omega-3.2.0/chronolith_omega/chronolith/__init__.py +1 -0
  5. chronolith_omega-3.2.0/chronolith_omega/chronolith/archive_manager.py +77 -0
  6. chronolith_omega-3.2.0/chronolith_omega/chronolith/automation_common.py +304 -0
  7. chronolith_omega-3.2.0/chronolith_omega/chronolith/bootstrap_context.py +40 -0
  8. chronolith_omega-3.2.0/chronolith_omega/chronolith/bootstrap_project.py +178 -0
  9. chronolith_omega-3.2.0/chronolith_omega/chronolith/cognitive_map.py +111 -0
  10. chronolith_omega-3.2.0/chronolith_omega/chronolith/context_loader.py +160 -0
  11. chronolith_omega-3.2.0/chronolith_omega/chronolith/continuity_status.py +86 -0
  12. chronolith_omega-3.2.0/chronolith_omega/chronolith/continuity_suggest.py +67 -0
  13. chronolith_omega-3.2.0/chronolith_omega/chronolith/decision_engine.py +136 -0
  14. chronolith_omega-3.2.0/chronolith_omega/chronolith/discover_project.py +109 -0
  15. chronolith_omega-3.2.0/chronolith_omega/chronolith/doc_parity_check.py +128 -0
  16. chronolith_omega-3.2.0/chronolith_omega/chronolith/encoding_sanitizer.py +174 -0
  17. chronolith_omega-3.2.0/chronolith_omega/chronolith/heal_parity.py +78 -0
  18. chronolith_omega-3.2.0/chronolith_omega/chronolith/hook_utils.py +89 -0
  19. chronolith_omega-3.2.0/chronolith_omega/chronolith/memory_graph_lite.py +64 -0
  20. chronolith_omega-3.2.0/chronolith_omega/chronolith/notify_webhook.py +143 -0
  21. chronolith_omega-3.2.0/chronolith_omega/chronolith/omega_engine.py +138 -0
  22. chronolith_omega-3.2.0/chronolith_omega/chronolith/run_chronolith_cycle.py +18 -0
  23. chronolith_omega-3.2.0/chronolith_omega/chronolith/run_chronolith_omega.py +215 -0
  24. chronolith_omega-3.2.0/chronolith_omega/chronolith/secret_detector.py +49 -0
  25. chronolith_omega-3.2.0/chronolith_omega/chronolith/summarize_memory.py +58 -0
  26. chronolith_omega-3.2.0/chronolith_omega/chronolith/sync_external_dev_context.py +236 -0
  27. chronolith_omega-3.2.0/chronolith_omega/chronolith/sync_translations.py +79 -0
  28. chronolith_omega-3.2.0/chronolith_omega/chronolith/system_membership_check.py +76 -0
  29. chronolith_omega-3.2.0/chronolith_omega/chronolith/update_memory.py +81 -0
  30. chronolith_omega-3.2.0/chronolith_omega/chronolith/vector_store_lite.py +69 -0
  31. chronolith_omega-3.2.0/chronolith_omega.egg-info/PKG-INFO +56 -0
  32. chronolith_omega-3.2.0/chronolith_omega.egg-info/SOURCES.txt +38 -0
  33. chronolith_omega-3.2.0/chronolith_omega.egg-info/dependency_links.txt +1 -0
  34. chronolith_omega-3.2.0/chronolith_omega.egg-info/entry_points.txt +2 -0
  35. chronolith_omega-3.2.0/chronolith_omega.egg-info/requires.txt +5 -0
  36. chronolith_omega-3.2.0/chronolith_omega.egg-info/top_level.txt +1 -0
  37. chronolith_omega-3.2.0/pyproject.toml +35 -0
  38. chronolith_omega-3.2.0/setup.cfg +4 -0
  39. chronolith_omega-3.2.0/tests/test_omega_logic.py +84 -0
  40. chronolith_omega-3.2.0/tests/test_parity_logic.py +35 -0
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Ethernium (X: @Steveblackbeard)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: chronolith-omega
3
+ Version: 3.2.0
4
+ Summary: Chronolith (Omega): Enterprise Oracle. Advanced RAG, cognitive maps, and proactive impact analysis.
5
+ Author: SteveBlackbeard
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/SteveBlackbeard/CHRONOLITH-by-Ethernium
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: typer>=0.9.0
14
+ Requires-Dist: rich>=13.0.0
15
+ Requires-Dist: chromadb>=0.4.0
16
+ Requires-Dist: networkx>=3.0
17
+ Requires-Dist: sentence-transformers
18
+ Dynamic: license-file
19
+
20
+ # Chronolith Omega
21
+
22
+ ![Version](https://img.shields.io/badge/version-3.0.3-blueviolet)
23
+
24
+ Chronolith Omega is part of the Ethernium Chronolith Python runtime.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install chronolith-omega
30
+ ```
31
+
32
+ ## Commands
33
+
34
+ ```bash
35
+ chronolith-omega --help
36
+ ```
37
+
38
+ ## Role
39
+
40
+ Advanced chronolith with RAG-oriented dependencies, cognitive maps, graph analysis, and higher-cost enterprise workflows.
41
+
42
+ ## Governance Boundary
43
+
44
+ Chronolith is the Python runtime and governance kernel. Chronolith Conekta is an external visual control surface, and AgentOps is an extractable incubated tool.
45
+
46
+ ## Quality Gate
47
+
48
+ Before release, run the root repository checks:
49
+
50
+ ```bash
51
+ python scripts/health_guard.py --strict
52
+ python scripts/golden_baseline.py verify
53
+ pytest -q
54
+ python -m build
55
+ python -m twine check dist\*
56
+ ```
@@ -0,0 +1,37 @@
1
+ # Chronolith Omega
2
+
3
+ ![Version](https://img.shields.io/badge/version-3.0.3-blueviolet)
4
+
5
+ Chronolith Omega is part of the Ethernium Chronolith Python runtime.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install chronolith-omega
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ ```bash
16
+ chronolith-omega --help
17
+ ```
18
+
19
+ ## Role
20
+
21
+ Advanced chronolith with RAG-oriented dependencies, cognitive maps, graph analysis, and higher-cost enterprise workflows.
22
+
23
+ ## Governance Boundary
24
+
25
+ Chronolith is the Python runtime and governance kernel. Chronolith Conekta is an external visual control surface, and AgentOps is an extractable incubated tool.
26
+
27
+ ## Quality Gate
28
+
29
+ Before release, run the root repository checks:
30
+
31
+ ```bash
32
+ python scripts/health_guard.py --strict
33
+ python scripts/golden_baseline.py verify
34
+ pytest -q
35
+ python -m build
36
+ python -m twine check dist\*
37
+ ```
@@ -0,0 +1 @@
1
+ """Core helpers for Chronolith."""
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ archive_manager.py — CHRONOLITH Pro
4
+ ==========================================
5
+ Systemic Log Rotation & Context Archiving.
6
+
7
+ Moves historical outputs and oversized log files from .chronolith/
8
+ to .chronolith/archive/ to prevent context window saturation while
9
+ preserving the project's 'Genetic Code'.
10
+ """
11
+
12
+ import os
13
+ import json
14
+ import shutil
15
+ import datetime
16
+ from pathlib import Path
17
+
18
+ # Config
19
+ MAX_LOG_SIZE_BYTES = 50 * 1024 # 50KB threshold for rotation
20
+ ARCHIVE_DIR_NAME = "archive"
21
+
22
+ def rotate_logs(repo_root: Path):
23
+ print(f"[*] Archive Manager: Evaluating log health in {repo_root}...")
24
+
25
+ chronolith_dir = repo_root / ".chronolith"
26
+ archive_dir = chronolith_dir / ARCHIVE_DIR_NAME
27
+
28
+ if not chronolith_dir.exists():
29
+ print("[!] No .chronolith directory found. Skipping archiving.")
30
+ return
31
+
32
+ archive_dir.mkdir(parents=True, exist_ok=True)
33
+
34
+ # Files to track for rotation
35
+ targets = [
36
+ "DECISIONS_LOG.md",
37
+ "TIMELINE.md",
38
+ ]
39
+
40
+ for target in targets:
41
+ file_path = chronolith_dir / target
42
+ if file_path.exists() and file_path.stat().st_size > MAX_LOG_SIZE_BYTES:
43
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
44
+ archive_name = f"{target.replace('.md', '')}_{timestamp}.md"
45
+ dest_path = archive_dir / archive_name
46
+
47
+ print(f" [>] Rotating {target} (Size: {file_path.stat().st_size} bytes)")
48
+ shutil.copy2(file_path, dest_path)
49
+
50
+ # Reset the active file with a link to the archive
51
+ with open(file_path, "w", encoding="utf-8") as f:
52
+ f.write(f"# {target.replace('.md', '').replace('_', ' ')}\n")
53
+ f.write(f"> [!] LOG ROTATED on {timestamp}. See archive/{archive_name} for history.\n\n")
54
+
55
+ # Cleanup old cycle reports in outputs
56
+ outputs_dir = repo_root / "outputs" / "chronolith"
57
+ if outputs_dir.exists():
58
+ # Keep only the last 10 reports, archive the rest? Or just prune.
59
+ # For '' we archive them.
60
+ report_archive = archive_dir / "reports"
61
+ report_archive.mkdir(exist_ok=True)
62
+
63
+ all_reports = sorted(list(outputs_dir.glob("*.json")), key=os.path.getmtime)
64
+ if len(all_reports) > 10:
65
+ to_archive = all_reports[:-10]
66
+ print(f" [>] Archiving {len(to_archive)} historical reports...")
67
+ for r in to_archive:
68
+ shutil.move(str(r), str(report_archive / r.name))
69
+
70
+ print("[✔] Archiving Cycle Complete.")
71
+
72
+ if __name__ == "__main__":
73
+ import argparse
74
+ parser = argparse.ArgumentParser(description="Rotate and archive chronolith logs.")
75
+ parser.add_argument("--repo-root", default=".", help="Root directory of the project")
76
+ args = parser.parse_args()
77
+ rotate_logs(Path(args.repo_root).resolve())
@@ -0,0 +1,304 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from copy import deepcopy
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ class Color:
11
+ """Terminal color constants for standardized elite CLI feedback."""
12
+ PURPLE = "\033[95m"
13
+ CYAN = "\033[96m"
14
+ DARKCYAN = "\033[36m"
15
+ BLUE = "\033[94m"
16
+ GREEN = "\033[92m"
17
+ YELLOW = "\033[93m"
18
+ RED = "\033[91m"
19
+ WHITE = "\033[97m"
20
+ BOLD = "\033[1m"
21
+ UNDERLINE = "\033[4m"
22
+ END = "\033[0m"
23
+
24
+
25
+ def echo(text: str, color: str = "") -> None:
26
+ """Prints text to the console with optional color formatting.
27
+
28
+ Args:
29
+ text: The string to be printed.
30
+ color: The ANSI color code to apply (e.g., Color.GREEN).
31
+ """
32
+ if color:
33
+ print(f"{color}{text}{Color.END}")
34
+ else:
35
+ print(text)
36
+
37
+
38
+ DEFAULT_CONFIG = {
39
+ "template_name": "CHRONOLITH",
40
+ "project_name": "YOUR_PROJECT",
41
+ "project_slug": "your_project",
42
+ "context_file": "PROJECT_CONTEXT.md",
43
+ "state_file": "STATE.json",
44
+ "roadmap_file": "ROADMAP.md",
45
+ "chronolith_dir": ".chronolith",
46
+ "outputs_dir": "outputs/chronolith",
47
+ "external_docs": {
48
+ "enabled": False,
49
+ "folder_name": "YOUR_PROJECTDEV",
50
+ "root_override": "",
51
+ },
52
+ "metadata": {
53
+ "generated_by": "Chronolith by Ethernium",
54
+ "tool_version": "5.0.0",
55
+ "creator": "@Steveblackbeard",
56
+ "include_in_reports": True,
57
+ },
58
+ }
59
+
60
+ ALLOWED_MEMBERSHIP_STATUSES = [
61
+ "canonical",
62
+ "bridge",
63
+ "archive_source",
64
+ "external_optional",
65
+ ]
66
+
67
+
68
+ def utc_now_iso() -> str:
69
+ return datetime.now(timezone.utc).isoformat()
70
+
71
+
72
+ def resolve_repo_root(repo_root: str | Path | None, current_file: str | Path) -> Path:
73
+ if repo_root:
74
+ resolved = Path(repo_root).resolve()
75
+ else:
76
+ resolved = Path(current_file).resolve().parents[2]
77
+
78
+ if not (resolved / "chronolith.json").exists() and not (resolved / ".chronolith").exists():
79
+ raise ValueError(f"Security Warning: Path '{resolved}' does not appear to be a CHRONOLITH repository.")
80
+
81
+ return resolved
82
+
83
+
84
+ def load_config(repo_root: Path) -> dict:
85
+ """Loads and merges the chronolith.json configuration.
86
+
87
+ Args:
88
+ repo_root: The filesystem path to the root of the project.
89
+
90
+ Returns:
91
+ A dictionary containing the merged project configuration.
92
+ """
93
+ config_file = repo_root / "chronolith.json"
94
+ payload = {}
95
+ if config_file.exists():
96
+ payload = json.loads(config_file.read_text(encoding="utf-8"))
97
+
98
+ return _deep_merge(DEFAULT_CONFIG, payload)
99
+
100
+
101
+ def save_config(repo_root: Path, config: dict) -> None:
102
+ """Saves the chronolith.json configuration to the repository root.
103
+
104
+ Args:
105
+ repo_root: The filesystem path to the root of the project.
106
+ config: The configuration dictionary to persist.
107
+ """
108
+ config_file = repo_root / "chronolith.json"
109
+ config_file.write_text(json.dumps(config, indent=2), encoding="utf-8")
110
+
111
+
112
+ def config_path(repo_root: str | Path) -> Path:
113
+ return Path(repo_root) / "chronolith.json"
114
+
115
+
116
+ def read_text(path: str | Path) -> str:
117
+ return Path(path).read_text(encoding="utf-8", errors="ignore")
118
+
119
+
120
+ def write_text(path: str | Path, content: str) -> None:
121
+ target = Path(path)
122
+ target.parent.mkdir(parents=True, exist_ok=True)
123
+ target.write_text(content.rstrip() + "\n", encoding="utf-8")
124
+
125
+
126
+ def read_json(path: str | Path, default: Any | None = None) -> Any:
127
+ file_path = Path(path)
128
+ if not file_path.exists():
129
+ return deepcopy(default)
130
+ return json.loads(file_path.read_text(encoding="utf-8"))
131
+
132
+
133
+ def write_json(path: str | Path, payload: Any) -> None:
134
+ target = Path(path)
135
+ target.parent.mkdir(parents=True, exist_ok=True)
136
+ target.write_text(json.dumps(payload, indent=2, ensure_ascii=True), encoding="utf-8")
137
+
138
+
139
+ def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
140
+ merged = deepcopy(base)
141
+ for key, value in override.items():
142
+ if isinstance(value, dict) and isinstance(merged.get(key), dict):
143
+ merged[key] = _deep_merge(merged[key], value)
144
+ else:
145
+ merged[key] = value
146
+ return merged
147
+
148
+
149
+ def build_context_snapshot(repo_root: Path, external_root_override: Path | None = None) -> dict:
150
+ """Builds a comprehensive snapshot of the project context from multiple sources.
151
+
152
+ Args:
153
+ repo_root: The root directory of the project.
154
+ external_root_override: Optional path to an external documentation root.
155
+
156
+ Returns:
157
+ A dictionary containing the aggregated 'truth' of the project.
158
+ """
159
+ config = load_config(repo_root)
160
+ state = read_json(state_path(repo_root, config), {})
161
+ context_text = read_text(context_path(repo_root, config))
162
+ roadmap_text = read_text(roadmap_path(repo_root, config))
163
+
164
+ return {
165
+ "project_name": config.get("project_name"),
166
+ "project_slug": config.get("project_slug"),
167
+ "phase": state.get("status", "unknown"),
168
+ "next_actions": state.get("next_actions", []),
169
+ "context_summary": context_text[:2000],
170
+ "roadmap_summary": roadmap_text[:2000],
171
+ "last_decision": state.get("last_decision", "none"),
172
+ "timestamp": utc_now_iso(),
173
+ }
174
+
175
+
176
+ def context_path(repo_root: str | Path, config: dict[str, Any] | None = None) -> Path:
177
+ config = config or load_config(repo_root)
178
+ return Path(repo_root) / config["context_file"]
179
+
180
+
181
+ def state_path(repo_root: str | Path, config: dict[str, Any] | None = None) -> Path:
182
+ config = config or load_config(repo_root)
183
+ return Path(repo_root) / config["state_file"]
184
+
185
+
186
+ def roadmap_path(repo_root: str | Path, config: dict[str, Any] | None = None) -> Path:
187
+ config = config or load_config(repo_root)
188
+ return Path(repo_root) / config["roadmap_file"]
189
+
190
+
191
+ def chronolith_dir(repo_root: str | Path, config: dict[str, Any] | None = None) -> Path:
192
+ config = config or load_config(repo_root)
193
+ return Path(repo_root) / config["chronolith_dir"]
194
+
195
+
196
+ def chronolith_doc_path(repo_root: str | Path, name: str, config: dict[str, Any] | None = None) -> Path:
197
+ return chronolith_dir(repo_root, config) / name
198
+
199
+
200
+ def outputs_dir(repo_root: str | Path, config: dict[str, Any] | None = None) -> Path:
201
+ config = config or load_config(repo_root)
202
+ return Path(repo_root) / config["outputs_dir"]
203
+
204
+
205
+ def bootstrap_output_path(repo_root: str | Path, config: dict[str, Any] | None = None) -> Path:
206
+ return outputs_dir(repo_root, config) / "context_bootstrap_summary.json"
207
+
208
+
209
+ def chronolith_report_path(repo_root: str | Path, config: dict[str, Any] | None = None) -> Path:
210
+ return outputs_dir(repo_root, config) / "chronolith_cycle_report.json"
211
+
212
+
213
+ def dependency_map_path(repo_root: str | Path, config: dict[str, Any] | None = None) -> Path:
214
+ return chronolith_dir(repo_root, config) / "registry" / "document_dependency_map.json"
215
+
216
+
217
+ def membership_registry_path(repo_root: str | Path, config: dict[str, Any] | None = None) -> Path:
218
+ return chronolith_dir(repo_root, config) / "registry" / "system_membership_registry.json"
219
+
220
+
221
+ def load_dependency_map(repo_root: str | Path, config: dict[str, Any] | None = None) -> dict[str, Any]:
222
+ return read_json(dependency_map_path(repo_root, config), {"version": "1.0", "documents": []}) or {
223
+ "version": "1.0",
224
+ "documents": [],
225
+ }
226
+
227
+
228
+ def load_membership_registry(repo_root: str | Path, config: dict[str, Any] | None = None) -> dict[str, Any]:
229
+ return read_json(
230
+ membership_registry_path(repo_root, config),
231
+ {
232
+ "version": "1.0",
233
+ "allowed_statuses": ALLOWED_MEMBERSHIP_STATUSES,
234
+ "entries": [],
235
+ },
236
+ ) or {
237
+ "version": "1.0",
238
+ "allowed_statuses": ALLOWED_MEMBERSHIP_STATUSES,
239
+ "entries": [],
240
+ }
241
+
242
+
243
+ def is_ignored(repo_root: str | Path, rel_path: str) -> bool:
244
+ ignore_file = Path(repo_root) / ".chronolithignore"
245
+ if not ignore_file.exists():
246
+ return False
247
+
248
+ import fnmatch
249
+ patterns = [line.strip() for line in ignore_file.read_text(encoding="utf-8").splitlines() if line.strip() and not line.startswith("#")]
250
+ for pattern in patterns:
251
+ if fnmatch.fnmatch(rel_path, pattern):
252
+ return True
253
+ return False
254
+
255
+
256
+ def external_root(
257
+ repo_root: str | Path,
258
+ config: dict[str, Any] | None = None,
259
+ override: str | Path | None = None,
260
+ ) -> Path | None:
261
+ config = config or load_config(repo_root)
262
+ external_cfg = config.get("external_docs", {})
263
+ if override:
264
+ return Path(override).resolve()
265
+ root_override = str(external_cfg.get("root_override") or "").strip()
266
+ if root_override:
267
+ return Path(root_override).resolve()
268
+ if not external_cfg.get("enabled"):
269
+ return None
270
+ return Path(repo_root).resolve().parent / str(external_cfg.get("folder_name") or "PROJECTDEV")
271
+
272
+
273
+ def calculate_sha256(path: str | Path) -> str:
274
+ """Calculates the SHA-256 hash of a file for high-fidelity DNA synthesis."""
275
+ import hashlib
276
+
277
+ p = Path(path)
278
+ if not p.exists():
279
+ return ""
280
+ return hashlib.sha256(p.read_bytes()).hexdigest()
281
+
282
+
283
+ def build_merkle_tree(hashes: list[str]) -> str:
284
+ """RFC 6962 compliant Merkle Tree with leaf/node prefix hardening."""
285
+ import hashlib
286
+
287
+ if not hashes:
288
+ return "0" * 64
289
+
290
+ current_level = [hashlib.sha256(b"\x00" + h.encode("utf-8")).hexdigest() for h in sorted(hashes)]
291
+ while len(current_level) > 1:
292
+ next_level = []
293
+ if len(current_level) % 2 != 0:
294
+ current_level.append(current_level[-1])
295
+
296
+ for i in range(0, len(current_level), 2):
297
+ combined = b"\x01" + (current_level[i] + current_level[i + 1]).encode("utf-8")
298
+ next_level.append(hashlib.sha256(combined).hexdigest())
299
+ current_level = next_level
300
+
301
+ return current_level[0]
302
+
303
+
304
+ build_merkle_root = build_merkle_tree
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ try:
8
+ from .automation_common import bootstrap_output_path, resolve_repo_root
9
+ from .context_loader import build_context_snapshot, summarize_snapshot
10
+ except (ImportError, ValueError):
11
+ from automation_common import bootstrap_output_path, resolve_repo_root
12
+ from context_loader import build_context_snapshot, summarize_snapshot
13
+
14
+
15
+ def parse_args() -> argparse.Namespace:
16
+ parser = argparse.ArgumentParser(description="Build a chronolith snapshot for the current project.")
17
+ parser.add_argument("--repo-root", default=None)
18
+ parser.add_argument("--output-json", default=None)
19
+ parser.add_argument("--external-root", default=None)
20
+ parser.add_argument("--no-print", action="store_true")
21
+ return parser.parse_args()
22
+
23
+
24
+ def main() -> None:
25
+ args = parse_args()
26
+ repo_root = resolve_repo_root(args.repo_root, __file__)
27
+ snapshot = build_context_snapshot(repo_root, args.external_root)
28
+ output_path = bootstrap_output_path(repo_root)
29
+ if args.output_json:
30
+ output_path = Path(args.output_json)
31
+ if not output_path.is_absolute():
32
+ output_path = repo_root / output_path
33
+ output_path.parent.mkdir(parents=True, exist_ok=True)
34
+ output_path.write_text(json.dumps(snapshot, indent=2, ensure_ascii=True), encoding="utf-8")
35
+ if not args.no_print:
36
+ print(summarize_snapshot(snapshot))
37
+
38
+
39
+ if __name__ == "__main__":
40
+ main()