mindslice 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.
Files changed (50) hide show
  1. mindslice/__init__.py +58 -0
  2. mindslice/__main__.py +5 -0
  3. mindslice/baking/__init__.py +1 -0
  4. mindslice/baking/archive.py +66 -0
  5. mindslice/baking/bake.py +299 -0
  6. mindslice/baking/bake_batch.py +537 -0
  7. mindslice/baking/slice_transfer.py +265 -0
  8. mindslice/errors.py +104 -0
  9. mindslice/interfaces/__init__.py +1 -0
  10. mindslice/interfaces/bake_review.py +230 -0
  11. mindslice/interfaces/cli.py +392 -0
  12. mindslice/interfaces/cli_addressing.py +52 -0
  13. mindslice/interfaces/doctor.py +184 -0
  14. mindslice/interfaces/mcp_server.py +536 -0
  15. mindslice/interfaces/render.py +346 -0
  16. mindslice/interfaces/staging_review.py +92 -0
  17. mindslice/model/__init__.py +1 -0
  18. mindslice/model/addressing.py +251 -0
  19. mindslice/model/card.py +641 -0
  20. mindslice/model/lineage.py +228 -0
  21. mindslice/model/text.py +19 -0
  22. mindslice/model/tomlfmt.py +129 -0
  23. mindslice/operations/__init__.py +1 -0
  24. mindslice/operations/audit.py +195 -0
  25. mindslice/operations/boot.py +183 -0
  26. mindslice/operations/impact.py +98 -0
  27. mindslice/operations/recovery.py +113 -0
  28. mindslice/operations/staging.py +641 -0
  29. mindslice/operations/status.py +137 -0
  30. mindslice/operations/validation.py +250 -0
  31. mindslice/operations/writer.py +481 -0
  32. mindslice/retrieval/__init__.py +1 -0
  33. mindslice/retrieval/index.py +634 -0
  34. mindslice/retrieval/lexical.py +215 -0
  35. mindslice/retrieval/query.py +593 -0
  36. mindslice/storage/__init__.py +1 -0
  37. mindslice/storage/card_io.py +14 -0
  38. mindslice/storage/config.py +192 -0
  39. mindslice/storage/fsutil.py +141 -0
  40. mindslice/storage/history.py +182 -0
  41. mindslice/storage/locking.py +125 -0
  42. mindslice/storage/snapshots.py +262 -0
  43. mindslice/workspace.py +385 -0
  44. mindslice-0.1.0.dist-info/METADATA +98 -0
  45. mindslice-0.1.0.dist-info/RECORD +50 -0
  46. mindslice-0.1.0.dist-info/WHEEL +5 -0
  47. mindslice-0.1.0.dist-info/entry_points.txt +2 -0
  48. mindslice-0.1.0.dist-info/licenses/LICENSE +202 -0
  49. mindslice-0.1.0.dist-info/licenses/NOTICE +22 -0
  50. mindslice-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,346 @@
1
+ """Human-readable renderings of CLI results; ``--json`` prints the underlying dicts instead."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Callable
7
+
8
+ Renderer = Callable[[dict[str, Any]], str]
9
+
10
+
11
+ def _join(lines: list[str]) -> str:
12
+ return "\n".join(lines).rstrip() + "\n"
13
+
14
+
15
+ def _refs(items: list[Any]) -> str:
16
+ """Comma-separated addresses from a list of addresses or ``{"address": ...}`` dicts."""
17
+ return ", ".join(item["address"] if isinstance(item, dict) else str(item) for item in items)
18
+
19
+
20
+ def _more(shown: int, total: int, flag: str, next_offset: int | None) -> str:
21
+ if next_offset is None or total <= shown:
22
+ return ""
23
+ return f" (+{total - shown} more; {flag} {next_offset})"
24
+
25
+
26
+ def _sync(sync: dict[str, Any]) -> str:
27
+ return f"index: {sync['added']} added, {sync['updated']} updated, {sync['removed']} removed"
28
+
29
+
30
+ # --- read / search -----------------------------------------------------------------------------
31
+
32
+
33
+ def render_read(result: dict[str, Any]) -> str:
34
+ """Composed view of a ``read()`` result: baselines, the card, extensions, then lineage notices."""
35
+ out: list[str] = []
36
+
37
+ def card(c: dict[str, Any], heading: str) -> None:
38
+ out.append(f"{heading} {c['title']} ({c['address']}, role={c['role']}, precedence={c['precedence']})")
39
+ if c.get("summary"):
40
+ out.append("")
41
+ out.append(c["summary"])
42
+ if c.get("body", "").strip():
43
+ out.append("")
44
+ out.append(c["body"].rstrip())
45
+ out.append("")
46
+
47
+ notes = []
48
+ if result.get("redirected_from"):
49
+ notes.append("redirected from " + result["redirected_from"] + " via " + " -> ".join(result.get("lineage_path", [])))
50
+ for key in ("shadows", "superseded_by_untrusted", "baseline_superseded_by", "missing_baselines"):
51
+ if result.get(key):
52
+ notes.append(f"{key}: {', '.join(result[key])}")
53
+ if result.get("unrebased_extensions"):
54
+ notes.append("unrebased_extensions: " + ", ".join(
55
+ f"{u['address']} (extends {u['extends']})" for u in result["unrebased_extensions"]))
56
+ if result.get("bypassed_replacements"):
57
+ notes.append("bypassed_replacements: " + ", ".join(
58
+ f"{b['address']} (supersedes {b['supersedes']})" for b in result["bypassed_replacements"]))
59
+ if result.get("competing_replacements"):
60
+ notes.append("competing_replacements: " + _refs(result["competing_replacements"]))
61
+ if result.get("unresolved_refs"):
62
+ notes.append("unresolved_refs: " + ", ".join(map(str, result["unresolved_refs"])))
63
+ for error in result.get("parse_errors", []):
64
+ notes.append("parse_error: " + error)
65
+ if result.get("extensions_next_offset") is not None:
66
+ shown = len(result.get("extensions", []))
67
+ notes.append(f"extensions: showing {shown} of {result['total_extensions']}; "
68
+ f"more with --extensions-offset {result['extensions_next_offset']}")
69
+ if result.get("backlinks"):
70
+ notes.append("backlinks: " + _refs(result["backlinks"])
71
+ + _more(len(result["backlinks"]), result.get("total_backlinks", 0),
72
+ "--backlinks-offset", result.get("backlinks_next_offset")))
73
+ for base in result.get("baselines", []):
74
+ card(base, "## Baseline:")
75
+ card(result, "#")
76
+ for ext in result.get("extensions", []):
77
+ card(ext, "## Extension:")
78
+ if notes:
79
+ out.append("---")
80
+ out.extend(f"> {n}" for n in notes)
81
+ out.append("")
82
+ out.append(f"status: {result.get('status')} sha256: {result.get('sha256')}")
83
+ return _join(out)
84
+
85
+
86
+ _SEARCH_NOTES = ("resolves_to", "matched_via_superseded", "supersedes", "extends", "superseded_by",
87
+ "superseded_by_untrusted", "baseline_superseded_by", "missing_baselines")
88
+
89
+
90
+ def render_search(result: dict[str, Any]) -> str:
91
+ hits = result["results"]
92
+ query = json.dumps(result["query"], ensure_ascii=False)
93
+ if not hits:
94
+ lines = [f"No matches for {query}." if not result["total_matches"]
95
+ else f"No matches for {query} at offset {result['offset']} ({result['total_matches']} total)."]
96
+ else:
97
+ first = result["offset"] + 1
98
+ lines = [f"Matches {first}-{result['offset'] + len(hits)} of {result['total_matches']} for {query}"
99
+ + (f"; next page: --offset {result['next_offset']}" if result.get("next_offset") is not None else ""),
100
+ ""]
101
+ for n, hit in enumerate(hits, result["offset"] + 1):
102
+ marks = [f"tags: {', '.join(hit['tags'])}"] if hit.get("tags") else []
103
+ if hit.get("load"):
104
+ marks.append(f"load: {hit['load']}")
105
+ lines.append(f"{n}. {hit['address']} — {hit['title']}" + (f" [{'; '.join(marks)}]" if marks else ""))
106
+ if hit.get("summary"):
107
+ lines.append(f" {hit['summary']}")
108
+ if hit.get("status", "active") != "active":
109
+ lines.append(f" status: {hit['status']}")
110
+ for key in _SEARCH_NOTES:
111
+ if hit.get(key):
112
+ value = hit[key] if isinstance(hit[key], list) else [hit[key]]
113
+ lines.append(f" {key.replace('_', ' ')}: {_refs(value)}")
114
+ if hit.get("bypassed_replacements"):
115
+ lines.append(" bypassed replacements: " + ", ".join(
116
+ f"{b['address']} (supersedes {b['supersedes']})" for b in hit["bypassed_replacements"]))
117
+ if hit.get("competing_replacements"):
118
+ lines.append(" competing replacements: " + _refs(hit["competing_replacements"]))
119
+ if hit.get("neighbors"):
120
+ extra = hit["total_neighbors"] - len(hit["neighbors"])
121
+ lines.append(" related: " + ", ".join(f"{nb['address']} ({nb['direction']})" for nb in hit["neighbors"])
122
+ + (f" (+{extra} more)" if extra > 0 else ""))
123
+ if result.get("unmatched_terms"):
124
+ lines += ["", "No card in scope contains: " + ", ".join(result["unmatched_terms"])]
125
+ return _join(lines)
126
+
127
+
128
+ # --- writes ----------------------------------------------------------------------------------
129
+
130
+
131
+ _VERBS = {"create": "Created", "replace": "Replaced", "append": "Appended to", "delete": "Deleted"}
132
+
133
+
134
+ def render_write(result: dict[str, Any]) -> str:
135
+ address, op = result["address"], result["op"]
136
+ if not result["changed"]:
137
+ lines = [f"No change: {address}"]
138
+ elif result["staged"]:
139
+ lines = [f"Staged {op}: {address} (pending review, not active yet)"]
140
+ if result.get("requires_approval"):
141
+ lines.append(f"Awaiting approval: {result['requires_approval']}")
142
+ else:
143
+ lines.append(f"Review with `mindslice diff`; accept with `mindslice apply {result['id']}`.")
144
+ else:
145
+ lines = [f"{_VERBS.get(op, op)} {address}"]
146
+ if result.get("forked_from"):
147
+ lines.append(f"copy of {result['forked_from']} (read-only slice), which it now replaces; the next bake folds it in")
148
+ if result.get("sha256"):
149
+ lines.append(f"sha256: {result['sha256']}")
150
+ if result.get("reason"):
151
+ lines.append(f"reason: {result['reason']}")
152
+ for key in ("affected_backlinks", "affected_extensions"):
153
+ if result.get(key):
154
+ total = result.get("total_" + key, len(result[key]))
155
+ lines.append(f"{key.replace('_', ' ')} ({total}): {_refs(result[key])}")
156
+ lines += [f"warning: {w}" for w in result.get("warnings", [])]
157
+ return _join(lines)
158
+
159
+
160
+ def render_apply(result: dict[str, Any]) -> str:
161
+ if not result["applied"] and not result.get("discarded") and not result["skipped"]:
162
+ lines = ["Nothing applied."]
163
+ else:
164
+ lines = [f"Applied {result['applied']} pending change(s); active now."]
165
+ for key in ("created", "modified", "deleted"):
166
+ lines += [f" {key}: {address}" for address in result[key]]
167
+ if result.get("discarded"):
168
+ lines.append(f"Rejected {result['discarded']} proposal(s): {', '.join(result.get('discarded_ids', []))}")
169
+ lines += [f" skipped: {item['address']}: {item['reason']}" for item in result["skipped"]]
170
+ return _join(lines)
171
+
172
+
173
+ def render_discard(result: dict[str, Any]) -> str:
174
+ if not result["discarded"]:
175
+ return "Nothing discarded.\n"
176
+ return _join([f"Discarded {result['discarded']} pending proposal(s); live memory unchanged."]
177
+ + [f" {item}" for item in result["ids"]])
178
+
179
+
180
+ def render_review(result: dict[str, Any]) -> str:
181
+ if result.get("cancelled"):
182
+ lines = ["Review cancelled; nothing changed."]
183
+ lines += [f" blocked: {c.get('address', '')}: {c.get('detail', c['reason'])}" for c in result.get("conflicts", [])]
184
+ return _join(lines)
185
+ if "created" not in result:
186
+ return f"Nothing reviewed; {result['remaining']} pending change(s) remain.\n"
187
+ return render_apply(result) + f"Pending: {result['remaining']} change(s) remain.\n"
188
+
189
+
190
+ # --- workspace and mounts ----------------------------------------------------------------------
191
+
192
+
193
+ def render_init(result: dict[str, Any]) -> str:
194
+ return _join([f"Initialized workspace: {result['workspace']}", f"Mount table: {result['mounts']}",
195
+ f"Next: mindslice -w {result['workspace']} doctor"])
196
+
197
+
198
+ def render_mount(result: dict[str, Any]) -> str:
199
+ lines = [f"Mounted {result['alias']}: {result['path']}",
200
+ f"precedence={result['precedence']} role={result['role']} cards={result['cards']} ({_sync(result['sync'])})"]
201
+ if result.get("retired_overlay_cards"):
202
+ lines.append(f"Retired {len(result['retired_overlay_cards'])} unchanged overlay copies: "
203
+ + ", ".join(result["retired_overlay_cards"]))
204
+ for key, value in result.items():
205
+ if key.endswith("warnings") and value:
206
+ lines += [f"warning: {w if isinstance(w, str) else w.get('message', w)}" for w in value]
207
+ return _join(lines)
208
+
209
+
210
+ def render_unmount(result: dict[str, Any]) -> str:
211
+ return f"Unmounted {result['alias']}; {result['removed_cards']} cards left the index. The directory is untouched.\n"
212
+
213
+
214
+ def render_validate(result: dict[str, Any]) -> str:
215
+ stats = result["stats"]
216
+ lines = [f"{'OK' if result['ok'] else 'FAILED'}: {result['target']}",
217
+ f"{stats['slices']} slice(s), {stats['cards']} card(s), {stats['errors']} error(s), {stats['warnings']} warning(s)"]
218
+ for issue in result["errors"] + result["warnings"]:
219
+ where = issue.get("address") or issue.get("file")
220
+ lines.append(f" {issue['severity']} [{issue['code']}] " + (f"{where}: " if where else "") + issue["message"])
221
+ if result.get("rebuilt_index"):
222
+ lines.append("Index rebuilt: " + ", ".join(f"{k}={v}" for k, v in result["rebuilt_index"].items()
223
+ if not isinstance(v, (dict, list))))
224
+ return _join(lines)
225
+
226
+
227
+ def render_manifest(result: dict[str, Any]) -> str:
228
+ lines = []
229
+ for mount in result["mounts"]:
230
+ head = f"{mount['alias']} {mount['card_count']} cards precedence={mount['precedence']} role={mount['role']}"
231
+ if mount.get("version"):
232
+ head += f" v{mount['version']}"
233
+ if mount["writable"]:
234
+ head += " [writable]"
235
+ if mount.get("missing"):
236
+ head += " [missing]"
237
+ lines.append(head)
238
+ if mount.get("description"):
239
+ lines.append(f" {mount['description']}")
240
+ lines.append(f" path: {mount['path']}")
241
+ if mount.get("dependencies"):
242
+ lines.append(f" dependencies: {', '.join(mount['dependencies'])}")
243
+ if mount.get("missing_dependencies"):
244
+ lines.append(f" missing dependencies: {', '.join(mount['missing_dependencies'])}")
245
+ lines += [f" manifest error: {e}" for e in mount.get("manifest_errors", [])]
246
+ if mount.get("root_cards"):
247
+ lines.append(f" (root): {mount['root_cards']} cards")
248
+ lines += [f" {folder['name']}/: {folder['cards']} cards" for folder in mount["folders"]]
249
+ if mount.get("folders_truncated"):
250
+ lines.append(f" ... {mount['total_folders'] - len(mount['folders'])} more folders (--max-folders)")
251
+ if mount.get("overview"):
252
+ lines += [""] + [f" {line}".rstrip() for line in mount["overview"].splitlines()]
253
+ lines.append("")
254
+ if result.get("next_offset") is not None:
255
+ lines.append(f"{result['total_mounts'] - result['offset'] - len(result['mounts'])} more mounts: "
256
+ f"--offset {result['next_offset']}")
257
+ if result.get("always_loaded"):
258
+ lines.append("Always-loaded cards: " + _refs(result["always_loaded"]))
259
+ if result.get("onboarding_hint"):
260
+ lines.append("Workspace is empty: mount a notes folder or write a card to get started.")
261
+ staging = result.get("staging") or {}
262
+ if staging.get("pending"):
263
+ lines.append(f"Pending review: {staging['pending']} change(s); see `mindslice diff`.")
264
+ lines += [f" awaiting approval: {item['id']} ({item['reason']})" for item in staging.get("awaiting_approval", [])]
265
+ return _join(lines)
266
+
267
+
268
+ def render_bake(result: dict[str, Any]) -> str:
269
+ """Single-slice bake (``bake @alias``) plan or receipt."""
270
+ dry = result["dry_run"]
271
+ action = "update" if result.get("previous_path") else "create"
272
+ lines = [f"{'Would ' + action if dry else ('Updated' if action == 'update' else 'Created')} "
273
+ f"{result['alias']} v{result['version']}: {result['output']}"
274
+ + (f" ({result['format']})" if result.get("format") not in (None, "directory") else "")]
275
+ if result.get("path") and result["path"] != result["output"]:
276
+ lines.append(f"mounted directory: {result['path']}")
277
+ if result.get("previous_path"):
278
+ lines.append(f"previous version: {result['previous_path']} (left as it was)")
279
+ lines.append(f"cards ({len(result['cards'])}): {', '.join(result['cards'])}")
280
+ for key in ("created", "replaced", "included_dependencies", "retired_overlay_cards"):
281
+ if result.get(key):
282
+ lines.append(f"{key.replace('_', ' ')}: {', '.join(result[key])}")
283
+ if result.get("dependencies"):
284
+ added = result.get("dependencies_added") or []
285
+ lines.append(f"dependencies: {', '.join(result['dependencies'])}" + (f" (new: {', '.join(added)})" if added else ""))
286
+ lines += [f"warning: {w if isinstance(w, str) else w.get('message', w)}" for w in result.get("warnings", [])]
287
+ validation = result.get("validation")
288
+ if validation:
289
+ lines.append(f"validation: {'ok' if validation['ok'] else 'FAILED'}"
290
+ + (f", {validation['warnings']} warning(s)" if validation.get("warnings") else ""))
291
+ lines += [f" error: {e.get('message', e) if isinstance(e, dict) else e}" for e in validation.get("errors", [])]
292
+ if dry:
293
+ lines.append("Dry run: nothing was written.")
294
+ return _join(lines)
295
+
296
+
297
+ def render_cancelled(result: dict[str, Any]) -> str:
298
+ lines = ["Cancelled; nothing changed."]
299
+ if result.get("plan_path"):
300
+ lines.append(f"Destination choices saved in {result['plan_path']}")
301
+ return _join(lines)
302
+
303
+
304
+ # --- errors ------------------------------------------------------------------------------------
305
+
306
+
307
+ def render_next_steps(steps: list[str]) -> str:
308
+ """``Next:`` guidance: one line for a single step, a numbered list for several."""
309
+ if not steps:
310
+ return ""
311
+ if len(steps) == 1:
312
+ return f"Next: {steps[0]}\n"
313
+ return _join(["Next:"] + [f" {n}. {step}" for n, step in enumerate(steps, 1)])
314
+
315
+
316
+ def render_error(error: dict[str, Any]) -> str:
317
+ """``error [code]: message``, readable details, then any ``next_steps`` the error carries, for stderr."""
318
+ head = f"error [{error['code']}]: {error['message']}" if error.get("code") else f"error: {error['message']}"
319
+ lines = [head]
320
+ details = dict(error.get("details") or {})
321
+ steps = details.pop("next_steps", None)
322
+ for key, value in details.items():
323
+ if key == "plan" and isinstance(value, dict):
324
+ if "slices" in value:
325
+ from .bake_review import render_plan
326
+ lines.append(render_plan(value).rstrip())
327
+ elif "previous_path" in value:
328
+ from ..operations.recovery import render_rollback
329
+ lines.append(render_rollback(value).rstrip())
330
+ continue
331
+ if key == "conflicts" and isinstance(value, list):
332
+ lines += [f" conflict: {c.get('address', '')}: {c.get('detail', c.get('reason', ''))}".replace(": ", ": ")
333
+ if isinstance(c, dict) else f" conflict: {c}" for c in value]
334
+ continue
335
+ if isinstance(value, list) and all(isinstance(v, (str, int, float)) for v in value):
336
+ text = ", ".join(map(str, value))
337
+ elif isinstance(value, list) and all(isinstance(v, dict) and "address" in v for v in value):
338
+ text = _refs(value)
339
+ elif isinstance(value, (dict, list)):
340
+ text = json.dumps(value, ensure_ascii=False)
341
+ else:
342
+ text = str(value)
343
+ lines.append(f" {key}: {text}")
344
+ if isinstance(steps, list):
345
+ lines.append(render_next_steps([str(step) for step in steps]).rstrip())
346
+ return _join(lines)
@@ -0,0 +1,92 @@
1
+ """Interactive staged-card review; gather choices before mutating the workspace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from ..errors import InvalidArgumentsError
8
+ from ..operations.staging import select_entries
9
+
10
+
11
+ def _answer(prompt):
12
+ sys.stderr.write(prompt)
13
+ sys.stderr.flush()
14
+ line = sys.stdin.readline()
15
+ if not line:
16
+ raise EOFError
17
+ return line.strip().lower()
18
+
19
+
20
+ def run(ws, ids=None):
21
+ if not sys.stdin.isatty():
22
+ raise InvalidArgumentsError("review needs an interactive terminal; use diff --json, apply ID... or discard ID...")
23
+ snapshot = ws.diff()
24
+ items = {item["id"]: item for item in snapshot["items"]}
25
+ chosen = select_entries(items, ids)
26
+ if not chosen:
27
+ return {"applied": 0, "discarded": 0, "remaining": len(items)}
28
+ accept, reject = [], []
29
+ retry = False
30
+ try:
31
+ for path, item in chosen.items():
32
+ sys.stderr.write(f"\n{item['address']} ({item['change']})\n")
33
+ for change in item.get("changes", []):
34
+ if change.get("reason"):
35
+ sys.stderr.write(f"Reason: {change['reason']}\n")
36
+ if item.get("requires_approval"):
37
+ sys.stderr.write(f"Approval: {item['requires_approval']}\n")
38
+ if item.get("conflict"):
39
+ sys.stderr.write(f"Conflict: {item['conflict']}\n")
40
+ for kind, refs in item.get("references", {}).items():
41
+ if refs:
42
+ sys.stderr.write(f"{kind}: {', '.join(refs)}\n")
43
+ impact = item.get("impact", {})
44
+ for field in ("affected_backlinks", "affected_extensions"):
45
+ affected = impact.get(field, [])
46
+ if affected:
47
+ sys.stderr.write(f"{field} ({impact.get('total_' + field, len(affected))} live cards): "
48
+ + ", ".join(c["address"] for c in affected) + "\n")
49
+ sys.stderr.write(item["diff"])
50
+ while True:
51
+ action = _answer("[Accept] [Reject] [Leave pending] [Cancel]: ")
52
+ if action in ("a", "accept"):
53
+ if item.get("conflict"):
54
+ sys.stderr.write("Resolve the conflict first, reject this proposal, or leave it pending.\n")
55
+ continue
56
+ accept.append(path)
57
+ elif action in ("r", "reject"):
58
+ reject.append(path)
59
+ elif action in ("", "l", "leave", "leave pending", "skip"):
60
+ pass
61
+ elif action in ("c", "cancel"):
62
+ return {"cancelled": True}
63
+ else:
64
+ continue
65
+ break
66
+ sys.stderr.write(f"\nAccept: {len(accept)}; reject: {len(reject)}; "
67
+ f"leave pending: {len(items) - len(accept) - len(reject)}.\n")
68
+ if not accept and not reject:
69
+ return {"applied": 0, "discarded": 0, "remaining": len(items)}
70
+ plan = ws.review(accept, reject, revision=snapshot["revision"], dry_run=True)
71
+ if not plan["ready"]:
72
+ for conflict in plan["conflicts"]:
73
+ sys.stderr.write(f"Blocked: {conflict.get('address', '')}: "
74
+ f"{conflict.get('detail', conflict['reason'])}\n")
75
+ retry = _answer("Review choices again? [yes/No]: ") in ("y", "yes")
76
+ if not retry:
77
+ return {"cancelled": True, "conflicts": plan["conflicts"]}
78
+ elif _answer("Execute these choices? [yes/No]: ") not in ("y", "yes"):
79
+ return {"cancelled": True}
80
+ except (EOFError, KeyboardInterrupt):
81
+ sys.stderr.write("\nReview cancelled; no review choices were executed.\n")
82
+ return {"cancelled": True}
83
+
84
+ if retry:
85
+ return run(ws, ids)
86
+ result = ws.review(accept, reject, revision=snapshot["revision"])
87
+ result["remaining"] = len(ws.diff()["items"])
88
+ if result["applied"]:
89
+ sys.stderr.write("Accepted memory is saved and available now. Bake is optional packaging/versioning.\n")
90
+ else:
91
+ sys.stderr.write("No live memory changes applied; see the receipt for rejected or skipped proposals.\n")
92
+ return result
@@ -0,0 +1 @@
1
+ """Card representation, addressing, lineage rules and text serialization."""