2 Commits

Author SHA1 Message Date
mAi
4fdeca8269 Merge branch 'mai/kahn/phase-6-sliceB-prep' (Phase 6: slice-B adapter interface contract + skeleton, no impl) 2026-05-29 15:18:15 +02:00
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
2 changed files with 386 additions and 0 deletions

View File

@@ -0,0 +1,228 @@
# Phase 6 Slice B — read-path adapter contract
**Status**: prep work (this doc). No implementation.
**Branch**: `mai/kahn/phase-6-sliceB-prep`.
**Author**: kahn (coder, prep mode), 2026-05-29.
**Parent plan**: `docs/plans/mbrian-backend-migration.md` (on `main`).
**Scope boundary**: contract + compile-checking skeleton only. The mBrian-backed implementation waits on m/mBrian#73 landing the migration + handing over the uuid map.
---
## §1 — Consumer inventory
Every read-path call site against `*store.Store` and the projax-shaped `Item` / `ItemLink` types. The interface (§2) is the union of these.
### §1.1 — `*store.Store` read methods (source: `store/store.go`)
| method | signature | semantics |
|---|---|---|
| `ListAll` | `(ctx) ([]*Item, error)` | every live item, ordered by `paths NULLS FIRST, slug` |
| `GetByID` | `(ctx, id) (*Item, error)` | single item by uuid |
| `GetByPath` | `(ctx, path) (*Item, error)` | resolve `dev.paliad` style path to leaf item |
| `GetByPathOrSlug` | `(ctx, key) (*Item, error)` | path first, fall back to bare slug |
| `Roots` | `(ctx) ([]*Item, error)` | items with `cardinality(parent_ids) = 0` |
| `MaiOrphans` | `(ctx) ([]*Item, error)` | mai-managed root items needing classify |
| `ListByFilters` | `(ctx, SearchFilters) ([]*Item, error)` | structured search (status / mgmt / has-link / paths-prefix) |
| `Search` | `(ctx, q, limit) ([]*Item, error)` | trigram + FTS title/content/aliases |
| `AllTags` | `(ctx) ([]string, error)` | union of every item's tags |
| `LinksByType` | `(ctx, itemID, refType) ([]*ItemLink, error)` | one item's links of a given `ref_type` (empty = all) |
| `LinksByRefType` | `(ctx, refType) ([]*ItemLink, error)` | every link of a given ref_type across items |
| `DatedLinks` | `(ctx, itemID) ([]*ItemLink, error)` | one item's links anchored to a date (PER artifacts) |
| `DatedLinksRange` | `(ctx, from, to) ([]*ItemLinkWithItem, error)` | dated links within window, joined with their item |
| `RecentDocuments` | `(ctx, since, limit) ([]*ItemLinkWithItem, error)` | recent dated docs, joined with their item |
| `ItemsCreatedInRange` | `(ctx, from, to) ([]*Item, error)` | items created within window |
### §1.2 — Consumer call sites (by file)
Each row = one read-path call site. Direct Pool access (admin.go counts, bulk.go filter-tx, links.go event-date update) is flagged separately at the bottom — those rework targets are out of slice B's read-path scope.
| consumer | method | use case |
|---|---|---|
| `web/server.go handleTree` | `ListAll`, `AllTags`, `linkKindsByItem` (LinksByRefType ×N) | render /views/tree with chip-counted forest |
| `web/server.go handleDetail` | `GetByPath` ×2 (PER fallback), `LinksByType` (caldav), `DatedLinks` | render /i/{path} detail page |
| `web/server.go parentOptions` | `ListAll` | populate parent <select> on /new + /reparent |
| `web/server.go handleClassify` | `MaiOrphans`, `parentOptions` | render /admin/classify |
| `web/dashboard.go handleDashboard` | `ListAll`, `LinksByRefType` (caldav), `LinksByType` (gitea) ×N, `RecentDocuments` | Tiles + tasks + events + docs cards |
| `web/calendar.go handleCalendar` | `ListAll` | month grid scope |
| `web/timeline.go handleTimeline` + `buildTimeline` | `ListAll`, `linkKindsByItem` | chronological spine |
| `web/graph.go handleGraph` | `ListAll`, `AllTags` | DAG SVG render |
| `web/bulk.go handleBulk` | `ListAll`, `AllTags`, `GetByID` | /admin/bulk filtered checklist |
| `web/caldav.go` (admin + create/unlink) | `ListAll`, `LinksByRefType`, `LinksByType`, `GetByPath` | /admin/caldav surface |
| `web/gitea.go detailIssues` | `LinksByType` (gitea-repo) | /i/{path} issues card |
| `web/gitea_writeback.go` | `GetByPath`, `LinksByType` | issue close/comment/create handlers |
| `web/links.go` (add/remove/list) | `GetByPath`, `DatedLinks` | /i/{path} documents section |
| `web/dashboard_pin.go` | `SetPinned` — WRITE, not slice B | pin toggle (slice C) |
| `web/views.go handleViewRender` | `ListAll`, `AllTags`, `linkKindsByItem` | /views/{slug} render (5j) |
| `web/system_views.go legacyRedirect` | `GetViewByID` — views CRUD (NOT in scope) | legacy 5i uuid → 5j slug redirect |
| `internal/aggregate aggregator.go` | takes `LinkLister` interface (LinksByType + ItemsCreatedInRange) | shared fan-out across tasks/events/issues/docs |
| `mcp/tools.go` (read tools) | `ListByFilters`, `LinksByRefType`, `GetByID`, `GetByPathOrSlug`, `LinksByType`, `ListAll`, `Search`, `RecentDocuments` (via dashboard fan-out reuse) | every read-side MCP tool |
### §1.3 — Direct Pool access (out-of-scope for slice B, flagged for slice C)
These bypass the store API and pull `*pgxpool.Pool` directly. Slice C (write-path) reworks them; flagging here so slice B's interface stays minimal:
- `web/admin.go` — three count queries (`SELECT count(*) FROM projax.items WHERE …`) for the admin index. Either: (a) add `Counts(ctx) (AdminCounts, error)` to the adapter, (b) compute in-handler from `ListAll`. Adapter pick.
- `web/bulk.go handleBulkApply` — multi-row UPDATE inside a tx. Pure write; slice C.
- `web/links.go handleSetEventDate` — single UPDATE on `item_links.event_date`. Pure write; slice C.
### §1.4 — `*Item` + `*ItemLink` shape contract (consumer side)
Adapter MUST return these exact field sets in the result types. Nothing under `metadata.projax.*` in mBrian leaks to consumers; the adapter parses + materialises into the `Item` fields below.
| field | semantics in slice B adapter |
|---|---|
| `Item.ID` | mBrian node uuid (post-migration); preserved old uuid OK per Q11 |
| `Item.Kind` | `[]string{"project", ...}` — mBrian `node.type[]` 1:1 |
| `Item.Title`, `Item.Slug`, `Item.ContentMD`, `Item.Aliases` | mBrian `node.title/slug/content_md/aliases` 1:1 |
| `Item.Paths` | **derived** from `child_of` edge walk + the node's own slug. Adapter computes per-call (cached per-request) |
| `Item.ParentIDs` | **derived** from outbound `child_of` edges |
| `Item.Metadata` | `node.metadata` MINUS the `projax` sub-key (which gets unpacked into the struct fields below) |
| `Item.Status` | `node.metadata.projax.status` (default "active") |
| `Item.Pinned`, `Item.Archived` | `node.pinned`, `node.archived` 1:1 |
| `Item.StartTime`, `Item.EndTime` | `node.metadata.projax.start_time` / `.end_time` (timestamptz strings) |
| `Item.Tags`, `Item.Management`, `Item.TimelineExclude` | `node.metadata.projax.tags` / `.management` / `.timeline_exclude` |
| `Item.Public`, `Item.PublicDescription`, `Item.PublicLiveURL`, `Item.PublicSourceURL`, `Item.PublicScreenshots` | `node.metadata.projax.public.{enabled, description, live_url, source_url, screenshots}` |
| `Item.CreatedAt`, `Item.UpdatedAt` | `node.created_at`, `node.updated_at` 1:1 |
| `Item.Source` | always `"projax"` (legacy field; new adapter sets this to maintain consumer assumption) |
| `Item.SourceRefID` | mai.projects.id from `projax-mai-project` edge metadata when present |
| `ItemLink.ID` | mBrian edge uuid |
| `ItemLink.ItemID` | edge `source_id` (the projax-side end) |
| `ItemLink.RefType` | strip `projax-` prefix from edge `rel` (`projax-caldav-list``caldav-list`) |
| `ItemLink.RefID` | edge `metadata.ref_id` OR derived from rel-specific payload (caldav: `url`; gitea-repo: `owner/repo`; mai-project: `mai_project_id`) — see §3 gaps |
| `ItemLink.Rel` | edge `note` (free-form annotation) OR a constant per rel-type (e.g. `'contains'`) |
| `ItemLink.Metadata` | edge `metadata` MINUS the `ref_id` extraction |
| `ItemLink.EventDate` | edge `metadata.event_date` (date string parsed) |
| `ItemLink.CreatedAt` | edge `created_at` 1:1 |
### §1.5 — Views (Phase 5j) — explicitly NOT in slice B
Per m's Q5=(a), `projax.views` stays projax-resident. All view CRUD methods (`ListViews`, `GetView`, `GetViewByID`, `CreateView`, `UpdateView`, `DeleteView`, `TouchView`, `MostRecentView`, `ReorderViews`) stay on the existing `*Store` and are NOT part of the adapter interface. The `Server` struct uses the adapter for items+links and the existing `Store` for views.
---
## §2 — Adapter interface contract
Defined in `store/adapter.go` (this branch). Pure projax-shaped structs in/out; zero mBrian type leakage. The existing `*store.Store` already satisfies this interface (it's just a subset of its public surface) — the compile-time assertion makes that explicit. Slice B impl ships a second satisfier (`*MBrianReader`) that wraps mBrian access.
```go
// ItemReader is the read-only contract every projax UI handler / aggregator /
// MCP read tool depends on. Slice B implements a second satisfier on top of
// mBrian's MCP/SQL surface.
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)
}
```
### §2.1 — Methods needing edge-walk-derived data
Slice B's mBrian impl must compute these from `child_of` edges + node fields. Cost is one outbound-edges fetch per node OR one bulk edges-by-rel query per request, depending on how the adapter caches.
- `Item.Paths` — every method returning `*Item` or `[]*Item`.
- `Item.ParentIDs` — same.
- `GetByPath` — walks edges to resolve `dev.paliad` to a leaf node.
- `Roots` — filter where no outbound `child_of` edge.
- `MaiOrphans``Roots``metadata.projax.management ⊇ {'mai'}`.
### §2.2 — Methods needing metadata-unpack
Adapter parses `metadata.projax.*` on read; writes (slice C) re-serialise. Affected fields: Status, Tags, Management, TimelineExclude, Public + 4 public_* fields, StartTime, EndTime.
### §2.3 — Methods needing edge.metadata filters
- `LinksByType(itemID, refType)`: WHERE source_id=$1 AND rel = 'projax-' || $2.
- `LinksByRefType(refType)`: WHERE rel = 'projax-' || $1.
- `DatedLinks(itemID)`: source_id=$1 AND metadata ? 'event_date'.
- `DatedLinksRange(from, to)`: metadata->>'event_date' BETWEEN $1 AND $2.
- `RecentDocuments(since, limit)`: dated links since $1 ORDER BY metadata->>'event_date' DESC LIMIT $2.
mBrian's `idx_edges_metadata` GIN index already exists (mig 010); these queries are index-eligible.
---
## §3 — Gap flags
Items the known mBrian schema needs to satisfy cleanly. The migration script handles most; flag here for the slice-B impl + the migration worker as cross-check items.
| gap | shape | status |
|---|---|---|
| **`item_links.rel` (free-form annotation) preservation** | projax has both a typed `ref_type` AND a free-form `rel` text (`"contains"`, `"source"`, etc.) on item_links. mBrian's edge `rel` is the typed name; the free-form annotation maps to `edge.note`. Migration must NOT drop the projax `rel` value. | Add to m/mBrian#73 §1 edge mapping: source `rel` → mBrian `edges.note`. |
| **`ItemLink.RefID` semantics per type** | projax `ref_id` is a typed external pointer (caldav url, gitea `owner/repo`, gitea-issue id, mai project uuid, bare url). mBrian edges carry the payload in metadata. Need a per-rel-type extraction rule. Suggested: `metadata.ref_id` for the canonical reference + leaves structured payload alongside (`url` for caldav, `owner`/`repo` for gitea). | Slice B impl reads back per-rel-type; document in m/mBrian#73 issue for the migration script to write consistently. |
| **`paths text[]` recomputation cost** | Adapter computes paths from `child_of` edge walk per call. For `ListAll` over ~65 items, one bulk edges-by-rel query joined with the node id set is N rows where N = total `child_of` edges. Cheap at m's scale; add per-request memoisation. | Slice B impl. No mBrian-side action. |
| **`AllTags` aggregation** | Union of `metadata.projax.tags[]` across all projax-managed nodes. No mBrian index on metadata-array-element. At m's scale (<200 nodes), full-scan is fine; if we grow, add a derived `[tag]` node graph (m's Q8 deferred to Phase 7). | Slice B impl, no mBrian-side action. |
| **`Roots` / `MaiOrphans` predicate** | "No outbound `child_of` edge" requires a subquery / left-join-where-null pattern. Index-eligible via `idx_edges_source_rel` on `(source_id, rel)`. | Slice B impl. |
| **`ItemsCreatedInRange`** | Direct over `nodes.created_at`; trivial. Scoped to `metadata.projax_origin IS NOT NULL` so non-projax mBrian nodes don't leak into projax surfaces. | Slice B impl + a `metadata GIN` query (already indexed). |
| **`Item.Source` field expectation** | The legacy `Source` field on `Item` reads `"projax"` everywhere consumers check it (some MCP tools branch on it). Adapter sets a constant. | Slice B impl detail, no DB action. |
| **`SourceRefID` for mai bridge** | When a node has a `projax-mai-project` edge, expose its `metadata.mai_project_id` as `Item.SourceRefID`. Slice D (mai bridge worker) writes these edges. | Slice B impl reads existing edges; slice D writes new ones. |
| **`ItemLinkWithItem` join shape** | Used by `DatedLinksRange` and `RecentDocuments`. Adapter does two queries (edges-with-dates + node-by-id batch) + an in-memory join, OR one combined MCP call if mBrian exposes a bulk-edges-with-source-node helper. Both work; pick by perf. | Slice B impl, no mBrian-side change required. |
| **Admin counts (web/admin.go direct Pool)** | Three count(*) queries (total items, total mai-managed, total public). Adapter gains `Counts(ctx) (AdminCounts, error)` small extension. | Add to ItemReader interface in slice B (low-risk; constant-return until impl) OR keep as a separate `AdminReader` interface. Recommend adding to ItemReader for cohesion. |
---
## §4 — Skeleton (this branch)
The Go file `store/adapter.go` ships in this branch with:
1. `ItemReader` interface as in §2.
2. `var _ ItemReader = (*Store)(nil)` compile-time assertion. (Drops in cleanly because `*Store` already exposes every method in the contract.)
3. `MBrianReader` struct with stubbed method bodies that return `errNotImplementedSliceB`. Each stub carries a one-line comment naming the §3 gap it depends on (if any) so slice B's impl-fill knows what to look up.
4. `var _ ItemReader = (*MBrianReader)(nil)` compile-time assertion so the stubs stay aligned with the interface.
`go build ./...` is green with the skeleton in place. No tests, no behaviour, no mBrian client dependency.
The actual mBrian client wiring (whether MCP-over-stdio, direct Postgres against `mbrian.*` schema, or the in-process submodule pattern flexsiebels uses) is the first decision slice-B-impl makes; it stays out of this prep step.
---
## §5 — Wiring shape after slice B impl
For reference of the post-slice-B shape (no code in this slice):
```go
// Server struct keeps two readers: ItemReader (slice-B mBrian-backed) +
// existing *Store (views CRUD only).
type Server struct {
Items ItemReader // slice B: MBrianReader; today: *Store
Store *store.Store // views CRUD only after slice B
// ... rest unchanged
}
```
Every handler that today reads `s.Store.ListAll(...)` becomes `s.Items.ListAll(...)`. Mechanical rename. Slice B impl ships both adapter wiring + the rename across handlers as one diff once the migration completes.
---
## §6 — What's NOT in this prep
- mBrian-MCP client wiring.
- Any test of mBrian-backed behaviour.
- Write-path methods (slice C scope).
- View CRUD migration (Q5=(a) stays projax-resident).
- mai bridge worker (slice D).
- Drop projax tables (slice E).
---
## §7 — References
- `docs/plans/mbrian-backend-migration.md` (on `main`) parent plan.
- `cmd/projax-snapshot/` (slice 0, merged at `38182df`) input for mBrian's migration.
- m/mBrian#73 mBrian-side schema convention node + migration script (in flight).
- `store/store.go` current `*Store` implementation; the interface `*Store` already satisfies.
- `internal/aggregate/aggregator.go` existing `LinkLister` interface precedent (a narrow projection of `*Store`).

158
store/adapter.go Normal file
View File

@@ -0,0 +1,158 @@
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
}