Skip to content

fix(plugin-kanban): delete resolveDisplay's unreachable isLookup guard, pin the surviving value-shaped rule - #6268

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-6063-kanban-isLookup-dead-guard
Aug 25, 2026
Merged

fix(plugin-kanban): delete resolveDisplay's unreachable isLookup guard, pin the surviving value-shaped rule#6268
yinlianghui merged 2 commits into
mainfrom
claude/issue-6063-kanban-isLookup-dead-guard

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#6063

Which arm — A (the guard is redundant), and why it is not "deletion because it is smaller"

The card named two readings and said the choice is behavioural. It is arm A: the
opaque-id suppression in resolveDisplay is a rule about the value, not about the
field's declared type. Four pieces of evidence, none of them "the code is shorter this way":

  1. The helper's own docblock declares both clauses. It says the helper "skips raw FK
    IDs, and skips lookup-typed fields whose value didn't get expanded". The
    unconditional line implements the first clause. Under reading B that clause would be
    deleted — i.e. B contradicts the stated intent rather than restoring it.
  2. The same predicate is already applied with no type gate a few lines down. The
    incoming description is run through isOpaqueId with no objectDef consulted at all
    (descMissing). Under B the board would suppress an id-shaped description but print
    an id-shaped company — two answers to one question in one function.
  3. objectDef is optional at the read site (objectDef?.fields?.[key]). When the
    object schema is thin or has not resolved, isExpandableFieldType(def) is false by
    construction, so B would suppress nothing exactly on the boards that cannot supply
    the type information B depends on. A gibberish-suppression heuristic that fails open
    when it cannot see field types is not the intent it would be restoring.
  4. History gives B no moment to have been overwritten. Both lines entered the tree in
    the same commit (9986f9114, the commit that added the file). There is no earlier
    state in which lookup-only suppression existed and was later broadened, which is the
    shape B's premise ("the unconditional line is the mistake") would need.

The unreachability argument, re-derived on today's main

Merge base ef2a3bd8d, packages/plugin-kanban/src/ObjectKanban.tsx lines 366-369:

constisLookup=isExpandableFieldType(def);if(isLookup&&isOpaqueId(raw))returnundefined;if(isOpaqueId(raw))returnundefined;returnraw;

The two conditions are genuinely identical, not merely similar:

  • same operand — raw is a const, unassigned between the two lines;
  • same predicate — one function, called twice;
  • the predicate is pure. This is the check that could have made the guard live and did
    not: OPAQUE_ID_RE is /^[A-Za-z0-9_-]{12,32}$/ with no g and no y flag, so
    .test() keeps no lastIndex and the second call cannot disagree with the first. A
    sticky or global regex here would have made the guard load-bearing.
  • isExpandableFieldType reads only fieldDef.type and has no side effect that could
    move the answer.

So whenever line 367 returned, line 368 would have returned too. Nothing else reaches the
guarded arm: resolveDisplay is local to the record mapper and called from exactly two
sites (the org-key loop and the owner-key loop, both inside the legacy semantic heuristic).

⚠️ Which assertions survive a revert — the deletion's green is not the evidence

All of them. Revert the source change, restore the dead guard, and every assertion in
this PR stays green. That is what "unreachable" means, and it is why the passing suite is
not the argument here. The argument is the subsumption above; the tests exist for the
other half — so that the deletion cannot be quietly upgraded into a behaviour change.

What the new pins actually discriminate is A vs B, and that was measured, not asserted:

ablationpredictionmeasured
implement B (delete the unconditional line, keep the guard)cases 1+2 RED, 3+4 GREENTests 2 failed | 2 passed (4) — exactly those two
re-fork the family (replace buildExpandFields with a member-identical private table)3 identity pins RED, $expand counter-probe GREENTests 3 failed | 6 passed (9) — exactly those three

Both ablations restored under trap … EXIT INT TERM with a cwd-independent command, both
mutations proved on disk by grepping the injected and the separately removed text with
the landing site printed and anchor uniqueness asserted, and both restores confirmed
byte-identical afterwards.

What changed

packages/plugin-kanban/src/ObjectKanban.tsx — the dead branch, the now-unused
const def, and the now-unused isExpandableFieldType import are gone; the comment block
that explained the removed read is replaced by one that explains why the surviving
suppression is untyped and names the pin that holds it.

packages/plugin-kanban/src/__tests__/resolveDisplay.opaqueId-6063.test.tsx (new) —
four cases: an id-shaped value on a text column is suppressed; suppressed too when the
object declares no fields at all; suppressed on a lookup column (the outcome the deleted
guard claimed, unchanged); and a positive control so the three absence assertions cannot be
satisfied by a board that renders no description at all.

packages/plugin-kanban/src/__tests__/expandableFamily.identity-5874.test.tsx — this
is the required consequence, not scope creep: #5874's identity pin was attributed to
resolveDisplay's read, and deleting the dead branch deletes that read, so two of its
assertions go red on this diff. They are re-anchored onto the face's live read,
buildExpandFields, and made per-field discriminating (each case types the other field out
of the family, so neither spelling can be supplied by the other). Two things improve in the
process:

  • the frame-filter test no longer claims a separation that no longer exists — it now pins,
    executably, that buildExpandFields is the only reader in this render, which is the
    condition under which the filter is equivalent to no filter (measured: every recorded
    EXPANDABLE_FIELD_TYPES.has call comes through isExpandableFieldType ← buildExpandFields);
  • the behavioural counter-probe finding: four more private copies of the reference-bearing field family (kanban, detail x2, resolveActionParams) — each missing user/tree and each carrying the undeclarable reference #5874 had to record as absent now exists. The
    membership delta is not observable in this face's rendered output, but it is observable
    on the wire: a user-typed field reaches $expand, the same field typed text does not.
    (Measured while writing it: the board fetches twice — once before the object schema
    resolves, with no $expand computable — so both cases wait for the post-schema fetch,
    otherwise the negative case would be green against a query that had not been built yet.)

Verification — all runs at f7a86bc1e (final commit, clean tree)

  • npx vitest run packages/plugin-kanban --maxWorkers=2 (from the repo root, per
    objectui#3378) → Test Files 16 passed (16), Tests 92 passed (92). Count sanity-checked
    against the target: 15 files / 86 tests on the merge base, +1 file and +6 tests here — not
    apps/console's 22.
  • pnpm --filter '@object-ui/plugin-kanban^...' build → exit 0 (the closure built first, so
    the type-check below reads rebuilt dist/*.d.ts rather than stale ones).
  • pnpm --filter @object-ui/plugin-kanban type-check → exit 0, both configs
    (tsc --noEmit && tsc -p tsconfig.test.json, script name echoed — not a zero-match
    silent pass). No errors to classify.
  • node scripts/check-changeset-presence.mjs verdict line, quoted:
    ✅ 3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/6063-kanban-resolvedisplay-dead-guard.md.
    Scored patch — shipped source changed; behaviour did not. Never major.
  • node scripts/check-changeset-no-major.mjs, check-control-bytes.mjs,
    check-vi-mock-specifiers.mjs, pnpm check:phantom-deps, check:self-import,
    check:spec-symbols → all exit 0 (each verdict line read from a redirected file, not
    through a pipe).
  • Lint, narrowed and declared.pnpm lint is turbo run lint = per-package
    eslint ., so for the only package this diff touches the package's own run is the
    gate: npx eslint . in packages/plugin-kanban31 files linted, 0 errors, 107
    warnings
    (all pre-existing no-explicit-any/unused-var style warnings; the 4 added by
    the new tests match the file they sit next to). Invariance: the repo has one root
    eslint.config.js with no project:/projectService, i.e. no type-aware linting, so
    this diff cannot move a verdict on any file it does not touch.
  • scripts/pm/check-half-states.mjs exits 3 in this container — "PREREQUISITE NOT MET …
    no reading at all" (no usable GitHub credential). Environmental, unrelated to this diff,
    and reported as no reading rather than as green.

Left draft deliberately: not marked ready, not enqueued, no auto-merge.


Generated by Claude Code

…d and pin the surviving rule
The guard sat directly above a line that returned on the same condition
unconditionally, over the same `raw` and the same pure predicate, so it was
computed, branched on and discarded.
The card named two readings and this takes the first: the opaque-id suppression
is a rule about the VALUE, not about the field's declared type. Gating it on
`isExpandableFieldType` would make the heuristic fail open on every board whose
object schema is thin — `objectDef` is optional at that read.
Deleting the dead branch also deleted this path's read of core's
EXPANDABLE_FIELD_TYPES, so objectui#5874's identity pin is re-anchored onto the
face's live read (`buildExpandFields`), where the membership delta is also
observable on the wire — the behavioural counter-probe that pin had to record
as missing.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3223.0 KB3266.6 KB
Main entry chunk (gzip)153.8 KB350 KB
Entry fileindex-DyHVOGa8.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)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
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)505.63KB114.68KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)171.74KB47.48KB
fields (index.js)238.40KB59.89KB
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.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.45KB
plugin-designer (index.js)212.33KB42.81KB
plugin-detail (index.js)244.74KB62.20KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)126.39KB30.80KB
plugin-gantt (index.js)164.17KB39.89KB
plugin-grid (index.js)201.14KB54.40KB
plugin-kanban (index.js)52.83KB14.55KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
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)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
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)7.54KB2.63KB
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)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.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.49KB2.14KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 06:24
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit a76b18cAug 25, 2026
28 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-6063-kanban-isLookup-dead-guard branch August 25, 2026 06:36
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(plugin-kanban): resolveDisplay's isLookup guard is unreachable — the next line returns on the same condition unconditionally

2 participants

@yinlianghui@claude