Files
projax/docs/plans/mbrian-backend-migration.md
mAi a5b0971b9d docs: Phase 6 plan re-baseline against live mBrian schema + m's answers
m answered all 11 §10 questions; every inventor pick confirmed.
m's overriding directive: "keep the database simple so it remains
easily modifiable."

Head verified the live mBrian schema after m's answers — original §3
was built off stale db/001_initial_schema.sql. Three of the six asks
turned out already-satisfied:

- MB-A (edges.metadata jsonb) — already added in db/010, GIN-indexed,
  used by migs 039/040. Drop the ask.
- MB-C (project type) — already in live schema, mig 033 confirms.
  Drop the ask.
- MB-D (per-user slug uniqueness) — already enforced by idx_nodes_slug
  in db/001. Drop the ask.

Plus 'area' as a separate mBrian type is killed per m's "keep it
simple": areas reuse type=['project'] with metadata.projax.kind='area'.
Zero DDL.

Remaining mBrian-side artifact compresses to ONE [schema] convention
node under a new [topic] projax-integration hub, plus mBrian-side
ownership of the one-shot data-migration script (per m's "mbrian must
own the migration").

Re-sequenced §8: six slices.
  0 (projax snapshot helper) → A (mBrian [schema] node + script run)
  → B (projax read-path adapter) → C (projax write-path)
  → D (mai bridge worker) → E (drop projax tables).

CalDAV/Gitea integrations stay where they are (m's Q3=(a)). No slice
F needed in the original sense.

§2 + §2.1 + §7 + §9 + §10 + §14 updated. §3 fully rewritten.

No code changes; this branch ships docs only. Slice 0 is the smallest
first projax-side step but waits for head's greenlight after the
m/mBrian issue is filed.
2026-05-29 13:56:50 +02:00

31 KiB

mBrian-as-backend migration — Phase 6 design

Status: Phase A design — re-baselined against live mBrian schema (2026-05-29). Branch: mai/kahn/phase-6a-mbrian-design. Author: kahn (inventor), 2026-05-29. Source decision (m, issue m/projax#5, 12:43 2026-05-29): Option A — full backend migration. "I think we need the project-management element inside of mBrian for it to be the complete 2nd Brain experience. The data itself is not too important yet."

m's overriding directive (2026-05-29 via head): "keep the database simple so it remains easily modifiable."

Constraint: data-loss tolerant on the 47 current projax.items.

m's answers on §10 (2026-05-29): every inventor pick confirmed.

Q1=reuse 'project' / Q2=(b) handler bridge / Q3=(a) clients projax-side / Q4=(a) file Gitea on m/mBrian via otto/head — m: "mbrian must own the migration" / Q5=(a) views stay projax-resident / Q6=(a) per-user slug / Q7=(a) hard-cut / Q8=(a) tags in metadata / Q9=(a) projax-side cycle detection / Q10=(a) keep projax MCP via adapter / Q11=keep projax_origin audit metadata.

Re-baseline note: §3's original ask was built off a stale db/001_initial_schema.sql read. Head verified the live mBrian schema after m's answers. Three of the six asks (MB-A, MB-C, MB-D) turned out already-satisfied — edges.metadata exists since db/010_flexsiebels_compat.sql, 'project' type exists since db/033, the per-user slug unique index ships in db/001. The remaining mBrian-side artifact is small. §3 + §8 now reflect that. The big shift: mBrian owns the one-shot data-migration script — that's what "mbrian must own the migration" means — while projax owns the read+write rewiring on its own side afterward.


§1 — Diagnosis

projax today stores its own structured data in projax.items + projax.item_links (msupabase, schema projax). It's a parallel knowledge surface to mBrian's main graph — both store nodes-with-content-and-edges, both speak SQL+jsonb, both ship MCP. The duplication has cost: project context (held by projax) is invisible to mBrian's reasoning paths; mBrian's relationship graph (held by mbrian) is invisible to projax's tile / timeline aggregations.

m's call closes the gap by making mBrian canonical. Projax keeps its UI — the /views routes, the Tiles dashboard, the calendar grid, the timeline spine, the /tree forest, the just-shipped /views/{slug} family, and the system-view chrome — but every read and write goes through mBrian instead of projax.items. Same surface, single source.

End-state contract:

  • One node graph. Every project, task-context, area, link bundle lives in mbrian.nodes + mbrian.edges.
  • projax's UI is a structured editor + aggregation surface over that graph (think paliad-shape views, mBrian-shape data).
  • mBrian's existing surfaces (the web editor, the trackers, the synthesis filings) keep working unchanged — projax data appears alongside everything else.
  • CalDAV / Gitea / mai.projects integrations stay projax-handled at the consumption layer; the items they hang off of live in mBrian.
  • The 47-item migration is one-shot. Anything lossy gets logged + flagged for manual repair; we don't preserve at all costs.

§2 — Schema mapping (the load-bearing section)

Per-column map: projax.items → mBrian shape

projax column mBrian destination notes
id (uuid) nodes.id new uuids on migration; legacy ids never round-trip
kind (text[]) nodes.type direct shape match; projax 'project' becomes mBrian 'project' (already in live schema, mig 033). Areas keep type=['project'] + metadata.projax.kind='area' — per m's "keep the database simple" directive, no new mBrian type. Zero DDL.
title nodes.title 1:1
slug nodes.slug mBrian = unique per user; projax = unique per parent — see §2.1
paths (text[]) derived from child_of edges + nodes.path cache DAG resolution via edge walk; see §2.2
parent_ids (uuid[]) edges (source=this, rel='child_of', target=parent) one edge per parent; preserves multi-parent
content_md nodes.content_md 1:1
aliases (text[]) nodes.aliases 1:1
metadata (jsonb) nodes.metadata merge; projax metadata keeps its existing shape under a projax sub-key to avoid colliding with mBrian's metadata schema
status (text) nodes.metadata.projax.status active/done/archived; mBrian's archived bool covers part of it but loses the active/done split
pinned (bool) nodes.pinned 1:1
archived (bool) nodes.archived 1:1; status='archived' implies this too
start_time, end_time (timestamptz) nodes.metadata.projax.start_time / end_time mBrian has no first-class start/end
tags (text[]) nodes.metadata.projax.tags mBrian convention puts tags as separate [tag] nodes joined via tagged edges; we keep tags in metadata for the migration window then optionally re-shape — see Q8
management (text[]) nodes.metadata.projax.management mai/self/external/unmanaged — projax-specific concept; stays in metadata
public, public_description, public_live_url, public_source_url, public_screenshots nodes.metadata.projax.public.{...} mBrian's visibility is a different model (personal/public/...); we keep projax's bundle in metadata so the flexsiebels portfolio renderer keeps working
timeline_exclude (text[]) nodes.metadata.projax.timeline_exclude projax-only concept
created_at nodes.created_at 1:1
updated_at nodes.updated_at trigger-maintained on both sides
deleted_at nodes.deleted_at 1:1

§2.1 — Slug uniqueness (settled)

projax today enforces slug uniqueness per parent. mBrian's live schema has CREATE UNIQUE INDEX idx_nodes_slug ON mbrian.nodes (user_id, slug) — uniqueness per user. Per m's Q6=(a), projax adopts mBrian's model: one paliad node total, connected to both dev and work via two child_of edges. The DAG-as-multiple-paths view is a render-time concept; the storage is one node.

projax handlers' itemwrite validator (Phase 5c) loses its per-parent slug rule, gains a per-user check (against the projax-managed subset of nodes). This is stricter — m can't have two different "paliad" projects under different roots. Settled per m's answer.

Pre-migration dedup: the 47-item migration script (which lives mBrian-side, see §3+§7) scans for slug collisions across the projax dataset and folds collisions into one node with multiple child_of edges. Skip-with-log on anything weirder.

§2.2 — paths array vs single path

projax's paths text[] is computed from parent_ids (one path per ancestor lineage). mBrian's path text is a single denormalized cache; the canonical structure is child_of edges.

For projax UI to keep showing multi-paths ("Also at: work.paliad"), the store-adapter layer (§4) re-derives paths[] from the edge graph on each fetch. Cheap at m's scale (≤200 nodes); cache lightly if profiling bites.

Each item_links row becomes a mBrian edge with a typed rel. The ref_id semantics differ:

projax ref_type mBrian shape notes
caldav-list edge rel='projax-caldav-list', metadata.url=... external URL — no target node exists; edge carries the URL in note or metadata
gitea-repo edge rel='projax-gitea-repo', metadata={owner, repo} same shape
gitea-issue edge rel='projax-gitea-issue', metadata={owner, repo, number} same
mai-project edge rel='projax-mai-project', metadata={mai_project_id} bridge for the Phase 1.5 bidirectional sync
mbrian-node edge (source=this, rel='related_to', target=<mbrian uuid>) already mBrian — this becomes a regular node-to-node edge
url edge rel='projax-url', metadata={url} unstructured link
document, note edge rel='projax-doc', metadata={...} PER day-granular dated artifacts

mBrian edges support note text plus an auto bool flag. Both used by projax: auto=false for human-added links, note carries human annotation. The structured payload (URL, repo info, etc.) lands in a metadata jsonb that we add via a new edges.metadata column — see §3.

Open question on edge payloads

mBrian's edges table today has no metadata jsonb column — only rel, note, sort_order, node_id, auto. For projax's typed external-ref payloads (caldav URLs, gitea repo names), we need either:

  • (a) Add metadata jsonb to mbrian.edges (mBrian-side schema work, see §3 Q-A).
  • (b) Use the node_id "complex edge" feature: the edge points at a third node that holds the metadata. Heavier per-link cost; one node per external ref.
  • (c) Stash structured payload inside note text as JSON. Hacky; loses index-ability.

Inventor pick: (a) — adds one nullable column to edges, indexes optionally, keeps the simple shape and matches projax's existing item_links model.


§3 — mBrian-side requirements (re-baselined against live schema)

Head verified the live mBrian schema after m's answers. Three of the original six asks turned out already-satisfied. What's actually needed reduces to one [schema] convention node + ownership of the one-shot data-migration script. Per m's Q4=(a), this lands as a Gitea issue on m/mBrian with the "blocks projax phase 6" tag; head files it.

Already satisfied (no DDL needed)

original ask live-schema status
MB-A — edges.metadata jsonb column Already exists — added in db/010_flexsiebels_compat.sql: ALTER TABLE mbrian.edges ADD COLUMN IF NOT EXISTS metadata jsonb NOT NULL DEFAULT '{}' plus GIN idx_edges_metadata. Already used by mig 039/040. projax link payloads land here directly.
MB-C — 'project' type registration Already exists — confirmed in db/033 + inbox tests. m's Q1=(a) reuses it.
MB-C — 'area' type registration NOT needed — per m's "keep the database simple," areas reuse type=['project'] with metadata.projax.kind='area'. Zero DDL.
MB-D — per-user slug uniqueness Already enforcedCREATE UNIQUE INDEX idx_nodes_slug ON mbrian.nodes (user_id, slug) in db/001. Handles the bulk migration as-is, modulo the pre-write dedup pass in the script (§7).
MB-E — read MCP coverage Confirmed by head — type-array filter, edge query by rel + source/target, FTS search all present in mBrian's MCP today. Optional bulk "node + outbound edges" endpoint may improve adapter perf, but v1 ships without it.
MB-F — write MCP coverage Confirmed by head — create_node, update_node, soft-delete, create_edge, delete_edge all present.

Remaining mBrian-side artifact

MB-B — projax-integration [schema] convention node. One new mBrian node, no DDL. Lives under a new [topic] hub projax-integration. Documents:

  1. The projax edge relations: child_of (already in use everywhere), projax-caldav-list, projax-gitea-repo, projax-gitea-issue, projax-mai-project, projax-url, projax-doc. Each entry: rel name + the metadata jsonb shape (e.g. projax-caldav-list carries {url: text}).
  2. The projax type usage: 'project' for both projects and areas; metadata.projax.kind distinguishes (area vs default project). 'mai-managed' as a co-type marker for nodes mirroring mai.projects rows.
  3. The projax metadata shape: metadata.projax.{status, tags, management, public, timeline_exclude, start_time, end_time, kind} — the subset of projax columns that don't have a first-class mBrian counterpart.
  4. A pointer to projax_origin audit metadata (set per migrated node, per m's Q11=keep).

mBrian-side coder writes this node by creating it via mBrian's editor or MCP. No migration file needed.

mBrian owns the data-migration script

Per m's directive "mbrian must own the migration," the one-shot script that creates the 47 nodes + their edges lives in m/mBrian (likely scripts/migrate-from-projax.ts or similar — mBrian's stack picks). projax-side provides:

  • A frozen snapshot of projax.items + projax.item_links rows (CSV or JSON dump produced by a projax-side helper).
  • The mapping rules from §2 + §2.2 in a form mBrian-side can implement against (this plan doc is the canonical source).
  • A spot-check checklist (5 representative items) for post-migration validation.

The script's blast radius lives on mBrian's side; projax-side blocks on its successful run before slice C kicks off.

Cross-repo coordination shape

One Gitea issue on m/mBrian (filed by head), tagged "blocks projax phase 6". The issue body covers MB-B + script ownership + the snapshot-handoff protocol. Body draft delivered to head with this re-baseline (see Phase A workflow §14).


§4 — projax-side read-path replacement

The store package becomes a thin adapter over mBrian. Consumers stay shape-stable: *store.Item still exposes Kind / Title / Slug / Paths / ParentIDs / ContentMD / Aliases / Metadata / Status / Pinned / Archived / Tags / Management / Public* / TimelineExclude / etc. Internally those come from mBrian nodes + metadata + edge-walks.

projax call site new implementation
store.Store.ListAll(ctx) mBrian: SELECT FROM mbrian.nodes WHERE 'projax' = ANY(metadata.projax_origin) ... ORDER BY title (or via MCP list_nodes). Returns []*Item adapted from each node.
store.Store.GetByPath(ctx, path) resolve path → leaf node by walking child_of edges from the path's root segment; cache hits during render
store.Store.GetByID(ctx, id) direct mBrian fetch
store.Store.LinksByRefType(ctx, t) edge query rel='projax-<t>' over all projax-managed nodes
store.Store.AllTags(ctx) aggregate over metadata.projax.tags arrays across projax nodes
store.Store.MaiOrphans(ctx) mBrian: find projax-managed nodes with no child_of edge + metadata.projax.management contains 'mai'
store.Store.DatedLinks(ctx, id) edge query rel IN ('projax-doc', 'projax-url') for the node, filtered to those with metadata.event_date set

The aggregator (internal/aggregate/) doesn't see mBrian — it gets []*store.Item from the adapter. CalDAV + Gitea external fetches stay where they are.

Views (Phase 5j projax.views table) decision point — see Q5.

Adapter layer surface

package store

type Store struct {
    mb *mbrian.Client  // MCP-style client or direct SQL
}

func (s *Store) ListAll(ctx context.Context) ([]*Item, error) { ... }
// every existing method keeps its signature; bodies rewrite to mBrian calls

The Item struct stays unchanged. Tests against the adapter assert "given this mBrian state, ListAll returns these items". Existing aggregator + handler tests stay green because they only see *Item.


§5 — projax-side write-path replacement

Every projax write rewires to mBrian.

projax handler new behaviour
POST /i/{path} (detail edit, handleDetailWrite) mBrian update_node + edge re-write for parent_ids changes
POST /new (handleNewSubmit) mBrian create_node + child_of edges
POST /i/{path}/reparent (handleReparent) edge delete + re-create for child_of
/admin/bulk (handleBulkApply, handleBulkChip) bulk mBrian updates; one mBrian write per row
/admin/classify (handleClassify) mBrian update + add child_of edge
POST /views/... (5j editor) unchanged if views stay in projax.views; rewired if they move (Q5)
MCP create_item / update_item / delete_item mBrian MCP create / update / soft_delete
MCP add_link / remove_link mBrian create_edge / delete_edge

Validation (Phase 5c itemwrite package)

The pre-flight validator stays as projax-handler logic — projax UI / MCP still surface friendly errors for KindInvalidSlugFormat / KindSlugCollision / KindCycle / etc. before round-tripping. The DB-level enforcement moves to mBrian's per-user unique index on slug (covers collision) + projax's paths recomputation (covers cycle detection). Trigger-level cycle detection on mBrian's edges is a mBrian-side ask (mb-G optional).

Cycle + slug-collision semantics

Per §2.1: projax loses per-parent slug uniqueness; per-user uniqueness wins. The validator's KindSlugCollision rule needs updating to reject any duplicate slug across the whole projax-managed set, not just under the same parent.

Cycle detection: projax today does it via the path trigger (cycle = self-ancestor). After migration, projax fetches all projax nodes + their child_of edges, walks the closure on every write, rejects cycles. Cheap at m's scale.


§6 — Integrations (CalDAV / Gitea / mai.projects)

CalDAV + Gitea

The link bundle (per §2.2) moves to mBrian edges with structured metadata. The CalDAV / Gitea clients + their caches stay projax-side (the aggregator owns these). The render path queries mBrian for "which items have caldav-list edges + what URLs," then fans out to the existing CalDAV client. Net effect: the fan-out stays where it is; only the source of "what to fan out for" changes.

mai.projects bidirectional sync (Phase 1.5)

The Phase 1.5 trigger pair (mai.projects ↔ projax.items) is the most fragile piece of the integration today. After Phase 6:

  • (a) Keep the trigger pair, pointing the mai.projects view at the migrated mBrian nodes. Requires rewriting the trigger functions to read from mBrian; significant complexity because mai.projects expects projax.items columns.
  • (b) Move the bridge to projax handler layer: a sync worker watches mai.projects changes + writes mBrian; mBrian node changes flow back via a webhook or periodic poll. Slower but decoupled.
  • (c) Drop the bridge entirely: mai.projects becomes legacy; mai workers consume mBrian directly via MCP. Cleanest, but requires mai-side work to migrate workers/tasks/sessions FKs.

Inventor pick: (b) — the bridge stays operational without bleeding mBrian schema details into mai.projects code, and m can sunset it gradually. (c) is the right long-term shape but it's another migration project; out of scope for Phase 6.

This is Q2 for m.


§7 — Migration mechanics (mBrian-owned)

Per m's Q7=(a) hard-cut + Q4=(a) "mbrian must own the migration": the one-shot script lives in m/mBrian. projax-side provides the input snapshot + the rules in this doc; mBrian-side owns the execution.

projax-side input snapshot

A helper command in cmd/projax-snapshot/main.go produces a projax_snapshot.json containing every live projax.items row + every projax.item_links row, shaped for direct consumption by the mBrian-side script. One file, deterministic, round-trippable. Ships in slice 0 (the snapshot handoff, see §8).

mBrian-side script outline (for the m/mBrian issue body)

  1. Load projax_snapshot.json.
  2. Two-pass: pass 1 creates every node; pass 2 writes every edge (parent edges + item_links → projax-* edges).
  3. For each item: a. New mBrian uuid OR preserve the projax uuid (mBrian-side picks; either works given m's Q11 audit metadata is the durable reference). b. INSERT into mbrian.nodes with type=['project'] (or ['project'] + co-type per kind), title, slug, aliases, metadata={projax: {...}, projax_origin: <old_id>}. c. Where projax had multiple paths (same node under multiple parents), DEDUPE by slug — one node, multiple child_of edges.
  4. For each parent edge: INSERT mbrian.edges (source=new_id, target=parent_new_id, rel='child_of').
  5. For each item_links row: INSERT mbrian.edges with rel='projax-<ref_type>' and metadata carrying the structured payload per §2.2.
  6. For projax.views (5j): NOT migrated — per m's Q5=(a), the views table stays projax-resident.
  7. Smoke check: count(mbrian.nodes WHERE metadata->>'projax_origin' is not null) == count(items in snapshot).
  8. Hand off to projax with the new uuid map ({old_uuid: new_uuid}) so projax-side caches can warm.

Idempotency

Pre-flight: the script checks metadata.projax_origin and skips already-migrated origins on re-run. m can re-run safely if the script aborts mid-way.

Lossy bits (acceptable per m's stance)

  • paths text[] array is not preserved — projax-side adapter recomputes from edges per §4.
  • mai.projects mirror rows: per Q2=(b), a handler-layer bridge worker re-syncs after migration; the Phase 1.5 trigger pair stays disabled.

Blast-radius containment

mBrian-side runs the script with triggers paused, smoke-checks the count + spot-checks the 5 representative items in projax's checklist, then commits + signals projax-side to start slice C (read-path).


§8 — Implementation slicing (re-baselined)

Six slices. The big shift from the original draft: the mBrian-side ask compresses to one [schema] convention node + one migration script (both mBrian-owned per m's Q4). Slice 0 is a small projax-side helper that ships the snapshot. The hard gate is the migration landing — projax-side B reads it as the trigger to start.

  • 0. projax-side snapshot helpercmd/projax-snapshot/main.go. Dumps live projax.items + projax.item_links to projax_snapshot.json. Ships first; minimal risk; deliverable mBrian needs.

  • A. mBrian-side: [schema] convention node + data-migration script — m/mBrian owns. The [schema] node lives under a new [topic] hub projax-integration. The script consumes the snapshot from slice 0 and writes 47ish nodes + their edges per §7. mBrian-side post-flight: smoke-check count + spot-check 5 items per the projax checklist.

  • B. projax-side read-path adapter — projax-side. store/ package rewired against mBrian's MCP / SQL surface. The Item struct stays; method bodies rewrite. All UI + aggregator tests stay green (they only see Item shape). Per-request snapshot cache to avoid N+1 calls. Reads-only soak before slice C.

  • C. projax-side write-path — projax-side. Every handler + MCP write rewires through the adapter to mBrian. itemwrite validator updates for the per-user slug rule (Q6). Cycle detection on the in-memory closure (Q9).

  • D. mai.projects bridge worker — projax-side (Q2=(b)). Disable the Phase 1.5 trigger pair; ship a small worker that observes mai.projects writes + reflects them into mBrian, and vice versa. Decoupled, killable.

  • E. Drop projax.items + projax.item_links — projax-side. Migration 0018_drop_projax_items.sql. Triggers off after one shift's stable read+write soak on mBrian. projax.views stays (Q5).

Dependency graph:

0 (projax snapshot) ──→ A (mBrian [schema] node + migration script run)
                               │
                               ▼
                      B (projax read-path) ──→ C (projax write-path)
                                                        │
                                                        ├──→ D (mai bridge worker)
                                                        ▼
                                                E (drop projax tables)

Slice 0 unblocks A. A is mBrian-owned and the hard gate for everything else. B → C can ship together if green; otherwise B-first soak.

CalDAV / Gitea integrations stay where they are (Q3=(a)) — no slice F needed in the original sense.


§9 — Cross-repo coordination (settled)

Per m's Q4=(a) + his words "mbrian must own the migration":

  1. Protocol: file a Gitea issue on m/mBrian with "blocks projax phase 6" tag. Routed via otto/head per global Channel Routing. Head files it; kahn drafts the body.

  2. Ownership split:

    • mBrian-side owns: the [schema] convention node (MB-B) + the one-shot data-migration script.
    • projax-side owns: the snapshot helper (slice 0), the read-path adapter (slice B), the write-path (slice C), the mai bridge (slice D), the table drop (slice E).
  3. Sequencing: slice 0 produces the snapshot → mBrian-side A consumes it + runs the migration → mBrian-side signals back → projax-side starts B. The Gitea issue is the durable trace; the delegation reply chain is the real-time signal.

  4. Design-doc sharing: this plan stays in m/projax. The m/mBrian issue body (drafted alongside this re-baseline, delivered to head) excerpts §2 (schema mapping), §3 (the one [schema] node ask), §7 (the script outline), and the spot-check checklist.


§10 — Open questions (all answered 2026-05-29)

All 11 questions resolved. m confirmed every inventor pick. Section retained as the historical record + so a future hand can audit the decision rationale.

The 8 from issue #5 plus what surfaced during this survey.

Q1 — mBrian node type for projax items

  • (a) Reuse existing 'project' type, add 'area' if missing, multi-typed for both. — inventor pick (existing type minimises mBrian-side churn).
  • (b) New dedicated 'projax-item' / 'work-item' type.

Q2 — mai.projects bidirectional sync disposition (§6)

  • (a) Keep the trigger pair (rewrite to read from mBrian).
  • (b) Move to projax handler-layer bridge worker. — inventor pick (clean decoupling).
  • (c) Drop entirely; migrate mai-side FKs.

Q3 — CalDAV + Gitea integration ownership (§6)

  • (a) Clients + caches stay projax-side; only the "which items have these links" lookup moves to mBrian. — inventor pick (minimal change to aggregator).
  • (b) Migrate CalDAV/Gitea ownership to mBrian edges + projax becomes a pure renderer.

Q4 — mBrian head contact protocol (§9)

  • (a) Through otto/head per Channel Routing (default per global rule). — inventor pick.
  • (b) Direct to a future mBrian/head worker.
  • (c) m himself owns mBrian schema work — file Gitea issue on m/mBrian.

Q5 — projax.views (5j) disposition

  • (a) Keep as projax-resident table — views are projax-UI state, not graph data. — inventor pick.
  • (b) Migrate to mBrian nodes with type=[view]; one node per saved view.
  • (c) Drop the table; user views become a derived shape from mBrian metadata on the items themselves.

Q6 — Slug uniqueness model

  • (a) Adopt mBrian's per-user unique (loses "two paliads under different roots" case). — inventor pick (simpler; m hasn't used the per-parent split in practice).
  • (b) Keep projax's per-parent rule via projax-handler validator + mBrian per-user check disabled for projax nodes (requires mBrian-side scoped-uniqueness work).

Q7 — Migration mechanics (§7)

  • (a) Hard-cut, one script, accept data loss. — inventor pick (matches m's stance).
  • (b) Phased dual-write + soak.

Q8 — Tags model

  • (a) Keep tags in metadata.projax.tags (projax sees them as before; mBrian doesn't index them). — inventor pick for v1.
  • (b) Lift each tag to a [tag] node + tagged edges (mBrian convention).
  • (c) Hybrid — keep metadata for projax compatibility AND wire tagged-edges for mBrian visibility.

Q8(c) is the "right" long-term shape but doubles the write surface in slice D. Recommend deferring to a Phase 7 polish.

Q9 — Cycle detection placement

  • (a) projax-handler-side via in-memory closure walk before write. — inventor pick (cheap at m's scale).
  • (b) mBrian-side via trigger on edges (mb-G ask).

Q10 — Projax MCP surface

  • (a) Keep projax MCP tools (mcp__projax__*); they now route through the adapter. — inventor pick (no MCP client change).
  • (b) Sunset projax MCP; users call mBrian MCP directly.

Q11 — projax_origin audit metadata (§7) Per the migration script, every migrated node carries metadata.projax_origin = <old uuid>. Keep indefinitely (audit trail), purge after one shift (cleanup), or never write it (trust). Inventor pick: keep indefinitely.


§11 — Risk register

risk likelihood mitigation
mBrian-side schema work (slice A) blocks projax indefinitely medium clear delegation + Gitea issue with "blocks projax phase 6" tag; m can dispatch fast-track
47-item migration script silently drops fields low smoke check (item count parity) + spot-check 5 items post-migration before slice C
Slug collision on multi-rooted items (e.g. two paliads) medium pre-migration script: detect collisions, dedupe to one node with multiple child_of edges, log skips
mai.projects trigger pair breaks mid-migration medium turn off the triggers before migration, rebuild post-migration (Q2 (b) bridge takes over)
Adapter introduces N+1 mBrian calls during render medium one ListAll + one LinksByRef query per request, cached per-request; profile after slice C
Phase 5j views surface breaks low views stay projax-resident per inventor pick on Q5; no migration cost
flexsiebels.de public-listing renderer breaks medium metadata.projax.public.* bundle preserves the shape; spot-test before slice E
Cross-repo coordination delay medium filed as Gitea issue (durable) + delegation (real-time signal); both paths active

§12 — Test plan headlines

Slice B (migration script)

  • TestMigrateScriptSmokes — 5 hand-crafted projax.items + 3 item_links → mBrian nodes + edges; count parity assertion.
  • TestMigrateScriptIdempotent — second run = no new nodes.
  • TestMigrateScriptSlugCollision — two multi-rooted items same slug → one node with two child_of edges, log entry.

Slice C (read-path)

  • TestAdapterListAllReturnsItemsFromMBrian — seed mBrian nodes with projax_origin, ListAll returns matching Items.
  • TestAdapterGetByPathResolvesEdgesdev.paliad walks child_of edges to leaf node.
  • TestAdapterPathsArrayMultiRoot — node with two child_of edges produces 2 entries in it.Paths.

Slice D (write-path)

  • TestHandleDetailWriteUpdatesMBrian — POST /i/dev.paliad updates the mBrian node's title.
  • TestHandleReparentRewritesChildOf — POST /i/dev.paliad/reparent deletes old edge + creates new one.
  • TestSlugCollisionRejected — second create with same slug rejected with KindSlugCollision.

Slice E (drop)

  • migration 0018_drop_projax_items.sql smoke test: \dt projax.* returns only projax.views + projax.schema_migrations.

Slice F (integrations)

  • per Q2 answer — bridge-worker test (Option b) OR mai-FK migration test (Option c).

§13 — References

  • ~/dev/mBrian/db/001_initial_schema.sql — mBrian schema baseline.
  • ~/dev/mBrian/docs/schema.md — schema doc.
  • ~/dev/mBrian/CLAUDE.md — mBrian conventions + relation to flexsiebels.
  • projax/store/store.go — current Item struct + projax store API.
  • projax/store/views.go — Phase 5j views table.
  • projax/docs/design.md — current PRD.
  • projax/docs/plans/views-redesign.md — Phase 5j design.
  • m/projax issue #5 — m's Option A pick.

§14 — Status

  • Phase A (this doc): drafted by kahn 2026-05-29, re-baselined same day against live mBrian schema after m's 11 answers landed. All §10 questions resolved.
  • m/mBrian Gitea issue: body drafted; head files it under "blocks projax phase 6" tag.
  • Phase B (projax-side coder): blocked on (1) slice 0 snapshot helper ships + (2) mBrian-side migration runs + signals back. NO coder flip yet.
  • Slice 0 (projax-side snapshot helper): scoped, not yet built. Smallest first-step on projax-side; ready when head greenlights.
  • No code changes in this branch beyond this doc.