Skip to content

fix(list): fold a closed or trashed project's to-dos into its row - #235

Merged
ryanlewis merged 1 commit into
mainfrom
fix/issue-229-fold-closed-project-children
Sep 10, 2026
Merged

ryanlewis merged 1 commit into
mainfrom
fix/issue-229-fold-closed-project-children

Conversation

@ryanlewis

Copy link
Copy Markdown
Owner

Closes #229

What changed

Things shows a closed project in the Logbook as one row, not a row plus every to-do inside it, and shows a trashed project in Trash the same way. The CLI listed each child separately, so things logbook returned 1328 rows where the app showed 910.

logbook now folds a to-do whose parent project is closed or trashed. trash folds only the trashed-parent case. The folded rows stay reachable by naming the project.

The trash half is narrower than the issue says

The issue asks for "closed or trashed" in both views. Measured, that is wrong for trash:

app's Trash rows by parent count
parent closed, not trashed 23
parent trashed 0
parent open 0

Throwing a to-do away out of a finished project is an ordinary thing to do, and the project is not in Trash to fold it into. Applying "closed or trashed" to trash would have dropped 27 rows rather than the 4 expected.

This leaves viewsIncludingTrashedProjects empty, so the clause it gated is unconditional and the map is gone.

Reaching the folded rows

A closed or trashed project's contents were unreachable before this, which is what made the flagless option workable only alongside a widening. The catch-all view pins t.status = 0, so things --project <closed uuid> returned nothing and things show <uuid> --agent reported "the project has no open to-dos" for a project holding 49.

Naming a closed or trashed project now returns its contents whatever their status. That is what the app answers: to dos of project id returned 78 for a project holding 65 completed, 13 cancelled and 3 trashed children, and 49 for one holding 42 and 7. So the status pin drops and t.trashed = 0 stays — a trashed child is in the Trash on its own account, not part of the project's contents. The widening is scoped to a named project, so a bare --area sweep is still the open set, which has its own test.

A closed project's brief would have listed those rows under "Open to-dos". The heading drops "Open" and each row carries [x], [~] or [ ].

Accepted consequences

A to-do thrown away out of a project that is itself in the Trash is listed nowhere, as agreed and as in the app, which returns nothing for a trashed project's contents.

things logbook --project X and things trash --project X on a closed or trashed project now return an empty list, where the bare things --project X returns its contents. The view genuinely no longer holds those rows, so this is parity rather than a bug, and the same commands on an open project are unchanged. Raising it because the repo has precedent for erroring on a contradictory view-and-filter pair, as someday --project does, and someone may prefer that here. Not changed in this PR.

Also unchanged and pre-existing: the brief's closing section still offers complete --yes and cancel --yes on an already-closed project.

How verified

By uuid against the app on live data:

view CLI before CLI after app difference
logbook 1328 910 910 none either way
trash 169 165 165 none either way

things --project on the two closed test projects returns 78 and 49.

Tests cover the fold in both views, that trash keeps a closed project's trashed child, the contents of a closed project, of a trashed one, and of an open one as the control, and that --area does not widen. Two existing tests asserted the behaviour this reverses and were rewritten to assert the fold plus the route that keeps the rows reachable. make test and make lint are clean.

Docs

internal/skill/SKILL.md, docs/content/commands.md, docs/content/agents.md and the README describe the fold, how to read a closed project's contents, and the one case that is reachable nowhere. The #230 wording about a closed item being in exactly one of the two lists was qualified: that holds while the parent project is open, and a to-do closed inside a project that is later closed is folded into the project row instead.

Things shows a closed project in the Logbook as one row, not as a row plus
every to-do inside it, and shows a trashed project in Trash the same way. The
CLI listed each child separately, so `things logbook` returned 1328 rows where
the app's Logbook showed 910.

Measured against the app on 10 Sep 2026 by uuid. The app's Logbook held no
to-do at all whose parent project was closed or trashed: 328 and 77 such rows
respectively in the CLI. Trash is not the same case — the app's Trash held 23
to-dos whose parent project was closed but not trashed, and none whose parent
was trashed, so throwing a to-do away out of a finished project keeps it a
Trash row on its own account. Only the trashed-parent fold applies there, and
it drops 4 rows.

That leaves viewsIncludingTrashedProjects empty, so the clause it gated is now
unconditional and the map is gone.

The folded rows have to stay reachable, and naming the project is how. A
closed or trashed project's contents were unreachable before this: the
catch-all view pins t.status = 0, so `things --project <closed uuid>` returned
nothing and `things show <uuid> --agent` reported "the project has no open
to-dos". It now returns the contents whatever their status, which is what the
app answers for the same question — `to dos of project id` returned 78 for a
project holding 65 completed, 13 cancelled and 3 trashed children, and 49 for
one holding 42 and 7. So the status pin drops and t.trashed = 0 stays. The
widening is scoped to a named project: a bare --area sweep is still the open
set.

A closed project's brief would have listed those rows under "Open to-dos", so
the heading drops "Open" and each row carries [x], [~] or [ ].

A to-do thrown away out of a project that is itself in the Trash is now listed
nowhere, as in the app: asking Things for a trashed project's contents returns
nothing for a row already in the Trash on its own account.

After the change `things logbook` and `things trash` match the app uuid for
uuid, 910 and 165 rows with no difference in either direction.

