Skip to content

Add trie index format 2 and fix format 1 bugs - #15534

Open
zaidoon1 wants to merge 4 commits into
meta:mainfrom
zaidoon1:zaidoon/udi-12-trie-index-v2
Open

zaidoon1 wants to merge 4 commits into
meta:mainfrom
zaidoon1:zaidoon/udi-12-trie-index-v2

Conversation

@zaidoon1

@zaidoon1 zaidoon1 commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds trie index format 2, an opt-in serialized format for the experimental trie user-defined index (TrieIndexFactory), and fixes correctness bugs in the existing format 1. Readers accept both formats; the factory keeps writing format 1 by default.

Stacked commits. This branch also carries three prerequisite commits that have no separate PR. They can be reviewed commit by commit:

  • 85a538c Add index_mode and read_index to db_stress, db_bench, and crashtest
  • 80766ff Document the IndexFactory component
  • 2e131b2 Add IndexFactory release notes

The rest of this description covers the main commit, a4d2c11.

Format 2 layout (full spec in utilities/trie_index/trie_v2.h):

index block (little-endian, read in place at any alignment)
├── header (120 B): magic "TRIE", version 2, counts, section offsets
├── prefix shared by every separator, stored once
├── trie over bucket heads: path-compressed typed nodes
│     (leaf / sparse / 256-bit bitmap), narrow backward child offsets
├── bucket table + bucket data: 8 separators per bucket, front coded
├── offset column (+ size column only if block gaps are irregular)
│     linear frame of reference; each group of 64 values has its own
│     base, slope, and residual bit width
├── run table: separators shared by several blocks (one user key)
└── tag table: blocks that need a packed seqno/type to disambiguate
Seek(key, tag)
  strip the shared prefix
  descend the trie to the floor/ceiling bucket head
  scan <= 8 front-coded separators in that bucket
  apply tag within a run of equal separators
  handle = offset column[i]; size derived from the next offset or read from the size column

Why the change is required

The trie index is meant to beat the standard index on prefix-heavy keys, but format 1 does not. On a 3M-key, 48-byte-key DB with about 2 blocks per prefix it is 8x the size of the standard index at index_block_restart_interval=16 and about twice as slow on Get as the default standard index (table below). Most of its size is per-edge child-position tables (also copied to the heap at open) and an always-on seqno table. Format 1 also has correctness gaps that matter for production use (below). Fixing size and speed needs a new layout, so this adds a format version instead of changing format 1 in place.

Previous behavior

  • Empty user key: the builder dropped a zero-length separator, leaving the handle and seqno arrays one entry short. An SST with "" as a separator (a lone Put("", v), or a first data block holding only "") failed to flush with Corruption in kStandardRequired, kCustomDefault, and kCustomOnly, which stopped writes; in kStandardDefault its trie was silently unused.
  • Comparator: DB::Open accepted the trie factory with a non-bytewise comparator; the first flush then failed with a background error.
  • 4 GiB: offsets and sizes are 32-bit, so a data block past 4 GiB fails SST construction with NotSupported in every mode. This is unchanged for format 1.
  • Byte order: arrays are host-order memcpys, so indexes are not portable between little- and big-endian hosts.
  • Malformed blocks: the reader trusted counts read from the block. A block that passes its checksum but is malformed (e.g. from a buggy writer) could throw std::length_error from a huge resize, or read out of bounds after an overflow-count sum wrapped in 32 bits. Unknown header flags and a nonzero reserved field were accepted.
  • Memory accounting: ApproximateMemoryUsage() omitted overflow_base_ and the aligned copy made for unaligned blocks.

New behavior

  • TrieIndexFactory takes format_version (1 or 2), also settable as user_defined_index_factory={id=trie_index;format_version=2}. Format 1 stays the default. NewReader dispatches on the version in the header, so every factory reads both formats and a DB can mix them.
  • Format 2 has no 4 GiB limit and is portable across byte orders.
  • Format 1: the empty user key is stored and found, malformed blocks fail with Corruption at open, and auxiliary memory is fully counted.
  • DB::Open (through TrieIndexFactory::ValidateOptions) rejects a non-bytewise comparator, a format_version other than 1 or 2, and format 1 on big-endian hosts.
  • db_stress, db_bench, and db_crashtest.py take --trie_index_format_version; the crash test draws it per invocation.

