# Task 11 — People workflow read models Status: DONE — fix round 2 ## Delivered scope - Added the exact 24-route People catalog covering the 39 migration-matrix entries across users, multi-account review, online/community, guilds, staff applications/teams, support tickets/help tickets, CFH, moderation overview, bans, IP rules, VPN settings, and word filter workflows. - Added canonical, JSON-serializable People DTOs, `/ase/people` link builders, bounded list normalization, and stable sorting with numeric-ID tie breaking. - Added injected query factories and narrow server-only production adapters for user/detail, community/guild, staff/applications/teams, support queues/tickets/help/CFH, and moderation/bans/sanctions sources. - Reused foundation `HousekeepingResult`, error codes, capability context, authorization, and canonical href contracts. People-local `ListInput` and `Page` were added because no shared foundation equivalents exist in this checkout. - Kept the People manifest, global handlers, pages, mutations, providers, widgets, search, and inbox unchanged for Task 12. ## Security and behavior decisions - User mail and current IP remain independently nullable fields. Each is projected only when the capability context contains the existing `PERMS.USERS_VIEW`; `PERMS.MOD_USERS_VIEW` alone receives the safe base projection with both values set to `null`, and a context with neither permission is forbidden. - No new ACL slug or rank threshold was introduced. Staff filtering reuses the existing `getMinStaffRank()` source. - The production user selection is explicit and excludes passwords, authentication tickets, secrets, and two-factor material. VPN settings intentionally exclude `vpn_api_key`. - Adapters fail closed. Malformed driver envelopes, invalid identifiers, corrupt links, non-serializable DTO values, and count failures map to `DEPENDENCY_UNAVAILABLE`. Primary/entity identifiers remain positive safe integers; zero is accepted only for the schema-declared guild `userId`/`roomId` and CFH `senderId`/`reportedId`/`roomId`/`moderatorId` sentinels. Missing valid detail entities map to `NOT_FOUND`; invalid request identifiers map to `VALIDATION`. - Pagination clamps page size to 100 and offset to 10,000. Deterministic primary sorting, numeric-ID tie breaking, and `LIMIT`/`OFFSET` now execute in the database; no list query fetches a prefix for locale re-sorting or second slicing. - Raw production adapters and `buildPeopleUserSelection` are module-private. Runtime exports expose only context-authorized query factories and singleton query surfaces. - Multi-account clusters use one bounded CTE/window page query plus one independent matching-cluster count query, cap accounts per cluster at 100, and never issue one query per IP cluster. - User detail/edit now includes the operator's watched state and canonical permission-rank data. Support ticket reads use the existing unified inbox through a strict, fail-closed, database-paged mode that includes CMS and help-center rows while leaving the legacy tolerant mode unchanged. - The unified `/support/tickets` inbox still merges CMS and help-center rows. The ticket desk now has its own strict CMS-only page loader preserving priority, category, assignee, and message count; help summaries include reply count. Support desk/detail DTOs explicitly include queue counts, bounded staff, and the relevant active ban. - Active bans are filtered before sorting. Expiry `0` remains the permanent-active sentinel; expired rows cannot hide permanent or future-active bans in lists or details. ## Official fix round 1 findings 1. **Authorization boundary:** fixed by making all raw production adapters and the user selection builder module-private and testing only guarded public query surfaces plus source/runtime export contracts. 2. **Pagination and sorting:** fixed by moving declared sort fields, deterministic tie ordering, and bounded `LIMIT`/`OFFSET` to production adapters. The exact `user10`/`user2` and support `status`/`updatedAt` page regressions are covered. 3. **Resource bounds:** fixed by replacing multi-account prefix loading plus per-row `Promise.all` with one bounded batched CTE/window query. No million-row prefix and no N+1 cluster query remain. 4. **Fail-closed database validation:** fixed across People models and community/support/moderation query boundaries. Invalid driver shapes and invalid identifiers cannot become empty lists, `NOT_FOUND`, ID `0`, or corrupt canonical links. 5. **Canonical dependencies:** fixed watched and permission-rank data for user detail/edit; `/support/tickets` now calls strict `fetchUnifiedTicketInbox`; support queue/staff/active-ban data is explicit in canonical query DTOs. 6. **Moderation capability equality:** fixed after direct user authorization. `moderationQuery.capability` now exactly equals the existing eleven-slug overview union already used by the route and `run`; no route, mutation, rank threshold, or new slug changed. 7. **Active bans:** fixed list/detail selection so permanent `ban_expire = 0` and future-active bans are deterministic and expired rows cannot hide them. The two official Minor findings remain parked and unchanged as instructed. ## Official fix round 2 findings 1. **Entity-aware sentinels:** canonical guild DTOs now use the real schema names `userId` and `roomId`. Serialization permits zero only on those two guild fields and the four named CFH fields when the containing DTO has the matching canonical entity href. Generic `*Id` zero values, negatives, unsafe integers, and primary ID zero remain unavailable failures. 2. **Support source fidelity:** `people.support.tickets` remains on strict `fetchUnifiedTicketInbox`. `people.support.ticket-desk` now dispatches to a distinct strict `website_tickets` loader with message aggregation and no help-center source. Real priority/category/assignee/message count and help reply count are present in canonical DTOs; malformed driver rows fail closed. 3. **Multi-account total:** the page CTE and matching-cluster count run as two bounded parallel queries. Empty pages retain the correct total without prefix loading or N+1 queries. ## Strict TDD evidence ### Cycle 1 — exact route catalog RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/routes.test.ts Test Files 1 failed Error: Cannot find module './routes' ``` GREEN: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/routes.test.ts Test Files 1 passed (1) Tests 3 passed (3) ``` ### Cycle 2 — canonical models and normalizers RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/models.test.ts Test Files 1 failed Error: Cannot find module './models' ``` GREEN: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/models.test.ts Test Files 1 passed (1) Tests 5 passed (5) ``` ### Cycle 3 — injected-adapter read queries RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts Test Files 1 failed Error: Cannot find module './community' ``` GREEN: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts Test Files 1 passed (1) Tests 10 passed (10) ``` Production-source contract RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts Test Files 1 failed (1) Tests 2 failed (2) Reason: raw production adapters and buildPeopleUserSelection were exported as bypassable runtime internals. ``` Pagination regression RED after adding the production contract fixture: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts Test Files 1 failed (1) Tests 1 failed | 2 passed (3) Expected ["203.0.113.1", "203.0.113.2"], received ["203.0.113.2"]. ``` GREEN for the original baseline implementation: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts Test Files 1 passed (1) Tests 3 passed (3) ``` The query tests cover adversarial page size/offset/search, stable tie sorting, empty/missing entities, adapter and count failures, PII capability combinations, serializable DTOs, explicit source projection, and production pagination. ## Fix round 1 strict behavioral TDD evidence ### Authorization boundary RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts Test Files 1 failed (1) Tests 2 failed (2) Observed runtime exports: buildPeopleUserSelection and peopleUsersAdapters. ``` GREEN: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts Test Files 1 passed (1) Tests 3 passed (3) ``` ### Database pagination and declared sorting RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/models.test.ts src/features/housekeeping/domains/people/queries/people-queries.test.ts Test Files 2 failed (2) Tests 4 failed | 14 passed (18) Failures: offset 999999 was not capped; DB pages were sliced a second time for user10/user2 and support status/updatedAt. ``` GREEN: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/models.test.ts src/features/housekeeping/domains/people/queries/people-queries.test.ts Test Files 2 passed (2) Tests 18 passed (18) ``` ### Multi-account resource bounds RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts Test Files 1 failed (1) Tests 1 failed | 2 passed (3) Observed prefix result plus one query per IP cluster. ``` GREEN: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts Test Files 1 passed (1) Tests 3 passed (3) ``` ### Fail-closed validation RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/models.test.ts src/features/housekeeping/domains/people/queries/people-queries.test.ts src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts Test Files 3 failed (3) Tests 5 failed | 21 passed (26) Failures covered ID 0, corrupt links/dates, corrupt detail shapes, and malformed driver envelopes. ``` GREEN: ```text same command Test Files 3 passed (3) Tests 26 passed (26) ``` ### Canonical dependencies and unified support inbox RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts -t "watched state|hydrates desk|strict unified" Test Files 2 failed (2) Tests 3 failed | 22 skipped (25) ``` GREEN: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/models.test.ts src/features/housekeeping/domains/people/queries/people-queries.test.ts src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts -t "watched state|hydrates desk|strict unified|normalizes BigInt" Test Files 3 passed (3) Tests 4 passed | 27 skipped (31) ``` ### Moderation capability equality RED captured before direct authorization: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts -t "eleven-permission" Test Files 1 failed (1) Tests 1 failed | 18 skipped (19) Expected the route/run eleven-slug union; query metadata still contains six slugs. ``` GREEN after the user directly authorized only this isolated metadata hunk: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts -t "eleven-permission" Test Files 1 passed (1) Tests 1 passed | 18 skipped (19) ``` ### Active-ban semantics RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts -t "permanent" Test Files 1 failed (1) Tests 1 failed | 4 skipped (5) Expected permanent expiresAt 0; received null. ``` GREEN: ```text same command Test Files 1 passed (1) Tests 1 passed | 4 skipped (5) ``` ## Fix round 2 strict behavioral TDD evidence ### Entity-aware schema sentinels RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts -t "zero sentinels" Test Files 1 failed (1) Tests 2 failed | 19 skipped (21) Valid guild userId/roomId zero and CFH senderId/reportedId/moderatorId/roomId zero were rejected by generic identifier validation. ``` GREEN: ```text same command Test Files 1 passed (1) Tests 2 passed | 19 skipped (21) ``` ### CMS-only ticket desk and help reply counts RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts -t "CMS-only context loader|reply counts" Test Files 2 failed (2) Tests 2 failed | 28 skipped (30) The desk received a help row with synthetic normal priority; help summary omitted replyCount. ``` GREEN: ```text same command Test Files 2 passed (2) Tests 2 passed | 28 skipped (30) ``` ### Independent multi-account total RED: ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts -t "beyond the last page" Test Files 1 failed (1) Tests 1 failed | 8 skipped (9) Expected total 4 on the empty page; received 0. ``` GREEN: ```text same command Test Files 1 passed (1) Tests 1 passed | 8 skipped (9) ``` ## Verification ```text pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/routes.test.ts src/features/housekeeping/domains/people/models.test.ts src/features/housekeeping/domains/people/queries/people-queries.test.ts src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts Test Files 4 passed (4) Tests 40 passed (40) pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people src/features/housekeeping/foundation/foundation-source-contract.test.ts src/features/housekeeping/foundation/authorization.test.ts src/features/housekeeping/foundation/capability-context.test.ts src/features/housekeeping/foundation/server-capability-context.test.ts src/features/housekeeping/foundation/contracts/contracts.test.ts Test Files 9 passed (9) Tests 86 passed (86) pnpm test:housekeeping Test Files 49 passed (49) Tests 435 passed (435) pnpm typecheck tsc --noEmit Exit 0 pnpm exec biome check --formatter-enabled=false <7 exact changed Task 11 TypeScript files> Checked 7 files. No fixes applied. git diff --check Exit 0 ``` Both `pnpm test:housekeeping` and `pnpm typecheck` emitted the environment warning: the repository requires Node `>=26.8.1 <27`, while this host runs Node `v26.7.0` with pnpm `11.24.0`. Tests and typecheck still exited successfully. No database operation, deployment, push, or pull-request update was performed.