You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/git"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/jobs"
|
||||
)
|
||||
|
||||
// maxDiffChars bounds the diffstat fed to the LLM so a huge commit can't blow the
|
||||
// prompt budget.
|
||||
const maxDiffChars = 12000
|
||||
|
||||
// WorkspaceLocator resolves the on-disk directory holding a commit's files.
|
||||
type WorkspaceLocator interface {
|
||||
LocateWorkspace(ctx context.Context, wsID uuid.UUID) (mainPath string, err error)
|
||||
LocateWorktree(ctx context.Context, worktreeID uuid.UUID) (path string, err error)
|
||||
}
|
||||
|
||||
// Summarizer turns a diffstat into a markdown summary. Implemented in app.go over
|
||||
// the llm registry (default chat endpoint); the SummaryRunner only sees this
|
||||
// narrow contract so tests can inject a canned-output fake.
|
||||
type Summarizer interface {
|
||||
// Summarize returns (title, bodyMarkdown, modelID, error).
|
||||
Summarize(ctx context.Context, branch, commitSHA, diff string) (title, bodyMD, model string, err error)
|
||||
}
|
||||
|
||||
// SummaryRunner is the jobs.Handler that generates a commit_summaries row. It is
|
||||
// event-driven (enqueued post-commit), never a periodic ticker.
|
||||
type SummaryRunner struct {
|
||||
repo Repository
|
||||
locator WorkspaceLocator
|
||||
gitr git.Runner
|
||||
summ Summarizer
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewSummaryRunner constructs a SummaryRunner. log may be nil.
|
||||
func NewSummaryRunner(repo Repository, locator WorkspaceLocator, gitr git.Runner, summ Summarizer, log *slog.Logger) *SummaryRunner {
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
}
|
||||
return &SummaryRunner{repo: repo, locator: locator, gitr: gitr, summ: summ, log: log}
|
||||
}
|
||||
|
||||
var _ jobs.Handler = (*SummaryRunner)(nil)
|
||||
|
||||
// Type returns the job type this handler processes.
|
||||
func (h *SummaryRunner) Type() jobs.JobType { return JobTypeCommitSummary }
|
||||
|
||||
// Handle generates and upserts a commit summary. Idempotent: a completed summary
|
||||
// for the same (workspace, commit, kind) is left untouched.
|
||||
func (h *SummaryRunner) Handle(ctx context.Context, job jobs.Job) error {
|
||||
var p summaryPayload
|
||||
if err := json.Unmarshal(job.Payload, &p); err != nil {
|
||||
return jobs.Permanent(fmt.Errorf("docs.commit_summary: bad payload: %w", err))
|
||||
}
|
||||
|
||||
// Skip if a completed summary already exists (idempotent re-enqueue).
|
||||
if existing, err := h.repo.Get(ctx, p.WorkspaceID, p.CommitSHA, KindCommit); err == nil && existing != nil && existing.Status == StatusCompleted {
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := ""
|
||||
if p.WorktreeID != nil {
|
||||
path, lerr := h.locator.LocateWorktree(ctx, *p.WorktreeID)
|
||||
if lerr != nil {
|
||||
return fmt.Errorf("locate worktree: %w", lerr)
|
||||
}
|
||||
dir = path
|
||||
} else {
|
||||
path, lerr := h.locator.LocateWorkspace(ctx, p.WorkspaceID)
|
||||
if lerr != nil {
|
||||
return fmt.Errorf("locate workspace: %w", lerr)
|
||||
}
|
||||
dir = path
|
||||
}
|
||||
|
||||
diff, err := h.gitr.DiffStat(ctx, dir, p.CommitSHA)
|
||||
if err != nil {
|
||||
return fmt.Errorf("git show --stat: %w", err)
|
||||
}
|
||||
if len(diff) > maxDiffChars {
|
||||
diff = diff[:maxDiffChars] + "\n...[truncated]"
|
||||
}
|
||||
|
||||
title, body, model, serr := h.summ.Summarize(ctx, p.Branch, p.CommitSHA, diff)
|
||||
if serr != nil {
|
||||
// Persist a failed row so the failure is visible, then return for retry.
|
||||
msg := serr.Error()
|
||||
if _, uerr := h.repo.Upsert(ctx, UpsertInput{
|
||||
WorkspaceID: p.WorkspaceID, WorktreeID: p.WorktreeID, Branch: p.Branch,
|
||||
CommitSHA: p.CommitSHA, Kind: KindCommit, BodyMD: "",
|
||||
Status: StatusFailed, Error: &msg,
|
||||
}); uerr != nil {
|
||||
h.log.Error("docs.summary.upsert_failed_row", "commit", p.CommitSHA, "err", uerr.Error())
|
||||
}
|
||||
return fmt.Errorf("summarize: %w", serr)
|
||||
}
|
||||
|
||||
var titlePtr, modelPtr *string
|
||||
if t := strings.TrimSpace(title); t != "" {
|
||||
titlePtr = &t
|
||||
}
|
||||
if m := strings.TrimSpace(model); m != "" {
|
||||
modelPtr = &m
|
||||
}
|
||||
if _, err := h.repo.Upsert(ctx, UpsertInput{
|
||||
WorkspaceID: p.WorkspaceID, WorktreeID: p.WorktreeID, Branch: p.Branch,
|
||||
CommitSHA: p.CommitSHA, Kind: KindCommit, Title: titlePtr,
|
||||
BodyMD: body, Model: modelPtr, Status: StatusCompleted,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("upsert summary: %w", err)
|
||||
}
|
||||
h.log.Info("docs.summary.completed", "workspace_id", p.WorkspaceID, "commit", p.CommitSHA)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user