chore(agent): add phases 20-23 from TODO.md — four owner-confirmed bug fixes; clear TODO.md

This commit is contained in:
2026-08-24 15:23:49 -04:00
parent 2afc77ee56
commit 824914ca3d
13 changed files with 1040 additions and 0 deletions
@@ -0,0 +1,120 @@
# Phase 23 — Containerfile: Build the Whole App Image Again
**Source:** `TODO.md` L6 — *"Fix Containerfile build not working"*
**Story:** `.agent/user_stories/containerfile-build.md` (created by task 02)
**Context:** `Containerfile` (3 stages: node:22-alpine + esbuild
0.25.5 frontend bundle → uv/python deps → slim runtime serving
`/app/static`); `frontend/` (4 pages: `index.html`, `sources.html`,
`document.html`, `login.html`; assets: `styles.css`, `markdown.js`
(classic script), `header.js`/`app.js`/`sources.js`/`document.js`/
`login.js` (ES modules)); `scripts/entrypoint.sh`.
## Verified diagnosis (2026-08-24, this conversion — not a guess)
1. **Root cause of the build failure:** phase 19 switched the page
scripts to `import … from "/assets/header.js"` (an absolute URL).
esbuild resolves that as the *filesystem* path `/assets/header.js`
and the stage-1 bundle dies:
`✘ [ERROR] Could not resolve "/assets/header.js"`
(reproduced with esbuild **0.25.5**, the exact pinned version, on a
copy of `frontend/`).
2. **Secondary gap (image would be broken even if it built):** stage 1
bundles only `app.js` + `sources.js` and copies only `index.html` +
`sources.html`. Missing from the image: `document.html` +
`login.html` (phases 10/16), `document.js` + `login.js`, and
`markdown.js` (classic script loaded by `index.html` +
`document.html`).
3. **Verified fix:** with relative imports (`from "./header.js"`) all
four page scripts bundle cleanly with esbuild 0.25.5.
4. **Latent double-evaluation trap:** all four HTML pages also load
`<script type="module" src="/assets/header.js">` directly while the
page script imports it. In dev the browser dedupes (same module
URL) — but in the image the bundled page script already contains the
header code, so shipping a raw `header.js` too would evaluate the
module **twice** (duplicate sign-out listener, double init). The
direct tags are redundant: the page script's `import` is hoisted and
guarantees `header.js` evaluates before the page script's body calls
`initSharedHeader()`, in dev and in the bundle alike.
## Objective
`podman build -f Containerfile .` succeeds, and the resulting image
serves the **whole app** — all four pages with their bundled, minified,
local-only assets (No CDN rule) — with `header.js` evaluated exactly
once per page.
## Owner-confirmed (2026-08-24, roadmap A4)
1. **Relative imports** (`./header.js`) over an esbuild alias — simpler,
verified working, dev-server behavior unchanged (files are
side-by-side).
2. **Remove the four redundant direct `header.js` script tags** (the
design above) rather than ship a raw `header.js` into the image —
single module evaluation, no duplicate listeners.
3. The image must cover **all four pages + all local assets** they
reference — the integration test (task 02) enforces this coverage so
the gap cannot silently reappear.
## Dependencies
- `19_shared_header` (complete) — introduced the absolute imports (root
cause) and the direct `header.js` tags.
- `10_story_document_viewer` / `16_admin_auth` (complete) — the pages
missing from the image.
- `08_story_dark_tech_theme` (complete) — No CDN rule the image must
honor.
## Tasks
1. `01_fix_containerfile_build.md` — relative imports, tag removal,
stage-1 asset coverage, green `podman build`, image smoke test.
2. `02_integration_test_commit.md` — `tests/integration/
test_containerfile_assets.py` (hermetic coverage pin), regression
suites, story file, final validation, the single atomic commit,
phase move to `complete/`.
## Locked decisions
- **A11 honored** — vanilla JS, no CDN, static serving from FastAPI.
**A16 honored** — integration test for the new build coverage; story
file + report; no Playwright suite required (this phase is
build/infrastructure — the phase gate is the hermetic integration
test + the real `podman build` + image smoke recorded in the report,
plus the dev-server E2E regressions). No anchor changed.
## Testing & Quality
- **Integration (new `tests/integration/test_containerfile_assets.py`,
hermetic — no podman, no network):** every `frontend/*.html` is
copied into stage 1's `/out`; every local `src`/`href` asset
referenced by the four pages is produced by a stage-1 line (esbuild
`--outfile` or `cp`); the four page module scripts are the exact set
esbuild bundles; `markdown.js` is produced; no HTML references
`/assets/header.js` directly (single-evaluation design pin); the
esbuild version stays pinned.
- **Unit:** none (no `app/` changes).
- **Coverage:** the >90% `app/` gate is unaffected, re-run to prove it.
- **Build gate (manual, recorded in the report):** `podman build
-f Containerfile .` green; image smoke (task 01 step 6) results +
log excerpt in `.agent/reports/23_containerfile_build/`.
- **Dev regressions (E2E, isolated):** `test_smoke.py`,
`test_shared_header.py`, `test_chat_persistence.py` (the HTML tag
removal touches dev page load).
- **Lint/types:** `uv run ruff check . && uv run pyright` clean.
## Completion Criteria
- [ ] Local esbuild 0.25.5 bundles all four page scripts cleanly.
- [ ] `podman build -f Containerfile .` green (log excerpt in the
report).
- [ ] Image smoke: container runs (throwaway Postgres 17 + pgvector);
`GET /`, `/sources.html`, `/document.html`, `/login.html` → 200;
`/assets/app.js` minified and contains the header code;
`/assets/markdown.js` 200; no `http(s)://` asset reference in any
served page (No CDN rule).
- [ ] Dev server unchanged in behavior: the three regression E2E suites
green in isolation.
- [ ] `uv run pytest` green; `uv run pytest --cov=app
--cov-report=term-missing` ≥ today's number.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] `.agent/user_stories/containerfile-build.md` exists.
- [ ] One `--no-gpg-sign` commit (below);
`.agent/phases/todo/23_containerfile_build/` moved to
`.agent/phases/complete/`.
## Commit
```bash
git add -A .agent/ Containerfile frontend/ tests/ && git commit --no-gpg-sign -m "fix(build): Containerfile builds again — relative module imports, all four pages and shared assets in the image"
```
@@ -0,0 +1,91 @@
# Task 01 — Fix the build: relative imports, tag removal, full stage-1 asset coverage
**Phase:** `23_containerfile_build` · **Source:** `TODO.md` L6 —
*"Fix Containerfile build not working"*
## Objective
Make `podman build -f Containerfile .` succeed and ship the **complete**
frontend in the image: all four pages, all four bundled page modules,
the classic `markdown.js`, and the minified `styles.css` — with
`header.js` evaluated exactly once per page.
## Work
1. **Reproduce the failure** and record it in
`.agent/reports/23_containerfile_build/` (log excerpt):
- fast: `npx -y esbuild@0.25.5` on a copy of `frontend/` → the
`Could not resolve "/assets/header.js"` error (root cause, already
reproduced during conversion);
- authoritative: `podman build -f Containerfile .` → stage 1 fails
at the same line.
2. **`frontend/assets/{app,sources,document,login}.js`** — change the
header import from absolute URL to relative (one line each; the
specifiers are currently `from "/assets/header.js"`):
```js
import { … } from "./header.js";
```
(owner-confirmed A4 — relative over esbuild alias; dev-server
behavior is unchanged since the files are side-by-side and the
module URL resolves to the same file.)
3. **Remove the four redundant direct `header.js` tags** (owner-confirmed
A4-2 — the single-evaluation design from `00_phase.md`):
- `frontend/index.html` (~line 119) —
`<script type="module" src="/assets/header.js"></script>`;
- `frontend/sources.html` (~line 125), `frontend/document.html`
(~line 80), `frontend/login.html` (~line 67) — same tag.
- Update the surrounding HTML comments that describe the
header-before-page-script load order (e.g. index.html ~lines
115–119): the order is now guaranteed by the page script's own
`import` (hoisted, evaluated before the page script body calls
`initSharedHeader()`).
4. **`Containerfile` stage 1** — cover the whole app (keep the pinned
`esbuild@0.25.5` and the existing flags):
```dockerfile
RUN mkdir -p /out/assets \
&& esbuild ./assets/app.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/app.js \
&& esbuild ./assets/sources.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/sources.js \
&& esbuild ./assets/document.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/document.js \
&& esbuild ./assets/login.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/login.js \
&& esbuild ./assets/markdown.js --minify --outfile=/out/assets/markdown.js \
&& esbuild ./assets/styles.css --minify --outfile=/out/assets/styles.css \
&& cp ./index.html ./sources.html ./document.html ./login.html /out/
```
(`markdown.js` is a classic script — minify only, **no** `--bundle`;
it exposes globals used by the pages.)
5. **Verify locally (no podman):** with esbuild 0.25.5, all four module
bundles + the markdown minify succeed on the real `frontend/` (not a
copy — the copy was only for the diagnosis).
6. **`podman build -f Containerfile .`** → green.
7. **Image smoke test** (results + log excerpt into the report dir):
- throwaway Postgres 17 + pgvector (`podman compose up -d db` and
point the container at it, or a one-off container with the same
env as `compose.yaml`);
- run the built image (migrations run via the entrypoint);
- `GET /`, `/sources.html`, `/document.html`, `/login.html` → 200;
- `GET /assets/app.js` → 200, minified (single-line-ish), and
contains the header code (e.g. the `clearChatStorage` function
body); `GET /assets/markdown.js`, `/styles.css`, the other three
page modules → 200;
- No CDN rule: none of the four served pages contain an `http(s)://`
`src`/`href` asset reference.
- Teardown the throwaway containers when done.
8. **Dev-server regression check** (the tag removal touches dev page
load — confirm boot order still holds): `uv run uvicorn
app.main:app --reload`, load all four pages, check the sign-out
binding exists exactly once (DevTools: no duplicate listener — one
`POST /api/logout` per click) and `initSharedHeader()` ran. (The
isolated E2E regressions run in task 02.)
## Testing & Quality
- Steps 5–8 above; `uv run ruff check . && uv run pyright` clean
(no Python changes, but keep the gate green).
## Completion Criteria
- [ ] The recorded build failure is fixed at the root cause (relative
imports) — not masked by an alias/patch.
- [ ] All four direct `header.js` tags removed + comments updated; the
page scripts' `import "./header.js"` is the only header load.
- [ ] Stage 1 produces: 4 HTML pages, 4 bundled modules, minified
`markdown.js`, minified `styles.css`.
- [ ] `podman build` green; image smoke all-200 + No CDN + single
header evaluation; dev-server boot unchanged (step 8).
- [ ] Log/screenshot evidence in `.agent/reports/23_containerfile_build/`.
@@ -0,0 +1,70 @@
# Task 02 — Integration coverage test, story file, validation, commit
**Phase:** `23_containerfile_build` · **Source:** `TODO.md` L6
## Objective
Pin the stage-1 asset coverage so it can't silently rot again (a new
page/script/asset without a matching Containerfile line fails CI), plus
regressions, story file, final validation, and the single atomic commit.
## Work
1. `tests/integration/test_containerfile_assets.py` (new — **hermetic**:
parses `Containerfile` + `frontend/` as text, no podman, no network).
Tests:
1. `test_every_html_page_is_copied_into_stage1` — for each
`frontend/*.html` in the repo, a stage-1 line copies it into
`/out` (regex over the `cp` line; the set must be exactly the
four current pages — a new page added to `frontend/` fails this).
2. `test_every_local_asset_reference_is_produced` — collect every
local `src=`/`href=` under `assets/` or `/assets/` from the four
HTML files; each basename must be produced by a stage-1 line
(an `esbuild … --outfile=/out/assets/<name>` or a `cp` of it).
(This is what catches a missing `markdown.js`-style gap.)
3. `test_page_module_scripts_are_bundled` — the set of `type="module"`
page scripts referenced by the HTML (basenames) equals the set of
scripts esbuild bundles in stage 1 (`app.js`, `sources.js`,
`document.js`, `login.js`).
4. `test_header_module_is_imported_not_directly_loaded` — no HTML
file contains a `<script … src="/assets/header.js">` (or
`assets/header.js`) tag (the single-evaluation design pin,
owner-confirmed A4-2); and each of the four page scripts imports
it relatively (`from "./header.js"`).
5. `test_markdown_js_is_a_produced_classic_script` — `markdown.js`
has a stage-1 minify line **without** `--bundle` (it is a classic
global script) and no `import`/`export` statements at its top
level (source pin of that assumption).
6. `test_esbuild_stays_pinned` — the frontend stage pins a concrete
`esbuild@X.Y.Z` version (no floating version).
2. `.agent/user_stories/containerfile-build.md` (new) — story file per
the repo format: goal, the bug report verbatim from `TODO.md` L6, the
verified diagnosis (root cause + missing-asset gap + double-eval
trap), the owner-confirmed A4 decisions, and the test mapping table.
3. Regressions, in isolation, one command each (prereq
`podman compose up -d db`):
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov`
- `uv run pytest tests/e2e/test_shared_header.py -v --no-cov`
- `uv run pytest tests/e2e/test_chat_persistence.py -v --no-cov`
4. Final validation: `uv run pytest` green (includes the new
integration test); `uv run pytest --cov=app --cov-report=term-missing`
≥ today's number (>90% gate); `uv run ruff check . && uv run pyright`
clean.
5. Finish the phase report (`.agent/reports/23_containerfile_build/` —
build log excerpt, smoke results, regression results).
6. Commit (one atomic commit) and move the phase:
```bash
git add -A .agent/ Containerfile frontend/ tests/
git commit --no-gpg-sign -m "fix(build): Containerfile builds again — relative module imports, all four pages and shared assets in the image"
mv .agent/phases/todo/23_containerfile_build .agent/phases/complete/
```
## Testing & Quality
- New integration suite green within `uv run pytest`; the three
regression E2E suites green in isolation; full suite green; `app/`
coverage at or above today's number (>90%); ruff + pyright clean.
## Completion Criteria
- [ ] `test_containerfile_assets.py` 6/6 within the full suite.
- [ ] Regressions (smoke, shared header, chat persistence) green in
isolation.
- [ ] Story file + phase report (build log + smoke evidence) exist.
- [ ] One `--no-gpg-sign` commit; phase directory in `complete/`.