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

19 KiB

AGENTS.md — ZDDC

Commands

# 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:

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:

  • 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.

./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

# 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)

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:

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.