ZDDC/AGENTS.md
ZDDC 7365e94cac docs: align with simplified release model
Updates to all six top-level docs to describe the new flow:

- Storage: HTML tools live in website/releases/ as committed static
  files. Per-version files are real bytes; partial-version pins and
  channel mirrors are checked-in symlinks. No manifest.json, no Codeberg
  indirection, no Caddy regex-rewrite.
- URL scheme: <tool>_v<X.Y.Z>.html (exact), <tool>_v<X.Y>.html (latest
  patch), <tool>_v<X>.html (latest minor), <tool>_<channel>.html
  (channel mirror). All resolve via the symlink chain.
- Cascade rule: stable cut → beta + alpha symlinks reset to stable;
  beta cut → alpha resets to beta. Channels are never stale.
- No -alpha.N / -beta.N counter tags. Channel URLs are stable URLs by
  design; counters defeat that. The on-page <date> · <sha> label is
  enough for traceability.
- bootstrap/install.sh is the canonical install path. The four hand-
  rolled snippets are gone; one script handles all three deployment
  patterns + both target shapes.
- Helm charts under helm/ (zddc-server-{prod,dev}/) build from source
  via init container; documented as the recommended k8s deployment
  path.
- zddc-server now publishes binaries on stable cuts only — no alpha/
  beta channel for binaries. Active dev runs through the dev helm chart
  which builds from source on each rollout.

Files updated:

- CLAUDE.md — Repo shape, Most-used commands, Things that bite if you
  forget. Drops mentions of manifest.json, the Codeberg-as-canonical
  model, and -alpha.N/-beta.N tags.
- AGENTS.md — website/ tree, Releasing — channels and layout, Channel
  discipline rules (renumbered to add coordinated minor/major bump
  rule), Freshen helper, Bootstrap stubs, zddc-server Release tagging.
- ARCHITECTURE.md — website/ tree, build.sh step 5, Channels section,
  level-2 bootstrap description.
- README.md — tool publishing description, link to helm/.
- bootstrap/README.md — install path is install.sh now; pin URL table
  uses static symlinks; CORS check uses release-asset URLs (not
  manifest.json).
- zddc/README.md — Quick Start uses Codeberg URLs directly (no proxy);
  Release tagging is stable-only; Distribution / Versioning sections
  rewritten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:56:34 -05:00

24 KiB

AGENTS.md — ZDDC

Commands

# Build all tools (writes to dist/ only; also regenerates website/releases/index.html + bootstrap stubs)
sh build.sh

# Build single tool
sh tool/build.sh              # archive | transmittal | classifier | mdedit | landing

# Cut a stable release (auto-increments patch version, writes website/releases/<tool>_v<X.Y.Z>.html, refreshes 5 symlinks, tags <tool>-v<X.Y.Z>)
sh tool/build.sh --release
sh tool/build.sh --release 1.2.0      # explicit version

# Cut an alpha/beta channel build (overwrites website/releases/<tool>_<channel>.html in place; on a beta cut, alpha cascades to a symlink → beta. 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                          hand-edited intro page (root URL)
  install.sh → ../bootstrap/install.sh   symlink so the upstream serves /install.sh
  releases/
    index.html                        versions index, regenerated by build.sh from filesystem scan
    <tool>_v<X.Y.Z>.html              real per-version files (committed, immutable)
    <tool>_v<X.Y>.html  -> ...        symlink: latest patch within X.Y.*
    <tool>_v<X>.html    -> ...        symlink: latest within X.*.*
    <tool>_stable.html  -> ...        symlink: current stable
    <tool>_beta.html    -> ...        symlink to stable (or real bytes if active beta)
    <tool>_alpha.html   -> ...        symlink to beta/stable (or real bytes if active alpha)
  bootstrap/
    level1/<tool>.html                same-origin level-1 stubs (4 tools, no landing)
    track-stable/<tool>.html          level-2 stubs that track the current-stable channel
    track-alpha/<tool>.html           level-2 stubs that track the alpha channel
    track-beta/<tool>.html            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
  install.sh         unified bootstrap script (modes: copy / track; channels: stable/beta/alpha/<version>)
  README.md          install / channel / pin docs

helm/
  zddc-server-prod/  production-shaped Helm chart (compiles from source via init container)
  zddc-server-dev/   dev-shaped variant (tracks main HEAD; debug-level logging; faster probes)
  README.md          chart design rationale + quick-start

Critical: dist/ files are gitignored. They're the canonical built artifact for testing and the source for --release writes into website/releases/, but they aren't checked in. Never edit them directly.

