Repository navigation
fix(output): emit [] instead of null for empty --json results - #124
Merged
Merged
Conversation
Nil slices from the db layer JSON-encode as null, which breaks the documented `--json | jq '.[]'` pipelines (jq exits 5 on null). Initialise result slices in collectTasks/ListProjects/ListAreas/ListTags so every list-shaped command emits [] when nothing matches. Also drop someday from the date-filterable views: its predicate requires startDate IS NULL while --on/--from/--to compare startDate, so the combination could never match anything — now rejected with the existing clear error instead of silently returning nothing. Docs updated.
ryanlewis
added a commit
that referenced
this pull request
Aug 8, 2026
ryanlewis
added a commit
that referenced
this pull request
Sep 10, 2026
Closes #211 Things' Someday list holds the deferred things you have not filed under a project. A to-do inside a project stays inside it however it is deferred: the app shows it greyed within the project rather than in the global list. The CLI returned those to-dos, so `things list someday` answered a different question from the one the app answers. The view gains p.uuid IS NULL. Because p resolves through COALESCE(t.project, h.project), a to-do filed under a project heading is excluded by the same clause rather than slipping through as unparented. Project rows have no parent project, so they still list (issue #206). The rule was measured, not assumed. The issue proposed excluding Someday to-dos whose parent project is not itself in Someday, but the database held no Someday projects, so that could not be told apart from the simpler rule. With the user's approval a throwaway project was created in Things, in Someday and holding one Someday to-do, and the app's own list membership read back: the project appeared in Someday, the child did not. The test is the presence of a parent, not the parent's bucket. The probe was deleted afterwards and the app's list is back to its 6 rows. Verified by uuid: the app lists 6, the CLI listed 15 before and 6 after, with no difference in either direction. The nine dropped are the eight named in the issue plus GXHKQn2Y12ChTiGXLdqQvB. --project on someday now errors instead of returning an empty list: the clauses contradict, so it could never match. It names the view and points at `things --project NAME`, the same call issue #124 made for date filters on this view.
ryanlewis
added a commit
that referenced
this pull request
Sep 10, 2026
Closes #211 ## What changed `someday` now returns only rows with no parent project. Things' Someday list is the deferred things you have not filed under a project: a to-do inside a project stays inside that project however it is deferred, and the app shows it greyed within the project rather than in the global list. The CLI returned those to-dos, so `things list someday` answered a different question from the one the app answers. The view gains `p.uuid IS NULL`. Because `p` is resolved through `COALESCE(t.project, h.project)`, a to-do filed under a project *heading* is excluded by the same clause rather than slipping through as unparented. Project rows have no parent project of their own, so they still list, which is what #206 established. ## The rule was measured, not assumed The issue proposed excluding Someday to-dos "whose parent project is not itself in Someday". That is not the rule, and the difference mattered enough to test rather than guess. Two rules fit the existing data equally well, because every one of the nine to-dos the CLI wrongly returned had an Anytime parent: - **A.** exclude a Someday to-do whose parent project is not itself in Someday (the issue's hypothesis) - **B.** exclude a Someday to-do that has any parent project They differ only for a Someday to-do inside a Someday project, and this database contained no Someday projects at all, so the discriminating case did not exist. With the user's approval I created it through the app and measured it. A throwaway project `PROBE-someday-20260910-122504` was created in Things via AppleScript, moved to Someday, given a deadline, and one to-do added inside it via the `things:///add` URL scheme with `when=someday`, so that both project and child sat in Someday with the child properly parented. Reading the app's own list membership: | | in the app's Someday list | |---|---| | the probe project (Someday, has a deadline) | yes | | its child to-do (Someday, parent is that Someday project) | no | The app's Someday list went from 6 rows to 7, gaining only the project. So the test is the presence of a parent, not the parent's bucket: **rule B**. The probe project also appeared in the app's `Later Projects` list, which is where Things files deferred projects, and that list was empty before and after. Both probe items were then deleted through the app. The app's Someday list is back to 6, `Later Projects` back to 0, and the four probe rows sit in the Trash. ## Verified against the real database With rule B implemented, `things list someday` returns 6 rows and the app's Someday list holds 6, with no difference in either direction by uuid. | | count | |---|---| | App's Someday list | 6 | | CLI `list someday` before | 15 | | CLI `list someday` after | 6 | | Rows in one and not the other | 0 | The nine dropped are the eight named in the issue plus `GXHKQn2Y12ChTiGXLdqQvB`, which the issue did not list. All nine are Someday to-dos with an Anytime parent project. ## `--project` on `someday` is now an error Narrowing this view to a project asks for the contents of a project the view has already excluded, so the two clauses contradict and the listing is empty whatever the project holds. On `main` that command listed a project's deferred to-dos, so leaving it silently empty would have been a regression with no explanation attached. It now fails with an error naming the view and pointing at `things --project "Name"`, which does return a project's own deferred to-dos. That follows the precedent this repo already set twice: #124 rejects date filters on this same view for the same reason, and `--include-completed` is rejected everywhere but `today`. Both the flag form and the positional form (`things someday "Name"`) are covered. This is a small CLI surface change rather than something #211 asked for, so it is worth a reviewer's eye. The alternative is a command that returns nothing and says nothing. ## Tests Five tests in `internal/db/tasks_test.go` over a new `seedSomedayParents` helper, which seeds an Anytime parent and a Someday parent so the discriminating case is in the fixture rather than only in the prose: - to-dos inside a project are excluded; the Someday project and the unparented to-dos remain - the discriminating case on its own — a Someday to-do inside a Someday project stays hidden while its parent lists - a to-do reaching its project through a heading is excluded by the same clause - narrowing does not empty the view: `--area` still finds the project row and the unparented to-do, and `--project` can no longer match anything here - the narrowing is someday-only — a parented to-do still lists in `anytime` `TestRunListSomedayRejectsProjectFilter` in `cmd/things/run_test.go` covers the new error in both its flag and positional forms, and asserts bare `someday` and `someday --area` still work. `TestTemplateProjectChildrenExcludedFromOpenViews` drove each view with a template's child and an ordinary project's child, expecting the ordinary one to list. Someday no longer carries either, so that sibling cannot exist there and the case has moved to `TestTemplateChildExcludedFromSomeday`, which asserts both children stay out while a top-level to-do and a Someday project row still list. Coverage of the template guard on this view is kept, not dropped. ## Docs `internal/skill/SKILL.md`, `docs/content/commands.md`, `docs/content/agents.md` and `README.md` describe Someday as the deferred things not filed under a project, say explicitly that the rule holds even when the parent project is itself in Someday, and point at `things --project "Name"` for a project's own deferred to-dos.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two of the v0.5.1 scout findings, both in the db layer:
Empty `--json` output was `null`, not `[]` — nil slices from `collectTasks`/`ListProjects`/`ListAreas`/`ListTags` encode as `null`, so `jq '.[]'` fails (exit 5) on any empty result. The bundled SKILL.md ships pipelines that hit exactly this path on an empty Today. Fixed at the db layer (non-nil initialisation) so `len()` checks in future callers also behave; regression tests at both the db and output layers.
Date filters on `someday` could never match — the view predicate requires `startDate IS NULL` while `--on/--from/--to` compare `startDate`. Removed `someday` from `dateFilterableViews` so it now gets the existing clear rejection error instead of silent empty output. README + SKILL.md corrected (README previously advertised the impossible combination).
Verified against the live Things DB: empty search prints `[]`; `things list someday --on …` errors clearly.