chronolith-lite 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.
- chronolith_lite-3.2.0/LICENSE +22 -0
- chronolith_lite-3.2.0/PKG-INFO +53 -0
- chronolith_lite-3.2.0/README.md +37 -0
- chronolith_lite-3.2.0/chronolith_lite/chronolith/run_chronolith_lite.py +362 -0
- chronolith_lite-3.2.0/chronolith_lite/chronolith/sync_translations.py +79 -0
- chronolith_lite-3.2.0/chronolith_lite.egg-info/PKG-INFO +53 -0
- chronolith_lite-3.2.0/chronolith_lite.egg-info/SOURCES.txt +12 -0
- chronolith_lite-3.2.0/chronolith_lite.egg-info/dependency_links.txt +1 -0
- chronolith_lite-3.2.0/chronolith_lite.egg-info/entry_points.txt +2 -0
- chronolith_lite-3.2.0/chronolith_lite.egg-info/requires.txt +2 -0
- chronolith_lite-3.2.0/chronolith_lite.egg-info/top_level.txt +1 -0
- chronolith_lite-3.2.0/pyproject.toml +32 -0
- chronolith_lite-3.2.0/setup.cfg +4 -0
- chronolith_lite-3.2.0/tests/test_lite_logic.py +126 -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,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chronolith-lite
|
|
3
|
+
Version: 3.2.0
|
|
4
|
+
Summary: Chronolith (Lite): A professional AI chronolith framework for zero-friction context handoffs.
|
|
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.8
|
|
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
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# Chronolith Lite
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
Chronolith Lite is part of the Ethernium Chronolith Python runtime.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install chronolith-lite
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
chronolith-lite --help
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Role
|
|
36
|
+
|
|
37
|
+
Minimal local chronolith for fast handoffs, state checks, and lightweight CLI workflows.
|
|
38
|
+
|
|
39
|
+
## Governance Boundary
|
|
40
|
+
|
|
41
|
+
Chronolith is the Python runtime and governance kernel. Chronolith Conekta is an external visual control surface, and AgentOps is an extractable incubated tool.
|
|
42
|
+
|
|
43
|
+
## Quality Gate
|
|
44
|
+
|
|
45
|
+
Before release, run the root repository checks:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python scripts/health_guard.py --strict
|
|
49
|
+
python scripts/golden_baseline.py verify
|
|
50
|
+
pytest -q
|
|
51
|
+
python -m build
|
|
52
|
+
python -m twine check dist\*
|
|
53
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Chronolith Lite
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Chronolith Lite is part of the Ethernium Chronolith Python runtime.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install chronolith-lite
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
chronolith-lite --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Role
|
|
20
|
+
|
|
21
|
+
Minimal local chronolith for fast handoffs, state checks, and lightweight CLI 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,362 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
15
|
+
from rich import print as rprint
|
|
16
|
+
|
|
17
|
+
def _configure_stdio_for_unicode() -> bool:
|
|
18
|
+
for stream_name in ("stdout", "stderr"):
|
|
19
|
+
stream = getattr(sys, stream_name, None)
|
|
20
|
+
if stream is not None and hasattr(stream, "reconfigure"):
|
|
21
|
+
try:
|
|
22
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
encoding = ((getattr(sys.stdout, "encoding", None) or "") + (getattr(sys.stderr, "encoding", None) or "")).lower()
|
|
26
|
+
return "utf" in encoding
|
|
27
|
+
|
|
28
|
+
UNICODE_OK = _configure_stdio_for_unicode()
|
|
29
|
+
|
|
30
|
+
LITE_ICON = "LITE"
|
|
31
|
+
|
|
32
|
+
# CHRONOLITH Lite (v2.1.0) - Evolution DNA Guardian
|
|
33
|
+
# -------------------------------------------------------------
|
|
34
|
+
# [!] Industrial Grade Refactor: Typer CLI, Rich UI, SHA-256 Signatures, Structured Logs.
|
|
35
|
+
|
|
36
|
+
app = typer.Typer(
|
|
37
|
+
help=f"{LITE_ICON} Chronolith Lite: The professional AI chronolith framework.",
|
|
38
|
+
add_completion=False,
|
|
39
|
+
no_args_is_help=True,
|
|
40
|
+
)
|
|
41
|
+
console = Console(emoji=False)
|
|
42
|
+
def setup_logger(repo_root: Path):
|
|
43
|
+
log_dir = repo_root / ".chronolith" / "logs"
|
|
44
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
log_file = log_dir / f"chronolith_{datetime.now().strftime('%Y%m%d')}.jsonl"
|
|
47
|
+
|
|
48
|
+
# Custom JSON Formatter
|
|
49
|
+
class JsonFormatter(logging.Formatter):
|
|
50
|
+
def format(self, record):
|
|
51
|
+
log_record = {
|
|
52
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
53
|
+
"level": record.levelname,
|
|
54
|
+
"message": record.getMessage(),
|
|
55
|
+
"module": record.module
|
|
56
|
+
}
|
|
57
|
+
if hasattr(record, "dna_hash"):
|
|
58
|
+
log_record["dna_hash"] = record.dna_hash
|
|
59
|
+
return json.dumps(log_record)
|
|
60
|
+
|
|
61
|
+
logger = logging.getLogger("chronolith")
|
|
62
|
+
logger.setLevel(logging.INFO)
|
|
63
|
+
|
|
64
|
+
if not logger.handlers:
|
|
65
|
+
fh = logging.FileHandler(log_file)
|
|
66
|
+
fh.setFormatter(JsonFormatter())
|
|
67
|
+
logger.addHandler(fh)
|
|
68
|
+
return logger
|
|
69
|
+
|
|
70
|
+
ASCII_ART = """
|
|
71
|
+
[bold cyan]
|
|
72
|
+
______ ____ _ __ ______ ____ _ __ _ __ ____ ______ __ __
|
|
73
|
+
/ ____// __ \\ / | / //_ __// _// | / // / / //_ __//_ __/ \\ \\/ /
|
|
74
|
+
/ / / / / // |/ / / / / / / |/ // / / / / / / / \\ /
|
|
75
|
+
/ /___ / /_/ // /| / / / _/ / / /| // /__/ / / / / / / /
|
|
76
|
+
\\____/ \\____//_/ |_/ /_/ /___//_/ |_/ \\____/ /_/ /_/ /_/
|
|
77
|
+
|
|
78
|
+
LEGACY [bold white]v2.1.0[/bold white] | [italic green]Industrial Evolution DNA Guardian[/italic green]
|
|
79
|
+
[/bold cyan]
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def calculate_sha256(path: Path | str) -> str:
|
|
83
|
+
path = Path(path) # accept str or Path, matching Pro/Omega's signature
|
|
84
|
+
if not (path.exists() and path.is_file()): return ""
|
|
85
|
+
|
|
86
|
+
# Cross-Platform DNA Protection: Normalize to LF (\n), rstrip lines, and exclude self-referencing badges
|
|
87
|
+
try:
|
|
88
|
+
content = path.read_text(encoding="utf-8")
|
|
89
|
+
except UnicodeDecodeError:
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
if path.suffix.lower() == ".md":
|
|
93
|
+
lines = content.splitlines()
|
|
94
|
+
filtered = []
|
|
95
|
+
for l in lines:
|
|
96
|
+
if "DNA CRYSTAL" in l or "img.shields.io/badge/DNA--Crystallized" in l:
|
|
97
|
+
continue
|
|
98
|
+
filtered.append(l.rstrip())
|
|
99
|
+
# Re-join with strict LF and final strip to ensure identical hashes everywhere (Windows/Linux)
|
|
100
|
+
return hashlib.sha256("\n".join(filtered).strip().encode("utf-8")).hexdigest()
|
|
101
|
+
|
|
102
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
103
|
+
|
|
104
|
+
def sign_state(data: dict) -> str:
|
|
105
|
+
"""Generates a SHA-256 signature for the state record.
|
|
106
|
+
Hardware binding has been removed to support Open Source collaboration across
|
|
107
|
+
different developer machines while maintaining semantic integrity.
|
|
108
|
+
"""
|
|
109
|
+
serialized = json.dumps({k: v for k, v in data.items() if k != "signature"}, sort_keys=True)
|
|
110
|
+
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
|
111
|
+
|
|
112
|
+
# Merkle leaf format tag. Bumped when the leaf derivation changes so an upgrade
|
|
113
|
+
# re-baselines once instead of raising a false drift alarm.
|
|
114
|
+
LEAF_FORMAT = "path-bound-v1"
|
|
115
|
+
|
|
116
|
+
def verify_signature(state: dict) -> bool:
|
|
117
|
+
"""Recompute the state signature and compare it to the stored one.
|
|
118
|
+
|
|
119
|
+
Without this, `sign_state` is write-only: a signature is stamped but never
|
|
120
|
+
checked, so STATE.json can be edited by hand undetected. A missing signature
|
|
121
|
+
is treated as unverifiable (returns True) so states written before signing
|
|
122
|
+
are not falsely rejected; a present-but-mismatched signature means the record
|
|
123
|
+
was tampered with (returns False)."""
|
|
124
|
+
stored = state.get("signature")
|
|
125
|
+
if not stored:
|
|
126
|
+
return True
|
|
127
|
+
return sign_state(state) == stored
|
|
128
|
+
|
|
129
|
+
def leaf_hash(rel_path: str, content_hash: str) -> str:
|
|
130
|
+
"""Bind the filename into the Merkle leaf.
|
|
131
|
+
|
|
132
|
+
Content-only leaves make the root blind to structure: a rename or a content
|
|
133
|
+
swap between two files leaves the root unchanged. Hashing the path with the
|
|
134
|
+
content makes both mutations alter the root."""
|
|
135
|
+
return hashlib.sha256(f"{rel_path}\n{content_hash}".encode("utf-8")).hexdigest()
|
|
136
|
+
|
|
137
|
+
def ensure_file(path: Path, template: str, description: str):
|
|
138
|
+
if not path.exists():
|
|
139
|
+
console.log(f"[yellow][?][/yellow] Missing Nucleotide: [bold]{description}[/bold]")
|
|
140
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
path.write_text(template, encoding="utf-8")
|
|
142
|
+
console.log(f" [green][OK][/green] Re-synthesized: [italic]{path.name}[/italic]")
|
|
143
|
+
return True
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def install_hooks(repo_root: Path):
|
|
147
|
+
hook_path = repo_root / ".git" / "hooks" / "pre-push"
|
|
148
|
+
if not (repo_root / ".git").exists():
|
|
149
|
+
console.log("[bold red][!][/bold red] ERROR: Not a git repository. Cannot install hooks.")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# Vector 1: Prevenir secuestro de $PATH inyectando ruta absoluta al binario
|
|
153
|
+
abs_python = sys.executable.replace("\\", "/")
|
|
154
|
+
abs_script = Path(__file__).resolve().as_posix()
|
|
155
|
+
|
|
156
|
+
# Vector 3: Mantener politica Fail-Closed (|| exit 1)
|
|
157
|
+
hook_content = f"#!/bin/sh\n# Chronolith Sentinel Guardian (Fail-Closed Security)\necho '[*] Ethernium: Guarding DNA Lineage...'\n\"{abs_python}\" \"{abs_script}\" check\nRESULT=$?\nif [ $RESULT -ne 0 ]; then\n echo '[!] PUSH REJECTED: DNA Drift Detected. Run chronolith-lite check.'\n exit 1\nfi\nexit 0\n"
|
|
158
|
+
hook_path.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
hook_path.write_text(hook_content, encoding="utf-8")
|
|
160
|
+
|
|
161
|
+
if os.name != "nt": os.chmod(hook_path, 0o755)
|
|
162
|
+
console.log(f"[green][OK][/green] Push Hook Guardian (v2.1.0) installed and active.")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build_merkle_root(hashes: list[str]) -> str:
|
|
166
|
+
"""RFC 6962 compliant Merkle Tree with leaf/node prefix hardening."""
|
|
167
|
+
if not hashes: return "0" * 64
|
|
168
|
+
# Leaf nodes: H(0x00 || data)
|
|
169
|
+
current_level = [hashlib.sha256(b"\x00" + h.encode("utf-8")).hexdigest() for h in sorted(hashes)]
|
|
170
|
+
while len(current_level) > 1:
|
|
171
|
+
next_level = []
|
|
172
|
+
if len(current_level) % 2 != 0:
|
|
173
|
+
current_level.append(current_level[-1])
|
|
174
|
+
for i in range(0, len(current_level), 2):
|
|
175
|
+
# Internal nodes: H(0x01 || left || right)
|
|
176
|
+
combined = b"\x01" + (current_level[i] + current_level[i+1]).encode("utf-8")
|
|
177
|
+
next_level.append(hashlib.sha256(combined).hexdigest())
|
|
178
|
+
current_level = next_level
|
|
179
|
+
return current_level[0]
|
|
180
|
+
|
|
181
|
+
@app.command()
|
|
182
|
+
def init(
|
|
183
|
+
repo_root: Path = typer.Option(".", "--repo-root", help="Project root directory."),
|
|
184
|
+
no_hook: bool = typer.Option(False, "--no-hook", help="Disable automatic Git-Hook installation.")
|
|
185
|
+
):
|
|
186
|
+
"""Initialize the Chronolith memory core in the current project."""
|
|
187
|
+
console.print(ASCII_ART)
|
|
188
|
+
logger = setup_logger(repo_root)
|
|
189
|
+
|
|
190
|
+
root = repo_root.resolve()
|
|
191
|
+
logger.info("Initializing Chronolith Core", extra={"repo_root": str(root)})
|
|
192
|
+
|
|
193
|
+
with Progress(
|
|
194
|
+
SpinnerColumn(),
|
|
195
|
+
TextColumn("[progress.description]{task.description}"),
|
|
196
|
+
transient=True,
|
|
197
|
+
) as progress:
|
|
198
|
+
progress.add_task(description="Forging memory core...", total=None)
|
|
199
|
+
|
|
200
|
+
# Create core files
|
|
201
|
+
ensure_file(root / "PROJECT_CONTEXT.md", "# Project Context\n\n- Define your project core here.", "PROJECT_CONTEXT.md")
|
|
202
|
+
ensure_file(root / "LIVE_HANDOFF.md", "# Live Handoff\n\n- No handoff pending.", "LIVE_HANDOFF.md")
|
|
203
|
+
|
|
204
|
+
# Create and SIGN STATE.json
|
|
205
|
+
state_path = root / "STATE.json"
|
|
206
|
+
if not state_path.exists():
|
|
207
|
+
state_data = {"phase": "stable", "last_update": datetime.utcnow().isoformat()}
|
|
208
|
+
state_data["signature"] = sign_state(state_data)
|
|
209
|
+
state_path.write_text(json.dumps(state_data, indent=2), encoding="utf-8")
|
|
210
|
+
console.log(f" [green][OK][/green] Crystallized + Signed: [italic]STATE.json[/italic]")
|
|
211
|
+
|
|
212
|
+
if not no_hook:
|
|
213
|
+
install_hooks(root)
|
|
214
|
+
|
|
215
|
+
console.print(Panel("[bold green]Success:[/bold green] Chronolith Core crystallized at root level.", title="Init Complete", expand=False))
|
|
216
|
+
|
|
217
|
+
@app.command()
|
|
218
|
+
def check(
|
|
219
|
+
repo_root: Path = typer.Option(".", "--repo-root", help="Project root directory."),
|
|
220
|
+
interactive: bool = typer.Option(False, "--interactive", help="Ask for confirmation on DNA drift."),
|
|
221
|
+
verbose: bool = typer.Option(False, "--verbose", help="Show individual nucleotide hashes.")
|
|
222
|
+
):
|
|
223
|
+
"""Validate the DNA parity, Merkle Root integrity, and state signature."""
|
|
224
|
+
console.print(ASCII_ART)
|
|
225
|
+
root = repo_root.resolve()
|
|
226
|
+
|
|
227
|
+
md_files = []
|
|
228
|
+
CANONICAL_AUDIT_DIRS = [".", "OTHER_LANGUAGES"]
|
|
229
|
+
for audit_dir in CANONICAL_AUDIT_DIRS:
|
|
230
|
+
a_path = root / audit_dir
|
|
231
|
+
if not a_path.exists(): continue
|
|
232
|
+
|
|
233
|
+
if audit_dir == ".":
|
|
234
|
+
for f in a_path.glob("*.md"):
|
|
235
|
+
if "PROJECT_DNA" not in f.name:
|
|
236
|
+
md_files.append(f)
|
|
237
|
+
else:
|
|
238
|
+
for f in a_path.rglob("*.md"):
|
|
239
|
+
if "PROJECT_DNA" not in f.name:
|
|
240
|
+
md_files.append(f)
|
|
241
|
+
|
|
242
|
+
sorted_md_tuples = sorted(
|
|
243
|
+
[(md.relative_to(root).as_posix(), md) for md in md_files],
|
|
244
|
+
key=lambda x: x[0]
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
nucleotides = []
|
|
248
|
+
|
|
249
|
+
if verbose:
|
|
250
|
+
table = Table(title="Nucleotide Audit", show_header=True, header_style="bold yellow")
|
|
251
|
+
table.add_column("File", style="cyan")
|
|
252
|
+
table.add_column("SHA-256 (LF-Norm)")
|
|
253
|
+
|
|
254
|
+
for rel_path, md in sorted_md_tuples:
|
|
255
|
+
content_h = calculate_sha256(md)
|
|
256
|
+
nucleotides.append(leaf_hash(rel_path, content_h))
|
|
257
|
+
if verbose:
|
|
258
|
+
table.add_row(rel_path, content_h[:16])
|
|
259
|
+
|
|
260
|
+
if verbose:
|
|
261
|
+
console.print(table)
|
|
262
|
+
|
|
263
|
+
merkle_root = build_merkle_root(nucleotides)
|
|
264
|
+
|
|
265
|
+
# Logic for DNA Parity Check
|
|
266
|
+
# We compare the current Merkle Root with the one stored in README.md or STATE.json
|
|
267
|
+
# For now, let's just use the current Merkle against the previous if it existed
|
|
268
|
+
|
|
269
|
+
drift_detected = False
|
|
270
|
+
state_path = root / "STATE.json"
|
|
271
|
+
if state_path.exists():
|
|
272
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
273
|
+
# Fail-closed: a present-but-invalid signature means STATE.json itself was
|
|
274
|
+
# edited outside the tool. Reject before trusting its stored baseline.
|
|
275
|
+
if not verify_signature(state):
|
|
276
|
+
console.print("[bold red][!] ERROR: STATE.json signature invalid — the state record was tampered with. Aborting.[/bold red]")
|
|
277
|
+
raise typer.Exit(code=1)
|
|
278
|
+
if "merkle_root" in state and state.get("leaf_format") != LEAF_FORMAT:
|
|
279
|
+
# Integrity leaf format was upgraded: re-baseline once rather than
|
|
280
|
+
# reporting the format change as semantic drift.
|
|
281
|
+
console.print(f"[yellow][i] Merkle leaf format upgraded ({state.get('leaf_format') or 'legacy'} -> {LEAF_FORMAT}); re-crystallizing baseline.[/yellow]")
|
|
282
|
+
elif "merkle_root" in state and state["merkle_root"] != merkle_root:
|
|
283
|
+
drift_detected = True
|
|
284
|
+
console.print(f"[bold yellow][!] DNA DRIFT DETECTED:[/bold yellow]")
|
|
285
|
+
console.print(f" Current: [cyan]{merkle_root[:16]}[/cyan]")
|
|
286
|
+
console.print(f" Expected: [magenta]{state['merkle_root'][:16]}[/magenta]")
|
|
287
|
+
|
|
288
|
+
if drift_detected:
|
|
289
|
+
if interactive:
|
|
290
|
+
confirm = typer.confirm("WARNING: DNA mismatch detected. This might indicate unauthorized changes. Proceed anyway?", default=False)
|
|
291
|
+
if not confirm:
|
|
292
|
+
console.print("[bold red]Aborting as requested.[/bold red]")
|
|
293
|
+
raise typer.Exit(code=1)
|
|
294
|
+
else:
|
|
295
|
+
console.print("[bold yellow]Proceeding despite DNA drift...[/bold yellow]")
|
|
296
|
+
else:
|
|
297
|
+
# Non-interactive mode (CI) is Fail-Closed
|
|
298
|
+
console.print("[bold red][!] ERROR: DNA Drift detected in non-interactive mode. Aborting.[/bold red]")
|
|
299
|
+
raise typer.Exit(code=1)
|
|
300
|
+
|
|
301
|
+
# If we are here, either there's no drift or the user said 'Yes' in interactive mode
|
|
302
|
+
# Update STATE.json with the new Merkle Root to 'crystallize' it
|
|
303
|
+
if state_path.exists():
|
|
304
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
305
|
+
state["merkle_root"] = merkle_root
|
|
306
|
+
state["leaf_format"] = LEAF_FORMAT
|
|
307
|
+
state["last_check"] = datetime.utcnow().isoformat()
|
|
308
|
+
state["signature"] = sign_state(state)
|
|
309
|
+
state_path.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
310
|
+
|
|
311
|
+
console.print(Panel(f"[bold green]Parity Confirmed:[/bold green] Merkle Root `{merkle_root[:16]}...`", title="DNA Guardian", expand=False))
|
|
312
|
+
|
|
313
|
+
@app.command()
|
|
314
|
+
def status(
|
|
315
|
+
repo_root: Path = typer.Option(".", "--repo-root", help="Project root directory.")
|
|
316
|
+
):
|
|
317
|
+
"""View the current status of the logical lineage."""
|
|
318
|
+
root = repo_root.resolve()
|
|
319
|
+
state_path = root / "STATE.json"
|
|
320
|
+
|
|
321
|
+
if not state_path.exists():
|
|
322
|
+
console.print("[yellow]Chronolith not initialized here. Use `init` first.[/yellow]")
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
326
|
+
signature_ok = verify_signature(state)
|
|
327
|
+
|
|
328
|
+
table = Table(title="Lineage Status", show_header=True, header_style="bold magenta")
|
|
329
|
+
table.add_column("Property", style="dim")
|
|
330
|
+
table.add_column("Value")
|
|
331
|
+
|
|
332
|
+
for k, v in state.items():
|
|
333
|
+
table.add_row(k, str(v))
|
|
334
|
+
|
|
335
|
+
table.add_row(
|
|
336
|
+
"signature_valid",
|
|
337
|
+
"[green]yes[/green]" if signature_ok else "[bold red]NO — tampered[/bold red]",
|
|
338
|
+
)
|
|
339
|
+
console.print(table)
|
|
340
|
+
|
|
341
|
+
@app.command()
|
|
342
|
+
def log(
|
|
343
|
+
intent: str = typer.Argument(..., help="Detailed session intent capture."),
|
|
344
|
+
repo_root: Path = typer.Option(".", "--repo-root", help="Project root directory.")
|
|
345
|
+
):
|
|
346
|
+
"""Capture session intent into the permanent timeline."""
|
|
347
|
+
root = repo_root.resolve()
|
|
348
|
+
log_path = root / "SESSION_LOG.md"
|
|
349
|
+
|
|
350
|
+
if not log_path.exists():
|
|
351
|
+
log_path.write_text("# Chronolith Session Log\n\n", encoding="utf-8")
|
|
352
|
+
|
|
353
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
354
|
+
f.write(f"- [{datetime.utcnow().isoformat()}Z] {intent}\n")
|
|
355
|
+
|
|
356
|
+
console.log(f"[green][OK][/green] Intent captured in SESSION_LOG.md")
|
|
357
|
+
|
|
358
|
+
def main():
|
|
359
|
+
app()
|
|
360
|
+
|
|
361
|
+
if __name__ == "__main__":
|
|
362
|
+
main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import argparse
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
# CHRONOLITH: Active Universal Translation Sync
|
|
11
|
+
# ----------------------------------------------------
|
|
12
|
+
# This script manages multilingual documentation across the 4 levels:
|
|
13
|
+
# Root, Pro, Lite, and Omega. It detects drift and can auto-generate READMEs.
|
|
14
|
+
|
|
15
|
+
LANG_CODES = ["es", "ja", "ru", "zh", "fr", "it", "de", "pt", "en"]
|
|
16
|
+
|
|
17
|
+
def calculate_md5(path: Path) -> str:
|
|
18
|
+
if not path.exists(): return ""
|
|
19
|
+
return hashlib.md5(path.read_bytes()).hexdigest()
|
|
20
|
+
|
|
21
|
+
def get_edition_name(root: Path) -> str:
|
|
22
|
+
# Detect which edition we are in
|
|
23
|
+
if (root / "CHRONOLITH Pro").exists(): return "Root Portal"
|
|
24
|
+
if "Pro" in root.name: return "Pro Edition"
|
|
25
|
+
if "Lite" in root.name: return "Lite Edition"
|
|
26
|
+
if "Omega" in root.name: return "Omega Edition"
|
|
27
|
+
return "Universal Core"
|
|
28
|
+
|
|
29
|
+
def generate_localized_readme(lang, edition_name, source_content):
|
|
30
|
+
# Professional localized templates
|
|
31
|
+
templates = {
|
|
32
|
+
"es": f"# CHRONOLITH: {edition_name}\n\nVersión localizada del framework de continuidad técnica.",
|
|
33
|
+
"ja": f"# CHRONOLITH: {edition_name}\n\nテクニカル・コンティニュイティ・フレームワークのローカライズ版。",
|
|
34
|
+
"ru": f"# CHRONOLITH: {edition_name}\n\nЛокализованная версия фреймворка технической непрерывности.",
|
|
35
|
+
"zh": f"# CHRONOLITH: {edition_name}\n\n技术连续性框架的本地化版本。",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Generic fallback
|
|
39
|
+
base = templates.get(lang, f"# CHRONOLITH: {edition_name}\n\nLocalized version of the technical chronolith framework.")
|
|
40
|
+
|
|
41
|
+
# Add strategic metadata footer
|
|
42
|
+
footer = f"\n\n---\n*CHRONOLITH: Global Infrastructure - Generated {datetime.utcnow().isoformat()}Z*"
|
|
43
|
+
return base + footer
|
|
44
|
+
|
|
45
|
+
def sync_all(repo_root: Path, auto_gen: bool):
|
|
46
|
+
edition = get_edition_name(repo_root)
|
|
47
|
+
source_path = repo_root / "README.md"
|
|
48
|
+
|
|
49
|
+
if not source_path.exists():
|
|
50
|
+
print(f"[!] ERROR: Source README.md not found in {repo_root}")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
print(f"[*] SYNC: Analyzing {edition} in {repo_root}...")
|
|
54
|
+
source_hash = calculate_md5(source_path)
|
|
55
|
+
|
|
56
|
+
lang_dir = repo_root / "OTHER_LANGUAGES"
|
|
57
|
+
lang_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
for lang in LANG_CODES:
|
|
60
|
+
target_path = lang_dir / f"README_{lang}.md"
|
|
61
|
+
|
|
62
|
+
if auto_gen:
|
|
63
|
+
print(f" -> Updating {lang} README...")
|
|
64
|
+
content = generate_localized_readme(lang, edition, source_path.read_text(encoding="utf-8"))
|
|
65
|
+
target_path.write_text(content, encoding="utf-8")
|
|
66
|
+
|
|
67
|
+
print(f"[✔] SYNC: {edition} is now globally synchronized.")
|
|
68
|
+
|
|
69
|
+
def main() -> None:
|
|
70
|
+
parser = argparse.ArgumentParser(description="Active Universal Translation Sync.")
|
|
71
|
+
parser.add_argument("--repo-root", default=".")
|
|
72
|
+
parser.add_argument("--auto-generate", action="store_true", help="Automatically generate/update localized files.")
|
|
73
|
+
args = parser.parse_args()
|
|
74
|
+
|
|
75
|
+
root = Path(args.repo_root).resolve()
|
|
76
|
+
sync_all(root, args.auto_generate)
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chronolith-lite
|
|
3
|
+
Version: 3.2.0
|
|
4
|
+
Summary: Chronolith (Lite): A professional AI chronolith framework for zero-friction context handoffs.
|
|
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.8
|
|
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
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# Chronolith Lite
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
Chronolith Lite is part of the Ethernium Chronolith Python runtime.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install chronolith-lite
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
chronolith-lite --help
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Role
|
|
36
|
+
|
|
37
|
+
Minimal local chronolith for fast handoffs, state checks, and lightweight CLI workflows.
|
|
38
|
+
|
|
39
|
+
## Governance Boundary
|
|
40
|
+
|
|
41
|
+
Chronolith is the Python runtime and governance kernel. Chronolith Conekta is an external visual control surface, and AgentOps is an extractable incubated tool.
|
|
42
|
+
|
|
43
|
+
## Quality Gate
|
|
44
|
+
|
|
45
|
+
Before release, run the root repository checks:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python scripts/health_guard.py --strict
|
|
49
|
+
python scripts/golden_baseline.py verify
|
|
50
|
+
pytest -q
|
|
51
|
+
python -m build
|
|
52
|
+
python -m twine check dist\*
|
|
53
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
chronolith_lite.egg-info/PKG-INFO
|
|
5
|
+
chronolith_lite.egg-info/SOURCES.txt
|
|
6
|
+
chronolith_lite.egg-info/dependency_links.txt
|
|
7
|
+
chronolith_lite.egg-info/entry_points.txt
|
|
8
|
+
chronolith_lite.egg-info/requires.txt
|
|
9
|
+
chronolith_lite.egg-info/top_level.txt
|
|
10
|
+
chronolith_lite/chronolith/run_chronolith_lite.py
|
|
11
|
+
chronolith_lite/chronolith/sync_translations.py
|
|
12
|
+
tests/test_lite_logic.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chronolith_lite
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chronolith-lite"
|
|
7
|
+
version = "3.2.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "SteveBlackbeard" },
|
|
10
|
+
]
|
|
11
|
+
description = "Chronolith (Lite): A professional AI chronolith framework for zero-friction context handoffs."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
license = "MIT"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"typer>=0.9.0",
|
|
21
|
+
"rich>=13.0.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
"Homepage" = "https://github.com/SteveBlackbeard/CHRONOLITH-by-Ethernium"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
chronolith-lite = "chronolith_lite.chronolith.run_chronolith_lite:main"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["."]
|
|
32
|
+
include = ["chronolith_lite*"]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import pytest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
# CHRONOLITH: Logic Audit Tests (v2.1.1 - RFC 6962 Hardened)
|
|
7
|
+
# ------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
sys.path.append(str(Path(__file__).parent.parent / 'chronolith_lite' / 'chronolith'))
|
|
10
|
+
|
|
11
|
+
from run_chronolith_lite import (
|
|
12
|
+
build_merkle_root,
|
|
13
|
+
calculate_sha256,
|
|
14
|
+
sign_state,
|
|
15
|
+
verify_signature,
|
|
16
|
+
leaf_hash,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# --- SHA-256 Tests ---
|
|
20
|
+
|
|
21
|
+
def test_calculate_sha256(tmp_path):
|
|
22
|
+
test_file = tmp_path / "test.txt"
|
|
23
|
+
content = b"Ethernium Chronolith"
|
|
24
|
+
test_file.write_bytes(content)
|
|
25
|
+
expected = hashlib.sha256(content).hexdigest()
|
|
26
|
+
assert calculate_sha256(test_file) == expected
|
|
27
|
+
|
|
28
|
+
def test_calculate_sha256_missing():
|
|
29
|
+
assert calculate_sha256(Path("non_existent_phantom.txt")) == ""
|
|
30
|
+
|
|
31
|
+
def test_calculate_sha256_accepts_str_path(tmp_path):
|
|
32
|
+
# API parity with Pro/Omega: accept a plain string, not only a Path.
|
|
33
|
+
f = tmp_path / "x.txt"
|
|
34
|
+
f.write_bytes(b"content")
|
|
35
|
+
assert calculate_sha256(str(f)) == calculate_sha256(f)
|
|
36
|
+
|
|
37
|
+
# --- Merkle Tree Tests (RFC 6962 compliant) ---
|
|
38
|
+
|
|
39
|
+
def test_build_merkle_root_empty():
|
|
40
|
+
assert build_merkle_root([]) == "0" * 64
|
|
41
|
+
|
|
42
|
+
def test_build_merkle_root_single():
|
|
43
|
+
h1 = "abc123"
|
|
44
|
+
# Single node: leaf hash = H(0x00 || "abc123")
|
|
45
|
+
expected = hashlib.sha256(b"\x00" + b"abc123").hexdigest()
|
|
46
|
+
assert build_merkle_root([h1]) == expected
|
|
47
|
+
|
|
48
|
+
def test_build_merkle_root_even():
|
|
49
|
+
# 2 nodes: sorted ["a", "b"]
|
|
50
|
+
# Leaf: L0 = H(0x00 || "a"), L1 = H(0x00 || "b")
|
|
51
|
+
# Root: H(0x01 || L0 || L1)
|
|
52
|
+
L0 = hashlib.sha256(b"\x00a").hexdigest()
|
|
53
|
+
L1 = hashlib.sha256(b"\x00b").hexdigest()
|
|
54
|
+
expected = hashlib.sha256(b"\x01" + (L0 + L1).encode("utf-8")).hexdigest()
|
|
55
|
+
assert build_merkle_root(["b", "a"]) == expected
|
|
56
|
+
|
|
57
|
+
def test_build_merkle_root_odd():
|
|
58
|
+
# 3 nodes sorted: ["a", "b", "c"]
|
|
59
|
+
# Leaves: L0=H(0x00||"a"), L1=H(0x00||"b"), L2=H(0x00||"c")
|
|
60
|
+
# Padded: L3 = L2 (duplicate)
|
|
61
|
+
# Level 1: N0=H(0x01||L0||L1), N1=H(0x01||L2||L3)
|
|
62
|
+
# Root: H(0x01||N0||N1)
|
|
63
|
+
L0 = hashlib.sha256(b"\x00a").hexdigest()
|
|
64
|
+
L1 = hashlib.sha256(b"\x00b").hexdigest()
|
|
65
|
+
L2 = hashlib.sha256(b"\x00c").hexdigest()
|
|
66
|
+
N0 = hashlib.sha256(b"\x01" + (L0 + L1).encode("utf-8")).hexdigest()
|
|
67
|
+
N1 = hashlib.sha256(b"\x01" + (L2 + L2).encode("utf-8")).hexdigest()
|
|
68
|
+
expected = hashlib.sha256(b"\x01" + (N0 + N1).encode("utf-8")).hexdigest()
|
|
69
|
+
assert build_merkle_root(["c", "b", "a"]) == expected
|
|
70
|
+
|
|
71
|
+
def test_merkle_deterministic_order():
|
|
72
|
+
"""Tree must be order-independent (sorted internally)."""
|
|
73
|
+
assert build_merkle_root(["z", "a", "m"]) == build_merkle_root(["a", "m", "z"])
|
|
74
|
+
|
|
75
|
+
# --- State Signature Tests (Expert #1: Cybersecurity) ---
|
|
76
|
+
|
|
77
|
+
def test_sign_state_deterministic():
|
|
78
|
+
data = {"phase": "stable", "last_update": "2026-01-01T00:00:00"}
|
|
79
|
+
sig1 = sign_state(data)
|
|
80
|
+
sig2 = sign_state(data)
|
|
81
|
+
assert sig1 == sig2
|
|
82
|
+
assert len(sig1) == 64 # SHA-256 hex digest
|
|
83
|
+
|
|
84
|
+
def test_sign_state_excludes_signature_field():
|
|
85
|
+
data = {"phase": "stable", "signature": "old_sig"}
|
|
86
|
+
sig = sign_state(data)
|
|
87
|
+
# Changing the signature field should NOT change the hash
|
|
88
|
+
data2 = {"phase": "stable", "signature": "different_sig"}
|
|
89
|
+
assert sign_state(data2) == sig
|
|
90
|
+
|
|
91
|
+
def test_sign_state_detects_tampering():
|
|
92
|
+
data = {"phase": "stable", "last_update": "2026-01-01T00:00:00"}
|
|
93
|
+
sig = sign_state(data)
|
|
94
|
+
data["phase"] = "compromised"
|
|
95
|
+
assert sign_state(data) != sig
|
|
96
|
+
|
|
97
|
+
# --- Signature Verification (closes the write-only-signature gap) ---
|
|
98
|
+
|
|
99
|
+
def test_verify_signature_accepts_valid():
|
|
100
|
+
data = {"phase": "stable", "last_update": "2026-01-01T00:00:00"}
|
|
101
|
+
data["signature"] = sign_state(data)
|
|
102
|
+
assert verify_signature(data) is True
|
|
103
|
+
|
|
104
|
+
def test_verify_signature_rejects_tampered_state():
|
|
105
|
+
data = {"phase": "stable", "last_update": "2026-01-01T00:00:00"}
|
|
106
|
+
data["signature"] = sign_state(data)
|
|
107
|
+
data["merkle_root"] = "deadbeef" * 8 # edited after signing
|
|
108
|
+
assert verify_signature(data) is False
|
|
109
|
+
|
|
110
|
+
def test_verify_signature_absent_is_unverifiable_not_rejected():
|
|
111
|
+
assert verify_signature({"phase": "stable"}) is True
|
|
112
|
+
|
|
113
|
+
# --- Path-Bound Merkle Leaves (root reacts to renames / content swaps) ---
|
|
114
|
+
|
|
115
|
+
def test_leaf_hash_binds_filename():
|
|
116
|
+
content_h = calculate_sha256(Path("non_existent_phantom.txt")) or "abc"
|
|
117
|
+
assert leaf_hash("A.md", content_h) != leaf_hash("B.md", content_h)
|
|
118
|
+
|
|
119
|
+
def test_leaf_hash_content_swap_changes_root():
|
|
120
|
+
# Two files swapping content must change the root when leaves are path-bound.
|
|
121
|
+
ha, hb = "hash_a", "hash_b"
|
|
122
|
+
original = build_merkle_root([leaf_hash("A.md", ha), leaf_hash("B.md", hb)])
|
|
123
|
+
swapped = build_merkle_root([leaf_hash("A.md", hb), leaf_hash("B.md", ha)])
|
|
124
|
+
assert original != swapped
|
|
125
|
+
|
|
126
|
+
|