@contentrain/mcp 1.1.0 → 1.1.2

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.
Files changed (67) hide show
  1. package/README.md +7 -0
  2. package/dist/{apply-manager-CMAoS_nu.mjs → apply-manager-Cf5OWOJ7.mjs} +7 -7
  3. package/dist/{apply-manager-CMAoS_nu.mjs.map → apply-manager-Cf5OWOJ7.mjs.map} +1 -1
  4. package/dist/{branch-lifecycle-B7TgkxGr.mjs → branch-lifecycle-wGJg25LT.mjs} +3 -3
  5. package/dist/{branch-lifecycle-B7TgkxGr.mjs.map → branch-lifecycle-wGJg25LT.mjs.map} +1 -1
  6. package/dist/{config-CPlhDwsv.mjs → config-B5kc2oeV.mjs} +2 -2
  7. package/dist/{config-CPlhDwsv.mjs.map → config-B5kc2oeV.mjs.map} +1 -1
  8. package/dist/{context-CUFulSi2.mjs → context-DIDt_TxX.mjs} +4 -4
  9. package/dist/{context-CUFulSi2.mjs.map → context-DIDt_TxX.mjs.map} +1 -1
  10. package/dist/core/apply-manager.mjs +10 -5
  11. package/dist/core/config.mjs +3 -1
  12. package/dist/core/content-manager.d.mts +1 -9
  13. package/dist/core/content-manager.d.mts.map +1 -1
  14. package/dist/core/content-manager.mjs +4 -1
  15. package/dist/core/context.mjs +6 -2
  16. package/dist/core/graph-builder.mjs +4 -1
  17. package/dist/core/meta-manager.mjs +3 -1
  18. package/dist/core/model-manager.mjs +4 -1
  19. package/dist/core/scan-config.mjs +3 -1
  20. package/dist/core/scanner.mjs +4 -1
  21. package/dist/core/validator.d.mts.map +1 -1
  22. package/dist/core/validator.mjs +6 -2
  23. package/dist/{detect-C8XmJ4S6.mjs → detect-DinOQAmK.mjs} +2 -2
  24. package/dist/{detect-C8XmJ4S6.mjs.map → detect-DinOQAmK.mjs.map} +1 -1
  25. package/dist/{fs-BbuGnVcG.mjs → fs-BnIQ4dKj.mjs} +2 -2
  26. package/dist/{fs-BbuGnVcG.mjs.map → fs-BnIQ4dKj.mjs.map} +1 -1
  27. package/dist/git/branch-lifecycle.mjs +4 -1
  28. package/dist/git/transaction.mjs +8 -3
  29. package/dist/{graph-builder-fPrDDwnu.mjs → graph-builder-tM7zj-SX.mjs} +3 -3
  30. package/dist/{graph-builder-fPrDDwnu.mjs.map → graph-builder-tM7zj-SX.mjs.map} +1 -1
  31. package/dist/{id-BdD4HvMY.mjs → id-DV_T9Ic8.mjs} +3 -5
  32. package/dist/id-DV_T9Ic8.mjs.map +1 -0
  33. package/dist/index.mjs +16 -9
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/{meta-manager-CECrfsKm.mjs → meta-manager--0IwAPCl.mjs} +2 -2
  36. package/dist/{meta-manager-CECrfsKm.mjs.map → meta-manager--0IwAPCl.mjs.map} +1 -1
  37. package/dist/{model-manager-kl78AAMg.mjs → model-manager-DW_fOGRM.mjs} +16 -186
  38. package/dist/model-manager-DW_fOGRM.mjs.map +1 -0
  39. package/dist/{scan-config-AJGxCdNd.mjs → scan-config-QgnJ1-52.mjs} +2 -2
  40. package/dist/{scan-config-AJGxCdNd.mjs.map → scan-config-QgnJ1-52.mjs.map} +1 -1
  41. package/dist/{scanner-BdTiv8sw.mjs → scanner-DPrbU-oK.mjs} +3 -3
  42. package/dist/{scanner-BdTiv8sw.mjs.map → scanner-DPrbU-oK.mjs.map} +1 -1
  43. package/dist/serializer-CnHTtmc1.mjs +2 -0
  44. package/dist/{server-CDv6JNRi.mjs → server-CjK63H6X.mjs} +16 -16
  45. package/dist/{server-CDv6JNRi.mjs.map → server-CjK63H6X.mjs.map} +1 -1
  46. package/dist/server.mjs +16 -9
  47. package/dist/templates/index.mjs +1 -1
  48. package/dist/{templates-3HWi-PTu.mjs → templates-bRPzYq4M.mjs} +1 -1
  49. package/dist/{templates-3HWi-PTu.mjs.map → templates-bRPzYq4M.mjs.map} +1 -1
  50. package/dist/{transaction-CncRbpI9.mjs → transaction-B0V0_SE2.mjs} +6 -6
  51. package/dist/{transaction-CncRbpI9.mjs.map → transaction-B0V0_SE2.mjs.map} +1 -1
  52. package/dist/util/detect.mjs +3 -1
  53. package/dist/util/fs.mjs +2 -1
  54. package/dist/util/id.d.mts +2 -1
  55. package/dist/util/id.d.mts.map +1 -1
  56. package/dist/util/id.mjs +1 -1
  57. package/dist/util/serializer.d.mts +2 -14
  58. package/dist/util/serializer.mjs +2 -2
  59. package/dist/{validator-CyckZAVr.mjs → validator-SQYUV2JP.mjs} +23 -197
  60. package/dist/validator-SQYUV2JP.mjs.map +1 -0
  61. package/package.json +3 -3
  62. package/dist/id-BdD4HvMY.mjs.map +0 -1
  63. package/dist/model-manager-kl78AAMg.mjs.map +0 -1
  64. package/dist/serializer-Dueih-i2.mjs +0 -33
  65. package/dist/serializer-Dueih-i2.mjs.map +0 -1
  66. package/dist/util/serializer.d.mts.map +0 -1
  67. package/dist/validator-CyckZAVr.mjs.map +0 -1
