Skip to content

feat: add Element::NonCounted wrapper to opt-out of count propagation#654

Merged
QuantumExplorer merged 8 commits into
developfrom
claude/ecstatic-euclid-fc6666
May 9, 2026
Merged

feat: add Element::NonCounted wrapper to opt-out of count propagation#654
QuantumExplorer merged 8 commits into
developfrom
claude/ecstatic-euclid-fc6666

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented May 9, 2026

Issue being fixed or feature implemented

GroveDB count-bearing trees (CountTree, ProvableCountTree, CountSumTree, ProvableCountSumTree) currently aggregate every child element into the parent's count. There's no way to insert a value that participates in storage and proofs but is exempt from the count — e.g. housekeeping rows, schema markers, or auxiliary indexes inside a count tree.

This adds that escape hatch.

What was done?

Added a new Element::NonCounted(Box<Element>) variant that:

  • behaves identically to its inner element for storage, hashing, sum propagation, and internal aggregation,
  • contributes 0 to the parent count tree's aggregate count,
  • still contributes the inner sum to a parent sum tree (only count is suppressed),
  • may only be inserted into a count-bearing tree.

Design choice: single wrapper variant (vs. ~15 explicit variants)

A wrapper means one new discriminant and one dispatch point for the "is non-counted?" question. Existing Element predicates (`is_tree`, `is_sum_item`, `is_any_item`, `is_non_empty_tree`, ...) keep working by looking through the wrapper via a new `underlying()` helper. Pattern-match sites in cost, proof, query, batch, and insert paths dispatch on the underlying element.

Cryptographic notes

The wrapper byte is included in the serialized value, so the value hash distinguishes `NonCounted(X)` from a bare `X`. The parent's `TreeFeatureType` for a non-counted child is the inner element's feature type with its count zeroed (via a new `TreeFeatureType::zero_count()` helper); no new `TreeFeatureType` variants are introduced.

Nested `NonCounted(NonCounted(...))` is rejected at construction, serialization, and deserialization to close a stack-overflow vector.

Scope

Touched 18 files across `grovedb-element`, `grovedb-query`, `grovedb-merk`, and `grovedb`:

  • enum + discriminant + constructor + helpers + serialization
  • `merk` insert validation, feature-type dispatch, get/cost paths
  • `grovedb` batch / insert / query / proof / lib pattern-match sites updated to dispatch on the underlying element

How Has This Been Tested?

New tests:

  • `grovedb-element` (10 tests): constructor (wrap, reject-nested, idempotent `into_non_counted`); helper passthrough (`is_*`, `count_value_or_default` returns 0, `sum_value_or_default` keeps inner sum, flag accessors); bincode round-trip; reject nested wrapper at deserialize and at serialize.
  • `grovedb-merk` (5 tests): insert into `NormalTree` and `SumTree` is rejected; insert into `CountTree` succeeds and contributes 0; `NonCounted(SumItem(10))` in a `ProvableCountSumTree` yields count=0, sum=10; a `NonCounted(ProvableCountTree)` whose internal count is 5 contributes 0 to a parent `CountTree`.

Full workspace test suite still passes:

  • `cargo test -p grovedb-element` — 39 passed
  • `cargo test -p grovedb-merk --features minimal,test_utils,full` — 360 passed
  • `cargo test -p grovedb` — 1449 passed

`cargo clippy` is clean for the changed code (3 pre-existing warnings in unrelated files).

Breaking Changes

None for existing callers. The change adds a new `Element` variant and is additive at the API surface. The on-disk serialization format gains a new bincode discriminant (15) — older code reading data written with this variant would fail to deserialize, so producer/consumer pairs need to be upgraded together. This matches the existing "ONLY APPEND TO THIS LIST" comment on the `Element` enum.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Support for non-counted elements that preserve storage/hashing and sums while contributing zero to parent counts
    • New helper APIs for transparent access to underlying elements
  • Behavior Changes

    • Visualization and display now indicate non-counted wrappers
    • Queries, proofs, batch execution, and cost/accounting now treat non-counted elements as transparent while accounting for wrapper overhead
  • Validation & Safety

    • Reject nested non-counted wrappers during serialization/deserialization
    • Prevent inserting non-counted elements into non-count-bearing trees
  • Tests

    • Added end-to-end and unit coverage for non-counted semantics and safeguards

A new `Element::NonCounted(Box<Element>)` variant that behaves identically
to its inner element except it contributes 0 to the parent count tree's
aggregate. Sums still propagate. May only be inserted into count-bearing
trees (CountTree, ProvableCountTree, CountSumTree, ProvableCountSumTree).

Use case: insert housekeeping rows, schema markers, or auxiliary indexes
inside a count tree without inflating its count.

Single-variant wrapper design (instead of 15+ explicit variants) keeps the
diff surface and audit footprint small. Existing `Element` predicates and
helpers look through the wrapper via a new `underlying()` accessor; cost,
proof, and pattern-match sites dispatch on the underlying element. The
wrapper byte is included in the serialized value so the value hash
distinguishes wrapped from unwrapped at the cryptographic layer.

Nested `NonCounted(NonCounted(...))` is rejected at construction,
serialization, and deserialization to close a stack-overflow vector.

Tests: 10 in grovedb-element (constructor, helpers, bincode round-trip,
nested-rejection), 5 in grovedb-merk (insert validation, aggregate_data
end-to-end). Full workspace test suite still passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 9, 2026

Warning

Rate limit exceeded

@QuantumExplorer has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 55 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a4a0d3ef-f921-43ac-89d4-56712ead09fe

📥 Commits

Reviewing files that changed from the base of the PR and between 10d3821 and 889d10f.

📒 Files selected for processing (12)
  • grovedb-element/src/element_type.rs
  • grovedb/src/batch/mod.rs
  • grovedb/src/element/aggregate_sum_query/mod.rs
  • grovedb/src/estimated_costs/average_case_costs.rs
  • grovedb/src/estimated_costs/worst_case_costs.rs
  • grovedb/src/operations/bulk_append_tree.rs
  • grovedb/src/operations/commitment_tree.rs
  • grovedb/src/operations/dense_tree.rs
  • grovedb/src/operations/get/mod.rs
  • grovedb/src/operations/get/query.rs
  • grovedb/src/operations/mmr_tree.rs
  • grovedb/src/tests/non_counted_tests.rs
📝 Walkthrough

Walkthrough

Adds Element::NonCounted wrapper (non-nestable) that preserves sums but suppresses parent counts; integrates wrapper handling across serialization, element helpers, cost accounting, insert/batch propagation, queries, proofs, verification, visualization, and tests.

Changes

NonCounted Wrapper Feature

Layer / File(s) Summary
Type Definition & Serialization
grovedb-element/src/element/mod.rs, grovedb-element/src/element_type.rs, grovedb-element/src/element/serialize.rs, grovedb-element/src/element/visualize.rs
Added Element::NonCounted(Box<Element>) enum variant with discriminant 15. Serialization validates and rejects nested NonCounted(NonCounted(_)). Updated ElementType parsing and display. Visualization outputs non_counted(...) format.
Wrapper Construction & Introspection
grovedb-element/src/element/constructor.rs, grovedb-element/src/element/helpers.rs
Added new_non_counted() (non-idempotent, rejects existing NonCounted), into_non_counted() (idempotent), and accessor methods (is_non_counted(), underlying(), underlying_mut(), into_underlying()).
Count & Sum Value Handling
grovedb-element/src/element/helpers.rs
Updated value-decoding helpers: count_value_or_default returns 0 for NonCounted, count_sum_value_or_default returns (0, inner_sum), while sum_value_or_default and big_sum_value_or_default delegate to inner element.
Predicate & Tree Type Delegation
grovedb-element/src/element/helpers.rs, merk/src/element/tree_type.rs
Updated predicates, item/reference helpers, and tree-type methods to transparently look through NonCounted wrapper via underlying() (e.g., is_sum_tree, is_reference, tree_type, tree_feature_type, flag accessors).
Cost Computation & Storage
merk/src/element/costs.rs, merk/src/element/get.rs
Specialized cost calculation accounts for wrapper discriminant (+1 byte) and unwraps before variant selection. Storage retrieval functions match on unwrapped element for cost-path determination and add wrapper_overhead when computing value_len.
Tree Feature Type & Insertion Validation
grovedb-query/src/proofs/tree_feature_type.rs, merk/src/tree_type/mod.rs, merk/src/element/insert.rs
Added TreeFeatureType::zero_count() to suppress count in counted merkle nodes while preserving sums. Added TreeType::is_count_bearing() predicate. Validates that NonCounted elements cannot be inserted into non-count-bearing trees. Sum-item insertion uses is_sum_item() so wrapped sum items follow specialized cost path. Tests added for acceptance/rejection and aggregate behavior.
Query & Item Resolution
grovedb/src/operations/get/query.rs
Updated dispatch logic in follow_element, query_item_value, query_item_value_or_sum, and query_sums to unwrap NonCounted via into_underlying() before type-based routing; remaining NonCounted arms marked unreachable!().
Insert Operations
grovedb/src/operations/insert/mod.rs
Updated element dispatch to match on element.underlying() before routing insert logic; grouped tree variants under unified handling and explicitly reject nested NonCounted.
Batch Operations
grovedb/src/batch/mod.rs
Updated reference-following and batch operation dispatch to unwrap NonCounted before element-variant matching while using outer serialized bytes for value-hash where required.
Proof Operations
grovedb/src/operations/proof/generate.rs, grovedb/src/operations/proof/verify.rs
Element deserialization during proof generation and verification (V0 and V1) now unwraps NonCounted via into_underlying() before subsequent type dispatch; extract_count_from_element updated to match on underlying.
Verification & Hash Integrity
grovedb/src/lib.rs
Verification loop in verify_merk_and_submerks_in_transaction unwraps decoded elements before hash/comparison logic and marks NonCounted arms as unreachable!().
Manifest
grovedb-commitment-tree/Cargo.toml
Pinned orchard git dependency updated to a new revision (same features).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A wrapper arrives, not counted but seen,
Through sum and tree the inner stays keen,
Counts drop to zero while values flow true,
Tests and proofs updated — bytes still accrue,
The rabbit hops, delighted: hooray you!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main feature addition: a new Element::NonCounted wrapper mechanism to opt out of count propagation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/ecstatic-euclid-fc6666

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
grovedb/src/batch/mod.rs (1)

