diff --git a/internal/session/session.go b/internal/session/session.go index adfb3fb..7e4428b 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -115,10 +115,20 @@ func LoadOverview(root string) Overview { names = append(names, e.Name()) } } - sort.Strings(names) for _, n := range names { ov.Specs = append(ov.Specs, buildCard(specsDir, n)) } + // Newest spec first, by spec.json created_at (an RFC3339 UTC string, so a + // lexical compare is chronological). Ties — and unreadable specs, whose + // CreatedAt is empty — fall back to feature name ascending for a stable + // order. + sort.Slice(ov.Specs, func(i, j int) bool { + a, b := ov.Specs[i], ov.Specs[j] + if a.CreatedAt != b.CreatedAt { + return a.CreatedAt > b.CreatedAt + } + return a.Feature < b.Feature + }) } ov.Steering = listSteering(paths.Steering(root), root) ov.Skills = listSkills(paths.Skills(root), root) diff --git a/internal/session/session_test.go b/internal/session/session_test.go index 8b29f40..d8de82f 100644 --- a/internal/session/session_test.go +++ b/internal/session/session_test.go @@ -56,7 +56,7 @@ func TestOverview(t *testing.T) { if len(ov.Specs) != 2 { t.Fatalf("specs = %d, want 2", len(ov.Specs)) } - // Sorted alphabetically: photo-albums then zebra. + // Ordered newest-created first: photo-albums (dated) before zebra (no created_at). pa := ov.Specs[0] if pa.Feature != "photo-albums" { t.Fatalf("first spec = %q, want photo-albums", pa.Feature) @@ -194,6 +194,54 @@ func TestOverviewMalformedSpecJSON(t *testing.T) { } } +func TestOverviewSpecsSortedByCreatedAtDesc(t *testing.T) { + // Three readable specs whose creation dates differ from alphabetical order, + // so the assertion distinguishes created-at ordering from name ordering. + spec := func(name, createdAt string) string { + return `{"feature_name":"` + name + `","phase":"requirements-generated","approvals":{},"created_at":"` + createdAt + `"}` + } + root := writeWorkspace(t, map[string]string{ + "specs/alpha/spec.json": spec("alpha", "2025-01-01T00:00:00Z"), + "specs/beta/spec.json": spec("beta", "2025-06-01T00:00:00Z"), + "specs/gamma/spec.json": spec("gamma", "2025-03-01T00:00:00Z"), + }) + + ov := LoadOverview(root) + + var got []string + for _, s := range ov.Specs { + got = append(got, s.Feature) + } + // Newest created_at first: beta (Jun) → gamma (Mar) → alpha (Jan). + want := []string{"beta", "gamma", "alpha"} + if strings.Join(got, ",") != strings.Join(want, ",") { + t.Errorf("spec order = %v, want %v", got, want) + } +} + +func TestOverviewSpecsCreatedAtTieBreaksByName(t *testing.T) { + // Equal created_at (and the empty-created_at unreadable case) must fall back + // to a deterministic name-ascending order. + root := writeWorkspace(t, map[string]string{ + "specs/bravo/spec.json": `{"feature_name":"bravo","approvals":{},"created_at":"2025-01-01T00:00:00Z"}`, + "specs/alpha/spec.json": `{"feature_name":"alpha","approvals":{},"created_at":"2025-01-01T00:00:00Z"}`, + "specs/zulu/spec.json": `{ broken`, + "specs/yank/spec.json": `{ broken`, + }) + + ov := LoadOverview(root) + + var got []string + for _, s := range ov.Specs { + got = append(got, s.Feature) + } + // Dated pair (tie) name-ascending first, then empty-created_at pair name-ascending. + want := []string{"alpha", "bravo", "yank", "zulu"} + if strings.Join(got, ",") != strings.Join(want, ",") { + t.Errorf("spec order = %v, want %v", got, want) + } +} + func TestSpecDetailValidation(t *testing.T) { // A leaf task missing _Requirements:_ must surface as a validator issue. root := writeWorkspace(t, map[string]string{