Implementation

  • Add utilities/trie_index/trie_v2.{h,cc}: TrieV2Builder, TrieV2Index, and TrieV2Iterator.
    • TrieV2Index::Init validates the header, section bounds, trie, columns, and side tables, and checks that block offsets never decrease (with room for the gap after each block when sizes are derived, so a size cannot wrap). Bucket data is checked as it is decoded. All loads are unaligned little-endian DecodeFixed* reads on the block in place, so the reader allocates nothing proportional to the index.
    • Each 64-value group of a column stores its own base, slope, and residual width, so one outlier block (e.g. a huge block or a large gap) widens only its own group. The same idea is used by Lucene's DirectMonotonicWriter and tantivy's blockwise linear codec.
    • TrieV2Builder::Finish returns InvalidArgument if block offsets decrease.
  • Add TrieV2IndexReader and TrieV2IndexIterator in trie_index_factory.{h,cc}. Scan-bound handling for Prepare() moved into a TrieScanBounds helper shared by both iterators.
  • Change TrieIndexBuilder to serialize its buffered entries in the configured format. For format 2, EstimatedSize() tracks key bytes, front-coding headers, handle ranges, duplicates, and tags incrementally and passes them to TrieV2Builder::SizeBound(), which sits next to the serializer. Blocks still being compressed in a parallel build are charged like the largest block so far, so the estimate can fall short until their handles arrive.
  • Fix format 1 in louds_trie.cc and bitvector.cc:
    • store the empty key at the root;
    • add LoudsTrie::ValidateStructure(): node and leaf counts, child-position tables (checked in one word-at-a-time pass over s_louds), a BFS level walk bounded by max_depth, and chains re-walked against the trie;
    • add Bitvector::ValidateDerivedData(): rank LUT, select hints, and bits past the end. Select-hint construction moved into ForEachSelectHint, shared by the writer and the validator;
    • bound num_chains, use a 64-bit overflow prefix sum, and reject unknown flags.
  • Docs: the table.h rollback text said lowering index_mode takes effect immediately. A table reader fixes its routing when it opens the SST, so a SetOptions() change reaches only SSTs opened afterwards; the text now says so. The IndexFactory component doc describes both formats and the new rejections. Three release notes are added.

Safety, compatibility, and limitations

  • Default unchanged. Without format_version=2, new SSTs are format 1 and readable by released versions.
  • Existing format 1 SSTs. The format 1 writer and bitvector serialization are unchanged since 11.1 apart from comments, so the new open-time validation accepts every released format 1 index. A golden format 1 index in the tests pins this. New format 1 SSTs that contain the empty key are readable by older versions, but older readers mishandle SeekToLast on an SST whose only separator is ""; such SSTs could not be written before.
  • Format 2 blocks downgrade. Released versions (11.1 to 11.13) fail the table open when the trie reader fails, in every index mode, so they cannot open an SST with a format 2 trie while the trie factory is configured. Downgrading requires switching back to format 1 and compacting first, as the component doc says; the release note says older versions cannot open these SSTs.
  • OPTIONS files now serialize user_defined_index_factory={id=trie_index;format_version=N;}. Older binaries need ignore_unknown_options to load them, as with any new option.
  • DB::Open rejections apply only when the trie factory is configured. Those configurations previously failed at flush time or, on big-endian, wrote non-portable indexes.
  • Open cost. Validation runs once per table open. CPU time of NewReader alone, release flags, on db2-shaped data (4 SSTs of about 14.8K data blocks each):
    • Format 1 NewReader goes from 121 to 320 us per SST with a 2.5 MB index. Most of the increase is the child-position check, at about 1 ns per trie node.
    • Format 2 NewReader takes 26 us per SST with a 0.33 MB index. 18 us of that is the offset-order check, which is reader code only and can be made cheaper without a format change.
  • Size tradeoff. On the 2-blocks-per-prefix shape, format 2 is 5% larger than the standard index at restart interval 16, though 2x smaller than at the default restart interval of 1. The writer fixes the bucket size at 8; the format stores it, so it can be tuned later without a format change.
  • Deferred: flipping the default to format 2 in a later release, removing the format 1 writer, and trie coverage in check_format_compatible.sh.

Door: two-way to merge, since format 2 is opt-in and the default is unchanged. Per DB, writing format 2 is one-way for downgrade until those SSTs are rewritten as format 1.

