PassLane — Vertical Portability & Future-Proofing Architecture (Run 5)
Bulletproof, not over-engineered · code-verified · plan only · 2026-06-19

THE Vertical Portability & Future-Proofing Architecture for PassLane

Principal / Chief Architect — Final. Build-ready, bulletproof-not-over-engineered. Verified against passlane/app/index.html (4,633 lines), sw.js, and questions.json this session.


1. The Portability Thesis

PassLane is already ~70–85% a config-driven engine by accident, not design: STATE_FILEloadQuestions (L1764) is a working per-pack content loader, PRODUCT_IDS (L2311) isolates IAP to one object, CSS custom properties + html[data-theme] is a real theming substrate, and the shipped scheduling primitives — questionMastery (L1972), computeReadiness (L1980), weakCategories (L1994), the binary Leitner recorder (L1887), buildQueue/orderForLearning — read only generic local mastery scalars (verified: zero insurance strings in the control flow that exists today). So a new vertical becomes data + config, never a code fork by separating four layers — generic engine CODE / per-exam rules CONFIG / per-vertical content PACK / per-app brand THEME — and routing every domain assumption through a named seam.

The discipline that keeps this minimal is the governing law from EXAM-PREP-ENGINE-STRATEGY.md: name the seams now (cheap, additive, default-preserving, harness-green), but build the framework only when vertical #2 (real estate, per the doc's fixed order) forces each seam's real shape. Pre-building renderers, type enums, or a regulatoryTier rules-engine for verticals that do not exist is the exact over-engineering simplicity-first forbids — bulletproof ≠ over-built.

One correction the recon forces, because it changes what "additive indirection" means: the run-4 coach (decideMove, weakCategoriesGated, the frequency governor, ORIENT, COACH_CONFIG) is net-new planned code, not shipped — verified zero occurrences in index.html, exactly as passlane-coaching-engine-SPEC.md states. The portability conclusion is unchanged (both the shipped scheduler and the planned coach are domain-neutral by construction), but the doc is precise about which is which: pointing shipped constants at VERTICAL.* is indirection over existing code; the coach's tunables are greenfield — placed correctly the first time, never retrofitted.

Every portability hook resolves at authoring time and is read as static data, adding zero runtime cost and zero per-user server state. PassLane ships undisturbed because every NOW seam is additive behind a default that reproduces today's behavior byte-for-byte.


2. The Four-Layer Separation

One umbrella VERTICAL object resolves four layers at boot. They are kept as separate files from day one (the strategy doc explicitly warns against one-giant-config); VERTICAL references them, never inlines them.

ENGINE CODE (shared, exam-agnostic, never forks)
What's in it SHIPPED: Leitner box math (L1887), questionMastery, computeReadiness, weakCategories (ungated), buildQueue/orderForLearning, exam-sim timer/verdict, the voice answer-by-key pipeline [FROZEN], billing scaffold, render surfaces. PLANNED (run-4, net-new): the decideMove cascade + frequency governor + ORIENT + the honesty/attempts gate.
Contract Reads only generic primitives + the four config/content/brand objects. Behavior with an empty VERTICAL = today, byte-for-byte.
Coach mapped on The entire scheduling+coaching brain is domain-neutral by construction. Its only vertical surfaces are (a) the coach{} tuning block (→ CONFIG) and (b) the authored repair pack (→ CONTENT).
EXAM-RULES CONFIG (exam-rules.json)
What's in it passMark, readiness bands[], examLengths, secondsPerQ, streakGoal, difficultyRange, the track taxonomy (tracks[]/combos{} replacing pc/es/lh/both/all), scoringModel flag, jurisdictionModel, and the coach{} tuning sub-block
Contract Sparse override: effectiveRules = deepMerge(DEFAULT_RULES, verticalFile). DEFAULT_RULES == today's literals, append-only/frozen.
Coach mapped on Coach band tunables (WEAK_MIN_ATTEMPTS, SHAKY, NOVELTY_MAX, LIVE_TURNS_PER_DAY_CAP…) live in coach{}; the coach reads config from day one — it is greenfield, so place it right once (zero hits to retrofit).
CONTENT PACK (questions*.json envelope + coach-<vertical>.json + audio pack + legal include)
What's in it The typed item bank inside the {questions:[...]} envelope, the null-by-default authored repair layer, per-question audio, per-vertical legal/disclaimer copy
Contract Pack carries a top-level envelope (§3f); items carry an opaque type (default "single") read through one normalizeItem() boundary; explanation mandatory (sole coach grounding source).
Coach mapped on The authored repair pack (coach-<vertical>.json) is parallel to and keyed identically with the question pack; the offline containment+novelty CI gate runs against explanation only — type-blind, scales the trust bar with no new mechanism.
BRAND THEME (theme.json + CSS-var substrate)
What's in it Name/proName, colors (role-keyed tokens, values-only swap), logo/splash/icon, support email, taglines, store copy, audio CDN origin, legalInclude, storagePrefix
Contract Role-key tokens never rename per vertical; every consumer defaults on a missing field. Caveat (§4 F5): global accent tokens are a pure values-swap; per-track card colors are not (hand-typed hex, duplicated across themes).
Coach mapped on The coach surfaces no brand strings; its filename/key derive from VERTICAL.id, so re-skinning never touches coaching logic.

