feat(capture): CaptureService use-case + ports + entities (#51)
The Clean-Architecture core of the capture capability (#49b). Pure orchestration over ports — no HTTP, no live Gitea, no audit I/O — fully unit-tested against fakes before any adapter exists. - Ports: BrainStore (#45 write/update/get), IssueTracker, SummaryWriter, ClassificationPolicy (satisfied by #50's classification.Config), AuditSink. Entities: Insight, Ticket, Summary, CaptureContext, CaptureInput, CaptureReceipt. - CaptureService.Capture: validate-before-write (fail-closed), resolve effective classification (stricter of declared vs target-derived; under-declaration logged as a security event), orchestrate insights (write/supersede) → tickets → summary best-effort, emit a request-level audit record of exactly what landed, return a partial-aware receipt. - dry_run short-circuits after validation, writes nothing (not even audit). Out of scope here, layered on later: the I1 origin sovereignty gate (#53, needs the server-derived principal) and the classification-aware audit degradation/refusal (#54). "Effective" is folded into the service as classification.Stricter rather than a port method — the stricter-wins rule is use-case policy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/classification"
|
||||
)
|
||||
|
||||
// Ref is the read-after-write handle returned by a brain write/update —
|
||||
// the #45 contract. ContentHash lets the caller confirm what landed
|
||||
// without a re-query; for an Update, Superseded is true.
|
||||
type Ref struct {
|
||||
ID string
|
||||
Path string
|
||||
ContentHash string
|
||||
Superseded bool
|
||||
}
|
||||
|
||||
// StoredNote is a brain note fetched by Get: the read-after-write
|
||||
// confirmation primitive (a direct fetch, never a semantic query).
|
||||
type StoredNote struct {
|
||||
ID string
|
||||
Path string
|
||||
ContentHash string
|
||||
Frontmatter map[string]string
|
||||
Body string
|
||||
}
|
||||
|
||||
// Note is the brain-write payload. It carries both the wing/hall taxonomy
|
||||
// and the legacy type/domain fields so a single BrainStore serves both
|
||||
// capture insights and the existing MCP brain_write surface. Reason is
|
||||
// the supersede rationale, used only by Update.
|
||||
type Note struct {
|
||||
Content string
|
||||
Filename string
|
||||
Wing string
|
||||
Hall string
|
||||
Type string
|
||||
Domain string
|
||||
Reason string
|
||||
}
|
||||
|
||||
// BrainStore is the brain persistence port — the shared implementation of
|
||||
// the #45 write/update/get verbs that both the MCP handlers and capture
|
||||
// call, so there is one implementation, not two. The read-after-write +
|
||||
// staleness discipline lives behind this interface so no caller carries
|
||||
// the rule.
|
||||
type BrainStore interface {
|
||||
Write(ctx context.Context, n Note) (Ref, error)
|
||||
Update(ctx context.Context, slug string, n Note) (Ref, error)
|
||||
Get(ctx context.Context, id string) (StoredNote, error)
|
||||
}
|
||||
|
||||
// IssueRef identifies a ticket touched by the tracker.
|
||||
type IssueRef struct {
|
||||
Repo string
|
||||
Number int
|
||||
URL string
|
||||
}
|
||||
|
||||
// IssueTracker is the Gitea ticket port. The implementation (#52) always
|
||||
// scopes to owner "mathias"; the port deliberately omits owner.
|
||||
type IssueTracker interface {
|
||||
CreateIssue(ctx context.Context, repo, title, body string) (IssueRef, error)
|
||||
CloseIssue(ctx context.Context, repo string, number int) (IssueRef, error)
|
||||
CommentIssue(ctx context.Context, repo string, number int, body string) (IssueRef, error)
|
||||
}
|
||||
|
||||
// SummaryWriter is the ai-sessions summary port.
|
||||
type SummaryWriter interface {
|
||||
WriteFile(ctx context.Context, repo, path, content string) error
|
||||
}
|
||||
|
||||
// ClassificationPolicy derives a target's sensitivity (model C). The
|
||||
// "stricter wins" combination of declared vs derived is use-case policy
|
||||
// and lives in the service, so the port stays minimal. Satisfied by
|
||||
// classification.Config (#50).
|
||||
type ClassificationPolicy interface {
|
||||
Derive(target classification.Target) classification.Level
|
||||
}
|
||||
|
||||
// AuditEntry is the request-level audit record (I5): who/what captured
|
||||
// what, when, via which principal. SecurityEvents carries anomalies such
|
||||
// as a caller under-declaring sensitivity relative to the target floor.
|
||||
type AuditEntry struct {
|
||||
Timestamp time.Time
|
||||
Principal string
|
||||
Actor string
|
||||
Harness string
|
||||
SessionRef string
|
||||
EffectiveClassification string
|
||||
Items []string
|
||||
SecurityEvents []string
|
||||
}
|
||||
|
||||
// AuditSink records the audit entry. The classification-aware
|
||||
// degradation/refusal policy (confidential fails closed, internal
|
||||
// degrades) is the caller's concern in #54; this port just records.
|
||||
type AuditSink interface {
|
||||
Record(ctx context.Context, e AuditEntry) error
|
||||
}
|
||||
Reference in New Issue
Block a user