From 4cee8ec6d6ef134a3d7578de08ed0f406fa223c3 Mon Sep 17 00:00:00 2001 From: Keith Avery Date: Tue, 14 Apr 2026 14:58:13 -0400 Subject: [PATCH 1/4] test(37-12): add failing tests for narrator confrontation re-declaration Source-scan tests on dispatch/prompt.rs verify the seven ACs for Story 37-12. Follows the Story 28-4 pattern (include_str! scan) since DispatchContext is too heavy to instantiate in integration tests. All 7 tests fail RED as expected: - prompt_no_longer_tells_narrator_to_only_emit_on_start - prompt_includes_transition_confrontation_section_marker - prompt_instructs_narrator_to_reemit_on_scene_shift - transition_block_iterates_confrontation_defs - prompt_emits_transition_guidance_otel_event - otel_transition_event_carries_alternative_count_field - transition_guidance_is_below_build_prompt_context_declaration 412 unrelated tests still passing. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tests/integration/main.rs | 1 + ...r_confrontation_redef_story_37_12_tests.rs | 237 ++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 crates/sidequest-server/tests/integration/narrator_confrontation_redef_story_37_12_tests.rs diff --git a/crates/sidequest-server/tests/integration/main.rs b/crates/sidequest-server/tests/integration/main.rs index 82b6cfca..f15cd3d4 100644 --- a/crates/sidequest-server/tests/integration/main.rs +++ b/crates/sidequest-server/tests/integration/main.rs @@ -26,6 +26,7 @@ mod lore_retrieval_story_18_4_tests; mod map_telemetry_wiring_tests; mod narration_single_send_wiring_tests; mod narrative_persist_story_15_29_tests; +mod narrator_confrontation_redef_story_37_12_tests; mod npc_registry_chargen_isolation_playtest_2026_04_11; mod npc_turns_beat_system_story_28_8_tests; mod ocean_shift_wiring_story_15_25_tests; diff --git a/crates/sidequest-server/tests/integration/narrator_confrontation_redef_story_37_12_tests.rs b/crates/sidequest-server/tests/integration/narrator_confrontation_redef_story_37_12_tests.rs new file mode 100644 index 00000000..53f7d295 --- /dev/null +++ b/crates/sidequest-server/tests/integration/narrator_confrontation_redef_story_37_12_tests.rs @@ -0,0 +1,237 @@ +//! Story 37-12: Narrator never re-declares confrontation after first emission +//! +//! # Background +//! +//! Story 37-13 built the encounter creation gate (`dispatch/encounter_gate.rs`), +//! which covers six observable cases for how to route a narrator-emitted +//! `"confrontation": ` signal against the current `snapshot.encounter` +//! state (Created, Redeclared, ReplacedPreBeat, RejectedMidEncounter, +//! UnknownType). The dispatch side is READY to receive re-emits from the +//! narrator. +//! +//! 37-12 is the prompt-side half: the narrator is never ASKED to re-emit. +//! Three concrete regressions in `dispatch/prompt.rs`: +//! +//! 1. Line 421 (current) contains the active misdirection +//! `"Only emit confrontation on the turn the encounter STARTS."` — the +//! exact inverse of the 37-13 gate's contract. The narrator reads the +//! prompt, sees this instruction, and dutifully stays silent through +//! every subsequent turn. +//! +//! 2. The `"AVAILABLE ENCOUNTER TYPES"` block (lines 412–434) is gated on +//! `ctx.snapshot.encounter.is_none()`. Once an encounter is active, the +//! narrator has zero visibility into what OTHER confrontation types exist +//! in the genre pack, so even if it wanted to signal a transition, it +//! couldn't name the destination type. +//! +//! 3. No OTEL event records whether the prompt included transition guidance. +//! The GM panel (CLAUDE.md § OTEL Observability) cannot verify that the +//! narrator was ever told about re-emit, so a prompt-template regression +//! would be invisible. +//! +//! # Acceptance Criteria +//! +//! | AC | What it proves | +//! |----------------------|------------------------------------------------------------| +//! | AC-NoOnlyOnStart | The "Only emit ... on the turn the encounter STARTS" | +//! | | misdirection is removed. | +//! | AC-TransitionMarker | prompt.rs contains a `TRANSITION CONFRONTATION` section | +//! | | header for the active-encounter branch. | +//! | AC-ReemitGuidance | prompt.rs tells the narrator to re-emit `confrontation` | +//! | | when the scene shifts to a new type. | +//! | AC-AltTypesListed | The active-encounter branch iterates `confrontation_defs` | +//! | | so the narrator sees what other types it can transition | +//! | | to. | +//! | AC-OTEL | prompt.rs emits `encounter.transition_guidance_injected` | +//! | | with an `alternative_count` field. | +//! | AC-Wiring | Guidance block lives in `build_prompt_context`, the sole | +//! | | production entry point to narrator prompt assembly. | +//! +//! # Test strategy +//! +//! Source-scan tests against `prompt.rs` via `include_str!`, matching the +//! convention established by Story 28-4 +//! (`encounter_context_wiring_story_28_4_tests.rs`). `DispatchContext` carries +//! 50+ fields including `AppState`, shared async session handles, render +//! queues, music directors, and an unbounded mpsc sender — building one in an +//! integration test would be larger than the fix itself. +//! +//! Source scanning is acceptable here because `build_prompt_context` is the +//! SOLE production entry point to narrator prompt assembly. Strings present +//! in that function body are reachable from the live dispatch path; the +//! CLAUDE.md wiring rule is satisfied by the fact that the scanned file IS +//! production code, not a helper nobody calls. +//! +//! The OTEL event name and marker strings (`TRANSITION CONFRONTATION`, +//! `encounter.transition_guidance_injected`, `alternative_count`) are a +//! contract between this test file and the Dev implementing the fix. If Dev +//! prefers different phrasing, update both sides in the same commit. + +/// Live snapshot of `dispatch/prompt.rs`. One copy, many assertions. +const PROMPT_SRC: &str = include_str!("../../src/dispatch/prompt.rs"); + +// --------------------------------------------------------------------------- +// AC-NoOnlyOnStart — the misdirection is removed +// --------------------------------------------------------------------------- + +/// The narrator prompt currently instructs: +/// "Only emit confrontation on the turn the encounter STARTS." +/// That line is the proximate cause of 37-12. The dispatch gate (37-13) will +/// never see a re-emit while this line is in the prompt, because the narrator +/// is explicitly told not to send one. This test fails today and must pass +/// after the fix. +#[test] +fn prompt_no_longer_tells_narrator_to_only_emit_on_start() { + assert!( + !PROMPT_SRC.contains("Only emit confrontation on the turn the encounter STARTS"), + "dispatch/prompt.rs still contains the Story 37-12 misdirection. \ + The narrator must be allowed to re-emit confrontation on scene \ + transitions — the 37-13 gate is built to route re-emits, but the \ + narrator never sends them while this instruction is in the prompt." + ); +} + +// --------------------------------------------------------------------------- +// AC-TransitionMarker — active-encounter branch carries the guidance header +// --------------------------------------------------------------------------- + +/// The active-encounter branch of `build_prompt_context` must inject a +/// dedicated `TRANSITION CONFRONTATION` section so the narrator can locate +/// re-emit guidance at a stable position in the prompt. Using an explicit +/// section marker (matching the style of other sections like +/// `=== AVAILABLE CONFRONTATIONS ===`) makes the guidance visible to prompt +/// analysis tools and to the GM panel's prompt inspector. +#[test] +fn prompt_includes_transition_confrontation_section_marker() { + assert!( + PROMPT_SRC.contains("TRANSITION CONFRONTATION"), + "dispatch/prompt.rs must inject a 'TRANSITION CONFRONTATION' section \ + when an encounter is active. This is the section where the narrator \ + learns it may re-emit a `confrontation` field when the scene shifts \ + to a different encounter type." + ); +} + +// --------------------------------------------------------------------------- +// AC-ReemitGuidance — narrator is told how, not just that +// --------------------------------------------------------------------------- + +/// Section headers are not enough; the guidance must name the action. +/// Accept any of several phrasings so Dev has room to pick wording that +/// reads naturally alongside the rest of the prompt, but at least one must +/// be present. +#[test] +fn prompt_instructs_narrator_to_reemit_on_scene_shift() { + let candidates = [ + "re-emit", + "re-declare", + "emit a new confrontation", + "emit the new confrontation", + "emit a different confrontation", + "emit the transition", + ]; + let matched: Vec<&&str> = candidates + .iter() + .filter(|c| PROMPT_SRC.contains(**c)) + .collect(); + assert!( + !matched.is_empty(), + "dispatch/prompt.rs must instruct the narrator to re-emit `confrontation` \ + when the scene transitions to a different type. None of the accepted \ + phrasings were found: {:?}", + candidates + ); +} + +// --------------------------------------------------------------------------- +// AC-AltTypesListed — narrator sees other types it can shift to +// --------------------------------------------------------------------------- + +/// Telling the narrator it MAY transition is useless if the prompt also +/// hides every other confrontation type. The `TRANSITION CONFRONTATION` +/// block must iterate `ctx.confrontation_defs` so the narrator sees the +/// alternatives by name. We verify by locating the marker and checking for +/// a `confrontation_defs` reference within a reasonable window below it. +#[test] +fn transition_block_iterates_confrontation_defs() { + let trans_idx = PROMPT_SRC + .find("TRANSITION CONFRONTATION") + .expect("TRANSITION CONFRONTATION marker missing — see AC-TransitionMarker test"); + + let window_end = (trans_idx + 2000).min(PROMPT_SRC.len()); + let window = &PROMPT_SRC[trans_idx..window_end]; + + assert!( + window.contains("confrontation_defs"), + "The TRANSITION CONFRONTATION block must iterate `ctx.confrontation_defs` \ + so the narrator sees the list of other types it could transition to. \ + Without this, the narrator knows it MAY transition but not WHAT IT CAN \ + transition TO." + ); +} + +// --------------------------------------------------------------------------- +// AC-OTEL — GM panel can verify the guidance was injected +// --------------------------------------------------------------------------- + +/// Per CLAUDE.md § "OTEL Observability", every subsystem decision must emit +/// a watcher event the GM panel can observe. If the narrator prompt fails +/// to include transition guidance (template regression, branch skipped, +/// feature flag flipped), the only way to detect it is an OTEL event that +/// fires on every successful injection. +#[test] +fn prompt_emits_transition_guidance_otel_event() { + assert!( + PROMPT_SRC.contains("encounter.transition_guidance_injected"), + "dispatch/prompt.rs must emit a watcher event \ + `encounter.transition_guidance_injected` whenever the transition \ + guidance section is added to the narrator prompt. Without this \ + event, the GM panel cannot distinguish 'no transition happened' \ + from 'narrator was never told about transitions'." + ); +} + +/// The OTEL event must carry `alternative_count` — the number of other +/// confrontation types shown to the narrator. A count of zero means the +/// guidance was injected but empty (genre pack has one type), and a count +/// equal to `confrontation_defs.len() - 1` is the happy path. Either way +/// the field is the mechanical fingerprint of the alternatives list. +#[test] +fn otel_transition_event_carries_alternative_count_field() { + assert!( + PROMPT_SRC.contains("alternative_count"), + "The `encounter.transition_guidance_injected` event must include \ + an `alternative_count` field so the GM panel can verify the \ + narrator was shown N alternative confrontation types. Without \ + this field the event is decorative, not diagnostic." + ); +} + +// --------------------------------------------------------------------------- +// AC-Wiring — guidance lives in the production prompt builder, not a helper +// --------------------------------------------------------------------------- + +/// CLAUDE.md § "Verify Wiring, Not Just Existence" — the fix must live in +/// `build_prompt_context` (or a helper it calls on every turn), not in a +/// dead module nobody imports. Because this test file scans the source of +/// `dispatch/prompt.rs`, and `build_prompt_context` is the only production +/// narrator prompt entry point in that file, the marker strings passing +/// above implicitly prove production reachability. This test makes the +/// coupling explicit by checking that the TRANSITION CONFRONTATION marker +/// appears somewhere after the `fn build_prompt_context` declaration. +#[test] +fn transition_guidance_is_below_build_prompt_context_declaration() { + let build_fn_start = PROMPT_SRC + .find("fn build_prompt_context") + .expect("build_prompt_context declaration not found — narrator prompt \ + entry point has moved; update this test"); + + let tail = &PROMPT_SRC[build_fn_start..]; + assert!( + tail.contains("TRANSITION CONFRONTATION"), + "TRANSITION CONFRONTATION marker must appear below the \ + `fn build_prompt_context` declaration. If the marker is ONLY above \ + it, the guidance is in a dead helper that `build_prompt_context` \ + does not call — a CLAUDE.md wiring violation." + ); +} From 89479996263231aeef83ac581cd7d764e8133023 Mon Sep 17 00:00:00 2001 From: Keith Avery Date: Tue, 14 Apr 2026 15:01:40 -0400 Subject: [PATCH 2/4] fix(37-12): narrator re-declares confrontation on scene transitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a TRANSITION CONFRONTATION section to the narrator prompt when an encounter is active, telling the narrator it may re-emit the `confrontation` field when the scene shifts to a different type. Lists the other confrontation types from the genre pack as concrete targets and excludes the current type (handled by the 37-13 gate's Case C redeclare no-op). Emits `encounter.transition_guidance_injected` with `current_encounter_type` and `alternative_count` fields so the GM panel can verify the narrator was shown N transition options on each turn. Removes the contradictory "Only emit confrontation on the turn the encounter STARTS" instruction from the is_none() branch — the 37-13 encounter gate was built to route re-emits across six cases, but the prompt was actively telling the narrator not to send them. Scope: prompt-string edit plus one watcher event. No new types, no new public API, no behavioral change to encounter_gate.rs. 7/7 Story 37-12 tests pass, 412 unrelated integration tests still pass, clippy clean. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../sidequest-server/src/dispatch/prompt.rs | 40 ++++++++++++++++++- ...r_confrontation_redef_story_37_12_tests.rs | 8 ++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/crates/sidequest-server/src/dispatch/prompt.rs b/crates/sidequest-server/src/dispatch/prompt.rs index 0b1337e6..f7d1796b 100644 --- a/crates/sidequest-server/src/dispatch/prompt.rs +++ b/crates/sidequest-server/src/dispatch/prompt.rs @@ -404,6 +404,45 @@ pub(crate) async fn build_prompt_context( .field("available_defs", ctx.confrontation_defs.len()) .send(); } + + // Story 37-12: Transition guidance. The encounter gate + // (dispatch/encounter_gate.rs) is built to route narrator re-emits of + // `confrontation` on every case (Redeclared / ReplacedPreBeat / + // RejectedMidEncounter), but without this section the narrator is + // never told the option exists. List the other types so the narrator + // has a concrete menu of transition targets. The current type is + // excluded because Case C (redeclare) is a no-op and there is no + // reason to invite redundant re-declarations. + if !ctx.confrontation_defs.is_empty() { + let alternatives: Vec<&sidequest_genre::ConfrontationDef> = ctx + .confrontation_defs + .iter() + .filter(|d| d.confrontation_type != enc.encounter_type) + .collect(); + state_summary.push_str("\n\n=== TRANSITION CONFRONTATION ===\n"); + state_summary.push_str( + "If the scene shifts to a different confrontation type \ + (e.g., a poker game erupts into a standoff, or a chase breaks \ + into combat), re-emit the `confrontation` field in your \ + game_patch with the new type. The encounter gate will replace \ + the current encounter with the new one when it is safe to do \ + so, or surface a mid-encounter divergence warning when it is \ + not. Available transition targets:\n", + ); + for alt in alternatives.iter() { + state_summary.push_str(&format!( + "- \"{}\" ({}, {})\n", + alt.confrontation_type, alt.label, alt.category + )); + } + state_summary.push_str("=== END TRANSITION CONFRONTATION ===\n"); + + WatcherEventBuilder::new("encounter", WatcherEventType::StateTransition) + .field("event", "encounter.transition_guidance_injected") + .field("current_encounter_type", &enc.encounter_type) + .field("alternative_count", alternatives.len()) + .send(); + } } // Inject available confrontation types so the narrator knows what encounters @@ -418,7 +457,6 @@ pub(crate) async fn build_prompt_context( def.confrontation_type, def.label, def.category )); } - state_summary.push_str("Only emit confrontation on the turn the encounter STARTS.\n"); WatcherEventBuilder::new("encounter", WatcherEventType::StateTransition) .field("action", "available_types_injected") diff --git a/crates/sidequest-server/tests/integration/narrator_confrontation_redef_story_37_12_tests.rs b/crates/sidequest-server/tests/integration/narrator_confrontation_redef_story_37_12_tests.rs index 53f7d295..c87f80f5 100644 --- a/crates/sidequest-server/tests/integration/narrator_confrontation_redef_story_37_12_tests.rs +++ b/crates/sidequest-server/tests/integration/narrator_confrontation_redef_story_37_12_tests.rs @@ -221,10 +221,10 @@ fn otel_transition_event_carries_alternative_count_field() { /// appears somewhere after the `fn build_prompt_context` declaration. #[test] fn transition_guidance_is_below_build_prompt_context_declaration() { - let build_fn_start = PROMPT_SRC - .find("fn build_prompt_context") - .expect("build_prompt_context declaration not found — narrator prompt \ - entry point has moved; update this test"); + let build_fn_start = PROMPT_SRC.find("fn build_prompt_context").expect( + "build_prompt_context declaration not found — narrator prompt \ + entry point has moved; update this test", + ); let tail = &PROMPT_SRC[build_fn_start..]; assert!( From fe7adcca85b69284af31d3792874f2266d1ea758 Mon Sep 17 00:00:00 2001 From: Keith Avery Date: Tue, 14 Apr 2026 15:07:07 -0400 Subject: [PATCH 3/4] refactor(37-12): drop Vec allocation on narrator prompt hot path Applied one high-confidence simplify-efficiency finding from TEA verify: the TRANSITION CONFRONTATION block in build_prompt_context() collected filtered confrontation defs into an intermediate Vec, then iterated the Vec to format list items. Replaced with a direct filter+counter loop that produces the same string output and the same alternative_count OTEL field without the intermediate allocation. The cost of a ~30-element Vec allocation is tiny compared to the downstream Claude CLI subprocess, but the counter variant is strictly simpler and has no tradeoff. 7/7 Story 37-12 tests still pass, 419-test integration suite green, clippy clean, cargo fmt no-op. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/sidequest-server/src/dispatch/prompt.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/sidequest-server/src/dispatch/prompt.rs b/crates/sidequest-server/src/dispatch/prompt.rs index f7d1796b..53dabad3 100644 --- a/crates/sidequest-server/src/dispatch/prompt.rs +++ b/crates/sidequest-server/src/dispatch/prompt.rs @@ -414,11 +414,6 @@ pub(crate) async fn build_prompt_context( // excluded because Case C (redeclare) is a no-op and there is no // reason to invite redundant re-declarations. if !ctx.confrontation_defs.is_empty() { - let alternatives: Vec<&sidequest_genre::ConfrontationDef> = ctx - .confrontation_defs - .iter() - .filter(|d| d.confrontation_type != enc.encounter_type) - .collect(); state_summary.push_str("\n\n=== TRANSITION CONFRONTATION ===\n"); state_summary.push_str( "If the scene shifts to a different confrontation type \ @@ -429,18 +424,24 @@ pub(crate) async fn build_prompt_context( so, or surface a mid-encounter divergence warning when it is \ not. Available transition targets:\n", ); - for alt in alternatives.iter() { + let mut alternative_count: usize = 0; + for alt in ctx + .confrontation_defs + .iter() + .filter(|d| d.confrontation_type != enc.encounter_type) + { state_summary.push_str(&format!( "- \"{}\" ({}, {})\n", alt.confrontation_type, alt.label, alt.category )); + alternative_count += 1; } state_summary.push_str("=== END TRANSITION CONFRONTATION ===\n"); WatcherEventBuilder::new("encounter", WatcherEventType::StateTransition) .field("event", "encounter.transition_guidance_injected") .field("current_encounter_type", &enc.encounter_type) - .field("alternative_count", alternatives.len()) + .field("alternative_count", alternative_count) .send(); } } From 435b377fc844a4154b8aaeabd752accdba4a8ed9 Mon Sep 17 00:00:00 2001 From: Keith Avery Date: Tue, 14 Apr 2026 15:19:21 -0400 Subject: [PATCH 4/4] =?UTF-8?q?review(37-12):=20apply=20four=20reviewer=20?= =?UTF-8?q?nits=20=E2=80=94=20comment=20honesty=20+=20test=20robustness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-review improvements from Colonel Potter's review team: 1. comment-analyzer #1: the prompt.rs code comment claimed 37-13 routes re-emits "on every case" but only listed three of six. Rewrote to name Cases C/D/E explicitly, note that A/B are reached via the is_none() branch, and document that the block sits outside the def-guard intentionally so a broken-def encounter can still be recovered via a transition menu. 2. edge-hunter #6 / test-analyzer #4: the transition_block_iterates_confrontation_defs test used a fixed 2000-byte window past the marker, which was fragile to block growth and could sweep into neighbouring AVAILABLE CONFRONTATIONS iteration. Replaced with a semantic window bounded by === TRANSITION CONFRONTATION === and === END TRANSITION CONFRONTATION ===. 3. rule-checker check #6: the phrase test accepted any of six candidates, which made future wording swaps invisible to CI. Now that GREEN settled on the canonical "re-emit the `confrontation`" phrasing, narrowed the assertion to that exact string so any reword surfaces in code review. 4. comment-analyzer #2: the test module doc said "six observable cases" but enumerated only five outcome names. Rewrote to note that Cases A and B share the `Created` outcome name, resolving the six-cases-five-names contradiction. All 7 Story 37-12 tests still pass, 419-test integration suite green, clippy clean, cargo fmt no-op. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../sidequest-server/src/dispatch/prompt.rs | 18 +++-- ...r_confrontation_redef_story_37_12_tests.rs | 73 +++++++++---------- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/crates/sidequest-server/src/dispatch/prompt.rs b/crates/sidequest-server/src/dispatch/prompt.rs index 53dabad3..24df0ac5 100644 --- a/crates/sidequest-server/src/dispatch/prompt.rs +++ b/crates/sidequest-server/src/dispatch/prompt.rs @@ -407,12 +407,18 @@ pub(crate) async fn build_prompt_context( // Story 37-12: Transition guidance. The encounter gate // (dispatch/encounter_gate.rs) is built to route narrator re-emits of - // `confrontation` on every case (Redeclared / ReplacedPreBeat / - // RejectedMidEncounter), but without this section the narrator is - // never told the option exists. List the other types so the narrator - // has a concrete menu of transition targets. The current type is - // excluded because Case C (redeclare) is a no-op and there is no - // reason to invite redundant re-declarations. + // `confrontation` through Cases C (Redeclared, no-op), D + // (ReplacedPreBeat), and E (RejectedMidEncounter), but without this + // section the narrator is never told the option exists. (Cases A and B + // create from None or a resolved encounter and are reached via the + // is_none() block below, not this one.) List the other types so the + // narrator has a concrete menu of transition targets. The current type + // is excluded because Case C (redeclare) is a no-op and there is no + // reason to invite redundant re-declarations. The block sits outside + // the inner find_confrontation_def() guard so it still fires when the + // current encounter's def is missing — in that state the narrator + // also needs a transition menu to recover, and the broken-def case is + // independently signalled by the existing ValidationWarning above. if !ctx.confrontation_defs.is_empty() { state_summary.push_str("\n\n=== TRANSITION CONFRONTATION ===\n"); state_summary.push_str( diff --git a/crates/sidequest-server/tests/integration/narrator_confrontation_redef_story_37_12_tests.rs b/crates/sidequest-server/tests/integration/narrator_confrontation_redef_story_37_12_tests.rs index c87f80f5..3feda504 100644 --- a/crates/sidequest-server/tests/integration/narrator_confrontation_redef_story_37_12_tests.rs +++ b/crates/sidequest-server/tests/integration/narrator_confrontation_redef_story_37_12_tests.rs @@ -3,11 +3,11 @@ //! # Background //! //! Story 37-13 built the encounter creation gate (`dispatch/encounter_gate.rs`), -//! which covers six observable cases for how to route a narrator-emitted -//! `"confrontation": ` signal against the current `snapshot.encounter` -//! state (Created, Redeclared, ReplacedPreBeat, RejectedMidEncounter, -//! UnknownType). The dispatch side is READY to receive re-emits from the -//! narrator. +//! which covers six cases (A–F) that collapse to five distinct outcome names: +//! `Created` (Cases A+B — create from None or from a resolved encounter), +//! `Redeclared` (Case C), `ReplacedPreBeat` (Case D), `RejectedMidEncounter` +//! (Case E), and `UnknownType` (Case F). The dispatch side is READY to +//! receive re-emits from the narrator. //! //! 37-12 is the prompt-side half: the narrator is never ASKED to re-emit. //! Three concrete regressions in `dispatch/prompt.rs`: @@ -117,29 +117,20 @@ fn prompt_includes_transition_confrontation_section_marker() { // --------------------------------------------------------------------------- /// Section headers are not enough; the guidance must name the action. -/// Accept any of several phrasings so Dev has room to pick wording that -/// reads naturally alongside the rest of the prompt, but at least one must -/// be present. +/// Dev's RED-phase brief allowed several phrasings; after GREEN the canonical +/// wording settled on `re-emit`, which is the word the rest of the prompt +/// uses for `game_patch` fields. Lock it in as the contract: a future refactor +/// that swaps the verb (e.g. to `"re-declare"`) must update this test in the +/// same commit, so the change surfaces in code review instead of silently +/// preserving multi-candidate flexibility that is no longer load-bearing. #[test] fn prompt_instructs_narrator_to_reemit_on_scene_shift() { - let candidates = [ - "re-emit", - "re-declare", - "emit a new confrontation", - "emit the new confrontation", - "emit a different confrontation", - "emit the transition", - ]; - let matched: Vec<&&str> = candidates - .iter() - .filter(|c| PROMPT_SRC.contains(**c)) - .collect(); assert!( - !matched.is_empty(), - "dispatch/prompt.rs must instruct the narrator to re-emit `confrontation` \ - when the scene transitions to a different type. None of the accepted \ - phrasings were found: {:?}", - candidates + PROMPT_SRC.contains("re-emit the `confrontation`"), + "dispatch/prompt.rs must instruct the narrator to `re-emit the \ + `confrontation`` field when the scene transitions to a different \ + type. The canonical phrasing is pinned — any reword must update \ + this test in the same commit." ); } @@ -150,23 +141,31 @@ fn prompt_instructs_narrator_to_reemit_on_scene_shift() { /// Telling the narrator it MAY transition is useless if the prompt also /// hides every other confrontation type. The `TRANSITION CONFRONTATION` /// block must iterate `ctx.confrontation_defs` so the narrator sees the -/// alternatives by name. We verify by locating the marker and checking for -/// a `confrontation_defs` reference within a reasonable window below it. +/// alternatives by name. We verify by bounding the scan between the +/// section's own start and end markers — a fixed byte offset would creep +/// into neighbouring sections when the block grows, and a raw `contains` +/// check could match the iteration in the pre-existing AVAILABLE +/// CONFRONTATIONS block a few hundred lines above. #[test] fn transition_block_iterates_confrontation_defs() { let trans_idx = PROMPT_SRC - .find("TRANSITION CONFRONTATION") + .find("=== TRANSITION CONFRONTATION ===") .expect("TRANSITION CONFRONTATION marker missing — see AC-TransitionMarker test"); - - let window_end = (trans_idx + 2000).min(PROMPT_SRC.len()); - let window = &PROMPT_SRC[trans_idx..window_end]; - + let end_idx = PROMPT_SRC[trans_idx..] + .find("=== END TRANSITION CONFRONTATION ===") + .map(|i| trans_idx + i) + .expect( + "END TRANSITION CONFRONTATION footer missing — the block must be \ + explicitly closed so this test can bound its scan", + ); + + let block = &PROMPT_SRC[trans_idx..end_idx]; assert!( - window.contains("confrontation_defs"), - "The TRANSITION CONFRONTATION block must iterate `ctx.confrontation_defs` \ - so the narrator sees the list of other types it could transition to. \ - Without this, the narrator knows it MAY transition but not WHAT IT CAN \ - transition TO." + block.contains("confrontation_defs"), + "The TRANSITION CONFRONTATION block (bounded by its start and end \ + markers) must iterate `ctx.confrontation_defs` so the narrator sees \ + the list of other types it could transition to. Without this, the \ + narrator knows it MAY transition but not WHAT IT CAN transition TO." ); }