@contentrain/rules 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ Start here:
14
14
 
15
15
  This package is the **policy layer** of the Contentrain ecosystem. It defines how agents should behave when they work with:
16
16
 
17
- - Contentrain MCP tools (17 operations)
17
+ - Contentrain MCP tools (27 operations)
18
18
  - schema and model design
19
19
  - content quality (SEO, accessibility, media, i18n)
20
20
  - normalize workflows (extraction + reuse)
@@ -75,7 +75,7 @@ The package root exports constants for tooling:
75
75
 
76
76
  - `FIELD_TYPES` — 27 flat field types
77
77
  - `MODEL_KINDS` — `singleton`, `collection`, `document`, `dictionary`
78
- - `MCP_TOOLS` — 26 MCP tool names: 21 core + 5 media (`contentrain_media_*`, registered only on media-capable providers)
78
+ - `MCP_TOOLS` — 27 MCP tool names: 22 core + 5 media (`contentrain_media_*`, registered only on media-capable providers)
79
79
  - `ESSENTIAL_RULES_FILE` — path to essential guardrails markdown
80
80
  - `STACKS` — supported framework stacks
81
81
 
@@ -84,7 +84,7 @@ The package root exports constants for tooling:
84
84
  ```ts
85
85
  import { MCP_TOOLS, ESSENTIAL_RULES_FILE, FIELD_TYPES } from '@contentrain/rules'
86
86
 
87
- console.log(MCP_TOOLS.length) // 24
87
+ console.log(MCP_TOOLS.length) // 27
88
88
  console.log(MCP_TOOLS.includes('contentrain_merge')) // true
89
89
  console.log(MCP_TOOLS.includes('contentrain_doctor')) // true
90
90
  console.log(ESSENTIAL_RULES_FILE) // 'essential/contentrain-essentials.md'
package/dist/index.cjs CHANGED
@@ -61,6 +61,10 @@ const MODEL_PROPERTIES = [
61
61
  name: "i18n",
62
62
  required: true
63
63
  },
64
+ {
65
+ name: "locales",
66
+ required: false
67
+ },
64
68
  {
65
69
  name: "title_field",
66
70
  required: true
@@ -80,6 +84,16 @@ const MODEL_PROPERTIES = [
80
84
  {
81
85
  name: "fields",
82
86
  required: true
87
+ },
88
+ {
89
+ name: "form",
90
+ required: false,
91
+ runtimeOwned: true
92
+ },
93
+ {
94
+ name: "comments",
95
+ required: false,
96
+ runtimeOwned: true
83
97
  }
84
98
  ];
85
99
  const MCP_TOOLS = [
package/dist/index.d.cts CHANGED
@@ -23,6 +23,9 @@ declare const MODEL_PROPERTIES: readonly [{
23
23
  }, {
24
24
  readonly name: "i18n";
25
25
  readonly required: true;
26
+ }, {
27
+ readonly name: "locales";
28
+ readonly required: false;
26
29
  }, {
27
30
  readonly name: "title_field";
28
31
  readonly required: true;
@@ -38,6 +41,14 @@ declare const MODEL_PROPERTIES: readonly [{
38
41
  }, {
39
42
  readonly name: "fields";
40
43
  readonly required: true;
44
+ }, {
45
+ readonly name: "form";
46
+ readonly required: false;
47
+ readonly runtimeOwned: true;
48
+ }, {
49
+ readonly name: "comments";
50
+ readonly required: false;
51
+ readonly runtimeOwned: true;
41
52
  }];
42
53
  type ModelProperty = (typeof MODEL_PROPERTIES)[number]['name'];
43
54
  declare const MCP_TOOLS: readonly ["contentrain_status", "contentrain_describe", "contentrain_describe_format", "contentrain_init", "contentrain_scaffold", "contentrain_model_save", "contentrain_model_delete", "contentrain_content_save", "contentrain_content_delete", "contentrain_content_list", "contentrain_vocabulary_save", "contentrain_vocabulary_delete", "contentrain_scan", "contentrain_apply", "contentrain_validate", "contentrain_submit", "contentrain_merge", "contentrain_reconcile", "contentrain_branch_list", "contentrain_branch_delete", "contentrain_bulk", "contentrain_doctor", "contentrain_media_list", "contentrain_media_get", "contentrain_media_ingest", "contentrain_media_update", "contentrain_media_delete"];
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;AAQA;;;;cAAa,WAAA;AAAA,KAUD,SAAA,WAAoB,WAAA;AAAA,cAInB,WAAA;AAAA,KACD,SAAA,WAAoB,WAAA;AAAA,cASnB,gBAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAiBD,aAAA,WAAwB,gBAAA;AAAA,cAIvB,SAAA;AAAA,KAkBD,OAAA,WAAkB,SAAA;AAAA,cAIjB,oBAAA;AAAA,cAIA,MAAA;AAAA,KACD,KAAA,WAAgB,MAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;AAQA;;;;cAAa,WAAA;AAAA,KAUD,SAAA,WAAoB,WAAA;AAAA,cAInB,WAAA;AAAA,KACD,SAAA,WAAoB,WAAA;AAAA,cASnB,gBAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAoBD,aAAA,WAAwB,gBAAA;AAAA,cAIvB,SAAA;AAAA,KAkBD,OAAA,WAAkB,SAAA;AAAA,cAIjB,oBAAA;AAAA,cAIA,MAAA;AAAA,KACD,KAAA,WAAgB,MAAA"}
package/dist/index.d.mts CHANGED
@@ -23,6 +23,9 @@ declare const MODEL_PROPERTIES: readonly [{
23
23
  }, {
24
24
  readonly name: "i18n";
25
25
  readonly required: true;
26
+ }, {
27
+ readonly name: "locales";
28
+ readonly required: false;
26
29
  }, {
27
30
  readonly name: "title_field";
28
31
  readonly required: true;
@@ -38,6 +41,14 @@ declare const MODEL_PROPERTIES: readonly [{
38
41
  }, {
39
42
  readonly name: "fields";
40
43
  readonly required: true;
44
+ }, {
45
+ readonly name: "form";
46
+ readonly required: false;
47
+ readonly runtimeOwned: true;
48
+ }, {
49
+ readonly name: "comments";
50
+ readonly required: false;
51
+ readonly runtimeOwned: true;
41
52
  }];
42
53
  type ModelProperty = (typeof MODEL_PROPERTIES)[number]['name'];
43
54
  declare const MCP_TOOLS: readonly ["contentrain_status", "contentrain_describe", "contentrain_describe_format", "contentrain_init", "contentrain_scaffold", "contentrain_model_save", "contentrain_model_delete", "contentrain_content_save", "contentrain_content_delete", "contentrain_content_list", "contentrain_vocabulary_save", "contentrain_vocabulary_delete", "contentrain_scan", "contentrain_apply", "contentrain_validate", "contentrain_submit", "contentrain_merge", "contentrain_reconcile", "contentrain_branch_list", "contentrain_branch_delete", "contentrain_bulk", "contentrain_doctor", "contentrain_media_list", "contentrain_media_get", "contentrain_media_ingest", "contentrain_media_update", "contentrain_media_delete"];
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;AAQA;;;;cAAa,WAAA;AAAA,KAUD,SAAA,WAAoB,WAAA;AAAA,cAInB,WAAA;AAAA,KACD,SAAA,WAAoB,WAAA;AAAA,cASnB,gBAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAiBD,aAAA,WAAwB,gBAAA;AAAA,cAIvB,SAAA;AAAA,KAkBD,OAAA,WAAkB,SAAA;AAAA,cAIjB,oBAAA;AAAA,cAIA,MAAA;AAAA,KACD,KAAA,WAAgB,MAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;AAQA;;;;cAAa,WAAA;AAAA,KAUD,SAAA,WAAoB,WAAA;AAAA,cAInB,WAAA;AAAA,KACD,SAAA,WAAoB,WAAA;AAAA,cASnB,gBAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAoBD,aAAA,WAAwB,gBAAA;AAAA,cAIvB,SAAA;AAAA,KAkBD,OAAA,WAAkB,SAAA;AAAA,cAIjB,oBAAA;AAAA,cAIA,MAAA;AAAA,KACD,KAAA,WAAgB,MAAA"}
package/dist/index.mjs CHANGED
@@ -60,6 +60,10 @@ const MODEL_PROPERTIES = [
60
60
  name: "i18n",
61
61
  required: true
62
62
  },
63
+ {
64
+ name: "locales",
65
+ required: false
66
+ },
63
67
  {
64
68
  name: "title_field",
65
69
  required: true
@@ -79,6 +83,16 @@ const MODEL_PROPERTIES = [
79
83
  {
80
84
  name: "fields",
81
85
  required: true
86
+ },
87
+ {
88
+ name: "form",
89
+ required: false,
90
+ runtimeOwned: true
91
+ },
92
+ {
93
+ name: "comments",
94
+ required: false,
95
+ runtimeOwned: true
82
96
  }
83
97
  ];
84
98
  const MCP_TOOLS = [
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @contentrain/rules — AI agent rules for Contentrain\n *\n * Quality standards, architecture conventions, and IDE integration.\n */\n\n// ─── Field Types (27 flat types) ───\n\nexport const FIELD_TYPES = [\n 'string', 'text', 'email', 'url', 'slug', 'color', 'phone', 'code', 'icon',\n 'markdown', 'richtext',\n 'number', 'integer', 'decimal', 'percent', 'rating',\n 'boolean', 'date', 'datetime',\n 'image', 'video', 'file',\n 'relation', 'relations',\n 'select', 'array', 'object',\n] as const\n\nexport type FieldType = (typeof FIELD_TYPES)[number]\n\n// ─── Model Kinds ───\n\nexport const MODEL_KINDS = ['singleton', 'collection', 'document', 'dictionary'] as const\nexport type ModelKind = (typeof MODEL_KINDS)[number]\n\n// ─── Model Definition Properties ───\n//\n// The parity anchor for the model schema. Docs describing `ModelDefinition` drift\n// silently: nothing failed when a property landed in @contentrain/types and never\n// reached schema-rules.md. The tests in tests/mcp-parity.test.ts assert this list\n// against MODEL_FIELD_ORDER in @contentrain/mcp and against the prose tables.\n\nexport const MODEL_PROPERTIES = [\n { name: 'id', required: true },\n { name: 'name', required: true },\n { name: 'kind', required: true },\n { name: 'domain', required: true },\n { name: 'i18n', required: true },\n { name: 'title_field', required: true },\n { name: 'description', required: false },\n { name: 'content_path', required: false },\n { name: 'locale_strategy', required: false },\n // `required` here means \"documented as required\", not the TS optionality.\n // `fields` is `fields?` on the interface but every non-dictionary model needs\n // it — a collection with no fields cannot even name a title_field. The\n // per-kind nuance lives in the docs cell (\"Yes (except dictionary)\").\n { name: 'fields', required: true },\n] as const\n\nexport type ModelProperty = (typeof MODEL_PROPERTIES)[number]['name']\n\n// ─── MCP Tools (27 tools: 22 core + 5 media) ───\n\nexport const MCP_TOOLS = [\n 'contentrain_status', 'contentrain_describe', 'contentrain_describe_format',\n 'contentrain_init', 'contentrain_scaffold',\n 'contentrain_model_save', 'contentrain_model_delete',\n 'contentrain_content_save', 'contentrain_content_delete', 'contentrain_content_list',\n 'contentrain_vocabulary_save', 'contentrain_vocabulary_delete',\n 'contentrain_scan', 'contentrain_apply',\n 'contentrain_validate', 'contentrain_submit',\n 'contentrain_merge', 'contentrain_reconcile',\n 'contentrain_branch_list', 'contentrain_branch_delete',\n 'contentrain_bulk',\n 'contentrain_doctor',\n // Media tools — registered only when the provider exposes a media stack\n // (Studio MCP Cloud); absent from local stdio servers.\n 'contentrain_media_list', 'contentrain_media_get', 'contentrain_media_ingest',\n 'contentrain_media_update', 'contentrain_media_delete',\n] as const\n\nexport type McpTool = (typeof MCP_TOOLS)[number]\n\n// ─── Essential Rules ───\n\nexport const ESSENTIAL_RULES_FILE = 'essential/contentrain-essentials.md' as const\n\n// ─── Stacks ───\n\nexport const STACKS = ['nuxt', 'next', 'astro', 'sveltekit', 'react', 'node'] as const\nexport type Stack = (typeof STACKS)[number]\n"],"mappings":";;;;;;AAQA,MAAa,cAAc;CACzB;CAAU;CAAQ;CAAS;CAAO;CAAQ;CAAS;CAAS;CAAQ;CACpE;CAAY;CACZ;CAAU;CAAW;CAAW;CAAW;CAC3C;CAAW;CAAQ;CACnB;CAAS;CAAS;CAClB;CAAY;CACZ;CAAU;CAAS;CACpB;AAMD,MAAa,cAAc;CAAC;CAAa;CAAc;CAAY;CAAa;AAUhF,MAAa,mBAAmB;CAC9B;EAAE,MAAM;EAAM,UAAU;EAAM;CAC9B;EAAE,MAAM;EAAQ,UAAU;EAAM;CAChC;EAAE,MAAM;EAAQ,UAAU;EAAM;CAChC;EAAE,MAAM;EAAU,UAAU;EAAM;CAClC;EAAE,MAAM;EAAQ,UAAU;EAAM;CAChC;EAAE,MAAM;EAAe,UAAU;EAAM;CACvC;EAAE,MAAM;EAAe,UAAU;EAAO;CACxC;EAAE,MAAM;EAAgB,UAAU;EAAO;CACzC;EAAE,MAAM;EAAmB,UAAU;EAAO;CAK5C;EAAE,MAAM;EAAU,UAAU;EAAM;CACnC;AAMD,MAAa,YAAY;CACvB;CAAsB;CAAwB;CAC9C;CAAoB;CACpB;CAA0B;CAC1B;CAA4B;CAA8B;CAC1D;CAA+B;CAC/B;CAAoB;CACpB;CAAwB;CACxB;CAAqB;CACrB;CAA2B;CAC3B;CACA;CAGA;CAA0B;CAAyB;CACnD;CAA4B;CAC7B;AAMD,MAAa,uBAAuB;AAIpC,MAAa,SAAS;CAAC;CAAQ;CAAQ;CAAS;CAAa;CAAS;CAAO"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @contentrain/rules — AI agent rules for Contentrain\n *\n * Quality standards, architecture conventions, and IDE integration.\n */\n\n// ─── Field Types (27 flat types) ───\n\nexport const FIELD_TYPES = [\n 'string', 'text', 'email', 'url', 'slug', 'color', 'phone', 'code', 'icon',\n 'markdown', 'richtext',\n 'number', 'integer', 'decimal', 'percent', 'rating',\n 'boolean', 'date', 'datetime',\n 'image', 'video', 'file',\n 'relation', 'relations',\n 'select', 'array', 'object',\n] as const\n\nexport type FieldType = (typeof FIELD_TYPES)[number]\n\n// ─── Model Kinds ───\n\nexport const MODEL_KINDS = ['singleton', 'collection', 'document', 'dictionary'] as const\nexport type ModelKind = (typeof MODEL_KINDS)[number]\n\n// ─── Model Definition Properties ───\n//\n// The parity anchor for the model schema. Docs describing `ModelDefinition` drift\n// silently: nothing failed when a property landed in @contentrain/types and never\n// reached schema-rules.md. The tests in tests/mcp-parity.test.ts assert this list\n// against MODEL_FIELD_ORDER in @contentrain/mcp and against the prose tables.\n\nexport const MODEL_PROPERTIES = [\n { name: 'id', required: true },\n { name: 'name', required: true },\n { name: 'kind', required: true },\n { name: 'domain', required: true },\n { name: 'i18n', required: true },\n { name: 'locales', required: false },\n { name: 'title_field', required: true },\n { name: 'description', required: false },\n { name: 'content_path', required: false },\n { name: 'locale_strategy', required: false },\n // `required` here means \"documented as required\", not the TS optionality.\n // `fields` is `fields?` on the interface but every non-dictionary model needs\n // it — a collection with no fields cannot even name a title_field. The\n // per-kind nuance lives in the docs cell (\"Yes (except dictionary)\").\n { name: 'fields', required: true },\n { name: 'form', required: false, runtimeOwned: true },\n { name: 'comments', required: false, runtimeOwned: true },\n] as const\n\nexport type ModelProperty = (typeof MODEL_PROPERTIES)[number]['name']\n\n// ─── MCP Tools (27 tools: 22 core + 5 media) ───\n\nexport const MCP_TOOLS = [\n 'contentrain_status', 'contentrain_describe', 'contentrain_describe_format',\n 'contentrain_init', 'contentrain_scaffold',\n 'contentrain_model_save', 'contentrain_model_delete',\n 'contentrain_content_save', 'contentrain_content_delete', 'contentrain_content_list',\n 'contentrain_vocabulary_save', 'contentrain_vocabulary_delete',\n 'contentrain_scan', 'contentrain_apply',\n 'contentrain_validate', 'contentrain_submit',\n 'contentrain_merge', 'contentrain_reconcile',\n 'contentrain_branch_list', 'contentrain_branch_delete',\n 'contentrain_bulk',\n 'contentrain_doctor',\n // Media tools — registered only when the provider exposes a media stack\n // (Studio MCP Cloud); absent from local stdio servers.\n 'contentrain_media_list', 'contentrain_media_get', 'contentrain_media_ingest',\n 'contentrain_media_update', 'contentrain_media_delete',\n] as const\n\nexport type McpTool = (typeof MCP_TOOLS)[number]\n\n// ─── Essential Rules ───\n\nexport const ESSENTIAL_RULES_FILE = 'essential/contentrain-essentials.md' as const\n\n// ─── Stacks ───\n\nexport const STACKS = ['nuxt', 'next', 'astro', 'sveltekit', 'react', 'node'] as const\nexport type Stack = (typeof STACKS)[number]\n"],"mappings":";;;;;;AAQA,MAAa,cAAc;CACzB;CAAU;CAAQ;CAAS;CAAO;CAAQ;CAAS;CAAS;CAAQ;CACpE;CAAY;CACZ;CAAU;CAAW;CAAW;CAAW;CAC3C;CAAW;CAAQ;CACnB;CAAS;CAAS;CAClB;CAAY;CACZ;CAAU;CAAS;CACpB;AAMD,MAAa,cAAc;CAAC;CAAa;CAAc;CAAY;CAAa;AAUhF,MAAa,mBAAmB;CAC9B;EAAE,MAAM;EAAM,UAAU;EAAM;CAC9B;EAAE,MAAM;EAAQ,UAAU;EAAM;CAChC;EAAE,MAAM;EAAQ,UAAU;EAAM;CAChC;EAAE,MAAM;EAAU,UAAU;EAAM;CAClC;EAAE,MAAM;EAAQ,UAAU;EAAM;CAChC;EAAE,MAAM;EAAW,UAAU;EAAO;CACpC;EAAE,MAAM;EAAe,UAAU;EAAM;CACvC;EAAE,MAAM;EAAe,UAAU;EAAO;CACxC;EAAE,MAAM;EAAgB,UAAU;EAAO;CACzC;EAAE,MAAM;EAAmB,UAAU;EAAO;CAK5C;EAAE,MAAM;EAAU,UAAU;EAAM;CAClC;EAAE,MAAM;EAAQ,UAAU;EAAO,cAAc;EAAM;CACrD;EAAE,MAAM;EAAY,UAAU;EAAO,cAAc;EAAM;CAC1D;AAMD,MAAa,YAAY;CACvB;CAAsB;CAAwB;CAC9C;CAAoB;CACpB;CAA0B;CAC1B;CAA4B;CAA8B;CAC1D;CAA+B;CAC/B;CAAoB;CACpB;CAAwB;CACxB;CAAqB;CACrB;CAA2B;CAC3B;CACA;CAGA;CAA0B;CAAyB;CACnD;CAA4B;CAC7B;AAMD,MAAa,uBAAuB;AAIpC,MAAa,SAAS;CAAC;CAAQ;CAAQ;CAAS;CAAa;CAAS;CAAO"}
@@ -35,7 +35,7 @@ MCP is **deterministic infrastructure**. The agent (you) is the **intelligence l
35
35
  - **ALWAYS** use MCP tools — do not write `.contentrain/` JSON files directly
36
36
  - **Publishing is yours, not MCP's.** `contentrain_content_save` preserves an existing entry's `status`; only a brand-new entry starts at `draft`. To change publish state, call `contentrain_bulk update_status` deliberately (`entry_ids` for collections, `slugs` for documents, neither for singletons/dictionaries) — and note that on the CDN an entry is delivered only when its status is `published`. `publish_at`/`expire_at` on `content_save` never change status: they gate delivery of a published entry, live in meta only, and `null` clears them.
37
37
  - **Schema validity is MCP's.** `contentrain_content_save` validates before it writes and **refuses** the save on any error — nothing is committed, so there is no branch to clean up. Fix the values and call it again. Warnings (email/url/colour/phone heuristics, `accept` extension checks) do not block; they come back in the response.
38
- - **Every model declares its `title_field`.** Required on create, and it must stay correct when you rename or drop a field. It names the field shown as an entry's title and must be a `string`/`text`/`slug`/`email`/`url`/`code`/`markdown`/`richtext` field on that model; dictionaries have no fields, so they use the reserved value `"key"`. Pick what a human reads to tell entries apart — the headline, the name, the question — never the slug, icon or a relation ID. Consumers used to guess this and titled rows `i-lucide-bot`.
38
+ - **Every model declares its `title_field`.** Required on create, and it must stay correct when you rename or drop a field. It names the field shown as an entry's title and must be a `string`/`text`/`slug`/`email`/`url`/`code`/`markdown`/`richtext` field on that model; dictionaries have no fields, so they use the reserved value `"key"`; a singleton of section objects uses a dotted path one level deep (`hero.heading`). Pick what a human reads to tell entries apart — the headline, the name, the question — never the slug, icon or a relation ID. Consumers used to guess this and titled rows `i-lucide-bot`.
39
39
  - **A declared constraint is a real constraint.** `model_save` rejects a property declared where it cannot apply (`options` on a non-select, `accept` on a non-media field, `unique` on a singleton) rather than accepting it and doing nothing. The one exception is `maxSize`, which MCP cannot enforce — your media provider does, at ingest, and `model_save` says so.
40
40
 
41
41
  ## MCP Tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/rules",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "license": "MIT",
5
5
  "description": "AI agent rules for Contentrain — quality standards, architecture conventions, IDE integration",
6
6
  "type": "module",
@@ -55,7 +55,7 @@
55
55
  "tsdown": "^0.21.0",
56
56
  "typescript": "^5.7.0",
57
57
  "vitest": "^3.0.0",
58
- "@contentrain/mcp": "3.1.8"
58
+ "@contentrain/mcp": "3.9.0"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "tsdown src/index.ts --format esm,cjs --dts",
@@ -224,8 +224,9 @@ Several fields are managed by the platform. Agents MUST NOT set or modify them.
224
224
  | `i18n: false` on model | Single file: `data.json` (or `{slug}.md` for documents) |
225
225
  | Locale code | ISO 639-1: `en`, `tr`, `de`, `fr`, `ja`, `ar` |
226
226
  | ID/slug are locale-agnostic | The same entry ID or document slug is used across all locales |
227
- | Entry parity (collections) | All locales MUST have the same set of entry IDs |
228
- | Key parity (dictionaries) | All locales MUST have the same set of keys |
227
+ | Entry parity (collections) | All covered locales MUST have the same set of entry IDs |
228
+ | Key parity (dictionaries) | All covered locales MUST have the same set of keys |
229
+ | `locales` on model (optional) | The project locales this model covers — a subset of `locales.supported`. Absent means all of them. Parity is checked against it |
229
230
 
230
231
  ### Validation Checks
231
232
 
@@ -10,7 +10,8 @@ Every rule applies whenever the project has more than one locale configured.
10
10
  ### 1.1 Coverage Requirement
11
11
 
12
12
  - Read `.contentrain/config.json` → `locales.supported` to determine the full list of supported locales.
13
- - EVERY entry that exists in the source locale MUST have a corresponding entry in ALL supported target locales.
13
+ - A model MAY narrow that list with an optional `locales` array (a subset of `locales.supported`). When present, it — not the project list — is the model's coverage. Absent means every supported locale.
14
+ - EVERY entry that exists in the source locale MUST have a corresponding entry in ALL of the model's covered target locales.
14
15
  - Missing translations are deployment blockers. Treat a missing locale file as an error, not a warning.
15
16
 
16
17
  ### 1.2 Dictionary Entries
@@ -48,7 +48,7 @@ MCP is **deterministic infrastructure**. The agent is the **intelligence layer**
48
48
 
49
49
  | Tool | Purpose | Parameters |
50
50
  |------|---------|------------|
51
- | `contentrain_model_save` | Create or update a model definition (upsert) | `id`, `name`, `kind`, `domain`, `i18n`, `title_field`, `fields?`, `description?`, `content_path?`, `locale_strategy?` |
51
+ | `contentrain_model_save` | Create or update a model definition (upsert) | `id`, `name`, `kind`, `domain`, `i18n`, `title_field`, `fields?`, `description?`, `locales?`, `content_path?`, `locale_strategy?` |
52
52
  | `contentrain_model_delete` | Delete a model and its content | `model`, `confirm: true` |
53
53
 
54
54
  ### 2.4 Content Tools
@@ -388,9 +388,11 @@ Creates or updates a model definition. Key parameters:
388
388
  - `id`: kebab-case model ID (e.g., `"blog-post"`).
389
389
  - `kind`: one of `singleton`, `collection`, `document`, `dictionary`.
390
390
  - `i18n`: whether the model supports localization.
391
- - `title_field`: **required** — the field shown as an entry's title. Must name a field declared on this model whose type is `string`, `text`, `slug`, `email`, `url`, `code`, `markdown` or `richtext`. Dictionary models use the reserved value `"key"`. Set it when creating a model, and keep it correct when renaming or removing fields.
391
+ - `title_field`: **required** — the field shown as an entry's title. Must name a field declared on this model whose type is `string`, `text`, `slug`, `email`, `url`, `code`, `markdown` or `richtext`. Dictionary models use the reserved value `"key"`. A dotted path (`hero.heading`) names a field one level inside an `object` field — for a singleton built from sections. Set it when creating a model, and keep it correct when renaming or removing fields.
392
+ - `locales`: optional subset of `config.locales.supported` naming the project locales this model covers. Omit it and the model covers every supported locale, which is what validation checks parity against. Declare it only when the content genuinely exists in a subset — a partially-translated site. A locale outside `locales.supported` is rejected.
392
393
  - `content_path`: optional framework-relative path (e.g., `"content/blog"`, `"locales"`). When set, content is written here instead of `.contentrain/content/`.
393
394
  - `locale_strategy`: how locale is encoded in filenames: `"file"` (default), `"suffix"`, `"directory"`, `"none"`.
395
+ - `contentrain_model_save` preserves existing `form` configuration and preserves existing `comments` configuration. These runtime-owned blocks are not structural tool inputs; use the runtime provider's settings surface to change them.
394
396
  - `locale_strategy: "none"` requires `i18n: false`. All other strategies work with both `i18n: true` and `i18n: false`.
395
397
 
396
398
  ### 5.5 contentrain_model_delete
@@ -9,7 +9,7 @@ Follow these guidelines when populating image, video, or file fields in any cont
9
9
 
10
10
  Media references are stored in one of two models. Identify which one the project uses before populating image, video, or file fields. The asset rules below (dimensions, formats, size limits, naming) apply to **both**; only the **Asset Organization** section (paths under `assets_path`, relative references) is specific to the local-file model.
11
11
 
12
- 1. **Local file model (default / OSS).** Media lives on disk under `config.json > assets_path` (default `.contentrain/assets/`) and is referenced by a **relative path from the project root** (e.g. `.contentrain/assets/blog/hero.webp`). To turn relative paths into URLs at read time, set `config.json > cdn.url` (or `contentrain generate --cdnBaseUrl <base>`) and use the `media()` resolver baked into the `@contentrain/query` generated client — `media('media/...') → {cdn.url}/{path}`.
12
+ 1. **Local file model (default / OSS).** Media lives on disk under `config.json > assets_path` (default `.contentrain/assets/`) and is referenced by a **relative path from the project root** (e.g. `.contentrain/assets/blog/hero.webp`). To turn relative paths into URLs at read time, set `config.json > cdn.url` (or `contentrain generate --mediaBaseUrl <base>`) and use the `media()`/`mediaBody()` resolvers baked into the `@contentrain/query` generated client — `media('media/...') → {mediaBaseUrl}/{path}`, `mediaBody(markdown)` for `](media/...)` references inside markdown. The Astro loader (`contentrainLoader({ mediaBaseUrl })`) applies the same resolver automatically to media fields and markdown bodies.
13
13
 
14
14
  2. **Studio CDN model.** Media lives in the Studio CDN/object store and is referenced by a relative storage path (`media/...`) returned by the media library. Pass either that `media/...` path or its URL to `contentrain_content_save`: in **cloud mode** the value is **automatically normalized to an absolute public delivery URL** on save (`{base}/{path}`), in both media fields and markdown bodies, so the committed content renders in a browser anywhere with no SDK. In **local mode** the relative path is kept verbatim. The rewrite is idempotent and never touches external URLs (`http(s)://`, `//`, `data:`) or already-absolute delivery URLs — so for external images (a CDN or Unsplash URL), just save the URL directly.
