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.
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 findingnpm run preflight:bake— audit + write the lock files topublic/_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:
| # | Check | What it catches |
|---|---|---|
| 1 | Route hygiene | Every apiXxx() definition vs. every route registration. Flags orphan handlers (defined, never routed) and dangling routes (routed, never defined). |
| 2 | Dead exports | Top-level functions, window.helmXxx handlers, window.* assignments that no other file references. |
| 3 | Schema drift | Every 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). |
| 4 | Shadow code | Files or symbols named *V2, *New, *Old, *Legacy, *Deprecated, *_backup, *_v1. |
| 5 | Commented-out code | 3+ consecutive // lines whose bodies look like code (a ;, =>, {}, function, const, …). The fastest-rotting kind of comment. |
| 6 | File bloat | Files 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
< 2times inpublic/index.html(i.e. only the definition site references it) - The assignment line + the 3 preceding lines contain no
@public-apimarker - 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:
-
QA dropdown → "Code bloat + regression check" — opens a printable modal showing the full
_preflight.jsonsummary: route hygiene, dead exports, schema drift, shadow code, commented-out code, file bloat. Useful for a Friday "did anything rot this week?" scan. -
User menu → "📝 Last sweep" — opens a modal showing the most recent autofix's
removed[]andskipped[]. 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
- Code style — what preflight enforces structurally
- Slice development pattern — preflight runs after every slice ships
- ADR-0027: Local preflight over remote operator login — why the migrations-039-then-040 saga ended here