Skip to main content

Preflight

A pre-deploy audit that runs every time npm run deploy triggers predeploy. Catches the six categories of rot that accumulate fastest in a fast-moving codebase, autofixes the safest of them, and writes both the snapshot and the change-log to disk so the operator can see what shipped + why.

Live 2026-05-18/19 — replaces the planned multi-shop Kvick operator login

The original spec (migration 039) called for a remote kvick_operators identity layer so Tom could sign into any shop as Kvick. After review, the actual need is local code hygiene before every deploy, not a remote auth surface. The identity tables were dropped in migration 040 and the toolchain on this page is the replacement. See ADR-0027.

What runs and when

npm run deploy

predeploy:
1. scripts/build_info.js — refresh build_info.json (existing)
2. scripts/preflight-autofix.js — opinionated SAFE deletions
3. scripts/preflight.js --bake — full audit; writes _preflight.json

wrangler deploy

Plus standalone targets:

  • npm run preflight — non-mutating audit, exit 0 always (won't gate)
  • npm run preflight:strict — same audit, exit non-zero on any non-INFO finding
  • npm run preflight:bake — audit + write the lock files to public/_preflight.json

The six checks

scripts/preflight.js walks ./src, ./public, ./migrations (cwd-relative; vertical-agnostic by design — drop into any KVICK.* repo and it works) and runs:

#CheckWhat it catches
1Route hygieneEvery apiXxx() definition vs. every route registration. Flags orphan handlers (defined, never routed) and dangling routes (routed, never defined).
2Dead exportsTop-level functions, window.helmXxx handlers, window.* assignments that no other file references.
3Schema driftEvery CREATE TABLE / ALTER TABLE ADD COLUMN in migrations/ vs. every table + column name actually mentioned in Worker source. Catches dead schema (migrations nobody queries) AND broken code (queries against unmigrated tables/columns).
4Shadow codeFiles or symbols named *V2, *New, *Old, *Legacy, *Deprecated, *_backup, *_v1.
5Commented-out code3+ consecutive // lines whose bodies look like code (a ;, =>, {}, function, const, …). The fastest-rotting kind of comment.
6File bloatFiles past configurable size / line-count thresholds. Top-10 fattest reported.

Exit code is 0 by default so it never blocks a deploy without consent. --strict flips that.

The autofix

scripts/preflight-autofix.js is the opinionated half. It runs before the audit so the audit's _preflight.json reflects the post-sweep state.

What it will auto-delete:

Dead window.* exports

A window.helmFoo = ... or window.kvickFoo = ... line gets removed only if all of:

  • The bare identifier appears < 2 times in public/index.html (i.e. only the definition site references it)
  • The assignment line + the 3 preceding lines contain no @public-api marker
  • The line isn't part of a destructuring or a complex expression (we match a single trailing ; pattern only)

The single preceding // comment line that references the same identifier is also removed so we don't leave an orphan comment.

Every removal writes a row to public/_preflight_changes.json with the file, line, kind, name, and reason. Every skipped candidate writes a row too — so when an item shows up in the dead-exports list but isn't auto-removed, the skip reason ("marked @public-api," "referenced 2+ times," …) is recorded.

@public-api markers

A magic comment that opts an export out of the autofix. Three forms:

// @public-api — used by the Beta Feedback popover
window.helmShowBetaComments = function() { ... };

// @public-api: surfaced as the in-product feature-list modal trigger
function helmBuildSettingRow(...) { ... }

/* @public-api */
window.helmConfirm = function(opts) { ... };

Anywhere in the line itself or in the 3 preceding lines. The marker doesn't change behaviour — it's documentation that this export is intentionally kept even if static analysis can't see a caller. Common cases: HTML onclick attributes, dynamic eval paths, dev-tools entry points.

Lock files

Both scripts write to public/ (so the operator app can fetch them at runtime):

  • _preflight.json — the full audit snapshot. Generated timestamp, files scanned, all 6 check results with per-finding detail. Read by the QA dropdown in the operator app's user menu.
  • _preflight_changes.json — what the autofix did this round. removed[] + skipped[]. Read by the in-product "📝 Last sweep" modal.

Both files are checked in (not gitignored). The autofix is deterministic, so they regenerate identically each deploy — diffs only show real movement.

Surfaces in the operator app

Two read-only surfaces consume the lock files:

  1. QA dropdown → "Code bloat + regression check" — opens a printable modal showing the full _preflight.json summary: route hygiene, dead exports, schema drift, shadow code, commented-out code, file bloat. Useful for a Friday "did anything rot this week?" scan.

  2. User menu → "📝 Last sweep" — opens a modal showing the most recent autofix's removed[] and skipped[]. The operator can see exactly what got swept the last deploy, and why each skipped item was preserved.

What this is not

  • Not a linter. ESLint runs separately. Preflight is about cross-file / cross-system invariants (routes, schema, exports) that single-file linting can't see.
  • Not a security scan. Doesn't audit auth, secrets, or input handling. That's a separate concern.
  • Not a guard against intentional deletions. If a real route or schema mention gets removed deliberately, preflight will note it as a drift; that's a signal, not a failure.

See also