Files
biggus-dickus/docs/superpowers/plans/2026-06-08-search-row-date.md

9.8 KiB
Raw Permalink Blame History

Search-Result Date Meta + Estimated-Count Copy — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Thread recording_date through the search index/hit/view (Rust), show it on the result row, and soften the estimated-count copy.

Architecture: Backend adds recording_date: Option<String> (YYYY-MM-DD) to SearchDocument (projected in build_document), SearchHit (mapped in search_objects), and SearchHitView (API). The frontend type comes from the regenerated/edited schema.d.ts; the row renders the date and the count copy gains a ~.

Tech Stack: Rust (search + api crates, Meilisearch, utoipa), React + TS + pnpm. Tests need the docker stack (Postgres :5442, Meilisearch :7700 — already up). Rust test runner: cargo nextest. Frontend: pnpm test.

Conventions: cargo +nightly fmt + cargo clippy --workspace --all-targets -- -D warnings for Rust; pnpm + no any/eslint-disable + en/sv parity for web; no codename; manage deps via cargo-mcp if any (none here). Run a single test pass.

Spec: docs/superpowers/specs/2026-06-08-search-row-date-design.md

Key facts:

  • crates/search/src/lib.rs: SearchDocument (:30), SearchHit (:52), build_document (:302, returns the SearchDocument { … } literal around :363), search_objects hit map (~:220). domain::Date Display = ISO YYYY-MM-DD; CatalogueObject.recording_date: Option<Date>.
  • crates/api/src/admin_search.rs: SearchHitView (:26) + map closure (:100).
  • crates/search/tests/search.rs: a doc(...) helper builds a SearchDocument literal; search_objects_returns_hits_… (:55) seeds 3 docs + asserts. tests/sync.rs/tests/reindex.rs construct CatalogueObject (already has recording_date — unaffected) but may also build SearchDocument — check.
  • web/src/api/schema.d.ts SearchHitView block (:601): keys are alphabetized — insert recording_date?: string | null; between object_number and snippet.
  • web/src/search/search-result-row.tsx: meta line <span>{hit.object_number}</span> <VisibilityBadge/>.
  • i18n search.resultCount_one/_other; web/src/test/fixtures.ts searchHits; web/src/search/search.test.tsx asserts /25 results/i.

Task 1: Backend — recording_date through index → hit → view (+ schema)

