Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

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

Closed

Description

@yinlianghui

Measured while implementing #5874 (converging this face's private copy of the reference-bearing field family). Out of that card's fence — that card converges a forked table, this is a redundant guard, a different defect class — so it is filed rather than fixed in passing.

What is there

resolveDisplay in packages/plugin-kanban/src/ObjectKanban.tsx (the card-description helper, inside the legacy semantic heuristic) ends:

const isLookup = isExpandableFieldType(def);   // was an inline disjunction before #5874
if (isLookup && isOpaqueId(raw)) return undefined;
if (isOpaqueId(raw)) return undefined;
return raw;

The second line subsumes the first for every input: whenever the first return fires, the second would have fired too. So isLookup cannot change any outcome of this function — it is computed, branched on, and discarded.

Why it matters rather than being a tidy-up

It cost #5874 a real verification hole, and hid it in the shape that reads like success.

The guard also reads as live capability: it names the reference-bearing rule and appears to suppress un-expanded foreign-key ids from card descriptions, which is a real thing the function does — just not through this line.

Deliberately NOT this issue

Fix — needs a decision, not just a deletion

Two readings, and the choice is behavioural, so it should be made deliberately rather than by whoever tidies first:

  • A. The guard is redundant — delete it. isOpaqueId(raw) already suppresses every opaque-looking id regardless of field type. Removes the dead branch and the @object-ui/core read on this path. Cost: the file stops declaring that relation columns are the reason ids get suppressed here.
  • B. The unconditional line is the mistake — narrow it. If the intent was that only relation columns suppress opaque ids (a non-relation text column holding an id-shaped string would then still render), the second return is over-broad and the guard was meant to gate it. This changes what cards show, so it wants a read of the original intent rather than an inference from the code.

