feat(capture): remove summary->ai-sessions write path
ai-sessions#13: capture's `summary` field wrote a frontmatter shape (title/harness/fidelity/captured_at/repos_touched) that ai-sessions' own extract/audit pipeline can't parse -- silently invisible to that repo's own audit tooling, and bypassing its redaction + Stage2 completeness gates by construction. Only 5 files ever landed this way over 3 weeks; the summary capability's whole value (speed) fights ai-sessions' whole value (redacted, audited, complete), so kill it rather than build a second parse branch. capture now persists insights -> brain and action items -> Gitea tickets only. Removes Summary/SummaryResult/SummaryWriter and all wiring (service, REST body, MCP tool schema); close-session updated to stop assembling a summary payload. specs/capture-*.md kept as historical record with a superseded note -- the feature shipped and is documented, just no longer current behaviour. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPdSHbp9Utb2hPFm9wDG59
This commit is contained in:
@@ -2,8 +2,6 @@ package capture
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -14,22 +12,19 @@ import (
|
||||
|
||||
// Service is the CaptureSession use-case. It depends only on ports.
|
||||
type Service struct {
|
||||
brain BrainStore
|
||||
issues IssueTracker
|
||||
summaries SummaryWriter
|
||||
policy ClassificationPolicy
|
||||
audit AuditSink
|
||||
brain BrainStore
|
||||
issues IssueTracker
|
||||
policy ClassificationPolicy
|
||||
audit AuditSink
|
||||
|
||||
// now is the clock, injectable for deterministic summary paths and
|
||||
// audit timestamps in tests.
|
||||
// now is the clock, injectable for deterministic audit timestamps in
|
||||
// tests.
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewService constructs a Service from its ports. summaries may be nil
|
||||
// when no summary persistence is wired; a CaptureInput with a Summary
|
||||
// then fails that item rather than panicking.
|
||||
func NewService(b BrainStore, tr IssueTracker, sw SummaryWriter, p ClassificationPolicy, a AuditSink) *Service {
|
||||
return &Service{brain: b, issues: tr, summaries: sw, policy: p, audit: a, now: time.Now}
|
||||
// NewService constructs a Service from its ports.
|
||||
func NewService(b BrainStore, tr IssueTracker, p ClassificationPolicy, a AuditSink) *Service {
|
||||
return &Service{brain: b, issues: tr, policy: p, audit: a, now: time.Now}
|
||||
}
|
||||
|
||||
var validActions = map[string]bool{"create": true, "close": true, "comment": true}
|
||||
@@ -131,9 +126,6 @@ func (s *Service) Capture(ctx context.Context, in CaptureInput) (CaptureReceipt,
|
||||
for _, tk := range in.Tickets {
|
||||
receipt.Tickets = append(receipt.Tickets, TicketResult{Repo: tk.Repo, Action: tk.Action, Number: tk.Number, OK: true})
|
||||
}
|
||||
if in.Summary != nil {
|
||||
receipt.Summary = &SummaryResult{Path: s.summaryPath(in.Context, in.Summary), OK: true}
|
||||
}
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
@@ -169,16 +161,6 @@ func (s *Service) Capture(ctx context.Context, in CaptureInput) (CaptureReceipt,
|
||||
landed = append(landed, fmt.Sprintf("ticket:%s#%d", tk.Repo, res.Number))
|
||||
}
|
||||
|
||||
if in.Summary != nil {
|
||||
res, err := s.persistSummary(ctx, in.Context, in.Summary)
|
||||
receipt.Summary = &res
|
||||
if err != nil {
|
||||
receipt.Errors = append(receipt.Errors, ItemError{Item: "summary", Error: err.Error()})
|
||||
} else {
|
||||
landed = append(landed, "summary:"+res.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// I5: persist the request-level audit record of exactly what landed,
|
||||
// using the outcome reserved before the writes. AuditBuffered surfaces
|
||||
// the degraded (locally-buffered) state on the receipt.
|
||||
@@ -259,11 +241,6 @@ func (s *Service) resolveClassification(declared classification.Level, in Captur
|
||||
for _, tk := range in.Tickets {
|
||||
consider(classification.RepoTarget, tk.Repo)
|
||||
}
|
||||
if in.Summary != nil {
|
||||
for _, repo := range in.Summary.ReposTouched {
|
||||
consider(classification.RepoTarget, repo)
|
||||
}
|
||||
}
|
||||
return effective, events
|
||||
}
|
||||
|
||||
@@ -309,60 +286,6 @@ func (s *Service) persistTicket(ctx context.Context, tk Ticket) (TicketResult, e
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) persistSummary(ctx context.Context, c CaptureContext, sum *Summary) (SummaryResult, error) {
|
||||
if s.summaries == nil {
|
||||
return SummaryResult{OK: false}, fmt.Errorf("no summary writer configured")
|
||||
}
|
||||
path := s.summaryPath(c, sum)
|
||||
content := s.renderSummary(c, sum)
|
||||
repo := "ai-sessions"
|
||||
if err := s.summaries.WriteFile(ctx, repo, path, content); err != nil {
|
||||
return SummaryResult{Path: path, OK: false}, err
|
||||
}
|
||||
return SummaryResult{Path: path, OK: true}, nil
|
||||
}
|
||||
|
||||
// summaryPath builds summaries/<harness>/<YYYY-MM>/<date>-<slug>-<ref8>.md.
|
||||
// The ref8 disambiguator is derived from the session_ref (or the title
|
||||
// when no ref is present) so distinct sessions never collide.
|
||||
func (s *Service) summaryPath(c CaptureContext, sum *Summary) string {
|
||||
t := s.now().UTC()
|
||||
slug := brain.Sanitise(sum.Title)
|
||||
if slug == "" {
|
||||
slug = "summary"
|
||||
}
|
||||
seed := c.SessionRef
|
||||
if seed == "" {
|
||||
seed = sum.Title + sum.Body
|
||||
}
|
||||
sum8 := shortHash(seed)
|
||||
return fmt.Sprintf("summaries/%s/%s/%s-%s-%s.md",
|
||||
brain.Sanitise(c.Harness), t.Format("2006-01"), t.Format("2006-01-02"), slug, sum8)
|
||||
}
|
||||
|
||||
// renderSummary stamps fidelity + session metadata into frontmatter so the
|
||||
// richer-fidelity-supersedes-thinner collision rule has the data it needs.
|
||||
func (s *Service) renderSummary(c CaptureContext, sum *Summary) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("---\n")
|
||||
fmt.Fprintf(&b, "title: %s\n", sum.Title)
|
||||
fmt.Fprintf(&b, "harness: %s\n", c.Harness)
|
||||
if c.SessionRef != "" {
|
||||
fmt.Fprintf(&b, "session_ref: %s\n", c.SessionRef)
|
||||
}
|
||||
fmt.Fprintf(&b, "fidelity: %s\n", c.Fidelity)
|
||||
fmt.Fprintf(&b, "captured_at: %s\n", s.now().UTC().Format(time.RFC3339))
|
||||
if len(sum.ReposTouched) > 0 {
|
||||
fmt.Fprintf(&b, "repos_touched: [%s]\n", strings.Join(sum.ReposTouched, ", "))
|
||||
}
|
||||
b.WriteString("---\n\n")
|
||||
b.WriteString(sum.Body)
|
||||
if !strings.HasSuffix(sum.Body, "\n") {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func kindString(k classification.TargetKind) string {
|
||||
if k == classification.RepoTarget {
|
||||
return "repo"
|
||||
@@ -381,8 +304,3 @@ func firstLine(s string) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func shortHash(s string) string {
|
||||
sum := sha256.Sum256([]byte(s))
|
||||
return hex.EncodeToString(sum[:])[:8]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user