mikebridge commented on PR #41551:
URL: https://github.com/apache/superset/pull/41551#issuecomment-5142155920
# Capstone review — versioning as a whole
This is a different animal from the three diff reviews already posted above:
a 10-lens panel over the **entire versioning feature as one system** — the
merged backend (`superset/versioning/`, routes, migrations, config) together
with this branch's UI — rather than this PR's diff. Findings against merged
code are marked as backend follow-ups, not asks on this PR; the "Read against
the merge plan" section at the bottom sorts every item into what belongs here
vs what comes after.
**Date:** 2026-07-31 · **Head:** `50eb210534`
**Scope:** merged backend + open UI branch and its integration points.
Soft-deletion/purge excluded — it has its own review track.
**Panel:** 10 lenses run in parallel — python, sqlalchemy, react, css,
clean-code, tidy-first, domain-modelling, continuous-delivery,
committer-perspective, SaaS-operations. Each was instructed to verify claims
against the actual code, skip this PR's already-known open Medium/Lows, and
look for what diff reviews structurally miss.
**⚑ = found independently by two or more lenses.**
Per-lens verdicts: no lens issued a BLOCKER. CD: *land it*. Committer:
*request changes at the epic level* (two HIGH code items + SIP process). React:
*needs changes* (two HIGHs on the UI branch). Others: no blockers / polish
first.
---
## HIGH
### H1 ⚑⚑ `/versions/` family and restore don't discriminate against
integer-id reuse — a successor entity inherits a hard-deleted predecessor's
history
`superset/versioning/queries.py:157-207,336,407,482` ·
`superset/versioning/restore.py:134-143` — **python-review +
superset-committer-review, independently**
Every version lookup (`list_versions`, `_get_version_count`,
`current_live_transaction_id`, `resolve_version`, `get_version`) and the
restore target query filter shadow rows on bare `ver_cls.id == entity.id`. Hard
delete does not prune shadow rows; on SQLite/MySQL id reuse, the successor
lists the predecessor's versions, `GET /versions/<v>/` returns the
predecessor's full snapshot to a principal with read on the **successor only**
(SECURITY.md read-capability row; assumed principal Gamma), and `POST
…/restore` writes the predecessor's content — including its versioned `uuid`
column, which no `__versioned__` exclude covers — onto the successor,
corrupting its API identity. The activity layer already pins `(id, uuid)` for
exactly this hazard (`activity/queries.py:94-115,546-614`), so the backend
disagrees with itself. **Fix:** add the same uuid predicate to the `/versions/`
family and restore's target lookup. Backend — needs its own PR against merged
code.
### H2 Version payloads expose `username`
`superset/versioning/schemas.py:31-37` · `queries.py:125,142` —
**superset-committer-review**
`VersionChangedBySchema` includes `username` in every `/versions/` row and
snapshot `_version` block, against the project posture (`dashboards/schemas.py`
excludes it; this epic's own `ActivityChangedBySchema` omits it "by design").
Any read-capable principal — including embedded guest tokens per H12 — can
enumerate past editors' login names. Breaking to remove after release; cheap
now. Coordinate with `types.ts:72`. Backend PR.
### H3 Chart preview never leaves the "applying" state — Restore never
appears for chart previews **[CONFIRMED]**
`ChartVersionPreview.tsx:115-205` · `reducer.ts:176` ·
`PreviewBanner.tsx:82-101` — **react-review**; confirmed by grep:
`versionPreviewApplied` is dispatched from exactly one place, the dashboard hook
`SET_VERSION_PREVIEW` unconditionally enters `isPreviewApplying`; only
`useDashboardVersionPreview.ts:341` ever dispatches `versionPreviewApplied`.
The chart path tracks loading in local state and never clears the store flag,
so every chart preview's banner reads "Loading historical version" forever and
the Restore button (gated on `!isApplying`) never renders. Introduced by the
loading-state fix (`6f1771cff9`); tests mask it by hand-crafting
`isPreviewApplying: false` instead of driving the real action. **This breaks
the primary chart restore flow on #41551. Verify and fix on the branch
immediately.**
### H4 Restore lacks the unsaved-changes guard that preview has
`useVersionActions.tsx:110-153` · `DashboardVersionHistory.tsx:213-224` ·
`ExploreVersionHistory.tsx:258-269` — **react-review**
`handlePreview` blocks on `hasUnsavedChanges` precisely because rehydration
wipes in-progress work; the kebab restore path has no such guard on either
page, and post-restore both hooks unconditionally refetch + rehydrate —
silently destroying unsaved layout/control edits and undo history.
`RestoreConfirmModal` says "your existing version history will not be lost" and
never mentions unsaved changes. Fix on #41551: apply the same gate (or an
explicit discard step) to `requestRestore`/`confirmRestore`.
### H5 ⚑ The backend's `truncated` contract dies at the frontend boundary
`types.ts:66-69` · `useVersionActivity.ts:219` vs `schemas.py:470-478` —
**clean-code-review + domain-driven-design-review, independently**
The backend carefully makes truncation honest (per-kind ceilings, clean cut,
"count is a floor", a `truncated` response field); the frontend type omits the
field and computes `hasMore` from `count` alone, so clamped history renders as
complete with no indicator. Fix on #41551: consume the flag (terminal "history
truncated" row) — or remove it from the wire.
### H6 Per-workspace retention lever is missing
`superset/tasks/version_history_retention.py:536-538` ·
`config.py:1696-1722` — **preset-review**
Retention reads only the env-seeded config; the sibling soft-delete purge
already established the pattern this was designed to follow (`SharedKey` read
live per run + CLI setter + config fallback — a follow-up already designed in
the planning docs but not yet implemented). Support cannot adjust one
workspace's window (legal hold, disk pressure) without config change + worker
restart. Known deferred; now confirmed as a fleet-rollout gate, not a
nice-to-have. Backend PR.
### H7 Activity pagination is O(full visible history) per page request
`orchestrator.py:300-318` · `queries.py:543` · `useVersionActivity.ts:36` —
**preset-review**
Every `/activity/` page re-materializes up to 5000 rows/kind × 3 kinds
through the full pipeline, then slices 25 in Python; server-side `q` search
rescans the set per debounced keystroke; no response caching, no ETag on
`/activity/` (wired only to `/versions/`). The client compounds it:
`MAX_CHAINED_PAGES = 8` per "Load more" click, plus refresh-on-save while the
panel is open. The code's own note records 4s p95 on rich dashboards for one
pass. Backend; needs a cost-model fix (or at least ceilings-as-config, M-13)
before busy-workspace rollout.
### H8 Entity-kind taxonomy is a shotgun-surgery hotspot
`kinds.py:73-115,198-209` · `visibility.py:136-163` · `orchestrator.py:421`
· `changes/table.py:86` · `changes/state.py:86-90,183-189` —
**clean-code-review**
"A versioned entity kind" is smeared across six parallel dicts and four
dispatch tables keyed on the same three class names; `visibility.py` even ships
a fail-closed guard for "updated one table, forgot the other." Adding a fourth
kind (SavedQuery) means ~10 edits in 5 modules. Fix: one `EntityKindSpec`
registry. Post-merge refactor, but do it before the fourth kind.
### H9 ⚑ The two page containers are ~120-line copy-pastes that have already
drifted
`ExploreVersionHistory.tsx:108-123,218-282` vs
`DashboardVersionHistory.tsx:92-107,165-237` — **clean-code-review +
tidy-first-review, independently**
URL-param effect, unmount-close, search/debounce, refresh effect and six
handlers duplicated; drift already present (initial-hydration sentinel
`undefined` vs `'|'`); co-change already demonstrated on this branch. The
save-signal selector is a third verbatim copy (M-24). Fix: extract a shared
container hook. Post-merge cleanup batch, first item.
### H10 Headline rendering is split across the boundary with two
vocabularies, one unlocalizable
`render.py:67-80,251-271` vs `display.ts:206-301` —
**domain-driven-design-review**
Related-record headlines are server-composed raw English (no i18n);
self-record headlines are client-composed, differently worded, `t()`-wrapped.
The same domain event gets different sentences depending on which side renders
it, and server English surfaces untranslated in a localized UI.
`schemas.py:40-46` documents the opposite of what happens. Decide one owner for
prose (client, which owns i18n) and make the machine-readable record the
contract. Cross-layer; post-merge but before translations are cut.
### H11 "One logical save" is re-derived in the UI with a 60-second
wall-clock heuristic
`grouping.ts:84-118,129-165` — **domain-driven-design-review**
`mergeAdjacentRelatedEntries` (60s window) and
`rollupSameTransactionRelated` approximate transaction semantics the backend
owns; two genuine saves 30s apart render as one in an audit-style timeline.
Both carry `TODO(version-history)` backend-workaround markers — the fix belongs
upstream (one transaction per logical save; dataset-level impact record). Known
compromise; schedule the upstream fix.
### H12 Meta text uses `colorTextQuaternary` — fails WCAG contrast
`SaveGroupItem.tsx:97` · `ActionRow.tsx:94` · `RelatedUpdateRow.tsx:95` ·
`CurrentVersionSection.tsx:100` — **css-review**
Disabled-level token (~1.9:1) on informative content at small size; needs
4.5:1. Codebase convention for meta text is
`colorTextTertiary`/`colorTextSecondary`; nothing else in `src/` uses
quaternary for content. One-token fix on #41551.
### H13 SIP-210 gates upstreaming: still "Open for Voting", UI explicitly
out of its scope, follow-up UI SIP not filed
apache/superset#39492 — **superset-committer-review**
Also: the `/activity/` endpoint family is outside SIP-210's declared API
surface but freezes as public contract on release (M-11), and the SIP text has
drifted from the implementation (restore relations, kill-switch posture —
L-16). None of this gates the fork merge; all of it gates upstream release.
Action: amend SIP-210 (or file the UI SIP) covering the UI, `/activity/`, and
the implemented restore semantics, then drive the dev@ vote.
---
## MEDIUM
- **M1 ⚑ `get_version` re-fetches by positional OFFSET after computing the
stable `transaction_id`** — a prune committing between the two queries returns
the wrong version's snapshot; the module's own docstring forbids positional
addressing. Filter on the tx id it already resolved. (`queries.py:468-487`;
**clean-code + sqlalchemy**, independently)
- **M2 `list_change_records_batch` doesn't chunk its `transaction_id IN`
clause** — ~990+ versions overflows SQLite's bind floor, and the resulting
OperationalError is swallowed by the missing-table catch, silently blanking
`changes` for every listed version. Chunk + narrow the except.
(`queries.py:268-281`; **sqlalchemy**)
- **M3 Backdated baseline `issued_at` makes retention prune fresh baselines
almost immediately** — baselines copy the entity's pre-versioning `changed_on`;
the prune cuts on `issued_at < cutoff`, so a legacy entity's baseline captured
today is prunable at the next 03:00 run — pre-edit state unrestorable within
hours, not 30 days. Anchor retention on capture time or exempt op=0 rows for N
days after insertion. (`baseline/insertion.py:120` +
`version_history_retention.py:198-207`; **python**)
- **M4 Naive-local and naive-UTC timestamps mixed in
`version_transaction.issued_at`** — Continuum stamps naive UTC; baselines copy
`AuditMixinNullable.changed_on` (naive local). On non-UTC servers baseline
ordering and since/until filters shift by the zone delta.
(`baseline/insertion.py:120`; **python**)
- **M5 No index on `table_columns_version.table_id` /
`sql_metrics_version.table_id`** — recurring FK-filtered queries (every dataset
snapshot, every dataset-save commit) degrade to full scans as shadow tables
grow. Add `(table_id, transaction_id)` composite indexes. (migration
`56cd24c07170:404-457,487-501`; **python**)
- **M6 `cap_records` doesn't bound layout records, contradicting its
docstring** — layout records get `path=[<component-id>]`, so every node is its
own group of 1 and the cap never fires; a large tab-restructure writes one row
per node, unbounded. Group under a synthetic bucket or add a per-transaction
total ceiling. (`diff.py:244-274,829-877`; **python**)
- **M7 `VERSION_RESTORED` is an unscoped global broadcast** — no entity
identity on the action; a restore resolving after navigation rehydrates the
*current* page's entity (clearing filters, wiping unsaved edits) for a restore
that happened elsewhere. Carry `entityUuid`/type and have consumers filter.
Related: `useDashboardVersionPreview` never checks `preview.entityUuid` against
the page's uuid. (`reducer.ts:181`; **react**) — #41551
- **M8 Session log records programmatic `SET_FIELD_VALUE` dispatches as user
edits** — transferred-control rewrites after load produce phantom "unsaved
changes" entries on untouched charts, which would also misfire any future
dirty-gate (H4's fix). Mark programmatic dispatches or log only
user-interaction paths. (`sessionLogMiddleware.ts:66-78`; **react**) — #41551
- **M9 Timeline rows unmemoized behind a container-level controlled search
input** — typing is O(total records) per keystroke once pages accumulate.
`memo()` the row components; handlers are already stable.
(`VersionHistoryPanel.tsx:261-290`; **react**) — #41551
- **M10 Embedded guest tokens can read full edit history + editor
identities** — guest short-circuit in `raise_for_access` passes the versioning
surface for the authorized dashboard; whether history metadata is inside the
guest entitlement is a question the SECURITY.md matrix doesn't answer. Filed as
a question per policy; recommend explicit `is_guest_user()` rejection or a
documented decision + pinning test. No guest-token test exists.
(`security/manager.py:4229-4237`; **committer**)
- **M11 `GET /versions/` is unpaginated** — bounded only by retention, which
is operator-configurable to 100 years; sibling `/activity/` already defines the
envelope. Retrofitting pagination post-release breaks clients.
(`api_helpers.py:222`; **committer**)
- **M12 Partial-restore warning rides a magic-string protocol** — client
compares the response to the literal `'OK'` (`message !== 'OK'`); any backend
rewording silently flips toast behaviour both ways. Add a structured
`skipped_count` field. (`useVersionActions.tsx:137` + `api_helpers.py:316-321`;
**tidy-first**)
- **M13 Fetch/page ceilings hardcoded** —
`_MAX_FETCHED_RECORDS`/`_MAX_PAGE_SIZE`/`_DEFAULT_PAGE_SIZE` have no config
override; support's only lever on a hot activity endpoint is the global capture
kill-switch, which doesn't touch read cost. (`queries.py:543`,
`orchestrator.py:71-72`; **preset**)
- **M14 ⚑ Write-path capture has no latency instrumentation** — the
kill-switch's documented trigger is "operator observes save-path slowdown", but
capture emits only error counters; the decision signal doesn't exist. Add a
timer around the listener body before the capture-on rollout step.
(`changes/listener.py:178`; **CD + preset**, independently)
- **M15 `ENABLE_VERSIONING_CAPTURE` opens the restore write API, not just
accrual** — capture-on day makes `POST …/restore` and `/activity/` live
production surface with no UI involved. Not a hole (editorship-gated, tested)
but the post-merge plan must treat them as live, not dark.
(`commands/version_restore.py:143`; **CD**)
- **M16 ⚑ `USER_FACING_KIND` duplicates `API_KIND_TO_TABLE`; `api_kind`
names the internal form** — the pipeline round-trips `entity_kind` through two
mapping tables back to its stored value; module docs claim the class-name form
reaches the DTO, contradicted by `render.py:145`. Merge the mappings into the
H8 registry; rename. (`kinds.py:94-98`; **clean-code + DDD**, independently)
- **M17 Restore headline persists `version_number`, an identifier the model
documents as unstable** — the immutable `__meta__` record stamps a display
index that drifts under pruning; "Restored to version 5" can later point at a
different position. `version_uuid` is already in the payload; resolve the
display number at read time. (`version_restore.py:119-126` +
`display.ts:309-314`; **DDD**)
- **M18 `kind='column'` means two different concepts** — dashboard layout
column vs dataset column, disambiguated only by entity kind; latent until a
dataset history panel reuses `buildTimeline`, at which point dataset column
edits get suppressed as layout scaffolding. Rename one side. (`diff.py:733-742`
vs `1052-1063` + `grouping.ts:174`; **DDD**)
- **M19 API strips the record's natural identity, forcing the client to hash
values** — `(transaction_id, entity_kind, entity_id, sequence)` is a real
unique key; decoration pops `entity_id` and `sequence`, so client dedup
reconstructs identity from eight fields including `JSON.stringify` of values.
Expose `sequence`. (`render.py:196-205` + `grouping.ts:31-47`; **DDD**)
- **M20 `__meta__` bends the record model's documented dimensional purity**
— `kind='__meta__'`/`path=['__meta__']` are sentinels inside namespaces
documented as pure; the deferral of a separate event stream is known — at
minimum document the exception in the `ChangeRecord` docstring.
(`diff.py:192-209`; **DDD**)
- **M21 ⚑ Presentation policy split across the boundary with drifted key
sets** — layout-node labels probed twice (backend knows `label`, frontend knows
`code`); kind→phrase vocabulary implemented on both sides with the seam
source-dependent. Pick one owner. (`display.ts:74-87` vs `diff.py:751-764`;
**clean-code + DDD**)
- **M22 `apply_record_decoration` does six jobs, one of which is a security
control** — the deleted-related-entity redaction ("deletion must not widen what
a requester can see") is a branch inside a cosmetically-named function; extract
it as a named function so a display refactor can't silently weaken it.
(`render.py:83-205,172-195`; **clean-code**)
- **M23 ⚑ `_resolve_uuids_for_kind` is a ~50-line copy of
`_resolve_names_for_kind`** — and the two near-identical shadow scans run per
request; selecting name + uuid in one statement halves them.
(`queries.py:620-732`; **clean-code + sqlalchemy**)
- **M24 The dashboard save-signal selector is duplicated, comment and all**
— third copy of the two-field join; a third save path bumping a different field
goes stale in one consumer silently. Export one selector.
(`DashboardVersionHistory.tsx:136-141` +
`useDashboardVersionPreview.ts:178-184`; **tidy-first**) — #41551
- **M25 The preview effect multiplexes three state machines** —
restore-reload / apply / exit coordinated through five refs in one ~160-line
effect; extract named flows so the ref protocol is testable per flow.
(`useDashboardVersionPreview.ts:218-380`; **clean-code**)
- **M26 `NAME_COLUMN` is the taxonomy-membership predicate** — a
display-column lookup gates security-relevant fail-closed behaviour in four
places; membership should test the kind registry. (`kinds.py:111` et al.;
**clean-code**)
- **M27 `VersionHistoryColumn` `height: 100vh` clips the panel bottom under
the navbar** — "Load more" can be permanently unreachable on short dashboards;
the flag-on empty column also forces a viewport-height grid minimum.
(`DashboardBuilder.tsx:158-167`; **css**) — #41551
- **M28 No responsive strategy for the dashboard-side panel** — explore
overlays below `screenXL`; the dashboard host pins 320px at every viewport.
Port the overlay treatment. (`VersionHistoryPanel.tsx:45-50`; **css**) — #41551
- **M29 ⚑ Timeline visual primitives and the kebab menu are built 2-4×
each** — `Meta`/`IconWrapper`/`ChevronWrapper`/`KebabButton`/`itemStyle`
duplicated across four components (CloseButton has already drifted); the
two-item restore/fork menu is constructed twice with the open-as-new label
ternary a third time. Extract a shared `styles.ts` + one `VersionActionsKebab`.
(**css + tidy-first**, independently) — #41551 / cleanup
- **M30 The 60s heuristic aside, `mergeActivityPages` reset UX**: every
debounced search/include change discards all chained pages and flashes the
skeleton; stale-while-revalidate would keep results visible.
(`useVersionActivity.ts:144-149`; **react**, LOW-rated there but user-visible)
## LOW (condensed)
- ⚑ **Two live `useVersionActions` instances per page** carry independent
in-flight locks — banner + panel can each launch a fork/restore concurrently
(tidy-first + react). Store-level in-flight flag, or hoist the hook when H9's
dedup happens.
- ⚑ **`resolve_version` derives UUIDv5 per row in Python** — O(lifetime
edits) per snapshot/restore when retention is disabled, which is a supported
operator choice (preset M + sqlalchemy + python). The docstring names itself
the revisit point.
- ⚑ **`shadow_row_count` runs `count(*)` + SAVEPOINT on every flush of every
versioned parent forever** — `EXISTS`/`LIMIT 1` or a per-process baselined
cache (preset + sqlalchemy).
- ⚑ **`path` segment typing disagrees across surfaces** — engine emits ints,
`ActivityRecordSchema` stringifies, `/versions/` preserves; use `fields.Raw`
(python + DDD).
- ⚑ **`EntityWindows` stays a positional tuple** next to the
deliberately-promoted `Window` dataclass (clean-code + DDD).
- **Check-then-insert baseline race** can produce duplicate op=0 rows under
concurrent first-saves; partial unique index or `INSERT … WHERE NOT EXISTS`
(python).
- **Chart restore can re-point `datasource_id` without a datasource-access
check** — consistent with PUT posture, but worth an explicit decision comment
(python).
- **`_matches_previous_version` probes without a SAVEPOINT**, unlike every
other defensive probe in the package — on PG a failure breaks the documented
fail-open fallback (sqlalchemy).
- **SQLite BigInteger PK autoincrement depends on Continuum's global
compiler shim** — `with_variant(sa.Integer(), 'sqlite')` would make the DDL
self-sufficient (sqlalchemy).
- **`q` search JSON-serialises every record's values per request** —
cheap-DoS shaped; pre-serialise or cap searched length (python).
- **`current_entity_version_info` runs the live-tx SELECT twice per write
response** (python).
- **Flag-on/capture-off empty state says "Saved changes will appear here"**
— untrue while capture is off; API exposes no capture state (CD).
- **Kill-switch off-window creates silent unattributed gaps in history** —
worth a line in the docs page (CD).
- **CI runs flag-on globally; no e2e asserts the panel is absent flag-off**
— one dark-mode Playwright assertion closes it (CD).
- **Retention beat entry lost by custom CeleryConfig** — warning exists; put
it in the rollout runbook (CD).
- **Retention drain loop has no per-run window cap or pacing** — first run
on a deep backlog contends all night (preset).
- **Capture switch is process-global** — per-workspace toggling depends on
process isolation; state it in the config comment (preset).
- **`?version_history=true` is never consumed** — a late `canRestore` flip
can re-open a panel the user closed (react).
- **`newestGroup` recomputed to null under `include='related'`** — "Current"
tag and restore notice vanish until refetch (react).
- **Post-restore, the save-signal effect discards the cache the restore
branch just warmed** (react).
- **`is_managed_externally` restore gate is client-side only** — matches PUT
posture, but the new docs page overstates it; either add the one-line server
check in `validate()` or soften the docs (committer). *Note: the docs page is
ours (`50eb210534`) — action owed either way.*
- **OpenAPI omits `version_number`'s instability under pruning**
(committer). **No UPDATING.md entry** for tables/dependency/knobs (committer).
**SIP text drift** on restore relations and kill-switch posture (committer).
- **Doc rot:** `changes/__init__` describes listener events that no longer
exist; `etag.py` docstring points at a nonexistent function; `diff.py:34`
self-referencing docstring; dead `_SUMMARY_VERBS` entries for removed kinds;
`find_active_by_uuid` promises filtering it doesn't do; `_MAX_FETCHED_RECORDS`
defined 200 lines after first use; magic `2` for Continuum DELETE at four
sites; `orchestrator.py` bundles four module responsibilities; pure layout
transforms live in `api.ts`; three hand-rolled fetch-guard shapes
(`useStaleGuard` candidate); CloseButton hover de-emphasizes; physical CSS
properties break RTL in four spots; undocumented z-indexes (99 ties the header;
magic 20 in explore); hover-only row highlight lacks `:focus-visible`; icon
buttons under touch-target size.
---
## Read against the merge plan
**Fix on #41551 before merge** (branch-local, cheap, real): H3 (chart
preview stuck applying — verify first, then fix + make the tests drive the real
action), H4 (unsaved-changes gate on restore), H5 (consume `truncated`), H12
(contrast token), M7 (scope `VERSION_RESTORED`), M8 (phantom session-log
entries — prerequisite for H4's gate being trustworthy), M27 (100vh clipping).
M9/M28/M29 if time allows.
**Backend PRs against merged code** (post-merge is fine; before broad
capture-on): H1 (uuid-pin `/versions/` + restore — security-grade, two-lens),
H2 (drop `username` — before the payload freezes), M1-M6 (OFFSET race,
IN-chunking, baseline retention/timezone, indexes, layout cap), M10
(guest-token decision), M11 (paginate `/versions/`), M12 (structured
partial-restore field), M14 (capture latency metric — gate for the capture-on
step), H6 (per-workspace retention), H7/M13 (activity cost model / ceilings).
**Upstream/process**: H13 + M11's SIP-scope point + SIP text drift — amend
SIP-210 or file the UI SIP, then the dev@ vote. Gates upstreaming, not this
fork.
**Post-merge cleanup batch** (the "fix later" the plan explicitly budgets):
H8 (kind registry), H9 (container hook), H10 (headline ownership), M16-M26
structure items, the LOW pile. Tidy-first's sequencing advice stands: normalize
the three fetch-guard shapes before extracting a shared hook.
**Known-and-confirmed**: H6 was already a known deferred follow-up; H11's
60s heuristic already carries backend-workaround TODOs; M15 restates that
capture is the real switch (documented in `capture_enabled()`); the
`canOverwriteSlice` out-of-flag change is already named in the PR description,
which is exactly what the CD lens asked for.
---
_This capstone review was generated by Claude (AI) on behalf of @mikebridge
— a 10-lens parallel review panel with findings verified against the code at
`50eb210534` and aggregated/de-duplicated by hand._
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]