2026-08-11. Every finding in 2026-08-10-main-bug-audit.md,
reorganized into priority tiers. That file stays the source of truth for
detail (reproductions, code snippets, confidence); this file is an index and
an ordering.
This is a reorganization, not a re-verification. Nothing here was
re-checked against current code — treat every item’s status as “reported
2026-08-10, unconfirmed since.” A day of dev/QA work has landed on top of
that snapshot (multiple QA sessions, the quality-control bluebook, entity
and query bug fixes — see qa/reports/ and recent memory), so some of these
may already be fixed, or partially fixed. Re-verify a finding before
starting work on it, and check qa/reports/ first in case it’s already
tracked (and possibly already resolved) there under a different name.
Priority within a tier follows the source audit’s own severity/confidence signals (blast radius, silence, confirmed vs. likely/possible) — it is not a new judgment call layered on top.
ID scheme: S1–S3 and H1–H14 are the source audit’s own labels.
M1–M29, L1–L24, and R1–R5 are assigned here (medium/low/Rust-parity
were unlabeled bullets in the source) — use these IDs when opening a fix or
cross-referencing qa/reports/.
75 findings total: 3 systemic, 14 high, 29 medium, 24 low, 5 Rust parity divergences.
Silent, unrecoverable, or hard-to-detect data corruption. The source audit’s own #1 priority; nothing supersedes it.
| ID | One-line | File | Confidence | Status |
|---|---|---|---|---|
| H1 | Entity commands run no argument gate — unknown args silently accepted, absent required args silently overwrite stored data with nil |
runtime/entity_interpreter.rb:27 |
confirmed | Fixed — PR #105, issue #104 |
| H2 | Era mint isn’t atomic — inner @db.transaction commits the whole mint early, releasing the advisory lock mid-flight; a later raise leaves a half-born era committed |
adapters/driven/postgres/lineage/head_compiler.rb:38 |
confirmed | Fixed — PR #165, issue #163 |
| H3 | Deleting an era-migrated record resurrects it in the head view — the ancestor era’s save row outranks the (untombstoned) delete forever |
adapters/driven/postgres.rb:192 |
likely (static) | open |
| H4 | Rekey SQL is invisible to the human-approval digest — two different rekey SQLs produce the identical digest; an approved rekey can be edited post-approval and still mint | translation/audit/approval_digest.rb |
confirmed | open |
| H5 | A dotted-member compute (e.g. price.cents) exempts the whole attribute from Layer-2’s cross-execution equivalence gate — the gate whose entire purpose is to catch silent migration data loss produces zero violations on it |
translation/audit/layer_two.rb:56 |
confirmed | open |
H1 and H2 are flagged in the source as the cheapest fixes (two missing dispatch-order steps; drop or savepoint the inner transaction).
Single points that fan out into multiple downstream bugs elsewhere in this document. Fixing the root is expected to retire (or at least shrink) the related medium-severity query findings noted below.
| ID | One-line | File | Confidence | Likely retires |
|---|---|---|---|---|
| S1 | render_value collapses every non-symbol to .to_s on the IR wire, erasing type tags — "007"→7, "true"→true, nil→"", eq: nil compiles to = '' instead of IS NULL |
query_specification/common/comparators.rb:6 |
confirmed | M1, M2, M4 (query-engine type/nil divergences) |
| S2 | h[k.to_sym] \|\| h[k] (5+ call sites) turns a stored false into nil via Ruby’s \|\|-falls-through-on-false — where(flags.active: false) never matches |
field_path.rb:44 + 4 others |
confirmed | any boolean-leaf query/ne bug not otherwise attributed |
| S3 | An aggregate’s identity, free-form unless pattern:-constrained, flows unescaped into URLs and HTML — root of H10 and H12 |
(see H10, H12) | — | H10, H12, L12 |
Before the Rust web surface (or any operator tooling) takes more untrusted exposure.
| ID | One-line | File | Confidence |
|---|---|---|---|
| H10 | action_link/index_row_html (rust/host/src/web.rs), which HTML-escape the id everywhere it’s rendered and percent-encode it in the query-string position. |
rust/host/src/web.rs (action_link, index_row_html) |
fixed |
| H11 | Session HMAC fails open on an empty SESSION_SECRET — unset, cookie/OAuth-state HMAC keys on a known empty string. Not currently exploitable (prod sets it from Secrets Manager) but fails open instead of refusing to boot, unlike every other required var. |
rust/host/src/web.rs:90 |
likely (latent) |
| L12 | Record ids are HTML-escaped but never URL-encoded in href/Location — &, +, ?, # in an id corrupt the link |
presentation/record_renderer.rb:75 |
— |
| L20 | rename-schema interpolates $(OLD)/$(NEW) unsanitized into SQL — operator-only, but against prod RDS |
bin/project_deploy:1517 |
— |
Confirmed or likely-live silent-wrong-answer bugs, grouped by subsystem. Ordered high-severity first, then the medium-severity items most likely related to (or exposed by) S1/S2 above.
Query/routing (high):
| ID | One-line | File |
|---|---|---|
| H6 | limit applied before offset in the in-memory query port — limit 10 offset 10 on 30 rows returns 0 rows instead of rows 11–20 |
ports/query/in_memory.rb:21 |
| H7 | Reference/entity query engine ignores offset, and reads fields directly instead of via FieldPath.dig — dotted where/order_by silently wrong on the only engine for entity queries |
runtime/query_interpreter.rb:78,149,261 |
| H8 | seal_defaults doesn’t cover one_of closed sets — a documented-in-its-own-comment case boots clean, then refuses every create at dispatch |
bluebook/dsl/aggregate_builder.rb:228 |
| H9 | Meta-validator cache key omits read-model filters — a same-process reload after editing only a filter serves the stale one | bluebook/meta_validator.rb:127 |
| H12 | Record ids containing . are unroutable — detail page, JSON view, and index link all 404 |
presentation/app.rb:70 |
Query engine semantics (medium — likely related to S1/S2):
| ID | One-line | File |
|---|---|---|
| M1 | ne matches nil-held rows in memory; SQL <> excludes NULL |
ports/query/in_memory.rb:33 |
| M2 | in stringifies both sides, matching across types and nil↔"" |
ports/query/in_memory.rb:37 |
| M3 | Default NULL ordering diverges between memory (SQLite convention) and Postgres | query_specification/common/null_policy.rb:16 |
| M4 | Multi-numeric value object unwraps to first-numeric in one engine, whole hash in the other — gt/lt silently false on one side |
ports/query/in_memory.rb:74 vs query_interpreter.rb:250 |
| M5 | FieldPath#read violates its “or nil, never raise” contract — raises TypeError through an Array |
query_specification/field_path.rb:45 |
Expression sublanguage (medium):
| ID | One-line | File |
|---|---|---|
| M6 | Negated membership (!names.include?(x)) is inexpressible — all three spellings raise |
bluebook/expression/evaluator.rb:69 |
| M7 | Canonical-form normalisation rewrites string-literal contents, quote-blind | bluebook/expression/canonical_form.rb:39 |
| M8 | modulo truncates floats and zero-checks before coercion → raw ZeroDivisionError |
bluebook/expression/resolver.rb:201 |
| M9 | Raw TypeErrors cross the DomainRefusal boundary |
resolver.rb:212 / evaluator.rb:72 |
IR round-trip losses (medium):
| ID | One-line | File |
|---|---|---|
| M10 | formerly_known_as and chapter-level @ports are ivars absent from to_h — lost on IR round-trip |
bluebook/ir/bluebook.rb:69 |
| M11 | correlates_by maps nil → "" on the wire |
bluebook/ir/process_manager.rb:113 |
DSL sealing / assembly (medium):
| ID | One-line | File |
|---|---|---|
| M12 | Duplicate attribute names both survive into IR — nothing refuses them | bluebook/dsl/attribute_collector.rb:34 |
| M13 | An entity’s attribute types are never resolved as references — undeclared type boots clean | meta_validator/judge.rb:260 |
| M14 | Entity query hops are not sealed — where through an undeclared aggregate boots clean |
bluebook_builder.rb:195 |
| M15 | Object-literal decoders corrupt on an embedded "; under Ruby ≥3.4 every object-literal default/binding decodes to {} |
marks.rb:171 / meta_validator/shapes.rb:94 |
| M16 | plural/singularize are not inverses for -es — has_many Boxes refuses a phantom Boxe |
naming.rb:61 + aggregate_builder.rb:88 |
Runtime (medium):
| ID | One-line | File |
|---|---|---|
| M17 | A composite-identity creating command that doesn’t redeclare its heads as attributes persists them as nil |
runtime/instance.rb:95 |
| M18 | Identity.of raises NoMethodError on a non-string identity part where a refusal belongs |
runtime/identity.rb:63 |
| M19 | In-process vs. SQLite read-model paths diverge on missing heads/root/join scope | read_model_interpreter.rb:78 vs sqlite/projection.rb:39 |
| M20 | @reaction_depth is a plain (non-thread-local) ivar on a thread-shared dispatcher — matches the known Puma-concurrency bug class |
dispatcher.rb:165 |
| ID | One-line | File |
|---|---|---|
| H13 | make deploy always exits non-zero for Shared-mode domains — the unautomated mint-era stub exit 1s after a successful sam deploy, training operators to ignore red deploys |
bin/project_deploy |
| H14 | Generated scaffold-translation/translation-audit targets open a real prod SSM tunnel, then silently scaffold/audit the developer’s local DB instead |
bin/project_deploy:1389 |
| M26 | Layer 1 checks lifecycle values against targets only, not ModelCheck.full_states — a valid from:-only state blocks the mint |
translation/audit/layer_one.rb:24 |
| M27 | Sequential in-place rename loses data on swap/chain renames ({a:b,b:a} on {a:1,b:2} → {a:1}) — Layer 2’s reference, so the compiled SQL agrees and it mints silently |
ports/persistence/lineage.rb:92 |
| M28 | Adding Google OAuth to a live stack deadlocks make deploy — pre-deploy mint-era queries outputs the deploy hasn’t created yet |
bin/project_deploy:1818 |
| M29 | RDS master passwords may contain %, invalid in libpq’s URI parser — ~29% of 32-char passwords |
bin/project_deploy:816 |
| L15 | bin/project missing the executable bit |
— |
| L16 | bin/present --port=8080 (equals form) silently ignored; -p abc binds an ephemeral port |
bin/present:64 |
| L17 | bin/evolve’s option swallows a following flag as its value |
bin/evolve:48 |
| L18 | bin/evolve rename has no rescue/ensure — a mid-cascade raise leaves syntax.bluebook half-renamed |
bin/evolve:63 |
| L19 | bin/stores on a nonexistent domain path exits 0 with no output |
bin/stores:14 |
| L13 | reset! on a lineage-provisioned journal (FORCE RLS, no DELETE policy) silently deletes zero rows |
adapters/driven/postgres.rb:225 |
Fixing these doesn’t fix a product bug directly, but it’s why several of the above stayed hidden behind a green suite — and why fixes to them need new coverage, not just a patch.
| ID | One-line | File |
|---|---|---|
| M21 | Three shipped framework aggregates (RoleAssignment, RoleTransition via composite id; ConsoleSettings Replace* via list_of) are half-unfuzzable — every acting step refused, zero events |
fuzzing/sequence_generator/outcome_tracker.rb:14 + step_builder.rb:36 |
| M22 | saga_advances_follow_declared_handlers passes vacuously — the log copies from:/to: from the handler it’s then checked against |
fuzzing/properties.rb:61 |
| M23 | The query oracle masks refusal-shaped divergence — both engines share one begin, an entry where either raised is skipped |
fuzzing/replay.rb:49 |
| M24 | shrink_arguments never accumulates drops — each accepted drop reverts the previous ones |
bin/fuzz:139 |
| M25 | RoleTransition has an absorbing revoked state — a revoked pair can never be re-granted, contradicting its own header comment |
framework/bluebook/governance.bluebook:84 |
| R1 | 6 events differ on the full corpus (not the 3 pinned fixtures) — Rust emits nil for an omitted optional arg where Ruby omits the key |
rust/src/kernel/json.rs:178 |
| R2 | Refusal counts diverge, Ruby 94 vs Rust 131 — 37 “query steps not generated yet” placeholders shift every later index | rust/src/kernel/cli.rs:76 |
| R3 | Dispatch-order inversion — Rust checks role/references before VO invariant/admits/pattern; Ruby is the reverse. Same bad input → different refusal per runtime. | rust/project/registry.rb:116 |
| R4 | Empty-string identity component accepted by Rust, rejected by Ruby — real persisted-state divergence | rust/src/kernel/* (to_id_component) vs Identity.of |
| R5 | cargo build --features banking fails to compile (mutually-exclusive domain features unmarked); plus latent codegen landmines (a domain named version/edition/path/default, or a description with #{/#@/control chars) |
Cargo.toml, bin/project_rust:160 |
The recorded Rust-parity status (“35/35 instances”) only holds for the 3 pinned fixtures — R1–R4 surface only against the full corpus.
Everything from the source audit’s Low section not already placed above.
| ID | One-line | File |
|---|---|---|
| L1 | catch_up! only enforces history consistency under the exact symbol :strict |
ports/projection.rb:44 |
| L2 | Chained non-root heads are still declaration-order dependent one level deeper than the root-first fix reached | runtime/read_model_interpreter.rb:55 |
| L3 | Dir[…].first reproduces the multi-file wrong-source bug EraCheck already fixed elsewhere; only reached from a spec today |
runtime/era_guard.rb:29 |
| L4 | sign_of returns -1 for any op without a sign; unreachable today |
runtime/command_rules/arithmetic.rb:85 |
| L5 | match_transition falls back to matches.first when no from: admits the state; dead code, zero callers |
bluebook/ir/lifecycle.rb:58 |
| L6 | A multi-column one_of is admitted on its first column only |
runtime/value/admission.rb:14 |
| L7 | one_of member values are stringified in to_h (Integer 1 → "1") |
bluebook/ir/value_object.rb:57 |
| L8 | Possessive-quantifier check doesn’t skip character-class interiors — [*+] wrongly refused, a{2}+ wrongly admitted |
bluebook/pattern_subset.rb:88 |
| L9 | validate_command! accepts creating-command names a Handle can’t dispatch → NoMethodError instead of 404 |
facade/json_door.rb:85 |
| L10 | run_query omits JSON::ParserError from its rescue list — a malformed list-of-VO query 500s instead of a 422 |
presentation/app.rb:200 |
| L11 | A record whose id equals a command/query name shadows its own detail page | presentation/app.rb:106 |
| L14 | SQLite/D1 store literal "null" text where Postgres stores SQL NULL — decodes the same today, diverges under a future IS NULL check |
adapters/driven/sqlite.rb:86 / d1.rb:136 |
| L21 | f64→i64 cast saturates silently where Ruby keeps a Bignum |
rust/src/kernel/json.rs:108 |
| L22 | Int + Int uses plain + (debug panic / release wrap) where Ruby promotes to Bignum |
rust/src/kernel/expr.rs:239 |
| L23 | nest()’s .as_object_mut().unwrap() panics (500) on a scalar/group path-prefix collision |
rust/host/src/web.rs:562 |
| L24 | Post-accept redirect id is mutations.last().last(), which can belong to a cascaded reaction’s aggregate, not the form’s target |
rust/host/src/web.rs:748 |
SQL identifier quoting and JSONB path building in the Postgres adapter;
search_path schema isolation; ORDER BY tiebreakers; the Handle
composite-identity fix; JsonDoor wiring; the Ruby presentation layer’s
HTML escaping; comma-bearing contains; AppendOnly#save’s shallow dup;
the rust_conformance comparison logic itself.
| Harness | Result @ 2026-08-10 |
|---|---|
| RSpec | 1170 examples, 0 real failures |
Property fuzzer (bin/fuzz --seeds 60 --steps 40) |
clean |
IR model checker (bin/model_check) |
clean |
Rust cargo test |
passes, zero unit tests exist |
Re-run before trusting this baseline for any fix — this branch
(fix/language-bluebook-validation) currently has 53 unrelated pre-existing
rspec failures as of 2026-08-11 that this snapshot predates.