Closes #229
@ryanlewis
ryanlewis merged commit b47206a into main Sep 10, 2026
10 checks passed
@ryanlewis
ryanlewis deleted the fix/issue-229-fold-closed-project-children branch September 10, 2026 13:00
ryanlewis added a commit that referenced this pull request Sep 10, 2026
…views (#252)

Closes #249

## What changed

Since #235 the logbook and trash fold a closed project's to-dos into the
project's own row, as the app does. The `--include-completed` variants
of today and anytime folded nothing, so the same to-do was folded in one
view and listed in another.

Both now apply the fold, from a constant shared with the logbook so the
two cannot disagree about what "closed" means.

## This is not a fresh measurement, and the PR should not be read as one

The case does not occur in the data. Nothing was closed today under a
closed or trashed parent, no project was closed today, and there is no
open to-do under a closed project anywhere. So the before and after are
identical and can only show no regression:

| check | before | after |
| --- | --- | --- |
| `today --include-completed` vs the app | 27 / 27 exact | 27 / 27 exact
|
| `anytime --include-completed` vs the app | 114 / 114 exact | 114 / 114
exact |
| `today`, `anytime`, `logbook`, `trash` | 18, 105, 910, 165 | unchanged
|

The rule comes from the #229 measurement instead, which was decisive
where it could be observed: the app's Logbook held **no** to-do at all
whose parent project was closed or trashed, and its Trash none whose
parent was trashed. I did not create the case in the live app to measure
it.

## Scoping

The fold sits **inside** the closed branch of the shared status test, so
it can only ever remove a row `--include-completed` just added. An open
to-do under a closed project is left alone: folding that would be a
larger claim, it would take real work out of Today if wrong, and #249
does not ask it. `TestIncludeCompletedKeepsOpenTodosUnderClosedProject`
pins it across all four view-and-flag combinations.

## Nothing is stranded

The trap on both #230 and #238, so worth stating explicitly. The logbook
already rejected these rows on its own parent clause, so no row moves
into or out of it, and `things --project <closed uuid>` still returns
them — which is exactly what #229 settled when it folded the same rows
away. `TestFoldedJustClosedRowIsReachableByProject` asserts the row is
in none of the three views and comes back from the project filter.

## Follow-up noticed, not fixed

`/code-review` found that the fold is unconditional even when the user
has explicitly named the closed parent. `things --project "Launch v2"
--include-completed` errors and points at `things today --project NAME
--include-completed`, which then returns an empty list, because the fold
drops every closed child and a project row never matches `p.uuid`.
Everywhere else, naming a closed project lifts the fold. Lifting it here
means plumbing the filter into a pure string builder and is a
behavioural call worth measuring, so it wants its own issue. The bare
`things --project <uuid>` form works today.

## Docs

Four passages said such a row is in neither `logbook` nor `today`, which
was aspirational before this change and left `anytime` out. All four now
say none of the three, and the agents page notes the fold applies to
both flag variants, so a day sweep after `things complete <project>
--yes` reports the project rather than the to-dos it closed.

`make test` and `make lint` are clean.
ryanlewis added a commit that referenced this pull request Sep 10, 2026
Closes #253

## What changed

Since #252 the `--include-completed` variants of today and anytime fold
a task closed inside a closed project into the project's row, as logbook
and trash do. The fold was unconditional, so it applied even when that
project had been named: `things today --project "Launch v2"
--include-completed` on a finished project returned nothing at all.

The fold exists so a closed project is one row rather than a row plus
its contents. Naming the project is asking for the contents — the answer
`things --project <uuid>` and `show --agent` already give. It now comes
off in the two views as well.

## How it is plumbed

Through the spec seam #240 created rather than string surgery: `where()`
takes a two-field `whereOpts`, the status test reads it, and
`buildListQuery` fills it at the one call site. With the parent pinned
by the `--project` predicate the lifted clause is a constant — true for
an open project, false for a closed one — so dropping it can only ever
affect the closed case. Naming an open project changes nothing, which
has its own test.

## The golden file is the record

Exactly **6 of the 200** view-and-filter combinations moved:

| view | includeCompleted | filters |
| --- | --- | --- |
| today | true | `project`, `project+area+tag`, `all` |
| anytime | true | `project`, `project+area+tag`, `all` |

Each differs only by the removed `AND NOT (COALESCE(p.status, 0) IN (2,
3))`. No other view, filter shape or flag state moved.

## Not measured against the app, and why

The case does not occur in the data: no closed project has a child that
is open and untrashed, or closed today. Every unfiltered view returns
what it did before, which I checked by building `origin/main` and
comparing rather than assuming. The rule comes from #229 and #235, where
the fold itself was measured against the app, and from the catch-all
view, which has answered this way since #235.

One thing that looked like a regression was not: `anytime
--include-completed` reads 115 where my earlier notes said 114. The base
build gives 115 too — a task was closed during the session — and it
still matches the app exactly, membership and order.

## Two asymmetries left in, recorded not guessed

- **Trashed projects.** Naming one still returns nothing in these two
views, because `untrashedParent` zeroes the result before the fold
matters. The catch-all returns its contents. Fixing it would change what
the *unfiltered* views show — 3 open rows in this data — which is beyond
a change scoped to the `--include-completed` fold. Worth its own issue.
- **`--project` takes a LIKE pattern**, so `--project '%'` lifts the
fold for every project at once. Pre-existing and not specific to this
change: no filter value is escaped anywhere, and the bare `things
--project '%'` already widens the same way.

## Docs

`internal/skill/SKILL.md`, `docs/content/commands.md` and
`docs/content/agents.md` each said such a task is "in none of the
three", which this makes false when the project is named. All three now
say the fold holds for the unfiltered sweeps and comes off when the
project is named, with the concrete command. The skill is scoped to
closed projects specifically, since the trashed case still only works in
the bare form.

`make test` and `make lint` are clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: logbook and trash list children of closed or trashed projects that the app folds into the project row

1 participant