Files
projax/store/adapter.go
mAi 9607d4b307 docs+skeleton: Phase 6 Slice B prep — read-path adapter interface contract
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.
2026-05-29 15:17:24 +02:00

159 lines
6.6 KiB
Go

package store
import (
"context"
"errors"
"time"
)
// ItemReader is the read-path contract every projax UI handler, the
// internal/aggregate fan-out engine, and the MCP read tools depend on.
// Pure projax-shaped structs in/out; the slice-B mBrian-backed
// implementation translates mBrian nodes/edges into the same shape
// without leaking mBrian types to consumers.
//
// Phase 6 Slice B prep — see docs/plans/slice-b-adapter-contract.md.
// The existing *Store already satisfies this interface (the compile-time
// assertion below pins that). Slice B impl ships a second satisfier
// (MBrianReader) once m/mBrian#73's migration completes and hands the
// uuid map over.
type ItemReader interface {
// --- item lookups ---
ListAll(ctx context.Context) ([]*Item, error)
GetByID(ctx context.Context, id string) (*Item, error)
GetByPath(ctx context.Context, path string) (*Item, error)
GetByPathOrSlug(ctx context.Context, key string) (*Item, error)
Roots(ctx context.Context) ([]*Item, error)
MaiOrphans(ctx context.Context) ([]*Item, error)
ListByFilters(ctx context.Context, f SearchFilters) ([]*Item, error)
Search(ctx context.Context, q string, limit int) ([]*Item, error)
ItemsCreatedInRange(ctx context.Context, from, to time.Time) ([]*Item, error)
AllTags(ctx context.Context) ([]string, error)
// --- link lookups ---
LinksByType(ctx context.Context, itemID, refType string) ([]*ItemLink, error)
LinksByRefType(ctx context.Context, refType string) ([]*ItemLink, error)
DatedLinks(ctx context.Context, itemID string) ([]*ItemLink, error)
DatedLinksRange(ctx context.Context, from, to time.Time) ([]*ItemLinkWithItem, error)
RecentDocuments(ctx context.Context, since time.Time, limit int) ([]*ItemLinkWithItem, error)
}
// Compile-time assertion that the existing pgx-backed *Store satisfies
// ItemReader. Drops in cleanly because every method in the interface is
// already part of *Store's public surface. If a future refactor removes
// or reshapes one of these methods on *Store, the compiler points at
// this line first.
var _ ItemReader = (*Store)(nil)
// errNotImplementedSliceB is the placeholder return from every method on
// the slice-B-prep stub. Slice B's impl replaces each return with the
// real mBrian-backed body.
var errNotImplementedSliceB = errors.New("not implemented: Phase 6 Slice B (mBrian-backed reader) — waits on m/mBrian#73 migration")
// MBrianReader is the slice-B implementation target. Every method body
// returns errNotImplementedSliceB during prep; slice B's coder fills
// each in once the migration completes and the uuid map lands. The type
// holds no mBrian client today — the client decision (MCP-over-stdio /
// direct pgxpool against mbrian.* / in-process submodule) is the first
// thing slice B's impl chooses, then this struct grows the
// corresponding field.
//
// Per-method comments name the §3 gaps in the contract doc each method
// will need to resolve at impl time.
type MBrianReader struct {
// Reserved for slice-B's mBrian client. Empty struct today so the
// compile-time assertion below stays meaningful.
}
// Compile-time assertion that MBrianReader satisfies ItemReader. Keeps
// the stubs in lockstep with the interface as slice B grows methods.
var _ ItemReader = (*MBrianReader)(nil)
// --- item lookups ---
// ListAll: §2.1 — edge-walk for Item.Paths + Item.ParentIDs per item;
// §2.2 — metadata-unpack across all returned items; §3 — Item.Source
// constant.
func (*MBrianReader) ListAll(ctx context.Context) ([]*Item, error) {
return nil, errNotImplementedSliceB
}
// GetByID: §2.2 metadata-unpack.
func (*MBrianReader) GetByID(ctx context.Context, id string) (*Item, error) {
return nil, errNotImplementedSliceB
}
// GetByPath: §2.1 — walks child_of edges from path's root segment to
// resolve a leaf node. Per-request cache reduces N+1.
func (*MBrianReader) GetByPath(ctx context.Context, path string) (*Item, error) {
return nil, errNotImplementedSliceB
}
// GetByPathOrSlug: GetByPath, fall back to slug lookup.
func (*MBrianReader) GetByPathOrSlug(ctx context.Context, key string) (*Item, error) {
return nil, errNotImplementedSliceB
}
// Roots: §2.1 — "no outbound child_of edge" predicate.
func (*MBrianReader) Roots(ctx context.Context) ([]*Item, error) {
return nil, errNotImplementedSliceB
}
// MaiOrphans: §2.1 — Roots ∩ metadata.projax.management ⊇ {'mai'}.
func (*MBrianReader) MaiOrphans(ctx context.Context) ([]*Item, error) {
return nil, errNotImplementedSliceB
}
// ListByFilters: structured search; status/management/has-link/paths-prefix
// dimensions map to metadata.projax.* predicates + edge existence checks.
func (*MBrianReader) ListByFilters(ctx context.Context, f SearchFilters) ([]*Item, error) {
return nil, errNotImplementedSliceB
}
// Search: mBrian already has trigram + FTS on title + content_md
// (idx_nodes_fts). Adapter narrows to projax-managed nodes.
func (*MBrianReader) Search(ctx context.Context, q string, limit int) ([]*Item, error) {
return nil, errNotImplementedSliceB
}
// ItemsCreatedInRange: direct over nodes.created_at, scoped to
// metadata.projax_origin IS NOT NULL (or whatever projax-managed marker
// the migration settles on).
func (*MBrianReader) ItemsCreatedInRange(ctx context.Context, from, to time.Time) ([]*Item, error) {
return nil, errNotImplementedSliceB
}
// AllTags: §3 — union over metadata.projax.tags[]. Full-scan at m's
// scale; tag-graph deferred to Phase 7 (m's Q8).
func (*MBrianReader) AllTags(ctx context.Context) ([]string, error) {
return nil, errNotImplementedSliceB
}
// --- link lookups ---
// LinksByType: §2.3 — WHERE source_id=$1 AND rel='projax-'||$2.
func (*MBrianReader) LinksByType(ctx context.Context, itemID, refType string) ([]*ItemLink, error) {
return nil, errNotImplementedSliceB
}
// LinksByRefType: §2.3 — WHERE rel='projax-'||$1.
func (*MBrianReader) LinksByRefType(ctx context.Context, refType string) ([]*ItemLink, error) {
return nil, errNotImplementedSliceB
}
// DatedLinks: §2.3 — one item's edges with metadata ? 'event_date'.
func (*MBrianReader) DatedLinks(ctx context.Context, itemID string) ([]*ItemLink, error) {
return nil, errNotImplementedSliceB
}
// DatedLinksRange: §2.3 — metadata->>'event_date' BETWEEN $1 AND $2,
// joined with source node for the ItemLinkWithItem shape.
func (*MBrianReader) DatedLinksRange(ctx context.Context, from, to time.Time) ([]*ItemLinkWithItem, error) {
return nil, errNotImplementedSliceB
}
// RecentDocuments: dated links since $1 ORDER BY event_date DESC LIMIT $2.
func (*MBrianReader) RecentDocuments(ctx context.Context, since time.Time, limit int) ([]*ItemLinkWithItem, error) {
return nil, errNotImplementedSliceB
}