Blast radius: trie index users only.

Test Plan

Performance. Release db_bench, format_version=8, on an Apple Silicon laptop:

  • Data: 3M keys, key_size=48, prefix_size=32, value_size=100, no compression, 10-bit bloom, 4 KiB blocks, fillrandom,compact with --seed=7 so every Get is found. db2 has 100 keys per prefix (about 2 blocks per prefix); db3 has 20000 keys per prefix (hundreds of blocks per prefix).
  • Trie DBs are written in kCustomOnly, so only the trie index is built and read.
  • Latencies are medians of three interleaved rounds of readrandom --reads=1500000 and seekrandom --reads=500000 --seek_nexts=10, single-threaded. "Cached" uses a 2 GiB block cache holding everything; "32 MiB" puts data, index, and filter blocks in a 32 MiB block cache.
  • Sizes are the index meta block (trie) or index block (standard) from table properties.
  • Format 1 was measured in a later run, interleaved with format 2. Format 2's numbers in that run were within -3% to +6% of the main run, so read the format 1 column as approximate.
us/op unless noted std, restart 1 std, restart 16 trie format 1 trie format 2
db2 Get, cached 1.285 1.351 2.580 1.263
db2 Seek + 10 Next, cached 2.692 2.817 4.417 2.709
db3 Get, cached 1.354 1.389 1.697 1.292
db3 Seek + 10 Next, cached 2.692 2.702 3.097 2.679
db2 Get, 32 MiB 2.254 2.318 3.755 2.210
db2 Seek + 10 Next, 32 MiB 3.832 3.869 5.884 3.748
db3 Get, 32 MiB 2.271 2.257 2.635 2.104
db3 Seek + 10 Next, 32 MiB 3.742 3.813 4.196 3.615
db2 index size (B/block) 43.8 21.1 168.5 22.1
db3 index size (B/block) 44.2 6.9 26.3 4.4

Not measured: Linux, concurrent reads, or reads from storage.

Unit and DB tests (macOS, Apple clang, debug):

  • trie_index_test (249 tests) covers:
    • the format 2 core: randomized Seek/Next/Prev against a linear-scan reference at bucket sizes 1, 2, 3, 8, and 64, plus same-key runs and tags, golden bytes, malformed data, an outlier block widening only its own column group, and decreasing offsets (rejected by the writer, and by the reader when forged);
    • the factory tests, parameterized over both formats, including the EstimatedSize() bound with uniform and irregular block handles;
    • the golden format 1 index, format selection through options strings, and the format 1 reader rejecting format 2.
  • trie_index_db_test (558) runs every test in kStandardDefault, kCustomDefault, and kCustomOnly with both formats, plus new tests for the empty user key, mixed formats in one DB, and the DB::Open rejections.
  • With ASSERT_STATUS_CHECKED=1, trie_index_test, trie_index_db_test, and table_test pass (7,873 tests). db_crashtest_test.py passes.
  • With COERCE_CONTEXT_SWITCH=1, the new and changed tests (OutlierBlockWidensOnlyItsGroup, DecreasingOffsets, EstimatedSizeBoundsSerializedTrie, BlockHandleEncodingLimit) pass 100 repeats.
  • MalformedIndexIsRejectedOrReadable fails with either format 1 reader fix reverted: an uncaught std::length_error without the num_chains bound, and the GetOverflowHandle bounds assertion without the 64-bit sum. DecreasingOffsets fails without the reader's offset-order check.

Fuzzing. ASAN and UBSAN builds (Homebrew clang) open and iterate truncated and byte-flipped indexes:

  • format 1 through TrieIndexFactory(1): 1.9M cases over 6 seeds, clean;
  • the format 2 core, on random builds at bucket sizes 1 to 9 with regular and irregular gaps and offsets past 4 GiB: 3.4M cases over 3 seeds, clean.

Crash test. db_crashtest.py blackbox --simple --use_trie_index=1 ran for 8 minutes each with --index_mode=1 and --index_mode=3. Each run had 9 kill-and-reopen invocations that drew format 1 or 2 per invocation (4 and 5, then 6 and 3), so each DB mixed both formats. Both passed. Compression was pinned to lz4 because this build lacks Snappy.

