Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
# Add `@suaveplan/api-plugin-search`
**Package**: `@suaveplan/api-plugin-search`
**Status**: PROPOSED
**Type**: new-package
**Design**: ./design.md
**Spec delta**: ./specs/api-plugin-search/spec.md
## Why
The Suaveplan API host needs an in-process full-text search plugin so consumer apps
can index domain documents (users, posts, products, audit events, ...) and serve
relevance-scored queries, facets, filters, highlights, and autocomplete suggestions
without a separate search-engine deployment. Upstream `@suaveplan/api-plugin-search`
provided this as an in-memory engine with TF-IDF scoring, four pluggable analyzers
(standard, simple, whitespace, keyword), faceted aggregation, boolean filters, and
optional highlighting. The genesis migration brings the full functional surface
across, swaps the upstream "old-style" `@suaveplan/api` Plugin contract for the
canonical `@suaveplan/api-plugin` `PluginDescriptor` shape, fixes the two `as any`
casts in the event bus call site by routing through typed payloads, replaces raw
`throw new Error(...)` sites with typed `SearchError` carrying stable `SEARCH.*`
codes, wires OpenTelemetry instruments (5 counters/histograms) onto every hot path,
and aligns the package with the rest of the genesis monorepo (5-files-per-feature,
sibling `.md` docs, runner-neutral test imports, 100% istanbul coverage).
## What changes
- ADDED: `createSearchPlugin(options?)` — `@suaveplan/api-plugin`-shaped
`PluginDescriptor<SearchPluginConfig>` factory that mounts the search routes at a
configurable `routePrefix` (default `/search`) and registers a `searchIndexes`
service in the host `ServiceRegistry`.
- ADDED: `SearchPluginOptionsSchema` / `SearchPluginOptions` /
`SearchPluginConfig` — Zod-validated factory options (`routePrefix`,
`maxIndexes`, `maxDocumentsPerIndex`, `defaultAnalyzer`).
- ADDED: In-memory `SearchService` functions
(`createSearchIndex` / `deleteSearchIndex` / `getSearchIndex` / `listSearchIndexes`
/ `indexDocument` / `bulkIndexDocuments` / `removeDocument` / `search` /
`suggest`) plus `DEFAULT_SEARCH_CONFIG` and the `SearchServiceConfig` type.
- ADDED: Four text analyzers via factory functions (`createStandardAnalyzer`,
`createSimpleAnalyzer`, `createWhitespaceAnalyzer`, `createKeywordAnalyzer`),
plus exported primitives `tokenize`, `removeStopWords`, `stem`, `getAnalyzer`,
and `listAnalyzers`. The `TextAnalyzer` and `AnalyzerOptions` interfaces are
public so consumers can author and register their own analyzers.
- ADDED: `createSearchRoutes(deps)` — OpenAPIHono router exposing 9 routes:
`POST /query`, `POST /suggest`, `GET /indexes`, `POST /indexes`,
`GET /indexes/:name`, `DELETE /indexes/:name`, `POST /indexes/:name/documents`,
`POST /indexes/:name/bulk`, `DELETE /indexes/:name/documents/:docId`. Every
route validates request bodies/params with Zod via `@hono/zod-openapi`, and
every error response is the canonical `{ error: string }` shape.
- ADDED: 10 `SEARCH.*` error codes via `defineCodes("SEARCH", [...])` —
`INDEX_NOT_FOUND`, `INDEX_ALREADY_EXISTS`, `INDEX_LIMIT_REACHED`,
`DOCUMENT_LIMIT_REACHED`, `DOCUMENT_NOT_FOUND`, `UNKNOWN_ANALYZER`,
`INVALID_OPTIONS`, `INVALID_QUERY`, `SERVICE_NOT_AVAILABLE`,
`ROUTE_PREFIX_COLLISION` — each thrown from at least one production code path.
- ADDED: 5 OTel instruments under the
`@suaveplan/api-plugin-search` meter — `search.queries_total{outcome}`,
`search.query_duration_ms` (histogram), `search.documents_indexed_total{op}`,
`search.indexes_total{op}` and `search.suggest_requests_total{outcome}`.
- ADDED: Typed event-bus extension for
`search:initialized`, `search:ready`, `search:index:created`,
`search:index:deleted`, `search:index:updated`, `search:document:indexed`,
`search:bulk:completed`, `search:document:removed`, and `search:query:executed`
— payloads strongly typed; no `as any` casts on the emitter.
- ADDED: `searchIndexes` slot on the canonical
`@suaveplan/api-plugin/service-registry` via module augmentation, typed as
`Map<string, SearchIndex>` so sibling plugins can read/write indexes with
full type safety.
## Out of scope
- Persistent backend (SurrealDB FTS / Typesense / Meilisearch) — the upstream
package is in-memory only and the migration matches parity. A separate
follow-up change can introduce a `SearchAdapter` driver against
`@suaveplan/api-plugin-database` once that package lands; the public
`SearchAdapter` interface ships here so the contract is stable.
- Tenant scoping, auth gates, rate limiting, audit logging — upstream did not
carry these; the enterprise audit document flags them as future work. They
are not in this migration to preserve parity.
- Vector search, semantic ranking, learning-to-rank, saved searches,
language-specific stemmers, query DSL — enterprise audit §4 enhancements,
deferred to dedicated follow-up changes.
- Benchmark suite under `src/__benchmarks__/` — landing as a follow-up change so
this migration ships at parity scope first.
## Source mapping
This change derives from `../suaveplan/enterprise/api-plugin-search.md` and the
upstream source at `../suaveplan/packages/server/api-plugin-search/src/`.
| Requirement | Source lines | Notes |
|---|---|---|
| In-memory search engine with TF-IDF scoring | enterprise L8-L13; `src/services/search/search.service.ts#scoreDocuments` L302-L336 | verbatim parity |
| Four built-in analyzers (standard/simple/whitespace/keyword) | enterprise L8-L13; `src/analyzers/text/text.analyzer.ts` L156-L223 | verbatim parity |
| Tokenize + stop-word + Porter-style stemmer | `src/analyzers/text/text.analyzer.ts` L52-L123 | verbatim parity |
| `createSearchPlugin` factory + four lifecycle hooks | `src/index.ts` L39-L100 | re-typed for `@suaveplan/api-plugin` |
| `SearchPluginConfigSchema` (basePath/maxIndexes/maxDocumentsPerIndex/defaultAnalyzer) | `src/index.ts` L21-L33 | renamed to `SearchPluginOptionsSchema` for genesis convention |
| 9 OpenAPIHono routes with Zod request/response schemas | `src/routes/search/search.ts` L41-L479 | verbatim route surface |
| Bulk index limit (1000 docs/request) | `src/types.ts#BulkIndexSchema` L113-L116 | verbatim |
| Faceted aggregation honouring `IndexField.facet` | `src/services/search/search.service.ts#buildFacets` L431-L459 | verbatim |
| Boolean / array filter pass | `src/services/search/search.service.ts#applyFilters` L338-L355 | verbatim |
| Sort orders (relevance / date_asc / date_desc / alpha_asc / alpha_desc) | `src/services/search/search.service.ts#sortResults` L357-L398 | verbatim |
| Highlighting with configurable HTML tag | `src/services/search/search.service.ts#buildHighlights` L400-L429 | verbatim |
| Prefix-based suggest | `src/services/search/search.service.ts#suggest` L250-L291 | verbatim |
| Per-index document limit enforcement | `src/services/search/search.service.ts#indexDocument` L117-L154 | enforced via `SEARCH.DOCUMENT_LIMIT_REACHED` |
| Event-bus emission on lifecycle, query, index updates | `src/index.ts` L64-L93; `src/routes/search/search.ts` L92-L99/L350-L356/L414-L420/L465-L471 | drop both `as any` casts (audit §3) |
| Typed `SearchQueryExecutedEvent` / `SearchIndexUpdatedEvent` payloads | `src/types.ts` L188-L218 | preserved verbatim |
| `SearchAdapter` interface (future SurrealFTS / Typesense backends) | `src/types.ts#SearchAdapter` L172-L184 | preserved verbatim |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| Typed `SEARCH.*` error registry via `defineCodes` | Genesis-wide rule (every package extends `SuaveplanError`); enterprise audit §3 explicitly calls for it. |
| OTel instruments on every hot path | Genesis-wide observability rule + enterprise audit §3. |
| Runner-neutral test imports (`@suaveplan/testing/runner`) | Genesis rule 11. |
| 100 / 100 / 100 / 100 istanbul coverage | Genesis-wide policy. |
| Sibling `.md` documentation per implementation file | Genesis-wide policy. |
| Subpath exports per feature module | Genesis-wide tree-shaking rule (§6). |
| `ServiceRegistry` module augmentation for `searchIndexes` slot | Genesis convention introduced by `@suaveplan/api`, `@suaveplan/api-plugin-auth`, `@suaveplan/api-plugin-user`. |
## Verification approach
Every requirement in the spec delta carries a `**Verified by:**` line at draft
time pointing at the planned `it(...)` titles below.
- `src/error-codes/error-codes.test.ts` — `SEARCH.*` registry shape + `SearchError`
cause chain.
- `src/otel/instruments.test.ts` — meter binding, cache reset, instrument names.
- `src/analyzer/analyzer.test.ts` — tokenize / stop-words / stem / four analyzer
factories / registry lookup / unknown-name failure.
- `src/schemas/schemas.test.ts` — every Zod schema parse/reject path.
- `src/schemas/options.test.ts` — `SearchPluginOptionsSchema` defaults and bounds.
- `src/service/service.test.ts` — index lifecycle, document indexing, search,
filters, sort, facets, highlighting, suggest, error codes, OTel emission.
- `src/routes/build-routes.test.ts` — every route (9 endpoints, success + error
branches).
- `src/plugin/plugin.test.ts` — descriptor metadata, `onInit` /
`onReady` / `onClose`, custom `routePrefix`, missing `app` service.
- `src/api-plugin-search.spec-conformance.test.ts` — type-level public-surface
pins + barrel exports + side-effect imports.
## Migration impact
None — additive only. No existing genesis package depends on
`@suaveplan/api-plugin-search`.
## Impact
### Affected dependents
No dependents yet. After publication, sibling plugins (`api-plugin-user`,
`api-plugin-cms`, etc.) can read the augmented `searchIndexes` service slot.
- N/A — [x] no action needed (new package, zero pre-existing consumers in genesis)