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>
18 KiB
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 told —
git pushrequires 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.
mainhistory 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.comin examples; the only real address allowed is the maintainer contactcaseywitt@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 notesARCHITECTURE.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 owndist/. Most outputdist/tool.html;landing/outputsdist/index.html(it's the project picker served at the root ofzddc-server).form/is the schema-driven renderer for the form-data system (any<name>.form.yamlfile 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 dedicatedmdedit/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 underZDDC_ROOT, applies.zddcACL 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, orX-Auth-Request-Emailinjected 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--rootbecomes 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-upstreamrecords provenance.--no-authskips ACL enforcement entirely on this instance (distinct from--insecurewhich only relaxes the no-root-.zddcstartup check);--skip-tls-verifyis a separate flag for self-signed upstream certs. Cross-compiled binaries are produced by./buildand live indist/release-output/(gitignored);./deployrsyncs them to/srv/zddc/releases/on the deploy host (Caddy serves them athttps://zddc.varasys.io/releases/). Thehelm/charts in this repo build from source at deploy time.shared/— CSS (base.css,fonts.css+ base64-inlined woff2 underfonts/,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'sbuild.shconcatenates the subset it needs. Alsobuild-lib.sh(POSIX sh helpers sourced by every tool'sbuild.shAND by the top-levelbuildfor 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/— justindex.html,reference.html,css/,js/,img/; no releases, no LFS). The live site atzddc.varasys.iois served from/srv/zddc/on the deploy host: Caddy bind-mounts that path, and it's populated by./deployfrom this repo'sdist/release-output/plus~/src/zddc-website/. Releases are NOT in any git history — they're reproducible from this repo's<tool>-vX.Y.Ztags 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 analogouszddc-server_v<X.Y.Z>_<platform>per-version binaries plus channel/partial-version symlinks pluszddc-server_<X>.htmlstub pages that fan out the four-platform download in one cell. Install model: local use is a download from/releases/. Server use iszddc-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.zddccascade, not hardcoded folder names — a baked-in default tree (export it as a.zddc.zip:zddc-server show-defaults) declares, via a recursivepaths:tree, per-folderdefault_tool(served at<dir>—archiveatarchive/,transmittalatarchive/<party>/staging/,browseatarchive/<party>/{working,reviewing}/(browse hosts the markdown editor),classifieratarchive/<party>/incoming/,tablesatarchive/<party>/{mdl,rsk}and at the project-levelssr/mdl/rskvirtual rollups,landingat root),dir_tool(served at<dir>/; defaults tobrowse),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$partysource-party column) andworking/staging/reviewing(browse folder-nav listings of parties with non-empty content; per-party clicks 302-redirect toarchive/<party>/<slot>/). Mkdir at project root is restricted toarchive+_/.-prefixed system names; the six virtual names are rejected with 409. A.zipfile is also a navigable directory (GET …/Foo.zip/→ member listing;…/Foo.zip/m.pdf→ that member);GET /dir/?zip=1streams an ACL-filtered zip of a subtree. Override the tool source by dropping a real<app>.htmlat the path or adding an<app>.htmlmember to a.zddc.zip(resolution: on-disk file →.zddc.zipmember → embedded; no fetch, noapps:key — removed). See AGENTS.md "URL handling"/"Install model" and ARCHITECTURE.md "Canonical folders, URL routing & the.zddccascade". 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 copyvalues.yaml.exampleand 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.jsvalidatestransmittal.schema.jsonagainst canonical fixtures viaajv(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>.htmlis the canonical built artifact for testing and the source for./build releasewrites.dist/release-output/is the local-only release bundle. Never hand-edit adist/file.- Build vs deploy are separate verbs.
./buildand./build releaseproduce artifacts underdist/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 releaseclearsdist/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.sigsidecars 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 releaseis the canonical path. Stable cuts atomically (1) regeneratezddc/internal/apps/embedded/with stable-labeled bytes, (2) make arelease: vX.Y.Z lockstepcommit, (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 fetchesappVersion, 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) leavesembedded/untouched — local dev iteration usestool/dist/<tool>.htmlopened 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.sigpairing. Plus a singlezddc-server.htmlstub 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.
- HTML tools:
- 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: cleanv<X.Y.Z>. ./build(no arg) is a source-side dev build. Assemblestool/dist/+ cross-compiled binaries; does NOT touchdist/release-output/, embedded files, or the live site. Use it to iterate without affecting anything../build betaadds the embedded regen + chore commit (BMC dev chart consumes the SHA via appVersion)../build releaseproduces the deployable bundle../deploypublishes. Nothing is pushed to Codeberg automatically.- Always build before running tests — Playwright opens
dist/tool.htmlviafile://. </in JS string/template literals breaks inline<script>embedding.shared/build-lib.shprovidesescape_js_close_tags; every tool'sbuild.shruns JS through it before inlining.- All ZDDC parsing/formatting/hashing goes through
window.zddc(fromshared/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 usetrackingNumber(string) andextension(string, no leading dot — usezddc.joinExtension(name, ext)to build a filename). Add edge cases totests/zddc.spec.js, not per-tool tests. - Two globals only:
window.app(per-tool app state + modules) andwindow.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 theaverb 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) grantsVerbAon that basis above the WORM clamp (config isn't WORM-protected data, andVerbAonly 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.zddcreads 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 Elevatedis the single bypass site in the decider;IsAdmin/IsSubtreeAdminstay elevation-gated (they guard the overrides). Carried in thezddc-elevate=1session cookie (no Max-Age; cleared onpagehide, so admin mode is per-page), armed by the on-page toggle every tool renders bottom-right only forcan_elevateusers, by?admin=true|false, or implicitly for bearer tokens.shared/elevation.jsapplies it in place + emitszddc:elevationchange(browse re-fetches verbs);handler.ACLMiddlewarebuildszddc.Principal{Email, Elevated}per request./.profile/accessexposescan_elevate; the access-log captureselevatedper request. - Secrets stay locked:
.zddc.d/(bearer tokens, access logs) is reserved regardless of read-ACL. The.zddc.zipbundle is visible+editable to config-editors of its directory (not wide-read — one file packs many subtrees' policy).
- Config-edit is STANDING (no toggle). A subtree admin (named in any
- Worktrees live at
~/src/zddc-<branch>. Checkgit worktree listbefore starting a feature branch; nevergit checkout/switchinside a worktree another agent might be using. - Build scripts are POSIX
shwithset -eu, not bash.concat_filestakes positional args only.