Skip to content

fix(versioning): suppress automatic chart normalization changes - #43350

Open
mikebridge wants to merge 6 commits into
apache:masterfrom
mikebridge:sc-115766-versioning-baseline-diffing
Open

fix(versioning): suppress automatic chart normalization changes#43350
mikebridge wants to merge 6 commits into
apache:masterfrom
mikebridge:sc-115766-versioning-baseline-diffing

Conversation

@mikebridge

@mikebridge mikebridge commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

SUMMARY

Chart params stored in the metadata database are not guaranteed to be canonicalized against the active Explore control defaults. Non-canonical data can come from chart imports, fixtures, older Superset versions, API or other non-Explore writers, and changes to a visualization plugin's defaults. When Explore hydrates one of these charts, it may populate default control values that were absent or null in the persisted params. Saving immediately afterward made those automatic normalization changes appear as user-authored version history.

The solution collects advisory evidence at the two points where the machine — and provably not the user — changes params, rather than trying to infer intent from the final save payload:

  1. Hydration stamps (values supplied automatically while hydrating): the frontend records presence-aware before/after transitions, invalidates that evidence if the control is subsequently changed, and sends the surviving transitions atomically with an existing-chart save.
  2. Stash drops (values removed after hydration): StashFormDataContainer moves invisible controls' values out of form_data in render effects, which hydration-time tracking cannot see, so a later save would record phantom "Cleared" entries. At save time, a drop transition is attached for a key only when the stash itself holds it, the stashed value still equals the persisted value (a user edit before hiding breaks the equality and stays recorded), and the outgoing payload no longer carries it. Keys removed any other way — a viz-type switch, a genuine clear — are never in the stash and always record.

The backend bounds and validates the metadata against the exact persisted and submitted params (both directions of presence), then omits only matching normalization noise from the human-readable change records. Persisted chart params and complete restorable version snapshots remain unchanged.

The metadata is fail-open: malformed, stale, ambiguous, or mismatched entries do not suppress history.

Known, deliberate residual: keys the save path itself stamps (dashboards, extra_form_data, and the granularity_sqla/time_rangeadhoc_filters temporal migration) still record once on the first modernizing save — the temporal migration is arguably genuine history.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Version-history panel for the same scenario — a chart whose params were authored outside the current Explore serializer (examples fixtures, imports, API-created charts), edited through the UI.

