Skip to content

fix(list): include scheduled projects in today, upcoming and anytime - #205

Merged
ryanlewis merged 1 commit into
mainfrom
fix/issue-201-today-projects
Sep 10, 2026
Merged

ryanlewis merged 1 commit into
mainfrom
fix/issue-201-today-projects

Conversation

@ryanlewis

Copy link
Copy Markdown
Owner

Closes #201

What changed

today, upcoming and anytime now list projects alongside to-dos. Things schedules a project exactly as it schedules a to-do — start, startBucket, startDate and todayIndex all live on the project row — and the app shows the project itself in those three lists. The CLI's view filters were pinned to t.type = 0, so a project put in Today was excluded by construction, not by data.

The three views now select t.type IN (0, 1). Nothing else about them moved: repeating project templates and the to-dos inside them stay out, trashed projects stay out, headings (type 2) stay out, and --include-completed keeps its Things-parity meaning for project rows too. inbox, someday, logbook, trash, deadlines and the bare-filter view stay to-do only.

Telling a project row from a to-do

No new marker was needed. printTasks already appends a dim (project) after the title for type = 1 rows (added for things repeating and things search), and JSON already carried "type". A today listing now reads:

    LHV
15.  [ ]  ★ Runbook audit (project)

One output fix went with it: the today ordering sorts a project's own to-dos straight after the project row, so their project group header restated the title on the line immediately above. That header is now folded into the row. Unrelated project groups still get their header, and the group state still advances so later groups break as before.

Filters

A project has no parent project, so --project X never matches a project row — it narrows to the project's contents, which is what it means. --area still finds it, because a project carries its own area. Both are covered by tests.

Why the ordering still holds

The today ORDER BY keys on COALESCE(p."index", 0), which a project row leaves at 0, so it lands in its area's group next to that area's unparented to-dos and ahead of the to-dos of any project with a non-zero index. Its own todayIndex then places it, the same signal the app orders Today by. Checked against the live database: Things sets todayIndexReferenceDate on scheduled project rows exactly as it does on to-dos, so the DESC leg of the sort has no NULLs to sink.

How tested

make test (race) and make lint (0 issues) both pass. New tests:

  • internal/db: projects appear in all three views with type = 1; templates, template children, trashed projects and headings stay excluded; --project drops project rows while --area keeps them; the today order is deterministic with a project in it; --include-completed carries a completed project.
  • cmd/things: things today end to end — JSON carries the project with "type": 1, plain text marks it (project) and does not mark to-dos.
  • internal/output: a project row followed by its own to-dos prints the title once.

Also verified against the real Things database from the issue: things today now returns X28bim63xxvGvWLncbfd7f "Runbook audit", which it did not before.

Docs

internal/skill/SKILL.md (the "Output and --json" list and the things list block), docs/content/commands.md (Listing) and docs/content/agents.md — including the bulk-reschedule example, which now filters select(.type==0) so a scheduled project is not handed to things edit.

Known gap, not addressed here

someday and logbook remain to-do only, though the app's Someday and Logbook lists both show projects. Out of scope for #201; worth a follow-up issue.

Things schedules a project exactly as it schedules a to-do: start, startBucket, startDate and todayIndex all live on the project row, and the app lists the project itself in Today, Upcoming and Anytime. The CLI's view filters were pinned to t.type = 0, so a project put in Today was invisible to `things today` and an agent driving the CLI had no way to know it was there.

The three scheduling views now select t.type IN (0, 1). Everything else about them is unchanged: repeating project templates and the to-dos inside them stay out, trashed projects stay out, headings stay out, and --include-completed keeps its Things-parity meaning for project rows too. inbox, someday, logbook, trash, deadlines and the bare filter view stay to-do only.

A project row is already marked `(project)` in plain output and already carries `"type": 1` in JSON, so nothing new was needed to tell the two kinds apart. One output fix was: the today ordering sorts a project's own to-dos straight after the project row, and their group header restated the title on the line above, so that header is now folded into the row.

