# Design

## Context

See proposal.md for motivation. Header input is presentational. Maintenance already reads `?wo=`, while Inventory detail and Pilot Plant hierarchy use local state. Equipment/item/part detail reads exist, but category-by-ID GET is absent. Prisma models live under prisma/models; existing archive predicates include the parent chain. WorkOrder has deletedAt and releaseAt. Part.productId is the UI SKU; partNumber is nullable and not unique. Durable OpenSpec specs are not yet registered; current source and architecture change artifacts guide compatibility.

## Goals / Non-Goals

**Goals:** bounded read-only search, predictable ranking, reliable deep links, independent destination authorization, and no operational mutations from searching.

**Non-Goals:** search infrastructure, fuzzy matching, stored query history, full results page, database changes, new dependencies, or redesign of destination pages. Planning covers apps/api, apps/app and packages/shared as explicitly requested, with artifacts held in the existing API OpenSpec root.

## Decisions

### One grouped read endpoint

Use `GET /api/v1/search?q=...` with existing requireAuth/trustedOrigin middleware, Zod input validation, controllers and shared response helpers. Trim q and allow 2-100 Unicode code points; reject missing, arrays, whitespace-only, short and overlong values with the existing validation error envelope. No client-controlled group limit or pagination in MVP.

Return `data: { query, pilotPlant: { items, hasMore }, inventory: { items, hasMore }, workOrders: { items, hasMore } }` with `meta.requestId`. Each group contains up to three entries. This is a grouped search-result resource, not a paginated entity collection. A failure in any group fails the request with the normal error envelope rather than silently presenting that group as empty.

Pilot entries include id, level, name, code, categoryId, equipmentId where applicable, and hierarchy labels. Inventory entries include id, name, productId, partNumber, groupName. WO entries include id, number, description excerpt, target label, lifecycle status, overdue. No price, QR token, user credentials or persistence internals. Reuse domain projection/visibility helpers where possible; search.service owns only read composition. Frontend maps typed IDs into routes rather than trusting URLs stored in data.

### Database-side bounded ranking

Rank 0: exact code/productId/partNumber/WO number. Rank 1: exact name/WO description. Rank 2: prefix of a searched field. Rank 3: substring. Compare case-insensitively using the existing MySQL collation, verify fixture behavior, and treat %, _ and backslash as literal input, not wildcard commands. Preserve punctuation in SKU/codes. Search is a single phrase, not tokenization/fuzzy search. Stable tie breaks use display code/number, entity kind and ID.

Rank/filter before LIMIT. Use parameterized queries with static SQL identifiers or bounded tiered Prisma queries; never fetch an arbitrary subset and rank only that subset. Get four candidates per domain branch, merge category/equipment/item into one ranked Pilot Plant group, deduplicate by kind+ID, and use the fourth candidate for hasMore. No total-count queries or unlimited loading. This favors a small endpoint over three separate browser requests and keeps one policy for ranking/visibility.

Visibility: active Category; Equipment with active Category; Item with active Equipment and Category; active Part regardless of stock; released PREVENTIVE/CORRECTIVE WO with deletedAt null. Completed and cancelled WOs remain eligible. Historical WO visibility follows its own lifecycle policy; an archived target does not hide a retained WO that existing detail access permits. Do not add new role restrictions beyond the existing read endpoints.

### React Query integration

Create services/search.service.ts using the shared Axios/request helper and AbortSignal, plus hooks/search.queries.ts and components/layout/global-search.tsx. Keep top-header.tsx as composition. Use the existing QueryClient, query keys scoped to authenticated user ID/role and normalized q, 300 ms debounce, and enable requests only for valid input while search is open. Do not show results for an old term as if they belong to the current term. Cancel obsolete requests and clear/hide results when query becomes invalid or auth scope changes; do not poll or write localStorage.

Use an existing accessible combobox/popover/dialog primitive after inspecting local components. Support Arrow keys, Enter to select, Escape to dismiss and labelled focus management. Display skeleton, empty and actionable error/retry separately. Closing or selecting closes results. A group with hasMore tells users to refine the query; no nonfunctional 'view all' link. Keep the header layout and use a bounded scroll area on small screens.

### Destination URL contracts

- `/maintenance?wo=<id>`: reuse existing detail behavior.
- `/inventory?part=<id>`: fetch detail by ID independent of current list page/filter, open the existing read-only detail panel. URL changes, refresh and back/forward restore it; closing removes only the owned parameter.
- `/pilot-plant?category=<id>`: open that category's equipment list.
- `/pilot-plant?equipment=<id>`: resolve equipment and category then open its item list/context.
- `/pilot-plant?item=<id>`: resolve item, equipment and category then open existing item detail.

Permit exactly one of the three Pilot target keys. Malformed/conflicting IDs show an actionable message, not a silent fallback to the wrong entity. Preserve unrelated query parameters. Normal hierarchy navigation updates/removes owned target parameters so stale URLs cannot reopen earlier targets. Reset conflicting list filters/page on explicit navigation to a selected result. Fetch parents through existing equipment/item detail endpoints and a new minimal category detail endpoint returning CategoryDto using existing requireCategory/categoryDto conventions. Do not search paginated lists to find a selected ID.

An unavailable/deleted/archived destination displays an informative error and a route back to the list. Search cached data is a hint, never authority for actions. Existing mutations invalidate search keys where returned searchable identity/status changes, and selection always retrieves current detail.

## Risks / Trade-offs

- Substring search may scan rows despite current indexes -> bounded outputs, representative query measurements; request approval for an index change if needed. No invented latency guarantee before measuring.
- Three Pilot results can favor one level -> rank across all levels and show hierarchy/type; users refine queries in MVP.
- URL and local state can diverge -> one URL-driven selected target, stale-request cancellation and explicit back/forward tests.
- Existing source retains legacy calibration enum -> exclude it only from new search; do not modify historical records or shared domain enums.
- Destination disappears between search and click -> fetch detail with existing authorization and handle unavailable state.

## Migration Plan

No schema migration or reseeding. Build shared contracts, deploy API/search and category GET, then deploy frontend. Validate envelope/auth/ranking in isolated database fixtures, UI keyboard/async behavior and destination links. Rollback frontend search integration first, then remove additive endpoints if necessary; there is no data rollback.
