Repository navigation
Add EXPLAIN query plan command with AI optimization insights - #56
Conversation
- Add explain_query() method to SemanticCache for cache similarity analysis - Add explain_action() methods to QueryOptimizer and QLearningAgent - Add explain_query_plan() function with formatted table output - Add QueryExplainer struct in Rust bridge for Python integration - Add EXPLAIN <query> command parsing in CLI - Include comprehensive test suite for all explain functionality Resolves aviralgarg05#48
Learn moreAll Green is an AI agent that automatically: ✅ Addresses code review comments ✅ Fixes failing CI checks ✅ Resolves merge conflicts |
WalkthroughAdds an EXPLAIN query-plan feature: Python modules provide explain helpers (optimizer + RL agent); Rust bridge exposes a new QueryExplainer and delegates to Python; CLI supports an EXPLAIN command; tests and a Python test harness exercise and validate formatted EXPLAIN output. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant CLI as nexum_cli (main.rs)
participant QE as QueryExplainer (Rust)
participant Bridge as PythonBridge
participant Optimizer as nexum_ai.optimizer
participant Cache as SemanticCache
participant RL as QLearningAgent
User->>CLI: EXPLAIN <query>
CLI->>QE: explain(query)
QE->>Bridge: call Python explain_query_plan(query)
Bridge->>Optimizer: explain_query_plan(query, cache?, optimizer?)
Optimizer->>Cache: explain_query(query)
Cache-->>Optimizer: similarity scores, best_match, would_hit_cache
Optimizer->>RL: explain_action(query_features)
RL-->>Optimizer: state, q_values, best_action, exploration info
Optimizer->>Optimizer: assemble parsing/cache/rl/execution_strategy
Optimizer->>Bridge: format_explain_output(explain_result)
Bridge-->>QE: formatted string
QE-->>CLI: formatted explain plan
CLI-->>User: display plan
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro 📒 Files selected for processing (2)
🧰 Additional context used📓 Path-based instructions (1)**/*.py⚙️ CodeRabbit configuration file
Files:
🧬 Code graph analysis (2)nexum_ai/rl_agent.py (2)
nexum_ai/optimizer.py (2)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
🔇 Additional comments (5)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (6)
nexum_ai/optimizer.pynexum_ai/rl_agent.pynexum_cli/src/main.rsnexum_core/src/bridge/mod.rsnexum_core/src/lib.rstest_explain_feature.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs
⚙️ CodeRabbit configuration file
Focus on Rust best practices, memory safety, error handling, and idiomatic code patterns.
Files:
nexum_core/src/lib.rsnexum_cli/src/main.rsnexum_core/src/bridge/mod.rs
**/*.py
⚙️ CodeRabbit configuration file
Check Python code quality, type hints usage, and PEP 8 compliance.
Files:
test_explain_feature.pynexum_ai/optimizer.pynexum_ai/rl_agent.py
🧬 Code graph analysis (3)
test_explain_feature.py (3)
nexum_ai/optimizer.py (6)
explain_query_plan(387-464)format_explain_output(467-517)SemanticCache(11-293)put(88-96)explain_query(246-279)explain_action(349-372)nexum_ai/rl_agent.py (1)
explain_action(176-212)nexum_core/src/bridge/mod.rs (2)
put(103-110)explain_query(153-161)
nexum_ai/optimizer.py (2)
nexum_core/src/bridge/mod.rs (5)
explain_query(153-161)vectorize(33-50)vectorize(112-114)get(88-101)put(103-110)nexum_ai/rl_agent.py (1)
explain_action(176-212)
nexum_ai/rl_agent.py (1)
nexum_ai/optimizer.py (2)
explain_action(349-372)get(76-86)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Python tests
- GitHub Check: Rust benchmarks
🔇 Additional comments (7)
nexum_core/src/lib.rs (1)
16-17: LGTM!The re-export correctly exposes
QueryExplaineralongside existing bridge types, enabling the CLI and other downstream crates to use the new explainability feature.nexum_cli/src/main.rs (1)
23-32: LGTM!The initialization pattern for
QueryExplainermirrors the existingNLTranslatorpattern with proper error handling and graceful degradation when Python integration is unavailable.nexum_core/src/bridge/mod.rs (2)
152-161: LGTM!The
explain_querymethod follows the established pattern of otherSemanticCachemethods, properly wrapping the Python call with error handling.
199-235: LGTM!The
QueryExplainerimplementation correctly follows the established pattern used byNLTranslator, maintaining thePythonBridgefor initialization lifetime. Bothexplainandexplain_rawmethods properly handle Python interop with appropriate error mapping.nexum_ai/optimizer.py (3)
246-279: LGTM! Method correctly analyzes cache without execution.The implementation properly vectorizes the query, computes similarities against all cache entries, and returns a structured analysis. The read-only nature is appropriate for EXPLAIN functionality.
592-605: LGTM! Integration test demonstrates EXPLAIN functionality.The test code appropriately exercises the EXPLAIN flow with sample cache data and prints a formatted execution plan. The test query is semantically similar to cached entries, which should demonstrate cache similarity scoring effectively.
387-464: The review comment is inaccurate.QueryExplainer.explain()is correctly implemented and calls bothexplain_query_plan()andformat_explain_output()in sequence before extracting the result. The method does not call.str()on a dict representation.The reviewer appears to have confused
SemanticCache.explain_query()(a separate Rust bridge method at lines 149-158) withQueryExplainer.explain(). WhileSemanticCache.explain_query()does convert the Python dict result to a string representation via.str(), this is a different concern and does not affect theexplain_query_plan()function or theQueryExplainer.explain()method shown in lines 387-464.Likely an incorrect or invalid review comment.
| # 3. RL Agent Analysis | ||
| if optimizer is None: | ||
| optimizer = QueryOptimizer() | ||
|
|
||
| available_actions = ['use_cache', 'bypass_cache', 'full_scan', 'index_scan'] | ||
| result['rl_agent'] = optimizer.explain_action(query, available_actions) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, check what's in the rl_agent.py file to see if QLearningAgent exists
echo "=== Checking rl_agent.py content ==="
if [ -f "nexum_ai/rl_agent.py" ]; then
head -100 nexum_ai/rl_agent.py
else
echo "rl_agent.py not found"
fi
# Check what QueryOptimizer is and where it's defined
echo -e "\n=== Searching for QueryOptimizer definition ==="
rg -nP 'class QueryOptimizer' --type=py
# Check what QLearningAgent is in the codebase
echo -e "\n=== Searching for QLearningAgent definition ==="
rg -nP 'class QLearningAgent' --type=py
# Check imports in optimizer.py
echo -e "\n=== Checking imports in optimizer.py ==="
head -30 nexum_ai/optimizer.pyRepository: aviralgarg05/NexumDB
Length of output: 4445
🏁 Script executed:
#!/bin/bash
# Check if the files exist and their content
echo "=== Checking if nexum_ai/optimizer.py exists ==="
if [ -f "nexum_ai/optimizer.py" ]; then
echo "File found"
echo "=== Lines 433-438 from optimizer.py ==="
sed -n '433,438p' nexum_ai/optimizer.py
echo -e "\n=== First 40 lines (imports) from optimizer.py ==="
head -40 nexum_ai/optimizer.py
else
echo "File not found. Checking directory structure..."
find . -name "optimizer.py" -o -name "rl_agent.py" 2>/dev/null | head -20
fi
# Check rl_agent.py
echo -e "\n=== Checking if nexum_ai/rl_agent.py exists ==="
if [ -f "nexum_ai/rl_agent.py" ]; then
echo "File found"
echo "=== Searching for class definitions ==="
grep -n "^class " nexum_ai/rl_agent.py
else
echo "File not found"
fi
# Search for QueryOptimizer and QLearningAgent definitions
echo -e "\n=== Searching for QueryOptimizer class ==="
rg -n 'class QueryOptimizer' --type=py
echo -e "\n=== Searching for QLearningAgent class ==="
rg -n 'class QLearningAgent' --type=pyRepository: aviralgarg05/NexumDB
Length of output: 2163
🏁 Script executed:
#!/bin/bash
echo "=== QueryOptimizer class and explain_action method ==="
sed -n '296,400p' nexum_ai/optimizer.py
echo -e "\n=== QLearningAgent methods ==="
rg -n 'def ' nexum_ai/rl_agent.py -A 3 | head -100
echo -e "\n=== Check if QLearningAgent is imported in optimizer.py ==="
rg 'QLearningAgent|from.*rl_agent' nexum_ai/optimizer.py
echo -e "\n=== Check full explain_query_plan function signature ==="
rg -n 'def explain_query_plan' nexum_ai/optimizer.py -A 20Repository: aviralgarg05/NexumDB
Length of output: 6832
Consider using QLearningAgent instead of QueryOptimizer for richer state analysis.
The codebase includes QLearningAgent in nexum_ai/rl_agent.py with a more sophisticated explain_action() method that accepts query_length, cache_hit, and complexity parameters, providing:
- Multi-dimensional state representation instead of simple query length bucketing
- Agent statistics (q_table size, episode count)
- Detailed state breakdown in explanations
Currently the code instantiates QueryOptimizer, which works but has a simpler interface. If consistency with the RL implementation is desired, QLearningAgent would provide better analysis.
🤖 Prompt for AI Agents
In nexum_ai/optimizer.py around lines 433 to 438, the code currently
instantiates QueryOptimizer and calls explain_action(query, available_actions);
replace it with QLearningAgent to get richer state analysis: import
QLearningAgent from nexum_ai.rl_agent, instantiate QLearningAgent() (or get a
singleton) and call its explain_action(query_length= len(query), cache_hit=
<compute boolean/cache lookup>, complexity= <compute complexity metric or
heuristic>, available_actions= available_actions); ensure you compute or
approximate cache_hit and complexity from existing context (fallback to
False/low complexity if unknown), and keep a fallback to QueryOptimizer if
QLearningAgent import/initialization fails so behavior remains stable.
| // Handle EXPLAIN command | ||
| if input.to_uppercase().starts_with("EXPLAIN ") { | ||
| let query_to_explain = input[8..].trim(); | ||
|
|
||
| if let Some(ref explainer) = query_explainer { | ||
| println!(); | ||
| match explainer.explain(query_to_explain) { | ||
| Ok(plan) => { | ||
| println!("{}", plan); | ||
| } | ||
| Err(e) => { | ||
| eprintln!("Explain error: {}", e); | ||
| } | ||
| } | ||
| } else { | ||
| eprintln!("Query explainer not available"); | ||
| } | ||
| continue; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider validating empty query after EXPLAIN.
If a user types EXPLAIN without a query, query_to_explain will be an empty string. While the Python side may handle this gracefully, providing a user-friendly error message would improve the experience.
🔎 Proposed enhancement
// Handle EXPLAIN command
if input.to_uppercase().starts_with("EXPLAIN ") {
let query_to_explain = input[8..].trim();
+ if query_to_explain.is_empty() {
+ eprintln!("Usage: EXPLAIN <query>");
+ continue;
+ }
+
if let Some(ref explainer) = query_explainer {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Handle EXPLAIN command | |
| if input.to_uppercase().starts_with("EXPLAIN ") { | |
| let query_to_explain = input[8..].trim(); | |
| if let Some(ref explainer) = query_explainer { | |
| println!(); | |
| match explainer.explain(query_to_explain) { | |
| Ok(plan) => { | |
| println!("{}", plan); | |
| } | |
| Err(e) => { | |
| eprintln!("Explain error: {}", e); | |
| } | |
| } | |
| } else { | |
| eprintln!("Query explainer not available"); | |
| } | |
| continue; | |
| } | |
| // Handle EXPLAIN command | |
| if input.to_uppercase().starts_with("EXPLAIN ") { | |
| let query_to_explain = input[8..].trim(); | |
| if query_to_explain.is_empty() { | |
| eprintln!("Usage: EXPLAIN <query>"); | |
| continue; | |
| } | |
| if let Some(ref explainer) = query_explainer { | |
| println!(); | |
| match explainer.explain(query_to_explain) { | |
| Ok(plan) => { | |
| println!("{}", plan); | |
| } | |
| Err(e) => { | |
| eprintln!("Explain error: {}", e); | |
| } | |
| } | |
| } else { | |
| eprintln!("Query explainer not available"); | |
| } | |
| continue; | |
| } |
🤖 Prompt for AI Agents
In nexum_cli/src/main.rs around lines 94 to 112, the EXPLAIN handling does not
validate that a query follows the "EXPLAIN " prefix, so an input like "EXPLAIN "
yields an empty query string; detect when query_to_explain.is_empty() after
trimming and print a clear user-facing error (e.g., "Please provide a query to
explain") or prompt the user instead of calling the explainer,
returning/continuing early; keep existing explainer logic unchanged for
non-empty queries and ensure the empty-case branches to continue the loop
without invoking explainer.explain.
| return; | ||
| } | ||
|
|
||
| let explainer = super::QueryExplainer::new().unwrap(); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Minor: Unnecessary super:: prefix.
QueryExplainer is already in scope via use super::* at line 239, so super::QueryExplainer::new() can be simplified to QueryExplainer::new() for consistency with other tests.
🔎 Proposed fix
- let explainer = super::QueryExplainer::new().unwrap();
+ let explainer = QueryExplainer::new().unwrap();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let explainer = super::QueryExplainer::new().unwrap(); | |
| let explainer = QueryExplainer::new().unwrap(); |
🤖 Prompt for AI Agents
In nexum_core/src/bridge/mod.rs around line 334, the code uses an unnecessary
super:: prefix when constructing QueryExplainer; change
super::QueryExplainer::new().unwrap() to QueryExplainer::new().unwrap() to match
the existing use super::* import and keep consistency with other tests.
| import sys | ||
| import os | ||
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'nexum_ai')) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider using a more robust import approach.
Manipulating sys.path directly is fragile. Consider using PYTHONPATH environment variable, installing the package in development mode (pip install -e .), or using a proper test framework with path fixtures.
🤖 Prompt for AI Agents
In test_explain_feature.py around lines 6 to 8, importing by mutating sys.path
is fragile; replace this with a robust approach: remove the sys.path
manipulation and instead ensure the package is available to tests by one of the
recommended methods (add instructions to set PYTHONPATH in CI/test runner,
install the package in editable mode with pip install -e . in setup, or use your
test framework's tmp_path/path fixtures or conftest to add the project root to
sys.path in a controlled way). Update project/test docs or CI setup to reflect
the chosen method so tests run reliably without inline sys.path changes.
| if __name__ == "__main__": | ||
| try: | ||
| test_explain_feature() | ||
| print("\n🎉 EXPLAIN query plan feature implementation complete!") | ||
| print("Usage: EXPLAIN <query> in the NexumDB CLI") | ||
| except Exception as e: | ||
| print(f"❌ Test failed: {e}") | ||
| sys.exit(1) No newline at end of file |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Improve error reporting and consider using a test framework.
The exception handler discards the traceback, making debugging harder. Consider using pytest or unittest for better test organization and assertion messages, and include traceback in error output.
🔎 Proposed improvement
if __name__ == "__main__":
try:
test_explain_feature()
print("\n🎉 EXPLAIN query plan feature implementation complete!")
print("Usage: EXPLAIN <query> in the NexumDB CLI")
except Exception as e:
- print(f"❌ Test failed: {e}")
+ import traceback
+ print(f"❌ Test failed: {e}")
+ traceback.print_exc()
sys.exit(1)🤖 Prompt for AI Agents
In test_explain_feature.py around lines 77-84, the top-level exception handler
swallows the traceback which hampers debugging; replace the bare print of the
exception with printing the full traceback (e.g., use traceback.print_exc() or
logging.exception) so stack details are emitted, and as a follow-up convert the
ad-hoc runner into a proper test using pytest or unittest (move
test_explain_feature into a test_ function with asserts and run via pytest) for
better assertions and test reporting.
- Remove random state mutations from explain methods (deterministic now) - Fix type hint: Dict[str, any] -> Dict[str, Any] - Add smart query truncation (only when needed) - Improve cached query display formatting - Add missing Any import in rl_agent.py All explain methods now have no side effects and provide consistent output.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
nexum_ai/optimizer.py (1)
486-493: Fixed: Query truncation is now conditional.The past review issue flagging unconditional truncation has been resolved. Lines 488-491 now correctly truncate only when
len(query) > 60, avoiding unnecessary ellipses for short queries.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
nexum_ai/optimizer.pynexum_ai/rl_agent.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
⚙️ CodeRabbit configuration file
Check Python code quality, type hints usage, and PEP 8 compliance.
Files:
nexum_ai/rl_agent.pynexum_ai/optimizer.py
🧬 Code graph analysis (2)
nexum_ai/rl_agent.py (1)
nexum_ai/optimizer.py (2)
explain_action(362-384)get(76-86)
nexum_ai/optimizer.py (2)
nexum_core/src/bridge/mod.rs (5)
explain_query(153-161)vectorize(33-50)vectorize(112-114)get(88-101)put(103-110)nexum_ai/rl_agent.py (1)
explain_action(176-210)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Python tests
- GitHub Check: Rust benchmarks
🔇 Additional comments (5)
nexum_ai/rl_agent.py (1)
7-7: LGTM: Import correctly addsAnyfor type annotations.The addition of
Anyto the typing imports is necessary for the newexplain_actionmethod's return type annotation.nexum_ai/optimizer.py (4)
246-292: LGTM! Well-structured explain method with proper truncation.The
explain_querymethod correctly analyzes cache similarity without executing the query. The conditional truncation logic (lines 261-264, 279-282) ensures display strings are readable, and the sorted top matches provide useful debugging information.
362-384: LGTM! Deterministic explain method with correct type hints.The method correctly analyzes the RL agent's action selection without side effects. Type hints are properly capitalized (
Dict[str, Any]), and the implementation is deterministic and read-only.Note: This implementation uses simple query-length bucketing for state representation. The
QLearningAgent.explain_action()inrl_agent.pyprovides richer multi-dimensional state analysis (query_length, cache_hit, complexity) and agent statistics. The current implementation is correct and sufficient for basic EXPLAIN functionality.
414-438: Basic keyword-based parsing is acceptable for EXPLAIN MVP.The parsing logic uses simple string matching to detect query type and features. This works for standard SQL queries but won't handle:
- Nested subqueries
- SQL comments (
-- commentor/* comment */)- Keywords within string literals (
'SELECT')- Case-sensitive identifiers
For a diagnostic EXPLAIN feature, this level of parsing is reasonable and avoids the complexity of a full SQL parser.
616-629: LGTM! Basic manual test demonstrates EXPLAIN functionality.The test code adds sample cache entries and exercises the EXPLAIN command with a similar query. This provides a quick smoke test for the feature. For automated testing, see
test_explain_feature.pywhich includes proper assertions.
| if cache is None: | ||
| cache = SemanticCache() | ||
| result['cache_analysis'] = cache.explain_query(query) | ||
|
|
||
| # 3. RL Agent Analysis | ||
| if optimizer is None: | ||
| optimizer = QueryOptimizer() | ||
|
|
||
| available_actions = ['use_cache', 'bypass_cache', 'full_scan', 'index_scan'] | ||
| result['rl_agent'] = optimizer.explain_action(query, available_actions) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider passing initialized cache/optimizer to preserve state.
When cache or optimizer are None, new instances are created at lines 442 and 447. This means:
- A fresh
SemanticCache()has no cached entries, soexplain_query()always returns empty matches. - A fresh
QueryOptimizer()has no learned Q-values, so all actions default to 0.0.
For meaningful EXPLAIN output, callers should pass pre-populated instances. Consider documenting this requirement in the docstring or logging a warning when defaults are used.
🔎 Optional documentation enhancement
def explain_query_plan(query: str, cache: Optional[SemanticCache] = None,
optimizer: Optional[QueryOptimizer] = None) -> Dict[str, Any]:
"""
Generate a complete EXPLAIN plan for a query
Shows parsing, cache analysis, and RL agent predictions
+
+ Args:
+ query: SQL query to analyze
+ cache: Pre-populated SemanticCache instance (defaults to empty cache)
+ optimizer: Trained QueryOptimizer instance (defaults to untrained agent)
+
+ Note: Pass initialized cache/optimizer for meaningful results.
"""Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In nexum_ai/optimizer.py around lines 441 to 450, when cache or optimizer are
None the code instantiates fresh SemanticCache() and QueryOptimizer(), which
yields empty explain results and zeroed Q-values; update the function to (a)
document in the docstring that callers should pass pre-populated cache and
optimizer to get meaningful EXPLAIN outputs, and (b) emit a warning log when the
function must create new default instances so callers are notified that stateful
information is missing; implement both: add the docstring note near the function
signature and add a logger.warning call right before creating the default
SemanticCache() and QueryOptimizer().
|
@aviralgarg05 I have fix the issue |