--project can never match a project row, since a project has no parent project; --area still finds it, because a project carries its own area.

Closes #201
@ryanlewis
ryanlewis force-pushed the fix/issue-201-today-projects branch from bd061cd to 16a1345 Compare September 10, 2026 10:05
@ryanlewis
ryanlewis merged commit d4883a6 into main Sep 10, 2026
10 checks passed
@ryanlewis
ryanlewis deleted the fix/issue-201-today-projects branch September 10, 2026 10:08
ryanlewis added a commit that referenced this pull request Sep 10, 2026
Closes #203

## What changed

Docs only. `things projects -j` has reported `taskCount` and `openCount`
since the counts were added to drive the progress icon, but nothing said
what they count, so the only documented way to find a project whose work
had all landed was one `things list` per project. This documents both
fields and adds a jq recipe for the daily reconcile.

- `internal/skill/SKILL.md` — a bullet in "Output and `--json`", two
comment lines on the `things projects` reference line, and a "projects
with no open to-dos" recipe in "Common flows".
- `docs/content/commands.md` — a block in "Listing", after the paragraph
about projects carrying `start` / `startBucket` / `startDate` /
`deadline`.
- `docs/content/agents.md` — a bullet in "Script it with `--json`" and a
line in the recipe block.

No new flags, no new JSON fields, no change to plain-text output, no Go
code touched. The user decided to keep the existing field names rather
than the `openTodoCount` / `completedTodoCount` the issue proposed, and
to leave the `●` progress icon as the human-output answer.

## Why these semantics, and how they were verified

The fields come straight from Things' own columns
`untrashedLeafActionsCount` and `openUntrashedLeafActionsCount`. Rather
than infer the meaning from the column names, I checked them against
real data with `sqlite3 -readonly` on the live database and on a backup
snapshot, recounting each project's children and comparing:

| Database | Projects compared | Match | Diverge |
| --- | --- | --- | --- |
| Live | 82 | 82 | 0 |
| Backup 2026-09-01 | 63 | 63 | 0 |

A deliberately wrong value matched zero rows, confirming the comparison
detects divergence rather than passing vacuously.

What that established, and what the docs now say:

- **To-dos under a heading are included.** A to-do filed under a heading
has `project` NULL and points only at `heading`, so a correct recount
has to union direct children with the children of the project's
headings. Ignoring headings gives the wrong total for exactly the 7
projects here that have them.
- **Heading rows themselves are never counted**, nor are trashed to-dos
or checklist items. The trashed exclusion is real rather than
theoretical: counting trashed to-dos would change the total for 13
projects.

## Three caveats, each documented

Each one can mislead someone acting on the recipe:

- `taskCount - openCount` counts to-dos that are no longer open, which
means **completed or cancelled**. 23 projects here contain at least one
cancelled to-do, so a project whose to-dos were all cancelled matches
the same filter.
- Under `--completed` the `●` icon marks **any** completed project,
including one with no to-dos at all, so the icon and the filter are not
identical there. Verified against a fixture database.
- A repeating to-do makes a project **unreachable** by the filter.
Things counts the hidden template row as an open to-do and a template
never completes. Measured on the backup snapshot: the project holding
two templates reports 68/21, which matches the recount only when the
templates are included; excluding them gives 66/19. All open
contributions came from the template rows themselves, with no open
generated instance. `things list -p` hides that template, so the two
disagree, and that is now called out.

## How it was verified

- `make test` and `make lint` green (lint reports 0 issues; the warning
names a stale sibling worktree unrelated to this change).
- All three jq recipes were run as written.
- Against a fixture database, the filter selects the all-done project
and correctly excludes both the half-done and the empty one.
- `hugo` builds the site and renders the new sections.
- The new text ships in the binary, confirmed via `things skill show`.

Rebased on `main` after #201 (#205) landed. It also appended a bullet to
the same SKILL.md list, so both bullets are kept, that one first.
ryanlewis added a commit that referenced this pull request Sep 10, 2026
Closes #206

