Skip to content

Phase A: memo-log redesign (add/todo split, <date>:<hash> refs) - #29

Merged
rn404 merged 14 commits into
mainfrom
memo-log-redesign-phase-a
Aug 2, 2026
Merged

Phase A: memo-log redesign (add/todo split, <date>:<hash> refs)#29
rn404 merged 14 commits into
mainfrom
memo-log-redesign-phase-a

Conversation

@rn404

@rn404 rn404 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

Phase A of the memo-log redesign (see docs/memo-log-redesign.md): the CLI
shifts from TODO-management-first to memo-log-first.

  • add becomes memo-only; TODOs move to a new sava todo command (-s
    start-immediately, -t tags)
  • sava start / sava end stay independent top-level commands (todo
    must never gain subcommands — nesting start/end under it made a TODO
    whose content is literally "start"/"end" impossible to create; found and
    reverted during review)
  • add/del now confirm what they changed, including the new item's hash
  • end accepts multiple hashes atomically, deduped
  • sava diff now requires <date>:<hash> for both sides instead of
    resolving a bare hash through index.json's hash→date map (that field,
    plus the lookup/self-heal machinery, is removed — hash uniqueness is only
    ever guaranteed within a single day's log, so a global reverse index was
    unsound)
  • sava del searches the last 30 days by default (clear's retention
    window), accepts <date>:<hash> directly, and --deep to search
    everything; an ambiguous match (same hash, multiple days) errors instead
    of guessing

This branch went through three rounds of high-effort multi-agent code
review; findings and fixes are recorded in docs/reports/ (untracked,
kept local per request) and in the commit messages themselves. A handful of
low-severity reuse/efficiency findings from the third round were
deliberately left as accepted tech debt rather than fixed, to avoid an
unbounded review loop on ever-smaller surface area.

Test plan

  • gofmt -l . clean
  • go vet ./... clean
  • go test ./... — all packages pass
  • Manual smoke test of add/todo/start/end/tag/diff/del
    (including --deep and ambiguous cross-day hash) against a sandbox
    $HOME
  • Live-reproduced and re-verified the fix for the parseRef empty-date
    bug found in review round 3

rn404 added 14 commits July 26, 2026 13:13
Reframe the CLI around a memo-first daily log instead of TODO
management: add becomes memo-only, TODOs move under sava todo,
and unfinished TODOs carry forward automatically on first touch
of the day instead of via an explicit command.
add now creates a memo only; TODO items move to a new todo command
family (todo, todo start, todo end) with end accepting multiple
hashes atomically (#10). add and del now confirm what they changed,
including the new item's hash so it can be used right away (#25).
Nesting start/end under todo made a TODO whose content is literally
"start" or "end" impossible to create, since cobra treats a matching
subcommand name as a command, not positional content. Record this as
a guardrail: add and todo must never gain subcommands. Also add the
Phase A code review report (English + Japanese translation).
Move start/end back to independent top-level commands so a TODO
whose content is literally "start" or "end" can be created again.
While touching Start/End, also confirm only after logfile.Update
succeeds (not before) and collapse duplicate hashes passed to end
so a repeated hash no longer produces a misleading "already
finished" error.
Add/Todo/Tag called view.Added/TagsUpdated only after both
logfile.Update and the follow-up index.Rebuild succeeded, so a
durably persisted item showed no confirmation if the (rebuildable,
cache-only) index rebuild failed afterward. Confirm right after the
real write succeeds instead, independent of the index step.
newAddCommand bound its one flag to a plain local variable while the
sibling newTodoCommand bundled its two into a TodoOptions struct, an
inconsistency between two constructors that do the same kind of job.
Give Add an AddOptions{ Tags []string } too so both follow the same
pattern.
Del could show "Deleted!!" for one item while log.Delete silently
removed every item sharing that hash, since hash generation had no
uniqueness check. Fix both ends: Add retries a fresh hash until it
doesn't collide with an existing item in the log, and Delete now
finds and removes only the first match (returning it, with a
not-found error otherwise, matching Finish/Start/AddTags/RemoveTags)
instead of filtering out every match. This also lets command.Del
call log.Delete directly, removing the now-redundant findItem
helper that duplicated lookup's scan loop.
Add and Todo repeated the same "persist, confirm, rebuild index if
tagged" tail. Extract it into one persistNewItem helper. The
surrounding "only call AddTags if len(tags) > 0" guards were also
removed: AddTags is already a no-op on empty tags, so calling it
unconditionally is equivalent and persistNewItem can decide whether
to rebuild the index by checking the item's own Tags instead.
diff resolving a bare hash via index.json's hash-to-date map assumed
hashes are unique across every day, but the collision retry only
guarantees that within a single day's log. Require the date up front
instead. del also moves from today-only to searching the last 30
days by default (matching clear's retention window), erroring on an
ambiguous match instead of guessing.
Diff now takes two <date>:<hash> refs and resolves each with a
direct logfile.Stat, so it never needs a global hash-to-date lookup.
index.json's Hashes field, command.lookup, and Diff's stale-index
self-heal retry are all removed since lookup was their only caller.

Del gains a <date>:<hash> form too, plus a bare-hash search across
the last 30 days (all days with --deep): zero matches is a not-found
error, one match deletes it, more than one aborts with an ambiguous-
hash error instead of guessing. internal/log.hashExists is exported
as HashExists so Del's search reuses it instead of a fourth
hand-rolled scan loop.
Unify Add/Todo/Tag's persist tail into one persistItem helper driven
by an explicit tagsChanged bool instead of inferring from the item's
tag count, closing the trap where removing an item's last tag would
skip the index rebuild it still needs (new regression test verified
to fail against the naive version). Also: point a stale test comment
at the test that actually exists, note End's atomicity doc comment
doesn't cover concurrent processes, share the duplicated
"break index.Rebuild" test setup, and collapse view.go's
Deleted/FinishedTask/StartedTask into one confirmItem helper.
HashExists and Delete each scanned items for a matching hash
independently; both now call a shared indexOf(items, hash) int
helper. uniqueID also drops the package-level generateID var in
favor of taking the generator as an explicit next func() string
parameter, so tests can force a collision by passing a stub directly
instead of swapping and defer-restoring global state.
Two spots in the design doc predate later decisions and would mislead
a future Phase C implementation: the freeze write-command list still
grouped del with add/todo/tag as "only touches today," which stopped
being true once del gained cross-day search; and carriedFrom was
still specified as a bare hash, reintroducing the cross-day ambiguity
the <date>:<hash> redesign was written to eliminate.
parseRef accepted a ref like ":abc12345" as valid (found=true from
strings.Cut, empty date), so diff/del silently resolved it against
today's log instead of rejecting the malformed input. Verified live
that sava del ":<hash>" previously deleted today's item instead of
erroring, and that the fix rejects it while leaving the item intact.
@rn404
rn404 merged commit e7b8b2b into main Aug 2, 2026
2 checks passed
@rn404
rn404 deleted the memo-log-redesign-phase-a branch August 2, 2026 08:04
@github-actions github-actions Bot mentioned this pull request Aug 17, 2026
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.

1 participant