diff --git a/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md b/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md index 80fa93d38..3a6047dc0 100644 --- a/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md +++ b/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md @@ -117,7 +117,7 @@ Rules: ## Current Version/Date -- Project Instructions Version: 2026.06.28.006 +- Project Instructions Version: 2026.06.28.007 - Date: 2026-06-28 - Owner: OWNER diff --git a/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS_VERSION.md b/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS_VERSION.md index b69da6446..b44470158 100644 --- a/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS_VERSION.md +++ b/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS_VERSION.md @@ -1,6 +1,6 @@ # Project Instructions Version -Current Project Instructions Version: 2026.06.28.006 +Current Project Instructions Version: 2026.06.28.007 Last Updated: 2026-06-28 @@ -14,6 +14,7 @@ Last Updated: 2026-06-28 - Added proposed repository layout architecture plan for future `www/`, `api/`, and `dev/` separation. - Added repository layout scaffold governance for `www/`, `api/`, and `dev/local-runtime/`. - Added `www/` migration map for safe browser-served application movement planning. +- Added local route-root compatibility toggle documentation for future `www/` activation. - Codex must read this version file and the latest repository copy of `PROJECT_INSTRUCTIONS.md` before performing work. - Codex must discard previously remembered Project Instructions and treat the repository copy as authoritative. - Codex must validate canonical report and ZIP paths, branching model, and legacy path avoidance before work proceeds. diff --git a/dev/build/ProjectInstructions/PROJECT_STATE.md b/dev/build/ProjectInstructions/PROJECT_STATE.md index 79b89c03d..6b22eb153 100644 --- a/dev/build/ProjectInstructions/PROJECT_STATE.md +++ b/dev/build/ProjectInstructions/PROJECT_STATE.md @@ -1,7 +1,7 @@ # Project State ```yaml -project_state_version: "2026.06.28.006" +project_state_version: "2026.06.28.007" last_updated: "2026-06-28" current_main_commit: "40de767476d70cadfd1292c916844c2f31b6f185" repository_status: @@ -10,9 +10,9 @@ repository_status: runtime_database: "Postgres" creator_metadata: "API to Postgres" creator_assets: "API to R2" -project_instructions_version: "2026.06.28.006" -repository_structure_version: "2026.06.28.006" -canonical_layout_version: "2026.06.28.006" +project_instructions_version: "2026.06.28.007" +repository_structure_version: "2026.06.28.007" +canonical_layout_version: "2026.06.28.007" active_teams: - "Owner" - "Alfa" @@ -20,8 +20,8 @@ active_teams: - "Charlie" - "Delta" - "Golf" -latest_owner_pr: "PR_26180_OWNER_006-www-migration-map" -latest_structure_pr: "PR_26180_OWNER_006-www-migration-map" +latest_owner_pr: "PR_26180_OWNER_007-www-route-root-compatibility" +latest_structure_pr: "PR_26180_OWNER_007-www-route-root-compatibility" valid_top_level_folders: - "account/" - "admin/" diff --git a/dev/build/ProjectInstructions/backlog/BACKLOG_MASTER.md b/dev/build/ProjectInstructions/backlog/BACKLOG_MASTER.md index e571a4024..3c52cbced 100644 --- a/dev/build/ProjectInstructions/backlog/BACKLOG_MASTER.md +++ b/dev/build/ProjectInstructions/backlog/BACKLOG_MASTER.md @@ -39,8 +39,8 @@ The backlog is the authoritative source for determining the next logical PRs. - Team: Owner - Product Area: Repository Architecture Simplification - Status: Active -- Percent Complete: 15% -- Active PR: PR_26180_OWNER_006-www-migration-map +- Percent Complete: 20% +- Active PR: PR_26180_OWNER_007-www-route-root-compatibility - Next Milestone: Move browser-served application to `www/` - Source / Reference: `dev/build/ProjectInstructions/repository/repository_layout_architecture_plan.md`; `dev/build/ProjectInstructions/repository/www_migration_map.md` diff --git a/dev/build/ProjectInstructions/repository/www_migration_map.md b/dev/build/ProjectInstructions/repository/www_migration_map.md index bd617513d..82f351cd9 100644 --- a/dev/build/ProjectInstructions/repository/www_migration_map.md +++ b/dev/build/ProjectInstructions/repository/www_migration_map.md @@ -170,6 +170,22 @@ Recommended approach for the actual move PR: 5. Do not introduce browser redirects unless a static route cannot be served directly. 6. Document every temporary shim with removal criteria for the legacy-layout cleanup PR. +## Route Root Compatibility Toggle + +`PR_26180_OWNER_007-www-route-root-compatibility` installs the local compatibility switch without moving browser files. + +The future activation toggle is: + +```text +GAMEFOUNDRY_LOCAL_WEB_ROOT=www +``` + +Default behavior remains current repository-root static serving when `GAMEFOUNDRY_LOCAL_WEB_ROOT` is unset, empty, `.`, `root`, or `repo-root`. + +When `GAMEFOUNDRY_LOCAL_WEB_ROOT=www`, local static serving prefers `www/` for public browser URLs while retaining repository-root fallback for compatibility during migration. + +This toggle is for local runtime and test helpers only. It does not change production deployment, public URLs, API routes, or package commands. + ## Required Sequence For Safe www Move 1. Freeze the public route contract. diff --git a/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_branch-validation.md b/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_branch-validation.md new file mode 100644 index 000000000..df1542475 --- /dev/null +++ b/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_branch-validation.md @@ -0,0 +1,23 @@ +# PR_26180_OWNER_007 Branch Validation + +## Result + +PASS + +## Checks + +| Check | Result | +|---|---| +| Current branch is `PR_26180_OWNER_007-www-route-root-compatibility` | PASS | +| Branch is stacked on `PR_26180_OWNER_006-www-migration-map` | PASS | +| Startup validation completed | PASS | +| Project Instructions version loaded | PASS: `2026.06.28.007` | +| `PROJECT_BRANCHING_POLICY.md` loaded | PASS | +| `BACKLOG_MASTER.md` loaded | PASS | +| No browser-served files moved | PASS | +| Reports path uses `dev/reports/` | PASS | +| ZIP path uses `dev/workspace/zips/` | PASS | + +## Notes + +This is a stacked Owner PR based on `PR_26180_OWNER_006-www-migration-map`. diff --git a/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_manual-validation-notes.md b/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_manual-validation-notes.md new file mode 100644 index 000000000..da5b9742b --- /dev/null +++ b/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_manual-validation-notes.md @@ -0,0 +1,22 @@ +# PR_26180_OWNER_007 Manual Validation Notes + +## Manual Review + +- Confirmed no browser-served files were moved. +- Confirmed public URLs remain unchanged. +- Confirmed the future web-root activation toggle is documented as `GAMEFOUNDRY_LOCAL_WEB_ROOT=www`. +- Confirmed the default remains repository-root static serving until the actual `www/` move PR. +- Confirmed route-root compatibility work is limited to local static serving and test helper support. + +## Manual Test Guidance + +After applying this PR, Owner can test: + +1. Start the current local development flow with no web-root override. +2. Open `/index.html`, `/toolbox/index.html`, and an `/assets/...` URL. +3. Confirm the routes continue to load from the current repository-root layout. +4. In a future migration branch, set `GAMEFOUNDRY_LOCAL_WEB_ROOT=www` after browser files are moved and confirm the same public URLs load from `www/`. + +## Runtime Scope + +No UI, API, database, or product behavior changes are expected from this PR. diff --git a/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_report.md b/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_report.md new file mode 100644 index 000000000..1107dbd3d --- /dev/null +++ b/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_report.md @@ -0,0 +1,47 @@ +# PR_26180_OWNER_007-www-route-root-compatibility Report + +## Executive Summary + +PASS. This PR prepares local static route-root compatibility for the future `www/` migration without moving browser-served files or changing public URLs. + +The implementation adds a shared local static route resolver that can prefer a configurable web root while preserving the current repository-root serving default. The documented future toggle is: + +```text +GAMEFOUNDRY_LOCAL_WEB_ROOT=www +``` + +Default behavior remains current root serving when the variable is unset, empty, `.`, `root`, or `repo-root`. + +## Source Plan + +Used `dev/build/ProjectInstructions/repository/www_migration_map.md` as the source plan. The map now documents the route-root compatibility toggle and the default/future behavior split. + +## Changed Files + +- `dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md` +- `dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS_VERSION.md` +- `dev/build/ProjectInstructions/PROJECT_STATE.md` +- `dev/build/ProjectInstructions/backlog/BACKLOG_MASTER.md` +- `dev/build/ProjectInstructions/repository/www_migration_map.md` +- `dev/scripts/start-dev.mjs` +- `dev/tests/dev-runtime/StaticWebRootCompatibility.test.mjs` +- `dev/tests/helpers/playwrightRepoServer.mjs` +- `dev/tests/playwright/tools/StaticWebRootCompatibility.spec.mjs` +- `src/dev-runtime/server/local-api-server.mjs` +- `src/dev-runtime/server/static-web-root.mjs` + +## Implementation Notes + +- Added `src/dev-runtime/server/static-web-root.mjs` for shared local static route resolution. +- Updated the local dev web server, Local API static fallback, and Playwright repo server helper to use the shared resolver. +- Added configurable `webRoot` support for test helpers and local server internals. +- Preserved public route URLs including `/index.html`, `/toolbox/index.html`, `/assets/...`, `/account/...`, `/admin/...`, and `/games/...`. +- Preserved compatibility alias behavior such as `/tools/...` resolving to `/toolbox/...`. +- Kept repository-root serving as the default until the actual `www/` move PR. +- No browser-served files were moved. +- No public URL changes were introduced. +- No API/server architecture changes were introduced. + +## Owner Recommendation + +Ready for review as the route-root compatibility preparation step before moving browser-served files into `www/`. diff --git a/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_requirement-checklist.md b/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_requirement-checklist.md new file mode 100644 index 000000000..29f00532a --- /dev/null +++ b/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_requirement-checklist.md @@ -0,0 +1,23 @@ +# PR_26180_OWNER_007 Requirement Checklist + +| Requirement | Result | Evidence | +|---|---|---| +| Use `www_migration_map.md` as source plan | PASS | Updated `dev/build/ProjectInstructions/repository/www_migration_map.md` with the compatibility toggle. | +| Preserve `/index.html` | PASS | Covered by Node and Playwright route-root tests. | +| Preserve `/toolbox/index.html` | PASS | Covered by Node and Playwright route-root tests. | +| Preserve `/assets/...` | PASS | Covered by Node and Playwright route-root tests. | +| Preserve `/account/...` | PASS | Covered by Node route normalization test. | +| Preserve `/admin/...` | PASS | Covered by Node route normalization test. | +| Preserve `/games/...` | PASS | Covered by Node route normalization test. | +| Add configurable web root support | PASS | Added shared `resolveLocalWebRoot` / `resolveStaticRouteTarget` helper. | +| Default remains current root serving | PASS | Default local web root is repository root. | +| Document future toggle | PASS | Documented `GAMEFOUNDRY_LOCAL_WEB_ROOT=www`. | +| Update targeted tests if needed | PASS | Added Node and Playwright route-root compatibility tests. | +| No browser files moved | PASS | No root HTML/pages/assets/toolbox files moved. | +| No public URL changes | PASS | URLs remain unchanged; only filesystem lookup is configurable. | +| No API/server architecture change | PASS | Local static resolution was shared; API architecture was not changed. | +| No product behavior change | PASS | Default local behavior remains root serving. | + +## Scope Result + +PASS. Changes are limited to route-root compatibility, tests/helpers, and governance/status documentation. diff --git a/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_validation-report.md b/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_validation-report.md new file mode 100644 index 000000000..852613ab9 --- /dev/null +++ b/dev/reports/PR_26180_OWNER_007-www-route-root-compatibility_validation-report.md @@ -0,0 +1,24 @@ +# PR_26180_OWNER_007 Validation Report + +## Required Validation + +| Command | Result | +|---|---| +| `node --check src/dev-runtime/server/static-web-root.mjs` | PASS | +| `node --check dev/scripts/start-dev.mjs` | PASS | +| `node --check src/dev-runtime/server/local-api-server.mjs` | PASS | +| `node --check dev/tests/helpers/playwrightRepoServer.mjs` | PASS | +| `node --check dev/tests/dev-runtime/StaticWebRootCompatibility.test.mjs` | PASS | +| `node --check dev/tests/playwright/tools/StaticWebRootCompatibility.spec.mjs` | PASS | +| `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/StaticWebRootCompatibility.test.mjs` | PASS | +| `npx playwright test dev/tests/playwright/tools/StaticWebRootCompatibility.spec.mjs --reporter=line` | PASS: 2/2 | +| `git diff --check` | PASS | +| `npm run validate:canonical-structure` | PASS | + +## Notes + +An exploratory broader Playwright route/navigation smoke was also attempted with `dev/tests/playwright/tools/ToolNavigationPrevNext.spec.mjs`. Two route-opening tests passed; one registry coverage assertion failed on a non-scope "Registry entry missing for Sprites" condition. The required focused route-root compatibility lane passed and no registry changes are included in this PR. + +## Result + +PASS diff --git a/dev/reports/codex_changed_files.txt b/dev/reports/codex_changed_files.txt index 573b9513c..5dc481813 100644 Binary files a/dev/reports/codex_changed_files.txt and b/dev/reports/codex_changed_files.txt differ diff --git a/dev/reports/codex_review.diff b/dev/reports/codex_review.diff index 11855db25..05d5c4f62 100644 Binary files a/dev/reports/codex_review.diff and b/dev/reports/codex_review.diff differ diff --git a/dev/scripts/start-dev.mjs b/dev/scripts/start-dev.mjs index a58a53697..33e8420a8 100644 --- a/dev/scripts/start-dev.mjs +++ b/dev/scripts/start-dev.mjs @@ -7,6 +7,7 @@ import process from "node:process"; import { fileURLToPath, pathToFileURL } from "node:url"; import { localAdminNotesHeaderPartialPath } from "../../src/dev-runtime/admin/admin-notes-menu.mjs"; import { startLocalApiServer } from "../../src/dev-runtime/server/local-api-server.mjs"; +import { resolveStaticRouteTarget } from "../../src/dev-runtime/server/static-web-root.mjs"; import { parseRoleArgument, parseTeamArgument, @@ -98,26 +99,6 @@ function contentTypeForPath(filePath) { return "application/octet-stream"; } -function isInsideRepoRoot(absolutePath) { - const relativePath = path.relative(repoRoot, absolutePath); - return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)); -} - -function resolveBrowserRoutePath(decodedPath) { - const normalizedPath = path.normalize(decodedPath).replace(/^(\.\.[/\\])+/, ""); - const webPath = normalizedPath.replace(/\\/g, "/"); - if (webPath === "/") { - return "/index.html"; - } - if (webPath === "/tools" || webPath.startsWith("/tools/")) { - return `/toolbox${webPath.slice("/tools".length)}`; - } - if (webPath === "/admin/admin-notes.html") { - return "/src/dev-runtime/admin/notes.html"; - } - return normalizedPath; -} - function parseModeArgument(args = []) { const values = Array.from(args); for (let index = 0; index < values.length; index += 1) { @@ -280,6 +261,7 @@ export async function startStaticWebServer({ apiBaseUrl, host = DEFAULT_HOST, port, + webRoot, } = {}) { if (!apiBaseUrl) { throw new Error("startStaticWebServer requires apiBaseUrl."); @@ -292,19 +274,13 @@ export async function startStaticWebServer({ return; } const decodedPath = decodeURIComponent(requestUrl.pathname); - const normalizedPath = resolveBrowserRoutePath(decodedPath); - const absolutePath = path.resolve(repoRoot, `.${normalizedPath}`); - if (!isInsideRepoRoot(absolutePath)) { - response.statusCode = 403; - response.end("Forbidden"); + const routeTarget = await resolveStaticRouteTarget({ decodedPath, repoRoot, webRoot }); + if (!routeTarget.targetPath) { + response.statusCode = 404; + response.end("Not Found"); return; } - let targetPath = absolutePath; - const stat = await fs.stat(targetPath).catch(() => null); - if (stat && stat.isDirectory()) { - targetPath = path.join(targetPath, "index.html"); - } - targetPath = localAdminNotesHeaderPartialPath(repoRoot, targetPath); + const targetPath = localAdminNotesHeaderPartialPath(repoRoot, routeTarget.targetPath); const responseContents = await fs.readFile(targetPath); response.statusCode = 200; response.setHeader("Content-Type", contentTypeForPath(targetPath)); @@ -321,7 +297,7 @@ export async function startStaticWebServer({ }); return { - baseUrl: localUrl(host, port), + baseUrl: localUrl(host, server.address()?.port || port), close: async () => { await new Promise((resolve, reject) => { server.close((error) => { diff --git a/dev/tests/dev-runtime/StaticWebRootCompatibility.test.mjs b/dev/tests/dev-runtime/StaticWebRootCompatibility.test.mjs new file mode 100644 index 000000000..ac4d3e70f --- /dev/null +++ b/dev/tests/dev-runtime/StaticWebRootCompatibility.test.mjs @@ -0,0 +1,82 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { startStaticWebServer } from "../../scripts/start-dev.mjs"; +import { + DEFAULT_LOCAL_WEB_ROOT, + LOCAL_WEB_ROOT_ENV, + WWW_LOCAL_WEB_ROOT, + resolveBrowserRoutePath, + resolveLocalWebRoot, + resolveStaticRouteTarget, +} from "../../../src/dev-runtime/server/static-web-root.mjs"; + +const repoRoot = path.resolve(fileURLToPath(new URL("../../..", import.meta.url))); + +test("local web root defaults to repository root and can be pointed at www", () => { + const defaultRoot = resolveLocalWebRoot({ env: {}, repoRoot }); + assert.equal(defaultRoot.relativePath, DEFAULT_LOCAL_WEB_ROOT); + assert.equal(defaultRoot.absolutePath, repoRoot); + assert.equal(defaultRoot.source, "default"); + + const wwwRoot = resolveLocalWebRoot({ + env: { + [LOCAL_WEB_ROOT_ENV]: WWW_LOCAL_WEB_ROOT, + }, + repoRoot, + }); + assert.equal(wwwRoot.relativePath, WWW_LOCAL_WEB_ROOT); + assert.equal(wwwRoot.absolutePath, path.join(repoRoot, "www")); + assert.equal(wwwRoot.source, "configured"); +}); + +test("browser route compatibility preserves public URLs while normalizing filesystem lookup", () => { + assert.equal(resolveBrowserRoutePath("/"), "/index.html"); + assert.equal(resolveBrowserRoutePath("/index.html"), "/index.html"); + assert.equal(resolveBrowserRoutePath("/toolbox/index.html"), "/toolbox/index.html"); + assert.equal(resolveBrowserRoutePath("/tools/game-design/index.html"), "/toolbox/game-design/index.html"); + assert.equal(resolveBrowserRoutePath("/assets/theme-v2/css/theme.css"), "/assets/theme-v2/css/theme.css"); + assert.equal(resolveBrowserRoutePath("/account/sign-in.html"), "/account/sign-in.html"); + assert.equal(resolveBrowserRoutePath("/admin/system-health.html"), "/admin/system-health.html"); + assert.equal(resolveBrowserRoutePath("/games/index.html"), "/games/index.html"); +}); + +test("static route target can prefer a configured www root before repo-root fallback", async () => { + const routeTarget = await resolveStaticRouteTarget({ + decodedPath: "/index.html", + repoRoot, + webRoot: "www", + }); + assert.equal(routeTarget.routePath, "/index.html"); + assert.equal(routeTarget.targetPath, path.join(repoRoot, "index.html")); + assert.equal(routeTarget.webRoot.relativePath, "www"); +}); + +test("local static web server can serve from a configurable web root", async () => { + const fixtureRoot = path.join(repoRoot, "dev", "workspace", "tmp", "static-web-root-compatibility"); + await fs.rm(fixtureRoot, { force: true, recursive: true }); + await fs.mkdir(path.join(fixtureRoot, "assets"), { recursive: true }); + await fs.writeFile(path.join(fixtureRoot, "index.html"), "