website/releases/ IS committed — per-version files as real bytes, partial-version pins (_v<X.Y>, _v<X>) and channel mirrors (_stable, _beta, _alpha) as symlinks. The build script (shared/build-lib.sh promote_release) maintains the symlink chain on each release. zddc-server binaries are NOT in this repo — they're attached as Codeberg release assets to clean zddc-server-vX.Y.Z tags by zddc/release.sh.

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:

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.

(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. Versioning is per-tool semver: stable owns clean vX.Y.Z; alpha and beta are mutable channel mirrors that get overwritten in place (no counter tags — channel URLs are stable URLs by design). The next-stable target X.Y.Z used in alpha/beta on-page labels is patch-bumped from the latest clean <tool>-vX.Y.Z tag.

Storage model. All HTML tool artifacts live in this repo under website/releases/. Per-version files (<tool>_v<X.Y.Z>.html) are real, immutable, committed bytes; partial-version pins (<tool>_v<X.Y>.html, <tool>_v<X>.html) and channel mirrors (<tool>_<channel>.html) are checked-in symlinks pointing at the appropriate concrete file. No manifest.json, no Codeberg indirection, no Caddy proxy magic. Caddy serves these as plain static files.

zddc-server binaries are a separate concern — they ship as Codeberg release assets attached to clean zddc-server-vX.Y.Z tags by zddc/release.sh. zddc-server has no alpha/beta channel for binaries; the helm charts under helm/ build from source.

shared/build-lib.sh promote_release is the single point of truth for HTML-tool releases:

  • Stable (sh tool/build.sh --release [version], or just --release to auto-bump patch): Writes website/releases/<tool>_v<X.Y.Z>.html (immutable real bytes), then refreshes 5 symlinks — <tool>_v<X.Y>.html, <tool>_v<X>.html, <tool>_stable.html, <tool>_beta.html, <tool>_alpha.html — all → the new versioned file. Tags <tool>-v<X.Y.Z>. Cascade rule: stable cut means beta and alpha both reset to stable (no active dev on either downstream channel). Skips silently if source has not changed since the latest stable tag.
  • Beta (sh tool/build.sh --release beta): Overwrites <tool>_beta.html with the dist HTML bytes (replacing the symlink with a real file if one was there). Cascade: <tool>_alpha.html<tool>_beta.html (symlink). No tag.
  • Alpha (sh tool/build.sh --release alpha): Overwrites <tool>_alpha.html with the dist HTML bytes. No tag, no other side-effects.
  • Plain dev builds (no --release): produce tool/dist/<tool>.html only. No website/releases/ side-effect, no commit. To publish, re-run with --release alpha.

On-page {{BUILD_LABEL}} format:

  • Plain dev: vX.Y.Z-alpha · <full-ts> · <sha>[-dirty] (red), where X.Y.Z is the next-stable target.
  • --release alpha: vX.Y.Z-alpha · <date> · <sha> (red).
  • --release beta: vX.Y.Z-beta · <date> · <sha> (red).
  • --release [version]: v<X.Y.Z> (black).

After cutting a stable release, git push origin <tag> to publish the tag. git push origin main publishes the new versioned file + updated symlinks.

No $CODEBERG_TOKEN is needed for HTML-tool releases — they never touch Codeberg. (zddc/release.sh for zddc-server stable cuts still requires it for binary uploads.)

website/index.html (the root URL of zddc.varasys.io) is hand-edited static content, not built by landing/build.sh. The landing tool's release file is website/releases/landing_v<X.Y.Z>.html; the unified install script (bootstrap/install.sh) curls the chosen channel's landing_<channel>.html to <deployment-root>/index.html at customer-deployment time.

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 a previously-published <tool>_v0.0.5.html in place. Stable per-version files are immutable.
  2. Coordinated minor/major bumps. When any tool needs a minor or major bump, all five tools cut at the same time even if patches differ. Per-tool patches stay independent. This is a release-time process rule, not enforced by tooling.
  3. 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 choice; they can move forward when they want.
  4. Alpha and beta are mutable. Document this anywhere you invite users to test them. Pinning a deployment to a channel mirror means it gets rebuilt without notice. For something stable, pin to a per-version URL (?v=0.0.5 or the <tool>_v0.0.5.html mirror).
  5. Cascade is automatic. A stable cut resets beta and alpha symlinks → stable. A beta cut resets alpha → beta. So "no active beta" silently shows current stable and "no active alpha" silently shows current beta. Operators don't need to run a freshen step after a stable release; the cascade handles it.
  6. Hotfix path. For critical bugs: fix on main, cut a new stable. Tag the commit message fix: or include "hotfix" so intent is visible in git log.
  7. 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 — useful when you want a channel to advance to current stable code without doing active dev on it (e.g. after upstream dependency changes). Most of the time you don't need it: the cascade rule (rule 5 above) means a stable cut already resets the downstream channel symlinks. Use this when you specifically want a fresh build with a new on-page label timestamp instead of a symlink.

./freshen-channel archive alpha
./freshen-channel transmittal beta

What it does:

  1. Finds the latest <tool>-v* clean stable 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, which overwrites <tool>_<channel>.html with the freshly-built bytes. (Note: this is in the worktree, not on main — you'll need to commit the resulting changes back to main afterward.)
  4. Removes the worktree.

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 stubs

build.sh regenerates website/bootstrap/ on every invocation:

  • bootstrap/level1/<tool>.html — 4 same-origin level-1 stubs (archive, transmittal, classifier, mdedit; landing has no level-1 stub since it only lives at deployment root).
  • bootstrap/track-{alpha,beta,stable}/<tool>.html — 5 level-2 stubs per channel that fetch <source>/releases/<tool>_<channel>.html directly. Static-file resolution end-to-end via the symlink chain — no JS indirection, no manifest lookup.

End users install via the unified bootstrap/install.sh script (also served at https://zddc.varasys.io/install.sh). The home page's "Install on your server" section prints one-liners that invoke the script with the appropriate --mode / --channel / --target flags.

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

zddc-server ships as a cross-compiled binary, not a container image. There's no Containerfile or compose file in this repo (the chart Dockerfiles in tnd-zddc-chart compile from source at deploy time, fetching the right tag from Codeberg).

# Compile a local binary for the host platform (requires Go 1.24+)
(cd zddc && go build -o zddc-server ./cmd/zddc-server)

# Or run directly without producing a binary
(cd zddc && go run ./cmd/zddc-server)

The repo's top-level sh build.sh cross-compiles the four release binaries (linux/amd64, darwin/amd64, darwin/arm64, windows/amd64) into zddc/dist/ when Go is on PATH. It's silently skipped otherwise — the HTML tools build regardless.

Run (development)

ZDDC_ROOT=/path/to/your/archive ZDDC_TLS_CERT=none ZDDC_ADDR=:8080 \
  go run ./cmd/zddc-server

For a release binary (downloaded from Codeberg or built via sh build.sh):

ZDDC_ROOT=/path/to/your/archive ZDDC_TLS_CERT=none ZDDC_ADDR=:8080 \
  ./zddc/dist/zddc-server-linux-amd64

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

zddc/release.sh is the canonical path. It tags the commit, cross-compiles binaries (native Go), and uploads them as Codeberg release assets. Stable cuts only — zddc-server has no alpha/beta channel for binary distribution. Active dev/soak happens via the helm/zddc-server-dev/ chart, which builds from source on every pod restart against any commit you point it at.

sh zddc/release.sh                       # patch-bump from latest clean stable tag
sh zddc/release.sh 0.1.0                 # explicit version

The script tags the commit but does NOT push — finish with git push origin main and git push origin <tag>.

Versioning — clean semver. Stable cuts get <tool>-vX.Y.Z tags; no -alpha.N / -beta.N counters. The historical zddc-server-v0.0.8-alpha.1 and -alpha.2 tags from the previous scheme stay as artifacts but no new alpha/beta tags get added.

Binary publishing — release.sh uploads the four cross-compiled binaries (zddc-server-{linux,darwin,windows}-{amd64,arm64}) as release assets attached to the new git tag on Codeberg. Operators download from https://codeberg.org/VARASYS/ZDDC/releases/download/zddc-server-vX.Y.Z/zddc-server-<platform> directly.

Prerequisites:

  • Go 1.24+ on PATH (or run from a Go container).
  • $CODEBERG_TOKEN exported, scoped to write the VARASYS/ZDDC repo.

There is no CI for this — solo workflow benefits from one canonical local 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+)
  • Portfolio files (*.portfolio) in the served tree appear as virtual group directories
  • Every folder exposes a .archive virtual directory backed by the same global index — the depth in the URL only matters so HTML produced for offline use can reach .archive/ via ../.archive/ relative links and have the browser resolve them before the request hits the server. The flat listing emits two redirect entries per tracking number: <tracking>.html (highest base rev) and <tracking>_<rev>.html (each specific base rev). Both redirect to the first chronologically received copy of the named revision. Modifier files (<tracking>_<rev>+C1.html etc.) remain reachable via the resolver but are not surfaced in the listing — they're return traffic, not primary documents. ACL is the only filter: the listing endpoint is gated by the contextPath's .zddc chain, and each entry is then filtered against the ACL of its resolved file's directory; per-target denials return 404 rather than 403 to avoid leaking that the tracking number exists in another subtree
  • 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 the _template/ directory created by the self-contained install snippet, still reachable by direct URL. Drop side-state under _ if it should be linkable; under . if it should be unreachable.