chronolith-pro 3.2.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 (36) hide show
  1. chronolith_pro/chronolith/__init__.py +1 -0
  2. chronolith_pro/chronolith/anchor.py +241 -0
  3. chronolith_pro/chronolith/archive_manager.py +77 -0
  4. chronolith_pro/chronolith/automation_common.py +732 -0
  5. chronolith_pro/chronolith/bootstrap_context.py +40 -0
  6. chronolith_pro/chronolith/bootstrap_project.py +178 -0
  7. chronolith_pro/chronolith/context_loader.py +160 -0
  8. chronolith_pro/chronolith/continuity_status.py +86 -0
  9. chronolith_pro/chronolith/continuity_suggest.py +67 -0
  10. chronolith_pro/chronolith/decision_engine.py +137 -0
  11. chronolith_pro/chronolith/discover_project.py +109 -0
  12. chronolith_pro/chronolith/doc_parity_check.py +128 -0
  13. chronolith_pro/chronolith/encoding_sanitizer.py +174 -0
  14. chronolith_pro/chronolith/ene_optimizer.py +76 -0
  15. chronolith_pro/chronolith/heal_parity.py +78 -0
  16. chronolith_pro/chronolith/hook_utils.py +83 -0
  17. chronolith_pro/chronolith/memory_graph_lite.py +64 -0
  18. chronolith_pro/chronolith/notify_webhook.py +143 -0
  19. chronolith_pro/chronolith/run_chronolith_cycle.py +996 -0
  20. chronolith_pro/chronolith/secret_detector.py +69 -0
  21. chronolith_pro/chronolith/sovereign_identity.py +135 -0
  22. chronolith_pro/chronolith/sovereign_vault.py +281 -0
  23. chronolith_pro/chronolith/summarize_memory.py +58 -0
  24. chronolith_pro/chronolith/sync_external_dev_context.py +236 -0
  25. chronolith_pro/chronolith/sync_translations.py +179 -0
  26. chronolith_pro/chronolith/system_membership_check.py +69 -0
  27. chronolith_pro/chronolith/tokenator.py +322 -0
  28. chronolith_pro/chronolith/update_memory.py +81 -0
  29. chronolith_pro/chronolith/vector_store_lite.py +69 -0
  30. chronolith_pro/chronolith/zip_bridge.py +88 -0
  31. chronolith_pro-3.2.0.dist-info/METADATA +68 -0
  32. chronolith_pro-3.2.0.dist-info/RECORD +36 -0
  33. chronolith_pro-3.2.0.dist-info/WHEEL +5 -0
  34. chronolith_pro-3.2.0.dist-info/entry_points.txt +2 -0
  35. chronolith_pro-3.2.0.dist-info/licenses/LICENSE +22 -0
  36. chronolith_pro-3.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ """Core helpers for Chronolith."""
