[ { "id": "T001", "featureIds": ["F001", "F003"], "description": "Migration up creates `app_search_index` table with exactly the columns and types listed in PRD §9.1", "implemented": true }, { "id": "T002", "featureIds": ["F003"], "description": "Primary key is `(tenant, object_type, object_id)` and `tenant` is NOT NULL", "implemented": true }, { "id": "T003", "featureIds": ["F002"], "description": "Migration is idempotent on `pg_trgm`: re-running CREATE EXTENSION IF NOT EXISTS does not fail", "implemented": true }, { "id": "T004", "featureIds": ["F004"], "description": "Under Citus, `pg_dist_partition` shows `app_search_index` distributed by `tenant`", "implemented": true }, { "id": "T005", "featureIds": ["F004"], "description": "Without Citus extension, migration completes without invoking `create_distributed_table`", "implemented": true }, { "id": "T006", "featureIds": ["F005"], "description": "GIN index on `search_vector` is present and is selected by EXPLAIN for an `@@` query", "implemented": true }, { "id": "T007", "featureIds": ["F006"], "description": "GIN trgm index on `title` is selected by EXPLAIN for `title %% 'abc'`", "implemented": true }, { "id": "T008", "featureIds": ["F006"], "description": "GIN trgm index on `subtitle` is selected by EXPLAIN for `subtitle %% 'abc'`", "implemented": true }, { "id": "T009", "featureIds": ["F007"], "description": "btree indexes on `(tenant, source_updated_at DESC)` and `(tenant, object_type)` exist", "implemented": true }, { "id": "T010", "featureIds": ["F008"], "description": "Migration down drops the table; re-running up succeeds afterwards", "implemented": true }, { "id": "T011", "featureIds": ["F009"], "description": "All 27 entity types compile-time exhaustive-check via a switch over `SearchObjectType`", "implemented": true }, { "id": "T012", "featureIds": ["F010", "F011"], "description": "SearchDoc / AclMetadata interfaces require ACL fields when the entity has non-trivial ACL (compile-time)", "implemented": true }, { "id": "T013", "featureIds": ["F012"], "description": "`flattenBlockNote` extracts visible text from a real document's BlockNote JSON fixture", "implemented": true }, { "id": "T014", "featureIds": ["F012"], "description": "`flattenBlockNote` drops image data URI payloads (no `data:image/...` substring in output)", "implemented": true }, { "id": "T015", "featureIds": ["F012"], "description": "`flattenBlockNote` handles deeply nested lists and inline marks without throwing", "implemented": true }, { "id": "T016", "featureIds": ["F013"], "description": "`flattenMarkdown` strips headings, lists, link syntax, bold/italic markers, code fences, leaves plain text", "implemented": true }, { "id": "T017", "featureIds": ["F014"], "description": "`flattenJsonbPayload` concatenates string leaves and skips keys matching the secret regex (`password|secret|token|api_key|authorization`)", "implemented": true }, { "id": "T018", "featureIds": ["F014"], "description": "`flattenJsonbPayload` returns an empty string for non-object/non-array inputs", "implemented": true }, { "id": "T019", "featureIds": ["F015"], "description": "`truncateForIndex` enforces ≤ max bytes on a UTF-8 multibyte string without splitting a code point", "implemented": true }, { "id": "T020", "featureIds": ["F015"], "description": "`truncateForIndex` is a no-op when input is already under the limit", "implemented": true }, { "id": "T021", "featureIds": ["F016"], "description": "`buildTsvectorSql` produces SQL containing `process_large_lexemes(`, weight 'A' for title, 'B' for subtitle, 'C' for body", "implemented": true }, { "id": "T022", "featureIds": ["F017", "F018"], "description": "Registry exposes exactly 27 indexers via `allIndexers()`; `getIndexer('client')` returns the client indexer", "implemented": true }, { "id": "T023", "featureIds": ["F019"], "description": "`upsertSearchDoc` inserts a new row when none exists for (tenant, type, id)", "implemented": true }, { "id": "T024", "featureIds": ["F019"], "description": "`upsertSearchDoc` updates an existing row, refreshing `search_vector` and `indexed_at`", "implemented": true }, { "id": "T025", "featureIds": ["F020"], "description": "`deleteSearchDoc` removes the row and is a no-op when none exists", "implemented": true }, { "id": "T026", "featureIds": ["F019"], "description": "Concurrent `upsertSearchDoc` calls for the same (tenant, type, id) end up with one row (last write wins)", "implemented": true }, { "id": "T027", "featureIds": ["F022"], "description": "Client indexer loadOne returns a SearchDoc with title=client_name and required_permission='client:read'", "implemented": true }, { "id": "T028", "featureIds": ["F022"], "description": "Backfilling client indexer for a seeded tenant produces an index row for every client in that tenant", "implemented": true }, { "id": "T029", "featureIds": ["F023"], "description": "Contact indexer subtitle includes email and phone", "implemented": true }, { "id": "T030", "featureIds": ["F024"], "description": "User indexer excludes users with `user_type='client'`", "implemented": true }, { "id": "T031", "featureIds": ["F025"], "description": "Ticket indexer denormalizes client_name into subtitle and exposes ticket_number in metadata.identifier", "implemented": true }, { "id": "T032", "featureIds": ["F026"], "description": "Ticket-comment indexer sets is_internal_only=true when source `comments.is_internal` is true", "implemented": true }, { "id": "T033", "featureIds": ["F026"], "description": "Ticket-comment indexer url ends with `#comment-{comment_id}`", "implemented": true }, { "id": "T034", "featureIds": ["F027"], "description": "Project indexer sets `client_scope_id` to the project's client_id", "implemented": true }, { "id": "T035", "featureIds": ["F028", "F029"], "description": "Project phase/task indexers inherit ACL fields from their parent project", "implemented": true }, { "id": "T036", "featureIds": ["F030"], "description": "Project-task-comment indexer prefers markdown_content over BlockNote content when both are present", "implemented": true }, { "id": "T037", "featureIds": ["F030"], "description": "Project-task-comment indexer falls back to `flattenBlockNote(note)` when markdown_content is null", "implemented": true }, { "id": "T038", "featureIds": ["F031"], "description": "Asset indexer's body contains flattened jsonb attribute string values but never key names like `password`", "implemented": true }, { "id": "T039", "featureIds": ["F031"], "description": "Asset indexer metadata.identifier equals asset_tag", "implemented": true }, { "id": "T040", "featureIds": ["F032"], "description": "Invoice indexer denormalizes client_name in subtitle and metadata.identifier equals invoice_number", "implemented": true }, { "id": "T041", "featureIds": ["F033", "F034"], "description": "Invoice-item and annotation indexers inherit invoice client_scope_id and required_permission", "implemented": true }, { "id": "T042", "featureIds": ["F035"], "description": "Contract indexer with `contracts.status='draft'` sets subtitle to 'Quote'", "implemented": true }, { "id": "T043", "featureIds": ["F035"], "description": "Contract indexer with `contracts.status='active'` sets subtitle to 'Contract'", "implemented": true }, { "id": "T044", "featureIds": ["F037"], "description": "Document indexer body length is ≤ 65536 bytes even when source content is multi-MB", "implemented": true }, { "id": "T045", "featureIds": ["F037"], "description": "Document indexer sets `client_scope_id` from `documents.client_id` when non-null; leaves `is_private` and `visible_to_user_ids` at defaults (false / empty)", "implemented": true }, { "id": "T046", "featureIds": ["F038"], "description": "KB article indexer pulls content via the FK join to documents", "implemented": true }, { "id": "T047", "featureIds": ["F039"], "description": "Service catalog indexer includes flattened `attributes` jsonb string values in body", "implemented": true }, { "id": "T048", "featureIds": ["F040"], "description": "Service-request-submission indexer flattens submitted_payload string fields but excludes secret-like keys", "implemented": true }, { "id": "T049", "featureIds": ["F041"], "description": "Service-request-definition indexer requires admin permission via required_permission column", "implemented": true }, { "id": "T050", "featureIds": ["F042"], "description": "Workflow-task indexer populates visible_to_user_ids with all assignee user_ids", "implemented": true }, { "id": "T051", "featureIds": ["F044"], "description": "Schedule-entry indexer populates visible_to_user_ids with the owner", "implemented": true }, { "id": "T052", "featureIds": ["F045"], "description": "Time-entry indexer SKIPS rows where notes is NULL or empty string (no index row produced)", "implemented": true }, { "id": "T053", "featureIds": ["F045"], "description": "Time-entry indexer produces a row when notes is any non-empty string (even a single character)", "implemented": true }, { "id": "T054", "featureIds": ["F046", "F047", "F048"], "description": "Board, Category, Tag indexers produce rows with only required_permission='ticket:read'", "implemented": true }, { "id": "T055", "featureIds": ["F049"], "description": "Creating a client publishes `CLIENT_CREATED` event with tenant + client_id", "implemented": true }, { "id": "T056", "featureIds": ["F049"], "description": "Updating a client publishes `CLIENT_UPDATED`", "implemented": true }, { "id": "T057", "featureIds": ["F049"], "description": "Deleting a client publishes `CLIENT_DELETED`", "implemented": true }, { "id": "T058", "featureIds": ["F050"], "description": "Contact CRUD emits CONTACT_* events", "implemented": true }, { "id": "T059", "featureIds": ["F051"], "description": "User CRUD and role-change emit corresponding events", "implemented": true }, { "id": "T060", "featureIds": ["F052"], "description": "Project CRUD and child entity CRUD emit project-family events", "implemented": true }, { "id": "T061", "featureIds": ["F053"], "description": "Asset CRUD emits ASSET_* events", "implemented": true }, { "id": "T062", "featureIds": ["F054"], "description": "Invoice CRUD (header, items, annotations) emits invoice-family events", "implemented": true }, { "id": "T063", "featureIds": ["F055"], "description": "Contract / client-contract CRUD emits contract-family events", "implemented": true }, { "id": "T064", "featureIds": ["F056"], "description": "Document content-change AND share-list-change both emit DOCUMENT_UPDATED", "implemented": true }, { "id": "T065", "featureIds": ["F057"], "description": "Service catalog CRUD emits SERVICE_CATALOG_* events", "implemented": true }, { "id": "T066", "featureIds": ["F058"], "description": "Service-request submission / definition CRUD emits corresponding events", "implemented": true }, { "id": "T067", "featureIds": ["F059"], "description": "Workflow task CRUD + assignment-change emit corresponding events", "implemented": true }, { "id": "T068", "featureIds": ["F060", "F061", "F062"], "description": "Interaction, schedule, time-entry, board/category/tag CRUD emit corresponding events", "implemented": true }, { "id": "T069", "featureIds": ["F063", "F064"], "description": "Subscriber starts subscribed to every `sourceEvents` set returned by registered indexers (union of events)", "implemented": true }, { "id": "T070", "featureIds": ["F065"], "description": "A `CLIENT_CREATED` event causes an upsert into `app_search_index` with object_type='client'", "implemented": true }, { "id": "T071", "featureIds": ["F066"], "description": "A `CLIENT_DELETED` event causes the index row to be deleted", "implemented": true }, { "id": "T072", "featureIds": ["F067"], "description": "With `SEARCH_INDEX_LIVE=false`, events are received but `app_search_index` is not written to", "implemented": true }, { "id": "T073", "featureIds": ["F067"], "description": "Flipping `SEARCH_INDEX_LIVE=true` without restart picks up live writes (or is documented to require a restart)", "implemented": true }, { "id": "T074", "featureIds": ["F065"], "description": "Subscriber handles a missing source row (race: row deleted before loadOne resolves) by deleting the index row instead of erroring", "implemented": true }, { "id": "T075", "featureIds": ["F068"], "description": "TICKET_UPDATED triggers a cascade re-index of all of the ticket's comments (their denormalized parent title updates)", "implemented": true }, { "id": "T076", "featureIds": ["F069"], "description": "INVOICE_UPDATED triggers cascade re-index of items and annotations", "implemented": true }, { "id": "T077", "featureIds": ["F070"], "description": "PROJECT_UPDATED triggers a paged async re-index covering all phases/tasks/task-comments", "implemented": true }, { "id": "T078", "featureIds": ["F071"], "description": "Inserting a `document_associations` row tying a document to a different client triggers re-index of the document row with updated `client_scope_id`", "implemented": true }, { "id": "T079", "featureIds": ["F072"], "description": "User role change kicks off background re-index of rows where the user appears in `visible_to_user_ids`", "implemented": true }, { "id": "T080", "featureIds": ["F073", "F074", "F075"], "description": "`npm run search:backfill -- --tenant= --type=client` indexes all clients for that tenant", "implemented": true }, { "id": "T081", "featureIds": ["F076"], "description": "Backfill processes a 10k-row table in batches of 500 without OOM", "implemented": true }, { "id": "T082", "featureIds": ["F077"], "description": "Running backfill twice produces identical row content (idempotent)", "implemented": true }, { "id": "T083", "featureIds": ["F074"], "description": "Backfill without --tenant flag iterates all tenants discovered via tenant catalog", "implemented": true }, { "id": "T084", "featureIds": ["F078"], "description": "`npm run search:backfill` is wired in root package.json", "implemented": true }, { "id": "T085", "featureIds": ["F079", "F080"], "description": "Reconciliation re-indexes a row whose `source_updated_at` advanced after the last index update", "implemented": true }, { "id": "T086", "featureIds": ["F081"], "description": "Reconciliation deletes an index row whose source row was deleted directly via SQL (event missed)", "implemented": true }, { "id": "T087", "featureIds": ["F082"], "description": "Reconciliation re-creates an index row that was manually deleted while source still exists", "implemented": true }, { "id": "T088", "featureIds": ["F083"], "description": "Reconciliation job is registered with pg-boss at startup with a daily cron schedule", "implemented": true }, { "id": "T089", "featureIds": ["F084"], "description": "parseQuery rejects queries > 200 chars with a typed error", "implemented": true }, { "id": "T090", "featureIds": ["F084"], "description": "parseQuery accepts and normalizes whitespace and casing", "implemented": true }, { "id": "T091", "featureIds": ["F085"], "description": "FTS branch returns matches via `search_vector @@ websearch_to_tsquery(...)`", "implemented": true }, { "id": "T092", "featureIds": ["F085"], "description": "FTS results ordered by `ts_rank_cd` desc", "implemented": true }, { "id": "T093", "featureIds": ["F086"], "description": "pg_trgm fallback returns results when FTS finds nothing but title is a similar string (`exhcange` → `Exchange`)", "implemented": true }, { "id": "T094", "featureIds": ["F086"], "description": "pg_trgm similarity is included in the composite score when present", "implemented": true }, { "id": "T095", "featureIds": ["F087"], "description": "Query `TIC-1023` pins the matching ticket as result #1 via metadata identifier match", "implemented": true }, { "id": "T096", "featureIds": ["F087"], "description": "Asset-tag style query (e.g., `LAP-0042`) pins the matching asset", "implemented": true }, { "id": "T097", "featureIds": ["F088"], "description": "All else equal, newer rows rank higher than older rows by ~time-decay factor", "implemented": true }, { "id": "T098", "featureIds": ["F088"], "description": "Time decay multiplier never drops below 0.05 (floor)", "implemented": true }, { "id": "T099", "featureIds": ["F089"], "description": "Cursor encoding round-trip is stable; decoded cursor reproduces page boundary correctly", "implemented": true }, { "id": "T100", "featureIds": ["F089"], "description": "Pagination is stable across two pages: row appearing on page 1 does not reappear on page 2", "implemented": true }, { "id": "T101", "featureIds": ["F090"], "description": "ts_headline output contains the configured sentinel tokens around matched terms in the body", "implemented": true }, { "id": "T102", "featureIds": ["F091"], "description": "Sanitizer rebuild produces output containing ONLY `` tags around match segments; given source body with literal `