---
name: audit-infrastructure
description: "Verter audit infrastructure — RequestAuditRecord, RequestKind variants, producer entry-points, AuditRequestRegistration lifecycle, HostAuditRuntime, NAPI/WASM bindings, BatchAuditAggregator"
---

# Audit Infrastructure

Per-request observability for every public host entry-point: component-meta resolution, compile, semantic analysis, type resolution, workspace ops, LSP handlers, MCP tool invocations, and bundler-batch summaries. Each audited request produces one `RequestAuditRecord` envelope carrying timing, memory, store counters, scheduler attribution, per-file reads, optional semantic footprint, and a strongly-typed kind-specific payload.

For end-user API reference and debug workflows see [`docs/audit-footprint/`](../../docs/audit-footprint/).

## Architecture Overview — Substrate Vs Session

### Optional capture policy and availability

The binding REQUIRED / REQUIRED-budget / REQUIRED-lifetime / OPTIONAL policy,
per-owner inventory format, default-off `semantic-observe` feature and generator
constraints live in [`docs/arch/semantic-observe.md`](../../../docs/arch/semantic-observe.md).
Inventory files are `crates/*/observe-inventory/*.md`; extend the owning file,
not a shared table. Required validity, budgets, diagnostic data and current
occupancy/ownership charges remain independent of capture.

`verter_audit::observe::CaptureAvailability::compiled()` reports `Unavailable`
when `semantic-observe` is off and `Available` when on. `observe::capture`
returns `None` without calling its collector when off; it returns the collected
payload when on, never fabricated zero metrics. `ObserveMode` supplies the
uncaptured/captured vocabulary; root selection and existing audit-endpoint
migration remain with the execution/consolidation owner. The feature currently
implies legacy measurement gates without removing them. Integration tests in
`crates/verter_audit/tests/cases/observe_feature_closure.rs` check resolver-2
production and dev-unified closures on host/WASM; run the audit tests both with
and without `--features semantic-observe`.

Audit state is split between a leaf substrate crate (`verter_audit`) and the session crate (`verter_session`). `verter_audit` may depend only on `verter_span` plus ecosystem crates — never on `verter_session` or any other `verter_*` crate.

| Layer | Crate | Owns |
| --- | --- | --- |
| **Substrate** (DTOs + observer trait) | `verter_audit` | `RequestAuditRecord`, `RequestTargetIdentity`, `RequestKind`, `RequestKindPayload`, per-kind payload structs, `AuditedResult<T, E>` (audit-bearing execution carrier), `AuditObserver` trait, `current_observer()` TLS accessor, `NoOpObserver`, `AuditConfig` + `AuditConsumerFilter`, the `StructuredAuditEvent` enum + variant payloads (in `verter_audit::origin_graph`), `AuditEvent` counter hook, `BatchAuditAggregator` + `AuditRecordSource`, `IncidentalFields` masking trait, `WALKER_DEPTH_CAP` |
| **Session** (lifecycle + runtime) | `verter_session` | `HostAuditRuntime`, `AuditRequestRegistration::{Active, Noop}`, `AuditRecordsStore`, `RequestContext` (implements `AuditObserver`), `RequestContextGuard`, peak-RSS sampler thread, the per-request accumulator + footprint miner (audit endpoints `why_loaded` / `why_instantiated` read from the accumulator), `LspAuditSession`, audited entry-points (`compile_with_audit`, `analyze_with_audit`, `resolve_type_with_audit`, `audit_workspace_op`, `audit_mcp_tool_call`, `get_component_meta_with_resolution`) |

Isolation enforced by: `verter_audit_no_upward_deps` guard (rejects any `verter_*` dep in `verter_audit/Cargo.toml` other than `verter_span`) and `audit_substrate_isolation` guard (rejects any `use verter_*` under `crates/verter_audit/src/` other than `verter_span`).

## `RequestAuditRecord` Envelope

Top-level record (`crates/verter_audit/src/record.rs`):

