Skip to content

Commit e8255f5

Browse files
tbitcsoz-agent
andcommitted
feat(esdb): add EsdbRecord.to_dict(); wire real esdb export/import/backup
- EsdbRecord.to_dict() returns self.data (original source dict) so esdb export and esdb backup emit proper JSON rather than str(record) - esdb export: uses r.to_dict() for clean JSON output - esdb import: now writes directly to .specsmith/requirements.json and .specsmith/testcases.json (real persistence, not just staging) - esdb backup: uses r.to_dict() for clean JSON output - Fix leftover conflict marker (>>>>>>>) in cli.py - Re-insert esdb export/import/backup/rollback/compact commands that were accidentally removed during merge conflict resolution Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 05fd251 commit e8255f5

2 files changed

Lines changed: 182 additions & 2 deletions

File tree

src/specsmith/cli.py

Lines changed: 172 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8516,13 +8516,183 @@ def esdb_replay_cmd(project_dir: str) -> None:
85168516
console.print(f"[bold]Replay check:[/bold] {st.backend}")
85178517
console.print(f" Records: {st.record_count}")
85188518
if st.chain_valid:
8519-
console.print("[green]\u2714[/green] WAL chain valid state consistent.")
8519+
console.print("[green]\u2714[/green] WAL chain valid \u2014 state consistent.")
85208520
else:
85218521
console.print("[red]\u2717[/red] WAL chain integrity failure detected.")
85228522

85238523

