- When probing for native API methods, always verify whether a function is a module-level free function or an instance method on a class. The old
offscreen.pycheckedhasattr(_native, "render_rgba")butrender_rgbais a method onSceneinstances, not a module-level export. This pattern of dead probes can persist silently for a long time because the fallback path works. - The PyO3
#[pyo3(text_signature = "($self)")]annotation onScene.render_rgbaconfirms it takes no positional arguments beyondself-- width and height are baked in atSceneconstruction time. Always read the Rust signature before wiring up Python calls. - When removing dead code that probed nonexistent module-level functions, also clean up imports that become unused (e.g.,
_forge3d,warningsinviewer.py). - Contract tests that assert "function X does NOT exist at module level" are valuable for documenting architectural decisions and preventing future developers from re-introducing the same mistake.
- When a Rust struct has
#[pyclass]but no correspondingm.add_class::<T>()?;in the#[pymodule]init function, the class is invisible to Python even though it compiles fine. Always check that every#[pyclass]has a matching registration insrc/lib.rs. - When importing multiple types from the same module (e.g.,
crate::sdf::py), consolidate into a singleusestatement with a braced group rather than adding separateuselines per type. - Negative contract tests ("X is NOT registered") should be flipped to positive assertions ("X IS registered") when the registration is intentionally added. Also add construction tests that verify the class is not just importable but actually usable (constructible, methods callable).
- The
EXPECTED_CLASSESlist in Section 1 oftest_api_contracts.pymust be updated whenever newm.add_classregistrations are added, otherwise the parametrized existence test won't cover them.
- When Rust code is gated behind a Cargo feature flag (e.g.,
#[cfg(feature = "enable-tbn")]), you must also add that feature to the maturin build features inpyproject.toml. Otherwise the module compiles but the functions are excluded from the extension. Thepyproject.toml[tool.maturin].featureslist is the single source of truth for which features are included in the Python wheel. - PyO3
#[pyfunction]wrappers that convert Rust structs to Python dicts should use a shared helper function (e.g.,tbn_result_to_py_dict) to avoid duplicating dict-building logic across multiple wrapper functions. - When the Python wrapper (e.g.,
mesh.py) has a feature-detection guard like_HAS_TBN = hasattr(_forge3d, 'mesh_generate_cube_tbn'), simply registering the function in the pymodule is enough to flip the flag. No changes to the Python wrapper itself are needed. - For feature-gated registrations in
lib.rs, wrap related function registrations in a#[cfg(feature = "...")]block to keep them conditional, matching the module-level gating inmod.rs. - The
EXPECTED_FUNCTIONSlist in the contract tests should be updated alongside registration to maintain the contract lock.
- When adding state tracking to
Scenefor a new feature (SSGI, SSR, bloom, etc.), follow the pattern: add fields to the struct, initialize with defaults in the constructor, add#[pymethods]for enable/disable/is_enabled/set_settings/get_settings, and update the.pyitype stubs. get_*_settings()should return a dict (not a typed object) for maximum flexibility and compatibility with the Python side. Usepyo3::types::PyDict::new(py)anddict.set_item(...).- Behavior tests for Scene methods should test at the class level (
hasattr(_native.Scene, "method_name")) when instance construction is blocked by GPU/shader issues. This avoids test fragility while still asserting the API contract.
HISTORICAL (superseded 2026-07-10): CENSOR deleted
PostFxEffect,BloomEffect,PostFxResourcePool, andPostFxChainas zero-caller dead structure. The notes below document the original wiring only;BloomConfigand Scene's CPU bloom path remain live.
- The
PostFxEffect::execute()trait method needsqueue: &Queueto upload uniform data before dispatching compute passes. When modifying a trait signature, check that the only implementor isBloomEffectto avoid a wider refactor. BloomEffectdiffers fromTerrainBloomProcessorin that it must use thePostFxResourcePool's ping-pong texture pairs for intermediate storage rather than owning its own textures. Allocate pairs duringinitialize()and retrieve views duringexecute().- The bloom composite pass requires a 4-binding layout (original + bloom + output + uniforms), distinct from the 3-binding brightpass/blur layouts. This was missing from the original stub and had to be added alongside the composite pipeline and shader loading.
- Bloom default-off semantics are critical for backward compatibility:
BloomConfig::default().enabled == false, andexecute()returnsOk(())immediately when disabled. - The
PostFxChain::execute_chain()must also acceptqueueand forward it to each effect'sexecute()call.
- The Rust
PointBufferstores positions as flatVec<f32>[x,y,z,...] and colors asOption<Vec<u8>>[r,g,b,...]. Thecreate_gpu_buffer()method interleaves them into [x,y,z,r,g,b] per point, normalizing u8 colors to 0..1 f32. Default is white when no colors are present. - PyO3 bindings for simple data structs work best as thin wrapper types (e.g.,
PyPointBufferwrappingpointcloud::PointBuffer) rather than deriving#[pyclass]directly on the Rust struct, because the inner struct lacksClone/Copyand holds non-PyO3-compatible fields. - When returning numpy arrays from PyO3, prefer
PyArray1::from_vec_bound(py, data)over the deprecatedfrom_vec(py, data)in pyo3 0.21+. - Constructor validation for
PointBuffer(positions length divisible by 3, colors matching point count) prevents downstream GPU buffer mismatches. Always validate at the boundary. - The
renderer.rsfile (353 lines) is slightly over the 300-line guideline due to pre-existing duplication betweenload_copc_pointsandload_ept_points. A future refactor could extract a commonload_points_generichelper. MemoryReportseparates observation from policy: it reports cache_used, cache_budget, utilization, and entry_count without deciding what to do about high utilization. This keeps the renderer testable without GPU access.
- The
lazcrate is a transitive dependency vialas = { features = ["laz"] }but must be added as a direct optional dependency (laz = { version = "0.9", optional = true }) to use its API directly incopc_decode.rs. A marker feature alone (copc_laz = []) is insufficient sinceuse laz::...requires the crate to be a direct dependency. - When splitting a file to stay under 300 lines, the natural boundary for COPC is dataset/hierarchy (copc.rs) vs chunk decoding/parsing (copc_decode.rs). The decode module uses
pub(crate)visibility to exposedecode_chunkandparse_uncompressed_pointsonly within the crate. - For feature-gated code using
#[cfg(feature = "copc_laz")], the non-feature path must explicitly suppress unused-variable warnings usinglet _ = (data, point_count, ...)or the compiler will warn about unused parameters. - The COPC file format stores LAZ compression parameters in a VLR with user_id "laszip encoded" and record_id 22204. The existing code only read the first VLR (COPC info); the fix iterates all
num_vlrsVLRs to also capture the LAZ VLR. - The MtStHelens.laz fixture uses NAD83 State Plane Washington South coordinates (US feet), not UTM. Always check actual coordinate values before writing range assertions in tests.
- When testing LAZ decompression end-to-end from Python, the
las::Readeralready decompresses LAZ transparently. Exposing aread_laz_points_info()PyO3 function that returns (count, coords, has_rgb) provides a lightweight fixture validation without requiring a COPC-specific fixture file.
- For PyO3 bindings of simple config structs like
LabelStyle, follow the establishedPySelectionStyle/PyHighlightStylepattern: separate#[pyclass]struct with#[pyo3(get, set)]on each field, a#[new]constructor with default values matching the RustDefaultimpl, bidirectionalFromconversions, and__repr__. - When exposing
[f32; 4]color arrays to Python, convert to/from tuples(f32, f32, f32, f32)rather thanVec<f32>for consistency with other bindings (e.g.,PySelectionStyle.color). Same for[f32; 2]offsets. - For
f32::MAXdefault values in Rust, use the literal value3.4028235e38in the#[pyo3(signature)]since PyO3 doesn't evaluate Rust constants in signature defaults. - Nested PyO3 classes (e.g.,
PyLabelFlagsinsidePyLabelStyle) must also be registered withm.add_class::<>()and deriveClone. When used as a field in another#[pyclass], the#[pyo3(get, set)]attribute works seamlessly. - New
py_bindings.rssubmodules should be declared unconditionally inmod.rs(not behindcfg(feature)), with the#[cfg(feature = "extension-module")]guard on individual items inside the file. This matches the pattern used bylighting/py_bindings.rsandterrain/cog/py_bindings.rs.
- The
allow_placeholderescape hatch is gone:MapScene.rendereither draws through the native GPU-terrain path or raisesMapSceneNativeUnavailable, whose.diagnosticscarry structured{"status": "diagnostic_block", "layer", "reason", "required_native"}dicts built by_map_scene_validation.diagnostic_block. Never reintroduce a CPU placeholder branch;tests/test_mapscene_sutura_integrity.py::test_no_allow_placeholder_symbolgreps the whole package for regressions. - Depth-occlusion label culling lives ONLY in
MapScene.compile_plan(), which is a total function of the serialized recipe (CPU camera/terrain sampler, never a live GPU depth frame)._render_native_offscreen_resultrequires aCompiledScenePlanand mutates no label state;MapScene.rendercompiles on demand whencompiled_planis None. - The frozen compiled plan (label plans + per-label visibility flags keyed by a camera+terrain hash) is stored in new
RecipeManifest.compiled_label_plans/depth_cullfields.manifest_to_jsoncanonicalizes floats (allow_nan=False,-0.0 -> 0.0) soto_json -> from_json -> to_jsonround-trips byte-identically. BUNDLE_VERSIONis 3: bundles persistscene/compiled_plan.jsonandMapScene.load_bundlerehydrates it verbatim; v2 bundles (no compiled plan) are recompiled once on load.- Dataclass serialization asymmetries (
Nonevs[]from_sequence) silently break byte-identical validation reports after a bundle round-trip — normalize at the decode boundary (_layer_from_dict), e.g.bounds=data.get("bounds") or None. - When a test asserts exact equality on
last_render_metadata, pop nondeterministic timing keys (offline_accumulation_ms,timing_source) first; they come fromtime.perf_counterin_render_terrain_renderer_result.
- The terrain PT reservoir now uses the CANONICAL ReSTIR layout (
src/path_tracing/restir/types.rsReservoir/LightSample, 80-byte storage stride). When a WGSL struct must match a RustPodstruct in a storage array, verify the STRIDE, not the field list — the original ad-hocTerrainReservoirhad an 80-byte WGSL stride against a 64-byte Rust allocation and wgpu will not catch a short runtime-sized array until the last pixels write out of the bound range. - Real temporal/spatial reuse = dispatching the existing
pt_restir_temporal.wgsl/pt_restir_spatial.wgslas standalone pipelines insideHybridPathTracerwith three canonical reservoir buffers (curr candidates -> temporal merges prev+curr -> out -> spatial writes back into prev). The kernel M-clamps prev in place (cap 512) before each merge, otherwisem/w_sumgrow ~9x per spatial pass and overflow within ~40 frames. - The spatial pass needs a G-buffer (
gbuffer_nr/gbuffer_posat group(1) bindings 10/11) for target-pdf re-evaluation. For a static scene + deterministic camera, write it ONCE from a dedicated entry (main_terrain_gbuffer) with its OWN pipeline layout — keeping those two storage bindings out of the main kernel's layout is what keeps every pipeline within 8 storage buffers per compute stage. - Bind-group portability: lighting moved from group(4) to group(0)@binding(1) so all hybrid pipelines fit
max_bind_groups = 4. When a shader comment claims "consolidated to stay within max_bind_groups=4", count the actual pipeline-layout groups — the claim had drifted. HybridScene::dummy_storage_buffer()must cover ONE ELEMENT of the largest runtime-sized array it stands in for (WGSLBvhNode= 48 bytes); a 4-byte dummy fails wgpu validation with "Buffer is bound with size 4 where the shader expects N" only at dispatch time.- Memory-tracker hygiene for multi-resource render paths: wrap every tracked allocation in a Drop guard (
TrackedGpu) and implementDropon tracked wrapper types (TerrainMinMaxPyramid,TerrainPtScene) so?/early-return paths cannot leak tracker state. Gate the budget on tracker metrics captured AFTER all allocations, not onpeak_host_visible_bytesalone (device-local resources never appear in host-visible metrics). - "Variance across the last N frames" (02-prometheus.md:59) is implementable as a WINDOWED Welford over the running-mean luminance: reset at
frame_index % window == 0, gate onm2/(n-1)at window boundaries. Raw per-frame sample variance never falls for a stochastic estimator; variance of the running mean does. - Background Bash gotcha that bit this session twice:
cmd | tail -Nin a background task truncates the retained log AND masks the exit code (pipeline status = tail's). Redirect to a scratchpad file and grep it instead.
- The 2026-07-05 audit's "adding the clipmap vertex entry to the shared terrain shader crashed Vulkan pipeline validation/construction" no longer reproduces on the current wgpu/driver stack. When a backend-conditional fallback exists only because of a historical crash, re-verify the crash before building more machinery around the gate — removing the two
Backend::Vulkanchecks and rendering produced near-identical output to the recorded DX12 evidence (RGB sum 1878111 vs 1877131). - WGSL entry points cost nothing until a pipeline uses them: moving
vs_clipmap_mainfrom a Rustpush_strhack intoterrain_pbr_pom.wgslis safe for every other pipeline compiled from the same preprocessed module (vs_main, AOV, offline HDR), because wgpu validates the resource interface per entry point at pipeline creation, not per module. cargo test <filter> --libwithout--features extension-modulesilently skips the entiresrc/terrain/renderer/tree. A run that reports "N passed, hundreds filtered out" can still mean your new tests never ran — check the module paths in the reported test names, then re-run with the feature.make_ring_skirtsstitched curtain quads across row/strip boundaries ("Simplified: add all for now"), creating triangles spanning the whole ring — caught bytests/test_geomorph_seams.py(max edge 4360 on a 1000 m extent). Skirt adjacency must be row-aware: passrow_width = resolution + 1and skip pairs wherei % row_width == 0. When a generator comment says "simplified for now", assume a test somewhere is already red because of it.- The fixed-LOD
HeightMosaicmode (slot = tile coords) makes the atlas a direct geographic mosaic that binds asheight_texwith plain [0,1] UVs — no page-table indirection needed in the mega-shader. Coarse-prefill (low-res read upsampled per tile at enable time) is what turns "tiles in flight" into "coarse terrain" instead of holes. ClipmapStreamer::update()only emits center/corner ring tiles, so camera-driven demand alone never converges full residency; pair it with a nearest-first top-up loop throughAsyncTileLoader::request()(which already dedups and enforces max-in-flight backpressure).- Clipmap vertex UVs are world-anchored (
(world + extent/2) / extent), so recentering the ring mesh on a moving camera keeps DEM alignment for free; putting the streaming center in the geometry cache key gives regeneration-on-move with zero per-frame CPU mesh cost when stationary. - Ten P2 recipe goldens (auto_water, clipmap, cloud_shadows, copc, arabic, screen-space pair, textured_gltf, thematic, tiles3d) were explicitly listed in
.gitignore— they existed only on this machine while the plan claimed "intended baselines are committed". When a golden gate depends on files, checkgit ls-filesfor them, not just the working tree.
- CENSOR is a small architectural execution-truth contract. Routine changes preserve capability negotiation, explicit degradations, tracked allocations, enforce-by-default budgeting, certificate/schema/tamper behavior, shader-use reporting, and honest probe outcomes.
- A downstream prompt that depends on CENSOR consumes those interfaces and invariants; it does not inherit CENSOR's full closure suite.
- The stable hosted pull-request gate is
PR Core Success. Complete Python/Rust/platform matrices, production signing, candidate-selected goldens, physical GPU checks, and scratch red-proof corruption are acceptance/release evidence reported byFull Acceptance Summary, not routine branch protection. Seedocs/censor-validation-policy.md. - Production signing is required only in protected acceptance/release workflows. Routine internal and fork PRs remain explicitly untrusted and must pass canonicalization, certificate-contract, and tamper-rejection tests without
FORGE3D_CERT_SIGNING_KEY.
- Project-wide honesty pass. Earlier areas (prior tasks): capability negotiation replaced the
Features::empty()device request so the device advertises what it actually enables; the host-visible budget default flipped to ENFORCE (BUDGET_POLICY_ENFORCE), over-budget host-visible allocations now returnRenderError::Budget; a source-level allocation gate (tests/test_allocation_gate.py) forbids rawcreate_buffer/create_textureoutsidesrc/core/resource_tracker.rs; and a signedRenderCertificate+ verifier attests real render provenance. - Task 14 (dead-feature + dead-structure removal): deleted six features from
Cargo.toml [features]that had ZERO#[cfg(feature="…")]refs anywhere insrc//tests//benches//build.rs—terrain_spike,exr(the standalone marker only;images = ["dep:exr"]kept because theexrcrate is still an optional dep the AOV path uses),enable-ibl,enable-csm,enable-render-bundles,enable-memory-pools.terrain_spikeguarded nothing, so its module already compiled unconditionally and was left alone. - The current portable feature inventory is
default,async_readback,copc_laz,cog_streaming,gis-remote,geos-topology,weighted-oit,wsI_bigbuf,wsI_double_buf,enable-pbr,enable-tbn,enable-normal-mapping,enable-hdr-offscreen,enable-renderer-config,enable-staging-rings,shader-contract-asserts..github/workflows/ci.ymland.cargo/config.tomlare authoritative; routine versus full execution is defined by the current workflow scope, not this historical list. Gate (c) intests/test_no_silent_degradation.pylocks routing and consistency. - Dead structure — all confirmed zero external callers before deletion. The final closure deletes the complete legacy
PostFxChain/PostFxEffect/PostFxResourcePoolgraph and its unreachable standaloneBloomEffect, while retaining the liveBloomConfigused by Scene's real CPU postfx path. The zero-caller legacysrc/core/framegraph.rswrapper and zero-callerTonemapProcessorwere also deleted;framegraph_implremains because diagnostics exercises it, andcore::tonemap::resolve_reference_hdr_to_rgba8remains as the authoritative adjudication resolve. Earlier deletions also removed render bundles and the parallelsrc/render/memory_budget.rs; the livesrc/util/memory_budget.rsIBL estimators are untouched. - Viewer bind groups are cached by resource identity in
viewer/render/main_loop/postfx_cache.rs, invalidated on resize and IBL replacement, and the frame-looppostfx.rscontains nocreate_bind_groupcalls. Snapshot composite bind groups are created lazily only on a cache miss; routine frames allocate none. - After deleting a method, sweep the file's imports: removing
execute_chainorphanedRenderErrorandGpuTimingManagerinchain.rs, andpostfx_apply_nooporphaneduse wgpu::*;inpostfx/mod.rs. Unused-import warnings are-D warningsclippy failures, so this is not optional cleanup. - F-11 closure removed the temporary sky/fog frame-loop exemptions: their bind groups are miss-only caches, resize invalidates every cache whose texture/depth identity changes, and the routine frame files contain no bind-group or sampler creation.
- F-06 production certificates use a random Ed25519 seed stored only as the
FORGE3D_CERT_SIGNING_KEYActions secret. The tracked public key and all committed certificates rotate atomically; protected acceptance/release lanes fail without the secret or on any key/signature mismatch. Routine internal and fork PRs are explicitly marked untrusted and never require the secret. - Pre-existing red found during verification (NOT caused by Task 14):
offscreen::adjudication_raster::tests::raster_twin_is_explicitly_blocked_and_reuses_shared_infraassertedforward.rscontains the literalglobal_tracker(), which sibling CENSOR commitf488592d("drop redundant manual tracking") had removed. RESOLVED ina9f1e05b: the lock now asserts the honest new invariant (read_hdr_texture+tracked_create_texture) and the test is green in the curated matrix. - Historical red-proof note: CI runners carried a newer clippy than local stable; three lints (
iter_kv_map,manual_filter, needless&inprintln!) failed the acceptance run and were fixed at the source. This does not make scratch red-proof reproduction a routine PR requirement.
- The independent Fable 5 audit (local
docs/audits/fable5-moonshots/14-censor-implementation-audit.md;docs/audits/is git-ignored by policy) scored 12/20 requirementsfull, 8partial. Remediation closed the code-side gaps in one pass: - Golden negative control (F-02):
UPDATE_GOLDENSwas bound at import, so the control'smonkeypatch.delenvwas dead code — underFORGE3D_UPDATE_RECIPE_GOLDENS=1the "rejecting" control would have COPIED the corrupted image over the committed golden. Update-mode is now the call-time_update_goldens_enabled(), the control simulates a refresh run (setenv → delenv) and asserts the golden's bytes are untouched. Lesson: any env-driven test-mode flag read at module import defeats per-test monkeypatching. - Live
gpu_mseverywhere (F-04): newOneShotTimingwrapper incore/gpu_timing.rs(alsofor_devicefor renderers owning a non-global device, e.g. TerrainSpike). Every certified GPU render path now records live per-pass timings — vector oit/pick/fill/demo, adjudication (raster fully; PT frame-0 representative region — a wavefront frame spans multiple encoders), hybrid PT (frame-0 per pipeline), instancing, spike, offline batch (first-sample representative), debug pattern, BRDF tile (its dormant timestamp query set finally gets READ).timestamp_validis now derived (begin != 0 && end >= begin), and all record loops report invalid stamps as 0.0. Pass labels/orders unchanged ⇒ signed payloads byte-identical ⇒ committed certificates stayed valid. - Render-surface honesty (F-05):
render_brdf_tile{,_overrides}gained thecertificate=contract;test_render_certificate_contract.pynow sweeps every publicrender_*callable (forge3d +_forge3d) — each must takecertificate=or sit inDOCUMENTED_EXCLUSIONSwith a reason mirrored in its docstring. - Probe honesty (F-10):
terrain_ci_probe.pyexit codes now distinguish ABSENT (2: no CI-safe adapter → marker + job success) from CRASH (3: adapter present, smoke render raised → golden job FAILS). The probe step lost itscontinue-on-error. - Gates hardened (F-08/F-09/F-11): allocation gate adds UFCS/
create_texture_with_data/line-split patterns (immediately caught a helper false-positive — pattern narrowed toDevice::create_*); dead-structure gate greps forbidden symbols repo-wide and checks the whole frame-loop dir against a documented bind-group allowlist; a new capability gate rejectsrequest_device(&wgpu::DeviceDescriptor::default()in production code, andextrude_polygon_gpu_pynow uses the negotiated global context instead of a private default device. - Ledger cross-invariant (F-07):
finish_ledger_capturedebug-asserts equality on both axes between the ledger andResourceRegistry's exactResourceHandlesubset (the public registry totals intentionally also include estimate-only legacy bookkeeping); mid-capture ownerless allocations are counted (only pre-existing ambient entries are excluded) — both the paired and deliberately unpaired cases are unit-locked. - Historical acceptance snapshot after remediation:
cargo fmt --check/forge3d-clippy/ curatedcargo test(693 passed) / focused pytest (84 passed, 0 skipped, live RTX 3070) all green; determinism SHA unchanged across the refactor (8e397b1a…). Do not treat the full matrix or physical-GPU reproduction as a routine PR requirement.
- A hermeticity test must exercise the public scheduler output, not compare a key against a second hand-written hash of the same inputs. The real-output mutation lane exposed that
render_sequence(..., capabilities=...)discarded explicit capabilities for the built-in executor. Preserve explicit fingerprints on every executor; only probe native capabilities when an external renderer supplies none. - A warm-store index that still opens every entry's
meta.jsonbefore loading defeats the cache on Windows. Validate the fast pack in memory, compare its key set with the directory inventory, and fall back to the full metadata scan only on mismatch. Count the fast-pack payload againstmax_bytesand remove the on-disk pack before writes/GC so the optimization cannot exceed the store budget. - Cross-backend portability requires two independent physical renders. A consumer may enter the portable TERRA equivalence class only after its own output matches the committed golden and its adapter metadata proves a non-software DX12 device; otherwise emit
ABSENTrather than treating a cached producer blob as proof of consumer determinism.
- The print-only
SetSunDirectionstub was fixed, not retained as a second path: observation and manual controls both update the live lit uniform and route terrain throughsync_terrain_sun_to_lit.forge3d.sky.set_observationpersists a process-level observation across viewer restarts; a later PythonViewerHandle.set_sunclears that replay, while a native terminal override is local to its viewer. Either manual path deactivates observation control in that viewer so reloads and intensity changes cannot mix the two sources. - Night rendering uses one 64-byte
NightInstancestorage layout for the 9,096-star Yale catalog, five planets, and the textured Moon. The Moon's quad basis points toward the projected Sun, its terminator uses exact illuminated-sphere geometry, and its LRO albedo comes from the NASA SVS CGI Moon Kit. Below the horizon, the live directional-light path switches from the Sun to a Krisciunas–Schaefer (1991) phase/extinction scale; the real Sun direction remains separate for the declared SIDERA −4° to −18° twilight ramp. The fixed Mauna Kea golden is generated only by_astro_night_golden_frame, and its certificate must name both models plusastro.night.shader.
- The Moon does not get annual aberration. It shares Earth's barycentric velocity, so the +20.5″ aberration is cancelled almost exactly by the Moon's barycentric light-time displacement over the same 1.3 s; only the ~0.7″ geocentric retardation survives. Applying both was a silent double-count worth 20″ — it still passed the 30″ gate (22.30″), which is precisely why a gate with margin is not proof of a correct reduction. Fixing it took the Moon to 4.69″. The Sun keeps aberration-only (its light-time is the aberration); planets keep retarded-position + aberration.
- A gate that compares a function against a stored copy of its own output proves nothing. Two shipped this way and both were replaced: GMST was checked against three hand-written constants (now: an independently coded IAU-1982/Aoki polynomial swept over 1,694 epochs, plus a third implementation in Python inside the test), and refraction was checked against a constant that was Sæmundsson's own output at 5° (error 1.1e-10 — the tell). DoD 4 asks for the Bennett value at 5° apparent altitude, so
unrefract_altitudeinverts the Sæmundsson fit first. Same family as the ANAMNESIS "second hand-written hash" note above. - Ablation gates must run the pipeline they claim to characterise. The precession ablation measured
DVec3::Xthrough the precession matrix — it would have stayed green withprecess_j2000_to_datedeleted fromcatalog::star_instances. It now runs all 9,096 catalog stars through the real reduction chain: max 22.27′, median 18.65′, min 0.30′. The min is supposed to be near zero (precession rotates about the ecliptic pole), and asserting it proves the measurement is precession and not something else. - Source-grep tests must read the file they are about. The
set_sunstub gate read five downstream files but neveripc_command.rs; reverting that one file to itsprintln!body left the test green. - Golden bytes are backend-dependent. In-process the night render is only two-run reproducible; equality with the committed PNG holds on a pinned backend, so that claim lives in a subprocess test with
WGPU_BACKENDSset, exactly liketest_determinism_hash.py, with a SHA-256 sidecar intests/goldens/determinism/.has_gpu()is true for WARP/lavapipe, so byte-equality is additionally gated ondevice_probeproving a non-software adapter (ANAMNESIS ABSENT rule).degradations == []is likewise only true on a device granting every negotiated capability — assert the degradation kinds instead. - Two viewer bugs the golden could never catch, because the golden does not use the viewer. (1)
RENDER_ATTACHMENTwas added toviewer.lit.outputinstead ofviewer.sky.outputinresize_render_targets, so the first window resize with an active observation would fail wgpu validation; init and resize now sharesky_init::sky_output_descriptor. (2)vs_nightput quads at unit distance while the terrain camera clampsnearto 1.0 world unit, clipping the entire night sky away in any real scene — fixed with the skybox depth pin (vec4(clip.xy, 0.0, clip.w)). A golden that allocates its own camera and target validates the shader, not the integration. - Declared numbers must be gates.
MANIFEST.toml's per-asset error budgets and sha256 values are now all asserted, and the ΔT residual is gated against Horizons' ownTDB−UT/UT1−UTCcolumns rather than asserted in prose. The V-band photometric zero point was Allen's spectral flux density (3.6e−8 W m⁻² µm⁻¹) mislabelled as an irradiance — 11× too large, and invisible to every test because it cancels in the magnitude ratio the renderer uses. Now the band-integrated 3.19e−9 W m⁻², anchored by a test that the Sun's V-band irradiance stays below the solar constant. - Redistributed third-party data needs its notice shipped with it:
moon_terms.binis MIT-licensed and compiled into every wheel viainclude_bytes!, soassets/astro/THIRD_PARTY_NOTICES.mdnow carries the notice and the manifest points at it.
- Height is the fourth feedback-driven VT family.
HeightVtFamilyRuntimekeeps the R32FloatHeightMosaicphysical representation required for numeric elevation, while demand retention,TileKeyidentity, family budgets, residency accounting, publicvt_stats(), and store-backed loading use the same policy as albedo, normal, and mask. Material feedback UVs feed height demand; camera ring requests are predictive prefetch. Both caller DEMs (ReaderPageStore) and COGs (CogPageStore) enterAsyncTileLoaderthroughVirtualTextureStore::page; shipped synthetic-reader callers remain forbidden. - Material VT and COG height input share
VirtualTextureStore::page. Packed material pages stay BC7/BC5 through the staging ring into a bindless three-texture atlas when the negotiated adapter grants BC compression and descriptor indexing; raw/single-atlas paths are explicit certificate degradations.