RIFF · session_facts pack module · use-case-first spec · 2026-07-07 · TDD twin: tests/test_session_facts_module.py · siblings: end-flow contract, replay & atlas

Session Facts as a Module

Until now the session-facts guarantees were engine conventions. This module makes them a declared unit in the pack — like welcome: or brand: — so a business pack states its facts and trust layers, the compiler enforces them at build, and the atlas draws declarations instead of guessing from state names.

The block (all fields optional except enabled; the five standard facts are the default):
session_facts:
  enabled: true
  layers:                      # explicit trust-layer attribution for gate states
    welcome_first: guest
    welcome_back: known
    welcome_unlock_granted: verified
  facts:                       # extend/override the standard five BY NAME (last wins)
    - {name: selected_route, writer: router, direction: transient}

Closed enums (Codex #2): layer ∈ {guest, known, verified, shared}; direction ∈ {up_only, durable, transient, forward, refine_only}; writer is a free-form attribution string. Duplicate fact names within the block = parse error. Inference rule (Codex #1): the compiler MAY infer a gate state's layer, but every inferred value is materialized into config.session_facts.layers at build — so the atlas never guesses at render time; it only ever reads declarations. A gate state the compiler cannot infer AND the pack does not declare fails the build.

Case M1 — Declaring the module embeds the registry TM1

  1. A pack adds session_facts: {enabled: true}.
  2. The compiled flow carries config.session_facts: the fact registry (name, writer, direction) and the resolved per-state layer map.
  3. A pack recompile never strips it (same durability rule as brand:).

Pass: registry + layer map present in the compiled YAML; five standard facts by default.

Case M2 — Violations fail the BUILD TM2

  1. A declaring pack whose compiled flow would leave a welcome_* state with no layer attribution (declared or inferable) fails compile — the atlas must never guess for a declaring flow.
  2. A layers: entry naming an unknown state or an invalid layer value fails parse.

Pass: both violations produce build errors naming the state. Never: a declaring pack that compiles with an unattributed gate state.

Case M3 — The atlas draws declarations TM3

call_replay.py --atlas on a declaring flow reads config.session_facts.layers first; name-heuristics remain only as fallback for legacy flows. A declaring flow renders zero unattributed states.

Case M4 — The live proof: a brand-new business on FLOW-101 TM4

A mock business built from scratch WITH the module, deployed on the loop test DID 512-FLOW-101 (+1 512-356-9101), exercising every guarantee end to end:

  1. First call (unknown number): business greeting → menu → say an option or press a key → your choice routes immediately (no gate in the way) → consent asked once if you take the follow-along text → goodbye.
  2. Second call (same phone): "welcome back" by name → auth text with code → reading the code gets one "Access granted" → a menu choice made right after verification is honored (the route survives the ack — the live defect this whole program started from).
  3. Replay: the call's page (call_replay.py <call-id>) shows all four lanes — the SLOTS lane proves who wrote every fact.

Never: a repeated "Access granted"; a menu choice destroyed by the gate; a consent re-ask after answering.

Go-live gate: SMS deliverability (shipped 2026-07-07)

How the build knows a flow texts — four static pack declarations, checkable before any compile: a capability with sms_verification: true, any field with consent: true, a welcome: block (returning-caller auth/follow-along text), or companion auto_send.

What the gate checks — the business phone's 10DLC campaign attachment in the carrier registry (assignmentStatus == ASSIGNED), one HTTP call, no message sent. Env-var readiness checks cannot see this class: the live incident logged status=sent while the carrier dropped every message (error 40010) and a caller sat at intake_verify_code waiting for a text that could never arrive.

Behavior — keyed compile/build FAIL with ❌ sms-not-deliverable; keyless builds print a LOUD ⚠️ SKIPPED (not passed) — a skipped check is never a green one. Code: riff/telecom_checks.py · TDD: tests/test_sms_deliverability_check.py (S1–S5). Proven on the incident: the gate failed the Bluebonnet build while its DID was PENDING_ASSIGNMENT and passed once carriers mapped it.

Open follow-ups: the opt-in real-send probe (--probe, polls FINAL delivery status, operator-invoked only) and the intake_verify_code resend/skip path for callers whose text is delayed.

Test matrix (TDD — red before the module exists)

idcaseasserts
TM1M1 registry embedconfig.session_facts in compiled flow; default facts; layers resolved
TM2M2 build teethunknown state / bad layer / unattributed gate state ⇒ compile FAIL naming it
TM3M3 atlas sourcedeclaring flow: 0 unattributed; declaration beats heuristic
TM4M4 mock business (deterministic half)the FLOW-101 pack compiles green with the module; endflow walks (T2-shape route survival, consent once) pass on it — this half gates CI
TM4LM4 live proof (evidence half)real DID call ids + replay artifacts recorded here after the operator's test calls — evidence only, never gates merge (Codex #3)