Detect renames, section moves, sharing and removals when updating from questions - #328
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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 #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.
computeQuestionSetAdditionsis gone, replaced bycomputeQuestionSetChanges— a four-pass matcher returningadd/update/sharing/removechoices — plusapplyQuestionSetChanges, 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:
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:
The concerns from the review
computeQuestionSetAdditions— deleted, tests ported. Its old assertions now read through anadditions(changes)helper, so the add path is still pinned by exactly the cases it was.crypto.randomUUID()— replaced withgenerateUUID()here and ingeneratePackingListItems, which is the generator this path calls. That wrapper exists for the older WebViews the Capacitor builds run in.computeQuestionSetChangestakesnull | undefinedand returns[]; the page catches a missing set explicitly, says so and takes the button away, instead of lettingquestionSet.people.mapthrow 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
categoryof 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
textEditedandquantityEditedonPackingListItem, both optional and additive. Wired through PouchDB (omit-list, so automatic), the pod's RDF serialisation, andfullyPopulatedFixtures— 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
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.update,removeandsharingthrough 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.🤖 Generated with Claude Code
https://claude.ai/code/session_01H6NK4soPoQHS2EfvchAf3W
Generated by Claude Code