The cleanest existing seam — the model for the whole extraction: poolForMode/orderForLearning/buildQueue already split what to study (content) from in what order (engine). That split is the proof the separation is natural, not imposed.


3. Schemas & Contracts

All four objects carry schemaVersion and evolve additive-only / default-preserving. The version-mismatch policy is partitioned (not global — this is the §3g fix): re-authorable/derivable state discards to default on mismatch (no migration); durable user state (entitlement, progress, streak, jurisdiction) is never discarded — it is the frozen-ns + alias-read + migration-preserving set. Content packs themselves are re-authored per vertical, never migrated in place.

3a. CONTENT PACK — item schema with the qtype extension point

The qtype extension point (the one non-deferrable seam): every item carries type and a normalizeItem(raw)→canonical read+score boundary (generalizing difficultyScore() L2016). normalizeItem runs on load, rewriting legacy packs to canonical keys in memory (object→array choices, string→array correct, modetrack, absent→"single") so the 323 AZ items + 6 state files change zero bytes and downstream code only ever sees canonical keys. The renderer/grader registry has one entry today (mcq); the ~10 verified hard-read sites across both grading paths — study (render L2748, compare L2969) and exam (compare L3217, section-score L3242) plus voice (L3814/L3818) — move behind RENDERERS[type], each mcq method body the existing code verbatim. Grade contract: grade(item, response) → { correct: bool }. The Leitner box (L1887) is binary by construction (verified: box up on true, reset on false), so multi-select (SATA) grades all-or-nothing (exact key-set match) into the existing recorder — zero engine change. Partial credit is explicitly NOT promised: it is a core engine decision (a fractional-to-box policy) deferred to the vertical that funds it, never guessed.

§3a-media (RESERVED, not pre-shaped — n=0 discipline): real estate (#2) is numeric, not media; the first media vertical is Construction (#3). Per the doc's own rule, only media? is reserved as a named seam now; its sub-fields (esp. answerCritical's exact offline-bundling semantics) are not pre-shaped until the first media vertical lands. The shipped mp audio key proves the offline-asset pattern and is the precedent the eventual media shape will generalize.

§3a-note (category consistency — a real correctness risk): category is free-text in data (correct) but is consumed by weakCategories/computeReadiness by string equality across the pool. A pack with inconsistent category spelling silently fragments weakness ranking. The spin-up checklist adds a cheap CI validator asserting category-set consistency within a pack (distinct categories enumerated, no near-duplicate strings) — a real correctness guard for SME-authored banks, zero engine change.

3b. EXAM-RULES CONFIG — with adaptive note

Adaptive-exam note (the deepest divergence, neutralized by scoping): NCLEX is computer-adaptive — variable length 85–150, logit ability-estimate stop, no fixed pass percentage — which makes the %-over-pool readiness model structurally invalid. Neutralization: scoring.model is a string union; only "fixed-percent" is implemented. "adaptive" is a reserved discriminator value that, when set, forces a CAT-honest verdict template (a confidence-band proxy — never a fake CAT engine; per the honesty moat, adaptive verticals show skill-coverage instead of a single ungroundable pass-probability number). Building an IRT calibrator before a CAT vertical is funded is over-engineering off n=0; even a future NCLEX vertical ships fixed-form practice through the fixed-percent branch first, exactly as commercial NCLEX banks do.

Pass-mark rewire (verified 6 sites, not 1): the >=70/85 literal is smeared across L1987, L1988, L2611, L3108, L3250 (the actual exam pass/fail verdict — passed = pct>=70), L3263, L4253 — six of them bare. All route through one passBand(pct)/readinessBand(pct) helper pair so the inline-threshold grep returns zero app-wide; the regression fixture asserts the rendered emoji and bar colors, not just the headline.

3c. BRAND THEME tokens

Decisive token rule: global accent tokens keep role names (--blue = primary accent, --green = success). The verified html[data-theme="light"] block recolors the app's chrome via ~20 same-named overrides — proof a global re-skin is a values-swap. But this is true for global accents only, NOT for per-track card colors (see §4 F5): those are hand-typed hex, duplicated in two theme blocks, and are the highest-frequency re-skin surface. Two further seams to verify: media answerCritical items must be offline-bundled (local-first honesty for diagram verticals — reserved per §3a-media), and voice carries three decoupled fields (manifestUrl app-relative today, clipOrigin remote CDN today, locale en-US today — collapsing any pair mis-wires a new vertical's voice).

3d. COACH_CONFIG (concrete, versioned, forward-compatible — greenfield)

COACH_CONFIG lives in exam-rules.coach{}. It is net-new (verified zero hits in code today), so it is placed correctly the first time — there is no existing constant to retrofit. The authored repair pack:

