srr84/agent-data-layer
0
1# Code Tour — Agent-Readable Data Layer POC2 3> Companion to **WALKTHROUGH.md**. Where the walkthrough is **structural** ("here is what each thing is"), this document is **flow-based** ("here is what happens when you do X, traced through every file and function") and goes deeper at the class/function level.4 5Read WALKTHROUGH.md §1–§3 first for the product and the module map. This tour assumes you know the system answers retail-scan questions with provenance and cannot fabricate, and that the LLM is confined to two job sites.6 7## Table of contents8 9- [Part 1 — The map](#part-1--the-map)10 - [1.1 File dependency graph](#11-file-dependency-graph)11 - [1.2 Class / dataclass composition graph](#12-class--dataclass-composition-graph)12 - [1.3 Class call graph (one happy-path request)](#13-class-call-graph-one-happy-path-request)13 - [1.4 The four layers of abstraction](#14-the-four-layers-of-abstraction)14 - [1.5 The two trust boundaries](#15-the-two-trust-boundaries)15 - [1.6 The data lifecycle of one fact](#16-the-data-lifecycle-of-one-fact)16 - [1.7 The closed tool menu](#17-the-closed-tool-menu)17- [Part 2 — Use case flows](#part-2--use-case-flows)18 - [UC1 — A scalar question, answered end-to-end](#uc1--a-scalar-question-answered-end-to-end)19 - [UC2 — An aggregate question (T4 count_stores_with_condition)](#uc2--an-aggregate-question-t4-count_stores_with_condition)20 - [UC2b — A window count (T6, the only every-event tool)](#uc2b--a-window-count-t6-the-only-every-event-tool)21 - [UC3 — A grounded zero (absence, not abstention)](#uc3--a-grounded-zero-absence-not-abstention)22 - [UC4 — Out-of-coverage → abstention](#uc4--out-of-coverage--abstention)23 - [UC5 — A raw-query / hallucinated-tool proposal → typed rejection](#uc5--a-raw-query--hallucinated-tool-proposal--typed-rejection)24 - [UC6 — Model-down → ModelUnavailable](#uc6--model-down--modelunavailable)25 - [UC7 — The independent resolver re-enumeration path](#uc7--the-independent-resolver-re-enumeration-path)26 - [UC8 — The eval harness run (dual-score)](#uc8--the-eval-harness-run-dual-score)27 - [UC9 — The observability flow (log line → metrics → surfaces)](#uc9--the-observability-flow-log-line--metrics--surfaces)28- [Part 3 — Class-by-class deep dive](#part-3--class-by-class-deep-dive)29- [Part 4 — Function & method index](#part-4--function--method-index)30- [Part 5 — State / data-object index](#part-5--state--data-object-index)31- [Appendix — Quick-reference flowchart](#appendix--quick-reference-flowchart)32 33---34 35## Part 1 — The map36 37### 1.1 File dependency graph38 39Arrows are one-way imports (A → B means A imports B). The `contracts/` package is the sink everything depends on; it depends on nothing in the project. Two production entrypoints sit at the top: `request_pipeline` (per-request) and `report` (eval).40 4142 43<details>44<summary>Mermaid source</summary>45 46```mermaid47flowchart TD48 REPORT["report.py<br/>(eval entrypoint)"] --> RP["request_pipeline.py<br/>(request entrypoint)"]49 REPORT --> CS["correctness_scorer.py"]50 REPORT --> RESV["provenance_resolver.py"]51 REPORT --> IDX["event_index.py"]52 REPORT --> GOLD["gold_loader.py"]53 REPORT --> STORE["store_loader.py"]54 55 RP --> QI["question_intake.py"]56 RP --> TS["tool_selector.py"]57 RP --> V["param_validator.py"]58 RP --> EX["tool_executor.py"]59 RP --> AT["provenance_attacher.py"]60 RP --> RE["result_envelope.py"]61 RP --> AC["answer_composer.py"]62 RP --> ST["envelope_stapler.py"]63 RP --> OBS["observability/log_line.py"]64 RP --> APP["observability/app_surfaces.py"]65 66 TS --> LMA["local_model_adapter.py"]67 AC --> LMA68 PIF["provider_iface.py"] --> LMA69 PIF --> HMA["hosted_model_adapter.py (stub)"]70 71 V --> REG["tool_registry.py"]72 RE --> REG73 EX --> IDX74 ST --> RESV75 AT --> EX76 77 EX --> MATCH["contracts/matching.py"]78 RESV --> MATCH79 APP --> STORE80 APP --> OBS81 82 subgraph CONTRACTS["contracts/ (M0 — the sink)"]83 MATCH --> CT["tools / ids / enums / filters /<br/>provenance / envelopes / errors /<br/>provider / settings / stub / data_model"]84 end85 86 QI -.-> CT87 TS -.-> CT88 V -.-> CT89 EX -.-> CT90 AT -.-> CT91 RE -.-> CT92 AC -.-> CT93 ST -.-> CT94 RESV -.-> CT95 CS -.-> CT96 REG -.-> CT97 LMA -.-> CT98 OBS -.-> CT99```100 101</details>102 103Two import facts carry weight:104- **`envelope_stapler.py` imports `provenance_resolver` but never any provider/adapter.** The single writer of `asserted_value`/`provenance` cannot see the model — non-LLM-writability is a graph property.105- **`request_pipeline.py` does not import `provider_iface.py`.** It takes the provider as a parameter, so it constructs no model factory and (with `all_events` also passed in) no store ingress.106 107A few more edges are worth pointing at when you read the graph:108- **`tool_executor` imports `event_index`, and `provenance_resolver` does not.** The executor reads per-tool index buckets; the resolver only ever touches `all_events` (passed in). That is the independent-data-path property visible as an *absent* edge — the resolver cannot accidentally reuse the executor's index because it never imports it.109- **Both `tool_executor` and `provenance_resolver` point at `contracts/matching.py`.** They share the predicate leaf and nothing else. This is the one intentional shared dependency: a single `matches()` so there is no second predicate to drift.110- **`provenance_attacher` imports `tool_executor` (for `ExecutedResult`), not the other way around.** The executor produces a raw result; the attacher turns it into a `ToolResult`. The direction matters — the executor knows nothing about provenance shaping.111- **The `scripts/` guards are not in this graph.** They run as standalone `ast` walkers over the source, so they are not importable nodes; they re-check the same properties the edges encode (allowlisted store reads, no model-derived value into a stapled field, no reachable stub) without being part of the runtime import closure.112 113### 1.2 Class / dataclass composition graph114 115Most types are frozen dataclasses. Composition (filled diamond) shows ownership of value objects.116 117118 119<details>120<summary>Mermaid source</summary>121 122```mermaid123classDiagram124 class ToolSpec {125 +str name126 +Tuple~ParamSpec~ params127 +ReturnSchemaType return_schema128 +ProvenanceKind provenance_kind129 }130 class ParamSpec {131 +str name132 +ParamType type133 +Optional~FrozenSet~ enum_domain134 +bool required135 }136 ToolSpec *-- ParamSpec137 138 class ValidatedToolCall {139 +str tool_name140 +Mapping params141 }142 class ExecutedResult {143 +ReturnSchema value144 +Tuple~ScanEvent~ contributing145 +ProvenanceKind provenance_kind146 +TypedFilter typed_filter147 +Window window148 +Optional~Timestamp~ observed_at149 }150 class ToolResult~V~ {151 +V asserted_value152 +Provenance provenance153 +Optional~Timestamp~ observed_at154 }155 class AnswerEnvelope~V~ {156 +V asserted_value157 +Provenance provenance158 +str nl_text159 }160 class EventPointer {161 +EventId event_id162 +StoreId store_id163 }164 class AbsenceAttestation {165 +TypedFilter typed_filter166 +Window window167 +str store_snapshot_id168 +int independently_enumerated_count169 }170 class TypedFilter {171 +Optional~StoreId~ store_id172 +Optional~ProductId~ product_id173 +Optional~ShelfState~ state174 +Optional~StoreCondition~ condition175 +Window window176 }177 class ResolverVerdict {178 +bool provenance_valid179 +bool asserted_value_matches180 +bool completeness_ok181 +Tuple~str~ reasons182 }183 184 ExecutedResult *-- ScanEvent185 ExecutedResult *-- TypedFilter186 ToolResult *-- AbsenceAttestation : provenance (zero)187 ToolResult o-- EventPointer : provenance (set)188 AnswerEnvelope --> ToolResult : value+provenance COPIED by stapler189 AbsenceAttestation *-- TypedFilter190 TypedFilter *-- Window191```192 193</details>194 195`Provenance` is `Union[ProvenanceSet, AbsenceAttestation]` where `ProvenanceSet = FrozenSet[EventPointer]`. `ToolResult` and `AnswerEnvelope` are generic over the concrete per-tool value type `V`.196 197The composition arrows encode two ownership rules that matter when you change a type. First, `AnswerEnvelope` carries the *same* `V` and the *same* `Provenance` instance as the `ToolResult` it was stapled from — the arrow is labelled "COPIED by stapler" because there is no transformation, only a copy under an equality check; if you ever made the envelope recompute the value, you would break the single-writer property. Second, `ToolResult`'s provenance is one of two shapes (`o--` to `EventPointer` for the set case, `*--` to `AbsenceAttestation` for the zero case) and never both — a positive fact has a non-empty `ProvenanceSet`, a grounded zero has an attestation, and the type union is what keeps "absence as an empty set" unrepresentable. `ExecutedResult` is the only type that owns raw `ScanEvent`s (its `contributing` tuple); by the time you reach a `ToolResult` they have been projected to `EventPointer`s, so the answer carries identity keys, not whole rows.198 199### 1.3 Class call graph (one happy-path request)200 201202 203<details>204<summary>Mermaid source</summary>205 206```mermaid207sequenceDiagram208 autonumber209 participant Caller210 participant AQ as answer_question211 participant QI as question_intake212 participant TS as tool_selector213 participant P as provider (LocalModelAdapter)214 participant V as param_validator215 participant EX as tool_executor216 participant AT as provenance_attacher217 participant RE as result_envelope218 participant AC as answer_composer219 participant ST as envelope_stapler220 participant RES as provenance_resolver221 222 Caller->>AQ: answer_question(q, index, all_events, provider)223 AQ->>QI: intake(q)224 QI-->>AQ: IntakeRequest225 AQ->>TS: select_tool(q, specs, provider)226 TS->>P: provider.select_tool(q, specs)227 P-->>TS: ToolCall (untrusted)228 TS-->>AQ: ToolCall229 AQ->>V: validate(ToolCall)230 V-->>AQ: ValidatedToolCall231 AQ->>EX: execute(call, index)232 EX-->>AQ: ExecutedResult233 AQ->>AT: attach(ExecutedResult)234 AT-->>AQ: ToolResult235 AQ->>RE: build_result_envelope(name, ToolResult)236 RE-->>AQ: ToolResult (schema-validated)237 AQ->>AC: compose_nl(ToolResult, provider)238 AC->>P: provider.compose_nl(ToolResult)239 P-->>AC: nl_text240 AC-->>AQ: nl_text241 AQ->>ST: staple(ToolResult, nl_text, query, all_events)242 ST->>RES: resolve(query, value, provenance, all_events)243 RES-->>ST: ResolverVerdict244 ST-->>AQ: AnswerEnvelope245 AQ->>AQ: _finish(...) — emit RequestLogLine (side-effect)246 AQ-->>Caller: AnswerEnvelope247```248 249</details>250 251Two things to notice in the trace. The provider (`P`) is called exactly twice — once by `tool_selector`, once by `answer_composer` — and never by anyone else; if you are auditing for model touch points, those are the only two arrows that cross into `P`. And `resolve` is called by the stapler on the happy path, not by `answer_question` directly: re-derivation is part of stapling, so a fact cannot be stapled without first being independently confirmed. The `_finish` self-call at the end is the single log emission; it observes the finished outcome and returns it unchanged.252 253### 1.4 The four layers of abstraction254 255256 257<details>258<summary>Mermaid source</summary>259 260```mermaid261flowchart LR262 subgraph L1["L1 — Agent Surface<br/>untrusted NL ↔ model"]263 A1["question_intake · tool_selector ·<br/>answer_composer · envelope_stapler ·<br/>request_pipeline"]264 end265 subgraph L2["L2 — Provider seam<br/>swappable, $0 local"]266 A2["provider_iface · local_model_adapter ·<br/>hosted_model_adapter (stub)"]267 end268 subgraph L3["L3 — Typed Data Layer<br/>deterministic, model-free"]269 A3["tool_registry · param_validator ·<br/>tool_executor · provenance_attacher ·<br/>result_envelope · provenance_resolver"]270 end271 subgraph L4["L4 — Store Substrate + contracts<br/>immutable data"]272 A4["store_loader · event_index ·<br/>gold_loader · contracts/*"]273 end274 L1 --> L2275 L1 --> L3276 L3 --> L4277 L2 --> L4278```279 280</details>281 282The model lives only in L2, reached only from two L1 modules. Everything load-bearing (fact, provenance, validation, resolution) is in L3/L4 — deterministic.283 284### 1.5 The two trust boundaries285 286There are exactly two places where untrusted data enters the system, and each has a named gate:287 288289 290<details>291<summary>Mermaid source</summary>292 293```mermaid294flowchart LR295 NL["NL question<br/>(UNTRUSTED)"] -->|intake, no data touch| SEL["tool_selector → model"]296 SEL -->|ToolCall, UNTRUSTED| WALL["param_validator<br/>(the wall)"]297 WALL -->|ValidatedToolCall| DATA["deterministic data layer"]298 DATA --> TR["ToolResult (the fact)"]299 TR -->|value only, never provenance| COMP["answer_composer → model"]300 COMP -->|nl_text, TAINTED| STAPLE["envelope_stapler<br/>(single writer)"]301 TR -->|value+provenance COPIED by code| STAPLE302 STAPLE --> ANS["AnswerEnvelope"]303```304 305</details>306 307- **Boundary 1 — the NL question and the model's tool proposal.** The question is untrusted text; the model's `ToolCall` is an untrusted proposal. The wall (`param_validator`) is the only path from a `ToolCall` to a callable `ValidatedToolCall`, and it reads no data, so a bad proposal is rejected before any store access.308- **Boundary 2 — the model's prose.** `answer_composer` is handed only the value (never the provenance) and returns `nl_text`, which is tainted. The stapler places that string into the envelope but copies `asserted_value`/`provenance` from the `ToolResult` by code — the model output and the fact meet only inside the frozen envelope, never in the same assignment.309 310Everything between the two boundaries is deterministic, and the static guards in `scripts/` re-prove across the whole program that (a) no store read happens outside the allowlist and (b) no model-derived value reaches a stapled field.311 312### 1.6 The data lifecycle of one fact313 314Following a single scalar fact from disk to the answer makes the ownership transfers concrete:315 3161. A `ScanEvent` is loaded once from `store.json` by `store_loader` (validated, frozen) and bucketed into `EventIndex` by `event_index`.3172. A request's `ValidatedToolCall` reaches `tool_executor`, which looks up the exact-key bucket and selects the latest-≤-as_of event — producing an `ExecutedResult` that carries the *actual event object* in `contributing`.3183. `provenance_attacher` projects that event to an `EventPointer` (identity = `event_id`) and wraps the value + the one-pointer `ProvenanceSet` into a `ToolResult`.3194. `result_envelope` confirms the value's dataclass matches the tool's declared return shape.3205. `envelope_stapler` re-derives the same fact independently via `provenance_resolver` over `all_events`, asserts equality, then copies the value + provenance into an `AnswerEnvelope` and attaches the model's sentence.321 322At no step does the fact's value originate from anything but the loaded event; the model never authors it, and three independent computations (executor index, resolver re-enumeration, gold) all agree on it before it is returned.323 324### 1.7 The closed tool menu325 326The registry (`registry/tool_registry.py`) is a dict literal built at import — there is no dynamic registration path, so the menu is fixed. Each `ToolSpec` declares only typed/enum param *slots*; the value checks happen at the wall. The six tools:327 328| tool (`name`) | params (all typed/enum) | return dataclass | provenance_kind |329|---|---|---|---|330| `get_stock_state` (T1) | `store_id`, `product_id`, `as_of` | `StockResult` | scalar |331| `get_price` (T2) | `store_id`, `product_id`, `as_of` | `PriceResult` | scalar |332| `get_compliance_flag` (T3) | `store_id`, `product_id`, `as_of` | `ComplianceResult` | scalar |333| `count_stores_with_condition` (T4) | `condition` (closed enum), `as_of` | `ConditionCountResult` | set |334| `list_products_in_state` (T5) | `store_id`, `state` (closed enum), `as_of` | `ProductListResult` | set |335| `count_events_in_window` (T6) | `store_id`, `state` (closed enum), `start`, `end` | `WindowCountResult` | set |336 337`get_tool_spec(name)` (line 171) returns the `ToolSpec` or a typed `UnregisteredTool` — it never raises, so a hallucinated name is a value, not an exception. `registered_tool_names()` (line 184) is the closed name set the validator checks membership against. Note T6's `start`/`end` are typed `Timestamp` slots; the `start <= end` relationship is *not* a slot-shape check (both are individually valid timestamps) — it is a well-shaped wrong-*value* check the wall owns (`_check_window_order`), which is why an out-of-order window is a `ValidationError`, not a `RawQueryRejected`.338 339---340 341## Part 2 — Use case flows342 343Each flow is traced with `file:function` (and line where load-bearing). All flows enter through `request_pipeline.py::answer_question` (line 89) unless noted.344 345The flows are chosen to cover the distinct *outcome shapes* the system can produce, because each shape exercises a different part of the containment story:346 347| flow | outcome | what it exercises |348|---|---|---|349| UC1 | grounded scalar answer | the happy path, scalar provenance (exactly one pointer) |350| UC2 | grounded aggregate answer | the complete-set provenance + T4's special condition handling |351| UC2b | grounded window count | the only every-event tool; the window boundary rule |352| UC3 | grounded zero (absence) | completeness-of-the-empty-set, distinct from refusal |353| UC4 | abstention | out-of-coverage → refusal with no value at all |354| UC5 | typed rejection | the wall: raw-query and hallucinated-tool, pre-data |355| UC6 | typed error | model-down at select vs at compose |356| UC7 | (internal) | the independent resolver re-enumeration the others rely on |357| UC8 | (harness) | dual-score over the gold set |358| UC9 | (operational) | log line → metrics → surfaces |359 360Read UC1 first; the rest are mostly "same as UC1 except here is where it diverges."361 362### UC1 — A scalar question, answered end-to-end363 364Question: *"What is the stock state of pr-007 at st-003 as of 2026-01-06T08:00:00Z?"*365 366367 368<details>369<summary>Mermaid source</summary>370 371```mermaid372sequenceDiagram373 autonumber374 participant AQ as answer_question375 participant QI as intake376 participant TS as select_tool377 participant V as validate378 participant EX as execute (_t1)379 participant AT as attach380 participant RE as build_result_envelope381 participant AC as compose_nl382 participant ST as staple383 AQ->>QI: intake(q) → IntakeRequest (trim only)384 AQ->>TS: select_tool → ToolCall("get_stock_state", {store_id, product_id, as_of})385 AQ->>V: validate → ValidatedToolCall (sole constructor)386 AQ->>EX: execute → ExecutedResult(StockResult, contributing=(latest_event,))387 AQ->>AT: attach → ToolResult(value, provenance={1 EventPointer})388 AQ->>RE: build_result_envelope → ToolResult (StockResult shape confirmed)389 AQ->>AC: compose_nl → "pr-007 is out of stock at st-003 ..."390 AQ->>ST: staple → resolve() OK → AnswerEnvelope391```392 393</details>394 395Step-by-step:396 3971. **`question_intake.py::intake`** (line 42). Trims whitespace; wraps into `IntakeRequest`. No data, no model. The `det_ms` deterministic-span accumulator in `answer_question` is still 0 — intake is not timed into the budget span (it is trivial and pre-model).3982. **`tool_selector.py::select_tool`** (line 53). Calls `provider.select_tool(question, tool_specs)`. The `LocalModelAdapter` (`local_model_adapter.py::select_tool`, line 113) builds the OpenAI `tools=[...]` body from the registry — note `_tool_schema` (line 192) renders each param via `_param_schema` (line 219): an enum param becomes `{"type":"string","enum":[...]}`, a typed scalar becomes `{"type":"string"}`. There is no free-string param to render. The response is normalized by `_parse_tool_call` (line 228) into a `ToolCall`. This is the model's only choice point on the happy path. **Not timed into `det_ms`** (inference is bracketed out — `request_pipeline.py` line 161).3993. **`param_validator.py::validate`** (line 102). The wall. `get_tool_spec("get_stock_state")` resolves; no extra keys; each param checked by `_check_param` (line 161): the raw-query pattern (line 179) does not fire on `st-003`/`pr-007`/the ISO timestamp; the typed scalars pass their format predicates (`_check_scalar`, line 210). Returns a `ValidatedToolCall`. **Timed into `det_ms`** (`request_pipeline.py` line 190–192).4004. **`tool_executor.py::execute`** (line 95) → `_t1_get_stock_state` (line 175). `_scalar_params` (line 162) pulls the three keys; `_latest_le_as_of` (line 124) does an exact-match `index.latest_by_kp.get((store_id, product_id), ())` and walks the timestamp-ordered bucket applying `matches()` to keep the latest ≤ as_of. Returns `ExecutedResult(StockResult(value=latest.state), contributing=(latest,), provenance_kind="scalar", ...)`.4015. **`provenance_attacher.py::attach`** (line 55). `result.contributing` is non-empty → `_provenance_set` (line 88) asserts scalar cardinality == 1 and builds a one-pointer `ProvenanceSet`. Returns `ToolResult(asserted_value, provenance, observed_at=latest.timestamp)`.4026. **`result_envelope.py::build_result_envelope`** (line 38). Looks up the spec, confirms `result.asserted_value` is exactly `StockResult` (line 61). Returns the `ToolResult`. Steps 4–6 are timed into `det_ms` as one block (`request_pipeline.py` line 205–215).4037. **`answer_composer.py::compose_nl`** (line 48). Calls `provider.compose_nl` → `_compose_prompt` (line 310) shows only the value (never the provenance); `_parse_nl_text` (line 274) returns the assistant content. The `str` is tainted display text. **Not timed into `det_ms`.**4048. **`envelope_stapler.py::staple`** (line 64). Builds the `ResolverQuery` via `request_pipeline.py::_resolver_query` (line 263), calls `resolve(...)`, asserts `verdict.asserted_value_matches AND asserted_value == tool_result.asserted_value` (line 89), then copies value + provenance from the `ToolResult` into a new `AnswerEnvelope` with `nl_text`. **Timed into `det_ms`** (line 245–248).4059. **`request_pipeline.py::_finish`** (line 124). Emits one `RequestLogLine` (outcome `"answered"`, `provenance_resolution_outcome="resolved"`, `latency_ms=det_ms`) as a side-effect, optionally records it on the `MetricsCounter`, returns the `AnswerEnvelope` unchanged.406 407What did and did not get timed, summarized, because it is the most common thing readers get wrong: steps 3 (validate), 4–6 (execute → attach → egress), and 8 (staple) are added to `det_ms`; steps 1 (intake), 2 (select), and 7 (compose) are not. The two un-timed model steps are the only ones that call the provider. So `latency_ms` answers "how long did the deterministic data work take," and the model's latency — however variable — is deliberately outside the budgeted number. If you ever see `latency_ms` swing with model load, something has wired a model call into the timed span, which is a bug.408 409### UC2 — An aggregate question (T4 count_stores_with_condition)410 411Question: *"How many stores have condition OUT_OF_STOCK as of 2026-01-06T08:00:00Z?"*412 413414 415<details>416<summary>Mermaid source</summary>417 418```mermaid419sequenceDiagram420 autonumber421 participant AQ as answer_question422 participant EX as _t4_count_stores_with_condition423 participant AT as attach424 participant ST as staple425 participant RES as _re_derive_condition426 AQ->>EX: execute(validated T4 call, index)427 Note over EX: per-pair latest-≤-as_of · _condition_satisfied on the latest · distinct stores428 EX-->>AQ: ExecutedResult(ConditionCountResult, contributing=complete set)429 AQ->>AT: attach → ToolResult(count+store_ids, ProvenanceSet=all contributing)430 AQ->>ST: staple(ToolResult, nl_text, query carrying condition)431 ST->>RES: resolve over all_events (independent re-derivation)432 Note over RES: enumerate pairs · own _condition_satisfied · same complete set433 RES-->>ST: ResolverVerdict (count + keyset agree)434 ST-->>AQ: AnswerEnvelope435```436 437</details>438 439The flow is identical to UC1 through validation. The differences are in execution and resolution:440 441- **`tool_executor.py::_t4_count_stores_with_condition`** (line 275). Iterates **sorted** `index.latest_by_kp.keys()` (deterministic order, so the result is reproducible — pure/idempotent execution). For each `(store, product)` it takes the latest-≤-as_of event and tests `_condition_satisfied(latest, condition)` (line 251) — where `condition` is the validated 5-key *string*, not a `StoreCondition` enum. It collects the **complete** set of satisfying latest events into `contributing` and counts **distinct** `store_ids`. Note: `contributing` can exceed the store count (a store with the condition on two products contributes two events — the complete contributing set is what provenance requires). `provenance_kind="set"`.442- **`provenance_attacher.py::attach`** builds a `ProvenanceSet` over all contributing events (no scalar-cardinality check for a set).443- **`request_pipeline.py::_resolver_query`** (line 263) special-cases T4: it carries `condition = str(call.params["condition"])` so the resolver can re-derive the store rollup (the executor's `typed_filter` leaves `condition=None` because it is not an event-grain predicate).444- **`provenance_resolver.py::_re_derive_condition`** (line 208) re-derives **independently**: it enumerates the distinct `(store, product)` pairs in `all_events`, takes each pair's latest-≤-as_of event via `_latest_satisfying` (line 155), applies `_condition_satisfied` (line 187 — its own copy of the predicate over `all_events`), and produces the same complete satisfying set + count. The stapler's equality then confirms the executor's count and the keyset match the independent re-derivation.445 446### UC2b — A window count (T6, the only every-event tool)447 448Question: *"How many OUT_OF_STOCK events were recorded for st-003 in [2026-01-01T00:00:00Z, 2026-01-07T00:00:00Z)?"*449 450T6 is worth tracing on its own because it is the one tool that does **not** collapse to a per-key latest — it counts *every* event in the half-open window, so the boundary rule does real work:451 452- **`tool_executor.py::_t6_count_events_in_window`** (line 367). It reads the `by_store_state` bucket for `(store_id, state)` and applies `matches()` with a `Window(start, end)` where `start` is set. Because `start` is not `None`, `matches()` (line 33) takes the `[start, end)` branch: an event exactly at `start` matches, an event exactly at `end` does **not**. The complete satisfying tuple becomes `contributing`; the count is its length. There is no "latest" collapse — two events for the same `(store, product)` inside the window both count.453- **`provenance_attacher.py::attach`** builds a `ProvenanceSet` over every matching event (a set tool, so no scalar-cardinality check). If the window is empty, the empty branch builds an `AbsenceAttestation` instead — a real "zero events in this window," proven by the resolver re-enumerating the same window and also finding nothing.454- **`provenance_resolver.py::_re_derive_window`** (line 290) re-derives independently over `all_events` with the same `matches()` boundary rule. The end-exclusive boundary is exactly the kind of off-by-one a second, independent implementation would expose — and it does not, because both sides import the single predicate.455 456The contrast with UC1/UC2: the scalar tools care only about the upper bound (`as_of` selection picks the latest ≤ end); T6 cares about both bounds and counts all of them. That difference is the whole reason the window rule is pinned in one place.457 458### UC3 — A grounded zero (absence, not abstention)459 460Question: a scalar question for a `(store, product)` with no event on/before `as_of`.461 462- **`tool_executor.py::_t1_get_stock_state`** (line 175): `_latest_le_as_of` returns `None`, so the body returns `ExecutedResult(value=StockResult(value=ShelfState.OUT_OF_STOCK), contributing=(), ...)` — an empty `contributing`.463- **`provenance_attacher.py::attach`** (line 55) takes the empty branch (line 72): it builds an `AbsenceAttestation(typed_filter, window, store_snapshot_id=DATASET_VERSION, independently_enumerated_count=0)` — never an empty `ProvenanceSet`, never a fake pointer.464- **`envelope_stapler.py::staple`** → **`provenance_resolver.py::_verdict_absence`** (line 362): the absence is accepted **only** if the independent re-enumeration over `all_events` also finds nothing (`independently_empty`) and the attestation's count is 0. `_absence_value_ok` (line 415) checks the typed zero shape. This is "completeness-of-the-empty-set" — the strongest anti-fabrication check.465- `_finish` emits `outcome="absence"`, `provenance_resolution_outcome="absence_attested"`.466 467This is *distinct from abstention* (UC4): a grounded zero is a real answer ("nothing matched, and we proved it"), carried in an `AnswerEnvelope` with an `AbsenceAttestation`. An abstention is a refusal carried in an `AbstentionEnvelope` with no value at all.468 469### UC4 — Out-of-coverage → abstention470 471Question: *"How many staff work at st-003?"* (no registered tool covers headcount).472 473- **`tool_selector.py::select_tool`** → the `LocalModelAdapter` returns `OutOfCoverage` when the model emits no tool call (`local_model_adapter.py::_parse_tool_call`, line 240: `tool_calls` empty → `OutOfCoverage(reason="model proposed no registered tool")`). The system prompt (`_SELECT_TOOL_SYSTEM_PROMPT`, line 56) nudges the model to emit no call for headcount/sales/suppliers — but even if it forced a wrong tool, that would be a typed rejection or wrong-tool answer downstream, never a fabrication.474- **`request_pipeline.py::answer_question`** (line 163): `isinstance(selection, OutOfCoverage)` → returns `AbstentionEnvelope(reason=...)` **before any data step**. `_finish` records `outcome="abstained"`, `validation_outcome="not_applicable"`.475 476No `ToolCall` was produced, so no `ToolResult`, so no `asserted_value` — an `AnswerEnvelope` is structurally impossible here.477 478Abstention and a grounded zero (UC3) are easy to conflate but are different claims, and the type system keeps them apart:479 480| | abstention (UC4) | grounded zero (UC3) |481|---|---|---|482| what it means | "no registered tool can answer this" | "a tool answered, and the answer is nothing" |483| envelope | `AbstentionEnvelope` (no value) | `AnswerEnvelope` carrying an `AbsenceAttestation` |484| was a tool run? | no — refused before any data step | yes — the tool ran and found no matching event |485| proven how? | by the model proposing no tool | by the resolver re-enumerating and *also* finding nothing |486| log `outcome` | `abstained` | `absence` |487 488The distinction is load-bearing: returning an abstention for a question a tool *could* have answered (over-abstention) is a quality miss scored by the harness, and returning a grounded zero for an out-of-coverage question would be claiming a fact the data cannot support. The two are separate types so the code physically cannot blur them.489 490### UC5 — A raw-query / hallucinated-tool proposal → typed rejection491 492Two sub-cases, both caught at the wall before any data read:493 494- **Raw-query in a typed slot** (e.g. the model proposes `get_price` with `product_id="pr-001' OR 1=1"`). `param_validator.py::_check_param` (line 161) runs the raw-query check **first** (line 179): `_RAW_QUERY_PATTERN` (line 86) matches the quote/`OR`/etc. → `RawQueryRejected(tool_name, field, reason)`. The pattern is tuned to fire on comparison/boolean/SQL syntax that a legitimate id/timestamp/enum token never carries; a merely out-of-domain plain token (e.g. `"on_fire"`) carries none of these and is correctly a `ValidationError` instead — keeping the two disjoint by input shape.495- **Hallucinated tool name** (e.g. `get_inventory_forecast`). `validate` (line 118) calls `get_tool_spec`, which returns `UnregisteredTool` (`tool_registry.py::get_tool_spec`, line 171). Never coerced to `OutOfCoverage` — a hallucinated tool is a distinct, scored failure, not a clean refusal.496 497In both, `request_pipeline.py` (line 193) returns the typed rejection; `_finish` records `outcome="rejected"`, `error_kind=type(rejection).__name__`. No exception is raised; no store is touched.498 499The disjointness of the three rejection types is what makes the wall predictable. Worked examples:500 501| model proposal | rejection | reason |502|---|---|---|503| `get_inventory_forecast(...)` | `UnregisteredTool` | name not in the closed menu |504| `get_price(store_id="st-001", product_id="pr-001", as_of=..., notes="x")` | `ValidationError` | an extra key the spec does not declare |505| `get_price(store_id="st-001", product_id="banana", as_of=...)` | `ValidationError` | `product_id` is a plain token that fails the `pr-NNN` format |506| `get_price(store_id="st-001", product_id="pr-001' OR 1=1", as_of=...)` | `RawQueryRejected` | the value carries SQL/boolean syntax the raw-query pattern matches |507| `count_events_in_window(..., start=T2, end=T1)` | `ValidationError` | both timestamps valid, but `start > end` (a wrong-value range) |508 509The first three and the last are well-shaped-but-wrong (`ValidationError` / `UnregisteredTool`); only a value carrying query/expression syntax is a `RawQueryRejected`. A reader sometimes expects `"banana"` to be a raw-query rejection — it is not, because it carries no operator/quote/keyword; it is simply an out-of-format token. Keeping the two disjoint by input shape is what lets the adversarial test assert *which* rejection each input produces, not merely that it was rejected.510 511### UC6 — Model-down → ModelUnavailable512 513The Ollama endpoint is unreachable (connection refused / timeout / 5xx).514 515- The transport (`local_model_adapter.py::urllib_transport`, line 324) catches the failure class and raises `ModelUnavailableError` carrying a typed `ModelUnavailable` whose `detail` is a *typed summary* (`transport_error=...` / `http_status=...`), never a raw model body echo.516- **`local_model_adapter.py::_post`** (line 159) re-stamps the stage; **`tool_selector.py::select_tool`** (line 67) catches `ModelUnavailableError` at the agent-surface boundary and returns `ModelUnavailable(stage="select_tool")` — so no exception escapes onto the request path.517- **`request_pipeline.py`** (line 174) returns it; `_finish` records `outcome="error"`, `error_kind="model_unavailable"`.518 519If instead the model-down happens at **compose** (job-2), the fact already exists in the `ToolResult` — only the prose is missing. `answer_composer.py::compose_nl` (line 62) returns `ModelUnavailable(stage="compose_nl")`, and `request_pipeline.py` (line 231) surfaces it. Either way: a typed error, never a fabricated answer — with no value to staple, an `AnswerEnvelope` is structurally impossible.520 521The two stages differ in how much work was already done, but not in safety:522 523| stage | what existed when it failed | outcome | containment |524|---|---|---|---|525| `select_tool` (job-1) | nothing — no `ToolCall` yet | `ModelUnavailable(stage="select_tool")` | no `ToolResult` could exist, so no fact to fabricate |526| `compose_nl` (job-2) | the full `ToolResult` (fact + provenance) | `ModelUnavailable(stage="compose_nl")` | the fact was computed and is correct; only the display sentence is missing |527 528In both cases the `detail` field is a *typed summary* of the transport failure (`transport_error=...` or `http_status=...`), never a raw echo of whatever bytes the endpoint returned — so even a misbehaving endpoint cannot smuggle attacker-influenced text back through the error path. The `stage` field is what an operator reads to tell "the model never engaged" from "the model answered the data step but could not phrase it."529 530### UC7 — The independent resolver re-enumeration path531 532This is the load-bearing independence. Triggered inside `envelope_stapler.py::staple` (every grounded answer) and inside the eval harness (every answered gold item). It never reads the executor's index.533 534535 536<details>537<summary>Mermaid source</summary>538 539```mermaid540flowchart TD541 Q["ResolverQuery<br/>(tool_name, typed_filter, condition?)"] --> RD["resolve()"]542 RD --> DER["_re_derive — dispatch by tool"]543 DER -->|T1/T2/T3| S["_re_derive_scalar<br/>_latest_satisfying over all_events"]544 DER -->|T4| C["_re_derive_condition<br/>per-pair latest + _condition_satisfied"]545 DER -->|T5| L["_re_derive_list<br/>per-product latest, then state filter"]546 DER -->|T6| W["_re_derive_window<br/>matches() over all_events"]547 S --> TR["_Truth(satisfying, value)"]548 C --> TR549 L --> TR550 W --> TR551 TR --> VP{"isinstance provenance<br/>AbsenceAttestation?"}552 VP -->|no| POS["_verdict_positive<br/>validity + value + completeness"]553 VP -->|yes| ABS["_verdict_absence<br/>completeness-of-empty-set"]554 POS --> VERD["ResolverVerdict"]555 ABS --> VERD556```557 558</details>559 560Trace of `_verdict_positive` (line 307):5611. `truth_keyset` = `{event_id for e in truth.satisfying}` (the independently re-derived satisfying set).5622. `asserted_keyset` = `keyset(provenance)` (the answer's provenance, projected to `event_id`).5633. `provenance_valid`: every asserted `event_id` must be in the re-derived satisfying set; an empty positive set fails (a positive fact must carry a non-empty contributing set, line 333).5644. `asserted_value_matches`: `asserted_value == truth.value`.5655. `completeness_ok`: `asserted_keyset == truth_keyset` — one set-equality catching both omission (under-count) and an extra pointer (over-count).566 567The resolver shares **only** `matches()` with the executor; its data path is `all_events`, fully independent. This is what makes three-site agreement (executor / gold / resolver) mean *completeness*, not a shared bug. SUITE-resolver-meta (`tests/test_suite_resolver_meta.py`) injects defects and confirms the resolver catches each — the test is the proof that the resolver is not vacuously passing:568 569| injected defect | which clause should fail | why |570|---|---|---|571| a dangling pointer (an `event_id` not in `all_events`) | `provenance_valid` | the asserted pointer does not resolve to a real satisfying event |572| a dropped event (omit one real contributor) | `completeness_ok` | the asserted keyset is a strict subset of the re-derived set (under-count) |573| an extra pointer (a real but non-satisfying event) | `completeness_ok` | the asserted keyset has a member the re-derived set lacks (over-count) |574| a wrong asserted value | `asserted_value_matches` | the copied value disagrees with the re-derivation |575| a faked absence (claim zero when events exist) | `_verdict_absence` | the independent enumeration is non-empty, so the attestation is rejected |576 577Each row is a way a fabrication could try to slip through; the resolver's three-clause verdict (plus the absence path) is constructed so that each maps to a specific failing clause. If you add a tool, the resolver-meta suite is where you prove its re-derivation actually catches a tampered answer.578 579### UC8 — The eval harness run (dual-score)580 581Entrypoint: **`report.py::run_acceptance`** (line 160) — or `load_and_run(provider)` (line 190) which does the one store-ingress first.582 583For each gold item, **`_eval_item`** (line 207):5841. Times and drives `answer_question(item.question, index, all_events, provider, ...)`.5852. **Correctness axis**: `correctness_scorer.py::score_correctness` (line 89). For an in-coverage item, `_score_in_coverage` (line 126) requires an `AnswerEnvelope` and dispatches `_score_value` (line 141) by gold `kind` — scalar `_eq`, aggregate `_eq_aggregate` (count + store_ids set), list `_eq_set`, window `_eq`. For an OOC item, `_score_out_of_coverage` (line 103) requires the `AbstentionEnvelope`.5863. **Provenance axis**: only when the outcome is an `AnswerEnvelope`. `_resolver_query_for` (line 429) re-derives the `ResolverQuery` from the gold *question template* (model-free, via the `_SCALAR_RE` / `_CONDITION_RE` / `_LIST_RE` / `_WINDOW_RE` regexes). If the model picked the **right** tool (`_shape_matches_kind`, line 285), the full `resolve(...)` runs (real GATE-b). If it picked a **wrong** tool, `_provenance_pointers_resolve` (line 296) does the tool-agnostic containment check: every pointer must be a real `event_id` in `all_events` — a wrong-tool answer is a correctness miss but still *contained*.5874. `item_passes = correctness.correct AND prov_axis_ok`.588 589**`_assemble`** (line 338) rolls up:590- `gate_a_ok`: in-coverage correctness ≥ 90% (≥41/45).591- `gate_e_action_ok`: OOC abstention ≥ 90%.592- `gate_e_containment_ok`: 100% structural — **every** answered/absence item's provenance resolves to real events (an unresolvable pointer is the fabrication signal, which the structural pipeline makes impossible).593- `provenance_ok`: 100% over the answered+absence subset.594 595**`_metrics`** (line 395) derives the 5 metrics over the per-item rows. **`write_report`** (line 562) is the single stateful write (indented JSON via `render_report`, line 502).596 597598 599<details>600<summary>Mermaid source</summary>601 602```mermaid603sequenceDiagram604 autonumber605 participant R as run_acceptance606 participant EI as _eval_item607 participant AQ as answer_question608 participant CS as score_correctness609 participant RES as resolve / _provenance_pointers_resolve610 participant AS as _assemble611 loop each gold item612 R->>EI: drive one item613 EI->>AQ: answer_question(item.question, ...)614 AQ-->>EI: typed outcome615 EI->>CS: correctness axis (model-free equality)616 EI->>RES: provenance axis (right-tool → full resolve · wrong-tool → containment)617 EI-->>R: ItemResult (passes iff BOTH axes pass)618 end619 R->>AS: roll up gates + 5 metrics620 AS-->>R: AcceptanceReport621 Note over R: write_report — the one stateful write622```623 624</details>625 626A representative live run (Ollama up, the demo model, local mode) recorded the in-coverage correctness gate at full marks, the abstention-action gate clean, and containment satisfied; the run output is a JSON report serialized by `write_report`. The containment number is the one that must be 100% — it is the structural property, and the rest of the pipeline is what makes producing an unresolvable pointer impossible.627 628### UC9 — The observability flow (log line → metrics → surfaces)629 630Every request emits exactly one log line; the metrics are *derived* from those lines.631 632633 634<details>635<summary>Mermaid source</summary>636 637```mermaid638sequenceDiagram639 autonumber640 participant AQ as answer_question641 participant F as _finish642 participant E as log_line.emit643 participant C as MetricsCounter644 participant M as metrics_from_lines645 participant EP as metrics_endpoint646 AQ->>F: every return path647 F->>E: emit(RequestLogLine) (append-only JSON, side-effect)648 opt counter supplied (live demo)649 F->>C: counter.record(line.model_dump())650 end651 Note over E: NEVER alters asserted_value/provenance (pure side-effect)652 EP->>C: counter.metrics()653 C->>M: metrics_from_lines(snapshot())654 M-->>EP: 5 metrics655```656 657</details>658 659- **`request_pipeline.py::_finish`** (line 124) builds the `RequestLogLine` (the structured log-line field set, `latency_ms=det_ms` — the deterministic span only) and calls `log_line.py::emit` (line 105). `emit` logs `line.to_json()` via stdlib `logging` — no dependency. If a `counter` was passed, it also records the line on the `MetricsCounter`.660- **`log_line.py::metrics_from_lines`** (line 144) aggregates parsed lines into the five metrics. `_require_field` (line 126) is the §10 falsifier: an omitted `outcome` / `latency_ms` / `provenance_resolution_outcome` raises `UncomputableMetric` rather than leaking a wrong value.661- **`app_surfaces.py::healthz`** (line 60) — 200 `{status, dataset_version, model_name}` (reusing `store_loader.store_status`) once loaded; 503 before.662- **`app_surfaces.py::metrics_endpoint`** (line 111) — gated by `METRICS_ENABLED` (404 when off); returns `counter.metrics()`, the same aggregation as the offline run-log roll-up.663 664The five metrics, each derived purely by aggregating the JSON lines (`metrics_from_lines`, line 144):665 666| metric | derived from | meaning |667|---|---|---|668| `request_count` | line count | how many requests were logged |669| `latency_p95` | `latency_ms` field | 95th percentile of the deterministic span (the budgeted path) |670| `error_rate` | `outcome` field | fraction of requests whose outcome was an error |671| `provenance_resolution_rate` | `provenance_resolution_outcome` field | fraction of answers whose provenance resolved |672| `abstention_rate` | `outcome` field | fraction of requests that abstained |673 674The point of deriving metrics from the durable log rather than from a separate in-code counter is that the log is the source of truth — the live `MetricsCounter` and the offline run-log roll-up call the *same* `metrics_from_lines`, so the live `/metrics` surface and an after-the-fact analysis of the log agree by construction. And because a missing field raises `UncomputableMetric` instead of defaulting, a log schema regression is a loud failure, not a silently-wrong number.675 676SUITE-load (`tests/test_suite_load.py`) drives 16 concurrent workers × 200 requests through `answer_question` with a pinned `OracleProvider` (no model), records every line on a shared `MetricsCounter` under its lock, and asserts `metrics_from_lines(...)["latency_p95"] <= 300`. The concurrency proves the counter's append-only path has no interleave corruption — the lock-guarded `record` is the one place mutable state is touched, and 3,200 concurrent appends exercising it without corruption is the evidence.677 678---679 680## Part 3 — Class-by-class deep dive681 682Ordered roughly bottom-up: the contracts vocabulary first, then the deterministic data layer, then the agent surface, then the harness and surfaces. Each subsection lists the load-bearing fields/methods, the invariant the type enforces, and the edge cases worth remembering.683 684A note on style before you read: most types here are frozen dataclasses, and the *type itself* is usually the invariant — `ValidatedToolCall` exists only because the wall constructed it, an `AnswerEnvelope` exists only because the stapler built it, a positive `ProvenanceSet` is never empty because the attacher fails closed otherwise. So when a subsection says "X is a distinct type," that is not bookkeeping; it is the mechanism. The functions are mostly pure (input → output, no hidden state), and the few exceptions (the loaders raise; the counter and the report write) are called out explicitly.685 686### 3.1 The typed scalars and enums (`contracts/ids.py`, `contracts/enums.py`)687 688The whole no-raw-query property starts here. `StoreId` / `ProductId` / `Timestamp` / `EventId` are distinct `NewType`s over `str` (`ids.py` lines 24–27), so `mypy --strict` keeps them apart and a plain `str` cannot flow into a slot typed as one of them. Each has a format validator — `is_valid_store_id` (line 37), `is_valid_product_id` (line 42), `is_valid_event_id` (line 47), `is_valid_timestamp` (line 52) — pinned to a regex (`st-\d{3}`, `pr-\d{3}`, `evt-\d{6}`, ISO-8601 UTC with a trailing `Z`). The timestamp validator is regex-only on purpose: the contracts layer owns the cheap *lexical* gate, and the semantic parse/tz-normalization is a loader concern. Because the timestamp is fixed-width and `Z`-suffixed, lexicographic string order equals chronological order — the property the window rule relies on.689 690`enums.py` holds three closed `str`-enums: `ShelfState` (4 members), `ComplianceFlag` (4 members), and `StoreCondition` (4 `HAS_*` members). Edge case: `StoreCondition`'s `HAS_*` names are *not* the strings the T4 condition param actually carries — see 3.9 and the pitfall in WALKTHROUGH §12. Both readings are deliberately kept; the index/param uses the 5-key string set (`OUT_OF_STOCK`, `LOW_STOCK`, `PRICE_MISMATCH`, `EXPIRED`, `PLANOGRAM_VIOLATION`), the enum keeps the rollup names. Because these enums subclass `str`, an enum member *is* its string value, which is why the loader can `ShelfState(raw)` straight from JSON and why a wire value round-trips without a translation table — at the cost of the one naming mismatch above, which is documented rather than "fixed" so the executor/resolver/gold stay in agreement.691 692### 3.2 `ScanEvent` and the row types (`contracts/data_model.py`)693 694`ScanEvent` (line 27) is the provenance anchor — one immutable observation of `(event_id, store_id, product_id, timestamp, state, price_cents, compliance_flag)`. Invariant: `event_id` is globally unique across the whole store and stable across loads (enforced at load, not here). `price_cents` is a non-negative integer — no float money. `Store` (line 46) and `Product` (line 53) are the reference rows. `GoldItem` (line 67) is one frozen eval question with its `expected_answer`, its `expected_provenance_event_ids` (the same `event_id` key the resolver compares on), its `category`, and `is_out_of_coverage`. Every type here is a frozen dataclass — the pure in-memory vocabulary the request path and the resolver read.695 696### 3.3 `ParamSpec` / `ToolSpec` / `ToolCall` / `ValidatedToolCall` — the closed menu (`contracts/tools.py`)697 698`ParamType` (line 30) is a closed `Literal` of six strings (`StoreId`, `ProductId`, `Timestamp`, `ShelfState`, `ComplianceFlag`, `StoreCondition`). There is no expression/predicate/passthrough member — **this is the tools-only / no-raw-query rule enforced at the type level**; naming a seventh free-form member is a type error. `ProvenanceKind` (line 35) is `"scalar" | "set"`.699 700The per-tool return dataclasses are concrete, never a loose `object`: `StockResult` (line 39, a `ShelfState`), `PriceResult` (line 46, int cents), `ComplianceResult` (line 53, a `ComplianceFlag`), `ConditionCountResult` (line 60, `count` + `store_ids`), `ProductListResult` (line 68, `product_ids`), `WindowCountResult` (line 75, `count`). `ReturnSchema` (line 84) is the closed union of those; `ReturnSchemaType` (line 96) is the union of their *class objects* (what `ToolSpec.return_schema` carries).701 702`ParamSpec` (line 106) carries `name`, `type` (a `ParamType`), `enum_domain` (the allowed string set for an enum param, or `None` for a typed scalar), and `required`. `ToolSpec` (line 118) is the menu entry: `name`, ordered `params`, `return_schema`, `provenance_kind`. `ToolCall` (line 130, untrusted, open `params` map) vs `ValidatedToolCall` (line 140, post-validation): the type distinction *is* the structural gate — the executor's signature accepts only `ValidatedToolCall`, and `param_validator.validate` is its sole constructor.703 704### 3.4 The provenance types (`contracts/provenance.py`, `contracts/filters.py`, `contracts/envelopes.py`)705 706`EventPointer` (`provenance.py` line 28) is declared `eq=False` so its custom `__eq__`/`__hash__` (lines 39/44) compare on `event_id` **alone** — `store_id` is denormalized for readability and is not identity. `provenance_key` (line 53) and `keyset` (line 59) project a pointer/set to its `event_id` keyset, the single projection used at all three comparison sites. `ProvenanceSet` (line 50) is `FrozenSet[EventPointer]`, never empty for a positive fact. `AbsenceAttestation` (line 66) carries the searched `typed_filter`, `window`, `store_snapshot_id`, and `independently_enumerated_count` (must be 0, set by the resolver).707 708`filters.py`: `Window` (line 26) is `(start: Optional[Timestamp], end: Timestamp)` — `start is None` means as_of selection; the inclusivity rule lives in `matches()`, not here. `TypedFilter` (line 39) is the closed selection surface: `store_id` / `product_id` / `state` / `condition` (each optional typed/enum) + a `window`. No free-form member exists, so a free-form filter is unrepresentable.709 710`envelopes.py`: `Provenance` (line 28) is `Union[ProvenanceSet, AbsenceAttestation]`. `ToolResult[V]` (line 34) is the deterministic fact — `asserted_value: V`, `provenance`, `observed_at`. `AnswerEnvelope[V]` (line 45) is the grounded answer — the same `asserted_value`/`provenance` copied in by the stapler plus `nl_text` (the only model-written field). `AbstentionEnvelope` (line 58) is a distinct type with no value. `ResolverVerdict` (line 67) carries the three booleans + `reasons`.711 712### 3.5 The error taxonomy, settings, and stub marker (`contracts/errors.py`, `settings.py`, `stub.py`)713 714`errors.py` is the typed request-path taxonomy — every cross-module non-happy return is one of these values, never a raised exception: `ValidationError` (line 27, well-shaped wrong value; `got` is a typed summary, never a raw echo), `UnregisteredTool` (line 43, never coerced to abstention), `RawQueryRejected` (line 51, structurally ill-typed), `OutOfCoverage` (line 63, the abstention signal), `ModelUnavailable` (line 71, a transport failure carrying a typed summary), and `ProvenanceResolutionError` (line 85, an offline-only fault the resolver detects).715 716`settings.py::Settings` (line 30) is the one pydantic boundary object (the pure contracts stay framework-free). `extra="forbid"` rejects an unknown env var; `DATASET_VERSION` is required with no default (the build fails fast if unset). The env-triple (`MODEL_BASE_URL`/`MODEL_API_KEY`/`MODEL_NAME`) plus `RUN_MODE` is the config-only swap. `_check_topology` (line 61) refuses a `deployed` run pointed at a host loopback — a mis-config caught at load.717 718`stub.py`: the `Stub` base (line 26) sets `__is_stub__ = True` and raises on construction; the `@stub` decorator (line 43) tags a function and raises on call; `is_stub` (line 62) is the predicate the no-stub-reachable CI walker keys on. At runtime the only stub is the hosted adapter, deliberately off the production import graph.719 720Edge cases worth knowing in this group: the `ModelUnavailable.stage` field is a closed `Literal["select_tool", "compose_nl"]`, so an operator can always tell *which* model call failed without parsing a free-form string. `ValidationError.got` is documented as a typed summary and is derived safely at each construction site — the type fixes the shape, the caller is responsible for never putting a raw attacker value there. And `Settings`'s topology validator only fires for `RUN_MODE=deployed`; the `demo_local` default permits a loopback `MODEL_BASE_URL`, which is exactly what the local Ollama demo needs — so the check protects the deployed path without getting in the way of the local one.721 722### 3.6 `matches` — the single predicate (`contracts/matching.py`)723 724`matches(event, f)` (line 33) is the one place that decides whether a scan event satisfies a typed filter: exact equality on each pinned dimension (`store_id`, `product_id`, `state`), then the window rule — `start is None` → `timestamp <= end` (inclusive upper bound); else `start <= timestamp < end` (half-open, start-inclusive/end-exclusive). It is a pure leaf depending only on `(event, filter)`, with no store/index/executor/resolver import, so the executor and the resolver can both import it without a cycle. `condition` is deliberately **not** applied at the event grain — it is a store-level rollup the executor/resolver compute from the per-store latest-≤-as_of set. The hand-authored truth-table test pins this predicate's expected booleans before any fixture exists, so it is proven against an independent oracle rather than itself.725 726The two boundary cases that bite if you change anything: with `start=None` and `end=T`, an event *at exactly* `T` matches (inclusive), so the scalar `as_of` selection includes an event observed at the as_of instant. With `start=T0` and `end=T1`, an event at `T0` matches and an event at `T1` does **not** (half-open) — so a single instant belongs to exactly one adjacent window, never both, and counts cannot double on a shared boundary. Because timestamps are fixed-width `Z`-suffixed strings, all of this is plain string comparison; there is no date parsing in the hot path, and no timezone to get wrong.727 728### 3.7 `EventIndex` & the loaders (`store/event_index.py`, `store_loader.py`, `gold_loader.py`)729 730`EventIndex` (`event_index.py` line 64) holds four maps, each value a `(timestamp, event_id)`-sorted tuple: `latest_by_kp`, `by_store_state`, `by_condition`, `all_events`. `build_index` (line 90) buckets every event by `(store, product)` and `(store, state)`, and into derived condition buckets via **independent `if`s** (lines 110–119) — an event can land in more than one condition bucket (out-of-stock *and* expired). `by_condition` is pre-seeded with all five keys (lines 49–61) so an empty bucket is a present key; every lookup is `.get(key, ())`, so the absence path returns `()`, never `KeyError`. `_order_key` (line 80) is the `(timestamp, event_id)` total order.731 732`store_loader.load_store` (line 198) reads `store.json`, rehydrates the frozen rows, and runs `_assert_invariants` (line 164): dataset-version lockstep, global `event_id` uniqueness, referential integrity. A violation raises `LoadError` (line 48) — fail closed at boot. `LoadedStore` (line 59) exposes `all_events` (line 73) and `store_status` (line 236, the store-side `/healthz` body). Edge case: `_parse_event` (line 131) rejects a JSON boolean for `price_cents` (line 147) because `bool` is an `int` subclass.733 734`gold_loader.load_gold` (line 246) loads the ≥48-item gold set, validating each id (`gold-\d{3}`), the typed-per-category `expected_answer`, and `expected_provenance_event_ids`. `GoldLoadError` / `GoldVersionMismatch` (lines 74/86) fail closed; the gold set is change-controlled.735 736### 3.8 `ValidatedToolCall` & the wall (`validator/param_validator.py`)737 738`validate(call)` (line 102) is a pure function of `(call, registry)` — no store read, so the rejection precedes any execution by construction. Order of checks (disjoint, ordered): resolve name → reject extra key (`_first_extra_key`, line 146) → per-param presence + value (`_check_param`, line 161, raw-query check **before** type check, line 179) → T6 `start <= end` (`_check_window_order`, line 231) → construct the token. `_check_enum` (line 191) and `_check_scalar` (line 210) are the per-type checks; `_expected_desc` (line 255) renders a value-free description (the enum domain or the scalar label) — `got` is never the offending value (no raw echo).739 740The `_RAW_QUERY_PATTERN` (line 86) is the structural raw-query detector: comparison/boolean operators, SQL keywords, quotes, semicolons, parens, braces. Edge case to remember: a plain out-of-domain token (e.g. `"on_fire"`) carries none of these → `ValidationError`, not `RawQueryRejected`. The two are disjoint by input shape.741 742### 3.9 `ExecutedResult` & the six tool bodies (`executor/tool_executor.py`)743 744`ExecutedResult` (line 73) carries the typed `value`, the exact `contributing` events, the `provenance_kind`, and the searched `typed_filter`/`window` (so the attacher can describe an absence). `execute` (line 95) dispatches by name (total over the closed six; a defensive `AssertionError` for the unreachable unregistered case, since validation guarantees a registered name).745 746- Scalars (T1–T3, lines 175/200/224): `_scalar_params` (line 162) pulls the three keys; `_latest_le_as_of` (line 124) does an exact-key bucket lookup + `matches()` to find the latest ≤ as_of. Empty → grounded zero (empty `contributing`).747- T4 (line 275): enumerate **sorted** `latest_by_kp` keys (deterministic order), take each pair's latest-≤-as_of, test `_condition_satisfied` (line 251) on the **latest** event — `condition` is the validated 5-key string, not the enum — collect the complete satisfying set, count **distinct** stores. `contributing` can exceed the store count (two products at one store contribute two events).748- T5 (line 327): per-product latest at the pinned store, keep those whose latest event is in the requested state.749- T6 (line 367): the one tool that counts **every** event in `[start, end)` (not just the latest) — `by_store_state` bucket + `matches()`.750 751### 3.10 The attacher and the egress gate (`attacher/provenance_attacher.py`, `result_envelope/result_envelope.py`)752 753`attach(result)` (`provenance_attacher.py` line 55): contributing events → `_provenance_set` (line 88, scalar cardinality + non-empty checks, fail-closed `ProvenanceIntegrityError`); empty → `AbsenceAttestation` seeded count 0. `_pointer` (line 110) builds an `EventPointer(event_id, store_id)` — identity is `event_id` only. `build_result_envelope` (`result_envelope.py` line 38) is the egress gate: it looks up the spec and confirms the computed `asserted_value` is exactly the dataclass the tool declares (line 61), catching a tool body that returned the wrong shape before the answer is stapled.754 755### 3.11 `provenance_resolver` — the oracle (`resolver/provenance_resolver.py`)756 757`ResolverQuery` (line 82) is `(tool_name, typed_filter, condition?)` — sufficient, with `all_events`, to re-compute the truth independently. `resolve` (line 100) → `_re_derive` (line 139, dispatch) → `_verdict_positive` (line 307) or `_verdict_absence` (line 362). `_Truth` (line 128) is the resolver's own re-derived `(satisfying, value)`. The four `_re_derive_*` functions (scalar line 170, condition line 208, list line 250, window line 290) each re-implement a tool's enumeration over `all_events` — the executor is never imported, and the resolver carries its **own** `_condition_satisfied` (line 187) and `_latest_satisfying` (line 155). `_absence_value_ok` (line 415) checks the typed-zero shape. See UC7 for the verdict logic; the independence is what makes three-site agreement mean completeness.758 759### 3.12 `question_intake`, `tool_selector`, `answer_composer` — the agent surface (`question_intake/`, `tool_selector/`, `answer_composer/`)760 761`intake(raw_question)` (`question_intake.py` line 42) wraps the untrusted string into `IntakeRequest` (line 29, trimmed `question` + the original for the log). No data, no model — the text stays untrusted.762 763`select_tool(question, tool_specs, provider)` (`tool_selector.py` line 53) is LLM job-1: it calls `provider.select_tool` and returns a `ToolCall` (untrusted), an `OutOfCoverage`, or — catching `ModelUnavailableError` at the boundary (line 67) — a `ModelUnavailable(stage="select_tool")`. The model picking a tool is not the tool being callable; the return is tainted.764 765`compose_nl(result, provider)` (`answer_composer.py` line 48) is LLM job-2: it asks the model for one sentence about an already-computed `ToolResult`, returns that `str`, and catches `ModelUnavailableError` (line 62) → `ModelUnavailable(stage="compose_nl")`. A compose failure is benign for containment — the fact already exists; only the prose is missing.766 767### 3.13 `envelope_stapler` — the single writer (`envelope_stapler/envelope_stapler.py`)768 769`staple(tool_result, nl_text, query, all_events)` (line 64): resolve → assert equality (line 89) → copy value+provenance into an `AnswerEnvelope`. `StapleEqualityError` (line 52) is a fail-closed guard, not a request-path outcome — the value was copied by code from the same `ToolResult`, so divergence is only possible if the copy itself is broken. **The module imports no provider** (line 47 note) — the structural non-LLM-writability guarantee, re-checked by the static taint guard.770 771### 3.14 The provider seam (`contracts/provider.py`, `provider_iface/provider_iface.py`, adapters)772 773`ProviderIface` (`provider.py` line 26) — a `runtime_checkable` Protocol with two methods (`select_tool`, `compose_nl`), wire-format-agnostic. `resolve_provider(settings, transport)` (`provider_iface.py` line 39) — config-only factory, total over the closed `RUN_MODE` literal. `LocalModelAdapter` (`local_model_adapter.py`, line 98) — a dataclass holding the env-triple + injectable transport; `select_tool` (line 113) builds the OpenAI `tools=[...]` body via `_tool_schema` (line 192) / `_param_schema` (line 219) — an enum param becomes `{"type":"string","enum":[...]}`, a typed scalar `{"type":"string"}`, no free-string param exists; `_parse_tool_call` (line 228) normalizes the response (empty tool_calls → `OutOfCoverage`); `urllib_transport` (line 324) is the stdlib POST that funnels every failure into a typed `ModelUnavailable`. `HostedModelAdapter` (`hosted_model_adapter.py`, line 45) — class-level `__is_stub__ = True` + `@stub` on both methods; constructable but raises on call; off the request-entrypoint graph.774 775### 3.15 `correctness_scorer` — the model-quality axis (`correctness_scorer/correctness_scorer.py`)776 777`CorrectnessVerdict` (line 71) carries `item_id`, `category`, `is_out_of_coverage`, `correct`, `reason`. `score_correctness` (line 89) is model-free structured equality: `_score_in_coverage` (line 126) requires an `AnswerEnvelope` and dispatches `_score_value` (line 141) by gold `kind` — scalar `_eq`, aggregate `_eq_aggregate` (count + store_ids set), list `_eq_set`, window `_eq`; `_score_out_of_coverage` (line 103) requires the `AbstentionEnvelope`. It never calls a model and never reads provenance — that is the resolver's separate axis.778 779### 3.16 `report` — the dual-score harness (`report/report.py`)780 781`run_acceptance` (line 160) — or `load_and_run` (line 190), which does the one store-ingress first — drives every gold item through `answer_question`, then scores both axes per item in `_eval_item` (line 207): correctness via `correctness_scorer`, provenance via `provenance_resolver` with the query re-derived from the gold question template by `_resolver_query_for` (line 429, the fixed-template regexes `_SCALAR_RE`/`_CONDITION_RE`/`_LIST_RE`/`_WINDOW_RE`, lines 89–92). `_shape_matches_kind` (line 285) decides right-tool vs wrong-tool; a wrong-tool answer falls back to the tool-agnostic `_provenance_pointers_resolve` (line 296) — still contained. `_assemble` (line 338) rolls per-item `ItemResult` (line 101) into the gates + `Metrics` (line 117) → `AcceptanceReport` (line 128); `_metrics` (line 395) derives the five; `render_report` (line 502) serializes and `write_report` (line 562) is the one stateful write.782 783The gold-question templates the regexes recover the structured query from:784 785| gold kind | template shape (abbreviated) | regex |786|---|---|---|787| scalar | "... of `pr-NNN` at `st-NNN` as of `<ts>`?" | `_SCALAR_RE` |788| aggregate | "... condition `<COND>` as of `<ts>`?" | `_CONDITION_RE` |789| list | "... state `<STATE>` at `st-NNN` as of `<ts>`?" | `_LIST_RE` |790| window | "... `<STATE>` events ... for `st-NNN` in [`<ts>`, `<ts>`)" | `_WINDOW_RE` |791 792Parsing the gold question deterministically (rather than re-using whatever the model proposed) is what keeps the provenance axis model-independent: the resolver is fed the query the *question* implies, so a model that picked a wrong tool cannot also dodge the containment check.793 794### 3.17 The observability classes and the HTTP shim (`observability/`, `app/server.py`)795 796`RequestLogLine` (`log_line.py`, line 73) — a pydantic model, the structured per-request log field set (`protected_namespaces=()` so `model_name` is a legal field name); `to_json` (line 100) serializes it. `emit` (line 105) logs it via stdlib `logging`. `metrics_from_lines` (line 144) aggregates parsed lines into the five metrics; `_require_field` (line 126) raises `UncomputableMetric` (line 116) on a missing field rather than substituting a wrong value. `MetricsCounter` (`app_surfaces.py`, line 81) — append-only row list; `record` (line 96) under a lock, `snapshot` (line 101), `metrics` (line 106) via the shared aggregation. `HttpResponse` (line 47) — a framework-free `(status, body)` value object; `healthz` (line 60) and `metrics_endpoint` (line 111) return one. `app/server.py` is the `ThreadingHTTPServer` shim: `AppState` (line 59) loads the store+index once (None on failure → 503), `_handler_factory` (line 86) builds the handler whose `do_GET` (line 104) routes only `/healthz` and `/metrics` and 404s the rest; `make_server` (line 118) / `main` (line 143) bind the port. `_resolve_port` (line 134) honors `$PORT` (a common platform convention) then `$APP_PORT`, falling back to 7860, so the same image runs on different hosts without code change. `make_server` constructs but does not start the server, so a test can drive the handler directly with no socket; `main` is the container entrypoint that calls `serve_forever`. It imports no provider, so it adds no stub to the production graph — the no-stub-reachable guard roots its walk at this module precisely because it is the deployed entrypoint.797 798### 3.18 `request_pipeline` — the composition entrypoint (`agent/request_pipeline.py`)799 800`answer_question` (line 89) wires the locked modules in the fixed order and returns a typed `RequestOutcome`. The `det_ms` accumulator (line 122) is advanced only around the deterministic steps (validate line 192, execute-through-egress line 215, staple line 248); the two model calls are excluded from the latency budget. `_finish` (line 124) emits exactly one `RequestLogLine` per return path (optionally recording it on a `MetricsCounter`) and returns the outcome unchanged. `_resolver_query` (line 263) builds the `ResolverQuery` the stapler re-derives from, special-casing T4 to carry the condition string. `all_events` is a parameter, so the entrypoint opens no store ingress of its own.801 802This module is intentionally thin: it makes no decisions of its own beyond ordering and branching on typed values. It does not validate (the wall does), it does not compute facts (the executor does), it does not write `asserted_value` (the stapler does), and it does not call the model (the two job modules do). What it owns is the *sequence* and the *latency accounting* — which is why it is "pure composition of the locked modules" rather than a 21st module with its own behavior. If you are looking for where a given decision is made, it is almost never here; this file just routes typed values between the modules that make them.803 804---805 806## Part 4 — Function & method index807 808Searchable reference for the load-bearing functions. Line numbers are against the current source; the index is grouped loosely by layer (entrypoints → agent surface → wall/registry → executor → attacher/egress → resolver → stapler → provider → harness → observability → contracts).809 810| function / method | file | line | one-line description |811|---|---|---|---|812| `answer_question` | `agent/request_pipeline.py` | 89 | the request entrypoint; pure composition of the locked modules |813| `_finish` (closure) | `agent/request_pipeline.py` | 124 | emit one RequestLogLine (side-effect), return the outcome unchanged |814| `_resolver_query` | `agent/request_pipeline.py` | 263 | build the ResolverQuery the stapler re-derives from (T4 carries condition) |815| `intake` | `question_intake/question_intake.py` | 42 | wrap the untrusted NL string; no data/model touch |816| `select_tool` | `tool_selector/tool_selector.py` | 53 | LLM job-1: propose a tool / OutOfCoverage / ModelUnavailable |817| `compose_nl` | `answer_composer/answer_composer.py` | 48 | LLM job-2: the display sentence only (tainted) |818| `validate` | `validator/param_validator.py` | 102 | the wall; sole ValidatedToolCall constructor; ordered disjoint checks |819| `_check_param` | `validator/param_validator.py` | 161 | raw-query check (first) then type/enum/format check for one param |820| `_check_window_order` | `validator/param_validator.py` | 231 | T6 start ≤ end (a well-shaped wrong-value range check) |821| `get_tool_spec` | `registry/tool_registry.py` | 171 | name → ToolSpec, or typed UnregisteredTool (never raises) |822| `registered_tool_names` | `registry/tool_registry.py` | 184 | the closed set of registered tool names |823| `execute` | `executor/tool_executor.py` | 95 | dispatch a ValidatedToolCall to its tool body (total over six) |824| `_latest_le_as_of` | `executor/tool_executor.py` | 124 | exact-key bucket lookup + matches() → latest ≤ as_of |825| `_t4_count_stores_with_condition` | `executor/tool_executor.py` | 275 | T4: distinct stores whose latest-≤-as_of event meets the condition |826| `_condition_satisfied` | `executor/tool_executor.py` | 251 | the T4 store-condition predicate on a single (latest) event |827| `attach` | `attacher/provenance_attacher.py` | 55 | data-layer provenance: set, 1-pointer, or AbsenceAttestation |828| `_provenance_set` | `attacher/provenance_attacher.py` | 88 | build a ProvenanceSet, fail-closed on a provenance-integrity violation |829| `build_result_envelope` | `result_envelope/result_envelope.py` | 38 | egress: validate ToolResult vs the declared return schema |830| `matches` | `contracts/matching.py` | 33 | THE single predicate; window boundary rule lives here |831| `build_index` | `store/event_index.py` | 90 | precompute the four exact-match indexes eagerly at load |832| `resolve` | `resolver/provenance_resolver.py` | 100 | independently re-derive truth + render the three-clause verdict |833| `_re_derive` | `resolver/provenance_resolver.py` | 139 | dispatch the per-tool independent re-derivation over all_events |834| `_latest_satisfying` | `resolver/provenance_resolver.py` | 155 | latest matches()-satisfying event over all_events (no index) |835| `_verdict_positive` | `resolver/provenance_resolver.py` | 307 | validity + value-equality + completeness for a positive answer |836| `_verdict_absence` | `resolver/provenance_resolver.py` | 362 | completeness-of-the-empty-set for a grounded zero |837| `staple` | `envelope_stapler/envelope_stapler.py` | 64 | the single writer of asserted_value/provenance: copy + resolver-backed equality |838| `resolve_provider` | `provider_iface/provider_iface.py` | 39 | config-only factory: local (demo) vs hosted (stub) |839| `LocalModelAdapter.select_tool` | `local_model_adapter/local_model_adapter.py` | 113 | build the OpenAI tools=[...] request; normalize the tool call |840| `urllib_transport` | `local_model_adapter/local_model_adapter.py` | 324 | stdlib POST; any failure → typed ModelUnavailable (typed summary) |841| `_parse_tool_call` | `local_model_adapter/local_model_adapter.py` | 228 | normalize a chat response into a ToolCall or OutOfCoverage |842| `score_correctness` | `correctness_scorer/correctness_scorer.py` | 89 | model-free structured equality (GATE-a / GATE-e action) |843| `run_acceptance` | `report/report.py` | 160 | eval entrypoint: drive gold set, dual-score, roll up |844| `_eval_item` | `report/report.py` | 207 | one item: drive + correctness axis + provenance axis |845| `_resolver_query_for` | `report/report.py` | 429 | re-derive the ResolverQuery from a gold question template |846| `_assemble` | `report/report.py` | 338 | roll per-item verdicts into the gates + the 5 metrics |847| `write_report` | `report/report.py` | 562 | the ONE stateful write of the eval module |848| `emit` | `observability/log_line.py` | 105 | append-only JSON log line (pure side-effect) |849| `metrics_from_lines` | `observability/log_line.py` | 144 | derive the 5 metrics; §10 falsifier raises UncomputableMetric |850| `healthz` | `observability/app_surfaces.py` | 60 | /healthz: 200 once loaded / 503 before; surfaces dataset_version |851| `metrics_endpoint` | `observability/app_surfaces.py` | 111 | /metrics gated by METRICS_ENABLED; same aggregation as offline |852| `is_stub` | `contracts/stub.py` | 62 | true iff an object carries the stub marker (the guard's predicate) |853| `intake` | `question_intake/question_intake.py` | 42 | wrap the untrusted NL string into IntakeRequest; no data/model |854| `is_valid_timestamp` | `contracts/ids.py` | 52 | regex gate for ISO-8601 UTC second-precision (keeps lexical=chrono) |855| `is_valid_store_id` | `contracts/ids.py` | 37 | regex gate for the st-NNN store-id format |856| `keyset` | `contracts/provenance.py` | 59 | project a ProvenanceSet to its event_id keyset (the equality key) |857| `provenance_key` | `contracts/provenance.py` | 53 | project one EventPointer to its event_id identity |858| `EventPointer.__eq__` | `contracts/provenance.py` | 39 | identity by event_id alone (store_id is denormalized) |859| `load_store` | `store/store_loader.py` | 198 | load+validate store.json into the frozen rows (fail closed) |860| `_assert_invariants` | `store/store_loader.py` | 164 | version lockstep + event_id uniqueness + referential integrity |861| `store_status` | `store/store_loader.py` | 236 | the store-side /healthz body (status, dataset_version, model_name) |862| `load_gold` | `store/gold_loader.py` | 246 | load+validate the ≥48-item frozen gold set |863| `_first_extra_key` | `validator/param_validator.py` | 146 | reject a call carrying a param not in the tool's spec |864| `_check_scalar` | `validator/param_validator.py` | 210 | typed-scalar format check for one param |865| `_check_enum` | `validator/param_validator.py` | 191 | closed-enum membership check for one param |866| `_t1_get_stock_state` | `executor/tool_executor.py` | 175 | T1 scalar: latest-≤-as_of stock state for (store, product) |867| `_t5_list_products_in_state` | `executor/tool_executor.py` | 327 | T5 set: products whose latest event at a store is in the state |868| `_t6_count_events_in_window` | `executor/tool_executor.py` | 367 | T6 set: count EVERY event in [start, end) for (store, state) |869| `_pointer` | `attacher/provenance_attacher.py` | 110 | build an EventPointer(event_id, store_id) for the set |870| `_re_derive_condition` | `resolver/provenance_resolver.py` | 208 | independently re-derive the T4 store-condition rollup |871| `_verdict_positive` (clauses) | `resolver/provenance_resolver.py` | 307 | validity + value + completeness; empty positive set fails (line 333) |872| `compose_nl` | `answer_composer/answer_composer.py` | 48 | LLM job-2: one display sentence; catches ModelUnavailableError |873| `LocalModelAdapter.compose_nl` | `local_model_adapter/local_model_adapter.py` | 143 | build the tool-free compose request; return nl_text only |874| `_tool_schema` | `local_model_adapter/local_model_adapter.py` | 192 | render a ToolSpec into the OpenAI tools=[...] entry |875| `_param_schema` | `local_model_adapter/local_model_adapter.py` | 219 | render one ParamSpec (enum→enum schema, scalar→string) |876| `_eval_item` | `report/report.py` | 207 | one gold item: drive + correctness axis + provenance axis |877| `_shape_matches_kind` | `report/report.py` | 285 | did the model pick a tool whose shape matches the gold kind |878| `_provenance_pointers_resolve` | `report/report.py` | 296 | tool-agnostic containment: every pointer is a real event_id |879| `_metrics` | `report/report.py` | 395 | derive the 5 metrics over the per-item rows |880| `to_json` | `observability/log_line.py` | 100 | serialize a RequestLogLine to one JSON string |881| `_require_field` | `observability/log_line.py` | 126 | raise UncomputableMetric on a missing source field |882| `MetricsCounter.record` | `observability/app_surfaces.py` | 96 | append a log row under a lock (concurrency-safe) |883| `do_GET` | `app/server.py` | 104 | route /healthz + /metrics; 404 everything else (no answer route) |884| `make_server` | `app/server.py` | 118 | construct the threaded server, loading store+index once |885 886## Part 5 — State / data-object index887 888These are immutable value objects (frozen dataclasses / pydantic models). There is no long-lived mutable global state on the request path — the only stateful pieces are the in-process `MetricsCounter._rows` (append-only, lock-guarded) and the one eval report write. The "lifetime" column tells you when an object is created and when it is gone; the "mutation rules" column tells you whether it can ever change after creation (almost always: it cannot — it is frozen). Read the table as the authoritative answer to "who is allowed to construct this?" — for the load-bearing answer objects there is exactly one constructor each, which is what the single-writer and sole-constructor invariants come down to in practice.889 890| object | owner / created by | lifetime | mutation rules |891|---|---|---|---|892| `IntakeRequest` | `question_intake.intake` | one request | frozen; `question` stays untrusted |893| `ToolCall` | the model (via adapter) | one request | frozen; **untrusted** until validated |894| `ValidatedToolCall` | `param_validator.validate` (sole site) | one request | frozen; the only shape the executor accepts |895| `ExecutedResult` | `tool_executor.execute` | one request | frozen; carries the exact contributing events |896| `ToolResult[V]` | `provenance_attacher.attach` | one request | frozen; `asserted_value`+`provenance` set once |897| `EventPointer` | `provenance_attacher._pointer` | as long as the answer | frozen; identity = `event_id` only |898| `ProvenanceSet` | `provenance_attacher._provenance_set` | with the ToolResult/answer | frozenset; never empty for a positive fact |899| `AbsenceAttestation` | `provenance_attacher.attach` (zero path) | with the answer | frozen; `independently_enumerated_count` must be 0 |900| `AnswerEnvelope[V]` | `envelope_stapler.staple` (sole site) | the answer | frozen; value/provenance COPIED from ToolResult; only `nl_text` is model-written |901| `AbstentionEnvelope` | `request_pipeline` (OOC branch) | the answer | frozen; no value/provenance |902| `ResolverVerdict` | `provenance_resolver.resolve` | one check | frozen; three booleans + reasons |903| `EventIndex` | `event_index.build_index` | process (built once at load) | frozen dataclass; immutable-store boundary; never mutated |904| `RequestLogLine` | `request_pipeline._finish` | emitted then discarded | pydantic model; a pure side-effect; never alters the outcome |905| `MetricsCounter._rows` | `app_surfaces.MetricsCounter` | process (live demo) | the one mutable state; append-only under a lock |906| `AcceptanceReport` | `report.run_acceptance` | the eval run | frozen; `write_report` serialises it (the one stateful write) |907| `ScanEvent` | `store_loader._parse_event` | process (loaded once) | frozen; the immutable provenance anchor; `event_id` globally unique |908| `LoadedStore` | `store_loader.load_store` | process (warm replica) | frozen; rows are tuples so readers cannot mutate shared state |909| `GoldItem` | `gold_loader._parse_item` | the eval run | frozen; the change-controlled oracle; never edited to pass a gate |910| `TypedFilter` / `Window` | executor / resolver per tool | one request/check | frozen; the closed selection surface (no free-form member) |911| `ResolverQuery` | `request_pipeline._resolver_query` / `report._resolver_query_for` | one check | frozen; re-derives truth with `all_events` (T4 carries condition) |912| `Settings` | `Settings()` (pydantic) | process | loaded from env; `extra="forbid"`; the topology validator runs at load |913| `det_ms` | `request_pipeline.answer_question` (local) | one request | a float accumulator; advanced only around deterministic steps |914| `AppState` | `app/server.make_server` | process (server) | holds the once-loaded store/index + counter; None handles on load fail |915| `HttpResponse` | `app_surfaces.healthz` / `metrics_endpoint` | one request | frozen `(status, body)`; framework-free so it is unit-testable |916 917A note on the request path's statelessness: every object above that is created during a request is frozen and discarded at the end of it. The only state that survives a request is the loaded store/index (built once at boot, never mutated) and, in the live demo, the `MetricsCounter`'s append-only row list (guarded by a lock). There is no per-request global, no cache that the next request could read a stale entry from, and no mutable shared answer state — which is why the deterministic path is a pure function of its inputs and the same question always produces the same fact.918 919---920 921## Appendix — Quick-reference flowchart922 923924 925<details>926<summary>Mermaid source</summary>927 928```mermaid929flowchart TD930 START["answer_question(question, index, all_events, provider)"] --> INTAKE["intake → IntakeRequest"]931 INTAKE --> SELECT["select_tool (LLM job-1)"]932 SELECT -->|OutOfCoverage| ABST["AbstentionEnvelope · outcome=abstained"]933 SELECT -->|ModelUnavailable| ERRA["ModelUnavailable · outcome=error"]934 SELECT -->|ToolCall| VAL["validate (the wall)"]935 VAL -->|Unregistered/Validation/RawQuery| REJ["typed rejection · outcome=rejected"]936 VAL -->|ValidatedToolCall| EXEC["execute (read-only, exact-key)"]937 EXEC --> ATTACH["attach (provenance)"]938 ATTACH --> EGRESS["build_result_envelope (schema gate)"]939 EGRESS -->|ValidationError| REJ940 EGRESS -->|ToolResult| COMPOSE["compose_nl (LLM job-2)"]941 COMPOSE -->|ModelUnavailable| ERRC["ModelUnavailable · outcome=error"]942 COMPOSE -->|nl_text| STAPLE["staple: resolve() + equality + COPY"]943 STAPLE -->|positive set| ANS["AnswerEnvelope · outcome=answered"]944 STAPLE -->|AbsenceAttestation| ABS["AnswerEnvelope · outcome=absence"]945 ANS --> LOG["_finish → emit RequestLogLine"]946 ABS --> LOG947 ABST --> LOG948 ERRA --> LOG949 ERRC --> LOG950 REJ --> LOG951 LOG --> OUT["typed RequestOutcome"]952```953 954</details>955 956Every terminal is a **typed value**, every fact is **copied by code under a resolver-backed equality check**, and the model touched only `select_tool` and `compose_nl`. That is the whole containment story, in one picture.957 958## How to navigate this codebase959 960A few entry points depending on what you came to do:961 962- **"Is the no-fabrication claim real?"** Read 1.5 (the two trust boundaries) and 3.13 (the single writer), then run the three `scripts/` guards and `tests/test_suite_resolver_meta.py`. The property is decided by the type graph, the import graph, and the resolver — none of which need the model running.963- **"How does one question get answered?"** Read UC1 end-to-end, then the Part 3 sections for the modules it touches (3.8 the wall, 3.9 the executor, 3.10 the attacher, 3.11 the resolver, 3.13 the stapler). The happy-path sequence in 1.3 is the index into those.964- **"I need to add a tool."** Read 3.3 (the closed menu), 3.9 (the tool bodies), and 3.11 (the resolver's independent re-derivation), then follow the recipe in WALKTHROUGH §11. The non-negotiables: no new `ParamType` member, no query-construction site, and a *separate* re-derivation branch in the resolver (do not import the executor).965- **"Why is the latency number what it is?"** Read 3.18 (`det_ms`) and UC9 (the log line → metrics). The budget is the deterministic span only; inference is measured out on purpose.966- **"What runs in the deployed image?"** Read 3.17 (`app/server.py`) — a stdlib HTTP shim serving `/healthz` and `/metrics`, no answer route, no model co-bundled.967 968When in doubt, start in `contracts/` (the vocabulary that imports nothing) and read outward toward the entrypoints; the type a value has usually tells you which gate it has already passed.969 970## Containment, one line per layer971 972A final compression of the whole tour, so you can hold the property in your head:973 974- **Contracts** — there is no type in which a free-form query, a model-written fact, or an absence-as-empty-set can be expressed.975- **Store substrate** — the data is loaded once, validated at boot (fail closed), and never mutated; the resolver gets an independent `all_events` enumeration.976- **The wall** — the only path from a model proposal to a callable is a typed, value-checked `ValidatedToolCall`; a bad proposal is rejected before any data read.977- **Executor + attacher** — facts are computed by exact-key index lookups and carry the complete contributing set (or a proven absence); no string is ever spliced into a query.978- **Resolver** — every fact is independently re-derived over `all_events` and checked for validity, value-equality, and completeness, with no model in the loop.979- **Stapler** — `asserted_value`/`provenance` have one writer, which copies from the `ToolResult` under the resolver's equality check and never imports the model.980- **Agent surface** — the model does exactly two jobs (pick a tool, write a sentence); both outputs are tainted and neither can reach a fact.981- **Guards + gates** — `mypy --strict`, three `ast` guards, and the deterministic test gates re-prove all of the above, so a regression is a build failure, not a silent drift.982 983Read top to bottom, that list is the answer to "why can't it fabricate?" — and every claim in it points at a specific module above.984 