8524+
@esdb_group.command(name="export")
8525+
@click.option("--project-dir", type=click.Path(exists=True), default=".")
8526+
@click.option(
8527+
"--output",
8528+
default="",
8529+
help="Output file path (default: <project>/.specsmith/esdb_export.json)",
8530+
)
8531+
@click.option("--json", "as_json", is_flag=True, default=False)
8532+
def esdb_export_cmd(project_dir: str, output: str, as_json: bool) -> None:
8533+
"""Export the full ESDB to a JSON file."""
8534+
import json as _json
8535+
8536+
from specsmith.esdb.bridge import EsdbBridge
8537+
8538+
bridge = EsdbBridge(project_dir)
8539+
st = bridge.status()
8540+
reqs = bridge.requirements()
8541+
tests = bridge.testcases()
8542+
payload = {
8543+
"esdb_version": 1,
8544+
"backend": st.backend,
8545+
"record_count": st.record_count,
8546+
"requirements": [r.to_dict() for r in reqs],
8547+
"testcases": [t.to_dict() for t in tests],
8548+
}
8549+
dest = output or str(Path(project_dir).resolve() / ".specsmith" / "esdb_export.json")
8550+
Path(dest).parent.mkdir(parents=True, exist_ok=True)
8551+
Path(dest).write_text(_json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
8552+
if as_json:
8553+
click.echo(_json.dumps({"ok": True, "path": dest, "records": st.record_count}, indent=2))
8554+
else:
8555+
console.print(f"[green]\u2714[/green] Exported {st.record_count} records to {dest}")
8556+
8557+
8558+
@esdb_group.command(name="import")
8559+
@click.argument("source")
8560+
@click.option("--project-dir", type=click.Path(exists=True), default=".")
8561+
@click.option("--json", "as_json", is_flag=True, default=False)
8562+
def esdb_import_cmd(source: str, project_dir: str, as_json: bool) -> None:
8563+
"""Import an ESDB JSON export into the project store."""
8564+
import json as _json
8565+
8566+
src = Path(source)
8567+
if not src.is_file():
8568+
console.print(f"[red]File not found:[/red] {source}")
8569+
raise SystemExit(1)
8570+
try:
8571+
data = _json.loads(src.read_text(encoding="utf-8"))
8572+
except ValueError as exc:
8573+
console.print(f"[red]Invalid JSON:[/red] {exc}")
8574+
raise SystemExit(1) from exc
8575+
8576+
reqs = data.get("requirements", [])
8577+
tests = data.get("testcases", [])
8578+
specsmith_dir = Path(project_dir).resolve() / ".specsmith"
8579+
specsmith_dir.mkdir(parents=True, exist_ok=True)
8580+
8581+
# Write requirements and testcases directly to the live JSON stores.
8582+
# Existing data is replaced with the imported snapshot.
8583+
reqs_path = specsmith_dir / "requirements.json"
8584+
tests_path = specsmith_dir / "testcases.json"
8585+
reqs_path.write_text(_json.dumps(reqs, indent=2, ensure_ascii=False), encoding="utf-8")
8586+
tests_path.write_text(_json.dumps(tests, indent=2, ensure_ascii=False), encoding="utf-8")
8587+
8588+
result = {"ok": True, "requirements": len(reqs), "testcases": len(tests)}
8589+
if as_json:
8590+
click.echo(_json.dumps(result, indent=2))
8591+
else:
8592+
console.print(
8593+
f"[green]\u2714[/green] Imported {len(reqs)} requirements, {len(tests)} test cases."
8594+
)
8595+
console.print(f" Wrote .specsmith/requirements.json and .specsmith/testcases.json")
8596+
8597+
8598+
@esdb_group.command(name="backup")
8599+
@click.option("--project-dir", type=click.Path(exists=True), default=".")
8600+
@click.option(
8601+
"--dir",
8602+
"backup_dir",
8603+
default="",
8604+
help="Directory for backup files (default: .specsmith/backups/)",
8605+
)
8606+
@click.option("--json", "as_json", is_flag=True, default=False)
8607+
def esdb_backup_cmd(project_dir: str, backup_dir: str, as_json: bool) -> None:
8608+
"""Create a timestamped snapshot backup of the ESDB."""
8609+
import datetime
8610+
import json as _json
8611+
8612+
from specsmith.esdb.bridge import EsdbBridge
8613+
8614+
bridge = EsdbBridge(project_dir)
8615+
st = bridge.status()
8616+
ts = datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
8617+
dest_dir = (
8618+
Path(backup_dir) if backup_dir else Path(project_dir).resolve() / ".specsmith" / "backups"
8619+
)
8620+
dest_dir.mkdir(parents=True, exist_ok=True)
8621+
dest = dest_dir / f"esdb_backup_{ts}.json"
8622+
reqs = bridge.requirements()
8623+
tests = bridge.testcases()
8624+
payload = {
8625+
"esdb_version": 1,
8626+
"timestamp": ts,
8627+
"backend": st.backend,
8628+
"record_count": st.record_count,
8629+
"requirements": [r.to_dict() for r in reqs],
8630+
"testcases": [t.to_dict() for t in tests],
8631+
}
8632+
dest.write_text(_json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
8633+
result = {"ok": True, "path": str(dest), "timestamp": ts, "records": st.record_count}
8634+
if as_json:
8635+
click.echo(_json.dumps(result, indent=2))
8636+
else:
8637+
console.print(f"[green]\u2714[/green] Backup created: {dest} ({st.record_count} records)")
8638+
8639+
8640+
@esdb_group.command(name="rollback")
8641+
@click.option("--project-dir", type=click.Path(exists=True), default=".")
8642+
@click.option("--steps", default=1, show_default=True, help="Number of WAL events to roll back.")
8643+
@click.option("--json", "as_json", is_flag=True, default=False)
8644+
def esdb_rollback_cmd(project_dir: str, steps: int, as_json: bool) -> None:
8645+
"""Roll back the ESDB by N WAL events (stub \u2014 reports what would be undone)."""
8646+
import json as _json
8647+
8648+
from specsmith.esdb.bridge import EsdbBridge
8649+
8650+
bridge = EsdbBridge(project_dir)
8651+
st = bridge.status()
8652+
result = {
8653+
"ok": True,
8654+
"steps_requested": steps,
8655+
"records_before": st.record_count,
8656+
"note": "Full WAL rollback requires ChronoMemory native engine (stub mode).",
8657+
}
8658+
if as_json:
8659+
click.echo(_json.dumps(result, indent=2))
8660+
else:
8661+
console.print(f"[yellow]\u26a0[/yellow] Rollback {steps} step(s) requested on {st.backend}")
8662+
console.print(f" Records before: {st.record_count}")
8663+
console.print(
8664+
" [dim]Full rollback active when ChronoMemory native engine is linked.[/dim]"
8665+
)
8666+
8667+
8668+
@esdb_group.command(name="compact")
8669+
@click.option("--project-dir", type=click.Path(exists=True), default=".")
8670+
@click.option("--json", "as_json", is_flag=True, default=False)
8671+
def esdb_compact_cmd(project_dir: str, as_json: bool) -> None:
8672+
"""Compact the ESDB WAL (merge tombstones, reclaim space)."""
8673+
import json as _json
8674+
8675+
from specsmith.esdb.bridge import EsdbBridge
8676+
8677+
bridge = EsdbBridge(project_dir)
8678+
st = bridge.status()
8679+
result = {
8680+
"ok": True,
8681+
"backend": st.backend,
8682+
"records": st.record_count,
8683+
"note": "Full WAL compaction active when ChronoMemory native engine is linked.",
8684+
}
8685+
if as_json:
8686+
click.echo(_json.dumps(result, indent=2))
8687+
else:
8688+
console.print(
8689+
f"[green]\u2714[/green] Compact requested on {st.backend} ({st.record_count} records)"
8690+
)
8691+
console.print(
8692+
" [dim]Full compaction active when ChronoMemory native engine is linked.[/dim]"
8693+
)
8694+
85248695

8525-
>>>>>>> 968261582aa77adba116f3c092ae13c2449a4df8
85268696
main.add_command(esdb_group)
85278697

85288698

src/specsmith/esdb/bridge.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ class EsdbRecord:
2727
data: dict[str, Any] = field(default_factory=dict)
2828
source_ids: list[str] = field(default_factory=list)
2929

30+
def to_dict(self) -> dict[str, Any]:
31+
"""Return the original source data dict (used for export/backup)."""
32+
return self.data if self.data else {
33+
"id": self.id,
34+
"kind": self.kind,
35+
"status": self.status,
36+
"confidence": self.confidence,
37+
"label": self.label,
38+
}
39+
3040

3141
@dataclass
3242
class EsdbStatus:

0 commit comments

Comments
 (0)