ZDDC/CLAUDE.md
ZDDC fd11278417 docs: pre-push PII guard + scrub conventions
main history was rewritten once to scrub a leaked work email. Document a
pre-push email guard (grep with a synthetic-domain allowlist; empty = clean) in
AGENTS.md and reference it from CLAUDE.md, plus the post-scrub conventions: no
real personal/work emails (use @example.com), the only real address allowed is
the maintainer contact caseywitt@proton.me, generic personas (admin/alice/sam),
party name Acme. Never push pre-scrub history or stale tags.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:40:56 -05:00

18 KiB

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 toldgit 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.
  • Pre-push PII guard — run before EVERY push. main history was rewritten once to scrub a leaked work email; a leak persists in history + tags, not just files. Before any push, run the guard in AGENTS.md ("Pre-push PII guard"). No real personal/work emails: use @example.com in examples; the only real address allowed is the maintainer contact caseywitt@proton.me (SECURITY.md + as commit author). Generic personas only (admin / alice / sam); party name Acme. Never push stale local tags, and never push a branch still carrying pre-scrub history.

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/, landing/, form/, tables/, browse/ — seven 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 <name>.form.yaml file in the tree becomes an editable form at <path>/<name>.form.html); tables/ is its read/aggregate counterpart, rendering a directory of YAML rows as a sortable table; browse/ is the file-tree navigator and also hosts the in-place markdown editor (browse/js/preview-markdown.js — Toast UI Editor + YAML front-matter pane + on-demand server-side MD→DOCX/HTML/PDF download buttons). A dedicated mdedit/ tool used to live alongside these but has been retired. 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 <token> validated against self-issued tokens at <ZDDC_ROOT>/.zddc.d/tokens/<sha256-hex> 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 <url> 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/ — CSS (base.css, fonts.css + base64-inlined woff2 under fonts/, nav.css, logo.css, toast.css) plus shared JS modules (zddc.js, hash.js, zddc-filter.js, zddc-source.js, zip-source.js, theme.js, toast.js, nav.js, logo.js, help.js, preview-lib.js) and vendored libs (vendor/: jszip, xlsx, utif, docx-preview, toastui-editor) — each tool's build.sh concatenates the subset it needs. Also build-lib.sh (POSIX sh helpers sourced by every tool's build.sh AND by the top-level build for lockstep release helpers). See AGENTS.md "Shared modules" for the full inventory.
  • 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 <tool>-vX.Y.Z tags by checking out the tag and running ./build release X.Y.Z. Per-version files (<tool>_v<X.Y.Z>.html) are immutable; partial-version pins (<tool>_v<X.Y>.html, <tool>_v<X>.html) and channel mirrors (<tool>_{stable,beta,alpha}.html) are symlinks; zddc-server has analogous zddc-server_v<X.Y.Z>_<platform> per-version binaries plus channel/partial-version symlinks plus zddc-server_<X>.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 seven HTML tools baked in via //go:embed (compile-time default). Which tool a directory URL serves is driven by the .zddc cascade, not hardcoded folder names — a baked-in default tree (export it as a .zddc.zip: zddc-server show-defaults) declares, via a recursive paths: tree, per-folder default_tool (served at <dir>archive at archive/, transmittal at archive/<party>/staging/, browse at archive/<party>/{working,reviewing}/ (browse hosts the markdown editor), classifier at archive/<party>/incoming/, tables at archive/<party>/{mdl,rsk} and at the project-level ssr/mdl/rsk virtual rollups, landing at root), dir_tool (served at <dir>/; defaults to browse), available_tools, plus the canonical-folder behaviour keys (auto_own, worm:, virtual, drop_target); operators override at any level. Project shape (May 2026 reshape): archive/ is the only physical project-root directory. Six top-level URLs are virtual aggregators: ssr/mdl/rsk (tables rollups across parties with a synthesized $party source-party column) and working/staging/reviewing (browse folder-nav listings of parties with non-empty content; per-party clicks 302-redirect to archive/<party>/<slot>/). Mkdir at project root is restricted to archive + _/.-prefixed system names; the six virtual names are rejected with 409. A .zip file is also a navigable directory (GET …/Foo.zip/ → member listing; …/Foo.zip/m.pdf → that member); GET /dir/?zip=1 streams an ACL-filtered zip of a subtree. Override the tool source by dropping a real <app>.html at the path or adding an <app>.html member to a .zddc.zip (resolution: on-disk file → .zddc.zip member → embedded; no fetch, no apps: key — removed). See AGENTS.md "URL handling"/"Install model" and ARCHITECTURE.md "Canonical folders, URL routing & the .zddc cascade".
  • 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

# Source-side dev build only — assembles tool/dist/ + cross-compiles
# zddc-server. Does NOT touch dist/release-output/ or the live site.
./build

# ./build beta — internal SHA snapshot for the BMC dev chart pipeline.
# Regenerates zddc/internal/apps/embedded/* and makes a
# `chore(embedded): cut v<X.Y.Z>-beta` commit. NO public artifacts.
# The chart's appVersion pins to "<X.Y.Z>-beta-<sha>"; its Dockerfile
# parses the suffix and fetches that SHA from git.
./build beta
#
# ./build release — coordinated stable cut. Regenerates embedded/,
# makes a release commit, tags all 8 artifacts, writes per-tool
# <tool>_v<X.Y.Z>.html + <tool>.html canonical symlink, and zddc-server
# per-platform binaries + canonical symlinks into dist/release-output/.
# Bundle seeded from /srv/zddc/releases/ so prior immutable per-version
# artifacts survive.
./build release                      # coordinated next-stable version
./build release X.Y.Z                # explicit stable 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 [X.Y.Z]   # single-tool stable cut (rare; prefer ./build release)

