Surface Boundary — Core Loop vs. Extensions
Status: Living. Source of truth: apps/lantern-garage/lib/surface-registry.js. Enforced by: apps/lantern-garage/test/surface-boundary.test.js (npm run test:boundary).
Why this exists
The Σ₀ briefing and CLAUDE.md forbid architectural sprawl: "name the loop stage you improve, or don't add it." With 40-odd public HTML surfaces, that rule had no teeth — nothing declared which surfaces are the Observe → Remember → Reason → Act → Verify → Converge loop and which are optional capabilities sitting beside it. The result reads as undifferentiated sprawl even when much of it is legitimate.
This boundary draws the line explicitly so it is auditable and gateable instead of implicit. It does not delete anything — it classifies. The fix for sprawl is not amputation; it is a declared boundary plus a contract test that stops new sprawl from landing silently.
Grounded in the modular-monolith pattern: clear module boundaries prevent organic sprawl, enforced by contract tests that verify expectations before merge. Refs: modularmonoliths.com, Microsoft multi-agent reference architecture — Modular Monolith.
The rule
Every top-level public/*.html surface is exactly one of:
- CORE — directly serves one loop stage. It must name which stage (
Observe/Remember/Reason/Act/Verify/Converge). - EXTENSION — an optional capability beside the loop. It must name a
modulecluster, and may name an envflag(gated throughlib/feature-graph.js).
A surface that is neither fails the contract test. To add a surface you must classify it; to promote/demote one you edit the registry deliberately.
Classification alone only labels sprawl, so the contract test also enforces two rules that push back on it:
- BUDGET — the
extension : coreratio may not exceedMAX_EXTENSION_RATIO(currently 0.95). Adding an extension without adding core value trips the gate; the fix is to grow the core loop, or to raise the cap as a deliberate, reviewable one-line edit — never silent accretion. - GATEABLE — every extension must be switch-off-able (name an env
flag), except the always-on shell modules (account,meta) that must always render. This makes "optional capability beside the loop" true in practice, not just on paper.
The hosted cloud profile (lib/deployment-profile.js) is a second, tighter list — and its contract test now cross-checks that every hosted surface exists on disk and is classified here, so the two lists can't drift apart (that check would have caught the former dangling help.html).
Current boundary (measured)
20 core :extension — ratio 0.9 : 1 (cap 0.95). The numbers come from surface-registry.summary() via npm run test:boundary, not an estimate. (A parallel non-HTML subsystems tier — bots + background services, #1948/#1980 — is classified by the same rule; see SUBSYSTEMS in the registry.)
Core — the convergence loop
| Stage | Surfaces |
|---|---|
| Observe | index.html |
| Remember | explore.html, knowledgecenter.html, rag-house.html, wide-search.html |
| Reason | dream-chat.html |
| Act | orchestration.html, work.html, admin-flags.html |
| Verify | proof.html, calibration.html, factcheck.html, grounding-diff.html, drift.html |
| Converge | agent-status.html, agent-leaderboard.html, metrics.html, systems.html, replay.html |
Extensions — optional capabilities (by module)
| Module | Count | Flag | Surfaces |
|---|---|---|---|
| account | 7 | — (always-on shell) | auth, entry, profile, reset-password, pricing, upgrade-lab, api-keys-settings |
| trading | 4 | TRADING_ENABLED |
trading, kalshi-terminal, kalshi-screener, stock-trader |
| meta | 3 | — (always-on shell) | changelog, whats-new, faq |
| creator | 1 | CREATOR_ENABLED |
create |
| media | 1 | RADIO_ENABLED |
fallout-radio |
What this buys
- Honest accounting. The sprawl is a number (0.9:1) with a cap that fails CI (0.95), not a vibe. Setting aside the account/meta shell (11 surfaces), no feature cluster exceedssurfaces (trading).
- No silent sprawl. A new unclassified
public/*.htmlfailsnpm run test:boundary— you must name the loop stage it serves, or declare it an extension. - Back-pressure, not just labels. A new extension that isn't offset by core value trips the budget; every extension is switch-off-able (a
flag) unless it's the account/meta shell. The sprawl can be turned off, not just counted. - No list drift. The cloud hosted subset is cross-checked against disk + this registry, so a hosted entry can't point at a missing or unclassified file.
This complements the existing governance scripts — find-orphan-pages.mjs (reachability) and lint-throwaway-pages.mjs (throwaway/test pages). Those ask "is it reachable / is it junk?"; this asks "does it belong to the loop, or is it a declared, gated, budgeted extension?"