## What changed

`someday` and `logbook` now list projects alongside to-dos, the same
rule #205 applied to `today`, `upcoming` and `anytime`. A project is
deferred exactly as a to-do is deferred, and completed exactly as a
to-do is completed, so the app shows the project itself in Someday and
in the Logbook. The CLI's two view filters were pinned to `t.type = 0`,
so those project rows were excluded by construction, not by data.

Both views now select `t.type IN (0, 1)` through the existing
`todoOrProject` constant. Nothing else about them moved: repeating
templates and the to-dos inside them stay out of `someday`, trashed rows
stay out of both, and headings (type 2) stay out. `logbook` keeps its
literal reading of the database — it is still one of the two views that
report trashed-project children and templates as they are stored.

`inbox`, `trash`, `deadlines` and the bare-filter view stay to-do only.

## Telling a project row from a to-do

No new marker was needed, and no output change either. `printTasks`
already appends a dim `(project)` for `type = 1` rows, and the
header-folding fix from #205 is view-agnostic, so a completed project
sitting directly above its own completed to-dos in the Logbook prints
its title once.

## Verified against the real database

Read from a checkpointed copy of the live Things database, compared
against the app's own list membership via AppleScript (`id of to dos of
list "Logbook"` / `list "Someday"`), uuids not titles.

- `logbook`: 36 project rows appear that the previous build did not
return, and all 36 are in the app's Logbook list. No false positives.
Total rows go from 1154 to 1190. The newest is `L16wNJD3D84AEJwr14QWni`
"OQ-11 — Salesforce to AWS threat model", which is also the first row of
the app's Logbook.
- `someday`: no project currently sits in Someday, so this was checked
by inserting one row into a copy of the real database.
`PROBE-someday-project` is absent before the change and present after,
with `(project)` in plain output. The six rows the app does show in
Someday all have `start = 2`, `startDate IS NULL`, `status = 0` and no
parent project, which is the filter minus the type pin.

## How tested

`make test` (race) and `make lint` (0 issues) both pass. New tests, all
confirmed to fail with the filters reverted:

- `internal/db`: a deferred project appears in `someday` and a completed
project in `logbook`, both with `type = model.TypeProject`; a
Someday-start repeating project template, the to-dos inside it, trashed
projects and headings stay out; the Logbook order still keys on
`stopDate` with a project in it; `--project` never matches a project row
while `--area` does.
- `cmd/things`: `things someday` and `things logbook` end to end — JSON
carries the project row, plain text marks it `(project)` and does not
mark to-dos.

No new `internal/output` test. The header-folding behaviour these views
can hit is already covered by `TestPrintTasksNoRepeatedProjectHeader`,
which is written against `Print` rather than a view.

## Docs

`internal/skill/SKILL.md` (the "Output and `--json`" list and the
`things list` block), `docs/content/commands.md` (Listing) and
`docs/content/agents.md` — extending the wording #205 added rather than
adding a parallel paragraph.

Two knock-on corrections in both SKILL.md and commands.md. They said
`trash` and `logbook` are to-do lists, so a project template never shows
in either; `logbook` now carries projects, so that is only true of
`trash`. And "the other views stay to-do only" now names which ones,
since `repeating` has carried project templates since #165.

The comment on the header fold in `internal/output/output.go` named only
the three views from #205 and said a project's to-dos sort straight
after it. It now names all five and says the fold applies where the
view's order puts them there, which is what the code actually tests for.

## Not addressed here

Two Someday/Logbook parity gaps found while verifying, both pre-existing
and unrelated to project rows:

- The `logbook` filter is `t.status = 3`, so cancelled items never
appear, but the app's Logbook shows them. 138 cancelled to-dos and 6
cancelled projects are missing.
- `things someday` returns 8 to-dos that the app's Someday list does not
show. Each is a Someday to-do inside an Anytime project; the app keeps
those inside the project rather than in the global Someday list.

`trash` and `deadlines` are the two views left pinned to `t.type = 0`.
Trashing a project in the app leaves no CLI route to see it, since
`things projects` filters trashed rows and `trash` excludes projects.
And `things deadlines` skips a project with a deadline, though `things
projects` reports `deadline` since #204. Both are behaviour changes
outside this issue and want their own tests.
ryanlewis added a commit that referenced this pull request Sep 10, 2026
Closes #212

## What changed

`trash` now lists projects alongside to-dos, the same rule #205 and #209
applied to the other views. Trashing a project in Things puts the
project row itself in the app's Trash, and `things projects` filters
trashed rows, so a trashed project was visible nowhere in the CLI —
excluded by construction, not by data.

The view now selects `t.type IN (0, 1)` through the existing
`todoOrProject` constant. Nothing else about it moved: headings (type 2)
stay out, and `trash` keeps the literal reading of the database it
shares with `logbook`, so the children of a trashed project and any
repeating template that ends up in the bin still list as they are
stored.

`inbox` and `deadlines` stay to-do only. #213 covers `deadlines`.

## Telling a project row from a to-do

No output change was needed. `printTasks` already appends a dim
`(project)` for project rows, and since #214 the JSON carries `"type":
"project"`.

## Verified against the real database

Read the live database with `sqlite3 -readonly`, and compared against
the app's own list membership via AppleScript (`id of to dos of list
"Trash"`), by uuid rather than title.

| | count |
|---|---|
| App's Trash list | 162 |
| CLI `list trash` before | 148 |
| CLI `list trash` after | 165 |
| Trashed project rows gained | 17 |

All 17 project rows the CLI now returns are in the app's Trash, and
nothing the app shows is missing from the CLI. No false positives: the
database holds no trashed headings and no trashed repeating templates,
so neither could leak in.

The 3 rows the CLI returns that the app does not are unchanged by this
PR. All three are to-dos whose parent project is itself trashed; the app
shows the trashed project as one row and folds its children into it.
They were already returned before this change, under the
`viewsIncludingTrashedProjects` decision from #155, and narrowing that
is a separate question from this issue.

## Tests

Three tests in `internal/db/tasks_test.go` over a new
`seedTrashedProjects` helper: trashed projects of both statuses are
listed and carry `model.TypeProject`; headings and live rows of either
kind stay out; `--area` matches a trashed project through its own area
while `--project` cannot.

`TestTrashAndLogbookKeepTrashedProjectChildren` seeds a trashed project
as the parent of its fixture, so its trash expectation gained the
project row it was already seeding. Its subject, that the child is kept,
is unchanged.

Docs updated in `internal/skill/SKILL.md`, `docs/content/commands.md`
and `docs/content/agents.md`: the sentence naming which views carry
projects, and the repeating-template note that said a project template
never reaches `trash`.
ryanlewis added a commit that referenced this pull request Sep 10, 2026
Closes #213

## What changed

`deadlines` now lists projects alongside to-dos. A project takes a
deadline exactly as a to-do does, `things projects -j` has reported it
since #204, and agents.md advertises this view as the way to sweep what
is due — so an agent following the docs missed every project deadline.
The view was pinned to `t.type = 0`, so those rows were excluded by
construction, not by data.

The view now selects `t.type IN (0, 1)` through the existing
`todoOrProject` constant, the same rule #205, #209 and #216 applied to
the other views. Its `ORDER BY t.deadline ASC` is unchanged, so project
rows fall in among the to-dos by date rather than forming a block of
their own. `--on`, `--from` and `--to` already filter `t.deadline` on
this view and now filter project rows by their own deadline.

`inbox` is now the only named view that stays to-do only, which is a
property of the Inbox rather than a limitation: an inbox item has not
been filed anywhere yet, so it is never a project. A bare `-p`/`-a`/`-t`
filter with no view named is the other exception, and the docs now say
so explicitly — it routes to the internal catch-all view, which is still
pinned to to-dos.

## Verification: negative only, and worth being plain about

There is nothing on this database that exercises the change, and I would
rather say so than imply a check I did not make.

The app has no Deadlines list to compare against, so the fallback is
`things projects -j`, which reports no project deadlines at all. Reading
the live database with `sqlite3 -readonly` shows why. Seven projects
carry a deadline, and every one is already excluded by the `status = 0
AND trashed = 0` clause this PR does not touch:

| why excluded | count |
|---|---|
| completed or cancelled | 4 |
| trashed | 3 |
| would list | 0 |

So `things deadlines` returns the same 13 to-do rows before and after
this change, and the SQL the new filter generates selects those same 13.
What I have verified is that no project is wrongly admitted and none is
wrongly held back for a reason this PR introduces. What I have not
verified is a project deadline appearing in the app and then in the CLI,
because no such project exists here.

The tests below cover what the live data cannot.

## Tests

Six tests in `internal/db/tasks_test.go` over a new `seedDeadlines`
helper. The fixture interleaves a project between two to-dos by deadline
while running `"index"` against that order, so an ordering that fell
back to `"index"` or grouped by type would fail:

- projects with a deadline are listed and carry `model.TypeProject`
- the project sorts between the two to-dos, by deadline
- a project with no deadline, a completed one, a trashed one and a
heading all stay out
- `--on` matches a project row by its own deadline
- `--area` finds a project through its own area; `--project` cannot
match one
- a repeating project template with a deadline, and the to-do inside it,
stay out — the one exclusion path that comes from
`viewsIncludingTemplates` rather than the view filter

## Docs

`internal/skill/SKILL.md`, `docs/content/commands.md`,
`docs/content/agents.md` and `README.md`: the view lists now say every
named view except `inbox` carries projects, and name the bare-filter
exception so an agent knows to name a view when project rows matter. The
README gains the general rule it never stated, so its `deadlines` table
row no longer reads as though that view is the only one carrying
projects. `internal/output/output.go` had a comment enumerating the old
set of views.
ryanlewis added a commit that referenced this pull request Sep 10, 2026
Closes #222

## What changed

The internal catch-all view — the one a bare
`--project`/`--area`/`--tag` filter routes to when no view is named —
was still pinned to `t.type = 0` while every named view except `inbox`
had been widened to `todoOrProject` (#205, #209, #216, #220). It now
uses the same set.

`--project` is unaffected in practice: a project has no parent project
of its own, so `p.uuid` never matches a project row and the filter still
returns a project's contents. The visible change is for `--area` and
`--tag`.

## Why

An agent sweeping an area with `things --area Work -j` got none of that
area's projects, which is exactly the miss #222 describes. Parity with
what Things.app shows in the matching list is the project's stated goal.

## How verified

Measured by uuid against Things.app on 10 Sep 2026. For one area the app
reports four projects; the CLI returned zero project rows before this
change and returns the same four uuids after. `things --project <uuid>
-j` still returns zero project rows.

Tests added in `internal/db/tasks_test.go` cover the widened set, the
`--area`/`--tag`/`--project` split, and the exclusions that must not
widen with it — headings, trashed projects, repeating project templates
and the to-dos inside them. Four existing tests asserted the old
to-do-only behaviour and now assert the project rows: two area filters,
the catch-all grouping order, and the CLI-level default-view test.

`make test` and `make lint` are clean.

## Docs

The paragraph naming the bare-filter exception was rewritten once, in
`internal/skill/SKILL.md`, `docs/content/commands.md`,
`docs/content/agents.md` and the README. It now states the `--project`
case rather than a whole-view exception.
ryanlewis added a commit that referenced this pull request Sep 10, 2026
Closes #217

## What changed

Three changes to the two views the app arranges differently from the
CLI, all measured against it on 10 Sep 2026 by uuid.

**`anytime` no longer lists project rows.** This is a revision of what
#205 and #216 widened, for this view alone. Every active project is
trivially "anytime", so listing them all buries the to-dos; the app uses
the project as the group header above its own to-dos instead. Measured:
the app's Anytime held **none** of the 23 active projects as a row, and
all 107 of their to-dos, which the CLI already matched exactly. `today`
keeps its project rows, and so do `upcoming` and `someday`, where a
project has actually been put somewhere.

**`anytime` is ordered to match that arrangement:** items filed nowhere
lead, then areas in area order, and inside an area its own loose to-dos
before those of its projects. It listed in bare `t."index"` order
before, which scattered each project's to-dos and made the rendered
group header repeat.

**`upcoming` reads by date**, then by the within-day `todayIndex` the
app also keys Today on. Bare `t."index"` order interleaved the dates.

## Getting the keys right took two corrections

Worth recording, because both were wrong in ways that still produced
plausible-looking output:

1. Sorting on the area index alone put the unfiled items **last**.
Things writes its indexes negative, so the COALESCE default of 0 that
stands for "not filed here" outranks every real area.
2. Sorting on the project index alone put each area's project to-dos
**before** its own loose ones, for the same reason. The app does the
opposite.

Each key is therefore a CASE rather than a plain index.

## How verified

By uuid against the app on live data:

| view | rows | order vs the app |
| --- | --- | --- |
| anytime | 105 shared | identical for all 105 |
| upcoming | 30 | identical for all 30, no membership difference either
way |

Plain output now renders the app's shape: unfiled items, then each area,
then each project name once as a group header above its to-dos.

## Deliberately left alone

`today` and the catch-all `--project`/`--area`/`--tag` ordering are
byte-identical to before. An earlier revision of this branch had
factored their ORDER BY into shared constants; I backed that out,
because neither was asked to change and `today` is already at parity.

Two ordering gaps are recorded in the comments rather than guessed at,
both raised by `/code-review`:

- A to-do inside a project that carries **no area** falls to the
COALESCE default and sorts after every area. There is no such project in
this data, so the app's answer could not be measured.
- `upcoming` orders on a nullable `todayIndex`, and SQLite sorts NULL
first. No upcoming row carries a NULL, and the `today` ordering has the
same shape, so changing one and not the other would invent a rule rather
than match one.

One consequence worth a decision, not fixed here: because `upcoming` now
orders by date, a project with to-dos on several days gets its group
header re-printed per day, since the CLI has no day header to group
under the way the app does.

## Docs

The "which views carry projects" paragraph was rewritten once in
`internal/skill/SKILL.md`, `docs/content/commands.md`,
`docs/content/agents.md` and the README: every named view except `inbox`
and `anytime`. Each says why `anytime` is the exception and points at
`things projects` for sweeping projects themselves.

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

Closes #243

## What changed

`printTasks`, `printProjects`, `printAreas` and `printTags` each
declared a local row struct, measured every column with `lipgloss.Width`
and padded with `padCol` before joining with a gap. Four copies of the
same twenty lines, and the copy inside `printTasks` also carried the
terminal-width column dropping. There is now one `table` in
`internal/output/table.go`.

Rows of cells go in, measured and padded lines come out. The column
dropping is a `dropOrder` field naming the columns that may be given up,
in the order they go, rather than a pair of booleans and a hand-written
width sum.

The rule the four copies shared is written down once: every column is
padded to its widest cell except the last one declared, which goes out
as it is. Dropping a column does not move that rule, which is why a
narrow listing still pads its title column. That is the behaviour the
copies had; it is now stated rather than implied.

`printTasks` keeps its group-header logic, which no table can own,
because it has to write a header between two rows. `table.lines()` hands
back one line per row for exactly that, so the header fold from #205 is
untouched.

## One place each for the kind word and the status text

`t.Type == model.TypeProject` was branched on six times across
`output.go` and `agent.go`. It is now `isProject(t)`, and the word that
reaches the output is `kindWord(t)`, which takes "task" and "project"
from model's type codec, the same source the JSON `type` field renders
from.

`kindWord` is deliberately not `TaskType.String()`. A heading, or a code
Things has yet to write, reads as a task on these surfaces, which is
what the CLI has always shown. The doc comment says so.

`statusText` no longer keeps its own switch over the three statuses. It
capitalises `model.Status.String()`, so the detail block, the JSON and
the agent brief cannot drift apart, and the unrecognised code still
renders "Unknown" because the codec already falls back to "unknown".
`checklistLine` takes its "(cancelled)" from the same source.

`statusIcon` stays as it is. `[ ]`, `[~]` and `[x]` are glyphs rather
than the status words, and Markdown's two-state checkbox in a brief is a
third thing again. Folding those together would have meant inventing a
rendering, which this change is not for.

## Proof the output did not change

The golden test went in first, in its own commit, before anything was
refactored.

`TestRenderGolden` renders a fixed fixture through every public entry
point: `Print` for tasks, projects, areas and tags and for a
`*model.Task`, `PrintTaskList` with and without a view label,
`PrintTaskWithChecklist` for a to-do, a project and a repeating item,
`PrintHint`, and `PrintAgentBrief` for a task, an open project, an empty
project, a closed project, a repeating item and a note carrying its own
code fence. Empty listings are cases too. Each case renders at both ends
of the colour profile, so the ANSI is pinned as well as the text, and
the task listings render at three terminal widths so both drop steps are
covered.

**The refactor commit does not touch `testdata/render_golden.txt`.**
That is the claim, and the file's absence from that commit's diff is the
evidence.

The golden values were captured from unmodified rendering code. The one
production change in that first commit is the seam the test needs:
`termWidth` becomes a var, like `nowFn` beside it, so a test can pin a
width instead of depending on how the test binary's stdout happens to be
attached. The body is unchanged. `origin/main` moved to 61542c3 while
this was in review, but #260 does not touch `internal/output`, so the
captured bytes are still the bytes of the base this merges onto.

Against the live database, 226 invocations of the built binary are
byte-identical to `origin/main`, 1.8 MB of output in all: nine views
plain, with `--include-completed`, as JSON, with `--color always` and
with `--no-hints`; `projects`, `areas` and `tags` in the same three
forms; and 43 real items through `show`, `show --agent`, `show --json`
and `show --color always`. Exit codes are compared too. The database was
read only through the binary.

Every line in the golden file ends in `|`, because a padded column emits
trailing spaces an editor or a hook would otherwise eat, and ANSI
escapes are written `\e` so the file stays readable and greppable.

## Review

`/code-review --fix` at high effort found one defect, and it was mine,
in the golden test rather than in the refactor.

`TestRenderGolden` returned early when the document matched and
otherwise reported from inside its per-case loop. A difference the case
splitter does not see, such as trailing whitespace or text before the
first header, both of which it drops, left the loop with nothing to
report and the test passed with `got != want`. Appending two newlines to
the golden file reproduced it. For a test whose whole job is pinning
bytes that is the one outcome it must not have, so it now fails loudly
and names the regeneration command.

The review separately re-derived the extraction rather than trusting the
tests: that `table.fits` reproduces the old arithmetic including the
two-step drop and the absent re-check after the last drop, that the last
declared column is the one left unpadded whether or not it survived,
that `kindWord` and `statusText` are equivalent to the switches they
replace down to the unknown-code arm, and that the golden file is the
same blob in both commits. It also confirmed the golden output is
timezone-stable, checked under `TZ=Pacific/Kiritimati` and `TZ=UTC`, and
that making `termWidth` a var introduces no race.

## Noted, not fixed

`TestListQueryGoldenSQL` in `internal/db` has the same gap the review
found here: it reports only from inside its per-case loop, so a
difference outside a `### view=` block would pass. It is a separate
package and a separate change, so it is left alone.

## Not touched

No command surface moves, so `internal/skill/SKILL.md` and the pages
under `docs/content/` have nothing to say about this. The `(project)`
marker, the group header fold, the project icons and the agent brief's
wording are all documented contracts and all unchanged.
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: things today omits projects that Things.app shows in Today

1 participant