@@ -0,0 +1,241 @@
1
+ """Bitcoin anchoring for the DNA transparency chain (external witness).
2
+
3
+ The signed chain proves lineage *to anyone who trusts the sovereign key*. That is
4
+ the honest limit: a party who controls both the repo and the key can rebuild a
5
+ consistent chain. OpenTimestamps closes that gap — it stamps the chain head into
6
+ the Bitcoin blockchain, so a **skeptical third party** can verify that this exact
7
+ DNA state existed at a given time without trusting the operator at all. And
8
+ because a past Bitcoin timestamp cannot be forged even with a stolen key, the
9
+ anchor also gives forward-security against history rewrites after a key
10
+ compromise.
11
+
12
+ Design (matching the Ethernium Frugal anchor): anchoring is optional
13
+ infrastructure, never a blocker. If the `ots` client is installed it produces a
14
+ real `.ots` proof; if not, a local sovereign record is still written and install
15
+ instructions are printed. No hard dependency, no network call from this module
16
+ itself — the `ots` client owns the calendar/network interaction.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import subprocess
23
+ from datetime import datetime, timezone
24
+ from pathlib import Path
25
+
26
+ ANCHOR_DIRNAME = "anchors"
27
+ _OTS_CANDIDATES = ("ots", "ots.exe")
28
+ # Public OpenTimestamps aggregator calendars (same set the ots client uses).
29
+ _CALENDARS = (
30
+ "https://alice.btc.calendar.opentimestamps.org",
31
+ "https://bob.btc.calendar.opentimestamps.org",
32
+ "https://finney.calendar.eternitywall.com",
33
+ )
34
+
35
+
36
+ def library_available() -> bool:
37
+ try:
38
+ import opentimestamps # noqa: F401
39
+ return True
40
+ except ImportError:
41
+ return False
42
+
43
+
44
+ def stamp_with_library(anchor_path: str | Path, *, timeout: int = 10, calendars=_CALENDARS) -> tuple[bool, str]:
45
+ """Stamp a file via the opentimestamps Python library — the friction-free
46
+ path when the `ots` CLI is broken (its python-bitcoinlib/OpenSSL DLL issue on
47
+ Windows). This is a thin wrapper: the library owns the calendar protocol and
48
+ .ots serialization; we only submit the file's SHA-256 to the aggregator
49
+ calendars and write the returned proof. Requires network to the calendars.
50
+ Never raises."""
51
+ import hashlib
52
+
53
+ try:
54
+ from opentimestamps.calendar import RemoteCalendar
55
+ from opentimestamps.core.op import OpSHA256
56
+ from opentimestamps.core.timestamp import DetachedTimestampFile, Timestamp
57
+ from opentimestamps.core.serialize import BytesSerializationContext
58
+ except ImportError:
59
+ return False, "opentimestamps library not installed (pip install chronolith-pro[anchor])"
60
+
61
+ path = Path(anchor_path)
62
+ digest = hashlib.sha256(path.read_bytes()).digest()
63
+ timestamp = Timestamp(digest)
64
+ reached = []
65
+ for url in calendars:
66
+ try:
67
+ timestamp.merge(RemoteCalendar(url).submit(digest, timeout=timeout))
68
+ reached.append(url)
69
+ except Exception:
70
+ continue # try the next calendar; one is enough
71
+ if not reached:
72
+ return False, "no OpenTimestamps calendar was reachable"
73
+ ctx = BytesSerializationContext()
74
+ DetachedTimestampFile(OpSHA256(), timestamp).serialize(ctx)
75
+ ots_path = path.with_suffix(path.suffix + ".ots")
76
+ ots_path.write_bytes(ctx.getbytes())
77
+ return True, f"{ots_path} (via {len(reached)} calendar(s))"
78
+
79
+
80
+ def anchor_dir(repo_root: str | Path) -> Path:
81
+ return Path(repo_root) / ".chronolith" / ANCHOR_DIRNAME
82
+
83
+
84
+ def build_anchor_record(
85
+ chain_head: dict | None,
86
+ merkle_root: str,
87
+ sovereign_pub_hex: str | None,
88
+ ) -> dict:
89
+ """The record that gets timestamped. Anchoring `chain_head['entry_hash']`
90
+ (which hash-links the entire history) timestamps the whole lineage, not just
91
+ the current root."""
92
+ return {
93
+ "project": "Chronolith (Pro)",
94
+ "anchored_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
95
+ "method": "opentimestamps",
96
+ "merkle_root": merkle_root,
97
+ "chain_seq": chain_head.get("seq") if chain_head else None,
98
+ "chain_head_hash": chain_head.get("entry_hash") if chain_head else None,
99
+ "sovereign_public_key": sovereign_pub_hex,
100
+ "note": (
101
+ "The chain head hash links the full DNA lineage; a Bitcoin timestamp "
102
+ "over this file proves that exact state existed at the confirmed time, "
103
+ "verifiable by anyone with the .ots proof — no trust in the operator."
104
+ ),
105
+ }
106
+
107
+
108
+ def write_anchor(repo_root: str | Path, record: dict) -> Path:
109
+ directory = anchor_dir(repo_root)
110
+ directory.mkdir(parents=True, exist_ok=True)
111
+ tag = (record.get("chain_head_hash") or record.get("merkle_root") or "state")[:12]
112
+ path = directory / f"ANCHOR_{tag}.json"
113
+ path.write_text(json.dumps(record, indent=2, sort_keys=True) + "\n", encoding="utf-8")
114
+ return path
115
+
116
+
117
+ def _find_ots() -> str | None:
118
+ for binary in _OTS_CANDIDATES:
119
+ try:
120
+ probe = subprocess.run([binary, "--version"], capture_output=True, text=True)
121
+ except (OSError, FileNotFoundError):
122
+ continue
123
+ if probe.returncode == 0 or probe.stdout or probe.stderr:
124
+ return binary
125
+ return None
126
+
127
+
128
+ def try_ots_stamp(anchor_path: str | Path) -> tuple[bool, str]:
129
+ """Create a `.ots` proof via the OpenTimestamps client, if installed.
130
+ Returns (stamped, message). Never raises — anchoring must not block a run."""
131
+ binary = _find_ots()
132
+ if not binary:
133
+ return False, "ots client not installed"
134
+ try:
135
+ result = subprocess.run([binary, "stamp", str(anchor_path)], capture_output=True, text=True)
136
+ except OSError as exc:
137
+ return False, f"ots stamp failed: {exc}"
138
+ if result.returncode == 0:
139
+ return True, f"{anchor_path}.ots"
140
+ return False, (result.stderr or result.stdout or "ots stamp returned non-zero").strip()
141
+
142
+
143
+ def inspect_ots_proof(ots_path: str | Path) -> tuple[bool, str]:
144
+ """Local structural check of a .ots proof (no network): confirmed if it
145
+ already carries a Bitcoin attestation, otherwise pending on the calendars.
146
+ Full blockchain confirmation still needs `ots verify` or a Bitcoin node."""
147
+ try:
148
+ from opentimestamps.core.serialize import BytesDeserializationContext
149
+ from opentimestamps.core.timestamp import DetachedTimestampFile
150
+ from opentimestamps.core.notary import BitcoinBlockHeaderAttestation
151
+ except ImportError:
152
+ return False, "opentimestamps library not installed"
153
+ path = Path(ots_path)
154
+ if not path.exists():
155
+ return False, f"proof not found: {path}"
156
+ try:
157
+ ctx = BytesDeserializationContext(path.read_bytes())
158
+ dtf = DetachedTimestampFile.deserialize(ctx)
159
+ except Exception as exc:
160
+ return False, f"malformed .ots proof: {exc}"
161
+ for _msg, attestation in dtf.timestamp.all_attestations():
162
+ if isinstance(attestation, BitcoinBlockHeaderAttestation):
163
+ return True, f"confirmed in Bitcoin block {attestation.height}"
164
+ return False, "pending: calendar attestation present, not yet confirmed on Bitcoin (upgrade later)"
165
+
166
+
167
+ def _walk_timestamps(ts):
168
+ """Yield a timestamp and all of its sub-timestamps (the proof tree)."""
169
+ yield ts
170
+ for _op, sub in ts.ops.items():
171
+ yield from _walk_timestamps(sub)
172
+
173
+
174
+ def upgrade_proof(ots_path: str | Path, *, timeout: int = 10) -> tuple[bool, str]:
175
+ """Fetch Bitcoin attestations from the calendars for a pending proof and
176
+ rewrite the .ots. Returns (upgraded, message). Best-effort and DLL-safe: the
177
+ calendar fetch and attestation reads do NOT import python-bitcoinlib's
178
+ OpenSSL-dependent key module, so this works where the `ots` CLI crashes on
179
+ Windows. Right after stamping the calendar has nothing yet (pending); it
180
+ succeeds a few hours later once Bitcoin confirms."""
181
+ try:
182
+ from opentimestamps.calendar import RemoteCalendar
183
+ from opentimestamps.core.timestamp import DetachedTimestampFile
184
+ from opentimestamps.core.notary import PendingAttestation
185
+ from opentimestamps.core.serialize import (
186
+ BytesDeserializationContext, BytesSerializationContext,
187
+ )
188
+ except ImportError:
189
+ return False, "opentimestamps library not installed"
190
+ path = Path(ots_path)
191
+ try:
192
+ dtf = DetachedTimestampFile.deserialize(BytesDeserializationContext(path.read_bytes()))
193
+ except Exception as exc:
194
+ return False, f"malformed .ots proof: {exc}"
195
+ upgraded = False
196
+ for sub in _walk_timestamps(dtf.timestamp):
197
+ for att in list(sub.attestations):
198
+ if isinstance(att, PendingAttestation):
199
+ uri = att.uri.decode() if isinstance(att.uri, bytes) else att.uri
200
+ try:
201
+ sub.merge(RemoteCalendar(uri).get_timestamp(sub.msg, timeout=timeout))
202
+ upgraded = True
203
+ except Exception:
204
+ continue # not confirmed yet, or calendar unreachable
205
+ if upgraded:
206
+ ctx = BytesSerializationContext()
207
+ dtf.serialize(ctx)
208
+ path.write_bytes(ctx.getbytes())
209
+ return upgraded, "upgraded from calendar" if upgraded else "no upgrade available yet (still pending)"
210
+
211
+
212
+ def verify_via_library(ots_path: str | Path) -> tuple[bool, str]:
213
+ """Full-as-possible verification via the library (no broken CLI): upgrade the
214
+ proof from the calendars, then report whether it now carries a Bitcoin
215
+ attestation. Confirmed = the timestamp is anchored in a Bitcoin block."""
216
+ upgrade_proof(ots_path)
217
+ return inspect_ots_proof(ots_path)
218
+
219
+
220
+ def try_ots_verify(ots_path: str | Path) -> tuple[bool, str]:
221
+ """Verify a `.ots` proof against the Bitcoin blockchain via the client.
222
+ Returns (verified, message)."""
223
+ binary = _find_ots()
224
+ if not binary:
225
+ return False, "ots client not installed (cannot verify the blockchain proof)"
226
+ path = Path(ots_path)
227
+ if not path.exists():
228
+ return False, f"proof not found: {path}"
229
+ try:
230
+ result = subprocess.run([binary, "verify", str(path)], capture_output=True, text=True)
231
+ except OSError as exc:
232
+ return False, f"ots verify failed: {exc}"
233
+ output = (result.stdout + result.stderr).strip()
234
+ # `ots verify` prints "Success! Bitcoin block ..." on a confirmed proof and a
235
+ # "Pending" notice while the calendar attestation awaits confirmation. Check
236
+ # pending FIRST — the pending message also mentions "Bitcoin".
237
+ if "pending" in output.lower():
238
+ return False, "pending: calendar attestation not yet confirmed on Bitcoin (retry in a few hours)"
239
+ if result.returncode == 0 and ("Success" in output or "Bitcoin block" in output):
240
+ return True, output
241
+ return False, output or "verification failed"
@@ -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())