docs(plans): search-row recording_date + count copy — 2-task plan (#61)
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# 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).**
|
||||
```bash
|
||||
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`):
|
||||
```ts
|
||||
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**
|
||||
```bash
|
||||
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:
|
||||
```tsx
|
||||
<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):**
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
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**
|
||||
```bash
|
||||
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 S3–S4); Rust + frontend gates (T1 S5, T2 S5). Acceptance criteria 1–4 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.
|
||||
Reference in New Issue
Block a user