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.
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.
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.
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.
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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Parent: #49 · Spec:
specs/capture-bdd-spec.md§1 · Depends on: nothing (buildable against fake ports) · Blocks: #49c, #49dScope
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 theCaptureServiceorchestration. 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_getcurrently live iningestion/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
ingestion/internal/capture/package.BrainStore—Write(ctx, note) (ref, error),Update(ctx, slug, note) (ref, error),Get(ctx, id) (note, error)whereref = {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).Insight,Ticket,Summary,CaptureContext,CaptureReceipt— plain structs.BrainStoreextraction: liftbrain_update/brain_get/brain_writelogic frommcp/into a concreteBrainStoreimpl; 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.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_runshort-circuits after validation.Acceptance criteria
capture/package: ports, entities,CaptureServiceBrainStoreconcrete impl with Write/Update/Get; MCP handlers re-pointed at it (no duplicate logic)ok/error; no rollbackdry_runreturns would-be receipt, writes nothingClassificationPolicy(stricter wins)brain_update/brain_gettests still pass after re-pointingtask checkgreenNotes
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.
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_runwrites nothing).brainstore/— the extraction: one concreteStoreimplementingcapture.BrainStore(api primitives + wing-index/auto-tunnel/graph). MCPbrain_write/brain_update/brain_getre-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/Targetrather than duplicating the enum → branch stacked on #56.classification.Configis a drop-inClassificationPolicy.Deliberate scope boundaries:
Effectivefolded into the service asStricter(port staysDerive-only).All ACs met;
task checkgreen. 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
IssueTrackerimpl — depends on this) → #53 (REST + I1 gate). TheIssueTracker/SummaryWriter/AuditSinkports are ready for their real adapters.