Repository navigation
[finding] The gantt link guard's group branch is unreachable: a type:'group' row renders no bar, so the drag can never target it #4209
Description
Measured while implementing objectui#4158 (PR for the link-rejection feedback) — not in that card's scope, which is user feedback for the guard's verdicts, not an audit of the verdicts themselves. Observation-class: nothing a user hits today, and the guard is not wrong — the branch simply cannot fire.
The branch
packages/plugin-gantt/src/GanttView.tsx, the built-in drop-target policy, refuses four kinds of dependency link. One of them:
if (target.type === 'group') return 'group';Why it cannot fire
The policy is consulted from exactly two places, both on the drag-to-link path: captureLinkTarget (a bar registering itself as the hover target) and the release handler. Both require a bar to exist for the target row.
A type: 'group' row renders none. From the row renderer:
if (task.type === 'group') {
// 分组层级 (项目/产品): a pure tree header — the left list still
// shows the caret + label, but the timeline row carries NO bar.
return (
<div … onPointerMove={clearLinkTarget}>
{tooltip}
</div>
);
}So for a group row there is no bar to hover, nothing that can call captureLinkTarget, and the row's own onPointerMove is clearLinkTarget — which actively clears any target. The branch is dead on this path.
Note the neighbouring case does not cover it: row.isSummary (a task with children, or type: 'summary') does render a bar and does register as a drop target — and the policy accepts it, because isSummary is not type === 'group'. So "you cannot link to a summary row" is not what the code does; only the explicit group type is refused, and only that type renders no bar. The type: 'group' check sits before the isSummary branch in the renderer, so a group row can never take the bar path either.
Why it is not simply dead code to delete
Two directions, and the choice is a product question rather than a cleanup:
- The branch is cheap insurance. If a group row ever gains a bar — or a second link-creation path appears that does not go through bar hover — the branch starts firing and is correct.
- But an unreachable branch cannot be tested end-to-end, and [gantt] Illegal dependency link (cycle/self/locked/group) is silently discarded — no toast, no cursor, no reason shown #4158 now attaches a user-visible message to it (
gantt.link.rejected.group, ten packs). That message is deliberately kept: without it the branch would render the raw keygantt.link.rejected.groupon screen if it ever did fire, which is strictly worse than an unused string. The [gantt] Illegal dependency link (cycle/self/locked/group) is silently discarded — no toast, no cursor, no reason shown #4158 PR pins the reachability fact instead, so the test goes red the day group rows gain a bar.
The adjacent question worth asking
The context-menu path (Add predecessor… / Add successor… → dependency picker) creates links without consulting this policy at all — it is a separate call site that does not call canReceiveLink. Whether the picker should offer group rows as candidates, and whether it should be subject to the same four-branch policy as the drag path, is a real question this finding does not answer. That is the direction in which the group branch might actually acquire a reachable meaning.
Options
- A. Leave it. The branch is correct-but-dormant, and [gantt] Illegal dependency link (cycle/self/locked/group) is silently discarded — no toast, no cursor, no reason shown #4158's test now pins the reason. Zero cost, keeps the insurance.
- B. Route the context-menu picker through the same policy. Gives the branch a live meaning and closes the inconsistency between the two link-creation paths. Needs a product call on whether the picker should filter illegal candidates or reject them with the same toast.
- C. Remove the branch and its message. Cleanest by enforce-or-remove, but it deletes the insurance and would have to be re-added the moment either of the above changes.
Recommendation: A for now, B as its own card if the picker/drag inconsistency is judged worth closing — B is the only one that turns the branch from dormant into meaningful, but it is a behaviour change to a second interaction and deserves its own scope rather than riding a feedback card.
Related: objectui#4158 (the feedback card that measured this).
Generated by Claude Code
Activity
claude commentedon Aug 11, 2026
Findings triage (first grade; premise check @
origin/maine1ade8f): HOLD —findingstands.Anchors verified live in
packages/plugin-gantt/src/GanttView.tsx: the refusal branch at:1378(if (target.type === 'group') return 'group';) and the row renderer's group short-circuit at:3989, which returns a bar-less row — so the unreachability argument reads off the source, not off inference. The neighbouring:2983and:4921group checks are separate paths and do not change it.Held rather than closed as dead code, which is the disposition the card argues for and it is the right one: the branch is correct-but-dormant, #4158 now pins the reachability fact so the pin goes red the day group rows gain a bar, and its user-visible message is deliberately retained so the branch cannot render a raw i18n key if it ever fires. Option C (delete it) would trade that insurance for nothing measurable.
The useful half is the adjacent question, and it is what a future round should grade, not this branch: the context-menu picker (
Add predecessor…/Add successor…) creates links without consultingcanReceiveLinkat all. That inconsistency between the two link-creation paths is where thegroupbranch could acquire a live meaning — and it is a behaviour change to a second interaction, so it deserves its own card (the card's option B) rather than riding this one or #4158. Recorded here so the next grader does not re-derive it.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Findings triage (cadence re-verify @
origin/main5e2e9fa): HOLD asfinding. Thegroupvocabulary is unchanged:GanttLinkRejectionstill enumerates'group'(GanttView.tsx:122),GanttTaskTypestill carries'group'(:136), and the docs still say group rows render as pure tree headers with no bar (:159) and synthetic group rows are never draggable (:433) — so the guard'sgroupbranch remains unreachable by drag exactly as filed. Dormant dead-branch class; promote only if a gantt sweep touches the link-guard region.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Findings triage (cadence re-check): HOLD as
finding— premise intact.origin/main@b954120: the refusal branch (GanttView.tsx:1378) and the no-bar group renderer (:3989) are both live; the drag path still cannot reach the branch. The [gantt] Illegal dependency link (cycle/self/locked/group) is silently discarded — no toast, no cursor, no reason shown #4158 reachability pin remains the tripwire.:4921now shows a candidate filterif (c.type === 'group' || c.locked) return false— if that is the dependency-picker path, the picker already FILTERS group rows rather than consultingcanReceiveLink, which narrows option B (the inconsistency may be smaller than filed — filter-vs-policy alignment rather than a missing check). Worth one look at promotion time.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 17, 2026
Concentrate round (state transition, no re-grade):
finding→pm:on-hold, executing the three standing HOLD verdicts (2026-08-11): Option A — the branch is correct-but-dormant, #4158's reachability pin is the mechanical tripwire, and its retained message prevents a raw i18n key if the branch ever fires. Option B (route/align the context-menu picker with the link policy) remains its-own-card material — note the 08-11 re-check already narrowed it: the picker appears to FILTER group rows (GanttView.tsxcandidate filter), so B may be filter-vs-policy alignment rather than a missing check.Restart-when: the #4158 reachability pin goes red (group rows gained a bar, or a second link path appeared). Trigger file:
packages/plugin-gantt/src/GanttView.tsx(row renderer / link policy / dependency-picker regions) — dispatchers touching it read this card first.This comment is from the triage seat Routine — session
session_01Y6RnGGr6eNirCHrU7QmFRd.Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Triage:
domain:ui(lane only —pm:on-holduntouched). Landing:packages/plugin-gantt— the link guard's unreachablegroupbranch.Routed via the maintainer direct-dispatch channel, this session, verbatim: 「然后 批 4–5」. PM session
session_0124Qg8rLvpXnQDwCmpKUmaJ. objectui three-stream split (maintainer 2026-08-21); not a Routine triage fire — the triage seat may re-grade.Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
关闭
not_planned—— 维护者同意:按卡片自己的结论不需要改,树里已有测试守着分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ,objectstack 座位贴 #6015),2026-09-24T11:10Z。objectui 清理第七批(#8587 的挂起卡一次性清点):本席读完了卡面和评论(5 条,全部读到),并在本地 main(62597c5)上重新测量,把建议报给维护者。维护者答:「objectui 第⑦批 同意」。本卡说的是什么
甘特图连线规则里的
if (target.type === 'group') return 'group'永远走不到:分组行不渲染条形,拖拽根本够不到它。卡面自己的推荐是 A:保留这段代码。它是正确但休眠的保险,配套的提示文案也保留着,万一哪天走到这里,不会在界面上显示原始 key。本席实测(本地 main
62597c5)packages/plugin-gantt/src/GanttView.tsx:1526。GanttView.linkrejection.test.tsx:154("group: the row renders no bar, so the drag cannot reach the branch")仍在,注释里引用了 objectui#4209。5316301859的重启条件(这条测试变红)没有发生。为什么关
按卡片自己的结论,这里没有要做的事。如果情况变化,树里的那条测试会直接变红,它才是真正的报警器。
什么时候重开
那条测试变红时重开(分组行有了条形,或者出现了第二条建立连线的路径)。如果有人想让右键菜单里的依赖选择和拖拽规则保持一致(卡面方向 B),另开一张卡。
标签:摘
pm:on-hold;以not_planned关闭。Generated by Claude Code