Skip to content

zettelkasten.dashboard.backend.routes.tables

zettelkasten.dashboard.backend.routes.tables

Matrix/table, schema, re-mine, and review-table routes.

Split out of the former flat routes.py; behaviour is unchanged.

BuildTableRequest

Bases: BaseModel

Body for the matrix build endpoints (mirrors tables.build_matrix).

Source code in zettelkasten/dashboard/backend/routes/tables.py
class BuildTableRequest(BaseModel):
    """Body for the matrix build endpoints (mirrors ``tables.build_matrix``)."""

    name: str = ""  # review name (artifact key)
    table_id: str = "matrix"
    title: str = ""
    columns: list[dict] | None = None
    # How rows are partitioned: {"kind": "source"|"note"|"tag"|"note_type", "ref", "include"}.
    # "note" = one row per note/entity (concept comparison). Omitted / null →
    # the historical one-row-per-source axis (backward compatible).
    row_axis: dict | None = None
    force: bool = False
    peek: bool = False
    # Live builder probe: gather + deterministic-fill the current config without
    # calling the agent or persisting. ``prompt`` columns stay [GAP].
    preview: bool = False

RecordAttributionRequest

Bases: BaseModel

Body for the blank-spine collision attribution endpoints.

Resolves ONE unattributable member surfaced in a grid's needs_attribution bucket: decision='own' claims it as this spine's (it rejoins on rebuild), decision='foreign' keeps it excluded. Keyed by (member_uid, dim_id).

Source code in zettelkasten/dashboard/backend/routes/tables.py
class RecordAttributionRequest(BaseModel):
    """Body for the blank-spine collision attribution endpoints.

    Resolves ONE unattributable member surfaced in a grid's ``needs_attribution``
    bucket: ``decision='own'`` claims it as this spine's (it rejoins on rebuild),
    ``decision='foreign'`` keeps it excluded. Keyed by ``(member_uid, dim_id)``.
    """

    name: str = ""  # review name (artifact key)
    table_id: str = "matrix"
    member_uid: str = ""
    dim_id: str = ""
    decision: str = ""  # "own" | "foreign"
    note_id: str = ""
    source_graph: str = ""
    title: str = ""

GenerateMatrixRequest

Bases: BaseModel

Body for the Generate (materialize) endpoints (mirrors tables.generate_matrix).

Generate is the WRITE path: it reifies the builder's column config into a spine and renders the matrix over it (design 260701_matrix-as-spine.md). Preview (/table with preview=true) stays the cheap, read-only lens projection.

org_id binds the materialize identity to the OPEN matrix's org (§8.3): pass it to resync a specific spine, or omit it to default to organizations.org_id_for(name, table_id) — the open matrix ↔ one org binding, never a hash of the columns. A brand-new/unbound matrix mints a new spine; a matrix already bound to a materialized spine resyncs it in place.

Source code in zettelkasten/dashboard/backend/routes/tables.py
class GenerateMatrixRequest(BaseModel):
    """Body for the Generate (materialize) endpoints (mirrors ``tables.generate_matrix``).

    Generate is the WRITE path: it reifies the builder's column config into a spine
    and renders the matrix over it (design 260701_matrix-as-spine.md). Preview
    (``/table`` with ``preview=true``) stays the cheap, read-only lens projection.

    ``org_id`` binds the materialize identity to the OPEN matrix's org (§8.3): pass
    it to resync a specific spine, or omit it to default to
    ``organizations.org_id_for(name, table_id)`` — the open matrix ↔ one org
    binding, never a hash of the columns. A brand-new/unbound matrix mints a new
    spine; a matrix already bound to a materialized spine resyncs it in place.
    """

    name: str = ""  # review name (artifact key)
    table_id: str = "matrix"
    title: str = ""
    columns: list[dict] | None = None
    row_axis: dict | None = None
    # Materialize identity (the open matrix's org id); defaults to org_id_for(name, table_id).
    org_id: str = ""
    # Approved THEMES (design §8.1/§8.2): recurring source groupings that
    # materialize as ROW-HUBS above the default source rows under the single spine.
    # Each is ``{"label": str, "note_ids": ["<graph>::<id>", …]}`` (the shape
    # ``suggest_from_description`` proposes). Omitted / empty = flat source rows
    # (today's behavior). A non-empty list is authoritative: a regenerate re-titles
    # existing theme hubs (identity = row id) and prunes those no longer present.
    themes: list[dict] | None = None
    force: bool = False

