feat: merge checkbox state on pull instead of overwriting it
A tick was lost in both directions: pull wrote the server body as-is, push sent the local body as-is, last writer won. Tick it in the web UI and the first `push.py --update` dropped it; tick it locally and the first pull dropped it. The usual answer is drift tracking and a three-way merge, which this repo rejected on purpose. It is not needed. A tick is monotone — an item only travels `[ ]` -> `[x]` — so unioning the two sides is a set union, not conflict resolution. One rule for one line type replaces the whole mechanism, and the store stays "not a mirror". `map.merge_checkbox_state` is pure and does the work; `from_api` takes the local body as an optional argument; `pull.py` hands it the copy already on disk. Checkbox parsing is imported from `skills/issue` (`checkboxes` / `set_checkbox`), never redefined here — the domain layer is untouched. The same item text more than once is read as a set: one ticked local item ticks every server line with that text. Pairing duplicates up by order is the alternative, and it can still drop a tick — which is the bug being fixed. The price is documented, not hidden: unticking is not monotone, so a box unticked in the web UI comes back on the next pull. Untick locally, then push. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+43
-2
@@ -89,7 +89,9 @@ not one per issue. Filters AND together; `--state` defaults to `open`;
|
||||
`--limit` to 100. Keys and filters are mutually exclusive.
|
||||
|
||||
**A pull overwrites the local body.** It is a fetch, not a merge — unpushed
|
||||
local edits are lost. `--cached` skips issues already on disk.
|
||||
local edits are lost, with one exception: [checkbox
|
||||
state](#checkboxes-are-the-one-exception). `--cached` skips issues already on
|
||||
disk.
|
||||
|
||||
**Closed issues stay out of the store.** In filter mode they are enumerated
|
||||
but not written: `--state all` still shows the whole picture, only `--state
|
||||
@@ -119,6 +121,39 @@ Two traps this handles for you:
|
||||
After a pull, draw the graph with `/tea:issue`'s `issue_tree.py` — offline, no
|
||||
extra requests.
|
||||
|
||||
### Checkboxes are the one exception
|
||||
|
||||
A checkbox is state, not prose, and it is the one thing a pull does **not**
|
||||
overwrite. For a checkbox line whose **text** matches a line in the local copy,
|
||||
`[x]` wins from whichever side has it — tick it in the web UI, tick it locally,
|
||||
tick it in both, the tick survives.
|
||||
|
||||
| part of the body | what a pull does to it |
|
||||
|---|---|
|
||||
| prose, headings, everything not a checkbox | overwritten from the server, whole, as before |
|
||||
| a checkbox whose text is in the local copy | `[x]` from **either** side wins |
|
||||
| a checkbox whose text is not in the local copy | taken from the server as it stands, ticked or not |
|
||||
| any issue the store has never seen | written exactly as the server sent it |
|
||||
|
||||
This is not drift tracking — [Drift](#drift) stands. A tick is **monotone**: an
|
||||
item only travels `[ ]` → `[x]`, so joining the two sides is a set union, not a
|
||||
conflict to resolve. No base version is kept and nothing is compared against
|
||||
one; one rule for one line type replaces the whole mechanism.
|
||||
|
||||
**The price, and it is real: a box unticked in the web UI comes back on the next
|
||||
pull.** Unticking is not monotone, so the union cannot see it. Untick locally,
|
||||
then `push.py --update` — the body goes up whole and the server follows.
|
||||
|
||||
Matching is on the item's text after the domain parser has stripped it and
|
||||
rejoined wrapped lines with single spaces, so rewrapping a long item keeps its
|
||||
tick. Rewording one does not: different text is a different item. The same text
|
||||
twice in a body is read as a set — one ticked local copy ticks every server line
|
||||
with that text.
|
||||
|
||||
The parsing is `/tea:issue`'s (`issue.checkboxes` / `issue.set_checkbox`),
|
||||
imported, never reimplemented here. The rule itself is
|
||||
`map.merge_checkbox_state`: pure, and testable without a Gitea anywhere.
|
||||
|
||||
## Pushing
|
||||
|
||||
```bash
|
||||
@@ -209,7 +244,8 @@ never check out, create, or write anything.
|
||||
| domain | Gitea | note |
|
||||
|---|---|---|
|
||||
| `id` (slug) | — | local only; the tracker never sees it |
|
||||
| title, body | `title`, `body` | verbatim, both directions |
|
||||
| title | `title` | verbatim, both directions |
|
||||
| body | `body` | verbatim up; verbatim down except checkbox state, which is unioned |
|
||||
| `state` | `state` | same vocabulary |
|
||||
| `labels` | `labels[]` | names both ways; ids only on write |
|
||||
| `assignees` | `assignees[]` | logins |
|
||||
@@ -235,6 +271,11 @@ nothing reconciles, nothing warns that a synced issue changed upstream.
|
||||
`synced:` tells you how old your copy is; `remote-updated:` what the server
|
||||
said at that moment. Re-pull when it matters.
|
||||
|
||||
Checkbox state is not an exception to this. The union a pull applies reads only
|
||||
the two bodies in front of it — there is no base version, no history, and no
|
||||
way for it to report that anything diverged. One rule for one line type,
|
||||
precisely so the mechanism this section rules out is not needed.
|
||||
|
||||
## Rich payloads for everything else
|
||||
|
||||
Comments and issues are wrapped by the scripts above. For **other** entities
|
||||
|
||||
Reference in New Issue
Block a user