docs: plan housekeeping routing recovery
This commit is contained in:
1 parent
f7c9c96478
commit
3da84ffd87
1 file changed
+902
@@ -0,0 +1,902 @@
|
||||
# Housekeeping Foundation and Routing Recovery 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:** Restore a non-production `/ase-next` Housekeeping foundation whose generated navigation, concrete routes, handlers, capability checks, and HTTP access semantics cannot produce the orphaned domain links that caused the reverted cutover to return 404.
|
||||
|
||||
**Architecture:** Keep `/admin` and `/mod` unchanged while replacing the old `/admin-next` preview namespace with a gated `/ase-next` surface. Separate static manifest validation from a runtime route-handler registry, project navigation only from accessible handled routes, and dispatch all preview pages through one deterministic matcher with explicit login, 403, and 404 behavior. This plan intentionally stops before People content; the People vertical receives its own implementation plan after this foundation passes review.
|
||||
|
||||
**Tech Stack:** Next.js 16.3.3 App Router, React 19.2.8 server components, TypeScript 7.0.2, Vitest 4.1.11, next-intl, Biome 2.5.9, pnpm 11.24.0, Node.js 26.8.1.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-30-housekeeping-stepwise-rebuild-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Work only in the canonical checkout on `codex/housekeeping-rebuild-stepwise`; do not use Git worktrees.
|
||||
- Keep `/admin` and `/mod` functional and unchanged throughout this plan.
|
||||
- Expose the rebuild only at `/ase-next`; do not create `/ase` or alter production administration links.
|
||||
- Keep `HOUSEKEEPING_NEXT_PREVIEW_ENABLED` defaulting to `false`, and keep the preview unavailable in `NODE_ENV=production`.
|
||||
- Recover no page or service from `codex/housekeeping-complete` unless a task names it explicitly and first proves the behavior with a failing test. This plan names no such recovery.
|
||||
- Generate navigation only for routes with a registered handler and satisfied domain/route capabilities.
|
||||
- Use `forbidden()` for authenticated capability denial and `notFound()` only for unknown domains, paths, or entities.
|
||||
- Use real workflow-specific content in later verticals; this foundation must not add placeholder dashboards or generic forms.
|
||||
- Preserve the untracked `.remember/` directory and stage only paths named by the active task.
|
||||
- Before every Node or pnpm command, select the required runtime:
|
||||
|
||||
```powershell
|
||||
$nodeDir = Join-Path (Join-Path $env:TEMP 'codex-node-v26.8.1') 'node-v26.8.1-win-x64'
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $nodeDir 'node.exe'))) {
|
||||
throw 'Node 26.8.1 portable runtime not found'
|
||||
}
|
||||
$env:PATH = "$nodeDir;$env:PATH"
|
||||
node --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Rename the gated preview namespace to `/ase-next`
|
||||
|
||||
**Files:**
|
||||
- Create: `src/features/housekeeping/foundation/preview-namespace.test.ts`
|
||||
- Move: `src/app/admin-next/layout.tsx` to `src/app/ase-next/layout.tsx`
|
||||
- Move: `src/app/admin-next/page.tsx` to `src/app/ase-next/page.tsx`
|
||||
- Move: `src/app/admin-next/[domain]/layout.tsx` to `src/app/ase-next/[domain]/layout.tsx`
|
||||
- Move: `src/app/admin-next/[domain]/page.tsx` to `src/app/ase-next/[domain]/page.tsx`
|
||||
- Modify: `src/features/housekeeping/foundation/contracts/domain.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/registry.ts`
|
||||
- Modify: all six `src/features/housekeeping/domains/*/manifest.ts` files
|
||||
- Modify: `src/features/housekeeping/foundation/contracts/contracts.test.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/foundation-source-contract.test.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/navigation.test.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx`
|
||||
- Modify: `src/features/housekeeping/foundation/preview-route-contract.test.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/registry.test.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx`
|
||||
- Modify: `src/lib/admin-theme-source-audit.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: existing `isHousekeepingPreviewEnabled()` and `HOUSEKEEPING_NEXT_PREVIEW_ENABLED` environment contract.
|
||||
- Produces: preview source files and manifest hrefs that use only `/ase-next`; later tasks consume the new namespace without compatibility aliases.
|
||||
|
||||
- [ ] **Step 1: Write the failing namespace contract**
|
||||
|
||||
Create `preview-namespace.test.ts` with a tracked-source scan that ignores historical documentation and rejects the old runtime namespace:
|
||||
|
||||
```ts
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("Housekeeping preview namespace", () => {
|
||||
it("uses /ase-next and removes /admin-next from runtime sources", () => {
|
||||
const files = execFileSync("git", ["ls-files", "src", ".env.example"], {
|
||||
encoding: "utf8",
|
||||
})
|
||||
.trim()
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean);
|
||||
|
||||
const offenders = files.filter((file) =>
|
||||
readFileSync(file, "utf8").includes("/admin-next"),
|
||||
);
|
||||
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the namespace test and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/preview-namespace.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL listing the existing `/admin-next` route, manifest, and test files.
|
||||
|
||||
- [ ] **Step 3: Move the route tree and replace runtime/test hrefs**
|
||||
|
||||
Run the four `git mv` operations, then change the manifest type and registry invariant to the exact new namespace:
|
||||
|
||||
```ts
|
||||
export interface HousekeepingDomainManifest {
|
||||
id: HousekeepingDomainId;
|
||||
labelKey: string;
|
||||
descriptionKey: string;
|
||||
iconId: "inbox" | "users" | "file-text" | "gem" | "hotel" | "settings";
|
||||
previewHref: `/ase-next/${HousekeepingDomainId}`;
|
||||
capability: CapabilityRequirement;
|
||||
routes: readonly HousekeepingRouteDefinition[];
|
||||
searchProviders: readonly HousekeepingSearchProvider[];
|
||||
inboxSources: readonly HousekeepingInboxSource[];
|
||||
widgets: readonly HousekeepingWidgetDefinition[];
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
if (manifest.previewHref !== `/ase-next/${manifest.id}`) {
|
||||
throw new Error(`invalid preview href: ${manifest.previewHref}`);
|
||||
}
|
||||
```
|
||||
|
||||
Replace `/admin-next` with `/ase-next` in the six manifests and in the named tests. Update imports from `@/app/admin-next/...` to `@/app/ase-next/...`. Do not rename the environment flag in this task.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN and the unchanged preview gate**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/preview-namespace.test.ts src/features/housekeeping/foundation/preview-gate.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/navigation.test.ts
|
||||
```
|
||||
|
||||
Expected: all selected tests PASS and the production preview-gate cases remain denied.
|
||||
|
||||
- [ ] **Step 5: Check the exact diff and commit**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git diff --check
|
||||
git status --short
|
||||
git add -A -- src/app/admin-next src/app/ase-next
|
||||
git add -- src/features/housekeeping/foundation/preview-namespace.test.ts src/features/housekeeping/foundation/contracts/domain.ts src/features/housekeeping/foundation/contracts/contracts.test.ts src/features/housekeeping/foundation/registry.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx src/features/housekeeping/foundation/preview-route-contract.test.ts src/features/housekeeping/foundation/foundation-source-contract.test.ts src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx src/features/housekeeping/domains src/lib/admin-theme-source-audit.test.ts
|
||||
git commit -m "refactor(housekeeping): restore ase preview namespace"
|
||||
```
|
||||
|
||||
Expected: `.remember/` remains untracked and is not staged.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add deterministic route matching and handler-runtime validation
|
||||
|
||||
**Files:**
|
||||
- Create: `src/features/housekeeping/foundation/routing/route-handler.ts`
|
||||
- Create: `src/features/housekeeping/foundation/routing/match-route.ts`
|
||||
- Create: `src/features/housekeeping/foundation/routing/match-route.test.ts`
|
||||
- Create: `src/features/housekeeping/foundation/routing/runtime.ts`
|
||||
- Create: `src/features/housekeeping/foundation/routing/runtime.test.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/contracts/domain.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/contracts/index.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/registry.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/registry.test.ts`
|
||||
- Modify: all six `src/features/housekeeping/domains/*/manifest.ts` files
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `HousekeepingRegistry`, `HousekeepingCapabilityContext`, and `HousekeepingDomainManifest`.
|
||||
- Produces: `HousekeepingPreviewHref`, `HousekeepingRouteMatch`, `HousekeepingRouteHandler`, `HousekeepingRouteRuntime`, `createHousekeepingRouteRuntime()`, and `matchHousekeepingRoute()`.
|
||||
|
||||
- [ ] **Step 1: Write failing route-matcher tests**
|
||||
|
||||
Create cases that require exact, dynamic, and rejected matches:
|
||||
|
||||
```ts
|
||||
it("matches concrete and dynamic preview routes", () => {
|
||||
expect(runtime.match("/ase-next/people/users")).toMatchObject({
|
||||
routeId: "people.users",
|
||||
domain: "people",
|
||||
params: {},
|
||||
});
|
||||
expect(runtime.match("/ase-next/people/users/42")).toMatchObject({
|
||||
routeId: "people.user-detail",
|
||||
domain: "people",
|
||||
params: { id: "42" },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
"/ase-next/people",
|
||||
"/ase-next/people/unknown",
|
||||
"/ase-next/people/users/",
|
||||
"/ase-next/people/users?rank=7",
|
||||
"/ase-next/people/users/%2F",
|
||||
])("rejects an unregistered canonical path: %s", (pathname) => {
|
||||
expect(runtime.match(pathname)).toBeNull();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run matcher tests and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/routing/match-route.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL because the routing modules and runtime do not exist.
|
||||
|
||||
- [ ] **Step 3: Add the concrete route and handler contracts**
|
||||
|
||||
Add these public contracts:
|
||||
|
||||
```ts
|
||||
export type HousekeepingPreviewHref = `/ase-next${"" | `/${string}`}`;
|
||||
|
||||
export interface HousekeepingRouteDefinition {
|
||||
id: string;
|
||||
labelKey: string;
|
||||
href: HousekeepingPreviewHref;
|
||||
capability: CapabilityRequirement;
|
||||
matchPrefixes?: readonly string[];
|
||||
}
|
||||
|
||||
export interface HousekeepingDomainManifest {
|
||||
// existing fields remain
|
||||
landingRouteId: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
import type { ReactNode } from "react";
|
||||
import type { HousekeepingCapabilityContext } from "../contracts";
|
||||
|
||||
export interface HousekeepingRouteMatch {
|
||||
routeId: string;
|
||||
domain: HousekeepingDomainId;
|
||||
params: Readonly<Record<string, string>>;
|
||||
canonicalHref: HousekeepingPreviewHref;
|
||||
}
|
||||
|
||||
export interface HousekeepingRouteRenderInput {
|
||||
context: HousekeepingCapabilityContext;
|
||||
match: HousekeepingRouteMatch;
|
||||
searchParams?: Readonly<Record<string, string | readonly string[] | undefined>>;
|
||||
translate: (key: string) => string;
|
||||
}
|
||||
|
||||
export interface HousekeepingRouteHandler {
|
||||
routeId: string;
|
||||
render(input: HousekeepingRouteRenderInput): Promise<ReactNode>;
|
||||
}
|
||||
```
|
||||
|
||||
All six current empty manifests set `landingRouteId: null`. Registry validation permits `null` only while `routes` is empty; once routes exist, it requires a landing ID owned by that manifest.
|
||||
|
||||
- [ ] **Step 4: Implement deterministic matching**
|
||||
|
||||
Implement `matchHousekeepingRoute()` by parsing canonical path segments, sorting literal candidates ahead of dynamic `:parameter` candidates, requiring an exact segment count, decoding each segment once, and rejecting query strings, fragments, backslashes, empty segments, trailing slashes, `.`/`..`, and decoded slashes.
|
||||
|
||||
The exported signature is:
|
||||
|
||||
```ts
|
||||
export function matchHousekeepingRoute(
|
||||
registry: HousekeepingRegistry,
|
||||
canonicalPath: string,
|
||||
): HousekeepingRouteMatch | null;
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Write failing runtime-bijection tests**
|
||||
|
||||
Add tests with a two-route manifest and assert these failures separately:
|
||||
|
||||
```ts
|
||||
expect(() => createHousekeepingRouteRuntime(registry, [])).toThrow(
|
||||
"missing route handler: people.users",
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
createHousekeepingRouteRuntime(registry, [
|
||||
handler("people.users"),
|
||||
handler("people.users"),
|
||||
]),
|
||||
).toThrow("duplicate route handler: people.users");
|
||||
|
||||
expect(() =>
|
||||
createHousekeepingRouteRuntime(registry, [handler("people.unknown")]),
|
||||
).toThrow("handler without route: people.unknown");
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Implement and verify the runtime registry**
|
||||
|
||||
Implement this public shape:
|
||||
|
||||
```ts
|
||||
export interface HousekeepingRouteRuntime {
|
||||
registry: HousekeepingRegistry;
|
||||
handlers: ReadonlyMap<string, HousekeepingRouteHandler>;
|
||||
match(pathname: string): HousekeepingRouteMatch | null;
|
||||
}
|
||||
|
||||
export function createHousekeepingRouteRuntime(
|
||||
registry: HousekeepingRegistry,
|
||||
handlers: readonly HousekeepingRouteHandler[],
|
||||
): HousekeepingRouteRuntime;
|
||||
```
|
||||
|
||||
The constructor rejects duplicate handlers, missing handlers for declared routes, and handlers without declared routes. Run:
|
||||
|
||||
```powershell
|
||||
pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/routing/match-route.test.ts src/features/housekeeping/foundation/routing/runtime.test.ts src/features/housekeeping/foundation/registry.test.ts
|
||||
```
|
||||
|
||||
Expected: all selected tests PASS.
|
||||
|
||||
- [ ] **Step 7: Commit the routing runtime**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git diff --check
|
||||
git add src/features/housekeeping/foundation/contracts src/features/housekeeping/foundation/registry.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/routing src/features/housekeeping/domains
|
||||
git commit -m "feat(housekeeping): validate preview route runtime"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Project navigation only from accessible handled routes
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/features/housekeeping/foundation/navigation.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/navigation.test.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `HousekeepingRouteRuntime`, handler-backed route definitions, and `HousekeepingCapabilityContext`.
|
||||
- Produces: `buildHousekeepingNavigation(runtime, context, translate)` whose domain `href` is always a concrete route and whose items are all resolvable.
|
||||
|
||||
- [ ] **Step 1: Write failing navigation reachability tests**
|
||||
|
||||
Add these behaviors to `navigation.test.ts`:
|
||||
|
||||
```ts
|
||||
it("links a domain to its accessible handled landing route", () => {
|
||||
const navigation = buildHousekeepingNavigation(
|
||||
runtimeWithPeopleRoutes,
|
||||
contextWith(PERMS.USERS_VIEW),
|
||||
identityTranslate,
|
||||
);
|
||||
|
||||
expect(navigation).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "people",
|
||||
href: "/ase-next/people/users",
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
id: "people.users",
|
||||
href: "/ase-next/people/users",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("falls back to the first accessible handled route", () => {
|
||||
const navigation = buildHousekeepingNavigation(
|
||||
runtimeWithPreferredUsersAndTicketFallback,
|
||||
contextWith(PERMS.TICKETS_VIEW),
|
||||
identityTranslate,
|
||||
);
|
||||
expect(navigation[0]?.href).toBe("/ase-next/people/support/tickets");
|
||||
});
|
||||
|
||||
it("omits domains with no accessible handled routes", () => {
|
||||
expect(
|
||||
buildHousekeepingNavigation(runtimeWithNoPeopleHandlers, moderator, identityTranslate),
|
||||
).toEqual([]);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run navigation tests and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/navigation.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL because the current function consumes a static registry, links to `previewHref`, and retains empty domains.
|
||||
|
||||
- [ ] **Step 3: Implement the navigation projection**
|
||||
|
||||
Change the signature and projection:
|
||||
|
||||
```ts
|
||||
export function buildHousekeepingNavigation(
|
||||
runtime: HousekeepingRouteRuntime,
|
||||
context: HousekeepingCapabilityContext,
|
||||
translate: (key: string) => string,
|
||||
): readonly HousekeepingNavigationDomain[] {
|
||||
return runtime.registry.domains.flatMap((domain) => {
|
||||
if (!satisfiesCapability(context, domain.capability)) return [];
|
||||
|
||||
const items = domain.routes
|
||||
.filter(
|
||||
(route) =>
|
||||
runtime.handlers.has(route.id) &&
|
||||
satisfiesCapability(context, route.capability),
|
||||
)
|
||||
.map((route) => ({
|
||||
id: route.id,
|
||||
href: route.href,
|
||||
label: translate(route.labelKey),
|
||||
}));
|
||||
|
||||
if (items.length === 0) return [];
|
||||
const landing =
|
||||
items.find((item) => item.id === domain.landingRouteId) ?? items[0];
|
||||
if (!landing) return [];
|
||||
|
||||
return [{
|
||||
id: domain.id,
|
||||
href: landing.href,
|
||||
iconId: domain.iconId,
|
||||
label: translate(domain.labelKey),
|
||||
description: translate(domain.descriptionKey),
|
||||
items,
|
||||
}];
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify route reachability for every projected link**
|
||||
|
||||
Add one property-style loop over representative capability contexts:
|
||||
|
||||
```ts
|
||||
for (const context of capabilityProfiles) {
|
||||
for (const domain of buildHousekeepingNavigation(runtime, context, identityTranslate)) {
|
||||
expect(runtime.match(domain.href), domain.href).not.toBeNull();
|
||||
for (const item of domain.items) {
|
||||
expect(runtime.match(item.href), item.href).not.toBeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit the navigation invariant**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git diff --check
|
||||
git add src/features/housekeeping/foundation/navigation.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx
|
||||
git commit -m "fix(housekeeping): link only reachable preview routes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Dispatch `/ase-next` with distinct 403 and 404 behavior
|
||||
|
||||
**Files:**
|
||||
- Create: `src/features/housekeeping/route-handlers.ts`
|
||||
- Create: `src/app/ase-next/[domain]/[[...segments]]/page.tsx`
|
||||
- Create: `src/app/ase-next/[domain]/[[...segments]]/loading.tsx`
|
||||
- Create: `src/app/ase-next/forbidden.tsx`
|
||||
- Delete: `src/app/ase-next/[domain]/page.tsx`
|
||||
- Modify: `src/app/ase-next/page.tsx`
|
||||
- Modify: `src/app/ase-next/[domain]/layout.tsx`
|
||||
- Modify: `src/features/housekeeping/foundation/preview-route-contract.test.ts`
|
||||
- Modify: `src/features/housekeeping/foundation/foundation-source-contract.test.ts`
|
||||
- Modify: `next.config.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `createHousekeepingRouteRuntime()`, `buildHousekeepingNavigation()`, `getHousekeepingCapabilityContext()`, and the six manifests.
|
||||
- Produces: an application dispatcher for exact handled routes, capability-aware bare-domain redirects, and Next.js HTTP access fallbacks.
|
||||
|
||||
- [ ] **Step 1: Write failing app-route tests for the original 404 regression**
|
||||
|
||||
Update route mocks to provide manifests plus handlers, then add:
|
||||
|
||||
```ts
|
||||
it("redirects a bare domain to its accessible handled landing page", async () => {
|
||||
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(
|
||||
capabilityContext([PERMS.USERS_VIEW]),
|
||||
);
|
||||
|
||||
await expect(
|
||||
HousekeepingDomainPage({
|
||||
params: Promise.resolve({ domain: "people", segments: [] }),
|
||||
}),
|
||||
).rejects.toThrow("NEXT_REDIRECT:/ase-next/people/users");
|
||||
});
|
||||
|
||||
it("renders a known permitted handled route", async () => {
|
||||
const html = await renderRoute(
|
||||
HousekeepingDomainPage({
|
||||
params: Promise.resolve({ domain: "people", segments: ["users"] }),
|
||||
}),
|
||||
);
|
||||
expect(html).toContain("Rendered people.users");
|
||||
});
|
||||
|
||||
it("returns forbidden for a known route without capability", async () => {
|
||||
await expect(
|
||||
HousekeepingDomainPage({
|
||||
params: Promise.resolve({ domain: "people", segments: ["users"] }),
|
||||
}),
|
||||
).rejects.toThrow("NEXT_FORBIDDEN");
|
||||
});
|
||||
|
||||
it("returns not found for an unknown path", async () => {
|
||||
await expect(
|
||||
HousekeepingDomainPage({
|
||||
params: Promise.resolve({ domain: "people", segments: ["missing"] }),
|
||||
}),
|
||||
).rejects.toThrow("NEXT_NOT_FOUND");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run route tests and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/preview-route-contract.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL because the existing domain page has no catch-all dispatch, handler runtime, redirect, or forbidden boundary.
|
||||
|
||||
- [ ] **Step 3: Enable supported Next.js auth interrupts**
|
||||
|
||||
Add the installed Next.js 16.3.3 option without changing other experimental flags:
|
||||
|
||||
```ts
|
||||
experimental: {
|
||||
authInterrupts: true,
|
||||
optimizePackageImports: ["lucide-react", "date-fns"],
|
||||
useTypeScriptCli: true,
|
||||
hideLogsAfterAbort: true,
|
||||
},
|
||||
```
|
||||
|
||||
Create `src/app/ase-next/forbidden.tsx` as a localized, accessible 403 page with one link back to `/` and no privileged data.
|
||||
|
||||
- [ ] **Step 4: Create the handler registry and catch-all dispatcher**
|
||||
|
||||
The initial application registry is intentionally empty until People supplies real routes:
|
||||
|
||||
```ts
|
||||
import type { HousekeepingRouteHandler } from "./foundation/routing/route-handler";
|
||||
|
||||
export const HOUSEKEEPING_ROUTE_HANDLERS =
|
||||
[] as const satisfies readonly HousekeepingRouteHandler[];
|
||||
```
|
||||
|
||||
In the catch-all page:
|
||||
|
||||
1. create the static registry and runtime;
|
||||
2. reject an unknown domain with `notFound()` before loading capability context;
|
||||
3. load the request-scoped capability context;
|
||||
4. build accessible navigation;
|
||||
5. redirect an empty suffix to that domain's projected landing href;
|
||||
6. match a non-empty canonical path;
|
||||
7. call `notFound()` when no route/handler exists;
|
||||
8. call `forbidden()` when domain or route capability is absent;
|
||||
9. render the handler with context, match, translations, and awaited search params.
|
||||
|
||||
Use this canonical path construction:
|
||||
|
||||
```ts
|
||||
const suffix = segments.map((segment) => encodeURIComponent(segment)).join("/");
|
||||
const canonicalPath = suffix
|
||||
? `${activeDomain.previewHref}/${suffix}`
|
||||
: activeDomain.previewHref;
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Make root and layout consume the runtime projection**
|
||||
|
||||
`/ase-next` redirects to `navigation[0].href`, not a domain namespace. If no accessible handled route exists, call `forbidden()`. The domain layout calls `notFound()` for an unknown domain and `forbidden()` for a known inaccessible domain, then renders the shell from the same runtime projection.
|
||||
|
||||
- [ ] **Step 6: Verify app-route semantics**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/preview-route-contract.test.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/preview-gate.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS for bare-domain redirect, concrete render, true forbidden, true missing path, request-context reuse, and production gate denial.
|
||||
|
||||
- [ ] **Step 7: Commit dispatcher and access semantics**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git diff --check
|
||||
git add next.config.ts src/features/housekeeping/route-handlers.ts src/features/housekeeping/foundation/preview-route-contract.test.ts src/features/housekeeping/foundation/foundation-source-contract.test.ts
|
||||
git add -A -- src/app/ase-next
|
||||
git commit -m "feat(housekeeping): dispatch gated preview routes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Complete shared loading, access, missing, and unexpected-error states
|
||||
|
||||
**Files:**
|
||||
- Create: `src/app/ase-next/error.tsx`
|
||||
- Create: `src/app/ase-next/loading.tsx`
|
||||
- Create: `src/app/ase-next/not-found.tsx`
|
||||
- Modify: `src/app/ase-next/forbidden.tsx`
|
||||
- Modify: `src/features/housekeeping/foundation/page/housekeeping-page-state.tsx`
|
||||
- Modify: `src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx`
|
||||
- Modify: `src/features/housekeeping/foundation/localization-contract.test.ts`
|
||||
- Modify: `src/messages/en.json`
|
||||
- Modify: `src/messages/it.json`
|
||||
- Modify: `src/messages/nl.json`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: App Router error/access conventions and the existing semantic admin color tokens.
|
||||
- Produces: localized state surfaces for `loading`, `empty`, `partial`, `validation`, `conflict`, `dependency`, `forbidden`, `not-found`, `error`, and `success` semantics.
|
||||
|
||||
- [ ] **Step 1: Write failing page-state and localization tests**
|
||||
|
||||
Extend the state union test matrix:
|
||||
|
||||
```ts
|
||||
it.each([
|
||||
["loading", "status"],
|
||||
["empty", "status"],
|
||||
["partial", "status"],
|
||||
["conflict", "alert"],
|
||||
["dependency", "alert"],
|
||||
["error", "alert"],
|
||||
["success", "status"],
|
||||
] as const)("renders %s with the expected live role", (state, role) => {
|
||||
const html = renderState(state);
|
||||
expect(html).toContain(`role="${role}"`);
|
||||
});
|
||||
```
|
||||
|
||||
Require these English, Italian, and Dutch keys under `pages.housekeeping.states`: `loading`, `empty`, `partial`, `conflict`, `dependency`, `forbidden`, `notFound`, `error`, `success`, `retry`, and `backToSite`.
|
||||
|
||||
- [ ] **Step 2: Run state tests and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx src/features/housekeeping/foundation/localization-contract.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL on the new state union and missing translation keys.
|
||||
|
||||
- [ ] **Step 3: Implement state semantics and three locale sources**
|
||||
|
||||
Use explicit tones rather than deriving every non-error as neutral:
|
||||
|
||||
```ts
|
||||
type HousekeepingPageState =
|
||||
| "loading"
|
||||
| "empty"
|
||||
| "partial"
|
||||
| "conflict"
|
||||
| "dependency"
|
||||
| "error"
|
||||
| "success";
|
||||
|
||||
const ALERT_STATES = new Set<HousekeepingPageState>([
|
||||
"conflict",
|
||||
"dependency",
|
||||
"error",
|
||||
]);
|
||||
```
|
||||
|
||||
Add complete operator-facing English, Italian, and Dutch messages. Other configured locales continue using the established English fallback and must never render raw keys.
|
||||
|
||||
- [ ] **Step 4: Implement App Router fallback files**
|
||||
|
||||
- `loading.tsx` renders the shared loading state.
|
||||
- `not-found.tsx` renders an actual missing-route message and links to `/ase-next` only when preview is accessible.
|
||||
- `forbidden.tsx` explains insufficient access without implying that the page is missing.
|
||||
- `error.tsx` is a client component that shows `error.digest` as the support reference when present and invokes `reset()` from a localized retry button.
|
||||
|
||||
Do not expose stack traces, exception messages, permission slugs, or database details.
|
||||
|
||||
- [ ] **Step 5: Verify states and route boundaries**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx src/features/housekeeping/foundation/localization-contract.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit shared state behavior**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git diff --check
|
||||
git add src/app/ase-next src/features/housekeeping/foundation/page src/features/housekeeping/foundation/localization-contract.test.ts src/messages/en.json src/messages/it.json src/messages/nl.json
|
||||
git commit -m "feat(housekeeping): distinguish preview page states"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Lock preview isolation and run the full foundation gate
|
||||
|
||||
**Files:**
|
||||
- Create: `src/features/housekeeping/foundation/cutover-isolation.test.ts`
|
||||
- Modify only if a test exposes a defect: files already named in Tasks 1-5
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the completed `/ase-next` foundation.
|
||||
- Produces: an automated boundary proving that the stable surfaces remain present and the final `/ase` cutover is absent.
|
||||
|
||||
- [ ] **Step 1: Write the isolation contract**
|
||||
|
||||
Create:
|
||||
|
||||
```ts
|
||||
import { existsSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("Housekeeping stepwise isolation", () => {
|
||||
it("keeps legacy administration while exposing only the gated preview", () => {
|
||||
expect(existsSync("src/app/admin/layout.tsx")).toBe(true);
|
||||
expect(existsSync("src/app/mod/layout.tsx")).toBe(true);
|
||||
expect(existsSync("src/app/ase-next/layout.tsx")).toBe(true);
|
||||
expect(existsSync("src/app/admin-next/layout.tsx")).toBe(false);
|
||||
expect(existsSync("src/app/ase/page.tsx")).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the isolation and Housekeeping suites**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/cutover-isolation.test.ts
|
||||
pnpm.cmd test:housekeeping
|
||||
```
|
||||
|
||||
Expected: both commands PASS. If a failure occurs, fix only the owning foundation behavior and rerun its focused RED/GREEN test before rerunning the gate.
|
||||
|
||||
- [ ] **Step 3: Run repository verification**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
pnpm.cmd typecheck
|
||||
pnpm.cmd test
|
||||
pnpm.cmd build
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: typecheck, all tests, production build, and whitespace validation PASS under Node 26.8.1. The production build must include the route tree while the runtime preview gate remains closed in production.
|
||||
|
||||
- [ ] **Step 4: Inspect the final branch boundary**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git diff --stat origin/main...HEAD
|
||||
git diff --name-status origin/main...HEAD
|
||||
git grep -n -I '/admin-next' -- src .env.example
|
||||
git status --short --branch
|
||||
```
|
||||
|
||||
Expected: no runtime `/admin-next` matches; no `/ase` cutover files; `/admin` and `/mod` are not deleted; `.remember/` is the only unrelated untracked path.
|
||||
|
||||
- [ ] **Step 5: Commit the isolation gate**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git add src/features/housekeeping/foundation/cutover-isolation.test.ts
|
||||
git commit -m "test(housekeeping): lock stepwise preview isolation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Publish the verified foundation as a draft pull request
|
||||
|
||||
**Files:**
|
||||
- No source changes.
|
||||
- PR title: `Rebuild Housekeeping foundation and preview routing`
|
||||
- PR body language order: English first, Dutch second.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: a clean verified branch from Tasks 1-6 plus the committed design and this plan.
|
||||
- Produces: remote branch `codex/housekeeping-rebuild-stepwise` and one draft PR targeting `main`.
|
||||
|
||||
- [ ] **Step 1: Verify the publication boundary**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git fetch origin
|
||||
git rev-list --left-right --count origin/main...HEAD
|
||||
git log --oneline origin/main..HEAD
|
||||
git status --short --branch
|
||||
```
|
||||
|
||||
Expected: the branch contains the design, plan, and focused foundation commits; no tracked modifications are pending; `.remember/` remains untracked.
|
||||
|
||||
- [ ] **Step 2: Push the dedicated branch**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git push -u origin codex/housekeeping-rebuild-stepwise
|
||||
```
|
||||
|
||||
Expected: pre-push typecheck/tests PASS and the remote branch is created or fast-forwarded.
|
||||
|
||||
- [ ] **Step 3: Create the bilingual draft PR through Forgejo**
|
||||
|
||||
Use Git Credential Manager without printing the credential:
|
||||
|
||||
```powershell
|
||||
$credentialLines = @("protocol=https", "host=gitlab.epicnabbo.nl", "", "") |
|
||||
git credential fill
|
||||
$credential = @{}
|
||||
foreach ($line in $credentialLines) {
|
||||
if ($line -match '^([^=]+)=(.*)$') { $credential[$matches[1]] = $matches[2] }
|
||||
}
|
||||
if (-not $credential.password) { throw 'Forgejo credential unavailable' }
|
||||
|
||||
$body = @'
|
||||
## English
|
||||
|
||||
### Scope
|
||||
- Restores the gated Housekeeping preview at `/ase-next` while keeping `/admin` and `/mod` unchanged.
|
||||
- Guarantees that every generated navigation link resolves to an accessible registered route with a handler.
|
||||
- Separates authenticated forbidden access (403) from unknown routes (404).
|
||||
- Adds localized loading, partial, conflict, dependency, forbidden, missing, error, and success states.
|
||||
|
||||
### Verification
|
||||
- Housekeeping tests
|
||||
- Full test suite
|
||||
- TypeScript typecheck
|
||||
- Production build
|
||||
- Preview isolation and route-reachability contracts
|
||||
|
||||
This PR remains draft. People content and later verticals will be added only after this foundation checkpoint is reviewed.
|
||||
|
||||
## Nederlands
|
||||
|
||||
### Omvang
|
||||
- Herstelt de afgeschermde Housekeeping-preview op `/ase-next`, terwijl `/admin` en `/mod` ongewijzigd blijven.
|
||||
- Garandeert dat elke gegenereerde navigatielink verwijst naar een toegankelijke geregistreerde route met een handler.
|
||||
- Maakt onderscheid tussen verboden toegang voor een aangemelde gebruiker (403) en een onbekende route (404).
|
||||
- Voegt gelokaliseerde statussen toe voor laden, gedeeltelijke resultaten, conflicten, afhankelijkheidsfouten, verboden toegang, ontbrekende pagina's, fouten en succes.
|
||||
|
||||
### Verificatie
|
||||
- Housekeeping-tests
|
||||
- Volledige testsuite
|
||||
- TypeScript-typecontrole
|
||||
- Productiebuild
|
||||
- Contracttests voor preview-isolatie en bereikbare routes
|
||||
|
||||
Deze PR blijft een concept. People-content en volgende domeinen worden pas toegevoegd nadat deze foundation-checkpoint is beoordeeld.
|
||||
'@
|
||||
|
||||
$payload = @{
|
||||
base = "main"
|
||||
head = "codex/housekeeping-rebuild-stepwise"
|
||||
title = "Rebuild Housekeeping foundation and preview routing"
|
||||
body = $body
|
||||
draft = $true
|
||||
} | ConvertTo-Json
|
||||
|
||||
$headers = @{ Authorization = "token $($credential.password)" }
|
||||
Invoke-RestMethod `
|
||||
-Method Post `
|
||||
-Uri 'https://gitlab.epicnabbo.nl/api/v1/repos/remco/EpicNext-Cms/pulls' `
|
||||
-Headers $headers `
|
||||
-ContentType 'application/json' `
|
||||
-Body $payload |
|
||||
Select-Object number, html_url, state, draft
|
||||
```
|
||||
|
||||
Expected: one draft PR targeting `main`; the credential value is never written to output or committed.
|
||||
|
||||
- [ ] **Step 4: Verify the remote PR and checks**
|
||||
|
||||
Query the returned PR number and branch status through the Forgejo API. Confirm `draft: true`, `base.ref: main`, `head.ref: codex/housekeeping-rebuild-stepwise`, and wait for all triggered checks to complete. Do not call the foundation deployed: the preview remains production-disabled and this task does not merge.
|
||||
|
||||
---
|
||||
|
||||
## Plan completion boundary
|
||||
|
||||
This plan is complete when the draft PR contains a green, non-production `/ase-next` routing foundation and the legacy administration surfaces remain untouched. The next written plan covers the People vertical: workflow audit, users, linked accounts, community/staff, moderation, support, real query/command services, content review, and visual approval. No People page is considered implemented by this foundation plan.
|
||||
Reference in new issue
Block a user