chore(agent): add phases 20-23 from TODO.md — four owner-confirmed bug fixes; clear TODO.md
This commit is contained in:
@@ -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/`.
|
||||
Reference in New Issue
Block a user