RowValuesRequest

Bases: BaseModel

Body for the row-axis enumeration endpoints (feeds the builder wizard).

Source code in zettelkasten/dashboard/backend/routes/tables.py
class RowValuesRequest(BaseModel):
    """Body for the row-axis enumeration endpoints (feeds the builder wizard)."""

    kind: str = "source"  # "source" | "note" | "tag" | "note_type" | "group"
    ref: str = ""
    # ``group`` axis controls (ignored for the simple axes).
    strategy: str = ""  # "field" | "link" | "semantic"
    relation: str = ""
    direction: str = "outgoing"
    apex: str = ""
    instruction: str = ""

DescribeMatrixRequest

Bases: BaseModel

Body for the natural-language matrix-suggestion endpoint.

project / graph are OPTIONAL. When one is supplied the suggestion runs the corpus-analysis pass (design §8.1): it reads a bounded sample of that scope's in-scope notes and proposes BOTH dimensions (columns) and recurring themes (row-hubs). Omitted, it degrades to the catalog-only mapping (themes is then []), so an unscoped call still returns a valid config.

Source code in zettelkasten/dashboard/backend/routes/tables.py
class DescribeMatrixRequest(BaseModel):
    """Body for the natural-language matrix-suggestion endpoint.

    ``project`` / ``graph`` are OPTIONAL. When one is supplied the suggestion runs
    the corpus-analysis pass (design §8.1): it reads a bounded sample of that
    scope's in-scope notes and proposes BOTH dimensions (columns) and recurring
    themes (row-hubs). Omitted, it degrades to the catalog-only mapping (``themes``
    is then ``[]``), so an unscoped call still returns a valid config.
    """

    description: str = ""
    doc_type: str = ""
    project: str = ""
    graph: str = ""

RemineProposeRequest

Bases: BaseModel

Body for the PROPOSE route.

intent is OPTIONAL. Empty (the default) keeps the historical schema-driven path: classify each member spine's corpus into the shared schema's fixed dimensions. A non-empty intent ("make a matrix for X") switches to facet INDUCTION — the columns are induced from the owner's corpus via :func:remine.induce_dimensions and ride back in the grid flagged induced=True — with NO registry schema required.

depth (default 0 = OFF) opts into Stage 4 emergent DEPTH: with depth >= 1 AND an intent (the emergent path), each induced column may sub-cluster its classified cell notes into children. It is forwarded only on the intent path; the schema-driven path ignores it so registry behavior is unchanged.

Source code in zettelkasten/dashboard/backend/routes/tables.py
class RemineProposeRequest(BaseModel):
    """Body for the PROPOSE route.

    ``intent`` is OPTIONAL. Empty (the default) keeps the historical
    schema-driven path: classify each member spine's corpus into the shared
    schema's fixed dimensions. A non-empty ``intent`` ("make a matrix for X")
    switches to facet INDUCTION — the columns are induced from the owner's corpus
    via :func:`remine.induce_dimensions` and ride back in the grid flagged
    ``induced=True`` — with NO registry schema required.

    ``depth`` (default ``0`` = OFF) opts into Stage 4 emergent DEPTH: with
    ``depth >= 1`` AND an ``intent`` (the emergent path), each induced column may
    sub-cluster its classified cell notes into ``children``. It is forwarded only
    on the intent path; the schema-driven path ignores it so registry behavior is
    unchanged.
    """

    intent: str = ""
    depth: int = 0

RemineApplyRequest

Bases: BaseModel

Body for the APPLY route — the approved proposal echoed back from PROPOSE.

Source code in zettelkasten/dashboard/backend/routes/tables.py
class RemineApplyRequest(BaseModel):
    """Body for the APPLY route — the approved proposal echoed back from PROPOSE."""

    proposal_id: str = ""
    grid: "dict | None" = None
    # Opt-in: also stamp each classified member's dimension tag onto its base note.
    # Default OFF so APPLY never mutates base notes unless explicitly asked.
    tag_stamp: bool = False