1444-1453: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Don't unwrap before computing the base element hash.

into_underlying() drops the NonCounted discriminant, so a multi-hop reference that resolves to an on-disk NonCounted(Item|SumItem|ItemWithSumItem) will hash X instead of NonCounted(X). That makes the reference value hash diverge from the serialized bytes actually stored on disk.

Suggested fix
-        let element = element.into_underlying();
-        match element {
+        match element.underlying() {
             Element::Item(..) | Element::SumItem(..) | Element::ItemWithSumItem(..) => {
                 let serialized =
                     cost_return_on_error_into_no_add!(cost, element.serialize(grove_version));
                 let val_hash = value_hash(&serialized).unwrap_add_cost(&mut cost);
                 Ok(val_hash).wrap_with_cost(cost)
             }
             Element::Reference(path, ..) => {
                 let path = cost_return_on_error_into_no_add!(
                     cost,
-                    path_from_reference_qualified_path_type(path, qualified_path)
+                    path_from_reference_qualified_path_type(path.clone(), qualified_path)
                 );
                 self.follow_reference_get_value_hash(
                     path.as_slice(),
                     ops_by_qualified_paths,
🤖 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 `@grovedb/src/batch/mod.rs` around lines 1444 - 1453, The bug is that
into_underlying() is called before hashing so NonCounted's wrapper byte is lost;
stop dropping the NonCounted discriminant before computing the stored value
hash. Specifically, in the block handling
Element::Item/::SumItem/::ItemWithSumItem, compute the hash from
element.serialize(grove_version) (or otherwise serialize the original element
including the NonCounted wrapper) and pass that serialized bytes into
value_hash(...) before calling into_underlying() or any wrapper-stripping;
ensure into_underlying() is only used for dispatch logic, not for deriving the
value_hash.
grovedb/src/operations/insert/mod.rs (1)

248-351: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace unreachable! with a typed error for nested wrappers.

element.underlying() unwraps a single level. If a nested Element::NonCounted(Box::new(Element::NonCounted(...))) is passed, Line 350 can panic on a public insert path.

Suggested fix
-            Element::NonCounted(_) => unreachable!("unwrapped above"),
+            Element::NonCounted(_) => {
+                return Err(Error::InvalidInput(
+                    "nested non-counted wrappers are not allowed",
+                ))
+                .wrap_with_cost(cost);
+            }
🤖 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 `@grovedb/src/operations/insert/mod.rs` around lines 248 - 351, The match arm
currently uses unreachable!() for Element::NonCounted(_) which can panic if
element.underlying() only unwraps one level and a nested NonCounted wrapper is
passed; change this to return a typed error instead (e.g.,
Err(Error::InvalidCodeExecution("nested Element::NonCounted wrapper
encountered")).wrap_with_cost(cost)) so the public insert path won't panic;
update the match arm for Element::NonCounted(_) to produce that error (keeping
use of wrap_with_cost(cost) consistent with surrounding error handling) and
reference element.underlying() and Element::NonCounted in the message to make
the cause clear.
🧹 Nitpick comments (1)
merk/src/tree_type/mod.rs (1)

215-400: ⚡ Quick win

Add a unit test for is_count_bearing to mirror the existing predicate tests.

The test module already has exhaustive variant coverage for uses_non_merk_data_storage and allows_sum_item. A matching test for is_count_bearing would lock the contract behind which the new Element::NonCounted insertion gate at merk/src/element/insert.rs operates.

🧪 Proposed test addition
+    #[test]
+    fn is_count_bearing() {
+        assert!(!TreeType::NormalTree.is_count_bearing());
+        assert!(!TreeType::SumTree.is_count_bearing());
+        assert!(!TreeType::BigSumTree.is_count_bearing());
+        assert!(TreeType::CountTree.is_count_bearing());
+        assert!(TreeType::CountSumTree.is_count_bearing());
+        assert!(TreeType::ProvableCountTree.is_count_bearing());
+        assert!(TreeType::ProvableCountSumTree.is_count_bearing());
+        assert!(!TreeType::CommitmentTree(0).is_count_bearing());
+        assert!(!TreeType::MmrTree.is_count_bearing());
+        assert!(!TreeType::BulkAppendTree(0).is_count_bearing());
+        assert!(!TreeType::DenseAppendOnlyFixedSizeTree(0).is_count_bearing());
+    }
🤖 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 `@merk/src/tree_type/mod.rs` around lines 215 - 400, Add a unit test in the
existing tests mod that mirrors the pattern used for uses_non_merk_data_storage
and allows_sum_item: iterate/assert the expected boolean for every TreeType
variant against TreeType::is_count_bearing() (including parameterized variants
like CommitmentTree(n), BulkAppendTree(n), DenseAppendOnlyFixedSizeTree(n)) so
all variants are covered and the contract behind Element::NonCounted insertion
is locked in.
🤖 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 `@grovedb/src/batch/mod.rs`:
- Around line 1867-1871: The current bug: wrapped (NonCounted(Tree)) subtrees
are handled by execute_ops_on_path() but apply_batch_structure() still matches
on the outer Element and thus treats writes under a wrapped tree as "insertion
under a non tree"; to fix, change apply_batch_structure() to check the element's
underlying variant (use element.underlying() or the same wrapper-aware helper
used elsewhere) when matching occupied parent ops and, when encountering
NonCounted(Tree) or equivalent wrappers with children-to-be-written, convert the
parent insertion op into the corresponding tree insertion ops
(InsertTreeWithRootHash or InsertNonMerkTree) instead of emitting the non-tree
insertion error; update the matching logic around Element/occupied parent ops in
apply_batch_structure() to mirror execute_ops_on_path()'s wrapper-aware behavior
so subtree insertions propagate correctly.

In `@grovedb/src/operations/get/query.rs`:
- Around line 212-215: The follow_element path currently only calls
element.into_underlying() on the value at the query edge, leaving cases where a
resolved Reference yields a NonCounted(Item) wrapped value inconsistent; update
follow_element so that after resolving a Reference you also normalize/unwrap any
NonCounted wrapper from the resolved result (i.e. ensure the value returned by
follow_element always calls into_underlying()/equivalent unwrapping on resolved
references), and apply the same fix to the other branch handling (the code
around the 225-237 block) so both direct and referenced resolutions return the
same unwrapped Item.

In `@grovedb/src/operations/proof/verify.rs`:
- Around line 485-489: verify_trunk_chunk_proof_v0 and
verify_trunk_chunk_proof_v1 still pass wrapped NonCounted elements straight into
extract_count_from_element causing "target is not a count tree element" for
trunks rooted in NonCounted; after deserializing the element with
Element::deserialize(...) and before calling extract_count_from_element, unwrap
the NonCounted wrapper (e.g., call .into_underlying() like the other
verify_layer_proof* paths do) so the count-extraction logic receives the
underlying CountTree/CountSumTree/ProvableCount element; apply the same unwrap
fix in both verify_trunk_chunk_proof_v0 and verify_trunk_chunk_proof_v1 (also
mirror the change at the other occurrence referenced around the later block).

In `@merk/src/element/costs.rs`:
- Around line 99-105: The NonCounted wrapper loses its discriminant when calling
into_underlying(), causing constant-size branches in the cost match to omit the
wrapper byte; update the match arms that compute value_len for the tree/sum-item
specialized branches to add the wrapper_overhead (i.e., add "+ wrapper_overhead"
to those constant-based value_len calculations) so that replacement-cost
accounting includes the 1-byte wrapper discriminant while keeping the other
paths unchanged (locate the logic where element = element.into_underlying() and
the subsequent match on element).

In `@merk/src/element/get.rs`:
- Around line 411-416: The tree cost arm currently uses element_for_cost =
element.as_ref().map(|e| e.underlying()) and then relies on
tree_type().cost_size(), which misses the 1-byte NonCounted discriminant and
undercounts storage_loaded_bytes; change the sizing for the tree arm to use the
actual serialized value length (e.g., value.as_ref().unwrap().len()) rather than
tree_type().cost_size() so the logged/read bytes include the wrapper byte.
Update the same pattern wherever tree sizing is computed (the other occurrences
referenced around the element_for_cost usage at the later spots) to consistently
use the serialized value length for tree arms instead of cost_size().

In `@merk/src/element/insert.rs`:
- Around line 166-171: The validation that NonCounted elements may only be
inserted into count-bearing trees is currently only in insert(); add the same
guard at the start of insert_reference and insert_subtree to reject NonCounted
elements when merk.tree_type.is_count_bearing() is false, returning the same
Error::InvalidInputError wrapped with cost as in insert. Also ensure any batch
insertion paths that know the target tree type apply the same check before
delegating to insert_reference/insert_subtree so the restriction cannot be
bypassed via batch APIs.

---

Outside diff comments:
In `@grovedb/src/batch/mod.rs`:
- Around line 1444-1453: The bug is that into_underlying() is called before
hashing so NonCounted's wrapper byte is lost; stop dropping the NonCounted
discriminant before computing the stored value hash. Specifically, in the block
handling Element::Item/::SumItem/::ItemWithSumItem, compute the hash from
element.serialize(grove_version) (or otherwise serialize the original element
including the NonCounted wrapper) and pass that serialized bytes into
value_hash(...) before calling into_underlying() or any wrapper-stripping;
ensure into_underlying() is only used for dispatch logic, not for deriving the
value_hash.

In `@grovedb/src/operations/insert/mod.rs`:
- Around line 248-351: The match arm currently uses unreachable!() for
Element::NonCounted(_) which can panic if element.underlying() only unwraps one
level and a nested NonCounted wrapper is passed; change this to return a typed
error instead (e.g., Err(Error::InvalidCodeExecution("nested Element::NonCounted
wrapper encountered")).wrap_with_cost(cost)) so the public insert path won't
panic; update the match arm for Element::NonCounted(_) to produce that error
(keeping use of wrap_with_cost(cost) consistent with surrounding error handling)
and reference element.underlying() and Element::NonCounted in the message to
make the cause clear.

---

Nitpick comments:
In `@merk/src/tree_type/mod.rs`:
- Around line 215-400: Add a unit test in the existing tests mod that mirrors
the pattern used for uses_non_merk_data_storage and allows_sum_item:
iterate/assert the expected boolean for every TreeType variant against
TreeType::is_count_bearing() (including parameterized variants like
CommitmentTree(n), BulkAppendTree(n), DenseAppendOnlyFixedSizeTree(n)) so all
variants are covered and the contract behind Element::NonCounted insertion is
locked in.
🪄 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

Run ID: 2c38ba03-458e-42c2-b5c8-b85471cec79d

📥 Commits

Reviewing files that changed from the base of the PR and between 1ed881b and 3082688.

📒 Files selected for processing (18)
  • grovedb-element/src/element/constructor.rs
  • grovedb-element/src/element/helpers.rs
  • grovedb-element/src/element/mod.rs
  • grovedb-element/src/element/serialize.rs
  • grovedb-element/src/element/visualize.rs
  • grovedb-element/src/element_type.rs
  • grovedb-query/src/proofs/tree_feature_type.rs
  • grovedb/src/batch/mod.rs
  • grovedb/src/lib.rs
  • grovedb/src/operations/get/query.rs
  • grovedb/src/operations/insert/mod.rs
  • grovedb/src/operations/proof/generate.rs
  • grovedb/src/operations/proof/verify.rs
  • merk/src/element/costs.rs
  • merk/src/element/get.rs
  • merk/src/element/insert.rs
  • merk/src/element/tree_type.rs
  • merk/src/tree_type/mod.rs

Comment thread grovedb/src/operations/get/query.rs
Comment thread grovedb/src/operations/proof/verify.rs
Comment thread merk/src/element/costs.rs Outdated
Comment thread merk/src/element/get.rs
Comment thread merk/src/element/insert.rs
QuantumExplorer and others added 4 commits May 9, 2026 21:33
CodeRabbit review findings on PR #654:

- batch/mod.rs (process_reference_with_hop_count_greater_than_one):
  hash must be over the WRAPPED bytes (which is what's on disk), not the
  underlying bytes. Now matches on element.underlying() for dispatch but
  serializes the outer element for value_hash.

- batch/mod.rs (apply_batch_structure): the long if/else-if chain that
  rewrites parent insert ops into InsertTreeWithRootHash /
  InsertNonMerkTree was matching on the outer Element, so a
  NonCounted-wrapped tree fell through to the "non tree" error path.
  Now rebinds via element.underlying() before the chain.

- query.rs (follow_element): a Reference resolving to NonCounted(Item)
  returned the wrapper while a directly-queried NonCounted(Item) was
  unwrapped. Normalize the resolved value via into_underlying() too.

- proof/verify.rs (extract_count_from_element): trunk proofs rooted at
  a NonCounted(CountTree/...) failed with "target is not a count tree
  element". Look through the wrapper.

- merk/element/costs.rs (specialized_costs_for_key_value): the +1
  wrapper byte was a deliberate under-count; replace with explicit
  wrapper_overhead added to all constant-size value_len computations
  so storage cost accounting matches on-disk bytes exactly.

- merk/element/get.rs (V0 + V1 cost paths): same wrapper_overhead fix
  for storage_loaded_bytes on tree and sum-item arms.

- merk/element/insert.rs: the NonCounted insert guard only ran in
  insert(); add the same check at the start of insert_reference and
  insert_subtree so NonCounted(Reference) and NonCounted(Tree) cannot
  be inserted into non-count-bearing trees through those paths.

- grovedb/operations/insert/mod.rs: replace unreachable!() for
  Element::NonCounted(_) on the public insert path with a typed
  Error::InvalidInput so a hand-built nested wrapper returns an error
  rather than panicking.

Tests:
- merk/tree_type/mod.rs: add is_count_bearing test for all variants
  (CodeRabbit nitpick).
- merk/element/insert.rs: add insert_subtree path validation test.

Full workspace tests still pass (1449 grovedb, 362 grovedb-merk, 41
grovedb-element).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Old orchard rev pulled core2 ^0.3, all of whose published versions are
yanked from crates.io, breaking fresh `cargo update` runs. The new
dashified branch is rebased on orchard 0.13.1 which uses corez instead.
This unblocks CI dependency resolution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the flat ElementType::NonCounted = 15 variant and replaces it with
15 NonCountedXxx variants whose discriminants are the base type's
discriminant with the high bit set (e.g. NonCountedItem = 128,
NonCountedTree = 130, ...). Bit 7 cleanly encodes "is this wrapped?"
and bits 0-6 encode the underlying base type.

The on-disk format is unchanged: Element::NonCounted still serializes
as [15, ...inner bytes]. ElementType::from_serialized_value detects
this by peeking byte 1 when byte 0 is the new
NON_COUNTED_WRAPPER_DISCRIMINANT (15) constant, then synthesizes the
NonCountedXxx variant. TryFrom<u8> on byte 15 alone is rejected (it
needs the inner byte to disambiguate) — every other base byte and the
new 128..=142 range round-trip normally.

New helpers on ElementType:
- is_non_counted(self) -> bool: (self as u8) & 0x80 != 0
- base(self) -> ElementType: strips the high bit, returns underlying
  base type (or self if already a base)

is_tree, is_item, is_reference, has_simple_value_hash, and
proof_node_type all dispatch via base(), so a NonCountedTree is still
a tree, a NonCountedItem still uses Kv proof nodes, etc.

Element::element_type() returns the synthetic NonCountedXxx for a
wrapped element, so callers get both "what kind" and "is wrapped" from
a single ElementType value.

Tests:
- test_element_type_from_discriminant updated: TryFrom(15) is now Err;
  added 128, 129, 142 mappings and out-of-range 127, 143.
- test_non_counted_helpers: is_non_counted and base() correctness +
  the discriminant relationship (twin = base | 0x80).
- test_simple_vs_combined_hash: NonCounted twins inherit base hash kind.
- test_proof_node_type_through_non_counted_wrapper: wrapper transparent
  for proof-node-type selection.
- test_from_serialized_value: 2-byte peek for byte-15 case, including
  truncated/nested/unknown-inner rejections.
- test_is_tree: covers NonCountedXxx + spot checks is_item/is_reference.
- test_non_counted_wrapper_discriminant_pinned: pins the bincode
  discriminant for Element::NonCounted to 15 and verifies the synthetic
  ElementType resolution end-to-end.

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

CI runs `cargo clippy --workspace --all-features -- -D warnings`.
Three pre-existing warnings in untouched files were already promoted to
errors by a recent clippy update and were blocking CI:

- merk/src/proofs/query/verify.rs:693: collapsible-match — fold the
  inner `if k.as_slice() == key` into the outer match arm guard.
- merk/src/test_utils/mod.rs:268: explicit-counter-loop — replace the
  hand-rolled `seed += 1` counter with `for seed in initial_seed..end`.
- grovedb/src/replication.rs:163: useless-conversion — drop the
  redundant `.into_iter()` call.

Also covers a non-exhaustive match in `grovedb/src/debugger.rs` (only
compiled with the `grovedbg` feature, hence missed earlier): the
visualizer wire format has no NonCounted variant, so we render the
inner element transparently — the wrapper is invisible in the debug UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 9, 2026

Codecov Report

❌ Patch coverage is 82.48276% with 127 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.54%. Comparing base (1ed881b) to head (889d10f).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
grovedb/src/operations/get/query.rs 19.44% 29 Missing ⚠️
grovedb-element/src/element_type.rs 86.12% 24 Missing ⚠️
grovedb-element/src/element/mod.rs 0.00% 19 Missing ⚠️
grovedb-element/src/element/helpers.rs 92.05% 12 Missing ⚠️
merk/src/element/get.rs 55.00% 9 Missing ⚠️
grovedb-query/src/proofs/tree_feature_type.rs 0.00% 6 Missing ⚠️
grovedb/src/batch/mod.rs 88.23% 6 Missing ⚠️
grovedb/src/operations/insert/mod.rs 69.23% 4 Missing ⚠️
merk/src/element/insert.rs 95.65% 4 Missing ⚠️
merk/src/element/tree_type.rs 33.33% 4 Missing ⚠️
... and 6 more
Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #654      +/-   ##
===========================================
- Coverage    90.63%   90.54%   -0.09%     
===========================================
  Files          182      182              
  Lines        52304    52873     +569     
===========================================
+ Hits         47405    47875     +470     
- Misses        4899     4998      +99     
Components Coverage Δ
grovedb-core 88.58% <72.15%> (-0.07%) ⬇️
merk 91.91% <89.44%> (-0.01%) ⬇️
storage 86.36% <ø> (ø)
commitment-tree 96.43% <ø> (ø)
mmr 96.76% <ø> (ø)
bulk-append-tree 89.65% <ø> (ø)
element 95.24% <84.77%> (-2.33%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@QuantumExplorer
Copy link
Copy Markdown
Member Author

Codex review findings:

  1. [P1] Reject nested NonCounted before decode
    grovedb-element/src/element/serialize.rs:52
    The nested-wrapper guard runs only after bincode::decode_from_slice has decoded the recursive Box<Element>. A malicious value starting with repeated NonCounted discriminants still recurses before this check, so the stack-exhaustion vector is not actually closed. Pre-check the raw bytes for a nested wrapper, or call the serialized-type parser, before bincode decoding.

  2. [P1] Batch propagation drops the wrapper
    grovedb/src/batch/mod.rs:2625
    This unwraps NonCounted(Tree) to choose the internal InsertTreeWithRootHash/InsertNonMerkTree op, but the internal op stores no information that the original element was wrapped. When a batch inserts a NonCounted tree and also writes under it, execution later reconstructs a bare tree, so the parent count includes it and the on-disk element loses the wrapper.

  3. [P1] Existing wrapped trees cannot propagate
    merk/src/element/reconstruct.rs:27
    reconstruct_with_root_key has no Element::NonCounted arm. Existing NonCounted tree elements can be opened because the tree-type helpers unwrap them, but any later subtree mutation that propagates a new root key reaches this helper and returns None, causing update_tree_item_preserve_flag to fail instead of re-wrapping the reconstructed inner tree.

  4. [P2] Batch insert bypasses the parent-type guard
    grovedb/src/batch/mod.rs:1867
    Direct Merk insertion rejects NonCounted children outside count-bearing parents, but this batch path never checks element.is_non_counted() && !in_tree_type.is_count_bearing() before adding the put op. User-facing batch inserts can therefore persist NonCounted elements into normal or sum trees, violating the new invariant.

  5. [P2] NonCounted references are not followed
    grovedb/src/operations/get/mod.rs:83
    The get path only treats a bare Element::Reference as a reference. A valid NonCounted(Reference) inserted into a count-bearing tree falls through and is returned as a value, and the loop below has the same issue for reference chains. This contradicts the wrapper-transparency contract and differs from the query/proof paths that unwrap before dispatch.

Verification performed:

  • cargo test -p grovedb-element non_counted --all-features passed.
  • cargo test -p grovedb-merk non_counted --all-features passed.
  • cargo test -p grovedb non_counted --all-features compiled and ran 0 matching tests, with existing warnings.

Codex review of PR #654 surfaced five real bugs in the NonCounted
implementation. All fixed; regression tests added.

P1 #1 — serialize.rs: stack-overflow vector still open
  The nested-wrapper guard ran AFTER bincode::decode_from_slice had
  already recursed through the Box<Element> chain. A payload of
  repeated [15, 15, 15, ...] bytes could blow the stack before any
  check fired. Fix: pre-check the leading two bytes for a wrapper-of-
  wrapper before invoking bincode. The post-check stays as defense in
  depth.
  Test: deserialize_rejects_long_nested_wrapper_chain_without_recursion
  feeds 1024 leading wrapper bytes — pre-check stops it on byte 1.

P1 #2 — batch propagation dropped the wrapper
  apply_batch_structure converted a NonCounted(Tree) into the internal
  InsertTreeWithRootHash op but stored no information that the original
  was wrapped. On execution the reconstructed element was bare, so the
  on-disk parent element lost the wrapper byte AND the parent count
  tree's aggregate started counting the subtree.
  Fix: add `non_counted: bool` to both InsertTreeWithRootHash and
  InsertNonMerkTree (#[non_exhaustive]). Set it from
  element.is_non_counted() during apply_batch_structure; re-wrap the
  reconstructed element via into_non_counted() during execution.
  Test: batch_propagation_preserves_non_counted_wrapper_on_subtree
  inserts a NonCounted(CountTree) plus a child under it in one batch
  and asserts get_raw on the parent still returns NonCounted.

P1 #3 — reconstruct_with_root_key dropped the wrapper
  No NonCounted arm meant existing wrapped trees couldn't propagate.
  Any subtree mutation that flowed through update_tree_item_preserve_flag
  reached the helper and got None, failing the operation.
  Fix: add a NonCounted arm that recurses on the inner element and
  re-wraps the reconstructed result.
  Test: reconstruct_preserves_non_counted_wrapper.

P2 #4 — batch insert path bypassed the parent-type guard
  Direct Merk insertion already rejected NonCounted children outside
  count-bearing parents, but the batch path skipped the same check.
  Users could persist NonCounted into Normal/Sum/BigSum trees via batch.
  Fix: mirror the per-merk guard with the same predicate
  (element.is_non_counted() && !in_tree_type.is_count_bearing()).
  Test: batch_insert_rejects_non_counted_into_normal_tree.

P2 #5 — get path didn't follow NonCounted(Reference)
  get_caching_optional matched only the bare Reference variant; a
  NonCounted-wrapped reference fell through and was returned as a
  value, contradicting the wrapper-transparency contract.
  Fix: into_underlying() on the get result before dispatching, and
  same in the follow_reference inner loop so reference chains hop
  through wrapped intermediate references.
  Test: get_follows_non_counted_reference inserts a NonCounted-wrapped
  reference inside a count tree and asserts get() returns the
  referenced item.

Test counts (with --features minimal,test_utils,full):
- grovedb-element: 45 (was 44, +1 stack-overflow regression)
- grovedb-merk: 364 (was 362, +2: reconstruct + reconstruct-non-tree)
- grovedb: 1452 (was 1449, +3: NonCounted regressions)

Build paths touched: GroveOp::InsertTreeWithRootHash and
GroveOp::InsertNonMerkTree gained a non_counted: bool field. The
struct literal construction sites in tests/cost-estimation modules
get non_counted: false (they predate the wrapper feature). Match-only
sites (e.g. cost estimators destructuring with `..`) are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
grovedb/src/operations/get/mod.rs (1)

73-85: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wrapper transparency is still incomplete for subtree validation.

get_caching_optional() now unwraps NonCounted, but check_subtree_exists() later in this file still only accepts bare tree variants. Any API that validates its path through that helper will keep rejecting parents stored as NonCounted(Tree), which breaks the “wrapper is transparent” behavior this change is introducing.

🤖 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 `@grovedb/src/operations/get/mod.rs` around lines 73 - 85, The
subtree-validation helper check_subtree_exists still only handles bare tree
variants while get_caching_optional/get_raw_caching_optional now return wrapped
NonCounted variants; update validation to treat the wrapper as transparent by
unwrapping NonCounted before checking. Specifically, locate where
check_subtree_exists is invoked after
get_caching_optional/get_raw_caching_optional and either: 1) change the caller
to call .into_underlying() (or unwrap the NonCounted) before passing the value
to check_subtree_exists, or 2) modify check_subtree_exists itself to match on
NonCounted(...) and recurse into the inner tree variant so parents stored as
NonCounted(Tree) validate correctly. Ensure you reference NonCounted,
get_caching_optional/get_raw_caching_optional, and check_subtree_exists when
making the change.
🧹 Nitpick comments (2)
grovedb/src/tests/non_counted_tests.rs (2)

101-105: ⚡ Quick win

Tighten this assertion to validate rejection reason, not just “some error”.
Right now Line 103 can pass on unrelated failures. Prefer asserting the specific NonCounted/parent-type validation failure signal.

Proposed assertion hardening
-        let result = db.apply_batch(vec![op], None, None, grove_version).unwrap();
-        assert!(
-            result.is_err(),
-            "batch insert of NonCounted into NormalTree must fail"
-        );
+        let err = db
+            .apply_batch(vec![op], None, None, grove_version)
+            .unwrap()
+            .expect_err("batch insert of NonCounted into NormalTree must fail");
+        assert!(
+            format!("{err:?}").contains("NonCounted"),
+            "expected NonCounted validation error, got: {err:?}"
+        );
🤖 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 `@grovedb/src/tests/non_counted_tests.rs` around lines 101 - 105, The test
currently only checks that db.apply_batch(vec![op], None, None, grove_version)
returns an error; tighten it to assert the specific validation failure for
inserting a NonCounted into a NormalTree by extracting the error (e.g., via
result.unwrap_err() or pattern matching) and asserting its variant/message
corresponds to the NonCounted/parent-type validation failure (reference
apply_batch, op, NonCounted and NormalTree) so unrelated errors don't make the
test pass.

109-115: ⚡ Quick win

Test intent mentions count exclusion, but the assertion only checks wrapper persistence.
Please also assert the outer count remains 1 (only plain counted) after the batch, so this test protects both invariants it documents.

Also applies to: 164-179

🤖 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 `@grovedb/src/tests/non_counted_tests.rs` around lines 109 - 115, The test
batch_propagation_preserves_non_counted_wrapper_on_subtree currently only checks
that the NonCounted wrapper is preserved; add an assertion after the batch
commit that the outer count-tree aggregate equals 1 (i.e., only the plain
counted child is counted) to ensure the count exclusion invariant; locate the
code around the batch application and the on-disk parent retrieval in function
batch_propagation_preserves_non_counted_wrapper_on_subtree and assert the parent
count/aggregate is 1. Apply the same additional assertion to the other test in
this file that similarly verifies wrapper persistence so both wrapper and
count-exclusion invariants are covered.
🤖 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 `@grovedb-element/src/element_type.rs`:
- Around line 200-215: The code currently masks inner_byte with NON_COUNTED_FLAG
and calls Self::try_from, which allows inner discriminants with the high bit set
to slip through; instead validate the raw inner_byte is a base discriminant
(0..=14) before wrapping. In the branch handling
NON_COUNTED_WRAPPER_DISCRIMINANT, after reading inner_byte from
serialized_value, explicitly check that inner_byte <= 14 (and also reject
inner_byte == 15), returning ElementError::CorruptedData with an appropriate
message if out of range or if inner_byte == NON_COUNTED_WRAPPER_DISCRIMINANT;
only then call Self::try_from(NON_COUNTED_FLAG | inner_byte).

In `@grovedb/src/batch/mod.rs`:
- Around line 1887-1891: The RefreshReference path in execute_ops_on_path
currently matches the element directly as Element::Reference and thus rejects
wrapped references; update the GroveOp::RefreshReference arm to inspect
element.underlying() (or otherwise unwrap wrapper variants) when checking for
Element::Reference so NonCounted(Reference) and similar wrappers are treated as
their inner Reference—i.e., replace the direct pattern match on `element` with a
match/if-let on `element.underlying()` and then proceed with the same refresh
logic.

---

Outside diff comments:
In `@grovedb/src/operations/get/mod.rs`:
- Around line 73-85: The subtree-validation helper check_subtree_exists still
only handles bare tree variants while
get_caching_optional/get_raw_caching_optional now return wrapped NonCounted
variants; update validation to treat the wrapper as transparent by unwrapping
NonCounted before checking. Specifically, locate where check_subtree_exists is
invoked after get_caching_optional/get_raw_caching_optional and either: 1)
change the caller to call .into_underlying() (or unwrap the NonCounted) before
passing the value to check_subtree_exists, or 2) modify check_subtree_exists
itself to match on NonCounted(...) and recurse into the inner tree variant so
parents stored as NonCounted(Tree) validate correctly. Ensure you reference
NonCounted, get_caching_optional/get_raw_caching_optional, and
check_subtree_exists when making the change.

---

Nitpick comments:
In `@grovedb/src/tests/non_counted_tests.rs`:
- Around line 101-105: The test currently only checks that
db.apply_batch(vec![op], None, None, grove_version) returns an error; tighten it
to assert the specific validation failure for inserting a NonCounted into a
NormalTree by extracting the error (e.g., via result.unwrap_err() or pattern
matching) and asserting its variant/message corresponds to the
NonCounted/parent-type validation failure (reference apply_batch, op, NonCounted
and NormalTree) so unrelated errors don't make the test pass.
- Around line 109-115: The test
batch_propagation_preserves_non_counted_wrapper_on_subtree currently only checks
that the NonCounted wrapper is preserved; add an assertion after the batch
commit that the outer count-tree aggregate equals 1 (i.e., only the plain
counted child is counted) to ensure the count exclusion invariant; locate the
code around the batch application and the on-disk parent retrieval in function
batch_propagation_preserves_non_counted_wrapper_on_subtree and assert the parent
count/aggregate is 1. Apply the same additional assertion to the other test in
this file that similarly verifies wrapper persistence so both wrapper and
count-exclusion invariants are covered.
🪄 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

Run ID: 8bd81d79-a0dc-4474-888c-1b56c6f2905c

📥 Commits

Reviewing files that changed from the base of the PR and between 4bf8c0a and 10d3821.

📒 Files selected for processing (18)
  • grovedb-element/src/element/helpers.rs
  • grovedb-element/src/element/mod.rs
  • grovedb-element/src/element/serialize.rs
  • grovedb-element/src/element_type.rs
  • grovedb/src/batch/estimated_costs/average_case_costs.rs
  • grovedb/src/batch/estimated_costs/worst_case_costs.rs
  • grovedb/src/batch/mod.rs
  • grovedb/src/debugger.rs
  • grovedb/src/operations/get/mod.rs
  • grovedb/src/replication.rs
  • grovedb/src/tests/batch_coverage_tests.rs
  • grovedb/src/tests/batch_rejection_tests.rs
  • grovedb/src/tests/batch_unit_tests.rs
  • grovedb/src/tests/mod.rs
  • grovedb/src/tests/non_counted_tests.rs
  • merk/src/element/reconstruct.rs
  • merk/src/proofs/query/verify.rs
  • merk/src/test_utils/mod.rs
✅ Files skipped from review due to trivial changes (5)
  • merk/src/proofs/query/verify.rs
  • grovedb/src/debugger.rs
  • grovedb/src/replication.rs
  • grovedb/src/tests/batch_coverage_tests.rs
  • grovedb/src/batch/estimated_costs/worst_case_costs.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • grovedb-element/src/element/serialize.rs
  • grovedb-element/src/element/mod.rs
  • grovedb-element/src/element/helpers.rs

Comment thread grovedb-element/src/element_type.rs
Comment thread grovedb/src/batch/mod.rs
… tests)

CodeRabbit follow-up review on 10d3821 surfaced three more issues:

1. ElementType::from_serialized_value accepted invalid inner discriminants.
   The bitwise OR `Self::try_from(NON_COUNTED_FLAG | inner_byte)` is a no-op
   when `inner_byte` already has bit 7 set. A payload like `[15, 128, ...]`
   silently parsed as `NonCountedItem` (since 0x80 | 0x80 == 0x80) instead
   of being rejected. Fix: explicitly require `inner_byte < 15` (i.e. a
   real base discriminant 0..=14) before applying the bit. New rejection
   tests cover [15, 128], [15, 142], [15, 16], [15, 100].

2. GroveOp::RefreshReference rejected wrapped references.
   The arm matched `&element` directly as `Element::Reference`, so a
   stored `NonCounted(Reference)` failed refresh as "not a reference".
   Fix: match `element.underlying()` so the wrapper is transparent for
   refresh — the inner reference's path is what matters.

3. check_subtree_exists rejected wrapped tree parents.
   Although get_caching_optional now unwraps NonCounted, the subtree
   validation helper still only accepted bare tree variants. Any path
   through a `NonCounted(Tree)` parent failed validation, breaking the
   wrapper-transparency contract. Fix: `element.map(|e| e.into_underlying())`
   before matching tree variants. Regression test
   `check_subtree_exists_through_non_counted_wrapper` inserts a child
   into a wrapped subtree and asserts it succeeds.

Also tightened existing tests per nitpicks:
- batch_insert_rejects_non_counted_into_normal_tree now asserts the
  error message contains "non-counted" (catches the parent-type guard
  specifically, not any unrelated batch failure).
- batch_propagation_preserves_non_counted_wrapper_on_subtree now also
  reads the outer count tree's aggregate and asserts it equals 1
  (i.e. the NonCounted subtree did NOT contribute to the count). This
  protects both invariants the test documents.

Test counts (with --features minimal,test_utils,full):
- grovedb-element: 45 (unchanged; from_serialized_value rejection
  cases added to existing test)
- grovedb-merk: 364 (unchanged)
- grovedb: 1453 (was 1452, +1: check_subtree_exists regression)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer
Copy link
Copy Markdown
Member Author

Codex follow-up review:

I re-fetched the updated PR at 10d38210 and rechecked the five prior findings. The fixes address my original concerns:

  • nested NonCounted is rejected before bincode decode
  • batch propagation preserves and re-applies the wrapper
  • reconstruct_with_root_key re-wraps NonCounted trees
  • batch insert now enforces count-bearing parents
  • get / follow_reference handle NonCounted(Reference)

Focused verification run:

  • cargo test -p grovedb-element deserialize_rejects_long_nested_wrapper_chain_without_recursion --all-features
  • cargo test -p grovedb-merk reconstruct_preserves_non_counted_wrapper --all-features
  • cargo test -p grovedb non_counted --all-features

All passed. The grovedb run still emits existing test warnings, but I do not see remaining blockers from my previous review.

Copy link
Copy Markdown
Member Author

@QuantumExplorer QuantumExplorer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed

Targeted audit (security-auditor agent) found 5 categories of catchall
match arms over Element where NonCounted was silently rejected or
mis-handled instead of dispatching through to the inner element.

HIGH: typed non-Merk tree APIs rejected wrapped trees
The mmr_tree_*, bulk_*, commitment_tree_*, and dense_tree_* methods all
matched bare variants like `Element::MmrTree(..)` and treated
`NonCounted(MmrTree)` as "not the expected tree", returning
InvalidInput. NonCounted-wrapped non-Merk trees are a perfectly valid
use case (insert a wrapped MmrTree inside a CountTree to suppress its
count contribution), so this broke every read/write path.
- operations/mmr_tree.rs (5 sites)
- operations/bulk_append_tree.rs (7 sites)
- operations/commitment_tree.rs (5 sites)
- operations/dense_tree.rs (5 sites)
Fix: dispatch via element.underlying() / into_underlying() everywhere.
Test: typed_mmr_api_works_through_non_counted_wrapper exercises a
wrapped MmrTree end-to-end via mmr_tree_leaf_count.

HIGH: query_encoded_many rejected wrapped references and resolved items
operations/get/query.rs:90 matched
QueryResultElement::ElementResultItem(Element::Reference(..)) directly,
so any wrapped reference fell into the catchall as "path_queries can
only refer to references". The resolved value at line 106 had the
same problem. Fix: extract the element first then match
.into_underlying(), mirroring the pattern already used elsewhere in
the file.

MEDIUM: aggregate_sum_query rejected NonCounted-wrapped sum items
element/aggregate_sum_query/mod.rs:775 — exactly the kind of element
the wrapper exists for (suppress count, keep sum). Now matches
.into_underlying().

MEDIUM: batch flag-update returned wrong/None cost for wrapped trees
batch/mod.rs:2443 — the just-in-time flag-update path dispatched on
bare new_element and routed `NonCounted(Tree)` through the `_ => None`
arm, dropping the layered cost. Fix: capture wrapper_overhead, then
match on .underlying() and add wrapper_overhead to value_len in each
constant-size branch (parallel to merk/src/element/costs.rs).

MEDIUM: estimated_costs replace paths inconsistent with insert paths
estimated_costs/average_case_costs.rs:329 (replace_element) and
estimated_costs/worst_case_costs.rs:189, 244 (insert_element +
replace_element) all routed `NonCounted(Tree)` through the
serialized_size fallback instead of the layered cost arm. Average's
insert path was already correct (uses tree_flags_and_type) — the
replace paths and worst_case insert/replace were inconsistent.
Fix: same wrapper_overhead pattern + match .underlying().

Test counts (with --features minimal,test_utils,full):
- grovedb-element: 45 (unchanged)
- grovedb-merk: 364 (unchanged)
- grovedb: 1454 (+1: typed_mmr_api_works_through_non_counted_wrapper)

Clippy clean with --workspace --all-features.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer
Copy link
Copy Markdown
Member Author

Self Reviewed

@QuantumExplorer QuantumExplorer merged commit 618affa into develop May 9, 2026
11 of 12 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/ecstatic-euclid-fc6666 branch May 9, 2026 17:12
QuantumExplorer added a commit to dashpay/platform that referenced this pull request May 9, 2026
…ange)

Brings in dashpay/grovedb#654 (Element::NonCounted wrapper) and #656
(QueryItem::AggregateCountOnRange + Node::HashWithCount). Both are
prerequisites for the `range_countable` index property that the
parallel design work in `book/src/drive/indexes.md` depends on:

- `Element::NonCounted(Box<Element>)` — wrapper variant whose count
  contributes 0 to a parent count tree's aggregate. Lets a count tree
  hold housekeeping rows / sibling sub-property continuations without
  polluting the count. Only insertable into count-bearing trees;
  nested wrappers rejected at construction / serialize / deserialize.
- `QueryItem::AggregateCountOnRange(Box<QueryItem>)` — count-only proof
  shape returning `(CryptoHash, u64)` in O(log n) bytes. Backed by a
  new self-verifying `Node::HashWithCount(kv_hash, l, r, count)` proof
  node so the count is bound by the proof, not trusted on faith.
  Restricted to `ProvableCountTree` / `ProvableCountSumTree` (and
  their `NonCounted*` wrappers) at proof time. Verified via
  `GroveDb::verify_aggregate_count_query`.

Together these unblock implementing `range_countable` indexes (per-
node counts on the property-name tree, NonCounted wrappers for
sibling continuations) and `return_distinct_counts_in_range` /
range count queries on the no-prove and prove paths — both currently
gated as "not yet supported" in the unified count handler.

Workspace fixups required by the bump:

- `wasm-drive-verify` JS shim: add a `QueryItem::AggregateCountOnRange`
  arm in `serialize_query_item` (descriptive type, no recursion into
  the inner range — the wasm verify path doesn't drive these queries
  today, but the variant must be matched for the workspace to compile).
- `rs-sdk-ffi` path-elements display: add `Element::NonCounted(_)` arm
  reporting `"non_counted"` (placeholder display; we'll inflate it
  to describe the inner element when the wrapper is actually used in
  contracts).
- `rs-drive-abci` shielded common: orchard's transitive bump made
  `Action::from_parts` return `Option<Action>`. Wrap with `.ok_or_else`
  surfacing `InvalidShieldedProofError("invalid action parts")` rather
  than panicking; otherwise behaviorally unchanged.

Tests: 14 rs-drive count-query lib tests, 5 drive-abci handler tests,
3079 rs-drive lib tests, and 3435 dpp lib tests all still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QuantumExplorer added a commit to dashpay/platform that referenced this pull request May 10, 2026
…ing)

Per-index `rangeCountable: bool` flag, additive on top of `countable`.
When true, the index is laid out so that range-count queries on the
indexed property can be answered in O(log n):

- Property-name level: `ProvableCountTree` (per-node counts let a range
  query walk just the boundary path).
- Each value tree under it: `CountTree` (count-bearing so the
  property-name aggregate sums per-value counts cleanly).
- Sibling continuations inside a value tree: wrapped with
  `Element::NonCounted` so their counts don't pollute the value
  tree's count.

Depends on the grovedb features bumped in the previous commit
(`Element::NonCounted` + `QueryItem::AggregateCountOnRange` from
dashpay/grovedb#654 + #656).

This commit lands the schema-level plumbing only:

- `Index.range_countable: bool` field + serde derive.
- Index parser reads `"rangeCountable"` (boolean only — no enum form
  needed).
- Cross-field validation in `Index::try_from`: `range_countable: true`
  requires `countable.is_countable()`. Without that, it would change
  layout of a non-count-bearing tree, which is meaningless.
- v1 meta-schema schema entry under each index in `documentSchemas`.
- Protocol-version gate in `try_from_schema/v1`: `range_countable: true`
  on protocol_version < 12 raises `UnsupportedFeatureError`. Pre-v12
  nodes therefore reject the contract at validation time, before any
  state mutation. Mirrors the existing v12 gate on countable indexes.
- `IndexLevelTypeInfo.range_countable` populated from the source index
  so the insert/delete walkers can reach it (used in a follow-up).
- `random_index` default + ~16 IndexLevel test-init sites updated.

Storage layout change (the actual `NonCounted` wrapping +
`ProvableCountTree`/`CountTree` selection in the insert / delete
walkers) is **deferred to a follow-up commit**. Until that lands,
`IndexLevelTypeInfo.range_countable` is read but not yet acted on —
the on-disk layout is unchanged, so the schema gate is the only gate
in effect right now. Combined with the v12 protocol gate, no v11 node
ever sees a `range_countable` contract, and no v12 node yet emits
NonCounted-wrapped writes.

Tests: 79 dpp index tests, 14 rs-drive count-query lib tests, 5
drive-abci handler tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QuantumExplorer added a commit to dashpay/platform that referenced this pull request May 10, 2026
Adds the foundational helper for the upcoming `range_countable` storage
layout, plus a runtime guard that fails loudly if a v12+ contract with
`range_countable: true` reaches the insert walker before the rest of the
storage-layout work lands.

- `LowLevelDriveOperation::for_known_path_key_empty_non_counted_normal_tree`:
  builds a `GroveOperation` that inserts `Element::NonCounted(empty_tree())`
  at the given path and key. The wrapper makes the inserted subtree
  contribute 0 to a parent count tree's aggregate (per dashpay/grovedb#654),
  which is what the index walker needs for sibling continuations under a
  `range_countable` value tree (e.g., the `'shape'` continuation under a
  `byColor` value tree, when `byColor` is range_countable but
  `byColorShape` shares its prefix). Construction is infallible by
  `new_non_counted`'s contract — the `expect` documents the invariant.

- `add_indices_for_top_index_level_for_contract_operations_v0` and
  `add_indices_for_index_level_for_contract_operations_v0`: both now
  inspect `sub_level.has_index_with_type().range_countable` and return
  `DriveError::NotSupported` if true, with TODO comments pointing to the
  exact lines that need to switch tree types and the helper to use for
  NonCounted wrapping. Belt-and-suspenders alongside the rs-dpp v12
  validation gate added in the previous commit — pre-v12 nodes already
  reject the contract; on v12+ the contract reaches here and we refuse
  rather than corrupt the count aggregation by writing a NormalTree
  where a CountTree / ProvableCountTree / NonCounted is required.

Tests: 79 dpp + 14 rs-drive + 5 drive-abci tests still pass.

Next chunks (still TODO on this PR — best as separate focused commits):
- Insert walker: switch property-name tree to ProvableCountTree, value
  tree to CountTree, and wrap sibling continuations with NonCounted
  when `IndexLevelTypeInfo.range_countable` is true. Threads a
  `parent_value_tree_is_count_bearing` flag through recursion.
- Same in cost-estimation paths (`EstimatedLayerInformation.tree_type`).
- Mirror in delete (`remove_*_for_index_level_*`).
- Count picker: accept `range_countable` indexes for range operators.
- `DriveDocumentCountQuery::execute_no_proof` range mode via
  grovedb's `AggregateCountOnRange` query item.
- Drive-abci handler: route `return_distinct_counts_in_range = true` to
  the new range-mode logic instead of erroring.
- Drop the `u16::MAX` materialization cap on prove path for range counts
  via `verify_aggregate_count_query`.
- Tests covering count-aggregation correctness with NonCounted siblings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QuantumExplorer added a commit that referenced this pull request May 10, 2026
)

* feat: add Element::NotSummed wrapper to opt-out of sum propagation

Symmetric counterpart to Element::NonCounted (#654). Where NonCounted
opts a child out of count propagation in count-bearing trees, NotSummed
opts a sum-bearing subtree out of sum propagation in sum-bearing trees.

The wrapper is strictly typed: it can ONLY contain one of the four
sum-tree variants (SumTree, BigSumTree, CountSumTree,
ProvableCountSumTree), and may only be inserted into sum-bearing parent
trees. Items, sum items, references, non-sum trees, and any wrapper
nesting are rejected at construction, serialization, and
deserialization.

When a NotSummed-wrapped sum-tree is inserted into a sum-bearing parent,
it contributes 0 to the parent's running sum. Counts (if any) still
propagate. The inner sum-tree retains its own internal sum aggregate
unchanged — only the outward propagation is suppressed.

Discriminant scheme:
- bincode 16 (Element variant)
- ElementType twins at 0x40 | base: 68, 69, 71, 74 (only the four legal
  sum-tree base discriminants). The 0x40 flag bit is disjoint from
  NonCounted's 0x80, so wrapper status is independently testable.

Stack-overflow defense: deserialize pre-checks the leading two bytes
for any wrapper combination ([15,15], [15,16], [16,15], [16,16]) and
rejects before bincode recurses through the Box<Element> chain.

Wiring:
- grovedb-element: enum + constructor whitelist + serialize/deserialize
  guards + helpers (sum_value_or_default → 0, count still propagates) +
  ElementType twins + visualize.
- grovedb-query: TreeFeatureType::zero_sum() symmetric helper.
- merk: TreeType::is_sum_bearing(), insert/insert_subtree/
  insert_reference parent-type guards, reconstruct preserves wrapper,
  costs/get account for the wrapper byte.
- grovedb: batch propagation tracks not_summed alongside non_counted in
  InsertTreeWithRootHash, parent-type guard, estimated cost overhead,
  debugger renders inner element transparently.

Tests (725+ existing pass, plus new):
- grovedb-element: constructor whitelist, helper passthrough, bincode
  round-trip, deserialize rejects nested chains and non-sum-tree inner.
- grovedb-merk: insert rejected in NormalTree/CountTree, accepted in
  SumTree contributes 0, NotSummed(SumTree(_,100,_)) in
  ProvableCountSumTree → count=1 sum=0, reconstruct preserves wrapper.
- grovedb: 5 end-to-end tests covering batch parent-type guard,
  batch propagation preserving the wrapper, check_subtree_exists
  through the wrapper, and outer sum aggregate excluding the subtree.

Breaking changes: none for existing callers. The on-disk serialization
format gains bincode discriminant 16 — older code reading data written
with this variant fails to deserialize, matching the existing
"ONLY APPEND TO THIS LIST" comment on Element.

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

* test: add coverage for zero_sum, NotSummed Display/element_type, and tree extensions

CodeRabbit's Codecov pass on PR #659 flagged 86.42% patch coverage. Most
gaps were unreachable!() arms (genuinely uncoverable), but several
testable additions had no direct unit tests yet.

- grovedb-query/src/proofs/tree_feature_type.rs: new tests module
  covering both new and existing helpers — `zero_sum` (each variant +
  no-op cases), `zero_count` (mirror), `count`. The file had no test
  module previously.
- grovedb-element/src/element/mod.rs: new tests for `Display` of
  `NotSummed` and `NonCounted`, plus `element_type()` resolving to the
  correct `NotSummedXxx` / `NonCountedXxx` synthetic twin for each
  legal inner.
- merk/src/element/tree_type.rs: tests asserting all
  `ElementTreeTypeExtensions` methods (`tree_type`, `maybe_tree_type`,
  `root_key_and_tree_type{,_owned}`, `tree_flags_and_type`,
  `tree_feature_type`) delegate through the `NotSummed` wrapper, plus
  `get_feature_type` zeros sum (and propagates count where
  applicable) for all four sum-bearing parent tree types.

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

* fix(reference_path): look through wrappers when following reference chains

`reference_path::follow_reference` matched on the resolved `element`
directly, so a `NonCounted(Reference)` would have been returned as a
target value rather than followed as a hop. The function is currently
only used from tests (production reference resolution goes through
`GroveDb::follow_reference` in `operations/get/mod.rs`, which already
unwraps via `into_underlying()`), but the divergence was a latent
trap for any future wiring.

Symmetric to the existing get-path unwrap. The wrapper byte is part of
`value_hash` (computed from the on-disk bytes earlier in the function),
so dropping it from `target_element` does not affect cryptographic
verification of subsequent hops.

`NotSummed` cannot wrap a reference (constructor whitelist enforces
sum-tree variants), so this fix is purely forward-safe and symmetric
to the `NonCounted` handling.

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

* fix(element): close cross-wrapper construction + serde validation gaps

Addresses Codex's review of PR #659.

**P1 — Reject cross-wrapper construction**
`new_non_counted` previously rejected only `NonCounted` inners, so a
caller could build `NonCounted(NotSummed(SumTree))` even though
serialization rejects cross-wrapper nesting. Such a value would reach
batch execution with `is_non_counted() == true` and
`underlying() == NotSummed`, then hit the supposedly-unreachable
wrapper arm.

- `Element::new_non_counted` now rejects both `NonCounted` and
  `NotSummed` inners with `InvalidInput`.
- `Element::into_non_counted` is now fallible (`Result<Self,
  ElementError>`) and returns `Err` for `NotSummed` input. Two
  in-tree callers in `grovedb/src/batch/mod.rs` already pass
  freshly-constructed bare tree elements; updated to `.expect(..)`
  with a comment documenting the precondition.
- New regression in `grovedb/src/tests/not_summed_tests.rs` builds a
  hand-rolled `NonCounted(NotSummed(SumTree))` (bypassing the
  constructor) and asserts `apply_batch` rejects it as a typed error
  rather than panicking.
- Two new unit tests in `grovedb-element/src/element/helpers.rs`:
  `into_non_counted_rejects_not_summed` and
  `new_non_counted_rejects_not_summed`.

**P2 — Manual serde Deserialize with wrapper validation**
The previous `derive(serde::Deserialize)` did not enforce the wrapper
invariants. With the `serde` feature enabled, payloads could
construct invalid `NotSummed(Item)` or cross-wrapper values, and
deeply-nested wrapper payloads recursed before any check fired.

- Replaced the derive on `Element` with a manual `Deserialize` impl
  in a `serde_impl` module, gated behind `feature = "serde"`.
- The impl deserializes through a private `ElementShadow` mirror enum
  (`#[serde(rename = "Element")]` so the wire format is unchanged),
  converts to `Element`, then calls
  `Element::check_recursive_wrapper_invariants` to validate every
  level of the tree.
- New public `Element::validate_wrapper_invariants` codifies the
  rules in one place (used by both the serde path and available to
  external callers).
- `Serialize` derive is preserved — serialization of a valid Element
  is always safe.

New serde-feature tests in `grovedb-element/src/element/mod.rs`:
- `serde_round_trip_valid_elements` — JSON round-trip for Item,
  SumTree, NonCounted(Item), NotSummed(SumTree).
- `serde_rejects_nested_non_counted` / `serde_rejects_nested_not_summed`
  — nested same-wrapper payloads.
- `serde_rejects_cross_wrapper_nesting` — both
  `NonCounted(NotSummed(_))` and `NotSummed(NonCounted(_))`.
- `serde_rejects_not_summed_with_non_sum_tree_inner` — six illegal
  inner types covering items, plain trees, count trees, MMR.
- `serde_rejects_deeply_nested_wrapper_chain` — depth-64 chain
  rejected via the per-level check fired during recursive
  conversion.

Adds `serde_json` as a dev-dependency for the JSON round-trip tests.

Test counts: grovedb-element 63 lib (was 57 + 6 serde = 63),
merk 418, grovedb 1496 (was 1495 + 1 regression).

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

* refactor(element): tighten NotSummed API + relocate twin discriminants

Three follow-ups from review:

**1. Replace `into_not_summed_unchecked` (panicking) with fallible
`into_not_summed`.** The panic path was a latent foot-gun. The new
helper mirrors `into_non_counted`'s API: returns `Result`, idempotent
on `NotSummed`, returns `Err(InvalidInput)` for `NonCounted` (mutually
exclusive) or any non-sum-tree variant. The single in-tree caller
(batch propagation) now uses `.expect(..)` with a comment documenting
that the input is always a freshly-constructed bare sum-tree element.

**2. Move `NotSummed` twin discriminants from `0x40 | base` to
`0xb0 | base`.** This places the four twins at 180, 181, 183, 186 —
inside the high-bit-set range alongside `NonCounted` twins (128..142),
rather than down at 64..78 mid-range. The two ranges are still
distinguished by the upper nibble: `0x80` for `NonCounted`, `0xb0` for
`NotSummed`. Detection is now an upper-nibble compare instead of a
single-bit test:
- `is_non_counted`: `disc & 0xf0 == 0x80` (was `disc & 0x80 != 0`).
- `is_not_summed`: `disc & 0xf0 == 0xb0` (was `disc & 0x40 != 0`).

The single-bit `is_non_counted` would have started returning true for
`NotSummed` twins too once they shared bit 7, which would have been
wrong. Upper-nibble compares keep the predicates disjoint. Renamed
the now-misnamed `NOT_SUMMED_FLAG` constant to `NOT_SUMMED_TWIN_PREFIX`
(`= 0xb0`) and updated `NOT_SUMMED_BASE_MASK` to `0x0F` (sufficient
since base discriminants are 0..14).

Updated the `try_from`, `from_serialized_value`, the discriminant-pinning
test, and added a new `test_not_summed_helpers` mirroring
`test_non_counted_helpers`.

**3. Remove unused `TreeFeatureType::zero_sum` helper.** It was added
preemptively for symmetry with `zero_count` but had no production
callers — `Element::sum_value_or_default` already returns 0 for
`NotSummed`, so `get_feature_type` produces the right zero-summed
feature type without needing this helper. Removed the function and
its unit test.

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

* docs(element): explain why the serde shadow enum is necessary

The `serde_impl` module already had a comment describing the *approach*
(shadow + From conversion + recursive validation), but not the *why* —
which serde patterns fail and which alternatives exist. Codifies the
trade-off so future maintainers can see at a glance that:

- `#[serde(try_from)]` needs a separate source type (no self-pointing).
- `#[serde(remote)]` is for foreign types only.
- `#[serde(deserialize_with)]` is field-level.
- `#[serde(transparent)]` / `flatten` are for single-field structs.

Leaving three real options for derive-and-validate: shadow enum,
manual Visitor, or drop the derive. We keep the derive (external
tooling consumers may rely on it) and use the shadow as the shortest
correct form.

Doc-only change.

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

* refactor(batch): replace .expect on wrapper conversions with typed errors

The three `.expect(..)` calls added in commit 4a93a88 around the
batch propagation wrapper-rewrap path would panic if a future change
ever passed a pre-existing wrapper element through these branches.
Replace them with `CorruptedCodeExecution` errors so the failure
surfaces as a typed `Result` and bubbles up via the cost-aware return
machinery instead of aborting the process.

Sites:
- `GroveOp::InsertTreeWithRootHash` propagation: `into_non_counted` and
  `into_not_summed` calls now `map_err(..)` to `CorruptedCodeExecution`,
  drained via `cost_return_on_error_no_add!`.
- `GroveOp::InsertNonMerkTree` propagation: same treatment for the
  `into_non_counted` call.

Behavior in the happy path is unchanged — the input elements are
freshly built from `aggregate_data` / `meta.to_element(..)`, never
wrapped, and the wrappers always succeed.

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

---------

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant