zettelkasten.organizations¶
zettelkasten.organizations ¶
Organizations: first-class, owner-scoped matrix configurations.
An organization is a named, persistent matrix definition — a row axis plus an
ordered set of typed columns — that lives at the owner (project or graph)
level rather than buried inside a single review's .tables.json. Decoupling
the definition from the review means the same "lens" on a corpus can be reused
across reviews, listed in a project switcher, and (later) promoted from a live
read-only projection into a materialized graph.
An organization has two states:
lens— a live, read-only projection. The grid is derived on demand from the org's row axis + columns; nothing is written back to the corpus.spine— a materialized structure.spine_refnames the apex graph the organization writes back into. (Promotion is Phase 2; 1a only models the field.)
Storage mirrors the project/review artifact discipline but keys by owner:
<base>/_organizations/<owner_type>/<owner_name>/<id>.json # one org per file
<base>/_organizations/<owner_type>/<owner_name>/index.json # derived listing
<base>/_organizations/<owner_type>/<owner_name>/_migrated.json # migration marker
The per-file layout gives each organization an independent, git-trackable
artifact (clean diffs, no whole-store write contention) while index.json is a
derived read-accelerator — rebuilt by scanning the directory on every write, so
it can never drift from the source-of-truth files. All writes go through the same
M3a substrate as reviews (review_write_lock → atomic_write_text →
_schedule_zettel_commit), keyed by a per-owner lock.
A lazy, idempotent migration imports each legacy _reviews/<name>.tables.json
into organizations owned by that review's scope, so existing matrices appear as
organizations the first time an owner is listed — without re-importing a table a
user later deletes (a per-owner marker records which reviews were already
absorbed).
normalize_organization ¶
normalize_organization(raw: Any, *, owner_type: str, owner_name: str, org_id: str = '') -> dict[str, Any]
Canonicalize an organization dict to its on-disk shape.
Reuses the matrix engine's :func:normalize_row_axis / :func:normalize_columns
so an organization's definition is byte-for-byte compatible with what
build_matrix consumes. Timestamps are preserved when present (so a re-save
keeps created_at); :func:save_organization fills the blanks.
Source code in zettelkasten/organizations.py
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 | |
save_organization ¶
save_organization(raw: dict[str, Any], *, owner_type: str, owner_name: str, org_id: str = '', graphs_dir: 'Path | None' = None) -> dict[str, Any]
Create or update an organization; returns the persisted (normalized) dict.
created_at is preserved across updates and stamped on first write;
updated_at is refreshed every save. The derived index is rebuilt after the
write so a subsequent :func:list_organizations is consistent.
Source code in zettelkasten/organizations.py
load_organization ¶
load_organization(owner_type: str, owner_name: str, org_id: str, *, graphs_dir: 'Path | None' = None, migrate: bool = True) -> 'dict[str, Any] | None'
Load one organization by id, or None if it doesn't exist.
Source code in zettelkasten/organizations.py
list_organizations ¶
list_organizations(owner_type: str, owner_name: str, *, graphs_dir: 'Path | None' = None, migrate: bool = True) -> list[dict[str, Any]]
All organizations for an owner (full definitions), newest-updated first.
Triggers a lazy, idempotent migration of any legacy review tables scoped to
this owner the first time it runs (disable with migrate=False).
Source code in zettelkasten/organizations.py
delete_organization ¶
delete_organization(owner_type: str, owner_name: str, org_id: str, *, graphs_dir: 'Path | None' = None) -> bool
Remove an organization file. Returns True if it existed.
The migration marker is intentionally untouched: deleting a migrated org must NOT cause it to be re-imported on the next listing.
Source code in zettelkasten/organizations.py
org_id_for ¶
find_owner ¶
Locate which owner currently holds an org with org_id (stable identity).
Scans both owner trees for <org_id>.json. Because an org id is
owner-independent (<review>-<table_id>), this recovers the owner an org
was created under even after the originating review's manifest scope was
edited or the manifest was deleted entirely. The registry — not the mutable
review manifest — is the source of truth for an existing org's location, so
rename/delete must resolve ownership through here before falling back to the
manifest (otherwise a re-scoped or deleted review silently orphans the org).
Deterministic: owner types are scanned in sorted order; in practice a given id exists under at most one owner.
Source code in zettelkasten/organizations.py
upsert_definition ¶
upsert_definition(owner_type: str, owner_name: str, *, review: str, table_id: str, title: str = '', row_axis: Any = None, columns: Any = None, state: 'str | None' = None, spine_ref: 'str | None' = None, overlay: Any = None, signature: str = '', generated_at: str = '', graphs_dir: 'Path | None' = None) -> dict[str, Any]
Create/refresh the organization mirroring a review's table definition.
The authored definition (title / row axis / columns) is overwritten, but the
lifecycle fields (state / spine_ref) and the author's corrections
overlay are PRESERVED across rebuilds unless explicitly overridden — so
regenerating a grid never silently demotes a promoted spine back to a lens nor
loses editorial corrections. created_at is preserved too.
A promoted spine also carries two lifecycle fields that are not part of the
authored definition and must survive a live rebuild the same way: the stashed
pre-promotion lens_definition (what demote/delete/re-promote restore and
re-key durable node ids from) and the embedded schema (the spine's own
extraction rubric). Preserving them here keeps a routine grid regenerate — the
_mirror_upsert path fired on every build_matrix — from silently dropping
them on a promoted org (which would break demote reversibility and re-source
extraction). normalize_organization would otherwise reset a lens_definition
to None and a schema to {}.
Source code in zettelkasten/organizations.py
714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 | |
rename_definition ¶
rename_definition(owner_type: str, owner_name: str, *, review: str, table_id: str, title: str, graphs_dir: 'Path | None' = None) -> bool
Patch a mirrored org's display title. Returns True if the org existed.
Source code in zettelkasten/organizations.py
retitle_spine_apex ¶
retitle_spine_apex(owner_type: str, owner_name: str, org_id: str, *, get_graph: Any, graphs_dir: 'Path | None' = None) -> bool
Re-title a materialized spine's apex node to the org's current display title.
A matrix rename updates the mirrored org title (rename_definition) but does
NOT touch the already-materialized apex NODE, so the apex would keep showing the
title it was minted with until the next re-promote. This re-titles it in place.
The apex is keyed by the durable spine.APEX_NODE_ID sentinel, so this
re-titles the SAME node (never mints a second apex) and preserves every edge on
it (including the primary sub-spine link). A no-op — returns False — for a
lens/unmaterialized org, a blank title, or a spine with no resolvable apex.
Callers that rename a matrix should call this (with get_graph) so the rename
reaches the apex immediately; tables.generate_matrix also invokes it on every
readback/resync so a rename propagates on the next regenerate without a route
change.
Source code in zettelkasten/organizations.py
delete_definition ¶
delete_definition(owner_type: str, owner_name: str, *, review: str, table_id: str, graphs_dir: 'Path | None' = None) -> bool
Remove the org mirroring a (review, table_id). Returns True if it existed.
Source code in zettelkasten/organizations.py
spine_is_synced ¶
True when a materialized spine already reflects the corpus + corrections.
A spine is synced when neither its in-scope corpus nor its edge-affecting
overlay corrections have changed since the last promote/resync stamp. When it is,
a Generate can READ BACK the grid over the existing spine-member edges instead
of re-materializing (re-routing + re-synthesizing + edge writes) — "open a matrix
I already built" becomes O(read), not an agent round-trip.
Recomputes :func:_synced_fingerprint and compares it to the synced_sig
stamp. Deliberately conservative — an empty/missing stamp, an empty recompute, or
any hiccup returns False so we resync rather than risk skipping a needed
materialize. A legacy spine (no synced_sig) therefore resyncs once (stamping
the field) and takes the fast path only thereafter.
Source code in zettelkasten/organizations.py
promote_organization ¶
promote_organization(owner_type: str, owner_name: str, org_id: str, *, get_graph: Any, graphs_dir: 'Path | None' = None, attach_relation: 'str | None' = None, row_relation: 'str | None' = None, link_relation: 'str | None' = None, group_fn: Any = None, proposed_grid: 'list[dict[str, Any]] | None' = None, tag_stamp: bool = False) -> dict[str, Any]
Materialize an org lens into a spine graph + bulk-attach its members.
Builds the spine scaffold (apex + one dimension per materializable column +
one hub per row), bulk-attaches the current cell members to their
dimension + hub nodes, sets state='spine' + spine_ref, and rewrites
the org's row axis + columns to their LINK-FORM so future builds route
deterministically via the materialized edges. The pre-promotion definition is
stashed in lens_definition for demote/delete_spine to restore.
Membership comes from the reconciled grid the live build route persisted (so a
semantic/agent lens materializes the agent-routed membership the author saw,
not an empty deterministic grid); group_fn is the agent seam a caller can
thread for a semantic lens with no persisted grid. The whole load→materialize→
save runs under a per-org lock so concurrent promote/resync/delete can't
clobber spine_ref/lens_definition or double-write edges.
The synthesis graph is NOT registered as a project source (spines are additive, selectable overlays — not corpus members).
A re-mining attach reuses this exact path by passing proposed_grid — an
already-classified, approved schema-matrix grid (rows whose cells carry
members with provenance='reviewer-inferred' + confidence/verified/quote).
Promotion then materializes it through the SAME scaffold/attach/reconcile
machinery (membership is reconciled, never add-only, so a re-run prunes
members no longer classified and adds new ones — idempotent). Members are
ADDITIVE to existing dimension nodes: a node's synthesized body (e.g. a ported
persona spine's portrait) is never overwritten. tag_stamp (default OFF) is
the one opt-in that mutates base notes — stamping each materializable column's
dimension tag onto its classified members so a future deterministic build
routes them without the classifier.
Source code in zettelkasten/organizations.py
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 1408 1409 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 | |
demote_organization ¶
demote_organization(owner_type: str, owner_name: str, org_id: str, *, graphs_dir: 'Path | None' = None) -> dict[str, Any]
Stop treating a spine as materialized: state='lens', KEEP nodes/edges.
Non-destructive — the synthesis graph, its nodes, and the attach edges all
survive (so a later re-promote / resync still finds them). spine_ref IS
cleared and its old value stashed in last_spine_ref: state=='spine' with
a non-empty spine_ref is the SINGLE source of truth for "materialized", so a
demoted org must read as a lens (state=='lens', blank spine_ref) while
last_spine_ref lets a later re-promote re-bind the SAME preserved graph. The
link-form row axis + columns are reverted to the stashed lens_definition so
the org reads as the agent-driven lens again.
Source code in zettelkasten/organizations.py
resync_organization ¶
resync_organization(owner_type: str, owner_name: str, org_id: str, *, get_graph: Any, graphs_dir: 'Path | None' = None, attach_relation: 'str | None' = None, row_relation: 'str | None' = None, link_relation: 'str | None' = None, group_fn: Any = None) -> dict[str, Any]
Incrementally re-route NEW/unrouted in-scope notes into an existing spine.
Re-runs the (lens-form) fill to recover the current membership, then adds any
MISSING attach edges (idempotent — existing edges are deduped). Corrections
are honored, not undone: a member_remove'd note is never re-attached, and
a member_add'ed note is attached like any other member. Refreshes the
org's drift signature. Unlike promote (which may reuse the persisted reconciled
grid), resync RE-ROUTES through group_fn via a fresh gather_rows so a
drifted corpus's new notes + overlay member_adds reach an EXISTING semantic
group instead of being silently dropped by a stale grid; it runs under the
per-org lock.
Source code in zettelkasten/organizations.py
materialize_themes ¶
materialize_themes(owner_type: str, owner_name: str, org_id: str, themes: 'list[dict[str, Any]] | None', *, get_graph: Any, graphs_dir: 'Path | None' = None, link_relation: 'str | None' = None, row_relation: 'str | None' = None) -> dict[str, Any]
Materialize approved THEMES as row-hubs under a materialized spine.
Design §8.1/§8.2: a kept theme is a ROW-HUB grouping its member sources under
the single spine — additive over the default per-source rows. Each theme
becomes one hub node (tagged :data:_THEME_HUB_TAG, keyed on the durable
:func:_theme_row_id), linked from the apex, with a spine-member edge to
every member note. Membership is RECONCILED (set_member_targets) so a
regenerate that drops a note prunes its edge; hub identity is durable
(ensure_hub re-titles the SAME hub on a relabel rather than orphaning it).
themes is a list of {"label": str, "note_ids": ["<graph>::<id>", …]}
(the shape :func:tables.suggest_from_description emits). When it is empty /
None this is a NO-OP that PRESERVES any existing theme hubs (an empty list
means "no new theme layer", NOT "wipe the themes") — the flat source-row grid
is today's behavior. When it is non-empty the passed set is authoritative:
theme hubs whose row id is not in it are pruned, mirroring the source-row
defunct-hub prune, and any live overlay correction keyed on a pruned theme hub
is SURFACED (never silently dropped) in orphaned_corrections.
North-star invariant: base notes are NEVER mutated — every edge is a
spine-side outgoing spine-member / structural link FROM the theme hub in
the spine graph. Runs under the per-org spine lock so it can't race a
concurrent promote/resync/delete.
Returns {"theme_hubs": {label: hub_id}, "attached_edges": int,
"orphaned_corrections": [...]}. A degrade (org missing / not a spine)
returns empties rather than raising, so Generate never fails just because a
theme could not be materialized.
Source code in zettelkasten/organizations.py
2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 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 | |
delete_spine ¶
delete_spine(owner_type: str, owner_name: str, org_id: str, *, get_graph: Any, graphs_dir: 'Path | None' = None) -> dict[str, Any]
Teardown of a spine: delete the spine graph folder, revert org to a lens.
Membership is stored spine-side, so teardown is trivial — deleting the spine
graph removes every spine-member edge with it and base note files stay
pristine (dangling edges are structurally impossible). A best-effort legacy
base-side scrub handles spines materialized under the old model. Then clear
spine_ref + revert to lens + clear any dangling default-spine pointer.
The org itself is kept (as a lens) — only the materialization is removed.
Source code in zettelkasten/organizations.py
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 | |
connect_sub_spine ¶
connect_sub_spine(child_owner_type: str, child_owner_name: str, child_org_id: str, parent_owner_type: str, parent_owner_name: str, parent_org_id: str, *, get_graph: Any, graphs_dir: 'Path | None' = None) -> dict[str, Any]
Nest the CHILD spine beneath the PARENT spine (a primary sub-spine edge).
Resolves each org's synthesis graph + apex node and writes the durable primary
child-apex --component-of--> parent-apex cross-graph edge on the child apex
(idempotent — a re-connect to the same parent is a no-op). A child has exactly
ONE primary parent, so connecting a child that already hangs under a DIFFERENT
parent MOVES it (the stale primary edge is removed first). This is an explicit
hand-authoring action, so it always wins over any prior edge — the stability
rule only protects a hand-authored edge from a BUILD-TIME seed, not from a
later explicit connect.
Returns {connected, added, moved_from, child_graph, child_apex, parent_graph,
parent_apex}.
Source code in zettelkasten/organizations.py
3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 | |
disconnect_sub_spine ¶
disconnect_sub_spine(child_owner_type: str, child_owner_name: str, child_org_id: str, *, parent_owner_type: str = '', parent_owner_name: str = '', parent_org_id: str = '', get_graph: Any, graphs_dir: 'Path | None' = None) -> dict[str, Any]
Detach the CHILD spine from its parent (remove the primary sub-spine edge).
With no parent org given, removes whatever primary parent edge the child apex
currently carries. When a parent org IS named, the edge is removed only if it
is the child's current primary parent (so a stale request never scrubs the
wrong edge). Idempotent: detaching an unnested child reports removed=False.
Returns {removed, parent, child_graph, child_apex}.
Source code in zettelkasten/organizations.py
3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 | |
verify_organization ¶
verify_organization(owner_type: str, owner_name: str, org_id: str, *, graphs_dir: 'Path | None' = None, verify_fn: 'Callable[[str, list[dict[str, Any]]], dict[str, Any]] | None' = None) -> dict[str, Any]
Read-only synthesis audit of an organization's grid (the synth-auditor).
Verifies that every SYNTHESIZED cell summary stays within the notes routed
into that cell — the synthesis-layer mirror of source grounding's verify.
Reads the persisted reconciled grid (the membership + summaries the live build
last wrote) with the corrections overlay applied, so author locked /
human cells are treated as authoritative and exempt. It NEVER builds or
writes; if no grid has been generated yet there is nothing to audit.
Deterministic checks (no agent required):
ungrounded_summary(error) — a member-bearing column cell carries a genuine synthesized summary but routed ZERO members: synthesis resting on nothing.empty_with_members(warn) — a cell has members but its summary was cleared: unfinished synthesis (the grid analogue of ascaffoldnode).synthesis_over_ungrounded_members(warn) — a 2+-member synthesized cell whose members are ALL ungrounded (no quote / unverified): the synthesis may be sound but its footing is not grounded.unbacked_value(info) — aprompt(agent free-text) cell carries a value but cites no member; can't be grounded-checked deterministically.
Semantic check (only when verify_fn is supplied):
overreach(error) — for a 2+-member genuinely-synthesized cell,verify_fn(summary, members)returns{"supported": bool, "issues": [str, ...]}; an unsupported verdict flags assertions the members don't back. Theverify_fnis the agent seam, injected exactly likebuild_matrix'ssummarize_fn— whenNonethe pass is deterministic-only.
Returns a structured report: verified (no error issues), per-cell
issues, counts, and per-column coverage. built: False signals no
grid exists yet.
Source code in zettelkasten/organizations.py
3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 | |