Files: crates/search/src/lib.rs, crates/api/src/admin_search.rs, crates/search/tests/search.rs (+ any test with a SearchDocument/SearchHit literal), web/src/api/schema.d.ts.

  • Step 1: SearchDocument + build_document (crates/search/src/lib.rs):

    • Add pub recording_date: Option<String>, to SearchDocument (after recorder, before visibility — keep a sensible order).
    • In the Ok(SearchDocument { … }) literal at the end of build_document, add recording_date: object.recording_date.map(|d| d.to_string()),.
  • Step 2: SearchHit + search_objects mapping (crates/search/src/lib.rs):

    • Add pub recording_date: Option<String>, to SearchHit (after visibility, before snippet or near it).
    • In search_objects, the SearchHit { id, object_number, object_name, brief_description, visibility, snippet } literal: add recording_date: doc.recording_date,.
  • Step 3: SearchHitView (crates/api/src/admin_search.rs):

    • Add pub recording_date: Option<String>, to SearchHitView.
    • In the .map(|h| SearchHitView { … }) closure: add recording_date: h.recording_date,.
  • Step 4: Fix test literals + extend the search test (crates/search/tests/search.rs):

    • The doc(...) helper builds a SearchDocument literal — add recording_date: None, to it (compile fix). Grep the search + api test dirs for other SearchDocument {/SearchHit { literals and add recording_date: None where needed (object fixtures that build CatalogueObject are unaffected — they already have recording_date).
    • In search_objects_returns_hits_with_highlight_filter_and_paging, set a date on one seeded doc before indexing: bronze_a.recording_date = Some("1962-04-03".to_string()); and after the existing hit lookup assert: assert_eq!(hit.recording_date.as_deref(), Some("1962-04-03"));.
  • Step 5: Build + lint + test (stack is up).

cd /Users/olsson/Laboratory/biggus-dickus
cargo build --workspace
cargo +nightly fmt
cargo clippy --workspace --all-targets -- -D warnings
cargo nextest run -p search -p api

Expected: compiles, fmt clean, clippy clean, tests pass (incl. the new recording_date assertion). Run the test suite ONCE (single Postgres — no concurrent cargo test runs). If clippy/fmt change files, include them.

  • Step 6: Update web/src/api/schema.d.ts — add the field to the SearchHitView block (alphabetical, between object_number and snippet):
        SearchHitView: {
            brief_description?: string | null;
            id: string;
            object_name: string;
            object_number: string;
            recording_date?: string | null;
            snippet?: string | null;
            visibility: components["schemas"]["Visibility"];
        };

(This mirrors what pnpm gen:api would emit now that the backend returns the field; a later regen reproduces it identically. Optionally verify with pnpm gen:api if a server is running — not required.)

  • Step 7: Commit
git add crates/search/src/lib.rs crates/api/src/admin_search.rs crates/search/tests/search.rs web/src/api/schema.d.ts
# (+ any other test files you fixed)
git commit -m "feat(search): index + return recording_date on search hits (#61)"

Task 2: Frontend — row date + softened count + tests + gate

Files: web/src/search/search-result-row.tsx, web/src/i18n/en.json, web/src/i18n/sv.json, web/src/test/fixtures.ts, web/src/search/search.test.tsx.

  • Step 1: search-result-row.tsx — show the date on the meta line when present:
<div className="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
  <span>{hit.object_number}</span>
  {hit.recording_date && <span>· {hit.recording_date}</span>}
  <VisibilityBadge visibility={hit.visibility} />
</div>

(Render the YYYY-MM-DD string verbatim — it's a recording date, not a UTC timestamp.)

  • Step 2: i18n — soften the count (both locales, parity; the #60 parity test guards this):

    • en.json search: "resultCount_one": "~{{count}} result", "resultCount_other": "~{{count}} results".
    • sv.json search: "resultCount_one": "~{{count}} träff", "resultCount_other": "~{{count}} träffar". (No code change in search-panel.tsx — the key already interpolates count.)
  • Step 3: Fixtures (web/src/test/fixtures.ts) — add recording_date to the first searchHits entry: recording_date: "1962-04-03", (the generated rest can be recording_date: null or omit it — it's optional; set null on the mapped Array.from(...) items to satisfy the type cleanly).

  • Step 4: Tests (web/src/search/search.test.tsx):

    • Assert the first result row shows the date: after results render, expect(screen.getByText("1962-04-03")).toBeInTheDocument(); (or scope within the row).
    • Tighten the count assertion to require the ~: change getByText(/25 results/i)getByText(/~\s*25 results/i) (or assert getByText(/~25 results/i)). Keep the existing load-more + visibility-filter tests green.
  • Step 5: FULL FRONTEND GATE (run tests EXACTLY ONCE):

cd web && pnpm typecheck && pnpm lint && pnpm test && pnpm build && pnpm check:size && pnpm check:colors

All green. Report test totals, largest chunk, check:colors line.

  • Step 6: Codename + status:
cd /Users/olsson/Laboratory/biggus-dickus
git grep -in 'biggus\|dickus' -- web/src crates; echo "codename-exit=$?"
git status --short
  • Step 7: Manual smoke (recommended). With the stack + server + web dev up: search the collection — result rows show the recording date (for objects that have one and have been (re)indexed); the count reads "~N results".

  • Step 8: Commit

git add web/src/search/search-result-row.tsx web/src/i18n/en.json web/src/i18n/sv.json web/src/test/fixtures.ts web/src/search/search.test.tsx
git commit -m "feat(web): show recording_date on search rows; flag estimated count as approximate (#61)"

Self-Review (completed)

Spec coverage: recording_date in SearchDocument/build_document (T1 S1), SearchHit/search_objects (T1 S2), SearchHitView (T1 S3), schema.d.ts (T1 S6); Rust test literal fixes + flow assertion (T1 S4); row date (T2 S1); softened count i18n (T2 S2); fixtures + tests (T2 S3S4); Rust + frontend gates (T1 S5, T2 S5). Acceptance criteria 14 mapped. ✓

Placeholder scan: the schema.d.ts edit is given verbatim; the test changes name exact assertions; "grep for other SearchDocument/SearchHit literals" is a concrete compile-driven step (the compiler names them if missed). No vague steps. ✓

Type/consistency: recording_date: Option<String> (Rust) ↔ recording_date?: string | null (TS) consistent; object.recording_date.map(|d| d.to_string()) (YYYY-MM-DD) matches AdminObjectView; the frontend reads hit.recording_date from the regenerated type. ✓

Notes

  • No new dependency. en/sv parity preserved (only resultCount_* values change; guarded by #60).
  • Backfill: already-indexed objects show no date until reindex_all; new/edited objects index it via sync_object. A reindex CLI is a follow-up.
  • Rust tests need the docker stack (up); run cargo tests ONCE (shared Postgres).
  • cargo nextest run -p search -p api covers the touched crates; a full cargo build --workspace ensures nothing else broke from the struct change.