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.
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_originaudit 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.
projax.item_links → mBrian edges
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 jsonbtombrian.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 textas 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 enforced — CREATE 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:
- 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-listcarries{url: text}). - The projax type usage:
'project'for both projects and areas;metadata.projax.kinddistinguishes (areavs defaultproject).'mai-managed'as a co-type marker for nodes mirroringmai.projectsrows. - 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. - A pointer to
projax_originaudit 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_linksrows (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)
- Load
projax_snapshot.json. - Two-pass: pass 1 creates every node; pass 2 writes every edge (parent edges + item_links → projax-* edges).
- 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.nodeswithtype=['project'](or['project']+ co-type perkind),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, multiplechild_ofedges. - For each parent edge: INSERT
mbrian.edges (source=new_id, target=parent_new_id, rel='child_of'). - For each item_links row: INSERT
mbrian.edgeswithrel='projax-<ref_type>'andmetadatacarrying the structured payload per §2.2. - For projax.views (5j): NOT migrated — per m's Q5=(a), the views table stays projax-resident.
- Smoke check: count(mbrian.nodes WHERE metadata->>'projax_origin' is not null) == count(items in snapshot).
- 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 helper —
cmd/projax-snapshot/main.go. Dumps liveprojax.items+projax.item_linkstoprojax_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]hubprojax-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. TheItemstruct 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. Migration0018_drop_projax_items.sql. Triggers off after one shift's stable read+write soak on mBrian.projax.viewsstays (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":
-
Protocol: file a Gitea issue on
m/mBrianwith "blocks projax phase 6" tag. Routed via otto/head per global Channel Routing. Head files it; kahn drafts the body. -
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).
- mBrian-side owns: the
-
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.
-
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 +taggededges (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 twochild_ofedges, log entry.
Slice C (read-path)
TestAdapterListAllReturnsItemsFromMBrian— seed mBrian nodes withprojax_origin, ListAll returns matching Items.TestAdapterGetByPathResolvesEdges—dev.paliadwalkschild_ofedges to leaf node.TestAdapterPathsArrayMultiRoot— node with twochild_ofedges produces 2 entries init.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.sqlsmoke test:\dt projax.*returns onlyprojax.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/projaxissue #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.