make check ran on the previous revision of this commit and passed except for failures independent of this change (listener_test file-operation tests that also fail at the base commit, two load-sensitive db_sst_test SstFileManager tests that pass alone, and tools missing from that build). It was not rerun after the column layout change, which touched only utilities/trie_index and the release note.

CI passes on a4d2c11, including the MSVC jobs. Earlier pushes failed on MSVC because a test local was named far, which <windows.h> defines as a macro, and on GCC -Wshadow because of a lambda parameter in louds_trie.cc. Both are fixed.

Not tested locally: Linux and Windows (covered by CI builds and their test shards), big-endian (no CI job; format 1 test cases are not instantiated there), and an actual downgrade to a released binary.

db_stress could only build the trie as the secondary index or as the
default read index (use_udi_as_primary_index), and selected it per read
through the legacy ReadOptions::table_index_factory pointer. db_bench
could only build it as the secondary index. kCustomOnly,
kStandardRequired, and the kBuiltin/kPreferCustom read selectors were
never exercised outside unit tests.

Replace use_udi_as_primary_index with --index_mode, defaulting to the
library default, and add --read_index, whose auto value keeps reading
through the trie when it is the secondary index. Both tools reject
combinations that cannot work.

The crashtest draws index_mode once per DB together with
use_trie_index, draws read_index per invocation, and only disables the
paths that cannot work in a mode: backup and partitioned index/filters
when reads default to the trie, and kBuiltin reads and format_version < 6
with kCustomOnly.
Describe the index modes, per-read selection, on-disk markers, open-time
fallback, cache accounting, parallel builds, migration, and known gaps.
The IndexFactory API and parallel compression with a custom index
landed without release notes. Also correct the parallel_threads comment,
which still said a custom index forces single-threaded compression.
@meta-cla meta-cla Bot added the CLA Signed label Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

⚠️ clang-tidy: 11 warning(s) on changed lines

Completed in 721.4s.

Summary by check

Check Count
cert-err58-cpp 1
concurrency-mt-unsafe 5
cppcoreguidelines-avoid-non-const-global-variables 1
modernize-use-emplace 1
performance-inefficient-vector-operation 3
Total 11

Details

db_stress_tool/db_stress_test_base.cc (5 warning(s))
db_stress_tool/db_stress_test_base.cc:7003:5: warning: function is not thread safe [concurrency-mt-unsafe]
db_stress_tool/db_stress_test_base.cc:7014:5: warning: function is not thread safe [concurrency-mt-unsafe]
db_stress_tool/db_stress_test_base.cc:7019:5: warning: function is not thread safe [concurrency-mt-unsafe]
db_stress_tool/db_stress_test_base.cc:7028:5: warning: function is not thread safe [concurrency-mt-unsafe]
db_stress_tool/db_stress_test_base.cc:7037:5: warning: function is not thread safe [concurrency-mt-unsafe]
utilities/trie_index/trie_index_factory.cc (2 warning(s))
utilities/trie_index/trie_index_factory.cc:30:49: warning: initialization of 'trie_index_factory_type_info' with static storage duration may throw an exception that cannot be caught [cert-err58-cpp]
utilities/trie_index/trie_index_factory.cc:30:49: warning: variable 'trie_index_factory_type_info' is non-const and globally accessible, consider making it const [cppcoreguidelines-avoid-non-const-global-variables]
utilities/trie_index/trie_index_test.cc (4 warning(s))
utilities/trie_index/trie_index_test.cc:2538:7: warning: 'push_back' is called inside a loop; consider pre-allocating the container capacity before the loop [performance-inefficient-vector-operation]
utilities/trie_index/trie_index_test.cc:6687:13: warning: use emplace_back instead of push_back [modernize-use-emplace]
utilities/trie_index/trie_index_test.cc:6769:5: warning: 'push_back' is called inside a loop; consider pre-allocating the container capacity before the loop [performance-inefficient-vector-operation]
utilities/trie_index/trie_index_test.cc:6792:5: warning: 'push_back' is called inside a loop; consider pre-allocating the container capacity before the loop [performance-inefficient-vector-operation]

@zaidoon1
zaidoon1 force-pushed the zaidoon/udi-12-trie-index-v2 branch from 819c9f4 to cd9b272 Compare October 31, 2026 17:41
The trie index (format 1, a LOUDS succinct trie) was larger than the
standard index with index_block_restart_interval=16 on every measured key
shape and up to 84% slower on Get. It also had correctness gaps: it
dropped the empty user key, stored offsets as 32-bit host-order integers,
accepted any comparator until flush time, and crashed on some malformed
index blocks that still passed their checksum.

