Initial Commit
This commit is contained in:
154
openspec/archive/2026-05-18-add-api-plugin-search/proposal.md
Normal file
154
openspec/archive/2026-05-18-add-api-plugin-search/proposal.md
Normal 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)
|
||||
Reference in New Issue
Block a user