# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Commits and pushes - **Commit freely** — make commits as appropriate for the work being performed. Each commit should be a coherent, reviewable unit (no WIP/checkpoint noise). The default rule "never commit without explicit ask" does NOT apply in this repo. - **Push only when explicitly told** — `git push` requires a fresh request from the user every time. Approval to commit does not carry forward to push, and approval to push once does not carry forward to a later push. - **No squashing on push** — keep granular history. Each commit should already be meaningful (per the rule above), so squashing erases useful detail rather than removing noise. Multi-commit branches with a clean history are preferred over force-pushed squash-merges. ## Authoritative docs — read these first This repo already has two thorough agent-facing references. **Always consult them before working** — they cover details intentionally omitted here: - **`AGENTS.md`** — commands, build-system rules, per-tool parser quirks, testing gotchas, git/worktree workflow, release process, zddc-server notes - **`ARCHITECTURE.md`** — single-file HTML pattern rationale, JS module/state patterns, per-tool architecture, security model If something in this CLAUDE.md conflicts with those, those win — and please update them rather than letting drift accumulate. ## Repo shape This is a **monorepo of independent tools**, not one application: - `archive/`, `transmittal/`, `classifier/`, `mdedit/`, `landing/`, `form/`, `tables/`, `browse/` — eight self-contained HTML tools, each compiled to a single inlined HTML file in its own `dist/`. Most output `dist/tool.html`; **`landing/` outputs `dist/index.html`** (it's the project picker served at the root of `zddc-server`). `form/` is the schema-driven renderer for the form-data system (any `.form.yaml` file in the tree becomes an editable form at `/.form.html`); `tables/` is its read/aggregate counterpart, rendering a directory of YAML rows as a sortable table; `browse/` is the file-tree navigator. See AGENTS.md "Form-data system" / "Tables system" and ARCHITECTURE.md "Form Renderer". - `zddc/` — Go HTTP server (separate sub-project; Go 1.24+). Two deployment shapes from the same binary: (1) **master** — owns a file tree under `ZDDC_ROOT`, applies `.zddc` ACL cascades, serves files / app HTML / archive listings. Two auth paths on master: `Authorization: Bearer ` validated against self-issued tokens at `/.zddc.d/tokens/` for CLI/scripted callers, or `X-Auth-Request-Email` injected by an upstream proxy for browser sessions. Self-service token UI at `/.tokens` + JSON API at `/.api/tokens`. (2) **client** — when `--upstream ` is set, the binary becomes a downstream proxy/cache/mirror (`zddc/internal/cache/`); master-side machinery is bypassed and `--root` becomes the cache directory. Three sub-modes via `--mode proxy|cache|mirror` (mirror is phase 3). Cache layout is a normal ZDDC root, so the cache dir can be served as a plain master if you unset `--upstream`. Marker file `.zddc-upstream` records provenance. `--no-auth` skips ACL enforcement entirely on this instance (distinct from `--insecure` which only relaxes the no-root-`.zddc` startup check); `--skip-tls-verify` is a separate flag for self-signed upstream certs. Cross-compiled binaries are produced by `./build` and live in `dist/release-output/` (gitignored); `./deploy` rsyncs them to `/srv/zddc/releases/` on the deploy host (Caddy serves them at `https://zddc.varasys.io/releases/`). The `helm/` charts in this repo build from source at deploy time. - `shared/` — `base.css` plus shared JS modules (`zddc.js`, `hash.js`, `zddc-filter.js`, `theme.js`, `help.js`) included by every tool's build, and `build-lib.sh` (POSIX sh helpers sourced by every tool's `build.sh` AND by the top-level `build` for lockstep release helpers). - **Two-repo + deploy-host model.** Source code lives here (`codeberg.org/VARASYS/ZDDC`). Hand-edited website content lives in a separate repo (`codeberg.org/VARASYS/ZDDC-website`, typically cloned at `~/src/zddc-website/` — just `index.html`, `reference.html`, `css/`, `js/`, `img/`; no releases, no LFS). The live site at `zddc.varasys.io` is served from `/srv/zddc/` on the deploy host: Caddy bind-mounts that path, and it's populated by `./deploy` from this repo's `dist/release-output/` plus `~/src/zddc-website/`. **Releases are NOT in any git history** — they're reproducible from this repo's `-vX.Y.Z` tags by checking out the tag and running `./build release X.Y.Z`. Per-version files (`_v.html`) are immutable; partial-version pins (`_v.html`, `_v.html`) and channel mirrors (`_{stable,beta,alpha}.html`) are symlinks; zddc-server has analogous `zddc-server_v_` per-version binaries plus channel/partial-version symlinks plus `zddc-server_.html` stub pages that fan out the four-platform download in one cell. **Install model:** local use is a download from `/releases/`. Server use is `zddc-server`, which has the current-stable build of all eight HTML tools baked in via `//go:embed` (compile-time default). Tools auto-served at folder-name-driven paths: `archive` everywhere, `classifier` in `Incoming`/`Working`/`Staging` subtrees, `mdedit` in `Working` subtrees, `transmittal` in `Staging` subtrees, `landing` only at root. Override via `.zddc apps:` cascade entry (channel/version/URL/path) — fetched once, cached at `/_app/`. Drop a real `.html` file at any path to override. - `helm/` — example Helm charts for zddc-server. Three flavors: `zddc-server-prod/` (production master), `zddc-server-dev/` (development master with OverlayFS isolation), `zddc-server-cache/` (downstream client running in proxy/cache/mirror mode against an upstream master, with bearer token from a Kubernetes Secret). All compile from source via init container. Operators copy `values.yaml.example` and customize. No secrets in repo — the cache chart references a separately-created Secret for the bearer token. - `tests/` — Playwright specs (Chromium only, requires File System Access API). `tests/schema.spec.js` validates `transmittal.schema.json` against canonical fixtures via `ajv` (only dev dep besides Playwright) ## Most-used commands ```bash # Source-side dev build only — assembles tool/dist/ + cross-compiles # zddc-server. Does NOT touch dist/release-output/ or the live site. ./build # Channel/release cuts — produce a complete release bundle in # dist/release-output/ (gitignored). Cuts seed from the live site # (/srv/zddc/releases/) so the bundle is a complete intended-live # snapshot, not a sparse diff. Run ./deploy to publish. ./build alpha # cut alpha (cascades nothing) ./build beta # cut beta (cascades alpha → beta) ./build release # cut stable, coordinated next version # (cascades alpha + beta → new stable; tags all nine artifacts) ./build release X.Y.Z # cut stable at explicit version ./build help # usage # Deploy — atomic-ish rsync of the build output + content repo to # /srv/zddc/, where Caddy serves it. The build does NOT auto-deploy. ./deploy # full sync: content + releases ./deploy --content # only ~/src/zddc-website/ → /srv/zddc/ ./deploy --releases # only dist/release-output/ → /srv/zddc/releases/ sh tool/build.sh # iterate on one HTML tool's dist/ sh tool/build.sh --release [...] # single-tool release (rare; prefer the lockstep ./build) ./freshen-channel # rebuild one tool's alpha/beta from its current stable tag npm test # all Playwright specs (build first!) npx playwright test # one spec ./dev-server start # stop # cache-busting HTTP on :8000 # zddc/ Go server (sub-project) (cd zddc && go test ./...) # unit tests (Go 1.24+) ``` No lint/typecheck/format commands exist for the HTML tools — vanilla JS + POSIX sh by design. ## Things that bite if you forget - **`dist/` is gitignored.** `tool/dist/.html` is the canonical built artifact for testing and as the source for `--release` writes. `dist/release-output/` is the local-only release bundle written by `./build alpha|beta|release`. Never hand-edit a `dist/` file. - **Build vs deploy are separate verbs.** `./build` and `./build alpha|beta|release` produce artifacts under `dist/release-output/`. Nothing escapes the source tree until the operator runs `./deploy`, which rsyncs into `/srv/zddc/` (Caddy's bind-mount). This decouples local iteration from live state. - **Channel/release cuts seed from live state.** Before running per-tool promote, `./build alpha|beta|release` clears `dist/release-output/` and copies `/srv/zddc/releases/` into it (preserving symlinks). The cut then mutates the channels being cut on top. Result: `dist/release-output/` is always a complete intended-live snapshot, the verifier sees a complete world, and `./deploy --releases` (rsync `--delete-after`) replaces live state cleanly. - **Lockstep releases.** Every release cut bumps all nine artifacts (8 HTML tools + zddc-server) to the same version, even if a tool didn't change. The coordinated next-stable target is `max(latest tag across all tools) + 1`. Per-tool independent versions are no longer the norm — `./build release` is the canonical path. Workflow: alpha = active dev, beta = ready for general testing, stable = ready to ship. Stable cuts atomically (1) regenerate `zddc/internal/apps/embedded/` with stable-labeled bytes, (2) make a `release: vX.Y.Z lockstep` commit, (3) tag all nine artifacts at that commit. Tags ALWAYS point at a clean release commit — never at a source-side commit with alpha-dirty embedded files. (Fixed in May 2026; see git log around the v0.0.9 re-anchor.) - **Bake-in invariant.** What zddc-server's binary embeds via `//go:embed`: prod images (built from `ZDDC_REF=stable`) ship the latest stable cut's bytes. Dev images (built from `ZDDC_REF=main`) ship whatever the last beta-or-stable cut wrote — no alpha. **Alpha is never baked in.** Active dev iteration uses `tool/dist/.html` opened directly, not the binary's embedded copy. The `./build` (no arg) and `./build alpha` paths intentionally leave `embedded/` untouched. - **Release artifact layout** (in `dist/release-output/`, mirrored to `/srv/zddc/releases/`). HTML tools: per-version `_v.html` (real immutable files) + partial-version pins (`_v.html`, `_v.html`) + channel mirrors (`_{stable,beta,alpha}.html`) — all symlinks except per-version. zddc-server: `zddc-server_v_` per-version binaries (raw bytes, no LFS), `_v_` / `_v_` / `__` symlinks, plus `zddc-server_.html` stub pages that surface the four platform downloads in one matrix-cell link. Same cascade rule for both: stable cut → beta + alpha both reset to stable; beta cut → alpha cascades to beta. - **No tags for alpha/beta.** Channel URLs are stable URLs by design — appending counter tags would defeat the purpose. The on-page label encodes ` · ` for traceability. Stable cuts get clean `-vX.Y.Z` tags for every tool (nine tags per cut, all sharing the same X.Y.Z). - **Pre-release semver in the on-page label.** Plain dev builds and `--release alpha|beta` cuts embed `vX.Y.Z-{alpha,beta}` in `{{BUILD_LABEL}}` where X.Y.Z is the next-stable target. Plain dev adds a full timestamp + `-dirty` marker; `--release alpha|beta` is date-only. - **Channel-link verifier.** Every `./build alpha|beta|release` ends with a check that every `_{stable,beta,alpha}.html` (and zddc-server's per-platform binary mirrors + stub pages) resolves. Because cuts seed from live state, the verifier always sees a complete world; missing-link errors mean a real problem, not a sparse-bundle artifact. - **`./build` (no arg) is a source-side dev build.** Assembles `tool/dist/` + cross-compiled binaries; does NOT touch `dist/release-output/` or the live site. Use it to iterate without affecting anything. To produce a deployable bundle, run `./build alpha|beta|release`. To publish, run `./deploy`. Nothing is pushed to Codeberg automatically. - **Always build before running tests** — Playwright opens `dist/tool.html` via `file://`. - **``** embedding. `shared/build-lib.sh` provides `escape_js_close_tags`; every tool's `build.sh` runs JS through it before inlining. - **All ZDDC parsing/formatting/hashing goes through `window.zddc`** (from `shared/zddc.js` + `shared/hash.js` + `shared/zddc-filter.js`). API: `parseFilename`, `parseFolder`, `parseRevision`, `formatFilename`, `formatFolder`, `compareRevisions`, `isValidStatus`, `splitExtension`, `joinExtension`, `crypto.{sha256Hex, sha256String, sha256File, bytesToHex}`, `filter.{parse, matches}`. File objects across tools use `trackingNumber` (string) and `extension` (string, **no leading dot** — use `zddc.joinExtension(name, ext)` to build a filename). Add edge cases to `tests/zddc.spec.js`, not per-tool tests. - **Two globals only**: `window.app` (per-tool app state + modules) and `window.zddc` (shared library). No others — anything that crosses tool boundaries goes through one of these. - **Worktrees live at `~/src/zddc-`.** Check `git worktree list` before starting a feature branch; never `git checkout`/`switch` inside a worktree another agent might be using. - **Build scripts are POSIX `sh` with `set -eu`**, not bash. `concat_files` takes positional args only.