diff --git a/server/internal/adxtrace/dataset.go b/server/internal/adxtrace/dataset.go index 092fc27cd..1033ea134 100644 --- a/server/internal/adxtrace/dataset.go +++ b/server/internal/adxtrace/dataset.go @@ -11,9 +11,12 @@ import ( "strconv" "time" + "github.com/githubnext/gh-cao/server/internal/logger" "github.com/githubnext/gh-cao/server/internal/model" ) +var datasetLog = logger.New("cao:adxtrace") + func Namespace(endpoint, database string) string { sum := sha256.Sum256([]byte(endpoint + "\x00" + database)) return hex.EncodeToString(sum[:]) @@ -67,32 +70,41 @@ func Inspect(ctx context.Context, config Config, namespace string) (Status, erro } status := Status{Stage: "waiting"} err = s.locked(ctx, func(_ Objects, state *ledger) error { - status.Frames = len(state.Frames) - for _, r := range state.Records { - if r.State == "eligible" { - status.Eligible++ - } else { - status.Gaps++ - } - if r.Delivered == "" { - status.Pending++ - } - if r.Mining != nil && r.Mining.Delivered == "" { - status.Pending++ - } - if r.Mining != nil && (r.Mining.State != "ready" || !time.Now().Before(r.Mining.FreshUntil)) { - status.Gaps++ - } - if config.MiningEnabled && r.Mining == nil && !r.MiningCheckedAt.IsZero() { - status.Gaps++ - } - } - status.Degraded = status.Gaps > 0 + status = summarizeLedger(state, config, time.Now()) return nil }) + if err == nil { + datasetLog.Printf("inspected spool: frames=%d eligible=%d gaps=%d pending=%d degraded=%t", status.Frames, status.Eligible, status.Gaps, status.Pending, status.Degraded) + } return status, err } +// summarizeLedger counts eligible, gap and undelivered items in a ledger. +func summarizeLedger(state *ledger, config Config, now time.Time) Status { + status := Status{Stage: "waiting", Frames: len(state.Frames)} + for _, r := range state.Records { + if r.State == "eligible" { + status.Eligible++ + } else { + status.Gaps++ + } + if r.Delivered == "" { + status.Pending++ + } + if r.Mining != nil && r.Mining.Delivered == "" { + status.Pending++ + } + if r.Mining != nil && (r.Mining.State != "ready" || !now.Before(r.Mining.FreshUntil)) { + status.Gaps++ + } + if config.MiningEnabled && r.Mining == nil && !r.MiningCheckedAt.IsZero() { + status.Gaps++ + } + } + status.Degraded = status.Gaps > 0 + return status +} + // ExportDataset creates a private immutable native-trajectory dataset. It does // not train or replay anything and requires separately supplied quality evidence. func ExportDataset(ctx context.Context, config Config, namespace string, specification []byte) (string, error) { diff --git a/server/internal/adxtrace/summarize_test.go b/server/internal/adxtrace/summarize_test.go new file mode 100644 index 000000000..5979ef894 --- /dev/null +++ b/server/internal/adxtrace/summarize_test.go @@ -0,0 +1,56 @@ +package adxtrace + +import ( + "testing" + "time" +) + +func TestSummarizeLedger(t *testing.T) { + now := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC) + fresh, stale := now.Add(time.Hour), now.Add(-time.Hour) + tests := []struct { + name string + state *ledger + config Config + want Status + }{ + {"empty", &ledger{}, Config{}, Status{Stage: "waiting"}}, + {"delivered eligible record", &ledger{ + Frames: map[string]*frame{"f": {}}, + Records: map[string]*record{"a": {State: "eligible", Delivered: "x"}}, + }, Config{}, Status{Stage: "waiting", Frames: 1, Eligible: 1}}, + {"undelivered gap", &ledger{ + Records: map[string]*record{"a": {State: "excluded"}}, + }, Config{}, Status{Stage: "waiting", Gaps: 1, Pending: 1, Degraded: true}}, + {"fresh ready mining pending", &ledger{ + Records: map[string]*record{"a": {State: "eligible", Delivered: "x", Mining: &miningProjection{State: "ready", FreshUntil: fresh}}}, + }, Config{}, Status{Stage: "waiting", Eligible: 1, Pending: 1}}, + {"stale mining is a gap", &ledger{ + Records: map[string]*record{"a": {State: "eligible", Delivered: "x", Mining: &miningProjection{State: "ready", Delivered: "y", FreshUntil: stale}}}, + }, Config{}, Status{Stage: "waiting", Eligible: 1, Gaps: 1, Degraded: true}}, + {"not ready mining is a gap", &ledger{ + Records: map[string]*record{"a": {State: "eligible", Delivered: "x", Mining: &miningProjection{State: "partial", Delivered: "y", FreshUntil: fresh}}}, + }, Config{}, Status{Stage: "waiting", Eligible: 1, Gaps: 1, Degraded: true}}, + {"checked without mining when enabled", &ledger{ + Records: map[string]*record{"a": {State: "eligible", Delivered: "x", MiningCheckedAt: now}}, + }, Config{MiningEnabled: true}, Status{Stage: "waiting", Eligible: 1, Gaps: 1, Degraded: true}}, + {"checked without mining when disabled", &ledger{ + Records: map[string]*record{"a": {State: "eligible", Delivered: "x", MiningCheckedAt: now}}, + }, Config{}, Status{Stage: "waiting", Eligible: 1}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := summarizeLedger(tt.state, tt.config, now); got != tt.want { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestSummarizeLedgerFreshnessBoundary(t *testing.T) { + now := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC) + state := &ledger{Records: map[string]*record{"a": {State: "eligible", Delivered: "x", Mining: &miningProjection{State: "ready", Delivered: "y", FreshUntil: now}}}} + if got := summarizeLedger(state, Config{}, now); got.Gaps != 1 || !got.Degraded { + t.Fatalf("mining expiring exactly now must be a gap: %+v", got) + } +}