| Field | Type | Description |
| --- | --- | --- |
| `request_id` | `u64` (decimal-string transport) | Monotonic id stamped at the public entry-point. Unique per audited request |
| `canonical_id` | `String` | Legacy compatibility projection: exact registered canonical, otherwise empty |
| `target_identity` | `Option<RequestTargetIdentity>` | Additive tagged identity: `RegisteredCanonical(String)`, `UnregisteredUri(String)`, or `NotApplicable`. New producers always emit `Some`; `None` is reserved for older serialized records |
| `kind` | `RequestKind` | Discriminant naming the producer surface |
| `parent_request_id` | `Option<String>` | Correlation id for nested audited requests (sniffed from scheduler-side TLS slot at construction) |
| `from_cache` | `bool` | `true` when satisfied from warm result cache |
| `timings` | `RequestTimingAudit` | Per-phase wall-clock timings (ms) |
| `memory` | `RequestMemoryAudit` | RSS snapshots (before/after/delta + peak from sampler) |
| `store` | `RequestStoreAudit` | Generic store/view counters |
| `footprint` | `Option<RequestFootprintAudit>` | Semantic footprint (component-meta only, gated by `HostConfig::footprint_capture`) |
| `scheduler` | `Option<SchedulerAudit>` | Scheduler-side attribution at first dispatch (native only) |
| `files` | `Vec<FileAudit>` | Per-file attribution deduplicated by canonical id |
| `waits` | `Option<WaitAudit>` | Lock + queue contention (gated by `audit_timing_capture`) |
| `kind_payload` | `RequestKindPayload` | Strongly-typed payload paired with `kind` |

### `RequestKind` Variants

| Variant | Payload | Producer |
| --- | --- | --- |
| `ComponentMeta` | `ComponentMetaPayload` | `VerterHost::get_component_meta_with_resolution` |
| `TypeResolution` | `TypeResolutionPayload` | `VerterHost::resolve_type_with_audit` |
| `SemanticAnalysis` | `SemanticAnalysisPayload` | `VerterHost::analyze_with_audit` |
| `Compile { target: CompileTargetTag }` | `CompilePayload` | `VerterHost::compile_with_audit` / `compile_with_audit_options` |
| `Workspace { op: WorkspaceOp }` | `WorkspacePayload` | `VerterHost::audit_workspace_op` |
| `Lsp { method: LspMethodTag }` | `LspRequestPayload` | `verter_lsp::audit_harness::run_with_audit` (per LSP handler) |
| `Mcp { tool: String }` | `McpToolPayload` | `VerterHost::audit_mcp_tool_call` |
| `BundlerBatch { kind: BundlerKindTag }` | `BundlerBatchPayload` | `BatchAuditAggregator::summarize` |
| `Custom { name: String }` | `RequestKindPayload::None` | Open-ended escape hatch |
| `TypeInfoGraph` | `TypeInfoGraphPayload` | `VerterHost::resolve_framework_surface_with_audit` (the typeinfo graph wire envelope) |
| `FlowReturnInference` | `FlowReturnInferencePayload` | `VerterHost::get_flow_return_type_with_audit` |

### Typed Payload Accessors

`RequestAuditRecord` typed accessors (each returns `None` when `kind_payload` is not the matching variant):

- `component_meta_payload() -> Option<&ComponentMetaPayload>`
- `type_resolution_payload() -> Option<&TypeResolutionPayload>`
- `compile_payload() -> Option<&CompilePayload>`
- `semantic_analysis_payload() -> Option<&SemanticAnalysisPayload>`
- `workspace_payload() -> Option<&WorkspacePayload>`
- `lsp_payload() -> Option<&LspRequestPayload>`
- `mcp_payload() -> Option<&McpToolPayload>`
- `bundler_batch_payload() -> Option<&BundlerBatchPayload>`
- `typeinfo_graph_payload() -> Option<&TypeInfoGraphPayload>`
- `flow_return_inference_payload() -> Option<&FlowReturnInferencePayload>`

### `FlowReturnInference` (U6 flow-return substrate)

`RequestKind::FlowReturnInference` audits the demand-sliced flow-return
entry `VerterHost::get_flow_return_type_with_audit(function, demand)`
(`crates/verter_session/src/host_flow_return_audit.rs`), which resolves ONE
`SemanticQueryKey::FlowReturn` through the shared dispatch and returns
`AuditedResult<Arc<FlowReturnResult>, FlowReturnError>` — the carrier's
`audit` field is populated on BOTH arms. `FlowReturnInferencePayload`
(`crates/verter_audit/src/payloads/flow_return.rs`) carries
`function_symbol`, three per-request counters mirroring the cold-path
structured events one to one, and the typed partiality reason:

