Skip to content

fix(CS-11713): fully replace array attributes in store.patch#5328

Open
lucaslyl wants to merge 1 commit into
mainfrom
CS-11713/containsmany-cant-shrink-via-storepatch
Open

fix(CS-11713): fully replace array attributes in store.patch#5328
lucaslyl wants to merge 1 commit into
mainfrom
CS-11713/containsmany-cant-shrink-via-storepatch

Conversation

@lucaslyl

Copy link
Copy Markdown
Contributor

linear: https://github.com/cardstack/boxel/pulls

Motivation: I have created a travel planner app tjat lets the AI assistant plan and able tomutate a "containsMany" of itinerary stops. Asking it to reduce the stops got stuck — the assistant replies that the stops have been reduced (e.g. "Day 2 trimmed to a relaxed 4-stop flow"), but the containsMany field is never actually patched (or only the first day shrank while later days kept stale stops). After switching the merge logic to mergeWith, the same prompts now mutate the stops correctly.


Root cause: StoreService.patch merged patch.attributes with lodash merge, which merges arrays by index and never shortens the destination array. A containsMany (or any array attribute) could grow or change in place via a patch but never shrink — a shorter array left the old trailing items behind. Because stops is one flat array (each carries a day), reducing every day reliably trimmed the low indices (Day 1) but left the high-index tail (Day 2+) untouched.

This only bit patch-based writes: the AI patchCardInstance tool and any PatchCardInstanceCommand caller. A component-side model.field = [...] assignment was never affected, because it replaces the whole field value.

Fix: merge attributes with mergeWith and a customizer that returns the source array, so a patched array fully replaces the existing one — matching PatchCardInstanceCommand's documented contract ("attributes specified will be fully replaced; display the full array in the patch code"). Non-array values still deep-merge (nested patches like cardInfo: { name } keep their siblings); the relationships and meta merge paths are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Before:
Screenshot 2026-06-24 at 8 08 54 PM

After:
Screenshot 2026-06-24 at 8 10 52 PM

Motivation: a card app lets the AI assistant plan and mutate a
containsMany of itinerary stops. Asking it to reduce the stops got stuck —
the assistant replies that the stops have been reduced (e.g. "Day 2 trimmed
to a relaxed 4-stop flow"), but the containsMany field is never actually
patched (or only the first day shrank while later days kept stale stops).
After switching the merge logic to mergeWith, the same prompts now mutate
the stops correctly.

Root cause: StoreService.patch merged patch.attributes with lodash `merge`,
which merges arrays by index and never shortens the destination array. A
containsMany (or any array attribute) could grow or change in place via a
patch but never shrink — a shorter array left the old trailing items
behind. Because stops is one flat array (each carries a `day`), reducing
every day reliably trimmed the low indices (Day 1) but left the high-index
tail (Day 2+) untouched.

This only bit patch-based writes: the AI patchCardInstance tool and any
PatchCardInstanceCommand caller. A component-side `model.field = [...]`
assignment was never affected, because it replaces the whole field value.

Fix: merge attributes with mergeWith and a customizer that returns the
source array, so a patched array fully replaces the existing one —
matching PatchCardInstanceCommand's documented contract ("attributes
specified will be fully replaced; display the full array in the patch
code"). Non-array values still deep-merge (nested patches like
`cardInfo: { name }` keep their siblings); the relationships and meta
merge paths are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lucaslyl lucaslyl self-assigned this Jun 24, 2026
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files      1 suites   2h 0m 40s ⏱️
3 201 tests 3 186 ✅ 15 💤 0 ❌
3 220 runs  3 205 ✅ 15 💤 0 ❌

Results for commit 6a758cc.

Realm Server Test Results

    1 files      1 suites   10m 27s ⏱️
1 733 tests 1 733 ✅ 0 💤 0 ❌
1 826 runs  1 826 ✅ 0 💤 0 ❌

Results for commit 6a758cc.

@lucaslyl

Copy link
Copy Markdown
Contributor Author

@codex comment

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

  • I reviewed the trigger, PR metadata, and existing PR comments. The trigger only says @codex comment, and there are no reviewer change requests or CI failure details requiring a code change.
  • I confirmed the current branch is clean, so I did not create a follow-up commit or PR.
  • The existing PR already contains the relevant fix: StoreService.patch now uses mergeWith so array attributes from patches replace destination arrays instead of lodash index-merging them. packages/host/app/services/store.tsL928-L933
  • The existing PR also includes focused regression coverage that grows a containsMany array and then patches it shorter, asserting that stale trailing items are removed. packages/host/tests/integration/commands/patch-instance-test.gtsL218-L264

Testing

  • git status --short
  • git diff --stat HEAD^..HEAD
  • git show --stat --oneline --name-only HEAD
  • rg -n "mergeWith|patch.attributes|fully replaces|shorter containsMany|nickNames" packages/host/app/services/store.ts packages/host/tests/integration/commands/patch-instance-test.gts

View task →

@lucaslyl lucaslyl marked this pull request as ready for review June 24, 2026 13:11
@lucaslyl lucaslyl requested review from a team June 24, 2026 13:11

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a758cc4b2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +929 to +933
doc.data.attributes = mergeWith(
doc.data.attributes,
patch.attributes,
(_dest, src) => (Array.isArray(src) ? src : undefined),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear stale array field metadata when replacing arrays

When the patched attribute is a shorter array for a field with per-index metadata (for example a polymorphic containsMany with data.meta.fields['items.1']), this replaces only data.attributes; the serialized doc still carries the old per-item metadata from serializeCard. updateFromSerialized can keep those stale field overrides on the live instance, so expanding the array again before a reload can write the removed element's type metadata back onto the new entry. Please clear or replace the matching doc.data.meta.fields entries when an array attribute is fully replaced.

Useful? React with 👍 / 👎.

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