Add format 2, opted into with TrieIndexFactory's new format_version
option. It is a path-compressed byte trie over buckets of eight
front-coded separators, with block handles in linear frame-of-reference
columns that store a residual width per group of 64 values,
little-endian and read in place. The reader checks the whole block at
open, including that block offsets never decrease, so a malformed index
fails with Corruption. Readers accept both formats; the factory still
writes format 1 by default.

Fix format 1 in place: store the empty user key, reject unknown header
flags, validate the trie structure and bitvector rank/select data on
open, bound counts read from the block, and count all auxiliary memory.
DB::Open now rejects the trie factory with a non-bytewise comparator, an
unknown format_version, or format 1 on big-endian hosts.

db_stress, db_bench, and the crash test take --trie_index_format_version;
the crash test draws it per invocation so DBs mix both formats. Also
correct the table.h rollback text: an index_mode change through
SetOptions() only reaches SSTs opened afterwards.
@zaidoon1
zaidoon1 force-pushed the zaidoon/udi-12-trie-index-v2 branch from cd9b272 to a4d2c11 Compare October 31, 2026 17:47
@github-actions

Copy link
Copy Markdown

✅ Claude Code Review

Auto-triggered after CI passed — reviewing commit a4d2c11


Summary

This PR adds trie index format 2 (path-compressed trie over front-coded buckets with frame-of-reference columns) and fixes several correctness bugs in format 1 (empty user key handling, malformed block validation, memory accounting). The changes are well-structured and the bug fixes are sound. I identified 3 HIGH and 5 MEDIUM findings, none of which are blockers -- the HIGHs are existing bugs that this PR correctly fixes.

High-severity findings (3):

  • H1: ApproximateAuxMemoryUsage() underreports heap memory by omitting overflow_base_, aligned_copy_, and chain vectors -- correctly fixed by this PR
  • H2: overflow_base_ prefix sum uses uint32_t running_sum which could silently wrap on crafted input -- correctly fixed by this PR (changed to uint64_t with early overflow check)
  • H3: InitFromData() does not reject unknown flags, allowing silent misinterpretation of future format extensions -- correctly fixed by this PR
Full review (click to expand)

Findings

🔴 HIGH

H1: ApproximateAuxMemoryUsage() underreports memory (existing bug, fixed by PR)

louds_trie.h:280-283 currently only counts s_child_start_pos_ and s_child_end_pos_. The PR correctly adds overflow_base_, aligned_copy_, and chain-related vectors (s_chain_suffix_offsets_, s_chain_lens_, s_chain_end_child_idx_). This fix is important because ApproximateMemoryUsage() feeds into block cache accounting via TrieIndexReader::ApproximateMemoryUsage() at trie_index_factory.cc:683.

H2: overflow_base_ prefix sum overflow (existing bug, fixed by PR)

At louds_trie.cc:1486, the current uint32_t running_sum could silently wrap to 0 if total overflow blocks exceed UINT32_MAX. While unlikely with legitimate data (kMaxReasonableKeys caps at 2^30), a malformed trie with large block_counts could trigger this. The PR changes to uint64_t running_sum with early overflow checking during accumulation -- correct fix.

H3: No unknown flags rejection (existing bug, fixed by PR)

Current InitFromData() at louds_trie.cc:1088 reads the flags field but does not reject unknown bits. A trie written by a future version with new flag bits would be silently read with incorrect interpretation. The PR adds if (flags & ~kKnownFlags) rejection -- correct forward-compatibility measure.

🔶 MEDIUM

M1: TrieIndexBuilder constructor signature change adds defaulted parameter

Per CLAUDE.md, defaulted parameters are the "#1 trap on refactoring." The change from explicit TrieIndexBuilder(const Comparator*) to TrieIndexBuilder(const Comparator*, int format_version = 1) means existing callers silently get format 1, but a future caller who forgets the parameter gets unexpected behavior. Consider making format_version explicit (no default) since the factory always knows the version.

M2: Format version dispatch asymmetry (write vs. read)

TrieIndexFactory takes format_version to control writing, but NewReader dispatches on the version in the serialized header. This is architecturally correct (any reader handles any version), but the factory's format_version member is ignored during reads. This should be documented to prevent confusion -- the factory's format_version is write-only configuration.

