Documents the write mechanism (scoped HTTP API), the ItemWriter interface
+ per-method mBrian mapping, the validator + bulk + MCP changes, and the
four known API gaps (G1 edge identity / G2 edge note / G3 pinned-archived /
G4 create metadata) for head to reconcile with mBrian before relying on
them. Referenced from store/adapter.go + store/mbrian_writer.go.
Per t-projax-6-sliceB-readpath. mBrian migration (m/mBrian#73) is live
on msupabase with 65 nodes + 78 child_of + 81 projax-* edges. This
commit makes the projax read path source from there behind an env
switch.
CLIENT ARCH: direct pgxpool against mbrian.* schema (same
SUPABASE_DATABASE_URL the projax binary already uses for projax.*) —
matches flexsiebels/head's cross-coupling pattern. No MCP token
plumbing.
CONTRACT (all three honoured)
- External links are SELF-EDGES (source=target=item, rel='projax-*',
payload in edges.metadata). linkFromEdge reads the node's outbound
projax-* edges; ref_id derived per ref_type from metadata (caldav
url, gitea owner/repo, mai-project mai_project_id).
- Slugs finalised: 'work'/'dania' resolve to mBrian's canonical nodes;
projax-side squatters (renamed-aside, not deleted) are documented in
the parity test as legacy-only and skipped from field comparison.
- created_at/updated_at NOT preserved — ItemsCreatedInRange orders off
metadata.projax.start_time when present, fall back to mBrian
created_at. Aggregator surfaces (timeline / dashboard) read off
caldav DTSTART + gitea updated_at, so they're unaffected.
NEW FILES
- store/mbrian.go: MBrianReader concrete impl. Bulk-loads projax-
managed nodes + child_of edges in one pair of queries per call,
builds a graphContext in memory, derives Paths via ancestor walk
(depth-capped at 64 like projax's trigger). Implements every
ItemReader method.
- store/mbrian_parity_test.go: 5 parity tests against the live db —
ListAll field equality (skipping the renamed squatter slugs),
spot-check resolves, caldav-list link round-trip, gitea-repo link
round-trip, AllTags union, NotFound consistency. All 5 GREEN.
- cmd/projax-remap-views/main.go: one-shot tool to rewrite
projax.views.filter_json.project_id from old projax uuids to new
mBrian uuids using the audit map mBrian dropped (head will relay
the path). Dry-run default; --apply commits. Idempotent.
- docs/plans/slice-b-views-projectid-gap.md: surfaces the gap + the
remediation path. Must run remap BEFORE slice E drops projax.items.
CHANGES
- store/adapter.go: kept the ItemReader interface + *Store assertion;
removed the prep stub (replaced by mbrian.go).
- web/server.go: Server.Items store.ItemReader field. web.New defaults
Items to the concrete *Store (legacy path). main.go overrides to
MBrianReader when PROJAX_BACKEND=mbrian.
- All read-path call sites in web/ swapped from s.Store.<readMethod>(
to s.Items.<readMethod>( for the 15 ItemReader methods. MCP tools
unchanged (separate scope; can pivot in a follow-up). Writes still
flow through s.Store.
- cmd/projax/main.go: PROJAX_BACKEND env switch with "store" (default)
and "mbrian" values. Logs the choice at startup. Unknown value
refuses to start.
SMOKE
- go build ./... green; go vet green.
- go test ./store/ -count=1 — all parity tests pass against live data.
- Local server boot with PROJAX_BACKEND=mbrian — backs binding logs
"backend=mbrian (read path via store.MBrianReader)" and serves
/views/tree (auth wall protects deeper smoke; parity tests cover
that surface).
PRE-EXISTING failure NOT addressed in this commit: 3 timeline_filter
tests in web/ already failed on main (legacy /timeline URL hits the
Phase 5j 301 redirect to /views/timeline). No diff vs main in those
test files; out of scope for slice B.
OUT OF SCOPE FOR SLICE B (deferred):
- MCP read tools migration to ItemReader (separate diff, low risk).
- Aggregator's LinkLister wired to ItemReader (currently consumes
*Store directly through Server.Aggregator()).
- views.filter_json.project_id remap RUN — tool ships here, run waits
on the head's relay of the audit-map path.
- Slice C write-path. Slice D mai-bridge worker. Slice E drop.
Per head's parallel-prep brief while m/mBrian#73 (migration script +
[schema] node) is being built mBrian-side. NO mBrian-MCP-backed
implementation yet — the migration worker may refine the landed
node/edge shape and building the impl now risks rework.
Built ONLY the parts stable regardless of mBrian internals:
1. CONSUMER INVENTORY (docs/plans/slice-b-adapter-contract.md §1)
- Every *store.Store read method (15 methods) with signature + semantics
- Every call site across web/, internal/aggregate/, mcp/ — table form
- Item / ItemLink field-by-field shape contract: which fields come
direct from node columns, which from edge-walk, which from
metadata-unpack
- Direct pgxpool access flagged out-of-scope (admin counts, bulk
tx, links event-date update — slice C reworks those)
- Views (5j) explicitly NOT in scope per m's Q5=(a)
2. INTERFACE CONTRACT (store/adapter.go)
- ItemReader Go interface — 15 methods, pure projax-shaped structs
in/out, zero mBrian type leakage
- var _ ItemReader = (*Store)(nil) compile-time assertion proving the
existing pgx-backed *Store satisfies the contract today
3. SKELETON (store/adapter.go MBrianReader)
- Empty struct (mBrian client choice deferred to slice B impl)
- All 15 methods stubbed, return errNotImplementedSliceB
- var _ ItemReader = (*MBrianReader)(nil) keeps the stubs in lockstep
with the interface as slice B grows
- Each stub carries a one-line comment naming the §3 gap(s) it
resolves at impl time
- `go build ./...` green; `go vet ./store/` green
4. GAP FLAGS (docs/plans/slice-b-adapter-contract.md §3)
- item_links.rel free-form annotation → mBrian edge.note (add to
m/mBrian#73 §1 for the migration script)
- ItemLink.RefID per-rel-type extraction rule (caldav URL vs gitea
owner/repo vs mai project uuid)
- paths[] recomputation cost (per-request memoisation)
- AllTags aggregation (full-scan ok at m's scale; tag-graph deferred
per m's Q8)
- Roots / MaiOrphans "no outbound child_of edge" predicate
- ItemsCreatedInRange scoped to projax_origin marker
- Item.Source / SourceRefID constant + mai-edge-derived fields
- ItemLinkWithItem join shape (two queries + in-memory join vs bulk
MCP helper)
- Admin counts — recommend adding Counts(ctx) to ItemReader for cohesion
Stays parked after this. Slice B IMPL (mBrian-MCP client wiring + per-
method bodies + handler rename from s.Store.X to s.Items.X) waits on
the migration completing and uuid map landing.
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.
m's decision on issue m/projax#5 (2026-05-29): Option A — full backend
migration to mBrian. mBrian becomes the canonical store for projax
data; projax UI surfaces stay (Tiles dashboard, calendar grid,
timeline spine, the just-shipped 5j /views routes) but read+write
goes through mBrian instead of projax.items.
The plan covers:
- §1 diagnosis: closing the parallel-knowledge-surface gap
- §2 column-by-column schema mapping (projax.items → mBrian nodes +
metadata, projax.item_links → mBrian edges + new edges.metadata)
- §3 mBrian-side requirements: schema fragments to add (edges.metadata
column, projax edge relations + types schema-nodes)
- §4 read-path replacement: store adapter over mBrian, UI shape stable
- §5 write-path replacement: every handler + MCP write rewired
- §6 integrations disposition: CalDAV/Gitea stay projax-handled at
consumption; mai.projects sync moves to a handler-layer bridge
- §7 migration mechanics: hard-cut script per m's loss tolerance
- §8 six-slice plan: A (mBrian schema) → B (data migration) →
C (read-path) → D (write-path) → E (drop projax tables) → F
(integrations)
- §9 cross-repo coordination protocol via otto/head (no mBrian/head
worker exists today)
- §10 eleven open questions for m, batched for head delegation
- §11 risk register
- §12 test plan headlines
Slice A is mBrian-side and is the hard gate — projax B–F cannot start
until mBrian's schema fragments land. Cross-repo coordination request
filed alongside the m delegation.
No code changes; this branch ships docs only. Coder shifts wait on
m's sign-off on §10 + mBrian-side slice A.
m's feedback on 5i (verbatim): "It's not really what I wanted. It
should like the paliad custom views, not of the existing views a
variant but individually created views."
5i modelled views as overlays on existing pages (?view=<uuid>). m wants
the paliad model: views are first-class URLs (/views/{slug}), each one
its own page. System defaults (dashboard, calendar, timeline, ...)
share the route shape with reserved slugs; user-created views land
beside them.
Plan covers: schema redesign (slug as URL key, drop is_default_for +
pinned, add icon + sort_order + show_count + last_used_at), four-route
table (landing with MRU redirect, render, editor blank/edit), system-
view shape (hybrid alias recommendation under Q1), editor surface
(dedicated pages, not modal), migration path from 5i (drop table +
delete overlay code; keep view_type enum and per-view_type renderers),
seven-slice implementation chain (A schema → B routes → C system views
→ D editor → E sidebar → F cleanup → G polish).
11 open questions batched in §9 — head delegation pending. NO chip-
picker without head's explicit re-grant (5i permission was one-time).
No code changes; this branch ships docs only. Coder shifts wait on m's
sign-off via head's relay.
m answered every open question directly via AskUserQuestion (greenlit
for inventor 2026-05-26 13:12). New §8.5 captures the picks + slice
implications. Inventor picks held on 6 of 9; m differed on Q5 (project
filter descendants) — wants an include-descendants toggle on the chip
rather than always-on, so Slice A grows an `IncludeDescendants` field
on TreeFilter + a toggle on the picker chip.
view_type enum locks at 5 (card/list/calendar/kanban/timeline). All
four out-of-scope items stay parked. No other slice changes.
Phase A (design) of Phase 5i — project filter dim, view-type as a
parameter, saved views, and per-page bindings. Five-slice implementation
plan (A: project filter → B: view-type URL → D: saved-views schema → C:
kanban → E: defaults). Nine open questions for m batched in §9 ready
for head delegation.
No code changes; this branch ships docs only. Coder shifts wait on m's
sign-off via head.
Phase 5c slice A. Pulls the structural rules out of the Postgres
triggers into a Go-side validator. The trigger stays as defence in
depth; the validator is the human-facing error path.
- docs/plans/itemwrite-validation.md enumerates every rule the
triggers in 0001 + 0010 enforce, with the ValidationError.Kind
callers will see for each. Eleven rules total (two SQL-only safety
rails kept untranslated).
- internal/itemwrite/itemwrite.go: ValidationError + Input + Reader
interface + ValidateFormat (pure: missing fields, slug format,
status whitelist, self-parent) + ValidateAgainstStore (DB-aware:
unknown-parent, slug-collision under any common parent, cycle via
ancestor-closure DFS capped at 64 hops to mirror the trigger).
- Eight kind constants exported: missing-required, invalid-slug-format,
invalid-status, slug-collision, cycle, self-parent, unknown-parent,
unresolvable-path.
Tests cover every kind on both happy and reject paths: missing /
whitespace fields, slug containing dot / upper / whitespace, invalid
status enum, self-parent guard, unknown parent id, root slug collision,
sibling slug collision under common parent, cycle on ancestor closure,
and the "Reader returns ListAll error → validator returns nil" path
(callers see the infra error later, validator doesn't mask it).
No caller migrates yet. Same Go-linker DCE caveat as 5a/5b slice A:
`strings <binary> | grep internal/itemwrite` returns 0 until slice B
imports.
Task: t-projax-5c-itemwrite
Phase 5a slice A: a new package that concentrates the "fan out across
linked items" pattern web/dashboard.go, web/timeline.go and mcp/tools.go
each had separate copies of. No callers touch it yet — slices B/C/D
migrate them in turn.
- Aggregator with five methods (Todos/Events/Issues/Docs/Creations) plus
All convenience for the MCP timeline. Each method takes a *store.Item
slice and (optionally) a Window, returns typed Row slices.
- Row types embed the underlying caldav.Todo / caldav.Event / gitea.Issue
so existing html/template field accesses (.Todo.UID, .Event.Summary,
…) keep resolving via Go field promotion in slices B/C.
- TimelineRow sum-type wrapper (with pointer slots per Kind) plus the
flat template-friendly fields. Lifted-but-untouched from web/.
- BuildTimelineDays + SortTimelineRows + EventStartLabel +
EventDurationHint lifted near-verbatim from web/timeline.go.
- CalDAV/Gitea/Store interfaces in the aggregator so unit tests stub IO
cleanly. Real *caldav.Client / *gitea.Client / *store.Store satisfy
by method set.
- Per-source error handling preserved: log at WARN + skip the bad
fetch, return surviving rows.
Tests cover empty inputs, fan-out call counts, per-source error
recovery, window narrowing for todos, issue-cache hit path, doc/creation
allow-list filtering, BuildTimelineDays asc/desc order, sticky pills,
far-future fade, within-day sort.
Plan doc captures the slicing strategy + design decisions:
docs/plans/aggregator-refactor.md.
Task: t-projax-5a-aggregator
Phase A of the 4c task brief. Survey found the integration already
exists and ships (mAi#228, 2026-05-15) — the question is which slice
deepens it next.
Plan covers:
- Otto-PWA structural notes (Go backend + Bun/TS frontend in m/mAi, not
m/otto — ADR-006 moved PWA code into mAi)
- Existing MCP-consumption pattern (Bearer-token JSON-RPC bridge,
graceful 501 degradation, 4 endpoints registered, frontend shells +
client TS, live at https://otto.msbls.de/projax/)
- 3 deepening slices: (S1) timeline surface, (S2) CalDAV writeback,
(S3) dated docs quick-add
- Recommendation: ship (S1) first — Phase 4a just landed /timeline
in projax web, the data + aggregation logic exist, exposing via MCP
is a clean wrap with no schema or auth model change
- Impl plan if greenlit: 3 slices across projax + mAi with cross-repo
deploy verification
Out of scope until head greenlights: writing any code in m/mAi.
Per docs/plans/mgmt-teardown.md §4 steps 5 + 6.
Step 5: deploy/dokploy.yaml — stale "federated with mgmt.msbls.de" line
in the header comment replaced with the current host-scoped /login cookie
model. The mgmt federation never happened in projax anyway (projax
cookies are host-scoped, no Domain attribute).
Step 6: append a "DONE 2026-05-16" section to docs/plans/mgmt-teardown.md
recording every step's commit hash across both repos, the head-approved
deviation from §4 step 1 (SvelteKit-side redirect instead of Dokploy
Traefik labels — Dokploy config is UI-only), verification curls, and the
post-teardown janitorial that's out of scope for the worker (env-var
cleanup in Dokploy, DNS at m's leisure).
m/msbls.de side merged separately (86bfa61) — three commits:
2941dc4 (redirect), <previous step's commit covers the rest>.
Research-only output: audit of every /mgmt/* route + auth shell + server
libs in m/msbls.de, mapping to projax equivalents, gap list, migration
sequence, risk register.
Headline:
- 4 mgmt routes audited (root, /login, /self redirect, /mgmt/* guard)
- 3 already at parity on projax (login, auth guard, CalDAV VTODOs)
- 1 small gap (VEVENTs on dashboard) is the only blocker — Phase 3l candidate
- 2 further "gaps" (mWorkRepo cards, mBrian topic cards) recommended
park-forever; mgmt never shipped them either
- Cross-repo grep confirms ZERO external dependencies on /mgmt/* — only
one stale comment in projax/deploy/dokploy.yaml
No code touched. m reads the plan + decides go/no-go on Gap 1 + migration
sequence (§4) before any teardown work.