diff --git a/ROADMAP.md b/ROADMAP.md
index 15348b7..fe036d8 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -3,29 +3,25 @@
## === LONG TERM ====
### 1. RetroArch Shader Support
-Verify compatibility with RetroArch shader presets
+Ongoing compatibility with RetroArch shader presets (currently 89% — 1702/1906 presets compile)
-**Rule**: Fix issues in our filter chain system (`src/render/filter_chain/`), not in shader files themselves
+**Rule**: Fix issues in our filter chain system (`filter-chain/`), not in shader files themselves
**Reference**: https://github.com/libretro/slang-shaders/blob/master/spec/SHADER_SPEC.md
-- [ ] (Known issues to be populated as discovered)
-
### 2. Latency Optimization
Minimize end-to-end latency while maintaining code quality
-**Example Areas** (tasks uncertain, use Tracy for measurement):
-- Compositor frame delivery latency
-- DMA-BUF import overhead
-- Shader pass execution time
-- Uniform buffer updates
-- Render pass barriers/transitions
-- Queue submission overhead
-- CPU-GPU synchronization points
+**Instrumented** (measurement infra in place): compositor latency tracking, GPU timestamps per pass, Tracy CPU zones, frame pacing
+
+**Known Issues**:
+
+- [ ] `device.waitIdle()` on every frame import (`external_frame_importer.cpp`) — full GPU pipeline stall. Replace with queue-level waits or timeline semaphores
+- [ ] No Presentation Time protocol feedback loop for monitor-sync frame pacing
## === Phase 1: Fundamental Infrastructure & Compositor Frame Delivery ===
-This roadmap covers core infrastructure work focused on establishing robust compositor-based frame capture and shader processing capabilities.
+Core infrastructure for robust compositor-based frame capture and shader processing.
---
@@ -34,47 +30,33 @@ Prevent regressions in filter chain when adding new features
- [x] Catch SPIR-V compilation errors early
- [x] Report shader compilation failures with diagnostics
-- [ ] Golden image generation for reference outputs
-- [ ] Comparison against golden images (pixel-by-pixel or perceptual)
-- [ ] Automated regression detection for various shader presets
-- [ ] Automated test runner for shader validation
+- [x] Golden image generation for reference outputs
+- [x] Comparison against golden images (pixel-by-pixel and SSIM)
+- [x] Automated regression detection for shader presets (14 curated upstream presets)
+- [x] Automated test runner for shader validation
+- [ ] Enable visual tests in CI — remove `DISABLED` guard in `tests/CMakeLists.txt`, set `GOGGLES_INCLUDE_VISUAL_TESTS=1` in CI lane, enable Git LFS in checkout
### 2. Tracy Profiling Improvements
- [ ] Add Tracy GPU profiling support (Vulkan)
- [x] Single-process Tracy timeline profiling, [context](https://github.com/wolfpld/tracy/issues/822)
-### 3. Error Traceback Integration
-
-- [ ] Integrate cpptrace for stack traces on errors
-- [ ] Hook into existing error handling (`tl::expected`)
-- [ ] Configure for debug/release builds
-
---
-### 4. Compositor Protocol Completeness
+### 3. Compositor Protocol Completeness
Extend nested compositor to support broader app compatibility
-**Current State**: Headless wlroots compositor with XDG Shell, XWayland, basic input
+**Current State**: Headless wlroots compositor with XDG Shell, XWayland, Layer Shell, pointer constraints, relative pointer, DMA-BUF explicit sync
**Missing Capabilities** (blocking specific app types):
-- [ ] **Layer Shell** (`wlr_layer_shell_v1`) - Game launcher overlays (Steam, Epic), desktop panels
+- [x] **Layer Shell** (`wlr_layer_shell_v1`) - Game launcher overlays (Steam, Epic), desktop panels
- [ ] **Presentation Time** (`wlr_presentation_time`) - Frame pacing, tear-free presentation, VRR
- [ ] **Data Device** (`wl_data_device_manager`) - Clipboard and drag-and-drop for launchers
-- [ ] **DRM Lease** (`wlr_drm_lease_v1`) - VR applications (SteamVR)
- [ ] **Idle Inhibit** (`zwlr_idle_inhibit_v1`) - Prevent screensaver during video playback
-- [ ] **Touch Input** (`wlr_touch`) - Mobile/touchscreen game ports
-- [ ] **Text Input** (`zwlr_text_input_v3`) - IME support for CJK languages
**Nice-to-Have Enhancements**:
-- [ ] **Primary Selection** (`zwlr_primary_selection_v1`) - Middle-click paste
-- [ ] **Output Management** (`wlr_output_manager_v1`) - Multi-monitor display configuration
-- [ ] **Fractional Scaling** (`wp_fractional_scale_v1`) - HiDPI text rendering
-- [ ] **Tablet/Stylus** (`wlr_tablet_tool`) - Drawing applications
-- [ ] **Session Lock** (`ext_session_lock_manager_v1`) - Screen locker support
-- [ ] **Gamma Control** (`wlr_gamma_control_manager`) - Color management
- [ ] **xdg-activation** - Window focus tokens for multi-window launchers
- [ ] **Keyboard Shortcuts Inhibit** (`zwp_keyboard_shortcuts_inhibit_v1`) - Global hotkeys
- [ ] **Tearing Control** (`wp_tearing_control_v1`) - Reduced latency mode
diff --git a/docs/policies/boundary.md b/docs/policies/boundary.md
new file mode 100644
index 0000000..2f84c2b
--- /dev/null
+++ b/docs/policies/boundary.md
@@ -0,0 +1,119 @@
+# Boundary Layer
+
+Contracts at module interfaces — what data crosses, who owns it, what the receiver may assume.
+
+**Governing principles:** Data flows down, events flow up (#1). Ownership is in the types (#3). Napkin dependency graph (#2).
+
+---
+
+### BOUND.modules.001 — No forwarding accessors
+
+**Guard:** Adding a public method that returns a mutable or const reference to an owned member, or a method that delegates to an owned member without adding logic.
+**Safety:** A type must not expose its internal components via accessor methods that let callers reach through to manipulate internals. If callers need functionality from an internal component, either: (a) the owning type provides a method that encapsulates the operation, or (b) the internal component should be a sibling, not a child — restructure the ownership tree so both are independently accessible.
+
+
+
+**Violation:** Callers couple to internal structure. Refactoring the owner's internals breaks all callers. The owner becomes a facade that adds no value — just an indirection layer.
+**Escapes:** Static tools cannot distinguish a legitimate accessor (exposing a stable interface) from a leaky abstraction (exposing implementation details). The test is: would callers break if the internal type changed? If yes, it's leaky.
+**Locked:** No `component()` style accessors that return references to owned members. No pass-through methods that add no logic.
+**Free:** Accessors for value-type properties (e.g., `width()`, `format()`). Methods that genuinely encapsulate multi-step operations on internals.
+
+
+
+### BOUND.modules.002 — Typed events, not callbacks
+
+**Guard:** Adding inter-module communication via `std::function` callbacks, callback registration methods (`set_*_callback`), or lambda wiring.
+**Safety:** Subsystems communicate through typed event structs emitted on the source object. Listeners hold RAII connection objects that auto-disconnect on destruction. Events are type-safe (subscribe to a specific struct type, not a string or enum). No `std::function` member variables for inter-module communication. No callback registration methods.
+
+
+
+**Violation:** Callback spaghetti — a mediator registers N callbacks between N subsystems, creating hidden coupling. Callback lifetimes are manual (dangling callbacks if listener is destroyed). Adding a new event type requires modifying the mediator.
+**Escapes:** Static tools cannot distinguish a callback used for internal strategy (fine) from one used for inter-module communication (bad). The distinction is scope: callbacks within a single module are acceptable; callbacks that cross module boundaries should be typed events.
+**Locked:** Typed event structs for cross-module communication. RAII connections for listener lifecycle. No `set_*_callback` registration patterns for inter-module communication.
+**Free:** Internal use of callbacks within a module (e.g., lambda passed to an algorithm). Event struct field design. Whether events are emitted synchronously or queued.
+
+
+
+### BOUND.modules.003 — No cross-module type leakage
+
+**Guard:** Importing types from one module's namespace into another module's public interface.
+**Safety:** A module's public interface must not reference types from peer modules. Boundary types shared across modules must live in a shared location (e.g., `util/`). If module A needs to display information from module B, B defines a snapshot/DTO type in a shared location, not in B's internal namespace.
+
+
+
+**Violation:** UI module imports compositor types directly — changes to compositor internals break the UI. Circular dependencies between modules. "Include what you use" becomes "include everything."
+**Escapes:** Static tools can detect circular includes but cannot judge whether a cross-module type reference is appropriate or leaky. The judgment is: does the referenced type belong to the module's public contract, or is it an internal type that happens to be accessible?
+**Locked:** No peer-module type references in public interfaces. Shared types live in shared locations.
+**Free:** Internal use of types within a module. Which types are shared vs module-internal.
+
+
+
+### BOUND.render.001 — DMA-BUF frame ownership transfer
+
+**Guard:** Modifying `ExternalImageFrame`, its production in the compositor, or its consumption in the render pipeline.
+**Safety:** DMA-BUF handle ownership transfers via `UniqueFd::dup()` — producer retains its copy, consumer owns the duplicate. `sync_fd` (if present) transfers with the frame via the same dup mechanism. Receiver must not assume `sync_fd` is always present. Single-plane DMA-BUF only.
+
+
+
+**Violation:** Double-close on DMA-BUF fd (shared instead of duplicated). Vulkan import fails on multi-plane buffer.
+**Escapes:** Ownership transfer is semantic — no tool verifies that `dup()` is used rather than raw fd copy.
+**Locked:** Ownership model (dup, not transfer). Single-plane requirement.
+**Free:** Adding fields to the frame type. Internal rendering that produces the buffer.
+
+
+
+### BOUND.render.002 — Vulkan result checking beyond semgrep scope
+
+**Guard:** Adding or modifying Vulkan API calls that return `vk::Result`.
+**Safety:** All `vk::Result` returns must be checked explicitly. Use `VK_TRY(call, code, msg)` for propagation. Semgrep only catches `static_cast(waitIdle())` — all other unchecked results escape the hard gate.
+
+
+
+**Violation:** Vulkan call silently fails; subsequent operations use invalid state.
+**Escapes:** Semgrep rule only matches one specific pattern. General result checking is semantic.
+**Locked:** Explicit result checking for all Vulkan calls. `VK_TRY` as the standard propagation mechanism.
+**Free:** Error codes and messages. Whether cleanup paths log or propagate.
+
+
+
+### BOUND.render.003 — Vulkan destruction ordering
+
+**Guard:** Modifying Vulkan object destruction logic. Adding new Vulkan objects.
+**Safety:** Vulkan objects must be destroyed in dependency order. GPU idle or fence-waited before destroying in-use objects. Destruction order should follow from the ownership tree (principle #5), not from manual sequencing.
+
+
+
+**Violation:** Validation layer error on destroying a referenced object. GPU crash from destroying a pipeline while in use.
+**Escapes:** Semgrep enforces manual destruction (no RAII wrappers), but cannot verify destruction ORDER.
+**Locked:** Dependency-ordered destruction. GPU idle before destroying in-use objects.
+**Free:** Which specific Vulkan objects exist. Internal organization of destroy calls.
+
+
+
+### BOUND.code.001 — Comment and documentation rules
+
+**Guard:** Adding or modifying comments or Doxygen docstrings in any C++ source file.
+**Safety:** Comments must explain non-obvious why, constraints, workarounds, or invariants. Comments must NOT narrate obvious what, provide step-by-step tutorials, or include LLM-verbose justifications. Doxygen `///` is required only when the declaration alone is insufficient.
+
+
+
+**Violation:** Codebase bloated with narration comments that obscure real invariant documentation.
+**Escapes:** No static tool can distinguish useful constraint comments from narration.
+**Locked:** "Why, not what" principle. Doxygen restricted to declarations where the type signature is insufficient.
+**Free:** Comment phrasing. Whether a particular line needs a comment.
+
+
+
+### BOUND.headers.001 — Pragma once in headers
+
+**Guard:** Creating new header files (`.hpp`, `.h`) in `src/` or `tests/`.
+**Safety:** All headers must use `#pragma once`. No `#ifndef`/`#define`/`#endif` include guards.
+
+
+
+**Violation:** Double-inclusion causing redefinition errors.
+**Escapes:** No hard gate enforces `#pragma once` presence.
+**Locked:** `#pragma once` as the sole include guard mechanism.
+**Free:** Header file naming. Header content beyond the guard.
+
+
diff --git a/docs/policies/liveness.md b/docs/policies/liveness.md
new file mode 100644
index 0000000..a02ba26
--- /dev/null
+++ b/docs/policies/liveness.md
@@ -0,0 +1,53 @@
+# Liveness Layer
+
+Capabilities that must remain reachable. Pipeline must not stall. Deadlock-freedom.
+
+**Governing principles:** Lifecycle follows the dependency graph (#5). No mediator objects (#4).
+
+---
+
+### LIVE.render.001 — Shader pipeline recordable during hot-reload
+
+**Guard:** Modifying shader hot-reload, pipeline slot management, or the active/pending swap logic.
+**Safety:** The active shader pipeline must always be recordable (or empty). Async compilation must never block the render loop. Failed compilation must not corrupt the active pipeline. Render loop calls `record()` every frame — this must complete in bounded time.
+
+
+
+**Liveness:** Render loop never blocks on compilation. Failed reload preserves previous pipeline. Retired pipelines outlive in-flight GPU work.
+**Violation:** Render stall during compile. Black frame after failed reload. Use-after-free on retired pipeline referenced by in-flight frame.
+**Escapes:** "Active always valid" depends on swap logic never removing active without a ready replacement. This is control flow. The retire delay must exceed frames in flight — a numeric relationship not checkable by tools.
+**Locked:** Non-blocking compilation. Retire delay before destruction. Failed reload preserves active.
+**Free:** Compilation mechanism. Retire delay value. Number of retired slots.
+**Related:** RES.render.003
+
+
+
+### LIVE.render.002 — Frame pipeline must advance
+
+**Guard:** Modifying the acquire → record → submit → present cycle.
+**Safety:** Frame slots are independent — no circular dependency between them. Each slot has its own fence, semaphore, and command buffer. Round-robin advancement ensures no slot waits on another slot's data (beyond the GPU fence signal). At least one present mode must be selectable (FIFO is mandatory per Vulkan spec).
+
+
+
+**Liveness:** No circular fence dependency. No deadlock from frame pipeline stall.
+**Violation:** Deadlock from circular wait. Black screen from pipeline stall.
+**Escapes:** Circular dependency freedom depends on the round-robin design. A refactor adding cross-slot data dependency could introduce a cycle. No tool verifies deadlock-freedom of a frame pipeline.
+**Locked:** Independent frame slots. Round-robin advancement. FIFO as fallback present mode.
+**Free:** Number of frame slots. Present mode preference. FPS pacing mechanism.
+
+
+
+### LIVE.compositor.001 — Event loop terminable
+
+**Guard:** Modifying compositor thread lifecycle, the event loop, or shutdown logic.
+**Safety:** `wl_display_terminate()` causes the event loop to return. Thread join must not deadlock. The compositor thread must not block indefinitely on any operation that prevents it from checking the terminate flag. Shutdown must complete in bounded time.
+
+
+
+**Liveness:** Compositor always terminable. Application shutdown completes in bounded time.
+**Violation:** Application hangs on shutdown — thread never joins.
+**Escapes:** If the compositor thread blocks in a long operation that doesn't return to the event loop, it won't see the terminate flag. No tool verifies all code paths return to dispatch.
+**Locked:** `wl_display_terminate()` as shutdown signal. Thread join in destructor. No infinite blocking in compositor thread.
+**Free:** Event loop iteration frequency. Timer sources. Work done per iteration.
+
+
diff --git a/docs/policies/process.md b/docs/policies/process.md
new file mode 100644
index 0000000..0cc3210
--- /dev/null
+++ b/docs/policies/process.md
@@ -0,0 +1,63 @@
+# Process Layer
+
+Invariants spanning the entire process lifetime.
+
+**Governing principles:** Lifecycle follows the dependency graph (#5). No mediator objects (#4).
+
+---
+
+### PROC.app.001 — Lifecycle follows the ownership tree
+
+**Guard:** Adding or modifying subsystem creation, destruction, or the dependency relationships between subsystems.
+**Safety:** Subsystem lifetime must be determined by ownership structure, not by manual ordering. If A depends on B, then B must be a member or constructor parameter of A — so B is constructed first and destroyed last automatically. No manually-maintained init/shutdown sequences. No `reset()` calls in a specific order.
+
+
+
+**Violation:** Use-after-free from destroying a dependency before its dependent. Deadlock from shutdown ordering mistakes. Fragile init sequences that break when a new subsystem is added.
+**Escapes:** Static tools verify individual lifetimes but cannot verify that the ownership tree correctly encodes all runtime dependencies. A subsystem could borrow a pointer at construction that outlives the pointee if the tree is structured wrong.
+**Locked:** Ownership tree as the sole mechanism for lifecycle ordering. No manual init/shutdown sequences.
+**Free:** Which subsystems exist. Internal init logic within each subsystem. Constructor parameter choices.
+
+
+
+### PROC.app.002 — Thread ownership is structural
+
+**Guard:** Modifying which thread creates, accesses, or destroys resources. Adding new thread-affine operations. Introducing new cross-thread communication.
+**Safety:** Each thread owns a set of types that cannot be accessed from other threads. Main thread owns: SDL window, Vulkan submission, ImGui, render loop. Compositor thread owns: wlroots event loop, all `wlr_*` objects, Wayland protocol handling. JobSystem threads own: async shader compilation only (no Vulkan submission, no wlroots access). Cross-thread communication uses typed message channels, not shared mutable state protected by convention.
+
+
+
+**Violation:** Data race on wlroots objects from main thread. Vulkan submission from compositor thread. Callback registered on one thread executing on another without synchronization.
+**Escapes:** Thread affinity is a runtime property. No static tool can verify that a function is only called from a specific thread. The goal is to make thread boundaries visible in the architecture (separate types per thread, messages at boundaries), not just documented in comments.
+**Locked:** Main thread as sole Vulkan submitter. Compositor thread as sole wlroots accessor. Communication through typed channels only.
+**Free:** Which specific types each thread owns. Internal threading within a subsystem (if fully encapsulated).
+
+
+
+### PROC.app.003 — Global mutable state restriction
+
+**Guard:** Introducing new global mutable state (static variables, process-wide singletons).
+**Safety:** Only two global mutable singletons are permitted: the spdlog logger and `JobSystem`. No other global mutable state may be introduced. Runtime settings flow through constructor parameters or typed message channels, not through globals.
+
+
+
+**Violation:** Hidden coupling between modules via shared global state. Init-order fiasco on static constructors. Untestable code due to global side effects.
+**Escapes:** Static tools cannot distinguish a benign `static const` from a mutable `static` singleton. The constraint is on design intent, not syntax.
+**Locked:** The set of permitted globals: logger and JobSystem only. Configuration propagation through constructor parameters or message channels, not globals.
+**Free:** Logger configuration. JobSystem thread pool size. Module-local `static constexpr` values.
+
+
+
+### PROC.app.004 — No mediator objects
+
+**Guard:** Adding a class that owns multiple subsystems and wires them together with callbacks, forwarding methods, or bidirectional references.
+**Safety:** No class may exist whose primary responsibility is "know about all subsystems and connect them." Subsystems communicate through typed events or direct narrow interfaces. `main()` (or a composition root) constructs the dependency tree, but does not mediate runtime communication. If two subsystems need to interact at runtime, they hold a typed interface to each other — not a callback registered by a third party.
+
+
+
+**Violation:** God object that grows without bound as subsystems are added. Callback spaghetti where the mediator registers N callbacks between N subsystems. Changes to any subsystem require changes to the mediator.
+**Escapes:** No static tool can distinguish a legitimate composition root (constructs objects, then steps aside) from a mediator (constructs objects AND manages their runtime communication). The distinction is behavioral: does the class participate in runtime data flow, or only in construction?
+**Locked:** No runtime mediator. Composition root constructs the tree only. Subsystem-to-subsystem communication is direct (typed events or narrow interfaces).
+**Free:** How the composition root is structured. Whether it's `main()` or a builder function. Constructor parameter wiring.
+
+
diff --git a/docs/policies/resource.md b/docs/policies/resource.md
new file mode 100644
index 0000000..3bee174
--- /dev/null
+++ b/docs/policies/resource.md
@@ -0,0 +1,65 @@
+# Resource Layer
+
+Lifecycle of owned objects — creation, valid usage window, destruction ordering.
+
+**Governing principles:** Ownership is in the types (#3). Lifecycle follows the dependency graph (#5).
+
+---
+
+### RES.render.001 — Swapchain recreation lifecycle
+
+**Guard:** Modifying swapchain creation, recreation, or destruction logic.
+**Safety:** Before recreation: all in-flight GPU work must complete (fence-waited). After creation: image, view, and semaphore counts must match. Partial creation must roll back. Swapchain resources should be owned by a single type whose destructor handles cleanup — no manual destroy sequencing across multiple call sites.
+
+
+
+**Violation:** Validation layer error on destroyed swapchain image access. Crash from mismatched arrays. GPU hang from destroying in-flight resources.
+**Escapes:** Array-size invariant is maintained by construction but nothing prevents a future change from breaking the count relationship. The fence-wait precondition is a calling convention, not enforced by types.
+**Locked:** Fence-wait before recreation. 1:1:1 images/views/semaphores. Atomic creation with rollback.
+**Free:** Format selection. Present mode. Image count.
+
+
+
+### RES.render.002 — DMA-BUF import lifecycle
+
+**Guard:** Modifying DMA-BUF import logic or the external frame import sequence.
+**Safety:** Import sequence: GPU idle → destroy old resources → create new resources. Any partial failure must clean up all already-created objects. The fd passed to Vulkan is consumed on success (`release()` after successful import). The import type should own all related Vulkan objects (image, memory, view) so destruction is automatic.
+
+
+
+**Violation:** Leaked Vulkan resources on failed import. Double-close of fd if `release()` not called.
+**Escapes:** Cleanup-on-partial-failure is control flow, not a pattern. Vulkan's fd ownership on import success is a spec-level requirement not expressible in types.
+**Locked:** GPU idle before reimport. `dup()` for import fd. `release()` after successful import. Cleanup on every error path.
+**Free:** Format negotiation. Memory type selection.
+
+
+
+### RES.render.003 — Shader pipeline hot-reload
+
+**Guard:** Modifying shader hot-reload, pipeline slot management, or async compilation integration.
+**Safety:** The active shader pipeline must always be recordable (or empty — rendering skips the shader pass). Async compilation must not block the render loop. Failed compilation must not corrupt the active pipeline. Retired pipelines must outlive any in-flight GPU work that references them. The hot-reload mechanism should use continuation-based async (not an ad-hoc active/pending/retired state machine with manual frame counting).
+
+
+
+**Liveness:** Render loop must never block waiting for shader compilation. Failed reload preserves previous pipeline.
+**Violation:** Render stall during compile. Black frame after failed reload. Use-after-free on retired pipeline.
+**Escapes:** "Active always valid" depends on swap logic never removing the active pipeline without a replacement. This is control flow. Retire delay correctness requires knowing the frames-in-flight count.
+**Locked:** Non-blocking async compilation. Retire delay exceeds frames in flight. Failed reload preserves active pipeline.
+**Free:** Compilation parameters. Async mechanism (futures, continuations, job system). Retire delay value.
+**Related:** LIVE.render.001
+
+
+
+### RES.compositor.001 — Surface listener lifecycle
+
+**Guard:** Modifying surface creation, mapping, or destruction handlers. Adding new wlroots listeners.
+**Safety:** wlroots listeners must be attached during surface creation and detached before the owning object is destroyed. Listener-to-owner binding should be structural — the listener is a member of the owner, so detachment in the destructor is automatic. No manual "remember to detach before erase" protocols.
+
+
+
+**Violation:** Use-after-free when wlroots fires a signal on a destroyed listener. Memory leak from undetached listeners.
+**Escapes:** wlroots listeners are C-level `wl_list` entries with no type-level binding to ownership. The detach requirement is a manual protocol.
+**Locked:** Listeners detached before owner destruction. One owner object per surface.
+**Free:** Which signals are listened to. Internal handling within callbacks.
+
+
diff --git a/docs/policies/sync.md b/docs/policies/sync.md
new file mode 100644
index 0000000..e42248f
--- /dev/null
+++ b/docs/policies/sync.md
@@ -0,0 +1,63 @@
+# Sync Layer
+
+Temporal ordering between threads and GPU work — fences, semaphores, mutexes, memory ordering.
+
+**Governing principles:** Thread ownership is structural (#2 from Process). Data flows down, events flow up (#1).
+
+---
+
+### SYNC.render.001 — DMA-BUF sync fd semaphore lifecycle
+
+**Guard:** Modifying DMA-BUF sync fd handling, semaphore import/retire, or the wait semaphore array in the submission path.
+**Safety:** `sync_fd` imported as `vk::SemaphoreImportFlagBits::eTemporary` into a per-frame-slot semaphore. Semaphore retired (destroyed) before a new one is imported into the same slot. Import fd released after successful Vulkan import. Semaphore is single-use: retired after the submit that waits on it.
+
+
+
+**Violation:** GPU race between compositor render and Vulkan import. Semaphore leak from overwritten slot. Double-close of sync fd.
+**Escapes:** Semaphore lifecycle spread across prepare, submit, and retire call sites. No tool tracks that a semaphore is retired after exactly one use. The `eTemporary` flag is a Vulkan spec requirement — wrong flag silently produces incorrect sync.
+**Locked:** `eTemporary` import flag. Per-slot retirement before re-import. Single-use lifecycle. `dup()` before import, `release()` after.
+**Free:** Wait pipeline stage. Graceful handling of absent `sync_fd`.
+
+
+
+### SYNC.render.002 — Frame fence before acquire
+
+**Guard:** Modifying frame acquisition, submission, or the frame slot cycle.
+**Safety:** Before acquiring the next frame: wait on the current slot's fence. After successful wait and acquire: reset the fence. Fence signaled by submit. This ensures per-frame resources are not in use when reused.
+
+
+
+**Violation:** Command buffer recorded while previous submission executes. GPU corruption from concurrent writes.
+**Escapes:** Fence-before-acquire is a calling convention within a single function. A refactor could separate the calls.
+**Locked:** `waitForFences` before acquire. `resetFences` after wait. Fence signaled by submit.
+**Free:** Fence timeout. Handling of out-of-date swapchain.
+
+
+
+### SYNC.compositor-main.001 — Cross-thread frame handoff
+
+**Guard:** Modifying how frames cross from the compositor thread to the main thread.
+**Safety:** Frame data must be fully copied (including fd duplication) under a single lock acquisition. No reference to compositor-owned data may escape the lock. The handoff mechanism should be a typed message channel, not raw mutex + shared mutable state. The channel type enforces the copy-under-lock invariant structurally.
+
+
+
+**Violation:** Data race on frame fields. Torn read of partially updated frame. Use-after-free from reference escaping the lock.
+**Escapes:** Mutex correctness is a runtime property. No tool verifies that every access path holds the lock. A typed channel makes this structural rather than conventional.
+**Locked:** Full copy under single lock. fd duplication inside the lock. No reference escapes.
+**Free:** Channel implementation. Frame data fields. Frame numbering scheme.
+
+
+
+### SYNC.compositor-main.002 — Cross-thread control channel
+
+**Guard:** Modifying how control messages (input events, configuration changes) flow from the main thread to the compositor thread.
+**Safety:** Bounded-capacity, non-blocking channel. Silent drop on overflow (no deadlock risk). Polled consumption by the compositor event loop. One unified channel type per direction rather than a mix of queues, atomics, and mutexes for different message types. Scalar flags (target FPS, cursor visibility, etc.) should be fields in a control message, not separate atomics with independent memory ordering.
+
+
+
+**Violation:** Deadlock from blocking on full channel. Lost messages from unbounded growth. Data race from inconsistent memory ordering across multiple atomics.
+**Escapes:** The contract (bounded, non-blocking, polled) is a design choice not verifiable by tool. Atomic memory ordering correctness requires understanding which thread stores and which loads.
+**Locked:** Bounded capacity. Non-blocking send. Polled consumption. Unified channel rather than scattered atomics.
+**Free:** Channel capacity. Message types. Polling frequency.
+
+
diff --git a/docs/project_policies.md b/docs/project_policies.md
index 50e1bcb..18b965a 100644
--- a/docs/project_policies.md
+++ b/docs/project_policies.md
@@ -1,417 +1,468 @@
-# Goggles Project Development Policies
+# Goggles Project Policies
-**Version:** 1.3
-**Status:** Active
-**Last Updated:** 2026-03-25
+**Version:** 2.0
-This document is the authoritative, normative policy for the Goggles codebase.
+## Policy Model
-## 1. Document Status and Precedence
+Goggles uses a three-tier enforcement system. Rules escalate from hard gates (automated, blocking) to soft gates (this document, agent self-check):
-### 1.1 Scope
+| Tier | Tool | What it catches | Failure mode |
+|------|------|----------------|--------------|
+| **Hard gate** | clang-format | Layout, include ordering | CI blocks merge |
+| **Hard gate** | clang-tidy | Identifier naming, static checks, function size | CI blocks merge |
+| **Hard gate** | Semgrep | Structural bans: raw `new`/`delete`, `using namespace` in headers, banned Vulkan wrappers, `Vk*` handles in app code, `std::thread` in render path, `cout`/`cerr`/`printf`, discarded Vulkan results | CI blocks merge |
+| **Soft gate** | This document | Semantic invariants: initialization ordering, ownership transfer, lifecycle sequencing, cross-module consistency, liveness | Agent self-check before task completion |
-These policies apply to all contributors (humans and coding agents) and all code in this repository unless a section explicitly narrows scope.
+This document contains **only** invariants that escape hard gates. If a static tool can catch the violation, the rule belongs in a hard gate, not here.
-### 1.2 Normative language
+### Invariant Layers
-The keywords **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are used as defined in RFC 2119/8174.
+Every rule belongs to exactly one layer. Layers are ordered by scope, from broadest to narrowest temporal window:
-### 1.3 Precedence rules
+| Layer | Prefix | Scope |
+|-------|--------|-------|
+| **Process** | `PROC` | Entire process lifetime — init ordering, shutdown ordering, singleton guarantees, thread ownership |
+| **Boundary** | `BOUND` | Module interface contracts — what data crosses, who owns it, what the receiver may assume |
+| **Resource** | `RES` | Owned object lifecycle — creation, valid usage window, destruction ordering |
+| **Sync** | `SYNC` | Temporal ordering — fences, semaphores, mutexes, memory ordering between threads and GPU |
+| **Liveness** | `LIVE` | Reachability — pipeline must not stall, deadlock-freedom, progress guarantees |
-If rules conflict:
+### Rule Schema
-1. This document takes precedence over all other guidance files.
-2. Section text takes precedence over summaries/checklists.
-3. Narrower-scope rules (for example, compositor) take precedence within that scope.
+Every rule follows this structure:
-### 1.4 Allowed exceptions
+> ### `` — one-line description
+>
+> **Guard:** predicate — when does this rule activate (scoped to files/functions)
+> **Safety:** verifiable post-condition — what must hold after the guarded action
+>
+>
+>
+> **Liveness:** capability that must remain reachable (omit if N/A)
+> **Violation:** observable symptom when broken
+> **Escapes:** why static tools cannot catch this (mandatory)
+> **Locked:** what this rule forbids changing without review
+> **Free:** what can change without triggering this rule
+> **Related:** other rule IDs in different layers (optional)
+>
+>
-Exceptions MUST be explicit in code review and MUST include:
+ID format: `..` where scope is a module or boundary name, seq is three-digit zero-padded.
-- reason,
-- scope,
-- rollback/remediation plan.
-
-Implicit exceptions MUST NOT be used.
+- **Guard** must be a predicate over files, functions, or data structures.
+- **Safety** must be a verifiable post-condition readable from resulting code.
+- **Escapes** is mandatory — if static tools can catch it, the rule does not belong here.
+- **Locked/Free** makes the refinement boundary explicit.
---
-## 2. Core Non-Negotiables (Quick Checklist)
+## Process Layer
-1. Fallible operations MUST return `tl::expected` (or project alias).
-2. Expected runtime failures MUST NOT use exceptions.
-3. Errors MUST be handled or propagated; silent failure MUST NOT occur.
-4. Errors SHOULD be logged once at subsystem boundaries; duplicate cascading logs MUST NOT occur.
-5. App-side Vulkan code MUST use `vk::` (vulkan-hpp), not raw `Vk*` handles.
-6. Vulkan `vk::Result` returns MUST be checked explicitly (no `static_cast(...)`).
-7. Owned file descriptors MUST use `goggles::util::UniqueFd`.
-8. Code comments MUST explain non-obvious why; narration comments MUST NOT be used.
-9. Build/test workflows MUST use Pixi tasks and CMake/CTest presets.
-10. Real-time render-path work MUST use `goggles::util::JobSystem` for concurrent pipeline/render work.
+### PROC.app.001 — Subsystem init/destroy ordering
----
+**Guard:** Modifying `Application::create()`, `Application::shutdown()`, the `run_app()` function in `main.cpp`, or the initialization/destruction sequence of any top-level subsystem.
+**Safety:** Init: logger → signal FD (headless only) → SDL → VulkanBackend → ImGuiLayer → shader system → CompositorServer. Destroy: ImGuiLayer → CompositorServer (thread joins) → VulkanBackend → SDL. Compositor thread must join before VulkanBackend destruction.
+
+
+
+**Liveness:** Compositor event loop must be terminable via `wl_display_terminate()` — join must not deadlock.
+**Violation:** Use-after-free in ImGui (destroyed Vulkan handles) or compositor thread accessing freed wlroots resources. Deadlock on compositor join if terminate signal not delivered.
+**Escapes:** Static tools see individual lifetimes but not cross-object ordering of `reset()` calls in `shutdown()`. The non-reverse ordering (ImGui before compositor) is intentional but not enforceable by pattern.
+**Locked:** Relative ordering of init/destroy. ImGui must be destroyed before compositor (holds callbacks referencing compositor state). Compositor must join before VulkanBackend destruction. New subsystems must be inserted at correct position in both sequences.
+**Free:** Internal init logic within each subsystem. Parameters to `create()` factories. Whether signal FD is created (headless-only).
+
+
+
+### PROC.app.002 — Thread ownership assignments
+
+**Guard:** Modifying which thread creates, accesses, or destroys SDL, Vulkan, ImGui, or wlroots resources. Adding new thread-affine operations.
+**Safety:** Main thread owns: SDL window, SDL events, Vulkan submission (device, queues, command buffers), ImGui frame cycle, filter chain recording. Compositor thread owns: wlroots event loop, all `wlr_*` objects, Wayland protocol handling, XWayland. JobSystem threads own: async shader compilation (no Vulkan submission, no wlroots access).
+
+
-## 3. Error Handling Policy
+**Violation:** Data race on wlroots objects from main thread. Vulkan submission from compositor thread. ImGui rendered from background thread. Validation layer errors from concurrent queue access.
+**Escapes:** Thread affinity is a runtime property. No static tool can verify that a function is only called from a specific thread. The ownership assignments are enforced by code structure, not by type system.
+**Locked:** Main thread as sole Vulkan submitter. Compositor thread as sole wlroots accessor. JobSystem restricted to off-critical-path work (no Vulkan submission, no wlroots).
+**Free:** Which specific Vulkan objects main thread creates (swapchain, pipelines, etc.). Internal wlroots protocol handling within compositor thread.
-**Applies to:** app, utilities, tests
-**Enforced by:** review, clang-tidy (partial)
+
-### 3.1 Result model
+### PROC.app.003 — Global mutable state restriction
-- Fallible operations MUST return `tl::expected` (or project alias).
-- APIs returning results SHOULD be marked `[[nodiscard]]`.
-- Returning `bool`, sentinel values (`-1`, `nullptr`, `0`) for multi-cause failures MUST NOT be used.
+**Guard:** Introducing new global mutable state (static variables, process-wide singletons) or modifying the logger or JobSystem globals.
+**Safety:** Only two global mutable singletons are permitted: the spdlog logger (initialized in `main.cpp` via `initialize_logger()`) and `JobSystem` (if used). No other global mutable state may be introduced without review.
-### 3.2 Exceptions
+
-- Expected runtime failures (I/O, parsing, resource creation, device/runtime states) MUST NOT use exceptions.
-- Exceptions MAY be used for programming errors/invariant violations.
+**Violation:** Hidden coupling between modules via shared global state. Init-order fiasco on static constructors. Untestable code due to global side effects.
+**Escapes:** Static tools cannot distinguish a benign `static const` from a mutable `static` singleton. The constraint is on design intent (no new globals), not on syntax.
+**Locked:** The set of permitted globals: logger and JobSystem only. Any new global requires explicit review.
+**Free:** Logger configuration (level, file sink). JobSystem thread pool size. Module-local `static constexpr` values.
-### 3.3 Error propagation and logging boundary
+
-- Each error MUST be either handled or propagated.
-- Subsystem boundaries SHOULD log once with actionable context.
-- Re-logging the same error at multiple stack layers MUST NOT occur.
-- Monadic composition (`and_then`, `or_else`, `transform`) SHOULD be used.
+### PROC.app.004 — Logging backend
-### 3.4 Error type
+**Guard:** Adding logging calls in application code (`src/app/`, `src/compositor/`, `src/render/`, `src/ui/`). Modifying logger initialization in `main.cpp`.
+**Safety:** Application logging must use `GOGGLES_LOG_*` macros backed by spdlog. One global logger initialized at startup via `initialize_logger()`. Config-driven log level applied via `set_log_level()`. Semgrep bans `cout`/`cerr`/`printf` but does not enforce the positive "must use spdlog" requirement.
-`Error` MUST remain lightweight and include an error code plus human-readable message. Source location metadata MAY be included when useful.
+
+
+**Violation:** Logging output appears in wrong format, wrong sink, or not at all. Logger used before initialization. Multiple logger instances with inconsistent configuration.
+**Escapes:** Semgrep catches banned alternatives (`cout`, `cerr`, `printf`) but cannot verify that code uses the project logging macros. A module could simply omit logging entirely — no tool flags silent failure. The "single global logger" constraint is an init-ordering invariant, not a pattern.
+**Locked:** spdlog as logging backend. Single global logger instance. `GOGGLES_LOG_*` macros as the API surface.
+**Free:** Log levels per message. File sink configuration. Timestamp formatting. Filter-chain library has its own log routing (instance-owned, not spdlog global).
+
+
---
-## 4. Logging Policy
+## Boundary Layer
-**Applies to:** app
-**Enforced by:** semgrep, review, runtime behavior
+### BOUND.compositor-render.001 — DMA-BUF frame ownership transfer
-### 4.1 Logging backend
+**Guard:** Modifying `ExternalImageFrame` (`src/util/external_image.hpp`), its production in `compositor_present.cpp`, or its consumption in the render backend (`src/render/backend/`).
+**Safety:** DMA-BUF handle ownership transfers via `UniqueFd::dup()` — compositor retains its copy, application owns the duplicate. `sync_fd` (if present) transfers with the frame via the same dup mechanism. Receiver must not assume `sync_fd` is always present. Single-plane DMA-BUF only (single `handle` field in `ExternalImage`, enforced at export).
-- Application logging MUST use project logging macros backed by `spdlog`.
+
-### 4.2 Levels
+**Violation:** Double-close on DMA-BUF fd (shared instead of duplicated), or Vulkan import fails on multi-plane buffer.
+**Escapes:** Ownership transfer is semantic — no tool verifies that `dup()` is used rather than raw fd copy. Single-plane constraint is a structural choice (`ExternalImage` has one `handle`), not a runtime check.
+**Locked:** `ExternalImageFrame` as sole data structure crossing this boundary. Ownership model (dup, not transfer). Single-plane requirement. `present_mutex` protects the stored frame on the compositor side.
+**Free:** Adding fields to `ExternalImageFrame`. Changing internal rendering in `compositor_present.cpp` that produces the buffer. Frame numbering scheme.
-Valid levels are: `trace`, `debug`, `info`, `warn`, `error`, `critical`.
+
-### 4.3 Initialization
+### BOUND.render-filterchain.001 — Filter chain record params are borrowed
-- The app MUST initialize one global logger at startup.
-- App `[logging].*` config applies only to the app logger.
+**Guard:** Modifying `FilterChainController::RecordParams`, the `record()` call site, or the filter chain library's `record_vk()` API.
+**Safety:** `RecordParams` fields (`command_buffer`, `source_image`, `source_view`, `target_view`) are borrowed references valid only for the duration of the `record()` call. The filter chain library must not store or extend the lifetime of these handles. All `VkImage`/`VkImageView`/`VkCommandBuffer` handles in `RecordParams` must be valid Vulkan objects owned by the caller.
----
+
-## 5. Naming, Layout, and Documentation Policy
+**Violation:** Filter chain library caches a `VkCommandBuffer` from a previous frame and records into it after the caller has reset it. Use-after-free on stale image view.
+**Escapes:** Borrow semantics are a calling convention, not a type-level guarantee. The C API (`goggles_fc_record_info_vk_t`) uses raw pointers with no ownership annotation. No static tool can verify that the library does not store the pointer.
+**Locked:** `RecordParams` as borrowed (not transferred). Caller owns all Vulkan objects passed in. Single `record()` call per frame per chain.
+**Free:** Which specific Vulkan objects are passed. Source/target dimensions. Frame index. Scale mode.
-**Applies to:** all C/C++ source and headers
-**Enforced by:** clang-format, clang-tidy (partial), semgrep, review
+
-### 5.1 Naming rules
+### BOUND.ui-render.001 — ImGui captures raw Vulkan pointers from backend
-- C++ identifier naming MUST follow the repository `clang-tidy` naming configuration.
-- Files MUST use `snake_case.hpp` / `snake_case.cpp`.
-- Scoped `clang-tidy` overrides MAY narrow naming enforcement for generated or C-interop surfaces.
+**Guard:** Modifying `ImGuiConfig` struct (`src/ui/imgui_layer.hpp`), `ImGuiLayer::create()`, or the Vulkan context fields read during ImGui initialization in `application.cpp`.
+**Safety:** `ImGuiConfig` captures `vk::Instance`, `vk::PhysicalDevice`, `vk::Device`, `vk::Queue`, swapchain format, and image count from `VulkanBackend`. These handles must remain valid for ImGui's entire lifetime. ImGuiLayer must be destroyed before VulkanBackend (enforced by PROC.app.001 shutdown ordering).
-### 5.2 Header and include rules
+
-- Headers MUST use `#pragma once`.
-- Include order in `.cpp` MUST be:
- 1) corresponding header, 2) C++ standard headers, 3) third-party headers,
- 4) project headers from other modules, 5) project headers from same module.
-- Includes within each group MUST be alphabetized.
+**Violation:** ImGui renders with a destroyed `vk::Device`. Validation layer error on stale queue handle. Crash in ImGui's Vulkan shutdown.
+**Escapes:** ImGui stores raw Vulkan handles internally (Dear ImGui Vulkan backend). No type-level lifetime binding between ImGuiLayer and VulkanBackend. The safety depends entirely on destruction ordering in `Application::shutdown()`.
+**Locked:** ImGuiLayer destroyed before VulkanBackend (cross-ref: PROC.app.001). Vulkan context handles must not be recreated while ImGui is alive.
+**Free:** Which swapchain format is chosen. Image count. Queue family index.
+**Related:** PROC.app.001
-### 5.3 Commenting rules
+
-- Comments MUST explain non-obvious why, constraints, workarounds, or invariants.
-- Comments that narrate obvious what MUST NOT be used.
-- Tutorial/step-by-step comments for straightforward code MUST NOT be used.
-- Trailing comments that restate the current line MUST NOT be used.
-- For files larger than 200 lines, section dividers SHOULD be used when they improve navigation.
+### BOUND.config.001 — Config flows read-only to modules
-### 5.4 Public API docstrings
+**Guard:** Modifying `Config` struct (`src/util/config.hpp`), config loading in `main.cpp`, or how config values are passed to `Application::create()` and subsystem init functions.
+**Safety:** `Config` is loaded once at startup, values are read and copied into subsystem-specific settings structs. No module holds a reference to the `Config` object. Runtime changes to behavior (e.g., target FPS, shader preset) go through explicit setter methods with callbacks, not by mutating config.
-Doxygen `///` comments MUST be used when the declaration alone is insufficient:
-- Macros (no type signature to infer from).
-- Thread safety or cross-thread contracts.
-- Ownership transfer semantics.
-- Preconditions, postconditions, or invariants not expressible in the type system.
-- Non-obvious error conditions or edge-case return values.
-- Side effects not obvious from the name.
+
-Doxygen `///` comments MUST NOT be used when the declaration is self-documenting:
-- Trivial getters, setters, and callback-registration methods with descriptive names.
-- `@param` that restates the parameter name and type.
-- `@return` that restates `Result` as "success or error."
-- Mechanically repeated template comments across similar declarations.
+**Violation:** Module reads stale config reference after the `Config` local in `main.cpp` is destroyed. Runtime parameter change not reflected because module cached the initial config value without a change callback.
+**Escapes:** "Read-only flow" is a design pattern, not a type constraint. Nothing prevents a module from storing a `const Config&` reference — the compiler cannot verify the reference outlives the referent across module boundaries.
+**Locked:** Config loaded once at startup. Modules receive values by copy, not by reference. Runtime changes go through setter methods.
+**Free:** Config file format (TOML). Config fields and defaults. CLI override mechanism.
-When a doc comment contains one useful clause in otherwise redundant text, keep only the useful clause.
+
----
+### BOUND.render.001 — Vulkan result checking beyond semgrep scope
+
+**Guard:** Adding or modifying Vulkan API calls that return `vk::Result` in `src/render/backend/`.
+**Safety:** All `vk::Result` returns must be checked explicitly. Use `VK_TRY(call, code, msg)` for propagation. Cleanup/destructor paths may log-and-continue when propagation is impossible. Semgrep only catches `static_cast(waitIdle())` — all other unchecked results escape the hard gate.
+
+
+
+**Violation:** Vulkan call silently fails; subsequent operations use invalid state. GPU hang or crash from ignored error. Memory leak from failed allocation that was never checked.
+**Escapes:** Semgrep rule `goggles-no-discarded-vulkan-result` only matches the specific `static_cast(waitIdle())` pattern. The vast majority of Vulkan result-returning calls are not covered. General result checking requires understanding that a function returns `vk::Result` (or a `vk::ResultValue`), which is semantic.
+**Locked:** Explicit result checking for all Vulkan calls. `VK_TRY` as the standard propagation mechanism. No `static_cast()` on any result-returning Vulkan call.
+**Free:** Error codes and messages passed to `VK_TRY`. Whether cleanup paths log or propagate.
-## 6. Ownership, Lifetime, and Vulkan Policy
+
-**Applies to:** app code
-**Enforced by:** semgrep, review, clang-tidy (partial)
+### BOUND.render.002 — Vulkan destruction ordering
-### 6.1 Ownership model
+**Guard:** Modifying destruction logic in `VulkanBackend`, `RenderOutput`, `ExternalFrameImporter`, or `FilterChainController`. Adding new Vulkan objects that require explicit destruction.
+**Safety:** Vulkan objects must be destroyed in dependency order: objects that reference other objects must be destroyed first. `vkDeviceWaitIdle()` or per-object fence waits must precede destruction of any object that may be in use by the GPU. Semgrep bans `vk::Unique*`/`vk::raii::*` but destruction order itself is semantic.
-- C++ owned resources MUST use RAII.
-- `std::unique_ptr` SHOULD be default owning pointer.
-- `std::shared_ptr` SHOULD be used only for true shared ownership and SHOULD include rationale when non-obvious.
+
-### 6.2 File descriptors
+**Violation:** Validation layer error on destroying a referenced object. GPU crash from destroying a pipeline while in use. Device lost from destroying a swapchain while a present is in flight.
+**Escapes:** Semgrep enforces that manual destruction is used (no RAII wrappers), but it cannot verify the ORDER of manual `destroy*()` calls. Dependency order is semantic — it depends on which objects reference which, which is not statically analyzable from call patterns alone.
+**Locked:** Manual explicit destruction via `device.destroy*()`. Dependency-ordered destruction. GPU idle or fence-waited before destroying in-use objects.
+**Free:** Which specific Vulkan objects exist. Internal organization of destroy calls within a single cleanup function.
-- Owned file descriptors MUST use `goggles::util::UniqueFd`.
-- Raw `int` fds MAY be used only at unavoidable API boundaries and SHOULD be wrapped immediately.
+
-### 6.3 Vulkan API split
+### BOUND.code.001 — Comment and documentation rules
-- Application code MUST use vulkan-hpp `vk::` APIs.
-- Application code MUST NOT use raw `Vk*` handles directly.
+**Guard:** Adding or modifying comments or Doxygen docstrings in any C++ source file.
+**Safety:** Comments must explain non-obvious why, constraints, workarounds, or invariants. Comments must NOT narrate obvious what, provide step-by-step tutorials, restate the current line, or include LLM-verbose justifications. Doxygen `///` is required only when the declaration alone is insufficient (macros, thread safety contracts, ownership transfer, preconditions not in the type system, non-obvious error conditions, side effects). Doxygen must NOT be used for trivial getters/setters, `@param` restating the name, `@return` restating `Result`, or mechanical repetition.
-### 6.4 Vulkan lifetime model
+
-- App code MUST use plain `vk::` handles with explicit destroy/free calls.
-- Destruction order MUST respect dependency order and synchronization requirements.
+**Violation:** Codebase bloated with narration comments that obscure real invariant documentation. Code review burden from removing noise comments. Agent-generated code with step-by-step commentary.
+**Escapes:** No static tool can distinguish a useful constraint comment from a narration comment. The distinction is semantic — "Vulkan takes ownership of fd on success" is useful; "Create command pool" is not. This is fundamentally a judgment call that requires understanding code context.
+**Locked:** "Why, not what" principle. Doxygen restricted to declarations where the type signature is insufficient.
+**Free:** Comment phrasing. Whether a particular line needs a comment (author's judgment). Section dividers in large files.
-### 6.5 Vulkan error checks
+
-- All `vk::Result` returns MUST be checked.
-- `static_cast(vulkan_call())` MUST NOT be used for result-returning Vulkan calls.
-- `VK_TRY`, `GOGGLES_TRY`, and `GOGGLES_MUST` SHOULD be used where applicable.
-- Cleanup/destructor paths MAY log-and-continue when propagation is impossible.
+### BOUND.headers.001 — Pragma once in headers
-### 6.6 Object initialization
+**Guard:** Creating new header files (`.hpp`, `.h`) in `src/` or `tests/`.
+**Safety:** All headers must use `#pragma once` as their include guard. No `#ifndef`/`#define`/`#endif` include guards.
-- Two-phase initialization (`constructor` + `init()`) for manager-like/resource-heavy types MUST NOT be used.
-- Such types MUST use factory creation returning result types (for example `ResultPtr`).
-- Existing legacy code MAY be migrated incrementally when modified.
+
+
+**Violation:** Double-inclusion causing redefinition errors. Inconsistent guard style across codebase.
+**Escapes:** No semgrep rule or clang-tidy check enforces `#pragma once` presence. A header could be created without any include guard and compilation would only fail if it is included twice — a condition that may not arise in all build configurations.
+**Locked:** `#pragma once` as the sole include guard mechanism.
+**Free:** Header file naming. Header content beyond the guard.
+
+
---
-## 7. Threading and Real-Time Policy
+## Resource Layer
-**Applies to:** render path, background work
-**Enforced by:** semgrep, review, profiling, architecture checks
+### RES.render.001 — Swapchain recreation lifecycle
-### 7.1 Default model
+**Guard:** Modifying swapchain creation (`RenderOutput::create_swapchain()`), cleanup (`cleanup_swapchain()`), or destruction logic in `render_output.cpp`.
+**Safety:** Before recreation: `wait_all_frames()` must complete (all in-flight fences waited). After creation: `swapchain_images.size() == swapchain_image_views.size() == render_finished_sems.size()`. Old swapchain resources destroyed only after wait completes. New resources (images, views, semaphores) are created atomically within `create_swapchain()` — partial creation rolls back via `cleanup_new_swapchain` lambda.
-- Render backend and pipeline execution MUST remain single-threaded on main thread by default.
-- Threading in render path SHOULD be introduced only when profiling justifies it.
+
-### 7.2 Render-path constraints
+**Violation:** Validation layer error on destroyed swapchain image access. Crash from indexing into mismatched image/view/semaphore arrays. GPU hang from destroying swapchain while frames are in flight.
+**Escapes:** Array-size invariant maintained by construction in a single function, but nothing prevents a future change from creating views in a separate path that breaks the count relationship. The `wait_all_frames()` precondition is a calling convention, not enforced by the type system.
+**Locked:** `wait_all_frames()` as precondition for recreation. 1:1:1 relationship between images, views, and semaphores. Atomic creation with rollback on partial failure.
+**Free:** Swapchain format selection logic. Present mode selection. Image count negotiation.
-Per-frame real-time code paths MUST NOT:
+
-- block on I/O,
-- rely on blocking synchronization in hot paths,
-- introduce unpredictable latency work that violates frame budget goals.
+### RES.render.002 — DMA-BUF import lifecycle
-### 7.3 Job system requirement
+**Guard:** Modifying `ExternalFrameImporter::import_external_image()` in `external_frame_importer.cpp`, or the import sequence in `VulkanBackend::render()`.
+**Safety:** Import sequence: `waitIdle()` → destroy old imported image (view, image, memory in that order) → clear source state → create new image → get memory requirements → `dup()` fd → allocate and bind memory → create view. Any failure after partial creation must destroy all already-created objects before returning error. The fd passed to `vkAllocateMemory` is consumed by Vulkan on success (`import_fd.release()`).
-- Concurrent pipeline/render work MUST use `goggles::util::JobSystem`.
-- External integration code outside real-time path MAY use `std::jthread` with RAII-safe lifecycle.
+
----
+**Violation:** Leaked Vulkan image/memory on failed import (partial creation without cleanup). Double-close of DMA-BUF fd if `release()` is not called after successful Vulkan import. Stale imported image used after source changed.
+**Escapes:** The cleanup-on-partial-failure pattern is control flow, not a matchable pattern. Each early-return path must call `destroy_imported_image()` — this is manually maintained. Vulkan's fd ownership on `vkAllocateMemory` success is a spec-level requirement not expressible in types.
+**Locked:** `waitIdle()` before reimport. Destroy-before-recreate sequence. `dup()` for import fd (not raw fd). `release()` after successful Vulkan import. Cleanup on every error path.
+**Free:** Image format negotiation. Memory type selection. Dedicated allocation heuristics.
+
+
-## 8. Configuration Policy
+### RES.render.003 — Filter chain hot-reload slot lifecycle
-**Applies to:** app configuration
-**Enforced by:** review, runtime validation
+**Guard:** Modifying shader hot-reload, `FilterChainController` slot management (`active_slot`, `pending_slot`), the `check_pending_chain_swap()` function, or `RetiredAdapterTracker` logic.
+**Safety:** `active_slot` always contains a valid, recordable filter chain (or is empty — rendering skips the filter pass). `pending_slot` compiled asynchronously via `std::async` must not overwrite `active_slot` until compilation succeeds and `check_pending_chain_swap()` is called on the main thread. Failed compilation must not corrupt `active_slot`. Retired adapters held for `FALLBACK_RETIRE_DELAY_FRAMES` (3) frames before destruction, tracked in `RetiredAdapterTracker` with `MAX_RETIRED` (4) slots.
-### 8.1 Format and source
+
-- Config files MUST use TOML.
-- Repository-shipped default config template MUST be `config/goggles.template.toml`.
+**Liveness:** Render loop must never block waiting for shader compilation. Reload failure leaves previous active chain operational.
+**Violation:** Render loop stalls during shader compile. Black frame or crash after failed preset reload. Use-after-free on retired adapter still referenced by in-flight frame.
+**Escapes:** "active_slot always valid" depends on swap logic in `check_pending_chain_swap()` never moving from active without a replacement ready — this is control flow, not a matchable pattern. Retire delay is a magic number (`FALLBACK_RETIRE_DELAY_FRAMES = 3`) that must exceed `MAX_FRAMES_IN_FLIGHT` (2).
+**Locked:** Async compilation via `std::async` (not main thread blocking). Retire delay before adapter destruction. Failed reload preserves active chain. `pending_chain_ready` atomic flag for cross-thread readiness signal.
+**Free:** Compilation parameters. Control value snapshot/restore logic. Number of retired adapter slots (`MAX_RETIRED`). Retire delay value (must exceed frames in flight).
+**Related:** LIVE.render.001
-### 8.2 Loading behavior
+
-- Config MUST be read at startup.
-- Default runtime config resolution MUST target `${XDG_CONFIG_HOME:-$HOME/.config}/goggles/goggles.toml`
- unless an explicit config path is provided.
-- If no user config exists at the resolved runtime path, the application MAY materialize the
- shipped template there.
-- Values MUST be validated.
-- Optional values MUST have defaults.
-- Invalid config MUST fail fast.
+### RES.compositor.001 — Surface listener lifecycle
-### 8.3 User config path
+**Guard:** Modifying surface creation, mapping, or destruction handlers in `compositor_xdg.cpp`, `compositor_xwayland.cpp`, or `compositor_layer.cpp`. Adding new wlroots listeners.
+**Safety:** wlroots listeners (`wl_listener`) must be attached during surface creation/association and detached (via `wl_list_remove`) before the hooks object is erased from the compositor's hooks vectors. The hooks vectors (`xdg_hooks`, `xdg_popup_hooks`, `xwayland_hooks`, `constraint_hooks`, `layer_hooks`) own the hooks objects via `std::unique_ptr`. Erasing a hooks object without first detaching its listeners causes use-after-free when wlroots fires the signal.
-User-level XDG config loading is canonical.
+
+
+**Violation:** Use-after-free crash when wlroots fires a signal on a destroyed listener. Memory leak from listeners never detached. Double-free from hooks object destroyed while listener callback is executing.
+**Escapes:** wlroots listeners are C-level `wl_list` entries. There is no type-level binding between listener attachment and detachment. The requirement to call `wl_list_remove` before erasing the hooks object is a manual protocol. No static tool tracks listener attach/detach pairs across function boundaries.
+**Locked:** Listeners detached before hooks object erasure. One hooks object per surface. Hooks vectors as sole ownership mechanism.
+**Free:** Which signals are listened to. Internal handling within listener callbacks. Surface lifecycle state machine (configure → ack → map → commit → destroy).
+
+
---
-## 9. Dependency Management Policy
+## Sync Layer
-**Applies to:** build and third-party dependencies
-**Enforced by:** review, lockfile checks
+### SYNC.render.001 — DMA-BUF sync fd semaphore lifecycle
-### 9.1 Source of truth
+**Guard:** Modifying DMA-BUF sync fd handling in `ExternalFrameImporter` (`prepare_wait_semaphore`, `retire_wait_semaphore`), or the wait semaphore array in the render submission path.
+**Safety:** `sync_fd` dup'd and imported as `vk::SemaphoreImportFlagBits::eTemporary` into a per-frame-slot semaphore (`pending_wait_semaphores[frame_slot]`). Semaphore added to submit wait list with `WAIT_STAGE` (`eFragmentShader`). Semaphore at `pending_wait_semaphores[frame_slot]` is retired (destroyed) before a new one is imported into the same slot — enforced by the caller in `VulkanBackend::render()` before calling `prepare_wait_semaphore`. Import fd released after successful `importSemaphoreFdKHR` (Vulkan owns the fd). Semaphore is single-use: retired after the submit that waits on it.
-- Pixi (`pixi.toml` + `pixi.lock`) MUST be the primary dependency source of truth.
-- `pixi.lock` MUST remain in sync with `pixi.toml`.
+
-### 9.2 Dependency sources
+**Violation:** GPU race between compositor render and Vulkan import (tearing, corruption). Semaphore leak (slot overwritten without destroy). Validation error on double-wait of retired semaphore. Double-close of sync fd if `release()` not called.
+**Escapes:** Semaphore lifecycle spread across `prepare_wait_semaphore`, the submission path in `VulkanBackend::render()`, and `retire_wait_semaphore`. No tool tracks that a semaphore created in one function is correctly retired in another after exactly one use. The `eTemporary` flag is a Vulkan spec requirement for sync fd import — using the wrong flag silently produces incorrect sync.
+**Locked:** `eTemporary` import flag. Per-slot retirement before re-import. Single-use-then-retire lifecycle. `dup()` before import, `release()` after.
+**Free:** Which pipeline stage the semaphore waits at (`WAIT_STAGE`). Whether `sync_fd` is present (graceful absence handling — `prepare_wait_semaphore` returns early if fd invalid).
-- Pixi packages SHOULD be used.
-- Local `packages/` MAY be used for pinned/forked/special builds.
-- System packages MAY be used only when not practical through Pixi/local packages and MUST be documented.
+
-### 9.3 Version and update policy
+### SYNC.render.002 — Frame fence before acquire
-- Dependency versions MUST be explicit.
-- Security updates SHOULD be applied immediately.
-- Minor/major upgrades SHOULD include changelog review and validation.
-- New dependencies MUST include rationale, license compatibility check, maintenance assessment, and team agreement.
+**Guard:** Modifying `RenderOutput::acquire_next_image()`, `prepare_headless_frame()`, or the frame submission path in `submit_and_present()` / `submit_headless()`.
+**Safety:** Before each `vkAcquireNextImageKHR`: wait on the current frame slot's `in_flight_fence` (`waitForFences` with `UINT64_MAX` timeout). After successful wait and acquire: reset the fence (`resetFences`). The fence is signaled by `vkQueueSubmit` at the end of the frame. This ensures the command buffer and per-frame resources are not in use when reused.
----
+
+
+**Violation:** Command buffer recorded while previous submission is still executing. Validation layer error on reusing an in-flight command buffer. GPU corruption from concurrent writes to per-frame resources.
+**Escapes:** The fence-before-acquire pattern is a calling convention within `acquire_next_image()`. Static tools cannot verify that the fence is always waited before the acquire, because the two calls are sequential statements, not structurally linked. A refactor could separate them.
+**Locked:** `waitForFences` before `vkAcquireNextImageKHR`. `resetFences` after successful wait. Fence signaled by `vkQueueSubmit`.
+**Free:** Fence timeout value (`UINT64_MAX` is fine). Handling of `eErrorOutOfDateKHR` / `eSuboptimalKHR` return codes (triggers resize flag).
-## 10. Build and Testing Policy
+
-**Applies to:** local development and CI
-**Enforced by:** CI, review
+### SYNC.compositor-main.001 — present_mutex frame handoff
-### 10.1 Build/test workflow
+**Guard:** Modifying `presented_frame` access in `CompositorState`, `get_presented_frame()` in `compositor_present.cpp`, or frame production in the compositor's render path.
+**Safety:** `present_mutex` must be held for all reads and writes of `presented_frame`, `presented_buffer`, `presented_surface`, and `presented_frame_number`. The compositor thread writes under lock (frame production). The main thread reads under lock via `get_presented_frame()`, which duplicates the DMA-BUF fd before releasing the lock (no reference to compositor-owned data escapes the lock).
-- Contributors MUST use Pixi tasks for routine workflows.
-- Build/test invocations MUST use CMake/CTest presets.
-- Ad-hoc non-preset build directories MUST NOT be used.
+
-### 10.2 Framework and current scope
+**Violation:** Data race on `presented_frame` (compositor writes while main thread reads). Torn read of frame fields (partially updated frame seen by main thread). Use-after-free if main thread holds a reference into the frame while compositor overwrites it.
+**Escapes:** Mutex correctness is a runtime property. No static tool verifies that every access path to `presented_frame` holds `present_mutex`. New code could read `presented_frame_number` without the lock — the field is not wrapped in an accessor that enforces locking.
+**Locked:** `present_mutex` for all `presented_frame` access. `dup()` inside the lock — no reference escapes. Frame number check inside the lock (atomic check of frame number alone would race with frame data update).
+**Free:** Which frame data fields are stored. Frame numbering scheme. How the compositor decides when to update the presented frame.
-- Unit testing framework MUST be Catch2 v3.
-- Test structure SHOULD mirror `src/` under `tests/`.
-- Current automated scope is primarily non-GPU logic.
-- GPU/Vulkan behavior MAY be validated manually/integration-first until justified for automation.
+
-### 10.3 Required commands
+### SYNC.compositor-main.002 — SPSC queue contract
-Before opening/merging significant code changes, contributors SHOULD run relevant preset builds/tests (via Pixi tasks) appropriate to touched code.
+**Guard:** Modifying `SPSCQueue` (`src/util/queues.hpp`), its instantiations in `CompositorState` (`event_queue`, `resize_queue`), or producer/consumer call sites.
+**Safety:** Mutex-guarded ring buffer with bounded capacity (64 for both queues). `try_push` returns `false` on overflow (silent drop — events may be lost). `try_pop` returns `std::nullopt` when empty. Queues are polled (no wake mechanism) — the compositor event loop checks queues each iteration. Producer: main thread (input events, resize requests). Consumer: compositor thread (event loop iteration). Atomics for scalar cross-thread flags in `CompositorState` (`target_fps`, `cursor_visible`, `pointer_locked`, `pending_focus_target`, `present_reset_requested`) use `memory_order_release` on store, `memory_order_acquire` on load (or `memory_order_relaxed` / `memory_order_acq_rel` for exchange operations).
+
+
+
+**Violation:** Data race from concurrent push/pop without mutex (if lock removed). Lost events from unbounded overflow. Deadlock from blocking on full queue (if try_push changed to blocking push). Torn read of atomic flag from wrong memory ordering.
+**Escapes:** Queue implementation is mutex-guarded, but the contract (bounded, silent drop, polled) is a design choice not verifiable by any tool. A change from `try_push` to a blocking `push` would deadlock the main thread if the compositor stalls. Atomic memory ordering correctness requires understanding which flag is set by which thread and read by which — no static tool verifies ordering sufficiency.
+**Locked:** Bounded capacity. Non-blocking `try_push` with silent drop. Polled consumption (no condition variable, no wake). Mutex-guarded internals. `release`/`acquire` ordering for cross-thread atomic flags.
+**Free:** Queue capacity value (currently 64). Event types carried. Which atomics use `relaxed` vs `acquire`/`release`.
+
+
---
-## 11. Enforcement and Change Management
+## Liveness Layer
-### 11.1 Compliance
+### LIVE.render.001 — Filter chain recordable during hot-reload
-- New code MUST comply with this document.
-- Existing code SHOULD be migrated when touched.
-- Non-compliant pull requests MAY be blocked.
+**Guard:** Modifying shader hot-reload, filter chain slot management, or the active/pending slot swap logic in `FilterChainController`.
+**Safety:** `active_slot` always contains a valid, recordable filter chain (or is empty — rendering skips the filter pass). Pending slot compiled asynchronously must not overwrite `active_slot` until compilation succeeds. Failed compilation must not corrupt `active_slot`. Render loop calls `record()` on `active_slot` every frame — this must never block.
-### 11.2 Enforcement matrix
+
-- `clang-format`: formatting/layout.
-- `clang-tidy`: static checks (partial policy coverage).
-- `semgrep`: repository policy bans that are practical to express as structural/static rules.
-- CI presets/tests: build and test gates.
-- Code review: semantic policy requirements not covered by tools.
+**Liveness:** Render loop must never block waiting for shader compilation. Reload failure leaves previous active chain operational. Retired adapters held for `FALLBACK_RETIRE_DELAY_FRAMES` (3) before destruction to cover in-flight frames.
+**Violation:** Render loop stalls during shader compile. Black frame or crash after failed preset reload. Use-after-free on retired adapter still referenced by in-flight frame.
+**Escapes:** "active_slot always valid" depends on swap logic never moving from active without a replacement ready. This is control flow, not a matchable pattern. The `pending_chain_ready` atomic signals readiness but the actual swap happens on the main thread in `check_pending_chain_swap()`.
+**Locked:** Async compilation (not main thread). Retire delay before adapter destruction. Failed reload preserves active chain.
+**Free:** Compilation parameters. Control value snapshot/restore logic. Number of retired adapter slots.
+**Related:** RES.render.003
-### 11.3 Policy updates
+
-- Policy changes MUST include rationale and team agreement.
-- Version field and changelog section MUST be updated with policy edits.
+### LIVE.render.002 — Frame pipeline must advance
----
+**Guard:** Modifying the acquire → record → submit → present cycle in `VulkanBackend::render()`, `RenderOutput::acquire_next_image()`, or `RenderOutput::submit_and_present()`.
+**Safety:** Frame pipeline uses `MAX_FRAMES_IN_FLIGHT` (2) frame slots round-robined via `current_frame`. Each slot has its own `in_flight_fence`, `image_available_sem`, and `command_buffer`. No circular dependency between frame slots — slot N+1's fence wait does not depend on slot N's completion beyond the GPU-side fence signal. At least one Vulkan present mode must be selectable after swapchain recreation (FIFO is always available per Vulkan spec).
-## 12. Changelog
+
-### 12.1 v1.2 -> v1.3
+**Liveness:** Frame pipeline must advance. No circular fence dependency between frame slots. Present must not deadlock waiting for a frame that will never complete.
+**Violation:** Deadlock from circular fence wait. Black screen from pipeline stall. Swapchain recreation fails because no present mode available (cannot happen — FIFO is mandatory per spec, but code should not assume a specific non-FIFO mode).
+**Escapes:** Circular dependency freedom depends on the round-robin frame slot design. A refactor that makes slot N wait on slot N-1's data (beyond the GPU fence) could introduce a cycle. No static tool can verify deadlock-freedom of a ring-buffered frame pipeline.
+**Locked:** `MAX_FRAMES_IN_FLIGHT` frame slots with independent fences. Round-robin advancement. FIFO as fallback present mode.
+**Free:** Present mode selection preference (mailbox if available). Frame slot count (currently 2). Target FPS pacing mechanism.
-- Replaced permissive SHOULD guidance in section 5.4 with explicit MUST/MUST NOT rules for Doxygen docstrings.
-- Stripped redundant `///` comments from public headers to align with the updated policy.
+
-### 12.3 v1.1 -> v1.2
+### LIVE.compositor.001 — Event loop terminable
-- Removed policy text that is now directly gated by Semgrep (`using namespace` in headers, raw `new`/`delete`, banned Vulkan RAII wrappers, render-path thread bans, std-stream/`printf` logging bans).
-- Moved identifier naming specifics to the repository `clang-tidy` configuration and kept only the high-level policy contract plus scoped-override allowance.
-- Expanded Semgrep logging coverage to include `printf` so the removed overlap remains CI-gated.
-- Updated enforcement tags and matrix to call out Semgrep-owned policy checks explicitly.
+**Guard:** Modifying `CompositorState::teardown()`, `run_compositor_display_loop()`, or the compositor thread lifecycle.
+**Safety:** `wl_display_terminate()` sets the display's "terminate" flag, causing `wl_display_run()` (or the equivalent manual event loop dispatch) to return. `compositor_thread.join()` in `teardown()` must not deadlock — the display must actually terminate. The compositor thread must not block indefinitely on any operation that would prevent it from checking the terminate flag.
-### 12.4 v1.0 -> v1.1
+
-- Rewrote document to RFC-style normative format.
-- Added explicit precedence and exception process.
-- Replaced long prose with canonical rules and scope/enforcement tags.
-- Consolidated duplicated constraints and aligned summary/checklist to body rules.
-- Moved examples into appendices for readability.
+**Liveness:** Compositor event loop must always be terminable. Application shutdown must complete in bounded time.
+**Violation:** Application hangs on shutdown — compositor thread never joins. `wl_display_terminate()` called but thread blocked in a syscall that ignores the flag.
+**Escapes:** `wl_display_terminate()` sets a flag checked by `wl_event_loop_dispatch()`. If the compositor thread is blocked in a long-running operation that doesn't call back into the event loop (e.g., a blocking `poll()` with infinite timeout not on the display fd), it will not see the terminate flag. No static tool can verify that all code paths in the compositor thread eventually return to the event loop dispatch.
+**Locked:** `wl_display_terminate()` as the shutdown signal. Thread join in `teardown()`. Event loop dispatch as the termination check point.
+**Free:** Event loop iteration frequency. What work is done per iteration. Timer sources.
+
+
---
-## Appendix A: Good/Bad Examples
+## Self-Check Protocol
-### A.1 Comment narration (MUST NOT)
+Before marking a task complete, the agent MUST:
-```cpp
-// BAD
-// Create command pool
-vk::CommandPoolCreateInfo pool_info{};
+### 1. Declare touched layers
-// GOOD
-vk::CommandPoolCreateInfo pool_info{};
-```
+List which of the five layers the change could affect. A change affects a layer if it modifies code matching any rule's Guard in that layer. When uncertain, include the layer.
-### A.2 LLM-style verbose justification (MUST NOT)
+### 2. Walk rules in declared layers
-```cpp
-// BAD
-// We use std::vector here because we need dynamic sizing and
-// the number of images is not known at compile time
-std::vector images;
+For each rule in a declared layer, evaluate the Guard predicate against the change. If the Guard matches, verify the Safety condition holds in the resulting code.
-// GOOD
-std::vector images;
-```
+### 3. Report
-### A.3 Step-by-step tutorial comments (MUST NOT)
+For each matched rule, state one of:
+- **Satisfied** — safety condition holds, no action needed.
+- **Resolved** — safety condition was initially broken, fixed during task.
+- **Escalate** — safety condition cannot be satisfied; requires human review.
-```cpp
-// BAD
-// 1. Get memory requirements
-auto mem_reqs = device.getImageMemoryRequirements(image);
-// 2. Find suitable memory type
-uint32_t type_index = find_memory_type(mem_reqs);
+### Layer Selection Guide
-// GOOD
-auto mem_reqs = device.getImageMemoryRequirements(image);
-uint32_t type_index = find_memory_type(mem_reqs);
-```
+| Change type | Layers to check |
+|-------------|----------------|
+| Single-module internals (no API change) | Resource, Sync |
+| Module API or data structure change | Boundary, Resource, Sync |
+| New subsystem or lifecycle added | Process, Boundary, Resource, Liveness |
+| Threading or synchronization change | Sync, Liveness |
+| Init/shutdown path change | Process, Liveness |
+| Public header or API documentation change | Boundary |
-### A.4 Constraint/workaround comments (required when non-obvious)
+---
-```cpp
-// Vulkan takes ownership of fd on success; caller must dup() to retain
-import_info.fd = dup(dmabuf_fd);
+## Refinement Protocol
-// vkGetMemoryFdPropertiesKHR requires dynamic dispatch on this path
-#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1
-```
+### Adding a rule
-### A.5 Vulkan result handling
+1. **Identify the layer.** Which layer does the invariant belong to? If it spans two layers, choose the one where violation is most observable. Add a `Related:` note pointing to the other layer.
-```cpp
-// BAD
-static_cast(device.waitIdle());
+2. **Write the compact form first.** ID, Guard, Safety. If Guard cannot be stated as a predicate over files/functions, the rule is too vague. If Safety cannot be stated as a verifiable post-condition, the rule is not actionable.
-// GOOD
-VK_TRY(device.waitIdle(), ErrorCode::VULKAN_DEVICE_LOST, "waitIdle failed");
-```
+3. **Write the detail block.** Violation, Escapes, Locked, Free. The Escapes field is mandatory — if static tools can already catch it, the rule does not belong here.
-### A.6 Two-phase init (MUST NOT for manager/resource-heavy types)
+4. **Check for gate migration.** If the new rule could be expressed as a semgrep pattern or clang-tidy check, implement the hard gate instead. Only add the soft rule if the invariant is semantic.
-```cpp
-// BAD
-VulkanBackend backend;
-auto result = backend.init(...);
+### Removing a rule
-// GOOD
-auto backend = GOGGLES_TRY(VulkanBackend::create(...));
-```
+Rules are removed when:
+- A hard gate now enforces the invariant (record in Gate Migration Log).
+- The invariant no longer applies (code it protected was removed).
+- The rule was superseded by a broader rule in the same layer.
---
-## Appendix B: Rationale (Compact)
+## Gate Migration Log
+
+When a rule moves from this document to a hard gate:
-- Compact normative rules reduce review ambiguity and improve agent behavior.
-- Explicit Vulkan lifetime rules prevent incorrect API/lifetime usage.
-- Strong error handling policy improves diagnosability and operational safety.
-- Minimal comments and naming consistency improve long-term maintainability.
-- Preset-based builds and pinned dependencies improve reproducibility.
+| Date | Rule ID | Destination | Hard Gate Rule ID |
+|------|---------|-------------|-------------------|
+| — | — | — | — |
diff --git a/filter-chain b/filter-chain
index d053e43..e9ce587 160000
--- a/filter-chain
+++ b/filter-chain
@@ -1 +1 @@
-Subproject commit d053e43fdd7f55ebe68edb04975fcd3d1eefa541
+Subproject commit e9ce587be300029c7517c227cb3375c36bff83c9
diff --git a/pixi.lock b/pixi.lock
index b2b1eaf..e595a0a 100644
--- a/pixi.lock
+++ b/pixi.lock
@@ -100,7 +100,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.328.1-h5279c79_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda
- - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.1-hca5e8e5_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.2-hca5e8e5_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.2-hf2a90c1_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.2-h031cc0b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda
@@ -1725,9 +1725,9 @@ packages:
purls: []
size: 100393
timestamp: 1702724383534
-- conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.1-hca5e8e5_0.conda
- sha256: d2195b5fbcb0af1ff7b345efdf89290c279b8d1d74f325ae0ac98148c375863c
- md5: 2bca1fbb221d9c3c8e3a155784bbc2e9
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.2-hca5e8e5_0.conda
+ sha256: 046f2ff4acebd8729fac03e99c8c307dfb48b6a32894ba8c11576e78f6e76e43
+ md5: dc8b067e22b414172bedd8e3f03f3c95
depends:
- __glibc >=2.17,<3.0.a0
- libgcc >=14
@@ -1740,8 +1740,8 @@ packages:
license: MIT/X11 Derivative
license_family: MIT
purls: []
- size: 837922
- timestamp: 1764794163823
+ size: 851166
+ timestamp: 1780213397575
- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.2-h031cc0b_0.conda
sha256: a9612f88139197b2777a00325c72d872507e70d4f4111021f65e55797f97de67
md5: 672c49f67192f0a7c2fa55986219d197
@@ -2657,7 +2657,7 @@ packages:
- wayland
- wayland >=1.25.0,<2.0a0
- libxkbcommon
- - libxkbcommon >=1.13.1,<2.0a0
+ - libxkbcommon >=1.13.2,<2.0a0
- pixman
- pixman >=0.46.4,<1.0a0
license: MIT