Skip to content

refactor(i18n): retire 25 confirmed-dead locale keys across all ten packs - #6662

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-4730-i18n-dead-key-batch
Aug 28, 2026
Merged

refactor(i18n): retire 25 confirmed-dead locale keys across all ten packs#6662
os-sales merged 1 commit into
mainfrom
claude/issue-4730-i18n-dead-key-batch

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Refs #4730
Fixes#5783

Retires 25 individually hand-confirmed dead locale keys from all ten packs — 250 translated strings with no reader anywhere in the repo. The umbrella #4730 stays open: it still carries the held key-level trims (search, table, report, form, appDesigner), which is why this is Refs and not a closing keyword.

Measured at 09a1f9b3, merge-base 813bf8323.

The most important finding: the evidence standard has a fourth blind spot

Five keys this sweep reported CONFIRMED-dead in this very round are LIVE. They were pulled back out of the batch after reading their consumer, and are now pinned as live.

packages/app-shell/src/chrome/LoadingScreen.tsx is bootstrap-critical UI: it must render before i18n loads, which is exactly when the server is unreachable. So it deliberately does not call useObjectTranslation — its own comment says so. Instead it imports the packs directly and reads them as plain object properties:

import { en as enLocale, builtInLocales } from '@object-ui/i18n';
...
strings.loadingSteps.connecting // en.ts console.loadingSteps.connecting
strings.error.connectionFailed // en.ts console.error.connectionFailed

That consumer is invisible to both legs of the #4658 standard:

  • the AST pass only classifies t() / tt() calls, and there is no call;
  • the text safety net greps the full dotted key, and the full dotted key is never spelled — the namespace segment is bound to a local variable, so the source reads strings.loadingSteps.connecting, never console.loadingSteps.connecting.

So the sweep reports the whole family at its strongest tier with no hint anything was missed. Deleting it ships a blank splash screen in ten locales on exactly the server-down boot the screen exists to explain.

Kept and pinned: console.loadingSteps.connecting / .loadingConfig / .preparingWorkspace, console.error.connectionFailed / .checkServer, plus console.initializing, console.loadingHint, console.actions.retry / .retrying.

A fourth leg was added to close this for the batch — a property-chain probe on the tail (.loadingSteps.connecting) rather than the full dotted key. It was validated with a positive and negative control before being trusted: the 5 keys proved live by reading source → detected; console.error.serverUnreachable / .timeout, which genuinely have no pack reader → 0.

Step 1 — re-derived counts, and why a matching number was NOT confirmation

card body2026-08-25 refresh (062943f86)re-derived (813bf8323)
candidates626389389
CONFIRMED384150146
namespaces585555
NEEDS-REVIEW242239243

Candidates and namespaces match the refresh exactly. That is a collision, not a match, and the population moved underneath it — measured with the instrument's own collectEnKeys:

So 389 = 389 sits on top of a 10-key population change with 4 keys migrating CONFIRMED → NEEDS-REVIEW. Reading the equal total as "nothing changed" would have been the wrong reading of a right number.

Step 2 — the batch, and how each key was confirmed

25 keys, deliberately bounded. Each cleared all four legs individually; none was taken from the tool's output.

Superseded twin vocabularies — a grid.* vocabulary won, and the twin lost its reader:

retiredlive winnerread at
rowAction.openMenu / .edit / .deletegrid.openMenu / grid.edit / grid.deleteRowActionMenu.tsx:638,652,663
cellRender.empty / .yes / .no / .systemFieldsgrid.empty / grid.yes / grid.no / grid.systemFieldsObjectGrid.tsx:3670,1725,1726,3726

RowActionMenu.tsx is fully i18n-wired and demonstrably reads the other namespace — the strongest leg-3 evidence available, since the consumer exists and can be shown to ask for something else.

Labels that outlived their control:

Surfaces that left the product:

  • map.* (5 keys) — the strongest form. @object-ui/plugin-map declares no@object-ui/i18n dependency and contains not() call at all, so it cannot consume a locale string. The messages have no hardcoded twin either.
  • home.stats.* (3) and recordDetail.viewersTooltip — name surfaces nothing renders.

Five namespaces held nothing but retired leaves and went with them: map, cellRender, rowAction, recordDetail, home.stats.

Counter-probe — per key, not per total

Identical grep -rFn -I pipeline, locale packs excluded, for candidates and controls.

  • 25 candidates → 0 hit lines each.
  • 28 controls → every one hits at least once, per key: calendar.today 3, .day 2, .week 2, .month 2, .newEvent 3, .moreEvents 4, .allDay 2, grid.empty 2, grid.yes 10, grid.no 10, grid.systemFields 2, grid.openMenu 2, grid.edit 3, grid.delete 3, home.quickActions.manageObjects 2, .title 1, .systemSettings 2, actionDialog.title 5, .description 2, .cancel 2, .confirm 2, layout.systemNav.systemSettings 2, .applications 2, .objectManager 2, chart.nullCategory 13, grid.bulk.confirmDefault 1, .affectedRecords 1, .retry 1.

Reported per key precisely because a total of 76 would be satisfied by one control hitting 76 times while the rest were as invisible as the retired keys.

Separately, the one fully-variable dynamic head in the repo (useObjectLabel.ts:604, t(`${ns}.${suffix}`)) was read rather than assumed: ns comes from getAppNamespaces() (app metadata namespaces only) and every suffix contains globalActions. or objects.…_actions.. It cannot reach any candidate.

Pack parity, proved before the first line came out

The deletion tool resolves every target key in every pack through the TypeScript AST and refuses to write anything unless all resolve:

PRESENCE CHECK PASSED: all 25 keys resolve in all 10 packs (250 resolutions).

Landing sites are AST node ranges, not regex anchors — uniqueness is by construction (a path resolves to exactly one property or the run aborts), and every site was printed as file:line before any write. Result: 10 files changed, 350 deletions(-), 35 per pack, zero insertions in any pack. Uniformity is a property of the method, not something the parity assertion had to catch afterwards.

Reverse verification — both legs, direction predicted first

Restore under trap … EXIT INT TERM; git diff HEAD empty after each.

