Appearance
Handoff: SVC-020-notification-recipient-route-proof notification recipient route proof
Scope
- Service/area:
attempt-serviceproducer andnotification-servicerecipient/replay boundary. - Goal: prove notification events carry already-resolved recipient ids and that notification, parent-alert, and attempt route rehearsals retain explicit rollback state.
- Non-goals: public/default route promotion, browser proof, gateway route changes, recipient discovery, parent-student authorization, delivery-provider runtime, provider secrets, or changes to legacy source.
Docs Read
docs/agents/handoff-phase-9-notification-foundation.mddocs/agents/service-tasks/notification-service.mddocs/agents/handoffs/SVC-020-attempt-grade-notification-producer-contract.mddocs/agents/service-tasks/attempt-service.mddocs/agents/service-tasks/profile-service.mddocs/agents/service-agent-next-slice-queue.jsondocs/agents/service-agent-next-slice-closeout-ledger.jsondocs/agents/agent-handoff-format.mddocs/api/attempt-service.mddocs/api/notification-service.mddocs/api/profile-service.mddocs/qa/notification-route-rehearsals.mddocs/qa/parent-alert-route-rehearsals.md
Legacy Evidence Read
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/notifications/notification.service.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/notifications/notifications.controller.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/app-data/app-data.notifications.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/analytics/analytics.controller.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/admin/inbox.controller.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/support/support.service.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/app-data/app-data.classrooms-engagement.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/app-data/app-data.exam-runtime-core.ts- Legacy source was read-only evidence; no files under
/Users/velikho/Desktop/WORKING/HOCTAPAZ/were changed.
Files Changed
docs/agents/handoffs/SVC-020-notification-recipient-route-proof.md- No attempt-service implementation, notification-service implementation, gateway route, migration, or legacy file was changed by this proof handoff.
Behavior Implemented
- None in this handoff. Existing producers already resolve parent recipients through profile-service or accept explicit recipient ids before publishing; notification-service remains the replay, preference, inbox, and delivery-state owner.
Tests And Verification
- Command:
GOTOOLCHAIN=go1.25.11 go test ./services/attempt-service/... - Result: passed with
-count=1. - Evidence: attempt-service HTTP, use-case, notification-sink, profile-sink, analytics-sink, and repository packages passed.
- Command:
GOTOOLCHAIN=go1.25.11 go test ./services/notification-service/... - Result: passed with
-count=1. - Evidence: notification-service auditsink, HTTP, repository, and use-case packages passed, including replay and preference tests.
- Command:
make test-notification-routes test-parent-alert-routes test-attempt-routes - Result: passed.
- Evidence: default route tables stayed legacy and all three non-default notification, parent-alert, and attempt rehearsal tables passed method/order/auth guards.
- Required verification: the three commands above match both
workflowVerificationandrequiredVerificationforSVC-020-notification-recipient-route-proof.
Current Reverification (2026-07-13)
- Command:
GOTOOLCHAIN=go1.25.11 go test ./services/attempt-service/... -count=1,GOTOOLCHAIN=go1.25.11 go test ./services/notification-service/... -count=1, andmake test-notification-routes test-parent-alert-routes test-attempt-routes. - Result: passed. This covered the attempt producer profile/notification sinks, notification replay and preference packages, and the notification, parent-alert, and attempt default/non-default route guards.
- Runtime: a disposable
hoctapaz-notification-proof-20260713Compose project created only two explicit-recipient events: an analytics weak-topic event withparentIds=[parent_svc020_20260713]and an attempt grade-release event withuserId=student_svc020_20260713. Replaying the weak-topic source created0new rows; replaying the attempt source returned the original event id. Artifact:output/qa/notification-recipient-route-proof-20260713/. - Parent Gateway rehearsal: an HS256
PARENTtoken reachedGET /api/parent/alerts,POST /api/parent/alerts/{id}/read, andPOST /api/parent/alerts/read-allthroughdeploy/gateway/routes.parent-alerts-native-localhost-example.jsonon a temporary Gateway. Responses carriedparent-alerts-list/parent-alerts-read/parent-alerts-read-allwithnative_readornative_write; aSTUDENTtoken was rejected with403 AUTH_FORBIDDEN. - Notification Gateway rehearsal: the temporary Gateway then used
deploy/gateway/routes.notifications-native-localhost-example.json. ASTUDENTtoken reached list, detail, mark-read, and preferences routes with the expectednotifications-*native route headers and a persistedreadAtvalue. This proves Gateway actor injection reaches the notification owner without recipient discovery. - Rollback: the same temporary Gateway was restarted with
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.json.GET /api/notificationsimmediately returned route statelegacy_proxy; its502came from the deliberately unreachable proof legacy target, confirming selection returned to the default legacy route rather than native notification-service.
Follow-up Candidate Recheck (2026-07-15)
- Commands:
make test-notification-routes test-parent-alert-routes,NOTIFICATION_EVENT_FANOUT_POSTGRES_CONFIRM=disposable-postgres make test-notification-event-fanout-postgres, andmake test-student-notification-browserall passed. - Browser artifact:
output/qa/student-notification-browser-local-runtime-20260715-034031/used a memory-only notification-service, temporary Gateway table, and Student Vite process. An anonymous list was denied; the browser then listed two owner rows, marked one and all read, and read/wrote preferences through the five expected native candidate routes. The shell list check was1.06 ms, the browser trace was14 ms, and the request trace storesBearer [redacted]only. - Storage artifact:
output/qa/notification-event-fanout-postgres-20260715-034023/revalidated rollback of the source event and recipient rows on an injected second-recipient failure, then exact-once retry/replay behavior in a disposable Postgres container. The browser runtime directory and container were removed; defaultdeploy/gateway/routes.jsonhad no diff.
Required Evidence
- already-resolved-recipient proof: attempt-service profile-sink tests resolve
parent_1through/v1/internal/profiles/parent-recipients/resolve; producer tests verifyGRADE_RELEASEDandGRADE_RELEASED_PARENTevents carry concrete recipient ids, while notification-service event adapters require explicit recipients and do not perform relationship lookup. The current runtime artifact uses supplied parent/student IDs and verifies source replay cannot duplicate their owner rows. - notification route proof:
make test-notification-routes test-parent-alert-routes test-attempt-routespassed. Fresh non-default Gateway calls cover parent alert list/read/read-all withPARENTenforcement plus notification list/detail/read/preferences with actor injection.docs/qa/notification-route-rehearsals.mdanddocs/qa/parent-alert-route-rehearsals.mdretain the bounded route/auth scope. - rollback notes:
deploy/gateway/routes.jsonremains the default rollback table. A temporary live restart proved/api/notificationsreturns tolegacy_proxy; broad notification, parent-alert, attempt, admin inbox, and fallback routes remain legacy.
Route State
- Legacy route: public
/api/notifications*,/api/parent/alerts*,/api/alerts*,/api/admin/inbox*, and attempt routes remain legacy-proxied by default. - Native route: non-default notification and parent-alert route-table rehearsals cover read/preferences and bounded parent alert reads; attempt-service can publish service-local notification events when its sink and profile resolver are configured.
- Gateway state:
public notification, parent-alert, and attempt routes remain default legacy; producer contract is service-local until live/browser and rollback proof exist; no default route table was changed. - Delegated gateway scope: route proof only; default gateway route tables remain orchestrator-owned unless a later slice produces reviewed promotion and rollback evidence.
- Rollback: keep
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.json; unsetNOTIFICATION_SERVICE_URL/NOTIFICATION_SERVICE_BASE_URLand profile resolver URLs to disable producer-side calls without changing core attempt writes.
Data Notes
- Tables touched: only the disposable notification database in the named Compose proof project; no shared or legacy table was read or written.
- Migration run: notification-service migration ran inside
hoctapaz-notification-proof-20260713. - Validation report:
docker compose -p hoctapaz-notification-proof-20260713 -f deploy/docker-compose.yml down -v --remove-orphanscompleted; no project container, volume, or listener remained on5433,8092, or18093.
Risks / Blockers
- The current proof includes a Student inbox/preferences browser candidate, but not notification-bell or parent-alert UI parity; both remain separate promotion gates.
- Parent relationship freshness remains profile-service responsibility; notification-service does not grant parent access or query classroom/profile databases.
- Provider delivery attempts, email/SMS/push runtime dispatch, admin recipient discovery, and support ticket storage remain separate slices.
- This bounded handoff does not complete
notification-serviceor the broader service rollout goal.
Next Step
- Run browser rechecks for the notification bell and parent alert UI against the same non-default route tables, then review any default route promotion independently from recipient discovery and provider delivery work.