capture #49b: CaptureService use-case + BrainStore extraction + ports/entities #51

Closed
opened 2026-06-22 20:26:55 +00:00 by mathias · 1 comment
Owner

Parent: #49 · Spec: specs/capture-bdd-spec.md §1 · Depends on: nothing (buildable against fake ports) · Blocks: #49c, #49d

Scope

The Clean-Architecture core of capture: extract #45's brain write/update/get logic out of the MCP-only layer into a shared BrainStore, define the ports and entities, and implement the CaptureService orchestration. Build and unit-test against fake ports — no live gating, no HTTP, no real gitea yet. The classification check is an injected policy (satisfied later by #49a/#50).

This is the biggest single chunk and the spine everything else hangs off.

Ground-truth (from #49)

brain_update/brain_get currently live in ingestion/internal/mcp/ only; /write (api.WriteNote, a free function) is the only brain write reachable from the REST/api layer. The extraction lifts the update/get logic into a shared impl both the MCP handlers and capture call — the Clean-Arch payoff, real work, not a wrapper.

What to build

  1. ingestion/internal/capture/ package.
  2. Ports (interfaces):
    • BrainStoreWrite(ctx, note) (ref, error), Update(ctx, slug, note) (ref, error), Get(ctx, id) (note, error) where ref = {id, path, content_hash}.
    • IssueTrackerCreateIssue, CloseIssue, CommentIssue (interface only here; impl in #49c).
    • SummaryWriterWriteFile(repo, path, content) error.
    • ClassificationPolicyDerive(target) level + Effective(declared, derived) level (interface only; impl in #49a/#50).
    • AuditSinkRecord(ctx, auditEntry) error (interface only; impl in #49e/#49f).
  3. Entities: Insight, Ticket, Summary, CaptureContext, CaptureReceipt — plain structs.
  4. BrainStore extraction: lift brain_update/brain_get/brain_write logic from mcp/ into a concrete BrainStore impl; re-point the existing MCP handlers at it so there's one implementation, not two (DRY — the whole point). Preserve the #45 read-after-write contract and the batch staleness discipline inside the store/use-case so callers never carry the rule.
  5. CaptureService.Capture(ctx, CaptureInput) (CaptureReceipt, error): validate-before-write (fail-closed), resolve effective classification via the policy port, orchestrate insights (write/supersede) → tickets → summary, best-effort with per-item outcome, assemble the partial-aware receipt. dry_run short-circuits after validation.

Acceptance criteria

  • capture/ package: ports, entities, CaptureService
  • BrainStore concrete impl with Write/Update/Get; MCP handlers re-pointed at it (no duplicate logic)
  • #45 read-after-write + staleness discipline preserved inside the store/use-case
  • Validate-before-write: malformed input → error, nothing written
  • Best-effort orchestration + partial receipt with per-item ok/error; no rollback
  • dry_run returns would-be receipt, writes nothing
  • Effective-classification resolution via injected ClassificationPolicy (stricter wins)
  • Unit tests against fake ports: write+supersede, partial-failure, dry-run, validation-reject, stricter-classification-wins
  • Existing MCP brain_update/brain_get tests still pass after re-pointing
  • task check green

Notes

No HTTP, no live gitea, no live audit in this issue — all injected as interfaces, real impls land in #49c/d/e/f. The value here is a fully unit-tested use-case before any I/O is wired. Gherkin scenarios this satisfies (against fakes): supersede-not-duplicate, fail-closed validation, partial-failure receipt, dry-run, stricter-classification-wins.

**Parent:** #49 · **Spec:** `specs/capture-bdd-spec.md` §1 · **Depends on:** nothing (buildable against fake ports) · **Blocks:** #49c, #49d ## Scope The Clean-Architecture core of capture: extract #45's brain write/update/get logic out of the MCP-only layer into a shared `BrainStore`, define the ports and entities, and implement the `CaptureService` orchestration. Build and unit-test against **fake ports** — no live gating, no HTTP, no real gitea yet. The classification check is an injected policy (satisfied later by #49a/#50). This is the biggest single chunk and the spine everything else hangs off. ## Ground-truth (from #49) `brain_update`/`brain_get` currently live in `ingestion/internal/mcp/` only; `/write` (`api.WriteNote`, a free function) is the only brain write reachable from the REST/api layer. The extraction lifts the update/get logic into a shared impl both the MCP handlers and capture call — the Clean-Arch payoff, real work, not a wrapper. ## What to build 1. **`ingestion/internal/capture/` package.** 2. **Ports (interfaces):** - `BrainStore` — `Write(ctx, note) (ref, error)`, `Update(ctx, slug, note) (ref, error)`, `Get(ctx, id) (note, error)` where `ref = {id, path, content_hash}`. - `IssueTracker` — `CreateIssue`, `CloseIssue`, `CommentIssue` (interface only here; impl in #49c). - `SummaryWriter` — `WriteFile(repo, path, content) error`. - `ClassificationPolicy` — `Derive(target) level` + `Effective(declared, derived) level` (interface only; impl in #49a/#50). - `AuditSink` — `Record(ctx, auditEntry) error` (interface only; impl in #49e/#49f). 3. **Entities:** `Insight`, `Ticket`, `Summary`, `CaptureContext`, `CaptureReceipt` — plain structs. 4. **`BrainStore` extraction:** lift `brain_update`/`brain_get`/`brain_write` logic from `mcp/` into a concrete `BrainStore` impl; **re-point the existing MCP handlers at it** so there's one implementation, not two (DRY — the whole point). Preserve the #45 read-after-write contract and the batch staleness discipline *inside* the store/use-case so callers never carry the rule. 5. **`CaptureService.Capture(ctx, CaptureInput) (CaptureReceipt, error)`:** validate-before-write (fail-closed), resolve effective classification via the policy port, orchestrate insights (write/supersede) → tickets → summary, best-effort with per-item outcome, assemble the partial-aware receipt. `dry_run` short-circuits after validation. ## Acceptance criteria - [ ] `capture/` package: ports, entities, `CaptureService` - [ ] `BrainStore` concrete impl with Write/Update/Get; **MCP handlers re-pointed at it** (no duplicate logic) - [ ] #45 read-after-write + staleness discipline preserved inside the store/use-case - [ ] Validate-before-write: malformed input → error, nothing written - [ ] Best-effort orchestration + partial receipt with per-item `ok`/`error`; no rollback - [ ] `dry_run` returns would-be receipt, writes nothing - [ ] Effective-classification resolution via injected `ClassificationPolicy` (stricter wins) - [ ] Unit tests against **fake ports**: write+supersede, partial-failure, dry-run, validation-reject, stricter-classification-wins - [ ] Existing MCP `brain_update`/`brain_get` tests still pass after re-pointing - [ ] `task check` green ## Notes No HTTP, no live gitea, no live audit in this issue — all injected as interfaces, real impls land in #49c/d/e/f. The value here is a fully unit-tested use-case before any I/O is wired. Gherkin scenarios this satisfies (against fakes): supersede-not-duplicate, fail-closed validation, partial-failure receipt, dry-run, stricter-classification-wins.
Author
Owner

Implemented — PR #57 (open, stacked on #56)

PR: #57 · base feat/capture-classification (merge #56 first; #57 is #51-only diff)

Two packages:

  • capture/ — pure use-case: ports (BrainStore/IssueTracker/SummaryWriter/ClassificationPolicy/AuditSink), entities, CaptureService.Capture (validate-fail-closed → effective classification → insights/tickets/summary best-effort → audit → partial receipt; dry_run writes nothing).
  • brainstore/ — the extraction: one concrete Store implementing capture.BrainStore (api primitives + wing-index/auto-tunnel/graph). MCP brain_write/brain_update/brain_get re-pointed at it — one impl, not two. Existing MCP tests pass unmodified; {id,path,content_hash} contract preserved.

Soft dep on #50: reuses classification.Level/Target rather than duplicating the enum → branch stacked on #56. classification.Config is a drop-in ClassificationPolicy.

Deliberate scope boundaries:

  • I1 origin gate → #53 (needs server-derived principal).
  • Classification-aware audit degradation/refusal → #54 (port exists, policy layers on).
  • Effective folded into the service as Stricter (port stays Derive-only).
  • Unknown-repo pre-validation is structural here; true repo-existence check lands with the real tracker (#52) — a bad repo currently surfaces as a per-item ticket error, never a silent write.

All ACs met; task check green. BDD scenarios satisfied against fakes: supersede-not-duplicate, fail-closed validation, partial-failure receipt, dry-run, stricter-wins, caller-raising-honoured, summary fidelity.

Critical path next: #52 (Gitea IssueTracker impl — depends on this) → #53 (REST + I1 gate). The IssueTracker/SummaryWriter/AuditSink ports are ready for their real adapters.

## Implemented — PR #57 (open, stacked on #56) **PR:** https://git.d-ma.be/mathias/hyperguild/pulls/57 · base `feat/capture-classification` (merge #56 first; #57 is #51-only diff) Two packages: - **`capture/`** — pure use-case: ports (`BrainStore`/`IssueTracker`/`SummaryWriter`/`ClassificationPolicy`/`AuditSink`), entities, `CaptureService.Capture` (validate-fail-closed → effective classification → insights/tickets/summary best-effort → audit → partial receipt; `dry_run` writes nothing). - **`brainstore/`** — the extraction: one concrete `Store` implementing `capture.BrainStore` (api primitives + wing-index/auto-tunnel/graph). **MCP `brain_write`/`brain_update`/`brain_get` re-pointed at it** — one impl, not two. Existing MCP tests pass unmodified; `{id,path,content_hash}` contract preserved. **Soft dep on #50:** reuses `classification.Level`/`Target` rather than duplicating the enum → branch stacked on #56. `classification.Config` is a drop-in `ClassificationPolicy`. **Deliberate scope boundaries:** - I1 origin gate → **#53** (needs server-derived principal). - Classification-aware audit degradation/refusal → **#54** (port exists, policy layers on). - `Effective` folded into the service as `Stricter` (port stays `Derive`-only). - Unknown-repo pre-validation is structural here; true repo-existence check lands with the real tracker (**#52**) — a bad repo currently surfaces as a per-item ticket error, never a silent write. All ACs met; `task check` green. BDD scenarios satisfied against fakes: supersede-not-duplicate, fail-closed validation, partial-failure receipt, dry-run, stricter-wins, caller-raising-honoured, summary fidelity. **Critical path next:** #52 (Gitea `IssueTracker` impl — depends on this) → #53 (REST + I1 gate). The `IssueTracker`/`SummaryWriter`/`AuditSink` ports are ready for their real adapters.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/hyperguild#51