ZDDC/AGENTS.md
ZDDC 40d9956e54 chore(release): default to alpha cascade; tidy stale CI references
- release-image.sh now defaults to alpha (was stable). Active dev no
  longer silently advances :stable; that tag only moves on a deliberate
  `sh release-image.sh <ver> stable`. Same cascade logic, reordered
  default. Updated AGENTS.md and zddc/README.md sections accordingly.
- zddc/Containerfile: dropped the "see .woodpecker.yml" comment since
  that file no longer exists; pointed the docs to release-image.sh.
- build.sh: dropped the "CI builds the runtime container directly"
  parenthetical; the cross-compiled host-binaries build is the only
  thing that step actually produces.

Why alpha as the default: caught it during active development —
:stable kept advancing every release because the script defaulted
there. Solo workflow + alpha default = `:stable` is a deliberate
gesture, not a side-effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:17:16 -05:00

311 lines
19 KiB
Markdown

# AGENTS.md — ZDDC
## Commands
```bash
# Build all tools (writes to dist/ only)
sh build.sh
# Build single tool
sh tool/build.sh # archive | transmittal | classifier | mdedit | landing
# Cut a stable release (auto-increments patch version, tags, writes to website/releases/)
sh tool/build.sh --release
sh tool/build.sh --release 1.2.0 # explicit version
# Cut an alpha/beta channel build (mutable, no git tag)
sh tool/build.sh --release alpha
sh tool/build.sh --release beta
# Release all tools at once
sh build.sh --release [version|alpha|beta]
# Test all tools
npm test
# Test single tool
npx playwright test tool # archive | transmittal | classifier | mdedit
# Dev server (cache-busting HTTP, on port 8000)
./dev-server start
./dev-server stop
```
No lint, typecheck, or format commands exist — the project is plain sh + vanilla JS.
## Architecture
Five independent single-file HTML tools (`archive`, `transmittal`, `classifier`, `mdedit`, `landing`). Each compiles to one self-contained `.html` in `dist/` with all CSS and JS inlined — the first four name their output `dist/tool.html`; `landing` writes `dist/index.html` (it's served at `/` by `zddc-server`). Tools share a small set of canonical helpers in `shared/` (filename parsing, ZDDC filter UI, theme, help) — see "Shared modules" below.
```
tool/
css/ source stylesheets (concatenated in order)
js/ vanilla JS IIFEs (concatenated in order)
template.html placeholder markers: {{CSS_PLACEHOLDER}}, {{JS_PLACEHOLDER}}, {{BUILD_LABEL}}
build.sh assembles dist/tool.html
dist/tool.html generated output — committed with `git add -f`
shared/
base.css CSS tokens and primitives included first by every tool's CSS build
zddc.js canonical filename/folder/revision parsers, formatters, status validation
zddc-filter.js shared ZDDC project/status filter UI module
theme.js light/dark theme switcher
help.js shared help dialog module
build-lib.sh POSIX sh helpers (ensure_exists, concat_files, build_timestamp)
sourced by every tool's build.sh via: . "$root_dir/../shared/build-lib.sh"
website/
index.html current stable landing (root URL)
releases/
<tool>_v<X>.<Y>.<Z>.html immutable stable release archives
<tool>_stable.html -> ... symlink to current stable (highest semver)
<tool>_alpha.html mutable; overwritten by --release alpha
<tool>_beta.html mutable; overwritten by --release beta
install.zip drop-in self-contained install (5 stable HTMLs + _template/ stubs)
track-stable.zip level-2 stubs that track the current-stable channel
track-alpha.zip level-2 stubs that track the alpha channel
track-beta.zip level-2 stubs that track the beta channel
bootstrap/
level1.html.tmpl per-project bootstrap template (relative ../<tool>.html)
level2.html.tmpl level-2 channel-tracking bootstrap template
README.md install / channel / pin docs
```
**Critical:** `dist/` files are gitignored but force-committed (`git add -f`). Never edit them directly.
## Shared CSS (`shared/base.css`)
Included as the **first** positional arg to every tool's `concat_files` CSS call. Provides:
- `:root` CSS custom properties — `--primary`, `--bg`, `--text`, `--border`, `--font`, etc.
- Brand color: `--primary: #2a5a8a` (matches zddc.varasys.io)
- Button primitive: `.btn`, `.btn-primary`, `.btn-secondary`, `.btn-sm`, `.btn-lg`, `.btn-link`
- `.app-header` + `.app-header__title` chrome rules
- `.build-timestamp`, `.hidden`, `.truncate`, webkit scrollbars
**Do not** define these in any tool's own CSS — they come from shared.
**Toast CSS** lives in `classifier/css/base.css` only (classifier is the only tool that uses toasts).
## Transmittal CSS quirks
- `transmittal/css/base.css` overrides `html { font-size: 16px }` inside `@media screen` — this must stay. `shared/base.css` sets `14px`; transmittal's floating labels are rem-based and were designed for 16px.
- The floating label position is defined in `transmittal/css/forms.css`, not Tailwind classes. If adding new Tailwind classes to `template.html`, add them to `transmittal/css/utilities.css` too — there is no Tailwind build step.
## Build system rules
- Every `build.sh` sources `shared/build-lib.sh` first (provides `ensure_exists`, `concat_files`, `build_timestamp`). Set `root_dir` before sourcing.
- Build scripts use **POSIX sh** (`#!/bin/sh` with `set -eu`), not bash.
- `concat_files` accepts **positional args only** (not array names).
- `awk` processes `template.html`, replacing `{{PLACEHOLDER}}` markers and stripping CDN `<script>`/`<link>` tags (pattern: `https?://`)
- `{{BUILD_LABEL}}` is substituted in all five tools via `gsub` in awk (use `gsub`, not `print` — the placeholder is inline in an HTML line). Value is `Built: <timestamp> BETA` for dev builds, `v<version>` for stable releases, and `<channel> · <date> · <sha>` for alpha/beta channel builds; computed before the awk step. The shared `is_red` flag controls whether the label is wrapped in a red+bold `<span>` (true for dev/alpha/beta, false for stable).
- Cleans up temp files via `trap cleanup EXIT`
**`</` escaping is mandatory.** Any JS containing `</tag>` inside string or template literals will break inline `<script>` embedding. Run:
```bash
sed 's#</#<\\/#g' "$input_js" > "$safe_js"
```
Required for any new tool with vendor JS or JS containing HTML template literals.
## JS module pattern
All JS is vanilla, no bundlers. Files are IIFEs, registered on `window.app.modules`. Load order = declaration order in `build.sh`. `window.app` is the only global.
```javascript
(function() {
window.app.modules.mymodule = { ... };
})();
```
**Exception:** archive uses plain globals (`APP_STATE`, top-level functions) — not the IIFE/modules pattern.
## ZDDC filename parsers
All parsing/formatting goes through `shared/zddc.js`, exposed as `window.zddc`. Tools call it directly — no per-tool wrappers.
`window.zddc` exports:
- `parseFilename(name)``{ trackingNumber, revision, status, title, extension, valid } | null` (extension WITHOUT leading dot)
- `parseFolder(name)``{ date, trackingNumber, status, title, valid } | null`
- `parseRevision(rev)``{ base, modifier, modifierType, modifierNumber, isDraft, modifierIsDraft, full }`
- `compareRevisions(a, b)` → number (canonical sort order)
- `formatFilename(parts)` / `formatFolder(parts)` — round-trips parsed output
- `isValidStatus(code)` — accepts known status codes plus `---`
All file objects across tools use `file.trackingNumber` (string) and `file.extension` (string, **no leading dot**, e.g. `'pdf'` not `'.pdf'`). When concatenating into a filename, write `name + '.' + ext`.
Coverage lives in `tests/zddc.spec.js` (47 cases). Add new edge cases there, not in tool tests.
## Testing quirks
- Playwright + Chromium only (File System Access API requirement)
- Tests open `dist/tool.html` via `file://` protocol — **always build before testing**
- File System Access API is mocked via `page.addInitScript()` using `tests/fixtures/mock-fs-api.js`
- Use `waitUntil: 'load'` or `'domcontentloaded'` not `'networkidle'` — bundled scripts keep the network "active"
- Archive's `#noDirectoryMessage` empty-state overlay is `position: absolute; top: 50px` — it must clear the header or it will block button clicks in tests
## ZDDC filename convention
Format: `trackingNumber_revision (status) - title.extension`
- `trackingNumber`: no spaces or underscores (e.g. `123456-EL-SPC-2623`)
- `revision`: `A`, `B`, `0`; draft prefix `~`; modifiers `+C1`, `+B1`, `+N1`, `+Q1`
- `status`: `IFA IFB IFC IFD IFI IFP IFR IFU REC RSA RSB RSC RSD RSI` or `---`
- Folder names prefix with date: `2025-10-31_trackingNumber (status) - title`
## Git workflow
- Feature-branch workflow; squash-merge feature branches to `main`
- Conventional commits: `feat(archive): ...`, `fix(transmittal): ...`
- Release tags: `archive-v1.0.0` (per-tool semver)
- Commit dist files: `git add -f tool/dist/tool.html`
### Releasing — channels and layout
Three channels:
- **Stable**: versioned, immutable. `sh tool/build.sh --release [version]` writes `website/releases/<tool>_v<version>.html`, refreshes the `<tool>_stable.html` symlink, and tags `<tool>-v<version>`. Skips automatically if source has not changed since the latest tag. Pass an explicit version to override auto-increment.
- **Beta**: mutable. `sh tool/build.sh --release beta` overwrites `website/releases/<tool>_beta.html` in place. No tag. The on-page label is `beta · <date> · <sha>` so the source is recoverable from git via the SHA.
- **Alpha**: mutable, analogous. `sh tool/build.sh --release alpha`. **Also**: every plain (non-release) `tool/build.sh` invocation reasserts a relative symlink `website/releases/<tool>_alpha.html``../../<tool>/dist/<tool>.html`, so the alpha hyperlinks on the website always serve whatever dist currently holds (no file copy, idempotent — git sees nothing on a rebuild). Symlinked alpha pages carry the dev label `Built: <ts> BETA` (red) since the dist file does. A deliberate `--release alpha` overwrites the symlink with a real file labeled `alpha · <date> · <sha>`; the next plain build re-symlinks it. Deployment must serve from the repo working folder (or otherwise resolve `../../<tool>/dist/` relative to `website/releases/`).
Stable releases do **not** automatically clobber `<tool>_alpha.html` / `<tool>_beta.html` — those keep whatever was last built into them. Use `./freshen-channel <tool> <channel>` (see "Freshen helper" below) to drag a channel forward to current stable; never `git checkout` the main worktree by hand for this.
After cutting a stable release, run `git push --tags` to publish the tag.
The "skip if no source change since last tag" guard for stable releases compares **HEAD** to the latest tag — uncommitted working-tree changes are invisible. If you edit a tool and want a stable release to actually fire, commit the change first; otherwise the build prints `no source changes since <tool>-vX.Y.Z — skipping` and exits 0. Alpha and beta channel builds always rebuild (no skip check).
Agents must **never** write to `website/releases/<tool>_v*.html`, `website/releases/<tool>_stable.html`, `website/releases/<tool>_beta.html`, or `website/index.html` directly — always go through `--release` or `./freshen-channel`. (The `<tool>_alpha.html` files are an exception: every plain build reasserts them as symlinks into `<tool>/dist/`, as described above.)
`landing/build.sh --release <version>` additionally writes `website/index.html` (the root URL of zddc.varasys.io).
### Channel discipline (MUST rules)
The build system does not enforce these. Treating channels carelessly defeats the point of having three. Be disciplined.
1. **Stable doesn't regress.** No known-broken features that worked in the previous stable. If you ship `v0.0.5` with a bug, the path forward is `v0.0.6` with a fix — never edit `v0.0.5` in place. Stable files are immutable.
2. **No backports.** Don't try to patch an older stable version. Always cut a new stable at a higher version. Users pinned to the old version stay pinned by their own choice; they can move forward when they want.
3. **Alpha and beta are mutable.** Document this anywhere you invite users to test them. Pinning `?v=alpha` (or `_alpha.html`) in a production deployment is a mistake; it gets rebuilt without notice.
4. **Stale-channel rule.** Users tracking alpha (or beta) MUST never see a build older than current stable. After every stable release, run `./freshen-channel <tool> alpha` and `./freshen-channel <tool> beta` so each channel is at-least-current. This is not optional.
5. **Hotfix path.** For critical bugs: fix on `main`, cut a new stable (no beta soak required), then freshen alpha + beta. Tag the commit message `fix:` or include "hotfix" so the intent is visible in `git log`.
6. **Beta soak before promoting (recommended).** Give a beta a few days of exposure before cutting the same code as stable. Not enforced; use judgment for trivial changes.
### Freshen helper
`./freshen-channel <tool> <channel>` rebuilds the alpha or beta channel of a tool from its current stable tag. Use it after every stable release (rule 4 above) and any other time alpha/beta has fallen behind stable.
```sh
./freshen-channel archive alpha
./freshen-channel transmittal beta
```
What it does:
1. Finds the latest `<tool>-v*` tag.
2. Creates a temporary git worktree at that tag — does **not** touch the main worktree's HEAD or working tree.
3. Runs `<tool>/build.sh --release <channel>` inside the worktree.
4. Copies the resulting `<tool>_<channel>.html` into the main repo's `website/releases/`.
5. Removes the worktree.
The on-page label of the freshened build is `<channel> · <today> · <stable-tag-sha>` — the SHA pins which stable was used as the source, recoverable via `git checkout`.
Note: the build pipeline used is the one **at the tag**, not on `main`. That is intentional (pure reproducibility). If you have made build-system improvements since stable was cut and want the freshen to use them, cut a new stable first.
### Bootstrap zips
`build.sh` regenerates three downloadable zips into `website/` on every invocation:
- `install.zip` — 5 current-stable HTMLs at root + `_template/` directory containing 4 level-1 bootstrap stubs (per-project use). Skipped if any tool has no stable release yet.
- `track-{alpha,beta,stable}.zip` — 5 level-2 stubs each, hardcoded to fetch the named channel from `zddc.varasys.io/releases/`. Drop one over a deployment root to switch the whole site to that channel.
See `bootstrap/README.md` for the install / pin / audit story.
### Worktrees
Use `git worktree` to run multiple agents on separate branches simultaneously without filesystem collisions.
- Worktrees live at `~/src/zddc-<branch-name>` (sibling of the main clone)
- Before starting work on a feature branch, check `git worktree list`; if no worktree exists, create one: `git worktree add ~/src/zddc-<branch-name> -b <branch-name>`
- All edits, builds (`sh build.sh`), and tests (`npm test`) run from within the worktree directory — build scripts use relative paths so this works correctly
- The `dist/` force-commit rule (`git add -f`) applies per-worktree
- After the branch is merged, clean up: `git worktree remove ~/src/zddc-<branch-name>` then delete the branch
- Never run `git checkout` or `git switch` inside a worktree that another agent may be using
## Transmittal-specific
- Two-phase hydration: `populateStatic()` before publish, `hydrate()` on load of published file
- Reactive state via Proxy — `app.state.mode = 'view'` auto-notifies subscribers
- Runtime CDN loads (jszip, docx-preview, xlsx) are allowed only for the optional DOCX/XLSX preview; core features work offline
- Published payload stored in `<script id="transmittal-data" type="application/json">`
## mdedit-specific
- `css/tailwind-utils.css` is a pre-generated static subset (~80 classes). Add new Tailwind classes here; do not re-run Tailwind.
- Toast UI Editor v3.2.2 is bundled in `vendor/`; `template.html` loads it from CDN for dev convenience
- `</` escaping is essential: `sed 's#</#<\\/#g'` runs on both app JS and vendor JS at build time
## zddc-server
Go HTTP server sub-project living at `zddc/`. Replaces `caddy file-server --browse` for ZDDC archives.
### Build
```sh
# Build the container image (from the zddc/ directory)
podman build -t zddc-server zddc/
# Or inside the zddc/ directory:
podman build -t zddc-server .
```
### Run (development)
```sh
ZDDC_DATA_DIR=/path/to/your/archive podman-compose -f zddc/podman-compose.yaml up --build
```
### Key environment variables
| Variable | Default | Purpose |
|---|---|---|
| `ZDDC_ROOT` | *(required)* | Path to served file tree |
| `ZDDC_ADDR` | `:8443` | Bind address |
| `ZDDC_EMAIL_HEADER` | `X-Auth-Request-Email` | Header set by upstream proxy with user email (oauth2-proxy / nginx auth-request convention) |
| `ZDDC_INDEX_PATH` | `.archive` | Virtual archive index URL segment |
| `ZDDC_LOG_LEVEL` | `info` | Logging verbosity |
| `ZDDC_CORS_ORIGIN` | `https://zddc.varasys.io` | Comma-separated CORS allowlist; empty value disables CORS. Default lets tools served from zddc.varasys.io call back into a customer-deployed server. |
### Release tagging
Two coordinated steps — git tag for auditability, then local image build:
```sh
git tag zddc-server-v0.0.4
git push origin zddc-server-v0.0.4
sh release-image.sh 0.0.4 # default: alpha → :0.0.4 :alpha
sh release-image.sh 0.0.4 beta # beta cascade → :0.0.4 :beta :alpha
sh release-image.sh 0.0.4 stable # stable cascade → :0.0.4 :stable :beta :alpha
```
**Default channel is `alpha`** so `:stable` never advances by accident
during active development. Pass `beta` to soak; pass `stable` only when
deliberately promoting. Cascades:
- `alpha``:<version> :alpha`
- `beta``:<version> :beta :alpha`
- `stable``:<version> :stable :beta :alpha`
Prerequisite: `podman login codeberg.org` (one-time, with a Codeberg
personal token scoped `package:write`).
There is no CI for this — `.woodpecker.yml` was removed in favor of
local-build-and-push because solo workflow benefits from one canonical
path that fails loudly and visibly on the developer's terminal.
### Notes
- No external test framework yet — Go unit tests run with `go test ./...` inside `zddc/` (requires Go 1.24+)
- The container image does NOT require Go on the host — the Containerfile uses a multi-stage build
- Portfolio files (`*.portfolio`) in the served tree appear as virtual group directories
- The `.archive` virtual path resolves ZDDC tracking numbers to their earliest-received revision
- ACL is enforced via cascading `.zddc` YAML files; authentication is delegated to the upstream proxy via the `X-Auth-Request-Email` header (configurable with `ZDDC_EMAIL_HEADER`)
- `.zddc` schema also supports a top-level `admins:` glob list, peer to `acl.allow`/`acl.deny`. Honored **only** at the root `.zddc` (subdir `admins` entries are ignored to prevent privilege escalation via subtree write access). Drives the built-in debug dashboard at `/.admin/` (sub-routes: `/whoami`, `/config`, `/logs`); non-admin requests get 404 so the page is invisible. See `zddc/README.md` § "Admin Debug Page".
- **Reserved entry prefixes** under `ZDDC_ROOT`: `.`-prefixed entries are excluded from listings AND 404 on direct fetch (only `.archive` and `.admin` are exempt) — for invisible side-state like dev-shell home dirs. `_`-prefixed entries are excluded from listings only — for operator scaffolding like install.zip's `_template/` that's still reachable by direct URL. Drop side-state under `_` if it should be linkable; under `.` if it should be unreachable.