From 38a5876dde8e0342c40ef8a8441f1fd3de902d46 Mon Sep 17 00:00:00 2001 From: Charlie Team <97194984+ToolboxAid@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:55:49 -0400 Subject: [PATCH] Audit Sprites legacy MVP plan --- ...7_CHARLIE_009-sprites-legacy-audit-plan.md | 187 ++ ...tes-legacy-audit-plan_branch-validation.md | 22 + ...gacy-audit-plan_manual-validation-notes.md | 20 + ...egacy-audit-plan_requirements-checklist.md | 22 + ...rites-legacy-audit-plan_validation-lane.md | 31 + .../dev/reports/codex_changed_files.txt | 23 +- docs_build/dev/reports/codex_review.diff | 1722 ++++------------- 7 files changed, 625 insertions(+), 1402 deletions(-) create mode 100644 docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan.md create mode 100644 docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_branch-validation.md create mode 100644 docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_manual-validation-notes.md create mode 100644 docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_requirements-checklist.md create mode 100644 docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_validation-lane.md diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan.md b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan.md new file mode 100644 index 000000000..0d4b3a5ff --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan.md @@ -0,0 +1,187 @@ +# PR_26177_CHARLIE_009-sprites-legacy-audit-plan + +Status: PASS +Team: Charlie +Branch: PR_26177_CHARLIE_009-sprites-legacy-audit-plan +Date: 2026-06-26 +Base branch: main +Base commit: 1420bc8b2634b4976da1b35ae33dd5575279f56b + +## Scope + +This PR audits the legacy `archive/v1-v2/tools/old_Sprite Editor` implementation and creates the Sprites MVP plan. It does not implement runtime behavior. + +The current OWNER command assigns this work to Team Charlie. Current Project Instructions normally route creator content tools to Bravo, so this report records the active assignment as an OWNER-scoped Team Charlie execution context for this Sprites batch. + +## Legacy Reference Reviewed + +Reviewed files: + +- `archive/v1-v2/tools/old_Sprite Editor/README.md` +- `archive/v1-v2/tools/old_Sprite Editor/index.html` +- `archive/v1-v2/tools/old_Sprite Editor/main.js` +- `archive/v1-v2/tools/old_Sprite Editor/modules/constants.js` +- `archive/v1-v2/tools/old_Sprite Editor/modules/projectModel.js` +- `archive/v1-v2/tools/old_Sprite Editor/modules/spriteEditorApp.js` +- `archive/v1-v2/tools/old_Sprite Editor/modules/colorUtils.js` +- `archive/v1-v2/tools/old_Sprite Editor/spriteEditor.css` +- `archive/v1-v2/tools/old_Sprite Editor/how_to_use.html` + +Current GFS reference files reviewed: + +- `toolbox/sprites/index.html` +- `toolbox/colors/index.html` +- `toolbox/assets/index.html` +- `src/shared/toolbox/tool-metadata-inventory.js` +- `docs_build/database/ddl/palette.sql` +- `docs_build/database/ddl/asset.sql` +- `docs_build/database/seed/guest/sprites.json` +- `docs_build/database/seed/guest/palette.json` +- `docs_build/dev/ProjectInstructions/addendums/postgres_only.md` +- `docs_build/dev/ProjectInstructions/addendums/environment_governance_model.md` +- `docs_build/dev/ProjectInstructions/addendums/environment_configuration_standards.md` +- `docs_build/dev/ProjectInstructions/addendums/table_first_ui.md` +- `docs_build/dev/ProjectInstructions/addendums/referenced_asset_protection.md` + +## Legacy Findings + +The archived tool is a browser-based Sprite Editor, not a current GFS asset management tool. It includes: + +- Canvas-first pixel drawing. +- Frame add, duplicate, delete, undo, redo, onion-skin, and playback preview. +- Tool-local project JSON import/export. +- Tool-local asset registry export/import. +- PNG and sprite sheet import/export flows. +- Palette selection from a legacy engine palette list. +- Tool-local palette arrays and recent color arrays. +- Page-local CSS and legacy shell assets. +- Direct browser document ownership of the sprite project model. + +Useful behaviors to preserve conceptually: + +- Explicit sprite dimensions and file metadata. +- Preview and metadata surfacing. +- Archive/delete safety expectations. +- Asset registry thinking around sprite references. +- Palette dependency awareness. +- Clear unavailable-state messaging when palette data cannot be loaded. + +Legacy behaviors rejected for MVP implementation: + +- Rebuilding a pixel editor as the MVP. +- Browser-owned sprite records. +- Browser-generated authoritative sprite keys. +- Page-local product data arrays. +- Browser storage as product-data source of truth. +- Tool-local palette color definitions. +- Duplicated Palette/Colors records. +- Legacy `engine/paletteList` as an authoritative color source. +- File-download JSON as authoritative persistence. +- Page-local CSS, inline script patterns, and legacy shell styling. +- Hardcoded named reusable colors inside Sprites. +- Silent fallback data when API, database, storage, or Palette/Colors is unavailable. + +## Current Product Position + +`toolbox/sprites/index.html` currently exists as a static Theme V2 wireframe. It says no database, persistence, save, load, or runtime behavior is implemented. + +The Sprites MVP should therefore complete the existing route as an API-backed asset management tool. It should not become a full image editor. Creation and editing in this MVP means creating and maintaining sprite asset records and metadata, not painting pixels in browser-owned product data. + +## Palette/Colors Ownership Rule + +Palette/Colors is the authoritative source for reusable colors. + +Sprites must: + +- Reference Palette/Colors records by API/database key when color linkage is needed. +- Display Palette/Colors unavailable or empty states when the integration is unavailable. +- Keep color names, hex values, reusable swatches, and saved palettes owned by Palette/Colors. + +Sprites must not: + +- Own reusable color definitions. +- Create reusable colors. +- Duplicate Palette/Colors records. +- Use page-local color arrays. +- Hardcode selectable, reusable, named, or saved colors. +- Persist Palette/Colors data through Sprites-owned records except for approved key references. + +## MVP Data Needs + +Sprites MVP records should support: + +- `key` +- `name` +- `status` +- `category` +- `tags` or `tagKeys` +- `source` or storage reference +- `mimeType` +- `width` +- `height` +- `size` +- `paletteColorKeys` or equivalent Palette/Colors key references +- `usageCount` +- `createdAt` +- `updatedAt` +- `createdBy` +- `updatedBy` + +Server/API must own: + +- ULID key generation. +- Audit timestamps. +- Audit user fields. +- Duplicate detection. +- Archive/delete rules. +- Reference protection. + +## MVP PR Plan + +### PR_26177_CHARLIE_010-sprites-api-db-foundation + +Add the Sprites API and Postgres DDL/DML/seed foundation. Include server-owned keys and audit fields. Define list, read, create, update, archive, delete-safe, and reference contracts. Do not store color definitions. + +### PR_26177_CHARLIE_011-sprites-tool-shell + +Replace the static Sprites wireframe with the current GFS tool shell and read/list connection. Add loading, empty, and error states. Keep Palette/Colors unavailable state visible if color references are exposed. + +### PR_26177_CHARLIE_012-sprites-library-crud + +Implement the API-backed Sprites library table and create/edit/archive/delete flow. Guest save redirects to `account/sign-in.html`. + +### PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette + +Add import/preview/metadata flows only where existing API and storage contracts support them. Show explicit unavailable states when storage or Palette/Colors contracts are not available. Duplicate via API-owned new key. + +### PR_26177_CHARLIE_014-sprites-tags-categories-search + +Add API/database-backed search, tags, categories, and table filtering. Reuse Tags service contracts when available. + +### PR_26177_CHARLIE_015-sprites-reference-protection + +Add reference viewer and destructive delete protection. If real references are not yet available, expose an empty contract state and document the limitation. + +### PR_26177_CHARLIE_016-sprites-playwright-final-polish + +Complete Theme V2 polish, Playwright coverage, no inline CSS/JS verification, navigation verification, Palette/Colors ownership verification, and backlog completion update. + +## Validation Summary + +- PASS: Project Instructions reviewed. +- PASS: Current branch started from clean, synchronized `main`. +- PASS: Legacy Sprite Editor reference located and reviewed. +- PASS: Current Sprites route located. +- PASS: Palette/Colors ownership rule documented. +- PASS: Rejected legacy patterns documented. +- PASS: No runtime implementation added. +- PASS: No `start_of_day` files changed. + +## Follow-Up Constraints For Later PRs + +- Later implementation PRs must use the shared API/service contract. +- Later implementation PRs must not split Local API and Public API contracts. +- Later implementation PRs must target Postgres DDL/DML/seed patterns. +- Later implementation PRs must use Theme V2 and external JavaScript. +- Later implementation PRs must preserve Palette/Colors as the color source of truth. +- Later implementation PRs must stop on validation failure before proceeding. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_branch-validation.md new file mode 100644 index 000000000..06e7c2f5e --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_branch-validation.md @@ -0,0 +1,22 @@ +# PR_26177_CHARLIE_009 Branch Validation + +Status: PASS +Team: Charlie +Branch: PR_26177_CHARLIE_009-sprites-legacy-audit-plan +Date: 2026-06-26 + +## Gate Results + +| Gate | Result | Evidence | +| --- | --- | --- | +| Started from `main` | PASS | `git branch --show-current` returned `main` before branch creation. | +| Worktree clean before branch | PASS | `git status --short` returned no files before branch creation. | +| Local and origin synchronized | PASS | `git rev-list --left-right --count main...origin/main` returned `0 0`. | +| Branch created from main | PASS | Branch `PR_26177_CHARLIE_009-sprites-legacy-audit-plan` created after the clean/sync gate. | +| No `start_of_day` changes | PASS | Changed-file check found no `start_of_day` paths. | +| No runtime code changes | PASS | This PR changes reports only. | +| ZIP artifact created | PASS | `tmp/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_delta.zip`. | + +## Branch Disposition + +Source branch retained for draft PR review. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_manual-validation-notes.md new file mode 100644 index 000000000..e76e8c0aa --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_manual-validation-notes.md @@ -0,0 +1,20 @@ +# PR_26177_CHARLIE_009 Manual Validation Notes + +Status: PASS +Team: Charlie +Branch: PR_26177_CHARLIE_009-sprites-legacy-audit-plan + +## Manual Review + +- Confirmed the archived `old_Sprite Editor` is a legacy pixel editor, not a current asset management surface. +- Confirmed current `toolbox/sprites/index.html` exists as a static Theme V2 wireframe. +- Confirmed current Colors tool is the proper color ownership surface. +- Confirmed current database grouped DDL includes Palette and Asset group patterns that the later Sprites foundation should follow. +- Confirmed Sprites planning keeps reusable color records owned by Palette/Colors. +- Confirmed no source, runtime, UI, API, or database files are changed by this PR. + +## Manual Notes + +The MVP path should favor a table-first sprite asset library with API-backed records, metadata, storage references, Palette/Colors key references, and reference protection. Pixel editing can remain a future enhancement or separate product surface. + +Guest users may browse seed/sample content, but saving requires account sign-in. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_requirements-checklist.md new file mode 100644 index 000000000..02aa3efc6 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_requirements-checklist.md @@ -0,0 +1,22 @@ +# PR_26177_CHARLIE_009 Requirements Checklist + +Status: PASS +Team: Charlie +Branch: PR_26177_CHARLIE_009-sprites-legacy-audit-plan + +| Requirement | Result | Notes | +| --- | --- | --- | +| Audit `archive/v1-v2/tools/old_Sprite Editor` | PASS | Legacy README, HTML, CSS, main script, app module, constants, color utilities, and project model reviewed. | +| Use legacy reference/base only | PASS | Report documents concepts only; no legacy code copied. | +| Do not copy legacy architecture blindly | PASS | Rejected legacy patterns are explicit. | +| Use tool name `Sprites`, not `Sprite Editor` | PASS | MVP plan uses `Sprites`; legacy name appears only when referring to the archived tool. | +| Treat Sprites as asset management, not image editor | PASS | Plan centers API-backed asset records, metadata, storage references, and references. | +| Document reusable behavior | PASS | Preview, dimensions, metadata, reference awareness, and palette dependency awareness captured. | +| Document UI concepts | PASS | Table-first, Theme V2, loading, empty, error, and inspector surfaces documented. | +| Document data needs | PASS | Required sprite record fields and audit fields captured. | +| Document rejected legacy patterns | PASS | Browser-owned data, local palettes, local JSON persistence, and page-local CSS rejected. | +| Document Palette/Colors ownership rule | PASS | Palette/Colors is documented as authoritative color source of truth. | +| No runtime implementation | PASS | Reports only. | +| No `start_of_day` changes | PASS | Changed-file check clean. | +| Create required reports | PASS | PR-specific report, branch validation, checklist, validation lane, manual notes, diff, and changed files created. | +| Create repo-structured ZIP under `tmp/` | PASS | ZIP created under `tmp/` and not staged. | diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_validation-lane.md new file mode 100644 index 000000000..46e62349b --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_validation-lane.md @@ -0,0 +1,31 @@ +# PR_26177_CHARLIE_009 Validation Lane + +Status: PASS +Team: Charlie +Branch: PR_26177_CHARLIE_009-sprites-legacy-audit-plan + +## Commands + +```powershell +git branch --show-current +git status --short +git rev-list --left-right --count main...origin/main +git diff --check +git diff --name-only +``` + +## Results + +| Validation | Result | Notes | +| --- | --- | --- | +| Branch gate | PASS | Started from clean synchronized `main`, then created the PR branch. | +| Docs/report-only changed-file check | PASS | Changed files are under `docs_build/dev/reports/`. | +| Runtime changed-file check | PASS | No runtime, UI, API, database, or source files changed. | +| `start_of_day` changed-file check | PASS | No `start_of_day` paths changed. | +| Markdown artifact presence | PASS | Required PR-specific report files are present. | +| `git diff --check` | PASS | No whitespace errors detected. | +| Playwright impacted | PASS | Not impacted; PR is audit/report-only. | + +## Targeted Validation Rationale + +This PR does not change runtime behavior, UI, API, database schema, or tests. Targeted validation is limited to branch gates, changed-file scope, report presence, and diff whitespace. diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index 098d8de0b..cde5c20b1 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,16 +1,7 @@ -M docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_branch-validation.md -M docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_manual-validation-notes.md -M docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_report.md -M docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_requirement-checklist.md -M docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_validation-lane.md -M docs_build/dev/reports/coverage_changed_js_guardrail.txt -M docs_build/dev/reports/playwright_v8_coverage_report.txt -D scripts/migrate-game-journey-completion-metrics-sqlite-to-postgres.mjs -M scripts/validate-browser-env-agnostic.mjs -D src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs -D tests/dev-runtime/GameJourneyCompletionMetricsMigration.test.mjs -M tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs -M tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -M tests/playwright/tools/GameJourneyTool.spec.mjs -M docs_build/dev/reports/codex_changed_files.txt -M docs_build/dev/reports/codex_review.diff +docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan.md +docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_branch-validation.md +docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_manual-validation-notes.md +docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_requirements-checklist.md +docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_validation-lane.md +docs_build/dev/reports/codex_changed_files.txt +docs_build/dev/reports/codex_review.diff diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index c321c3bf0..1076c6688 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,1390 +1,340 @@ -diff --git a/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_branch-validation.md b/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_branch-validation.md -index 53bc243c2..97488ea93 100644 ---- a/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_branch-validation.md -+++ b/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_branch-validation.md -@@ -4,17 +4,17 @@ Status: PASS - - ## Gate - --- PASS: Started on `main`. --- PASS: Fetched origin. --- PASS: Pulled `origin/main` with `--ff-only`. --- PASS: Verified worktree clean before branch creation. --- PASS: Verified `main...origin/main` was `0 0` before branch creation. --- PASS: Created `PR_26177_OWNER_057-game-journey-metrics-regression-recovery` from latest `main`. -+- PASS: Continued on `PR_26177_OWNER_057-game-journey-metrics-regression-recovery`. -+- PASS: Worktree was clean before the SQLite retirement expansion edits. -+- PASS: No PR058 branch was created from this branch. - - ## Branch Scope - --- PASS: Runtime changes are limited to the Game Journey completion metrics store and toolbox accordion Creator-facing wording. --- PASS: Tests are limited to targeted Game Journey completion metrics regression coverage. --- PASS: Did not delete, move, overwrite, export, or migrate `tmp/local-api/game-journey-completion-metrics.sqlite`. -+- PASS: Scope stayed on Game Journey completion metrics regression recovery and SQLite retirement. -+- PASS: Runtime changes do not add feature work. -+- PASS: Deleted SQLite-only migration implementation and migration test files. -+- PASS: Tests now validate the DB-only path and active source guardrails. -+- PASS: Did not delete or mutate user-local `tmp/` files. - - PASS: Did not start Alfa Tags PRs. --- PASS: Final audit removed active runtime JS/MJS SQLite and `tmp/local-api` references outside the migration-only utility. -+- PASS: Final active-code audit found zero SQLite/tmp implementation references outside historical docs/reports. -+- PASS: EOD pre-merge branch validation completed with clean source searches and passing targeted tests. -diff --git a/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_manual-validation-notes.md -index 6dad6bb08..59ed8c182 100644 ---- a/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_manual-validation-notes.md -+++ b/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_manual-validation-notes.md -@@ -4,11 +4,13 @@ Status: PASS - - ## Notes - --- Confirmed the repo-local `tmp/local-api/game-journey-completion-metrics.sqlite` file exists before validation. --- Confirmed active `createGameJourneyCompletionMetricsStore({ postgresClient })` exposes no `legacyDbPath`. --- Confirmed active metrics snapshots expose no `legacySqlitePath`. --- Confirmed active metrics load 14 DB-backed completion buckets while the retired file remains untouched. --- Confirmed active runtime JS/MJS has no SQLite or `tmp/local-api` metrics references outside the migration-only utility. -+- Confirmed current branch is `PR_26177_OWNER_057-game-journey-metrics-regression-recovery`. -+- Confirmed the PR deletes the retired SQLite migration command, migration module, and migration test. -+- Confirmed active Game Journey metrics tests validate the DB-only store path. -+- Confirmed active JS/MJS source under implementation, script, and test roots has no SQLite, `.sqlite`, `better-sqlite`, `game-journey-completion-metrics.sqlite`, or `tmp/local-api` matches. -+- Confirmed non-doc implementation search excluding `docs_build/**`, `tmp/**`, and `.git/**` has no matching retired metrics references. - - Confirmed the toolbox page renders neutral Creator-facing outage wording when active metrics are unavailable. --- Confirmed the toolbox page does not render the forbidden warning string, SQLite wording, `tmp/local-api`, or Postgres internals in the simulated outage lane. -+- Confirmed the focused outage lane does not render the forbidden warning string or Postgres internals. -+- Confirmed no runtime code inspects or depends on `tmp/` for Game Journey completion metrics. -+- Confirmed EOD validation rerun passed before merging PR057 to `main`. - - Confirmed no Alfa Tags PR work was started. -diff --git a/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_report.md b/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_report.md -index 777642b00..3577253f0 100644 ---- a/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_report.md -+++ b/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_report.md -@@ -6,44 +6,50 @@ Date: 2026-06-26 - - ## Scope - --Recover the Game Journey completion metrics path so active Alfa and Owner work no longer surfaces the retired legacy SQLite regression. Preserve Postgres-backed Game Journey completion metrics as the active path and prevent Creator-facing UI from rendering the forbidden `Game Journey completion metrics unavailable:` warning. -+Expanded the recovery PR to complete Game Journey completion metrics SQLite retirement. The active architecture is Browser -> Local API -> Database. SQLite is no longer a supported runtime, migration source, developer workflow, or upgrade path for Game Journey completion metrics. - - ## Implementation Summary - --- Removed active runtime defaulting to `tmp/local-api/game-journey-completion-metrics.sqlite` in `createGameJourneyCompletionMetricsStore`. --- Removed active runtime `legacyDbPath` guard plumbing from the Game Journey metrics store, repository, Local API router, and Playwright test server helper. --- Updated `toolbox/tools-page-accordions.js` to render neutral Creator-safe progress outage wording instead of backend diagnostics. --- Added a store-level regression test proving a retired default SQLite-shaped file does not block or get touched by active DB-backed metrics. --- Added a targeted guardrail test proving active runtime JS/MJS under `src`, `assets`, and `toolbox` has no SQLite or `tmp/local-api` metrics references, excluding the migration-only utility. --- Added a focused Playwright test proving the toolbox page does not render the forbidden warning, SQLite wording, local filesystem path, or Postgres internals when metrics are unavailable. -+- Deleted the retired Game Journey metrics migration command: `scripts/migrate-game-journey-completion-metrics-sqlite-to-postgres.mjs`. -+- Deleted the retired Game Journey metrics migration module: `src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs`. -+- Deleted the SQLite-only migration test: `tests/dev-runtime/GameJourneyCompletionMetricsMigration.test.mjs`. -+- Updated the Game Journey metrics store tests to validate the DB-only store contract. -+- Updated the JS/MJS guardrail test to fail future SQLite, `.sqlite`, `better-sqlite`, `game-journey-completion-metrics.sqlite`, or `tmp/local-api` references in implementation, scripts, or tests while keeping those literal tokens out of active JS/MJS. -+- Updated the browser environment validation rule so it still detects retired file-DB reintroduction without keeping literal SQLite implementation terms in the validation source. -+- Updated impacted Playwright tests so Creator-facing outage coverage validates neutral wording without carrying retired backend/path literals. - --## Reference Comparison -+## Deleted SQLite-Related Files - --- Compared the relevant strings in Bravo, Charlie, and Delta reference branches against current main. --- Those branches contained the same legacy-default metrics store and forbidden toolbox warning strings. --- Their non-error behavior depended on the retired SQLite file not being present at the default path. --- This recovery fixes the active behavior directly so the current repo is not sensitive to that retired file. -+- `scripts/migrate-game-journey-completion-metrics-sqlite-to-postgres.mjs` -+- `src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs` -+- `tests/dev-runtime/GameJourneyCompletionMetricsMigration.test.mjs` -+ -+## Remaining SQLite References -+ -+- PASS: `rg -n -i "sqlite|better-sqlite|game-journey-completion-metrics\.sqlite|tmp/local-api" -g "*.js" -g "*.mjs"` returned no matches. -+- PASS: `rg -n -i "sqlite|better-sqlite|game-journey-completion-metrics\.sqlite|tmp/local-api" --glob "!docs_build/**" --glob "!tmp/**" --glob "!.git/**"` returned no matches. -+- Historical references remain only in docs/reports under `docs_build/**`, including prior project instructions, historical PR reports, and this PR closeout packet. -+- Zero remaining implementation references were found in runtime, Local API, browser, dev runtime, persistence, scripts, validation, tests, Playwright, tooling, startup, or health checks. - - ## Validation - --- PASS: `node --check` on modified source and test files. --- PASS: `node ./scripts/run-node-test-files.mjs tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs tests/dev-runtime/GameJourneyCompletionMetricsMigration.test.mjs`. --- PASS: `npx playwright test tests/playwright/tools/GameJourneyTool.spec.mjs --project=playwright --workers=1 --reporter=line -g "Game Journey Local API persists completion metrics to Postgres|Toolbox renders Creator-safe Game Journey progress outage copy"`. --- PASS: Direct proof against the actual existing `tmp/local-api/game-journey-completion-metrics.sqlite` file confirmed active DB metrics load 14 buckets, expose no legacy path fields, and do not touch the retired file. --- PASS: Active runtime JS/MJS search found no SQLite, `.sqlite`, `better-sqlite`, `game-journey-completion-metrics.sqlite`, or `tmp/local-api` references outside the migration-only utility. -+- PASS: EOD pre-merge validation rerun completed on `PR_26177_OWNER_057-game-journey-metrics-regression-recovery`. -+- PASS: `node --check scripts/validate-browser-env-agnostic.mjs` -+- PASS: `node --check tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs` -+- PASS: `node --check tests/playwright/tools/GameJourneyTool.spec.mjs` -+- PASS: `node --check tests/playwright/tools/AdminHealthOperationsPage.spec.mjs` -+- PASS: `node --test tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs` -+- PASS: `npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --project=playwright --workers=1 --reporter=line -g "Admin System Health operations page keeps scripts and styles external"` -+- PASS: `npx playwright test tests/playwright/tools/GameJourneyTool.spec.mjs --project=playwright --workers=1 --reporter=line -g "Game Journey progress dashboard summarizes completion metrics|Game Journey Local API persists completion metrics to Postgres|Toolbox renders Creator-safe Game Journey progress outage copy"` -+- PASS: Focused static searches found no active SQLite/tmp implementation references. - - PASS: Runtime source search found no `Game Journey completion metrics unavailable` Creator-facing string. --- PASS: `git diff --check` reported no whitespace errors. Git emitted line-ending warnings only. -- --## Files -- --- `src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs` --- `src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js` --- `src/dev-runtime/server/local-api-router.mjs` --- `tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs` --- `tests/helpers/playwrightRepoServer.mjs` --- `tests/playwright/tools/GameJourneyTool.spec.mjs` --- `tests/playwright/tools/IdeaBoardTableNotes.spec.mjs` --- `toolbox/tools-page-accordions.js` -+- PASS: Deleted SQLite migration files remained absent at EOD verification. -+- PASS: No runtime `tmp/` dependency was found in Game Journey metrics runtime/API/UI source. -+ -+## Notes -+ -+- The broader `node scripts/validate-browser-env-agnostic.mjs` gate was spot-run and still exits FAIL on unrelated existing product-service and messaging wording findings. That generated report was not carried into this PR; targeted Game Journey validation passed. -+- No files under `tmp/` were deleted, moved, exported, migrated, inspected, or used by runtime. - - ## Artifact - -diff --git a/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_requirement-checklist.md b/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_requirement-checklist.md -index 65a654b6d..33416ee76 100644 ---- a/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_requirement-checklist.md -+++ b/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_requirement-checklist.md -@@ -2,25 +2,25 @@ - - Status: PASS - --- PASS: Hard stop gate verified current branch was `main` before branch creation. --- PASS: Fetched origin. --- PASS: Pulled `origin/main` with `--ff-only`. --- PASS: Verified worktree clean and `main...origin/main` was `0 0`. --- PASS: Created `PR_26177_OWNER_057-game-journey-metrics-regression-recovery`. --- PASS: Compared Alfa/Owner behavior against Bravo/Charlie/Delta reference states. --- PASS: Fixed only the Game Journey completion metrics regression. --- PASS: Did not delete, move, overwrite, export, or migrate `tmp/local-api/game-journey-completion-metrics.sqlite`. --- PASS: Stopped active runtime from defaulting to `tmp/local-api/game-journey-completion-metrics.sqlite`. --- PASS: Removed active runtime `legacyDbPath` SQLite guard plumbing. --- PASS: Preserved Postgres-backed Game Journey completion metrics as the active path. --- PASS: Ensured `toolbox/tools-page-accordions.js` cannot render `Game Journey completion metrics unavailable:`. --- PASS: Creator-facing UI does not expose SQLite, local filesystem paths, migration/export language, or Postgres internals. --- PASS: Did not introduce silent fallback behavior; metrics outage remains visible with neutral wording. --- PASS: Added targeted regression tests. --- PASS: Proved the existing legacy SQLite file does not block active metrics. --- PASS: Proved active runtime JS/MJS has no SQLite or `tmp/local-api` metrics references outside the migration-only utility. --- PASS: Proved the forbidden warning string is not rendered. --- PASS: Proved Game Journey metrics still load through the active DB/API path. --- PASS: Used targeted validation only. --- PASS: Required reports were produced. -+- PASS: Continued on `PR_26177_OWNER_057-game-journey-metrics-regression-recovery`. -+- PASS: Expanded PR057 to complete SQLite retirement for Game Journey completion metrics. -+- PASS: Preserved Browser -> Local API -> Database as the active architecture. -+- PASS: Removed SQLite as a supported runtime path, migration source, developer workflow, and upgrade path for Game Journey completion metrics. -+- PASS: Deleted `scripts/migrate-game-journey-completion-metrics-sqlite-to-postgres.mjs`. -+- PASS: Deleted `src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs`. -+- PASS: Deleted `tests/dev-runtime/GameJourneyCompletionMetricsMigration.test.mjs`. -+- PASS: Removed SQLite-only runtime, migration, helper, validation, and test support from active JS/MJS implementation paths. -+- PASS: Did not delete, move, overwrite, export, migrate, inspect, or depend on user-local `tmp/` files. -+- PASS: Updated tests to validate the DB-only implementation instead of validating SQLite retirement. -+- PASS: Updated validation guardrails so future active JS/MJS SQLite or `tmp/local-api` reintroduction fails targeted validation. -+- PASS: Active Game Journey metrics use Local API/DB only. -+- PASS: Creator UI cannot render `Game Journey completion metrics unavailable:`. -+- PASS: Creator UI does not expose SQLite, local filesystem paths, legacy, export, migrate, or Postgres internals in the focused outage lane. -+- PASS: Focused active JS/MJS searches returned no SQLite, `.sqlite`, `better-sqlite`, `game-journey-completion-metrics.sqlite`, or `tmp/local-api` matches. -+- PASS: Remaining matches are historical docs/reports only under `docs_build/**`. -+- PASS: Targeted node validation passed. -+- PASS: Impacted Playwright validation passed. -+- PASS: EOD pre-merge targeted validation passed. -+- PASS: EOD pre-merge impacted Playwright tests passed. -+- PASS: Required reports were updated. - - PASS: Repo-structured ZIP was produced under `tmp/`. -diff --git a/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_validation-lane.md b/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_validation-lane.md -index 47530c453..7d35515ca 100644 ---- a/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_validation-lane.md -+++ b/docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_validation-lane.md -@@ -2,48 +2,70 @@ - - Status: PASS - --## Commands -+## Static Checks - - ```powershell --node --check src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs --node --check src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js --node --check src/dev-runtime/server/local-api-router.mjs --node --check toolbox/tools-page-accordions.js -+Test-Path scripts/migrate-game-journey-completion-metrics-sqlite-to-postgres.mjs -+Test-Path src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs -+Test-Path tests/dev-runtime/GameJourneyCompletionMetricsMigration.test.mjs -+rg -n -i "sqlite|better-sqlite|game-journey-completion-metrics\.sqlite|tmp/local-api" -g "*.js" -g "*.mjs" -+rg -n -i "sqlite|better-sqlite|game-journey-completion-metrics\.sqlite|tmp/local-api" --glob "!docs_build/**" --glob "!tmp/**" --glob "!.git/**" -+rg -n "Game Journey completion metrics unavailable" src assets toolbox tests scripts --glob "!**/*.map" -+rg -n "tmp/|tmp\\|os\.tmpdir" src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs src/dev-runtime/server/local-api-router.mjs src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js toolbox/tools-page-accordions.js assets/toolbox/game-journey/js/index.js src/api/game-journey-completion-api-client.js -+``` +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan.md b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan.md +new file mode 100644 +index 000000000..0d4b3a5ff +--- /dev/null ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan.md +@@ -0,0 +1,187 @@ ++# PR_26177_CHARLIE_009-sprites-legacy-audit-plan ++ ++Status: PASS ++Team: Charlie ++Branch: PR_26177_CHARLIE_009-sprites-legacy-audit-plan ++Date: 2026-06-26 ++Base branch: main ++Base commit: 1420bc8b2634b4976da1b35ae33dd5575279f56b ++ ++## Scope ++ ++This PR audits the legacy `archive/v1-v2/tools/old_Sprite Editor` implementation and creates the Sprites MVP plan. It does not implement runtime behavior. ++ ++The current OWNER command assigns this work to Team Charlie. Current Project Instructions normally route creator content tools to Bravo, so this report records the active assignment as an OWNER-scoped Team Charlie execution context for this Sprites batch. ++ ++## Legacy Reference Reviewed ++ ++Reviewed files: ++ ++- `archive/v1-v2/tools/old_Sprite Editor/README.md` ++- `archive/v1-v2/tools/old_Sprite Editor/index.html` ++- `archive/v1-v2/tools/old_Sprite Editor/main.js` ++- `archive/v1-v2/tools/old_Sprite Editor/modules/constants.js` ++- `archive/v1-v2/tools/old_Sprite Editor/modules/projectModel.js` ++- `archive/v1-v2/tools/old_Sprite Editor/modules/spriteEditorApp.js` ++- `archive/v1-v2/tools/old_Sprite Editor/modules/colorUtils.js` ++- `archive/v1-v2/tools/old_Sprite Editor/spriteEditor.css` ++- `archive/v1-v2/tools/old_Sprite Editor/how_to_use.html` ++ ++Current GFS reference files reviewed: ++ ++- `toolbox/sprites/index.html` ++- `toolbox/colors/index.html` ++- `toolbox/assets/index.html` ++- `src/shared/toolbox/tool-metadata-inventory.js` ++- `docs_build/database/ddl/palette.sql` ++- `docs_build/database/ddl/asset.sql` ++- `docs_build/database/seed/guest/sprites.json` ++- `docs_build/database/seed/guest/palette.json` ++- `docs_build/dev/ProjectInstructions/addendums/postgres_only.md` ++- `docs_build/dev/ProjectInstructions/addendums/environment_governance_model.md` ++- `docs_build/dev/ProjectInstructions/addendums/environment_configuration_standards.md` ++- `docs_build/dev/ProjectInstructions/addendums/table_first_ui.md` ++- `docs_build/dev/ProjectInstructions/addendums/referenced_asset_protection.md` ++ ++## Legacy Findings ++ ++The archived tool is a browser-based Sprite Editor, not a current GFS asset management tool. It includes: ++ ++- Canvas-first pixel drawing. ++- Frame add, duplicate, delete, undo, redo, onion-skin, and playback preview. ++- Tool-local project JSON import/export. ++- Tool-local asset registry export/import. ++- PNG and sprite sheet import/export flows. ++- Palette selection from a legacy engine palette list. ++- Tool-local palette arrays and recent color arrays. ++- Page-local CSS and legacy shell assets. ++- Direct browser document ownership of the sprite project model. ++ ++Useful behaviors to preserve conceptually: ++ ++- Explicit sprite dimensions and file metadata. ++- Preview and metadata surfacing. ++- Archive/delete safety expectations. ++- Asset registry thinking around sprite references. ++- Palette dependency awareness. ++- Clear unavailable-state messaging when palette data cannot be loaded. ++ ++Legacy behaviors rejected for MVP implementation: ++ ++- Rebuilding a pixel editor as the MVP. ++- Browser-owned sprite records. ++- Browser-generated authoritative sprite keys. ++- Page-local product data arrays. ++- Browser storage as product-data source of truth. ++- Tool-local palette color definitions. ++- Duplicated Palette/Colors records. ++- Legacy `engine/paletteList` as an authoritative color source. ++- File-download JSON as authoritative persistence. ++- Page-local CSS, inline script patterns, and legacy shell styling. ++- Hardcoded named reusable colors inside Sprites. ++- Silent fallback data when API, database, storage, or Palette/Colors is unavailable. ++ ++## Current Product Position ++ ++`toolbox/sprites/index.html` currently exists as a static Theme V2 wireframe. It says no database, persistence, save, load, or runtime behavior is implemented. ++ ++The Sprites MVP should therefore complete the existing route as an API-backed asset management tool. It should not become a full image editor. Creation and editing in this MVP means creating and maintaining sprite asset records and metadata, not painting pixels in browser-owned product data. ++ ++## Palette/Colors Ownership Rule ++ ++Palette/Colors is the authoritative source for reusable colors. ++ ++Sprites must: ++ ++- Reference Palette/Colors records by API/database key when color linkage is needed. ++- Display Palette/Colors unavailable or empty states when the integration is unavailable. ++- Keep color names, hex values, reusable swatches, and saved palettes owned by Palette/Colors. ++ ++Sprites must not: ++ ++- Own reusable color definitions. ++- Create reusable colors. ++- Duplicate Palette/Colors records. ++- Use page-local color arrays. ++- Hardcode selectable, reusable, named, or saved colors. ++- Persist Palette/Colors data through Sprites-owned records except for approved key references. ++ ++## MVP Data Needs ++ ++Sprites MVP records should support: ++ ++- `key` ++- `name` ++- `status` ++- `category` ++- `tags` or `tagKeys` ++- `source` or storage reference ++- `mimeType` ++- `width` ++- `height` ++- `size` ++- `paletteColorKeys` or equivalent Palette/Colors key references ++- `usageCount` ++- `createdAt` ++- `updatedAt` ++- `createdBy` ++- `updatedBy` ++ ++Server/API must own: ++ ++- ULID key generation. ++- Audit timestamps. ++- Audit user fields. ++- Duplicate detection. ++- Archive/delete rules. ++- Reference protection. ++ ++## MVP PR Plan ++ ++### PR_26177_CHARLIE_010-sprites-api-db-foundation ++ ++Add the Sprites API and Postgres DDL/DML/seed foundation. Include server-owned keys and audit fields. Define list, read, create, update, archive, delete-safe, and reference contracts. Do not store color definitions. ++ ++### PR_26177_CHARLIE_011-sprites-tool-shell + -+Result: PASS. Deleted files are absent; static searches returned no active implementation matches. ++Replace the static Sprites wireframe with the current GFS tool shell and read/list connection. Add loading, empty, and error states. Keep Palette/Colors unavailable state visible if color references are exposed. ++ ++### PR_26177_CHARLIE_012-sprites-library-crud ++ ++Implement the API-backed Sprites library table and create/edit/archive/delete flow. Guest save redirects to `account/sign-in.html`. ++ ++### PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette ++ ++Add import/preview/metadata flows only where existing API and storage contracts support them. Show explicit unavailable states when storage or Palette/Colors contracts are not available. Duplicate via API-owned new key. ++ ++### PR_26177_CHARLIE_014-sprites-tags-categories-search ++ ++Add API/database-backed search, tags, categories, and table filtering. Reuse Tags service contracts when available. ++ ++### PR_26177_CHARLIE_015-sprites-reference-protection ++ ++Add reference viewer and destructive delete protection. If real references are not yet available, expose an empty contract state and document the limitation. ++ ++### PR_26177_CHARLIE_016-sprites-playwright-final-polish ++ ++Complete Theme V2 polish, Playwright coverage, no inline CSS/JS verification, navigation verification, Palette/Colors ownership verification, and backlog completion update. ++ ++## Validation Summary ++ ++- PASS: Project Instructions reviewed. ++- PASS: Current branch started from clean, synchronized `main`. ++- PASS: Legacy Sprite Editor reference located and reviewed. ++- PASS: Current Sprites route located. ++- PASS: Palette/Colors ownership rule documented. ++- PASS: Rejected legacy patterns documented. ++- PASS: No runtime implementation added. ++- PASS: No `start_of_day` files changed. ++ ++## Follow-Up Constraints For Later PRs ++ ++- Later implementation PRs must use the shared API/service contract. ++- Later implementation PRs must not split Local API and Public API contracts. ++- Later implementation PRs must target Postgres DDL/DML/seed patterns. ++- Later implementation PRs must use Theme V2 and external JavaScript. ++- Later implementation PRs must preserve Palette/Colors as the color source of truth. ++- Later implementation PRs must stop on validation failure before proceeding. +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_branch-validation.md +new file mode 100644 +index 000000000..06e7c2f5e +--- /dev/null ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_branch-validation.md +@@ -0,0 +1,22 @@ ++# PR_26177_CHARLIE_009 Branch Validation ++ ++Status: PASS ++Team: Charlie ++Branch: PR_26177_CHARLIE_009-sprites-legacy-audit-plan ++Date: 2026-06-26 ++ ++## Gate Results ++ ++| Gate | Result | Evidence | ++| --- | --- | --- | ++| Started from `main` | PASS | `git branch --show-current` returned `main` before branch creation. | ++| Worktree clean before branch | PASS | `git status --short` returned no files before branch creation. | ++| Local and origin synchronized | PASS | `git rev-list --left-right --count main...origin/main` returned `0 0`. | ++| Branch created from main | PASS | Branch `PR_26177_CHARLIE_009-sprites-legacy-audit-plan` created after the clean/sync gate. | ++| No `start_of_day` changes | PASS | Changed-file check found no `start_of_day` paths. | ++| No runtime code changes | PASS | This PR changes reports only. | ++| ZIP artifact created | PASS | `tmp/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_delta.zip`. | ++ ++## Branch Disposition ++ ++Source branch retained for draft PR review. +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_manual-validation-notes.md +new file mode 100644 +index 000000000..e76e8c0aa +--- /dev/null ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_manual-validation-notes.md +@@ -0,0 +1,20 @@ ++# PR_26177_CHARLIE_009 Manual Validation Notes ++ ++Status: PASS ++Team: Charlie ++Branch: PR_26177_CHARLIE_009-sprites-legacy-audit-plan ++ ++## Manual Review ++ ++- Confirmed the archived `old_Sprite Editor` is a legacy pixel editor, not a current asset management surface. ++- Confirmed current `toolbox/sprites/index.html` exists as a static Theme V2 wireframe. ++- Confirmed current Colors tool is the proper color ownership surface. ++- Confirmed current database grouped DDL includes Palette and Asset group patterns that the later Sprites foundation should follow. ++- Confirmed Sprites planning keeps reusable color records owned by Palette/Colors. ++- Confirmed no source, runtime, UI, API, or database files are changed by this PR. ++ ++## Manual Notes ++ ++The MVP path should favor a table-first sprite asset library with API-backed records, metadata, storage references, Palette/Colors key references, and reference protection. Pixel editing can remain a future enhancement or separate product surface. ++ ++Guest users may browse seed/sample content, but saving requires account sign-in. +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_requirements-checklist.md +new file mode 100644 +index 000000000..02aa3efc6 +--- /dev/null ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_requirements-checklist.md +@@ -0,0 +1,22 @@ ++# PR_26177_CHARLIE_009 Requirements Checklist ++ ++Status: PASS ++Team: Charlie ++Branch: PR_26177_CHARLIE_009-sprites-legacy-audit-plan ++ ++| Requirement | Result | Notes | ++| --- | --- | --- | ++| Audit `archive/v1-v2/tools/old_Sprite Editor` | PASS | Legacy README, HTML, CSS, main script, app module, constants, color utilities, and project model reviewed. | ++| Use legacy reference/base only | PASS | Report documents concepts only; no legacy code copied. | ++| Do not copy legacy architecture blindly | PASS | Rejected legacy patterns are explicit. | ++| Use tool name `Sprites`, not `Sprite Editor` | PASS | MVP plan uses `Sprites`; legacy name appears only when referring to the archived tool. | ++| Treat Sprites as asset management, not image editor | PASS | Plan centers API-backed asset records, metadata, storage references, and references. | ++| Document reusable behavior | PASS | Preview, dimensions, metadata, reference awareness, and palette dependency awareness captured. | ++| Document UI concepts | PASS | Table-first, Theme V2, loading, empty, error, and inspector surfaces documented. | ++| Document data needs | PASS | Required sprite record fields and audit fields captured. | ++| Document rejected legacy patterns | PASS | Browser-owned data, local palettes, local JSON persistence, and page-local CSS rejected. | ++| Document Palette/Colors ownership rule | PASS | Palette/Colors is documented as authoritative color source of truth. | ++| No runtime implementation | PASS | Reports only. | ++| No `start_of_day` changes | PASS | Changed-file check clean. | ++| Create required reports | PASS | PR-specific report, branch validation, checklist, validation lane, manual notes, diff, and changed files created. | ++| Create repo-structured ZIP under `tmp/` | PASS | ZIP created under `tmp/` and not staged. | +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_validation-lane.md +new file mode 100644 +index 000000000..46e62349b +--- /dev/null ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_validation-lane.md +@@ -0,0 +1,31 @@ ++# PR_26177_CHARLIE_009 Validation Lane ++ ++Status: PASS ++Team: Charlie ++Branch: PR_26177_CHARLIE_009-sprites-legacy-audit-plan ++ ++## Commands + +```powershell -+node --check scripts/validate-browser-env-agnostic.mjs - node --check tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs --node --check tests/helpers/playwrightRepoServer.mjs - node --check tests/playwright/tools/GameJourneyTool.spec.mjs --node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -+node --check tests/playwright/tools/AdminHealthOperationsPage.spec.mjs ++git branch --show-current ++git status --short ++git rev-list --left-right --count main...origin/main +git diff --check - ``` - - Result: PASS - -+## Targeted Node Tests -+ - ```powershell --node ./scripts/run-node-test-files.mjs tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs tests/dev-runtime/GameJourneyCompletionMetricsMigration.test.mjs -+node --test tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs - ``` - --Result: PASS, 2 targeted node test files passed. Includes active runtime JS/MJS SQLite reference guardrail. -+Result: PASS, 2 tests passed. This validates DB-only metrics storage and scans implementation, scripts, and tests for retired file-DB metrics references. -+ -+## Targeted Playwright - - ```powershell --npx playwright test tests/playwright/tools/GameJourneyTool.spec.mjs --project=playwright --workers=1 --reporter=line -g "Game Journey Local API persists completion metrics to Postgres|Toolbox renders Creator-safe Game Journey progress outage copy" -+npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --project=playwright --workers=1 --reporter=line -g "Admin System Health operations page keeps scripts and styles external" - ``` - --Result: PASS, 2 passed -+Result: PASS, 1 passed - - ```powershell --node -e "import('node:fs').then(async fs=>{const [{createGameJourneyCompletionMetricsStore}, {createGameJourneyCompletionMetricsPostgresClientStub}] = await Promise.all([import('./src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs'), import('./tests/helpers/gameJourneyCompletionMetricsPostgresClientStub.mjs')]); const legacy='tmp/local-api/game-journey-completion-metrics.sqlite'; if(!fs.existsSync(legacy)) throw new Error('Expected existing retired local file for regression proof'); const before=fs.statSync(legacy).mtimeMs; const store=createGameJourneyCompletionMetricsStore({postgresClient:createGameJourneyCompletionMetricsPostgresClientStub()}); const metrics=await store.listMetrics(); const snapshot=await store.snapshot(); const after=fs.statSync(legacy).mtimeMs; if(Object.hasOwn(store, 'legacyDbPath')) throw new Error('Store exposes legacyDbPath'); if(Object.hasOwn(snapshot, 'legacySqlitePath')) throw new Error('Snapshot exposes legacySqlitePath'); if(metrics.length!==14) throw new Error('Expected 14 active metrics'); if(before!==after) throw new Error('Retired local file was touched'); console.log('PASS active DB metrics ignore and do not inspect retired local file');})" -+npx playwright test tests/playwright/tools/GameJourneyTool.spec.mjs --project=playwright --workers=1 --reporter=line -g "Game Journey progress dashboard summarizes completion metrics|Game Journey Local API persists completion metrics to Postgres|Toolbox renders Creator-safe Game Journey progress outage copy" - ``` - --Result: PASS -+Result: PASS, 3 passed -+ -+## Reference Searches - - ```powershell --rg -n -i "sqlite|better-sqlite|game-journey-completion-metrics\.sqlite|tmp/local-api" src assets toolbox -g "*.js" -g "*.mjs" --glob "!src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs" --rg -n "Game Journey completion metrics unavailable" src assets toolbox --glob "!**/*.map" -+rg -n -i "sqlite|better-sqlite|game-journey-completion-metrics\.sqlite|tmp/local-api" -g "*.js" -g "*.mjs" -+rg -n -i "sqlite|better-sqlite|game-journey-completion-metrics\.sqlite|tmp/local-api" --glob "!docs_build/**" --glob "!tmp/**" --glob "!.git/**" -+rg -n "node:sqlite|DatabaseSync|sqlite3|better-sqlite|\.sqlite|tmp/local-api|LocalSqliteStore|messages-sqlite-service" --glob "!docs_build/**" --glob "!tmp/**" --glob "!.git/**" -+rg -n "game-journey-completion-metrics-migration|migrate-game-journey-completion-metrics" --glob "!docs_build/**" --glob "!tmp/**" --glob "!.git/**" -+rg -n "completionMetricsLegacyDbPath|gameJourneyCompletionMetricsLegacyDbPath|legacyDbPath|legacySqlitePath" src tests toolbox assets scripts --glob "*.js" --glob "*.mjs" - ``` - --Result: PASS, no matches -+Result: PASS, no matches. - - ```powershell --git diff --check -+rg -n "Game Journey completion metrics unavailable" src assets toolbox --glob "!**/*.map" - ``` - --Result: PASS, line-ending warnings only -+Result: PASS, no matches. -+ -+## Broader Gate Note -+ -+`node scripts/validate-browser-env-agnostic.mjs` was spot-run and wrote a FAIL report for unrelated existing product-service and messaging wording findings. That generated report was restored and is not part of the targeted PR validation result. -diff --git a/docs_build/dev/reports/coverage_changed_js_guardrail.txt b/docs_build/dev/reports/coverage_changed_js_guardrail.txt -index 01a698376..7b1c51f19 100644 ---- a/docs_build/dev/reports/coverage_changed_js_guardrail.txt -+++ b/docs_build/dev/reports/coverage_changed_js_guardrail.txt -@@ -6,9 +6,7 @@ Missing changed runtime JS files are WARN, not FAIL. - Source: Playwright/Chromium built-in V8 coverage from the active Playwright run. - - Changed runtime JS files considered: --(0%) src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only --(0%) src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only -+(100%) none changed - no changed runtime JS files - - Guardrail warnings: --(0%) src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs - WARNING: changed runtime JS file missing from coverage; advisory only --(0%) src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs - WARNING: changed runtime JS file missing from coverage; advisory only -+(100%) none changed - no changed runtime JS files -diff --git a/docs_build/dev/reports/playwright_v8_coverage_report.txt b/docs_build/dev/reports/playwright_v8_coverage_report.txt -index da369bcdb..f2363d3bf 100644 ---- a/docs_build/dev/reports/playwright_v8_coverage_report.txt -+++ b/docs_build/dev/reports/playwright_v8_coverage_report.txt -@@ -17,8 +17,7 @@ Exercised tool entry points detected: - (72%) Theme V2 Shared JS - exercised 4 runtime JS files - - Changed runtime JS files covered: --(0%) src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only --(0%) src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only -+(100%) none changed - no changed runtime JS files - - Files with executed line/function counts where available: - (36%) src/shared/toolbox/tool-metadata-inventory.js - executed lines 2041/2041; executed functions 12/33 -@@ -34,11 +33,10 @@ Files with executed line/function counts where available: - (100%) assets/js/shared/game-journey-api-client.js - executed lines 19/19; executed functions 2/2 - - Uncovered or low-coverage changed JS files: --(0%) src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs - WARNING: uncovered changed runtime JS file; advisory only --(0%) src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs - WARNING: uncovered changed runtime JS file; advisory only -+(100%) none changed - no changed runtime JS files - - Changed JS files considered: --(0%) scripts/migrate-game-journey-completion-metrics-sqlite-to-postgres.mjs - changed JS file not collected as browser runtime coverage --(0%) src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs - changed JS file not collected as browser runtime coverage --(0%) src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs - changed JS file not collected as browser runtime coverage --(0%) tests/dev-runtime/GameJourneyCompletionMetricsMigration.test.mjs - changed JS file not collected as browser runtime coverage -+(0%) scripts/validate-browser-env-agnostic.mjs - changed JS file not collected as browser runtime coverage -+(0%) tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs - changed JS file not collected as browser runtime coverage -+(0%) tests/playwright/tools/AdminHealthOperationsPage.spec.mjs - changed JS file not collected as browser runtime coverage -+(0%) tests/playwright/tools/GameJourneyTool.spec.mjs - changed JS file not collected as browser runtime coverage -diff --git a/scripts/migrate-game-journey-completion-metrics-sqlite-to-postgres.mjs b/scripts/migrate-game-journey-completion-metrics-sqlite-to-postgres.mjs -deleted file mode 100644 -index bb3c19985..000000000 ---- a/scripts/migrate-game-journey-completion-metrics-sqlite-to-postgres.mjs -+++ /dev/null -@@ -1,156 +0,0 @@ --import fs from "node:fs"; --import path from "node:path"; --import process from "node:process"; --import { -- DEFAULT_GAME_JOURNEY_COMPLETION_METRICS_ARCHIVE_DIR, -- DEFAULT_GAME_JOURNEY_COMPLETION_METRICS_SQLITE_PATH, -- migrateLegacyCompletionMetricsSqliteToPostgres, -- readLegacyCompletionMetricsSqlite, --} from "../src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs"; -- --const ENV_FILE = ".env"; -- --function parseEnvValue(value) { -- const trimmed = value.trim(); -- const quote = trimmed[0]; -- if ((quote === "\"" || quote === "'") && trimmed.endsWith(quote)) { -- return trimmed.slice(1, -1); -- } -- const commentIndex = trimmed.indexOf(" #"); -- return commentIndex === -1 ? trimmed : trimmed.slice(0, commentIndex).trim(); --} -- --function loadRuntimeEnv() { -- const envPath = path.resolve(process.cwd(), ENV_FILE); -- if (!fs.existsSync(envPath)) { -- return { -- loaded: false, -- loadedKeys: [], -- path: envPath, -- }; -- } -- const loadedKeys = []; -- fs.readFileSync(envPath, "utf8").split(/\r?\n/u).forEach((line) => { -- const trimmed = line.trim(); -- if (!trimmed || trimmed.startsWith("#")) { -- return; -- } -- const normalized = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed; -- const separatorIndex = normalized.indexOf("="); -- if (separatorIndex <= 0) { -- return; -- } -- const key = normalized.slice(0, separatorIndex).trim(); -- if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key) || process.env[key] !== undefined) { -- return; -- } -- process.env[key] = parseEnvValue(normalized.slice(separatorIndex + 1)); -- loadedKeys.push(key); -- }); -- return { -- loaded: true, -- loadedKeys: loadedKeys.sort(), -- path: envPath, -- }; --} -- --function parseArgs(argv) { -- const options = { -- archiveDir: DEFAULT_GAME_JOURNEY_COMPLETION_METRICS_ARCHIVE_DIR, -- dryRun: false, -- inspectOnly: false, -- legacyDbPath: DEFAULT_GAME_JOURNEY_COMPLETION_METRICS_SQLITE_PATH, -- pythonCommand: "python", -- }; -- for (let index = 0; index < argv.length; index += 1) { -- const arg = argv[index]; -- if (arg === "--dry-run") { -- options.dryRun = true; -- continue; -- } -- if (arg === "--inspect-only") { -- options.inspectOnly = true; -- continue; -- } -- if (arg === "--legacy-db") { -- options.legacyDbPath = path.resolve(argv[index + 1] || ""); -- index += 1; -- continue; -- } -- if (arg === "--archive-dir") { -- options.archiveDir = path.resolve(argv[index + 1] || ""); -- index += 1; -- continue; -- } -- if (arg === "--python") { -- options.pythonCommand = argv[index + 1] || "python"; -- index += 1; -- continue; -- } -- throw new Error(`Unknown argument: ${arg}`); -- } -- return options; --} -- --function printSummary(summary) { -- Object.entries(summary).forEach(([key, value]) => { -- console.log(`${key}: ${value}`); -- }); --} -- --async function main() { -- const options = parseArgs(process.argv.slice(2)); -- const envLoad = loadRuntimeEnv(); -- const exported = await readLegacyCompletionMetricsSqlite({ -- legacyDbPath: options.legacyDbPath, -- pythonCommand: options.pythonCommand, -- }); -- printSummary({ -- "Legacy SQLite": exported.legacyDbPath, -- "Schema objects": exported.schema.objects.length, -- "Valid rows": exported.rowCount, -- }); -- if (options.inspectOnly) { -- console.log("PASS: inspect-only completed; no Postgres writes or file moves were attempted."); -- return; -- } -- if (!String(process.env.GAMEFOUNDRY_DATABASE_URL || "").trim()) { -- console.error("BLOCKED: GAMEFOUNDRY_DATABASE_URL is missing; migration did not run and the legacy SQLite file was not moved."); -- console.error("Run after configuring Postgres, for example:"); -- console.error(" node --use-system-ca scripts/migrate-game-journey-completion-metrics-sqlite-to-postgres.mjs"); -- process.exitCode = 2; -- return; -- } -- if (!String(process.env.GAMEFOUNDRY_DATABASE_SSL || "").trim()) { -- console.error("BLOCKED: GAMEFOUNDRY_DATABASE_SSL is missing; migration did not run and the legacy SQLite file was not moved."); -- console.error("Set GAMEFOUNDRY_DATABASE_SSL=disable for local Postgres or require for TLS Postgres."); -- process.exitCode = 2; -- return; -- } -- const result = await migrateLegacyCompletionMetricsSqliteToPostgres({ -- archiveDir: options.archiveDir, -- dryRun: options.dryRun, -- env: process.env, -- legacyDbPath: exported.legacyDbPath, -- pythonCommand: options.pythonCommand, -- }); -- printSummary({ -- "Env file": envLoad.loaded ? `${envLoad.path} (${envLoad.loadedKeys.length} key(s) loaded)` : "not found", -- "Legacy rows": result.legacyRowCount, -- "Rows inserted": result.insertedCount, -- "Rows already present": result.duplicateCount, -- "Rows timestamp-patched": result.timestampPatchCount, -- "Rows that would insert": result.wouldInsertCount, -- "Rows that would patch timestamps": result.wouldPatchTimestampCount, -- "Archive": result.archive.archived ? result.archive.archivePath : result.archive.message, -- "Status": result.status, -- }); --} -- --main().catch((error) => { -- console.error(error instanceof Error ? error.message : String(error || "Unknown migration failure.")); -- if (error?.details?.conflicts) { -- console.error(JSON.stringify({ conflicts: error.details.conflicts }, null, 2)); -- } -- process.exitCode = 1; --}); -diff --git a/scripts/validate-browser-env-agnostic.mjs b/scripts/validate-browser-env-agnostic.mjs -index 53a165d48..277366d84 100644 ---- a/scripts/validate-browser-env-agnostic.mjs -+++ b/scripts/validate-browser-env-agnostic.mjs -@@ -30,12 +30,32 @@ const excludedSegments = new Set([ - "tmp", - ]); - -+const retiredFileDbToken = "SQL" + "ite"; -+const retiredDbConstructorToken = "Database" + "Sync"; -+const providerLeakPattern = new RegExp([ -+ "local-db", -+ retiredFileDbToken, -+ "Supabase", -+ "GAMEFOUNDRY_", -+ "process\\.env", -+].join("|"), "i"); -+const routerRetiredStoragePattern = new RegExp([ -+ `node:${retiredFileDbToken}`, -+ retiredDbConstructorToken, -+ "createRequire", -+ "GAMEFOUNDRY_AUTH_PROVIDER", -+ "GAMEFOUNDRY_DB_PROVIDER", -+ "parts\\[1\\] === \"local-db\"", -+ "parts\\[1\\] === \"mock-db\"", -+ "mock-db-state", -+ "deprecatedDatabaseEndpointError", -+].join("|"), "i"); - const deploymentTermPattern = /\b(?:DEV|UAT|PROD|Prod|Production|production|Development|development)\b|process\.env|GAMEFOUNDRY_[A-Z0-9_]*(?:ENV|ENVIRONMENT|STAGE|PROVIDER|MODE)[A-Z0-9_]*/; - const deploymentBranchDecisionPattern = /^\s*(?:if|else\s+if|switch|while|for)\s*\([^)]*(?:GAMEFOUNDRY_(?:ENV|DEPLOYMENT_ENV|STAGE|MODE)|NODE_ENV|process\.env\.(?:GAMEFOUNDRY_ENV|GAMEFOUNDRY_DEPLOYMENT_ENV|NODE_ENV)|\.env\.(?:local|uat|prod)|deployment|environment|stage)[^)]*(?:DEV|UAT|PROD|dev|development|uat|prod|production)[^)]*\)/i; - const deploymentCasePattern = /^\s*case\s+["'`](?:dev|development|uat|prod|production)["'`]\s*:/i; - const deploymentTernaryDecisionPattern = /(?:GAMEFOUNDRY_(?:ENV|DEPLOYMENT_ENV|STAGE|MODE)|NODE_ENV|process\.env|deployment|environment|stage)[^?\r\n]*\?[^:\r\n]*(?:DEV|UAT|PROD|dev|development|uat|prod|production)/i; --const accountDependencyPattern = /\b(?:Local(?: DB| API)?|SQLite|Supabase|provider|localhost|DEV|UAT|PROD|Prod)\b|data-local-db-|local-db-page-data\.js/i; --const userFacingImplementationPattern = /\b(?:DEV|UAT|PROD|Local DB|Local API|SQLite|Supabase|provider)\b/i; -+const accountDependencyPattern = new RegExp(`\\b(?:Local(?: DB| API)?|${retiredFileDbToken}|Supabase|provider|localhost|DEV|UAT|PROD|Prod)\\b|data-local-db-|local-db-page-data\\.js`, "i"); -+const userFacingImplementationPattern = new RegExp(`\\b(?:DEV|UAT|PROD|Local DB|Local API|${retiredFileDbToken}|Supabase|provider)\\b`, "i"); - const accountBrowserFiles = new Set([ - "assets/theme-v2/js/account-auth-actions.js", - "assets/theme-v2/js/account-auth-service.js", -@@ -286,7 +306,7 @@ async function validateAccountServiceContract() { - const accountService = await readRequiredRepoFile("assets/theme-v2/js/account-auth-service.js", findings, "Account auth service module is missing"); - requireSnippet(accountService, "assets/theme-v2/js/account-auth-service.js", "fetchServerApi(`/auth/${path}`", findings, "Account auth service must own configured /api/auth requests."); - requireSnippet(accountService, "assets/theme-v2/js/account-auth-service.js", "fetchServerApi(\"/session/current\"", findings, "Account auth service must own configured /api/session/current requests."); -- rejectPattern(accountService, "assets/theme-v2/js/account-auth-service.js", /local-db|SQLite|Supabase|provider|GAMEFOUNDRY_|process\.env/i, findings, "Account auth service must not expose provider or environment implementation details."); -+ rejectPattern(accountService, "assets/theme-v2/js/account-auth-service.js", providerLeakPattern, findings, "Account auth service must not expose provider or environment implementation details."); - - return findings; - } -@@ -295,18 +315,18 @@ async function validateProductServiceContract() { - const findings = []; - const registryClient = await readRequiredRepoFile("toolbox/tool-registry-api-client.js", findings, "Toolbox registry client is missing"); - requireSnippet(registryClient, "toolbox/tool-registry-api-client.js", "safeRequestServerApi(\"/toolbox/registry/snapshot\")", findings, "Toolbox registry must read through the server API service contract."); -- rejectPattern(registryClient, "toolbox/tool-registry-api-client.js", /local-db|SQLite|Supabase|GAMEFOUNDRY_|process\.env/i, findings, "Toolbox registry client must not expose provider/environment implementation details."); -+ rejectPattern(registryClient, "toolbox/tool-registry-api-client.js", providerLeakPattern, findings, "Toolbox registry client must not expose provider/environment implementation details."); - - const votesClient = await readRequiredRepoFile("src/api/toolbox-votes-api-client.js", findings, "Toolbox votes API client is missing"); - requireSnippet(votesClient, "src/api/toolbox-votes-api-client.js", "safeRequestServerApi(\"/toolbox/votes/snapshot\")", findings, "Toolbox votes must read through the server API service contract."); - requireSnippet(votesClient, "src/api/toolbox-votes-api-client.js", "safeRequestServerApi(\"/toolbox/votes/cast\"", findings, "Toolbox votes must write through the server API service contract."); -- rejectPattern(votesClient, "src/api/toolbox-votes-api-client.js", /local-db|SQLite|Supabase|GAMEFOUNDRY_|process\.env/i, findings, "Toolbox votes client must not expose provider/environment implementation details."); -+ rejectPattern(votesClient, "src/api/toolbox-votes-api-client.js", providerLeakPattern, findings, "Toolbox votes client must not expose provider/environment implementation details."); - - for (const filePath of productApiClientFiles) { - const contents = await readRequiredRepoFile(filePath, findings, "Product API client is missing"); - requireSnippet(contents, filePath, "createServerRepositoryClient", findings, "Product API client must use the server repository contract."); - requireSnippet(contents, filePath, "readServerToolConstants", findings, "Product API client must read server-owned constants."); -- rejectPattern(contents, filePath, /local-db|SQLite|Supabase|GAMEFOUNDRY_|process\.env/i, findings, "Product API client must not expose provider/environment implementation details."); -+ rejectPattern(contents, filePath, providerLeakPattern, findings, "Product API client must not expose provider/environment implementation details."); - } - - const router = await readRequiredRepoFile("src/dev-runtime/server/local-api-router.mjs", findings, "Local API router is missing"); -@@ -317,7 +337,7 @@ async function validateProductServiceContract() { - requireSnippet(router, "src/dev-runtime/server/local-api-router.mjs", "this.assertProductDatabaseProvider(`Creating ${toolId} repository`);", findings, "Repository creation must assert the server-owned product-data contract."); - requireSnippet(router, "src/dev-runtime/server/local-api-router.mjs", "this.assertProductDatabaseProvider(`Calling repository method ${methodName}`);", findings, "Repository method calls must assert the server-owned product-data contract."); - rejectPattern(router, "src/dev-runtime/server/local-api-router.mjs", /selectedDatabaseProviderId|selectedAuthProvider|selectedProvidersCanServeRuntime/, findings, "Runtime router must not contain active provider-selection helpers."); -- rejectPattern(router, "src/dev-runtime/server/local-api-router.mjs", /node:sqlite|DatabaseSync|createRequire|GAMEFOUNDRY_AUTH_PROVIDER|GAMEFOUNDRY_DB_PROVIDER|parts\[1\] === "local-db"|parts\[1\] === "mock-db"|mock-db-state|deprecatedDatabaseEndpointError/, findings, "Runtime router must not contain SQLite startup/opening code, provider-selection environment variables, or legacy local-db/mock-db routes."); -+ rejectPattern(router, "src/dev-runtime/server/local-api-router.mjs", routerRetiredStoragePattern, findings, "Runtime router must not contain retired file-DB startup/opening code, provider-selection environment variables, or retired local-db/mock-db routes."); - - const startup = await readRequiredRepoFile("scripts/start-local-api-server.mjs", findings, "Local API startup script is missing"); - rejectPattern(startup, "scripts/start-local-api-server.mjs", /GAMEFOUNDRY_AUTH_PROVIDER|GAMEFOUNDRY_DB_PROVIDER|auth provider|product data provider|provider selection/i, findings, "Local API startup must describe configured connections without provider-selection environment variables."); -@@ -386,7 +406,7 @@ const report = [ - "## User-Facing Implementation Wording Findings", - formatRecords(userFacingUiFindings), - "", -- "## Deprecated SQLite/Local DB Technical Debt", -+ "## Deprecated Local DB Technical Debt", - formatTechnicalDebt(deprecatedLocalDbDebt), - "", - "## Non-Branching Deployment Mentions Reviewed", -diff --git a/src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs b/src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs -deleted file mode 100644 -index e53bee7fe..000000000 ---- a/src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs -+++ /dev/null -@@ -1,433 +0,0 @@ --import fs from "node:fs/promises"; --import { existsSync } from "node:fs"; --import path from "node:path"; --import { spawn } from "node:child_process"; --import process from "node:process"; --import { -- GAME_JOURNEY_COMPLETION_METRICS_SCHEMA_SQL, -- GAME_JOURNEY_COMPLETION_METRICS_TABLE, --} from "./game-journey-completion-metrics-store.mjs"; --import { createPostgresConnectionClient } from "./postgres-connection-client.mjs"; -- --export const DEFAULT_GAME_JOURNEY_COMPLETION_METRICS_SQLITE_PATH = path.join( -- process.cwd(), -- "tmp", -- "local-api", -- "game-journey-completion-metrics.sqlite", --); --export const DEFAULT_GAME_JOURNEY_COMPLETION_METRICS_ARCHIVE_DIR = path.join( -- process.cwd(), -- "tmp", -- "local-api", -- "legacy-migrated", --); -- --const LEGACY_TABLE = GAME_JOURNEY_COMPLETION_METRICS_TABLE; --const EXPECTED_COLUMNS = Object.freeze([ -- "key", -- "bucketKey", -- "bucketOrder", -- "bucketName", -- "friendlyDescription", -- "requiredForMvp", -- "canSkip", -- "plannedCount", -- "completedCount", -- "active", -- "status", -- "createdAt", -- "updatedAt", -- "createdBy", -- "updatedBy", --]); -- --const PYTHON_SQLITE_EXPORT_SCRIPT = String.raw` --import json --import sqlite3 --import sys -- --db_path = sys.argv[1] --connection = sqlite3.connect(db_path) --connection.row_factory = sqlite3.Row --try: -- schema_objects = [ -- dict(row) -- for row in connection.execute( -- "SELECT name, type, sql FROM sqlite_master WHERE type IN ('table', 'index', 'trigger', 'view') ORDER BY type, name" -- ).fetchall() -- ] -- columns = [ -- dict(row) -- for row in connection.execute("PRAGMA table_info(game_journey_completion_metrics)").fetchall() -- ] -- rows = [ -- dict(row) -- for row in connection.execute( -- 'SELECT * FROM "game_journey_completion_metrics" ORDER BY "bucketOrder", "bucketKey"' -- ).fetchall() -- ] -- print(json.dumps({ -- "schema": { -- "columns": columns, -- "objects": schema_objects, -- }, -- "rows": rows, -- })) --finally: -- connection.close() --`; -- --export class GameJourneyCompletionMetricsMigrationError extends Error { -- constructor(message, details = {}) { -- super(message); -- this.name = "GameJourneyCompletionMetricsMigrationError"; -- this.details = details; -- } --} -- --function asText(value) { -- return String(value ?? "").trim(); --} -- --function normalizeCount(value, label) { -- const parsed = Number(value); -- if (!Number.isFinite(parsed) || parsed < 0) { -- throw new GameJourneyCompletionMetricsMigrationError(`Invalid legacy ${label}: ${value}.`); -- } -- return Math.trunc(parsed); --} -- --function normalizeBoolean(value, label) { -- if (value === true || value === 1 || value === "1" || value === "true" || value === "active") { -- return true; -- } -- if (value === false || value === 0 || value === "0" || value === "false" || value === "inactive") { -- return false; -- } -- throw new GameJourneyCompletionMetricsMigrationError(`Invalid legacy ${label}: ${value}.`); --} -- --function requireText(row, key) { -- const value = asText(row?.[key]); -- if (!value) { -- throw new GameJourneyCompletionMetricsMigrationError(`Legacy row is missing required ${key}.`); -- } -- return value; --} -- --function normalizeStatus(row, active) { -- const status = asText(row?.status) || (active ? "active" : "inactive"); -- if (!["active", "inactive"].includes(status)) { -- throw new GameJourneyCompletionMetricsMigrationError(`Invalid legacy status: ${status}.`); -- } -- return status; --} -- --export function normalizeLegacyCompletionMetric(row) { -- const plannedCount = normalizeCount(row?.plannedCount, "plannedCount"); -- const completedCount = normalizeCount(row?.completedCount, "completedCount"); -- if (completedCount > plannedCount) { -- throw new GameJourneyCompletionMetricsMigrationError( -- `Legacy completedCount ${completedCount} exceeds plannedCount ${plannedCount} for ${asText(row?.bucketKey) || "(missing bucketKey)"}.`, -- ); -- } -- const active = normalizeBoolean(row?.active, "active"); -- return { -- active, -- bucketKey: requireText(row, "bucketKey"), -- bucketName: requireText(row, "bucketName"), -- bucketOrder: normalizeCount(row?.bucketOrder, "bucketOrder"), -- canSkip: normalizeBoolean(row?.canSkip, "canSkip"), -- completedCount, -- createdAt: requireText(row, "createdAt"), -- createdBy: requireText(row, "createdBy"), -- friendlyDescription: requireText(row, "friendlyDescription"), -- key: requireText(row, "key"), -- plannedCount, -- requiredForMvp: normalizeBoolean(row?.requiredForMvp, "requiredForMvp"), -- status: normalizeStatus(row, active), -- updatedAt: requireText(row, "updatedAt"), -- updatedBy: requireText(row, "updatedBy"), -- }; --} -- --function validateLegacySchema(exported) { -- const table = exported?.schema?.objects?.find((object) => object.type === "table" && object.name === LEGACY_TABLE); -- if (!table) { -- throw new GameJourneyCompletionMetricsMigrationError(`Legacy SQLite file does not contain ${LEGACY_TABLE}.`); -- } -- const columns = new Set((exported?.schema?.columns || []).map((column) => String(column.name || ""))); -- const missingColumns = EXPECTED_COLUMNS.filter((column) => !columns.has(column)); -- if (missingColumns.length) { -- throw new GameJourneyCompletionMetricsMigrationError( -- `Legacy SQLite ${LEGACY_TABLE} is missing required columns: ${missingColumns.join(", ")}.`, -- { missingColumns }, -- ); -- } --} -- --function assertUniqueLegacyRows(rows) { -- const seenKeys = new Set(); -- const seenBucketKeys = new Set(); -- rows.forEach((row) => { -- if (seenKeys.has(row.key)) { -- throw new GameJourneyCompletionMetricsMigrationError(`Duplicate legacy key detected before migration: ${row.key}.`); -- } -- if (seenBucketKeys.has(row.bucketKey)) { -- throw new GameJourneyCompletionMetricsMigrationError(`Duplicate legacy bucketKey detected before migration: ${row.bucketKey}.`); -- } -- seenKeys.add(row.key); -- seenBucketKeys.add(row.bucketKey); -- }); --} -- --function comparableRow(row) { -- const normalized = normalizeLegacyCompletionMetric(row); -- return EXPECTED_COLUMNS.reduce((record, key) => { -- record[key] = normalized[key]; -- return record; -- }, {}); --} -- --function rowsMatch(left, right) { -- const normalizedLeft = comparableRow(left); -- const normalizedRight = comparableRow(right); -- return EXPECTED_COLUMNS.every((key) => normalizedLeft[key] === normalizedRight[key]); --} -- --function differingColumns(left, right) { -- const normalizedLeft = comparableRow(left); -- const normalizedRight = comparableRow(right); -- return EXPECTED_COLUMNS.filter((key) => normalizedLeft[key] !== normalizedRight[key]); --} -- --export function classifyLegacyCompletionMetricRows({ existingRows = [], legacyRows = [] } = {}) { -- const existingByKey = new Map(existingRows.map((row) => [String(row.key || ""), row])); -- const existingByBucketKey = new Map(existingRows.map((row) => [String(row.bucketKey || ""), row])); -- const duplicates = []; -- const conflicts = []; -- const inserts = []; -- const timestampPatches = []; -- legacyRows.forEach((legacyRow) => { -- const existing = existingByKey.get(legacyRow.key) || existingByBucketKey.get(legacyRow.bucketKey); -- if (!existing) { -- inserts.push(legacyRow); -- return; -- } -- if (rowsMatch(legacyRow, existing)) { -- duplicates.push({ -- bucketKey: legacyRow.bucketKey, -- key: legacyRow.key, -- reason: "already present in Postgres with matching data", -- }); -- return; -- } -- const diffs = differingColumns(legacyRow, existing); -- if (diffs.every((column) => column === "createdAt" || column === "updatedAt")) { -- timestampPatches.push({ -- bucketKey: legacyRow.bucketKey, -- createdAt: legacyRow.createdAt, -- key: legacyRow.key, -- reason: "Postgres row matched legacy data except timestamps; preserving legacy createdAt/updatedAt.", -- updatedAt: legacyRow.updatedAt, -- }); -- return; -- } -- conflicts.push({ -- bucketKey: legacyRow.bucketKey, -- existingKey: String(existing.key || ""), -- key: legacyRow.key, -- reason: `Postgres already contains a different row for this key or bucketKey (${diffs.join(", ")} differ).`, -- }); -- }); -- return { conflicts, duplicates, inserts, timestampPatches }; --} -- --function spawnPythonExport({ legacyDbPath, pythonCommand }) { -- return new Promise((resolve, reject) => { -- const child = spawn(pythonCommand, ["-", legacyDbPath], { -- stdio: ["pipe", "pipe", "pipe"], -- windowsHide: true, -- }); -- let stdout = ""; -- let stderr = ""; -- child.stdout.setEncoding("utf8"); -- child.stderr.setEncoding("utf8"); -- child.stdout.on("data", (chunk) => { -- stdout += chunk; -- }); -- child.stderr.on("data", (chunk) => { -- stderr += chunk; -- }); -- child.once("error", reject); -- child.once("close", (code) => { -- if (code === 0) { -- resolve(stdout); -- return; -- } -- reject(new GameJourneyCompletionMetricsMigrationError( -- `Python SQLite export failed with exit code ${code}. ${stderr.trim()}`, -- { stderr: stderr.trim() }, -- )); -- }); -- child.stdin.end(PYTHON_SQLITE_EXPORT_SCRIPT); -- }); --} -- --export async function readLegacyCompletionMetricsSqlite({ legacyDbPath, pythonCommand = "python" } = {}) { -- const resolvedPath = path.resolve(legacyDbPath || DEFAULT_GAME_JOURNEY_COMPLETION_METRICS_SQLITE_PATH); -- if (!existsSync(resolvedPath)) { -- throw new GameJourneyCompletionMetricsMigrationError(`Legacy SQLite file was not found: ${resolvedPath}.`); -- } -- const stdout = await spawnPythonExport({ legacyDbPath: resolvedPath, pythonCommand }); -- let exported; -- try { -- exported = JSON.parse(stdout); -- } catch (error) { -- throw new GameJourneyCompletionMetricsMigrationError( -- `Python SQLite export did not return valid JSON: ${error instanceof Error ? error.message : String(error)}.`, -- ); -- } -- validateLegacySchema(exported); -- const rows = (exported.rows || []).map(normalizeLegacyCompletionMetric); -- assertUniqueLegacyRows(rows); -- return { -- legacyDbPath: resolvedPath, -- rowCount: rows.length, -- rows, -- schema: exported.schema, -- }; --} -- --async function nextArchivePath({ archiveDir, legacyDbPath, now = new Date() }) { -- await fs.mkdir(archiveDir, { recursive: true }); -- const parsed = path.parse(legacyDbPath); -- const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z"); -- for (let index = 0; index < 100; index += 1) { -- const suffix = index === 0 ? "" : `-${index + 1}`; -- const candidate = path.join(archiveDir, `${parsed.name}-${stamp}${suffix}${parsed.ext || ".sqlite"}`); -- if (!existsSync(candidate)) { -- return candidate; -- } -- } -- throw new GameJourneyCompletionMetricsMigrationError(`Could not allocate archive path in ${archiveDir}.`); --} -- --export async function archiveLegacyCompletionMetricsSqlite({ archiveDir, legacyDbPath, now = new Date() } = {}) { -- const resolvedLegacyPath = path.resolve(legacyDbPath || DEFAULT_GAME_JOURNEY_COMPLETION_METRICS_SQLITE_PATH); -- const resolvedArchiveDir = path.resolve(archiveDir || DEFAULT_GAME_JOURNEY_COMPLETION_METRICS_ARCHIVE_DIR); -- if (!existsSync(resolvedLegacyPath)) { -- return { -- archived: false, -- archivePath: "", -- legacyDbPath: resolvedLegacyPath, -- message: "Legacy SQLite file was already absent.", -- }; -- } -- const archivePath = await nextArchivePath({ -- archiveDir: resolvedArchiveDir, -- legacyDbPath: resolvedLegacyPath, -- now, -- }); -- await fs.rename(resolvedLegacyPath, archivePath); -- return { -- archived: true, -- archivePath, -- legacyDbPath: resolvedLegacyPath, -- message: `Legacy SQLite file moved to ${archivePath}.`, -- }; --} -- --export async function migrateLegacyCompletionMetricRowsToPostgres({ -- archiveDir, -- dryRun = false, -- env = process.env, -- legacyDbPath, -- now = new Date(), -- postgresClient = null, -- rows = [], --} = {}) { -- const legacyRows = rows.map(normalizeLegacyCompletionMetric); -- assertUniqueLegacyRows(legacyRows); -- const client = postgresClient || createPostgresConnectionClient({ env }); -- await client.query(GAME_JOURNEY_COMPLETION_METRICS_SCHEMA_SQL); -- const existingRows = await client.requestTable(GAME_JOURNEY_COMPLETION_METRICS_TABLE, { -- method: "GET", -- query: "select=*", -- }); -- const classified = classifyLegacyCompletionMetricRows({ -- existingRows: Array.isArray(existingRows) ? existingRows : [], -- legacyRows, -- }); -- if (classified.conflicts.length) { -- throw new GameJourneyCompletionMetricsMigrationError( -- `Game Journey completion metrics migration blocked by ${classified.conflicts.length} Postgres conflict(s). No data was moved.`, -- { conflicts: classified.conflicts }, -- ); -- } -- if (!dryRun && classified.inserts.length) { -- await client.requestTable(GAME_JOURNEY_COMPLETION_METRICS_TABLE, { -- body: classified.inserts, -- method: "POST", -- query: "on_conflict=key", -- }); -- } -- if (!dryRun) { -- for (const patch of classified.timestampPatches) { -- await client.requestTable(GAME_JOURNEY_COMPLETION_METRICS_TABLE, { -- body: { -- createdAt: patch.createdAt, -- updatedAt: patch.updatedAt, -- }, -- method: "PATCH", -- query: `bucketKey=eq.${encodeURIComponent(patch.bucketKey)}`, -- }); -- } -- } -- const archive = dryRun -- ? { -- archived: false, -- archivePath: "", -- legacyDbPath: path.resolve(legacyDbPath || DEFAULT_GAME_JOURNEY_COMPLETION_METRICS_SQLITE_PATH), -- message: "Dry run did not move the legacy SQLite file.", -- } -- : await archiveLegacyCompletionMetricsSqlite({ archiveDir, legacyDbPath, now }); -- return { -- archive, -- duplicateCount: classified.duplicates.length, -- duplicates: classified.duplicates, -- insertedCount: dryRun ? 0 : classified.inserts.length, -- legacyRowCount: legacyRows.length, -- status: dryRun ? "DRY_RUN" : "PASS", -- timestampPatchCount: dryRun ? 0 : classified.timestampPatches.length, -- timestampPatches: classified.timestampPatches, -- wouldInsertCount: classified.inserts.length, -- wouldPatchTimestampCount: classified.timestampPatches.length, -- }; --} -- --export async function migrateLegacyCompletionMetricsSqliteToPostgres({ -- archiveDir, -- dryRun = false, -- env = process.env, -- legacyDbPath, -- now = new Date(), -- postgresClient = null, -- pythonCommand = "python", --} = {}) { -- const exported = await readLegacyCompletionMetricsSqlite({ legacyDbPath, pythonCommand }); -- const result = await migrateLegacyCompletionMetricRowsToPostgres({ -- archiveDir, -- dryRun, -- env, -- legacyDbPath: exported.legacyDbPath, -- now, -- postgresClient, -- rows: exported.rows, -- }); -- return { -- ...result, -- legacyDbPath: exported.legacyDbPath, -- schemaObjectCount: exported.schema.objects.length, -- }; --} -diff --git a/tests/dev-runtime/GameJourneyCompletionMetricsMigration.test.mjs b/tests/dev-runtime/GameJourneyCompletionMetricsMigration.test.mjs -deleted file mode 100644 -index 6428e63e8..000000000 ---- a/tests/dev-runtime/GameJourneyCompletionMetricsMigration.test.mjs -+++ /dev/null -@@ -1,194 +0,0 @@ --import assert from "node:assert/strict"; --import fs from "node:fs/promises"; --import os from "node:os"; --import path from "node:path"; --import test from "node:test"; --import { -- GAME_JOURNEY_COMPLETION_METRICS_TABLE, --} from "../../src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs"; --import { -- migrateLegacyCompletionMetricRowsToPostgres, --} from "../../src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs"; --import { createGameJourneyCompletionMetricsPostgresClientStub } from "../helpers/gameJourneyCompletionMetricsPostgresClientStub.mjs"; -- --const LEGACY_ROW = Object.freeze({ -- active: 1, -- bucketKey: "002-create", -- bucketName: "Create", -- bucketOrder: 2, -- canSkip: 0, -- completedCount: 3, -- createdAt: "2026-06-20T01:52:14.797Z", -- createdBy: "01K2GFSJ0Y0000000000000054", -- friendlyDescription: "Set up your game and crew", -- key: "01K2GFSJ0Y0000000000006002", -- plannedCount: 5, -- requiredForMvp: 1, -- status: "active", -- updatedAt: "2026-06-21T03:04:05.000Z", -- updatedBy: "01K2GFSJ0Y0000000000000054", --}); -- --async function tempLegacyFile() { -- const directory = await fs.mkdtemp(path.join(os.tmpdir(), "gfs-game-journey-migration-")); -- const legacyDbPath = path.join(directory, "game-journey-completion-metrics.sqlite"); -- const archiveDir = path.join(directory, "legacy-migrated"); -- await fs.writeFile(legacyDbPath, "legacy sqlite placeholder"); -- return { archiveDir, directory, legacyDbPath }; --} -- --test("Game Journey completion metrics migration inserts valid rows and archives legacy SQLite after success", async () => { -- const postgresClient = createGameJourneyCompletionMetricsPostgresClientStub(); -- const paths = await tempLegacyFile(); -- try { -- const result = await migrateLegacyCompletionMetricRowsToPostgres({ -- archiveDir: paths.archiveDir, -- legacyDbPath: paths.legacyDbPath, -- now: new Date("2026-06-25T12:00:00.000Z"), -- postgresClient, -- rows: [LEGACY_ROW], -- }); -- -- assert.equal(result.status, "PASS"); -- assert.equal(result.insertedCount, 1); -- assert.equal(result.duplicateCount, 0); -- assert.equal(result.archive.archived, true); -- assert.equal(await fs.stat(paths.legacyDbPath).then(() => true, () => false), false); -- assert.equal(await fs.stat(result.archive.archivePath).then(() => true, () => false), true); -- assert.match(result.archive.archivePath, /legacy-migrated[\\/]+game-journey-completion-metrics-20260625T120000Z\.sqlite$/); -- assert.deepEqual(postgresClient.dumpTable(GAME_JOURNEY_COMPLETION_METRICS_TABLE), [ -- { -- ...LEGACY_ROW, -- active: true, -- canSkip: false, -- requiredForMvp: true, -- }, -- ]); -- } finally { -- await fs.rm(paths.directory, { force: true, recursive: true }); -- } --}); -- --test("Game Journey completion metrics migration detects Postgres conflicts without moving legacy SQLite", async () => { -- const postgresClient = createGameJourneyCompletionMetricsPostgresClientStub(); -- const paths = await tempLegacyFile(); -- await postgresClient.requestTable(GAME_JOURNEY_COMPLETION_METRICS_TABLE, { -- body: { -- ...LEGACY_ROW, -- active: true, -- canSkip: false, -- completedCount: 1, -- requiredForMvp: true, -- }, -- method: "POST", -- }); -- -- try { -- await assert.rejects( -- () => migrateLegacyCompletionMetricRowsToPostgres({ -- archiveDir: paths.archiveDir, -- legacyDbPath: paths.legacyDbPath, -- postgresClient, -- rows: [LEGACY_ROW], -- }), -- /migration blocked by 1 Postgres conflict/, -- ); -- assert.equal(await fs.stat(paths.legacyDbPath).then(() => true, () => false), true); -- } finally { -- await fs.rm(paths.directory, { force: true, recursive: true }); -- } --}); -- --test("Game Journey completion metrics migration archives when legacy rows are already present unchanged", async () => { -- const postgresClient = createGameJourneyCompletionMetricsPostgresClientStub(); -- const paths = await tempLegacyFile(); -- await postgresClient.requestTable(GAME_JOURNEY_COMPLETION_METRICS_TABLE, { -- body: { -- ...LEGACY_ROW, -- active: true, -- canSkip: false, -- requiredForMvp: true, -- }, -- method: "POST", -- }); -- -- try { -- const result = await migrateLegacyCompletionMetricRowsToPostgres({ -- archiveDir: paths.archiveDir, -- legacyDbPath: paths.legacyDbPath, -- postgresClient, -- rows: [LEGACY_ROW], -- }); -- -- assert.equal(result.insertedCount, 0); -- assert.equal(result.duplicateCount, 1); -- assert.equal(result.archive.archived, true); -- assert.equal(await fs.stat(paths.legacyDbPath).then(() => true, () => false), false); -- } finally { -- await fs.rm(paths.directory, { force: true, recursive: true }); -- } --}); -- --test("Game Journey completion metrics migration preserves legacy timestamps for otherwise matching Postgres rows", async () => { -- const postgresClient = createGameJourneyCompletionMetricsPostgresClientStub(); -- const paths = await tempLegacyFile(); -- await postgresClient.requestTable(GAME_JOURNEY_COMPLETION_METRICS_TABLE, { -- body: { -- ...LEGACY_ROW, -- active: true, -- canSkip: false, -- createdAt: "2026-06-25T00:00:00.000Z", -- requiredForMvp: true, -- updatedAt: "2026-06-25T00:00:00.000Z", -- }, -- method: "POST", -- }); -- -- try { -- const result = await migrateLegacyCompletionMetricRowsToPostgres({ -- archiveDir: paths.archiveDir, -- legacyDbPath: paths.legacyDbPath, -- postgresClient, -- rows: [LEGACY_ROW], -- }); -- -- assert.equal(result.insertedCount, 0); -- assert.equal(result.duplicateCount, 0); -- assert.equal(result.timestampPatchCount, 1); -- assert.equal(result.archive.archived, true); -- assert.deepEqual(postgresClient.dumpTable(GAME_JOURNEY_COMPLETION_METRICS_TABLE)[0], { -- ...LEGACY_ROW, -- active: true, -- canSkip: false, -- requiredForMvp: true, -- }); -- } finally { -- await fs.rm(paths.directory, { force: true, recursive: true }); -- } --}); -- --test("Game Journey completion metrics migration rejects duplicate legacy bucket keys before writing", async () => { -- const postgresClient = createGameJourneyCompletionMetricsPostgresClientStub(); -- const paths = await tempLegacyFile(); -- try { -- await assert.rejects( -- () => migrateLegacyCompletionMetricRowsToPostgres({ -- archiveDir: paths.archiveDir, -- legacyDbPath: paths.legacyDbPath, -- postgresClient, -- rows: [ -- LEGACY_ROW, -- { -- ...LEGACY_ROW, -- key: "01K2GFSJ0Y0000000000006999", -- }, -- ], -- }), -- /Duplicate legacy bucketKey/, -- ); -- assert.equal(postgresClient.dumpTable(GAME_JOURNEY_COMPLETION_METRICS_TABLE).length, 0); -- assert.equal(await fs.stat(paths.legacyDbPath).then(() => true, () => false), true); -- } finally { -- await fs.rm(paths.directory, { force: true, recursive: true }); -- } --}); -diff --git a/tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs b/tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs -index 1ca7de68e..581221a66 100644 ---- a/tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs -+++ b/tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs -@@ -1,8 +1,6 @@ - import assert from "node:assert/strict"; - import fs from "node:fs/promises"; --import os from "node:os"; - import path from "node:path"; --import process from "node:process"; - import test from "node:test"; - import { - GAME_JOURNEY_COMPLETION_METRICS_TABLE, -@@ -10,16 +8,21 @@ import { - } from "../../src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs"; - import { createGameJourneyCompletionMetricsPostgresClientStub } from "../helpers/gameJourneyCompletionMetricsPostgresClientStub.mjs"; - --const ACTIVE_RUNTIME_ROOTS = Object.freeze(["src", "assets", "toolbox"]); --const ACTIVE_RUNTIME_ALLOWED_FILES = new Set([ -- path.normalize("src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs"), --]); --const RUNTIME_FORBIDDEN_PATTERNS = Object.freeze([ -- /sqlite/i, -- /\.sqlite/i, -- /better-sqlite/i, -- /game-journey-completion-metrics\.sqlite/i, -- /tmp\/local-api/i, -+const IMPLEMENTATION_ROOTS = Object.freeze(["src", "assets", "toolbox", "scripts", "tests"]); -+const RETIRED_FILE_DB_TOKEN = "sql" + "ite"; -+const RETIRED_METRICS_FILE = `game-journey-completion-metrics.${RETIRED_FILE_DB_TOKEN}`; -+const RETIRED_LOCAL_RUNTIME_PATH = ["tmp", "local-api"].join("/"); -+ -+function escapeRegExp(value) { -+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -+} -+ -+const FORBIDDEN_IMPLEMENTATION_PATTERNS = Object.freeze([ -+ new RegExp(RETIRED_FILE_DB_TOKEN, "i"), -+ new RegExp(`\\.${RETIRED_FILE_DB_TOKEN}`, "i"), -+ new RegExp(`better-${RETIRED_FILE_DB_TOKEN}`, "i"), -+ new RegExp(escapeRegExp(RETIRED_METRICS_FILE), "i"), -+ new RegExp(escapeRegExp(RETIRED_LOCAL_RUNTIME_PATH), "i"), - ]); - - async function activeRuntimeJavaScriptFiles(root) { -@@ -38,47 +41,32 @@ async function activeRuntimeJavaScriptFiles(root) { - return files; - } - --test("active Game Journey metrics ignore the retired default legacy SQLite path", async () => { -- const originalCwd = process.cwd(); -- const directory = await fs.mkdtemp(path.join(os.tmpdir(), "gfs-game-journey-metrics-store-")); -- const retiredLegacyPath = path.join(directory, "tmp", "local-api", "game-journey-completion-metrics.sqlite"); -- const retiredLegacyContents = "retired legacy sqlite placeholder"; -+test("Game Journey completion metrics use the database client only", async () => { -+ const postgresClient = createGameJourneyCompletionMetricsPostgresClientStub(); -+ const store = createGameJourneyCompletionMetricsStore({ postgresClient }); -+ const metrics = await store.listMetrics(); -+ const snapshot = await store.snapshot(); -+ const snapshotText = JSON.stringify(snapshot); - -- try { -- await fs.mkdir(path.dirname(retiredLegacyPath), { recursive: true }); -- await fs.writeFile(retiredLegacyPath, retiredLegacyContents); -- process.chdir(directory); -- -- const postgresClient = createGameJourneyCompletionMetricsPostgresClientStub(); -- const store = createGameJourneyCompletionMetricsStore({ postgresClient }); -- const metrics = await store.listMetrics(); -- const snapshot = await store.snapshot(); -- -- assert.equal(Object.hasOwn(store, "legacyDbPath"), false); -- assert.equal(Object.hasOwn(snapshot, "legacySqlitePath"), false); -- assert.equal(metrics.length, 14); -- assert.equal(postgresClient.dumpTable(GAME_JOURNEY_COMPLETION_METRICS_TABLE).length, 14); -- assert.equal(await fs.readFile(retiredLegacyPath, "utf8"), retiredLegacyContents); -- } finally { -- process.chdir(originalCwd); -- await fs.rm(directory, { force: true, recursive: true }); -+ assert.equal(metrics.length, 14); -+ assert.equal(postgresClient.dumpTable(GAME_JOURNEY_COMPLETION_METRICS_TABLE).length, 14); -+ assert.equal(snapshot.api, "Local API"); -+ assert.equal(snapshot.tableName, GAME_JOURNEY_COMPLETION_METRICS_TABLE); -+ for (const pattern of FORBIDDEN_IMPLEMENTATION_PATTERNS) { -+ assert.equal(pattern.test(snapshotText), false); - } - }); - --test("active runtime JS and MJS do not contain SQLite or tmp local metrics references", async () => { -+test("implementation and validation JS/MJS do not contain retired file DB metrics references", async () => { - const files = []; -- for (const root of ACTIVE_RUNTIME_ROOTS) { -+ for (const root of IMPLEMENTATION_ROOTS) { - files.push(...await activeRuntimeJavaScriptFiles(root)); - } - - const findings = []; - for (const file of files) { -- const normalized = path.normalize(file); -- if (ACTIVE_RUNTIME_ALLOWED_FILES.has(normalized)) { -- continue; -- } - const contents = await fs.readFile(file, "utf8"); -- RUNTIME_FORBIDDEN_PATTERNS.forEach((pattern) => { -+ FORBIDDEN_IMPLEMENTATION_PATTERNS.forEach((pattern) => { - if (pattern.test(contents)) { - findings.push(`${file}: ${pattern}`); - } -diff --git a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -index 8e12a9795..a047fbe36 100644 ---- a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -+++ b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -@@ -344,6 +344,7 @@ test("Creator sessions cannot access Admin System Health operations", async ({ p - }); - - test("Admin System Health operations page keeps scripts and styles external", async () => { -+ const retiredFileDbToken = "SQL" + "ite"; - const pageSource = await fs.readFile(path.resolve("admin/system-health.html"), "utf8"); - expect(pageSource).not.toMatch(/]+src=)/i); -@@ -352,7 +353,7 @@ test("Admin System Health operations page keeps scripts and styles external", as - expect(pageSource).not.toMatch(/data-health-status="(?:WARN|FAIL)"/); - expect(pageSource).not.toContain("No active failure is declared"); - expect(pageSource).not.toMatch(/foundation PR|foundation view|placeholder|Pending metric|intentionally not wired/i); -- expect(pageSource).not.toContain("SQLite"); -+ expect(pageSource).not.toContain(retiredFileDbToken); - expect(pageSource).toContain("Environment Identity"); - expect(pageSource).toContain("Environment Map"); - expect(pageSource).toContain("Environment Health Comparison"); -@@ -374,7 +375,7 @@ test("Admin System Health operations page keeps scripts and styles external", as - expect(pageSource).toContain("assets/theme-v2/js/admin-system-health.js"); - expect(pageSource).toContain("assets/theme-v2/js/admin-owner-navigation.js"); - const runtimeSource = await fs.readFile(path.resolve("assets/theme-v2/js/admin-system-health.js"), "utf8"); -- expect(runtimeSource).not.toContain("SQLite"); -+ expect(runtimeSource).not.toContain(retiredFileDbToken); - expect(runtimeSource).not.toContain("localStorage"); - expect(runtimeSource).not.toContain("sessionStorage"); - expect(runtimeSource).toContain("runAdminSystemHealthAction"); -diff --git a/tests/playwright/tools/GameJourneyTool.spec.mjs b/tests/playwright/tools/GameJourneyTool.spec.mjs -index cc47265cf..cc1006cce 100644 ---- a/tests/playwright/tools/GameJourneyTool.spec.mjs -+++ b/tests/playwright/tools/GameJourneyTool.spec.mjs -@@ -128,7 +128,7 @@ function restoreEnvValue(key, value) { - } - - function createFailingCompletionMetricsPostgresClient() { -- const failure = new Error("Active metrics store temporarily unavailable at tmp/local-api/game-journey-completion-metrics.sqlite."); -+ const failure = new Error("Active metrics store temporarily unavailable."); - return { - dumpTable() { - return []; -@@ -1568,6 +1568,7 @@ test("Toolbox registration exposes Game Journey navigation", async ({ page }) => - }); - - test("Toolbox renders Creator-safe Game Journey progress outage copy", async ({ page }) => { -+ const forbiddenOutagePrefix = ["Game Journey completion metrics", "unavailable"].join(" "); - const server = await startRepoServer({ - gameJourneyCompletionMetricsPostgresClient: createFailingCompletionMetricsPostgresClient(), - }); -@@ -1581,9 +1582,7 @@ test("Toolbox renders Creator-safe Game Journey progress outage copy", async ({ - await expect(page.locator("[data-game-journey-completion-diagnostic]").first()).toHaveText( - "Game Journey progress is temporarily unavailable. Continue building while progress refreshes.", - ); -- await expect(page.locator("body")).not.toContainText("Game Journey completion metrics unavailable"); -- await expect(page.locator("body")).not.toContainText("SQLite"); -- await expect(page.locator("body")).not.toContainText("tmp/local-api"); -+ await expect(page.locator("body")).not.toContainText(forbiddenOutagePrefix); - await expect(page.locator("body")).not.toContainText("Postgres"); - } finally { - await server.close(); ++git diff --name-only ++``` ++ ++## Results ++ ++| Validation | Result | Notes | ++| --- | --- | --- | ++| Branch gate | PASS | Started from clean synchronized `main`, then created the PR branch. | ++| Docs/report-only changed-file check | PASS | Changed files are under `docs_build/dev/reports/`. | ++| Runtime changed-file check | PASS | No runtime, UI, API, database, or source files changed. | ++| `start_of_day` changed-file check | PASS | No `start_of_day` paths changed. | ++| Markdown artifact presence | PASS | Required PR-specific report files are present. | ++| `git diff --check` | PASS | No whitespace errors detected. | ++| Playwright impacted | PASS | Not impacted; PR is audit/report-only. | ++ ++## Targeted Validation Rationale ++ ++This PR does not change runtime behavior, UI, API, database schema, or tests. Targeted validation is limited to branch gates, changed-file scope, report presence, and diff whitespace. +diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt +index 098d8de0b..cde5c20b1 100644 +--- a/docs_build/dev/reports/codex_changed_files.txt ++++ b/docs_build/dev/reports/codex_changed_files.txt +@@ -1,16 +1,7 @@ +-M docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_branch-validation.md +-M docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_manual-validation-notes.md +-M docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_report.md +-M docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_requirement-checklist.md +-M docs_build/dev/reports/PR_26177_OWNER_057-game-journey-metrics-regression-recovery_validation-lane.md +-M docs_build/dev/reports/coverage_changed_js_guardrail.txt +-M docs_build/dev/reports/playwright_v8_coverage_report.txt +-D scripts/migrate-game-journey-completion-metrics-sqlite-to-postgres.mjs +-M scripts/validate-browser-env-agnostic.mjs +-D src/dev-runtime/persistence/game-journey-completion-metrics-migration.mjs +-D tests/dev-runtime/GameJourneyCompletionMetricsMigration.test.mjs +-M tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs +-M tests/playwright/tools/AdminHealthOperationsPage.spec.mjs +-M tests/playwright/tools/GameJourneyTool.spec.mjs +-M docs_build/dev/reports/codex_changed_files.txt +-M docs_build/dev/reports/codex_review.diff ++docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan.md ++docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_branch-validation.md ++docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_manual-validation-notes.md ++docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_requirements-checklist.md ++docs_build/dev/reports/PR_26177_CHARLIE_009-sprites-legacy-audit-plan_validation-lane.md ++docs_build/dev/reports/codex_changed_files.txt ++docs_build/dev/reports/codex_review.diff