Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
12 changes: 6 additions & 6 deletions dev/build/ProjectInstructions/PROJECT_STATE.md
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -10,18 +10,18 @@ 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"
- "Bravo"
- "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/"
Expand Down
4 changes: 2 additions & 2 deletions dev/build/ProjectInstructions/backlog/BACKLOG_MASTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
16 changes: 16 additions & 0 deletions dev/build/ProjectInstructions/repository/www_migration_map.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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`.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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/`.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
Binary file modified dev/reports/codex_changed_files.txt
Binary file not shown.
Binary file modified dev/reports/codex_review.diff
Binary file not shown.
40 changes: 8 additions & 32 deletions dev/scripts/start-dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -280,6 +261,7 @@ export async function startStaticWebServer({
apiBaseUrl,
host = DEFAULT_HOST,
port,
webRoot,
} = {}) {
if (!apiBaseUrl) {
throw new Error("startStaticWebServer requires apiBaseUrl.");
Expand All @@ -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));
Expand All @@ -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) => {
Expand Down
82 changes: 82 additions & 0 deletions dev/tests/dev-runtime/StaticWebRootCompatibility.test.mjs
Original file line number Diff line number Diff line change
@@ -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"), "<!doctype html><title>Fixture Root</title>", "utf8");
await fs.writeFile(path.join(fixtureRoot, "assets", "probe.txt"), "fixture asset", "utf8");

const server = await startStaticWebServer({
apiBaseUrl: "http://127.0.0.1:1",
port: 0,
webRoot: fixtureRoot,
});

try {
const indexResponse = await fetch(`${server.baseUrl}/index.html`);
assert.equal(indexResponse.status, 200);
assert.match(await indexResponse.text(), /Fixture Root/);

const assetResponse = await fetch(`${server.baseUrl}/assets/probe.txt`);
assert.equal(assetResponse.status, 200);
assert.equal(await assetResponse.text(), "fixture asset");
} finally {
await server.close();
await fs.rm(fixtureRoot, { force: true, recursive: true });
}
});
Loading
Loading