graph_table

graph_table(name: str, req: BuildTableRequest | None = None) -> dict

Build the synthesis matrix for a single source graph.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/graphs/{name}/table")
def graph_table(name: str, req: BuildTableRequest | None = None) -> dict:
    """Build the synthesis matrix for a single source graph."""
    _get_graph_structural(name)  # 404 validation guard; no eager embedding build
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    return _build_matrix(graph=local, req=req or BuildTableRequest())

project_table

project_table(name: str, req: BuildTableRequest | None = None) -> dict

Build the synthesis matrix across a project's sources.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/projects/{name}/table")
def project_table(name: str, req: BuildTableRequest | None = None) -> dict:
    """Build the synthesis matrix across a project's sources."""
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    _load_project_or_404(name)
    return _build_matrix(project=local, req=req or BuildTableRequest())

graph_table_attribution

graph_table_attribution(name: str, req: RecordAttributionRequest) -> dict

Record a blank-spine collision attribution for a graph-scoped matrix.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/graphs/{name}/table/attribution")
def graph_table_attribution(name: str, req: RecordAttributionRequest) -> dict:
    """Record a blank-spine collision attribution for a graph-scoped matrix."""
    _get_graph_structural(name)  # 404 validation guard; no eager embedding build
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    return _record_attribution(graph=local, req=req)

project_table_attribution

project_table_attribution(name: str, req: RecordAttributionRequest) -> dict

Record a blank-spine collision attribution for a project-scoped matrix.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/projects/{name}/table/attribution")
def project_table_attribution(name: str, req: RecordAttributionRequest) -> dict:
    """Record a blank-spine collision attribution for a project-scoped matrix."""
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    _load_project_or_404(name)
    return _record_attribution(project=local, req=req)

graph_table_stream

graph_table_stream(name: str, req: BuildTableRequest | None = None)

Streaming twin of graph_table (SSE: stage/grid/cells/result frames).

The Generate / Regenerate path uses this so the grid renders live. Guards match the JSON route: 404 unknown source, 403 federated (read-only).

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/graphs/{name}/table/stream")
def graph_table_stream(name: str, req: BuildTableRequest | None = None):
    """Streaming twin of ``graph_table`` (SSE: stage/grid/cells/result frames).

    The Generate / Regenerate path uses this so the grid renders live. Guards
    match the JSON route: 404 unknown source, 403 federated (read-only).
    """
    _get_graph_structural(name)  # 404s on an unknown/unsafe name (no eager embedding build)
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    return _table_stream_response(graph=local, req=req or BuildTableRequest())

project_table_stream

project_table_stream(name: str, req: BuildTableRequest | None = None)

Streaming twin of project_table (SSE: stage/grid/cells/result frames).

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/projects/{name}/table/stream")
def project_table_stream(name: str, req: BuildTableRequest | None = None):
    """Streaming twin of ``project_table`` (SSE: stage/grid/cells/result frames)."""
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    _load_project_or_404(name)
    return _table_stream_response(project=local, req=req or BuildTableRequest())

graph_table_build async

graph_table_build(name: str, req: BuildTableRequest | None = None) -> dict

Schedule a durable background matrix build for a source graph (fire-and-poll).

Declared async so scheduling never takes the single web-threadpool token — the multi-second build runs off it in build_jobs. Guards match the stream route: 404 unknown source, 403 federated (read-only).

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/graphs/{name}/table/build")
async def graph_table_build(name: str, req: BuildTableRequest | None = None) -> dict:
    """Schedule a durable background matrix build for a source graph (fire-and-poll).

    Declared ``async`` so scheduling never takes the single web-threadpool token —
    the multi-second build runs off it in ``build_jobs``. Guards match the stream
    route: 404 unknown source, 403 federated (read-only).
    """
    _get_graph_structural(name)  # 404s on an unknown/unsafe name (no eager embedding build)
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    return _schedule_table_build(graph=local, req=req or BuildTableRequest())

project_table_build async

