Skip to content

Bump proptest-state-machine from 0.3.1 to 0.8.0 - #9

Closed
dependabot[bot] wants to merge 2 commits into
mainfrom
dependabot/cargo/proptest-state-machine-0.8.0
Closed

Bump proptest-state-machine from 0.3.1 to 0.8.0#9
dependabot[bot] wants to merge 2 commits into
mainfrom
dependabot/cargo/proptest-state-machine-0.8.0

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Jun 4, 2026

Copy link
Copy Markdown
Contributor

Bumps proptest-state-machine from 0.3.1 to 0.8.0.

Commits

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

troykelly and others added 2 commits June 4, 2026 11:01
Multiview — an efficient, hardware-accelerated Rust live video multiview/mosaic
generator. It ingests many live sources (RTSP/HLS/SRT/RTMP/NDI/file/synthetic),
composites them into a templated grid on CPU or GPU, and writes HLS/file output;
the encode-once-mux-many design fans one encode to many transports.

Highlights:
- Fixed-cadence output clock — one valid frame per tick, never stalls; inputs are
  sampled, never pacing (the cardinal continuous-output invariant).
- Custom CPU/GPU compositor with a fixed linear-light colour pipeline (NV12
  throughout); per-tile overlays, captions (HLS WebVTT + DVB-sub), analog/wall
  clocks, audio meters, and fault badges.
- 16-crate Rust workspace under strict typing + TDD + adversarial-review
  guardrails; LGPL-clean default build (GPL codecs + NDI opt-in).
- Multi-arch Docker images, docker compose examples, and SemVer release automation.

Status: early stage — the engine ingests, composites, encodes, and writes HLS/file
output today; the web UI, control API, and live RTSP/NDI/RTMP output servers are
built as libraries and on the near-term roadmap (see ROADMAP.md). Dual-licensed
MIT OR Apache-2.0.

