Project group placement, sort order, column guards, note append (#2, #9, #7, #4) #11

Merged
ethan merged 1 commit from feat/write-side-gaps into main 2026-08-09 20:38:46 -05:00
Member

Closes #2, closes #9, closes #7. Completes #4.

Stacked on #10 — targets feat/wire-timestamps-and-project-groups because the group guard needs list_project_groups() from that PR. Merge #10 first and this retargets to main cleanly.

The finding that shapes this PR

v2 does not validate id references on write. It accepts a nonsense id, returns success, and produces a silently misplaced item. Confirmed live 2026-08-09:

POST batch/project  {"add":[{"name":"…","groupId":"ffffffffffffffffffffffff"}]}
→ {"id2etag": {...}, "id2error": {}}          # success
→ project created, carrying the non-existent group

That silent fallback is precisely the problem these issues exist to remove, so both new id parameters are validated before the write and the request is never sent.

#2 — group_id on create/update project

create_project(group_id=…) places a project directly into a sidebar folder. Unknown id → APIError, nothing created (asserted live).

#9 — sort_order on create/update project

Worth correcting the issue's assumption: sidebar sort_order values are positive integers, not the "large negative" the issue expected — that's task sortOrder. Verified against the real sidebar, in visible top-to-bottom order:

0, 393216, 655360, 1572864, 1718499, 1835008, …     # lower sorts higher, gaps ~65536

Documented on the tool so a caller can read two neighbours' values and pick one between them.

#4 — already shipped, now completed

update_note has accepted column_id since v0.2.0 at all three layers, and the deployed gateway exposes it — the caveat the issue quoted was stale. The one genuinely unbuilt item on its acceptance list was rejecting a column from a different project, which v2 also accepts-and-ignores.

_require_column now guards update_task and update_note, validating against the destination project so moving project and column in a single call still works.

#7 — append / prepend on update_note

The motivation is narrower than convenience, and it's the near-miss the issue describes:

content replaces the whole body, so an additive edit otherwise means read-modify-write. A body read through any size-capped listing may be shortened — writing it back destroys the rest of the note. append/prepend read the body from the full-state payload update_note already fetches, so the caller never sends existing content and that class of bug becomes unreachable.

  • Combining with content returns an input error rather than guessing an order.
  • No separator is inserted; callers own their newlines.
  • An append bumps modified_time like any other write (the interaction with #1 the issue asked us to document).

Note for reviewers

Two existing column tests now seed a matching column in the fake transport. That's not test massage — setting a column that doesn't exist in the target project is exactly the behaviour this PR deliberately rejects.

Testing

165 passed. Live tests cover group placement, the bogus-group rejection creating nothing, append/prepend composition, and the foreign-column guard leaving the note untouched. All 5 live tests pass against a real account.

Contract in DESIGN.md §12.

🤖 Generated with Claude Code

https://claude.ai/code/session_012VU4wBhFAAosZLQcWqvHsf

Closes #2, closes #9, closes #7. Completes #4. > **Stacked on #10** — targets `feat/wire-timestamps-and-project-groups` because the group guard needs `list_project_groups()` from that PR. Merge #10 first and this retargets to `main` cleanly. ## The finding that shapes this PR **v2 does not validate id references on write.** It accepts a nonsense id, returns success, and produces a silently misplaced item. Confirmed live 2026-08-09: ``` POST batch/project {"add":[{"name":"…","groupId":"ffffffffffffffffffffffff"}]} → {"id2etag": {...}, "id2error": {}} # success → project created, carrying the non-existent group ``` That silent fallback is precisely the problem these issues exist to remove, so both new id parameters are validated **before** the write and the request is never sent. ## #2 — `group_id` on create/update project `create_project(group_id=…)` places a project directly into a sidebar folder. Unknown id → `APIError`, nothing created (asserted live). ## #9 — `sort_order` on create/update project Worth correcting the issue's assumption: sidebar `sort_order` values are **positive** integers, not the "large negative" the issue expected — that's task `sortOrder`. Verified against the real sidebar, in visible top-to-bottom order: ``` 0, 393216, 655360, 1572864, 1718499, 1835008, … # lower sorts higher, gaps ~65536 ``` Documented on the tool so a caller can read two neighbours' values and pick one between them. ## #4 — already shipped, now completed `update_note` has accepted `column_id` since v0.2.0 at all three layers, and the deployed gateway exposes it — the caveat the issue quoted was stale. The one genuinely unbuilt item on its acceptance list was rejecting a column from a *different* project, which v2 also accepts-and-ignores. `_require_column` now guards `update_task` and `update_note`, validating against the **destination** project so moving project and column in a single call still works. ## #7 — `append` / `prepend` on `update_note` The motivation is narrower than convenience, and it's the near-miss the issue describes: `content` replaces the whole body, so an additive edit otherwise means read-modify-write. A body read through any size-capped listing may be shortened — writing it back destroys the rest of the note. `append`/`prepend` read the body from the full-state payload `update_note` **already fetches**, so the caller never sends existing content and that class of bug becomes unreachable. - Combining with `content` returns an `input` error rather than guessing an order. - No separator is inserted; callers own their newlines. - An append bumps `modified_time` like any other write (the interaction with #1 the issue asked us to document). ## Note for reviewers Two existing column tests now seed a matching column in the fake transport. That's not test massage — setting a column that doesn't exist in the target project is exactly the behaviour this PR deliberately rejects. ## Testing `165 passed`. Live tests cover group placement, the bogus-group rejection creating nothing, `append`/`prepend` composition, and the foreign-column guard leaving the note untouched. All 5 live tests pass against a real account. Contract in DESIGN.md §12. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012VU4wBhFAAosZLQcWqvHsf
Closes #2, closes #9, closes #7. Completes #4.

#2 create_project/update_project take group_id + sort_order.

Live probing turned up the reason the acceptance list asked for loud failure:
v2 does NOT validate id references on write. POSTing a project with
groupId "ffffffffffffffffffffffff" returns an empty id2error and creates a
project carrying that non-existent group. Reproducing that silent fallback is
the exact problem #2 exists to fix, so _require_group validates first and the
write is never sent.

#9 sort_order on create_project/update_project. The issue guessed the values
were large negative integers; they are POSITIVE and lower sorts higher, spaced
~65536 apart — verified against the real sidebar. Documented on the tools so a
caller can slot a project between two neighbours.

#4 was already shipped end to end: update_note has accepted column_id since
v0.2.0, at all three layers, and the deployed gateway exposes it. The only
unbuilt item on its acceptance list was rejecting a column from another
project, which v2 also accepts-and-ignores. _require_column now guards
update_task and update_note, validating against the DESTINATION project so
moving project and column in one call still works.

#7 append/prepend on update_note.

The motivation is narrower than convenience. content replaces the whole body,
so an additive edit otherwise means read-modify-write — and a body read through
any size-capped listing may be shortened, so writing it back destroys the rest
of the note. append/prepend read the body from the full-state payload
update_note already fetches, making that class of bug unreachable. Combining
them with content is rejected rather than resolved by guessing an order.
No separator is inserted; callers own their newlines.

Two existing column tests now seed a matching column, since setting a column
that does not exist is the behaviour this change deliberately rejects.

165 unit tests, plus live tests covering group placement, the bogus-group
rejection, append/prepend and the foreign-column guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VU4wBhFAAosZLQcWqvHsf
mcp-gateway changed target branch from feat/wire-timestamps-and-project-groups to main 2026-08-09 20:29:40 -05:00
ethan merged commit f6ec15049e into main 2026-08-09 20:38:46 -05:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
halfst/ticktick-mcp!11
No description provided.