get-todos finds tasks carried forward by block reference - #16
Merged
Merged
Conversation
A todo block exists once. Carrying it into later journals is done with a ((block-ref)), which is not a copy but the same block in a second place — checking off the reference checks off the original. get-todos found tasks through :block/page alone, so a task carried forward was invisible to every date range: --from 2026-09-14 --to 2026-09-16 returned nothing on a graph where three tasks stood in exactly those journals. The failure was quiet, which is what made it hard to catch: an empty task list looks like an empty week. Resolve the :block/refs relation instead, in one extra query for the whole command rather than one per task (~0.17s against 256 tasks). A task stays one row: page and uuid keep naming the original block, and the days it was carried into are added as references, sorted newest first because Datalog guarantees no order. --refs-limit (default 10) caps that list and reports the rest as references_withheld, the bargain get-backlinks --limit and find-block --limit already make. 10 rather than their 3 because an entry here is a date, not a block of text, and because the measured distribution breaks there: a cap of 3 trims 12 of 58 carried tasks, a cap of 10 trims 4. --no-follow-refs restores the older reading and skips the read rather than paying for it. A reference on a page carrying no journal-day falls out of a range, the same rule origin pages have followed since 0.11.0 — 44 of 248 measured reference occurrences sit on ordinary pages, and letting them through would reopen the silent gap that decision closed. Closes #15
"0 keeps all" reads as though --refs-limit were the only reason an occurrence goes uncounted. It is not: with --from/--to, occurrences outside the range and on pages with no journal-day are withheld too, so --refs-limit 0 leaves a non-zero count and looks like a bug. Found in review against a live graph, where it cost two runs to work out that the withheld occurrences lay outside the queried week rather than above the limit. The docstring on _place_references already explained it; the help text and README did not, and those are what a caller reads. Behaviour unchanged — a task carried since March must not look new just because the query asked about this week. A test now holds the distinction, verified by making --refs-limit 0 zero the counter and watching it fail.
Ten cases at the boundaries of the new block-ref handling, each written after running it against the implementation and each checked by breaking the branch it covers and confirming this test is the one that fails. Two of them cover branches that would have failed quietly rather than loudly: - The marker filter has to reach the reference query. Pass a fixed marker set instead of markers_str and nothing in the output looks wrong — occurrences simply attach to tasks --status was meant to exclude. The test asserts the query string, not the result, because the result cannot show this. - A null reference result must not kill the command. LogseqAPI.call returns resp.json() and screens only dicts carrying an "error" key, so a null body arrives as None and the command dies on TypeError while the todo query itself succeeded. Also pinned: the default cap of 10 at the boundary where an off-by-one would show, refusal of a negative --refs-limit (without the guard, deduped[:limit] silently drops the last entry and withheld overcounts), the order of dated against undated occurrences, the original-name/name fallback, and the plain-text branch that reports a count when no date survives the range. Three further cases were written, measured, and dropped again: their plausible mutations turned out behaviour-equivalent, so the tests would have recorded coverage without holding anything. Fixes the error text for a negative --refs-limit, which still said "0 keeps all" after the help text stopped saying it.
The README's example block showed get-todos with two ordinary tasks, which is the case that never had a problem. Someone who carries tasks forward by ((block-ref)) — the ordinary way to keep a task alive in Logseq — could read the whole section without learning that the command now follows them. Shows the output rather than describing it: [page] and "also on" side by side say by themselves that origin and occurrence are different things and that both are reported.
test_get_todos_markers checked rec.queries[0]. get-todos now builds two
queries, and the second one — the :block/refs read — carries the same markers.
The test passes today because both share one string formatted by edn_string,
so it holds by accident of the implementation rather than by checking what it
claims to check: give the reference query its own formatting and the test
stays green while the contract is gone.
Checks every query the command built instead. Verified by having
_fetch_todo_references format the markers itself, which the assertion now
catches and did not before.
Also names the cost of following refs in the --help notes, where the other
commands that pay for an extra read name theirs (--with-children "one extra
API read per match", --with-context "costs no extra read"). It sat only in a
code comment and in the CHANGELOG, neither of which a caller reads, and the
negative form on --no-follow-refs ("saves one read") is not where someone
deciding whether a call is expensive would look.
examples/weekly-todos.sh read `data.get('tasks', [])`, but get-todos --json
has answered `{"todos": …, "count": …}` since the initial import. The default
swallowed the mismatch: the script printed "Total: 0 open tasks" against any
graph, and the per-page breakdown under it stayed empty. That reads as a quiet
week, not as a broken example — the same shape of silent wrong answer this
tool argues against elsewhere.
Reads `data['todos']` now, so a future rename fails loudly instead of counting
zero. Two tests pin both halves: the example may only read keys the payload
carries, and the payload keeps carrying them.
Predates this branch by every commit — found while checking the block-ref work
for consistency against the rest of the repo.
A task's date used to be the day it was written down, so "how long have I been carrying this?" had no answer in the payload. With occurrences resolved it does: references counts the journals a task was pulled into inside the window, references_withheld the ones before it, and their sum is a duration. carried-over-todos.sh reports that, longest-carried first. It reads --json once and passes the payload to both jq stages rather than querying twice, and splits the PAYLOAD declaration from its assignment so a failed read stops the script instead of printing an empty week — verified by pointing it at a closed port and checking the exit code. Run against a real graph before being committed, which is how two wordings got fixed: a task written on the day it stands now reads "not carried" instead of "carried: 0 day(s)", and the summary counts the same threshold as the detail lines rather than one higher. The example-payload test now scans the examples directory instead of naming one file, so the next example is covered the day it is added.
The list under "Scripting Examples" is a hand-maintained view of a directory, which is the defect this file already guards for the command tables: nothing recomputed it, so a new script is listed only if whoever added it remembered. Adding carried-over-todos.sh was that case — the entry was written by hand and could as easily have been forgotten. Derives the expectation from examples/*.sh instead, in both directions: a script that is not listed fails, and a listed script that no longer exists fails too. Verified by removing an entry and by inventing one. CONTRIBUTING.md names only the directory, not the scripts, so it needs nothing.
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.
Closes #15.
A todo block exists once. Carrying it into later journals is done with a
((block-ref)), which is not a copy but the same block in a second place — checking off the reference checks off the original.get-todosfound tasks through:block/pagealone, so a task carried forward was invisible to every date range:The failure was quiet, which is what made it hard to catch: an empty task list looks like an empty week. The same reading is what Logseq's own
(between …)does, which is why this arrives in the forum rather than in a bug tracker — "the tasks are not in the journal pages and the between query only looks at the journal page dates" (discuss.logseq.com).What changed
:block/refsis a real relation, so no string matching on the((uuid))form. One extra query for the whole command, not one per task — ~0.17s against 256 tasks.A task stays one row.
pageanduuidstill name the original block; the days it was carried into are added asreferences, sorted newest first because Datalog guarantees no order.--refs-limit N(default 10,0keeps all) caps the list; the remainder is reported asreferences_withheld--no-follow-refsrestores the older reading and skips the read rather than paying for itWhy 10 and not 3
get-backlinks --limitdefaults to 3 and the issue proposed inheriting it. Measured first — it does not carry over, because an entry there is a block of text and an entry here is a date. Against the live graph (58 carried tasks, 204 occurrences):--refs-limitThe distribution breaks at 10: above it sit only four long-carried tasks (11, 14, 18, 33 occurrences) — exactly what
references_withheldis for.Breaking
A range query can return more tasks than before, up to 58 more on the measured graph. Nothing was removed;
pageanduuidare unchanged, and a task with no references carries no new field. CHANGELOG entry sits underChanged.The
jd is Nonerule from #4 is untouched and now governs reference pages too: 44 of 248 occurrences sit on ordinary pages and fall out of a range, rather than reopening the gap #4 closed.Verification
--no-follow-refsreproduces the defect exactly, the default returns the three tasksTwo things only the real graph showed: a
pullanswersNonerather than{}for a page with none of the pulled attributes, and plain-text output needed semicolons — a journal page is named2026-09-16, Wednesday, so comma-joining two of them reads as four.Docs
README command table and a new design section ("A task is where it stands, not only where it was written"), AGENTS.md, CHANGELOG, and the
--helptext for--from, which claimed tasks were dated by the page they sit on.