npm test                             # all Playwright specs (build first!)
npx playwright test <tool>           # one spec
./dev-server start  # stop           # cache-busting HTTP on :8000

# zddc/ Go server (sub-project). Go is NOT on the host — run go test/build
# through the localhost/zddc-go:1.24 container (canonical wrapper, with the
# GOPROXY/GOPRIVATE env it needs, in AGENTS.md § Test). The bare command
# below fails on the host.
(cd zddc && go test ./...)           # unit tests (Go 1.24+) — via the podman wrapper, not host

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/<tool>.html is the canonical built artifact for testing and the source for ./build release writes. dist/release-output/ is the local-only release bundle. Never hand-edit a dist/ file.
  • Build vs deploy are separate verbs. ./build and ./build 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.
  • Stable cuts seed from live state. Before running per-tool promote, ./build release clears dist/release-output/ and copies only the per-version immutable files (<tool>_v<X.Y.Z>.html, zddc-server_v<X.Y.Z>_<plat>) plus their .sig sidecars from /srv/zddc/releases/. The cut writes this version's per-version files + refreshes the canonical <tool>.html / zddc-server_<plat> symlinks on top. ./deploy --releases (rsync --delete-after) cleanses any stale files in the live tree that this cut didn't include.
  • Lockstep releases. Every release cut bumps all 8 artifacts (7 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 gone — ./build release is the canonical path. 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 8 artifacts at that commit. Tags always point at a clean release commit. (Anchor fix 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 (chart's Dockerfile.prod fetches the latest stable tag) ship that cut's bytes. Dev images (chart's Dockerfile fetches appVersion, which is either a stable tag or a <X.Y.Z>-beta-<sha> snapshot SHA) ship the bytes that ref carries. Plain ./build (no arg) leaves embedded/ untouched — local dev iteration uses tool/dist/<tool>.html opened directly, not the baked binary copy.
  • Release artifact layout (in dist/release-output/, mirrored to /srv/zddc/releases/):
    • HTML tools: <tool>_v<X.Y.Z>.html (real immutable file) + <tool>.html (symlink → current stable's per-version file). Each carries a sibling .sig (real for per-version, symlink for canonical).
    • zddc-server: zddc-server_v<X.Y.Z>_<platform> (real immutable binary, no LFS) + zddc-server_<platform> (symlink → current stable's per-version binary). Same .sig pairing. Plus a single zddc-server.html stub page that surfaces the four-platform downloads of the current stable.
    • No channel mirrors (_alpha, _beta, _stable), no partial-version pins (_v<X.Y>, _v<X>). Dropped in the May 2026 simplification.
  • On-page build label. Plain dev builds: v<X.Y.Z>-dev · <full-ts> · <sha>[-dirty] (red), where X.Y.Z is the next-stable target. ./build beta: v<X.Y.Z>-beta · <full-ts> · <sha> (red) — only seen on the dev chart's compiled binary. Stable cuts: clean v<X.Y.Z>.
  • ./build (no arg) is a source-side dev build. Assembles tool/dist/ + cross-compiled binaries; does NOT touch dist/release-output/, embedded files, or the live site. Use it to iterate without affecting anything. ./build beta adds the embedded regen + chore commit (BMC dev chart consumes the SHA via appVersion). ./build release produces the deployable bundle. ./deploy publishes. Nothing is pushed to Codeberg automatically.
  • Always build before running tests — Playwright opens dist/tool.html via file://.
  • </ in JS string/template literals breaks inline <script> 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.
  • Admin authority is layered: standing config-edit + additive sudo overrides. Two distinct things — don't conflate them:
    • Config-edit is STANDING (no toggle). A subtree admin (named in any admins: on the cascade) or anyone holding the a verb may read and edit the .zddc / .zddc.zip / role definitions of subtrees they administer without elevating — zddc.IsConfigEditor(chain, email). The decider (policy.InternalDecider.Allow) grants VerbA on that basis above the WORM clamp (config isn't WORM-protected data, and VerbA only ever authorises config mutation, never write/delete of records). "Admin of X = owns X's policy," bounded to that subtree (authority cascades down only, never up). Plain .zddc reads are governed by directory read-ACL (ServeZddcFile), so config is transparent to anyone who can read the path.
    • Elevation is the sudo escape hatch — purely ADDITIVE. It only unlocks "things you otherwise couldn't do": WORM bypass, recursive directory delete, rearranging records, acting outside your admin scope. IsActiveAdmin = (admin authority on the chain) AND Elevated is the single bypass site in the decider; IsAdmin/IsSubtreeAdmin stay elevation-gated (they guard the overrides). Carried in the zddc-elevate=1 session cookie (no Max-Age; cleared on pagehide, so admin mode is per-page), armed by the on-page toggle every tool renders bottom-right only for can_elevate users, by ?admin=true|false, or implicitly for bearer tokens. shared/elevation.js applies it in place + emits zddc:elevationchange (browse re-fetches verbs); handler.ACLMiddleware builds zddc.Principal{Email, Elevated} per request. /.profile/access exposes can_elevate; the access-log captures elevated per request.
    • Secrets stay locked: .zddc.d/ (bearer tokens, access logs) is reserved regardless of read-ACL. The .zddc.zip bundle is visible+editable to config-editors of its directory (not wide-read — one file packs many subtrees' policy).
  • Worktrees live at ~/src/zddc-<branch>. 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.