Add migration for rescaling time frames after block time change - #1455
Conversation
WalkthroughRewires Executive migrations typing; adds a public multi-step Changes
Sequence DiagramsequenceDiagram
actor Scheduler as Block/Runtime
participant Migration as TimeFrameRescaleMigration
participant Cursor as MigrationCursor (storage)
participant Storage as RuntimeStorage
Scheduler->>Migration: invoke on_runtime_upgrade / run_step(weight)
activate Migration
Migration->>Cursor: load or init cursor state
Cursor-->>Migration: {current_time_frame, offset, max_source_time_frame}
Migration->>Storage: read source time-frame buckets & MarketIds
Storage-->>Migration: return batch data
loop while weight remains and work left
Migration->>Storage: move/insert MarketIds to target frames (bounded batch)
Migration->>Storage: remove/mark processed source keys
Migration->>Cursor: advance offset / mark frame complete
end
alt incomplete
Migration->>Storage: persist Cursor state
Migration-->>Scheduler: return InProgress (consumed weight)
else complete
Migration->>Storage: set STORAGE_VERSION = 9
Migration-->>Scheduler: return Completed
end
deactivate Migration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
zrml/prediction-markets/src/migrations.rs (3)
32-38: Tie TIME_FRAME_SCALE_FACTOR explicitly to this specific block-time change
TIME_FRAME_SCALE_FACTORis computed via integer division ofPREVIOUS_MILLISECS_PER_BLOCKbyMILLISECS_PER_BLOCKand then used to rescaleLastTimeFrame. That works for the current “12s → 6s” halving, but:
- If
MILLISECS_PER_BLOCKchanges again, this constant will silently become wrong, and the migration would mis‑scale or collapse to 0.- The migration is also implicitly assuming an integer scaling factor.
I’d strongly suggest either:
- Making this migration clearly one‑off (documenting and guarding it with a check that the current
MILLISECS_PER_BLOCKyields the expected ratio), or- Adding a debug/assert check that
TIME_FRAME_SCALE_FACTORequals the intended value (e.g., 2) before doing any rescaling, so it fails loudly if reused in a different context.Also applies to: 73-83
85-100:insert_with_shiftcan be expensive and is invisible to the WeightMeter
insert_with_shiftperforms an unboundedloopover successive time frames, doing storageget/insertcalls until it finds a non‑full bucket. From the perspective of the migration weight accounting:
- The outer
steploop only budgetsdb_weight.reads_writes(3, 3)per item before callinginsert_with_shift, but this helper may perform multiple reads/writes if it has to walk through several full buckets.- Those extra DB ops are not reflected in the
WeightMeter, so the migration may under‑account weight and do more work per block than intended (not a consensus bug, but it can stress block execution during migration).If you expect many time frames near capacity, consider:
- Either charging a more conservative weight per item (e.g., assume a small bounded number of shifts), or
- Pushing the bucket shifting into the main
steploop where you can account for each additionalget/insertviaweight_or_insufficient.
226-269: Tests cover only the simplest happy path; consider adding partial-step and multi-bucket casesThe current test (
migration_rescales_time_frames) nicely validates:
- Storage version bump from 8 → 9,
- Rescaling of
LastTimeFrame,- Moving a single time frame key with one market ID.
To better lock in migration correctness, it would be valuable to add tests that:
- Run with a tight
WeightMeterlimit to force at least oneInsufficientWeightpath, ensuring cursor persistence (current_time_frame,offset) works correctly over multiplestepcalls.- Cover multiple time frames and multiple markets per frame (including where buckets are at or near
CacheSize), validating that all buckets are eventually migrated and removed.- Include at least one scenario where some keys should not be migrated (e.g., explicitly simulating “post‑upgrade” bucket(s)) if that’s part of the intended behavior.
These would make it much easier to refactor the stepping logic safely in the future.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
runtime/common/src/lib.rs(2 hunks)zrml/prediction-markets/src/migrations.rs(1 hunks)
⏰ 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). (1)
- GitHub Check: Summary
🔇 Additional comments (1)
runtime/common/src/lib.rs (1)
135-142: Executive/SingleBlockMigrations wiring looks correct; please confirm all intended migrations are coveredSwitching
Executive’s last generic to()and routing single‑block migrations viaframe_system::Config::SingleBlockMigrations = SingleBlockMigrationsmatches the newer Substrate migration pattern withpallet_migrationshandling multi‑block upgrades. Just make sure:
- Every single‑block migration you still expect to run on version changes is included in the
SingleBlockMigrationsalias.- The new time‑frame rescale migration is wired into your
MultiBlockMigrationstuple so it actually executes viapallet_migrations.Also applies to: 538-571
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
integration-tests/package.json (1)
20-41: Consider documenting the purpose of the pnpm overrides reorganization.The pnpm overrides block has been reorganized to explicitly pin Polkadot packages alongside other dependencies. While the current structure is clear, adding a brief comment or ensuring related Rust-side changes are well-documented would help future maintainers understand why these specific versions are required in tandem with the migration refactoring.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
integration-tests/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (2)
integration-tests/package.json(2 hunks)zrml/prediction-markets/src/migrations.rs(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- zrml/prediction-markets/src/migrations.rs
⏰ 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). (1)
- GitHub Check: Summary
🔇 Additional comments (1)
integration-tests/package.json (1)
34-40: Based on my verification, I now have sufficient information to rewrite the review comment. Let me provide the updated version:
Polkadot package versions are compatible and appropriate for supporting the block time protocol change.
The pnpm overrides pin @polkadot/api@16.5.2 and @polkadot/util@13.5.2, which are documented as compatible by polkadot-js. This version split (core packages at 16.5.2; utility packages at 13.5.2) aligns with the TimeFrameRescaleMigration in
zrml/prediction-markets/src/migrations.rs, which rescales time frames following the AsyncBacking upgrade that halved block time from 12s to 6s. The v16.5.2 upgrade includes metadata v16 changes required to support this protocol change.The integration tests (runtime upgrade and smoke tests) verify basic functionality after the upgrade, though they do not explicitly test the migration behavior itself. Consider adding a focused integration test that validates the TimeFrameRescaleMigration completes successfully and correctly rescales storage values if migration-specific verification is needed.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
zrml/prediction-markets/src/migrations.rs (1)
104-155: Concurrent writes can still cause new markets to be rescaled as “legacy” (double‑scaling risk)The migration now:
- Snapshots
max_source_time_frameonce viaiter_keys().max()(lines 138–140),- Uses
next_start_time_frame(max)which picks any key<= maxfromiter_keys()each time (lines 104–108, 153–155),- Processes and removes each such key while the migration runs across multiple blocks.
MarketIdsPerCloseTimeFrameis also written by normal pallet logic (insert_auto_close/manually_close_market) without any guard on the pallet storage version. While the stepped migration is in progress, new markets created by extrinsics can insert entries whose time frame key is<= max_source_time_frame(e.g., if existing markets already reach very far into the future). Those new buckets will then be picked up bynext_start_time_frame(max)and rescaled as if they were legacy entries, effectively pushing their close time frame further into the future than intended.This is the same underlying concurrency hazard that was raised in an earlier review: the current changes (iterating over keys and snapshotting
max_source_time_frame) address iterator‑ordering issues but do not fully isolate pre‑migration “source space” from new inserts.Possible hardening options:
- Gate writes to
MarketIdsPerCloseTimeFrameonStorageVersion::<Pallet<T>>() >= TARGET_STORAGE_VERSION(or the inverse) inside the pallet’s insertion paths, or- Use a stricter notion of the legacy range (e.g., a stored “max legacy frame” written once at upgrade time) and ensure new markets always land strictly above that boundary, or
- Explicitly document and enforce at the runtime level that no markets are created while this migration is active.
#!/bin/bash # Inspect all write sites for MarketIdsPerCloseTimeFrame to see if any are gated rg -n "MarketIdsPerCloseTimeFrame::<" zrml/prediction-markets/src/lib.rs -n -C3
🧹 Nitpick comments (2)
zrml/prediction-markets/src/migrations.rs (2)
73-102: Weight‑bounded insert_with_shift trades safety for resilience; consider whether silent drops are acceptable
insert_with_shiftnow:
- Tries at most 3 adjacent time frames to fit a
market_idinto a bounded vec,- Charges read/write weights per attempt,
- Logs an error and returns
Ok(())if all three buckets are full.Functionally this means that in the (very unlikely) case that three consecutive
TimeFramebuckets are saturated, that market id is silently omitted from any close‑time cache; the migration will still “succeed” and bump the storage version.If you want stronger guarantees, consider returning
SteppedMigrationError::Failedinstead of swallowing the failure so the network clearly sees the migration as incomplete, or at least emit a prominent comment documenting that under extreme contention some markets may lose their auto‑close entry during this migration.
222-263: Tests nicely cover happy‑path rescaling but miss weight‑limited steppingThe two tests validate:
- LastTimeFrame is rescaled once via Pallet::on_runtime_upgrade,
- All buckets and IDs are moved from old to new time frames at the expected scale,
- Storage version is bumped from 8 to 9 only after the stepped migration completes.
Given the non‑trivial weight‑metering logic, it would be worth adding at least one unit test that exercises the “insufficient weight” branch (tiny weight limit, enough IDs to require multiple steps) to catch regressions in cursor/offset handling and partial writes.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
zrml/prediction-markets/src/lib.rs(4 hunks)zrml/prediction-markets/src/migrations.rs(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
zrml/prediction-markets/src/migrations.rs (1)
zrml/prediction-markets/src/lib.rs (5)
market_id(2086-2086)Self(1945-1949)StorageVersion(1879-1879)StorageVersion(1897-1897)on_runtime_upgrade(1876-1893)
zrml/prediction-markets/src/lib.rs (1)
zrml/prediction-markets/src/migrations.rs (2)
StorageVersion(236-236)StorageVersion(268-268)
⏰ 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). (1)
- GitHub Check: Summary
🔇 Additional comments (3)
zrml/prediction-markets/src/lib.rs (3)
37-71: Imports and new try‑runtime dependencies look consistent with migration usagePulling
migrationsinto scope alongsideweightsand gatingDecode/Encode/TryRuntimeErrorundertry-runtimeis idiomatic and matches howpost_upgradeis written. No issues here.
89-91: Storage version bump aligns with new migration targetUpdating
STORAGE_VERSIONto9matchesTARGET_STORAGE_VERSIONinmigrations::mbmand the tests, so the pallet’s declared version is consistent with the stepped migration.
1875-1918: Migration wiring verified across all runtimes; no issues foundThe verification confirms the original review comment is accurate. Both active runtimes (zeitgeist and battery-station) have
TimeFrameRescaleMigrationproperly wired into their migration pipelines via theLegacyMigrationstuple, which is assigned to theMultiBlockMigrationstype used bypallet_migrations. Theon_runtime_upgradeguard and try-runtime validation hooks are correctly implemented, and the version bump is appropriately delegated toTimeFrameRescaleMigration::step().
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
zrml/prediction-markets/src/migrations.rs (1)
145-229: Stepping logic now avoids order‑dependence and cursor bugs; re‑check invariants for new markets during migrationThe new snapshotting (
max_source_time_frame = iter_keys().max()) plusnext_start_time_frame(max)no longer relies onStorageMapiteration order, and the updated continuation branch (resettingoffsetto 0 when persisting the tail) fixes the earlier bug where some IDs could be skipped when resuming mid‑bucket. Overall the multi‑block stepping logic looks sound.One thing to double‑check at the runtime level: do we guarantee that any new timestamp‑based markets created while this migration is running will always land in time frames strictly greater than
max_source_time_frame? If not, those new entries (with keys<= max_source_time_frame) will also be treated as “source” and rescaled, which may or may not be intended.
🧹 Nitpick comments (2)
zrml/prediction-markets/src/migrations.rs (2)
73-124:insert_with_shiftsilently drops auto‑close entry when all probed buckets are fullBounding the shift loop to three adjacent time frames is reasonable, but if all three buckets are full you only log an error and return
Ok(()), effectively losing the market’s auto‑close entry for this migration run.If you consider that too lossy, you might instead return
SteppedMigrationError::Failed(or at least surface a stronger signal via metrics/docs) so operators can detect and react to the data-loss risk rather than relying only on logs.
251-335: Tests cover basic paths; consider a bounded‑weight, multi‑step scenarioThe two tests nicely exercise LastTimeFrame rescaling plus single‑ and multi‑bucket cache migration, but they always run with effectively unlimited weight, so the continuation path (cursor/offset when the meter runs out mid‑bucket) is never hit.
A small additional test that runs with a tight
WeightMeterlimit and repeatedly callsTimeFrameRescaleMigration::<Runtime>::stepuntil it returnsNonewould help guard against regressions in the multi‑step continuation logic.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
zrml/prediction-markets/src/lib.rs(4 hunks)zrml/prediction-markets/src/migrations.rs(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
zrml/prediction-markets/src/migrations.rs (1)
zrml/prediction-markets/src/lib.rs (4)
Self(1951-1955)StorageVersion(1879-1879)StorageVersion(1903-1903)on_runtime_upgrade(1876-1899)
zrml/prediction-markets/src/lib.rs (1)
zrml/prediction-markets/src/migrations.rs (2)
StorageVersion(254-254)StorageVersion(286-286)
⏰ 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). (14)
- GitHub Check: Coverage
- GitHub Check: Test standalone build
- GitHub Check: Checks (standalone)
- GitHub Check: Format
- GitHub Check: Coverage
- GitHub Check: Test parachain build
- GitHub Check: Fuzz
- GitHub Check: Checks (standalone)
- GitHub Check: Checks (parachain)
- GitHub Check: Test standalone build
- GitHub Check: Checks (clippy)
- GitHub Check: Quick check benchmarks
- GitHub Check: Coverage
- GitHub Check: Summary
🔇 Additional comments (2)
zrml/prediction-markets/src/lib.rs (2)
37-38: Imports and storage version bump are consistent with the new migrationExposing
migrationsat the crate level and switching touse crate::{migrations, weights::*};plus bumpingSTORAGE_VERSIONtoStorageVersion::new(9)line up withTARGET_STORAGE_VERSION: u16 = 9inmbmand ensure the pallet’s metadata matches the migration you wired into the runtimes.Also applies to: 56-61, 90-91
1875-1899:on_runtime_upgradeand try‑runtime hooks correctly isolateLastTimeFramerescalingThe new
on_runtime_upgradegates on the on‑chain storage version, rescalesLastTimeFrameonce usingmigrations::mbm::TIME_FRAME_SCALE_FACTOR, and leaves the actual bump to version 9 to the multi‑block migration (which callsStorageVersion::new(9).put), matching the tests inmigrations.rs. Thepre_upgrade/post_upgradepair undertry-runtimecleanly captures whether the upgrade ran and asserts the expected rescaledLastTimeFrame.From a pallet perspective this looks coherent; just ensure the Executive’s multi‑block migration wiring calls
TimeFrameRescaleMigrationin addition to this hook so version 9 is only considered “done” once the cache migration has completed.Also applies to: 1901-1924
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1455 +/- ##
==========================================
- Coverage 93.32% 93.28% -0.04%
==========================================
Files 181 181
Lines 34769 35049 +280
==========================================
+ Hits 32448 32696 +248
- Misses 2321 2353 +32
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…ation error handling
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
zrml/prediction-markets/src/migrations.rs (2)
73-117: Weight accounting ininsert_with_shiftis conservative and may double‑chargeThe combination of:
- outer
meter.try_consume(db_weight.reads_writes(3, 3))instep, and- inner
weight_or_insufficient(meter, db_weight.reads(1))/...writes(1)insideinsert_with_shiftmeans each migrated ID can be charged more weight than it actually consumes. That’s safe for correctness (no under‑charging) but can cause earlier‑than‑necessary
InsufficientWeighterrors and reduce progress per block.If you want more predictable stepping, consider either:
- dropping the outer
reads_writes(3, 3)reservation and relying solely on the innerweight_or_insufficientcalls, or- making
insert_with_shifta pure storage op (no extraweight_or_insufficient) and letting the caller account all reads/writes.
254-395: Tests cover core flows; consider adding a targeted multi‑block/weight‑exhaustion testThe new tests exercise:
- single‑bucket happy path (including StorageVersion and LastTimeFrame interactions),
- the “target buckets full” hard‑failure case, and
- multi‑bucket rescaling and
on_runtime_upgradeidempotence.What’s still untested is the mid‑bucket weight‑exhaustion path that persists a tail slice and resumes via the cursor. Given the previous bug in this area, it’s worth adding a small test that:
- Seeds a bucket with several IDs.
- Uses a very tight
WeightMeterlimit to force early return in the inner loop.- Asserts that repeated
stepcalls eventually migrate all IDs with none lost and that the source key is fully removed.This would lock in the new cursor semantics and guard future refactors.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
zrml/prediction-markets/src/lib.rs(5 hunks)zrml/prediction-markets/src/migrations.rs(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
zrml/prediction-markets/src/migrations.rs (1)
zrml/prediction-markets/src/lib.rs (4)
Self(1965-1969)StorageVersion(1880-1880)StorageVersion(1909-1909)on_runtime_upgrade(1876-1905)
zrml/prediction-markets/src/lib.rs (1)
zrml/prediction-markets/src/migrations.rs (4)
StorageVersion(257-257)StorageVersion(291-291)StorageVersion(327-327)StorageVersion(344-344)
⏰ 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). (12)
- GitHub Check: Checks (clippy)
- GitHub Check: Format
- GitHub Check: Coverage
- GitHub Check: Test parachain build
- GitHub Check: Checks (parachain)
- GitHub Check: Format
- GitHub Check: Fuzz
- GitHub Check: Checks (clippy)
- GitHub Check: Quick check benchmarks
- GitHub Check: Test standalone build
- GitHub Check: Checks (standalone)
- GitHub Check: Summary
🔇 Additional comments (7)
zrml/prediction-markets/src/migrations.rs (2)
153-183: Iteration overMarketIdsPerCloseTimeFramenow correctly avoids ordering assumptionsThe new snapshot logic:
max_source_time_frame = MarketIdsPerCloseTimeFrame::<T>::iter_keys().max();next_start_time_frame(max)usingiter_keys().find(|tf| *tf <= max)drops the previous reliance on
iter().last()and hash‑order monotonicity. Combined with key removal after processing, this guarantees every original bucket withtf <= max_source_time_frameis visited exactly once, independent of underlying storage ordering.This is a solid fix to the earlier iterator‑ordering pitfall, and the code reads clearly with the updated comments.
192-212: Cursor continuation after weight exhaustion now preserves all remaining IDsIn the weight‑exhaustion branch you now:
- write back
remaining = ids[idx..].to_vec()to storage,- either remove the key or re‑insert the truncated bucket, and
- reset
state.current_time_frameandstate.offsetso the next step starts at index 0 of the persisted slice.This removes the previous off‑by‑offset behavior where some markets at the front of the truncated slice could be silently skipped on resume. The continuation invariants for multi‑block runs look correct with this change.
zrml/prediction-markets/src/lib.rs (5)
37-38: Importingmigrationsat the pallet level aligns the runtime upgrade pathSwitching to
use crate::{migrations, weights::*};cleanly exposesmigrations::mbm::TIME_FRAME_SCALE_FACTORto the pallet and keeps all migration‑specific constants encapsulated undercrate::migrations. This keeps the pallet’s upgrade logic decoupled from the concrete migration implementation while still allowing the tests and hooks to reference the scale factor.
90-105: STORAGE_VERSION bump to 9 is consistent with the new migration target
STORAGE_VERSION: StorageVersion = StorageVersion::new(9);now matchesTARGET_STORAGE_VERSION: u16 = 9inmigrations.rs, and the pallet is annotated with#[pallet::storage_version(STORAGE_VERSION)]. Together with the final step inTimeFrameRescaleMigrationthat writes StorageVersion 9, this keeps the versioning story coherent between the pallet and the stepped migration.
1874-1905:on_runtime_upgrade+LastTimeFrameRescaledcleanly separate LastTimeFrame scaling from the stepped migrationThe new hook:
- Guards on
StorageVersion::get::<Self>() < STORAGE_VERSIONso it only runs for the 8→9 upgrade.- Rescales
LastTimeFrameonce usingmigrations::mbm::TIME_FRAME_SCALE_FACTOR, with proper db‑weight accounting.- Sets
LastTimeFrameRescaled::<T>as an idempotence flag.With
TimeFrameRescaleMigrationlater killingLastTimeFrameRescaledand bumping the pallet StorageVersion to 9, the responsibilities are nicely split:
- pallet
on_runtime_upgrade: rescale the singleLastTimeFramevalue and mark that it happened,- stepped migration: rescale the per‑time‑frame caches and finalize the upgrade.
The logic and weight handling here look sound.
1907-1938: Try‑runtimepre_upgrade/post_upgradeassertions correctly pin the expected LastTimeFrame behaviorThe try‑runtime hooks:
- Encode whether the upgrade should run (
ran), the pre‑upgradeLastTimeFrame, and the pre‑upgradeLastTimeFrameRescaledflag.- In
post_upgrade, short‑circuit ifranis false, otherwise:
- assert the flag was not already set pre‑upgrade,
- verify that
LastTimeFramewas multiplied byTIME_FRAME_SCALE_FACTOR, and- ensure
LastTimeFrameRescaledis now set.This is a good, minimal invariant set for the LastTimeFrame portion of the migration and should catch mis‑ordering or missed updates in future changes to the runtime upgrade wiring.
2056-2060: EphemeralLastTimeFrameRescaledstorage item is used consistently and cleaned up after migrationDefining
LastTimeFrameRescaled<T>as aStorageValue<_, bool, ValueQuery>with a getter, then:
- setting it in
on_runtime_upgrade, and- deleting it at the end of
TimeFrameRescaleMigration::stepgives you a simple handshake flag between the single‑block LastTimeFrame rescale and the multi‑block cache migration. Once the migration completes, the flag is removed, so it doesn’t linger as dead state for new deployments.
This pattern looks appropriate for a one‑off migration marker.
What does it do?
Add migration for rescaling time frames after block time change.
try-runtime-zeitgeist-output.txt
try-runtime-battery-station-output.txt
What important points should reviewers know?
Is there something left for follow-up PRs?
What alternative implementations were considered?
Are there relevant PRs or issues?
References
Summary by CodeRabbit
Refactor
New Features
Bug Fixes / Improvements
Tests
Chore
✏️ Tip: You can customize this high-level summary in your review settings.