Appearance
search-service Agent Tasks
Agent workflow: follow README.md for Audit -> Investigate -> Code -> Test -> Fix; keep this pack's writable scope and verification commands authoritative.
Dispatch type: per-service
Dispatch ID: SVC-014
Current implementation: services/search-service foundation.
Owns: search indexes, sync checkpoints, replay receipts/version tombstones, and query APIs over indexed copies.
Must read:
- legacy search/filter code for questions, exams, courses, documents:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/questions.controller.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/question-bank.service.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/exams/exams.controller.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/courses/courses.controller.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/storage/storage.controller.ts. - question/course/document service task packs.
Legacy source evidence:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/**/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/exams/**/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/courses/**/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/storage/**
Writable files:
services/search-service/**contracts/events/search.projection.changed.v1.schema.jsoncontracts/events/README.mdcontracts/openapi/services/search-service.yamldocs/api/search-service.md
Orchestrator-owned files:
- owner-service canonical writes/backfill contracts, gateway route cutovers, and shared deploy manifests unless explicitly delegated.
Delegated gateway verification scope:
- Search-service agents must not add public gateway routes until a real legacy controller route is selected from
docs/contracts/legacy/legacy-route-snapshot.jsonand mapped to search-service indocs/architecture/legacy-api-map.md. - Only after that orchestrator assignment, this service agent may update non-default
deploy/gateway/routes.search*.jsonexamples,scripts/test/search-*.sh,docs/qa/search-*.md, and rootMakefiletest-search-*targets for the named route rehearsal. - Until a route is selected,
docs/qa/search-route-rehearsals.mdandmake test-search-routesare the guard that proves no synthetic search-service public route has been introduced. - The default gateway route table, owner-service canonical write contracts, public catalog ownership decisions, and shared deploy manifests remain orchestrator-owned. Public search route promotion requires browser/runtime parity and rollback proof.
Completed foundation:
- Defines rebuildable
QUESTION,EXAM,COURSE, andDOCUMENTindex documents. - Persists
sourceServicewith the owner implementation id used by the current rebuild APIs, not the planning alias. For example, question rebuild state usesquestion-bank-service, while the canonical planning alias remainsquestion-service. - Supports
query/q, entity type, organization, owner, status, visibility, tag, taxonomy, pagination, and sort filters. - Stores index documents and source sync checkpoints in service-owned Postgres tables.
- Exposes index upsert/bulk/delete, search, and sync-state APIs.
- Keeps canonical writes in owner services.
- Question-bank projection fixture coverage exists at
services/search-service/internal/http/testdata/question_bank_projection_rebuild.json. It exercises bulk rebuild, search filters, idempotent re-indexing, and sync state through the HTTP routes backed byMemorySearchStore. - Question-bank-service exposes the owner backfill route
GET /v1/questions/search-projectionsfor copiedQUESTIONdocuments. Search rebuild jobs should consume that route instead of reading question-bank tables. - Search-service can pull one question-bank projection page through
POST /v1/rebuild/question-bank/questionswhenQUESTION_BANK_SERVICE_BASE_URLis configured. The route indexes copied documents, writes sync-state, and returnsPARTIALwhile owner pages remain. scripts/migration/rebuild-question-search-index.pyis the external migration workflow loop forquestion-bank,course,exam, anddocumentsources. It repeatedly calls the matching search-service rebuild route, forwards org/actor scope, carries the returned cursor, verifies the ownersourceService/entityType, and stops whenhasMore=false.- Course-service exposes
GET /v1/courses/search-projectionsfor copiedCOURSEdocuments, and search-service can pull one page throughPOST /v1/rebuild/course-service/courseswhenCOURSE_SERVICE_BASE_URLis configured. - Exam-service exposes
GET /v1/exams/search-projectionsfor copiedEXAMdocuments, and search-service can pull one page throughPOST /v1/rebuild/exam-service/examswhenEXAM_SERVICE_BASE_URLis configured. - Document-service exposes
GET /v1/documents/search-projectionsfor copiedDOCUMENTteaching document projections, and search-service can pull one page throughPOST /v1/rebuild/document-service/documentswhenDOCUMENT_SERVICE_BASE_URLis configured. Fixture coverage exists atservices/search-service/internal/http/testdata/document_service_projection_rebuild.json. Its owner now also has a durable per-document source revision/outbox and an opt-in JetStream publisher. Disposable PostgreSQL and broker proof cover archive DELETE tombstones, restart recovery, duplicate replay, and stale resurrection rejection. Course and Exam now have the same opt-in durable transport proof. Course archive is anARCHIVEDUPSERT; ExamCLOSEDis an UPSERT; both use a tombstone only for physical deletion. Pull rebuild remains available only for a tuple without an accepted versioned event; an event-owned tuple is fenced from delayed pull UPSERTs and must recover through owner outbox retry/replay or a controlled version reset. - Search and rebuild requests now require organization scope unless
allOrganizations=trueis requested by anADMINactor. HTTP routes default scope and actor context fromX-Organization-Id,X-User-Id, andX-User-Role; explicitorganizationIdwins overallOrganizationsso a scoped request cannot accidentally become global. - Search-service now has an opt-in projection freshness worker controlled by
SEARCH_REBUILD_WORKER_ENABLED=1. It cycles the configuredquestion-bank,course,exam, anddocumentowner sources, resumes from existing sync-state cursor andnextPagecheckpoints, caps each tick withSEARCH_REBUILD_WORKER_MAX_PAGES, runs two independent owner sources in parallel by default throughSEARCH_REBUILD_WORKER_MAX_CONCURRENT_SOURCES, and records owner-source failures as sync-stateERRORwithout making search-service unavailable. Set the concurrent-source limit to1for a strictly serial owner-load profile. - Each worker source result records
latencyMsfrom checkpoint read through owner fetch, copied-index mutation, and sync-state persistence.Runlogs the same per-source latency with source, entity, page, requested, indexed, cursor, and error fields so cadence changes can be based on observed owner behavior rather than an assumed fixed interval. TestProjectionFreshnessWorkerFromEnvRebuildsOwnerFixturesConcurrentlystarts four disposable HTTP owner fixtures, configures the worker from its runtime environment, holds every owner response until all four requests are in flight, and verifies copied index rows plusSYNCEDcheckpoints. It also proves the worker forwards its internal ADMIN scope, page limit, and archive setting without creating a Gateway route.- Search-service now accepts the internal
search.projection.changed.v1contract through its use case/repository boundary. The contract records(source,event id)receipts and an ownersourceVersiontombstone per copied entity in the same transaction as the index mutation. Duplicate deliveries are no-ops; a same-or-lower version is recorded stale, so an old UPSERT cannot resurrect a higher-version DELETE. Event updates preserve the existing pull-rebuild cursor while updating theSYNCEDfreshness checkpoint. There is deliberately no native HTTP or Gateway route. An opt-in JetStream subscriber starts only withSEARCH_PROJECTION_EVENT_TRANSPORT=nats, consumesSEARCH_PROJECTIONS/search.projection.changed.v1, and ACKs valid or stale deliveries while NAKing transient persistence failures. The default is disabled; Question Bank, Document, Course, and Exam publish only when their own opt-in transport is configured.
Next tasks:
- Collect owner/source freshness and recovery latency for the four opt-in producers before changing worker cadence or enabling any transport by default. Define a controlled version-reset runbook before using it; it must not become a blind pull overwrite.
- Use
docs/agents/handoffs/SVC-014-search-projection-event-transport.mdfor the owner revision/outbox prerequisites. In particular, do not treat anupdatedAttimestamp, a random event id, or question version alone as a sufficientsourceVersionwithout proving metadata and delete coverage. - Do not add a search-service gateway rehearsal until the target is a real legacy route selected from
docs/contracts/legacy/legacy-route-snapshot.jsonand mapped to search-service indocs/architecture/legacy-api-map.md. Current/api/public/catalog*entries are course-service-owned public catalog routes; they may feed copiedCOURSEindex data, but they are not a search-service cutover target. - When a real public search route is selected, add a non-default route-table rehearsal with scope guards, rollback evidence, and browser/runtime parity before any default route promotion. Do not invent synthetic
/api/searchcompatibility routes without legacy evidence. - Add browser/runtime parity evidence before any default route cutover.
Acceptance:
- Search rows are rebuildable from owner service data/events.
- Search does not become source of truth.
- Index freshness and sync state are observable.
- Public search/catalog promotion requires the existing organization-scope enforcement and admin/global search bypass rules to be paired with gateway route guards, browser/runtime proof, and rollback evidence.
- Public route promotion must cite the exact legacy controller and owner mapping; copied index ownership alone is not enough to move a catalog route to search-service.
- Search queries must not bypass owner-service visibility rules; copied index rows may be filtered, but canonical data and authorization decisions remain with owner services or gateway/BFF contracts.
Verification:
GOTOOLCHAIN=go1.25.11 go test ./services/search-service/... -count=1GOTOOLCHAIN=go1.25.11 go test ./services/search-service/internal/http -run TestQuestionBankProjectionFixtureRebuild -count=1 -vGOTOOLCHAIN=go1.25.11 go test ./services/search-service/internal/usecase -run 'TestProjectionFreshnessWorker|TestRebuildFromProjection|TestHTTPProjectionSource' -count=1 -vGOTOOLCHAIN=go1.25.11 go test -race ./services/search-service/internal/usecase -run 'TestProjectionFreshnessWorker' -count=1 -vproves independent owner sources can start in parallel without a race while result ordering and per-source error/sync-state handling remain intact.GOTOOLCHAIN=go1.25.11 go test -race ./services/search-service/internal/usecase -run 'TestProjectionChange' -count=1 -vproves duplicate receipt handling, stale-version rejection, delete tombstone behavior, and pull cursor preservation in memory.SEARCH_PROJECTION_EVENTS_POSTGRES_CONFIRM=disposable-postgres make test-search-projection-events-postgresstarts an isolated PostgreSQL container and proves the receipt, version tombstone, copied index mutation, stale replay outcome, and pull-rebuild fence transactionally.SEARCH_PROJECTION_PULL_WORKER_POSTGRES_CONFIRM=disposable-postgres make test-search-projection-pull-worker-postgresbuilds the real search-service binary with four local owner fixtures and one disposable Postgres database. It proves a one-page initial tick writes fourPARTIALcursor/nextPage=2checkpoints, process restart resumes all four owner sources at page two, and the second tick writes four terminalSYNCEDnextPage=1checkpoints, eight copied rows, native search/sync responses, and per-sourcelatencyMslogs.SEARCH_PROJECTION_BROKER_SMOKE_CONFIRM=disposable-runtime make test-search-projection-broker-livestarts isolated Question Bank/Search PostgreSQL and JetStream containers; it proves subscriber restart recovery, duplicate replay, stale ordering, and DELETE tombstones for Question Bank only, without a Gateway route.GOTOOLCHAIN=go1.25.11 go test -race ./services/search-service/internal/http -run TestProjectionFreshnessWorkerFromEnvRebuildsOwnerFixturesConcurrently -count=1 -vproves the environment-configured worker reaches all four disposable HTTP owner fixtures concurrently, then indexes their copied documents and writesSYNCEDcheckpoints.make test-search-projection-freshness-latencyproves per-source latency includes the owner delay and the environment worker still rebuilds all four HTTP owner fixtures concurrently.DOCUMENT_SEARCH_PROJECTION_OUTBOX_POSTGRES_CONFIRM=disposable-postgres make test-document-search-projection-outbox-postgresproves Document owner create/archive/delete revision, rollback, lease/retry, and dead-letter state in an isolated PostgreSQL container.DOCUMENT_SEARCH_PROJECTION_BROKER_SMOKE_CONFIRM=disposable-runtime make test-document-search-projection-broker-liveproves only Document-to-Search restart recovery, duplicate/stale replay, and the native archive DELETE tombstone through isolated JetStream/PostgreSQL.COURSE_SEARCH_PROJECTION_OUTBOX_POSTGRES_CONFIRM=disposable-postgres make test-course-search-projection-outbox-postgresproves Course owner count coalescing, rollback, organization-scope move, archiveARCHIVEDUPSERT, physical-delete tombstone, and lease/retry/dead state in isolated PostgreSQL.COURSE_SEARCH_PROJECTION_BROKER_SMOKE_CONFIRM=disposable-runtime make test-course-search-projection-broker-liveproves only Course-to-Search restart recovery, duplicate/stale replay, archive state, and the physical-delete tombstone through isolated JetStream/PostgreSQL.EXAM_SEARCH_PROJECTION_OUTBOX_POSTGRES_CONFIRM=disposable-postgres make test-exam-search-projection-outbox-postgresproves Exam owner revision/outbox creation, copied child-count mutation, rollback, redaction, and lease/retry/dead state in isolated PostgreSQL.EXAM_SEARCH_PROJECTION_BROKER_SMOKE_CONFIRM=disposable-runtime make test-exam-search-projection-broker-liveproves only Exam-to-Search restart recovery, duplicate/stale replay,CLOSEDUPSERT, and the physical-delete tombstone through isolated JetStream/PostgreSQL.GOTOOLCHAIN=go1.25.11 go test ./services/search-service/internal/http -run 'TestQuestionBankProjectionFixtureRebuild|TestDocumentServiceProjectionFixtureRebuild' -count=1 -vGOTOOLCHAIN=go1.25.11 go test ./services/search-service/internal/http -run 'TestSearchRebuildQuestionBankQuestionsRoute|TestSearchRebuildCourseServiceCoursesRoute|TestSearchRebuildExamServiceExamsRoute|TestSearchRebuildDocumentServiceDocumentsRoute' -count=1 -vmake test-search-rebuildmake test-search-routesSEARCH_REBUILD_SELF_TEST=1 python3 scripts/migration/rebuild-question-search-index.pycovers the external rebuild helper forquestion-bank,course,exam, anddocumentsources.- Optional generic Postgres index/sync integration:
SEARCH_SERVICE_POSTGRES_TEST_DATABASE_URL=... GOTOOLCHAIN=go1.25.11 go test ./services/search-service/internal/repository -run TestPostgresSearchIndexAndSyncIntegration -count=1 -v - Backfill/rebuild slices must add and run a named rebuild fixture test before route promotion; current HTTP/repository tests only prove local index/search/sync.