This PR implements the EXPLAIN command requested in issue #48, providing detailed query execution plans that show AI optimization decisions and cache predictions without executing queries.
🚀 Features Added
📋 Usage
-- Basic usage
EXPLAIN SELECT * FROM users WHERE age > 25
-- Works with complex queries
EXPLAIN SELECT u.name, COUNT(o.id)
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.age > 25
GROUP BY u.name
🔧 Implementation Details
Python Components:
Rust Integration:
✅ Testing
Comprehensive test suite in test_explain_feature.py
Tests all components: parsing, cache analysis, RL agent, formatting
Validates output format and data structure integrity
All tests pass with fallback vectorization
🎯 Benefits
Debug AI Decisions: Understand why the RL agent chooses specific actions
Cache Optimization: See which queries would hit cache before execution
Query Analysis: Get insights into query complexity and features
Performance Prediction: Estimate execution time and strategy
Safe Analysis: No side effects - queries are never actually executed
📝 Files Changed
nexum_ai/optimizer.py - Core explain functionality
nexum_ai/rl_agent.py - RL agent explain methods
nexum_core/src/bridge/mod.rs - Rust-Python bridge
nexum_core/src/lib.rs - Export QueryExplainer
nexum_cli/src/main.rs - CLI command handling
test_explain_feature.py - Test suite (new)
Closes #48
Summary by CodeRabbit
New Features
API
Tests
✏️ Tip: You can customize this high-level summary in your review settings.