Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
158 lines
27 KiB
JSON
158 lines
27 KiB
JSON
[
|
||
{ "id": "F001", "description": "Create migration file `server/migrations/20260513120000_create_app_search_index.cjs` (concrete timestamp; adjust only if a later date is needed at implementation time)", "prdRefs": ["9.1", "16"], "implemented": true },
|
||
{ "id": "F002", "description": "Migration enables the `pg_trgm` Postgres extension if not already present", "prdRefs": ["9.1", "13.1"], "implemented": true },
|
||
{ "id": "F003", "description": "Migration creates the `app_search_index` table with all columns defined in PRD §9.1 (tenant, object_type, object_id, parent_type, parent_id, title, subtitle, body, url, metadata, visible_to_user_ids, visible_to_roles, is_internal_only, is_private, client_scope_id, required_permission, search_vector, search_lang, source_updated_at, indexed_at, PK (tenant, object_type, object_id))", "prdRefs": ["9.1"], "implemented": true },
|
||
{ "id": "F004", "description": "Migration calls `create_distributed_table('app_search_index', 'tenant')` only when the `citus` extension is present", "prdRefs": ["9.1", "15"], "implemented": true },
|
||
{ "id": "F005", "description": "Migration creates GIN index on `search_vector`", "prdRefs": ["9.1"], "implemented": true },
|
||
{ "id": "F006", "description": "Migration creates GIN trgm indexes on `title` and `subtitle` for fuzzy fallback", "prdRefs": ["9.1", "13.1"], "implemented": true },
|
||
{ "id": "F007", "description": "Migration creates btree indexes on `(tenant, source_updated_at DESC)` and `(tenant, object_type)`", "prdRefs": ["9.1"], "implemented": true },
|
||
{ "id": "F008", "description": "Migration provides a `down` step that drops the table", "prdRefs": ["9.1"], "implemented": true },
|
||
|
||
{ "id": "F009", "description": "Define `SearchObjectType` TS union covering all 27 entity types in `packages/types/src/search.ts`", "prdRefs": ["7", "9.2"], "implemented": true },
|
||
{ "id": "F010", "description": "Define `SearchDoc` interface with title, subtitle, body, url, metadata, acl, sourceUpdatedAt in `packages/types/src/search.ts`", "prdRefs": ["9.2"], "implemented": true },
|
||
{ "id": "F011", "description": "Define `AclMetadata` interface (visibleToUserIds, visibleToRoles, isInternalOnly, isPrivate, clientScopeId, requiredPermission)", "prdRefs": ["9.2", "14"], "implemented": true },
|
||
|
||
{ "id": "F012", "description": "Implement `flattenBlockNote(json)` in `server/src/lib/search/normalize.ts` that walks BlockNote nodes, concatenates text leaves, and drops image data URIs", "prdRefs": ["9.3"], "implemented": true },
|
||
{ "id": "F013", "description": "Implement `flattenMarkdown(md)` that strips markdown tokens (headings, lists, links, bold/italic, code fences) leaving plain text", "prdRefs": ["9.3"], "implemented": true },
|
||
{ "id": "F014", "description": "Implement `flattenJsonbPayload(obj)` that recursively concatenates string leaves, skipping keys matching `password|secret|token|api_key|authorization`", "prdRefs": ["9.3"], "implemented": true },
|
||
{ "id": "F015", "description": "Implement `truncateForIndex(text, maxBytes=65536)` that does UTF-8-safe byte truncation without splitting code points", "prdRefs": ["9.3"], "implemented": true },
|
||
{ "id": "F016", "description": "Implement `buildTsvectorSql(title, subtitle, body)` that emits Postgres SQL using `setweight(to_tsvector('english', process_large_lexemes($x)), 'A|B|C')` for the three weight tiers", "prdRefs": ["13.2"], "implemented": true },
|
||
|
||
{ "id": "F017", "description": "Define `EntityIndexer` interface with `objectType`, `sourceEvents`, `loadOne`, `loadBatch` in `packages/types/src/search.ts`", "prdRefs": ["10"], "implemented": true },
|
||
{ "id": "F018", "description": "Build registry in `server/src/lib/search/index.ts` exporting `allIndexers()`, `getIndexer(objectType)`, and `registeredObjectTypes()`. Imports both `ceIndexers` (from `./indexers`) and `eeIndexers` (from `ee/server/src/lib/search/indexers` — stubbed to `[]` in CE builds) and merges them. See PRD §19.", "prdRefs": ["10", "19.1"], "implemented": true },
|
||
{ "id": "F019", "description": "Implement `upsertSearchDoc(knex, doc)` in `server/src/lib/search/upsert.ts` that inserts/updates a row and computes `search_vector` server-side", "prdRefs": ["9", "11.1"], "implemented": true },
|
||
{ "id": "F020", "description": "Implement `deleteSearchDoc(knex, tenant, objectType, objectId)`", "prdRefs": ["11.1"], "implemented": true },
|
||
{ "id": "F021", "description": "Provide a typed `composeAclHints(opts)` helper to consistently build the denormalized ACL columns from indexer code", "prdRefs": ["9.1", "14"], "implemented": true },
|
||
|
||
{ "id": "F022", "description": "Implement `clientIndexer` in `server/src/lib/search/indexers/client.ts` (title=client_name, subtitle=email/phone, body=notes, acl: required_permission='client:read')", "prdRefs": ["7", "10"], "implemented": true },
|
||
{ "id": "F023", "description": "Implement `contactIndexer` (title=full_name, subtitle=email/phone/role, acl: required_permission='contact:read')", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F024", "description": "Implement `userIndexer` for team members (internal users only; title=full name; subtitle=username/email/title; acl: required_permission='user:read')", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F025", "description": "Implement `ticketIndexer` (title=ticket.title, subtitle=denormalized client name + ticket_number, acl: required_permission='ticket:read', visible_to_roles from board scope, metadata.identifier=ticket_number)", "prdRefs": ["7", "13.3"], "implemented": true },
|
||
{ "id": "F026", "description": "Implement `ticketCommentIndexer` (parent_type='ticket', body=flattenMarkdown(note), acl inherits ticket + is_internal_only when `is_internal=true`, url with `#comment-{id}` anchor)", "prdRefs": ["7", "14"], "implemented": true },
|
||
{ "id": "F027", "description": "Implement `projectIndexer` (title=project_name, body=description, acl: required_permission='project:read', client_scope_id=client_id)", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F028", "description": "Implement `projectPhaseIndexer` (inherits project ACL; subtitle=project name)", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F029", "description": "Implement `projectTaskIndexer` (inherits project ACL; subtitle=project name)", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F030", "description": "Implement `projectTaskCommentIndexer` — prefers `markdown_content`, falls back to `flattenBlockNote(note)`; inherits project ACL", "prdRefs": ["7", "9.3"], "implemented": true },
|
||
{ "id": "F031", "description": "Implement `assetIndexer` (title=name, subtitle=asset_tag + serial_number, body=location + flattenJsonbPayload(attributes), acl: required_permission='asset:read', optional client_scope_id, metadata.identifier=asset_tag)", "prdRefs": ["7", "13.3"], "implemented": true },
|
||
{ "id": "F032", "description": "Implement `invoiceIndexer` (title=invoice_number, subtitle=denormalized client name + status, acl: required_permission='invoice:read', client_scope_id, metadata.identifier=invoice_number)", "prdRefs": ["7", "13.3"], "implemented": true },
|
||
{ "id": "F033", "description": "Implement `invoiceItemIndexer` (parent_type='invoice', body=description, inherits invoice ACL, url with `#item-{id}` anchor)", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F034", "description": "Implement `invoiceAnnotationIndexer` (parent_type='invoice', body=content, inherits invoice ACL, url with `#annotation-{id}` anchor)", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F035", "description": "Implement `contractIndexer` against the `contracts` table (columns: contract_id, contract_name, contract_description, status, is_active). title=contract_name, body=contract_description, subtitle='Quote' when status='draft' else 'Contract'. acl: required_permission='contract:read'. metadata.identifier=contract_name", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F036", "description": "Implement `clientContractIndexer` against `client_contracts` (PK (tenant, client_contract_id); columns client_id, contract_id, start_date, end_date, is_active). Title = `{client_name} – {contract_name}` derived via joins to `clients` and `contracts`. Sets `client_scope_id` from `client_id` for ACL.", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F037", "description": "Implement `documentIndexer` (title=document_name, body=truncateForIndex(flattenBlockNote(content), 64KB), acl: required_permission='document:read', optional client_scope_id from `documents.client_id` when set). NOTE: CE has no internal per-user document share mechanism — `is_private` and `visible_to_user_ids` are intentionally NOT set on document rows at v1.", "prdRefs": ["7", "9.3", "14"], "implemented": true },
|
||
{ "id": "F038", "description": "Implement `kbArticleIndexer` (joins documents; acl: required_permission='kb:read')", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F039", "description": "Implement `serviceCatalogIndexer` (title=service_name, body=description + flattenJsonbPayload(attributes), acl: required_permission='service_catalog:read')", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F040", "description": "Implement `serviceRequestSubmissionIndexer` (title=request_name, body=flattenJsonbPayload(submitted_payload), acl: required_permission='service_request:read', optional client_scope_id)", "prdRefs": ["7", "9.3"], "implemented": true },
|
||
{ "id": "F041", "description": "Implement `serviceRequestDefinitionIndexer` (title=name, body=description, acl: required_permission='admin')", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F042", "description": "Implement `workflowTaskIndexer` against `workflow_tasks` (PK is `task_id` STRING alone — `tenant` is a regular text column, not in the PK; assignees stored as JSONB `assigned_users`). title=title, body=description, acl: required_permission='workflow_task:read', visible_to_user_ids parsed from assigned_users JSONB. All upserts/queries must still filter by `tenant`.", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F043", "description": "Implement `interactionIndexer` (title=`interactions.title`; body=`flattenBlockNote(notes)` truncated to 64 KB; subtitle=interaction type name + counterparty (client/contact/ticket reference); acl: required_permission='interaction:read')", "prdRefs": ["7", "9.3"], "implemented": true },
|
||
{ "id": "F044", "description": "Implement `scheduleEntryIndexer` (title=title, body=notes, acl: required_permission='schedule:read', visible_to_user_ids from owner)", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F045", "description": "Implement `timeEntryIndexer` (skip rows where `notes IS NULL OR notes = ''`; visible_to_user_ids from owner; acl: required_permission='time:read')", "prdRefs": ["7", "18-Q3"], "implemented": true },
|
||
{ "id": "F046", "description": "Implement `boardIndexer` (title=channel_name, acl: required_permission='ticket:read')", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F047", "description": "Implement `categoryIndexer` (title=category_name, acl: required_permission='ticket:read')", "prdRefs": ["7"], "implemented": true },
|
||
{ "id": "F048", "description": "Implement `tagIndexer` (title=tag_text, acl: required_permission='ticket:read')", "prdRefs": ["7"], "implemented": true },
|
||
|
||
{ "id": "F049", "description": "Add `CLIENT_CREATED`, `CLIENT_UPDATED`, `CLIENT_DELETED` to `server/src/lib/eventBus/events.ts` and publish from existing client actions", "prdRefs": ["11.2"], "implemented": true },
|
||
{ "id": "F050", "description": "Add `CONTACT_*` event family and publish from contact actions", "prdRefs": ["11.2"], "implemented": true },
|
||
{ "id": "F051", "description": "Audit existing `USER_*` events; add missing CRUD events; publish from user provisioning + role-change actions", "prdRefs": ["11.2", "11.5"], "implemented": true },
|
||
{ "id": "F052", "description": "Add `PROJECT_*`, `PROJECT_PHASE_*`, `PROJECT_TASK_*`, `PROJECT_TASK_COMMENT_*` event families and publishes", "prdRefs": ["11.2"], "implemented": true },
|
||
{ "id": "F053", "description": "Add `ASSET_*` event family and publishes", "prdRefs": ["11.2"], "implemented": true },
|
||
{ "id": "F054", "description": "Add `INVOICE_*`, `INVOICE_ITEM_*`, `INVOICE_ANNOTATION_*` events and publishes", "prdRefs": ["11.2"], "implemented": true },
|
||
{ "id": "F055", "description": "Add `CONTRACT_*` and `CLIENT_CONTRACT_*` event families and publishes", "prdRefs": ["11.2"], "implemented": true },
|
||
{ "id": "F056", "description": "Add `DOCUMENT_*` and `KB_ARTICLE_*` events; publish on content change AND on `document_associations` insert/delete (changes client_scope_id derived from associations)", "prdRefs": ["11.2", "11.5"], "implemented": true },
|
||
{ "id": "F057", "description": "Add `SERVICE_CATALOG_*` event family and publishes", "prdRefs": ["11.2"], "implemented": true },
|
||
{ "id": "F058", "description": "Add `SERVICE_REQUEST_SUBMISSION_*` and `SERVICE_REQUEST_DEFINITION_*` events and publishes", "prdRefs": ["11.2"], "implemented": true },
|
||
{ "id": "F059", "description": "Audit `WORKFLOW_TASK_*` events; add missing CRUD + assignment-change events", "prdRefs": ["11.2", "11.5"], "implemented": true },
|
||
{ "id": "F060", "description": "Add `INTERACTION_*` event family and publishes", "prdRefs": ["11.2"], "implemented": true },
|
||
{ "id": "F061", "description": "Add `SCHEDULE_ENTRY_*` and `TIME_ENTRY_*` event families and publishes", "prdRefs": ["11.2"], "implemented": true },
|
||
{ "id": "F062", "description": "Add `BOARD_*`, `CATEGORY_*`, `TAG_*` event families and publishes", "prdRefs": ["11.2"], "implemented": true },
|
||
|
||
{ "id": "F063", "description": "Create `server/src/lib/eventBus/subscribers/searchIndexSubscriber.ts` and register it during eventBus initialization", "prdRefs": ["11.1"], "implemented": true },
|
||
{ "id": "F064", "description": "Subscriber resolves event → object_type → indexer via registry", "prdRefs": ["11.1"], "implemented": true },
|
||
{ "id": "F065", "description": "Subscriber handles CREATED/UPDATED by calling `indexer.loadOne` and `upsertSearchDoc`", "prdRefs": ["11.1"], "implemented": true },
|
||
{ "id": "F066", "description": "Subscriber handles DELETED by calling `deleteSearchDoc`", "prdRefs": ["11.1"], "implemented": true },
|
||
{ "id": "F067", "description": "Subscriber is gated by env var `SEARCH_INDEX_LIVE` (default `false`); when false, events are acknowledged but no DB writes occur", "prdRefs": ["11.1", "16"], "implemented": true },
|
||
|
||
{ "id": "F068", "description": "On TICKET_UPDATED, cascade a re-index of all the ticket's comments (denormalized parent title)", "prdRefs": ["11.1"], "implemented": true },
|
||
{ "id": "F069", "description": "On INVOICE_UPDATED, cascade re-index of its items and annotations", "prdRefs": ["11.1"], "implemented": true },
|
||
{ "id": "F070", "description": "On PROJECT_UPDATED, cascade re-index of phases, tasks, task-comments (paged async job)", "prdRefs": ["11.1"], "implemented": true },
|
||
{ "id": "F071", "description": "On `document_associations` insert/delete affecting a document, re-index the document row (its `client_scope_id` may have changed if the primary association was the source)", "prdRefs": ["11.5"], "implemented": true },
|
||
{ "id": "F072", "description": "On user role change, enqueue background job that re-indexes rows where the user is in `visible_to_user_ids`", "prdRefs": ["11.5"], "implemented": true },
|
||
|
||
{ "id": "F073", "description": "Create CLI script `server/src/scripts/search-backfill.ts`", "prdRefs": ["11.3"], "implemented": true },
|
||
{ "id": "F074", "description": "Backfill iterates all tenants by default; supports `--tenant=<uuid>` flag", "prdRefs": ["11.3"], "implemented": true },
|
||
{ "id": "F075", "description": "Backfill iterates all registered indexers; supports `--type=<object_type>` flag for a single entity", "prdRefs": ["11.3"], "implemented": true },
|
||
{ "id": "F076", "description": "Backfill pages source rows in batches of 500 via `loadBatch(cursor, limit)`", "prdRefs": ["11.3"], "implemented": true },
|
||
{ "id": "F077", "description": "Backfill is idempotent: re-runs produce identical row state in `app_search_index`", "prdRefs": ["11.3"], "implemented": true },
|
||
{ "id": "F078", "description": "Wire `npm run search:backfill` script in root package.json", "prdRefs": ["11.3"], "implemented": true },
|
||
|
||
{ "id": "F079", "description": "Create pg-boss job `search:reconcile` in workflow-worker or scheduled jobs registry", "prdRefs": ["11.4"], "implemented": true },
|
||
{ "id": "F080", "description": "Reconciliation queries source rows where `updated_at > max(source_updated_at) per (tenant, object_type)` and re-indexes them", "prdRefs": ["11.4"], "implemented": true },
|
||
{ "id": "F081", "description": "Reconciliation deletes `app_search_index` rows whose source row no longer exists", "prdRefs": ["11.4"], "implemented": true },
|
||
{ "id": "F082", "description": "Reconciliation inserts source rows that are missing from the index (covers backfill gaps)", "prdRefs": ["11.4"], "implemented": true },
|
||
{ "id": "F083", "description": "Schedule reconciliation to run daily; document the schedule in deploy runbook", "prdRefs": ["11.4", "16"], "implemented": true },
|
||
|
||
{ "id": "F084", "description": "Implement `parseQuery(raw)` that trims, lowercases identifier patterns, validates length ≤ 200 chars", "prdRefs": ["12", "13", "18-Q2"], "implemented": true },
|
||
{ "id": "F085", "description": "Build SQL query using `websearch_to_tsquery('english', $1)` and `ts_rank_cd(search_vector, tsq)` for FTS branch", "prdRefs": ["13.1"], "implemented": true },
|
||
{ "id": "F086", "description": "Add `pg_trgm` fallback branch using `similarity(title|subtitle, raw)` and `%` operator", "prdRefs": ["13.1"], "implemented": true },
|
||
{ "id": "F087", "description": "Detect identifier patterns (`^[A-Z]+-?\\d+$`, ticket-number-style, asset-tag-style) and pin exact `metadata->>'identifier'` matches at the top", "prdRefs": ["13.3"], "implemented": true },
|
||
{ "id": "F088", "description": "Apply time-decay multiplier `exp(-age_days/90)` with floor 0.05 in the ORDER BY composite score", "prdRefs": ["13.4"], "implemented": true },
|
||
{ "id": "F089", "description": "Implement opaque cursor encoding/decoding for stable pagination (encodes rank + object_id tiebreaker)", "prdRefs": ["12"], "implemented": true },
|
||
|
||
{ "id": "F090", "description": "Build snippet generation via `ts_headline('english', body, tsq, 'MaxFragments=2,StartSel=<mark>,StopSel=</mark>')`", "prdRefs": ["13.1"], "implemented": true },
|
||
{ "id": "F091", "description": "Sanitize ts_headline output server-side via controlled-sentinel rebuild: emit ts_headline with unique sentinel tokens (not `<mark>`); split on sentinels, HTML-escape each segment, re-wrap match segments in `<mark>` — guarantees no other HTML can survive", "prdRefs": ["6.3", "18-Q1"], "implemented": true },
|
||
{ "id": "F092", "description": "Skip snippet generation in the typeahead variant for sub-100ms p50", "prdRefs": ["12"], "implemented": true },
|
||
|
||
{ "id": "F093", "description": "Implement `aclPredicateSql(user)` that returns a parameterized SQL fragment combining required_permission, visible_to_user_ids, visible_to_roles, is_internal_only, is_private, client_scope_id checks", "prdRefs": ["14.1"], "implemented": true },
|
||
{ "id": "F094", "description": "Resolve the user's set of granted permissions once at request time and pass as an array parameter to `required_permission = ANY($perms)` style check", "prdRefs": ["14.1"], "implemented": true },
|
||
{ "id": "F095", "description": "Implement `visible_to_user_ids && ARRAY[$user_id]::uuid[]` overlap test as part of acl predicate", "prdRefs": ["14.1"], "implemented": true },
|
||
{ "id": "F096", "description": "Implement `is_internal_only` filter against user.is_internal AND `is_private` filter against share-list membership AND `client_scope_id` filter against accessible-clients", "prdRefs": ["14.1"], "implemented": true },
|
||
|
||
{ "id": "F097", "description": "Implement `verifyResultVisibility(user, rows)` that runs each row through the entity's authoritative permission check", "prdRefs": ["14.1", "14.3"], "implemented": true },
|
||
{ "id": "F098", "description": "Wire per-entity assert helpers: ticket (assertTicketReadable), project (assertProjectReadable), document (share-list), workflow_task (assignee)", "prdRefs": ["14.2"], "implemented": true },
|
||
{ "id": "F099", "description": "Emit telemetry `search.acl_drift` counter (server log + Sentry) when SQL filter and record-level check disagree", "prdRefs": ["14.3"], "implemented": true },
|
||
|
||
{ "id": "F100", "description": "Implement `searchAppAction` as a `withAuth` server action returning `SearchAppResult` (full results, snippets, grouping)", "prdRefs": ["12"], "implemented": true },
|
||
{ "id": "F101", "description": "Implement `searchAppTypeaheadAction` as a `withAuth` server action returning top-5 title-only suggestions", "prdRefs": ["12"], "implemented": true },
|
||
{ "id": "F102", "description": "Add Zod input schema validating query (1–200 chars), types subset, limit (≤100), cursor", "prdRefs": ["12", "18-Q2"], "implemented": true },
|
||
{ "id": "F103", "description": "Add Zod output schema for `SearchAppResult` and `SearchResultRow` (or assert at action boundary)", "prdRefs": ["12"], "implemented": true },
|
||
|
||
{ "id": "F104", "description": "Create `server/src/components/search/SearchPalette.tsx` using `cmdk` for the sidebar typeahead", "prdRefs": ["6.1"], "implemented": true },
|
||
{ "id": "F105", "description": "Bind `Cmd/Ctrl+K` global shortcut to open the sidebar input", "prdRefs": ["6.1"], "implemented": true },
|
||
{ "id": "F106", "description": "Typeahead renders up to 5 title-only suggestions as native `<a href>` anchors so Cmd/Ctrl+click opens in new tab", "prdRefs": ["6.1"], "implemented": true },
|
||
{ "id": "F107", "description": "Typeahead's last row is `→ See all N results` linking to `/msp/search?q=...`", "prdRefs": ["6.1"], "implemented": true },
|
||
{ "id": "F108", "description": "Insert the SearchPalette launcher into `Sidebar.tsx` at the top, above the main nav", "prdRefs": ["6.1"], "implemented": true },
|
||
|
||
{ "id": "F109", "description": "Create `server/src/app/msp/search/page.tsx` (server component) that reads `q`, `type`, `cursor`, `sort` from search params and calls `searchAppAction`", "prdRefs": ["6.2"], "implemented": true },
|
||
{ "id": "F110", "description": "Create `SearchPageClient.tsx` that manages URL state via Next.js router (debounced 200ms on input change)", "prdRefs": ["6.2"], "implemented": true },
|
||
{ "id": "F111", "description": "Render filter chips on the results page: `All` plus one chip per entity type with count badge from `groups`", "prdRefs": ["6.2"], "implemented": true },
|
||
{ "id": "F112", "description": "Render grouped result lists (by entity type, max 10 per group) when `type=All`", "prdRefs": ["6.2"], "implemented": true },
|
||
{ "id": "F113", "description": "Render flat paginated list when a single `type` filter is selected", "prdRefs": ["6.2"], "implemented": true },
|
||
{ "id": "F114", "description": "Implement cursor-based prev/next pagination on the results page", "prdRefs": ["6.2"], "implemented": true },
|
||
{ "id": "F115", "description": "Implement loading skeleton AND a clear empty state that echoes the query", "prdRefs": ["6.2"], "implemented": true },
|
||
{ "id": "F116", "description": "Every result row on the page is a native `<a>` element with canonical `href` for new-tab support", "prdRefs": ["6.2"], "implemented": true },
|
||
{ "id": "F117", "description": "Add `sort` query param toggle: `relevance` (default) | `recent` (orders by source_updated_at DESC only)", "prdRefs": ["6.2"], "implemented": true },
|
||
|
||
{ "id": "F118", "description": "ARIA combobox semantics on sidebar input (role, aria-expanded, aria-controls, aria-activedescendant); ARIA region role on results page", "prdRefs": ["6.3"], "implemented": true },
|
||
{ "id": "F119", "description": "Keyboard navigation: ↑/↓ to move, Enter to open or submit, Esc to close typeahead, Tab to advance focus normally; works identically on sidebar and results page", "prdRefs": ["6.3"], "implemented": true },
|
||
{ "id": "F120", "description": "Stable kebab-case `id`s on every interactive element (e.g., `app-search-input`, `app-search-result-row-{type}-{id}`, `app-search-filter-chip-{type}`)", "prdRefs": ["6.3"], "implemented": true },
|
||
|
||
{ "id": "F121", "description": "Add `search.*` keys to `server/public/locales/en/msp/core.json` (placeholders, helper text, group labels, empty state, error states)", "prdRefs": ["6.3"], "implemented": true },
|
||
{ "id": "F122", "description": "Run lang-pack pipeline (`generate-pseudo-locales.cjs` + `validate-translations.cjs`) to propagate keys to all locales", "prdRefs": ["6.3"], "implemented": true },
|
||
{ "id": "F123", "description": "Wire `useTranslation('msp/core')` in `SearchPalette` and `SearchPageClient`; no hardcoded strings", "prdRefs": ["6.3"], "implemented": true },
|
||
|
||
{ "id": "F124", "description": "Document `SEARCH_INDEX_LIVE` env var in env reference doc and helm/values charts", "prdRefs": ["11.1", "16"], "implemented": true },
|
||
{ "id": "F125", "description": "Document deploy runbook: migrate → deploy w/ `SEARCH_INDEX_LIVE=false` → backfill → flip env → enable UI", "prdRefs": ["16"], "implemented": true },
|
||
{ "id": "F126", "description": "Emit telemetry counters: `search.query.count`, `search.query.empty`, `search.query.latency_ms`, `search.acl_drift`", "prdRefs": ["14.3"], "implemented": true },
|
||
{ "id": "F127", "description": "Add server-side rate limiting to `searchAppAction` and `searchAppTypeaheadAction` (per-user, e.g. 30/sec typeahead and 10/sec full)", "prdRefs": ["12"], "implemented": true },
|
||
|
||
{ "id": "F128", "description": "Ticket detail page honors `#comment-{id}` hash: scrolls into view and applies a brief highlight class", "prdRefs": ["6.2", "Acceptance #5"], "implemented": true },
|
||
{ "id": "F129", "description": "Invoice detail page honors `#item-{id}` and `#annotation-{id}` hashes (scroll + highlight)", "prdRefs": ["6.2"], "implemented": true },
|
||
{ "id": "F130", "description": "Project task page honors `#comment-{id}` hash (scroll + highlight)", "prdRefs": ["6.2"], "implemented": true },
|
||
|
||
{ "id": "F131", "description": "Create CE stub at `ee/server/src/lib/search/indexers/index.ts` exporting `export const eeIndexers: EntityIndexer[] = []`. Register the stub via the existing CE/EE stub-generator config (whichever mechanism the repo uses — see ce-ee-stub-fixer skill) so CE builds resolve the import to the empty array and EE builds resolve to the real module.", "prdRefs": ["19.1"], "implemented": true },
|
||
{ "id": "F132", "description": "Restrict search queries to `object_type = ANY(registeredObjectTypes())` so orphan rows from a previous EE deploy never surface in CE builds (orphan-safety).", "prdRefs": ["19.5"], "implemented": true },
|
||
{ "id": "F133", "description": "Make filter chips and group headers on `/msp/search` iterate over `registeredObjectTypes()` rather than a hard-coded list, with i18n fallback to a humanized object_type when the locale key is missing.", "prdRefs": ["19.4"], "implemented": true },
|
||
{ "id": "F134", "description": "Reconciliation job skips any `object_type` for which no indexer is currently registered (does not error, does not attempt to load source rows).", "prdRefs": ["19.5"], "implemented": true }
|
||
]
|