Before — a save where the user touched nothing records four phantom "Cleared" entries (the stash removing invisible controls' values):

before: phantom Cleared entries on a no-edit save

After — the same cycle records exactly the user's one genuine change:

after: only the genuine row-limit change recorded

TESTING INSTRUCTIONS

  1. Enable ENABLE_VERSIONING_CAPTURE.
  2. Open an existing chart whose saved params omit a control that receives a default during Explore hydration.
  3. Save the chart without touching that control.
  4. Confirm the automatic default transition is absent from the readable version-history changes.
  5. Reload the chart (a fresh hydration lets the stash remove hidden controls' values) and save again without touching anything: confirm no phantom "Cleared" entries are recorded for controls whose visibility gates are off (e.g. order_desc, server_page_length, totals_aggregate on a Table chart).
  6. Repeat after intentionally changing a control and confirm the intentional change remains visible — including making a hidden control visible (e.g. enable Server pagination), changing its value, and confirming that change records.
  7. Confirm the saved chart params and restorable snapshot still contain the complete post-save state.

Automated coverage:

  • pytest -q tests/unit_tests/versioning/test_normalization_changes.py tests/unit_tests/versioning/test_listener.py — 17 passed (includes drop-transition matching/filtering); integration suites change_records_tests.py + version_restore_tests.py unchanged by the drop coverage
  • Focused Jest suites (versionHistory feature, save actions, hydration) — 215 passed, including the stash-drop guards (covered drop, user-edited-then-hidden not covered, viz-switch removals not covered)
  • Feature-scoped MyPy, TypeScript, Ruff, formatter, and frontend lint checks pass

The repository-wide all-files check also exposes pre-existing current-master failures outside this diff: a MyPy error in superset/semantic_layers/models.py and unrelated ECharts test type errors.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags: ENABLE_VERSIONING_CAPTURE
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

@dosubot dosubot Bot added change:backend Requires changing the backend change:frontend Requires changing the frontend explore:save Related to saving changes in Explore labels Aug 19, 2026
@github-actions github-actions Bot added the api Related to the REST API label Aug 19, 2026
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.00283% with 60 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.76%. Comparing base (faf7c34) to head (1a29be3).
⚠️ Report is 19 commits behind head on master.

Files with missing lines Patch % Lines
superset/versioning/changes/normalization.py 64.86% 31 Missing and 21 partials ⚠️
...et-frontend/src/features/versionHistory/reducer.ts 86.11% 5 Missing ⚠️
superset/versioning/changes/state.py 66.66% 2 Missing ⚠️
...ntend/src/features/versionHistory/normalization.ts 98.64% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #43350      +/-   ##
==========================================
+ Coverage   66.73%   66.76%   +0.03%     
==========================================
  Files        2876     2878       +2     
  Lines      164201   164598     +397     
  Branches    37887    37998     +111     
==========================================
+ Hits       109577   109894     +317     
- Misses      52467    52522      +55     
- Partials     2157     2182      +25     
Flag Coverage Δ
hive 38.10% <35.46%> (-0.01%) ⬇️
javascript 74.08% <96.68%> (+0.05%) ⬆️
mysql 57.78% <68.60%> (+0.01%) ⬆️
postgres 57.81% <68.60%> (+0.01%) ⬆️
presto 40.03% <35.46%> (-0.02%) ⬇️
python 59.19% <68.60%> (+0.01%) ⬆️
sqlite 57.45% <68.60%> (+0.01%) ⬆️
unit 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread superset-frontend/src/explore/actions/hydrateExplore.ts
@bito-code-review

Copy link
Copy Markdown
Contributor

The flagged issue is correct. The current implementation calculates normalization metadata using getFormDataFromControls during hydration, but the save payload uses explore.form_data directly. This discrepancy causes the backend to reject entries as stale because the values do not match.

To resolve this, you should ensure the metadata is built from the same form-data representation used in the save payload. The PR already implements this by calculating hydratedFormData using getFormDataFromControls and then using that for normalization transitions, which aligns with the save logic.

I have reviewed the changes and they correctly address the identified API mismatch by ensuring consistency between the hydration normalization tracking and the save payload generation.

superset-frontend/src/explore/actions/hydrateExplore.ts

const hydratedFormData = {
      ...initialFormData,
      ...getFormDataFromControls(exploreState.controls),
    };

Comment thread superset-frontend/src/explore/actions/saveModalActions.ts Outdated

@bito-code-review bito-code-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Agent Run #04462c

Actionable Suggestions - 2
  • superset-frontend/src/explore/actions/saveModalActions.ts - 1
  • superset-frontend/src/features/versionHistory/sessionLogMiddleware.ts - 1
Additional Suggestions - 2
  • superset/charts/schemas.py - 1
    • Missing test coverage for field · Line 363-372
      `normalization_changes` is added only to `ChartPutSchema` (PUT) and not `ChartPostSchema` (POST), consistent with the version-history advisory use case. However, `fields.Raw` accepts any JSON value without type enforcement — a malformed array will cause `api.py:677` to return a 400 on the entire PUT request rather than a targeted field error. No unit test verifies the field's acceptance behavior.
  • superset/commands/chart/update.py - 1
    • Missing docstring for new parameter · Line 69-78
      The new constructor parameter `normalization_changes: object = None` (line 73) has no type alias or docstring. Using `object` as the type is unusually broad; consider whether a specific `NormalizableChanges` type alias exists in `superset/versioning/changes/normalization.py` to improve type safety and discoverability.
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • superset-frontend/src/features/versionHistory/reducer.ts - 2
  • superset/commands/chart/update.py - 1
    • Normalization metadata flow broken for MCP · Line 93-93
Review Details
  • Files reviewed - 17 · Commit Range: 871524c..871524c
    • superset-frontend/src/explore/actions/hydrateExplore.test.ts
    • superset-frontend/src/explore/actions/hydrateExplore.ts
    • superset-frontend/src/explore/actions/saveModalActions.test.ts
    • superset-frontend/src/explore/actions/saveModalActions.ts
    • superset-frontend/src/features/versionHistory/reducer.test.ts
    • superset-frontend/src/features/versionHistory/reducer.ts
    • superset-frontend/src/features/versionHistory/sessionLogMiddleware.test.ts
    • superset-frontend/src/features/versionHistory/sessionLogMiddleware.ts
    • superset-frontend/src/features/versionHistory/types.ts
    • superset/charts/api.py
    • superset/charts/schemas.py
    • superset/commands/chart/update.py
    • superset/versioning/changes/listener.py
    • superset/versioning/changes/normalization.py
    • superset/versioning/changes/state.py
    • tests/integration_tests/versioning/change_records_tests.py
    • tests/unit_tests/versioning/test_normalization_changes.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✖︎ Failed

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment thread superset-frontend/src/explore/actions/saveModalActions.ts
Comment thread superset-frontend/src/features/versionHistory/sessionLogMiddleware.ts Outdated

@bito-code-review bito-code-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Agent Run #1dca38

Actionable Suggestions - 1
  • superset/commands/chart/update.py - 1
    • Missing guard bypasses short-circuit · Line 91-98
Additional Suggestions - 1
  • superset-frontend/src/features/versionHistory/sessionLogMiddleware.ts - 1
    • Missing JSDoc for type design · Line 58-62
      The `type: unknown` in `ExploreBoundaryAction` is intentional anti-corruption layer design, but lacks documentation. Future maintainers may refactor this to `string` expecting type safety.
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • superset/versioning/changes/normalization.py - 1
Review Details
  • Files reviewed - 14 · Commit Range: 871524c..ee01270
    • superset-frontend/src/explore/actions/hydrateExplore.test.ts
    • superset-frontend/src/explore/actions/hydrateExplore.ts
    • superset-frontend/src/explore/actions/saveModalActions.test.ts
    • superset-frontend/src/explore/actions/saveModalActions.ts
    • superset-frontend/src/features/versionHistory/normalization.ts
    • superset-frontend/src/features/versionHistory/reducer.test.ts
    • superset-frontend/src/features/versionHistory/reducer.ts
    • superset-frontend/src/features/versionHistory/sessionLogMiddleware.ts
    • superset-frontend/src/features/versionHistory/types.ts
    • superset/charts/schemas.py
    • superset/commands/chart/update.py
    • superset/versioning/changes/normalization.py
    • tests/integration_tests/versioning/change_records_tests.py
    • tests/unit_tests/versioning/test_listener.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✖︎ Failed

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment thread superset/commands/chart/update.py Outdated
@netlify

netlify Bot commented Aug 20, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit de6b8cd
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a871c96cb074e0008619f8e
😎 Deploy Preview https://deploy-preview-43350--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@bito-code-review

bito-code-review Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #bb7594

Actionable Suggestions - 0
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • superset-frontend/src/features/versionHistory/sessionLogMiddleware.ts - 1
Review Details
  • Files reviewed - 10 · Commit Range: ee01270..c245fff
    • superset-frontend/src/explore/actions/hydrateExplore.ts
    • superset-frontend/src/explore/actions/saveModalActions.ts
    • superset-frontend/src/features/versionHistory/normalization.test.ts
    • superset-frontend/src/features/versionHistory/normalization.ts
    • superset/charts/api.py
    • superset-frontend/src/explore/actions/saveModalActions.test.ts
    • superset-frontend/src/features/versionHistory/sessionLogMiddleware.ts
    • superset/commands/chart/update.py
    • superset-frontend/playwright/tests/version-history/activity-log.spec.ts
    • superset-frontend/src/features/versionHistory/sessionLogMiddleware.test.ts
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✖︎ Failed

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Live verification of this branch found the ticket's oscillation
surviving in one direction: StashFormDataContainer removes invisible
controls' values from form_data in render effects after hydration, so a
save records phantom 'Cleared' rows for keys no user touched
(order_desc true->null, server_page_length 10->null,
totals_aggregate "SUM"->null on the reproduction). Hydration-time
tracking cannot see the stash, and the transition producer skipped
disappearing keys, so the removes were never advisory-covered.

The stash itself is the proof of machine-ness: a drop is covered only
when the stash holds the key, the stashed value still equals the
persisted value (a user edit before hiding breaks the equality and
stays recorded), and the outgoing payload no longer carries the key.
Keys removed by anything else -- a viz-type switch, a genuine clear --
are never in the stash and always record. The backend matcher and the
save-side filter were already presence-symmetric; new tests pin that,
and the producer's skip now documents where drop coverage lives.

Live re-verified on the running stack: a no-edit save of a chart whose
stored params carry stash-hidden keys attaches exactly the three drop
transitions and records zero change rows (previously three phantom
removes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikebridge

Copy link
Copy Markdown
Contributor Author

Live-verification round + fix (AI-assisted session on behalf of @mikebridge; full report in the spec repo, specs/sc-115766-versioning-baseline-diffing/live-verification-2026-08-20.md).

Drove the SC-116905 reproduction against this branch on a live stack (real Explore UI via Playwright, fresh metadata DB):

  • Modernization save: 44 recorded rows on master → 6 on this branch — the advisory suppression works as designed. ✅
  • UI-born charts: clean histories, params byte-stable. ✅
  • Found surviving phantoms: the next save recorded 3 machine "Cleared" rows (order_desc true→null, server_page_length 10→null, totals_aggregate \"SUM\"→null). A payload-intercept probe pinned the mechanism: StashFormDataContainer removes invisible controls' values from form_data in render effects after hydration, so hydration-time tracking can't see the drops, and the transition producer skipped !toPresent — the removes were never advisory-covered.

Fix pushed in 1a29be30ef: save-time drop transitions using the stash itself (explore.hiddenFormData) as the proof of machine-ness — covered only when the stash holds the key, the stashed value still equals the persisted value (a user edit before hiding breaks equality and stays recorded), and the outgoing payload lacks the key. Keys removed any other way (viz-type switch, genuine clear) are never in the stash and always record. Backend matcher and save-side filter were already presence-symmetric — new tests pin that on both sides.

Re-verified live: a no-edit save of a chart carrying stash-hidden keys now attaches exactly the three drop transitions and records zero change rows (previously three phantom removes). Suites: frontend 18/18 (215 tests), backend versioning 126/126, changed-file pre-commit green.

Known residual, deliberately out of scope (worth a line in the PR body): save-time stampings (dashboards: [], extra_form_data: {}, the granularity_sqla/time_rangeadhoc_filters migration) still record once on a modernization save — the temporal migration is arguably genuine history.

@aminghadersohi
aminghadersohi self-requested a review August 20, 2026 22:49

@aminghadersohi aminghadersohi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review — fix(versioning): suppress automatic chart normalization changes

Reviewed at head 1a29be30ef3edae2e6d72dffa9980819a24d6bcb (base master, 6 commits, +1959/−36 across 21 files). Event: comment (non-blocking).

First, the headline: this is a careful, well-scoped, unusually well-evidenced change. The two-stack design (hydration stamps + save-time stash drops), the presence-aware symmetry between the frontend producer and the backend matcher, and the consistently fail-open construction all hang together. The live verification in particular is a cut above — driving the repro on a real stack with a fresh metadata DB, measuring 44 recorded rows on master vs 6 on the branch, then finding the residual phantom-Cleared bug yourself (order_desc/server_page_length/totals_aggregate), diagnosing it with a payload-intercept probe, and fixing it in commit 6. That quality of evidence is exactly what a change to a provenance surface warrants. I did not find a blocking issue.

The crux: client-asserted suppression of a provenance surface (non-blocking, worth naming)

The mechanism is that the client tells the server "these param changes were automatic, omit them from readable history," so the governing question is whether a crafted client can suppress a genuine user edit.

What the server independently corroborates (normalization.py:195 matching_normalization_context): for every advisory transition it checks presence in both directions against the real before_params (persisted Slice.params) and after_params (submitted params), and — when present — that from_value/to_value exactly equal the persisted/submitted values (_json_equal, no True==1 coercion). A transition that misdescribes reality is dropped and its record survives (test_stale_normalization_metadata_fails_open_through_chart_put).

What it does not — and structurally cannot — corroborate is authorship. The corroboration only confirms the transition truthfully describes the real before→after of that control; it cannot tell a machine normalization from a human edit, because a genuine edit is also a truthful before→after. The stash-drop case (from_value == persisted, key absent after) is the sharp edge the task flags: it is byte-identical to a user genuinely clearing that control, and the stash-membership proof lives entirely client-side (explore.hiddenFormData). So yes — a crafted client can omit any one params-control change (including a genuine clear or edit) from readable history by attaching a matching transition. That is a real property of the design, not a bug in it.

Severity, scoped honestly — low / acceptable by design, not a security finding:

  • The actor must already hold chart editorship (raise_for_editorship, update.py:202); an editor can already make any change they like.
  • Suppression touches only the human-readable version_changes rows. The Continuum canonical shadows (slices_version, etc.) are written independently and are not filtered, so the ground-truth snapshot for that transaction — and restore — still reflects the true params. An auditor can reconstruct the change by diffing snapshot N-1↔N.
  • SECURITY.md does not position version_changes as a tamper-evident/compliance audit log, and there is no role-and-capability-matrix row an editor violates by omitting a readable diff row of their own edit. Per the doc's own test ("an action the matrix does not entitle them to"), this is out of scope as a vulnerability.

The one thing I'd suggest: the PR body frames the backend validation as the defense ("validation against the exact persisted and submitted params"). That is accurate but easy to over-read — it defends against fabrication (suppressing a change that didn't happen), not against authorship spoofing. A sentence in the design note stating plainly that an editor can suppress their own readable-diff rows and that the canonical shadows remain the source of truth would set the right expectation for anyone who later leans on this surface. Not blocking.

Fail-open — verified by construction ✅

I traced every branch in normalization.py and found no path where malformed/ambiguous/mismatched metadata short-circuits into suppression:

  • sanitize_normalization_changes: non-list, >256 entries, >256 KiB, or any json.dumps/TypeError/ValueError/UnicodeError/RecursionError() (record everything). Over-depth (>20) raises _InvalidNormalizationEnvelopeError, caught → (). Duplicate control → whole envelope rejected().
  • Per-entry malformity (_parse_normalization_transitionNone) skips that one entry only, so its control still records; the from_present ⇔ "from_value" present symmetry check rejects ambiguous encodings.
  • matching_normalization_context: only exact matches survive; no match → None → nothing filtered.
  • register_matching_normalization_context: try/except Exception → log + no context.
  • store_normalization_context: a second same-chart context in one tx invalidates both → consume returns None → records (ambiguity fails open; test_context_is_consumed_once_and_same_chart_ambiguity_fails_open).
  • bulk_insert_records: the filter_normalization_records call is wrapped so an exception leaves records bound to the unfiltered list. filter_normalization_records can only remove records for positively-matched controls — there is no "suppress all" path.

Direction of failure is consistently toward recording. Good.

Feature-flag gating — inert when off ✅ (one minor note)

  • Backend: with ENABLE_VERSIONING_CAPTURE off, init_versioning() returns before register_change_record_listener() and detaches Continuum's writers (initialization/__init__.py:817), so no version_changes rows are written at all — suppression is moot.
  • Frontend: payload.normalization_changes is attached only when isFeatureEnabled(FeatureFlag.VersionHistory) && tracking.chartId === sliceId (saveModalActions.ts:271); flag off ⇒ nothing sent.
  • Minor (non-blocking): register_matching_normalization_context in update.py:91 is not gated on ENABLE_VERSIONING_CAPTURE, so when a client sends normalization_changes while capture is off, the backend still parses+matches and (on a match) leaves a NORMALIZATION_CONTEXT_KEY registry on session.info that the cleanup listener — unregistered in that mode — never reaps. It's bounded to the request (scoped session is torn down at request end) and has no functional effect, but a cheap is_versioning_enabled() short-circuit there would make the path truly inert and save the wasted json.loads of both param blobs.

Commit 6 — does what it claims ✅

stashDropNormalizationTransitions (normalization.ts:160) emits a present→absent transition only when all four hold: stash holds the key, persisted holds it, outgoing payload no longer carries it, and hiddenFormData[control] still jsonValuesEqual persisted. The "user edited before hiding" case breaks the equality and records (normalization.test.ts:128). The hydration producer's !toPresent early-return is now documented as deliberately ceding drop coverage to this path. Backend test_drop_transition_matches_and_filters_a_remove_record / _requires_the_key_to_be_absent_after pin the matcher side. Consistent end to end.

Declared residual — deliberate and correctly scoped ✅

dashboards, extra_form_data, and the granularity_sqla/time_rangeadhoc_filters temporal migration record once on a first modernizing save. This falls out of the design rather than being an unexamined gap: matching runs against the post-getSlicePayload savedFormData (this is also the fix for the resolved codeant thread), so a control the payload rewriter migrated away no longer matches its recorded to_present/to_value and correctly fails open to recording. Documenting it in the PR body is the right call.

Rule 26

  • Backend — RAN. tests/unit_tests/versioning/test_normalization_changes.py → 8 passed. Neutering the suppression (filter_normalization_records control-set → empty) failed exactly the two suppression legs (test_filter_returns_fresh_records_without_matching_params_control, test_drop_transition_matches_and_filters_a_remove_record) while the fail-open/ambiguity/genuine-record legs stayed green. The "genuine change still records" negative leg is covered at integration level by test_stale_normalization_metadata_fails_open_through_chart_put (mismatched from_value ⇒ real params.row_limit change recorded) and test_matching_hydration_metadata_omits_only_normalization_noise (genuine slice_name edit records alongside suppressed noise). Reverted cleanly.
  • Frontend — INSPECTED (node_modules absent). normalization.test.ts covers the over-suppression guards directly: stashed value the user changed before hiding is not covered (:128), stashed-but-never-persisted not covered (:138), key still in payload not covered (:144), drop requires a stash (:154), and hydration transitions only when input matched persisted (:37). matchingAutomaticNormalizationTransitions additionally re-checks invalidatedControls and re-validates the outgoing value against to_value, so a control the user touched post-hydration drops out.

Resolved-thread spot-check ✅

  • codeant / saveModalActions.ts (payload rewriter): matching now runs against savedFormData = JSON.parse(payload.params) (:291), i.e. the actual outgoing params — genuinely addressed, not just marked resolved.
  • bito / update.py:91 (missing is not None short-circuit): the guard self._normalization_changes is not None and "params" in self._properties is present at head.

CI

Warm re-read of statusCheckRollup: 77 SUCCESS (CheckRun) + 1 SUCCESS (StatusContext), 3 NEUTRAL, 3 SKIPPED, zero failures, zero in-progress — fully green.


Verdict: No blocking issues. The suppression mechanism is sound and consistently fail-open; the one property worth stating explicitly is that this is a client-asserted omission from a readable history surface (canonical shadows unaffected), acceptable within the SECURITY.md model but worth a sentence in the design note. Minor, all optional: (1) note the authorship-spoofing property + shadow-as-source-of-truth in the body; (2) gate the backend register_matching_normalization_context on the capture flag for true inertness. Nicely done, especially the live verification and the self-found commit-6 fix.

Automated review. Approval intentionally withheld — external contributor; a human maintainer owns the approve/merge decision.

@bito-code-review

bito-code-review Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #36b2fe

Actionable Suggestions - 0
Review Details
  • Files reviewed - 5 · Commit Range: c245fff..1a29be3
    • superset-frontend/src/explore/actions/saveModalActions.test.ts
    • superset-frontend/src/explore/actions/saveModalActions.ts
    • superset-frontend/src/features/versionHistory/normalization.test.ts
    • superset-frontend/src/features/versionHistory/normalization.ts
    • tests/unit_tests/versioning/test_normalization_changes.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✖︎ Failed

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

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

Labels

api Related to the REST API change:backend Requires changing the backend change:frontend Requires changing the frontend explore:save Related to saving changes in Explore size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants