zettelkasten.review¶
zettelkasten.review ¶
Literature-review overlay: the editorial layer over the claim/paper engine.
Where :mod:zettelkasten.papers exposes scored papers and
:mod:zettelkasten.claims the claim read layer, this module assembles a
review — a curated, theme-structured narrative over the existing corpus —
and the durable editorial overlay an author layers on top of it. Like its
siblings it is parametrized by a get_graph callable and is pure given its
inputs; the only side effects live in :func:create_review /
:func:update_review, which persist a _reviews/<name>.yaml manifest through
the crash-safe substrate (atomic write + per-review lock + debounced commit).
The design separates two strictly different things:
- The projection (:func:
assemble_review) — a deterministic, LLM-free PROJECTION of the corpus: themes (reusing :func:syllabus.theme_model), the ranked key claims (reusing :mod:zettelkasten.claims) grouped by theme, each theme'sholding_pen(in-theme papers engaged with no claim) andcompleteness. It owns no editorial state and is recomputed from scratch on every call. - The overlay (the manifest's
overlayblock) — the author's editorial decisions stored as ID-REFERENCES, never a copy of the projection: per-claimtiering,exclusions, narrativeordering,section_renames, and author-insertedscaffolding(notes/questions/todos as first-class ordered items).
:func:reconcile (via :func:reproject + :func:apply_overlay) merges the two:
it preserves editorial edits whose referenced ids still exist, drops overlay
entries whose ids vanished upstream (recorded as removed_upstream rather than
crashing), and surfaces projection entities with no overlay placement as
unplaced. A regenerate therefore never silently loses editorial work and
never crashes on a deleted upstream id.
Claim membership is explicit-edge-only (a claim belongs to whatever its
support/contradiction edges connect it to — there is no embedding-inferred claim
membership); theme/paper membership reuses :func:theme_model.
assemble_review ¶
assemble_review(get_graph: GetGraph, *, project: str = '', graph: str = '', graphs_dir: 'Path | None' = None, namespace: Callable[[str], str] | None = None, localize: Callable[[str], str] | None = None, scope_name: str = '') -> dict[str, Any]
The review-core PROJECTION over the existing corpus (no LLM, no writes).
Builds the corpus + theme model exactly as :func:papers.assemble_papers
does (build_corpus/_graph_scoped_corpus → :func:theme_model with
relation_edges/landscape_hubs/anchor_scores), ranks the key
claims (:func:claims.key_claims), groups them by theme (via each claim's
core/home/supporting papers), and per theme produces the theme's claims,
a holding_pen (in-theme papers engaged with no claim), and a
completeness ratio (claim-engaged theme papers ÷ all theme papers).
Deterministic and JSON-serializable. Claims whose papers sit in no theme go
in the top-level unplaced bucket (the untagged band).
Source code in zettelkasten/review.py
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 | |
reproject ¶
reproject(get_graph: GetGraph, manifest: dict[str, Any], *, graphs_dir: 'Path | None' = None, namespace: Callable[[str], str] | None = None, localize: Callable[[str], str] | None = None) -> dict[str, Any]
Recompute the fresh projection for a manifest's scope (no overlay).
Source code in zettelkasten/review.py
apply_overlay ¶
apply_overlay(projection: dict[str, Any], overlay: Any, *, split_derived: bool = False) -> dict[str, Any]
Merge a stored overlay onto a fresh projection — the durability merge.
Preserves every overlay edit whose referenced id STILL EXISTS in the
projection; DROPS overlay entries whose referenced id vanished upstream
(collected into removed_upstream — never a crash); and SURFACES
projection claims that still need authorial attention into unplaced (the
worklist of not-yet-fully-placed claims). The id-keying is robust to ids that
no longer exist on either side, so a regenerate can neither lose editorial
work nor raise on a deleted upstream id.
When split_derived is set (the opt-in theming path used by
:func:reconcile), DERIVED (cluster / cold-start) themes are lifted OUT of
the placed themes list into suggested_themes and their claims fall
back to unplaced; only landscape hubs and proposed drafts stay placed.
The default (False) preserves the historical behavior — every derived
theme is placed — so structural consumers like the outline gatherer are
unaffected. suggested_themes is always present (an empty list unless
split_derived lifts any), keeping the returned dict backward-compatible.
Invariant: every projected, non-excluded claim ALWAYS retains at least one
payload row — in its theme if themed, and in unplaced until it is fully
placed (themed AND positioned by ordering). ordering controls
narrative ORDER only; it never determines membership/visibility. In
particular an UNTAGGED claim (with no theme home) is never suppressed from
unplaced merely because it carries an ordering ref — that bug let
default-seeded untagged claims vanish into a dangling reference.
The returned dict is a DERIVED, READ-ONLY VIEW. It must NEVER be persisted
back to disk: only the stored overlay (mutated by :func:create_review /
:func:update_review) is ever written. Writing this view back would bake the
transient projection into the durable manifest and could destroy editorial
work on a degenerate/partial load.
Source code in zettelkasten/review.py
1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 | |
reconcile ¶
reconcile(get_graph: GetGraph, name: str | None = None, *, manifest: dict[str, Any] | None = None, outline_id: str = '', graphs_dir: 'Path | None' = None, namespace: Callable[[str], str] | None = None, localize: Callable[[str], str] | None = None, split_derived: bool = True) -> dict[str, Any]
Load (or accept) a manifest, reproject, and apply an outline's overlay.
The top-level read path for a stored review: returns the reconciled state
the UI/consumers render — fresh projection with the editorial overlay merged
on top (see :func:apply_overlay). Pass either name (to load
_reviews/<name>.yaml) or an in-memory manifest.
outline_id selects WHICH outline's structure to reconcile. The default
(empty / "outline") uses the manifest overlay verbatim — identical to the
historical behavior. An additional outline id merges the shared review-wide
overlay (tiering/exclusions/favorites, from the manifest) with that outline's
own per-outline structure (from the sidecar) and reports the outline's own
title/question. The result carries outline_id so consumers know which
outline they are looking at.
The returned state is a DERIVED, READ-ONLY VIEW and must NEVER be written
back to the manifest. Only the stored overlay (mutated by
:func:create_review / :func:update_review) is persisted; persisting this
reconciled view would bake a transient projection into the durable overlay
and could lose editorial work (see the staleness guard in
:func:apply_overlay, which keys the stale flag on the result).
split_derived selects the theming policy (opt-in split, see
:func:apply_overlay). It DEFAULTS to True — the builder/board policy the
dashboard and the outline gatherer render, where DERIVED (cluster / cold-start)
tiers are lifted out of themes into suggested_themes so the exported
document matches the left pane 1:1. NON-builder MCP consumers that treat
derived tiers as existing centroids — the review load read path and
propose_placements' REBUILD clustering — pass split_derived=False to
keep every derived theme placed (their historical behavior), so they do not
re-cluster already-derived groupings from scratch.
Source code in zettelkasten/review.py
1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 | |
create_review ¶
create_review(name: str, *, title: str | None = None, project: str = '', graph: str = '', question: str | None = None, graphs_dir: 'Path | None' = None, get_graph: GetGraph | None = None) -> dict[str, Any]
Create (or return) the _reviews/<name>.yaml manifest — idempotent.
A fresh review starts PRISTINE: an empty overlay with no seeded ordering, so
its default structure reports as unbuilt and the dashboard shows the blank
"build a structure" state. (get_graph is accepted for call-site
compatibility but no longer used at creation.) If the review already exists it
is returned unchanged except that a supplied title/question is updated
WITHOUT clobbering the overlay. All writes go through the substrate
(per-review lock → atomic write → scheduled commit).
Source code in zettelkasten/review.py
update_review ¶
update_review(name: str, *, title: str | None = None, question: str | None = None, project: str | None = None, graph: str | None = None, ordering: list[Any] | None = None, set_tier: Any = None, exclude: Any = None, unexclude: Any = None, add_scaffold: dict[str, Any] | None = None, remove_scaffold: Any = None, rename_section: Any = None, set_section_note: Any = None, set_placement: Any = None, favorite: Any = None, unfavorite: Any = None, reset_structure: bool = False, save_structure: bool = False, restore_structure: bool = False, add_proposed_claim: dict[str, Any] | None = None, remove_proposed_claim: Any = None, add_proposed_theme: dict[str, Any] | None = None, remove_proposed_theme: Any = None, remove_derived_theme: Any = None, restore_derived_theme: Any = None, promote_proposed_theme: dict[str, Any] | None = None, undo_promote: bool = False, set_section_framing: dict[str, Any] | None = None, set_apex: Any = _UNSET, outline_id: str = '', graphs_dir: 'Path | None' = None) -> dict[str, Any]
Apply one or more overlay mutations to an existing review and persist.
Supported mutations (any combination, all optional):
title/question— manifest scalars.project/graph— the projection SCOPE scalars. PassingNoneleaves a scalar unchanged; passing""clears it (back to the full corpus). Changing scope re-projects on the next reconcile.ordering— replace the narrative order (reorder).set_tier—{uid: tier}or(uid, tier); aNonetier clears.exclude/unexclude— add/remove claim/paper id(s) from exclusions (an excluded id is also pulled out of the ordering).add_scaffold—{"kind", "text", "id"?, "position"?}; appends (or inserts atposition) a scaffolding item AND an ordering ref for it.remove_scaffold— scaffold id(s) to remove (also pulled from ordering).rename_section—{theme_id: label}or(theme_id, label); aNonelabel clears the rename.set_section_note—{theme_id: text}or(theme_id, text); sets the theme's freeform author note. ANone/empty text clears it.set_placement—{uid: theme_id}or(uid, theme_id); places a claim into a theme (drag-to-place) and clears any standing unplaced marker. ANonetheme_id RETURNS THE CLAIM TO THE UNPLACED WORKLIST: it drops the placement override AND records an explicitunplaced_claimsmarker, so a projection-homed claim stays unplaced instead of snapping back to its projection theme.favorite/unfavorite— claim uid(s) to star / unstar.reset_structure— when truthy, snaps the theme layout back to the engine's projection: clears ALL placements and section renames, and drops everyclaimandsectionordering ref (scaffold refs are preserved so author interjections keep their narrative spots). Tiers, favorites, exclusions, scaffolds, and proposed claims/themes are left intact. The_crossgraph is untouched — this only resets the overlay's structure.save_structure— when truthy, captures the CURRENT theme layout (placements, section renames, ordering, and the detach-carriedsection_framing/apex) intooverlay.saved_structureas a restore point. Overwrites any previous snapshot (single-slot). The human-defined counterpart to the engine projection thatreset_structuresnaps back to.restore_structure— when truthy, reapplies the layout captured by the most recentsave_structure(placements, section renames, ordering, and the detach-carriedsection_framing/apex), leaving the snapshot itself intact so it can be restored again. Tiers, favorites, exclusions, scaffolds, and proposed items are untouched. RaisesValueErrorif there is no saved structure to restore.add_proposed_claim—{"title", "body"?, "theme"?, "tier"?, "id"?, "position"?}; appends an OVERLAY-ONLY proposed claim (never a_crossnode) and an ordering ref for it, optionally placing it intothemeand tiering it. Reusing the sameidoverwrites that proposed claim's title.remove_proposed_claim— proposed-claim id(s) to delete from the overlay entirely (the structure-only delete), pulling any placement / ordering / tier / favorite / exclusion ref along with it.add_proposed_theme—{"title", "body"?, "members"?, "id"?, "position"?, "claims"?}; appends an OVERLAY-ONLY proposed SECTION (never a_crosslandscape hub) and asectionordering ref for it, optionally placing the claim uids inclaimsinto it. Reusing the sameidoverwrites that section's title/body/members.remove_proposed_theme— proposed-theme id(s) to delete from the overlay entirely, pulling its ordering ref and rename along with it and returning every claim placed into it to its projection home / unplaced (the placements pointing at it are cleared).remove_derived_theme— DERIVED (cluster/cold-start/ hybrid-remainder) theme id(s) to SUPPRESS. A derived theme has no_crosshub to soft-delete and is recomputed from the corpus each projection, so it cannot be deleted on disk; suppression hides the section at reconcile (the section analogue of an exclusion). Mirrorsremove_proposed_theme's structural cleanup: drops the section's ordering ref + rename + note and returns every claim placed into it to its projection home / unplaced. The claims are never deleted. Reversible viarestore_derived_theme. Use the on-diskdelete_themefor a reallandscapehub, not this.restore_derived_theme— derived theme id(s) to UN-suppress, bringing the section back into the view (the reverse ofremove_derived_theme).promote_proposed_theme—{"theme_id", "hub_id", "claim_remap"?}; the OVERLAY half of promoting a draft section to a real landscape hub (the caller has already materialized the_crosshub + claims). Drops the proposed theme and the promoted draft claims, RE-POINTS the section's ordering ref + rename + every placement fromtheme_idtohub_id, and RE-KEYS each promoted draft claim's placement/ordering/tier/favorite/ exclusion from its draft id to its materialized uid (claim_remapmapsdraft_id → new_uid). The graph writes happen in the producer; this only reconciles the overlay so the now-real hub keeps the draft's editorial state.undo_promote— when truthy, reverses the OVERLAY half of the most recent promote by restoring theoverlay_beforesnapshot stashed inoverlay.promote_undo(and clearing it). The caller (route) has already discarded the materialized_crosshub + claims. Single-level: only the most recent promote is undoable, and the restore discards overlay edits made after that promote. RaisesValueErrorif there is nothing to undo.set_section_framing—{theme_id: {"concepts": [uid,...], "fallback": [uid,...], "order": [uid,...]}}; REPLACES the per-outline spine-DERIVED framing snapshot (an empty dict clears it). Carried by a "Detach from spine" so the settled export can rebuild each section's concept tags + claim-sparse fallback units live, and restore the within-section unit order (order) the live spine export used. Omitted (None) → no change.set_apex— the outline-level apex synthesis-root framing dict (orNoneto clear it); re-surfaced on the settled export'sscopefor a detached outline. Omitted (the_UNSETsentinel) → no change.
The read-modify-write is wrapped in :func:review_write_lock and persisted
atomically via the substrate. Raises FileNotFoundError if the review
does not exist.
Source code in zettelkasten/review.py
2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 | |
detach_spine ¶
detach_spine(get_graph: Any, name: str, *, outline_id: str = '', project: str = '', graph: str = '', graphs_dir: 'Path | None' = None, namespace: 'Callable[[str], str] | None' = None, localize: 'Callable[[str], str] | None' = None) -> dict[str, Any]
Snapshot an outline's SPINE partition into its overlay, then clear the spine.
Turns a live, spine-organized outline into an editable THEME outline: the
spine's dimension sections are materialized into the overlay as proposed
themes + single-home placements, then the outline's spine ref is
cleared so the board overlay (no longer overridden by the spine) becomes the
authoritative structure.
The snapshot is taken from the SAME gather the spine view renders
(:func:zettelkasten.outline.gather_outline_material with the spine active).
Parity is asymmetric by kind: the detached export reproduces the spine export's
FRAMING — concept tags, claim-sparse fallback units, the apex, and the
within-section unit order — 1:1, but grounded CLAIMS are deliberately
SINGLE-HOMED. The live spine export MULTI-homes a claim (it can appear in every
dimension whose membership carries it); the editable overlay's placements
map is single-valued, so each claim is collapsed FIRST-WINS in dimension order
into exactly one section. That multi-membership→single-home collapse is the one
accepted lossy step of detach, surfaced to the user in the detach confirmation.
- Sections — one proposed theme per spine dimension, in the export's
dimension order (the matrix column order), each keyed by a DETERMINISTIC id
(
proposed-theme-<node_id>) so a re-run overwrites rather than duplicates. The section label is the dimension's label; empty dimensions become empty sections (matching the spine export's skeleton). - Membership — a spine is MULTI-membership (a claim can belong to
dimensions A and B); the overlay
placementsmap is SINGLE-home. Each GROUNDED claim is collapsed FIRST-WINS in dimension order (deterministic), so it lands in exactly one section. - Order — per-claim
orderingrefs are written in the export's within-section narrative order, so the settled branch renders each section in the SAME order rather than bare projection order. - Derived framing — spine-DERIVED framing (per-dimension concept tags,
claim-sparse fallback units, and the apex synthesis root) is ALSO carried,
so the detached outline reproduces the spine export's FRAMING, not just its
claims. Framing units cannot ride
placementsat all —placementsmaps a grounded-CLAIM uid to a single section, whereas concept tags and fallback bundles are DERIVED rendering artifacts, not claim placements — so the detach records a per-sectionsection_framingsnapshot (concept-tag note uids + claim-sparse fallback unit uids + the within-section unitorder, keyed by the settled section id) plus the outline-levelapex; the settled export rebuilds those units LIVE from the note keys and restores the capturedorder(see the settled branch of :func:zettelkasten.outline.gather_outline_material), skipping any uid whose note no longer resolves. Theordercarry matters because the live spine export narrative-orders claims and framing fallback units TOGETHER (asynthesisfallback can sit BEFORE a claim), whereas the settled branch keeps the board's authoritative claim order and would otherwise append framing units last — so withoutorderthe detached within-section order would DIVERGE from the live export. Concepts are SINGLE-HOMED in both the live spine export and this snapshot: the live export adds each concept to the shared placed set, so it surfaces only in the FIRST dimension whose membership carries it, and this snapshot inherits that (it records exactly whatmaterial.sectionsshows) — matching the live spine export is the parity target. The SOURCE notes are untouched and keep projecting in sibling outlines and on the review board.
Replace, not append + recovery. Before writing the snapshot the outline's structural overlay (layout + every pre-existing proposed section) is CLEARED, so no dormant board-edited section survives the detach and a retry re-derives cleanly from the live spine (a membership change between a failed clear and a retry can never mis-home a claim). The overlay write happens FIRST, then the spine ref is cleared. If the clear fails after the overlay write, the state stays RECOVERABLE: the overlay sits DORMANT under the still-active spine, and a retry re-clears + re-writes the same (idempotent) snapshot and finishes the clear — never a half-materialized loss.
Raises FileNotFoundError if the review / outline does not exist, and
ValueError if the outline has no spine or the spine ref does not resolve
to a materialized spine in this scope (nothing to detach).
Source code in zettelkasten/review.py
2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 | |
attach_spine_members ¶
attach_spine_members(get_graph: Any, name: str, *, spine: str, members: 'list[dict[str, Any]]', outline_id: str = '', project: str = '', graph: str = '', graphs_dir: 'Path | None' = None, namespace: 'Callable[[str], str] | None' = None, localize: 'Callable[[str], str] | None' = None) -> dict[str, Any]
Attach author-placed claims to a spine's dimension nodes — WRITES.
The complement of a spine-organized board being a READ-ONLY VIEW of the spine
(its sections come from spine membership, not the board overlay): when the
Structure builder builds an outline with a spine as its section axis, the
claims the author placed under each dimension theme are routed HERE into that
spine dimension as spine-member edges (dim --spine-member--> note@home),
so they read back into the board's dimension section instead of being
discarded. Editing a spine-organized board therefore means editing the spine —
exactly what this does.
Membership is stored SPINE-SIDE (§11 of the spine-promotion design), matching
:func:organizations._attach_members: each edge is an OUTGOING cross-graph
link FROM the dimension node IN THE SPINE GRAPH to the member's base note
(target_graph = the claim's home). The base note is never mutated. The
write is idempotent (SpineGraphOps.ensure_link dedupes) and ADDITIVE — it
never removes membership — so re-running (e.g. re-attaching a claim already a
member) is a no-op.
members is a list of {"dimension": <node_id>, "claims": [{"id","graph"}, ...]}
groups. A group whose dimension node does not resolve in the spine graph, or a
claim missing id/graph, is skipped (counted in skipped). Returns
{"spine", "attached", "skipped"}.
Raises FileNotFoundError if the review does not exist and ValueError
if spine is empty or does not resolve to a materialized spine in scope.
Source code in zettelkasten/review.py
2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 | |
rename_review ¶
Rename a review's slug — move its on-disk artifacts and re-stamp name.
The slug is the _reviews/<name> filename stem that keys EVERY one of a
review's files: the durable manifest (<name>.yaml), the regenerable
outline sidecar (<name>.md), and the persisted matrices
(<name>.tables.json). A rename therefore moves all three (whichever
exist) to the <new_name> stem and updates the manifest's own name
field so the in-file id agrees with its filename.
Both names are path-traversal validated. Raises FileNotFoundError if the
source review does not exist and ValueError if new_name is already
taken (the rename must never clobber another review). A no-op (new_name
equal to name) just returns the current manifest.
Both slugs' write locks are held for the move (acquired in a stable sorted
order so two concurrent renames can't deadlock), and every moved path — the
vacated source AND the new destination — is scheduled for a debounced
zettelkasten-mcp commit so the rename is version-controlled exactly like
a create/delete pair.
Returns the updated manifest (with the new name).
Source code in zettelkasten/review.py
2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 | |
delete_review ¶
Delete a review's manifest (and its regenerable outline sidecar).
Removes the durable _reviews/<name>.yaml overlay AND, when present, the
regenerable _reviews/<name>.md outline artifact, then schedules each
removed path for a debounced zettelkasten-mcp git commit so the deletion
is version-controlled exactly like every other review write (the committer
stages a tracked-but-deleted path with git add -A).
The read-modify-delete is wrapped in :func:review_write_lock (the same
two-layer thread + cross-process lock the writers take) so a concurrent
create/update in the MCP server or the dashboard backend can never race the
removal. Idempotent: deleting an absent review is a no-op that returns
{"deleted": False} rather than raising. The name is validated against
path traversal before any file is touched.
Returns {"name", "deleted", "removed"} where removed lists the
repo-relative paths actually unlinked.