This release includes 4 breaking changes for platform teams planning a safe upgrade.
✓ No known CVEs patched in this version
Topics
+8 more
Affected surfaces
ReleasePort's take
Light signal`content_save` and `model_save` now validate inputs before committing, rejecting invalid content; several API behaviors have changed accordingly.
Why it matters: All callers of `content_save` and `model_save` must handle rejection responses for previously accepted but invalid payloads (severity 70).
Summary
AI summarycontent_save and model_save now enforce field constraints, rejecting previously committed invalid content.
Changes in this release
| Type | Severity | Summary | CVE |
|---|---|---|---|
| Breaking | High |
`content_save` now validates before committing and rejects invalid content. `content_save` now validates before committing and rejects invalid content. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Breaking | High |
`model_save` rejects models with unknown keys, `min > max`, invalid constraints, and `unique` on singletons. `model_save` rejects models with unknown keys, `min > max`, invalid constraints, and `unique` on singletons. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Breaking | High |
`validateModelDefinition` now returns `{ errors, warnings }` instead of a string array. `validateModelDefinition` now returns `{ errors, warnings }` instead of a string array. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Feature | Medium |
`slug`, `date`, `datetime`, and `percent` fields now enforce semantic validation rules. `slug`, `date`, `datetime`, and `percent` fields now enforce semantic validation rules. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Feature | Low |
New helper functions `validateSemanticType`, `validateAccept`, and `isMediaType` added to `@contentrain/types`. New helper functions `validateSemanticType`, `validateAccept`, and `isMediaType` added to `@contentrain/types`. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Deprecation | Low |
`accept`, `maxSize`, and similar constraints are documented as non‑enforced by MCP. `accept`, `maxSize`, and similar constraints are documented as non‑enforced by MCP. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Bugfix | Medium |
Array item validation now recurses through the same validator as scalar fields. Array item validation now recurses through the same validator as scalar fields. Source: llm_adapter@2026-07-16 Confidence: high |
— |
| Bugfix | Medium |
`unique` constraint now correctly rejects usage on singleton documents. `unique` constraint now correctly rejects usage on singleton documents. Source: llm_adapter@2026-07-16 Confidence: high |
— |
Full changelog
Minor Changes
-
173326c: feat(mcp)!: enforce the field constraints the schema already accepted
A project reported that
items,acceptandmaxSizeare accepted on a field but
never enforced —emails: ["not-an-email"]andaccept: "image/jpeg"against a
.webpboth produced zero errors. The report was right, and the surface was larger
than the three properties it named: 4 of 27 field types had any semantic
validation, three constraints were read by nothing, and none of it blocked a write.A constraint that isn't a constraint is worse than no constraint — the author stops
looking.content_savenow validates before committing and refuses to write. It ran
plan → commit → validate → report, so an invalid value landed in git, was
auto-merged, and the caller learned about it from a string innext_stepswhile
statusstill said"committed". Validation now runs on the pending changes and
blocks on errors, returningisErrorand no commit. Warnings still pass — they are
heuristics, and a legitimate value can sit outside an approximate pattern. Only the
entries being saved are fatal: a pre-existing bad entry elsewhere in the model does
not hold up an unrelated save.Array items share the scalar rule set. They ran through a parallel type switch
that knew 10 of the 27 types and checked onlytypeof, somin/max/pattern/
optionsnever reached an item, anditemsgiven as a FieldDef with a non-object
type ({type:'array', items:{type:'string', max:50}}) matched no branch at all —
silently unvalidated, while the type emitter rendered it as real. Items now recurse
through the same validator, which also closes theintegersplit where3.7was
rejected inside an array but accepted as a scalar.17 types were pure
typeofchecks.slugnow uses theSLUG_PATTERNthe
codebase already owned — every shipped template declaresslug: { type: 'slug' },
so"Hello World!!"used to validate clean.date/datetimeare parsed (the same
checkschedule.tsalready did for meta),percentis range-checked, andcolor/
phonewarn. Mechanical rules are errors; heuristics are warnings.email/url
keep their existing warning severity.ratingis deliberately untouched — its scale
is never declared, so any range would be invented.uniqueworks on documents. It was gated on a context only the collection
validator passed, so it was a no-op exactly where every shipped template declares it.
On singletons it is now rejected at model_save: the model holds one record per
locale, so there is nothing to compare against.The dead constraints, handled honestly.
acceptis enforced by extension-sniff
and says that is what it is.defaultis coherence-checked at model_save (right
type, within its ownoptions) but not written into content.maxSizecannot be
enforced by MCP — it holds a path, never the bytes — so model_save now says so and
points at the provider, which owns the policy at ingest. The docs claimed all three
worked; they no longer do.model_save rejects what it will not enforce.
optionson a non-select,items
on a non-array,accept/maxSizeon a non-media field,min > max, and an
uncompilablepatternare now errors instead of silent no-ops. Nestedfields/
itemsschemas are validated recursively — they were typedz.unknown()and never
checked. The field schema is.strict(): a typo'd constraint (requird: true) used
to be stripped without a word.BREAKING CHANGE:
content_saverejects content it previously committed. Runcontentrain_validate
before upgrading to see what would now be blocked.model_saverejects models it previously accepted (unknown keys,min > max,
optionson a non-select,uniqueon a singleton).validateModelDefinitionreturns{ errors, warnings }instead ofstring[].- Array-item type errors carry
validateFieldValue's message ("Type mismatch:
expected string, got number") instead of "must be a string". The field path is
unchanged. - Nested object errors are qualified by their parent (
seo.title, nottitle) —
a bare name was ambiguous with a top-level field.
@contentrain/typesgainsvalidateSemanticType,validateAcceptand
isMediaType;validateFieldValuenow applies semantic andacceptrules.Studio picks all of this up automatically — its
content-validation.tsdelegates to
this validator.
Breaking Changes
- `content_save` rejects content it previously committed; run `contentrain_validate` before upgrading to see what would now be blocked.
- `model_save` rejects models with unknown keys, invalid ranges (`min > max`), inappropriate constraints (`options` on non-select, `unique` on singleton), and unenforceable media constraints (`accept`, `maxSize`).
- Deprecation: `validateModelDefinition` returns `{ errors, warnings }` instead of a string array.
- `content_save` validation runs before commit rather than after.
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
About Contentrain/ai
Local-first MCP server for AI content governance — 13 tools for model/content CRUD, validation, normalization, and i18n across any framework.
Related context
Related tools
Earlier breaking changes
- v@contentrain/[email protected] `validateModelDefinition` now returns `{ errors, warnings }` instead of a string array.
- v@contentrain/[email protected] `model_save` rejects models with unknown keys, `min > max`, invalid constraints, and `unique` on singletons.
- v@contentrain/[email protected] `content_save` now validates before committing and rejects invalid content.
- v@contentrain/[email protected] Migration required: projects must run `contentrain_validate` and fix draft status drift before upcoming Studio enforcement.
- v@contentrain/[email protected] `CdnDocumentQuery.first()` now returns `DocumentIndexEntry<T> | undefined`, no longer including `body`.
Beta — feedback welcome: [email protected]