feat(artwork): add a resolution chain trace collector

This commit is contained in:
Deluan 2026-08-14 12:15:21 -04:00
commit bda549f8a8
2 changed files with 97 additions and 0 deletions

54
core/artwork/trace.go Normal file
View file

@ -0,0 +1,54 @@
package artwork
import (
"context"
"slices"
"sync"
)
const (
outcomeHit = "hit"
outcomeMiss = "miss"
outcomeUnreadable = "unreadable"
outcomeSkipped = "skipped"
outcomeWouldTry = "would-try"
outcomeError = "error"
outcomeNotReached = "not-reached"
)
// traceStep is one candidate the priority chain considered.
type traceStep struct {
Candidate string
Outcome string
Detail string
}
// chainTrace collects the walk of a single resolution. The artwork worker never attaches
// one; only the CLI does, so resolution stays allocation-free in the hot path.
type chainTrace struct {
mu sync.Mutex
steps []traceStep
}
func (t *chainTrace) add(step traceStep) {
t.mu.Lock()
defer t.mu.Unlock()
t.steps = append(t.steps, step)
}
func (t *chainTrace) Steps() []traceStep {
t.mu.Lock()
defer t.mu.Unlock()
return slices.Clone(t.steps)
}
type traceCtxKey struct{}
func withTrace(ctx context.Context, t *chainTrace) context.Context {
return context.WithValue(ctx, traceCtxKey{}, t)
}
func traceFrom(ctx context.Context) *chainTrace {
t, _ := ctx.Value(traceCtxKey{}).(*chainTrace)
return t
}

View file

@ -0,0 +1,43 @@
package artwork
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("chainTrace", func() {
It("returns nil when no trace is attached", func() {
Expect(traceFrom(context.Background())).To(BeNil())
})
It("collects steps in order", func() {
t := &chainTrace{}
ctx := withTrace(context.Background(), t)
traceFrom(ctx).add(traceStep{Candidate: "cover.*", Outcome: outcomeMiss})
traceFrom(ctx).add(traceStep{Candidate: "embedded", Outcome: outcomeHit, Detail: "/music/a.flac"})
Expect(t.Steps()).To(Equal([]traceStep{
{Candidate: "cover.*", Outcome: outcomeMiss},
{Candidate: "embedded", Outcome: outcomeHit, Detail: "/music/a.flac"},
}))
})
It("is safe to use concurrently", func() {
t := &chainTrace{}
done := make(chan struct{})
for range 10 {
go func() {
defer GinkgoRecover()
t.add(traceStep{Candidate: "x", Outcome: outcomeMiss})
done <- struct{}{}
}()
}
for range 10 {
<-done
}
Expect(t.Steps()).To(HaveLen(10))
})
})