From 3b8bacf114f2d2dc3cab9c2f7dbea451c0740374 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 2 Nov 2026 22:36:35 +0000 Subject: [PATCH] Filter publication record names before sorting directory results --- docs/roadmap/remediation.md | 5 +++++ internal/publish/attempts.go | 36 ++++++++++++++++++++++++++------- internal/publish/record_test.go | 28 +++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 35e965a..733d26c 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -456,6 +456,11 @@ with a controlled error; repository production and test files were unchanged. ## R11: Filter directory entries before ordering them — P3 +**Completed:** Discovery filters unsorted directory batches and sorts selected +record paths once. Directory work is `O(D + R log R)` with bounded intermediate +entry storage. Mixed-directory, ordering, and cancellation regressions passed. +R13 addresses preservation of records alongside enumeration failures. + **Relevant code:** `internal/publish/attempts.go:41` through the subsequent filter and `sort.Strings(paths)`. diff --git a/internal/publish/attempts.go b/internal/publish/attempts.go index dfdcd58..a7a64aa 100644 --- a/internal/publish/attempts.go +++ b/internal/publish/attempts.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "os" "path/filepath" "sort" @@ -40,16 +41,10 @@ func ListAttempts(ctx context.Context, repositoryDir, override string) (Attempts if err := ctx.Err(); err != nil { return result, &RecoveryError{Kind: "canceled", cause: err} } - entries, err := os.ReadDir(directory) + paths, err := recordPaths(ctx, directory) if err != nil { return result, recoveryFailure(ctx, "storage_enumeration", fmt.Errorf("enumerate records in %q: %w", directory, err)) } - paths := make([]string, 0, len(entries)) - for _, entry := range entries { - if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") { - paths = append(paths, filepath.Join(directory, entry.Name())) - } - } sort.Strings(paths) for _, path := range paths { if err := ctx.Err(); err != nil { @@ -73,6 +68,33 @@ func ListAttempts(ctx context.Context, repositoryDir, override string) (Attempts return result, nil } +// recordPaths filters unsorted batches, retaining only selected record names. +func recordPaths(ctx context.Context, directory string) ([]string, error) { + file, err := os.Open(directory) + if err != nil { + return nil, err + } + defer file.Close() + var paths []string + for { + if err := ctx.Err(); err != nil { + return paths, err + } + entries, err := file.ReadDir(128) + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") { + paths = append(paths, filepath.Join(directory, entry.Name())) + } + } + if errors.Is(err, io.EOF) { + return paths, nil + } + if err != nil { + return paths, err + } + } +} + func recordDirectoryFor(ctx context.Context, repositoryDir, override string) (string, error) { if filepath.IsAbs(override) { return filepath.Clean(override), nil diff --git a/internal/publish/record_test.go b/internal/publish/record_test.go index aef1037..2617fd1 100644 --- a/internal/publish/record_test.go +++ b/internal/publish/record_test.go @@ -371,6 +371,34 @@ func TestListAttemptsReturnsReadableRecordsAndDiagnosticsInPathOrder(t *testing. } } +func TestListAttemptsFiltersMixedDirectoryAcrossBatches(t *testing.T) { + directory := t.TempDir() + for i := range 400 { + if err := os.WriteFile(filepath.Join(directory, fmt.Sprintf("unrelated-%04d.txt", i)), []byte("not a record"), 0600); err != nil { + t.Fatal(err) + } + } + if err := os.Mkdir(filepath.Join(directory, "directory.json"), 0700); err != nil { + t.Fatal(err) + } + names := []string{"z.json", "b.json", "a.json"} + for _, name := range names { + if err := os.WriteFile(filepath.Join(directory, name), []byte(`{"format_version":1,"evidence":{"tag":"v1.2.3"}}`), 0600); err != nil { + t.Fatal(err) + } + } + listed, err := ListAttempts(t.Context(), "", directory) + if err != nil || len(listed.Diagnostics) != 0 || len(listed.Attempts) != len(names) { + t.Fatalf("filtered result = %#v, %v", listed, err) + } + sort.Strings(names) + for i, name := range names { + if listed.Attempts[i].RecordPath != filepath.Join(directory, name) { + t.Fatalf("incorrect path order: %#v", listed.Attempts) + } + } +} + func TestListAttemptsDistinguishesEmptyMissingAndCanceledStorage(t *testing.T) { empty := t.TempDir() listed, err := ListAttempts(t.Context(), "", empty)