project_table_build(name: str, req: BuildTableRequest | None = None) -> dict

Schedule a durable background matrix build across a project's sources.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/projects/{name}/table/build")
async def project_table_build(name: str, req: BuildTableRequest | None = None) -> dict:
    """Schedule a durable background matrix build across a project's sources."""
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    _load_project_or_404(name)
    return _schedule_table_build(project=local, req=req or BuildTableRequest())

build_job_active async

build_job_active(kind: str = Query(...), review: str = Query(...), id: str = Query(default='')) -> dict

The in-flight job_id for (kind, review, id) (reconnect), else null.

kind is matrix or outline; id is the table_id (matrix) or outline_id (outline) — the second half of the job's dedup key.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.get("/build-jobs/active")
async def build_job_active(
    kind: str = Query(...), review: str = Query(...), id: str = Query(default="")
) -> dict:
    """The in-flight ``job_id`` for ``(kind, review, id)`` (reconnect), else ``null``.

    ``kind`` is ``matrix`` or ``outline``; ``id`` is the ``table_id`` (matrix) or
    ``outline_id`` (outline) — the second half of the job's dedup key.
    """
    if kind not in ("matrix", "outline"):
        raise HTTPException(status_code=400, detail="kind must be 'matrix' or 'outline'")
    # Fall back to the SAME per-kind default the schedule side normalizes to when
    # the client omits ``id``: matrix → ``"matrix"`` (BuildTableRequest.table_id),
    # outline → ``outlines.DEFAULT_OUTLINE_ID`` (== "outline"), matching
    # ``_schedule_outline_build``'s ``req.outline_id or DEFAULT_OUTLINE_ID`` key.
    # The frontend now always passes the real ``id``; this default only covers the
    # legacy id-less probe, and the two sides MUST agree exactly or reconnect dies.
    default_id = "matrix" if kind == "matrix" else outlines.DEFAULT_OUTLINE_ID
    # Normalize the matrix review with the SAME canonical helper the schedule side
    # uses (:func:`_gate_review`), so reconnect keys on the identical (stripped)
    # ``review`` that ``_schedule_table_build`` / ``_schedule_generate_matrix`` build
    # their dedup+gate key from — a whitespace-bearing name can no longer miss its
    # own in-flight job. Outline keeps its own review verbatim (its schedule lives
    # in the outlines route module, which normalizes independently).
    review_key = _gate_review(review) if kind == "matrix" else review
    return {"job_id": build_jobs.find_active(kind, (review_key, id or default_id))}

build_job async

build_job(job_id: str) -> dict

The durable job record for job_id (404 if unknown).

Shape: {job_id, kind, key, status, stage, progress?, partial, result?, error?, created_at, updated_at} where status is pending/running/done/error and partial carries live grid / cells / synthesis (matrix) or markdown (outline).

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.get("/build-jobs/{job_id}")
async def build_job(job_id: str) -> dict:
    """The durable job record for ``job_id`` (404 if unknown).

    Shape: ``{job_id, kind, key, status, stage, progress?, partial, result?,
    error?, created_at, updated_at}`` where ``status`` is
    ``pending``/``running``/``done``/``error`` and ``partial`` carries live
    ``grid`` / ``cells`` / ``synthesis`` (matrix) or ``markdown`` (outline).
    """
    rec = build_jobs.get_job(job_id)
    if rec is None:
        raise HTTPException(status_code=404, detail=f"Build job '{job_id}' not found.")
    return rec

graph_table_generate async

graph_table_generate(name: str, req: GenerateMatrixRequest | None = None) -> dict

Generate (materialize) a single source graph's matrix into a spine.