| Counter | Paired structured event | Bumped when |
| --- | --- | --- |
| `cold_computes` | `FlowReturnStarted` | a cold whole-function flow evaluation runs (root + nested inline frames) |
| `budget_exceeded_events` | `FlowSliceBudgetExceeded { axis: FlowSliceBudgetAxisTag }` | a flow-slice budget refusal routes through `ReturnOnly` |
| `cycle_reentry_holds` | `FlowCycleSentinelHit` | a coinductive re-entry hold is recorded on the shared obligation runtime |

The counters report THAT a request did cold work, hit a budget, or held
on a cycle. `partiality: Option<FlowPartialityTag>` reports WHY it came
back incomplete, and is `None` for the complete, warm-admissible outcome
(and on the default-filled filtered / audit-disabled record, where no
payload was collected at all):

| Arm | Carries | Populated from |
| --- | --- | --- |
| `FlowPartialityTag::Degraded(FlowDegradationTag)` | the degraded-but-usable `Ok` outcome's reason | `FlowReturnResult::degradation()` |
| `FlowPartialityTag::NoValue(FlowFailureTag)` | the `Err` outcome's no-value reason | the typed `FlowReturnError` |

`partiality` reports exactly ONE reason, never a set: the producer's
typed outcome already reduced every observed gap to the FIRST in source
order, so a function carrying several distinct gaps still names only the
earliest. Read it as "the reason this request was partial", never as
"the complete inventory of what is missing".

`FlowDegradationTag` and `FlowFailureTag` are CLOSED MIRRORS of the
session's `FlowReturnDegradation` / `FlowGap` and `FlowReturnFailure`
vocabularies, so the leaf audit substrate keeps no back-edge to
`verter_session`. Both flatten their domain's nested closed enums —
`FlowReturnDegradation::FlowGap(_)` reduces through the gap variant
(`GapGuardNarrowing`, `GapNominalRelation`, `GapClosureCapture`,
`GapAbruptCompletion`, `GapUnmodeledExpression`), and
`FlowReturnFailure`'s `Unsupported` / `CallResolution` / `Budget` arms
reduce through their inner reason (`UnsupportedLoop`, `CallUndecidable`,
`BudgetWorkExceeded`, …) — so every distinct reason keeps its own wire
spelling instead of collapsing into a catch-all bucket. `FlowFailureTag`
additionally carries `UnstableState` for the host's own
`FlowReturnError::UnstableState` refusal, so the `Err` arm never reports
an unexplained no-value.

The projection lives at the ONE producer,
`observed_partiality` in `crates/verter_session/src/host_flow_return_audit.rs`,
and maps through exhaustive matches (a new domain variant is a compile
error, never a silently collapsed reason). It is READ-ONLY telemetry: it
runs after the outcome is bound, and no admission decision, warm/cold
classification, or cache identity reads it back.