Leg A — restore one deleted key (calendar.agenda, en only). Anchor uniqueness asserted before writing: ^ calendar: { occurs exactly 1 time. Landing site printed (en.ts:610) and confirmed namespace-aware via the AST — calendar.agenda resolves YES, grid.import.agenda resolves no. Injection proved on disk (0 → 1).

⚠️The predicted direction was wrong, and the real one is more interesting. I predicted CONFIRMED +1. Observed: NEEDS-REVIEW +1, naming calendar.agenda, with CONFIRMED unmoved. The reason is structural — the new pin lists every retired key as a literal string, so a returning key now has a textual footprint and lands in NEEDS-REVIEW rather than CONFIRMED. Same mechanism, measured: committing the pin moved the 5 rescued bootstrap keys from CONFIRMED to NEEDS-REVIEW (121 → 116), while console.error.serverUnreachable / .timeout — the two with genuinely no pack reader — correctly stayed CONFIRMED. The pin does not merely assert; it demotes these in the instrument's own output so a future round will not see them at the top tier.

The rest of Leg A fired as predicted: the pin went red on exactly one assertion, naming en :: calendar.agenda, with the other five green; all-locales-key-parity went red naming calendar.agenda missing from the other nine packs.

Leg B — delete one LIVE key (calendar.today, all ten packs). This is the discriminating leg.

  • check:i18n-keysEXIT=1, 2585/2586 literal keys resolve (was 2586/2586), naming packages/plugin-calendar/src/CalendarView.tsx:291:17 [missing-key] calendar.today.
  • The pin split: the deletion swept around the live siblings RED (expected [ 'en :: calendar.today', …(9) ]), while no pack defines any retired key, no pack defines a namespace root …, keeps the grid.* vocabulary … and keeps the bootstrap strings … all stayed GREEN.

A 1-failed/5-passed split is the proof the classifier separates live from dead; a uniform red would have proved nothing.

Leg B's landing evidence is the tool's per-pack presence assertion plus git diff --numstat (10 packs, 0 insertions, 10 deletions) and the AST check that calendar.today is absent while calendar.day survives.

Why a negative pin was added

packages/i18n/src/__tests__/dead-key-batch-retired-4730.test.ts, following the five-instance convention already in that directory. It is load-bearing: every i18n gate in this repo runs call site → key, so a dead key coming back into the packs is invisible to all of them.check-i18n-dead-keys.mjs is the only reverse-direction reader and is report-only by design. The pin is the sole guard on that direction, and it also pins the live-but-reported-dead bootstrap family so the next round cannot repeat the near-miss.

Gates (exit codes captured before any pipe, verdicts quoted)

All at 09a1f9b3, clean tree.

  • pnpm exec vitest run packages/i18n/EXIT=0Test Files 57 passed (57), Tests 923 passed (923)
  • pnpm --filter @object-ui/i18n type-checkEXIT=0 — script echoed (tsc --noEmit && tsc -p tsconfig.test.json), so not a zero-match no-op. Coverage of the new pin proved with --listFiles: 1 hit under tsconfig.test.json, 0 under the base config (tests are excluded there) — so "type-check is clean" genuinely covers the new file.
  • pnpm --filter @object-ui/i18n lintEXIT=0 — 0 errors, 34 pre-existing warnings; the new file alone: 1 file, 0 errors, 0 warnings. (Plain eslint ., this repo's actual spelling — --no-inline-config manufactures phantom errors and was not used.)
  • pnpm check:i18n-keysEXIT=0 — "Every in-scope call-site key resolves against the en pack (2831 keys)"
  • pnpm check:i18n-driftEXIT=0 — "0 en value(s) changed (0 key(s) added, 25 removed)"
  • pnpm check:control-bytesEXIT=0 — "scanned 5500 tracked text file(s)"
  • pnpm check:published-distEXIT=0 — "Inspected 39 published package(s)"
  • pnpm check:phantom-deps / check:entry-guard / check:esm-specifiers / check:self-import — all EXIT=0
  • node scripts/check-changeset-presence.mjsEXIT=0 — "10 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
  • Consumer suites pnpm exec vitest run packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx packages/plugin-calendar/ packages/plugin-map/ packages/plugin-dashboard/EXIT=0Test Files 114 passed (114), Tests 984 passed (984)
  • pnpm exec vitest run packages/plugin-grid/EXIT=0Test Files 96 passed (96), Tests 884 passed (884)
  • node scripts/check-i18n-dead-keys.mjsEXIT=0 — before 2856 pack keys / 146 CONFIRMED; after 2831 / 116, of which 25 are this deletion and 5 are the pin demoting the rescued bootstrap keys. NEEDS-REVIEW is untouched at 243→248 (+5 from that same demotion), which is the scope fence showing up in the instrument's own output.

⚠️--strict exits 1 both before and after this change. That 1 is the standing backlog, not a regression from this PR.

Scope fence

  • NEEDS-REVIEW tier untouched in every namespace.
  • search (partially live, 7 t() sites) and appDesigner (heavy indirect-reference tier) not touched.
  • scripts/check-i18n-dead-keys.mjs untouched and still report-only.
  • The umbrella keeps its held trims; repo-wide CONFIRMED now stands at 116.

Generated by Claude Code

Each key cleared the objectui#4658 evidence standard individually: zero t()
call sites, zero textual footprint outside the packs, and a read of its
plausible consumer. Five namespaces held nothing but retired leaves and went
with them: map, cellRender, rowAction, recordDetail, home.stats.
Adds a negative retirement pin, following the five-instance convention in
packages/i18n/src/__tests__/. Every i18n gate here runs call site to key, so a
dead key returning to the packs is invisible to all of them.
Seven console.* bootstrap strings the same sweep reported CONFIRMED-dead are
LIVE and were kept: LoadingScreen.tsx reads them as object properties off the
imported pack rather than through t(), which both legs of the sweep miss. The
pin asserts they stay.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.4 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-BnsRfUVF.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)507.87KB115.07KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.41KB34.47KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.62KB54.56KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT — flipped ready, auto-merge on

Reviewer: domain:ui @ objectui execution seat, PM session 8ca04858-ea8e-5b85-9182-de59aa49e00c. Verified against GitHub and the code tree, not against the report's self-description.

Gate reading: all 29 check runs read, and the returned count was compared against total_count: 29 so nothing was truncated out of view. 26 success, 3 skipped (dependabot, Test (coverage), and the un-expanded Test (coverage shard ${{ matrix.shard }}/4) matrix template), 0 failing, 0 still running. Entry qualification is every check green, not the required subset. Ready flipped first, auto-merge second — that order is not reversible.

What I re-measured myself, and where MY probe was the thing that was wrong

claimmy independent checkverdict
LoadingScreen reads the packs as plain properties, no t()strings.loadingSteps.connecting / .loadingConfig / .preparingWorkspace at :56-58, strings.error.connectionFailed / .checkServer at :97,:104
the 5 rescued bootstrap keys are KEPTall five present in en.ts
grid.bulk.selectPlaceholder is retirednamespace-scoped differential: PRESENT on origin/main, ABSENT on the branch

Two of my three first-pass probes were faulty, and both faults are worth recording because they are the same class this PR is about:

  1. grep -c useObjectTranslation on LoadingScreen.tsx returned 1, not the 0 the report implies. The single hit is :21// Do not use useObjectTranslation here — it can suspend on first render and …. My probe counted the prose forbidding the thing as an instance of the thing.
  2. A bare selectPlaceholder grep on en.ts returned 1 after the retirement. That hit is actionDialog.selectPlaceholder at :2722 — a different, live namespace. My grep was namespace-blind, which is precisely the inadmissible probe this dev had already named, rejected, and replaced with an AST check in the report. I reproduced the rejected instrument before reading far enough to notice.

The corrected reading used origin/main as its own positive control in the same query shape. Both claims hold.

Why the headline finding is worth more than the deletion

Five keys this sweep reported CONFIRMED-dead at its strongest tier are live, and the dev pulled them back out. LoadingScreen is bootstrap-critical: it must render before i18n loads, which is exactly the server-down boot it exists to explain. It is invisible to both legs of the #4658 standard — no t() call for the AST pass, and the full dotted key is never spelled because the namespace segment is bound to a local. Deleting them ships a blank splash screen in ten locales, on the one boot where the screen is the only thing the user can see.

The fourth leg added to close this carried its own positive and negative control before being trusted: the 5 keys proved live by reading source were detected; console.error.serverUnreachable / .timeout, which genuinely have no pack reader, returned 0. A leg that demoted everything would have been useless, and this one discriminates.

Two other things I am accepting deliberately

  • A wrong prediction, reported as wrong. Leg A predicted CONFIRMED +1 and observed NEEDS-REVIEW +1. The cause is structural — the new pin lists every retired key as a literal, so a returning key now has a textual footprint. Reporting the miss and finding its mechanism is worth more than a prediction that happened to land.
  • The 389 = 389 collision. Candidates and namespaces matched the 2026-08-25 refresh exactly, on top of a population that had moved by 10 keys across 115 commits with 4 keys migrating tiers. Reading an equal total as "nothing changed" would have been the wrong reading of a right number, and the dev said so instead of banking it.

Also accepted: the batch was capped by evidence rather than by count — table (10), report (9) and form (8) were allowed by the fence and deliberately not taken for want of equally strong evidence. Choosing well over choosing many is the right call on a sweep whose whole risk is a confident wrong deletion.

Fixes #5783 is correct and in-lane: that card is #4730's own sub-issue and its body asked for exactly this folding. It was moved pm:blocked to pm:dispatched before this PR was opened so the label told the truth at the moment of closure. #4730 itself stays open with its held key-level trims, which is why this is Refs.

Follow-up finding filed by the dev and labelled: #6666 (the sweep's fourth blind spot, with the validated remedy).


Generated by Claude Code

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

Labels

Projects

None yet

2 participants

@os-sales@claude