Files
familienarchiv/docs/superpowers/plans/2026-06-07-remove-nlp-search.md
Marcel be7ad1d1fa refactor(search): delete backend NLP search package
Remove entire backend search domain including:
- NlSearchController, NlQueryParserService, NlpClient implementations
- Rate limiting, properties, DTOs (NlSearchRequest/Response/NlQueryInterpretation)
- All domain logic and tests (5 test files deleted)

Backend compiles successfully post-deletion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 18:41:36 +02:00

769 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Remove NLP/Smart Search 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:** Remove the NLP/smart-search feature entirely from the codebase — backend search package, frontend components, i18n keys, infrastructure config, and the nlp-service microservice.
**Architecture:** Pure deletion + targeted edits. No new code. Each task deletes a self-contained layer, then verifies compilation passes before committing. Order: backend first (most isolated), then frontend, then infrastructure, then docs.
**Tech Stack:** Spring Boot 4 (Java 21, Maven), SvelteKit 2 / Svelte 5, Docker Compose, Paraglide i18n.
---
## File Map
### Delete entirely
- `backend/src/main/java/org/raddatz/familienarchiv/search/` — 14 Java source files
- `backend/src/test/java/org/raddatz/familienarchiv/search/` — 6 Java test files
- `frontend/src/routes/search/SmartModeToggle.svelte` + `.spec.ts`
- `frontend/src/routes/search/SmartSearchStatus.svelte` + `.spec.ts`
- `frontend/src/routes/search/InterpretationChipRow.svelte` + `.spec.ts`
- `frontend/src/routes/search/DisambiguationPicker.svelte` + `.spec.ts`
- `frontend/src/routes/search/chip-types.ts`
- `frontend/src/routes/documents/theme-chip-removal.ts` + `.spec.ts`
- `infra/observability/grafana/provisioning/dashboards/ollama.json`
- `nlp-service/` (entire directory)
- `docs/adr/028-nl-search-ollama.md`
- `docs/adr/028-ollama-docker-compose-service.md`
- `docs/adr/034-ollama-production-deployment-and-keep-alive.md`
- `docs/adr/035-rule-based-nlp-service.md`
### Modify
- `backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java` — remove 2 enum values
- `backend/src/main/resources/application.yaml` — remove `nlp` + `nl-search` config blocks
- `backend/src/main/resources/application-dev.yaml` — remove `nlp` config block
- `frontend/src/routes/SearchFilterBar.svelte` — remove SmartModeToggle, smartMode prop, smart callbacks
- `frontend/src/routes/SearchFilterBar.svelte.spec.ts` — remove smart-mode describe block
- `frontend/src/routes/documents/+page.svelte` — remove all NL state, functions, template block
- `frontend/src/lib/shared/errors.ts` — remove 2 error codes + their switch cases
- `frontend/messages/de.json` — remove 8 smart-search keys
- `frontend/messages/en.json` — remove 8 smart-search keys
- `frontend/messages/es.json` — remove 8 smart-search keys
- `docker-compose.yml` — remove nlp-service block + backend depends_on + env var
- `docker-compose.prod.yml` — remove nlp-service block + backend depends_on + env var
- `infra/observability/prometheus/prometheus.yml` — remove ollama scrape job
- `CLAUDE.md` — remove search package reference + error code entries
- `backend/CLAUDE.md` — no change needed (search package already absent from structure)
- `frontend/CLAUDE.md` — update routes/search/ description
---
### Task 1: Delete backend search package
**Files:**
- Delete: `backend/src/main/java/org/raddatz/familienarchiv/search/` (14 files)
- Delete: `backend/src/test/java/org/raddatz/familienarchiv/search/` (6 files)
- [ ] **Step 1: Delete all source files**
```bash
rm -rf backend/src/main/java/org/raddatz/familienarchiv/search
rm -rf backend/src/test/java/org/raddatz/familienarchiv/search
```
- [ ] **Step 2: Verify backend compiles**
```bash
cd backend && . ~/.sdkman/candidates/java/current/bin/../.. && source ~/.sdkman/bin/sdkman-init.sh && ./mvnw compile -q
```
Expected: BUILD SUCCESS with no errors.
- [ ] **Step 3: Commit**
```bash
git add -A
git commit -m "refactor(search): delete backend NLP search package"
```
---
### Task 2: Remove ErrorCode entries and backend config
**Files:**
- Modify: `backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java:138-142`
- Modify: `backend/src/main/resources/application.yaml:133-138`
- Modify: `backend/src/main/resources/application-dev.yaml:15-17`
- [ ] **Step 1: Remove NL Search enum values from ErrorCode.java**
Remove these lines (138142):
```java
// --- NL Search ---
/** Ollama is unreachable or timed out. 503 */
SMART_SEARCH_UNAVAILABLE,
/** NL search rate limit exceeded (5 requests per user per minute). 429 */
SMART_SEARCH_RATE_LIMITED,
```
The block between `TAG_MERGE_INVALID_TARGET,` and `// --- Generic ---` becomes empty.
- [ ] **Step 2: Remove nlp and nl-search config from application.yaml**
Remove these lines (133138):
```yaml
nlp:
base-url: http://nlp-service:8001
nl-search:
rate-limit:
max-requests-per-minute: 20
```
- [ ] **Step 3: Remove nlp config from application-dev.yaml**
Remove these lines (1517):
```yaml
app:
nlp:
base-url: http://localhost:8001
```
Note: only remove the `nlp:` sub-key under `app:`, preserving any other `app:` config above it.
- [ ] **Step 4: Verify backend still compiles**
```bash
cd backend && source ~/.sdkman/bin/sdkman-init.sh && ./mvnw compile -q
```
Expected: BUILD SUCCESS.
- [ ] **Step 5: Commit**
```bash
git add backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java \
backend/src/main/resources/application.yaml \
backend/src/main/resources/application-dev.yaml
git commit -m "refactor(search): remove NLP error codes and application config"
```
---
### Task 3: Delete frontend NL search components and utilities
**Files:**
- Delete: `frontend/src/routes/search/SmartModeToggle.svelte` + `.spec.ts`
- Delete: `frontend/src/routes/search/SmartSearchStatus.svelte` + `.spec.ts`
- Delete: `frontend/src/routes/search/InterpretationChipRow.svelte` + `.spec.ts`
- Delete: `frontend/src/routes/search/DisambiguationPicker.svelte` + `.spec.ts`
- Delete: `frontend/src/routes/search/chip-types.ts`
- Delete: `frontend/src/routes/documents/theme-chip-removal.ts` + `.spec.ts`
- [ ] **Step 1: Delete all NL search components, specs, and utilities**
```bash
rm frontend/src/routes/search/SmartModeToggle.svelte \
frontend/src/routes/search/SmartModeToggle.svelte.spec.ts \
frontend/src/routes/search/SmartSearchStatus.svelte \
frontend/src/routes/search/SmartSearchStatus.svelte.spec.ts \
frontend/src/routes/search/InterpretationChipRow.svelte \
frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts \
frontend/src/routes/search/DisambiguationPicker.svelte \
frontend/src/routes/search/DisambiguationPicker.svelte.spec.ts \
frontend/src/routes/search/chip-types.ts \
frontend/src/routes/documents/theme-chip-removal.ts \
frontend/src/routes/documents/theme-chip-removal.spec.ts
```
- [ ] **Step 2: Commit**
```bash
git add -A
git commit -m "refactor(search): delete frontend NLP search components and utilities"
```
---
### Task 4: Remove NL search from SearchFilterBar
**Files:**
- Modify: `frontend/src/routes/SearchFilterBar.svelte`
- Modify: `frontend/src/routes/SearchFilterBar.svelte.spec.ts:199-233`
- [ ] **Step 1: Rewrite SearchFilterBar.svelte**
Replace the entire `<script>` block with:
```svelte
<script lang="ts">
import PersonTypeahead from '$lib/person/PersonTypeahead.svelte';
import TagInput from '$lib/tag/TagInput.svelte';
import DateInput from '$lib/shared/primitives/DateInput.svelte';
import SortDropdown from '$lib/shared/primitives/SortDropdown.svelte';
import { slide } from 'svelte/transition';
import { m } from '$lib/paraglide/messages.js';
let {
q = $bindable(''),
from = $bindable(''),
to = $bindable(''),
senderId = $bindable(''),
receiverId = $bindable(''),
tagNames = $bindable<{ name: string; id?: string; color?: string; parentId?: string }[]>([]),
tagQ = $bindable(''),
tagOperator = $bindable<'AND' | 'OR'>('AND'),
undated = $bindable(false),
undatedCount = 0,
sort = $bindable('DATE'),
dir = $bindable('desc'),
showAdvanced = $bindable(false),
initialSenderName = '',
initialReceiverName = '',
navKey = 0,
isLoading = false,
onSearch,
onSearchImmediate,
onfocus,
onblur
}: {
q?: string;
from?: string;
to?: string;
senderId?: string;
receiverId?: string;
tagNames?: { name: string; id?: string; color?: string; parentId?: string }[];
tagQ?: string;
tagOperator?: 'AND' | 'OR';
undated?: boolean;
undatedCount?: number;
sort?: string;
dir?: string;
showAdvanced?: boolean;
initialSenderName?: string;
initialReceiverName?: string;
navKey?: number;
isLoading?: boolean;
onSearch: () => void;
onSearchImmediate?: () => void;
onfocus?: () => void;
onblur?: () => void;
} = $props();
// Plain (non-reactive) flag — not $state, so no reactive assignment inside $effect
let sortDirMounted = false;
$effect(() => {
void sort;
void dir;
if (!sortDirMounted) {
sortDirMounted = true;
return;
}
onSearch();
});
</script>
```
- [ ] **Step 2: Update the search input element in the template**
Replace the `<input type="text" ...>` element (lines 92105) with:
```svelte
<input
type="text"
bind:value={q}
oninput={onSearch}
onfocus={onfocus}
onblur={onblur}
aria-label={m.docs_search_placeholder()}
placeholder={m.docs_search_placeholder()}
class="block w-full border-line py-2.5 pl-10 pr-20 placeholder-ink-3 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
/>
```
- [ ] **Step 3: Remove the SmartModeToggle component from the template**
Delete this line (135):
```svelte
<SmartModeToggle bind:smartMode={smartMode} onToggle={onModeToggle} />
```
- [ ] **Step 4: Remove smart-mode describe block from SearchFilterBar.svelte.spec.ts**
Delete lines 199233 (the entire final `describe` block):
```typescript
describe('SearchFilterBar smart-mode chip lifecycle hooks', () => {
// ...
});
```
- [ ] **Step 5: Run the SearchFilterBar tests to verify they pass**
```bash
cd frontend && source ~/.nvm/nvm.sh && npm run test -- --project=client src/routes/SearchFilterBar.svelte.spec.ts
```
Expected: all tests pass, no failures.
- [ ] **Step 6: Commit**
```bash
git add frontend/src/routes/SearchFilterBar.svelte \
frontend/src/routes/SearchFilterBar.svelte.spec.ts
git commit -m "refactor(search): remove smart mode from SearchFilterBar"
```
---
### Task 5: Remove NL search from documents/+page.svelte
**Files:**
- Modify: `frontend/src/routes/documents/+page.svelte`
This is the largest edit. Remove all NL search state, derived values, functions, and the NL results template block.
- [ ] **Step 1: Remove NL search imports (lines 1116, 2327)**
Remove these import lines:
```typescript
import SmartSearchStatus from '../search/SmartSearchStatus.svelte';
import InterpretationChipRow from '../search/InterpretationChipRow.svelte';
import type { ChipType } from '../search/chip-types.js';
import { buildThemeRemovalUrl } from './theme-chip-removal.js';
import DisambiguationPicker from '../search/DisambiguationPicker.svelte';
```
Remove these type aliases:
```typescript
type NlQueryInterpretation = components['schemas']['NlQueryInterpretation'];
type NlSearchResponse = components['schemas']['NlSearchResponse'];
type DocumentSearchResult = components['schemas']['DocumentSearchResult'];
type PersonHint = components['schemas']['PersonHint'];
type SmartSearchErrorCode = 'SMART_SEARCH_UNAVAILABLE' | 'SMART_SEARCH_RATE_LIMITED';
```
Also remove the `import { csrfFetch } from '$lib/shared/cookies';` line — it is only used by `runSmartSearch`.
- [ ] **Step 2: Remove all NL state and derived values (lines 5170)**
Remove these declarations:
```typescript
// Smart (NL) search — UI-local state, resets on real page navigation (away + back).
let smartMode = $state(false);
let nlSubmitted = $state(false);
let nlLoading = $state(false);
let nlError = $state<SmartSearchErrorCode | null>(null);
let nlInterpretation = $state<NlQueryInterpretation | null>(null);
let nlResult = $state<DocumentSearchResult | null>(null);
const showNlView = $derived(smartMode && nlSubmitted);
const nlHasResults = $derived((nlResult?.items.length ?? 0) > 0);
const ambiguousPersons = $derived(nlInterpretation?.ambiguousPersons ?? []);
const nlIsAmbiguous = $derived(ambiguousPersons.length > 0);
const disambiguationHeading = $derived(
ambiguousPersons.length === 1
? m.search_disambiguation_did_you_mean({ name: ambiguousPersons[0].displayName })
: m.search_disambiguation_heading()
);
const showDisambiguationCue = $derived(ambiguousPersons.length >= 2);
```
- [ ] **Step 3: Remove all NL search functions (lines 202318)**
Remove these functions entirely:
- `resetNlState()`
- `onModeToggle()`
- `runSmartSearch()`
- `switchToKeywordMode()`
- `applyResolvedAndSearch()`
- `paramsFromInterpretation()`
- `removeChip()`
- `selectDisambiguated()`
- [ ] **Step 4: Update SearchFilterBar usage in the template**
Replace the SearchFilterBar call with (removing `bind:smartMode`, `onSmartSearch`, `onModeToggle`):
```svelte
<SearchFilterBar
bind:q={q}
bind:from={from}
bind:to={to}
bind:senderId={senderId}
bind:receiverId={receiverId}
bind:tagNames={tagNames}
bind:showAdvanced={showAdvanced}
bind:sort={sort}
bind:dir={dir}
bind:tagQ={tagQ}
bind:tagOperator={tagOperator}
bind:undated={undated}
undatedCount={data.undatedCount ?? 0}
initialSenderName={initialSenderName}
initialReceiverName={initialReceiverName}
navKey={navKey}
isLoading={navigating.to !== null}
onSearch={handleTextSearch}
onSearchImmediate={handleImmediateSearch}
onfocus={() => (qFocused = true)}
onblur={() => (qFocused = false)}
/>
```
- [ ] **Step 5: Remove the NL results template block**
Replace the entire `{#if showNlView}...{:else}...{/if}` block with just the content of the `{:else}` branch — the `<div class="mt-3 mb-4 hidden lg:block">` block through the closing `{/if}`, unwrapped. The result should be:
```svelte
<div class="mt-3 mb-4 hidden lg:block">
<TimelineDensityFilter
... (keep as-is)
/>
</div>
<div class="mb-3 flex items-center justify-between gap-4">
... (keep as-is)
</div>
<DocumentList ... (keep as-is) />
<Pagination ... (keep as-is) />
```
- [ ] **Step 6: Verify frontend type-checks cleanly for this file**
```bash
cd frontend && source ~/.nvm/nvm.sh && npm run check 2>&1 | grep "documents/+page.svelte"
```
Expected: no errors for `+page.svelte` (pre-existing errors in other files are acceptable).
- [ ] **Step 7: Commit**
```bash
git add frontend/src/routes/documents/+page.svelte
git commit -m "refactor(search): remove NLP smart search from documents page"
```
---
### Task 6: Remove error codes from frontend errors.ts
**Files:**
- Modify: `frontend/src/lib/shared/errors.ts`
- [ ] **Step 1: Remove error code union members (lines 5657)**
Remove from the `ErrorCode` type union:
```typescript
| 'SMART_SEARCH_UNAVAILABLE'
| 'SMART_SEARCH_RATE_LIMITED'
```
- [ ] **Step 2: Remove switch cases (lines 183186)**
Remove from the `getErrorMessage()` switch:
```typescript
case 'SMART_SEARCH_UNAVAILABLE':
return m.error_smart_search_unavailable();
case 'SMART_SEARCH_RATE_LIMITED':
return m.error_smart_search_rate_limited();
```
- [ ] **Step 3: Commit**
```bash
git add frontend/src/lib/shared/errors.ts
git commit -m "refactor(search): remove smart search error codes from frontend"
```
---
### Task 7: Remove i18n messages from all three language files
**Files:**
- Modify: `frontend/messages/de.json`
- Modify: `frontend/messages/en.json`
- Modify: `frontend/messages/es.json`
Remove the same set of keys from each file. The keys to remove are:
```
"error_smart_search_unavailable"
"error_smart_search_rate_limited"
"smart_search_keywords_not_applied"
"search_toggle_smart_label"
"search_toggle_smart_label_suffix"
"search_toggle_keyword_label"
"search_toggle_keyword_label_suffix"
"search_error_unavailable"
"search_error_unavailable_body"
"search_error_rate_limited"
"search_error_rate_limited_body"
"search_empty_nl"
"search_empty_retry_keyword"
"search_disambiguation_did_you_mean"
"search_disambiguation_heading"
```
Note: not all keys may exist in all three files — remove whichever are present.
- [ ] **Step 1: Remove smart-search keys from de.json**
Open `frontend/messages/de.json` and delete every key listed above.
- [ ] **Step 2: Remove smart-search keys from en.json**
Open `frontend/messages/en.json` and delete every key listed above.
- [ ] **Step 3: Remove smart-search keys from es.json**
Open `frontend/messages/es.json` and delete every key listed above.
- [ ] **Step 4: Verify JSON is still valid**
```bash
cd frontend && node -e "
['messages/de.json','messages/en.json','messages/es.json'].forEach(f => {
try { JSON.parse(require('fs').readFileSync(f,'utf8')); console.log(f+': OK'); }
catch(e) { console.error(f+': INVALID - '+e.message); process.exit(1); }
})
"
```
Expected: all three files print `OK`.
- [ ] **Step 5: Commit**
```bash
git add frontend/messages/de.json frontend/messages/en.json frontend/messages/es.json
git commit -m "refactor(search): remove smart search i18n keys from all language files"
```
---
### Task 8: Remove nlp-service from docker-compose files
**Files:**
- Modify: `docker-compose.yml`
- Modify: `docker-compose.prod.yml`
#### docker-compose.yml
- [ ] **Step 1: Remove the nlp-service service block**
Delete the entire `nlp-service:` top-level service definition (lines ~148179, from ` nlp-service:` through its closing line before the next service).
- [ ] **Step 2: Remove nlp-service from backend depends_on**
Find the `backend:` service's `depends_on:` list and remove the `nlp-service:` entry.
- [ ] **Step 3: Remove APP_NLP_BASE_URL from backend environment**
Find the `backend:` service's `environment:` section and remove:
```yaml
APP_NLP_BASE_URL: "http://nlp-service:8001"
```
#### docker-compose.prod.yml
- [ ] **Step 4: Remove the nlp-service service block**
Delete the entire `nlp-service:` top-level service definition (lines ~206221, from ` nlp-service:` through its closing line).
- [ ] **Step 5: Remove nlp-service from backend depends_on**
Find the `backend:` service's `depends_on:` list and remove the `nlp-service:` entry.
- [ ] **Step 6: Remove APP_NLP_BASE_URL from backend environment**
Find the `backend:` service's `environment:` section and remove:
```yaml
APP_NLP_BASE_URL: http://nlp-service:8001
```
- [ ] **Step 7: Validate compose files parse correctly**
```bash
docker compose -f docker-compose.yml config --quiet && echo "dev: OK"
docker compose -f docker-compose.prod.yml config --quiet && echo "prod: OK"
```
Expected: both print `OK` (warnings are acceptable, errors are not).
- [ ] **Step 8: Commit**
```bash
git add docker-compose.yml docker-compose.prod.yml
git commit -m "refactor(infra): remove nlp-service from docker-compose files"
```
---
### Task 9: Remove observability config for Ollama/NLP
**Files:**
- Modify: `infra/observability/prometheus/prometheus.yml`
- Delete: `infra/observability/grafana/provisioning/dashboards/ollama.json`
- [ ] **Step 1: Remove ollama scrape job from prometheus.yml**
Delete these lines from `infra/observability/prometheus/prometheus.yml`:
```yaml
- job_name: ollama
static_configs:
- targets: ['ollama:11434']
```
- [ ] **Step 2: Delete the Grafana Ollama dashboard**
```bash
rm infra/observability/grafana/provisioning/dashboards/ollama.json
```
- [ ] **Step 3: Commit**
```bash
git add infra/observability/prometheus/prometheus.yml
git add -A infra/observability/grafana/provisioning/dashboards/
git commit -m "refactor(infra): remove Ollama/NLP observability config"
```
---
### Task 10: Delete nlp-service directory and ADR docs
**Files:**
- Delete: `nlp-service/` (entire directory)
- Delete: `docs/adr/028-nl-search-ollama.md`
- Delete: `docs/adr/028-ollama-docker-compose-service.md`
- Delete: `docs/adr/034-ollama-production-deployment-and-keep-alive.md`
- Delete: `docs/adr/035-rule-based-nlp-service.md`
- [ ] **Step 1: Delete the nlp-service microservice**
```bash
rm -rf nlp-service/
```
- [ ] **Step 2: Delete NLP/Ollama ADRs**
```bash
rm docs/adr/028-nl-search-ollama.md \
docs/adr/028-ollama-docker-compose-service.md \
docs/adr/034-ollama-production-deployment-and-keep-alive.md \
docs/adr/035-rule-based-nlp-service.md
```
- [ ] **Step 3: Commit**
```bash
git add -A
git commit -m "refactor(search): delete nlp-service microservice and Ollama ADRs"
```
---
### Task 11: Update CLAUDE.md files
**Files:**
- Modify: `CLAUDE.md`
- Modify: `frontend/CLAUDE.md`
#### CLAUDE.md (root)
- [ ] **Step 1: Remove search package from backend package structure table**
Delete this row from the Package Structure section:
```
├── search/ NL search domain — NlSearchController, NlQueryParserService, RestClientOllamaClient, NlSearchRateLimiter
```
- [ ] **Step 2: Remove NLP error codes from Error Handling section**
In the **Error Handling** LLM reminder, remove:
- `SMART_SEARCH_UNAVAILABLE (HTTP 503 — Ollama inference service offline or timed out)`
- `SMART_SEARCH_RATE_LIMITED (HTTP 429 — user exceeded 5 NL search requests per minute)`
from both the Backend Architecture and Frontend Architecture sections.
#### frontend/CLAUDE.md
- [ ] **Step 3: Update routes/search/ description**
Replace:
```
│ ├── search/ # Smart (NL) search sub-components — SmartModeToggle, InterpretationChipRow, SmartSearchStatus, DisambiguationPicker (no +page; consumed by documents/ and SearchFilterBar)
```
With:
```
│ ├── search/ # (empty — NL search removed)
```
Or delete the line entirely if the directory will be empty after removing all component files.
- [ ] **Step 4: Commit**
```bash
git add CLAUDE.md frontend/CLAUDE.md
git commit -m "docs(claude): remove NLP search references from CLAUDE.md files"
```
---
### Task 12: Verify and regenerate API types
- [ ] **Step 1: Install frontend dependencies if needed**
```bash
cd frontend && source ~/.nvm/nvm.sh && npm install
```
- [ ] **Step 2: Run frontend vitest to verify no regressions**
```bash
cd frontend && source ~/.nvm/nvm.sh && npm run test -- --reporter=verbose 2>&1 | tail -30
```
Expected: all tests pass. The deleted spec files are gone and no longer run.
- [ ] **Step 3: Start backend to regenerate API types (requires Docker stack)**
If backend is running locally:
```bash
cd frontend && source ~/.nvm/nvm.sh && npm run generate:api
```
If not running, skip — the generated types remain valid until the next backend change (no NLP types are referenced after this plan).
- [ ] **Step 4: Run a final backend compile**
```bash
cd backend && source ~/.sdkman/bin/sdkman-init.sh && ./mvnw compile -q
```
Expected: BUILD SUCCESS.
- [ ] **Step 5: Commit regenerated API types (if generate:api was run)**
```bash
git add frontend/src/lib/generated/
git commit -m "chore(api): regenerate API types after NLP search removal"
```
---
### Task 13: Check and clean the empty search/ directory
- [ ] **Step 1: Check if routes/search/ is now empty**
```bash
ls frontend/src/routes/search/
```
If only `chip-types.ts` was in there (already deleted in Task 3) and all Svelte files are gone, the directory should be empty.
- [ ] **Step 2: Delete the empty directory**
```bash
rmdir frontend/src/routes/search/ 2>/dev/null && echo "deleted" || echo "not empty — check contents"
```
If not empty, list what remains and delete the stray files.
- [ ] **Step 3: Commit**
```bash
git add -A
git commit -m "refactor(search): remove empty search/ route directory"
```