@@ -1,75 +1,11 @@
1
- import { a as readJson, c as writeText, i as readDir, o as readText, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-BbuGnVcG.mjs";
2
- import { t as readConfig } from "./config-CPlhDwsv.mjs";
3
- import { r as writeMeta } from "./meta-manager-CECrfsKm.mjs";
4
- import { _ as resolveMdFilePath, f as parseFrontmatter, g as resolveLocaleStrategy, h as resolveJsonFilePath, m as resolveContentDir, o as listModels, s as readModel } from "./model-manager-kl78AAMg.mjs";
1
+ import { a as readJson, c as writeText, i as readDir, o as readText, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-BnIQ4dKj.mjs";
2
+ import { t as readConfig } from "./config-B5kc2oeV.mjs";
3
+ import { r as writeMeta } from "./meta-manager--0IwAPCl.mjs";
4
+ import { _ as resolveMdFilePath, f as parseFrontmatter, g as resolveLocaleStrategy, h as resolveJsonFilePath, m as resolveContentDir, o as listModels, s as readModel } from "./model-manager-DW_fOGRM.mjs";
5
5
  import { join } from "node:path";
6
6
  import { rm } from "node:fs/promises";
7
+ import { detectSecrets, validateFieldValue } from "@contentrain/types";
7
8
  //#region src/core/validator.ts
8
- const SECRET_PATTERNS = [
9
- /sk_/,
10
- /pk_/,
11
- /api_key/i,
12
- /apikey/i,
13
- /ghp_/,
14
- /gho_/,
15
- /Bearer /,
16
- /postgres:\/\//,
17
- /mysql:\/\//,
18
- /mongodb:\/\//,
19
- /-----BEGIN/,
20
- /AKIA/,
21
- /aws_secret/i
22
- ];
23
- function checkSecret(value) {
24
- if (typeof value !== "string") return false;
25
- return SECRET_PATTERNS.some((p) => p.test(value));
26
- }
27
- function fieldTypeMatches(value, fieldDef) {
28
- if (value === null || value === void 0) return true;
29
- switch (fieldDef.type) {
30
- case "string":
31
- case "text":
32
- case "email":
33
- case "url":
34
- case "slug":
35
- case "color":
36
- case "phone":
37
- case "code":
38
- case "icon":
39
- case "markdown":
40
- case "richtext":
41
- case "date":
42
- case "datetime":
43
- case "image":
44
- case "video":
45
- case "file":
46
- case "select": return typeof value === "string";
47
- case "number":
48
- case "integer":
49
- case "decimal":
50
- case "percent":
51
- case "rating": return typeof value === "number";
52
- case "boolean": return typeof value === "boolean";
53
- case "relation": return typeof value === "string";
54
- case "relations":
55
- case "array": return Array.isArray(value);
56
- case "object": return typeof value === "object" && !Array.isArray(value);
57
- default: return true;
58
- }
59
- }
60
- function checkMinMax(value, fieldDef) {
61
- if (fieldDef.min !== void 0) {
62
- if (typeof value === "number" && value < fieldDef.min) return `Value ${value} is below minimum ${fieldDef.min}`;
63
- if (typeof value === "string" && value.length < fieldDef.min) return `String length ${value.length} is below minimum ${fieldDef.min}`;
64
- if (Array.isArray(value) && value.length < fieldDef.min) return `Array length ${value.length} is below minimum ${fieldDef.min}`;
65
- }
66
- if (fieldDef.max !== void 0) {
67
- if (typeof value === "number" && value > fieldDef.max) return `Value ${value} is above maximum ${fieldDef.max}`;
68
- if (typeof value === "string" && value.length > fieldDef.max) return `String length ${value.length} is above maximum ${fieldDef.max}`;
69
- if (Array.isArray(value) && value.length > fieldDef.max) return `Array length ${value.length} is above maximum ${fieldDef.max}`;
70
- }
71
- return null;
72
- }
73
9
  function validateScheduleFields(meta, ctx, issues) {
74
10
  if (meta.publish_at !== void 0) {
75
11
  const d = new Date(meta.publish_at);
@@ -188,7 +124,7 @@ async function validateCollectionModel(projectRoot, model, config, issues, fix)
188
124
  }
189
125
  for (const [entryId, fields] of Object.entries(data)) {
190
126
  entriesChecked++;
191
- for (const [fieldName, value] of Object.entries(fields)) if (checkSecret(value)) issues.push({
127
+ for (const [fieldName, value] of Object.entries(fields)) if (detectSecrets(value).length > 0) issues.push({
192
128
  severity: "error",
193
129
  model: model.id,
194
130
  locale,
@@ -199,52 +135,14 @@ async function validateCollectionModel(projectRoot, model, config, issues, fix)
199
135
  if (!model.fields) continue;
200
136
  for (const [fieldName, fieldDef] of Object.entries(model.fields)) {
201
137
  const value = fields[fieldName];
202
- if (fieldDef.required && (value === void 0 || value === null || value === "")) issues.push({
203
- severity: "error",
138
+ const fieldErrors = validateFieldValue(value, fieldDef);
139
+ for (const err of fieldErrors) issues.push({
140
+ ...err,
204
141
  model: model.id,
205
142
  locale,
206
143
  entry: entryId,
207
- field: fieldName,
208
- message: `Required field "${fieldName}" is missing`
144
+ field: fieldName
209
145
  });
210
- if (value !== void 0 && value !== null && !fieldTypeMatches(value, fieldDef)) issues.push({
211
- severity: "error",
212
- model: model.id,
213
- locale,
214
- entry: entryId,
215
- field: fieldName,
216
- message: `Type mismatch: expected ${fieldDef.type}, got ${typeof value}`
217
- });
218
- if (value !== void 0 && value !== null) {
219
- const minMaxErr = checkMinMax(value, fieldDef);
220
- if (minMaxErr) issues.push({
221
- severity: "error",
222
- model: model.id,
223
- locale,
224
- entry: entryId,
225
- field: fieldName,
226
- message: minMaxErr
227
- });
228
- }
229
- if (fieldDef.pattern && typeof value === "string") try {
230
- if (!new RegExp(fieldDef.pattern).test(value)) issues.push({
231
- severity: "error",
232
- model: model.id,
233
- locale,
234
- entry: entryId,
235
- field: fieldName,
236
- message: `Value "${value}" does not match pattern /${fieldDef.pattern}/`
237
- });
238
- } catch {
239
- issues.push({
240
- severity: "warning",
241
- model: model.id,
242
- locale,
243
- entry: entryId,
244
- field: fieldName,
245
- message: `Invalid pattern regex: ${fieldDef.pattern}`
246
- });
247
- }
248
146
  if (fieldDef.unique && value !== void 0 && value !== null) {
249
147
  const key = `${fieldName}:${locale}`;
250
148
  if (!uniqueFieldValues[key]) uniqueFieldValues[key] = /* @__PURE__ */ new Map();
@@ -365,7 +263,7 @@ async function validateSingletonModel(projectRoot, model, config, issues, fix) {
365
263
  continue;
366
264
  }
367
265
  entriesChecked++;
368
- for (const [fieldName, value] of Object.entries(data)) if (checkSecret(value)) issues.push({
266
+ for (const [fieldName, value] of Object.entries(data)) if (detectSecrets(value).length > 0) issues.push({
369
267
  severity: "error",
370
268
  model: model.id,
371
269
  locale,
@@ -374,47 +272,13 @@ async function validateSingletonModel(projectRoot, model, config, issues, fix) {
374
272
  });
375
273
  if (model.fields) for (const [fieldName, fieldDef] of Object.entries(model.fields)) {
376
274
  const value = data[fieldName];
377
- if (fieldDef.required && (value === void 0 || value === null || value === "")) issues.push({
378
- severity: "error",
275
+ const sFieldErrors = validateFieldValue(value, fieldDef);
276
+ for (const err of sFieldErrors) issues.push({
277
+ ...err,
379
278
  model: model.id,
380
279
  locale,
381
- field: fieldName,
382
- message: `Required field "${fieldName}" is missing`
280
+ field: fieldName
383
281
  });
384
- if (value !== void 0 && value !== null && !fieldTypeMatches(value, fieldDef)) issues.push({
385
- severity: "error",
386
- model: model.id,
387
- locale,
388
- field: fieldName,
389
- message: `Type mismatch: expected ${fieldDef.type}, got ${typeof value}`
390
- });
391
- if (value !== void 0 && value !== null) {
392
- const minMaxErr = checkMinMax(value, fieldDef);
393
- if (minMaxErr) issues.push({
394
- severity: "error",
395
- model: model.id,
396
- locale,
397
- field: fieldName,
398
- message: minMaxErr
399
- });
400
- }
401
- if (fieldDef.pattern && typeof value === "string") try {
402
- if (!new RegExp(fieldDef.pattern).test(value)) issues.push({
403
- severity: "error",
404
- model: model.id,
405
- locale,
406
- field: fieldName,
407
- message: `Value "${value}" does not match pattern /${fieldDef.pattern}/`
408
- });
409
- } catch {
410
- issues.push({
411
- severity: "warning",
412
- model: model.id,
413
- locale,
414
- field: fieldName,
415
- message: `Invalid pattern regex: ${fieldDef.pattern}`
416
- });
417
- }
418
282
  await checkRelation(projectRoot, fieldName, fieldDef, value, locale, {
419
283
  model: model.id,
420
284
  locale
@@ -472,7 +336,7 @@ async function validateDictionaryModel(projectRoot, model, config, issues, fix)
472
336
  }
473
337
  entriesChecked++;
474
338
  localeKeys[locale] = new Set(Object.keys(data));
475
- for (const [key, value] of Object.entries(data)) if (checkSecret(value)) issues.push({
339
+ for (const [key, value] of Object.entries(data)) if (detectSecrets(value).length > 0) issues.push({
476
340
  severity: "error",
477
341
  model: model.id,
478
342
  locale,
@@ -585,7 +449,7 @@ async function validateDocumentModel(projectRoot, model, config, issues, fix) {
585
449
  }
586
450
  entriesChecked++;
587
451
  const { frontmatter, body } = parseFrontmatter(raw);
588
- for (const [fieldName, value] of Object.entries(frontmatter)) if (checkSecret(value)) issues.push({
452
+ for (const [fieldName, value] of Object.entries(frontmatter)) if (detectSecrets(value).length > 0) issues.push({
589
453
  severity: "error",
590
454
  model: model.id,
591
455
  locale,
@@ -593,7 +457,7 @@ async function validateDocumentModel(projectRoot, model, config, issues, fix) {
593
457
  field: fieldName,
594
458
  message: `Potential secret detected in field "${fieldName}"`
595
459
  });
596
- if (checkSecret(body)) issues.push({
460
+ if (detectSecrets(body).length > 0) issues.push({
597
461
  severity: "error",
598
462
  model: model.id,
599
463
  locale,
@@ -604,52 +468,14 @@ async function validateDocumentModel(projectRoot, model, config, issues, fix) {
604
468
  if (model.fields) for (const [fieldName, fieldDef] of Object.entries(model.fields)) {
605
469
  if (fieldName === "body") continue;
606
470
  const value = frontmatter[fieldName];
607
- if (fieldDef.required && (value === void 0 || value === null || value === "")) issues.push({
608
- severity: "error",
609
- model: model.id,
610
- locale,
611
- slug,
612
- field: fieldName,
613
- message: `Required field "${fieldName}" is missing`
614
- });
615
- if (value !== void 0 && value !== null && !fieldTypeMatches(value, fieldDef)) issues.push({
616
- severity: "error",
471
+ const dFieldErrors = validateFieldValue(value, fieldDef);
472
+ for (const err of dFieldErrors) issues.push({
473
+ ...err,
617
474
  model: model.id,
618
475
  locale,
619
476
  slug,
620
- field: fieldName,
621
- message: `Type mismatch: expected ${fieldDef.type}, got ${typeof value}`
477
+ field: fieldName
622
478
  });
623
- if (value !== void 0 && value !== null) {
624
- const minMaxErr = checkMinMax(value, fieldDef);
625
- if (minMaxErr) issues.push({
626
- severity: "error",
627
- model: model.id,
628
- locale,
629
- slug,
630
- field: fieldName,
631
- message: minMaxErr
632
- });
633
- }
634
- if (fieldDef.pattern && typeof value === "string") try {
635
- if (!new RegExp(fieldDef.pattern).test(value)) issues.push({
636
- severity: "error",
637
- model: model.id,
638
- locale,
639
- slug,
640
- field: fieldName,
641
- message: `Value "${value}" does not match pattern /${fieldDef.pattern}/`
642
- });
643
- } catch {
644
- issues.push({
645
- severity: "warning",
646
- model: model.id,
647
- locale,
648
- slug,
649
- field: fieldName,
650
- message: `Invalid pattern regex: ${fieldDef.pattern}`
651
- });
652
- }
653
479
  await checkRelation(projectRoot, fieldName, fieldDef, value, locale, {
654
480
  model: model.id,
655
481
  locale,
@@ -752,4 +578,4 @@ async function validateProject(projectRoot, options) {
752
578
  //#endregion
753
579
  export { validateProject as t };
754
580
 
755
- //# sourceMappingURL=validator-CyckZAVr.mjs.map
581
+ //# sourceMappingURL=validator-SQYUV2JP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator-SQYUV2JP.mjs","names":[],"sources":["../src/core/validator.ts"],"sourcesContent":["import type { ValidationError, ModelDefinition, ContentrainConfig, FieldDef, EntryMeta } from '@contentrain/types'\nimport { detectSecrets, validateFieldValue } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { contentrainDir, readDir, readJson, readText, writeJson, writeText, pathExists } from '../util/fs.js'\nimport { readConfig } from './config.js'\nimport { listModels, readModel } from './model-manager.js'\nimport { writeMeta } from './meta-manager.js'\nimport { parseFrontmatter, resolveContentDir, resolveJsonFilePath, resolveMdFilePath, resolveLocaleStrategy } from './content-manager.js'\n\n// ─── Types ───\n\nexport interface ValidateOptions {\n model?: string\n fix?: boolean\n}\n\nexport interface ValidateResult {\n valid: boolean\n summary: {\n errors: number\n warnings: number\n notices: number\n models_checked: number\n entries_checked: number\n }\n issues: ValidationError[]\n fixed: number\n}\n\n// ─── Schedule validation ───\n\nfunction validateScheduleFields(\n meta: EntryMeta,\n ctx: { model: string; locale: string; entry?: string; slug?: string },\n issues: ValidationError[],\n): void {\n if (meta.publish_at !== undefined) {\n const d = new Date(meta.publish_at)\n if (Number.isNaN(d.getTime())) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `Invalid publish_at date: \"${meta.publish_at}\". Must be a valid ISO 8601 date string.`,\n })\n }\n }\n if (meta.expire_at !== undefined) {\n const d = new Date(meta.expire_at)\n if (Number.isNaN(d.getTime())) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `Invalid expire_at date: \"${meta.expire_at}\". Must be a valid ISO 8601 date string.`,\n })\n }\n }\n if (meta.publish_at !== undefined && meta.expire_at !== undefined) {\n const pubDate = new Date(meta.publish_at)\n const expDate = new Date(meta.expire_at)\n if (!Number.isNaN(pubDate.getTime()) && !Number.isNaN(expDate.getTime()) && expDate <= pubDate) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `expire_at (\"${meta.expire_at}\") must be after publish_at (\"${meta.publish_at}\").`,\n })\n }\n }\n}\n\n// ─── Shared field validation ───\n\ninterface FieldContext {\n model: string\n locale: string\n entry?: string\n slug?: string\n}\n\nasync function checkRelation(\n projectRoot: string,\n fieldName: string,\n fieldDef: FieldDef,\n value: unknown,\n locale: string,\n ctx: FieldContext,\n issues: ValidationError[],\n): Promise<void> {\n if ((fieldDef.type !== 'relation' && fieldDef.type !== 'relations') || value === undefined || value === null) return\n\n const targets = Array.isArray(fieldDef.model) ? fieldDef.model : fieldDef.model ? [fieldDef.model] : []\n const refs = fieldDef.type === 'relations' && Array.isArray(value) ? value : [value]\n\n for (const ref of refs) {\n if (typeof ref !== 'string') continue\n let found = false\n for (const targetModelId of targets) {\n const targetModel = await readModel(projectRoot, targetModelId)\n if (!targetModel) {\n issues.push({\n severity: 'error',\n ...ctx,\n field: fieldName,\n message: `Broken relation: target model \"${targetModelId}\" not found`,\n })\n found = true\n break\n }\n\n if (targetModel.kind === 'document') {\n // Document relations reference by slug — use locale-strategy-aware discovery\n const docContentDir = resolveContentDir(projectRoot, targetModel)\n const slugs = await discoverDocumentSlugs(docContentDir, targetModel)\n if (slugs.includes(ref)) {\n found = true\n break\n }\n } else if (targetModel.kind === 'singleton' || targetModel.kind === 'dictionary') {\n // Relations to singletons/dictionaries are unusual but shouldn't error\n found = true\n break\n } else {\n // Collection: validate against entry IDs (object-map keys)\n const targetDir = resolveContentDir(projectRoot, targetModel)\n const targetData = await readJson<Record<string, unknown>>(\n resolveJsonFilePath(targetDir, targetModel, locale),\n )\n if (targetData && ref in targetData) {\n found = true\n break\n }\n }\n }\n if (!found) {\n issues.push({\n severity: 'error',\n ...ctx,\n field: fieldName,\n message: `Broken relation: referenced ${targets.some(t => t) ? 'ref' : 'ID'} \"${ref}\" not found in target model(s)`,\n })\n }\n }\n}\n\n// ─── Per-model validators ───\n\nasync function validateCollectionModel(\n projectRoot: string,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n const cDir = resolveContentDir(projectRoot, model)\n const metaDir = join(contentrainDir(projectRoot), 'meta', model.id)\n const locales = config.locales.supported\n\n // Collect all entry IDs per locale for parity check\n const localeEntryIds: Record<string, Set<string>> = {}\n const allEntryIds = new Set<string>()\n\n // Collect unique values for unique constraint check\n const uniqueFieldValues: Record<string, Map<string, string>> = {}\n\n for (const locale of locales) {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, Record<string, unknown>>>(filePath)\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n\n if (fix) {\n await writeJson(filePath, {})\n fixed++\n }\n }\n continue\n }\n\n const ids = new Set(Object.keys(data))\n localeEntryIds[locale] = ids\n for (const id of ids) allEntryIds.add(id)\n\n // Check canonical sort\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix) {\n const resorted: Record<string, Record<string, unknown>> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(filePath, resorted)\n fixed++\n }\n }\n\n // Validate each entry\n for (const [entryId, fields] of Object.entries(data)) {\n entriesChecked++\n\n // Secret detection\n for (const [fieldName, value] of Object.entries(fields)) {\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n entry: entryId,\n field: fieldName,\n message: `Potential secret detected in field \"${fieldName}\"`,\n })\n }\n }\n\n if (!model.fields) continue\n\n // Field-level validation\n for (const [fieldName, fieldDef] of Object.entries(model.fields)) {\n const value = fields[fieldName]\n\n // Field schema validation (type, required, min/max, pattern, select)\n const fieldErrors = validateFieldValue(value, fieldDef)\n for (const err of fieldErrors) {\n issues.push({ ...err, model: model.id, locale, entry: entryId, field: fieldName })\n }\n\n // Unique constraint\n if (fieldDef.unique && value !== undefined && value !== null) {\n const key = `${fieldName}:${locale}`\n if (!uniqueFieldValues[key]) uniqueFieldValues[key] = new Map()\n const existing = uniqueFieldValues[key]!.get(String(value))\n if (existing) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n entry: entryId,\n field: fieldName,\n message: `Duplicate value \"${value}\" violates unique constraint (also in entry \"${existing}\")`,\n })\n } else {\n uniqueFieldValues[key]!.set(String(value), entryId)\n }\n }\n\n // Broken relation\n await checkRelation(projectRoot, fieldName, fieldDef, value, locale, { model: model.id, locale, entry: entryId }, issues)\n }\n }\n }\n\n // Entry parity check (i18n)\n if (model.i18n && Object.keys(localeEntryIds).length > 1) {\n const localeKeys = Object.keys(localeEntryIds)\n for (let i = 1; i < localeKeys.length; i++) {\n const locA = localeKeys[0]!\n const locB = localeKeys[i]!\n const idsA = localeEntryIds[locA]!\n const idsB = localeEntryIds[locB]!\n\n for (const id of idsA) {\n if (!idsB.has(id)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale: locB,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locA} but missing in ${locB}`,\n })\n }\n }\n for (const id of idsB) {\n if (!idsA.has(id)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale: locA,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locB} but missing in ${locA}`,\n })\n }\n }\n }\n }\n\n // Orphan meta check\n for (const locale of locales) {\n const metaPath = join(metaDir, `${locale}.json`)\n const metaData = await readJson<Record<string, EntryMeta>>(metaPath)\n const contentData = await readJson<Record<string, unknown>>(resolveJsonFilePath(cDir, model, locale)) ?? {}\n\n // Forward check: meta entries without content + schedule validation\n if (metaData) {\n for (const metaEntryId of Object.keys(metaData)) {\n // Validate schedule fields on every meta entry\n validateScheduleFields(metaData[metaEntryId]!, { model: model.id, locale, entry: metaEntryId }, issues)\n\n if (!(metaEntryId in contentData)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n entry: metaEntryId,\n message: `Orphan meta: meta entry \"${metaEntryId}\" exists but content entry missing`,\n })\n if (fix) {\n delete metaData[metaEntryId]\n if (Object.keys(metaData).length > 0) {\n await writeJson(metaPath, metaData)\n } else {\n await rm(metaPath, { force: true })\n }\n fixed++\n }\n }\n }\n }\n\n // Reverse check: content entries without meta\n for (const entryId of Object.keys(contentData)) {\n if (!metaData || !(entryId in metaData)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n entry: entryId,\n message: `Orphan content: entry \"${entryId}\" has no metadata`,\n })\n if (fix) {\n await writeMeta(projectRoot, model, { locale, entryId }, {\n status: 'draft',\n source: 'import',\n updated_by: 'contentrain-mcp',\n })\n fixed++\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function validateSingletonModel(\n projectRoot: string,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n const cDir = resolveContentDir(projectRoot, model)\n\n for (const locale of config.locales.supported) {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, unknown>>(filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n if (fix) {\n await writeJson(filePath, {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n\n // Secret detection\n for (const [fieldName, value] of Object.entries(data)) {\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: fieldName,\n message: `Potential secret detected in field \"${fieldName}\"`,\n })\n }\n }\n\n // Field validation\n if (model.fields) {\n for (const [fieldName, fieldDef] of Object.entries(model.fields)) {\n const value = data[fieldName]\n\n // Field schema validation (type, required, min/max, pattern, select)\n const sFieldErrors = validateFieldValue(value, fieldDef)\n for (const err of sFieldErrors) {\n issues.push({ ...err, model: model.id, locale, field: fieldName })\n }\n\n // Broken relation\n await checkRelation(projectRoot, fieldName, fieldDef, value, locale, { model: model.id, locale }, issues)\n }\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix) {\n const resorted: Record<string, unknown> = {}\n for (const key of sorted) {\n resorted[key] = data[key]\n }\n await writeJson(filePath, resorted)\n fixed++\n }\n }\n\n // Validate schedule fields in singleton meta\n const singletonMetaPath = join(contentrainDir(projectRoot), 'meta', model.id, `${locale}.json`)\n const singletonMetaData = await readJson<EntryMeta>(singletonMetaPath)\n if (singletonMetaData) {\n validateScheduleFields(singletonMetaData, { model: model.id, locale }, issues)\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function validateDictionaryModel(\n projectRoot: string,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n const cDir = resolveContentDir(projectRoot, model)\n\n const localeKeys: Record<string, Set<string>> = {}\n\n for (const locale of config.locales.supported) {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, string>>(filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n if (fix) {\n await writeJson(filePath, {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n localeKeys[locale] = new Set(Object.keys(data))\n\n // Secret detection\n for (const [key, value] of Object.entries(data)) {\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: key,\n message: `Potential secret detected in key \"${key}\"`,\n })\n }\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix) {\n const resorted: Record<string, string> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(filePath, resorted)\n fixed++\n }\n }\n }\n\n // Key parity check\n if (model.i18n && Object.keys(localeKeys).length > 1) {\n const localeNames = Object.keys(localeKeys)\n for (let i = 1; i < localeNames.length; i++) {\n const locA = localeNames[0]!\n const locB = localeNames[i]!\n const keysA = localeKeys[locA]!\n const keysB = localeKeys[locB]!\n\n for (const k of keysA) {\n if (!keysB.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locB,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locA} but missing in ${locB}`,\n })\n }\n }\n for (const k of keysB) {\n if (!keysA.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locA,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locB} but missing in ${locA}`,\n })\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function discoverDocumentSlugs(\n cDir: string,\n model: ModelDefinition,\n): Promise<string[]> {\n const strategy = resolveLocaleStrategy(model)\n const entries = await readDir(cDir)\n\n // When i18n is disabled, documents are flat {slug}.md files\n if (!model.i18n) {\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n }\n\n if (strategy === 'file') {\n // Each slug is a subdirectory\n return entries.filter(e => !e.startsWith('.'))\n }\n\n if (strategy === 'suffix') {\n // Files like {slug}.{locale}.md — extract unique slugs\n const slugs = new Set<string>()\n for (const f of entries) {\n if (!f.endsWith('.md')) continue\n // Remove last two dot-separated segments (.locale.md)\n const parts = f.replace(/\\.md$/, '').split('.')\n if (parts.length >= 2) {\n parts.pop() // remove locale\n slugs.add(parts.join('.'))\n }\n }\n return [...slugs]\n }\n\n if (strategy === 'directory') {\n // Locale subdirectories contain {slug}.md files — collect slugs from all locale dirs\n const slugs = new Set<string>()\n for (const localeDir of entries) {\n if (localeDir.startsWith('.')) continue\n const files = await readDir(join(cDir, localeDir))\n for (const f of files) {\n if (f.endsWith('.md')) slugs.add(f.replace(/\\.md$/, ''))\n }\n }\n return [...slugs]\n }\n\n // 'none' — {slug}.md files, no locale\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n}\n\nasync function validateDocumentModel(\n projectRoot: string,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n const cDir = resolveContentDir(projectRoot, model)\n\n if (!await pathExists(cDir)) return { entries: 0, fixed: 0 }\n\n const slugs = await discoverDocumentSlugs(cDir, model)\n const locales = config.locales.supported\n\n for (const slug of slugs) {\n if (slug.startsWith('.')) continue\n\n for (const locale of locales) {\n const filePath = resolveMdFilePath(cDir, model, locale, slug)\n const raw = await readText(filePath)\n\n if (!raw) {\n if (model.i18n) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n slug,\n message: `Missing translation: document \"${slug}\" missing ${locale} locale file`,\n })\n if (fix) {\n // Create empty template\n const template = `---\\nslug: ${slug}\\n---\\n`\n await writeText(filePath, template)\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n const { frontmatter, body } = parseFrontmatter(raw)\n\n // Secret detection in frontmatter\n for (const [fieldName, value] of Object.entries(frontmatter)) {\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: fieldName,\n message: `Potential secret detected in field \"${fieldName}\"`,\n })\n }\n }\n\n // Secret detection in body\n if (detectSecrets(body).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: 'body',\n message: 'Potential secret detected in document body',\n })\n }\n\n // Field validation\n if (model.fields) {\n for (const [fieldName, fieldDef] of Object.entries(model.fields)) {\n if (fieldName === 'body') continue\n const value = frontmatter[fieldName]\n\n // Field schema validation (type, required, min/max, pattern, select)\n const dFieldErrors = validateFieldValue(value, fieldDef)\n for (const err of dFieldErrors) {\n issues.push({ ...err, model: model.id, locale, slug, field: fieldName })\n }\n\n // Broken relation\n await checkRelation(projectRoot, fieldName, fieldDef, value, locale, { model: model.id, locale, slug }, issues)\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\n// ─── Orphan content check ───\n\nasync function checkOrphanContent(\n projectRoot: string,\n validModelIds: Set<string>,\n issues: ValidationError[],\n _fix: boolean,\n): Promise<number> {\n let fixed = 0\n const contentBase = join(contentrainDir(projectRoot), 'content')\n const domains = await readDir(contentBase)\n\n for (const domain of domains) {\n if (domain.startsWith('.')) continue\n const domainDir = join(contentBase, domain)\n const modelDirs = await readDir(domainDir)\n\n for (const modelDir of modelDirs) {\n if (modelDir.startsWith('.')) continue\n if (!validModelIds.has(modelDir)) {\n issues.push({\n severity: 'warning',\n model: modelDir,\n message: `Orphan content: content directory exists for deleted model \"${modelDir}\"`,\n })\n }\n }\n }\n\n return fixed\n}\n\n// ─── Main validation function ───\n\nexport async function validateProject(\n projectRoot: string,\n options?: ValidateOptions,\n): Promise<ValidateResult> {\n const issues: ValidationError[] = []\n const fix = options?.fix ?? false\n let totalEntries = 0\n let totalFixed = 0\n let modelsChecked = 0\n\n const config = await readConfig(projectRoot)\n if (!config) {\n return {\n valid: false,\n summary: { errors: 1, warnings: 0, notices: 0, models_checked: 0, entries_checked: 0 },\n issues: [{ severity: 'error', message: 'Project not initialized: config.json missing' }],\n fixed: 0,\n }\n }\n\n const modelSummaries = await listModels(projectRoot)\n const validModelIds = new Set(modelSummaries.map(m => m.id))\n const modelsToCheck = options?.model\n ? modelSummaries.filter(m => m.id === options.model)\n : modelSummaries\n\n for (const summary of modelsToCheck) {\n const model = await readModel(projectRoot, summary.id)\n if (!model) continue\n\n modelsChecked++\n let result: { entries: number; fixed: number }\n\n switch (model.kind) {\n case 'collection':\n result = await validateCollectionModel(projectRoot, model, config, issues, fix)\n break\n case 'singleton':\n result = await validateSingletonModel(projectRoot, model, config, issues, fix)\n break\n case 'dictionary':\n result = await validateDictionaryModel(projectRoot, model, config, issues, fix)\n break\n case 'document':\n result = await validateDocumentModel(projectRoot, model, config, issues, fix)\n break\n default:\n result = { entries: 0, fixed: 0 }\n }\n\n totalEntries += result.entries\n totalFixed += result.fixed\n }\n\n // Orphan content check (only when checking all models)\n if (!options?.model) {\n totalFixed += await checkOrphanContent(projectRoot, validModelIds, issues, fix)\n }\n\n const errors = issues.filter(i => i.severity === 'error').length\n const warnings = issues.filter(i => i.severity === 'warning').length\n const notices = issues.filter(i => i.severity === 'notice').length\n\n return {\n valid: errors === 0,\n summary: {\n errors,\n warnings,\n notices,\n models_checked: modelsChecked,\n entries_checked: totalEntries,\n },\n issues,\n fixed: totalFixed,\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/mcp",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "mcpName": "io.github.Contentrain/contentrain",
5
5
  "license": "MIT",
6
6
  "description": "Local-first MCP server for AI-generated content governance — 13 deterministic tools for any platform",
@@ -10,7 +10,7 @@
10
10
  "url": "git+https://github.com/Contentrain/ai.git",
11
11
  "directory": "packages/mcp"
12
12
  },
13
- "homepage": "https://github.com/Contentrain/ai/tree/main/packages/mcp",
13
+ "homepage": "https://ai.contentrain.io/packages/mcp",
14
14
  "bugs": {
15
15
  "url": "https://github.com/Contentrain/ai/issues"
16
16
  },
@@ -107,7 +107,7 @@
107
107
  "simple-git": "^3.27.0",
108
108
  "typescript": "^5.7.0",
109
109
  "zod": "^3.24.0",
110
- "@contentrain/types": "0.3.0"
110
+ "@contentrain/types": "0.4.1"
111
111
  },
112
112
  "optionalDependencies": {
113
113
  "@astrojs/compiler": "^2.10.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"id-BdD4HvMY.mjs","names":[],"sources":["../src/util/id.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\n\nexport function branchTimestamp(): string {\n const ts = Math.floor(Date.now() / 1000).toString()\n const suffix = randomUUID().slice(0, 4)\n return `${ts}-${suffix}`\n}\n\nexport function generateEntryId(): string {\n return randomUUID().replace(/-/g, '').slice(0, 12)\n}\n"],"mappings":";;AAEA,SAAgB,kBAA0B;AAGxC,QAAO,GAFI,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK,CAAC,UAAU,CAEtC,GADE,YAAY,CAAC,MAAM,GAAG,EAAE;;AAIzC,SAAgB,kBAA0B;AACxC,QAAO,YAAY,CAAC,QAAQ,MAAM,GAAG,CAAC,MAAM,GAAG,GAAG"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"model-manager-kl78AAMg.mjs","names":[],"sources":["../src/core/content-manager.ts","../src/core/model-manager.ts"],"sourcesContent":["import type { ModelDefinition, ContentrainConfig, EntryMeta, LocaleStrategy, DocumentEntry } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { contentrainDir, readDir, readJson, readText, writeJson, writeText } from '../util/fs.js'\nimport { generateEntryId } from '../util/id.js'\nimport { writeMeta, deleteMeta } from './meta-manager.js'\nimport { readModel } from './model-manager.js'\n\n// ─── Validation helpers ───\n\nconst SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/\nconst ENTRY_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,39}$/\nconst LOCALE_RE = /^[a-z]{2}(?:-[A-Z]{2})?$/\n\nexport function validateSlug(slug: string): string | null {\n if (!slug) return 'Slug is required'\n if (!SLUG_RE.test(slug)) return `Invalid slug \"${slug}\": must be kebab-case alphanumeric (a-z, 0-9, hyphens)`\n if (slug.startsWith('.') || slug.includes('..')) return `Invalid slug \"${slug}\": path traversal not allowed`\n return null\n}\n\nexport function validateEntryId(id: string): string | null {\n if (!ENTRY_ID_RE.test(id)) return `Invalid entry ID \"${id}\": must be 1-40 alphanumeric characters (hyphens and underscores allowed)`\n return null\n}\n\nexport function validateLocale(locale: string, config: ContentrainConfig): string | null {\n if (!LOCALE_RE.test(locale)) return `Invalid locale \"${locale}\": must be ISO 639-1 format (e.g. \"en\", \"tr\", \"pt-BR\")`\n if (!config.locales.supported.includes(locale)) {\n return `Locale \"${locale}\" is not in supported locales [${config.locales.supported.join(', ')}]. Update config first.`\n }\n return null\n}\n\n// ─── Frontmatter helpers ───\n\nexport function parseFrontmatter(content: string): { frontmatter: Record<string, unknown>; body: string } {\n // Normalize CRLF to LF for cross-platform compatibility\n const normalized = content.replace(/\\r\\n/g, '\\n')\n const match = normalized.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/)\n if (!match) return { frontmatter: {}, body: normalized }\n\n const frontmatterStr = match[1]!\n const body = match[2]!.trim()\n const frontmatter: Record<string, unknown> = {}\n\n let currentKey: string | null = null\n let currentArray: string[] | null = null\n\n for (const line of frontmatterStr.split('\\n')) {\n // Array item\n if (/^\\s+-\\s+/.test(line) && currentKey) {\n const value = line.replace(/^\\s+-\\s+/, '').trim()\n if (!currentArray) currentArray = []\n currentArray.push(value)\n continue\n }\n\n // Flush previous array\n if (currentKey && currentArray) {\n frontmatter[currentKey] = currentArray\n currentKey = null\n currentArray = null\n }\n\n const kvMatch = line.match(/^([\\w][\\w.-]*)\\s*:\\s*(.*)$/)\n if (!kvMatch) continue\n\n const key = kvMatch[1]!\n const rawValue = kvMatch[2]!.trim()\n\n if (rawValue === '') {\n currentKey = key\n currentArray = []\n continue\n }\n\n // Inline array: [item1, item2]\n if (rawValue.startsWith('[') && rawValue.endsWith(']')) {\n const items = rawValue.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean)\n frontmatter[key] = items\n continue\n }\n\n frontmatter[key] = parseYamlValue(rawValue)\n }\n\n // Flush last array\n if (currentKey && currentArray) {\n frontmatter[currentKey] = currentArray\n }\n\n return { frontmatter, body }\n}\n\nfunction parseYamlValue(raw: string): unknown {\n if (raw === 'true') return true\n if (raw === 'false') return false\n if (raw === 'null') return null\n if (/^-?\\d+$/.test(raw)) return parseInt(raw, 10)\n if (/^-?\\d+\\.\\d+$/.test(raw)) return parseFloat(raw)\n if ((raw.startsWith('\"') && raw.endsWith('\"')) || (raw.startsWith(\"'\") && raw.endsWith(\"'\"))) {\n return raw.slice(1, -1)\n }\n return raw\n}\n\nfunction yamlScalar(value: unknown): string {\n const str = String(value)\n // Quote strings that contain YAML-special characters\n if (str.includes(':') || str.includes('#') || str.includes('{') || str.includes('}')\n || str.includes('[') || str.includes(']') || str.includes('*') || str.includes('&')\n || str.includes('!') || str.includes('|') || str.includes('>') || str.includes(\"'\")\n || str.includes('\"') || str.includes('%') || str.includes('@') || str.includes('`')\n || str.startsWith('-') || str.startsWith('?')\n || str === 'true' || str === 'false' || str === 'null' || str === 'yes' || str === 'no'\n || str === '') {\n return `\"${str.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`\n }\n return str\n}\n\nfunction serializeYamlValue(value: unknown, indent: number): string[] {\n const pad = ' '.repeat(indent)\n if (value === null || value === undefined) return []\n if (typeof value === 'object' && !Array.isArray(value)) {\n const lines: string[] = []\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (v === null || v === undefined) continue\n if (typeof v === 'object' && !Array.isArray(v)) {\n lines.push(`${pad}${k}:`)\n lines.push(...serializeYamlValue(v, indent + 1))\n } else if (Array.isArray(v)) {\n lines.push(`${pad}${k}:`)\n for (const item of v) {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n const nested = serializeYamlValue(item, indent + 2)\n if (nested.length > 0) {\n // First line of nested object gets the \"- \" prefix\n lines.push(`${pad} - ${nested[0]!.trimStart()}`)\n lines.push(...nested.slice(1))\n }\n } else {\n lines.push(`${pad} - ${yamlScalar(item)}`)\n }\n }\n } else {\n lines.push(`${pad}${k}: ${yamlScalar(v)}`)\n }\n }\n return lines\n }\n if (Array.isArray(value)) {\n const lines: string[] = []\n for (const item of value) {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n const nested = serializeYamlValue(item, indent + 1)\n if (nested.length > 0) {\n lines.push(`${pad}- ${nested[0]!.trimStart()}`)\n lines.push(...nested.slice(1))\n }\n } else {\n lines.push(`${pad}- ${yamlScalar(item)}`)\n }\n }\n return lines\n }\n return [`${pad}${yamlScalar(value)}`]\n}\n\nfunction serializeYamlFields(data: Record<string, unknown>): string[] {\n const lines: string[] = []\n for (const [key, value] of Object.entries(data)) {\n if (key === 'body') continue\n if (value === null || value === undefined) continue\n if (typeof value === 'object' && !Array.isArray(value)) {\n lines.push(`${key}:`)\n lines.push(...serializeYamlValue(value, 1))\n } else if (Array.isArray(value)) {\n lines.push(`${key}:`)\n for (const item of value) {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n const nested = serializeYamlValue(item, 2)\n if (nested.length > 0) {\n lines.push(` - ${nested[0]!.trimStart()}`)\n lines.push(...nested.slice(1))\n }\n } else {\n lines.push(` - ${yamlScalar(item)}`)\n }\n }\n } else {\n lines.push(`${key}: ${yamlScalar(value)}`)\n }\n }\n return lines\n}\n\nexport function serializeFrontmatter(data: Record<string, unknown>, body: string): string {\n // If body starts with frontmatter (---), merge model fields into body's frontmatter\n const trimmedBody = body.trimStart()\n if (trimmedBody.startsWith('---')) {\n const endIdx = trimmedBody.indexOf('---', 3)\n if (endIdx > 0) {\n const bodyFmContent = trimmedBody.slice(3, endIdx).trim()\n const afterFm = trimmedBody.slice(endIdx + 3)\n\n const lines: string[] = ['---']\n lines.push(...serializeYamlFields(data))\n // Body frontmatter fields (skip top-level duplicates from model fields)\n if (bodyFmContent) {\n const modelKeys = new Set(Object.keys(data))\n for (const line of bodyFmContent.split('\\n')) {\n // Only check top-level keys (lines that don't start with whitespace)\n if (line.length > 0 && line[0] !== ' ' && line[0] !== '-') {\n const colonIdx = line.indexOf(':')\n const lineKey = colonIdx > 0 ? line.slice(0, colonIdx).trim() : ''\n if (lineKey && modelKeys.has(lineKey)) continue\n }\n lines.push(line)\n }\n }\n lines.push('---')\n if (afterFm.trim()) {\n lines.push(afterFm.trimStart())\n }\n lines.push('')\n return lines.join('\\n')\n }\n }\n\n // Standard: model fields as frontmatter, body as markdown\n const lines: string[] = ['---']\n lines.push(...serializeYamlFields(data))\n lines.push('---')\n lines.push('')\n if (body) {\n lines.push(body)\n lines.push('')\n }\n return lines.join('\\n')\n}\n\n// ─── Content paths ───\n\nexport function resolveContentDir(projectRoot: string, model: ModelDefinition): string {\n if (model.content_path) {\n return join(projectRoot, model.content_path)\n }\n return join(contentrainDir(projectRoot), 'content', model.domain, model.id)\n}\n\nexport function resolveLocaleStrategy(model: ModelDefinition): LocaleStrategy {\n return model.locale_strategy ?? 'file'\n}\n\n/** Build the file path for a JSON content file (singleton/collection/dictionary) */\nexport function resolveJsonFilePath(dir: string, model: ModelDefinition, locale: string): string {\n // When i18n is disabled, always use data.json (locale parameter is ignored)\n if (!model.i18n) return join(dir, 'data.json')\n\n switch (resolveLocaleStrategy(model)) {\n case 'suffix': return join(dir, `${model.id}.${locale}.json`)\n case 'directory': return join(dir, locale, `${model.id}.json`)\n case 'none': return join(dir, `${model.id}.json`)\n case 'file':\n default: return join(dir, `${locale}.json`)\n }\n}\n\n/** Build the file path for a markdown document */\nexport function resolveMdFilePath(dir: string, model: ModelDefinition, locale: string, slug: string): string {\n // When i18n is disabled, always use {slug}.md (locale parameter is ignored)\n if (!model.i18n) return join(dir, `${slug}.md`)\n\n switch (resolveLocaleStrategy(model)) {\n case 'suffix': return join(dir, `${slug}.${locale}.md`)\n case 'directory': return join(dir, locale, `${slug}.md`)\n case 'none': return join(dir, `${slug}.md`)\n case 'file':\n default: return join(dir, slug, `${locale}.md`)\n }\n}\n\n// ─── Types ───\n\nexport interface ContentEntry {\n id?: string\n slug?: string\n locale?: string\n data: Record<string, unknown>\n}\n\nexport interface WriteResult {\n action: 'created' | 'updated'\n id?: string\n slug?: string\n locale: string\n}\n\nexport interface DeleteOpts {\n id?: string\n slug?: string\n locale?: string\n keys?: string[]\n}\n\nexport interface ListOpts {\n locale?: string\n filter?: Record<string, unknown>\n resolve?: boolean\n limit?: number\n offset?: number\n}\n\n// ─── Default meta ───\n\nfunction defaultMeta(data?: Record<string, unknown>): EntryMeta {\n const meta: EntryMeta = {\n status: 'draft',\n source: 'agent',\n updated_by: 'contentrain-mcp',\n }\n if (data?.['publish_at'] !== undefined) {\n meta.publish_at = data['publish_at'] as string\n }\n if (data?.['expire_at'] !== undefined) {\n meta.expire_at = data['expire_at'] as string\n }\n return meta\n}\n\n// ─── writeContent ───\n\nexport async function writeContent(\n projectRoot: string,\n model: ModelDefinition,\n entries: ContentEntry[],\n config: ContentrainConfig,\n): Promise<WriteResult[]> {\n const results: WriteResult[] = []\n const defaultLocale = config.locales.default\n\n for (const entry of entries) {\n const locale = entry.locale ?? defaultLocale\n\n // Validate locale\n const localeErr = validateLocale(locale, config)\n if (localeErr) throw new Error(localeErr)\n\n // Validate entry ID if provided\n if (entry.id) {\n const idErr = validateEntryId(entry.id)\n if (idErr) throw new Error(idErr)\n }\n\n // Validate slug if provided\n if (entry.slug) {\n const slugErr = validateSlug(entry.slug)\n if (slugErr) throw new Error(slugErr)\n }\n\n switch (model.kind) {\n case 'singleton': {\n const filePath = resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale)\n await writeJson(filePath, entry.data)\n await writeMeta(projectRoot, model, { locale }, defaultMeta(entry.data))\n results.push({ action: 'updated', locale })\n break\n }\n\n case 'collection': {\n const isNew = !entry.id\n const id = entry.id ?? generateEntryId()\n const filePath = resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale)\n const existing = await readJson<Record<string, Record<string, unknown>>>(filePath) ?? {}\n\n const action: 'created' | 'updated' = (isNew || !(id in existing)) ? 'created' : 'updated'\n existing[id] = entry.data\n\n // Sort by entry ID for canonical output\n const sorted: Record<string, Record<string, unknown>> = {}\n for (const key of Object.keys(existing).toSorted()) {\n sorted[key] = existing[key]!\n }\n\n await writeJson(filePath, sorted)\n await writeMeta(projectRoot, model, { locale, entryId: id }, defaultMeta(entry.data))\n results.push({ action, id, locale })\n break\n }\n\n case 'document': {\n const slug = entry.slug ?? entry.data['slug'] as string\n if (!slug) throw new Error('Document entries require a slug')\n const slugErr = validateSlug(slug)\n if (slugErr) throw new Error(slugErr)\n\n const bodyContent = (entry.data['body'] as string) ?? ''\n const fmData = { ...entry.data }\n delete fmData['body']\n if (!fmData['slug']) fmData['slug'] = slug\n\n // Check if existing\n const docPath = resolveMdFilePath(resolveContentDir(projectRoot, model), model, locale, slug)\n const existingRaw = await readText(docPath)\n const action: 'created' | 'updated' = existingRaw ? 'updated' : 'created'\n\n const mdContent = serializeFrontmatter(fmData, bodyContent)\n await writeText(docPath, mdContent)\n await writeMeta(projectRoot, model, { locale, slug }, defaultMeta(entry.data))\n results.push({ action, slug, locale })\n break\n }\n\n case 'dictionary': {\n const filePath = resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale)\n const existing = await readJson<Record<string, string>>(filePath) ?? {}\n const collisions: string[] = []\n for (const key of Object.keys(entry.data as Record<string, string>)) {\n if (key in existing && existing[key] !== (entry.data as Record<string, string>)[key]) {\n collisions.push(key)\n }\n }\n if (collisions.length > 0) {\n throw new Error(\n `Dictionary \"${model.id}\" (${locale}): ${collisions.length} key collision(s) — [${collisions.join(', ')}] already exist with different values. ` +\n `Read existing keys with contentrain_content_list first, or include all keys in a single save call.`,\n )\n }\n const merged = { ...existing, ...entry.data as Record<string, string> }\n await writeJson(filePath, merged)\n await writeMeta(projectRoot, model, { locale }, defaultMeta(entry.data))\n results.push({ action: 'updated', locale })\n break\n }\n }\n }\n\n return results\n}\n\n// ─── deleteContent ───\n\nexport async function deleteContent(\n projectRoot: string,\n model: ModelDefinition,\n opts: DeleteOpts,\n): Promise<string[]> {\n const removed: string[] = []\n const cDir = resolveContentDir(projectRoot, model)\n\n switch (model.kind) {\n case 'collection': {\n if (!opts.id) throw new Error('Collection delete requires an entry ID')\n const locales = opts.locale\n ? [opts.locale]\n : (await readDir(cDir)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', '').replace(`${model.id}.`, ''))\n\n for (const loc of locales) {\n const filePath = resolveJsonFilePath(cDir, model, loc)\n const data = await readJson<Record<string, unknown>>(filePath)\n if (data && opts.id in data) {\n delete data[opts.id]\n await writeJson(filePath, data)\n removed.push(`content/${model.domain}/${model.id}/${loc}.json#${opts.id}`)\n }\n }\n\n for (const loc of locales) {\n await deleteMeta(projectRoot, model, { locale: loc, entryId: opts.id })\n }\n break\n }\n\n case 'document': {\n if (!opts.slug) throw new Error('Document delete requires a slug')\n const slugDelErr = validateSlug(opts.slug)\n if (slugDelErr) throw new Error(slugDelErr)\n\n const strategy = resolveLocaleStrategy(model)\n if (!model.i18n) {\n // No i18n: single {slug}.md file\n await rm(join(cDir, `${opts.slug}.md`), { force: true })\n } else if (strategy === 'file') {\n // slug is a directory — remove entire slug directory\n await rm(join(cDir, opts.slug), { recursive: true, force: true })\n } else if (opts.locale) {\n // Remove specific locale file\n const filePath = resolveMdFilePath(cDir, model, opts.locale, opts.slug)\n await rm(filePath, { force: true })\n } else {\n // Remove all locale files for this slug\n const files = await readDir(strategy === 'directory' ? cDir : cDir)\n for (const f of files) {\n if (strategy === 'suffix' && f.startsWith(`${opts.slug}.`) && f.endsWith('.md')) {\n await rm(join(cDir, f), { force: true })\n } else if (strategy === 'directory') {\n // f is a locale dir — remove slug.md from it\n await rm(join(cDir, f, `${opts.slug}.md`), { force: true })\n } else if (strategy === 'none') {\n if (f === `${opts.slug}.md`) await rm(join(cDir, f), { force: true })\n }\n }\n }\n removed.push(`${model.content_path ?? `content/${model.domain}/${model.id}`}/${opts.slug}`)\n\n await deleteMeta(projectRoot, model, { slug: opts.slug, locale: opts.locale })\n break\n }\n\n case 'singleton': {\n if (model.i18n && !opts.locale) throw new Error('Singleton delete requires a locale when i18n is enabled')\n const locale = opts.locale ?? 'data'\n const filePath = resolveJsonFilePath(cDir, model, locale)\n await rm(filePath, { force: true })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n await deleteMeta(projectRoot, model, { locale: model.i18n ? locale : undefined })\n break\n }\n\n case 'dictionary': {\n if (model.i18n && !opts.locale) throw new Error('Dictionary delete requires a locale when i18n is enabled')\n const locale = opts.locale ?? 'data'\n const filePath = resolveJsonFilePath(cDir, model, locale)\n\n if (opts.keys?.length) {\n // Delete specific keys from dictionary\n const existing = await readJson<Record<string, string>>(filePath) ?? {}\n const notFound: string[] = []\n for (const key of opts.keys) {\n if (key in existing) {\n delete existing[key]\n } else {\n notFound.push(key)\n }\n }\n if (notFound.length > 0) {\n throw new Error(`Dictionary \"${model.id}\" (${locale}): keys not found — [${notFound.join(', ')}]`)\n }\n await writeJson(filePath, existing)\n removed.push(...opts.keys.map(k => `${model.id}/${locale}:${k}`))\n } else {\n // Delete entire locale file\n await rm(filePath, { force: true })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n await deleteMeta(projectRoot, model, { locale: model.i18n ? locale : undefined })\n }\n break\n }\n }\n\n return removed\n}\n\n// ─── listContent ───\n\nexport async function listContent(\n projectRoot: string,\n model: ModelDefinition,\n opts: ListOpts,\n config: ContentrainConfig,\n): Promise<unknown> {\n const cDir = resolveContentDir(projectRoot, model)\n const locale = opts.locale ?? config.locales.default\n\n switch (model.kind) {\n case 'singleton': {\n const data = await readJson<Record<string, unknown>>(resolveJsonFilePath(cDir, model, locale))\n return { kind: 'singleton', data: data ?? {}, locale }\n }\n\n case 'collection': {\n const data = await readJson<Record<string, Record<string, unknown>>>(resolveJsonFilePath(cDir, model, locale)) ?? {}\n let entries: Array<Record<string, unknown>> = Object.entries(data).map(([id, fields]) => {\n const entry: Record<string, unknown> = { id }\n Object.assign(entry, fields)\n return entry\n })\n\n // Filter\n if (opts.filter) {\n entries = entries.filter(entry => {\n for (const [key, value] of Object.entries(opts.filter!)) {\n if (entry[key] !== value) return false\n }\n return true\n })\n }\n\n const total = entries.length\n\n // Pagination\n const offset = opts.offset ?? 0\n const limit = opts.limit ?? entries.length\n entries = entries.slice(offset, offset + limit)\n\n // Resolve relations\n if (opts.resolve && model.fields) {\n entries = await resolveRelations(projectRoot, model, entries, locale)\n }\n\n return { kind: 'collection', data: entries, total, locale, offset, limit }\n }\n\n case 'document': {\n const entries: DocumentEntry[] = []\n const strategy = resolveLocaleStrategy(model)\n\n if (!model.i18n) {\n // No i18n: flat {slug}.md files, no locale in path\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else if (strategy === 'file') {\n // Each slug is a subdirectory containing locale.md files\n const slugDirs = await readDir(cDir)\n for (const slug of slugDirs) {\n const raw = await readText(join(cDir, slug, `${locale}.md`))\n if (!raw) continue\n const { frontmatter, body } = parseFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else if (strategy === 'suffix') {\n // Files like {slug}.{locale}.md in flat directory\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) {\n if (!f.endsWith(suffix)) continue\n const slug = f.slice(0, -suffix.length)\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else if (strategy === 'directory') {\n // Files in {locale}/ directory as {slug}.md\n const localeDir = join(cDir, locale)\n const files = await readDir(localeDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(localeDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else {\n // none — {slug}.md, no locale in filename\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n }\n\n const total = entries.length\n const offset = opts.offset ?? 0\n const limit = opts.limit ?? entries.length\n const paged = entries.slice(offset, offset + limit)\n\n return { kind: 'document', data: paged, total, locale, offset, limit }\n }\n\n case 'dictionary': {\n const data = await readJson<Record<string, string>>(resolveJsonFilePath(cDir, model, locale)) ?? {}\n return { kind: 'dictionary', data, total_keys: Object.keys(data).length, locale }\n }\n }\n}\n\n// ─── readContent (for external use / relation resolving) ───\n\nexport async function readContent(\n projectRoot: string,\n model: ModelDefinition,\n opts: { locale: string; entryId?: string; slug?: string },\n): Promise<unknown> {\n const cDir = resolveContentDir(projectRoot, model)\n\n switch (model.kind) {\n case 'singleton':\n return readJson<Record<string, unknown>>(resolveJsonFilePath(cDir, model, opts.locale))\n\n case 'collection': {\n if (!opts.entryId) return null\n const data = await readJson<Record<string, Record<string, unknown>>>(resolveJsonFilePath(cDir, model, opts.locale))\n return data?.[opts.entryId] ? { id: opts.entryId, ...data[opts.entryId] } : null\n }\n\n case 'document': {\n if (!opts.slug) return null\n const raw = await readText(resolveMdFilePath(cDir, model, opts.locale, opts.slug))\n if (!raw) return null\n const { frontmatter, body } = parseFrontmatter(raw)\n return { slug: opts.slug, ...frontmatter, body }\n }\n\n case 'dictionary':\n return readJson<Record<string, string>>(resolveJsonFilePath(cDir, model, opts.locale))\n }\n}\n\n// ─── Relation resolving ───\n\nasync function resolveRelations(\n projectRoot: string,\n model: ModelDefinition,\n entries: Array<Record<string, unknown>>,\n locale: string,\n): Promise<Array<Record<string, unknown>>> {\n if (!model.fields) return entries\n\n const relationFields: Array<{ name: string; targetModels: string[]; multi: boolean }> = []\n for (const [name, field] of Object.entries(model.fields)) {\n if (field.type === 'relation' || field.type === 'relations') {\n const targets = Array.isArray(field.model) ? field.model : field.model ? [field.model] : []\n relationFields.push({ name, targetModels: targets, multi: field.type === 'relations' })\n }\n }\n\n if (relationFields.length === 0) return entries\n\n // Load target model contents into cache (with circular reference protection)\n const targetCache: Record<string, Record<string, Record<string, unknown>>> = {}\n const visited = new Set<string>([model.id])\n\n for (const rf of relationFields) {\n for (const targetModelId of rf.targetModels) {\n if (targetCache[targetModelId] || visited.has(targetModelId)) continue\n visited.add(targetModelId)\n\n const targetModel = await readModel(projectRoot, targetModelId)\n if (!targetModel) continue\n\n if (targetModel.kind === 'collection') {\n const targetData = await readJson<Record<string, Record<string, unknown>>>(\n resolveJsonFilePath(resolveContentDir(projectRoot, targetModel), targetModel, locale),\n ) ?? {}\n targetCache[targetModelId] = targetData\n } else if (targetModel.kind === 'document') {\n // Build slug-keyed cache by listing document entries for this locale\n const docCache: Record<string, Record<string, unknown>> = {}\n const cDir = resolveContentDir(projectRoot, targetModel)\n const strategy = resolveLocaleStrategy(targetModel)\n\n if (!targetModel.i18n) {\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else if (strategy === 'file') {\n const slugDirs = await readDir(cDir)\n for (const slug of slugDirs) {\n const raw = await readText(join(cDir, slug, `${locale}.md`))\n if (!raw) continue\n const { frontmatter, body } = parseFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else if (strategy === 'suffix') {\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) {\n if (!f.endsWith(suffix)) continue\n const slug = f.slice(0, -suffix.length)\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else if (strategy === 'directory') {\n const localeDir = join(cDir, locale)\n const files = await readDir(localeDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(localeDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else {\n // 'none' strategy\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n }\n\n targetCache[targetModelId] = docCache\n }\n }\n }\n\n return entries.map(entry => {\n const resolved = { ...entry }\n for (const rf of relationFields) {\n const value = resolved[rf.name]\n if (!value) continue\n\n if (rf.multi && Array.isArray(value)) {\n resolved[rf.name] = value.map(id => {\n for (const targetModelId of rf.targetModels) {\n const cached = targetCache[targetModelId]?.[id as string]\n if (cached) return { id, ...cached }\n }\n return id\n })\n } else if (typeof value === 'string') {\n for (const targetModelId of rf.targetModels) {\n const cached = targetCache[targetModelId]?.[value]\n if (cached) {\n resolved[rf.name] = { id: value, ...cached }\n break\n }\n }\n }\n }\n return resolved\n })\n}\n","import type { ModelDefinition, ModelSummary } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { z } from 'zod'\nimport { contentrainDir, ensureDir, readDir, readJson, writeJson } from '../util/fs.js'\nimport { resolveContentDir, resolveLocaleStrategy } from './content-manager.js'\n\nexport type { ModelSummary } from '@contentrain/types'\n\nexport async function listModels(projectRoot: string): Promise<ModelSummary[]> {\n const modelsDir = join(contentrainDir(projectRoot), 'models')\n const files = await readDir(modelsDir)\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n\n const models = await Promise.all(\n jsonFiles.map(file => readJson<ModelDefinition>(join(modelsDir, file))),\n )\n\n return models\n .filter((m): m is ModelDefinition => m !== null && !!m.id)\n .map(model => ({\n id: model.id,\n kind: model.kind,\n domain: model.domain,\n i18n: model.i18n,\n fields: model.fields ? Object.keys(model.fields).length : 0,\n }))\n .toSorted((a, b) => a.id.localeCompare(b.id, 'en'))\n}\n\nexport async function readModel(projectRoot: string, modelId: string): Promise<ModelDefinition | null> {\n const filePath = join(contentrainDir(projectRoot), 'models', `${modelId}.json`)\n return readJson<ModelDefinition>(filePath)\n}\n\nasync function countDocumentFileStrategy(\n contentDir: string,\n entries: string[],\n): Promise<{ total: number; locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n let total = 0\n\n const results = await Promise.all(\n entries.map(async (entry) => {\n const localeFiles = await readDir(join(contentDir, entry))\n return localeFiles\n .map(lf => lf.replace(/\\.(json|md|mdx)$/, ''))\n .filter((locale, i) => locale !== localeFiles[i])\n }),\n )\n\n for (const entryLocales of results) {\n for (const locale of entryLocales) {\n locales[locale] = (locales[locale] ?? 0) + 1\n total++\n }\n }\n\n return { total, locales }\n}\n\nasync function countDocumentSuffixStrategy(\n _contentDir: string,\n files: string[],\n): Promise<{ total: number; locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n const slugsByLocale: Record<string, Set<string>> = {}\n\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n // Pattern: {slug}.{locale}.md\n const match = f.match(/^(.+)\\.([a-z]{2}(?:-[A-Z]{2})?)\\.md$/)\n if (!match) continue\n const locale = match[2]!\n if (!slugsByLocale[locale]) slugsByLocale[locale] = new Set()\n slugsByLocale[locale].add(match[1]!)\n }\n\n let total = 0\n for (const [locale, slugs] of Object.entries(slugsByLocale)) {\n locales[locale] = slugs.size\n total += slugs.size\n }\n\n return { total, locales }\n}\n\nasync function countDocumentDirectoryStrategy(\n contentDir: string,\n localeDirs: string[],\n): Promise<{ total: number; locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n let total = 0\n\n const results = await Promise.all(\n localeDirs.map(async (localeDir) => {\n const files = await readDir(join(contentDir, localeDir))\n const mdFiles = files.filter(f => f.endsWith('.md'))\n return { locale: localeDir, count: mdFiles.length }\n }),\n )\n\n for (const { locale, count } of results) {\n locales[locale] = count\n total += count\n }\n\n return { total, locales }\n}\n\nasync function countDocumentNoneStrategy(\n projectRoot: string,\n modelId: string,\n files: string[],\n i18n: boolean,\n): Promise<{ total: number; locales: Record<string, number> }> {\n const mdFiles = files.filter(f => f.endsWith('.md'))\n\n if (!i18n) {\n return { total: mdFiles.length, locales: { _: mdFiles.length } }\n }\n\n // With i18n + none strategy, content files have no locale info.\n // Use meta/{modelId}/{slug}/ directories to determine locale counts.\n const metaDir = join(contentrainDir(projectRoot), 'meta', modelId)\n const locales: Record<string, number> = {}\n let total = 0\n\n const slugs = mdFiles.map(f => f.replace('.md', ''))\n const results = await Promise.all(\n slugs.map(async (slug) => {\n const metaFiles = await readDir(join(metaDir, slug))\n return metaFiles\n .filter(f => f.endsWith('.json'))\n .map(f => f.replace('.json', ''))\n }),\n )\n\n for (const slugLocales of results) {\n for (const locale of slugLocales) {\n locales[locale] = (locales[locale] ?? 0) + 1\n total++\n }\n }\n\n return { total, locales }\n}\n\nasync function countCollectionEntries(\n contentDir: string,\n jsonFiles: string[],\n): Promise<{ total: number; locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n let total = 0\n\n const results = await Promise.all(\n jsonFiles.map(async (file) => {\n const locale = file.replace(/\\.json$/, '')\n const data = await readJson<Record<string, unknown>>(join(contentDir, file))\n return { locale, count: data ? Object.keys(data).length : 0 }\n }),\n )\n\n for (const { locale, count } of results) {\n locales[locale] = count\n total += count\n }\n\n return { total, locales }\n}\n\nexport async function countEntries(\n projectRoot: string,\n model: ModelDefinition,\n): Promise<{ total: number; locales: Record<string, number> }> {\n const cDir = resolveContentDir(projectRoot, model)\n const strategy = resolveLocaleStrategy(model)\n const files = await readDir(cDir)\n\n if (model.kind === 'document') {\n if (!model.i18n) {\n // No i18n: flat {slug}.md files\n return countDocumentNoneStrategy(projectRoot, model.id, files, false)\n }\n\n switch (strategy) {\n case 'file':\n return countDocumentFileStrategy(cDir, files)\n case 'suffix':\n return countDocumentSuffixStrategy(cDir, files)\n case 'directory':\n return countDocumentDirectoryStrategy(cDir, files)\n case 'none':\n return countDocumentNoneStrategy(projectRoot, model.id, files, true)\n }\n }\n\n if (model.kind === 'collection') {\n if (!model.i18n) {\n // Non-i18n: single data.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n return countCollectionEntries(cDir, jsonFiles)\n }\n switch (strategy) {\n case 'suffix': {\n // Files: {model}.{locale}.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n const locales: Record<string, number> = {}\n for (const f of jsonFiles) {\n const match = f.match(/^.+\\.([a-z]{2}(?:-[A-Z]{2})?)\\.json$/)\n if (match) {\n const data = await readJson<Record<string, unknown>>(join(cDir, f))\n locales[match[1]!] = data ? Object.keys(data).length : 0\n }\n }\n const total = Object.values(locales).reduce((a, b) => a + b, 0)\n return { total, locales }\n }\n case 'directory': {\n // Dirs: {locale}/... containing json files\n const locales: Record<string, number> = {}\n let total = 0\n for (const localeDir of files) {\n const subFiles = await readDir(join(cDir, localeDir))\n const jsonFile = subFiles.find(f => f.endsWith('.json'))\n if (jsonFile) {\n const data = await readJson<Record<string, unknown>>(join(cDir, localeDir, jsonFile))\n const count = data ? Object.keys(data).length : 0\n locales[localeDir] = count\n total += count\n }\n }\n return { total, locales }\n }\n case 'none': {\n // Single file: {model}.json — count entries inside\n const noneFile = files.find(f => f === `${model.id}.json`)\n if (!noneFile) return { total: 0, locales: {} }\n const data = await readJson<Record<string, unknown>>(join(cDir, noneFile))\n const count = data ? Object.keys(data).length : 0\n return { total: count, locales: { _: count } }\n }\n default: {\n // 'file': {locale}.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n return countCollectionEntries(cDir, jsonFiles)\n }\n }\n }\n\n // singleton / dictionary — one entry per locale file\n if (!model.i18n) {\n // Non-i18n: single data.json\n const hasData = files.some(f => f === 'data.json')\n return { total: hasData ? 1 : 0, locales: {} }\n }\n\n switch (strategy) {\n case 'suffix': {\n // Files: {model}.{locale}.json\n const locales: Record<string, number> = {}\n for (const f of files) {\n const match = f.match(/^.+\\.([a-z]{2}(?:-[A-Z]{2})?)\\.json$/)\n if (match) locales[match[1]!] = 1\n }\n return { total: Object.keys(locales).length, locales }\n }\n case 'directory': {\n // Dirs: {locale}/...\n const locales: Record<string, number> = {}\n for (const localeDir of files) {\n const subFiles = await readDir(join(cDir, localeDir))\n if (subFiles.some(f => f.endsWith('.json'))) {\n locales[localeDir] = 1\n }\n }\n return { total: Object.keys(locales).length, locales }\n }\n case 'none': {\n // Single file: {model}.json\n const hasFile = files.some(f => f === `${model.id}.json`)\n return { total: hasFile ? 1 : 0, locales: {} }\n }\n default: {\n // 'file': {locale}.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n const locales: Record<string, number> = {}\n for (const file of jsonFiles) {\n locales[file.replace(/\\.json$/, '')] = 1\n }\n return { total: jsonFiles.length, locales }\n }\n }\n}\n\nconst MODEL_FIELD_ORDER = ['id', 'name', 'kind', 'domain', 'i18n', 'description', 'content_path', 'locale_strategy', 'fields']\n\nexport async function writeModel(projectRoot: string, model: ModelDefinition): Promise<void> {\n const filePath = join(contentrainDir(projectRoot), 'models', `${model.id}.json`)\n await writeJson(filePath, model, MODEL_FIELD_ORDER)\n\n // Ensure content and meta directories exist (respects content_path)\n await ensureDir(resolveContentDir(projectRoot, model))\n await ensureDir(join(contentrainDir(projectRoot), 'meta', model.id))\n}\n\nexport async function deleteModel(projectRoot: string, modelId: string): Promise<string[]> {\n const model = await readModel(projectRoot, modelId)\n if (!model) return []\n\n const crDir = contentrainDir(projectRoot)\n const removed: string[] = []\n\n const modelPath = join(crDir, 'models', `${modelId}.json`)\n const contentPath = resolveContentDir(projectRoot, model)\n const metaPath = join(crDir, 'meta', modelId)\n\n await rm(modelPath, { force: true })\n removed.push(`models/${modelId}.json`)\n\n try {\n await rm(contentPath, { recursive: true, force: true })\n removed.push(model.content_path ?? `content/${model.domain}/${modelId}/`)\n } catch {\n // directory may not exist\n }\n\n try {\n await rm(metaPath, { recursive: true, force: true })\n removed.push(`meta/${modelId}/`)\n } catch {\n // directory may not exist\n }\n\n return removed\n}\n\nexport interface ModelReference {\n model: string\n field: string\n type: 'relation' | 'relations'\n}\n\nexport async function checkReferences(projectRoot: string, modelId: string): Promise<ModelReference[]> {\n const summaries = await listModels(projectRoot)\n const others = summaries.filter(s => s.id !== modelId)\n\n const models = await Promise.all(\n others.map(s => readModel(projectRoot, s.id)),\n )\n\n const refs: ModelReference[] = []\n for (const model of models) {\n if (!model?.fields) continue\n\n for (const [fieldName, fieldDef] of Object.entries(model.fields)) {\n if (fieldDef.type !== 'relation' && fieldDef.type !== 'relations') continue\n const targets = Array.isArray(fieldDef.model) ? fieldDef.model : [fieldDef.model]\n if (targets.includes(modelId)) {\n refs.push({ model: model.id, field: fieldName, type: fieldDef.type })\n }\n }\n }\n\n return refs\n}\n\n// ─── Shared Field Type Catalog ───\n\nexport const FIELD_TYPE_ENUM = [\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\n/**\n * Shared Zod schema for field definitions.\n * Used by both model_save and normalize extract for full parity.\n */\nexport const fieldDefZodSchema: z.ZodType<Record<string, unknown>> = z.record(z.string(), z.object({\n type: z.enum(FIELD_TYPE_ENUM).describe('Field type from the 27-type catalog'),\n required: z.boolean().optional(),\n unique: z.boolean().optional(),\n default: z.unknown().optional(),\n min: z.number().optional(),\n max: z.number().optional(),\n pattern: z.string().optional(),\n options: z.array(z.string()).optional(),\n model: z.union([z.string(), z.array(z.string())]).optional(),\n items: z.union([z.string(), z.lazy(() => z.record(z.string(), z.unknown()))]).optional(),\n fields: z.lazy(() => z.record(z.string(), z.unknown())).optional(),\n accept: z.string().optional(),\n maxSize: z.number().optional(),\n description: z.string().optional(),\n}).refine(\n (f) => {\n if ((f.type === 'relation' || f.type === 'relations') && !f.model) return false\n if (f.type === 'select' && (!f.options || f.options.length === 0)) return false\n return true\n },\n { message: 'relation/relations requires \"model\", select requires non-empty \"options\"' },\n))\n\n// ─── Model Validation (shared between model_save and normalize extract) ───\n\nconst VALID_FIELD_TYPES = new Set<string>(FIELD_TYPE_ENUM)\n\n/**\n * Validate a model definition before writing.\n * Returns array of error messages (empty = valid).\n * Used by both model_save tool and normalize extract.\n */\nexport function validateModelDefinition(input: { id: string; kind: string; fields?: Record<string, unknown> }): string[] {\n const errors: string[] = []\n\n // ID format: kebab-case\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.id)) {\n errors.push(`Invalid model ID \"${input.id}\": must be kebab-case`)\n }\n\n // Dictionary models cannot have fields\n if (input.kind === 'dictionary' && input.fields && Object.keys(input.fields).length > 0) {\n errors.push('Dictionary models cannot have fields. Dictionaries store flat key-value pairs.')\n }\n\n // Fields validation\n if (input.fields) {\n for (const [fieldName, fieldDef] of Object.entries(input.fields)) {\n const def = fieldDef as { type?: string; model?: unknown; options?: unknown }\n\n // Field name format\n if (!/^[a-z][a-z0-9_]*$/.test(fieldName)) {\n errors.push(`Field \"${fieldName}\": invalid name — must be snake_case starting with letter`)\n }\n\n // Type check\n if (!def.type || !VALID_FIELD_TYPES.has(def.type)) {\n errors.push(`Field \"${fieldName}\": invalid type \"${def.type}\"`)\n }\n\n // Relation requires model\n if ((def.type === 'relation' || def.type === 'relations') && !def.model) {\n errors.push(`Field \"${fieldName}\": ${def.type} type requires \"model\" property`)\n }\n\n // Select requires options\n if (def.type === 'select' && (!def.options || !Array.isArray(def.options) || def.options.length === 0)) {\n errors.push(`Field \"${fieldName}\": select type requires non-empty \"options\" array`)\n }\n }\n }\n\n return errors\n}\n"],"mappings":"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"}
@@ -1,33 +0,0 @@
1
- //#region src/util/serializer.ts
2
- /**
3
- * Canonical JSON serialization — deterministic output for git diff stability.
4
- *
5
- * Rules (spec Section 14.10):
6
- * - Top-level keys: lexicographic sort
7
- * - Entry fields: follow fieldOrder if provided, else lexicographic
8
- * - 2-space indent, UTF-8, trailing newline
9
- * - Null/undefined values omitted
10
- */
11
- function sortKeys(obj, fieldOrder) {
12
- if (obj === null || obj === void 0) return void 0;
13
- if (Array.isArray(obj)) return obj.map((item) => sortKeys(item, fieldOrder));
14
- if (typeof obj !== "object") return obj;
15
- const record = obj;
16
- const sorted = {};
17
- const keys = fieldOrder ? [...new Set([...fieldOrder, ...Object.keys(record).toSorted()])] : Object.keys(record).toSorted();
18
- for (const key of keys) {
19
- if (!(key in record)) continue;
20
- const val = record[key];
21
- if (val === null || val === void 0) continue;
22
- sorted[key] = sortKeys(val);
23
- }
24
- return sorted;
25
- }
26
- function canonicalStringify(data, fieldOrder) {
27
- const sorted = sortKeys(data, fieldOrder);
28
- return JSON.stringify(sorted, null, 2) + "\n";
29
- }
30
- //#endregion
31
- export { canonicalStringify as t };
32
-
33
- //# sourceMappingURL=serializer-Dueih-i2.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"serializer-Dueih-i2.mjs","names":[],"sources":["../src/util/serializer.ts"],"sourcesContent":["/**\n * Canonical JSON serialization — deterministic output for git diff stability.\n *\n * Rules (spec Section 14.10):\n * - Top-level keys: lexicographic sort\n * - Entry fields: follow fieldOrder if provided, else lexicographic\n * - 2-space indent, UTF-8, trailing newline\n * - Null/undefined values omitted\n */\n\nfunction sortKeys(obj: unknown, fieldOrder?: string[]): unknown {\n if (obj === null || obj === undefined) return undefined\n if (Array.isArray(obj)) return obj.map(item => sortKeys(item, fieldOrder))\n if (typeof obj !== 'object') return obj\n\n const record = obj as Record<string, unknown>\n const sorted: Record<string, unknown> = {}\n\n const keys = fieldOrder\n ? [...new Set([...fieldOrder, ...Object.keys(record).toSorted()])]\n : Object.keys(record).toSorted()\n\n for (const key of keys) {\n if (!(key in record)) continue\n const val = record[key]\n if (val === null || val === undefined) continue\n sorted[key] = sortKeys(val)\n }\n\n return sorted\n}\n\nexport function canonicalStringify(data: unknown, fieldOrder?: string[]): string {\n const sorted = sortKeys(data, fieldOrder)\n return JSON.stringify(sorted, null, 2) + '\\n'\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAS,SAAS,KAAc,YAAgC;AAC9D,KAAI,QAAQ,QAAQ,QAAQ,KAAA,EAAW,QAAO,KAAA;AAC9C,KAAI,MAAM,QAAQ,IAAI,CAAE,QAAO,IAAI,KAAI,SAAQ,SAAS,MAAM,WAAW,CAAC;AAC1E,KAAI,OAAO,QAAQ,SAAU,QAAO;CAEpC,MAAM,SAAS;CACf,MAAM,SAAkC,EAAE;CAE1C,MAAM,OAAO,aACT,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,YAAY,GAAG,OAAO,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAChE,OAAO,KAAK,OAAO,CAAC,UAAU;AAElC,MAAK,MAAM,OAAO,MAAM;AACtB,MAAI,EAAE,OAAO,QAAS;EACtB,MAAM,MAAM,OAAO;AACnB,MAAI,QAAQ,QAAQ,QAAQ,KAAA,EAAW;AACvC,SAAO,OAAO,SAAS,IAAI;;AAG7B,QAAO;;AAGT,SAAgB,mBAAmB,MAAe,YAA+B;CAC/E,MAAM,SAAS,SAAS,MAAM,WAAW;AACzC,QAAO,KAAK,UAAU,QAAQ,MAAM,EAAE,GAAG"}