33 KiB
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
/adminand/modfunctional and unchanged throughout this plan. - Expose the rebuild only at
/ase-next; do not create/aseor alter production administration links. - Keep
HOUSEKEEPING_NEXT_PREVIEW_ENABLEDdefaulting tofalse, and keep the preview unavailable inNODE_ENV=production. - Recover no page or service from
codex/housekeeping-completeunless 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 andnotFound()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:
$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.tsxtosrc/app/ase-next/layout.tsx - Move:
src/app/admin-next/page.tsxtosrc/app/ase-next/page.tsx - Move:
src/app/admin-next/[domain]/layout.tsxtosrc/app/ase-next/[domain]/layout.tsx - Move:
src/app/admin-next/[domain]/page.tsxtosrc/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.tsfiles - 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()andHOUSEKEEPING_NEXT_PREVIEW_ENABLEDenvironment 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:
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:
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:
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[];
}
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:
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:
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.tsfiles
Interfaces:
-
Consumes:
HousekeepingRegistry,HousekeepingCapabilityContext, andHousekeepingDomainManifest. -
Produces:
HousekeepingPreviewHref,HousekeepingRouteMatch,HousekeepingRouteHandler,HousekeepingRouteRuntime,createHousekeepingRouteRuntime(), andmatchHousekeepingRoute(). -
Step 1: Write failing route-matcher tests
Create cases that require exact, dynamic, and rejected matches:
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:
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:
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;
}
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:
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:
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:
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:
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:
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, andHousekeepingCapabilityContext. -
Produces:
buildHousekeepingNavigation(runtime, context, translate)whose domainhrefis always a concrete route and whose items are all resolvable. -
Step 1: Write failing navigation reachability tests
Add these behaviors to navigation.test.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:
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:
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:
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:
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:
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:
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:
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:
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:
import type { HousekeepingRouteHandler } from "./foundation/routing/route-handler";
export const HOUSEKEEPING_ROUTE_HANDLERS =
[] as const satisfies readonly HousekeepingRouteHandler[];
In the catch-all page:
- create the static registry and runtime;
- reject an unknown domain with
notFound()before loading capability context; - load the request-scoped capability context;
- build accessible navigation;
- redirect an empty suffix to that domain's projected landing href;
- match a non-empty canonical path;
- call
notFound()when no route/handler exists; - call
forbidden()when domain or route capability is absent; - render the handler with context, match, translations, and awaited search params.
Use this canonical path construction:
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:
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:
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, andsuccesssemantics. -
Step 1: Write failing page-state and localization tests
Extend the state union test matrix:
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:
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:
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.tsxrenders the shared loading state. -
not-found.tsxrenders an actual missing-route message and links to/ase-nextonly when preview is accessible. -
forbidden.tsxexplains insufficient access without implying that the page is missing. -
error.tsxis a client component that showserror.digestas the support reference when present and invokesreset()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:
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:
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-nextfoundation. -
Produces: an automated boundary proving that the stable surfaces remain present and the final
/asecutover is absent. -
Step 1: Write the isolation contract
Create:
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:
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:
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:
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:
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-stepwiseand one draft PR targetingmain. -
Step 1: Verify the publication boundary
Run:
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:
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:
$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.