fix(content): hold new field names to snake_case, and title models the way MCP backfills them - #230
Merged
Merged
Conversation
added 3 commits
September 2, 2026 16:25
Studio pinned types 1.2.0 / mcp 3.1.0 / query 7.0.6, seven releases behind. The gap is narrower than it looks: between mcp 3.1.0 and 3.1.7 the package source did not change (types bumps only), and types 1.3 through 1.9 added the WordPress migration contracts without touching FieldDef, ModelDefinition, PATH_PATTERNS or any symbol Studio imports. The substantive change is 3.1.8 / 1.9.1: the generic `api_key = …` secret rule now checks what it captured before it fires. Studio's validateContent comes from the MCP validator and calls detectSecrets, so until now a setup guide that said `api_key = your_project_api_key_here` was refused by every Studio write path — UI, agent, Conversation API. The new test pins that the fix reaches Studio, and that an issued key is still refused. Also in 3.1.8, for the next PRs: publish_at/expire_at ride on ContentEntry (meta only, null clears), legacy field names are grandfathered by the MCP validator, and title_field inference matches Studio's resolver.
… in meta MCP 3.1.8 moved publish_at/expire_at onto the entry: meta only, never the content file, never a status change. Studio had no way to set them — the save_content tool, the content route and shapeEntriesForSave carried data alone — and could not have kept them if it did: applyStudioMetaOverrides rebuilt the meta record from the file on contentrain and threw away every key the plan had just set. The override now starts from the meta MCP planned, which already holds the prior record plus this write's schedule, and re-owns only status, updated_by and updated_at (autoPublish is the one rule Studio adds on top; MCP already keeps the current status). One fewer read per save. save_content takes publish_at/expire_at beside data (null clears, absent leaves alone); the content route accepts the same body keys; the engine validates the dates and lifts any the caller still put inside data, so nothing leaks into the content file — the habit the older MCP taught the agent, and the leak its validator now warns about. A prompt rule says scheduling is a delivery gate, not a publish.
…e way MCP backfills them Two places where Studio and MCP 3.1.8 disagreed about a model. Field names. MCP's contentrain_model_save now keeps a legacy non-snake_case name the model already has and refuses a new one; its validator lists the kept ones as notices. Studio enforced nothing, so a field minted through the chat agent could carry a name the rest of the ecosystem then flags. saveModel applies the same rule with the same tolerance, through MCP's own collectFieldPaths/legacyFieldNames: a new camelCase field is refused with the field path, a legacy one is kept and reported in `warnings` so the agent can say what it tolerated. Titles. validate --fix backfills title_field with requiredness as the tiebreak on every rung and never picks a slug, url or code field. Studio's fallback resolver ranked name-likeness first, so `authors` with an optional `title` (the job title) and a required `name` was titled by the job title — the exact ai #120 case. The chain is now the same one, with Studio's first-field last rung kept because a read path has to render something where the writer can decline and ask.
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.
Summary
Stacked on #228 → #229 (merge in that order; this PR's own diff is the last commit).
Two agreements with MCP 3.1.8 on what a valid model is and how a model without
title_fieldis titled.content-engine/save-model.ts). MCP'scontentrain_model_savekeeps a legacy non-snake_case name the model already has and refuses a new one (ai perf: ~6x fewer GitHub API calls in the agent write path (W1-W4) #116); its validator lists kept names as notices. Studio enforced nothing, so the chat agent could mintheroImageand the ecosystem would flag it.saveModelnow applies the same rule with the same tolerance via MCP's exportedcollectFieldPaths/legacyFieldNames: a new non-snake_case field (nested paths included, e.g.seo.ogTitle) is refused naming the path; a legacy one is kept and reported inModelWriteResult.warnings, which the conversation engine passes to the agent. Tool description states the rule.shared/utils/entry-title.ts).validate --fixbackfills with requiredness as the tiebreak on every rung, a name-like token anywhere in the key, and never a slug/url/code field (ai Chore/bump contentrain 1.8.0 #120). Studio's fallback ranked name-likeness first, so anauthorsmodel with an optionaltitle(job title) and a requirednamewas titled by the job title. The chain is now MCP's — name-like with required first, required prose by type priority, any prose, never slug/url/code — plus Studio's first-field last rung, kept because a read path must render something where the writer can decline and ask.Test plan
content-engine.test.ts: new camelCase field (top-level and nested) refused before git; legacy field kept on a title_field-only update with a warningentry-title.test.ts: the Chore/bump contentrain 1.8.0 #120 authors case, token match (post_title), type priority among required prose, url never inferred; all prior expectations unchangednuxt typecheck, unit + integration (129 files), nuxt (31 files)title_fieldthrough the picker — the save succeeds and the field is untouched🤖 Generated with Claude Code
https://claude.ai/code/session_01Chid3TnLdPHjsKip4n63Ky