Repository navigation
finding(plugin-kanban): resolveDisplay's isLookup guard is unreachable — the next line returns on the same condition unconditionally #6063
Description
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.
- No membership delta on this face is observable. finding: four more private copies of the reference-bearing field family (kanban, detail x2, resolveActionParams) — each missing
user/treeand each carrying the undeclarablereference#5874's required counter-probe — "auserfield is now treated as reference-bearing here" — cannot be written againstObjectKanban's rendered output, in either direction. finding: four more private copies of the reference-bearing field family (kanban, detail x2, resolveActionParams) — each missinguser/treeand each carrying the undeclarablereference#5874 records that absence in its test docblock rather than writing an assertion that cannot fail. - The first identity pin written for this face passed its own ablation.
ObjectKanban.tsxalso importsbuildExpandFields, which consults the same sharedEXPANDABLE_FIELD_TYPESobject once per schema field on every render, so a barevi.spyOn(EXPANDABLE_FIELD_TYPES, 'has').mock.callspin stayed green with the private copy restored. Measured, not feared. finding: four more private copies of the reference-bearing field family (kanban, detail x2, resolveActionParams) — each missinguser/treeand each carrying the undeclarablereference#5874's pin now filters recorded calls by the calling stack frame, and goes red as designed — but the reason a frame filter was needed at all is that this guard has no behaviour of its own to assert against.
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
- The family convergence itself — finding: four more private copies of the reference-bearing field family (kanban, detail x2, resolveActionParams) — each missing
user/treeand each carrying the undeclarablereference#5874, landed with this face converged and pinned. Converging it stays correct on its own terms: the day the guard is made live, it reads the shared family rather than a stale literal.
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/coreread 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
textcolumn holding an id-shaped string would then still render), the secondreturnis 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
claude commentedon Aug 25, 2026
Concentrated triage batch:
finding→pm:queue+domain:ui, Task, S —resolveDisplay'sisLookupguard 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
yinlianghui commentedon Aug 25, 2026
Claimed + dispatched —
domain:ui@ objectui execution seatPM session
session_01CSoz9uGhaaSgiq3hshtN7L, branchclaude/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'sisLookupguard is provably unreachable: the next line returns on the same condition unconditionally. Computed, branched on, discarded.⛔ 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
pnpm --filter <pkg> exec vitest/cd packages/x && pnpm exec vitesthit a guard (objectui#3378) — one form refuses loudly, another silently runsapps/console's 22 files and reports green. Always run vitest from the repo root; sanity-check the file/test count.tscresolves workspace deps throughdistwhilevitestaliases tosrc/— 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 TERMwith a cwd-independent command and confirmgit diff HEAD --statempty. Derive gates by enumerating each CI job's own step list. Changeset: letcheck-changeset-presencedecide and quote its verdict line; ⛔ nevermajor.Collision bounds
packages/plugin-kanbanis yours alone. Eight sibling agents are live elsewhere — ⛔ do not editpackages/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
yinlianghui commentedon Aug 25, 2026
{ "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
yinlianghui commentedon Aug 25, 2026
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