The write twin of graph_table: reifies the builder's columns into a spine and renders over it. Routed through the background build worker (returns a job_id to poll) so the multi-second materialize never occupies the single web token and serializes against every other spine writer for the review. Declared async so scheduling never takes that token. Guards match the spine lifecycle routes — 404 unknown source, 403 federated / writes disabled.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/graphs/{name}/table/generate")
async def graph_table_generate(name: str, req: GenerateMatrixRequest | None = None) -> dict:
    """Generate (materialize) a single source graph's matrix into a spine.

    The write twin of ``graph_table``: reifies the builder's columns into a spine
    and renders over it. Routed through the background build worker (returns a
    ``job_id`` to poll) so the multi-second materialize never occupies the single
    web token and serializes against every other spine writer for the review.
    Declared ``async`` so scheduling never takes that token. Guards match the
    spine lifecycle routes — 404 unknown source, 403 federated / writes disabled.
    """
    _get_graph_structural(name)  # 404/federation validation guard; no eager embedding build
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    _spine_write_guard()
    return await _schedule_generate_matrix(graph=local, req=req or GenerateMatrixRequest())

project_table_generate async

project_table_generate(name: str, req: GenerateMatrixRequest | None = None) -> dict

Generate (materialize) a project's matrix into a spine and render over it.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/projects/{name}/table/generate")
async def project_table_generate(name: str, req: GenerateMatrixRequest | None = None) -> dict:
    """Generate (materialize) a project's matrix into a spine and render over it."""
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    _load_project_or_404(name)
    _spine_write_guard()
    return await _schedule_generate_matrix(project=local, req=req or GenerateMatrixRequest())

graph_table_row_values

graph_table_row_values(name: str, req: RowValuesRequest | None = None) -> dict

Auto-list the candidate row values for an axis over a single source graph.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/graphs/{name}/table/row-values")
def graph_table_row_values(name: str, req: RowValuesRequest | None = None) -> dict:
    """Auto-list the candidate row values for an axis over a single source graph."""
    if _federated_repo(name) is not None:
        raise HTTPException(status_code=404, detail="Row values are not available for federated repos.")
    _get_graph_structural(name)
    local, _repo = _resolve_ref(name)
    return _row_values(graph=local, req=req or RowValuesRequest())

project_table_row_values

project_table_row_values(name: str, req: RowValuesRequest | None = None) -> dict

Auto-list the candidate row values for an axis over a project's corpus.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/projects/{name}/table/row-values")
def project_table_row_values(name: str, req: RowValuesRequest | None = None) -> dict:
    """Auto-list the candidate row values for an axis over a project's corpus."""
    if _federated_repo(name) is not None:
        raise HTTPException(status_code=404, detail="Row values are not available for federated repos.")
    local, _repo = _resolve_ref(name)
    _load_project_or_404(name)
    return _row_values(project=local, req=req or RowValuesRequest())

table_suggest_columns

table_suggest_columns(doc_type: str = Query(default='')) -> dict

Propose typed columns from extraction schemas, note types, and CCC slots.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.get("/table/suggest-columns")
def table_suggest_columns(doc_type: str = Query(default="")) -> dict:
    """Propose typed columns from extraction schemas, note types, and CCC slots."""
    return tables.suggest_columns(doc_type=doc_type)

table_suggest_from_description

table_suggest_from_description(req: DescribeMatrixRequest) -> dict

Agent-map a plain-English comparison request to a {row_axis, columns, themes} config.

Deterministic-first: the agent prefers grounded catalog columns (schema dimensions / note types / CCC slots / note fields) and only falls back to free-text prompt columns when nothing fits. When a project / graph scope is supplied it additionally reads the corpus and proposes themes (recurring source groupings → row-hubs). Returns a config the builder can load directly; an empty / unparseable request degrades to the source axis.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/table/suggest-from-description")
def table_suggest_from_description(req: DescribeMatrixRequest) -> dict:
    """Agent-map a plain-English comparison request to a {row_axis, columns, themes} config.

    Deterministic-first: the agent prefers grounded catalog columns (schema
    dimensions / note types / CCC slots / note fields) and only falls back to
    free-text ``prompt`` columns when nothing fits. When a ``project`` / ``graph``
    scope is supplied it additionally reads the corpus and proposes ``themes``
    (recurring source groupings → row-hubs). Returns a config the builder can load
    directly; an empty / unparseable request degrades to the source axis.
    """
    project = graph = ""
    get_graph = None
    if req.graph:
        local, repo = _resolve_ref(req.graph)
        if repo is None:
            graph, get_graph = local, _get_graph_structural
    elif req.project:
        local, repo = _resolve_ref(req.project)
        if repo is None:
            project, get_graph = local, _get_graph_structural
    return tables.suggest_from_description(
        req.description,
        doc_type=req.doc_type,
        get_graph=get_graph,
        project=project,
        graph=graph,
        graphs_dir=GRAPHS_DIR,
        localize=(lambda d: _resolve_ref(d)[0]),
    )

review_tables

review_tables(name: str, meta: bool = False, drift: bool = False, table_id: str = Query(default='')) -> dict

List the persisted matrices for a review — or one view's drift badge.

meta=false (default) returns the FULL grids — retained for the bulk Excel export, which walks every view's rows. meta=true returns METADATA ONLY (id, title, columns, row axis, synthesis, lifecycle, counts — no per-cell rows): the always-polled matrices dropdown only needs each view's shape, so it uses this lighter listing and never ships every review's full grids. The active grid is loaded on demand separately (the cost-free peek).

drift=true (with table_id) is the LAZY drift/unrouted badge lens: it returns ONLY the {unrouted, unrouted_ids, attached, stale} cue for that one view (tables.count_unrouted), NOT a tables list. This is what moves the two full-corpus walks the badge needs OFF the grid-load critical path — the grid renders from the peek immediately and the frontend fetches this after it paints. A query flag (rather than a second route) keeps the mounted (method, path) set — and the route-manifest snapshot — unchanged.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.get("/reviews/{name}/tables")
def review_tables(
    name: str, meta: bool = False, drift: bool = False, table_id: str = Query(default="")
) -> dict:
    """List the persisted matrices for a review — or one view's drift badge.

    ``meta=false`` (default) returns the FULL grids — retained for the bulk Excel
    export, which walks every view's rows. ``meta=true`` returns METADATA ONLY
    (id, title, columns, row axis, synthesis, lifecycle, counts — no per-cell
    ``rows``): the always-polled matrices dropdown only needs each view's shape,
    so it uses this lighter listing and never ships every review's full grids.
    The active grid is loaded on demand separately (the cost-free peek).

    ``drift=true`` (with ``table_id``) is the LAZY drift/unrouted badge lens: it
    returns ONLY the ``{unrouted, unrouted_ids, attached, stale}`` cue for that one
    view (``tables.count_unrouted``), NOT a ``tables`` list. This is what moves the
    two full-corpus walks the badge needs OFF the grid-load critical path — the
    grid renders from the peek immediately and the frontend fetches this after it
    paints. A query flag (rather than a second route) keeps the mounted
    (method, path) set — and the route-manifest snapshot — unchanged.
    """
    if _federated_repo(name) is not None:
        return dict(_EMPTY_DRIFT) if drift else {"tables": []}
    local, _repo = _resolve_ref(name)
    if drift:
        # Resolve the view's owner scope from the review the SAME way the org
        # mirror does (existing org → explicit scope → review manifest), so the
        # corpus walk spans the right graph/project without a scope in the path.
        owner = tables._mirror_owner(local, table_id or "matrix", "", "", GRAPHS_DIR)
        project = owner[1] if owner and owner[0] == "project" else ""
        graph = owner[1] if owner and owner[0] == "graph" else ""
        return _table_drift(project=project, graph=graph, name=local, table_id=table_id)
    if meta:
        return {"tables": tables.list_table_meta(local, graphs_dir=GRAPHS_DIR)}
    return {"tables": tables.list_tables(local, graphs_dir=GRAPHS_DIR)}

project_schemas

project_schemas(name: str) -> dict

List a project's schema-grouped matrix entries (shared by >= 2 spines).

Source code in zettelkasten/dashboard/backend/routes/tables.py
def project_schemas(name: str) -> dict:
    """List a project's schema-grouped matrix entries (shared by >= 2 spines)."""
    return _spine_schema_groups("project", name)

graph_schemas

graph_schemas(name: str) -> dict

List a graph's schema-grouped matrix entries (shared by >= 2 spines).

Source code in zettelkasten/dashboard/backend/routes/tables.py
def graph_schemas(name: str) -> dict:
    """List a graph's schema-grouped matrix entries (shared by >= 2 spines)."""
    return _spine_schema_groups("graph", name)

project_spine_group_matrix

project_spine_group_matrix(name: str, fingerprint: str) -> dict

The ephemeral comparison matrix for one of a project's shared schemas.

Source code in zettelkasten/dashboard/backend/routes/tables.py
def project_spine_group_matrix(name: str, fingerprint: str) -> dict:
    """The ephemeral comparison matrix for one of a project's shared schemas."""
    return _spine_group_matrix("project", name, fingerprint)

graph_spine_group_matrix

graph_spine_group_matrix(name: str, fingerprint: str) -> dict

The ephemeral comparison matrix for one of a graph's shared schemas.

Source code in zettelkasten/dashboard/backend/routes/tables.py
def graph_spine_group_matrix(name: str, fingerprint: str) -> dict:
    """The ephemeral comparison matrix for one of a graph's shared schemas."""
    return _spine_group_matrix("graph", name, fingerprint)

project_schema_overlay

project_schema_overlay(name: str, fingerprint: str) -> dict

The ephemeral hub + branch edges for a project's shared schema (GraphView).

Source code in zettelkasten/dashboard/backend/routes/tables.py
def project_schema_overlay(name: str, fingerprint: str) -> dict:
    """The ephemeral hub + branch edges for a project's shared schema (GraphView)."""
    return _schema_overlay("project", name, fingerprint)

graph_schema_overlay

graph_schema_overlay(name: str, fingerprint: str) -> dict

The ephemeral hub + branch edges for a graph's shared schema (GraphView).

Source code in zettelkasten/dashboard/backend/routes/tables.py
def graph_schema_overlay(name: str, fingerprint: str) -> dict:
    """The ephemeral hub + branch edges for a graph's shared schema (GraphView)."""
    return _schema_overlay("graph", name, fingerprint)

project_nested_spines

project_nested_spines(name: str) -> dict

The multi-tier nested-spine composition across a project's spines.

Source code in zettelkasten/dashboard/backend/routes/tables.py
def project_nested_spines(name: str) -> dict:
    """The multi-tier nested-spine composition across a project's spines."""
    return _nested_spines("project", name)

graph_nested_spines

graph_nested_spines(name: str) -> dict

The multi-tier nested-spine composition across a graph's spines.

Source code in zettelkasten/dashboard/backend/routes/tables.py
def graph_nested_spines(name: str) -> dict:
    """The multi-tier nested-spine composition across a graph's spines."""
    return _nested_spines("graph", name)

project_schema_remine

project_schema_remine(name: str, fingerprint: str, req: RemineProposeRequest = RemineProposeRequest()) -> dict

PROPOSE re-mining for a project's shared schema (READ-ONLY classify).

Source code in zettelkasten/dashboard/backend/routes/tables.py
def project_schema_remine(name: str, fingerprint: str, req: RemineProposeRequest = RemineProposeRequest()) -> dict:
    """PROPOSE re-mining for a project's shared schema (READ-ONLY classify)."""
    return _remine_propose("project", name, fingerprint, intent=req.intent, depth=req.depth)

graph_schema_remine

graph_schema_remine(name: str, fingerprint: str, req: RemineProposeRequest = RemineProposeRequest()) -> dict

PROPOSE re-mining for a graph's shared schema (READ-ONLY classify).

Source code in zettelkasten/dashboard/backend/routes/tables.py
def graph_schema_remine(name: str, fingerprint: str, req: RemineProposeRequest = RemineProposeRequest()) -> dict:
    """PROPOSE re-mining for a graph's shared schema (READ-ONLY classify)."""
    return _remine_propose("graph", name, fingerprint, intent=req.intent, depth=req.depth)

project_schema_remine_apply async

project_schema_remine_apply(name: str, fingerprint: str, req: RemineApplyRequest) -> dict

APPLY an approved project re-mine proposal (writes spine-member edges).

Source code in zettelkasten/dashboard/backend/routes/tables.py
async def project_schema_remine_apply(name: str, fingerprint: str, req: RemineApplyRequest) -> dict:
    """APPLY an approved project re-mine proposal (writes spine-member edges)."""
    return await _remine_apply("project", name, fingerprint, req)

graph_schema_remine_apply async

graph_schema_remine_apply(name: str, fingerprint: str, req: RemineApplyRequest) -> dict

APPLY an approved graph re-mine proposal (writes spine-member edges).

Source code in zettelkasten/dashboard/backend/routes/tables.py
async def graph_schema_remine_apply(name: str, fingerprint: str, req: RemineApplyRequest) -> dict:
    """APPLY an approved graph re-mine proposal (writes spine-member edges)."""
    return await _remine_apply("graph", name, fingerprint, req)

review_table_synthesis

review_table_synthesis(name: str, req: ColumnSynthesisRequest) -> dict

On-demand AI one-line synthesis for a single matrix column.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.post("/reviews/{name}/table/synthesis")
def review_table_synthesis(name: str, req: ColumnSynthesisRequest) -> dict:
    """On-demand AI one-line synthesis for a single matrix column."""
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    persisted = {t["table_id"]: t for t in tables.list_tables(local, graphs_dir=GRAPHS_DIR)}
    table = persisted.get(req.table_id)
    if table is None:
        raise HTTPException(status_code=404, detail=f"Table '{req.table_id}' not found.")
    column = next((c for c in table["columns"] if c["key"] == req.column_key), None)
    if column is None:
        raise HTTPException(status_code=404, detail=f"Column '{req.column_key}' not found.")
    try:
        summary = tables.generate_column_synthesis(
            column, table["rows"], summarize_fn=tables._default_summarize_fn
        )
    except (RuntimeError, FileNotFoundError, OSError) as e:
        raise HTTPException(status_code=502, detail=f"Synthesis failed: {e}")
    # Persist the narrative onto the stored grid (``synthesis[key]["ai"]``) so it
    # survives a reload and a cross-session peek, exactly like the streaming build's
    # auto-synthesis — otherwise it lived only in transient client state.
    if summary:
        tables.set_column_synthesis(
            local, req.table_id, req.column_key, summary, graphs_dir=GRAPHS_DIR
        )
    return {"table_id": req.table_id, "column_key": req.column_key, "summary": summary}

rename_review_table

rename_review_table(name: str, table_id: str, req: RenameTableRequest) -> dict

Rename a persisted matrix view (display title only; table_id stays stable).

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.patch("/reviews/{name}/table/{table_id}")
def rename_review_table(name: str, table_id: str, req: RenameTableRequest) -> dict:
    """Rename a persisted matrix view (display title only; table_id stays stable)."""
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    renamed = tables.rename_table(local, table_id, req.title, graphs_dir=GRAPHS_DIR)
    if not renamed:
        raise HTTPException(status_code=404, detail=f"Table '{table_id}' not found.")
    # Propagate the new display title to the backing organization so the spine
    # apex/hub label tracks the matrix name (best-effort: the durable grid is
    # already renamed; a missing/absent org mirror just means there is no spine
    # label to sync). Resolve the org's owner the SAME way the persist mirror does.
    owner = tables._mirror_owner(local, table_id, "", "", GRAPHS_DIR)
    if owner is not None:
        try:
            organizations.rename_definition(
                owner[0], owner[1], review=local, table_id=table_id,
                title=req.title, graphs_dir=GRAPHS_DIR,
            )
        except Exception:  # noqa: BLE001 — the org title sync is advisory, never fatal
            pass
    return {"table_id": table_id, "title": (req.title or "").strip() or table_id}

delete_review_table

delete_review_table(name: str, table_id: str) -> dict

Delete a persisted matrix from a review.

Source code in zettelkasten/dashboard/backend/routes/tables.py
@router.delete("/reviews/{name}/table/{table_id}")
def delete_review_table(name: str, table_id: str) -> dict:
    """Delete a persisted matrix from a review."""
    local, repo = _resolve_ref(name)
    if repo is not None:
        raise HTTPException(status_code=403, detail="Federated repos are read-only.")
    removed = tables.delete_table(local, table_id, graphs_dir=GRAPHS_DIR)
    if not removed:
        raise HTTPException(status_code=404, detail=f"Table '{table_id}' not found.")
    return {"deleted": table_id}