--- 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` (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` | 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` | 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` | Semantic footprint (component-meta only, gated by `HostConfig::footprint_capture`) | | `scheduler` | `Option` | Scheduler-side attribution at first dispatch (native only) | | `files` | `Vec` | Per-file attribution deduplicated by canonical id | | `waits` | `Option` | 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, 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` 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` Carrier `AuditedResult` (`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` into `packages/types/audit.generated.ts`; `packages/typeinfo` imports the generated type. The typeinfo native session's `_with_audit` methods return `AuditedResult, 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, Option)`. 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)` 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, Option)`. 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, Option)`. 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)` wraps a closure `FnOnce(&Arc) -> McpToolOutcome` 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` 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>` (`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` — borrow the config snapshot. - `audit_records_store() -> &Arc` — 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` — 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` only — never `Arc`. 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`) |