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.
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
- A pack adds
session_facts: {enabled: true}. - The compiled flow carries
config.session_facts: the fact registry (name, writer, direction) and the resolved per-state layer map. - 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
- 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. - 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:
- 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.
- 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).
- 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)
| id | case | asserts |
|---|---|---|
| TM1 | M1 registry embed | config.session_facts in compiled flow; default facts; layers resolved |
| TM2 | M2 build teeth | unknown state / bad layer / unattributed gate state ⇒ compile FAIL naming it |
| TM3 | M3 atlas source | declaring flow: 0 unattributed; declaration beats heuristic |
| TM4 | M4 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 |
| TM4L | M4 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) |