15
15
 
@@ -156,17 +156,25 @@ Model definitions live at `.contentrain/models/{model-id}.json`. One file per mo
156
156
  | `kind` | `string` | Yes | One of: `singleton`, `collection`, `document`, `dictionary`. |
157
157
  | `domain` | `string` | Yes | Organizational group (maps to content subdirectory). |
158
158
  | `i18n` | `boolean` | Yes | Whether the model supports multiple locales. |
159
- | `title_field` | `string` | Yes | Field shown as an entry's title in listings, pickers and relation references. Must name a field declared on this model whose type is `string`, `text`, `slug`, `email`, `url`, `code`, `markdown` or `richtext`. Dictionary models have no fields — they use the reserved value `"key"`, meaning the entry key is the title. |
159
+ | `locales` | `string[]` | No | The project locales this model covers — a subset of `config.locales.supported`. Absent (the default) means every supported locale, which is what parity is validated against. Declare it only when the content genuinely exists in a subset, e.g. a partially-translated site. A locale outside `locales.supported` is a validation error; the field has no effect on an `i18n: false` model. |
160
+ | `title_field` | `string` | Yes | Field shown as an entry's title in listings, pickers and relation references. Must name a field declared on this model whose type is `string`, `text`, `slug`, `email`, `url`, `code`, `markdown` or `richtext`. Dictionary models have no fields — they use the reserved value `"key"`, meaning the entry key is the title. A dotted path (`hero.heading`) names a field one level inside an `object` field. |
160
161
  | `description` | `string` | No | Model description for documentation and agent context. |