Cold-vs-warm contract: a warm family hit emits NO `FlowReturnStarted` and
bumps NO counter (`cold_computes == 0` is the counter-side witness), and
allocates no audit payload without an active accumulator. A degraded
success never warms at all, so it reports its partiality on every call.
Guards:
`crates/verter_session/tests/cases/g_type/flow_return_audit_contract.rs`
(cold/warm event + payload contract, the partial-vs-complete partiality
contract, and the filter-driven projected-vs-unprojected equivalence —
denying `KindBit::FlowReturnInference` takes the `Noop` arm and removes
the projection outright, and the served value, degradation verdict and
warm/cold sequence are unchanged) and
`crates/verter_session/tests/cases/g_misc0/flow_return_audit_tls_propagation.rs`
(TLS observer propagation across the dispatch's worker hops); the wire
surface is pinned by `crates/verter_audit/tests/cases/ts_bindings.rs`.

## `AuditedResult<T, E>` Carrier

`AuditedResult<T, E>` (`crates/verter_audit/src/audited_result.rs`) pairs the outcome — success `T` or typed error `E` — with the `RequestAuditRecord` captured while producing it. `#[serde(tag = "kind")]` discriminated enum (`Ok { value, audit }` / `Err { error, audit }`); both arms carry the record so the envelope survives regardless of outcome.

Lives in `verter_audit`, not `verter_protocol`: it is generic over `T`/`E` (protobuf cannot express) and embeds `RequestAuditRecord` — putting it in the protobuf-authoritative `verter_protocol` would invert the dependency or force a hand-written TS mirror. Rides the ts-rs path, exporting as `export type AuditedResult<T, E>` into `packages/types/audit.generated.ts`; `packages/typeinfo` imports the generated type. The typeinfo native session's `_with_audit` methods return `AuditedResult<Arc<...>, TypeInfoRequestError>`.

Surface: `ok(value, audit)` / `err(error, audit)` constructors; `audit()`, `as_result()`, `into_parts()`, `into_result()`, `map()`, `map_err()`. Home + export rule pinned by `audited_result_lives_in_audit_and_exports_through_generated_ts` (`crates/verter_session/tests/cases/g_block/typeinfo_audit_contract_guards.rs`).

## Producer Entry-Points

Every public audited entry-point follows the same lifecycle: stamp a request id, build a `RequestContext` keyed by the matching `RequestKind`, construct an `AuditRequestRegistration` BEFORE installing the TLS guard, run the producer body under either `RequestContextGuard` (active) or `install_noop_observer()` (filtered), assemble the typed payload from per-request counters, and finalise through the registration. Filtered kinds short-circuit to `None`; the producer body always runs regardless of audit state.

### Component-Meta

`VerterHost::get_component_meta_with_resolution(canonical_id, mode)` returns `(Option<ComponentMetaAnalysis>, Option<ResolvedComponentMetaState>)`. The audit record is published into the host's bounded `AuditRecordsStore` and drained via `HostAuditRuntime::take_record(request_id)`.

`AuditedRequest` builder (`crates/verter_session/src/audited_request.rs`) wraps one call in a request-scoped audit harness, resets per-thread counters, validates exactly one request was created, and returns `(ComponentMetaAnalysis, ResolvedComponentMetaState, RequestAuditRecord)` as a triple. `AuditedRequestBuilder::resolve_component_meta` is the test-facing convenience; `AuditedRequestBuilder::run_custom` lets a closure issue arbitrary single-request audited work.

### Compile

`VerterHost::compile_with_audit(canonical_id, target) -> (VerterCompileResult, Option<RequestAuditRecord>)` and `compile_with_audit_options(canonical_id, target, verter_options)` for explicit `force_vapor` / `force_js` control. The `target` bitset maps to `CompileTargetTag` (`Vdom`, `Ide`, `Vapor`) on `kind`. Producer-side instrumentation in `verter_compiler` emits `record_phase_timing` at parse/transform/codegen/css_analysis/sourcemap boundaries and `record_event(CompileCodeTransformOp)` at every `CodeTransform` operation — the session-side `RequestContext` accumulates these into per-request atomics that `assemble_compile_payload` reads at finalize time.

### Semantic Analysis

`VerterHost::analyze_with_audit(canonical_id) -> (Option<AnalysisReady>, Option<RequestAuditRecord>)`. Probes `FileArtifactStore` cache before constructing the registration so `from_cache` is unaffected by audit work. Audit-disabled fast path runs `materialize_analysis_ready` with no `RequestContextGuard`.

### Type Resolution

`VerterHost::resolve_type_with_audit(query: SemanticQueryKey, canonical_hint: &str) -> (Option<TypeResolutionResult>, Option<RequestAuditRecord>)`. Drives one `ProjectSemanticDispatch::execute(query)` inside the audit window. `TypeResolutionPayload` reports the caller's projection mode (derived from the query variant) plus per-mode counters mined off the active `RequestContext`.

### Workspace

`VerterHost::audit_workspace_op(op: WorkspaceOp) -> RequestAuditRecord`. Drives `WorkspaceAccess::audit_op(op)` under audit. Constructs the `AuditRequestRegistration` first so the registry slot precedes the workspace traversal. Returns the record unconditionally; `Noop` arm only suppresses the records-store side effect.

### LSP

`verter_lsp::audit_harness::run_with_audit(host, method, target_identity, position, body, populate)` wraps each LSP handler future in:

1. An `LspAuditSession` keyed by `LspMethodTag` and `RequestTargetIdentity` (constructed via `VerterHost::lsp_audit_begin`). Registered URIs use the registry's exact stored identity; request-before-registration uses the raw URI; `NotApplicable` is reserved for operations with no single document target.
2. The same explicit `request_deadlines` policy used when audit is disabled. Production defaults every request deadline to zero (unbounded); audit never adds a feature/provider timeout.
3. `finalize_ok(payload)` on success or RPC error. `audit_supersede` is an observational latency SLO only: exceeding it emits telemetry but does not cancel or alter the response. Explicit client cancellation may still finalize a session with `finalize_cancelled()` through the cancellation lifecycle.
4. Optional drain to `VERTER_LSP_AUDIT_TRACE_OUT` (JSON-lines append, configurable via env var).

Audit-disabled fast path runs the body under the identical explicit request-deadline policy without registration cost. The audit-on and audit-off paths are therefore semantically equivalent.

Position-bound LSP payloads carry the same additive tagged identity in `PositionInfo::target_identity`. `PositionInfo::canonical_id` remains the legacy projection; an unregistered URI never becomes `NotApplicable`.

### MCP

`VerterHost::audit_mcp_tool_call(tool_name, canonical_id, args_size_bytes, f) -> (T, Option<RequestAuditRecord>)` wraps a closure `FnOnce(&Arc<Self>) -> McpToolOutcome<T>` under audit. `McpToolOutcome { value, result_size_bytes, error }` carries the two facts the wrapper cannot infer (response size and optional error message). A non-empty `canonical_id` is tagged `RegisteredCanonical`; an empty value represents a tool with no single file target and is tagged `NotApplicable` while the retained legacy field stays empty. Sub-requests inherit the MCP request's id as `parent_request_id` via the scheduler-side TLS slot.

## `AuditRequestRegistration` Lifecycle

Every audited entry-point allocates exactly one `AuditRequestRegistration` (`crates/verter_session/src/host_audit_runtime.rs`):

```text
AuditRequestRegistration ::= Active(ActiveRegistration) | Noop
```

- **`Active`** — captures a `Weak<RequestContext>` slot in `HostAuditRuntime::active_requests`. `finalize(record)` atomically removes the slot and publishes the record into `AuditRecordsStore` (idempotent — first call wins). `Drop` defensively sweeps the slot when `finalize` did not run (panic/cancellation paths).
- **`Noop`** — returned when `AuditConfig::consumer_filter` rejects the request's `RequestKind`. Holds no state; `finalize` returns `false` and emits no record.

The three lifecycle methods on `HostAuditRuntime` (`register_active_request`, `finalize_active_request`, `drop_active_request`) are crate-private and have exactly ONE in-tree call site each, all in `host_audit_runtime.rs`. The `audit_request_registration_lifecycle` architecture guard mechanically enforces this.

## Substrate TLS — `current_observer()`

Lower crates emit audit signals through `verter_audit::current_observer() -> Option<Arc<dyn AuditObserver>>` (`crates/verter_audit/src/observer.rs`). Reads a thread-local slot installed by either `RequestContextGuard::install` (active) or `install_noop_observer()` (filtered).

`AuditObserver` trait carries default no-op implementations; producers override only what they care about:

- `record_event(event: AuditEvent)` — counter-style attribution (`InflightAbortedRetry`, `ColdAbortSwept`, `CompileCodeTransformOp`).
- `record_cache_event(layer: &'static str, hit: bool)` — per-layer hit/miss.
- `record_file(canonical_id, layer: VfsLayer, bytes_read, cache_hit)` — workspace file read.
- `record_lock_acquisition(lock_name: &'static str, wait_ns: u64)` — single lock acquisition wait.
- `record_phase_timing(phase: &'static str, elapsed_ms: f64)` — phase-boundary timing.
- `record_scheduler_dispatch(audit: SchedulerAudit)` — first-dispatch attribution (subsequent calls bump dispatch counter).

Session-side `RequestContext` provides full implementations; `NoOpObserver` leaves them defaulted. The `audit_observer_single_accessor` architecture guard enforces that the five lower crates (`verter_compiler`, `verter_semantic`, `verter_workspace`, `verter_lsp`, `verter_mcp_server`) reach audit state ONLY through `verter_audit::current_observer()` — the session-internal `current_request_context()` typed accessor is forbidden in those crates.

## Consumer Filter (Install-Time)

`AuditConfig::consumer_filter` (`crates/verter_audit/src/config.rs`) is a `u32` bitset deciding which `RequestKind` variants emit records. Bits are positionally stable via `KindBit` enum (`ComponentMeta = 0`, `TypeResolution = 1`, `SemanticAnalysis = 2`, `Compile = 3`, `Workspace = 4`, `Lsp = 5`, `Mcp = 6`, `BundlerBatch = 7`, `Custom = 8`, `TypeInfoGraph = 9`, `FlowReturnInference = 10`).

| Constructor | Behaviour |
| --- | --- |
| `AuditConsumerFilter::default()` / `allow_all()` | Allow every kind |
| `deny_all()` | Reject every kind |
| `allow_only([KindBit::…, …])` | Allow only the listed kinds |
| `.allow(KindBit::…)` / `.deny(KindBit::…)` | Toggle a single bit (chainable) |

Filter is read ONCE at registration time inside `AuditRequestRegistration::new` and CANNOT change for that request's lifetime. The current `AuditConfig` snapshot is mirrored from `HostConfig` flags in `host_construction.rs` (today only `audit_timing_capture` is wired; consumer filter defaults to allow-all). Tests that need a non-default filter swap the runtime's `AuditConfig` via a test-only helper without bypassing `active_requests` privacy.

## `HostAuditRuntime` & Sampler Thread

`HostAuditRuntime` (`crates/verter_session/src/host_audit_runtime.rs`) mints and solely owns the host's `AuditRecordsStore`, and holds the `AuditConfig` snapshot and the active-request registry. Each `VerterHost` owns one independent runtime; multiple hosts in one process do NOT share audit state. The host keeps no second store handle and has no audit-record methods of its own: records publish through an `AuditRequestRegistration` or, for a read outside an audited entry-point, the crate-private `publish_record`; consumers drain with `host_audit_runtime().take_record(id)`. Every record is keyed by an id from the host's one request-id counter (`VerterHost::next_request_id`) — there is no process-wide id counter, so an unregistered record can never land on, and replace, an audited record's id (`audit_request_ids_share_one_host_key_space`).

### Public surface

- `audit_config() -> Arc<AuditConfig>` — borrow the config snapshot.
- `audit_records_store() -> &Arc<AuditRecordsStore>` — borrow the records store.
- `snapshot() -> AuditRuntimeSnapshot` — read-only view of `(active_request_count, active_request_ids, records_store_size, records_store_capacity)`.
- `take_record(request_id) -> Option<RequestAuditRecord>` — drain a specific record.

`audit_records_store` is bounded — `AUDIT_RECORDS_STORE_CAPACITY = 256`. Insertion at capacity evicts the oldest entry by insertion order.

### Peak-RSS sampler thread (native only)

- Spawns lazily on the first `AuditRequestRegistration::new` call when `AuditConfig::audit_timing_capture` is on (single-shot start latch via `compare_exchange`).
- Holds `Arc<SamplerState>` only — never `Arc<HostAuditRuntime>`. Runtime drop cannot land on the sampler thread.
- Ticks every 50 ms; writes `fetch_max(current_process_rss())` into each in-flight request's `process_rss_peak_bytes` slot.
- Owner drop sends stop, unparks, and joins the sampler on the owner thread. Per-host observer state, not process-static join counters, discriminates spawn vs join.

WASM targets gated off via `#[cfg(not(target_arch = "wasm32"))]` — no sampler thread, `process_rss_peak_bytes` stays at `0` regardless of `audit_timing_capture`.

## Architecture Guards

All live in `crates/verter_session/tests/cases/architecture_guards.rs` unless noted:

| Guard | Role |
| --- | --- |
| `verter_audit_no_upward_deps` | `verter_audit/Cargo.toml` may declare only `verter_span` from the `verter_*` namespace |
| `audit_substrate_isolation` | Source files under `crates/verter_audit/src/` may `use` only `verter_span`, `std`, and external crates |
| `audit_request_registration_lifecycle` | The three lifecycle methods (`register_active_request`, `finalize_active_request`, `drop_active_request`) on `HostAuditRuntime` have exactly ONE in-tree caller each, all inside `host_audit_runtime.rs` |
| `audit_observer_single_accessor` | The five lower crates (`verter_compiler`, `verter_semantic`, `verter_workspace`, `verter_lsp`, `verter_mcp_server`) reach the substrate ONLY via `verter_audit::current_observer()` — `current_request_context` is forbidden |
| `audit_no_hot_loop_instrumentation` | Phase-boundary instrumentation only; the canonical `(crate, function_path)` denylist forbids `current_observer()` calls inside hot-loop bodies |
| `audit_counter_single_helper` | The two `record_inflight_aborted_retry` / `record_cold_abort_swept` increments live in helper bodies only — no inline `fetch_add` callers anywhere else |
| `wave_3_entry_points_propagate_tls` | Each audited `*_with_audit` entry-point has at least one paired test that drives it AND calls `assert_observer_reaches(...)` so TLS propagation is mechanically verified |
| `every_consumer_has_production_call_site` | Every `RequestKind` variant has at least one production producer under `crates/*/src/` that constructs the variant in expression context (not match-arm pattern). `Custom` and `BundlerBatch` are documented exemptions in `KIND_EXEMPTIONS` |
| `audit_ts_bindings_are_in_sync` (in `tests/cases/g_misc1/ts_bindings.rs`) | `packages/types/audit.generated.ts` matches what `ts-rs` would regenerate from current Rust DTOs |

The general `external_corpus_paths_not_present_outside_gated_tests` guard applies across the workspace, including audit code, as does the review-enforced no-roadmap-archaeology rule.

### TLS Propagation Coverage

`wave_3_entry_points_propagate_tls` pins one TLS-propagation driver per Wave-3 audited entry-point. Each driver invokes the production entry-point through `assert_observer_reaches(...)` and asserts the substrate observer is reachable inside the audited window AND that the calling thread's harness-installed guard remains visible after the nested entry-point guard drops:

| Entry-point | Paired TLS driver |
| --- | --- |
| `resolve_type_with_audit` | `crates/verter_session/tests/cases/g_type/type_resolution_audit_tls_propagation.rs` |
| `compile_with_audit` | `crates/verter_session/tests/cases/g_misc0/tls_harness_cross_crate.rs` |
| `analyze_with_audit` | `crates/verter_session/tests/cases/g_misc0/semantic_analysis_audit_tls_propagation.rs` |
| `audit_op` (`WorkspaceAccess` trait method, driven via the host wrapper `audit_workspace_op`) | `crates/verter_session/tests/cases/g_misc0/workspace_audit_tls_propagation.rs` |
| `verter_lsp::audit_harness::run_with_audit` | `crates/verter_lsp/tests/cases/lsp_audit_tls_propagation.rs` |
| `audit_mcp_tool_call` | `crates/verter_session/tests/cases/g_misc0/mcp_audit_tls_propagation.rs` |

The guard's `MISSING_TLS_TEST` allow-list is empty: every Wave-3 entry-point is paired. Adding a new audited entry-point requires landing a paired TLS driver in the same change and pinning the pair into `WAVE_3_ENTRY_POINTS`; the stale-allow-list check rejects an unpaired entry that has a TLS driver already.

## NAPI / WASM Bindings

| JS export | Rust binding | Returns |
| --- | --- | --- |
| `getComponentMetaWithAudit` | `MetaSession::get_component_meta_with_audit` | `Buffer` (JSON `{ payload, audit }`) |
| `compileWithAudit` | `VerterHost::compile_with_audit` | `Buffer` (JSON record) |
| `analyzeWithAudit` | `VerterHost::analyze_with_audit` | `Buffer` (JSON record) |
| `resolveTypeWithAudit` | `VerterHost::resolve_type_with_audit` | `Buffer` (JSON record) |
| `auditWorkspaceOp` | `VerterHost::audit_workspace_op` | `Buffer` (JSON record) |
| `getLastAuditRecord` | drains the most recent record from `AuditRecordsStore` | `Buffer` (JSON record or empty) |
| `getAuditRecords({ kind?, sinceRequestId?, limit? })` | non-destructive filtered query | `Buffer` (JSON array) |
| `getBundlerBatchSummary({ kind?, sinceRequestId? })` | invokes `BatchAuditAggregator` over the store | `Buffer` (JSON `BundlerBatchPayload`) |

NAPI bindings: `crates/verter_napi/src/audit.rs` (helper types + decoders) and inline `#[napi] impl NapiVerterHost` in `crates/verter_napi/src/lib.rs`. WASM bindings: `crates/verter_wasm/src/audit.rs` + `crates/verter_wasm/src/lib.rs`. All exports return `Buffer` (JSON UTF-8 payload) for parity with the original `getComponentMetaWithAudit` contract; consumers decode against `@verter/types/audit.generated.ts`.

## `BatchAuditAggregator`

`BatchAuditAggregator` (`crates/verter_audit/src/batch.rs`) folds an `AuditRecordSource` into a `BundlerBatchPayload`. The substrate stays leaf — the aggregator depends only on the trait callback contract:

```text
trait AuditRecordSource {
    fn for_each_record(&self, f: &mut dyn FnMut(Instant, &RequestAuditRecord));
}
```

`AuditRecordsStore` implements `AuditRecordSource`; non-destructive iteration exposes each record with its insertion `Instant`. `BatchAuditAggregator::summarize(since)` partitions by `RequestKind`, accumulates total duration, total bytes parsed, `from_cache_count`, and `cache_hit_rate`, and tracks the top-`SLOWEST_RECORD_LIMIT` (= 5) slowest records as `SlowRecordSummary` entries. Each slow summary carries the additive `target_identity` alongside the retained legacy `canonical_id`; CLI rendering reads the tag and falls back to the legacy field only when the source record predates the tag. Empty sources yield a zeroed payload with no division-by-zero on `cache_hit_rate`.

`since` filters records inserted strictly after the supplied `Instant`. Bundler integrations call `summarize(Some(last_summary_instant))` on every flush so each batch reports only work since the last call.

## Tests & TLS-Propagation Harness

`verter_session::tests::audit_tls_harness::assert_observer_reaches(install_audit, f)` is the primary verification primitive for TLS propagation. Runs the closure under either a `RequestContextGuard` (`install_audit = true`) or no guard (`install_audit = false`, the control case), records whether `verter_audit::current_observer().is_some()` was visible on the calling thread, and exposes a `WorkerSinkHandle` so workers spawned inside the closure can report their own observation via `report_worker_observer_presence`.

Worker threads spawned bare via `std::thread::spawn` get a fresh TLS slot by construction. Closures needing observer propagation into a worker pool must either install the guard again on the worker or rely on a runtime that plumbs `RequestContextGuard` through to its workers (the production scheduler does this for its rayon pool).

The `wave_3_entry_points_propagate_tls` guard pins the `(entry_point_symbol, paired_test_files)` invariant — every `*_with_audit` entry-point has at least one test that both invokes the symbol AND calls `assert_observer_reaches(...)`. Tests living in `crates/verter_session/tests/cases/g_misc0/tls_harness_in_crate.rs`, `tls_harness_cross_crate.rs`, and `semantic_analysis_audit_tls_propagation.rs` exercise the harness across in-crate, cross-crate, and analysis-specific propagation.

## Key Files

| File | Role |
| --- | --- |
| `crates/verter_audit/src/lib.rs` | Substrate root + re-exports |
| `crates/verter_audit/src/record.rs` | `RequestAuditRecord`, `RequestTargetIdentity`, `RequestKind`, `RequestKindPayload`, `IncidentalFields` |
| `crates/verter_audit/src/observer.rs` | `AuditObserver` trait, `current_observer()`, `install_observer` guard |
| `crates/verter_audit/src/noop.rs` | `NoOpObserver`, `install_noop_observer()` |
| `crates/verter_audit/src/config.rs` | `AuditConfig`, `AuditConsumerFilter`, `KindBit` |
| `crates/verter_audit/src/payloads/` | Per-`RequestKind` payload data structs |
| `crates/verter_audit/src/batch.rs` | `BatchAuditAggregator`, `AuditRecordSource`, `SLOWEST_RECORD_LIMIT` |
| `crates/verter_session/src/host_audit_runtime.rs` | `HostAuditRuntime`, `AuditRequestRegistration`, sampler thread |
| `crates/verter_session/src/component_meta_audit/audit_records_store.rs` | `AuditRecordsStore`, capacity = 256 |
| `crates/verter_session/src/audited_request.rs` | `AuditedRequest` builder + run-custom harness |
| `crates/verter_session/src/host_compile_audit.rs` | `VerterHost::compile_with_audit` |
| `crates/verter_session/src/host_analyze_audit.rs` | `VerterHost::analyze_with_audit` |
| `crates/verter_session/src/host_resolve_type_audit.rs` | `VerterHost::resolve_type_with_audit` |
| `crates/verter_session/src/host_workspace_audit.rs` | `VerterHost::audit_workspace_op` |
| `crates/verter_session/src/host_mcp_audit.rs` | `VerterHost::audit_mcp_tool_call`, `McpToolOutcome` |
| `crates/verter_session/src/host_lsp_audit.rs` | `LspAuditSession`, `lsp_audit_begin` |
| `crates/verter_lsp/src/audit_harness.rs` | `run_with_audit`, `payload_with_position`, `drain_to_trace_out` |
| `crates/verter_session/src/tests/audit_tls_harness.rs` | `assert_observer_reaches`, `WorkerSinkHandle`, `report_worker_observer_presence` |
| `crates/verter_napi/src/audit.rs` + `crates/verter_napi/src/lib.rs` | NAPI typed entry-points |
| `crates/verter_wasm/src/audit.rs` + `crates/verter_wasm/src/lib.rs` | WASM typed entry-points |
| `packages/types/audit.generated.ts` | TS bindings (regenerated via `ts-rs`) |