M3: TrieScanBounds extraction must preserve exact CheckBounds semantics

The extraction of scan bound checking from TrieIndexIterator::CheckBounds into a shared TrieScanBounds helper must preserve the exact reference_key semantics: for Seek, reference_key = seek target; for Next, reference_key = previous separator. The conservative bound check (comparing reference_key, not current separator, against limit) is subtle and must not be altered. Verify that TrieV2IndexIterator passes the same reference_key as TrieIndexIterator in each call site.

M4: db_crashtest.py module-level random choices reduce per-invocation coverage diversity

_USE_TRIE_INDEX and _INDEX_MODE are drawn once at module level, fixing them for the entire test run. This is documented as intentional, but it means a single crash test invocation exercises only one configuration. The trie_index_format_version and read_index randomized per invocation partially compensate.

M5: Relaxed atomics in TrackHandle are correct but deserve a comment

max_block_step_, max_block_size_, and irregular_block_gaps_ use std::memory_order_relaxed for cross-thread size estimation during parallel compression. This is correct because: (1) these are approximate estimates, (2) they're read-only after the parallel phase completes (serialized Finish() runs single-threaded). A brief comment noting the ordering rationale would help future readers.

⚪ LOW / OBSERVATIONS

L1: Empty key fix is correct

The cutoff_level_=1 when max_depth_==0 ensures the root becomes a dense node, which is required for prefix key emission. The level > 0 guard on skip-pure-leaf prevents the root from being incorrectly skipped. The DescendToRightmostLeaf fix handles the case where root has a prefix key but no labels by checking d_is_prefix_key_ before attempting label traversal. Test coverage for empty keys should include: (a) single empty key, (b) empty key + non-empty keys, (c) seek to empty key, (d) reverse iteration from first non-empty key to empty key.

L2: ValidateStructure() performance is documented and acceptable

~200us per SST open is well within tolerance. The validation runs once per SST open for format 1 indexes and catches structural corruption that would otherwise cause silent wrong results or crashes during read operations.

L3: Format 2 portability improvement

Format 2 uses DecodeFixed32/64 (little-endian reads) instead of format 1's host-byte-order memcpy. This makes format 2 portable to big-endian architectures (e.g., s390x). Format 1's host-byte-order limitation is pre-existing and documented.

L4: num_chains bounded by num_internal in InitFromData

Good defense-in-depth. Prevents crafted input from causing oversized vector allocations that could OOM the process during SST open.

Cross-Component Analysis

  • DB Mutex impact: None. Trie code executes during SST build (table builder, no mutex) and SST open (table reader, no mutex). No path through DBImpl::mutex_ is affected.
  • Alternative execution contexts: WritePreparedTxnDB, ReadOnly DB, and SecondaryDB all use the standard table reader path. The trie index integrates through IndexFactory SPI, so these contexts are transparent.
  • Block cache interaction: ApproximateMemoryUsage() fix (H1) directly affects cache charge accounting. The fix correctly includes all heap allocations.

Test Coverage Assessment

The PR should include tests for:

  1. Empty key round-trip (build trie with empty key, verify Seek/Next/Prev)
  2. ValidateStructure() with crafted malformed tries (corrupt node counts, invalid child positions)
  3. ValidateDerivedData() with corrupted rank LUT / select hints
  4. Format 2 round-trip (build with format 2, read back, verify correctness)
  5. Format version cross-compatibility (write format 1, read with format 2 factory; write format 2, read with format 1 factory)
  6. ApproximateAuxMemoryUsage() includes all vectors
  7. TrieScanBounds semantic equivalence with inline CheckBounds

Behavioral Compatibility

No breaking behavioral changes for existing users:

  • Format 1 remains the default
  • Existing SST files continue to be read correctly
  • The use_udi_as_primary_index -> index_mode change affects only db_stress tooling, not the production API
  • Format 2 is opt-in via format_version=2 on the factory

ℹ️ About this response

Generated by Claude Code.
Review methodology: claude_md/ci_review_prompt.md

Limitations:

  • Claude may miss context from files not in the diff
  • Large PRs may be truncated
  • Always apply human judgment to AI suggestions

Commands:

  • /claude-review [context] — Request a code review
  • /claude-query <question> — Ask about the PR or codebase

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant