diff --git a/docs/plans/mbrian-backend-migration.md b/docs/plans/mbrian-backend-migration.md new file mode 100644 index 0000000..ad7084b --- /dev/null +++ b/docs/plans/mbrian-backend-migration.md @@ -0,0 +1,380 @@ +# mBrian-as-backend migration — Phase 6 design + +**Status**: Phase A design (this doc). +**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."* + +**Constraint**: data-loss tolerant on the 47 current `projax.items`. + +--- + +## §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 exists per mig 030); add `'area'` if missing | +| `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 + +projax enforces slug uniqueness **per parent** (a `paliad` slug can exist under both `dev` and `work`). mBrian enforces slug uniqueness **per user** (one `paliad` total). For a multi-parent item like `paliad` living under both `dev` and `work`, mBrian today only allows one `paliad` node — which is actually what we want: a single canonical node connected to both parents via `child_of` edges. The DAG-as-multiple-paths view is a render-time concept; the storage is one node. + +The projax handlers' itemwrite validator (Phase 5c) loses its per-parent slug rule and gains an uniqueness-per-user check. This is **stricter** — m can't have two different "paliad" projects under different roots. Flag for Q6. + +### §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=)` | 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 + +Schema fragments mBrian needs to register. Each is a separate migration on the mBrian side, written by mBrian's coder. These are the cross-repo asks: + +**MB-A — Add `edges.metadata jsonb NOT NULL DEFAULT '{}'`** (per §2.2). Backfill is no-op; new column starts empty. Optional GIN index if projax queries by metadata content (not in v1 — projax queries by `rel` only). + +**MB-B — Register projax-specific edge relations.** mBrian has no enum on `edges.rel`; new values just appear. Document the projax-namespaced rels (`projax-caldav-list`, `projax-gitea-repo`, `projax-gitea-issue`, `projax-mai-project`, `projax-url`, `projax-doc`) in a schema-node so mBrian's tooling knows they belong to projax. Concrete deliverable: a `[schema]` node `projax-edge-relations` under a `[topic]` hub `projax-integration` (created same migration). + +**MB-C — Optional: register projax types as known types.** mBrian's `'project'` type already exists. Add `'area'` if missing (projax's top-level area concept). Document the projax type set in another `[schema]` node so future readers know `'mai-managed'` means "this projax node mirrors a mai.projects row." + +**MB-D — Confirm no slug-uniqueness blockers.** projax migration creates ~47 nodes with potentially-colliding slugs (e.g. two `paliad`-rooted DAG paths today are ONE node post-migration; need to dedupe pre-write). mBrian-head to confirm the per-user unique index doesn't choke on bulk insert ordering. + +**MB-E — Read-only API surface.** projax read-path (§4) calls mBrian. We need: +- A way to filter nodes by `type` array containment (mBrian already exposes via `list_nodes(type='...')`). +- Edge query by `rel` + `source_id` or `target_id`. +- Trigram / FTS search across title + content_md (mBrian already has this). +- Optionally: a bulk endpoint that returns nodes + their outbound edges in one call (projax's tree-render path needs ~all nodes + all edges). + +Confirm MCP surface coverage. If anything's missing, mBrian-coder adds it. + +**MB-F — Write API surface.** projax write-path (§5) calls mBrian. We need create_node, update_node, soft-delete, create_edge, delete_edge — all already exist in mBrian's MCP. + +Cross-repo coordination shape: this lands as a Gitea issue on `m/mBrian` repo, batched as one ticket with sub-tasks for A–F. The issue links back to this plan + projax/#5. + +--- + +## §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-'` 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 + +```go +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 + +Per m's loss-tolerance signal: hard-cut. One script, run once, with a clear blast radius. + +Script outline (Go, lives in `cmd/migrate-mbrian/main.go`): + +1. Connect to msupabase as `postgres`. +2. Read every row from `projax.items` where `deleted_at IS NULL`. +3. For each row: + a. Generate new mBrian uuid (or preserve old uuid — projax uuids don't collide with mBrian's; we can preserve, but the migration script picks). + b. INSERT into `mbrian.nodes` with type/title/slug/aliases/metadata mapped per §2. Add `metadata.projax_origin: ` so a future audit can reconcile. + c. For each `parent_id`, INSERT `mbrian.edges (source=new_node_id, target=parent_new_id, rel='child_of')`. Parent uuids resolved via a two-pass walk: first pass creates all nodes, second pass writes edges. +4. For each `projax.item_links` row: + a. Translate `ref_type` → mBrian edge `rel` per §2.2 table. + b. INSERT `mbrian.edges` with `metadata` carrying the structured payload. +5. For each `projax.views` row (5j paliad-shape views): see Q5. +6. Smoke check: count(mbrian.nodes WHERE metadata->>'projax_origin' is not null) == count(projax.items WHERE deleted_at IS NULL). +7. DON'T drop projax.items + item_links in the same migration. Drop happens in slice E after stable read+write. + +Idempotency: the script checks `metadata.projax_origin` on each insert to avoid duplicates on re-run. + +Lossy bits (acceptable per m's stance): the projax `paths` array isn't preserved — it's recomputed from edges. The Phase 1.5 mai.projects mirror rows: the bridge worker handles re-sync after migration (Q2). + +--- + +## §8 — Implementation slicing + +Slices A–F, with hard cross-repo coordination on A. Each slice independently shippable. + +- **A. mBrian schema-fragment** — mBrian-side. Adds `edges.metadata`, registers projax edge relations + types in schema-nodes, confirms read+write MCP coverage. Lands as a Gitea issue on `m/mBrian`, coordinated through §9. + +- **B. Data migration script** — projax-side. `cmd/migrate-mbrian/main.go`. Runs once against the 47 items + their links + the 5j views (per Q5). Test on a scratch mBrian dataset before the real migration. + +- **C. Read-path replacement** — projax-side. `store/` package rewired to mBrian. The Item struct stays; method bodies rewrite. All UI + aggregator tests stay green (they only see Item). Adapter caches a per-request snapshot to avoid N+1 mBrian calls. + +- **D. Write-path replacement** — projax-side. Every handler + MCP write rewires. itemwrite validator updates for slug-uniqueness semantics. + +- **E. Drop projax tables** — projax-side. After stable read+write on mBrian for one shift, drop `projax.items` + `projax.item_links`. Migration `0018_drop_projax_items.sql`. + +- **F. Integrations disposition** — depending on Q2's answer. If (b), build the bridge worker. If (c), coordinate with mai-side to migrate workers/tasks FKs. + +Dependency graph: + +``` +A ──→ B ──→ C ──→ D ──→ E + ↘ F (parallel after D) +``` + +A is the gate. C and D can ship together if the test surface stays green; otherwise C ships first (read-path) and D follows after one shift's worth of read-only soak. + +--- + +## §9 — Cross-repo coordination + +mBrian is m's actual second brain. Schema changes carry blast radius. The protocol: + +1. **mBrian's head identity**. Looking at the m/mBrian repo: m manages it directly today (no mBrian/head worker registered in mai). Per global Channel Routing rule (only `otto/head` writes to m directly), the projax coordination request routes through `otto/head` who relays to m and back. Confirm with **Q4**. + +2. **The schema-fragment ask**. One delegation message to head with the §3 MB-A through MB-F items batched. Head decides whether to: file as an issue on `m/mBrian` directly, route through otto/head for m to dispatch a mBrian worker, or coordinate paired implementation. + +3. **Sequencing**. mBrian-side A must land + deploy before projax-side B can run. Recommend filing the schema request as `m/mBrian` issue with a clear "blocks projax phase 6" tag. + +4. **Design-doc sharing**. This plan stays in `m/projax`. m/mBrian gets a pointer issue with the relevant §2+§3 excerpts. + +--- + +## §10 — Open questions for head delegation + +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 = `. 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 `paliad`s) | 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. +- `TestAdapterGetByPathResolvesEdges` — `dev.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. Awaiting m's answers on §10 via head delegation, AND cross-repo coordination via head with mBrian. +- **Phase B (coder)**: blocked on (1) m's sign-off on §10 + (2) mBrian-side schema-fragment landed (slice A complete + deployed). +- **No code changes** in this branch beyond this doc. Slice A is mBrian-side; projax-side slices B–F wait on it.