diff --git a/.env.example b/.env.example index 346d985b..869e715e 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,8 @@ NODE_ENV=production PORT=3002 NEXT_TELEMETRY_DISABLED=1 UV_THREADPOOL_SIZE=16 +# Non-production preview only; production always returns 404. +HOUSEKEEPING_NEXT_PREVIEW_ENABLED=false # --- HOTEL & URLS --- HOTEL_NAME=EPIC WEB CONTROL diff --git a/docs/superpowers/plans/2026-08-24-housekeeping-inventory-foundation.md b/docs/superpowers/plans/2026-08-24-housekeeping-inventory-foundation.md new file mode 100644 index 00000000..171b4b62 --- /dev/null +++ b/docs/superpowers/plans/2026-08-24-housekeeping-inventory-foundation.md @@ -0,0 +1,1401 @@ +# Housekeeping Inventory & Foundation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Produce the complete 137-page HK migration inventory and a tested, non-production Command Deck foundation without changing the current `/admin` or `/mod` experience. + +**Architecture:** Add a modular `src/features/housekeeping` boundary containing migration evidence, capability-aware contracts, a validated domain registry, shell primitives, and a non-production preview. Existing authentication and ACL data remain authoritative through `getAdminContext()`; no business workflow is migrated in this subproject. + +**Tech Stack:** Node.js `>=26.7.0 <27`, pnpm `11.22.0`, Next.js `16.3.2`, React `19.2.8`, TypeScript `7.0.2`, Vitest `4.1.11`, Zod `4.4.3`, next-intl `4.13.7`, Tailwind CSS `4.3.3`, Biome `2.5.9`. + +**Spec:** `docs/superpowers/specs/2026-08-24-housekeeping-modernization-design.md` + +## Global Constraints + +- Work directly in `E:\Users\simol\Desktop\EpicNext-cms`; do not create a Git worktree or another checkout. +- Preserve all unrelated tracked, deleted, untracked, and committed local work. Stage only the files named by the current task. +- Pull from the configured canonical upstream before execution. This repository has `origin/main` and no `origin/dev`. +- Keep the current `/admin` and `/mod` route behavior unchanged throughout this subproject. +- Do not expose the preview in production. `NODE_ENV=production` must disable it even if the preview flag is set. +- Do not add operational inbox aggregation, entity search, command execution, domain mutations, preferences persistence, redirects, legacy deletion, or database migrations. +- Reuse `getAdminContext()` and the existing `PermissionSet`; do not add rank thresholds or duplicate ACL queries. +- Rank may influence presentation defaults only. Effective capability slugs determine visibility and access. +- Use only semantic `--admin-*` theme tokens for HK chrome. Do not add hard-coded Tailwind palette colors to ordinary HK UI. +- Put source copy under `pages.housekeeping` in `src/messages/en.json` and `src/messages/it.json`; all other locales use the repository's English fallback. +- Keep files focused. Foundation modules must not import `@/lib/db`, `@/actions/*`, or domain page modules. +- Use TDD: observe the requested failing assertion before writing its implementation, then run the narrow test before widening validation. +- Use CRLF-compatible edits and run `git diff --check` before every commit. + +## Locked File Structure + +```text +src/features/housekeeping/ + migration/ + types.ts migration row and evidence types + discover-legacy-pages.ts filesystem-to-route discovery + validate-matrix.ts structural and coverage validation + operations.ts `/admin` dashboard row + people.ts people, support, moderation, all `/mod` rows + content.ts editorial and engagement rows + economy.ts economy and catalog rows + hotel.ts hotel, radio, rooms, studio rows + system.ts configuration, ACL, logs, DevOps rows + matrix.ts aggregate export only + *.test.ts per-domain audit contracts + foundation/ + contracts/ + capability.ts capability requirements + result.ts typed success/error outcomes + domain.ts domain and route manifests + query.ts query contract + command.ts command metadata/runtime contract + search.ts search provider contract + inbox.ts derived work-item/source contract + widget.ts dashboard widget contract + index.ts public contract barrel + capability-context.ts pure capability helpers + server-capability-context.ts `getAdminContext()` adapter + registry.ts manifest validation and visibility + navigation.ts registry-to-shell navigation model + preview-gate.ts non-production flag policy + shell/ + housekeeping-shell.tsx Command Deck frame + domain-rail.tsx six-domain primary navigation + context-nav.tsx active-domain navigation + command-trigger.tsx disabled phase-01 command affordance + operator-summary.tsx authenticated actor summary + page/ + housekeeping-page-shell.tsx standard title/action/content frame + housekeeping-page-state.tsx loading/empty/partial/error states + domains/ + operations/manifest.ts + people/manifest.ts + content/manifest.ts + economy/manifest.ts + hotel/manifest.ts + system/manifest.ts + manifests.ts aggregate of the six manifests +src/app/admin-next/ + layout.tsx global preview gate + page.tsx first-visible-domain redirect + [domain]/layout.tsx capability-filtered Command Deck + [domain]/page.tsx foundation-only domain placeholder +scripts/verify-housekeeping-matrix.ts +``` + +The feature folder contains no database access and no migrated business workflow. The only route added is `/admin-next`, which returns 404 outside an explicitly enabled non-production environment. + +--- + +### Task 1: Legacy page discovery and migration schema + +**Files:** +- Create: `src/features/housekeeping/migration/types.ts` +- Create: `src/features/housekeeping/migration/discover-legacy-pages.ts` +- Create: `src/features/housekeeping/migration/validate-matrix.ts` +- Test: `src/features/housekeeping/migration/discover-legacy-pages.test.ts` +- Test: `src/features/housekeeping/migration/validate-matrix.test.ts` + +**Interfaces:** +- Consumes: filesystem roots `src/app/admin` and `src/app/mod`. +- Produces: `discoverLegacyPages(rootDir?: string): LegacyPage[]`, `ownedLegacyPages(prefixes: readonly string[], pages?: readonly LegacyPage[]): LegacyPage[]`, `validateMigrationEntries(discovered: readonly LegacyPage[], entries: readonly MigrationEntry[]): string[]`, and the migration types used by Tasks 2-7. + +- [ ] **Step 1: Write the failing discovery tests** + +```ts +import { describe, expect, it } from "vitest"; +import { discoverLegacyPages } from "./discover-legacy-pages"; + +describe("discoverLegacyPages", () => { + it("discovers the exact legacy administration inventory", () => { + const pages = discoverLegacyPages(); + expect(pages).toHaveLength(137); + expect(pages).toContainEqual({ + surface: "admin", + legacyPath: "/admin/users/:id/edit", + sourceFile: "src/app/admin/users/[id]/edit/page.tsx", + }); + expect(pages).toContainEqual({ + surface: "mod", + legacyPath: "/mod/cfh/:id", + sourceFile: "src/app/mod/cfh/[id]/page.tsx", + }); + }); + + it("sorts by surface, route, then source file", () => { + const pages = discoverLegacyPages(); + expect(pages).toEqual([...pages].sort((a, b) => + `${a.surface}:${a.legacyPath}:${a.sourceFile}`.localeCompare( + `${b.surface}:${b.legacyPath}:${b.sourceFile}`, + ), + )); + }); +}); +``` + +- [ ] **Step 2: Run the discovery test and confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/discover-legacy-pages.test.ts` + +Expected: FAIL because `discover-legacy-pages.ts` does not exist. + +- [ ] **Step 3: Define the exact migration types** + +```ts +export const HOUSEKEEPING_DOMAIN_IDS = [ + "operations", + "people", + "content", + "economy", + "hotel", + "system", +] as const; + +export type HousekeepingDomainId = (typeof HOUSEKEEPING_DOMAIN_IDS)[number]; +export type LegacySurface = "admin" | "mod"; +export type MigrationDecision = "REHOST" | "REBUILD" | "MERGE" | "REMOVE"; +export type AuditRequirement = "NONE" | "MUTATION" | "PRIVILEGED_MUTATION"; +export type AuditState = "UNAUDITED" | "PARTIAL" | "COMPLETE"; +export type MigrationStatus = "PLANNED" | "IN_PROGRESS" | "VERIFIED" | "REMOVED"; + +export interface LegacyPage { + surface: LegacySurface; + legacyPath: string; + sourceFile: string; +} + +export interface MigrationEntry extends LegacyPage { + targetDomain: HousekeepingDomainId; + targetPath: string | null; + decision: MigrationDecision; + capabilities: { read: readonly string[]; mutate: readonly string[] }; + dependencies: { queries: readonly string[]; mutations: readonly string[] }; + auditRequirement: AuditRequirement; + localization: AuditState; + accessibility: AuditState; + requiredTests: readonly ("unit" | "integration" | "e2e" | "visual")[]; + parityEvidence: readonly string[]; + status: MigrationStatus; + notes: readonly string[]; +} +``` + +- [ ] **Step 4: Implement deterministic route discovery** + +Use `readdirSync(..., { withFileTypes: true })`, recurse only below the two locked roots, retain `page.tsx`, remove route-group segments such as `(group)`, and convert `[id]` to `:id` and `[...slug]` to `:slug*`. Normalize every file path with `/` separators before sorting. + +```ts +export function discoverLegacyPages(rootDir = process.cwd()): LegacyPage[] { + return (["admin", "mod"] as const) + .flatMap((surface) => discoverSurface(rootDir, surface)) + .sort((a, b) => + `${a.surface}:${a.legacyPath}:${a.sourceFile}`.localeCompare( + `${b.surface}:${b.legacyPath}:${b.sourceFile}`, + ), + ); +} + +export function ownedLegacyPages( + prefixes: readonly string[], + pages: readonly LegacyPage[] = discoverLegacyPages(), +): LegacyPage[] { + return pages.filter((page) => prefixes.some((prefix) => + page.legacyPath === prefix || page.legacyPath.startsWith(`${prefix}/`), + )); +} +``` + +- [ ] **Step 5: Write the failing validator tests** + +```ts +const page = (legacyPath: string): LegacyPage => ({ + surface: legacyPath.startsWith("/mod") ? "mod" : "admin", + legacyPath, + sourceFile: `src/app${legacyPath}/page.tsx`, +}); + +const entry = (legacyPath: string): MigrationEntry => ({ + ...page(legacyPath), + targetDomain: "system", + targetPath: "/admin/system/example", + decision: "REHOST", + capabilities: { read: ["admin.dashboard"], mutate: [] }, + dependencies: { queries: [], mutations: [] }, + auditRequirement: "NONE", + localization: "COMPLETE", + accessibility: "COMPLETE", + requiredTests: ["unit"], + parityEvidence: [], + status: "PLANNED", + notes: [], +}); + +it("reports missing, duplicate, and unknown legacy rows", () => { + const discovered = [page("/admin"), page("/admin/users")]; + const issues = validateMigrationEntries(discovered, [ + entry("/admin"), + entry("/admin"), + entry("/admin/ghost"), + ]); + expect(issues).toEqual([ + "duplicate legacyPath: /admin", + "missing legacyPath: /admin/users", + "unknown legacyPath: /admin/ghost", + ]); +}); + +it("rejects incomplete decisions", () => { + const issues = validateMigrationEntries([page("/admin")], [ + { ...entry("/admin"), targetPath: null, decision: "REBUILD" }, + ]); + expect(issues).toContain("REBUILD requires targetPath: /admin"); +}); +``` + +- [ ] **Step 6: Implement structural validation** + +Validation must return sorted strings and enforce: + +- one entry for every discovered page and no unknown entry; +- `targetPath === null` only for `REMOVE`; +- non-`REMOVE` targets start with `/admin/`; +- `MERGE` and `REBUILD` declare at least one required test; +- mutation dependencies require mutation capabilities and non-`NONE` audit; +- `VERIFIED` requires non-empty parity evidence; +- no string field contains `TBD`, `TODO`, `FIXME`, or `UNCLASSIFIED`. + +- [ ] **Step 7: Run the narrow tests** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/discover-legacy-pages.test.ts src/features/housekeeping/migration/validate-matrix.test.ts` + +Expected: PASS with 137 discovered pages. + +- [ ] **Step 8: Commit Task 1** + +```powershell +git add src/features/housekeeping/migration/types.ts src/features/housekeeping/migration/discover-legacy-pages.ts src/features/housekeeping/migration/validate-matrix.ts src/features/housekeeping/migration/discover-legacy-pages.test.ts src/features/housekeeping/migration/validate-matrix.test.ts +git diff --cached --check +git commit -m "test: define housekeeping migration inventory" +``` + +### Task 2: Operations and People migration audit + +**Files:** +- Create: `src/features/housekeeping/migration/operations.ts` +- Create: `src/features/housekeeping/migration/people.ts` +- Test: `src/features/housekeeping/migration/operations.test.ts` +- Test: `src/features/housekeeping/migration/people.test.ts` + +**Interfaces:** +- Consumes: `discoverLegacyPages()`, `MigrationEntry`, and `validateMigrationEntries()` from Task 1. +- Produces: `operationsMigrationEntries` and `peopleMigrationEntries` with one reviewed row for every owned page. + +Owned prefixes are exact: + +```ts +const PEOPLE_PREFIXES = [ + "/admin/users", "/admin/online", "/admin/guilds", "/admin/applications", + "/admin/teams", "/admin/bans", "/admin/ip", "/admin/vpn", + "/admin/wordfilter", "/admin/moderation", "/admin/tickets", + "/admin/help-tickets", "/mod", +] as const; +``` + +Operations owns only `/admin` in this subproject. + +- [ ] **Step 1: Write failing ownership tests** + +```ts +it("covers the legacy dashboard once", () => { + expect(operationsMigrationEntries).toHaveLength(1); + expect(operationsMigrationEntries[0]).toMatchObject({ + legacyPath: "/admin", + targetDomain: "operations", + targetPath: "/admin/work", + decision: "REBUILD", + status: "PLANNED", + }); +}); + +it("merges every mod page into a people workflow", () => { + const modRows = peopleMigrationEntries.filter((row) => row.surface === "mod"); + expect(modRows).toHaveLength(13); + expect(modRows.every((row) => row.decision === "MERGE")).toBe(true); + expect(modRows.every((row) => row.targetPath?.startsWith("/admin/people/"))) + .toBe(true); +}); +``` + +- [ ] **Step 2: Run the tests and confirm missing audit modules** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/operations.test.ts src/features/housekeeping/migration/people.test.ts` + +Expected: FAIL because both entry arrays are missing. + +- [ ] **Step 3: Audit Operations** + +Read `src/app/admin/page.tsx` and record its real Drizzle query dependencies, current localization state, current accessibility state, and required parity checks. Use this locked decision: + +```ts +{ + surface: "admin", + legacyPath: "/admin", + sourceFile: "src/app/admin/page.tsx", + targetDomain: "operations", + targetPath: "/admin/work", + decision: "REBUILD", + capabilities: { read: [PERMS.ADMIN_DASHBOARD], mutate: [] }, + dependencies: { + queries: ["users", "active bans", "website articles", "staff activities"], + mutations: [], + }, + auditRequirement: "NONE", + localization: "COMPLETE", + accessibility: "PARTIAL", + requiredTests: ["integration", "e2e", "visual"], + parityEvidence: [], + status: "PLANNED", + notes: ["Replace metric dashboard with capability-derived operational home"], +} +``` + +- [ ] **Step 4: Audit People route groups in small batches** + +Review these batches separately so every row names actual imported actions/services: + +1. users, multi-account aliases, online, guilds, applications; +2. teams, moderation, CFH, bans, IP, VPN, word filter; +3. tickets and help tickets; +4. all 13 `/mod` pages. + +Primary `/admin` workflows may be `REHOST` only when their current UI, ACL, localization, and service boundary are already sound. Duplicate show/edit aliases and every `/mod` route are `MERGE`. Targets use `/admin/people/users`, `/admin/people/community`, `/admin/people/staff`, `/admin/people/moderation`, or `/admin/people/support` plus stable `:id` suffixes. + +- [ ] **Step 5: Add per-domain validation** + +```ts +const expected = discoverLegacyPages().filter( + (page) => page.legacyPath === "/admin" || + PEOPLE_PREFIXES.some((prefix) => + page.legacyPath === prefix || page.legacyPath.startsWith(`${prefix}/`), + ), +); +expect(validateMigrationEntries(expected, [ + ...operationsMigrationEntries, + ...peopleMigrationEntries, +])).toEqual([]); +``` + +- [ ] **Step 6: Run and commit the audit** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/operations.test.ts src/features/housekeeping/migration/people.test.ts` + +Expected: PASS; exactly 13 `mod` rows are `MERGE`. + +```powershell +git add src/features/housekeeping/migration/operations.ts src/features/housekeeping/migration/people.ts src/features/housekeeping/migration/operations.test.ts src/features/housekeeping/migration/people.test.ts +git diff --cached --check +git commit -m "docs: audit housekeeping people workflows" +``` + +### Task 3: Content and Engagement migration audit + +**Files:** +- Create: `src/features/housekeeping/migration/content.ts` +- Test: `src/features/housekeeping/migration/content.test.ts` + +**Interfaces:** +- Consumes: Task 1 discovery and validation. +- Produces: `contentMigrationEntries` for every path below the locked prefixes. + +```ts +const CONTENT_PREFIXES = [ + "/admin/articles", "/admin/photos", "/admin/media", "/admin/banners", + "/admin/ads", "/admin/events", "/admin/polls", "/admin/help-questions", + "/admin/tags", "/admin/prefixes", "/admin/writeable-boxes", + "/admin/email-templates", "/admin/theme", "/admin/favicon", + "/admin/translations", +] as const; +``` + +- [ ] **Step 1: Write the failing coverage and target tests** + +```ts +it("covers every content page without legacy-domain targets", () => { + const expected = ownedLegacyPages(CONTENT_PREFIXES); + expect(validateMigrationEntries(expected, contentMigrationEntries)).toEqual([]); + expect(contentMigrationEntries.every((row) => + row.targetPath === null || row.targetPath.startsWith("/admin/content/"), + )).toBe(true); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/content.test.ts` + +Expected: FAIL because `contentMigrationEntries` is missing. + +- [ ] **Step 3: Audit editorial and media batches** + +Inspect collection, create, and detail pages for articles, photos, media, banners, and ads. Record actual action/service imports in `dependencies`; preserve `:id` in target paths. Classify duplicate create/detail variants as `MERGE` only when one target workflow owns the same state. + +- [ ] **Step 4: Audit engagement and help-content batches** + +Inspect events, polls, help questions, tags, prefixes, writable boxes, and email templates. Targets use `/admin/content/editorial`, `/admin/content/media`, `/admin/content/engagement`, or `/admin/content/help`. + +- [ ] **Step 5: Audit brand and localization batches** + +Inspect theme, favicon, and translations. Targets use `/admin/content/brand` and `/admin/content/localization`. Mark mutation pages `MUTATION` or `PRIVILEGED_MUTATION` based on whether they affect global runtime configuration. + +- [ ] **Step 6: Run and commit the audit** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/content.test.ts` + +Expected: PASS with every content-owned legacy page represented once. + +```powershell +git add src/features/housekeeping/migration/content.ts src/features/housekeeping/migration/content.test.ts +git diff --cached --check +git commit -m "docs: audit housekeeping content workflows" +``` + +### Task 4: Economy and Catalog migration audit + +**Files:** +- Create: `src/features/housekeeping/migration/economy.ts` +- Test: `src/features/housekeeping/migration/economy.test.ts` + +**Interfaces:** +- Consumes: Task 1 discovery and validation. +- Produces: `economyMigrationEntries` for the locked economy prefixes. + +```ts +const ECONOMY_PREFIXES = [ + "/admin/catalog", "/admin/items", "/admin/shop", "/admin/marketplace", + "/admin/transactions", "/admin/vouchers", "/admin/subscriptions", + "/admin/rare-values", "/admin/badges", "/admin/achievements", + "/admin/sounds", "/admin/calendar", +] as const; +``` + +- [ ] **Step 1: Write the failing economy audit test** + +```ts +it("requires privileged audit for economy mutations", () => { + const mutationRows = economyMigrationEntries.filter( + (row) => row.dependencies.mutations.length > 0, + ); + expect(mutationRows.length).toBeGreaterThan(0); + expect(mutationRows.every( + (row) => row.auditRequirement === "PRIVILEGED_MUTATION", + )).toBe(true); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/economy.test.ts` + +Expected: FAIL because the audit module is missing. + +- [ ] **Step 3: Audit catalog and item workflows** + +Inspect catalog collection/detail/builder-club/maintenance and item collection/detail pages. Record direct database access separately from service/action dependencies. Target `/admin/economy/catalog` and `/admin/economy/items`; mark overlapping editors `MERGE` when they mutate the same catalog entity. + +- [ ] **Step 4: Audit commerce and value workflows** + +Inspect shop, marketplace, transactions, vouchers, subscriptions, rare values, badges, achievements, sounds, and calendar. Use `/admin/economy/commerce`, `/admin/economy/history`, `/admin/economy/value`, and `/admin/economy/rewards` targets. + +- [ ] **Step 5: Validate the domain inventory** + +```ts +expect(validateMigrationEntries( + ownedLegacyPages(ECONOMY_PREFIXES), + economyMigrationEntries, +)).toEqual([]); +``` + +- [ ] **Step 6: Run and commit the audit** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/economy.test.ts` + +Expected: PASS and every economy mutation row requires privileged audit. + +```powershell +git add src/features/housekeeping/migration/economy.ts src/features/housekeeping/migration/economy.test.ts +git diff --cached --check +git commit -m "docs: audit housekeeping economy workflows" +``` + +### Task 5: Hotel and World migration audit + +**Files:** +- Create: `src/features/housekeeping/migration/hotel.ts` +- Test: `src/features/housekeeping/migration/hotel.test.ts` + +**Interfaces:** +- Consumes: Task 1 discovery and validation. +- Produces: `hotelMigrationEntries` for rooms, navigator, radio, and studio. + +```ts +const HOTEL_PREFIXES = [ + "/admin/rooms", "/admin/navigation", "/admin/radio", "/admin/studio", +] as const; +``` + +- [ ] **Step 1: Write the failing hotel coverage test** + +```ts +it("maps all hotel tools below the hotel target root", () => { + expect(validateMigrationEntries( + ownedLegacyPages(HOTEL_PREFIXES), + hotelMigrationEntries, + )).toEqual([]); + expect(hotelMigrationEntries.every((row) => + row.targetPath === null || row.targetPath.startsWith("/admin/hotel/"), + )).toBe(true); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/hotel.test.ts` + +Expected: FAIL because the hotel audit module is missing. + +- [ ] **Step 3: Audit rooms and navigator** + +Inspect room list/show/edit/furni paths and navigator management. Merge duplicate room detail aliases into `/admin/hotel/rooms/:id`; keep furni as `/admin/hotel/rooms/:id/furni`. + +- [ ] **Step 4: Audit radio and studio** + +Inspect all radio and studio pages, including API keys, monitoring, moderation, imports, sync, maintenance, and audit surfaces. Record external RCON/API/filesystem dependencies explicitly and mark credential or runtime mutation pages `PRIVILEGED_MUTATION`. + +- [ ] **Step 5: Run and commit the audit** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/hotel.test.ts` + +Expected: PASS with all owned pages represented once. + +```powershell +git add src/features/housekeeping/migration/hotel.ts src/features/housekeeping/migration/hotel.test.ts +git diff --cached --check +git commit -m "docs: audit housekeeping hotel workflows" +``` + +### Task 6: System audit and complete 137-row matrix + +**Files:** +- Create: `src/features/housekeeping/migration/system.ts` +- Create: `src/features/housekeeping/migration/matrix.ts` +- Create: `scripts/verify-housekeeping-matrix.ts` +- Modify: `package.json: scripts` +- Test: `src/features/housekeeping/migration/system.test.ts` +- Test: `src/features/housekeeping/migration/matrix.test.ts` + +**Interfaces:** +- Consumes: all migration arrays from Tasks 2-5. +- Produces: `HOUSEKEEPING_MIGRATION_MATRIX` and the `pnpm hk:matrix:check` CLI. + +```ts +const SYSTEM_PREFIXES = [ + "/admin/alerts", "/admin/analytics", "/admin/commandocentrum", + "/admin/devops", "/admin/emulator", "/admin/housekeeping", + "/admin/logs", "/admin/maintenance", "/admin/menu", + "/admin/permissions", "/admin/settings", +] as const; +``` + +- [ ] **Step 1: Write the failing system decisions** + +```ts +it("removes legacy foundation-owned pages", () => { + expect(systemMigrationEntries).toEqual(expect.arrayContaining([ + expect.objectContaining({ legacyPath: "/admin/housekeeping", decision: "REMOVE", targetPath: null }), + expect.objectContaining({ legacyPath: "/admin/menu", decision: "REMOVE", targetPath: null }), + ])); +}); + +it("keeps live permissions as a system workflow", () => { + expect(systemMigrationEntries).toContainEqual(expect.objectContaining({ + legacyPath: "/admin/permissions", + targetPath: "/admin/system/access/permissions", + decision: expect.not.stringMatching("REMOVE"), + })); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/system.test.ts` + +Expected: FAIL because `systemMigrationEntries` is missing. + +- [ ] **Step 3: Audit system route groups** + +Inspect alerts, analytics, command center, DevOps, emulator, logs, maintenance, permissions, and settings. Targets use `/admin/system/access`, `/admin/system/configuration`, `/admin/system/observability`, or `/admin/system/operations`. Preserve the read-only value of the legacy HK export/comparison in the row notes even though its route decision is `REMOVE`. + +- [ ] **Step 4: Write the failing aggregate coverage test** + +```ts +it("covers all 137 legacy pages exactly once", () => { + const discovered = discoverLegacyPages(); + expect(HOUSEKEEPING_MIGRATION_MATRIX).toHaveLength(137); + expect(validateMigrationEntries( + discovered, + HOUSEKEEPING_MIGRATION_MATRIX, + )).toEqual([]); +}); + +it("contains no undecided evidence markers", () => { + expect(JSON.stringify(HOUSEKEEPING_MIGRATION_MATRIX)).not.toMatch( + /TBD|TODO|FIXME|UNCLASSIFIED/, + ); +}); +``` + +- [ ] **Step 5: Aggregate the six arrays without adding behavior** + +```ts +export const HOUSEKEEPING_MIGRATION_MATRIX = [ + ...operationsMigrationEntries, + ...peopleMigrationEntries, + ...contentMigrationEntries, + ...economyMigrationEntries, + ...hotelMigrationEntries, + ...systemMigrationEntries, +].sort((a, b) => a.legacyPath.localeCompare(b.legacyPath)); +``` + +- [ ] **Step 6: Add the CLI verifier** + +`scripts/verify-housekeeping-matrix.ts` calls `validateMigrationEntries(discoverLegacyPages(), HOUSEKEEPING_MIGRATION_MATRIX)`. It prints every returned issue to stderr and sets `process.exitCode = 1`; a valid matrix prints `Housekeeping migration matrix: 137/137 valid`. Add: + +```json +"hk:matrix:check": "tsx scripts/verify-housekeeping-matrix.ts" +``` + +- [ ] **Step 7: Run and commit the complete matrix** + +Run: + +```powershell +pnpm exec vitest run src/features/housekeeping/migration +pnpm hk:matrix:check +``` + +Expected: all migration tests PASS and CLI prints `137/137 valid`. + +```powershell +git add src/features/housekeeping/migration/system.ts src/features/housekeeping/migration/matrix.ts src/features/housekeeping/migration/system.test.ts src/features/housekeeping/migration/matrix.test.ts scripts/verify-housekeeping-matrix.ts package.json +git diff --cached --check +git commit -m "docs: complete housekeeping migration matrix" +``` + +### Task 7: Foundation contract types + +**Files:** +- Create: `src/features/housekeeping/foundation/contracts/capability.ts` +- Create: `src/features/housekeeping/foundation/contracts/result.ts` +- Create: `src/features/housekeeping/foundation/contracts/domain.ts` +- Create: `src/features/housekeeping/foundation/contracts/query.ts` +- Create: `src/features/housekeeping/foundation/contracts/command.ts` +- Create: `src/features/housekeeping/foundation/contracts/search.ts` +- Create: `src/features/housekeeping/foundation/contracts/inbox.ts` +- Create: `src/features/housekeeping/foundation/contracts/widget.ts` +- Create: `src/features/housekeeping/foundation/contracts/index.ts` +- Test: `src/features/housekeeping/foundation/contracts/contracts.test.ts` + +**Interfaces:** +- Consumes: `HousekeepingDomainId` from migration types only. +- Produces: every public foundation type used by Tasks 8-11. + +- [ ] **Step 1: Write failing constructor tests** + +```ts +it("creates typed success and error results", () => { + expect(ok({ count: 2 }, "corr-1")).toEqual({ + ok: true, + data: { count: 2 }, + correlationId: "corr-1", + }); + expect(fail("CAPABILITY_DENIED", "corr-2")).toEqual({ + ok: false, + error: { code: "CAPABILITY_DENIED" }, + correlationId: "corr-2", + }); +}); + +it("rejects an empty capability requirement", () => { + expect(() => anyCapability()).toThrow("capability requirement is empty"); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/foundation/contracts/contracts.test.ts` + +Expected: FAIL because the contract barrel does not exist. + +- [ ] **Step 3: Define capability and result contracts** + +```ts +export type CapabilityRequirement = + | { mode: "all"; slugs: readonly string[] } + | { mode: "any"; slugs: readonly string[] }; + +export interface HousekeepingActor { + id: number; + username: string; + rank: number; +} + +export interface HousekeepingCapabilityContext { + actor: HousekeepingActor; + isSuperAdmin: boolean; + has(slug: string): boolean; + hasAny(...slugs: string[]): boolean; + hasAll(...slugs: string[]): boolean; +} + +export type HousekeepingErrorCode = + | "VALIDATION_FAILED" | "AUTHENTICATION_REQUIRED" | "CAPABILITY_DENIED" + | "NOT_FOUND" | "CONFLICT" | "DEPENDENCY_UNAVAILABLE" + | "PARTIAL_RESULT" | "INTERNAL_ERROR"; +``` + +- [ ] **Step 4: Define domain, query, and command contracts** + +```ts +export interface HousekeepingRouteDefinition { + id: string; + labelKey: string; + href: string; + capability: CapabilityRequirement; + matchPrefixes?: readonly string[]; +} + +export interface HousekeepingDomainManifest { + id: HousekeepingDomainId; + labelKey: string; + descriptionKey: string; + iconId: "inbox" | "users" | "file-text" | "gem" | "hotel" | "settings"; + previewHref: `/admin-next/${HousekeepingDomainId}`; + capability: CapabilityRequirement; + routes: readonly HousekeepingRouteDefinition[]; +} + +export interface HousekeepingQuery { + id: string; + owner: HousekeepingDomainId; + capability: CapabilityRequirement; + run(context: HousekeepingCapabilityContext, input: I): Promise>; +} + +export interface HousekeepingCommand { + id: string; + owner: HousekeepingDomainId; + risk: "safe" | "sensitive"; + capability: CapabilityRequirement; + requiresReason: boolean; + execute(context: HousekeepingCapabilityContext, input: I): Promise>; +} +``` + +- [ ] **Step 5: Define search, inbox, and widget contracts** + +Use these exact public methods: + +- `HousekeepingSearchProvider.search(context, { term, limit }): Promise>` +- `HousekeepingInboxSource.getItems(context, signal): Promise>` +- `HousekeepingWidgetDefinition.load(context): Promise>` + +`HousekeepingWorkItem` contains `sourceId`, `itemId`, `deduplicationKey`, `domain`, `capability`, `severity`, `occurredAt`, `titleKey`, `context`, `href`, and `freshness`. No assignment, dismissal, completion, or task-owner field is permitted. + +- [ ] **Step 6: Run tests and typecheck the public barrel** + +Run: + +```powershell +pnpm exec vitest run src/features/housekeeping/foundation/contracts/contracts.test.ts +pnpm typecheck +``` + +Expected: PASS; no type or unused-export errors. + +- [ ] **Step 7: Commit Task 7** + +```powershell +git add src/features/housekeeping/foundation/contracts +git diff --cached --check +git commit -m "feat: define housekeeping foundation contracts" +``` + +### Task 8: Request-scoped capability context + +**Files:** +- Create: `src/features/housekeeping/foundation/capability-context.ts` +- Create: `src/features/housekeeping/foundation/server-capability-context.ts` +- Test: `src/features/housekeeping/foundation/capability-context.test.ts` +- Test: `src/features/housekeeping/foundation/server-capability-context.test.ts` + +**Interfaces:** +- Consumes: `PermissionSet`, `getAdminContext()`, and Task 7 capability contracts. +- Produces: `createHousekeepingCapabilityContext()`, `satisfiesCapability()`, and cached `getHousekeepingCapabilityContext()`. + +- [ ] **Step 1: Write pure failing capability tests** + +```ts +const actor: HousekeepingActor = { id: 42, username: "operator", rank: 7 }; + +const permissionSet = ( + slugs: readonly string[], + isSuperAdmin = false, +): PermissionSet => { + const granted = new Set(slugs); + const has = (slug: string) => isSuperAdmin || granted.has(slug); + return { + isSuperAdmin, + has, + hasAny: (...requested) => requested.some(has), + hasAll: (...requested) => requested.every(has), + }; +}; + +const superAdminPermissionSet = () => permissionSet([], true); + +it("evaluates any/all requirements from effective permission methods", () => { + const context = createHousekeepingCapabilityContext(actor, permissionSet([ + "admin.users.view", + "admin.tickets.view", + ])); + expect(satisfiesCapability(context, anyCapability("admin.users.view", "admin.logs.view"))).toBe(true); + expect(satisfiesCapability(context, allCapabilities("admin.users.view", "admin.logs.view"))).toBe(false); +}); + +it("keeps the super administrator explicit", () => { + const context = createHousekeepingCapabilityContext(actor, superAdminPermissionSet()); + expect(context.isSuperAdmin).toBe(true); + expect(context.has("unknown.future.slug")).toBe(true); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/foundation/capability-context.test.ts` + +Expected: FAIL because the helper module is missing. + +- [ ] **Step 3: Implement the pure adapter** + +```ts +export function createHousekeepingCapabilityContext( + actor: HousekeepingActor, + permissions: PermissionSet, +): HousekeepingCapabilityContext { + return { + actor, + isSuperAdmin: permissions.isSuperAdmin, + has: (slug) => permissions.has(slug), + hasAny: (...slugs) => permissions.hasAny(...slugs), + hasAll: (...slugs) => permissions.hasAll(...slugs), + }; +} +``` + +`satisfiesCapability()` returns true for super administrators, then delegates to `hasAny` or `hasAll`; an empty requirement is impossible because Task 7 constructors throw. + +- [ ] **Step 4: Write the failing server adapter test** + +Mock `@/lib/permissions` and assert that the adapter calls `getAdminContext()` once, uses the database-refreshed `session.user` actor, and forwards the returned `PermissionSet` without querying the database directly. + +```ts +expect(await getHousekeepingCapabilityContext()).toMatchObject({ + actor: { id: 42, username: "operator", rank: 7 }, + isSuperAdmin: false, +}); +expect(getAdminContext).toHaveBeenCalledTimes(1); +``` + +- [ ] **Step 5: Implement the cached server adapter** + +```ts +export const getHousekeepingCapabilityContext = cache(async () => { + const { session, permissions } = await getAdminContext(); + return createHousekeepingCapabilityContext( + { + id: session.user.id, + username: session.user.username, + rank: session.user.rank, + }, + permissions, + ); +}); +``` + +- [ ] **Step 6: Run authorization contracts and commit** + +Run: + +```powershell +pnpm exec vitest run src/features/housekeeping/foundation/capability-context.test.ts src/features/housekeeping/foundation/server-capability-context.test.ts src/lib/admin/authorization-contract.test.ts src/lib/admin/guard.test.ts +pnpm typecheck +``` + +Expected: PASS; central authorization still contains no fixed rank threshold. + +```powershell +git add src/features/housekeeping/foundation/capability-context.ts src/features/housekeeping/foundation/server-capability-context.ts src/features/housekeeping/foundation/capability-context.test.ts src/features/housekeeping/foundation/server-capability-context.test.ts +git diff --cached --check +git commit -m "feat: add housekeeping capability context" +``` + +### Task 9: Domain registry, manifests, and navigation model + +**Files:** +- Create: `src/features/housekeeping/foundation/registry.ts` +- Create: `src/features/housekeeping/foundation/navigation.ts` +- Create: `src/features/housekeeping/domains/operations/manifest.ts` +- Create: `src/features/housekeeping/domains/people/manifest.ts` +- Create: `src/features/housekeeping/domains/content/manifest.ts` +- Create: `src/features/housekeeping/domains/economy/manifest.ts` +- Create: `src/features/housekeeping/domains/hotel/manifest.ts` +- Create: `src/features/housekeeping/domains/system/manifest.ts` +- Create: `src/features/housekeeping/manifests.ts` +- Modify: `src/messages/en.json: pages` +- Modify: `src/messages/it.json: pages` +- Test: `src/features/housekeeping/foundation/registry.test.ts` +- Test: `src/features/housekeeping/foundation/navigation.test.ts` +- Test: `src/features/housekeeping/foundation/localization-contract.test.ts` + +**Interfaces:** +- Consumes: Task 7 manifests and Task 8 `satisfiesCapability()`. +- Produces: `HOUSEKEEPING_MANIFESTS`, `createHousekeepingRegistry()`, and `buildHousekeepingNavigation()`. + +- [ ] **Step 1: Write failing registry validation tests** + +```ts +const manifest = ( + id: HousekeepingDomainId, + capabilitySlug = PERMS.ADMIN_DASHBOARD, +): HousekeepingDomainManifest => ({ + id, + labelKey: `pages.housekeeping.domains.${id}.title`, + descriptionKey: `pages.housekeeping.domains.${id}.description`, + iconId: "settings", + previewHref: `/admin-next/${id}`, + capability: anyCapability(capabilitySlug), + routes: [], +}); + +const duplicateA = manifest("operations"); +const duplicateB = manifest("operations"); +const duplicateRouteManifest: HousekeepingDomainManifest = { + ...manifest("people"), + routes: [ + { id: "one", labelKey: "one", href: "/admin-next/people/users", capability: anyCapability(PERMS.ADMIN_DASHBOARD) }, + { id: "two", labelKey: "two", href: "/admin-next/people/users", capability: anyCapability(PERMS.ADMIN_DASHBOARD) }, + ], +}; +const unknownPermissionManifest = manifest("system", "admin.ghost.view"); + +it("rejects duplicate domains, routes, and unknown capability slugs", () => { + expect(() => createHousekeepingRegistry([duplicateA, duplicateB])) + .toThrow(/duplicate domain id/); + expect(() => createHousekeepingRegistry([duplicateRouteManifest])) + .toThrow(/duplicate route href/); + expect(() => createHousekeepingRegistry([unknownPermissionManifest])) + .toThrow(/unknown capability slug: admin\.ghost\.view/); +}); + +it("registers exactly the six approved domains", () => { + const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS); + expect(registry.domains.map((domain) => domain.id)).toEqual([ + "operations", "people", "content", "economy", "hotel", "system", + ]); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/foundation/registry.test.ts` + +Expected: FAIL because registry and manifests are missing. + +- [ ] **Step 3: Implement registry validation** + +Build the known permission catalog from `Object.values(PERMS)`. Validate domain IDs, preview hrefs, route IDs, route hrefs, non-empty label keys, and every `CapabilityRequirement` slug. Freeze the returned domain array. + +- [ ] **Step 4: Define the six empty-workflow manifests** + +Each manifest has no migrated business route; only its preview href and domain capability are present. Use these capability groups: + +- operations: `admin.dashboard`; +- people: any user, moderation, ticket, ban, or `mod.*` view capability; +- content: any news, pages, banners, events, polls, or prefixes view capability; +- economy: any catalog or shop view capability; +- hotel: any room, radio, or asset-import capability; +- system: any settings, logs, analytics, DevOps, notifications, permissions, or RCON capability. + +- [ ] **Step 5: Add English and Italian source copy** + +Under `pages.housekeeping`, add `preview`, `navigation`, `domains`, and `states`. Domain keys are `operations`, `people`, `content`, `economy`, `hotel`, and `system`, each with `title` and `description`. Add explicit strings for preview badge, disabled command trigger, back to site, empty foundation state, partial state, error state, and loading state. + +- [ ] **Step 6: Implement and test the navigation projection** + +```ts +export interface HousekeepingNavigationDomain { + id: HousekeepingDomainId; + href: string; + iconId: HousekeepingDomainManifest["iconId"]; + label: string; + description: string; + items: readonly { id: string; href: string; label: string }[]; +} +``` + +`buildHousekeepingNavigation(registry, context, translate)` filters unauthorized domains and routes before translating. Tests must prove a moderator without `admin.dashboard` can see People when a `mod.*` capability is present, while Economy stays hidden. + +- [ ] **Step 7: Verify localization keys** + +The localization contract reads `en.json` and `it.json`, resolves every manifest key by dot path, and fails on missing or non-string values. + +- [ ] **Step 8: Run and commit Task 9** + +Run: + +```powershell +pnpm exec vitest run src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/localization-contract.test.ts +pnpm typecheck +``` + +Expected: PASS; six domains in locked order and all manifest keys present in English and Italian. + +```powershell +git add src/features/housekeeping/foundation/registry.ts src/features/housekeeping/foundation/navigation.ts src/features/housekeeping/domains src/features/housekeeping/manifests.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/localization-contract.test.ts src/messages/en.json src/messages/it.json +git diff --cached --check +git commit -m "feat: add housekeeping domain registry" +``` + +### Task 10: Command Deck shell and standard page states + +**Files:** +- Create: `src/features/housekeeping/foundation/shell/housekeeping-shell.tsx` +- Create: `src/features/housekeeping/foundation/shell/domain-rail.tsx` +- Create: `src/features/housekeeping/foundation/shell/context-nav.tsx` +- Create: `src/features/housekeeping/foundation/shell/command-trigger.tsx` +- Create: `src/features/housekeeping/foundation/shell/operator-summary.tsx` +- Create: `src/features/housekeeping/foundation/page/housekeeping-page-shell.tsx` +- Create: `src/features/housekeeping/foundation/page/housekeeping-page-state.tsx` +- Test: `src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx` +- Test: `src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx` + +**Interfaces:** +- Consumes: `HousekeepingActor`, `HousekeepingDomainId`, and `HousekeepingNavigationDomain`. +- Produces: pure server-renderable shell/page components with no search, mutation, persistence, or database behavior. + +- [ ] **Step 1: Write the failing shell accessibility test** + +```tsx +const navigation: readonly HousekeepingNavigationDomain[] = [{ + id: "people", + href: "/admin-next/people", + iconId: "users", + label: "People", + description: "Users, moderation, and support", + items: [{ id: "users", href: "/admin-next/people/users", label: "Users" }], +}]; + +it("renders the Command Deck landmarks and active domain", () => { + const html = renderToStaticMarkup( + +

content

+
, + ); + expect(html).toContain('href="#housekeeping-content"'); + expect(html).toContain('aria-current="page"'); + expect(html).toContain('id="housekeeping-content"'); + expect(html).toContain("operator"); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx` + +Expected: FAIL because the shell components are missing. + +- [ ] **Step 3: Implement the shell components** + +Use `Link`, semantic `