161
162
  | `fields` | `object` | Yes (except dictionary) | Field definitions. Dictionary kind has NO fields. |
162
163
  | `content_path` | `string` | No | Framework-relative path for content files (e.g., `"content/blog"`, `"locales"`). When set, content is written here instead of `.contentrain/content/`. |
163
164
  | `locale_strategy` | `string` | No | How locale is encoded in file names: `"file"` (default), `"suffix"`, `"directory"`, `"none"`. |
165
+ | `form` | `object` | No | Runtime-owned public form settings. Structural model edits preserve the existing block; configure through the runtime provider. |
166
+ | `comments` | `object` | No | Runtime-owned public comments settings, preserved across structural model edits. |
164
167
 
165
168
  #### Choosing `title_field`
166
169
 
167
170
  Declare it on create AND keep it correct when renaming or removing fields — a
168
171
  `title_field` pointing at a field that no longer exists fails validation.
169
172
 
173
+ A singleton built from section objects (`hero`, `services`, …) has no top-level
174
+ text field. Title it by a section's heading with a dotted path: `"hero.heading"`.
175
+ The path reaches one level into an `object` field — never deeper, never into an
176
+ array. Do not add a copied top-level `title` for this: it is a field no page prints.
177
+
170
178
  Pick the field a human would read to tell one entry from another: the headline,
171
179
  the name, the question. Not the slug, not the icon, not a relation ID — those are
172
180
  identifiers, and a list titled `instagram-launch-post` or `i-lucide-bot` tells the