la-li-lu-le-lo/AGENTS.md
rain fbb5f02f49 Add AGENTS.md and move the verification tools into the repo
The checks that were being run by hand lived in /tmp and would have been
lost, and the colour-contrast pass was ad-hoc enough that it missed
--dim2 entirely.

- tools/verify.js: headless smoke test. Boots the page against a stubbed
  DOM/canvas and checks all 9 tabs render, all 10 maps draw with no
  non-finite geometry and no squashed labels, dog-tag sub-checkboxes cover
  every tag in every list, no photo appears in two steps, every theme has a
  CSS block, and no personal data shipped.
- tools/theme-contrast.py: WCAG check across all themes, now also covering
  --on-acc on --acc.

Running the stricter check found real problems: --dim2 (small captions) was
2.3-3.0:1 in four themes and --acc-d was 2.7-2.8:1 in two. Lightened those.
Worst theme is now 3.2:1, all on-accent pairs are 4.8:1 or better.

- AGENTS.md documents the architecture, build stages, assertions, data
  provenance, deployment and the traps already hit, so a future session does
  not rediscover them the hard way.
2026-09-15 17:41:48 -04:00

167 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AGENTS.md
Orientation for anyone (human or agent) picking this repo up cold. Read this before changing anything.
## What this is
A single-page companion for 100%-ing **Metal Gear Solid 2: Sons of Liberty — Master Collection Version** (Steam appid `2131640`). It contains a full walkthrough, all 51 achievements, all 394 dog tags, rebuilt guard-position maps, location screenshots, and live Steam achievement sync.
- **Live:** <https://rain.pages.melonbread.xyz/la-li-lu-le-lo/>
- **Repo:** <https://git.melonbread.xyz/rain/la-li-lu-le-lo> (branch `pages`, which is the default branch)
- **Owner:** `rain` / `rain@melonbread.xyz`
## Golden rules
1. **Never edit `index.html`.** It is generated. Your change will be silently overwritten on the next build. Edit `build/` and run `./deploy.sh`.
2. **Run `node tools/verify.js` before deploying.** It catches most of the ways this project breaks.
3. **Run `python3 tools/theme-contrast.py` after touching any colour or theme.**
4. The build has assertions. If one fires, **fix the data, do not delete the assertion.**
## Layout
```
index.html generated - do not edit
steam-sync.py generated copy of build/steam-sync.py
steam-proxy-worker.js generated copy of build/steam-proxy-worker.js
deploy.sh build + commit + push
tools/verify.js headless smoke test (stubbed DOM, no browser)
tools/theme-contrast.py WCAG contrast check across all themes
build/ ALL the sources
build.py the build script
template.html the page: CSS + JS + markup, with __PLACEHOLDER__ slots
achievements_src.py hand-written how-to text for all 51 achievements
roadmap_src.py hand-written walkthrough steps + Big Boss content
*.json extracted data (see Provenance)
```
`build.py` writes to `OUT = os.path.dirname(HERE)`, i.e. the repo root, and copies the two helper files up from `build/`. So the helpers are edited in `build/` and published to the root.
## How the page works
One self-contained HTML file. No framework, no build step at runtime, no network needed except for hot-linked photos.
`build.py` injects five JSON blobs into `template.html`:
| Blob | What |
|---|---|
| `ACHIEVEMENTS` | 51 entries: name, desc, official Steam icon, category, missable flag, guide HTML, photos |
| `ROADMAP` | 7 phases / 48 steps, each with body HTML, `gets`, `areas`, `photos` |
| `DOGTAGS` | 10 lists (chapter × difficulty), 394 tags with name, area, conditional note |
| `MAPS` | 10 spreadsheet sheets: palette, RLE fill runs, text cells, merges, col widths, row heights |
| `BIGBOSS` | criteria, heal points, time checkpoints, boss strategies |
All interactive state lives in **one `localStorage` key**, `mgs2mc-guide-v1`:
```js
S = { ach:{id:true}, tags:{'listId:index':true}, road:{stepId:true},
items:{'stepId:slug':true}, hideDone, tab, roadDiff,
steam:{profile,id64,persona,unlocked:{api:true},lastSync} }
```
There is a **separate** key `mgs2-theme` for the colour theme, so it is not affected by Export/Import.
## Build pipeline
Order matters — later stages depend on earlier ones:
1. **achievements** — merge `achievements_src.py` with `steam_official.json` (names/descriptions/icons from Steam's own XML).
2. **screenshots** — assemble per-achievement photo lists from the two guides, drop perceptual duplicates, pin multi-location photos to steps, attach `st.photos`.
3. **colour coding** — classify every `<b>`/`<i>` in the roadmap as tag / item / achievement / missable / boss / area.
4. **step sub-checklists** — extract `gets` from the colourised HTML, map tag areas (`TAGMAP`), attach `st.areas`.
5. **dog tags**, then **inject** everything into `template.html`.
### Assertions (do not remove)
- every dog-tag area is claimed by exactly one step (`TAGMAP`) — nothing silently unmapped
- every `PHOTO_STEP` key matches at least one real photo — catches typos in the 40-char hashes
- every photo of a split achievement is pinned to a step
- no achievement whose photos span several steps is left unpinned
## Verifying changes
**`node tools/verify.js`** loads `index.html`, runs its script against a stubbed DOM/canvas and checks: boot does not throw, all 9 tabs render, all 10 maps draw without non-finite geometry, no label is squashed, dog-tag sub-checkboxes cover every tag in every list, no photo appears twice, every theme has a CSS block, and no personal data shipped.
**`python3 tools/theme-contrast.py`** checks every foreground against `--panel` plus `--on-acc` against `--acc`, across all themes. Fails under 3.0:1.
**There is no browser tooling available.** `browser_screenshot` crashes the harness GUI process (SIGSEGV in the Electron/sharp path) — do not call it. The Node stub approach above is the substitute; it verifies logic, not layout, so **layout changes cannot be verified here** and should be stated as unverified.
Quick manual checks worth knowing:
```bash
grep -c 'class="lb"' index.html # photos present in the build
node -e "..." # see tools/verify.js for the stub pattern
```
## Data provenance
All third-party content is credited in the page's Sources tab and in README.md.
| Data | Source | Notes |
|---|---|---|
| Achievement list, icons, apinames | Steam community XML | `steam_official.json` — 51 entries, `ach_002_001``ach_002_051` |
| Achievement how-to text | Steam guide by Cole ヴ Viper + Dayngl's Guides | hand-written into `achievements_src.py` |
| 394 dog tags | Video Chums checklist | `dogtags_vc.json` (names + areas) |
| Conditional-spawn notes | u/Spikeyroxas spreadsheet | `dogtag_notes.json` (81 notes) |
| Area maps | same spreadsheet | `maps.json` — the sheets are cell-grid art, not images |
| Location photos | Steam guide + Dayngl's Guides | hot-linked, never downloaded into the repo |
| Achievement removal check | TrueSteamAchievements | confirmed 51, incl. `Hold Up-aholic` which the Steam guide skips |
**Refreshing a dataset** means re-extracting into `build/*.json` and re-running the build. The extraction was originally done with ad-hoc Python against the source pages; if a source changes shape, re-derive rather than patching the JSON by hand.
## Traps already hit (don't repeat)
- **Canvas `fillText` with `maxWidth` condenses text, it does not truncate.** Narrow clip widths turn labels into an unreadable smear. `drawMap` measures each label and steps the font down (to 7px), then lets it overflow rather than squashing. `tools/verify.js` fails if any label comes out condensed.
- **Excel does not clip overflowing text at a coloured cell.** Only a cell with actual content stops it. Clipping map labels at fill cells made them tiny; clip at text cells only.
- **`data-sub` must hold the handler key, not the CSS class.** It was set to the class, so missable achievements emitted `data-sub="ach miss"` and their tick boxes did nothing — and missables are the whole point.
- **Per-achievement photo caps break per-step distribution.** Capping at 4 dropped exactly the photos that needed spreading across steps. Deduplicate globally, distribute per step, cap per step.
- **Normalize both sides of a name lookup.** An items list written with spaces (`"stun grenades"`) was compared against space-stripped text, so no multi-word item ever matched — 4 spans instead of 31.
- **`pkill -f "steam-sync"` kills the invoking shell**, because that shell's own command line contains the pattern. Use `pkill -f "[s]team-sync"` **in a call that does not also contain the literal string**, or split kill and start into separate calls.
- **A bad slice edit duplicated half of `build.py`.** The tell was the build printing every stage twice — check build output length when editing that file.
- **Code that uses a computed value must run after it is computed.** The photo-to-step assignment needed `gets`, which is built later in the script.
## Steam sync
Steam's community XML sends no CORS headers, so a browser cannot read it directly. Two ways around it:
**Local helper (what is deployed uses).** `steam-sync.py` serves the folder and proxies `/api/steam?profile=…`. A hosted HTTPS page calls it at `http://127.0.0.1:8765`, so **the helper must be running on the viewer's machine**:
```bash
nohup python3 ~/Desktop/la-li-lu-le-lo/steam-sync.py > /tmp/steam-sync.log 2>&1 &
```
It answers `Access-Control-Allow-Private-Network: true` on the preflight — Chrome blocks a public page from reaching localhost without it.
**Cloudflare Worker.** `steam-proxy-worker.js` does the same job with no local process. Deploy it and set `STEAM_PROXY` near the top of the page script. `ALLOW_ORIGIN` is pinned to the Pages origin.
`syncEndpoint()` resolves: localhost → same-origin `/api/steam`; `STEAM_PROXY` set → that; otherwise → the local helper.
Parsing notes: match achievements by **apiname** (`ach_002_0xx`), never by display name. The XML has **no `<steamID>` persona field** — fall back to `customURL`, then to what the user typed.
## Themes
Ten dark themes, switched from the header and applied by a small script in `<head>` before first paint (no flash). Everything is themed through CSS custom properties on `:root` and `html[data-theme="…"]`. **Any new colour must go through a variable**, otherwise it will be wrong in nine themes. Tints use `color-mix()` so they adapt automatically.
Two deliberate choices: the **area maps keep a white background** (they reproduce a spreadsheet; inverting them would make the colour legend lie), and there are **no light themes** (removed on request).
## Deployment
```bash
./deploy.sh "what changed"
```
Builds, commits, pushes to `pages`. Pages redeploys in ~15s. Verify with:
```bash
curl -s -L https://rain.pages.melonbread.xyz/la-li-lu-le-lo/ | sha256sum
sha256sum index.html
```
The two must match. A stale deploy is the most common confusing failure — check the hash before debugging anything else.
## Conventions
- British-ish plain English in user-facing copy; no marketing voice.
- Comments explain **why**, not what, and are only added where the reasoning is non-obvious.
- Content lives in the `*_src.py` files as HTML strings; structural/page changes live in `template.html`.
- Keep the page dependency-free. Every external thing is a hot-linked image; nothing else loads off-origin.
- Do not commit personal data. `tools/verify.js` fails if `DEFAULT_PROFILE` is non-empty.