Skip to content

Detect renames, section moves, sharing and removals when updating from questions - #328

Merged
timgent merged 1 commit into
mainfrom
claude/pack-me-up-304-gta4wy
Aug 25, 2026
Merged

Detect renames, section moves, sharing and removals when updating from questions#328
timgent merged 1 commit into
mainfrom
claude/pack-me-up-304-gta4wy

Conversation

@timgent

@timgent timgent commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Closes #304

Updating a list from your questions could only ever add to it. Renaming an item in your questions left the old one behind; removing an option left its items stranded on every list that used it.

computeQuestionSetAdditions is gone, replaced by computeQuestionSetChanges — a four-pass matcher returning add / update / sharing / remove choices — plus applyQuestionSetChanges, which applies a selection in one pass so a mixed selection lands atomically rather than each kind starting from its own snapshot.

The passes run most-certain-first, so the unambiguous answers are settled before anything is guessed:

  1. Same name, same person — already there; nothing to do unless its section or suggested amount moved.
  2. Same name, different owner — the item crossed the communal boundary.
  3. Same slot in the questions, different name — one item left in an answer on each side is a rename, not a coincidence.
  4. Leftovers — a regenerated item nobody claimed is new; a list item nobody claimed has gone from the questions.

The two product decisions

A hand edit wins. This is the one the issue flagged as needing a deliberate answer. A generated item the user renames or re-quantifies on the list now carries textEdited / quantityEdited, and the question set is no longer allowed to change that field back.

It needs a stored flag because the two cases are otherwise indistinguishable: "I renamed it in my questions" and "I renamed it on the list" both end as these two strings differ. Without a record of which side moved, pass 3 would pair them and offer to undo the user's own edit — every time they opened the dialog. Quantities are worse: anyone who had ever nudged an amount by hand would be nagged to revert it on every run.

The item is protected per-field, not wholesale — a renamed item still follows its section into a new one, and an item with a hand-set amount can still be renamed. It is still offered for removal once it leaves the questions: it's the user's name, not their exemption from the list changing. (Items edited before this PR carry no flag, so a first run may offer to revert them; it's opt-in per checkbox.)

The button stays visible. #281 hid it when there was nothing to do; this keeps it and keeps the reassuring "This list already matches your questions". An affordance that vanishes is one the user can't ask a question of, and "did my questions reach this list?" is exactly the question they have. (A count badge would be nicer still, but it means computing the diff eagerly on every list open — deliberately not done here, see below.)

Two smaller calls that follow from the same instinct:

  • Removals arrive unticked. Everything else is pre-selected. An item leaving your questions isn't the same as you being finished with it on this trip.
  • Removals leave no tombstone. A tombstone means "not this, on this trip" and blocks the item for good; these come off because the questions changed, so putting the option back brings the item back with it.

The concerns from the review

  • computeQuestionSetAdditions — deleted, tests ported. Its old assertions now read through an additions(changes) helper, so the add path is still pinned by exactly the cases it was.
  • crypto.randomUUID() — replaced with generateUUID() here and in generatePackingListItems, which is the generator this path calls. That wrapper exists for the older WebViews the Capacitor builds run in.
  • Recomputing on every mutation — not introduced. The diff is computed on demand when the button is pressed, so ticking a checkbox never re-reads the question set or re-runs the matcher.
  • Absent question setcomputeQuestionSetChanges takes null | undefined and returns []; the page catches a missing set explicitly, says so and takes the button away, instead of letting questionSet.people.map throw into a .catch.

One thing the four-pass design could have done and deliberately doesn't: guess that a differently-named regenerated item is really one the user deleted. Deletion is matched by name and owner and nothing else. A wrongly withheld addition is invisible to the user; an addition they didn't want is one untick away.

Sections get the same treatment in reverse: an item with no category of its own is never reported as "moved". Every item on a list made before sections existed is like that, and filling one in isn't a move worth approving — it would have turned the dialog into a wall of no-op checkboxes for exactly the oldest lists.

New persisted fields

textEdited and quantityEdited on PackingListItem, both optional and additive. Wired through PouchDB (omit-list, so automatic), the pod's RDF serialisation, and fullyPopulatedFixtures — the round-trip guards from #260 cover them, and a test asserts a hand-edited name and amount survive the local database and still suppress the revert.

Testing

  • 44 unit tests in updateFromQuestions.test.ts: all four change kinds, the hand-edit decision from both directions, ambiguous groups (two renames in one answer → add + remove, never a guess), and the existing guards — custom items, unanswered questions, deleted items, deleted people, deleted questions.
  • E2E C10 / C11 / C12 drive update, remove and sharing through a real browser. Those are the three paths Person profile photos, past-trip folding, navigation & wizard improvements #281 changed ~200 lines of and never once ran by hand; C11 also asserts removals arrive unticked with the confirm button disabled, and C10/C12 assert the list matches afterwards, so a rename is proven to have replaced rather than duplicated.
  • Full suite: 2037 unit tests and 41 E2E (suites B and C) green; no new lint warnings.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H6NK4soPoQHS2EfvchAf3W


Generated by Claude Code

…m questions

Updating a list from your questions could only ever add to it. Renaming an
item in your questions left the old one behind; removing an option left its
items stranded on every list that used it.

`computeQuestionSetAdditions` is replaced by `computeQuestionSetChanges`, a
four-pass matcher — exact identity, then the communal boundary, then a rename
within one answer, then leftovers — returning add / update / sharing / remove
choices. `applyQuestionSetChanges` applies a selection in one pass, so a mixed
selection lands atomically.

Two product decisions the issue asked for:

* **A hand edit wins.** A generated item the user renames or re-quantifies on
  the list now carries `textEdited` / `quantityEdited`, and the question set is
  no longer allowed to change that field back. The two sides are otherwise
  indistinguishable — a rename in the questions and a rename on the list both
  end as "these two strings differ" — so without a record of which side moved,
  every update would offer to undo the user's own edits. The rest of the item
  still follows the questions: a renamed item can still change section.
* **The button stays.** It does not hide itself when there is nothing to do;
  clicking still answers "This list already matches your questions". An
  affordance that vanishes is one the user cannot ask a question of.

Also: removals arrive unticked (nothing leaves the list unasked) and leave no
tombstone, so putting the option back brings the item back; the diff is still
computed on demand, never on the packed-state path; an absent question set is
handled explicitly rather than thrown and swallowed; and `generateUUID()`
replaces `crypto.randomUUID()` on this path for the older WebViews the
Capacitor builds run in.

An item with no section of its own is never reported as "moved": every item on
a list made before sections existed is like that, and filling one in is not a
move worth approving.

Unit tests cover all four change kinds and the guards (custom items,
unanswered questions, deleted items, ambiguous groups). E2E C10/C11/C12 drive
update, remove and sharing through a real browser — the three paths #281 never
exercised by hand.

Closes #304

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6NK4soPoQHS2EfvchAf3W
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-packing-app Ready Ready Preview Aug 25, 2026 6:48am

@timgent
timgent merged commit 1a7c236 into main Aug 25, 2026
4 checks passed
@timgent
timgent deleted the claude/pack-me-up-304-gta4wy branch August 25, 2026 06:53
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.

Update-from-questions: detect renames, section moves, quantity changes and removals, not just additions

2 participants