Skip to content

fix(agent): merge a model save into the definition it updates - #226

Merged
ABB65 merged 1 commit into
mainfrom
fix/save-model-merge
Sep 2, 2026
Merged

fix(agent): merge a model save into the definition it updates#226
ABB65 merged 1 commit into
mainfrom
fix/save-model-merge

Conversation

@ABB65

@ABB65 ABB65 commented Sep 2, 2026

Copy link
Copy Markdown
Member

Summary

Closes #212.

save_model with a payload that carried only the field being added replaced the whole definition and auto-merged the truncation to main (iterum: 38 of 39 fields gone from home-page, content untouched). Three things were missing — the tool never said it replaced, nothing merged with the existing definition, and nothing checked the content. All three are addressed at the engine, so the API PATCH route and the ee Conversation API get the same behaviour.

  • Merge, never replace (content-engine/model-merge.ts, save-model.ts). saveModel reads the definition on contentrain and merges: omitted fields and top-level keys (title_field, description, a form config) are kept; a sent field is merged property by property, so adding label does not drop required; removal is only via removeFields.
  • Breaking-change gate. A removed or retyped field, or a changed kind/i18n, is refused with the affected entry count while content still carries it — counted from the model's content across its locales (collections, singletons, documents; an entry present in several locales counts once), and only computed when a structural candidate exists, so the common add-a-field save never reads content. allowBreaking overrides. The refusal names the field and the count.
  • title_field is validated with the same rule as MCP and the PATCH route (must name a text-capable field; key on dictionaries). Absent is still allowed — models predate the field.
  • Agent contract. The save_model description states the merge semantics, documents label/order (closing the agent-side gap for Let models declare field order instead of rendering the edit form alphabetically #204/Per-field display label, so editors see "Herkese açık gövde" instead of body_public #205), adds title_field, remove_fields and allow_breaking to the schema; a prompt rule (rules.model_update_merge) tells the agent to ask before retrying with allow_breaking. The conversation engine splits the flags off the definition and turns a refused save into a hard error, as save_content already did (before, a refused model save went on to call merge with an empty branch).
  • The result carries modelChange (addedFields / changedFields / removedFields / keptFields) so the agent reports exactly what happened.

Not changed: detectBreakingChanges in the health audit keeps field_removed as a warning — that path scores a project's state after the fact; the gate here is at write time with a precise count. MCP's own contentrain_model_save (and so MCP Cloud) still replaces per MCP's contract; noted in CLAUDE.md.

Test plan

  • tests/unit/model-merge.test.ts — merge rules, change summary, candidates, usage filtering, messages, title-field rule
  • tests/unit/content-engine.test.ts — the exact iterum payload keeps 39→40 fields; used-field removal refused with count; unused field dropped; confirmed removal proceeds; type change refused counting entries once across locales; bad title field refused; new model created as sent
  • tests/unit/agent-tools.test.ts — schema carries the new handles
  • lint, nuxt typecheck, full pnpm test (157 files green)
  • Staging: on iterum, ask the agent to "add a hero_background_image image field to home-page" and confirm the committed model keeps every field; then "remove pricing_preview" and confirm the agent reports the entry count and asks before proceeding

🤖 Generated with Claude Code

https://claude.ai/code/session_01Chid3TnLdPHjsKip4n63Ky

A chat-agent save_model call that carried only the field being added
replaced the entire model definition, and auto-merge landed the
truncation on main without a check. On Contentrain/iterum an "add one
image field" request wiped 38 of 39 fields from home-page while every
entry still used them; an earlier save on the same model had dropped
pricing_preview the same way. The tool said "create or update", the
engine passed the payload straight to planModelSave, and validation
looked at the new definition alone.

saveModel now reads the definition on contentrain and merges: omitted
fields and top-level keys (title_field, description, form) are kept, a
sent field is merged property by property, and only removeFields drops
one. A removal, type change, kind change or i18n change that still has
content behind it is refused with the affected entry count — read from
the model's content across its locales, and only when a candidate
exists — unless allowBreaking is passed. The result carries what the
save did to the field list so the agent reports it exactly.

The tool schema now says updates merge, documents label/order and
title_field, and adds remove_fields/allow_breaking; the conversation
engine splits those flags off the definition and treats a refused save
as a hard error like save_content. MCP's own contentrain_model_save
still replaces — that is MCP's contract, noted in CLAUDE.md.

Closes #212
@ABB65
ABB65 merged commit 0982b06 into main Sep 2, 2026
2 checks passed
@ABB65
ABB65 deleted the fix/save-model-merge branch September 2, 2026 09:08
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.

Chat-agent save_model replaces the whole model definition, so adding one field wipes every other field and auto-merges to main

1 participant