memoryledger 0.1.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.
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ try:
4
+ from ._version import __version__
5
+ except Exception: # pragma: no cover
6
+ __version__ = "0.0.0"
7
+
8
+ __all__ = ["__version__"]
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = 'ga711d99f3'
memoryledger/adopt.py ADDED
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import shutil
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from ledgercore.atomic import atomic_write_text
9
+
10
+ from .errors import MemoryledgerError
11
+ from .guardrails import safe_to_replace
12
+ from .models import GENERATED_MARKER, EvidenceRef
13
+ from .render import render_all
14
+ from .review import transition
15
+ from .storage import Store
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class AdoptionProposal:
20
+ title: str
21
+ content: str
22
+ kind: str
23
+ origin: str
24
+ source_hash: str
25
+
26
+ def to_dict(self) -> dict[str, str]:
27
+ return {
28
+ "title": self.title,
29
+ "content": self.content,
30
+ "kind": self.kind,
31
+ "origin": self.origin,
32
+ "source_hash": self.source_hash,
33
+ }
34
+
35
+
36
+ def source_hash(text: str) -> str:
37
+ return hashlib.sha256(text.encode()).hexdigest()
38
+
39
+
40
+ def parse_markdown(path: Path, root: Path) -> tuple[str, list[AdoptionProposal]]:
41
+ text = path.read_text()
42
+ if GENERATED_MARKER in text:
43
+ raise MemoryledgerError("ALREADY_GENERATED", "target is already generated")
44
+ digest = source_hash(text)
45
+ rel = path.resolve().relative_to(root.resolve()).as_posix()
46
+ proposals: list[AdoptionProposal] = []
47
+ heading = "Imported agent document"
48
+ body: list[str] = []
49
+ index = 0
50
+
51
+ def flush() -> None:
52
+ nonlocal index, body
53
+ content = "\n".join(body).strip()
54
+ if not content:
55
+ body = []
56
+ return
57
+ index += 1
58
+ proposals.append(
59
+ AdoptionProposal(
60
+ heading,
61
+ content,
62
+ "document",
63
+ f"adopt:{rel}:{index}",
64
+ digest,
65
+ )
66
+ )
67
+ body = []
68
+
69
+ for line in text.splitlines():
70
+ if line.startswith("## "):
71
+ flush()
72
+ heading = line[3:].strip() or "Imported section"
73
+ elif not line.startswith("# "):
74
+ body.append(line)
75
+ flush()
76
+ if not proposals:
77
+ raise MemoryledgerError(
78
+ "EMPTY_CONTENT", "manual target has no importable content"
79
+ )
80
+ return digest, proposals
81
+
82
+
83
+ def adopt(
84
+ store: Store,
85
+ target: Path,
86
+ *,
87
+ backup: bool,
88
+ accept: bool,
89
+ reason: str,
90
+ ) -> tuple[list[str], Path]:
91
+ if not backup:
92
+ raise MemoryledgerError(
93
+ "ADOPTION_BACKUP_REQUIRED", "adoption requires --backup"
94
+ )
95
+ initial_hash, proposals = parse_markdown(target, store.config.root)
96
+ existing = {memory.origin: memory for memory in store.all_memories()}
97
+ for proposal in proposals:
98
+ old = existing.get(proposal.origin)
99
+ if old and old.origin_hash == proposal.source_hash:
100
+ continue
101
+ store.validate_new(
102
+ proposal.kind,
103
+ proposal.title,
104
+ proposal.content,
105
+ f"Adopted from {target.relative_to(store.config.root)}.",
106
+ "repo",
107
+ "",
108
+ "root_agents",
109
+ "adopt",
110
+ origin=proposal.origin,
111
+ origin_hash=proposal.source_hash,
112
+ )
113
+ ids: list[str] = []
114
+ evidence = EvidenceRef(
115
+ kind="file",
116
+ title="Original manual agent file",
117
+ uri=target.relative_to(store.config.root).as_posix(),
118
+ content_hash=initial_hash,
119
+ )
120
+ for proposal in proposals:
121
+ old = existing.get(proposal.origin)
122
+ if old and old.origin_hash == proposal.source_hash:
123
+ ids.append(old.id)
124
+ continue
125
+ memory = store.create(
126
+ proposal.kind,
127
+ proposal.title,
128
+ proposal.content,
129
+ f"Adopted from {target.relative_to(store.config.root)}.",
130
+ "repo",
131
+ "",
132
+ "root_agents",
133
+ "adopt",
134
+ origin=proposal.origin,
135
+ origin_hash=proposal.source_hash,
136
+ evidence_refs=[evidence],
137
+ )
138
+ ids.append(memory.id)
139
+ if accept:
140
+ transition(store, memory.id, "accepted", reason)
141
+ result = render_all(store.config)
142
+ if source_hash(target.read_text()) != initial_hash:
143
+ raise MemoryledgerError("ADOPTION_SOURCE_CHANGED", "manual target changed")
144
+ extra_targets = [
145
+ (store.config.root / rel, text) for rel, text in result.linked_docs.items()
146
+ ]
147
+ if store.config.render.nested_agents_enabled:
148
+ extra_targets.extend(
149
+ (store.config.root / rel, text) for rel, text in result.nested_docs.items()
150
+ )
151
+ for path, _text in extra_targets:
152
+ safe_to_replace(path)
153
+ backup_path = _backup_path(target)
154
+ shutil.copy2(target, backup_path)
155
+ try:
156
+ atomic_write_text(target, result.root_text)
157
+ for path, text in extra_targets:
158
+ path.parent.mkdir(parents=True, exist_ok=True)
159
+ atomic_write_text(path, text)
160
+ except Exception:
161
+ if source_hash(backup_path.read_text()) == initial_hash:
162
+ shutil.copy2(backup_path, target)
163
+ raise
164
+ return ids, backup_path
165
+
166
+
167
+ def _backup_path(target: Path) -> Path:
168
+ number = 1
169
+ while True:
170
+ candidate = target.with_name(f"{target.name}.memoryledger-adopt-{number}.bak")
171
+ if not candidate.exists():
172
+ return candidate
173
+ number += 1