Forward-compatibility: distractor repair keys on a generic response token (for single the token is the letter — today's exact behavior, zero change); repair eligibility is gated to type==="single" so multi-select falls to the cascade's default-silence (honest, no new logic). rdHist keys on tracks[].key, and changing the track set bumps the coach schemaVersion so discard-to-default is intentional (this is re-authorable/derivable state per §3's partition — not the durable set), never silent readiness-history loss.

3e. The storage-namespace contract (three mechanisms — verified)

Verified: 69 az_ hits across ~30 keys, no helper, plus the pre-paint az_theme read at L26 (before any JS/VERTICAL exists) and PRO_KEY='az_pro' (L2127, bridges an offline lifetime buyer). So the namespace is not one find-replace:

3f. The PACK ENVELOPE contract (the §missed-obstacle fix)

normalizeItem quarantines item drift, but loadQuestions (L1764) hard-requires a top-level pack shape — verified L1775 returns j.questions only when Array.isArray(j.questions) && j.questions.length, rejecting bare arrays and empty packs. The pack envelope is therefore a named contract:

The existing j.questions guard is the envelope seam (today's AZ files already satisfy it → zero change). Pack-level schemaVersion/checksum is where the coach src_hash re-run gate anchors — resolving the otherwise-dangling "vs pack.checksum" reference in §3d. A vertical authored as a bare array, or with schemaVersion at item-level only, fails the L1775 guard loudly at load — which the spin-up checklist treats as a required validation pass, not a silent fallback.

3g. The version-mismatch partition (the §global-rule fix — stated once)

"Discard-to-default on schemaVersion mismatch, no migration" applies only to re-authorable/derivable state: coach rdHist, content-derived caches, voiced-id sets. It does not apply to durable user stateaz_pro (entitlement), az_study_progress (Leitner state), streak, and az_state (jurisdiction). Those are precisely the keys the frozen-ns (§3e), the alias-read (§8 J2), and the deferred jurisdiction migration (§8 C1) exist to preserve. Stated plainly so the discard rule is never read as license to wipe a paying user on a schema bump.


4. Engine-Portability Audit & Fixes

The brain ports clean; every leak is at the edges. Each domain assumption → its config/content-driven fix:

1
Domain assumption (verified site) {A,B,C,D} + single-letter correct (render L2748, TTS L2832, grade L2969/L3217/L3242, voice L3814) — the one structural CODE leak
Made config/content-driven by type field + normalizeItem() + RENDERERS[type] + array-correct. CONTENT/CODE.
2
Domain assumption (verified site) Voice answer-capture is en-US-hardcoded (NATO seed L3723 A–D only; speechPlugin.start({language:'en-US'}) L3554; lang='en-US' L3397/L4444)
Made config/content-driven by Loop iterates item.choices.map(c=>c.key); pack.locale/theme.voice.locale drives start({language}) + a locale-keyed phonetic map (en-US→NATO; es→Spanish phonetics). Per-type voiceSupported false-degrades non-mcq to text. All gated behind voice-sandbox/harness.js (FROZEN contract). CONFIG+CODE. (Seed extends to E only when a 5-option vertical lands — a named content add.)
3
Domain assumption (verified site) Pass mark / readiness bands (6 sites incl. exam verdict L3250)
Made config/content-driven by scoring.passMark + readinessBands[] via passBand/readinessBand. CONFIG.
4
Domain assumption (verified site) Track taxonomy pc/es/lh/both/alllogic (modeMatches L1966, counts L2590, validators)
Made config/content-driven by tracks[]/combos{} in exam-rules. CONFIG. (Card-rendering half of this leak is row F5 below.)
5
Domain assumption (verified site) 70%/CAT scoring model
Made config/content-driven by scoring.model union; "adaptive" reserved. CONFIG.
6
Domain assumption (verified site) Jurisdiction = US-state (STATE_FILE L1764)
Made config/content-driven by `jurisdictionModel: "us-state"
"none"; "none"` is a render-GATING seam (suppress state picker + requestState funnel). CONFIG.
7
Domain assumption (verified site) Disclaimer / brand / email / az_ / appId / audio CDN (L1210–1228, L1510, L4043, L4270, L2311)
Made config/content-driven by Brand theme + content legal include + k() namespace + build-stamp. BRAND/CONTENT/CONFIG.
8
Domain assumption (verified site) Coach rdHist keyed by insurance mode
Made config/content-driven by Keys on tracks[].key; track-set change bumps coach schemaVersion. CONFIG.
F5
Domain assumption (verified site) Track/mode CARDS are hardcoded HTML + per-track hex CSS, DUPLICATED across BOTH theme blocks (dark L151+; light L1046+; every track color hand-typed: #10d98c, #4f8ef7, #a78bfa, plus warm-olive light variants)
Made config/content-driven by Two-tier. NOW: tracks[] logic indirection (row 4) + theme.trackColors{} reservation. DEFER (#2): data-driven card render from tracks[] + a runtime --track-color token, eliminating the static HTML cards and the dual-theme CSS duplication. CONFIG+CODE. Real estate's pc/es split differs from insurance's 5 tracks, so this is the highest-frequency re-skin surface — and #2 is its forcing function.

The grep test = the CI-able definition of portable (and doubles as the extraction worklist). After replacing ONLY config + content + brand, these return zero hits in engine/coach code:

Today every block fails; that failing set is the precise worklist.

Seams-now vs defer: NOW = VERTICAL object pointing existing constants at config (pure indirection); type+normalizeItem+grade registry-of-one; passBand/readinessBand over all 6 sites; tracks[] logic indirection; theme.trackColors{} + pack.locale/voice.locale reservation; k() namespace (ns pinned "az"); coach key/file derivation; the legal-include slot; pack-envelope validation + category-consistency validator. DEFER = ES-module split, TS, build-stamper, non-mcq renderers, track-card generation (F5), voice-locale phonetic maps (row 2 body), "adaptive"/"none" bodies, partial-credit policy, media field shape, the remote kill-switch.


5. Pedagogy Portability

The coach is a mastery-scheduling engine wrapped around a feedback layer. The split is the portability seam:

UNIVERSAL (ships as engine code, identical every vertical — learning-science laws): spaced retrieval / Leitner, the testing-effect retrieval loop, repair-as-a-MOVE (the slot, content varies), the calibration/readiness scalar, weakness ranking + the attempts-floor honesty gate, the default-silence cascade + frequency governor, cold-start ORIENT. These hold for a statute, a lab value, or a proration. No per-vertical config touches them beyond numeric constants. (Note: of these, only the scheduler half is shipped today; the cascade/governor/gate are the run-4 net-new layer per §1 — but both halves are domain-neutral, which is what makes the split port.)

DOMAIN-TUNABLE (config + content hooks): what a miss MEANS and what coaching SAYS. The grounded-cognition divergence is real but treated as a hypothesis, not pre-built architecture (the authored repair layer is gated/null-by-default and may scope to near-zero — prove genre #1 for insurance via the Phase-1.5 dwell gate before asserting genres #2/#3):

The hooks, as config/content (minimal — the 7-knob coachingProfile was cut as n=1 over-reach): ONE nullable reserved coachingProfile placeholder (default null → run-4 hardcoded behavior = insurance ships as-is); tone/register/framing vocabulary are authored strings in coach-<vertical>.json (verdictStrings), not a config enum; the repair-strategy registry is an open, additively-extensible map (image/diagram repair is near-term for both real-estate and nursing). A per-type Scorer→[0,1] seam is named but the fractional-to-box policy is a real Health design task, not a freebie. Honesty pre-commitment: for logit/CAT verticals the readiness gauge shows skill-coverage, never an ungroundable percentage.


6. The Per-Vertical Spin-Up Playbook

The repeatable path #2/#3 walk — each step writes one layer. Content → SME/trust → config → brand → build → ship.

0. Classify
Action Pick regulatory tier (1 commercial / 2 financial-legal / 3 health-safety) as a label + jurisdiction model. Drives the human checklist, zero code.
Rough effort minutes
1. Provenance (FIRST — the likeliest vertical-killer)
Action Record contentOrigin + licenseRef + copyrightAttestation in TRUST.md. Testing bodies (NCSBN/FINRA/Pearson VUE/PSI) enforce copyright + test-security aggressively; an accurate-but-infringing bank passes every fabrication gate.
Rough effort hours (legal)
2. Question-type
Action If non-MCQ (NGN), the type model + renderer must land BEFORE trust gates (explanation-based containment is invalid for SATA otherwise).
Rough effort 0 for MCQ verticals; days for NGN
3. Content + audio
Action Author/acquire the bank in the typed envelope ({schemaVersion,packId,locale,checksum,questions}; start all-single; real-estate adds numeric, Construction adds numeric+media). Run the pack-envelope + category-consistency validators. Budget the per-vertical TTS GENERATION as authoring work — audio is metered/Pro-gated/remote-CDN, not free/offline/sunk (per coaching SPEC): batch-generate clips, provision the clipOrigin, name per clipNaming. Tier-3: SME reviews the questions, not just coach cells.
Rough effort the bulk — weeks (content + audio batch)
4. SME / trust
Action Name the SME-of-record (tier-3 = licensed RN + contract/indemnification — willingness-to-sign is the real blocker); run the offline containment+novelty CI gate to author coach-<vertical>.json null-by-default.
Rough effort days–weeks (gated by SME)
5. Config
Action Write exam-rules.json (passMark, bands, lengths, tracks, scoringModel, jurisdiction).
Rough effort ~1 day
6. Brand
Action Write theme.json (global accent values + trackColors into the CSS-var substrate) + legal-<vertical>.html; sweep residual color literals; run AA-contrast check; grep EVERY shipped file for the prior brand/domain term (index.html, manifest.json, privacy.html, sw.js, start.sh).
Rough effort ~1 day + designer assets
7. Build
Action Thin stamper injects appId + icon + IAP + theme + sw-cache-name — the hard ceiling, nothing else. MUST rewrite sw.js's CACHE to a per-vertical+per-version value (see §8 F1).
Rough effort hours (once stamper exists at #2)
8. Ship
Action Per-vertical IAP ids, store listing + age-rating/medical questionnaire, kill-switch denylist namespace.
Rough effort days (store review)

The trust pipeline (Step 1→4) is the highest-liability seam and rises by tier: tier-3 inverts the SME scope to review the questions, adds a clinical-claim containment rubric on the shared offline gate, and treats the disclaimer as a per-vertical content include with explicit "not medical advice." Rough total effort for #2 (real estate, MCQ+numeric, tier-1): dominated by content authoring + audio batch (weeks); the engineering delta is the numeric renderer + track-card generation (F5) + first config/brand/legal files + the build stamper (~1–2 weeks). #3+ that share the engine are theme+content+rules only.


7. The Extraction Roadmap

PHASE 0 — PassLane launch (NAME-NOW seams; additive, individually-revertible, harness-green — the entire pre-launch budget, ~5 edit clusters):

  1. ONE VERTICAL object; point shipped constants (READINESS_THRESHOLD, EXAM_LENGTHS, PRODUCT_IDS, tracks, streak goal, support email, audio CDN) at VERTICAL. with today's values — pure indirection. (The coach's tunables are NOT in this step — they are greenfield, written into coach{} when the run-4 coach is built, per §1/§3d.)*
  2. type:"single" default + normalizeItem() (run on load) + RENDERERS registry-of-one + grade()→{correct} + all-or-nothing multi grader. + fixture tests T1 (legacy questions.json resolves every canonical field) and T2 (mcq grade on BOTH study + exam paths). Add the pack-envelope validator (the L1775 guard formalized) + the category-consistency validator.
  3. Storage namespace: pre-paint literal + runtime k() + ns pinned "az" (or land pre-first-install). Coach key/file derive from NS/id. State the version-mismatch partition (§3g) in code comments at the save/load funnel so durable keys are never discard-eligible.
  4. exam-rules.json extraction of scattered constants (equal values, zero behavior change); passBand/readinessBand over all 6 pass-mark sites; tracks[]/combos{} logic indirection; scoringModel/jurisdictionModel as bare enums (no branch logic); reserve pack.locale/theme.voice.locale + theme.trackColors{} (no consumers yet).
  5. Externalize the L1210–1228 disclaimer DOM block as one legal-<vertical>.html include slot; --grad-wordmark token (fixes the verified L113 wordmark bug); residual-color-literal sweep; comment-fence the engine module regions.

DEFER to vertical #2 (real estate, the forcing function — build only what it proves): ES-module split (voice module cut FIRST, harness-gated), TS on the engine, the numeric renderer + tolerance grading, track-card generation + --track-color token (F5), the media field shape + origin/bundling (Construction's actual need, #3), theme.json file + build-stamper at the hard ceiling (incl. the sw.js cache-name rewrite), the jurisdiction-registry generalization with an az_state-preserving migration, the remote kill-switch (un-park infra). Defer to nursing (#6, unconfirmed): SATA/multi renderers + the partial-credit fractional-to-box policy, CAT-proxy, clinical containment rubric + two-axis ranking, voice-locale phonetic maps if a non-English pack lands first (see Open Q1).

Don't-over-abstract rules (enforced, not just stated): ship type:"single" opaque (do NOT pre-author the enum — guessing it off n=0 is already provably incomplete vs cloze/matrix/highlight); reserve only type + array-correct + media? (shape-less); bare enum names only where defensible (no pre-built branch logic); no coachingProfile fields, no score/partial-credit, no jurisdiction-matrix engine, no regulatoryTier code-branches, no remote kill-switch wired at launch. Tripwire = building a handler or field-shape for an unused type/media/discriminator value is the over-build smell.

How PassLane ships undisturbed: every Phase-0 item is additive behind a default that reproduces today byte-for-byte (normalizeItem→single/A-D; envelope = today's {questions:[...]}; NS"az"; rules == current constants; STATE_FILE untouched; localeen-US). Drop any edit that risks launch — PassLane ships fine without the seams. The choices-array change AND any voice-locale touch hit the answer-by-letter path → must pass voice-sandbox/harness.js before merge. The coach is byte-for-byte removable.


8. The Obstacle Catalog / Risk Register

The "every possible obstacle" deliverable. (L = likelihood.)

A. Question-type & schema
A1
Obstacle / path Multi-select / SATA breaks single-letter correct
L High (certain at nursing)
Neutralizing design decision type+array-correct via normalizeItem; all-or-nothing into binary recorder (no engine change)
Now / Deferred SEAM NOW (array-correct); grade trivial
A2
Obstacle / path Numeric/calculation (real-estate, dosage)
L High (real estate #2)
Neutralizing design decision type:"numeric" {value,tolerance,unit}+workings; FIRST deferred renderer
Now / Deferred NAME now; BUILD at #2
A3
Obstacle / path Ordering/matrix/cloze/highlight/hotspot (NGN)
L Med (nursing)
Neutralizing design decision OPEN type value space; build only the 1–2 types #2 uses
Now / Deferred CONTRACT-DEFER + WATCH
A4
Obstacle / path Shared-stem case studies (NGN unfolding)
L High if nursing
Neutralizing design decision caseId/stem grouping named in prose; Leitner schedules per child
Now / Deferred CONTRACT-DEFER
A5
Obstacle / path Partial-credit breaks the binary Leitner box (L1887)
L Med
Neutralizing design decision Named Scorer→[0,1] seam + a real fractional-to-box policy decision
Now / Deferred DEFER to nursing (engine decision, not handler)
A6
Obstacle / path Voice A/B/C/D biasing; NATO seed A–D only (L3723)
L High
Neutralizing design decision Loop iterates choice keys; voiceSupported false-degrade non-mcq; seed-E is a named content add
Now / Deferred SEAM NOW (variable count)
A7
Obstacle / path Pack ENVELOPE: loadQuestions L1775 rejects bare arrays / item-level-only version
L Med
Neutralizing design decision {schemaVersion,packId,locale,checksum,questions:[]} named (§3f); existing j.questions guard IS the seam; validator in spin-up
Now / Deferred SEAM NOW (zero change for AZ)
A8
Obstacle / path Category string-equality fragments weakness ranking on spelling drift
L Med
Neutralizing design decision category free-text but CI consistency validator per pack (§3a-note); engine unchanged
Now / Deferred VALIDATOR NOW
B. Exam-rules
B1
Obstacle / path Pass mark ≠70% + bands (6 sites, exam verdict L3250)
L High
Neutralizing design decision passBand/readinessBand over all 6; fixture asserts bar colors
Now / Deferred SEAM NOW
B2
Obstacle / path Adaptive/CAT (NCLEX: no fixed pass mark)
L High if nursing
Neutralizing design decision scoringModel:"adaptive" reserved → forces CAT-honest template + skill-coverage gauge; ship fixed-form practice first
Now / Deferred CONTRACT now, body DEFER
B3
Obstacle / path Exam lengths / seconds-per-q / retakes PSI-tuned
L High
Neutralizing design decision Externalize to exam-rules.json, equal values
Now / Deferred SEAM NOW
B4
Obstacle / path Two-part/sectioned exams (contractor, securities)
L Med-High
Neutralizing design decision sections[] named in prose; finishExam breakdown reads config
Now / Deferred CONTRACT-DEFER
B5
Obstacle / path Open-book exams (Ohio contractor)
L Low-Med
Neutralizing design decision mode:"open-book" rules flag toggles reference affordance
Now / Deferred CONTRACT-DEFER
C. Jurisdiction
C1
Obstacle / path "State" is the only axis (STATE_FILE L1764); NCLEX national
L High
Neutralizing design decision `jurisdictionModel:"us-state"
Now / Deferred "none"; "none" gates the picker. Rename DEFERREDaz_state` is a live persisted DURABLE string (orphan risk; §3g)
NAME now; registry+migration at #2
C2
Obstacle / path Multi-jurisdiction reciprocity / hybrid
L Med
Neutralizing design decision Future registry packs compose; per-jurisdiction legal override
Now / Deferred CONTRACT-DEFER
D. Domain-cognition
D1
Obstacle / path Rote-law vs clinical vs math changes what coaching IS
L Med-High
Neutralizing design decision Deterministic cascade ports; cognition lives in authored repair pack; case-level coaching is a #2 discovery
Now / Deferred DEFER + WATCH
D2
Obstacle / path Two-axis weakness (topic × CJMM skill)
L Med (nursing)
Neutralizing design decision Hypothesis Health tests; NOT a free re-point of single-axis weakCategories
Now / Deferred DEFER to nursing
E. Regulatory / liability / trust
E1
Obstacle / path Content provenance / IP / test-security on the bank
L High, universal
Neutralizing design decision TRUST.md Step 1 (origin+license+copyright attestation) — accurate-but-infringing passes every fabrication gate
Now / Deferred NOW (markdown)
E2
Obstacle / path Medical advice ≫ insurance; store health policy
L High if nursing
Neutralizing design decision Per-vertical legal include + type-blind containment judge tuned with a clinical rubric; SME-of-record + store form
Now / Deferred Slot NOW; rubric/SME at nursing
E3
Obstacle / path SME-of-record personal liability / willingness to sign
L High (tier-3)
Neutralizing design decision Hash-keyed signature vs src_hash; treat as contract+indemnification step
Now / Deferred DEFER + WATCH (needs run-4 CI gate)
E4
Obstacle / path Trademark non-affiliation (NCSBN/FINRA/PSI)
L Certain
Neutralizing design decision affiliationClause per-vertical config string; verbatim from incumbents
Now / Deferred Externalize NOW
E5
Obstacle / path App Store 2025 medical-content questionnaire/age
L Certain tier-3
Neutralizing design decision Checklist step ties questionnaire to in-app disclaimer
Now / Deferred Checklist step (not schema)
F. Brand / voice / asset
F1
Obstacle / path Name/logo/hex/email hard-coded; multi-file (privacy.html, sw.js, manifest.json separate surfaces); sw.js CACHE='passlane-v103' is a launch-stability + multi-vertical-collision footgun
L Certain
Neutralizing design decision theme.json + grep-EVERY-file ship gate; build stamper MUST rewrite sw.js CACHE to per-vertical+per-version; "bump sw cache name" is a hard ship-gate. Evidence it's fragile: the L1764 cache-buster retry exists because stuck-SW is a prior failure (sw.js comment L38)
Now / Deferred Sweep+sw-cache-gate NOW (checklist); stamper at #2
F2
Obstacle / path Is af_bella voice per-vertical? Three audio fields
L Med
Neutralizing design decision persona+style in theme.voice; manifestUrl (app-rel) vs clipOrigin (remote) vs locale split; per-vertical TTS GENERATION is metered authoring cost, not sunk (Step 3); one batch-TTS recipe per pack
Now / Deferred Descriptor NOW; tooling+budget at #2
F3
Obstacle / path Build step becomes its own product
L Med
Neutralizing design decision Hard ceiling: appId+icon+IAP+theme+sw-cache only
Now / Deferred Ceiling ENFORCED now; build at #2
F4
Obstacle / path Wordmark gradient diverges (L113 ≠ --grad-brand)
L Certain (live bug)
Neutralizing design decision --grad-wordmark token (default = --grad-brand)
Now / Deferred NOW (one token)
F5
Obstacle / path Track/mode CARDS = hardcoded HTML + per-track hex CSS DUPLICATED in BOTH theme blocks (L151+ dark, L1046+ light); a different track SET or COUNT forces hand-editing both
L High (#2 pc/es ≠ insurance 5-track)
Neutralizing design decision tracks[] logic indirection NOW + theme.trackColors{} reservation; data-driven card render + --track-color token at #2. The "~20 same-named overrides = pure values-swap" claim holds for GLOBAL accents only, NOT these
Now / Deferred NAME now; BUILD card-gen at #2
G. i18n
G1
Obstacle / path Spanish exams are a CURRENT insurance market (22 states; CA §1677)
L High, near-term
Neutralizing design decision Language = content-pack axis (questions-az-es.json via existing STATE_FILE); chrome i18n gated to demand
Now / Deferred CONTRACT-DEFER + WATCH
G2
Obstacle / path Voice answer-capture is en-US-hardcoded (NATO L3723 + start({language:'en-US'}) L3554 + lang L3397/L4444) — a Spanish pack needs locale-keyed phonetics, not just translated text
L High if Spanish ships
Neutralizing design decision pack.locale/voice.locale drives start({language}) + a locale-phonetic map; i18n is content-pack for TEXT but a NAMED voice seam for SPEECH — gated behind the voice harness. May make Spanish an EARLIER voice forcing-function than #2's numeric (→ Open Q1)
Now / Deferred NAME now; phonetic-map BUILD when first non-en pack lands
H. Multimedia
H1
Obstacle / path Diagrams/images break local-first if CDN-only
L High (Construction #3)
Neutralizing design decision media? reserved as a NAMED seam (shape deferred, §3a-media); mp proves the offline-bundle pattern; answerCritical/offlineComplete semantics defined when #3 lands
Now / Deferred RESERVE NOW; renderer+shape at #3
J. Platform / storage
J1
Obstacle / path web→iOS/Android
L
Neutralizing design decision Web-platform bedrock IS the hedge; CdvPurchase abstracts stores
Now / Deferred WATCH
J2
Obstacle / path az_ namespace collision (2 family verticals co-install)
L Med
Neutralizing design decision Config the prefix, pin live value to az_, distinct for new verticals + alias-read on the DURABLE keys (§3g)
Now / Deferred SEAM NOW (conservative)
J3
Obstacle / path Storage backend shift (localStorage→SQLite)
L Low-Med
Neutralizing design decision Read/write funnelled through saveProgress; versioned-discard keys (re-authorable only, §3g)
Now / Deferred CONTRACT-DEFER
K. AI model / scale
K1
Obstacle / path Haiku/Sonnet/Opus rename/reprice/deprecate
L Med
Neutralizing design decision $0-deterministic default; model touches 2 gated points behind modelRouting; src_hash re-run
Now / Deferred CONTRACT + WATCH
K2
Obstacle / path Vector-DB temptation
L
Neutralizing design decision recall@k ≥ 0.95 is the only spend trigger
Now / Deferred WATCH
L1
Obstacle / path 10k→1M learners × N verticals
L Med
Neutralizing design decision Free plane $0 in-binary at any N; Pro tail linear + capped
Now / Deferred WATCH
M/N. Discipline
M1
Obstacle / path Non-exam vertical (CE, language)
L Low-Med
Neutralizing design decision Engine primitive is generic; examModel:"none" hides mock-exam — config subset. The over-abstraction canary
Now / Deferred WATCH (don't design now)
N1
Obstacle / path Premature extraction off n=1
L Med (self-inflicted)
Neutralizing design decision Sequencing law: extract driven by #2's real needs; tripwire = handler/field-shape for an unused type/media/discriminator
Now / Deferred DISCIPLINE NOW
N2
Obstacle / path Config sprawl (one giant config)
L Med
Neutralizing design decision Four separate files; VERTICAL references, never inlines
Now / Deferred DISCIPLINE NOW
N3
Obstacle / path Destabilizing launch / breaking voice contract
L Low if disciplined
Neutralizing design decision All seams additive behind today-preserving defaults; choices+voice-locale changes harness-gated; coach removable
Now / Deferred DISCIPLINE NOW
N4
Obstacle / path Local-first recall: kill-switch can't reach offline installs
L Med
Neutralizing design decision Denylist is best-effort/until-next-connect; real tier-3 firewall = pre-ship CI gate + sign-off
Now / Deferred DEFER infra; name namespace

9. Future-Proofing & Open-Closed

The open/closed test: an extension is cheap if it is adding a row to data or a key to config; expensive if it forces touching the engine's control flow or re-shaping durable stored state. This architecture converts expensive→cheap via (a) the type-discriminated item contract with an open value space, (b) the version-partitioned local keys (re-authorable discards, durable preserved — §3g), (c) the single VERTICAL config object + four separate files, (d) the normalizeItem item-boundary + the {questions:[...]} envelope boundary quarantining content drift.

New state/Spanish pack (TEXT)
Cost after this design cheap (file + registry row)
Why existing loader + envelope
Spanish pack (VOICE/speech)
Cost after this design moderate (locale phonetic map, harness-gated)
Why en-US hardcoded in the voice pipeline (G2)
New pass mark / bands / lengths / track set
Cost after this design cheap (edit rules)
Why constants externalized, equal values
3/5-option MCQ, multi-select/SATA
Cost after this design cheap (data + 1 grade branch)
Why array choices; all-or-nothing into binary recorder
New track SET/COUNT (cards)
Cost after this design moderate (card-gen + --track-color at #2)
Why today hardcoded HTML+CSS dup'd across themes (F5)
Numeric / ordering / matrix / case / media
Cost after this design moderate (one leaf renderer each at #2/#3)
Why open type; no speculative field/shape
Brand/theme (global accents), regulated disclaimer
Cost after this design cheap (values / content include)
Why CSS-var substrate + discrete legal slot
Jurisdiction model change
Cost after this design moderate→bounded (#2 migration preserving az_state)
Why deferred, not forced; durable key preserved
CAT/adaptive scoring
Cost after this design expensive→bounded (proxy handler)
Why scoringModel flag isolates it; honesty template forced
Model swap/reprice
Cost after this design cheap (config)
Why $0-default, 2 gated touchpoints
Partial-credit grading
Cost after this design expensive (engine change)
Why binary Leitner; explicit #2 decision, never promised

How to make the expensive cheap without over-building now: the two genuinely expensive cells (CAT, partial-credit) are each bounded behind a reserved discriminator + a forced-honesty contract, so the future fits without a fork — but neither handler is built until a real vertical funds it. Reserving the seam costs one enum value.

The bulletproof check: Is there any future question shape, exam rule, jurisdiction, cognition, liability tier, brand, language, or platform shift that forces touching the engine's control flow or re-shaping DURABLE stored state? — No. The worst case for a new question shape is adding a leaf renderer behind an already-open type switch; for a new track set it is generating cards from tracks[] (the one moderate re-skin surface, scheduled at #2); for a new rule/brand/jurisdiction/text-language it is editing data/config; for speech-language it is a harness-gated locale phonetic map; for the two expensive cases it is filling a reserved, honesty-gated branch. Durable state is forward-safe by the §3g partition + the frozen az_ namespace + the alias-read. The frozen voice contract, the $0 deterministic coach, and the launch-imminent app are never destabilized because every NOW seam is additive behind a default that preserves today byte-for-byte and is harness-gated. That is the bulletproof line — drawn where the code actually bends, not where we imagine future verticals might.


10. Open Questions

These are genuine — they require an owner decision or a real second data point, and the architecture exposes but cannot resolve them:

  1. *The voice-wedge forcing function (blocks GTM, decision needed NOW) — and it may arrive before real estate. Two paths threaten the hands-free differentiator: (a) real estate (#2) is numeric-heavy, so by the voiceSupported:false rule its items are largely non-voiceable; (b) a Spanish insurance pack is a current near-term market (22 states) and the voice pipeline is en-US-hardcoded* (NATO map + speech language), so Spanish needs a locale phonetic map behind the frozen harness. Either could be the first vertical where the voice promise must be scoped or extended. Decide: build a "say the number" grammar for numeric and/or a Spanish phonetic map, or scope the voice promise to en-US MCQ verticals and ship the rest tap-first?
  2. Tier-3 SME willingness-to-sign and indemnification. A licensed RN signing an NCLEX manifest against a content hash attaches personal professional liability and may decline or demand indemnification. This is a contracting/funding gate the architecture surfaces but cannot neutralize — the real nursing go/no-go.
  3. CAT honesty UX divergence. For logit-scored verticals we pre-commit to a skill-coverage gauge instead of a pass-probability number — meaning the nursing readiness UX diverges from insurance's gauge. Accept that product-design cost up front, or invest in an honestly-grounded proxy?
  4. Is the af_bella voice persona per-vertical or per-brand? Placement of voice (brand vs content) is provisional pending #2 — does a clinical vertical reuse the persona (voice = app identity) or re-narrate (one brand, two reads)? Compounds with the per-vertical TTS generation budget (F2): each new persona×locale is a metered batch-authoring cost, not a freebie.
  5. Pre-launch vs alias-read for the az_ namespace. The safest path is landing the namespace indirection before first PassLane install (making the alias moot). Is the launch timeline such that this can land pre-install, or must the alias-read on the durable keys ship as the fallback?
  6. Vertical-order confirmation. The roadmap anchors the first deferred renderer (numeric) and track-card generation (F5) on real estate being #2 per EXAM-PREP-ENGINE-STRATEGY.md L14. If the owner reorders nursing earlier, the SATA/CAT/partial-credit work moves onto the critical path — and that math is genuinely larger.

Build-ready artifacts referenced (all absolute):

Private working document — unlisted, not indexed. PassLane / Somos LLC.