Tracker v1 (ports.Tracker + backend/internal/adapters/tracker/github) has been on a shelf since PR #36 — zero callers in backend/internal/{daemon,service,session_manager,lifecycle,cdc,cli} (grep confirms). #35 tracks the full observer loop. Before that observer can be built, two pieces of scaffolding need to land so the work can be done in small reviewable chunks instead of one large PR.
Part 1 — ports.TrackerObservation DTO + lifecycle.Manager.ApplyTrackerFacts
Mirror the SCM lane shape:
ports.TrackerObservation carrying Fetched bool, ObservedAt time.Time, Provider/Host/Repo, the normalized issue facts (state, assignee, title, comments), and a Changed TrackerChanged{State, Assignee, Comments} discriminator. See backend/internal/ports/scm_observations.go:54-89 as the template.
lifecycle.Manager.ApplyTrackerFacts(ctx, sessionID, ports.TrackerObservation) error, mirroring ApplySCMObservation at backend/internal/lifecycle/reactions.go:77-82. Initial reactions: issue closed externally → MarkTerminated; assignee changed away from AO → log only (no reaction yet); new bot mention in a comment → nudge once.
- Unit tests in
backend/internal/lifecycle/manager_test.go covering each reaction branch.
Landing this scaffolding alone, with no observer wired, is intentional: it locks the contract so adapter authors (and #35's observer) have something concrete to satisfy.
Part 2 — Extract shared observer skeleton
Today backend/internal/observe/scm/observer.go carries ~1185 LOC and several pieces are observer-pattern-general, not SCM-specific:
- The
Start(ctx) <-chan struct{} + loop (lines 161-184) — immediate first poll inside the goroutine, ticker, ctx-done exit.
- The optional
credentialChecker interface + checkCredentials (lines 60-62, 400-424) — lazy first-poll credential gate with disabled flag.
- The bounded
ObserverCache shape + FIFO eviction helpers (lines 78-113, 1133-1186) — cacheSetString, cacheSetTime, cacheSetBool, evictStrings. Three near-identical helpers; a generic cacheSet[V any] collapses them.
Extracting these into backend/internal/observe/observer.go (or backend/internal/observe/runtime/) lets the tracker observer reuse the same shape. Without this, #35 will fork a near-copy of observe/scm/observer.go and the two will drift on every fix.
Acceptance
Why scope this together
The DTO + reducer is small but useless without the observer; the skeleton extraction is mechanical but only worth doing once we have a second consumer (Tracker) on the horizon. Doing both in one PR keeps the diff focused on "make Tracker observer cheap to build."
Related
Tracker v1 (
ports.Tracker+backend/internal/adapters/tracker/github) has been on a shelf since PR #36 — zero callers inbackend/internal/{daemon,service,session_manager,lifecycle,cdc,cli}(grep confirms). #35 tracks the full observer loop. Before that observer can be built, two pieces of scaffolding need to land so the work can be done in small reviewable chunks instead of one large PR.Part 1 —
ports.TrackerObservationDTO +lifecycle.Manager.ApplyTrackerFactsMirror the SCM lane shape:
ports.TrackerObservationcarryingFetched bool,ObservedAt time.Time,Provider/Host/Repo, the normalized issue facts (state, assignee, title, comments), and aChanged TrackerChanged{State, Assignee, Comments}discriminator. Seebackend/internal/ports/scm_observations.go:54-89as the template.lifecycle.Manager.ApplyTrackerFacts(ctx, sessionID, ports.TrackerObservation) error, mirroringApplySCMObservationatbackend/internal/lifecycle/reactions.go:77-82. Initial reactions: issue closed externally → MarkTerminated; assignee changed away from AO → log only (no reaction yet); new bot mention in a comment → nudge once.backend/internal/lifecycle/manager_test.gocovering each reaction branch.Landing this scaffolding alone, with no observer wired, is intentional: it locks the contract so adapter authors (and #35's observer) have something concrete to satisfy.
Part 2 — Extract shared observer skeleton
Today
backend/internal/observe/scm/observer.gocarries ~1185 LOC and several pieces are observer-pattern-general, not SCM-specific:Start(ctx) <-chan struct{}+loop(lines 161-184) — immediate first poll inside the goroutine, ticker, ctx-done exit.credentialCheckerinterface +checkCredentials(lines 60-62, 400-424) — lazy first-poll credential gate with disabled flag.ObserverCacheshape + FIFO eviction helpers (lines 78-113, 1133-1186) —cacheSetString,cacheSetTime,cacheSetBool,evictStrings. Three near-identical helpers; a genericcacheSet[V any]collapses them.Extracting these into
backend/internal/observe/observer.go(orbackend/internal/observe/runtime/) lets the tracker observer reuse the same shape. Without this, #35 will fork a near-copy ofobserve/scm/observer.goand the two will drift on every fix.Acceptance
ports.TrackerObservation+TrackerChangedexist with field-level documentation matching the SCM DTO style.lifecycle.Manager.ApplyTrackerFactsexists as a working reducer with the three initial reactions covered by unit tests.go build ./... && go test -race ./...clean.Why scope this together
The DTO + reducer is small but useless without the observer; the skeleton extraction is mechanical but only worth doing once we have a second consumer (Tracker) on the horizon. Doing both in one PR keeps the diff focused on "make Tracker observer cheap to build."
Related