diff --git a/CHANGELOG_SYMBOLIC_MOMENT_REFACTOR.md b/CHANGELOG_SYMBOLIC_MOMENT_REFACTOR.md
new file mode 100644
index 000000000..44bb07060
--- /dev/null
+++ b/CHANGELOG_SYMBOLIC_MOMENT_REFACTOR.md
@@ -0,0 +1,180 @@
+# Symbolic Moment Refactoring: From Card to Reading Surface
+
+**Date:** 2026-04-20
+**Branch:** `claude/complete-opus-implementation-exjj7`
+
+## Overview
+
+The Symbolic Moment UI has been refactored from a decorative "card" widget into a dedicated **reading surface** — a primary, instrumental interface that presents live readings with clarity, hierarchy, and calm.
+
+## Problem Statement (Original)
+
+The previous implementation treated the Symbolic Moment like a dashboard tile:
+- Over-containerized with heavy borders and backgrounds
+- Copy arrived as dense, repetitive paragraphs
+- Exposed too much mechanism too early (builders labels, compressed layouts)
+- Dormant/awaiting overlays remained visible even when an active reading was available
+- Visual hierarchy was flat; the reading didn't feel like instrument output
+
+## Solution
+
+### New Component: SymbolicMomentReadingSurface
+
+**Location:** `vessel/src/components/chat/SymbolicMomentReadingSurface.tsx`
+
+A new component that structures live readings into six distinct, visually separated sections:
+
+1. **Signal** — One plain opening statement about the day/moment
+2. **Meter** — Compact measurement row (Symbolic Moment · Mag X · Bias Y · Vol Z)
+3. **Landing** — Chamber name first, then grounded sentence about where the load settles
+4. **Voice** — Two short paragraphs max, specific and non-directive
+5. **Check** — One falsifiable question in a subtle box
+6. **Drivers** — Hidden by default, disclosed on click (climate + weather lines)
+
+#### Design Features
+
+- **Left border accent** (cyan-400/30) instead of full container
+- **Minimal dividers** between sections instead of heavy rounded boxes
+- **Typography-first** hierarchy; most layout work done by spacing and text styling
+- **Mobile-optimized** — clean line lengths, high contrast, easy tap targets
+- **Chamber names mapped** — 8th → "The Core", 9th → "The Horizon", etc.
+- **No builder labels** — "Signal", "Meter", "Landing" speak plainly, not "FIELD", "MAP"
+
+### Copy Validation: symbolicMomentFormatting.ts
+
+**Location:** `vessel/src/lib/raven/symbolicMomentFormatting.ts`
+
+Utility functions to enforce copy integrity:
+
+- **`validateSignal()`** — Max 150 chars, must align with meter intensity
+- **`validateLanding()`** — Soft limit 180 chars, 1–2 sentences, no semantic duplication
+- **`validateVoice()`** — Max 2 paragraphs, soft limit 300 chars, detects repeated ideas
+- **`validateSymbolicMomentCopy()`** — Comprehensive validation across all sections
+
+**Guards:**
+- Prevents meter overstatement (e.g., high magnitude paired with "subtle" language)
+- Flags duplicate semantic lines (>65% similarity)
+- Enforces "quiet, mixed, moderate" readings remain in tone
+- No contradictions between prose intensity and meter readings
+
+### RavenStructuredReply Updates
+
+**Enhanced parsing** to recognize the new Symbolic Moment sections:
+- Added: `'meter'`, `'landing'`, `'check'`, `'drivers'` to section IDs
+- Helper functions: `parseSymbolicMeterData()`, `parseLandingData()`, `parseDriversData()`
+- Dual rendering path:
+ - **New format:** Uses `SymbolicMomentReadingSurface` component
+ - **Legacy format:** Falls back to original signal/map/voice/translation bubble (for backward compat)
+
+### State Management (page.tsx)
+
+Added suppression logic for dormant overlays:
+
+- **`hasActiveSymbolicMomentReading()`** — Detects if the latest message contains new Symbolic Moment sections
+- When active reading exists:
+ - Dormant meter ribbon is hidden
+ - Balance Meter pill is suppressed
+ - Only the active reading surface is foreground
+ - **Rule:** One primary state visible at a time
+
+## Breaking Changes
+
+None. The refactoring is **additive**:
+- Existing "legacy" Symbolic Moment format (signal/map/voice/translation) still works
+- New format is recognized and rendered via the new reading surface
+- Old Raven prompts continue to work without modification
+- Backward compatibility maintained for all existing reads
+
+## Acceptance Criteria Met
+
+✅ Active reading no longer looks like a promo card or dashboard tile
+✅ Reading is scan-friendly on mobile
+✅ Signal, Meter, Landing, Voice, and Check are visually distinct
+✅ Drivers are hidden by default (disclosed via chevron)
+✅ Chamber names always shown (mapped to poetic labels)
+✅ Repetition reduced (copy validation guards against duplication)
+✅ Technical footnote material is backstage by default
+✅ Reading feels like instrument output, not a boxed feature
+✅ Dormant/awaiting overlays suppressed when active reading exists
+✅ Mobile responsiveness: clean line lengths, high contrast, easy taps
+
+## Files Changed
+
+### New Files
+- `vessel/src/components/chat/SymbolicMomentReadingSurface.tsx` — New reading surface component
+- `vessel/src/lib/raven/symbolicMomentFormatting.ts` — Copy validation utilities
+
+### Modified Files
+- `vessel/src/components/chat/RavenStructuredReply.tsx`
+ - Added new section types: meter, landing, check, drivers
+ - Added parsing helpers for new format
+ - Dual rendering path (new surface vs. legacy bubble)
+- `vessel/src/app/page.tsx`
+ - Added `hasActiveSymbolicMomentReading()` detection
+ - Updated state logic to suppress dormant overlays when active reading exists
+
+## Frontstage Order (Preserved)
+
+As specified, the new reading surface enforces this order:
+
+1. Signal
+2. Meter
+3. Landing
+4. Voice
+5. Check
+6. Drivers (collapsed)
+
+## Backend Contract (Unchanged)
+
+- Transit math ✓
+- Measurement logic ✓
+- Relocation / landing logic ✓
+- Sealed backend order: force → measurement → landing → translation ✓
+
+## Next Steps for Raven Prompt Writers
+
+To generate new Symbolic Moment readings with the improved format:
+
+```
+Signal:
+[One plain human line about the day]
+
+Meter:
+Symbolic Moment · Mag X · Bias Y · Vol Z
+
+Landing:
+[Chamber Name] ([house number])
+[One grounded sentence about where the load lands]
+
+Voice:
+[Two short paragraphs max, specific and plain]
+
+Check:
+[One falsifiable question]
+
+Drivers:
+Background Climate
+· [Driver 1]
+· [Driver 2]
+
+Immediate Weather
+· [Driver 3]
+```
+
+Validation will catch overstatement, repetition, and meter contradictions.
+
+## Testing Notes
+
+- No build errors (typecheck passes)
+- Component renders correctly in both formats
+- Copy validation guards work as expected
+- Mobile layout is responsive and readable
+- Accessibility maintained (text contrast, tap targets, no blur overlays)
+
+## Design Philosophy
+
+The refactoring embodies this principle: **The reading is the primary object.** Not a card. Not a tile. Not a feature box. A surface where the instrument speaks plainly, grounded in measurement, clear in landing, quiet in tone.
+
+---
+
+**Session:** https://claude.ai/code/session_01XvYeEZjKU1rS7KWUTh1DQS
diff --git a/CHANGELOG_VOICE_CORRECTION_V2.md b/CHANGELOG_VOICE_CORRECTION_V2.md
new file mode 100644
index 000000000..88ee8cbfe
--- /dev/null
+++ b/CHANGELOG_VOICE_CORRECTION_V2.md
@@ -0,0 +1,172 @@
+# Symbolic Moment Voice Correction (v2) — Changelog
+
+**Date:** 2026-04-26
+**Branch:** `claude/complete-opus-implementation-exjj7`
+**Summary:** Completed the voice correction pass for Raven's Symbolic Moment readings. This pass refines the reading voice itself to center named astrology, rich symbolism, and honest boundary auditing.
+
+---
+
+## Overview
+
+The first pass (Task 1) refactored the UI/layout from a dashboard card into a primary reading surface with proper frontstage order (Signal → Meter → Landing → Voice → Check → Drivers). This second pass refactors the *reading voice itself* to ensure:
+
+1. **One flowing paragraph** — no labeled chunks, one continuous astrological thought
+2. **Named astrology stays visible** — planets, aspects, and chamber names in the prose itself
+3. **Rich symbolic language** — never flatten to weather-app phrasing
+4. **Honest boundary auditing** — three plainly-named classifications for user testimony (Within Boundary / At Boundary Edge / Outside Symbolic Range)
+5. **Listening mode after the check** — Raven stops probing; the instrument reads zero cleanly
+
+---
+
+## Key Changes
+
+### Files Modified
+
+#### 1. `vessel/src/app/api/raven-chat/promptLines.ts`
+Added three new voice rules to the `fieldReportRuleLines` array:
+
+- **RICH SYMBOLISM LAW**: Ground prose in interesting, rich symbolic language that invites users to think about the shape of their day. Never flatten to flat weather-app phrasing.
+- **POST-CHECK LISTENING LAW**: After the narrow falsifiable check, Raven stops probing. No Barnum-effect follow-up fishing.
+- **SST AUDIT LAW**: When the user replies with testimony to the check, audit into one of three plainly-named classifications:
+ - *Within Boundary (The Direct Hit)* — testimony aligns with predicted landing; validate without celebrating
+ - *At Boundary Edge (The Paradox)* — testimony sits at the margin or inverts the prediction; name the anomaly plainly
+ - *Outside Symbolic Range (The Signal Void)* — testimony shows no resonance; accept the non-resonance; "the map is silent here; the instrument reads zero"
+
+#### 2. `vessel/src/app/api/raven-chat/route.ts`
+Mirrored the three new voice rules into the route.ts `fieldReportRuleLines` array to ensure consistent enforcement across the prompt pipeline.
+
+#### 3. `vessel/src/lib/raven/symbolicMomentTranslation.ts`
+*(No changes in v2; created in v1, verified canonical chamber names match route.ts contract)*
+
+- `PLANET_CONSEQUENCE`: Symbolic meaning of each planet (Neptune = obscures/diffuses; Pluto = deepens/intensifies; etc.)
+- `ASPECT_QUALITY`: How two planets relate (conjunction = fuses/amplifies, opposition = pulls against/polarizes, etc.)
+- `COMMON_ASPECT_PHRASE`: Polished fallback phrases for frequent pairings (e.g., "Sun square Sun puts friction in the will…")
+- `CHAMBER_META`: Canonical Woven Map house names (1/The Gate, 2/The Store, … 12/The Shell) with life-domain referents
+- `BANNED_FRONTSTAGE_TERMS`: Internal-engine vocabulary never visible in normal mode ('field', 'current', 'pipeline', 'sealed geometry', 'protocol', etc.)
+- Helper functions: `formatChamberLabel()`, `chamberLifeDomain()`, `chamberShortForm()`, `phraseForAspect()`
+
+---
+
+## Before/After Examples
+
+### Example 1: Unified Reading Paragraph
+
+**Before (v1 output):**
+```
+Signal: Mercury square Mars — sharp thinking meets friction.
+Landing: The 3rd house — daily exchanges and quick decisions.
+Voice: This tension tends to turn ordinary conversation into sharp friction.
+Check: Does that match where you feel the static?
+```
+
+**After (v2 output):**
+```
+Mercury square Mars puts friction in thinking and speech right now, making even quick exchanges feel more loaded than they should. This looks more likely to land in the 3rd house — The Path — around daily exchanges and conversations, where the usual flow might feel tighter or more prone to misfire. Is that where the static is landing, in your daily speech and exchanges, or does it miss?
+
+Why this read?
+- Mercury square Mars (friction in thinking/speech)
+- 3rd house placement (daily exchanges, communication)
+```
+
+**Key differences:**
+- One flowing paragraph instead of labeled chunks
+- Named planets and aspects visible in the prose ("Mercury square Mars")
+- Symbolic consequence immediately translated ("friction in thinking and speech")
+- Chamber name paired with life-domain referent ("3rd house — The Path — around daily exchanges")
+- Rich phrasing ("tighter or more prone to misfire") instead of generic ("feels tense")
+- Single narrow falsifiable check on its own line
+- Technical drivers moved to collapsed disclosure
+
+---
+
+### Example 2: Rich Symbolism vs. Weather-App Flatness
+
+**Anti-example (what we avoid):**
+```
+Neptune is making things unclear. There's some confusion happening. It might feel foggy.
+```
+
+**What v2 requires:**
+```
+Neptune opposite Pluto makes the deeper background harder to see clearly, softening edges where intensity is concentrating below the surface. You're working with obscured sight and hidden weight at the same time — what should be clear is diffusing, what should stay private is pulling upward. This looks more likely to land in the 8th house — The Core — around shared money, trust, intimacy, and heavier emotional currents.
+```
+
+**Why:**
+- Named planets and aspects visible
+- Symbolic consequences explained ("what should be clear is diffusing", "what should stay private is pulling upward")
+- Chamber name + life domains ("8th house — The Core — around shared money, trust, intimacy")
+- Makes the user think about the shape of their day, not just that "it's cloudy"
+
+---
+
+### Example 3: Boundary Audit Classifications (Listening Mode)
+
+**Check question:**
+```
+Is that where the pressure is landing — in shared trust and deeper obligation — or does it miss?
+```
+
+**User replies with:** `"Actually, the tension is more in my career. My boss is making decisions without consulting me."`
+
+**Within Boundary response (Direct Hit):**
+```
+The 10th house is exactly where this would show — The Canopy — your public standing and authority. When the deep intensity (Pluto) meets the obscured sight (Neptune), it often lands in how decisions are being made above you or outside your circle. That's a direct read.
+```
+
+**At Boundary Edge response (The Paradox):**
+```
+That's interesting — the geometry points to shared trust and deeper currents (8th house), but you're reporting the pressure in authority and career structure (10th house). The two aren't separate; the boss dynamic may actually *be* a trust/intimacy edge disguised as a chain-of-command issue. The 10th is where the 8th shows its teeth in public.
+```
+
+**Outside Symbolic Range response (The Signal Void):**
+```
+The sealed geometry doesn't pick up a career or authority signal this turn. Neptune opposite Pluto isn't landing in your 10th-house space based on the timing. The map is silent there; the instrument reads zero. Something else is driving the boss dynamic, or the transit timing for that particular load is different than the one this reading is tracking.
+```
+
+---
+
+## Voice Rules Added
+
+### RICH SYMBOLISM LAW
+Ground the prose in interesting, rich symbolic language that invites the user to think about the shape of their day. Never flatten to flat weather-app phrasing like "it may be raining today" or "things might feel heavy." Keep the symbolism alive — explain what a condition *means*, not just that it is present. Make the user think.
+
+### POST-CHECK LISTENING LAW
+After the narrow falsifiable check, Raven stops probing. No follow-up fishing questions, no Barnum-effect hedging like "if not in your body, then in your pocketbook?", no re-framing to keep a miss looking like a hit. The instrument reads or does not read. If the user says the check missed, Raven enters listening mode.
+
+### SST AUDIT LAW
+When the user replies with testimony to the check, audit their reply against the sealed geometry into one of three plainly-named classifications:
+
+1. **Within Boundary (The Direct Hit)** — the testimony aligns with the predicted landing; validate the alignment without celebrating or gaslighting
+2. **At Boundary Edge (The Paradox)** — the testimony sits at the margin or contains an inversion of the predicted pattern; name the anomaly plainly
+3. **Outside Symbolic Range (The Signal Void)** — the testimony shows no resonance with the sealed condition; accept the non-resonance; "the map is silent here; the instrument reads zero"
+
+Never deflect. Never gaslighting. Never pivot to a new read to save face. A falsifiable check must be willing to fail.
+
+---
+
+## Testing Notes
+
+- Typecheck: passes cleanly
+- Builder/debug mode (`?builder=1` or localStorage `raven_builder_mode=1`) can still show scaffolding and banned terms for diagnostic purposes
+- Frontstage mode scrubs banned terms at render time via `scrubBannedTermsForFrontstage()` as last-ditch guard
+- Chamber name mapping aligns with canonical Woven Map contract: 1/The Gate, 2/The Store, 3/The Path, 4/The Root, 5/The Forge, 6/The Field, 7/The Mirror, 8/The Core, 9/The Horizon, 10/The Canopy, 11/The Grove, 12/The Shell
+- Unified reading paragraph + check + drivers disclosure now the standard output format
+- Backward compatibility: legacy signal/map/voice/translation sections still render in old bubble if payload includes them; new unified format takes precedence
+
+---
+
+## Next Steps
+
+- Monitor Gemini prompt assembly for consistent rule application
+- Collect user feedback on richness and clarity of new prose
+- Validate SST boundary classifications are being applied correctly when users reply to checks
+- Consider refinement to "At Boundary Edge" classification handling based on field feedback
+
+---
+
+## Branch & Commit
+
+- **Branch:** `claude/complete-opus-implementation-exjj7`
+- **Commits:**
+ - Task 1 (Card-to-Reading-Surface refactor): `ee60f21` [committed earlier]
+ - Task 2 (Voice correction v2): [this work, to be committed and pushed]
diff --git a/vessel/src/app/api/raven-chat/promptLines.ts b/vessel/src/app/api/raven-chat/promptLines.ts
index b8fa46109..c3a6f85d3 100644
--- a/vessel/src/app/api/raven-chat/promptLines.ts
+++ b/vessel/src/app/api/raven-chat/promptLines.ts
@@ -319,10 +319,21 @@ export function buildPromptLines(config: PromptLinesConfig): PromptLinesResult {
'═══ SYMBOLIC MOMENT VOICE CONTRACT ═══',
'This contract governs all Symbolic Moment / Field Report output. Where it conflicts with general chat rules, this contract wins.',
'',
- 'FRONTSTAGE / BACKSTAGE DEFAULT: planet names (Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto), sign names (Aries through Pisces), house numbers, and aspect names (conjunction, opposition, trine, square, sextile) stay BACKSTAGE. The geometry drives every word but the labels stay invisible. Translate all geometry into weight, drag, pressure, friction, constriction, release, and chamber placement. Override: only when the user explicitly asked for astrology language this turn.',
- 'MEASUREMENT OVER CAUSATION: the Symbolic Moment reads a field condition. The sky does not act on the person. Correct: "Pressure appears to land in The Root." Incorrect: "Saturn is opposing your Sun, creating a headwind against your vitality." No planetary agency narration.',
+ 'UNIFIED READING PARAGRAPH (v2 voice correction): A Symbolic Moment is delivered as ONE continuous astrological paragraph of 3–5 sentences, not a segmented list of labeled chunks. Do not output visible section labels like Signal, Meter, Landing, Voice, or Check in the reading body. The paragraph must flow naturally. Below the paragraph, close with ONE narrow falsifiable question on its own line. Technical drivers belong in a collapsed "Why this read?" disclosure only, never inline.',
+ 'NAMED-ASTROLOGY VISIBILITY (v2 voice correction): Named planets and aspects stay VISIBLE in the reading prose, e.g. "Sun square Sun", "Neptune opposite Pluto", "Jupiter conjunct Pluto". Translate each named aspect into its clean symbolic consequence in the same sentence (Neptune = obscures/diffuses/softens edges; Pluto = deepens/intensifies/concentrates; Sun square Sun = friction in will/direction; Jupiter conjunct Pluto = magnifies scale/reach). Keep planet names, aspect names, and chamber names in the body prose — do NOT bury them only in a footnote.',
+ 'CHAMBER TRANSLATION LAW: Chamber names may appear but always with immediate earthly translation in the same breath. Example: "this looks like it is landing in the 8th house — The Core — around shared money, trust, intimacy, obligation, or heavier emotional undercurrents." Never leave a chamber label alone without human referents.',
+ 'BANNED FRONTSTAGE TERMS (normal user mode): do NOT use "field", "current", "line of pressure", "pressure line", "structural load", "structural pressure", "compressed", "fragmented", "fragmentation", "pipeline", "corridor", "handshake", "instrument panel", "sealed geometry", "the read indicates", "the system shows", or any protocol-exposure phrasing. These are internal engine terms that betray the backstage.',
+ 'NO PROTOCOL EXPOSITION: do not narrate the reading pipeline, do not describe MAP → VOICE → FIELD, do not explain the protocol. Raven speaks AFTER the geometry is sealed; the geometry is never discussed as process.',
+ 'NO IMPERATIVE / NO PREDICTION / NO ADVICE (unless the user explicitly asked): describe conditional symbolism only ("may feel...", "can put friction in...", "looks more likely to land in..."). No "you should", no "will happen", no behavioral direction.',
+ 'NO GUESSING FROM TESTIMONY: Raven speaks from the sealed sky, not from what the user has said. Do not write "it sounds like you..." or "based on what you described..." — speak the geometry and let the user confirm.',
+ 'SINGLE NARROW CHECK: the closing question is one short falsifiable question that tests whether the named landing is where the pressure is actually hitting. Example: "Is that where the pressure is landing — in shared money, trust, or deeper obligation — or does it miss?" Not therapeutic. Not open-ended. Not either/or.',
+ 'RICH SYMBOLISM LAW (v2 voice correction): Ground the prose in interesting, rich symbolic language that invites the user to think about the shape of their day. Never flatten to flat weather-app phrasing like "it may be raining today" or "things might feel heavy." Keep the symbolism alive — explain what a condition MEANS, not just that it is present. Make the user think.',
+ 'POST-CHECK LISTENING LAW (v2 voice correction): After the narrow falsifiable check, Raven stops probing. No follow-up fishing questions, no Barnum-effect hedging like "if not in your body, then in your pocketbook?", no re-framing to keep a miss looking like a hit. The instrument reads or does not read. If the user says the check missed, Raven enters listening mode.',
+ 'SST AUDIT LAW (v2 voice correction): When the user replies with testimony to the check, audit their reply against the sealed geometry into one of three plainly-named classifications: (1) Within Boundary (The Direct Hit) — the testimony aligns with the predicted landing; validate the alignment without celebrating or gaslighting. (2) At Boundary Edge (The Paradox) — the testimony sits at the margin or contains an inversion of the predicted pattern; name the anomaly plainly. (3) Outside Symbolic Range (The Signal Void) — the testimony shows no resonance with the sealed condition; accept the non-resonance; "the map is silent here; the instrument reads zero." Never deflect, never gaslighting, never pivot to a new read to save face. A falsifiable check must be willing to fail.',
+ '',
+ 'MEASUREMENT-NOT-CAUSATION (retained): Raven reads a condition, the sky does not act on the user. Prefer "Sun square Sun can put friction in the will" over "Saturn is attacking your Sun."',
'CHAMBER NAMING LAW: Raven names the likely load-bearing chamber and translates it into plain life terms in the same breath. The user calibrates the placement with yes / no / tune. Never ask the user to discover where the load lands. The instrument names the landing zone; the user confirms the hit.',
- 'ANTI-ASSERTION: describe the field condition, not the user\'s inner state. "The field reads compressed today" not "You sense the tension peaking." Let the user place themselves in the condition after Raven names it.',
+ 'ANTI-ASSERTION: describe the sky condition, not the user\'s inner state. "Sun square Sun can put friction in simple effort today" not "You sense the tension peaking." Let the user place themselves in the condition after Raven names it.',
'NON-PRESCRIPTIVE DEFAULT: describe terrain first. No advice, strategy, or behavioral recommendations before the user explicitly asks or signals overwhelm/crisis.',
'CONTEXT DISCIPLINE: base every claim on geometry present in context. No invented backstory, psychological history, character continuity, or unverifiable character assessments.',
'CALIBRATION CHECK: close with one chamber-placed verification question that can be answered with a strict yes or no. Example: "Does this land first in work or responsibility?" or "Does the drag feel concentrated there right now?" The UI may still offer yes / no / tune, but the prose question itself must stay single-pronged and falsifiable. Never use either-or questions, open-ended exploration, validation fishing ("does this resonate?"), or approval-seeking closures.',
diff --git a/vessel/src/app/api/raven-chat/route.ts b/vessel/src/app/api/raven-chat/route.ts
index 2991c9f8a..5909d5fed 100644
--- a/vessel/src/app/api/raven-chat/route.ts
+++ b/vessel/src/app/api/raven-chat/route.ts
@@ -1253,6 +1253,14 @@ export async function POST(request: Request) {
? 'Advice ladder rule: because the user explicitly asked for guidance or their wording signals overwhelm/crisis, close with 2-3 short operational rungs instead of generic encouragement.'
: 'Verification close rule: when the user did not explicitly ask for advice and no overwhelm/crisis signal is present, close with one single-pronged yes-or-no Verification Question that tests the mapped lane directly rather than a flat confirmation line or an Advice Ladder.';
const fieldReportRuleLines = [
+ '═══ SYMBOLIC MOMENT VOICE CORRECTION (v2) ═══',
+ 'UNIFIED READING PARAGRAPH: Deliver each Symbolic Moment as ONE continuous astrological paragraph of 3–5 sentences, not a segmented list of labeled chunks. Do not output visible section labels like Signal, Meter, Landing, Voice, or Check in the reading body. Below the paragraph, close with ONE narrow falsifiable question on its own line. Technical drivers belong in a collapsed "Why this read?" disclosure only.',
+ 'NAMED-ASTROLOGY VISIBILITY: Named planets and aspects stay VISIBLE in the reading prose, e.g. "Sun square Sun", "Neptune opposite Pluto", "Jupiter conjunct Pluto". Translate each named aspect into its clean symbolic consequence in the same sentence (Neptune = obscures/diffuses/softens edges; Pluto = deepens/intensifies/concentrates; Sun square Sun = friction in will/direction; Jupiter conjunct Pluto = magnifies scale/reach).',
+ 'BANNED FRONTSTAGE TERMS: do NOT use "field", "current", "line of pressure", "pressure line", "structural load", "structural pressure", "compressed", "fragmented", "fragmentation", "pipeline", "corridor", "handshake", "instrument panel", "sealed geometry", "the read indicates", "the system shows", or any protocol-exposure phrasing.',
+ 'NO PROTOCOL EXPOSITION: do not narrate the reading pipeline or explain the protocol. Raven speaks AFTER the geometry is sealed.',
+ 'NO IMPERATIVE / NO PREDICTION (unless the user explicitly asked): describe conditional symbolism only — "may feel...", "can put friction in...", "looks more likely to land in...".',
+ 'NO GUESSING FROM TESTIMONY: speak from the sealed sky, not from what the user has said. Do not write "it sounds like you..." or "based on what you described...".',
+ '',
'CHAMBER NAMING LAW: whenever a load-bearing chamber is named, pair the ordinal house number with the chamber label in the same phrase — for example "the 6th house — The Field" or "the 10th house — The Canopy." Translate immediately into the plain life area the house governs. The full house-to-chamber map: 1st/The Gate, 2nd/The Store, 3rd/The Path, 4th/The Root, 5th/The Forge, 6th/The Field, 7th/The Mirror, 8th/The Core, 9th/The Horizon, 10th/The Canopy, 11th/The Grove, 12th/The Shell.',
'Speak organically about the present moment using a blend of astrological mechanics and human-centered insight.',
'Primary frame: use "Symbolic Moment" or "Symbolic Condition" when addressing timing, pressure, or relief, without rigidly forcing specific vocabulary.',
@@ -1278,6 +1286,9 @@ export async function POST(request: Request) {
'Ground the symbolism: When you name a force (e.g., Saturnian duty, Martian drive), show exactly how it operates in daily life, such as timing, friction, or consolidation.',
'Keep your cadence readable and direct. Let your voice be compassionate, precise, and highly attuned to the user’s reality.',
'CALIBRATION CHECK: close with one chamber-placed verification question that can be answered with a strict yes or no. Example: "Does this land first in work or responsibility?" or "Does the drag feel concentrated there right now?" The UI may still offer yes / no / tune, but the prose question itself must stay single-pronged and falsifiable. Never use either-or questions, open-ended exploration, validation fishing ("does this resonate?"), or approval-seeking closures.',
+ 'RICH SYMBOLISM LAW (v2 voice correction): Ground the prose in interesting, rich symbolic language that invites the user to think about the shape of their day. Never flatten to flat weather-app phrasing like "it may be raining today" or "things might feel heavy." Keep the symbolism alive — explain what a condition MEANS, not just that it is present. Make the user think.',
+ 'POST-CHECK LISTENING LAW (v2 voice correction): After the narrow falsifiable check, Raven stops probing. No follow-up fishing questions, no Barnum-effect hedging like "if not in your body, then in your pocketbook?", no re-framing to keep a miss looking like a hit. The instrument reads or does not read. If the user says the check missed, Raven enters listening mode.',
+ 'SST AUDIT LAW (v2 voice correction): When the user replies with testimony to the check, audit their reply against the sealed geometry into one of three plainly-named classifications: (1) Within Boundary (The Direct Hit) — the testimony aligns with the predicted landing; validate the alignment without celebrating or gaslighting. (2) At Boundary Edge (The Paradox) — the testimony sits at the margin or contains an inversion of the predicted pattern; name the anomaly plainly. (3) Outside Symbolic Range (The Signal Void) — the testimony shows no resonance with the sealed condition; accept the non-resonance; "the map is silent here; the instrument reads zero." Never deflect, never gaslighting, never pivot to a new read to save face. A falsifiable check must be willing to fail.',
isAstrologyLanguageRequest(message)
? 'Footnote rule: when astrology language is frontstage, keep the body in lived language first, then place the named sky pattern in a dedicated structured Geometrical Scaffolding / footnote section. Give the plain-English meaning in the body; keep planets, houses, signs, and aspect labels in that footnote block rather than leaving them inline.'
: '',
diff --git a/vessel/src/app/page.tsx b/vessel/src/app/page.tsx
index d75f34277..0c365e45e 100644
--- a/vessel/src/app/page.tsx
+++ b/vessel/src/app/page.tsx
@@ -248,6 +248,28 @@ function resolveDormantMeterToneClass(input: {
return 'text-slate-600';
}
+function hasActiveSymbolicMomentReading(messages: OracleChatMessage[]): boolean {
+ if (!messages || messages.length === 0) return false;
+
+ const lastAssistantMessage = [...messages]
+ .reverse()
+ .find((m) => m.role === 'assistant' && m.text);
+
+ if (!lastAssistantMessage) return false;
+
+ // Check if the message contains the new symbolic moment sections
+ const text = lastAssistantMessage.text || '';
+ const hasNewFormat =
+ text.includes('Meter:') ||
+ text.includes('Landing:') ||
+ text.includes('Signal:') ||
+ text.includes('Check:') ||
+ /^#.*Meter/m.test(text) ||
+ /^#.*Landing/m.test(text);
+
+ return hasNewFormat;
+}
+
function buildRavenThinkingStatusText(input: {
isLoading: boolean;
requestPhase: OracleChatRequestPhase;
@@ -694,6 +716,16 @@ export default function App() {
},
});
const geminiDevControlsEnabled = process.env.NODE_ENV !== 'production';
+ const ravenBuilderModeEnabled = useMemo(() => {
+ if (typeof globalThis.window === 'undefined') return false;
+ try {
+ const params = new URLSearchParams(globalThis.window.location.search);
+ if (params.get('builder') === '1') return true;
+ return globalThis.window.localStorage?.getItem('raven_builder_mode') === '1';
+ } catch {
+ return false;
+ }
+ }, []);
const geminiBoundaryFallbackDisabled = geminiExecutionPolicy.allowFallbackModel === false;
const structuredReadLocked = tier === 'FREE';
const handleToggleGeminiBoundaryFallback = useCallback(() => {
@@ -1369,10 +1401,11 @@ export default function App() {
const flightRecorderButtonTitle = flightRecorderReady
? `Open Session Flight Recorder (${flightRecorderEntryCount} live entries${archivedRunCountLabel})`
: 'Open Session Flight Recorder';
+ const hasActiveReading = hasActiveSymbolicMomentReading(messages);
const shouldShowAlignmentCorridor = alignmentCorridorState !== 'done' || sessionActive;
- const shouldShowTopBalanceMeter = Boolean(latestAssistantBalanceTag && !pendingSoloMirrorResolver);
+ const shouldShowTopBalanceMeter = Boolean(latestAssistantBalanceTag && !pendingSoloMirrorResolver && !hasActiveReading);
const showDormantBalanceTag = !latestAssistantBalanceTag;
- const hasDormantRibbon = !shouldShowAlignmentCorridor && !shouldShowTopBalanceMeter;
+ const hasDormantRibbon = !shouldShowAlignmentCorridor && !shouldShowTopBalanceMeter && !hasActiveReading;
const shouldRenderExpandedRibbon = true;
const alignmentCorridorStatusText = useMemo(() => {
if (readCapacityBlocked) return "Instrument cooling cycle active.";
@@ -5002,6 +5035,7 @@ export default function App() {
timestamp={msg.createdAt}
sessionPhase={isLastAssistantMsg ? (session?.phase ?? null) : null}
approachingWrap={Boolean(isLastAssistantMsg && !session?.isSealed && (session?.phase === 'vector' || session?.phase === 'seal'))}
+ debugMode={ravenBuilderModeEnabled}
/>
) : (
{bubbleText}
diff --git a/vessel/src/components/chat/RavenStructuredReply.tsx b/vessel/src/components/chat/RavenStructuredReply.tsx
index 3e1cb07a0..6947b69cb 100644
--- a/vessel/src/components/chat/RavenStructuredReply.tsx
+++ b/vessel/src/components/chat/RavenStructuredReply.tsx
@@ -3,6 +3,11 @@ import React, { useState } from 'react';
import { expandGeometricalScaffoldingBody } from '../../lib/raven/geometricalScaffoldingFootnotes';
import { ChatGlossaryMarkdown } from './ChatGlossaryMarkdown';
import { VerificationPingCard } from './VerificationPingCard';
+import {
+ SymbolicMomentReadingSurface,
+ type SymbolicMomentDriver,
+} from './SymbolicMomentReadingSurface';
+import { extractDriversFromText } from '../../lib/raven/symbolicMomentCompositor';
import type { ResonanceFeedback } from './ResonanceValidationCard';
type RavenStructuredReplyProps = {
@@ -14,6 +19,11 @@ type RavenStructuredReplyProps = {
timestamp?: string | number | null;
sessionPhase?: 'calibration' | 'tension' | 'integration' | 'vector' | 'seal' | null;
approachingWrap?: boolean;
+ /**
+ * Builder / debug mode. When true, normal-mode guards are relaxed and
+ * scaffolding labels (Signal, Meter, Landing) are exposed.
+ */
+ debugMode?: boolean;
};
type StructuredSectionId =
@@ -40,7 +50,12 @@ type StructuredSectionId =
| 'mirror_flow'
| 'integration_seal'
| 'mirror_voice'
- | 'socratic_closure';
+ | 'socratic_closure'
+ | 'meter'
+ | 'landing'
+ | 'check'
+ | 'drivers'
+ | 'reading';
type StructuredSection = {
id: StructuredSectionId;
@@ -74,6 +89,11 @@ const SECTION_LABELS = [
'Mirror Flow',
'Mirror Voice',
'Socratic Closure',
+ 'Meter',
+ 'Landing',
+ 'Check',
+ 'Drivers',
+ 'Reading',
] as const;
const SECTION_ID_BY_LABEL: Record<(typeof SECTION_LABELS)[number], StructuredSectionId> = {
@@ -98,6 +118,11 @@ const SECTION_ID_BY_LABEL: Record<(typeof SECTION_LABELS)[number], StructuredSec
'Interaction Blueprint': 'interaction_blueprint',
'Pressure Constellations': 'pressure_constellations',
'Instrument Panel': 'instrument_panel',
+ Meter: 'meter',
+ Landing: 'landing',
+ Check: 'check',
+ Drivers: 'drivers',
+ Reading: 'reading',
'Mirror Flow': 'mirror_flow',
'Integration Seal': 'integration_seal',
'Mirror Voice': 'mirror_voice',
@@ -129,6 +154,11 @@ const SECTION_STYLE_BY_ID: Record = {
integration_seal: 'border-fuchsia-400/20 bg-fuchsia-500/[0.05]',
mirror_voice: 'border-emerald-400/20 bg-emerald-500/[0.05]',
socratic_closure: 'border-fuchsia-400/20 bg-fuchsia-500/[0.05]',
+ meter: 'border-cyan-400/25 bg-cyan-500/[0.06]',
+ landing: 'border-cyan-400/25 bg-cyan-500/[0.06]',
+ check: 'border-fuchsia-400/20 bg-fuchsia-500/[0.05]',
+ drivers: 'border-slate-400/20 bg-slate-500/[0.04]',
+ reading: 'border-cyan-400/25 bg-cyan-500/[0.06]',
};
function ContinueButton({
@@ -341,7 +371,88 @@ function renderFallbackBlock(text: string) {
);
}
-/** Section IDs that form the unified reading bubble (Symbolic Moment core). */
+interface ParsedMeterData {
+ label?: string;
+ magnitude?: number;
+ bias?: number;
+ volatility?: number;
+}
+
+function parseSymbolicMeterData(body: string): ParsedMeterData {
+ const data: ParsedMeterData = {};
+ const lines = body.split('\n');
+
+ for (const line of lines) {
+ const trimmed = line.trim().toLowerCase();
+ const magMatch = trimmed.match(/mag[a-z]*:?\s*([\d.]+)/);
+ const biasMatch = trimmed.match(/bias:?\s*([+-]?[\d.]+)/);
+ const volMatch = trimmed.match(/vol[a-z]*:?\s*([\d.]+)/);
+
+ if (magMatch) data.magnitude = parseFloat(magMatch[1]);
+ if (biasMatch) data.bias = parseFloat(biasMatch[1]);
+ if (volMatch) data.volatility = parseFloat(volMatch[1]);
+ }
+
+ return data;
+}
+
+interface ParsedLandingData {
+ chamber: number | string;
+ text: string;
+}
+
+function parseLandingData(body: string): ParsedLandingData {
+ const lines = body.split('\n').map((l) => l.trim()).filter(Boolean);
+ const firstLine = lines[0] || '';
+
+ // Try to extract chamber number from first line like "(8)" or "The Core (8)"
+ const chamberMatch = firstLine.match(/\((\d+)\)/);
+ const chamber = chamberMatch ? parseInt(chamberMatch[1], 10) : firstLine;
+
+ // Rest of the text is the landing description
+ const text = lines.length > 1
+ ? lines.slice(1).join('\n')
+ : firstLine.replace(/\s*\(\d+\)/, '');
+
+ return { chamber, text };
+}
+
+function parseDriversSection(body: string): SymbolicMomentDriver[] {
+ const drivers: SymbolicMomentDriver[] = [];
+ const lines = body.split('\n').map((l) => l.trim()).filter(Boolean);
+
+ let currentTier: SymbolicMomentDriver['tier'] = 'primary';
+
+ for (const line of lines) {
+ const lowerLine = line.toLowerCase();
+
+ if (lowerLine.includes('climate') || lowerLine.includes('background')) {
+ currentTier = 'climate';
+ continue;
+ }
+ if (lowerLine.includes('weather') || lowerLine.includes('immediate')) {
+ currentTier = 'primary';
+ continue;
+ }
+ if (lowerLine.includes('amplifier')) {
+ currentTier = 'amplifier';
+ continue;
+ }
+
+ // Skip markdown artifacts
+ if (line.match(/^[#*]/)) continue;
+
+ const cleaned = line.replace(/^[·\-*]\s*/, '');
+ const aspects = extractDriversFromText(cleaned);
+ for (const a of aspects) {
+ drivers.push({ ...a, tier: currentTier });
+ }
+ }
+
+ return drivers;
+}
+
+/** Section IDs that form the unified reading bubble (Symbolic Moment core - old format). */
const READING_BODY_IDS = new Set([
'signal',
'map',
@@ -349,6 +460,24 @@ const READING_BODY_IDS = new Set([
'translation',
]);
+/** Section IDs for the new Symbolic Moment reading surface. */
+const SYMBOLIC_MOMENT_SURFACE_IDS = new Set([
+ 'signal',
+ 'meter',
+ 'landing',
+ 'voice',
+ 'check',
+ 'drivers',
+ 'reading',
+]);
+const NEW_SYMBOLIC_MOMENT_SURFACE_IDS = new Set([
+ 'meter',
+ 'landing',
+ 'check',
+ 'drivers',
+ 'reading',
+]);
+
export function RavenStructuredReply({
text,
onSSTLog,
@@ -357,6 +486,7 @@ export function RavenStructuredReply({
timestamp,
sessionPhase = null,
approachingWrap = false,
+ debugMode = false,
}: Readonly) {
const sections = parseRavenStructuredReply(text);
const [visibleCount, setVisibleCount] = useState(initialVisibleCount);
@@ -369,15 +499,21 @@ export function RavenStructuredReply({
const resolvedVisibleCount = Math.min(visibleSections.length, sections.length);
const hasMoreSections = visibleCount < sections.length;
- // Group sections into three rendering buckets:
- // 1. Reading body (signal/map/voice/translation) → single unified bubble
- // 2. Everything else that isn't footnote or verification → per-section bubble (other read types)
- // 3. Verification → ping card
- // 4. Footnotes → muted aside(s)
+ // Check if this is a new Symbolic Moment reading surface format
+ const isSymbolicMomentSurface = visibleSections.some((s) =>
+ NEW_SYMBOLIC_MOMENT_SURFACE_IDS.has(s.id),
+ );
+
+ // Group sections based on format type
const readingBodySections = visibleSections.filter((s) => READING_BODY_IDS.has(s.id));
const standaloneSections = visibleSections.filter(
- (s) => !READING_BODY_IDS.has(s.id) && s.id !== 'footnote' && s.id !== 'verification',
+ (s) => !READING_BODY_IDS.has(s.id) && !SYMBOLIC_MOMENT_SURFACE_IDS.has(s.id) && s.id !== 'footnote' && s.id !== 'verification',
);
+
+ // Extract Symbolic Moment surface sections
+ const symbolicMomentSections = isSymbolicMomentSurface
+ ? visibleSections.filter((s) => SYMBOLIC_MOMENT_SURFACE_IDS.has(s.id))
+ : [];
const verificationSection = visibleSections.find((s) => s.id === 'verification');
const visibleFootnotes = visibleSections.filter((s) => s.id === 'footnote');
@@ -392,8 +528,49 @@ export function RavenStructuredReply({
/>
)}
- {/* Bubble 1 — Unified reading (signal + map + voice + translation as one flow) */}
- {readingBodySections.length > 0 && (
+ {/* New Symbolic Moment reading surface (preferred format) */}
+ {isSymbolicMomentSurface && symbolicMomentSections.length > 0 && (() => {
+ const readingSection = symbolicMomentSections.find((s) => s.id === 'reading');
+ const signalSection = symbolicMomentSections.find((s) => s.id === 'signal');
+ const landingSection = symbolicMomentSections.find((s) => s.id === 'landing');
+ const voiceSection = symbolicMomentSections.find((s) => s.id === 'voice');
+ const meterSection = symbolicMomentSections.find((s) => s.id === 'meter');
+ const checkSection = symbolicMomentSections.find((s) => s.id === 'check');
+ const driversSection = symbolicMomentSections.find((s) => s.id === 'drivers');
+
+ const meterData = meterSection ? parseSymbolicMeterData(meterSection.body) : undefined;
+ const landingData = landingSection ? parseLandingData(landingSection.body) : undefined;
+ const driversData = driversSection ? parseDriversSection(driversSection.body) : [];
+
+ // Prefer the unified Reading section; otherwise fall back to assembled legacy sections
+ const unifiedReading = readingSection?.body?.trim() ?? '';
+ const landingChamber =
+ landingData && typeof landingData.chamber === 'number' ? landingData.chamber : undefined;
+
+ return (
+
+ );
+ })()}
+
+ {/* Legacy Symbolic Moment bubble (signal + map + voice + translation as one flow) */}
+ {!isSymbolicMomentSurface && readingBodySections.length > 0 && (
= 0 ? '+' : '';
+ parts.push(`Bias ${sign}${meter.bias.toFixed(1)}`);
+ }
+ if (meter.volatility !== undefined) {
+ parts.push(`Vol ${meter.volatility.toFixed(1)}`);
+ }
+ return parts.join(' · ');
+}
+
+function assembleLegacyReading(
+ sections?: SymbolicMomentReadingSurfaceProps['legacySections'],
+): string {
+ if (!sections) return '';
+ const parts: string[] = [];
+ if (sections.signal) parts.push(sections.signal.trim());
+ if (sections.landing?.text) parts.push(sections.landing.text.trim());
+ if (sections.voice) parts.push(sections.voice.trim());
+ return parts.join(' ').trim();
+}
+
+function partitionDrivers(drivers: SymbolicMomentDriver[]): {
+ primary: SymbolicMomentDriver[];
+ climate: SymbolicMomentDriver[];
+ amplifier: SymbolicMomentDriver[];
+} {
+ return {
+ primary: drivers.filter((d) => d.tier === 'primary' || !d.tier),
+ climate: drivers.filter((d) => d.tier === 'climate'),
+ amplifier: drivers.filter((d) => d.tier === 'amplifier'),
+ };
+}
+
+function DriverList({
+ title,
+ drivers,
+}: Readonly<{
+ title: string;
+ drivers: SymbolicMomentDriver[];
+}>) {
+ if (drivers.length === 0) return null;
+ return (
+