feat(visual): tetrahedral spatial formalism for 3D scroom#3718
feat(visual): tetrahedral spatial formalism for 3D scroom#3718ryanklee wants to merge 29 commits into
Conversation
…yout Replace ad hoc shelf-based content placement with a spatial formalism derived from the AoA's stella octangula geometry. Core changes: - 30 anchor points (8 cube-vertices HIGH, 10 octahedron/child MEDIUM, 12 trisection LOW) computed from tetrahedral dual geometry - Three mandala zones (Utama r<2.5, Madya 2.5-4.5, Nista >4.5) enforce spatial discipline around AoA centroid - Content sources classified by entropy (HIGH/MEDIUM/LOW) and placed at geometrically principled positions - Camera FOV widened from 60 to 75 degrees for stronger peripheral depth - Energy-modulated orbital drift (radius 1.25-2.0, period 72-90s) - Atmospheric perspective: depth-dependent desaturation and light falloff - Tensegrity breathing: opacity-driven radial push/pull in spatial drift - Triangular grid on floor/ceiling (tetrahedral tiling at AoA edge spacing) - Diagonal grid lines on walls - Volumetric beams retargeted to dual tetrahedron vertex positions - YouTube JPEG loaded directly from compositor SHM for insphere texture - DoF shader written but disabled (NVIDIA 595.71 SPIR-V driver crash) Migrated work from 3 worktrees: - alpha/reverie-spherical-surface (sphere + YouTube + shaders) - alpha/aoa-sphere-warmth-signal (insphere + warmth) - alpha/aoa-naming-migration-completion (AoA rename + heatmap + tests) 6 REQ specs written for the complete formalism (REQ-20260522-scroom-*). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds AoA feature: a heatmap producer, a local visual-pool loader, a Cairo-based AOA renderer, WGSL shader integrations (heatmap, Reverie, DoF, entity restore), Rust scene and renderer wiring, headless SHM output, and many test/registry updates. ChangesAOA feature migration
🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
|
Auto-fix classified CI failure categories as |
…t gaps Fixes from 4-agent parallel audit: 1. GPU texture leak: upload_yt_jpeg_if_fresh now reuses a persistent texture instead of creating a new one every 3 frames. Only recreates when YouTube frame dimensions change. 2. Sphere warmth atomic corruption: split single AtomicU32 into separate FRAME counter and CACHED warmth value. Previously the fetch_add frame counter and the stored warmth bits corrupted each other. 3. Cross-role anchor fallback: when same-role anchors are exhausted, sources fall back to unused anchors of other roles instead of being silently dropped. Prevents invisible content in 40+ source scenes. 4. Four new unit tests: - scene_anchors_returns_30_points_with_correct_role_counts - classify_source_entropy_routes_correctly - anchor_exhaustion_drops_excess_sources_gracefully - orbital_drift_with_max_energy_stays_bounded 5. Restored overflow test assertion strength (unwrap instead of if-let). 6. Fixed misleading dof=enabled log message to dof=disabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c556f0a576
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| register("AlbumOverlayCairoSource", AlbumOverlayCairoSource) | ||
| register("CBIPSignalDensityCairoSource", CBIPSignalDensityCairoSource) | ||
| register("SierpinskiCairoSource", SierpinskiCairoSource) | ||
| register("AoaCairoSource", AoaCairoSource) |
There was a problem hiding this comment.
Re-register Sierpinski class name alias
Registering only AoaCairoSource here removes the SierpinskiCairoSource key that existing layouts still reference (for example config/compositor-layouts/default.json source sierpinski), so SourceRegistry.construct_backend will raise a KeyError when it resolves params.class_name during compositor startup. This makes the canonical layout fail to instantiate unless every layout is migrated in the same change; keep a compatibility alias (register both names) until the JSON corpus is updated.
Useful? React with 👍 / 👎.
| let frame = LAST.fetch_add(1, std::sync::atomic::Ordering::Relaxed); | ||
| if frame % 30 != 0 { | ||
| return f32::from_bits(LAST.load(std::sync::atomic::Ordering::Relaxed)); |
There was a problem hiding this comment.
Separate warmth cache from frame counter
read_sphere_warmth() uses one AtomicU32 both as a frame counter (fetch_add) and as cached float bits (store(warmth.to_bits())), so after the first cache write every subsequent fetch_add mutates the float bit pattern instead of counting frames. In practice the function returns drifting/garbage warmth values on most frames and can inject unstable color modulation into the sphere; use distinct atomics (or local statics) for frame cadence and cached warmth.
Useful? React with 👍 / 👎.
|
|
||
| def _pane_ordinal_depth2(domain: int, family_slot: int, material: int) -> int: | ||
| base = 20 | ||
| slot = domain * 16 + family_slot * 4 + (material % 4) |
There was a problem hiding this comment.
Preserve unique pane for fire material
Depth-2 pane indexing collapses fire onto the same bucket as void because MATERIAL_INDEX maps fire to 4 but _pane_ordinal_depth2 uses (material % 4). Any impingement/recruitment tagged fire will overwrite the void pane instead of its own material lane, so the AoA heatmap can no longer represent those two materials distinctly.
Useful? React with 👍 / 👎.
|
Auto-fix classified CI failure categories as |
…rchy Narrow the size ratio between HIGH/MEDIUM/LOW content quads from ~8:1 to ~3:1 area ratio. LOW sources (tickers, atmospheric) were illegibly small at 0.20 height; raised to 0.28. HIGH (cameras) reduced from 0.50 to 0.44. Viewer experience: content sources should be distinguishable by size (hierarchy) but all must remain legible at livestream resolution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Auto-fix classified CI failure categories as |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
agents/studio_compositor/compositor.py (1)
1383-1389:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate stale loader name in the debug log.
Line 1388 still logs
"sierpinski slot asset read failed"even though this path now uses_aoa_loader, which can mislead incident triage.Proposed fix
- log.debug("sierpinski slot asset read failed", exc_info=True) + log.debug("aoa slot asset read failed", exc_info=True)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agents/studio_compositor/compositor.py` around lines 1383 - 1389, The debug message is stale: when iterating loader = getattr(self, "_aoa_loader", None) and calling slot.current_asset() you should update the log.debug call to reference the new loader name (e.g., "_aoa_loader" or "aoa_loader") instead of "sierpinski"; change the message in the exception handler inside the loop over getattr(loader, "video_slots", ()) (and keep exc_info=True) so it clearly indicates the failure came from reading a slot asset from the _aoa_loader.tests/test_cairo_source.py (2)
296-309:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate function name to match the migrated implementation.
The function name
test_sierpinski_cairo_source_render_into_small_canvasstill references Sierpinski, but the function now testsAoaCairoSource. Rename it totest_aoa_cairo_source_render_into_small_canvasfor consistency with the migration.📝 Proposed fix
-def test_sierpinski_cairo_source_render_into_small_canvas(): - """Direct render into a small ImageSurface — sanity-check the source - is decoupled from the facade and works standalone. - """ +def test_aoa_cairo_source_render_into_small_canvas(): + """Direct render into a small ImageSurface — sanity-check the source + is decoupled from the facade and works standalone. + """🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_cairo_source.py` around lines 296 - 309, Rename the test function to match the migrated implementation: change the test function name from test_sierpinski_cairo_source_render_into_small_canvas to test_aoa_cairo_source_render_into_small_canvas so it reflects that it exercises AoaCairoSource.render; update the test declaration (def ...) and any references to the old name inside the file to ensure consistency with AoaCairoSource usage in the test.
312-344:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate function name to match the migrated implementation.
The function name
test_sierpinski_audio_energy_smoothed_clamped_instantstill references Sierpinski, but the function now testsAoaCairoSource. Rename it totest_aoa_audio_energy_smoothed_clamped_instantfor consistency with the migration.📝 Proposed fix
-def test_sierpinski_audio_energy_smoothed_clamped_instant(): +def test_aoa_audio_energy_smoothed_clamped_instant(): """The line-width-modulating energy is clamped at SIERPINSKI_AUDIO_BURST_CLAMP and responds instantly (attack/release alphas default to 1.0 since `#2743`).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_cairo_source.py` around lines 312 - 344, Rename the test function symbol from test_sierpinski_audio_energy_smoothed_clamped_instant to test_aoa_audio_energy_smoothed_clamped_instant so the name reflects the migrated implementation (update the def line and any internal references or test-suite expectations that call this function); ensure the docstring and any comments remain valid and run the tests to confirm the renamed test is discovered by pytest.
🧹 Nitpick comments (9)
agents/studio_compositor/aoa_heatmap.py (2)
287-295: 💤 Low valueConsider adding telemetry spans per tick.
The coding guidelines specify using
shared/telemetry.pyhapax_span ExitStack pattern for Python modules. This loop would benefit from tracing to observe tick latency and event throughput in production.As per coding guidelines:
**/*.py: Useshared/telemetry.pyhapax_span ExitStack pattern.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agents/studio_compositor/aoa_heatmap.py` around lines 287 - 295, Wrap each tick iteration in a telemetry span using the hapax_span ExitStack pattern from shared/telemetry.py: import the hapax_span context manager and inside run_heatmap_loop() open a hapax_span (e.g., named "aoa_heatmap.tick") around hm.tick() to measure latency and throughput; ensure the span is created before calling AoaHeatmap.tick and closed after (including on exceptions) so the existing except block logs the error but the span still records duration and status; keep the sleep logic and TICK_HZ interval unchanged.
234-252: ⚡ Quick winCursor invalidation on file truncation or rotation.
If the JSONL file is truncated or rotated (replaced with a new file), the stored cursor position may exceed the new file's length. While
f.seek()to a position beyond EOF won't error, subsequent reads will return nothing until the file grows past the old cursor—missing all events written to the rotated file.Consider checking if the file size is smaller than the cursor (indicating rotation) and resetting to 0:
Proposed resilience improvement
def _read_new_impingements(self) -> list[dict]: if not IMPINGEMENT_PATH.exists(): return [] try: with open(IMPINGEMENT_PATH) as f: + f.seek(0, 2) # Seek to end + file_size = f.tell() + if file_size < self._cursor: + self._cursor = 0 # File was rotated/truncated f.seek(self._cursor) lines = f.readlines() self._cursor = f.tell()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agents/studio_compositor/aoa_heatmap.py` around lines 234 - 252, The _read_new_impingements method can miss lines when the JSONL is truncated/rotated because self._cursor may point past the new file length; before seeking, check the current file size (e.g., via IMPINGEMENT_PATH.stat().st_size or by using f.seek(0,2) to get size) and if size < self._cursor reset self._cursor = 0 (optionally also detect rotation by comparing inode/mtime and reset if changed); then seek to self._cursor, read lines, parse JSON, and update self._cursor after reading as currently implemented.hapax-logos/crates/hapax-visual/src/scene.rs (1)
966-969: 💤 Low valueRemove unused layout constants.
on_ring_forward,mid_ring_forward, andfar_ring_forwardare defined but never used after the shelf-to-anchor migration. Onlyprimary_forwardis still used for ticker placement.🧹 Proposed cleanup
let mut nodes = Vec::new(); let primary_forward = 1.78; -let on_ring_forward = 2.08; -let mid_ring_forward = 2.36; -let far_ring_forward = 2.95;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hapax-logos/crates/hapax-visual/src/scene.rs` around lines 966 - 969, Remove the three unused layout constants declared in scene.rs—on_ring_forward, mid_ring_forward, and far_ring_forward—since only primary_forward is used for ticker placement after the shelf-to-anchor migration; update the const/let block that currently defines primary_forward, on_ring_forward, mid_ring_forward, and far_ring_forward to only declare primary_forward (1.78) so there are no unused bindings left.agents/shaders/nodes/colorgrade.wgsl (1)
151-155: ⚡ Quick winOptimize: avoid redundant texture sample.
Line 153 samples
texagain, but the original color was already sampled at line 95 and stored incolor. Sampling the same texture twice per fragment is inefficient.⚡ Proposed fix: reuse the original sample
Store the original color before modifications:
fn main_1() { var color: vec4<f32>; + var original_color: vec3<f32>; var gray: f32; var sep: vec3<f32>; var hsv: vec3<f32>; var source_luma: f32; var surface_presence: f32; var graded: vec3<f32>; let _e14 = v_texcoord_1; let _e15 = textureSample(tex, tex_sampler, _e14); color = _e15; + original_color = _e15.xyz; source_luma = dot(color.xyz, vec3<f32>(0.299f, 0.587f, 0.114f));Then use
original_colorinstead of re-sampling:graded = mix(_e117.xyz, _e116, vec3(surface_presence)); // Preserve entity color identity: blend graded result back toward // original so entity hue always shows through the color grade. - let original = textureSample(tex, tex_sampler, v_texcoord_1).xyz; let entity_preserve = 0.90; - graded = mix(graded, original, vec3(entity_preserve * surface_presence)); + graded = mix(graded, original_color, vec3(entity_preserve * surface_presence)); fragColor = vec4<f32>(graded.x, graded.y, graded.z, _e117.w);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agents/shaders/nodes/colorgrade.wgsl` around lines 151 - 155, The shader re-samples the same texture into `original` even though the fragment's original sample is already in `color`; remove the redundant textureSample call and reuse the existing `color` variable (e.g., use color.xyz or color.rgb) when computing `entity_preserve` blending into `graded` so `graded = mix(graded, color.xyz, vec3(entity_preserve * surface_presence));` and eliminate the extra sample to reduce per-fragment work.agents/studio_compositor/aoa_renderer.py (1)
920-921: 💤 Low valueAccessing private attributes of
CairoSourceRunner.Directly setting
_publish_opacityand_publish_z_ordercouples this class to internal implementation details ofCairoSourceRunner. If those internals change, this code will silently break or misbehave.Consider exposing these as constructor parameters or a public setter on
CairoSourceRunner.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agents/studio_compositor/aoa_renderer.py` around lines 920 - 921, This code directly mutates private attributes _publish_opacity and _publish_z_order on CairoSourceRunner (via self._runner), which couples AoA renderer to internals; instead add a public API on CairoSourceRunner (either constructor args publish_opacity, publish_z_order or methods/properties like set_publish_opacity(value) and set_publish_z_order(value) or .publish_opacity/.publish_z_order properties) and update this module to call those public setters or pass values during CairoSourceRunner construction rather than touching underscored attributes.agents/studio_compositor/aoa_loader.py (2)
173-175: 💤 Low valueDuplicate import of
programme_provider.
programme_provideris already imported at lines 134-136; this re-import at lines 173-175 is redundant.Proposed fix
if os.environ.get("HAPAX_STRUCTURAL_DIRECTOR_ENABLED", "1").lower() not in { "0", "false", "off", "no", }: try: - from agents.studio_compositor.programme_context import ( - default_provider as programme_provider, - ) from agents.studio_compositor.structural_director import StructuralDirector self._structural_director = StructuralDirector( programme_provider=programme_provider, )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agents/studio_compositor/aoa_loader.py` around lines 173 - 175, Remove the redundant re-import of programme_provider from agents.studio_compositor.programme_context by deleting the duplicate import block that imports default_provider as programme_provider (the import already exists earlier in the file); ensure only the original import of programme_provider remains so there are no duplicate symbol definitions.
1-1: 💤 Low valueNaming inconsistency: docstring and thread names still reference "Sierpinski".
The module docstring says "Sierpinski content loader" and thread names use
"sierpinski-loader"/"sierpinski-director-init", but the class isAoaLoader. Given this is part of the Sierpinski-to-AoA migration, consider updating for consistency.Also applies to: 113-113, 118-118
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agents/studio_compositor/aoa_loader.py` at line 1, The module docstring and thread name literals still reference "Sierpinski" but the class is AoaLoader; update the module docstring to describe this as the AoA/AoaLoader content loader and replace thread name strings "sierpinski-loader" and "sierpinski-director-init" with consistent AoA names (e.g., "aoa-loader" and "aoa-director-init") so the docstring, class name AoaLoader, and thread names match; ensure any other occurrences in the file are renamed accordingly.hapax-logos/crates/hapax-visual/src/scene_renderer.rs (1)
1257-1284: ⚖️ Poor tradeoffFunction name suggests freshness check but none is implemented.
upload_yt_jpeg_if_freshreads and decodes the JPEG on every call without checking whether the file has actually changed (e.g., via mtime or content hash). Given this is called every 3 frames, unnecessary JPEG decodes add CPU overhead.Consider caching the file mtime or a content hash to skip redundant work.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hapax-logos/crates/hapax-visual/src/scene_renderer.rs` around lines 1257 - 1284, upload_yt_jpeg_if_fresh currently decodes the JPEG every call; add a freshness check by storing the last-seen file metadata on the renderer struct (e.g., a field like last_yt_mtime: Option<SystemTime> or last_yt_hash: Option<u64>) and early-return when unchanged. Implement: before reading the file, call std::fs::metadata(YT_FRAME_PATH).and_then(|m| m.modified()) (or compute a quick hash of the file bytes) and compare to the cached value on self; if equal return, otherwise proceed to read/decompress, update the cached field (last_yt_mtime or last_yt_hash) and continue to create/upload the texture via set_reverie_texture. Ensure error paths still return without updating the cache.hapax-logos/crates/hapax-visual/src/shaders/scene_grid.wgsl (1)
276-311: 💤 Low valueComputed
sphere_depthis unused; hardcoded 0.999 returned instead.Line 278 computes perspective-correct depth via
hit_clip.z / hit_clip.w, but Line 311 returns a hardcoded0.999. While this may be intentional for the "panes occlude sphere" ordering, the computation is then wasted. If hardcoded depth is desired, consider removing the unused computation or documenting the intent.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hapax-logos/crates/hapax-visual/src/shaders/scene_grid.wgsl` around lines 276 - 311, The computed sphere_depth (from hit_clip.z / hit_clip.w) is never used and either should be applied to the fragment output depth or removed with a comment; update the shader in the sphere shading block to either pass sphere_depth into FragOutput instead of the hardcoded 0.999 (replace the hardcoded depth with sphere_depth) or delete the hit_clip/sphere_depth computation and add a brief comment explaining that a fixed depth (0.999) is intentionally used for occlusion ordering; locate the variables hit_clip, sphere_depth, and the return FragOutput(...) to make the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@agents/studio_compositor/aoa_heatmap.py`:
- Around line 130-133: The function _pane_ordinal_depth2 currently compresses
material indices with (material % 4), which maps MATERIAL_INDEX entry 4 (fire)
to the same slot as 0 (void); update the slot calculation in
_pane_ordinal_depth2 to use a stride of 5 (e.g., replace material % 4 with
material % 5 or change the family_slot*4 stride to family_slot*5) so each
material index 0–4 maps to a distinct pane; ensure any other constants relying
on a 4-wide packing are adjusted or documented if you choose to keep
compression.
In `@agents/studio_compositor/aoa_loader.py`:
- Around line 186-187: stop() currently only flips _running and fails to stop
directors started in _start_director(); update stop() to call stop/cleanup on
self._director, self._twitch_director, and self._structural_director (if they
exist) and then clear them (or join/wait if those directors expose join/await
methods) to ensure background threads exit. Also initialize those attributes to
None in __init__ (or ensure they always exist) and replace any hasattr checks
with explicit "is not None" checks when guarding calls to
_director/_twitch_director/_structural_director; reference the
_start_director(), stop(), _director, _twitch_director, and _structural_director
symbols when making the changes.
- Around line 91-107: The constructor __init__ neglects to declare the instance
attributes that _start_director() sets ( _director, _twitch_director,
_structural_director ), which can lead to AttributeError if those are accessed
before startup or if startup fails; declare and initialize these attributes in
__init__ (e.g., self._director = None, self._twitch_director = None,
self._structural_director = None) with appropriate typing hints or forward refs
(use TYPE_CHECKING imports if needed) so callers and linters know the attributes
exist even before _start_director() runs.
In `@agents/studio_compositor/aoa_renderer.py`:
- Around line 381-394: The current block can leave orphaned temp files if
os.replace raises OSError because tmp_path is never cleaned up; modify the
try/except so tmp_path is tracked (e.g., initialize tmp_path = None before the
with), and in the exception handler (or a finally) attempt to
os.unlink(tmp_path) if tmp_path is set and the file exists; ensure you only call
os.unlink after ensuring tmp_path is not None and catch/remove any OSError from
the unlink to avoid masking the original error. Use the existing
VIDEO_ATTENTION_PATH code path and tmp_path (from the NamedTemporaryFile) to
locate and remove the orphaned .video-attention.*.tmp file.
In `@hapax-logos/crates/hapax-visual/src/scene_renderer.rs`:
- Around line 347-361: The function read_sphere_warmth incorrectly uses a single
static AtomicU32 (LAST) for both frame counting and caching, corrupting the
cached float bits; split responsibilities by introducing two statics (e.g.,
FRAME_COUNTER: AtomicU32 = AtomicU32::new(0) for fetch_add increments and
LAST_WARMTH: AtomicU32 = AtomicU32::new(0.5f32.to_bits()) for cached warmth).
Change fetch_add and modulus checks to use FRAME_COUNTER, use
LAST_WARMTH.load(Ordering::Relaxed) when returning cached values, and store
warmth bits into LAST_WARMTH.store(warmth.to_bits(), Ordering::Relaxed) after
reading from the file. Ensure initial LAST_WARMTH is set to a sane default
(0.5f32.to_bits()) and keep Ordering::Relaxed for both operations.
In `@hapax-logos/src-imagination/src/headless.rs`:
- Around line 159-160: The field scene_shm (ShmOutput) is allocated but never
used to publish scene frames; update the render pipeline in render_frame() to
copy the rendered texture into the SHM buffer and call scene_shm.write_frame so
the direct scene output path works. Specifically, after the scene is rendered
(use scene.output_texture() or the texture returned by Scene::render), map or
blit the texture into the scene_shm pixel buffer using the same format/stride
used by ShmOutput, then call scene_shm.write_frame() (or the equivalent method
on ShmOutput) with the populated buffer and proper timestamp/frame metadata;
ensure this logic mirrors how intermediate/primary outputs are handled and guard
it behind the existing scene_shm presence checks so the branch only runs when
scene_shm is enabled.
In `@tests/studio_compositor/test_aoa_no_yt_extraction.py`:
- Around line 43-45: The AST import guard only checks alias.name which misses
"from subprocess import run"; update the check in the loop over node (used for
ast.Import and ast.ImportFrom) to also inspect node.module when node is an
ast.ImportFrom and assert node.module != "subprocess" (or fail with the same
message referencing path) so that both plain imports and from-imports from the
subprocess module are caught; refer to the existing symbols node, ast.Import,
ast.ImportFrom, alias.name, names, node.module and path to locate and modify the
assertion logic.
In `@tests/studio_compositor/test_geal_source.py`:
- Around line 360-365: Update the stale inline comment that mentions
"Sierpinski's geometry cache" to accurately reference the AoA geometry source
used here: change the comment to state that the centre invariant is resolved via
AoaCairoSource's geometry cache; locate the comment above the import/usage of
AoaCairoSource and geometry_cache and replace the wording so it explicitly
mentions AoaCairoSource().geometry_cache (target_depth=2, canvas_w=1280,
canvas_h=720) instead of Sierpinski's geometry cache.
In `@tests/studio_compositor/test_video_attention.py`:
- Around line 25-28: This test file is failing Ruff formatting checks; run the
formatter (e.g., ruff format) on the test file and fix import/spacing issues so
imports like VIDEO_ATTENTION_PATH and AoaCairoSource (and the other import
groups referenced) follow project style; ensure imports are grouped/ordered and
whitespace is normalized across the file, then re-run ruff format --check to
verify the file passes CI.
In `@tests/test_audio_visual_correlation.py`:
- Around line 21-27: The file's import blocks aren't formatted to Ruff's
standards; run the formatter (ruff format) on the test file and fix import
formatting so multi-line imports like AUDIO_LINE_WIDTH_BASE_PX,
AUDIO_LINE_WIDTH_SCALE_PX, SIERPINSKI_AUDIO_ATTACK_ALPHA,
SIERPINSKI_AUDIO_BURST_CLAMP, and AoaCairoSource are arranged and wrapped per
Ruff/PEP8 conventions and applied consistently for the other import groups
referenced in the review; ensure the import lines are reorganized/line-broken
uniformly across the file so ruff format --check passes.
---
Outside diff comments:
In `@agents/studio_compositor/compositor.py`:
- Around line 1383-1389: The debug message is stale: when iterating loader =
getattr(self, "_aoa_loader", None) and calling slot.current_asset() you should
update the log.debug call to reference the new loader name (e.g., "_aoa_loader"
or "aoa_loader") instead of "sierpinski"; change the message in the exception
handler inside the loop over getattr(loader, "video_slots", ()) (and keep
exc_info=True) so it clearly indicates the failure came from reading a slot
asset from the _aoa_loader.
In `@tests/test_cairo_source.py`:
- Around line 296-309: Rename the test function to match the migrated
implementation: change the test function name from
test_sierpinski_cairo_source_render_into_small_canvas to
test_aoa_cairo_source_render_into_small_canvas so it reflects that it exercises
AoaCairoSource.render; update the test declaration (def ...) and any references
to the old name inside the file to ensure consistency with AoaCairoSource usage
in the test.
- Around line 312-344: Rename the test function symbol from
test_sierpinski_audio_energy_smoothed_clamped_instant to
test_aoa_audio_energy_smoothed_clamped_instant so the name reflects the migrated
implementation (update the def line and any internal references or test-suite
expectations that call this function); ensure the docstring and any comments
remain valid and run the tests to confirm the renamed test is discovered by
pytest.
---
Nitpick comments:
In `@agents/shaders/nodes/colorgrade.wgsl`:
- Around line 151-155: The shader re-samples the same texture into `original`
even though the fragment's original sample is already in `color`; remove the
redundant textureSample call and reuse the existing `color` variable (e.g., use
color.xyz or color.rgb) when computing `entity_preserve` blending into `graded`
so `graded = mix(graded, color.xyz, vec3(entity_preserve * surface_presence));`
and eliminate the extra sample to reduce per-fragment work.
In `@agents/studio_compositor/aoa_heatmap.py`:
- Around line 287-295: Wrap each tick iteration in a telemetry span using the
hapax_span ExitStack pattern from shared/telemetry.py: import the hapax_span
context manager and inside run_heatmap_loop() open a hapax_span (e.g., named
"aoa_heatmap.tick") around hm.tick() to measure latency and throughput; ensure
the span is created before calling AoaHeatmap.tick and closed after (including
on exceptions) so the existing except block logs the error but the span still
records duration and status; keep the sleep logic and TICK_HZ interval
unchanged.
- Around line 234-252: The _read_new_impingements method can miss lines when the
JSONL is truncated/rotated because self._cursor may point past the new file
length; before seeking, check the current file size (e.g., via
IMPINGEMENT_PATH.stat().st_size or by using f.seek(0,2) to get size) and if size
< self._cursor reset self._cursor = 0 (optionally also detect rotation by
comparing inode/mtime and reset if changed); then seek to self._cursor, read
lines, parse JSON, and update self._cursor after reading as currently
implemented.
In `@agents/studio_compositor/aoa_loader.py`:
- Around line 173-175: Remove the redundant re-import of programme_provider from
agents.studio_compositor.programme_context by deleting the duplicate import
block that imports default_provider as programme_provider (the import already
exists earlier in the file); ensure only the original import of
programme_provider remains so there are no duplicate symbol definitions.
- Line 1: The module docstring and thread name literals still reference
"Sierpinski" but the class is AoaLoader; update the module docstring to describe
this as the AoA/AoaLoader content loader and replace thread name strings
"sierpinski-loader" and "sierpinski-director-init" with consistent AoA names
(e.g., "aoa-loader" and "aoa-director-init") so the docstring, class name
AoaLoader, and thread names match; ensure any other occurrences in the file are
renamed accordingly.
In `@agents/studio_compositor/aoa_renderer.py`:
- Around line 920-921: This code directly mutates private attributes
_publish_opacity and _publish_z_order on CairoSourceRunner (via self._runner),
which couples AoA renderer to internals; instead add a public API on
CairoSourceRunner (either constructor args publish_opacity, publish_z_order or
methods/properties like set_publish_opacity(value) and
set_publish_z_order(value) or .publish_opacity/.publish_z_order properties) and
update this module to call those public setters or pass values during
CairoSourceRunner construction rather than touching underscored attributes.
In `@hapax-logos/crates/hapax-visual/src/scene_renderer.rs`:
- Around line 1257-1284: upload_yt_jpeg_if_fresh currently decodes the JPEG
every call; add a freshness check by storing the last-seen file metadata on the
renderer struct (e.g., a field like last_yt_mtime: Option<SystemTime> or
last_yt_hash: Option<u64>) and early-return when unchanged. Implement: before
reading the file, call std::fs::metadata(YT_FRAME_PATH).and_then(|m|
m.modified()) (or compute a quick hash of the file bytes) and compare to the
cached value on self; if equal return, otherwise proceed to read/decompress,
update the cached field (last_yt_mtime or last_yt_hash) and continue to
create/upload the texture via set_reverie_texture. Ensure error paths still
return without updating the cache.
In `@hapax-logos/crates/hapax-visual/src/scene.rs`:
- Around line 966-969: Remove the three unused layout constants declared in
scene.rs—on_ring_forward, mid_ring_forward, and far_ring_forward—since only
primary_forward is used for ticker placement after the shelf-to-anchor
migration; update the const/let block that currently defines primary_forward,
on_ring_forward, mid_ring_forward, and far_ring_forward to only declare
primary_forward (1.78) so there are no unused bindings left.
In `@hapax-logos/crates/hapax-visual/src/shaders/scene_grid.wgsl`:
- Around line 276-311: The computed sphere_depth (from hit_clip.z / hit_clip.w)
is never used and either should be applied to the fragment output depth or
removed with a comment; update the shader in the sphere shading block to either
pass sphere_depth into FragOutput instead of the hardcoded 0.999 (replace the
hardcoded depth with sphere_depth) or delete the hit_clip/sphere_depth
computation and add a brief comment explaining that a fixed depth (0.999) is
intentionally used for occlusion ordering; locate the variables hit_clip,
sphere_depth, and the return FragOutput(...) to make the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7c4460cb-7ae0-4925-805c-66e863f711eb
📒 Files selected for processing (49)
agents/reverie/_uniforms.pyagents/shaders/nodes/colorgrade.wgslagents/shaders/nodes/content_layer.wgslagents/shaders/nodes/feedback.wgslagents/shaders/nodes/postprocess.wgslagents/studio_compositor/aoa_heatmap.pyagents/studio_compositor/aoa_loader.pyagents/studio_compositor/aoa_renderer.pyagents/studio_compositor/cairo_source_registry.pyagents/studio_compositor/cairo_sources/__init__.pyagents/studio_compositor/compositor.pyagents/studio_compositor/fx_chain.pyagents/studio_compositor/geal_source.pyagents/studio_compositor/lifecycle.pyagents/studio_compositor/overlay.pyagents/studio_compositor/overlay_zones.pyagents/studio_compositor/packed_cameras_source.pyagents/studio_compositor/text_render.pyagents/studio_compositor/youtube_turn_taking.pyhapax-logos/crates/hapax-visual/src/dynamic_pipeline.rshapax-logos/crates/hapax-visual/src/effect_drift.rshapax-logos/crates/hapax-visual/src/scene.rshapax-logos/crates/hapax-visual/src/scene_renderer.rshapax-logos/crates/hapax-visual/src/shaders/entity_restore.wgslhapax-logos/crates/hapax-visual/src/shaders/fullscreen_blit.wgslhapax-logos/crates/hapax-visual/src/shaders/scene_dof.wgslhapax-logos/crates/hapax-visual/src/shaders/scene_grid.wgslhapax-logos/crates/hapax-visual/src/shaders/scene_quad.wgslhapax-logos/src-imagination/src/headless.rstests/studio_compositor/test_3d_director_runtime.pytests/studio_compositor/test_aoa_featured_slot.pytests/studio_compositor/test_aoa_local_visual_pool.pytests/studio_compositor/test_aoa_no_yt_extraction.pytests/studio_compositor/test_aoa_renderer.pytests/studio_compositor/test_cairo_source_registry.pytests/studio_compositor/test_cairo_sources_migration.pytests/studio_compositor/test_fx_slot_count.pytests/studio_compositor/test_geal_anti_personification.pytests/studio_compositor/test_geal_source.pytests/studio_compositor/test_layout_class_registration.pytests/studio_compositor/test_layout_integrity_full_corpus.pytests/studio_compositor/test_m8_oscilloscope_source.pytests/studio_compositor/test_overlay_hot_path_gates.pytests/studio_compositor/test_scale_parity.pytests/studio_compositor/test_video_attention.pytests/test_audio_reactivity_tightness.pytests/test_audio_visual_correlation.pytests/test_cairo_source.pytests/test_cairo_sources_package.py
| def _pane_ordinal_depth2(domain: int, family_slot: int, material: int) -> int: | ||
| base = 20 | ||
| slot = domain * 16 + family_slot * 4 + (material % 4) | ||
| return base + (slot % 64) |
There was a problem hiding this comment.
Material "fire" collides with "void" due to material % 4.
MATERIAL_INDEX defines 5 materials (indices 0–4), but material % 4 maps index 4 (fire) to 0, causing fire events to update the same pane as void events. This likely should be material % 5 or the slot stride should be 5 instead of 4.
Proposed fix
def _pane_ordinal_depth2(domain: int, family_slot: int, material: int) -> int:
base = 20
- slot = domain * 16 + family_slot * 4 + (material % 4)
+ slot = domain * 20 + family_slot * 5 + (material % 5)
return base + (slot % 64)Note: This changes the slot stride and may require adjusting other constants if pane distribution matters. Alternatively, if the compression is intentional, document that fire maps to the void slot.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@agents/studio_compositor/aoa_heatmap.py` around lines 130 - 133, The function
_pane_ordinal_depth2 currently compresses material indices with (material % 4),
which maps MATERIAL_INDEX entry 4 (fire) to the same slot as 0 (void); update
the slot calculation in _pane_ordinal_depth2 to use a stride of 5 (e.g., replace
material % 4 with material % 5 or change the family_slot*4 stride to
family_slot*5) so each material index 0–4 maps to a distinct pane; ensure any
other constants relying on a 4-wide packing are adjusted or documented if you
choose to keep compression.
| def __init__( | ||
| self, | ||
| *, | ||
| pool_root: Path | str | None = None, | ||
| aesthetic_tags: list[str] | tuple[str, ...] = DEFAULT_SIERPINSKI_TAGS, | ||
| max_content_risk: ContentRisk = DEFAULT_MAX_CONTENT_RISK, | ||
| ) -> None: | ||
| self._running = False | ||
| self._thread: threading.Thread | None = None | ||
| self._active_slot = 0 | ||
| self._selector = LocalVisualPoolSelector( | ||
| root=pool_root, | ||
| aesthetic_tags=aesthetic_tags, | ||
| max_content_risk=max_content_risk, | ||
| ) | ||
| self._egress_gate = EgressManifestGate(producer_id="studio_compositor.aoa_loader") | ||
| self.video_slots = [VisualPoolSlotStub(i, self._selector) for i in range(VIDEO_SLOT_COUNT)] |
There was a problem hiding this comment.
Missing instance attribute declarations for director references.
_director, _twitch_director, and _structural_director are conditionally assigned in _start_director() but not declared in __init__. This could cause AttributeError if accessed before initialization completes or if startup is skipped/fails.
Proposed fix
def __init__(
self,
*,
pool_root: Path | str | None = None,
aesthetic_tags: list[str] | tuple[str, ...] = DEFAULT_SIERPINSKI_TAGS,
max_content_risk: ContentRisk = DEFAULT_MAX_CONTENT_RISK,
) -> None:
self._running = False
self._thread: threading.Thread | None = None
self._active_slot = 0
+ self._director: DirectorLoop | None = None
+ self._twitch_director: TwitchDirector | None = None
+ self._structural_director: StructuralDirector | None = None
self._selector = LocalVisualPoolSelector(Note: You may need forward references or TYPE_CHECKING imports to avoid circular imports.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@agents/studio_compositor/aoa_loader.py` around lines 91 - 107, The
constructor __init__ neglects to declare the instance attributes that
_start_director() sets ( _director, _twitch_director, _structural_director ),
which can lead to AttributeError if those are accessed before startup or if
startup fails; declare and initialize these attributes in __init__ (e.g.,
self._director = None, self._twitch_director = None, self._structural_director =
None) with appropriate typing hints or forward refs (use TYPE_CHECKING imports
if needed) so callers and linters know the attributes exist even before
_start_director() runs.
| def stop(self) -> None: | ||
| self._running = False |
There was a problem hiding this comment.
Incomplete stop() — directors started by this loader are not stopped.
stop() sets _running = False but does not stop _director, _twitch_director, or _structural_director that were started in _start_director(). This could leave background threads running after the loader is stopped.
Proposed fix
def stop(self) -> None:
self._running = False
+ if hasattr(self, "_director") and self._director is not None:
+ self._director.stop()
+ if hasattr(self, "_twitch_director") and self._twitch_director is not None:
+ self._twitch_director.stop()
+ if hasattr(self, "_structural_director") and self._structural_director is not None:
+ self._structural_director.stop()Note: If you adopt the __init__ fix declaring these as None, replace hasattr checks with simple is not None checks.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@agents/studio_compositor/aoa_loader.py` around lines 186 - 187, stop()
currently only flips _running and fails to stop directors started in
_start_director(); update stop() to call stop/cleanup on self._director,
self._twitch_director, and self._structural_director (if they exist) and then
clear them (or join/wait if those directors expose join/await methods) to ensure
background threads exit. Also initialize those attributes to None in __init__
(or ensure they always exist) and replace any hasattr checks with explicit "is
not None" checks when guarding calls to
_director/_twitch_director/_structural_director; reference the
_start_director(), stop(), _director, _twitch_director, and _structural_director
symbols when making the changes.
| try: | ||
| VIDEO_ATTENTION_PATH.parent.mkdir(parents=True, exist_ok=True) | ||
| with tempfile.NamedTemporaryFile( | ||
| mode="wb", | ||
| dir=str(VIDEO_ATTENTION_PATH.parent), | ||
| prefix=".video-attention.", | ||
| suffix=".tmp", | ||
| delete=False, | ||
| ) as fh: | ||
| fh.write(payload) | ||
| tmp_path = fh.name | ||
| os.replace(tmp_path, VIDEO_ATTENTION_PATH) | ||
| except OSError: | ||
| log.debug("video_attention publish failed", exc_info=True) |
There was a problem hiding this comment.
Orphaned temp file on os.replace failure.
If os.replace raises OSError, the temp file at tmp_path is not cleaned up and will remain in the SHM directory. Over time, repeated failures could accumulate orphaned .video-attention.*.tmp files.
Proposed fix to clean up temp file on failure
os.replace(tmp_path, VIDEO_ATTENTION_PATH)
except OSError:
log.debug("video_attention publish failed", exc_info=True)
+ try:
+ os.unlink(tmp_path)
+ except OSError:
+ pass🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@agents/studio_compositor/aoa_renderer.py` around lines 381 - 394, The current
block can leave orphaned temp files if os.replace raises OSError because
tmp_path is never cleaned up; modify the try/except so tmp_path is tracked
(e.g., initialize tmp_path = None before the with), and in the exception handler
(or a finally) attempt to os.unlink(tmp_path) if tmp_path is set and the file
exists; ensure you only call os.unlink after ensuring tmp_path is not None and
catch/remove any OSError from the unlink to avoid masking the original error.
Use the existing VIDEO_ATTENTION_PATH code path and tmp_path (from the
NamedTemporaryFile) to locate and remove the orphaned .video-attention.*.tmp
file.
| /// Direct SHM/V4L2 output for the scene (bypasses Reverie). | ||
| scene_shm: ShmOutput, |
There was a problem hiding this comment.
scene_shm is allocated but never used to publish scene frames.
Lines 159/212/239 add the direct scene SHM path, but render_frame() never copies scene.output_texture() into scene_shm or calls scene_shm.write_frame. So the “direct scene output” path is currently non-functional.
Suggested wiring
@@
if let Some(mut scene) = self.scene_renderer.take() {
scene.render(
&self.device,
&self.queue,
time,
Some(&self.content_source_mgr),
);
+
+ // Publish raw scene output to dedicated SHM sink.
+ let mut scene_encoder =
+ self.device
+ .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+ label: Some("scene_shm_copy"),
+ });
+ self.scene_shm
+ .copy_to_staging(&mut scene_encoder, scene.output_texture());
+ self.queue.submit(std::iter::once(scene_encoder.finish()));
+ self.scene_shm.write_frame(&self.device);
self.pipeline.set_live_texture_override(
&self.device,
&self.queue,
scene.output_texture(),Also applies to: 212-213, 239-239
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hapax-logos/src-imagination/src/headless.rs` around lines 159 - 160, The
field scene_shm (ShmOutput) is allocated but never used to publish scene frames;
update the render pipeline in render_frame() to copy the rendered texture into
the SHM buffer and call scene_shm.write_frame so the direct scene output path
works. Specifically, after the scene is rendered (use scene.output_texture() or
the texture returned by Scene::render), map or blit the texture into the
scene_shm pixel buffer using the same format/stride used by ShmOutput, then call
scene_shm.write_frame() (or the equivalent method on ShmOutput) with the
populated buffer and proper timestamp/frame metadata; ensure this logic mirrors
how intermediate/primary outputs are handled and guard it behind the existing
scene_shm presence checks so the branch only runs when scene_shm is enabled.
| if isinstance(node, (ast.Import, ast.ImportFrom)): | ||
| names = [alias.name for alias in node.names] | ||
| assert "subprocess" not in names, f"{path} imports subprocess" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify the current check misses ImportFrom(module="subprocess")
python - <<'PY'
import ast
sample = "from subprocess import run\nimport subprocess as sp\n"
tree = ast.parse(sample)
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)):
names = [a.name for a in node.names]
legacy_passes = ("subprocess" not in names)
print(type(node).__name__, "module=", getattr(node, "module", None), "names=", names, "legacy_passes=", legacy_passes)
PY
# Expected: ImportFrom shows module=subprocess and legacy_passes=True (false negative).Repository: hapax-systems/hapax-council
Length of output: 199
Catch from subprocess import ... in the AST guard
Line 43–45 only checks alias.name; from subprocess import run has alias.name == "run" so it currently passes. For ast.ImportFrom, also assert on node.module == "subprocess".
Suggested fix
- for node in ast.walk(tree):
- if isinstance(node, (ast.Import, ast.ImportFrom)):
- names = [alias.name for alias in node.names]
- assert "subprocess" not in names, f"{path} imports subprocess"
+ for node in ast.walk(tree):
+ if isinstance(node, ast.Import):
+ names = [alias.name.split(".")[0] for alias in node.names]
+ assert "subprocess" not in names, f"{path} imports subprocess"
+ elif isinstance(node, ast.ImportFrom):
+ assert node.module != "subprocess", f"{path} imports subprocess"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/studio_compositor/test_aoa_no_yt_extraction.py` around lines 43 - 45,
The AST import guard only checks alias.name which misses "from subprocess import
run"; update the check in the loop over node (used for ast.Import and
ast.ImportFrom) to also inspect node.module when node is an ast.ImportFrom and
assert node.module != "subprocess" (or fail with the same message referencing
path) so that both plain imports and from-imports from the subprocess module are
caught; refer to the existing symbols node, ast.Import, ast.ImportFrom,
alias.name, names, node.module and path to locate and modify the assertion
logic.
| # Spec invariant: the centre of every inscribed rect must be | ||
| # untouched (alpha = 0). Use Sierpinski's geometry cache to resolve | ||
| # rect positions at this canvas size. | ||
| from agents.studio_compositor.sierpinski_renderer import SierpinskiCairoSource | ||
| from agents.studio_compositor.aoa_renderer import AoaCairoSource | ||
|
|
||
| geom = SierpinskiCairoSource().geometry_cache(target_depth=2, canvas_w=1280, canvas_h=720) | ||
| geom = AoaCairoSource().geometry_cache(target_depth=2, canvas_w=1280, canvas_h=720) |
There was a problem hiding this comment.
Update stale inline comment to match AoA geometry source.
The comment still says “Use Sierpinski's geometry cache,” but the code now uses AoaCairoSource, which is confusing during debugging.
Suggested edit
- # untouched (alpha = 0). Use Sierpinski's geometry cache to resolve
+ # untouched (alpha = 0). Use AoA geometry cache to resolve🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/studio_compositor/test_geal_source.py` around lines 360 - 365, Update
the stale inline comment that mentions "Sierpinski's geometry cache" to
accurately reference the AoA geometry source used here: change the comment to
state that the centre invariant is resolved via AoaCairoSource's geometry cache;
locate the comment above the import/usage of AoaCairoSource and geometry_cache
and replace the wording so it explicitly mentions
AoaCairoSource().geometry_cache (target_depth=2, canvas_w=1280, canvas_h=720)
instead of Sierpinski's geometry cache.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e0a6aa0eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| f.seek(self._cursor) | ||
| lines = f.readlines() | ||
| self._cursor = f.tell() |
There was a problem hiding this comment.
Reset cursor when log file shrinks
_read_new_impingements stores a byte offset and seeks to it unconditionally, but it never handles file truncation/recreation. When /dev/shm/hapax-dmn/impingements.jsonl (or similarly the recruitment log) is rotated or rewritten, the saved cursor can point past EOF, so subsequent reads return no lines and the heatmap silently stops ingesting new events until the file grows beyond the stale offset. Add a size/inode check and reset the cursor to 0 when the file has been replaced or shrunk.
Useful? React with 👍 / 👎.
| let shading = 0.55 + ndotl * 0.45 * shadow; | ||
| var sphere_color = rev_content * shading + rim; | ||
| let sphere_alpha = clamp(0.88 + fresnel * 0.08, 0.86, 0.95); | ||
| return FragOutput(vec4<f32>(sphere_color, sphere_alpha), 0.999); |
There was a problem hiding this comment.
Use ray-marched depth for insphere fragments
The shader computes sphere_depth from the ray-sphere hit point but returns a hardcoded fragment depth of 0.999, so every insphere pixel is written at nearly the far plane regardless of actual geometry. This breaks depth-correct occlusion as the camera moves (the sphere cannot properly interleave with other 3D content) and defeats the purpose of the hit-point projection done just above; return the computed depth instead of a constant.
Useful? React with 👍 / 👎.
Anchors with near-vertical direction from AoA centroid (vertical_ratio > 0.7) now get a lateral spread applied before distance normalization. This prevents content from clustering directly below the AoA and intersecting the floor plane. Previously anchor 6 (cube-vertex B-dual, directly below centroid) would be placed at y=-3.0 after scaling, well below the floor at y=-2.0. Now it gets pushed sideways, staying at the correct radial distance while keeping a visible position above floor level. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Auto-fix classified CI failure categories as |
…am res Replace 30-point tetrahedral scatter with two-band concentric arc layout designed for the 2D projection the viewer actually sees. - Band 1 (inner, r=3.8): HIGH-entropy sources (cameras, IR, CBIP) at height 0.56, full opacity. Spread across full circle. - Band 2 (outer, r=4.6): MEDIUM/LOW sources (wards, tickers) at height 0.44, 0.72 opacity. Offset rotation for visual separation. Key insight from operator: the tetrahedral scatter was mathematically principled but visually illegible — "postage stamps scattered randomly." Content must be large enough to read at 1080p and arranged so the projection makes spatial sense to the viewer. The tetrahedral anchor system remains as infrastructure (scene_anchors(), classify_source_entropy(), mandala zones) for future use by the tensegrity breathing system. The arc layout is the immediate fix for viewer legibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
hapax-logos/crates/hapax-visual/src/scene.rs (1)
116-140:⚠️ Potential issue | 🟠 Major | ⚡ Quick winClamp scaled anchors back inside the room envelope.
Line 116 and Line 117 add explicit floor/ceiling limits, but
scale_anchor_outward()never applies them. The bottom HIGH anchor seeded fromVec3::new(0.000, -1.875, -2.340)currently resolves to roughlyy = -2.37, well belowANCHOR_FLOOR_Y = -1.40, so the new spread logic can still drive quads through the floor.Suggested fix
fn scale_anchor_outward(local: Vec3, role: AnchorRole) -> Vec3 { let mut dir = local - AOA_CENTROID; let dist = dir.length(); if dist < 0.001 { return local; @@ - if dist < target_min { - AOA_CENTROID + dir.normalize() * target_min + let scaled = if dist < target_min { + AOA_CENTROID + dir.normalize() * target_min } else { - AOA_CENTROID + dir.normalize() * dist - } + AOA_CENTROID + dir.normalize() * dist + }; + Vec3::new( + scaled.x, + scaled.y.clamp(ANCHOR_FLOOR_Y, ANCHOR_CEILING_Y), + scaled.z, + ) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hapax-logos/crates/hapax-visual/src/scene.rs` around lines 116 - 140, The scaled anchor can end up outside the room vertical bounds because scale_anchor_outward computes a new position but never applies ANCHOR_FLOOR_Y / ANCHOR_CEILING_Y; modify scale_anchor_outward so after computing the candidate position (the AOA_CENTROID + dir.normalize() * target_min or * dist) you clamp its y component between ANCHOR_FLOOR_Y and ANCHOR_CEILING_Y before returning. Update the final return path in scale_anchor_outward (the branches that currently return AOA_CENTROID + dir.normalize() * target_min and AOA_CENTROID + dir.normalize() * dist) to produce a Vec3, clamp that Vec3.y, and return the clamped Vec3; keep usage of AOA_CENTROID, dir, target_min unchanged.hapax-logos/crates/hapax-visual/src/scene_renderer.rs (1)
937-1001:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftParameterize/enforce
SceneRendererpost-process render target format.
scene_renderer.rshardcodes bothentity_restore_pipelineandblit_pipelinetoColorTargetState { format: wgpu::TextureFormat::Rgba8UnormSrgb, ... }, butrestore_entities()/blit_scene_to_target()accept arbitrary&wgpu::TextureViewand bindtargetdirectly as the render pass color attachment (RenderPassColorAttachment { view: target, ... }). Passing a view backed by any otherTextureFormatwill hit wgpu validation failures at runtime. Derive the pipeline target format from the actual target texture (or make the required format an explicit constructor/API input).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hapax-logos/crates/hapax-visual/src/scene_renderer.rs` around lines 937 - 1001, The pipelines entity_restore_pipeline and blit_pipeline are hardcoded to Rgba8UnormSrgb but restore_entities() and blit_scene_to_target() accept arbitrary TextureView leading to wgpu validation errors; fix by making the required render-target format explicit (e.g. add a SceneRenderer field like target_format: wgpu::TextureFormat set in the SceneRenderer constructor or pass a wgpu::TextureFormat into the constructor/factory) and use that value when creating the ColorTargetState format for entity_restore_pipeline and blit_pipeline, and validate (or document) that restore_entities()/blit_scene_to_target() only receive TextureViews backed by that format (or alternatively create per-format pipelines on demand using the provided format).
🧹 Nitpick comments (2)
hapax-logos/crates/hapax-visual/src/scene.rs (1)
1081-1084: ⚡ Quick winMake overflow fallback order role-aware.
Line 1081 uses the same
Low -> Medium -> Highoverflow chain for every role, so exhausted HIGH sources skip unused MEDIUM anchors and MEDIUM/LOW also pay duplicate rescans of roles they've already tried. A smallmatch rolekeeps overflow deterministic and preserves the intended hierarchy.Suggested fix
- let ai = assign_anchor(&anchors, role, &anchor_used) - .or_else(|| assign_anchor(&anchors, AnchorRole::Low, &anchor_used)) - .or_else(|| assign_anchor(&anchors, AnchorRole::Medium, &anchor_used)) - .or_else(|| assign_anchor(&anchors, AnchorRole::High, &anchor_used)); + let ai = match role { + AnchorRole::High => assign_anchor(&anchors, AnchorRole::High, &anchor_used) + .or_else(|| assign_anchor(&anchors, AnchorRole::Medium, &anchor_used)) + .or_else(|| assign_anchor(&anchors, AnchorRole::Low, &anchor_used)), + AnchorRole::Medium => assign_anchor(&anchors, AnchorRole::Medium, &anchor_used) + .or_else(|| assign_anchor(&anchors, AnchorRole::Low, &anchor_used)) + .or_else(|| assign_anchor(&anchors, AnchorRole::High, &anchor_used)), + AnchorRole::Low => assign_anchor(&anchors, AnchorRole::Low, &anchor_used) + .or_else(|| assign_anchor(&anchors, AnchorRole::Medium, &anchor_used)) + .or_else(|| assign_anchor(&anchors, AnchorRole::High, &anchor_used)), + };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hapax-logos/crates/hapax-visual/src/scene.rs` around lines 1081 - 1084, The overflow fallback currently always tries Low -> Medium -> High for every role when computing ai via assign_anchor(&anchors, role, &anchor_used), causing skipped/duplicate rescans; change the chain to be role-aware by matching on role and ordering the or_else fallback calls accordingly (e.g., for AnchorRole::High try High -> Medium -> Low; for Medium try Medium -> High -> Low; for Low try Low -> Medium -> High), keeping the same assign_anchor, anchors, anchor_used identifiers and assigning the result back to ai.hapax-logos/crates/hapax-visual/src/scene_renderer.rs (1)
1266-1301: ⚡ Quick win
upload_yt_jpeg_if_freshstill does the full hot-path work.Line 1268 rereads the shm file, Line 1269 re-decodes the JPEG, and Lines 1295-1299 reupload the full texture on every call. Reusing the
wgpu::Texturefixes the leak, but it doesn't make this fresh-only yet. Cache the last observed mtime/sequence and return early when the frame file hasn't advanced.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hapax-logos/crates/hapax-visual/src/scene_renderer.rs` around lines 1266 - 1301, The method upload_yt_jpeg_if_fresh currently always reads and decodes YT_FRAME_PATH and writes the texture; instead stat the file first (e.g. with std::fs::metadata) and compare its modification time/sequence to a cached field on the struct (add a field like yt_sphere_mtime or yt_frame_seq on self), return early if unchanged, and only call std::fs::read, turbojpeg::decompress and queue.write_texture when the mtime/sequence has advanced; keep the existing texture reuse logic (self.yt_sphere_texture, yt_sphere_w/yt_sphere_h) but avoid re-decoding and reuploading when the file is unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@hapax-logos/crates/hapax-visual/src/scene_renderer.rs`:
- Around line 937-1001: The pipelines entity_restore_pipeline and blit_pipeline
are hardcoded to Rgba8UnormSrgb but restore_entities() and
blit_scene_to_target() accept arbitrary TextureView leading to wgpu validation
errors; fix by making the required render-target format explicit (e.g. add a
SceneRenderer field like target_format: wgpu::TextureFormat set in the
SceneRenderer constructor or pass a wgpu::TextureFormat into the
constructor/factory) and use that value when creating the ColorTargetState
format for entity_restore_pipeline and blit_pipeline, and validate (or document)
that restore_entities()/blit_scene_to_target() only receive TextureViews backed
by that format (or alternatively create per-format pipelines on demand using the
provided format).
In `@hapax-logos/crates/hapax-visual/src/scene.rs`:
- Around line 116-140: The scaled anchor can end up outside the room vertical
bounds because scale_anchor_outward computes a new position but never applies
ANCHOR_FLOOR_Y / ANCHOR_CEILING_Y; modify scale_anchor_outward so after
computing the candidate position (the AOA_CENTROID + dir.normalize() *
target_min or * dist) you clamp its y component between ANCHOR_FLOOR_Y and
ANCHOR_CEILING_Y before returning. Update the final return path in
scale_anchor_outward (the branches that currently return AOA_CENTROID +
dir.normalize() * target_min and AOA_CENTROID + dir.normalize() * dist) to
produce a Vec3, clamp that Vec3.y, and return the clamped Vec3; keep usage of
AOA_CENTROID, dir, target_min unchanged.
---
Nitpick comments:
In `@hapax-logos/crates/hapax-visual/src/scene_renderer.rs`:
- Around line 1266-1301: The method upload_yt_jpeg_if_fresh currently always
reads and decodes YT_FRAME_PATH and writes the texture; instead stat the file
first (e.g. with std::fs::metadata) and compare its modification time/sequence
to a cached field on the struct (add a field like yt_sphere_mtime or
yt_frame_seq on self), return early if unchanged, and only call std::fs::read,
turbojpeg::decompress and queue.write_texture when the mtime/sequence has
advanced; keep the existing texture reuse logic (self.yt_sphere_texture,
yt_sphere_w/yt_sphere_h) but avoid re-decoding and reuploading when the file is
unchanged.
In `@hapax-logos/crates/hapax-visual/src/scene.rs`:
- Around line 1081-1084: The overflow fallback currently always tries Low ->
Medium -> High for every role when computing ai via assign_anchor(&anchors,
role, &anchor_used), causing skipped/duplicate rescans; change the chain to be
role-aware by matching on role and ordering the or_else fallback calls
accordingly (e.g., for AnchorRole::High try High -> Medium -> Low; for Medium
try Medium -> High -> Low; for Low try Low -> Medium -> High), keeping the same
assign_anchor, anchors, anchor_used identifiers and assigning the result back to
ai.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c4c8c806-bc65-40b4-abe6-1d095eb64a49
📒 Files selected for processing (2)
hapax-logos/crates/hapax-visual/src/scene.rshapax-logos/crates/hapax-visual/src/scene_renderer.rs
|
Auto-fix classified CI failure categories as |
…munication above, grounding below Replace arc scatter with four-region semantic layout that communicates Hapax's functional architecture to the viewer: - LEFT: cameras + IR (perception — what Hapax sees). 2-column grid. - RIGHT: wards + data (cognition — what Hapax thinks). 2-column grid. - ABOVE: tickers, chat, programme context (communication — Hapax speaking). - BELOW: provenance, precedent, evidence (grounding — epistemic floor). - CENTER: AoA tetrix (Hapax's self-representation). The spatial organization is now legible at 1080p and communicates function, not arbitrary geometry. Camera feeds are large enough to identify. Ward text is readable. The layout tells the viewer: this system has perception, cognition, communication, and grounding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Auto-fix classified CI failure categories as |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
hapax-logos/crates/hapax-visual/src/scene.rs (1)
2089-2090: ⚡ Quick winFiniteness-only assertions are too weak for layout-regression protection.
These checks pass even when nodes collapse onto AoA; add a minimum separation invariant here (as done in nearby tests) to preserve geometric intent.
Also applies to: 2098-2099
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hapax-logos/crates/hapax-visual/src/scene.rs` around lines 2089 - 2090, The finiteness-only assertion around ir.position (the check ir.position.is_finite()) is too weak — add a minimum-separation invariant to prevent nodes collapsing onto each other/AoA: compute the Euclidean distance between ir.position and the relevant anchor/neighbor position(s) (use the same distance helper used in nearby tests or Vec2::distance) and assert it exceeds a named constant like MIN_IR_SEPARATION (pick a sensible value from nearby tests or define it close by). Replace the plain is_finite() assertion with a combined check (is_finite() && distance > MIN_IR_SEPARATION) and update the error message to mention the minimum separation; apply the same change to the other occurrence that mirrors this check (the second ir.position assertion).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@hapax-logos/crates/hapax-visual/src/scene.rs`:
- Around line 1079-1085: The communication branch currently checks
id.contains("ticker") etc. before the grounding-related keywords, causing IDs
like "precedent_ticker" to be captured by communication; fix by ensuring
grounding-related matches (e.g., id.contains("provenance") ||
id.contains("precedent") || id.contains("chronicle") || id.contains("pressure"))
are evaluated before the communication branch or by adding an exclusion to the
communication condition to skip when grounding keywords are present; update the
logic around the same id checks that push into communication (and the
counterpart grounding push) so grounding ids are classified into the grounding
collection rather than being shadowed by the ticker/chat branch.
---
Nitpick comments:
In `@hapax-logos/crates/hapax-visual/src/scene.rs`:
- Around line 2089-2090: The finiteness-only assertion around ir.position (the
check ir.position.is_finite()) is too weak — add a minimum-separation invariant
to prevent nodes collapsing onto each other/AoA: compute the Euclidean distance
between ir.position and the relevant anchor/neighbor position(s) (use the same
distance helper used in nearby tests or Vec2::distance) and assert it exceeds a
named constant like MIN_IR_SEPARATION (pick a sensible value from nearby tests
or define it close by). Replace the plain is_finite() assertion with a combined
check (is_finite() && distance > MIN_IR_SEPARATION) and update the error message
to mention the minimum separation; apply the same change to the other occurrence
that mirrors this check (the second ir.position assertion).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 559565c6-33cd-41cd-a8e3-df7aefc2dd66
📒 Files selected for processing (1)
hapax-logos/crates/hapax-visual/src/scene.rs
| } else if id.contains("ticker") || id.contains("chat") || id.contains("programme") | ||
| || id.contains("activity") || id.contains("impingement") | ||
| { | ||
| communication.push(idx); | ||
| } else if id.contains("provenance") || id.contains("precedent") | ||
| || id.contains("chronicle") || id.contains("pressure") | ||
| { |
There was a problem hiding this comment.
Grounding classifier is shadowed by the ticker classifier.
IDs like precedent_ticker/chronicle_ticker match the communication branch first, so they never reach grounding classification even when they carry grounding semantics.
Suggested fix
- } else if id.contains("ticker") || id.contains("chat") || id.contains("programme")
- || id.contains("activity") || id.contains("impingement")
- {
- communication.push(idx);
- } else if id.contains("provenance") || id.contains("precedent")
+ } else if id.contains("provenance") || id.contains("precedent")
|| id.contains("chronicle") || id.contains("pressure")
{
grounding.push(idx);
+ } else if id.contains("ticker") || id.contains("chat") || id.contains("programme")
+ || id.contains("activity") || id.contains("impingement")
+ {
+ communication.push(idx);
} else {
cognition.push(idx);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } else if id.contains("ticker") || id.contains("chat") || id.contains("programme") | |
| || id.contains("activity") || id.contains("impingement") | |
| { | |
| communication.push(idx); | |
| } else if id.contains("provenance") || id.contains("precedent") | |
| || id.contains("chronicle") || id.contains("pressure") | |
| { | |
| } else if id.contains("provenance") || id.contains("precedent") | |
| || id.contains("chronicle") || id.contains("pressure") | |
| { | |
| grounding.push(idx); | |
| } else if id.contains("ticker") || id.contains("chat") || id.contains("programme") | |
| || id.contains("activity") || id.contains("impingement") | |
| { | |
| communication.push(idx); | |
| } else { | |
| cognition.push(idx); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hapax-logos/crates/hapax-visual/src/scene.rs` around lines 1079 - 1085, The
communication branch currently checks id.contains("ticker") etc. before the
grounding-related keywords, causing IDs like "precedent_ticker" to be captured
by communication; fix by ensuring grounding-related matches (e.g.,
id.contains("provenance") || id.contains("precedent") ||
id.contains("chronicle") || id.contains("pressure")) are evaluated before the
communication branch or by adding an exclusion to the communication condition to
skip when grounding keywords are present; update the logic around the same id
checks that push into communication (and the counterpart grounding push) so
grounding ids are classified into the grounding collection rather than being
shadowed by the ticker/chat branch.
Grounding sources at y=-1.60 were projecting onto the translucent floor grid (y=-2.0), creating a false mirroring effect. Raised to y=-1.15 and pushed z forward so they read as content above the floor, not embedded in it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Auto-fix classified CI failure categories as |
Grounding sources were placed behind the AoA (z=-2.76) where the translucent floor grid draws over them from the camera's perspective, creating a mirroring artifact regardless of y position. Moved to z=-1.26 (in front of AoA, between camera and tetrix) so the floor grid is behind them, not in front. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Auto-fix classified CI failure categories as |
Merge communication, cognition, and grounding into a single right-side column at the same depth as cameras (cam_z). Eliminates the floor-grid overlay artifact that persisted through two fix attempts. The four-region model (perception/cognition/communication/grounding) was creating three different depth bands which each had floor interaction problems. The bilateral model (cameras left, everything else right, AoA center) is simpler, more legible, and artifact-free. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Auto-fix classified CI failure categories as |
… room Spread content across the full room volume instead of cramming into narrow columns near the AoA: - LEFT WALL (x=-4.5 to -5.9): cameras in 2-col grid, y range ±1.2 - RIGHT WALL (x=4.0 to 6.4): wards in 3-col grid, y range ±1.2 - TOP (y=1.6-1.95): communication, spread horizontally near ceiling - FRONT-BOTTOM (y=-1.2, z forward): grounding, above floor plane Room is 30 units wide — use it. No more floor-plane clipping because nothing extends below y=-1.5. The semantic organization is maintained (perception left, cognition right, communication above, grounding front) while using the spatial depth the room provides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Auto-fix classified CI failure categories as |
|
Auto-fix classified CI failure categories as |
Replace the ray-marched cylinder (which caused severe perspective warping, invisible walls, and backward-ray artifacts) with 8 flat octagonal wall panels + 4 level platforms. Architecture: - 8 wall panels (45° each, radius 6, full tower height) - 4 level platforms at y=1, 4, 7, 10 (the cornices become floors) - Ground floor (quad 0) and ceiling retained - Light marker + 4 volumetric beams renumbered to quads 13-17 - AoA insphere at quad 18 Camera: - Gentle pendulum (up then down, no snap) - Rises from y=1 to y=8, descends back - 120-150s period, one revolution - Targets always look at AoA height (y=5.5) Content fixes: - rot_y corrected: theta+PI (was atan2(cos,-sin), 90° wrong) - All content now faces inward toward the tower axis The flat panels are proven technology (floor/ceiling already work perfectly). No ray-marching, no backward rays, no billboard coverage gaps. The viewer sees actual wall surfaces, not thin lines in a void. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Auto-fix classified CI failure categories as |
Level platforms are now spiral ramp segments — wide tilted quads that hug the outer wall (r=2.5 to 5.7), leaving the central void open. Each segment covers 90° of arc and rises 3 units (one level height). 4 segments create a continuous ascending spiral ramp from y=-2 to y=10. The ramp is the visible path the camera follows through the tower. The open center void means you can look down/up through the tower shaft at any point, seeing the AoA and other levels. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Auto-fix classified CI failure categories as |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Auto-fix classified CI failure categories as |
Ramp segments now follow the octagonal wall geometry — each plank aligns with a pair of wall panels (90° arc), outer edge flush with the wall. Inner edge at r=2.0 leaves the central void open. Each plank rises 1.5 units across its width, creating a gentle slope. 4 planks cover the tower's vertical extent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Auto-fix classified CI failure categories as |
Each shelf is a 3.5×3.0 rectangle positioned tangent to the wall at y=0, 3, 6, 9. Outer edge flush with wall (r=5.5), inner edge at r=2.5 leaving central void open. Slight 0.5-unit tilt for ramp feel. Rotated 90° per shelf (0°, 90°, 180°, 270°) creating a spiral staircase of wide flat platforms ascending the tower. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Auto-fix classified CI failure categories as |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Auto-fix classified CI failure categories as |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Called by health monitor timer, not static import path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Auto-fix classified CI failure categories as |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Auto-fix classified CI failure categories as |
|
Governed stale-PR reconciliation note (task |
Summary
Research basis
8 parallel research agents produced converging evidence across:
6 REQ specs written: REQ-20260522-scroom-{tetrahedral-spatial-formalism, depth-of-field-focus-pull, enactive-camera-system, tensegrity-layout-breathing, tetrahedral-grid-projection, deliberate-occlusion-depth-revelation}
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Refactor
Tests
cc-task: 20260522-scroom-tetrahedral-spatial-formalism