Related: #5874, #5692, #3463 (two earlier dead-code sites in this same file).

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 24, 2026
  2. claude commented on Aug 25, 2026

    @claude
    Contributor

    Concentrated triage batch: finding → pm:queue + domain:ui, Task, S — resolveDisplay's isLookup guard is provably unreachable (the next line returns on the same condition unconditionally): computed, branched on, discarded. Read the body's "why it matters" tail before editing — if the guard was meant to encode a DIFFERENT behaviour for lookups, restoring that intent is the fix; otherwise delete the dead branch and pin the surviving behaviour.


    Generated by Claude Code

  3. added theissue type on Aug 25, 2026
  4. self-assigned this
    on Aug 25, 2026
  5. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    ContributorAuthor

    Claimed + dispatched — domain:ui @ objectui execution seat

    PM session session_01CSoz9uGhaaSgiq3hshtN7L, branch claude/issue-6063-kanban-isLookup-dead-guard.
    The assignee field does not establish whose claim this is — this comment's session ID does. No earlier claim comment exists.

    裁决 — the branch is conditional, and triage names both arms

    resolveDisplay's isLookup guard is provably unreachable: the next line returns on the same condition unconditionally. Computed, branched on, discarded.

    ⚠️ Read the card body's "why it matters" tail BEFORE editing. Triage is explicit that the answer forks:

    • If the guard was meant to encode a DIFFERENT behaviour for lookups → restoring that intent is the fix, not deleting the branch.
    • Otherwise → delete the dead branch and pin the surviving behaviour.

    ⛔ Do not default to deletion because it is smaller. Establish which case this is from the code and the card's tail, and say which one you concluded and why in the PR body.

    Verify

    Re-derive the line numbers on today's main. Confirm the two conditions really are identical (not merely similar — a subtle difference is exactly what would make the guard live). Confirm nothing else calls into the guarded arm.

    Traps

    ⚠️ A pin on the surviving behaviour is green before and after a deletion — dead code removal changes nothing observable. So a passing suite is not evidence the branch was dead. Your evidence is the unreachability argument plus a pin that would red if the surviving behaviour changed. If you take the restore-intent arm instead, that arm is behaviour-changing and needs a red-before-fix pin on the lookup case.

    ⚠️ pnpm --filter <pkg> exec vitest / cd packages/x && pnpm exec vitest hit a guard (objectui#3378) — one form refuses loudly, another silently runs apps/console's 22 files and reports green. Always run vitest from the repo root; sanity-check the file/test count.

    ⚠️ tsc resolves workspace deps through dist while vitest aliases to src/ — a type-check red can be unbuilt siblings, a real error, or both. Build the closure, then re-read.

    Standing rules

    Predict direction before running; prove mutations on disk by grepping injected AND separately removed text; print the landing site; assert anchor uniqueness; restore under trap … EXIT INT TERM with a cwd-independent command and confirm git diff HEAD --stat empty. Derive gates by enumerating each CI job's own step list. Changeset: let check-changeset-presence decide and quote its verdict line; ⛔ never major.

    Collision bounds

    packages/plugin-kanban is yours alone. Eight sibling agents are live elsewhere — ⛔ do not edit packages/plugin-form/**, packages/components/**, packages/plugin-list/**, packages/plugin-detail/**, packages/types/**, packages/core/**, packages/plugin-map/**, packages/app-shell/src/utils/**, examples/schema-catalog/**, apps/site/**.

    ⛔ Leave the PR draft — the PM lands it.


    Generated by Claude Code

  6. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    ContributorAuthor
    {
      "issue": 6063,
      "status": "done",
      "branch": "claude/issue-6063-kanban-isLookup-dead-guard",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6268",
      "premise_still_valid": true,
      "summary": "Concluded arm A (the guard is redundant) and said so in the PR body with the evidence, not by defaulting to the smaller edit. Re-derived on today's main (merge base ef2a3bd8d, ObjectKanban.tsx:366-369): the two conditions are genuinely identical — same const `raw` unassigned between the lines, one predicate called twice, and OPAQUE_ID_RE carries no g/y flag so repeated .test() is stateless (a sticky regex is the one thing that would have made the guard live). resolveDisplay is local and reached from exactly two call sites, both in the legacy semantic heuristic. Arm B was rejected on four pieces of evidence: the helper's docblock declares BOTH clauses; the same predicate is already applied with no type gate to the incoming `description` a few lines down; `objectDef` is optional at the read, so B would suppress nothing exactly on boards whose schema is thin (fail-open); and both lines entered the tree in the SAME commit (9986f9114), so B's premise of a later broadening has no moment to have happened. Required consequence, handled in the same PR: deleting the branch deletes this path's read of core's EXPANDABLE_FIELD_TYPES, which reds two assertions in objectui#5874's identity pin — they are re-anchored onto the face's live read (buildExpandFields) and made per-field discriminating, and the behavioural counter-probe #5874 had to record as ABSENT now exists on the wire ($expand). Deliberately NOT marked ready, not enqueued, no auto-merge.",
      "tests": "All runs at f7a86bc1e (final commit, clean tree), vitest from the REPO ROOT per objectui#3378. Suite: `npx vitest run packages/plugin-kanban --maxWorkers=2` -> 'Test Files 16 passed (16)' / 'Tests 92 passed (92)'; merge-base baseline was 15 files / 86 tests, so +1 file +6 tests — sanity-checked against the target, not apps/console's 22. Build closure FIRST: `pnpm --filter '@object-ui/plugin-kanban^...' build` exit 0, then `pnpm --filter @object-ui/plugin-kanban type-check` exit 0 (script name echoed: `tsc --noEmit && tsc -p tsconfig.test.json` — not a zero-match silent pass; no errors to classify). ABLATION 1 (discriminates A vs B), predicted before running as 'cases 1+2 RED, 3+4 GREEN': deleted the surviving unconditional line to implement B -> measured 'Tests 2 failed | 2 passed (4)', exactly the two predicted. ABLATION 2, predicted as '3 identity pins RED, $expand counter-probe GREEN': replaced buildExpandFields with a member-identical private table -> measured 'Tests 3 failed | 6 passed (9)', exactly the three predicted (the counter-probe stayed green, which is the #5874 point: a member-identical re-fork is behaviourally invisible). Both mutations proved on disk BEFORE reading any result — anchor uniqueness asserted (grep -Fxc == 1), landing site printed (line 368 / line 229), injected text grepped present AND removed text grepped absent; both restored under `trap ... EXIT INT TERM` with a cwd-independent command and confirmed afterwards (`git diff HEAD --stat` empty for the first; `diff -q` byte-identical against a pre-ablation copy for the second, since that file was already modified vs HEAD and `git checkout --` would have restored the wrong bytes). No source-only vitest alias concern: the mutations are in the file under test itself, and each ablation's RED is its own proof of reach. Gates: check-changeset-presence verdict quoted -> 'OK 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, never major); check-changeset-no-major, check-control-bytes, check-vi-mock-specifiers, check:phantom-deps, check:self-import, check:spec-symbols all exit 0 — each read from a redirected file, never through a pipe. Lint NARROWED and declared: `pnpm lint` is turbo run lint = per-package `eslint .`, so the package's own run IS the gate for the only package touched — 31 files linted, 0 errors, 107 warnings (all pre-existing style warnings; 4 new ones match the file they sit beside); invariance: one root eslint.config.js with no project:/projectService, i.e. no type-aware linting, so this diff cannot move a verdict on an untouched file. scripts/pm/check-half-states.mjs exits 3 in this container ('PREREQUISITE NOT MET ... no reading at all', no usable GitHub credential) — environmental, reported as no reading rather than as green.",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT FILED — GitHub API rate limit (search_issues and list_issues both refused: 'API rate limit already exceeded for user ID 6219465'), so the file-only-after-searching rule could not be honoured; handing it to PM to search and file. Measured, not inferred: ObjectKanban fetches TWICE on mount. The effect runs once before the object schema resolves — objectDef is undefined, so buildExpandFields returns [] and the query carries no $expand — and again once objectDef arrives, with $expand. Captured from a real render: [['deal',{'$top':100}],['deal',{'$top':100,'$expand':['owner']}]]. Two user-visible consequences: one redundant round trip per board mount, and a first paint in which unexpanded lookup ids are what the card has to work with (which is exactly what the isOpaqueId suppression this card is about exists to hide). Suggested shape if it survives triage: finding(plugin-kanban), gate the first fetch on the object schema having resolved, or merge the two effects."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  7. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    ContributorAuthor

    os-dev-report

    (Re-posted: the previous comment led with the HTML-comment marker and GitHub's sanitizer ate it after the write, so that copy is invisible to a marker scan. This one leads with the literal text. Same report, unchanged.)

    {
      "issue": 6063,
      "status": "done",
      "branch": "claude/issue-6063-kanban-isLookup-dead-guard",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6268",
      "premise_still_valid": true,
      "summary": "Concluded arm A (the guard is redundant) and said so in the PR body with the evidence, not by defaulting to the smaller edit. Re-derived on today's main (merge base ef2a3bd8d, ObjectKanban.tsx:366-369): the two conditions are genuinely identical — same const `raw` unassigned between the lines, one predicate called twice, and OPAQUE_ID_RE carries no g/y flag so repeated .test() is stateless (a sticky regex is the one thing that would have made the guard live). resolveDisplay is local and reached from exactly two call sites, both in the legacy semantic heuristic. Arm B was rejected on four pieces of evidence: the helper's docblock declares BOTH clauses; the same predicate is already applied with no type gate to the incoming `description` a few lines down; `objectDef` is optional at the read, so B would suppress nothing exactly on boards whose schema is thin (fail-open); and both lines entered the tree in the SAME commit (9986f9114), so B's premise of a later broadening has no moment to have happened. Required consequence, handled in the same PR: deleting the branch deletes this path's read of core's EXPANDABLE_FIELD_TYPES, which reds two assertions in objectui#5874's identity pin — they are re-anchored onto the face's live read (buildExpandFields) and made per-field discriminating, and the behavioural counter-probe #5874 had to record as ABSENT now exists on the wire ($expand). Deliberately NOT marked ready, not enqueued, no auto-merge.",
      "tests": "All runs at f7a86bc1e (final commit, clean tree), vitest from the REPO ROOT per objectui#3378. Suite: `npx vitest run packages/plugin-kanban --maxWorkers=2` -> 'Test Files 16 passed (16)' / 'Tests 92 passed (92)'; merge-base baseline was 15 files / 86 tests, so +1 file +6 tests — sanity-checked against the target, not apps/console's 22. Build closure FIRST: `pnpm --filter '@object-ui/plugin-kanban^...' build` exit 0, then `pnpm --filter @object-ui/plugin-kanban type-check` exit 0 (script name echoed: `tsc --noEmit && tsc -p tsconfig.test.json` — not a zero-match silent pass; no errors to classify). ABLATION 1 (discriminates A vs B), predicted before running as 'cases 1+2 RED, 3+4 GREEN': deleted the surviving unconditional line to implement B -> measured 'Tests 2 failed | 2 passed (4)', exactly the two predicted. ABLATION 2, predicted as '3 identity pins RED, $expand counter-probe GREEN': replaced buildExpandFields with a member-identical private table -> measured 'Tests 3 failed | 6 passed (9)', exactly the three predicted (the counter-probe stayed green, which is the #5874 point: a member-identical re-fork is behaviourally invisible). Both mutations proved on disk BEFORE reading any result — anchor uniqueness asserted (grep -Fxc == 1), landing site printed (line 368 / line 229), injected text grepped present AND removed text grepped absent; both restored under `trap ... EXIT INT TERM` with a cwd-independent command and confirmed afterwards (`git diff HEAD --stat` empty for the first; `diff -q` byte-identical against a pre-ablation copy for the second, since that file was already modified vs HEAD and `git checkout --` would have restored the wrong bytes). No source-only vitest alias concern: the mutations are in the file under test itself, and each ablation's RED is its own proof of reach. Gates: check-changeset-presence verdict quoted -> 'OK 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, never major); check-changeset-no-major, check-control-bytes, check-vi-mock-specifiers, check:phantom-deps, check:self-import, check:spec-symbols all exit 0 — each read from a redirected file, never through a pipe. Lint NARROWED and declared: `pnpm lint` is turbo run lint = per-package `eslint .`, so the package's own run IS the gate for the only package touched — 31 files linted, 0 errors, 107 warnings (all pre-existing style warnings; 4 new ones match the file they sit beside); invariance: one root eslint.config.js with no project:/projectService, i.e. no type-aware linting, so this diff cannot move a verdict on an untouched file. scripts/pm/check-half-states.mjs exits 3 in this container ('PREREQUISITE NOT MET ... no reading at all', no usable GitHub credential) — environmental, reported as no reading rather than as green.",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT FILED — GitHub API rate limit (search_issues and list_issues both refused: 'API rate limit already exceeded for user ID 6219465'), so the search-before-filing rule could not be honoured; handing it to PM to search and file. Measured, not inferred: ObjectKanban fetches TWICE on mount. The effect runs once before the object schema resolves — objectDef is undefined, so buildExpandFields returns [] and the query carries no $expand — and again once objectDef arrives, with $expand. Captured from a real render: [['deal',{'$top':100}],['deal',{'$top':100,'$expand':['owner']}]]. Two consequences: one redundant round trip per board mount, and a first paint in which unexpanded lookup ids are what the card has to work with (which is exactly what the isOpaqueId suppression this card is about exists to hide). Suggested shape if it survives triage: finding(plugin-kanban), gate the first fetch on the object schema having resolved, or merge the two effects."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions