Tarot App Prompt System — Full Inspection Report
Date: 2026-05-22 Scope: apps/tarot-app — all prompt-related architecture Method: Full source code audit of prompt layers, runtime assembly, actor definitions, symbolic engine, services, screens, and dev infrastructure. Constraint: No modifications — pure inspection.
1. Prompt Topology Map
The prompt system has 5 distinct layers, each with different stability, ownership, and insertion mechanics:
┌─────────────────────────────────────────────────────────────┐
│ SURFACE CONTRACT │
│ (readingPhasePrompts.ts, readingFocusLens.ts, │
│ loungeSystemPrompt.ts) │
│ → Phase instructions, focus lens, lounge persona │
│ → Direct insertion into final prompt │
├─────────────────────────────────────────────────────────────┤
│ ACTOR │
│ (readingBasePersonality.ts) │
│ → Teteh's core identity, voice rules, nicknames, emoji │
│ → Direct insertion — the stable foundation │
├─────────────────────────────────────────────────────────────┤
│ ENGINE (RUNTIME) │
│ (emotionalClassification.ts → buildReadingRecipe.ts → │
│ buildSymbolicRecipeModifiers.ts → emotionalCompass.ts → │
│ renderEmotionalCompassGuidance.ts) │
│ → Derives emotional posture from card data │
│ → Derived/transformed insertion │
├─────────────────────────────────────────────────────────────┤
│ SYMBOLIC KNOWLEDGE │
│ (tarotSymbolicStructure.ts, tarotRankStructure.ts, │
│ tarotCourtStructure.ts, tarotOrientationEffects.ts, │
│ tarotSpreadPatterns.ts, tarotAtmosphereAnalyzer.ts, │
│ card-essence/*.ts) │
│ → Pure data — no prompts, no side effects │
│ → Consumed by Engine layer │
├─────────────────────────────────────────────────────────────┤
│ RUNTIME METADATA / DEVOPS │
│ (testSubjectPresets.ts, providerRouter.ts, demoMode.ts, │
│ continuityMap.ts) │
│ → Dev-only overlays, simulated users, provider routing │
│ → Conditional insertion (NODE_ENV) │
└─────────────────────────────────────────────────────────────┘Prompt Assembly Order (Phase 1/2)
The generate-reading/route.ts POST handler assembles prompts in this exact order:
- Spread Order Rule —
buildSpreadOrderRule()(direct) - Spread Ontology —
buildSpreadOntologySection()(direct) - Base Personality —
BASE_PERSONALITY_PROMPT(direct) - Simulated User Guidance —
buildSimulatedDayGuidance()(conditional, dev-only) - Focus Area Lens —
FOCUS_AREA_LENS_PROMPT+buildFocusLensSection()(conditional, direct) - Phase Prompt —
PHASE_1_PROMPT/PHASE_2_PROMPT/PHASE_3_EXPORT_PROMPT(direct) - Emotional Profile Section —
buildEmotionalProfileSection()(derived) - Recipe Section —
renderRecipeForPrompt()(derived) - Symbolic Modifiers —
renderSymbolicModifiersForPrompt()(derived) - Emotional Compass —
renderEmotionalCompassGuidance()(derived, Phase 1/2 only) - Director Notes —
buildDirectorNotesSection()(conditional, dev-only) - User Signal Context —
buildUserSignalContextSection()(conditional) - Reading Context / Payload — inline card data (derived)
2. Prompt Layer Inventory
2.1 Actor Layer — readingBasePersonality.ts (121 lines)
Status: ✅ Stable, well-defined, single source of truth.
This is the primary actor definition for Teteh. Contains:
- Core identity: "Teteh Tarot — kakak perempuan hangat dari Bandung"
- Voice rules: Indonesian-dominant (70/30), Sundanese seasoning, self-reference "aku"
- Nickname system: Beb, Darling, Sist, Honey, Sweety
- Emoji rules: sparing, contextual, never decorative
- Cadence/compression guidance
- Anti-fortune-telling posture: "Teteh bukan peramal"
Assessment: Clean. No fragmentation. Single file, single responsibility. This is the canonical Actor definition.
2.2 Surface Contract Layer — readingPhasePrompts.ts (342 lines)
Status: ✅ Stable, well-structured.
Three phase prompts:
- Phase 1 (
PHASE_1_PROMPT): Conversational exploration of 3-card spread. "Ajak ngobrol, bukan ngasih kuliah." - Phase 2 (
PHASE_2_PROMPT): Intimate deep dive into chosen card. "Deeper symbolic exploration." - Phase 3 (
PHASE_3_EXPORT_PROMPT): Narrated keepsake reflection. "Teteh punya sesuatu buat kamu."
Also contains dynamic spread-aware builders:
buildSpreadOrderRule()— critical ordering constraintbuildPhase1Positions()— position-aware prompt injectionbuildSpreadOntologySection()— full spread ontology injection
Assessment: Clean. The phase prompts are well-separated. The dynamic builders are appropriate.
2.3 Surface Contract Layer — readingFocusLens.ts (69 lines)
Status: ✅ Stable.
FOCUS_AREA_LENS_PROMPT— emotional lens for interpreting cards through focus areabuildFocusLensSection()— dynamic focus area injectionbuildPhase3FocusInfluence()— Phase 3 embedded focus
6 focus areas: love, work, money, self, decision, life.
Assessment: Clean. Single responsibility.
2.4 Surface Contract Layer — loungeSystemPrompt.ts (55 lines)
Status: ✅ Stable.
buildSystemPrompt()— lounge persona definition- Room ontology: Pantry = decompression room
- Core voice: 70% Indonesian, 30% English
- Boundary handling: explicit, self-harm, dependency, politics, coding, pop_culture
- Emoji rules
Assessment: Clean. Well-bounded.
2.5 Engine Layer — emotionalClassification.ts (310 lines)
Status: ✅ Stable, well-factored.
classifyEmotionalProfile()— pure function classifying 3-card spread + focusArea intoEmotionalProfile- 8 tones: hopeful, heavy, transitional, tender, chaotic, stagnant, expansive, introspective
TONE_BEHAVIOR_RULES— exported constant used in prompt assemblynormalizeCardName(),isReversed()— shared utilities
Assessment: Clean. Pure classification. No side effects. No prompt text.
2.6 Engine Layer — buildReadingRecipe.ts (402 lines)
Status: ✅ Stable.
- Converts
EmotionalProfileintoTetehReadingRecipe(tone, pacing, teasingLevel, reassuranceLevel, intimacyLevel, directness, emojiStyle, warmth, restraint, humor, avoid[], guidance[]) - Phase-aware adjustments
renderRecipeForPrompt()— compacts recipe into prompt-safe string
Assessment: Clean. Well-structured transformation pipeline.
2.7 Engine Layer — buildSymbolicReadingContext.ts (196 lines)
Status: ✅ Stable.
- Bridge layer packaging symbolic tarot knowledge into structured context
- Maps cards + spreadPatternId into
SymbolicReadingContext - Contains spreadPattern, atmosphere, per-card contexts
Assessment: Clean bridge layer.
2.8 Engine Layer — buildSymbolicRecipeModifiers.ts (343 lines)
Status: ✅ Stable.
- Converts symbolic context into behavior modifiers
- Modifiers: softenCertainty, slowPacing, increaseGrounding, increaseReflectiveDepth, reduceHumor, etc.
- AtmosphereTone priority: archetypal > blocked > relational > tense > tender > grounded > mixed > light
- CautionStyle priority: blocked > heavy > choice > gentle > none
renderSymbolicModifiersForPrompt()— compact prompt injection
Assessment: Clean. Well-structured priority system.
2.9 Engine Layer — emotionalCompass.ts (234 lines)
Status: ✅ Stable.
- Narrator-posture layer
- Maps symbolic context to
CompassDirection: grounded, tender, heavy, quiet, playful, spark buildEmotionalCompass()returns primary/secondary direction, posture string, reason
Assessment: Clean. Distinct responsibility.
2.10 Engine Layer — renderEmotionalCompassGuidance.ts (63 lines)
Status: ✅ Stable.
- Converts compass posture into compact prompt-safe guidance string
Assessment: Clean. Thin renderer.
2.11 Engine Layer — renderPreviewReadingGuidance.ts (157 lines)
Status: ⚠️ Experimental / Preview-only.
- Preview-only prompt assembly combining symbolic atmosphere, modifiers, compass guidance, spread framing
- Used by preview screens, not production reading flow
Assessment: Could be consolidated with main assembly pipeline. Low priority.
2.12 Engine Layer — renderPromptComparisonPreview.ts (125 lines)
Status: ⚠️ Experimental / Dev-only.
- Side-by-side dry-run comparison for current vs future symbolic guidance
- Dev-only utility
Assessment: Dev tooling. Fine where it is.
3. Actor Analysis
3.1 Primary Actor: Teteh
Defined in: readingBasePersonality.ts (BASE_PERSONALITY_PROMPT)
Identity: Warm Bandung older-sister energy. "Kakak perempuan hangat dari Bandung."
Voice characteristics:
- Indonesian-dominant (~70%), English seasoning (~30%)
- Sundanese words: "mangga", "teu", "nya"
- Self-reference: "aku" (not "Teteh" in third person)
- Nicknames: Beb, Darling, Sist, Honey, Sweety
- Emoji: sparing, contextual (🤭, 😭, 🥺, etc.)
- Anti-fortune-telling: "Teteh bukan peramal. Teteh cuma bacain kartu."
Assessment: The Actor is well-defined and lives in a single file. No fragmentation. No duplicated persona shaping across files.
3.2 Secondary Actor: Lounge Teteh
Defined in: loungeSystemPrompt.ts (buildSystemPrompt())
Identity: Same Teteh, post-reading decompression mode. "Pantry" room ontology.
Voice: Same core voice, but more casual. Room is a decompression space, not a reading space.
Boundary handling: Explicit categories for content moderation (self_harm, explicit, politics, coding, pop_culture, dependency, weird_input, harmful, other_boundary).
Assessment: Clean separation. The lounge persona is a contextual overlay on the base Actor, not a separate definition.
3.3 Actor Fragmentation
Finding: No significant fragmentation. The Actor definition is centralized in readingBasePersonality.ts. The lounge overlay is in loungeSystemPrompt.ts. Both are clean.
Minor concern: The buildSystemPrompt() in loungeSystemPrompt.ts re-defines some voice characteristics (70/30 language mix, emoji rules) that overlap with BASE_PERSONALITY_PROMPT. This is intentional (lounge has different constraints) but worth noting as a potential drift point.
4. Surface Contract Analysis
4.1 Reading Surface Contract
Defined in: readingPhasePrompts.ts
The surface contract defines:
- What the model should produce (conversational reading, deep dive, export)
- How to structure the output (spread order, position awareness)
- Constraints (anti-diagnosis, anti-fortune-telling, anti-overexplaining)
Assessment: The phase prompts are well-structured. Each phase has clear boundaries. The dynamic builders (buildSpreadOrderRule, buildSpreadOntologySection) correctly inject spread-specific context.
4.2 Lounge Surface Contract
Defined in: loungeSystemPrompt.ts
The lounge contract defines:
- Post-reading decompression behavior
- Message classification boundaries
- Phase-aware pacing (early, settling, deeper, winding-down, closing)
Assessment: Clean. The boundary handling is explicit and well-categorized.
4.3 Surface Contract Overlap
Finding: The buildUserSignalContextSection() in generate-reading/route.ts contains its own anti-diagnosis/anti-overexplaining rules that partially overlap with the phase prompts. This is intentional (the signal context section needs its own guardrails) but creates a maintenance surface.
Recommendation: Consider extracting shared guardrails into a constants file. Low priority.
5. Runtime Assembly Analysis
5.1 Main Assembly Pipeline — generate-reading/route.ts (1478 lines)
Status: ⚠️ Large file, but well-structured.
The route handler is the central prompt assembly orchestrator. It:
- Validates input (cards, spread, focus area, signals)
- Classifies emotional profile from cards
- Builds reading recipe from profile
- Builds symbolic context from cards
- Derives symbolic modifiers from context
- Builds emotional compass from context
- Assembles system prompt from all layers
- Calls LLM (OpenAI GPT-5.5 default, dev provider override)
- Extracts and returns text
- Logs emotional spread classifications (dev-only)
- Builds engine trace (dev-only)
Assembly order (from buildPromptAssemblyTrace()):
1. spreadOrderRule → direct
2. spreadOntologySection → direct
3. activeBasePrompt → direct
4. simulatedUserGuidance → direct (conditional, dev)
5. FOCUS_AREA_LENS_PROMPT → direct (conditional)
6. phase1Prompt → direct
7. emotionalProfileSection → derived
8. recipeSection → derived
9. symbolicSection → derived
10. compass inline → derived (Phase 1/2 only)
11. directorNotesSection → direct (conditional, dev)
12. userSignalSection → direct (conditional)
13. readingContext → derivedAssessment: The assembly pipeline is well-documented with PromptContribution types and buildPromptAssemblyTrace(). The file is large (1478 lines) but the structure is clear. The observability infrastructure (engine trace, prompt section parsing) is sophisticated.
5.2 Lounge Assembly Pipeline — lounge-chat/route.ts (572 lines)
Status: ✅ Stable.
The lounge route:
- Builds system prompt from
buildSystemPrompt()+ simulatedUserGuidance + directorNotesSection - Classifies incoming message against boundary categories
- Includes pantry phase residue for phase-aware responses
- Includes afterglow bleed guidance for emotional continuity
Assessment: Clean. Well-bounded. The message classification is explicit.
5.3 Client-Side Assembly — readingGeneration.ts (406 lines)
Status: ✅ Stable.
generateReading()— main client-side reading generation with caching and retrygenerateDeepReading()— deep reading wrappergenerateExportSummary()— Phase 3 export summary- In-memory cache with deduplication via
readingRequestsmap - Exponential backoff retry (2 retries, 1s/2s base delay)
- 30s timeout for reading, 20s for export
Assessment: Clean. The caching layer is appropriate. The retry logic is well-structured.
5.4 Client-Side Assembly — loungeChat.ts (99 lines)
Status: ✅ Stable.
sendLoungeMessage()— sends message to lounge API- Demo mode fallback with canned responses
- 25s timeout
- Graceful fallback on error
Assessment: Clean. Minimal. Appropriate.
6. Redundancy / Drift Detection
6.1 Duplicated Persona Shaping
Finding: Minor overlap between BASE_PERSONALITY_PROMPT and buildSystemPrompt() (lounge).
- Both define language mix ratios (70/30)
- Both define emoji rules
- Both define anti-fortune-telling posture
Severity: Low. The lounge overlay intentionally re-states some rules because the lounge context is different. However, if BASE_PERSONALITY_PROMPT is updated, the lounge version could drift.
Recommendation: Consider having buildSystemPrompt() reference BASE_PERSONALITY_PROMPT as a base and only override lounge-specific rules. Low priority.
6.2 Overlapping Guardrails
Finding: Anti-diagnosis/anti-overexplaining guardrails appear in:
buildUserSignalContextSection()(route.ts)PHASE_1_PROMPT(readingPhasePrompts.ts)PHASE_2_PROMPT(readingPhasePrompts.ts)PHASE_3_EXPORT_PROMPT(readingPhasePrompts.ts)BASE_PERSONALITY_PROMPT(readingBasePersonality.ts)
Severity: Low. Each instance is context-appropriate. The guardrails are slightly different in each location (signal context has its own specific rules, phase prompts have reading-specific rules, base personality has identity-level rules).
Recommendation: No action needed. The guardrails are intentionally layered.
6.3 Duplicate Card Name Normalization
Finding: normalizeCardName() exists in:
emotionalClassification.ts(exported)card-essence/helpers.ts(exported asnormaliseCardName— note British spelling)
Severity: Low. Different modules, different responsibilities. The card-essence version is a simple lowercase/trim. The emotionalClassification version may have additional logic.
Recommendation: Consider consolidating into a shared utility. Very low priority.
6.4 Historical Residue
Finding: The store (useTarotStore.ts) has deprecated fields:
lifeArea— deprecated, no longer active runtime flowemotionalNote— deprecated, replaced bysignalInput"lifeArea"mode — deprecated
Severity: Low. These are marked with @deprecated JSDoc comments. They're preserved for compatibility but not actively used.
Recommendation: Clean up in a future refactor. Not urgent.
7. Stable vs Experimental
7.1 Stable Production Architecture
| Module | Status | Notes | |--------|--------|-------| | readingBasePersonality.ts | ✅ Stable | Core Actor definition | | readingPhasePrompts.ts | ✅ Stable | Phase 1/2/3 prompts | | readingFocusLens.ts | ✅ Stable | Focus area lens | | loungeSystemPrompt.ts | ✅ Stable | Lounge persona | | emotionalClassification.ts | ✅ Stable | Emotional profile classification | | buildReadingRecipe.ts | ✅ Stable | Recipe builder | | buildSymbolicReadingContext.ts | ✅ Stable | Symbolic context bridge | | buildSymbolicRecipeModifiers.ts | ✅ Stable | Symbolic modifiers | | emotionalCompass.ts | ✅ Stable | Narrator posture | | renderEmotionalCompassGuidance.ts | ✅ Stable | Compass renderer | | symbolicResidue.ts | ✅ Stable | Afterglow compression | | pantryDecompression.ts | ✅ Stable | Pantry opening | | pantryPhaseResidue.ts | ✅ Stable | Phase tracking | | localNicknameContinuity.ts | ✅ Stable | Nickname continuity | | localReadingRhythm.ts | ✅ Stable | Reading rhythm | | localSymbolicClimate.ts | ✅ Stable | Symbolic climate | | tarotSpreadPatterns.ts | ✅ Stable | 6 spread patterns | | tarotAtmosphereAnalyzer.ts | ✅ Stable | Atmosphere analysis | | tarotSymbolicStructure.ts | ✅ Stable | Symbolic structure data | | tarotRankStructure.ts | ✅ Stable | Rank structure data | | tarotCourtStructure.ts | ✅ Stable | Court structure data | | tarotOrientationEffects.ts | ✅ Stable | Orientation effects data | | card-essence/*.ts | ✅ Stable | 78-card essence library | | readingGeneration.ts | ✅ Stable | Client reading service | | loungeChat.ts | ✅ Stable | Client lounge service | | generate-reading/route.ts | ✅ Stable | Main API route | | lounge-chat/route.ts | ✅ Stable | Lounge API route |
7.2 Experimental / Dev-Only
| Module | Status | Notes | |--------|--------|-------| | testSubjectPresets.ts | 🔬 Experimental | Simulated user presets for API Lab | | providerRouter.ts | 🔬 Experimental | Dev provider routing | | demoMode.ts | 🔬 Experimental | Demo mode toggle | | renderPreviewReadingGuidance.ts | ⚠️ Preview-only | Preview prompt assembly | | renderPromptComparisonPreview.ts | ⚠️ Dev-only | Side-by-side comparison | | continuityMap.ts | 📋 Documentation | Architecture map (dev tooling) | | Engine trace infrastructure | 🔬 Experimental | Observatory-grade trace (dev-only) |
7.3 Historical Sludge
| Item | Status | Notes | |------|--------|-------| | lifeArea in store | 🗑️ Deprecated | No longer active | | emotionalNote in store | 🗑️ Deprecated | Replaced by signalInput | | "lifeArea" mode | 🗑️ Deprecated | No longer active | | "cards" mode | 🗑️ Deprecated | No longer active | | "privacy" mode | 🗑️ Deprecated | No longer active | | "preview" mode | 🗑️ Deprecated | No longer active |
8. Recommended Future Shape
8.1 Engine + Actor + Surface Contract Architecture
The codebase is already close to this architecture. The recommended formalization:
┌─────────────────────────────────────────────────────────────┐
│ SURFACE CONTRACT │
│ Purpose: Define WHAT the model should produce │
│ Files: readingPhasePrompts.ts, readingFocusLens.ts, │
│ loungeSystemPrompt.ts │
│ Rule: No actor identity. No engine logic. │
│ Pure output specification. │
├─────────────────────────────────────────────────────────────┤
│ ACTOR │
│ Purpose: Define WHO the model is │
│ Files: readingBasePersonality.ts │
│ Rule: Single source of truth for Teteh identity. │
│ No phase logic. No engine logic. │
├─────────────────────────────────────────────────────────────┤
│ ENGINE (RUNTIME) │
│ Purpose: Derive emotional posture from card data │
│ Files: emotionalClassification.ts, buildReadingRecipe.ts, │
│ buildSymbolicRecipeModifiers.ts, emotionalCompass.ts, │
│ renderEmotionalCompassGuidance.ts │
│ Rule: Pure transformations. No prompt text. │
│ No actor identity. │
├─────────────────────────────────────────────────────────────┤
│ SYMBOLIC KNOWLEDGE │
│ Purpose: Provide symbolic tarot data │
│ Files: tarot*.ts, card-essence/*.ts │
│ Rule: Pure data. No prompts. No side effects. │
├─────────────────────────────────────────────────────────────┤
│ RUNTIME ASSEMBLY │
│ Purpose: Orchestrate prompt construction │
│ Files: generate-reading/route.ts, lounge-chat/route.ts │
│ Rule: Assembly only. No prompt content. │
│ Import from layers above. │
└─────────────────────────────────────────────────────────────┘8.2 Specific Recommendations
- Consolidate lounge persona — Have
buildSystemPrompt()referenceBASE_PERSONALITY_PROMPTas a base and only override lounge-specific rules. This prevents drift.
- Extract shared guardrails — Consider extracting anti-diagnosis/anti-overexplaining guardrails into a shared constants file if they continue to proliferate. Not urgent.
- Clean up deprecated store fields — Remove
lifeArea,emotionalNote, and deprecated modes in a future refactor. Marked with@deprecatedalready.
- Consolidate card name normalization — Merge
normalizeCardName()(emotionalClassification.ts) andnormaliseCardName()(card-essence/helpers.ts) into a shared utility.
- Keep engine trace as dev-only — The observatory-grade trace infrastructure is valuable for debugging but should remain behind
NODE_ENV === "development".
- No urgent refactors needed — The architecture is already well-structured. The Engine + Actor + Surface Contract pattern is mostly emergent. Formalizing it would improve clarity but is not blocking.
8.3 What NOT to Change
- Do not merge phase prompts into a single file — they have different responsibilities
- Do not inline the symbolic knowledge layers — they are pure data and should remain separate
- Do not remove the engine trace infrastructure — it's valuable for debugging
- Do not simplify the assembly pipeline — the layered approach is correct
- Do not remove deprecated fields without migration — they may be used by external integrations
Summary
The apps/tarot-app prompt system is well-architected with clear separation of concerns. The Actor definition is centralized, the Engine layer is pure transformation, the Surface Contract is explicit, and the Runtime Assembly is well-documented with observability infrastructure.
Key strengths:
- Single source of truth for Teteh identity
- Clean separation between data (symbolic knowledge), transformation (engine), and output specification (surface contract)
- Sophisticated observability infrastructure (engine trace, prompt section parsing)
- Well-structured assembly pipeline with documented contribution order
- Explicit boundary handling in lounge
Minor concerns:
- Lounge persona partially duplicates base personality rules
- Guardrails are duplicated across multiple files (intentional but creates maintenance surface)
- Card name normalization is duplicated with different spelling
- Deprecated store fields should be cleaned up eventually
Overall assessment: The system is ready for the Engine + Actor + Surface Contract formalization. No urgent refactors needed. The architecture is stable and well-understood.