Summary
A dependency whose successor is a summary task can be created through the UI, is persisted to the file, is drawn as an arrow, and is editable in the properties panel — but it never affects scheduling. The constraint is silently a no-op in both directions.
Found while reviewing #88; it is independent of that feature and reproduces on main @ dd0165e (v1.7.9).
Reproduce
- Create a summary task with at least one child.
- Create a leaf task
A that ends after the summary starts.
- Hover
A, drag from its connection handle, and drop on the summary's bar body.
- An arrow is drawn and an FS dependency is created.
- Move
A later in time.
Expected: either the dependency is rejected at creation, or the summary (and its children) are pushed.
Actual: the arrow is drawn and saved, and nothing is ever rescheduled.
Why it happens
The guard is asymmetric — creation is blocked on one side only, and enforcement is missing entirely.
| Site |
Behaviour |
src/components/GanttChart/ChartCanvas.tsx:341 |
Summaries are excluded from taskGeometriesMap, which feeds ConnectionHandles. → a summary can never be a drag source. |
src/components/GanttChart/ChartCanvas.tsx:486 |
onMouseUp={() => handleTaskMouseUp(task.id, "start")} is bound to the <g> wrapper of every bar, summaries included. → a summary can be a drop target. |
src/hooks/useDependencyDrag.ts:98 |
resolveDragTargets partitions candidates purely on checkWouldCreateCycle. No task-type filter, so summaries land in validTargets. |
src/store/slices/dependencySlice.ts |
validateNewDependency checks self-reference, existence, duplicates and cycles. No task-type guard. |
src/utils/graph/dateAdjustment.ts:416 |
propagateDateChanges does if (task.type === "summary") continue; — the constraint is never enforced. |
The reverse chain is dead too: when a child moves, changedTaskIds contains the child id, and getSuccessors(child) never reaches the summary's own successors. So A → Summary → B propagates in neither direction.
Additional gaps
- No test coverage.
grep summary returns nothing in dependencySlice.test.ts, useDependencyDrag.test.ts or DependencyArrows.test.tsx.
- File validation does not check task type.
src/utils/fileOperations/validate.ts validates UUIDs, duplicate ids, type ∈ {FS,SS,FF,SF}, lag, self-reference and dangling refs — a .ownchart file containing summary dependencies in either direction loads clean.
- Indent is unguarded. A leaf that has dependencies and is then indented into becoming a summary keeps them.
Proposed fix — forbid in both directions
Allowing summary dependencies properly would mean defining roll-up scheduling semantics ("push a summary → push all its children by the delta"), which contradicts recalculateSummaryAncestors being the sole writer of summary dates. That is a feature, not a guard.
Prior art: MS Project permits them but restricts and discourages them; Syncfusion blocks predecessors on parent tasks by default; DHTMLX allows them and moves the summary as a unit. None allow one direction while enforcing neither.
validateNewDependency — add a task-type guard returning "Dependencies cannot connect summary tasks — link their child tasks instead".
resolveDragTargets — place summaries in invalidTargets so they render as invalid drop targets during a drag.
ChartCanvas.tsx:486 — skip the onMouseUp binding for summaries (cosmetic, but do both).
validate.ts — add a SUMMARY_DEPENDENCY validation code.
- Guard the indent path: drop dependencies when a task becomes a summary, with a toast.
Migration
Make the file-load check a warning, not a rejection: drop the offending dependencies during deserialize, collect the task names, and show one toast — "2 dependencies involving summary tasks were removed (not supported)."
This is safe: the dropped constraints provably had no scheduling effect, so no dates change. Rejecting an entire file over a dead constraint would be hostile, and the only way a user can have one today is a body-drop onto a summary bar, which most will never have discovered.
Tests to add
- Dragging onto a summary is rejected (drop target renders invalid).
addDependency rejects summary source and summary target.
- Loading a file containing a summary dependency drops it, warns, and leaves all dates unchanged.
- Indenting a task that has dependencies into a summary drops them.
Summary
A dependency whose successor is a summary task can be created through the UI, is persisted to the file, is drawn as an arrow, and is editable in the properties panel — but it never affects scheduling. The constraint is silently a no-op in both directions.
Found while reviewing #88; it is independent of that feature and reproduces on
main@dd0165e(v1.7.9).Reproduce
Athat ends after the summary starts.A, drag from its connection handle, and drop on the summary's bar body.Alater in time.Expected: either the dependency is rejected at creation, or the summary (and its children) are pushed.
Actual: the arrow is drawn and saved, and nothing is ever rescheduled.
Why it happens
The guard is asymmetric — creation is blocked on one side only, and enforcement is missing entirely.
src/components/GanttChart/ChartCanvas.tsx:341taskGeometriesMap, which feedsConnectionHandles. → a summary can never be a drag source.src/components/GanttChart/ChartCanvas.tsx:486onMouseUp={() => handleTaskMouseUp(task.id, "start")}is bound to the<g>wrapper of every bar, summaries included. → a summary can be a drop target.src/hooks/useDependencyDrag.ts:98resolveDragTargetspartitions candidates purely oncheckWouldCreateCycle. No task-type filter, so summaries land invalidTargets.src/store/slices/dependencySlice.tsvalidateNewDependencychecks self-reference, existence, duplicates and cycles. No task-type guard.src/utils/graph/dateAdjustment.ts:416propagateDateChangesdoesif (task.type === "summary") continue;— the constraint is never enforced.The reverse chain is dead too: when a child moves,
changedTaskIdscontains the child id, andgetSuccessors(child)never reaches the summary's own successors. SoA → Summary → Bpropagates in neither direction.Additional gaps
grep summaryreturns nothing independencySlice.test.ts,useDependencyDrag.test.tsorDependencyArrows.test.tsx.src/utils/fileOperations/validate.tsvalidates UUIDs, duplicate ids, type ∈ {FS,SS,FF,SF}, lag, self-reference and dangling refs — a.ownchartfile containing summary dependencies in either direction loads clean.Proposed fix — forbid in both directions
Allowing summary dependencies properly would mean defining roll-up scheduling semantics ("push a summary → push all its children by the delta"), which contradicts
recalculateSummaryAncestorsbeing the sole writer of summary dates. That is a feature, not a guard.Prior art: MS Project permits them but restricts and discourages them; Syncfusion blocks predecessors on parent tasks by default; DHTMLX allows them and moves the summary as a unit. None allow one direction while enforcing neither.
validateNewDependency— add a task-type guard returning"Dependencies cannot connect summary tasks — link their child tasks instead".resolveDragTargets— place summaries ininvalidTargetsso they render as invalid drop targets during a drag.ChartCanvas.tsx:486— skip theonMouseUpbinding for summaries (cosmetic, but do both).validate.ts— add aSUMMARY_DEPENDENCYvalidation code.Migration
Make the file-load check a warning, not a rejection: drop the offending dependencies during deserialize, collect the task names, and show one toast —
"2 dependencies involving summary tasks were removed (not supported)."This is safe: the dropped constraints provably had no scheduling effect, so no dates change. Rejecting an entire file over a dead constraint would be hostile, and the only way a user can have one today is a body-drop onto a summary bar, which most will never have discovered.
Tests to add
addDependencyrejects summary source and summary target.