Skip to content

Centralize deployment-mode gating behind a capability definition#155

Open
CarsonDavis wants to merge 6 commits into
tests-both-modes-cifrom
lean/capability-gating
Open

Centralize deployment-mode gating behind a capability definition#155
CarsonDavis wants to merge 6 commits into
tests-both-modes-cifrom
lean/capability-gating

Conversation

@CarsonDavis

@CarsonDavis CarsonDavis commented Jun 17, 2026

Copy link
Copy Markdown

Closes #152.

What this does

Replaces the ~30 scattered isFull()/isLean() deployment-mode checks with one capability definition that every gate reads, so "what does lean turn off?" is answered in one place and adding a mode is a one-table edit. Pure restructuring — no behavior change in either mode.

  • One backend definition (API/Backend/Utils/capabilities.js): a capability → enabling mode(s) map resolved through one accessor; throws on an unknown capability so a typo fails at boot.
  • Whole modules gate at the discovery seam (API/setups.js): each gated setup.js declares capability: "<name>" and the seam decides. onceInit/onceStarted are gated; onceSynced runs unconditionally — models register at require-time and sync in every mode (ADR D2: keep, env-gated), so every gated-off feature still creates its (then-unused) DB tables. Gating model registration per-mode would re-spread mode logic into every module for no benefit; deployments never switch modes, so this isn't about a migration-free flip.
  • Partial gates (Missions mount, utils raster endpoints, mmgis-stac creation, the WITH_* Configure flags, the adjacent-servers spawn/proxy, the upload S3-vs-disk storage fork) ask the definition by capability name.
  • localSidecars collapses the sidecar cluster; localMissions is its own capability; s3AssetUploads (lean-only) drives the upload storage fork.
  • updateTools reads each tool's declared capability (Draw declares "draw") instead of a hardcoded name.
  • The Configure twin (configure/src/core/capabilities.js) is extended and Panel/SaveBar/DeploymentsWatcher migrated off raw mode-string checks. The twin uses the same list shape as the backend (warns + hides on unknown, rather than throwing — a render-time gate fails safe).

After this PR, isFull()/isLean()/isLeanMode() are gone from the codebase entirely. deploymentMode.js is the one env read and now exports only MODE; capabilities.js is the single place that interprets it. The only thing left untouched is staticHandlers.js (a call's static disposition isn't a pure function of its capability).

Fixed during review — last mode checks removed

The first cut of this PR deliberately left two raw mode reads behind: the upload S3-vs-disk storage fork (uploadRouter.js, a direct isLean()) and a now-unused isFull() export. That leaves two ways to ask "what mode am I in" — exactly the scattering this refactor exists to remove — so it's finished here rather than deferred:

  • uploadRouter.js: isLean()enabled('s3AssetUploads'), with s3AssetUploads: ["lean"] added to the definition (uploads persist to the shared S3 asset bucket in lean, local disk in full — a lean-only thing, no other backends planned).
  • deploymentMode.js: isFull()/isLean() deleted; it exports only MODE and documents that capabilities.js is the sole interpreter. (isLeanMode on the Configure twin was already removed earlier in this PR.)
  • Tests updated to match: deploymentMode.spec asserts on MODE only; capabilities.spec covers the new lean-only capability; uploadRouterS3.spec busts the capabilities require-cache too (the router now reads mode through it).

Net: zero isFull/isLean/isLeanMode references remain in code (only the historical ADR planning docs under docs/adr/... still mention them, as a record of the original plan).

Also: one authority for the sidecar on/off question (runtime + CI)

A further review pass found three readers still answering "are the bundled sidecars on?" off the raw WITH_* env instead of enabled("localSidecars"). Folded in so the capability is the single authority for the runtime and env/CI answer (the build-time image question stays a separate axis):

  • connection.js: the mmgis-stac connection is gated on enabled("localSidecars") && WITH_STAC, mirroring setup.js/init-db.js. Full reduces to today's WITH_STAC check; lean never builds the catalog even if a stray WITH_STAC=true leaks in.
  • scripts/mode-env.js (new): emits the sidecar WITH_* disables implied by the mode (lean → all off, full → nothing), derived from capabilities.js. The CI lean leg's five hand-written echoes + paragraph note collapse to one node scripts/mode-env.js >> .env. This is a CI mechanism change (behavior-preserving for both legs), not pure restructuring — the env/bash world now reads the same definition the app does.
  • capabilities.js: removed the unused boolean-rule branch in coerce.
  • tests/unit/capabilitiesTwin.spec.js (new): asserts the backend and Configure capability maps agree on every shared capability in both modes (guards silent drift).
  • Fixed the two now-stale isFull() comments in the workflow.