Developed iteratively with AI assistance; the full internal development history is
retained privately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bumps [proptest-state-machine](https://github.com/proptest-rs/proptest) from 0.3.1 to 0.8.0.
- [Release notes](https://github.com/proptest-rs/proptest/releases)
- [Changelog](https://github.com/proptest-rs/proptest/blob/main/CHANGELOG.md)
- [Commits](proptest-rs/proptest@proptest-state-machine-0.3.1...proptest-state-machine-0.8.0)

---
updated-dependencies:
- dependency-name: proptest-state-machine
  dependency-version: 0.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file rust Pull requests that update rust code labels Jun 4, 2026
@troykelly troykelly closed this Jun 4, 2026
@dependabot @github

dependabot Bot commented on behalf of github Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting @dependabot ignore this major version or @dependabot ignore this minor version. You can also ignore all major, minor, or patch releases for a dependency by adding an ignore condition with the desired update_types to your config file.

If you change your mind, just re-open this PR and I'll resolve any conflicts on it.

@dependabot
dependabot Bot deleted the dependabot/cargo/proptest-state-machine-0.8.0 branch June 4, 2026 11:17
troykelly pushed a commit that referenced this pull request Jun 7, 2026
…iple active programs)

Design for running SEVERAL concurrent output programs, from the
multi-program-research ultracode workflow (4 code-grounded lanes +
verified synthesis):

- A unified `Program` abstraction over three kinds: (A) multiview
  composite (today), (B) passthrough = REMUX with no re-encode (zero
  decode/encode — the muxer copies the demuxed packets), (C) transcode
  (decode→[scale]→encode). A/C share the existing ProgramEncoder →
  StreamEgress encode-once-mux-many tail; B reuses the same per-sink mux
  consumer fed by a packet copy instead of an encoder.
- Engine: per-program `OutputClock` + `EngineRuntime` (NO master clock —
  shared read-only TimeSource, independent output clocks), so different
  cadences (25 vs 60fps, inv #3 exact rationals) and per-program
  isolation (inv #1: one program stalling never stalls another) are
  STRUCTURAL. A `Program` actor under the existing Supervisor/RestartPolicy;
  a `ProgramSet` coordinator owns the shared source registry + placement.
- Efficiency (the product): decode-once-use-many via a reference-counted
  shared source registry/framestore (a source feeding N programs/tiles
  decoded ONCE); shared GPU residency; admission control (can the box
  afford a new program — decode MP/s, encode sessions, VRAM) tied to the
  ADR-0017/0018 placement engine; per-program + global resource-adaptive
  degradation (inv #9).
- Config: a backward-compatible `[[programs]]` model (adjacently-tagged
  by kind; today's single top-level program = one implicit program).
- API/UI: programs as a first-class resource (CRUD + per-program
  start/stop 202+op-id, live-apply class inv #11) + per-program realtime
  state + a UI program list/editor.

The synthesis verified its claims against the as-built code (EngineRuntime
is already per-program-shaped + isolation-clean; Actor/Supervisor/
RestartPolicy exist; the remux primitives exist but no packet-copy sink is
wired). Adds docs/research/multi-program.md + ADR-0030 (Proposed) + the
MP-0..MP-8 backlog; indexes both READMEs. Docs only — no code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
troykelly pushed a commit that referenced this pull request Jun 8, 2026
…aphics cap

ROOT CAUSE of the demo burning 5 CPU cores at 4K with the GPU ~80% idle: the
wgpu compositor is Vulkan-based, but the nvidia image had NO Vulkan loader
(`libvulkan1`) and NVIDIA_DRIVER_CAPABILITIES lacked `graphics`. With neither,
wgpu's request_adapter returns nothing and compositing SILENTLY falls back to the
CPU reference path — decode + composite on CPU while the 4060 does only NVENC.

Verified on the box: with `graphics` the toolkit mounts nvidia_icd.json +
libGLX/EGL_nvidia, but `libvulkan1` (the loader that resolves the ICD) is still
absent, so wgpu sees no device. Fix = install libvulkan1 + set the graphics cap;
the chain becomes loader → nvidia ICD → 4060 adapter → GPU compositing.

This is the enablement; a follow-up adds capability DETECTION + a clear UI warning
so "GPU present but unusable for compositing" is surfaced, never silently wasted
(bulletproof + easy-to-use), and feeds the real-time placement loop (inv #9).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
troykelly pushed a commit that referenced this pull request Jun 8, 2026
…the placement loop (ADR-0035)

From a verification-hardened fan-out (21 agents; 11/14 claims held, 3 refined).
Key finding: the placement engine is ~90% BUILT but never WIRED — the cost model
(cost.rs), the DRF placement policy (select.rs select_device), the engine
controller (placement.rs observe), and the degradation loop (degrade.rs) all exist,
pure + tested, but are NEVER called with live loads in the run; the LoadSource feeds
only the UI footer. And the silent GPU→CPU fallback is computed-then-discarded:
EngineRuntime::backend_kind() already knows it fell back, but that fact reaches
NOTHING — not an event, not a warning, not a hardware cross-check.

ADR-0035 defines the sense→detect→warn→plan→apply subsystem on the EXISTING parts:
- DETECT: probe actually-usable backends (wgpu adapter device_type, NVDEC/NVENC
  via libav) + cross-check vs NVML-discovered hardware.
- WARN: an actionable HealthWarning catalog (gpu-present-no-vulkan-adapter w/ the
  libvulkan/graphics remediation, software-decode/encode-on-gpu-host, nvenc-ceiling,
  vram-pressure, cpu-saturation, degradation-active), LATCHED/debounced, with a
  load-bearing NO-FALSE-POSITIVE rule (fires only on hardware-present AND
  software-tier-resolved → zero warnings on a CPU-only/software host).
- PLAN+APPLY: wire LoadSource→select_device/PlacementController into the run, net of
  co-tenant (ours-vs-total) load, affinity-preserving (never fragment a pipeline,
  GPU-placement principle), inv #1/#9/#10 — the runtime re-plan runs OFF the
  output-clock thread (NOT the per-tick hook; folded from the adversarial review).

SA-0..N backlog; SA-0 = the smallest win: detect+warn the compositor mismatch so the
silent fallback that burned 5 CPU cores becomes a clear banner + /api/v1/health.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
troykelly added a commit that referenced this pull request Jun 8, 2026
…degradation rung (PRV-4)

Wire preview focus into the resource-adaptive degradation ladder
(inv #9, ADR-E007/P001/P004): under sustained overload the closed
control loop sheds load cheapest-impact-first BEFORE the program output
is touched, and PREVIEW is the very first rung shed.

multiview-hal::degradation
- Prepend five PREVIEW rungs ABOVE every tile/program rung, in the exact
  preview-subsystem.md §8 order: ShedFocusWhep -> DropPreviewGridFps ->
  DropPreviewGridRes -> DropOffAirCueDecoders -> SuspendPreviewEntirely.
  LADDER (now 13), rung(), and MAX_LEVEL (derived) shift consistently.
- Add affects_preview() + the const boundaries first_non_preview_level()
  and first_program_level(); affects_program() is now expressed against
  first_program_level() so it remains false for every preview rung (the
  program-affecting boundary stays at FasterEncoderPreset). The change is
  purely additive (prepend) — existing rungs keep their relative order.

multiview-preview::focus
- FocusGate gains suspend()/resume()/is_suspended() — the hook the ladder
  drives on the preview rungs. While suspended, try_acquire refuses every
  new focus with FocusDenied::Suspended regardless of cap headroom (the
  existing 503 fallback: ws-jpeg shape, mapped by GatedWhep), so the
  operator sheds to the always-available JPEG transport; restored on
  resume() when load clears. is_suspended() and the poisoned-lock paths
  fail closed. Held leases are untouched (their sessions are torn down
  out-of-band by the driver; Drop still frees the slot).

Re-asserts inv #1 (no output path touched), #9 (cheapest-impact-first,
hysteresis/no-flap unchanged) and #10 (gate is preview-only counters
behind a short-lived Mutex the engine never touches). The Hysteresis ->
FocusGate::suspend binary glue lives in multiview-cli (out of scope here,
concurrently edited); this lands the tested HAL + preview core.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
troykelly pushed a commit that referenced this pull request Jun 8, 2026
…iple active programs)

Design for running SEVERAL concurrent output programs, from the
multi-program-research ultracode workflow (4 code-grounded lanes +
verified synthesis):

- A unified `Program` abstraction over three kinds: (A) multiview
  composite (today), (B) passthrough = REMUX with no re-encode (zero
  decode/encode — the muxer copies the demuxed packets), (C) transcode
  (decode→[scale]→encode). A/C share the existing ProgramEncoder →
  StreamEgress encode-once-mux-many tail; B reuses the same per-sink mux
  consumer fed by a packet copy instead of an encoder.
- Engine: per-program `OutputClock` + `EngineRuntime` (NO master clock —
  shared read-only TimeSource, independent output clocks), so different
  cadences (25 vs 60fps, inv #3 exact rationals) and per-program
  isolation (inv #1: one program stalling never stalls another) are
  STRUCTURAL. A `Program` actor under the existing Supervisor/RestartPolicy;
  a `ProgramSet` coordinator owns the shared source registry + placement.
- Efficiency (the product): decode-once-use-many via a reference-counted
  shared source registry/framestore (a source feeding N programs/tiles
  decoded ONCE); shared GPU residency; admission control (can the box
  afford a new program — decode MP/s, encode sessions, VRAM) tied to the
  ADR-0017/0018 placement engine; per-program + global resource-adaptive
  degradation (inv #9).
- Config: a backward-compatible `[[programs]]` model (adjacently-tagged
  by kind; today's single top-level program = one implicit program).
- API/UI: programs as a first-class resource (CRUD + per-program
  start/stop 202+op-id, live-apply class inv #11) + per-program realtime
  state + a UI program list/editor.

The synthesis verified its claims against the as-built code (EngineRuntime
is already per-program-shaped + isolation-clean; Actor/Supervisor/
RestartPolicy exist; the remux primitives exist but no packet-copy sink is
wired). Adds docs/research/multi-program.md + ADR-0030 (Proposed) + the
MP-0..MP-8 backlog; indexes both READMEs. Docs only — no code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
troykelly pushed a commit that referenced this pull request Jun 8, 2026
…aphics cap

ROOT CAUSE of the demo burning 5 CPU cores at 4K with the GPU ~80% idle: the
wgpu compositor is Vulkan-based, but the nvidia image had NO Vulkan loader
(`libvulkan1`) and NVIDIA_DRIVER_CAPABILITIES lacked `graphics`. With neither,
wgpu's request_adapter returns nothing and compositing SILENTLY falls back to the
CPU reference path — decode + composite on CPU while the 4060 does only NVENC.

Verified on the box: with `graphics` the toolkit mounts nvidia_icd.json +
libGLX/EGL_nvidia, but `libvulkan1` (the loader that resolves the ICD) is still
absent, so wgpu sees no device. Fix = install libvulkan1 + set the graphics cap;
the chain becomes loader → nvidia ICD → 4060 adapter → GPU compositing.

This is the enablement; a follow-up adds capability DETECTION + a clear UI warning
so "GPU present but unusable for compositing" is surfaced, never silently wasted
(bulletproof + easy-to-use), and feeds the real-time placement loop (inv #9).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
troykelly pushed a commit that referenced this pull request Jun 8, 2026
…the placement loop (ADR-0035)

From a verification-hardened fan-out (21 agents; 11/14 claims held, 3 refined).
Key finding: the placement engine is ~90% BUILT but never WIRED — the cost model
(cost.rs), the DRF placement policy (select.rs select_device), the engine
controller (placement.rs observe), and the degradation loop (degrade.rs) all exist,
pure + tested, but are NEVER called with live loads in the run; the LoadSource feeds
only the UI footer. And the silent GPU→CPU fallback is computed-then-discarded:
EngineRuntime::backend_kind() already knows it fell back, but that fact reaches
NOTHING — not an event, not a warning, not a hardware cross-check.

ADR-0035 defines the sense→detect→warn→plan→apply subsystem on the EXISTING parts:
- DETECT: probe actually-usable backends (wgpu adapter device_type, NVDEC/NVENC
  via libav) + cross-check vs NVML-discovered hardware.
- WARN: an actionable HealthWarning catalog (gpu-present-no-vulkan-adapter w/ the
  libvulkan/graphics remediation, software-decode/encode-on-gpu-host, nvenc-ceiling,
  vram-pressure, cpu-saturation, degradation-active), LATCHED/debounced, with a
  load-bearing NO-FALSE-POSITIVE rule (fires only on hardware-present AND
  software-tier-resolved → zero warnings on a CPU-only/software host).
- PLAN+APPLY: wire LoadSource→select_device/PlacementController into the run, net of
  co-tenant (ours-vs-total) load, affinity-preserving (never fragment a pipeline,
  GPU-placement principle), inv #1/#9/#10 — the runtime re-plan runs OFF the
  output-clock thread (NOT the per-tick hook; folded from the adversarial review).

SA-0..N backlog; SA-0 = the smallest win: detect+warn the compositor mismatch so the
silent fallback that burned 5 CPU cores becomes a clear banner + /api/v1/health.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
troykelly pushed a commit that referenced this pull request Jun 16, 2026
…rules (ADR-I006)

Add decision point #9 (and three alternatives-considered rows) for the round-4
panel fixes: re-FETCH key/revocation material at lease-acceptance (revocation
TOCTOU), the single binding-anchor chokepoint in LeaseStore::install_binding,
and the retry-stable per-operation Idempotency-Key. Correct the now-stale #8
note that said the binding anchor is recorded via record_binding_id (it moved
into install_binding).

Also tidies the round-4 test scaffolding (`upload_binding_for`): rustfmt of the
multi-line return type + rename `signer`/`signed` to `envelope_signer`/
`signed_lease` to satisfy clippy::similar_names under --features heartbeat (no
behaviour change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
troykelly pushed a commit that referenced this pull request Jun 16, 2026
Extend round-4 decision point #9 + add an alternatives row: the device-identity
anchor must come from SIGNED material, so instance_binding_id is bound into the
SignedLease signing bytes and verified at install — never anchored from an
unsigned LeaseBinding sidecar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
troykelly pushed a commit that referenced this pull request Jun 17, 2026
…turation residual; refresh SourcesPage L2 copy

Pre-PR review close-outs (orchestrator-flagged):

- pipeline.rs: the `select_admission_pick` budget comment cited "ADR-0035 §5" for
  the per-GPU perf-class CostBudget table being a future refinement — that
  citation is WRONG (ADR-0035 has no §5 and does not scope the budget table).
  Corrected to cite ADR-0018's hard-gate principle ("hard gates are the real
  safety": VRAM headroom + capability + NVENC-session), and state the perf-class
  table is future work (not yet built) — rule 27, no aspirational citation.
- pipeline.rs: `select_live_decode_pick` now DISCLOSES the known residual inline
  — a VRAM-roomy but decode-engine-saturated GPU passes both the permissive
  Mpix/s budget and the headroom gate, so a live add CAN be admitted onto a GPU
  that cannot sustain another decode. Acceptable for this ship: the never-off-air
  contract holds (an over-subscribed decode degrades the NEW tile, never the
  program — inv #1/#2, the clock samples last-good) and inv #9's closed-loop
  degradation sheds the cheapest tile if it bites. Comment-only; no code change.
- web SourcesPage: the apply-semantics callout + saved description said network/
  file sources "go live via config export + restart" — now FALSE after L2 (they
  apply live on a full-engine run). Refreshed the copy to match: synthetic +
  network/file apply live on a full-engine run; NDI/YouTube/AES67 (and any kind
  on the software engine) restart. The UI already reads the real
  X-Multiview-Apply header — this is the explanatory copy catching up (rule 27).

Web gate (npm ci then): eslint --max-warnings=0 EXIT 0; tsc --noEmit EXIT 0;
lingui compile EXIT 0; vitest run 632 passed / 63 files EXIT 0 (incl the 2
SourcesPage suites 10/10). Rust: fmt clean; clippy -p multiview-cli
--features ffmpeg,overlay,gpu EXIT 0 (the comments compile clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aperim-agent added a commit that referenced this pull request Jun 18, 2026
…pine) (#181)

* test(control,cli): RED, 3 new files +624 for ADR-W018 level 2 — network live add

TDD-first: failing tests for network source live add/edit functionality.
Tests expect APIs not yet implemented:
- typed_resources.rs delta: LiveSourceCapability + with_live_sources + capability-driven header
- live_apply_header_http.rs: bind_and_serve HTTP honesty (live only when spawner wired)
- live_network_apply.rs (ffmpeg-gated): e2e real ingest loop + state machine + never-falter proof

These tests pin the exact behavior: network/file kinds live on full-pipeline,
ndi/youtube/aes67 always restart, capability derives from ingest.is_some().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(config,control): add LiveSourceCapability for ADR-W018 L2 network live-add

- config/schema.rs: add SourceKind::is_network_media() classifier (rtsp/hls/ts/srt/rtmp/file/rist)
- control/state.rs: add LiveSourceCapability struct {synthetic, network} with constructors + is_live(kind)
- control/state.rs: add AppState.live_sources field (default synthetic_only)
- control/state.rs: add AppState::with_live_sources() builder
- control/lib.rs: re-export LiveSourceCapability

These provide the run-path capability signal the control plane threads to declare
which source kinds the engine can live-apply, ensuring X-Multiview-Apply headers
stay honest per build/run path (synthetic-only on software, synthetic+network on
ffmpeg).

BREAKING: bind_and_serve and related functions will need the live_sources param
in next commits; routes/sources.rs will call state.live_sources.is_live().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(control): update live_apply_upsert to check LiveSourceCapability

Changes live_apply_upsert to call state.live_sources.is_live(kind) instead of
just checking is_synthetic(). This allows network/file kinds to be live-applied
when the capability is set (full-pipeline run), and keeps ndi/youtube/aes67
always restart.

Also updates the kind-change check to use is_live() for both synthetic->network
and network->non-live transitions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cli): ADR-W018 L2 — live add/edit network sources via the uniform ingest path

Network/file kinds (rtsp/hls/ts/srt/rtmp/rist/file) now apply LIVE on the
running full-pipeline engine through ONE uniform ingest path — the level-2 spine
of the DEV-stack, re-authored against current main (the original PR #108 commits
9-way-conflict; pipeline.rs churned 49x since).

- cli live_sources: SourceSpawn + IngestSpawner seam + SpawnedProducer;
  LiveSourceHub::start_with_ingest threads the run's spawner; the worker's
  SpawnSource arm tears down any prior producer (edit = store-reuse swap) then
  spawns through the seam; no spawner => held + warned (slate, inv #10).
- cli pipeline: spawn_ingest_producer extracted — IngestSupervisor::start and
  the new LiveIngestSpawner call the SAME fn, so a runtime add runs exactly the
  startup supervised ingest_loop (reconnect/jitter/PTS-normalize/rw-timeout).
  Placement (ADR-W018 §7, gpu): drive_streaming publishes the pinned LiveIsland;
  select_live_decode_pick re-polls NVML and consults the SAME select_device
  scorer with the candidate set restricted to the island device (never frags/
  migrates) — admit => island ordinal stamped, reject/island-vanished => that
  source decodes in software, warned. The output never falters (inv #1).
- cli control: bind_and_serve takes the run's LiveSourceCapability; upsert_source
  routes a network kind to request_spawn_source (store create/reuse + route key
  + config mirror at the frame boundary, heavy spawn off-thread BEFORE the
  rebind — inv #1/#10); ndi/youtube/aes67 held with the reason.
- cli main: ControlPlaneWiring carries the ingest spawner; the capability sent
  to bind_and_serve is DERIVED from ingest.is_some() — the header claims live
  for network kinds exactly when a real spawner backs it. Pipeline path wires
  Pipeline::live_ingest_spawner(); software path wires None.
- control routes/sources: live_apply_upsert answers X-Multiview-Apply from the
  run capability; OpenAPI + doc updated. ADR-W018 amended for the as-built L2.

Restores the e2e chroma pin PR #179 deferred (live_decode_chroma.rs): a
live-added decode's U/V plane stats match the startup decode of the same clip.

Gates (lane-local target, env -u CARGO_TARGET_DIR, -j1): fmt clean; clippy GREEN
on default, ffmpeg, and ffmpeg+overlay+gpu; control typed_resources 23/23;
8 gpu placement unit tests incl the seam-pin spy; both ffmpeg e2e realtime proofs
GREEN (live add reaches LIVE, remove slates to NO_SIGNAL, churn never falters).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(cli,web): correct the inert-budget citation + disclose decode-saturation residual; refresh SourcesPage L2 copy

Pre-PR review close-outs (orchestrator-flagged):

- pipeline.rs: the `select_admission_pick` budget comment cited "ADR-0035 §5" for
  the per-GPU perf-class CostBudget table being a future refinement — that
  citation is WRONG (ADR-0035 has no §5 and does not scope the budget table).
  Corrected to cite ADR-0018's hard-gate principle ("hard gates are the real
  safety": VRAM headroom + capability + NVENC-session), and state the perf-class
  table is future work (not yet built) — rule 27, no aspirational citation.
- pipeline.rs: `select_live_decode_pick` now DISCLOSES the known residual inline
  — a VRAM-roomy but decode-engine-saturated GPU passes both the permissive
  Mpix/s budget and the headroom gate, so a live add CAN be admitted onto a GPU
  that cannot sustain another decode. Acceptable for this ship: the never-off-air
  contract holds (an over-subscribed decode degrades the NEW tile, never the
  program — inv #1/#2, the clock samples last-good) and inv #9's closed-loop
  degradation sheds the cheapest tile if it bites. Comment-only; no code change.
- web SourcesPage: the apply-semantics callout + saved description said network/
  file sources "go live via config export + restart" — now FALSE after L2 (they
  apply live on a full-engine run). Refreshed the copy to match: synthetic +
  network/file apply live on a full-engine run; NDI/YouTube/AES67 (and any kind
  on the software engine) restart. The UI already reads the real
  X-Multiview-Apply header — this is the explanatory copy catching up (rule 27).

Web gate (npm ci then): eslint --max-warnings=0 EXIT 0; tsc --noEmit EXIT 0;
lingui compile EXIT 0; vitest run 632 passed / 63 files EXIT 0 (incl the 2
SourcesPage suites 10/10). Rust: fmt clean; clippy -p multiview-cli
--features ffmpeg,overlay,gpu EXIT 0 (the comments compile clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): explicit DecodePlacement tri-state — a placement-rejected live decode forces software (2a67e4e)

Carries the 2a67e4e amendment my initial re-author flattened away — a real
overcommit/fragmentation bug, not a refinement.

THE BUG (in my prior commit): select_live_decode_pick returned Option<String>,
so a placement reject returned None, leaving IngestPlan.cuda_ordinal = None. In
open_and_stream that means `new_preferring_hw(.., want_hw=true, None)` — NVDEC on
libav's DEFAULT CUDA device. On a single-GPU over-headroom island the default
device IS that island (overcommit); on a multi-GPU host it may be a DIFFERENT
GPU (silent island fragmentation, forbidden by ADR-0018 never-fragment). "No
ordinal" cannot distinguish *no placement decision* (default device fine) from
*placement rejected* (hardware must NOT open).

THE FIX: an explicit, closed tri-state DecodePlacement::{Default, Pinned(ordinal),
SoftwareOnly} on IngestPlan + a single decoder_open_args gate:
- SoftwareOnly -> (want_hw=false, None): hardware never attempted, even when
  NVDEC is compiled/present/not env-disabled.
- Pinned(ordinal)/Default -> keep the canonical want_hw_decode env reading
  (MULTIVIEW_DISABLE_NVDEC opt-out still wins over a pin); Pinned threads the
  island ordinal.
- select_live_decode_pick -> select_live_decode_placement: admit -> Pinned (or
  Default when the island resolved no ordinal); reject AND island-vanished ->
  SoftwareOnly, with truthful FORCING-software warnings.
- drive_streaming stamps Pinned; ingest_plan_for defaults Default; open_and_stream
  routes through decoder_open_args.

TDD: restored tests/decode_placement.rs (the f0a6b7f/2a67e4e4 RED pin) — it
opens the REAL decoder via the gate's exact args and asserts a SoftwareOnly
source comes back software (used_hw=false, no cuvid named); the gpu-lane unit
tests now assert the placement value AND the decode-open behaviour per placement
(not just the picked ordinal). ADR-W018 §7 amended for the as-built tri-state.

Gate (env -u CARGO_TARGET_DIR, lane-local, -j1): fmt clean; clippy --workspace
EXIT 0; clippy -p multiview-cli --features ffmpeg,overlay,gpu EXIT 0;
decode_placement 2/2, gpu placement 8/8, live_network_apply + live_decode_chroma
1/1 each (ffmpeg).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(control,cli): gate RIST live-apply on the rist feature — never over-claim X-Multiview-Apply:live (panel major)

Codex panel (#181) MAJOR: RIST was advertised live-appliable unconditionally —
`is_network_media()` includes `Self::Rist` and the binary derives
`LiveSourceCapability::synthetic_and_network()` from `ingest.is_some()`. On an
`ffmpeg`-WITHOUT-`rist` build the hub wires a real ingest spawner (so
rtsp/hls/ts/srt/rtmp/file correctly flip to `live`), but a RIST source's
`ingest_plan_for` arm is a typed refusal (`librist` not built) — so the header
claimed `X-Multiview-Apply: live` for a spawn that refuses, and the tile rode
the slate. The header over-claimed.

Fix: `LiveSourceCapability` gains a separate `rist: bool` the binary sets from
`cfg!(feature = "rist")` (via the new `with_rist` builder); `is_live` returns
`self.rist` for `SourceKind::Rist` and `self.network` for the other network
kinds. `synthetic_only` ⇒ rist:false; `synthetic_and_network` ⇒ rist:true (the
canonical "full network" meaning), narrowed by the binary's
`serve_control_plane` to the build's actual rist truth. RIST classification
(`is_network_media`) is unchanged — RIST IS network media; only its *live-apply
capability* is feature-gated, the honest level.

Tests (control typed_resources, 23→25): `rist_stays_restart_when_the_engine_
build_lacks_the_rist_feature` (with_rist(false) ⇒ RIST restart + no UpsertSource,
while a non-RIST network kind still flips live) and `rist_applies_live_when_the_
engine_build_carries_the_rist_feature` (with_rist(true) ⇒ RIST live + UpsertSource).

Gate (env -u CARGO_TARGET_DIR, lane-local, -j1): fmt clean; clippy --workspace
EXIT 0; clippy -p multiview-cli --features ffmpeg EXIT 0 AND --features
ffmpeg,rist EXIT 0 (both cfg branches compile); control typed_resources 25/25.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): ADR-W018 §7 round-2 — placement fail-closed + bounded-join startup producers

Codex panel round-2 on #181 found three real data-plane defects beyond the round-1 tri-state; all fixed here:

- EMPTY-ISLAND fall-through (blocker): LiveIngestSpawner consulted select_live_decode_placement only when an island was published; an empty slot (startup admission rejected the GPU / no NVML) kept DecodePlacement::Default -> default-device NVDEC. decode_placement_for now forces SoftwareOnly when admission was attempted but named no island (fail closed, ADR-W018 §7).
- ADMIT-with-no-ordinal (major): an island that passes budget but resolves no CUDA ordinal returned Default -> default-device NVDEC on the LIVE path (the compositor island is already pinned). Now forces SoftwareOnly.
- TWO-WRITER race on a reused single-writer TileStore for a live EDIT of a STARTUP-origin source (blocker): teardown only joined hub-owned producers; a startup producer JoinHandle lives in IngestSupervisor, so the old decode thread could still write while the replacement published. register_stop now returns an exited latch every producer carries as an ExitGuard (flips on return or panic); teardown raises stop then bounded-waits on the latch (await_exits) before the replacement publishes -- covering startup-origin producers, not just hub-owned ones.

ProducerStop.stop is pub(crate) so the cross-module registration tests raise it; the gpu-gated placement matches are if-let-else (clippy single_match_else). Gate (lane-local, env -u CARGO_TARGET_DIR, -j1): fmt + clippy --workspace + clippy -p multiview-cli --features ffmpeg,overlay,gpu + test -p multiview-cli --features ffmpeg + test --workspace all EXIT 0. Completes the stalled dev108-live-add lane WIP (round-2 authored there; the 5 ExitGuard call-site completions + the ProducerStop accessor + clippy/test fixups by the integrator).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): build ExitGuard before spawn so a failed thread-spawn never orphans the latch (round-2 panel)

Codex round-2 panel on #181 flagged the ExitGuard-on-spawn-failure gap: register_stop inserts the stop entry + returns the exited latch BEFORE Builder::spawn, but the ExitGuard that flips exited was constructed INSIDE the closure — so a (rare) thread-spawn Err left an orphaned exited=false entry, and a later teardown of that id busy-waited the full TEARDOWN_JOIN_GRACE (3s) before detaching (a regression from the round-1 no-op join; bounded + off the output-clock thread, never an inv #1/#10 breach).

Fix: construct the ExitGuard BEFORE the spawn and move it into the closure at all 7 producer-spawn sites (run.rs synth generator; pipeline.rs audio/tone/captions/youtube-reresolve + the spawn_ingest_producer decode thread; live_sources.rs hub synth). Its Drop now flips exited whether the thread runs (drops on exit) OR Builder::spawn fails (the closure owning the guard is dropped) — so a failed spawn deregisters cleanly and teardown never busy-waits an orphan.

Gate (lane-local, env -u CARGO_TARGET_DIR, -j1): fmt + clippy --workspace + clippy -p multiview-cli --features ffmpeg,overlay,gpu + test -p multiview-cli --features ffmpeg + test --workspace all EXIT 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(adr): ADR-W018 §7 — admit-no-ordinal forces SoftwareOnly (not Default) on the live path + RIST live-apply is rist-feature-gated (round-3 panel)

Two doc-drift minors the round-3 Codex panel flagged (rule 27 — the ADR prose contradicted the verified code):
- §7 said an admitted live decode with no resolved CUDA ordinal stamps Default; the round-2 fix forces SoftwareOnly on the live path (Default is the startup placement, where nothing is pinned yet). Corrected.
- The level-2 amendment said rist is included unconditionally; the code gates RIST live-applicability on cfg!(feature=rist) (with_rist), keeping an ffmpeg-without-rist build at restart. Corrected.

Docs-only; the Rust build/clippy/tests are unaffected (green at 46ceea3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: verify <verify@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aperim-agent added a commit that referenced this pull request Jul 10, 2026
…sions (ADR-RT010, task #9) (#231)

* docs(adr): ADR-RT010 — live authz revocation on established WS/SSE sessions

Design-first for realtime security task #9 (PR #211 auth-panel finding A1,
pre-existing): WS/SSE capture the Principal (role + scoped_object_ids) at
connect ONLY, so a mid-session scope narrow/widen, role downgrade, or key
revoke keeps delivering now-unauthorized deltas (and displaying now-hidden
cached objects) until reconnect.

Decision: auth-generation re-resolve (wait-free AtomicU64 the session samples)
+ hybrid response — graceful in-place $resync (new ResyncReason::AuthzChanged)
for scope changes, forced 4403 disconnect for loss of read access. ApiKeyStore
becomes the interior-mutable single source of truth (RwLock map + generation;
revoke/set_principal bump it, principal_for_key re-resolves). Invariant #10:
a per-session read-side decision (one atomic load fast path, control-plane
read-lock only on a change) — no engine lock/await/channel; resume-by-seq +
the #211 object-scope filter + the ADR-RT009 watermark all intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(control): RED — live authz revocation on established WS/SSE sessions (ADR-RT010)

Failing test for realtime security task #9: the new authz-revocation surface
does not exist yet, so these fail to compile (26 errors) against the intended
API — ApiKeyStore::{generation,principal_for_key,revoke,set_principal},
SessionStream::{with_live_reauth,reauthorize,resync_frame}, ReauthOutcome,
ResyncReason::AuthzChanged.

Covers: scope revocation of one object stops delivering it mid-session; a
revoked key disconnects the established session; a still-reading role downgrade
is adopted without a spurious disconnect; scope narrowing emits a $resync
rebuild directive (closes the display hole); scope widening reveals new
objects; a session without live re-auth (local-admin/JWT) is untouched; and the
store mutators bump the generation + re-resolve (set_principal keeps the digest,
revoke removes the key).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(control,events): honor mid-session authz changes on WS/SSE (ADR-RT010, task #9)

GREEN for realtime security task #9 (PR #211 auth-panel finding A1). WS/SSE
captured the principal's role + object scope at connect ONLY, so a mid-session
scope narrow/widen, role downgrade, or key revoke kept delivering
now-unauthorized deltas (and left now-hidden objects on the client's display)
until reconnect.

- auth: ApiKeyStore is the interior-mutable single source of truth — RwLock key
  map + wait-free `generation: AtomicU64`. `revoke`/`set_principal` bump the
  generation under the write lock (a session observing it reads the mutation);
  `principal_for_key`/`generation` re-resolve. `register` stays &mut self
  (construction via RwLock::get_mut, no lock, no test churn); locks are
  poison-resilient.
- realtime: SessionStream gains a live-authz handle (store-managed API keys
  only; local-admin/JWT keep connect-time authz). `reauthorize()` samples the
  generation (one atomic load fast path) and, on a change, re-resolves:
  scope narrow/widen → adopt + `ReauthOutcome::ScopeChanged`; key revoked / role
  can't Read → `Disconnect`. The transport re-resolves BEFORE projecting each
  delta (split recv_event from frame_for) and on a 5s idle REAUTH_TICK, so a
  change filters the very next delta with no seq gap; ScopeChanged emits a
  server `$resync` (new ResyncReason::AuthzChanged) + new-scope re-snapshot
  (closes the display hole); Disconnect closes WS 4403 / ends SSE.
- events: ResyncReason::AuthzChanged (+ AsyncAPI schema regen).

Invariant #10: a per-session read-side decision — no engine lock/await/channel,
no back-pressure on the publish path. Resume-by-seq + the #211 object-scope
filter + the ADR-RT009 watermark all intact (regression suites green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(control): RED — connect-window revoke/re-scope escapes live reauth; $resync strands switcher (ADR-RT010, #231 panel)

The 3-reviewer auth panel on PR #231 found two authorization/protocol defects and
a vacuous test:

1. TOCTOU: a store API key revoked or re-scoped in the window BETWEEN connect-auth
   and live-authz install is silently retained — `install_live_reauth` gates on a
   racy `principal_for_key().is_some()` re-probe and `with_live_reauth` samples the
   generation AFTER the connect principal was resolved.
2. `$resync` advertises a `Switcher` rebuild `build_resync_frames` never sends,
   stranding a rebuild-not-merge client's switcher state.

Add the baseline-aware `with_live_reauth_at` + observable `live_role` API surface
(here still buggy — `with_live_reauth_at` ignores the baseline; `resync_frame`
still lists Switcher) and the failing regression tests. Also strengthen the
role-downgrade test to observe adoption via `live_role()` (was vacuous).

RED (3 new tests fail, 7 pass):
  revoke_racing_connect_is_caught_not_retained         Unchanged != Disconnect
  rescope_racing_connect_is_adopted_not_stale          Unchanged != ScopeChanged
  resync_rebuild_topics_match_the_re_snapshotted_set   [Tiles,Devices,Switcher] != [Tiles,Devices]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(control): close live-authz connect-race + $resync switcher gap + register poison-swallow (ADR-RT010, #231 panel)

GREEN for the RED regression tests. Addresses the 3-reviewer auth panel on PR #231:

1. Connect-race TOCTOU (critical): `resolve_principal` now captures the store
   authorization generation BEFORE resolving the key and threads it as the
   live-authz baseline (`Some` for store keys, `None` for local-admin/JWT) through
   `RealtimeViewer` / `ws_handler` / `sse_handler` / `install_live_reauth`. Install
   wires the handle for EVERY store key — dropping the racy
   `principal_for_key(&key_id).is_some()` re-probe that conflated "revoked store
   key" with "not a store key" and stranded a revoked-in-window session authorized
   forever. Both transports run a pre-snapshot reauthorize gate, so a revoke landing
   between auth and install disconnects with no snapshot leak, and a re-scope adopts
   the new scope before the first snapshot.
2. `$resync` switcher gap: `resync_frame` no longer advertises `Topic::Switcher`
   (never re-snapshotted); the rebuild set equals what `build_resync_frames` sends
   (Tiles + Devices), so a rebuild-not-merge client is not stranded.
3. `ApiKeyStore::register` recovers a poisoned lock instead of silently dropping the
   insert (rule 37), consistent with `read_keys`/`write_keys`.
4. `live_role()` accessor makes role adoption observable (de-vacuoused the downgrade
   test); the role-can't-Read arm is documented as currently-unreachable forward-compat.

fmt + clippy clean; multiview-control suite green (incl. 3 new connect-race/resync
regressions + the corrected scope-narrow assertion); cargo check --workspace green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(adr-rt010): correct $resync topic set (drop switcher) + deterministic connect-race handling (#231 panel)

- resubscribe is [tiles, devices] — exactly what build_resync_frames re-snapshots;
  switcher is excluded (not object-scoped, never re-snapshotted → would strand a
  rebuild-not-merge client's switcher state).
- Replace the "caught at the next generation bump (self-healing)" claim (which
  documented the connect-race rather than closing it) with the deterministic fix:
  baseline generation captured at auth + pre-snapshot reauthorize gate + handle
  installed for every store key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(control,adr-rt010): honest bounded-latency framing + accurate poison/close justifications (#231 re-review)

The 3-panel re-review of the fix delta found NO logic defect (the concurrency/#10
review passed) — three honesty/guardrail corrections:

- RR1: the pre-snapshot gate closes the auth→install RETENTION race, but a change in
  the sub-tick window after the gate is caught by the first pump tick (bounded
  latency), not "no leak". Fully closing it would require holding the store lock
  across the socket send, which invariant #10 forbids. Reworded the WS/SSE gate
  comments + ADR-RT010 to the honest guarantee.
- RR3: justified the `let _ = socket.send(Close)` discard (rule 20 — the socket is
  being torn down) and corrected the poison-recovery justification in
  register/read_keys ("insert/remove/reassign whole KeyRecord values", not "replaced
  wholesale" — rule 27).

Switcher-exclusion verified against object_authz_scope_id: Device*/Cast*/MediaPlayer/
input-bound-Tile events are Some(scoped); all Switcher events are None (unscoped), so
a scope change does not affect switcher visibility and it is correctly omitted from
the $resync rebuild set.

Comment/doc only; no logic change. fmt + clippy clean; tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
aperim-agent added a commit that referenced this pull request Jul 10, 2026
…, inv #9) (#237)

* test(audio): RED — cross-fade leaks mixer strips (invariant #9)

`repoint_crossfade` adds a temporary outgoing `InputStrip` that fades out;
on completion `retire_completed_fades` only *unroutes* it (flips a bool) and
drops it from the `ProgramBus` route table, but the strip stays resident in
`Mixer.inputs` forever. Every cross-fade therefore leaks one strip — unbounded
RAM growth on a 24/7 multi-program host, violating invariant #9 (bounded
queues/pools "allocated at start, never grow").

Add read-only strip-count observability (`Mixer::slot_count` /
`live_input_count` and `ProgramBus::mixer_slot_count` / `live_strip_count`) and
a failing test that drives 200 completed cross-fade cycles and asserts (1) only
the channel strip stays live after each fade and (2) physical strip storage
stays bounded. Fails RED at cross-fade #0: live_strip_count == 2, expected 1
(the faded-out strip is never reclaimed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(audio): reclaim completed cross-fade mixer strips

Replace the mixer's append-only InputStrip vector with a slot-map:
Vec<Option<InputStrip>> plus parallel u64 generations and a free-list.
Completed cross-fade outgoing strips are now removed from the mixer and their
slots returned for reuse instead of merely being unrouted and retained forever.
Physical strip storage is bounded by the high-water mark of concurrently-live
strips (one channel + one outgoing slot for serial cross-fades), satisfying
invariant #9.

RoutePoint now carries the slot generation. Every read/write validates both
index and generation, so a stale handle cannot address or free a reused slot
(the ABA corruption footgun in the resilience brief). Generation exhaustion
tombstones the slot rather than wrapping.

Add a 2,000-cross-fade soak proving the physical storage stays flat at exactly
2 slots, plus an adversarial stale-handle test covering reads, writes, submit,
double-remove, slot reuse, and fresh-occupant integrity. The untouched click,
equal-power, true-peak, and post-ramp cursor tests remain green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(audio): overlapping cross-fades retire independently and reuse slots

Additive regression closing the review-flagged gap: start OVERLAP concurrent
cross-fades one tick apart so each earlier outgoing strip is still mid-fade when
the next begins, then tick to completion. Asserts the channel plus all OVERLAP
outgoing strips are live at once, that the live-strip count passes through every
intermediate value down to 1 as the strips retire on distinct ticks (independent,
not all-at-once retirement), and that physical mixer storage stays exactly at the
concurrent high-water (OVERLAP + 1) across 4 rounds — proving every reclaimed
slot is reused, never leaked. Test-only; no production change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
aperim-agent added a commit that referenced this pull request Jul 10, 2026
…tch pool (inv #9) (#238)

* test(compositor): RED — persistent-backend LUT + band-scratch reuse contract

Efficiency findings #5 (transfer LUTs rebuilt every composite tick) and #6
(per-band accumulator vec-allocated every tick) on the CPU reference
compositor. Asserts the persistent RunBackend builds the transfer LUTs once
per stable transfer-set and reuses the band scratch after warmup, plus that
reused ticks stay byte-stable.

Fails to compile: RunBackend has no lut_build_count()/scratch_alloc_count()
yet (E0599). GREEN adds the memoized LutCache + reusable ScratchPool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(compositor): GREEN — memoized transfer LUTs + reusable CPU composite pool

Efficiency findings #5 and #6 on the CPU reference compositor (the golden-frame
oracle), which composites once per output tick forever (invariant #1). Both
per-tick costs are now paid once and reused, owned by the persistent RunBackend.

#5 — transfer LUTs. composite_with_threads rebuilt LutSet::for_transfers every
tick (thousands of pow/exp node evaluations: a BT.709 EOTF+OETF pair samples
6144+4096 nodes, a PQ EOTF 12288). New LutCache memoizes the LutSet keyed on the
order-independent SET of transfer characteristics in play (canvas + tiles) and
rebuilds only when that set changes.

#6 — per-band composite scratch. composite_band vec-allocated the premultiplied
accumulator + a bool coverage vec every tick (~33 MB for a full-cover 1080p
band), and composite_with_threads vec-allocated the output NV12 planes. New
ScratchPool (CPU twin of gpu::pool::SurfacePool) reuses one BandScratch per
worker band; coverage is a generation-stamped sentinel (Coverage) so neither the
accumulator nor the coverage map is cleared per tick; output planes are leased
and returned to bounded one-slot caches on Nv12Image::drop. Grow-only; steady
state performs zero allocation.

Both the free composite* functions (transient LUTs + pool) and the persistent
RunBackend (memoized + reused) route through one composite_core kernel, so the
pooled backend stays byte-for-byte the free-function oracle. Bit-exactness is
pinned by the untouched pixel-driven oracle (composite_band_reference) via the
composite_tile_driven equivalence proptest, backend_select byte-for-byte, and
parallel_bands thread-count invariance — all still green.

RunBackend::Cpu now carries a Mutex-guarded CpuBackend (interior mutability, &self
composite, poison-recovered — matches GpuCompositor's Mutex<SurfacePool>); adds
observability accessors lut_build_count()/scratch_alloc_count().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(compositor): RED — force ≥2 workers on the pooled parallel path

Panel finding (rule 18): the `*_parallel_path` cases in
composite_scratch_reuse.rs drove `RunBackend::composite`, which sizes its
worker fan-out from `auto_thread_count()`. On a single-vCPU CI runner that is
1, so both "parallel" cases silently ran the SERIAL branch — a worker-scratch
aliasing or shared-LUT race would pass CI undetected and the parallel
bit-exactness claim was not deterministically guarded.

Drive a not-yet-existing `RunBackend::composite_with_thread_count(n)` seam with
a fixed FORCE_THREADS and assert the multi-band branch actually ran via a
not-yet-existing `band_count()` accessor (`>= 2`), independent of host CPU
count. Add `forced_parallel_matches_forced_serial_byte_for_byte`: pins the
reused-pool parallel output to the single-band serial reference byte-for-byte
(tile spanning several band boundaries + non-trivial bg + partial opacity) — a
real regression that fails if two workers alias one band's scratch or a band
mis-addresses its rows.

Fails to compile: 16× E0599 (no method `composite_with_thread_count` /
`band_count` on `RunBackend`) — the RED.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(compositor): GREEN — deterministic forced-thread seam for the pooled parallel path

Add the seam the RED needs, source-only (the test is untouched):

* `RunBackend::composite_with_thread_count(n)` (+ its `CpuBackend` twin) — as
  `composite` but with an explicit CPU worker count. `composite` now delegates
  to it with `auto_thread_count()`, so production is unchanged; a test can force
  the multi-band parallel branch (or serial) on any host. The band split only
  partitions the same deterministic per-pixel pipeline, so the output is
  byte-identical for any `n_threads` — this changes which path runs, never the
  pixels. `#[doc(hidden)]`; the GPU arm ignores `n_threads`.
* `RunBackend::band_count()` / `ScratchPool::band_count()` — the grow-only
  high-water band-slot count (1 = serial branch, worker count = parallel), the
  deterministic signal that the multi-band branch actually ran.

Proves the new `forced_parallel_matches_forced_serial_byte_for_byte` guard is a
real regression, not tautological: injecting a one-off band row-misaddress
(`py_start += 2`) into `composite_parallel` makes only that case FAIL on the
Y-plane byte-identity assertion; reverted, all 7 pass.

Gate (worktree-local target): fmt --all --check clean; clippy -p
multiview-compositor --all-targets -D warnings clean; test -p
multiview-compositor all green incl golden-frame guards (backend_select 4/4,
composite_tile_driven 7/7, parallel_bands 2/2, lut_vs_reference 4/4,
composite_scratch_reuse 7/7); cargo check --workspace clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
aperim-agent added a commit that referenced this pull request Jul 10, 2026
#242)

* test(audio): RED bounded-memory test for publish_at union-span over-alloc

`AudioStore::publish_at` allocates the full `[min(base,at), max(head,block_end))`
union-span buffer BEFORE applying the drop-oldest capacity clamp. A late/reordered
RTP-audio packet whose rebased index lands up to ~10 s (DEFAULT_DISCONTINUITY_FRAMES
= 480_000, the rebaser's re-anchor threshold) below the live head therefore makes
the transient buffer ~10 s of audio even though the store capacity
(STORE_CAPACITY_FRAMES = 96_000 = 2 s) is far smaller. Allocation is decoupled from
capacity: a burst of ~9.9 s-reordered packets is a per-packet zero+memcpy CPU
amplification vector on the ingest data plane, and "queues drop, never grow"
(invariant #2/#5/#9) is violated for the transient.

The transient is observable without a global allocator (the crate is
`unsafe_code = forbid`): `Vec::drain` shifts survivors down but never reclaims
capacity, so the over-allocated span stays visible via the new doc-hidden
`window_backing_capacity_frames` probe even after the post-merge clamp shrinks the
length back to capacity.

RED — the new test fails on the transient bound:
  backing 479999 frames > capacity 96000 + one block 4800

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(audio): bound publish_at transient to capacity, drop-oldest before alloc

`AudioStore::publish_at` sized its merge buffer to the FULL
`[min(base,at), max(head,block_end))` union span and only applied drop-oldest
AFTER, via a post-merge `Vec::drain`. A late/reordered RTP-audio packet whose
rebased index lands up to ~10 s below the live head (the rebaser only re-anchors
past DEFAULT_DISCONTINUITY_FRAMES = 480_000, ≫ STORE_CAPACITY_FRAMES = 96_000)
therefore allocated a ~10 s (~3.8 MB stereo) transient per packet even though the
store retains only 2 s — a zero+memcpy CPU-amplification vector on the ingest data
plane, and "queues drop, never grow" (invariant #2/#5/#9) violated for the
transient.

Apply drop-oldest UP FRONT: clamp the working base to
`max(union_base, new_head - capacity_frames)` before allocating, so `merged` is
never wider than `capacity_frames` and no evicted frame is ever allocated. A new
`overlay_from` helper copies each source (existing window, incoming block) into the
clamped buffer, skipping the frames that fall below the new base — that is the
drop-oldest, now applied during the copy rather than by a trailing `drain`. The
final published window is byte-identical to before in every case (the clamp is a
no-op whenever the union span already fits capacity, so in-capacity placement,
reorder-within-capacity, gap silence-fill and last-write-wins all behave exactly
as before); only the transient allocation shrinks.

Measured (test publish_at_reordered_far_below_head_allocates_bounded): a packet
reordered ~10 s below the head now backs a 96_000-frame buffer, was 479_999 — the
transient is bounded by capacity, not the discontinuity span. All existing
publish_at/store tests pass unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
aperim-agent added a commit that referenced this pull request Jul 10, 2026
…nv#9, generation-safe) (#244)

* test(framestore): RED — steady-state ring must prune consumed frames (inv#9)

The per-tile media-time ring (TileStore::ring, RING_CAPACITY=256) bounds only
by COUNT and never prunes frames the output clock has already advanced past.
Output `now` is monotone (inv #1/#3), so a frame older than the current latch
can never be selected again — yet under perfect 1:1 pacing (the shipping
PtsWallClock path) the ring sits pinned at 256 dead frames (~3.2 GB/tile at a
1×1 2160p tile), an invariant #9 bounded-memory defect.

Adds a doc-hidden `retained_frames()` introspection accessor (for this test and
future soak gates) and a failing bounded-memory test that publishes+reads 1:1
well past the ceiling and asserts the ring retains a tiny trailing window, not
the full backlog. Fails RED today: "retained 256 of cap 256".

The two existing decode-ahead guards (read_at_does_not_race_ahead,
state_at_ages_a_finite_clip) batch-publish BEFORE any read, so they are
unaffected — the fix must keep them green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(framestore): prune consumed frames from the media-time ring (inv#9)

The per-tile latch-on-tick ring (ADR-T009) bounded only by COUNT and never
released frames the output clock had advanced past, so even under perfect 1:1
pacing it sat pinned at RING_CAPACITY (256) dead frames — ~3.2 GB/tile at a
1×1 2160p tile, an invariant #9 bounded-memory over-allocation. (This ring is
the media-time sampler, distinct from the inv#2 single-slot last-good store
`LatestSlot`, which is untouched.)

Root cause is a missing prune, not an oversized ceiling: output `now` is
monotone (inv #1/#3), so any entry stamped strictly before the current latch is
provably never selected again. Fix:

* readers (`read_at`/`state_at`) record a monotone latch watermark via
  `fetch_max` (one relaxed atomic on the output-clock thread);
* the producer's next `publish_arc` drops entries older than the watermark,
  inside the existing O(capacity) copy-on-write on the sampled input thread —
  never on the output clock;
* a backwards stamp (source-generation change) resets the watermark so the
  re-anchored frame is never pruned before a reader latches it.

Steady state collapses to the latched frame + any decode-ahead future (~2
frames/tile: 3.2 GB → tens of MB). RING_CAPACITY stays as a documented hard
ceiling bounding the worst case (a stalled output clock that stops advancing the
watermark while the producer runs on). Lock-free single-slot semantics and the
latch-on-tick selection rule are unchanged.

The two decode-ahead guards batch-publish before any read, so the watermark
stays at its MIN sentinel and nothing is pruned — they pass unchanged. Turns
the RED steady-state bounded-memory test green (retained 256 -> 2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(framestore): harden prune guards — boundary, reconnect, monitored tile

Mutation-hardening for the media-time ring prune (kills the surviving mutants
`cargo mutants --in-diff` would otherwise report on tile.rs):

* prune_keeps_the_latched_boundary_frame — the frame AT the watermark must
  survive (kills `>=` -> `>` on the retain bound);
* backwards_stamp_resets_the_watermark_so_a_reconnect_is_not_pruned — a
  re-anchor (reconnect/discontinuity) resets the watermark so the fresh low
  generation is not pruned by the old high latch (kills deleting the reset);
* state_at_alone_bounds_the_ring_for_a_monitored_uncomposited_tile — state_at
  advances the watermark too, so a monitored-but-not-composited tile stays
  bounded (kills deleting state_at's fetch_max).

Each encodes a real behaviour (boundary retention, reconnect resilience,
off-canvas monitoring), not just a mutant trap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(framestore): kill equal-timestamp re-anchor mutant in ring prune

The `< with <=` mutant on publish_arc's backwards check (`at < tail.at`)
treats a duplicate/clamped equal stamp as a discontinuity — resetting the
latch watermark and defeating the prune. No existing test published two
frames at the same instant, so the mutant survived `cargo mutants --in-diff`.

This test establishes the watermark (publish @0/@40/@80ms, read_at 80ms),
republishes at the SAME 80ms instant, then asserts the ring still pruned the
consumed @0/@40ms frames (retained == 2, not 4) and newest-wins at the shared
instant. Fails on the `<=` mutant, passes on `<`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(framestore): fuse ring select+watermark-advance; document prune safety

Data-plane pre-merge hardening for the inv#9 ring prune (team-lead checklist):

- Fuse frame selection and the prune-watermark `fetch_max` into one private
  `latch_and_advance`; `read_at` and `state_at` both route through it. A reader
  now physically cannot select a ring frame without first advancing the
  watermark past it, so `publish_arc`'s prune is correct BY CONSTRUCTION for any
  future reader (e.g. a wired-up degradation sampler) — fail-safe, not
  fail-checked.
- `read_at`'s only new per-tick cost stays one relaxed-atomic `fetch_max` (no
  lock, no allocation); the read site says so.
- State the prune boundary at the `retain`: keep entries at-or-after the
  watermark, drop ONLY those strictly before it. The latched boundary frame sits
  exactly at the watermark and is never pruned.

Behaviour-preserving: all 15 ring tests + the state-machine suite pass unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(framestore): RED — cross-generation watermark race drops new-gen boundary frame

The ADR-T009 data-plane review (Codex) found a concurrency defect in the
ring prune watermark: a reader holding a superseded ring snapshot can
advance the media-time watermark AFTER a reconnect re-anchors to a fresh,
low media-time generation. The next forward publish then prunes the new
generation's own frames as "older than the stale watermark", dropping the
boundary frame the compositor is showing (invariant #2 / #9).

Deterministic two-thread reproduction: two barriers force the exact
interleaving publish(reconnect) -> stale latch_and_advance -> publish(fwd),
so the RED is reliable rather than a flaky stress loop. Fails on the
current media-time watermark (read_at(0) yields the later frame 101 instead
of the reconnected boundary frame 100); passes once the watermark is keyed
on the globally-monotonic publish sequence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(framestore): key ring prune watermark on publish seq for cross-generation safety

The prune watermark was a media timestamp, which is NOT monotonic across a
reconnect: after a backwards re-anchor to a fresh low-media-time generation,
a reader still holding the superseded high-media-time snapshot could
`fetch_max` the watermark to an old high value AFTER the re-anchor, and the
next forward publish then pruned the new generation's own frames (retain
`>= watermark`) — dropping the boundary frame the compositor is showing
(inv #2 / #9). The store()-reset on backwards could not close it: the stale
reader's advance lands after the reset.

Fix: key the watermark on the tile-global publish `seq` (from LatestSlot),
which is strictly increasing and NEVER reset across generations. A newer
generation's frames always carry strictly higher `seq`s than any frame an
old snapshot holds, so a stale cross-generation `fetch_max` can only leave
the watermark BELOW them and `retain(|e| e.seq >= watermark_seq)` keeps
them. This makes the prune generation-safe by construction — no generation
counter, no bit-packing, no reset — and removes the now-unnecessary
backwards watermark reset (sequence monotonicity subsumes it). RingEntry
carries its publish `seq`; the media-time `at` remains the selection key.

Read path stays wait-free/allocation-free (one relaxed `fetch_max`, now on
`seq`). Turns the RED cross-generation race test green; all existing
latch/prune/reconnect/decode-ahead tests pass unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(framestore): retitle reconnect test to the seq-monotonicity mechanism

The prune watermark is now keyed on the tile-global publish seq rather than
media time, so a reconnect is protected by sequence monotonicity, not by a
watermark reset. Retitle backwards_stamp_resets_the_watermark_* ->
backwards_stamp_reanchors_* and correct its comment/message to describe the
real mechanism (rule 27: no misleading test docs). Assertion comparisons are
byte-identical — only the name, comments, and message strings change; no
weakening.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
aperim-agent added a commit that referenced this pull request Jul 11, 2026
…Page (ADR-W030, #9) (#263)

* docs(decisions): ADR-W030 — GET /system/capabilities honest default-build surface

Design-first record (task #9/#176, mgmt-completeness §2.7): assemble an honest
capability+licence DTO from hal::probe + cfg!(feature) + the resolved AdapterReport
rather than serialize a non-existent CapabilityReport (that rich telemetry is the
separate SA-1+ lane #180). Pins effective_license to AGENTS.md §G / ADR-0012,
build{} inline, viewer-read no-BOLA auth, control-keeps-no-hal-dep, and the static
with_capabilities snapshot (invariant #10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(control,cli): RED — failing /system/capabilities DTO+route+licence tests

Failing tests first (rule 18). The SystemCapabilities DTO + CLI hal->DTO mapping
compile with incomplete first-cut bodies so assertions fail:
- control: gpl-codecs -> GPL (resolve still returns LGPL-clean); route 404 (not
  yet registered) for the viewer-200 + bad-bearer-401 cases.
- cli: software backends available on the default build (no backends yet); a
  resolved adapter classifies the compositor (class still None).

RED evidence:
  control: FAILED. 3 passed; 3 failed (gpl_codecs.., viewer_reads.., a_bad_bearer..)
  cli:     FAILED. 1 passed; 2 failed (software_backends.., a_resolved_adapter..)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(control,cli): GET /api/v1/system/capabilities + honest build-capability DTO (ADR-W030)

Implement the honest default-build capability + licence surface (task #9/#176,
mgmt-completeness §2.7) to green.

multiview-control:
- SystemCapabilities DTO (system.rs): backends (codec decode/encode + software
  composite), compositor class/device/driver, build{effective_license,
  redistributable,features,ndi}, optional NDI attribution. Primitives/enums only
  — control keeps zero dep on multiview-hal.
- BuildInfo::resolve pins the compliance mapping (§G/ADR-0012): gpl-codecs -> GPL,
  else LGPL-clean; every shippable build redistributable. EffectiveLicense
  serializes to the exact strings 'LGPL-clean' / 'GPL'.
- GET /api/v1/system/capabilities route (routes/system.rs), viewer-read,
  system-global (no BOLA axis); registered in routes/mod + openapi.
- AppState::with_capabilities — a static startup snapshot, no engine channel
  (invariant #10); default is the coarse software-only surface.

multiview-cli:
- system_capabilities.rs maps hal::probe + cfg!(feature) + the resolved
  AdapterReport onto the DTO; graceful (absent backend -> available:false, never
  panics). Composite-stage GPU backends are NOT emitted as available:false rows
  (hal has no env probe for them) — the compositor field is authoritative.
- control.rs wires .with_capabilities(...) at startup (adapter read only under
  the gpu feature).

The SA-1+ vendor-caps deep probe (per-codec profiles, NVENC sessions, VRAM, PSI,
per-backend compiled_in) is the separate tracked lane #180.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web,control): CapabilitiesPage at /system/capabilities + regen OpenAPI/client (ADR-W030)

Wire the web surface for GET /api/v1/system/capabilities and regenerate the
committed spec + generated client.

web:
- api/system.ts + api/systemQueries.ts: getSystemCapabilities + the
  useSystemCapabilities() TanStack hook (static startup snapshot; not polled).
- pages/CapabilitiesPage.tsx (/system/capabilities): three panels — Backends
  matrix, Compositor tier, Build & licence (effective licence, redistributable,
  compiled features, NDI attribution). Status by value + label, never colour
  alone (WCAG 1.4.1). Nav entry + route added.
- pages/CapabilitiesPage.test.tsx: MSW render test (availability, compositor
  class, exact licence string, NDI attribution shown iff the ndi feature is on).

control: rename the handler capabilities -> system_capabilities so its OpenAPI
operationId no longer collides with preview::capabilities in the generated client.

regen: docs/api/openapi.json + web/src/api/schema.ts.

Web gate green: eslint 0-warnings, tsc --noEmit, lingui compile + vite build,
vitest (CapabilitiesPage 2/2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* i18n(web): extract lingui catalogs for the CapabilitiesPage strings

Adds the CapabilitiesPage + nav <Trans> strings to the source and locale
catalogs (en/ar/pseudo) so the i18n catalog-freshness CI gate passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* i18n(web): compile lingui catalogs for the CapabilitiesPage strings

Run the gate's canonical i18n:extract --clean + i18n:compile and commit the
regenerated compiled catalogs (en/ar messages.ts) so the i18n catalog-freshness
CI gate passes (the production macro strips the source fallback, so compiled
catalogs must match the .po sources).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(cli): cfg-gate system-capabilities licence assertions per build profile

Codex review (rule 16) on PR #263: the compile-time-resolver test
unconditionally asserted `LGPL-clean` + no-NDI-attribution, but
`resolve_system_capabilities` correctly returns `GPL` under
`--features gpl-codecs` and includes attribution under `--features ndi`.
The test therefore would FAIL for exactly the compliance-sensitive
profiles it claims to validate, passing on default CI only because CI
builds default features — a mis-scoped test.

Gate the expectations on the profile the test binary is compiled with
(`GPL` when `gpl-codecs` else `LGPL-clean`; attribution present when
`ndi` else absent), mirroring the `cfg!` bridge inside the resolver.
This strengthens coverage (the GPL/NDI profiles are now asserted, not
skipped) rather than weakening it; the exhaustive per-combo mapping is
already runtime-tested in `multiview-control` via `BuildInfo::resolve`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
aperim-agent added a commit that referenced this pull request Jul 12, 2026
* docs(adr): ADR-M014 — SA-1+ vendor-caps deep probe design (#180)

Design record for task #180 (the SA-1+ deep-probe half deferred from
#9/#176 by ADR-W030). Captures the load-bearing findings:

- surfacing-not-probe reframe: ADR-0017 DeviceLoad already probes the
  live half via the runtime-loaded NVML poller; the new work is the
  static vendor L2 caps EnvProbe defers + a host block;
- the inv#10 static-in-snapshot vs live-on-telemetry boundary (VRAM
  total / NVENC session-cap ceiling / engine topology / codec profiles /
  cgroup+PSI presence in the caps snapshot; live gauges stay on the
  telemetry stream, excluded — folding them in forces per-request
  probing or a stale-snapshot lie);
- additive-only DTO extension keeping control's zero-hal-dep boundary;
- hal caps.rs (VendorCaps + VendorCapsProbe seam) + host.rs API;
- the #180-A (GPU-free-completable, ships post-#271 as one fanned chain)
  vs #180-B (double-gated rule-6 seam + rule-26 GPU runners, grouped
  #198) decomposition;
- the red-first TDD plan and the LGPL-clean/deny-clean licence posture.

Status Proposed — flips to Accepted when the #180-A code lands (repo #97
pattern). Docs-only; no code, no dependency change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(adr): ADR-M014 revision — address 7 Codex cross-vendor findings (#180)

Cross-vendor design review (Codex gpt-5.6-sol) returned BLOCKED with 7 valid
findings against the A/B boundary and the honesty claims. Revised on the same
lane; verified the exact DeviceLoad fields firsthand against
multiview-hal/src/load.rs before redrawing the line.

BLOCKING (A/B-boundary contradictions):
1. rule-6 seam contradiction — caps.rs (`VendorCaps` + `VendorCapsProbe` +
   call site + ALL its tests) is now ENTIRELY #180-B, introduced atomically
   with >=1 real hardware-validated impl. #180-A has NO VendorCapsProbe and no
   mock-probe test (only host.rs + surface-existing DeviceLoad). Scrubbed
   the §5 territory table (LANE-ENG-A host.rs vs LANE-ENG-B caps.rs) and the
   §6 TDD plan.
2. rule-26 over-claim — cited the exact static slice DeviceLoad produces today
   (DeviceId {vendor, stable_id, pci_bus_id} + vram_total_bytes, load.rs:220-250)
   and restricted #180-A to precisely those; moved model/driver/engine-topology/
   session-cap-ceiling/per-codec (all needing a never-exercised vendor query) to
   #180-B's GPU-runner gate. Footnoted the one nuance (device name is already
   read by the sibling device_perf NVML call) and kept it in #180-B with its
   per-device cohort.

SHOULD-FIX (accuracy):
3. "byte-compatible" was false — now "backward-compatible, not byte-identical"
   (host/detection/observed_at add keys even on a software build; old clients
   ignore unknown fields). Fixed the §6 control test to assert additive
   backward-compat, not byte-identity.
4. Licensing/default-build — named the exact off-by-default gates
   (cuda/vaapi/qsv/videotoolbox); NVML runtime-loaded via nvml-wrapper; deny is
   re-run on the real dep change, not asserted prospectively.

SHOULD-ADDRESS (completeness/honesty):
5. inv#10 at CONSTRUCTION — stated the one-way construction boundary: caps
   assembled once at startup off the output-clock thread from a non-blocking
   LoadSource::poll() copy with immediate absent-fallback; no engine lock/channel
   borrowed; engine start/output never awaits caps assembly.
6. rule-27 provenance — added an `observed_at` startup timestamp; the wire
   contract frames every caps field as a fact-as-observed-at-startup
   (known-vs-unknown), never current-vs-stale.
7. detection{} granularity — replaced 3 global booleans with a `ProbeStatus`
   enum {not_attempted|succeeded|unsupported|failed}: L1/L3 host-global, L2 rides
   each device's `caps`; `psi: ProbeStatus` and `thermal_sensors: Option<Vec>`
   distinguish confirmed-absence from probe-failure.

Also updated the docs/decisions/README.md M014 index line to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(adr): ADR-M014 — mechanize inv#10 construction boundary + observed_at always-serialized (#180)

Second cross-vendor re-review (Codex-confirmed 6/7 resolved) left two scoped
items before merge; both fixed on the same lane.

1. Finding #5 (inv#10 at CONSTRUCTION) — was asserted ("off the output-clock
   thread", "engine never awaits") without the mechanism. §1 now states it
   concretely, verified firsthand against the code:
   - WHERE: caps assembly extends the same one-shot AppState::with_capabilities
     seam W030 fills (multiview-cli/src/control.rs) — on the CLI control-plane
     bring-up (Tokio control-IO plane), which by ADR-0009's two-plane split is
     NOT the output-clock data-plane OS thread.
   - WHAT: the single LoadSource::poll() pass is a BOUNDED VENDOR QUERY (an
     NVML/DRM sample_all() pass, per-pass-bounded by PollInterval, via the
     default_load_source() the system-metrics task already selects;
     load.rs:657/:705) — not a cached read, not a per-request probe. Corrected
     the imprecise "non-blocking" wording (an NVML call is bounded, not
     non-blocking) here, in Consequences, and in the README index line.
   - ORDERING: the engine OutputClock runs on its own dedicated data-plane OS
     threads and neither awaits nor is awaited by caps assembly; caps borrows no
     engine lock/channel/poller (polls its OWN LoadSource) → structurally
     incapable of coupling to the engine. Absent-fallback = honest absent state,
     not "not-yet-ready".
   Added an Alternatives row (assert-outcome-without-mechanism, rejected) and a
   §6 cli test that exercises the one-shot own-source boundary.

2. NEW: observed_at must not use skip_serializing_if. §1 makes it the always-
   present provenance anchor, but §2 lumped it with the optional/defaulted
   fields. Now exempted: observed_at is ALWAYS serialized (a required OpenAPI
   field, stamped on every snapshot incl. the absent-fallback); devices/host/
   detection stay optional/defaulted. Added the §6 control-test assertion
   (observed_at present even with empty devices + None host) and an Alternatives
   row.

Verified firsthand: crates/multiview-hal/src/load.rs (LoadPoller::poll →
probe.sample_all, trait doc "bounded vendor query"), multiview-cli/src/
system_metrics.rs (default_load_source, spawn = tokio task), multiview-cli/src/
control.rs:279 (with_capabilities one-shot). All doc links resolve
(added ADR-0009).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant