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.
PassLane is already ~70–85% a config-driven engine by accident, not design: STATE_FILE→loadQuestions (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.
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.
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.VERTICAL = today, byte-for-byte.coach{} tuning block (→ CONFIG) and (b) the authored repair pack (→ CONTENT).exam-rules.json)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-blockeffectiveRules = deepMerge(DEFAULT_RULES, verticalFile). DEFAULT_RULES == today's literals, append-only/frozen.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).questions*.json envelope + coach-<vertical>.json + audio pack + legal include){questions:[...]} envelope, the null-by-default authored repair layer, per-question audio, per-vertical legal/disclaimer copytype (default "single") read through one normalizeItem() boundary; explanation mandatory (sole coach grounding source).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.theme.json + CSS-var substrate)legalInclude, storagePrefixVERTICAL.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.
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.
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, mode→track, 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.
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.
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).
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.
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:
VERTICAL isn't parsed yet).k(name) => \${NS}_${name}\` over the other ~67 sites, NS = VERTICAL.config.storagePrefix`.ns="az" FROZEN for vertical #1, forever — renaming PassLane's own keys post-install wipes every existing user's progress/streak/pro-entitlement on update. The seam is fresh ns for NEW verticals, never a rename. NS resolves to "az" for the live app → zero key change, zero migration. The unshipped coach key/filename derive from NS/id so vertical #2 gets plh_coach/coach-passlane-health.json for free.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.
"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 state — az_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.
The brain ports clean; every leak is at the edges. Each domain assumption → its config/content-driven fix:
correct (render L2748, TTS L2832, grade L2969/L3217/L3242, voice L3814) — the one structural CODE leaktype field + normalizeItem() + RENDERERS[type] + array-correct. CONTENT/CODE.speechPlugin.start({language:'en-US'}) L3554; lang='en-US' L3397/L4444)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.)scoring.passMark + readinessBands[] via passBand/readinessBand. CONFIG.pc/es/lh/both/all — logic (modeMatches L1966, counts L2590, validators)tracks[]/combos{} in exam-rules. CONFIG. (Card-rendering half of this leak is row F5 below.)scoring.model union; "adaptive" reserved. CONFIG.STATE_FILE L1764); "none"` is a render-GATING seam (suppress state picker + requestState funnel). CONFIG.az_ / appId / audio CDN (L1210–1228, L1510, L4043, L4270, L2311)k() namespace + build-stamp. BRAND/CONTENT/CONFIG.rdHist keyed by insurance modetracks[].key; track-set change bumps coach schemaVersion. CONFIG.#10d98c, #4f8ef7, #a78bfa, plus warm-olive light variants)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.
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):
weakCategories.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.
The repeatable path #2/#3 walk — each step writes one layer. Content → SME/trust → config → brand → build → ship.
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.{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.coach-<vertical>.json null-by-default.exam-rules.json (passMark, bands, lengths, tracks, scoringModel, jurisdiction).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).sw.js's CACHE to a per-vertical+per-version value (see §8 F1).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.
PHASE 0 — PassLane launch (NAME-NOW seams; additive, individually-revertible, harness-green — the entire pre-launch budget, ~5 edit clusters):
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.)*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.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.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).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; locale→en-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.
The "every possible obstacle" deliverable. (L = likelihood.)
correcttype+array-correct via normalizeItem; all-or-nothing into binary recorder (no engine change)type:"numeric" {value,tolerance,unit}+workings; FIRST deferred renderertype value space; build only the 1–2 types #2 usescaseId/stem grouping named in prose; Leitner schedules per childScorer→[0,1] seam + a real fractional-to-box policy decisionvoiceSupported false-degrade non-mcq; seed-E is a named content addloadQuestions L1775 rejects bare arrays / item-level-only version{schemaVersion,packId,locale,checksum,questions:[]} named (§3f); existing j.questions guard IS the seam; validator in spin-upcategory free-text but CI consistency validator per pack (§3a-note); engine unchangedpassBand/readinessBand over all 6; fixture asserts bar colorsscoringModel:"adaptive" reserved → forces CAT-honest template + skill-coverage gauge; ship fixed-form practice firstexam-rules.json, equal valuessections[] named in prose; finishExam breakdown reads configmode:"open-book" rules flag toggles reference affordanceSTATE_FILE L1764); NCLEX national; "none" gates the picker. Rename DEFERRED — az_state` is a live persisted DURABLE string (orphan risk; §3g)packs compose; per-jurisdiction legal overrideweakCategoriesTRUST.md Step 1 (origin+license+copyright attestation) — accurate-but-infringing passes every fabrication gatesrc_hash; treat as contract+indemnification stepaffiliationClause per-vertical config string; verbatim from incumbentssw.js CACHE='passlane-v103' is a launch-stability + multi-vertical-collision footguntheme.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)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--grad-brand)--grad-wordmark token (default = --grad-brand)pc/es ≠ insurance 5-track)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 thesequestions-az-es.json via existing STATE_FILE); chrome i18n gated to demandstart({language:'en-US'}) L3554 + lang L3397/L4444) — a Spanish pack needs locale-keyed phonetics, not just translated textpack.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)media? reserved as a NAMED seam (shape deferred, §3a-media); mp proves the offline-bundle pattern; answerCritical/offlineComplete semantics defined when #3 landsaz_ namespace collision (2 family verticals co-install)az_, distinct for new verticals + alias-read on the DURABLE keys (§3g)saveProgress; versioned-discard keys (re-authorable only, §3g)modelRouting; src_hash re-runrecall@k ≥ 0.95 is the only spend triggerexamModel:"none" hides mock-exam — config subset. The over-abstraction canarytype/media/discriminatorVERTICAL references, never inlinesThe 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.
--track-color at #2)type; no speculative field/shapeaz_state)scoringModel flag isolates it; honesty template forcedHow 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.
These are genuine — they require an owner decision or a real second data point, and the architecture exposes but cannot resolve them:
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?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.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?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):
/Users/arizona/CLAUDE CODE/passlane/app/index.html, /Users/arizona/CLAUDE CODE/passlane/app/questions.json, /Users/arizona/CLAUDE CODE/passlane/app/privacy.html, /Users/arizona/CLAUDE CODE/passlane/app/manifest.json, /Users/arizona/CLAUDE CODE/passlane/app/sw.js/Users/arizona/CLAUDE CODE/docs/passlane-coaching-engine-SPEC.md, /Users/arizona/CLAUDE CODE/docs/EXAM-PREP-ENGINE-STRATEGY.md/Users/arizona/CLAUDE CODE/passlane/voice-sandbox/harness.jsdocs/trust/clause-library.md, docs/trust/VERTICAL-SPINUP-CHECKLIST.md, per-pack TRUST.md, and the four config/content/brand schemas placed as Phase-0 seams.