Deferred — different axis, not folded in: the Dockerfile build-time ARG WITH_STAC (whether STAC is baked into the image, vs. whether the sidecar is on at runtime — a later rename, not this PR), and the configuration/env.js WITH_TITILER frontend injectable (already carries the correct value in lean via the env, so it's a purity gap, not a live bug; gating it on the capability is a frontend-consumer refactor of its own).

Verification

  • Unit suite: 676/676.
  • scripts/mode-env.js verified emitting the same env the old echo block did (lean → the five WITH_*=false; full → nothing), with the mode supplied only via .env the way CI invokes it.
  • Live, both modes (init-db boot → assert gated tables exist → present/absent route checks):
    • full: 6/6 table groups present; 8/8 present/absent (full-only features present, deployments absent).
    • lean: 6/6 table groups present (despite routes gated off); 8/8 inverse (full-only features absent, deployments present).
  • Independent review pass: behaviour-preserving, completeness full, seam invariant holds. Dead isLeanMode export removed and the Configure twin DRY'd as part of review.

Merge order

Stacked on #154 (base tests-both-modes-ci). Merge after the test chain: #150#153#154 → this. The both-modes coverage from #154 is what gates this refactor in CI.

Replace scattered isFull()/isLean() checks with a single capability
definition (API/Backend/Utils/capabilities.js) that every gate reads.

- Backend modules declare `capability`; the discovery seam (API/setups.js)
  decides what to wire. onceInit/onceStarted are gated; onceSynced runs
  unconditionally so gated-off tables still exist in both modes.
- Partial gates (Missions mount, utils routes, mmgis-stac, WITH_* flags,
  adjacent-servers spawn/proxy) ask the definition by capability name.
- localSidecars collapses the sidecar cluster; localMissions kept separate.
- updateTools reads each tool's declared capability (Draw declares "draw").
- Configure twin extended; Panel/SaveBar/DeploymentsWatcher migrated off
  raw mode-string checks.
- Upload storage fork left as a direct mode check; staticHandlers untouched.

Unit suite: 674/674 green.
The capability centralization left one production mode check behind — the
S3-vs-disk upload storage fork in uploadRouter.js — plus the now-unused
isFull() export. Leaving them means the codebase still has two ways to ask
"what mode am I in" (raw MODE predicates and the capability accessor), which
is exactly the scattering this refactor set out to remove.

Route the upload fork through the capability definition like everything else
and drop the helpers entirely:

- capabilities.js: add `s3AssetUploads: ["lean"]` (uploads persist to the
  shared S3 asset bucket in lean, local disk in full).
- uploadRouter.js: `isLean()` -> `enabled('s3AssetUploads')`.
- deploymentMode.js: remove isFull()/isLean(); it now exports only MODE and
  documents that capabilities.js is the single interpreter of the mode.
- tests: deploymentMode.spec asserts on MODE only; capabilities.spec covers
  the new lean-only capability; uploadRouterS3.spec busts the capabilities
  require-cache too (the router now reads mode through it); e2e comments name
  the capabilities instead of isFull()/isLean().

No behavior change in either mode. Unit suite: 674/674.
Three readers still answered "are the bundled sidecars on?" off the raw env
instead of the one capability definition. Bring the runtime + CI answers onto
enabled("localSidecars"); the build-time image question stays a separate axis.

- connection.js: gate the mmgis-stac connection on
  enabled("localSidecars") && WITH_STAC, mirroring setup.js/init-db.js. In full
  this reduces to today's WITH_STAC check; in lean the catalog is never built
  even if a stray WITH_STAC=true leaks in. (required after dotenv so MODE
  resolves from .env; no import cycle — capabilities -> deploymentMode only.)
- scripts/mode-env.js: emit the sidecar WITH_* disables implied by the mode
  (lean -> all off, full -> nothing), derived from capabilities.js so the
  env/bash world reads the same authority. The CI lean leg's five hand-written
  echoes + paragraph note collapse to one `node scripts/mode-env.js >> .env`.
  Loads dotenv before requiring capabilities (CI writes the mode into .env, not
  the process env), same order as init-db.js.
- capabilities.js: drop the unused boolean-rule branch in coerce (no FEATURES
  rule is a boolean; re-add with a test when one is).
- capabilitiesTwin.spec.js: assert the backend and Configure twin maps agree on
  every shared capability in both modes, guarding silent drift.
- Fix the two now-stale isFull() comments in the workflow.

Deferred (different axis, documented in the PR): the Dockerfile build-time
ARG WITH_STAC, and the configuration/env.js WITH_TITILER frontend injectable
(already carries the correct value in lean via the env; gating it means a
frontend-consumer refactor).

No behavior change in either mode. Unit suite: 676/676. mode-env.js verified
emitting the same env the echo block did, with the mode supplied only via .env.
…newline can't merge the first flag onto its last line
…ration

The seam comments justified unconditional model sync as 'so a later mode flip
needs no migration.' That reason doesn't hold: deployments never switch modes,
and sequelize.sync() is additive and runs every boot, so a missing table would
self-heal anyway. The real reason is ADR D2 (keep, env-gated): gate route mounts
only, leave models registered — a gated-off feature's tables are created but
unused in the mode that gates it, and per-mode-gating model registration would
re-scatter the mode logic this refactor removed. Comments only; no behavior
change.
…apability-gating

# Conflicts:
#	.github/workflows/playwright-tests.yml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Centralize deployment-mode gating behind a single capability definition

1 participant