@contentrain/mcp 1.0.0 → 1.0.4

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 (31) hide show
  1. package/README.md +7 -0
  2. package/dist/{apply-manager-CaTuu7Lj.mjs → apply-manager-BzpUJEbA.mjs} +4 -4
  3. package/dist/apply-manager-BzpUJEbA.mjs.map +1 -0
  4. package/dist/{context-yh3cuw5N.mjs → context-CUFulSi2.mjs} +2 -2
  5. package/dist/{context-yh3cuw5N.mjs.map → context-CUFulSi2.mjs.map} +1 -1
  6. package/dist/core/apply-manager.d.mts +3 -4
  7. package/dist/core/apply-manager.d.mts.map +1 -1
  8. package/dist/core/apply-manager.mjs +4 -4
  9. package/dist/core/content-manager.d.mts.map +1 -1
  10. package/dist/core/content-manager.mjs +1 -1
  11. package/dist/core/context.mjs +2 -2
  12. package/dist/core/model-manager.d.mts +3 -10
  13. package/dist/core/model-manager.d.mts.map +1 -1
  14. package/dist/core/model-manager.mjs +1 -1
  15. package/dist/core/validator.mjs +2 -2
  16. package/dist/git/transaction.mjs +3 -3
  17. package/dist/index.mjs +6 -6
  18. package/dist/{model-manager-BcWWu5l2.mjs → model-manager-kl78AAMg.mjs} +81 -28
  19. package/dist/model-manager-kl78AAMg.mjs.map +1 -0
  20. package/dist/{server-CTcDpxwW.mjs → server-CDytKqf2.mjs} +15 -8
  21. package/dist/server-CDytKqf2.mjs.map +1 -0
  22. package/dist/server.d.mts.map +1 -1
  23. package/dist/server.mjs +6 -6
  24. package/dist/{transaction-BsECnmL5.mjs → transaction-BhSvWfSV.mjs} +2 -2
  25. package/dist/{transaction-BsECnmL5.mjs.map → transaction-BhSvWfSV.mjs.map} +1 -1
  26. package/dist/{validator-ku84AyFr.mjs → validator-CyckZAVr.mjs} +2 -2
  27. package/dist/{validator-ku84AyFr.mjs.map → validator-CyckZAVr.mjs.map} +1 -1
  28. package/package.json +13 -7
  29. package/dist/apply-manager-CaTuu7Lj.mjs.map +0 -1
  30. package/dist/model-manager-BcWWu5l2.mjs.map +0 -1
  31. package/dist/server-CTcDpxwW.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"transaction-BsECnmL5.mjs","names":[],"sources":["../src/git/transaction.ts"],"sourcesContent":["import { simpleGit } from 'simple-git'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { readConfig } from '../core/config.js'\nimport { writeContext } from '../core/context.js'\nimport { branchTimestamp } from '../util/id.js'\n\nexport interface ContextUpdate {\n tool: string\n model: string\n locale?: string\n entries?: string[]\n}\n\nexport interface GitTransaction {\n worktree: string\n branch: string\n write(callback: (worktreePath: string) => Promise<void>): Promise<void>\n commit(message: string): Promise<string>\n complete(contextUpdate?: ContextUpdate): Promise<{ action: 'auto-merged' | 'pending-review'; commit: string }>\n cleanup(): Promise<void>\n}\n\nexport async function createTransaction(\n projectRoot: string,\n branchName: string,\n options?: { workflowOverride?: 'review' | 'auto-merge' },\n): Promise<GitTransaction> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n const workflow = options?.workflowOverride ?? config?.workflow ?? 'auto-merge'\n\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n // Smart base branch detection: env → config → current branch → 'main'\n let baseBranch = process.env['CONTENTRAIN_BRANCH'] ?? config?.repository?.default_branch ?? ''\n if (!baseBranch) {\n try {\n baseBranch = (await git.raw(['branch', '--show-current'])).trim()\n } catch {\n // fallback below\n }\n }\n if (!baseBranch) {\n baseBranch = 'main'\n }\n\n // Detect remote\n let hasRemote = false\n try {\n const remotes = await git.getRemotes()\n hasRemote = remotes.some(r => r.name === remoteName)\n } catch {\n hasRemote = false\n }\n\n // Fetch latest if remote exists\n if (hasRemote) {\n try {\n await git.fetch(remoteName, baseBranch)\n } catch {\n // fetch may fail (no network), continue with local state\n }\n }\n\n const worktreePath = join(tmpdir(), `cr-${randomUUID()}`)\n const branch = branchName\n\n // Create worktree with new branch from base\n const baseRef = hasRemote ? `${remoteName}/${baseBranch}` : baseBranch\n await git.raw(['worktree', 'add', worktreePath, '-b', branch, baseRef])\n\n const wtGit = simpleGit(worktreePath)\n\n // Configure author\n const authorName = process.env['CONTENTRAIN_AUTHOR_NAME'] ?? 'Contentrain'\n const authorEmail = process.env['CONTENTRAIN_AUTHOR_EMAIL'] ?? 'mcp@contentrain.io'\n await wtGit.addConfig('user.name', authorName)\n await wtGit.addConfig('user.email', authorEmail)\n\n let commitHash = ''\n\n return {\n worktree: worktreePath,\n branch,\n\n async write(callback) {\n await callback(worktreePath)\n },\n\n async commit(message) {\n await wtGit.add('.')\n // Exclude context.json from branch commits — it contains timestamps\n // that cause merge conflicts on sequential operations. Context is\n // updated separately on the base branch after merge.\n try {\n await wtGit.raw(['reset', 'HEAD', '--', '.contentrain/context.json'])\n } catch {\n // context.json may not be staged — that's fine\n }\n const result = await wtGit.commit(message, { '--allow-empty': null })\n commitHash = result.commit || ''\n return commitHash\n },\n\n async complete(contextUpdate?: ContextUpdate) {\n if (workflow === 'review') {\n if (hasRemote) {\n await git.push(remoteName, branch)\n }\n return { action: 'pending-review', commit: commitHash }\n }\n\n // auto-merge: merge branch into base\n if (hasRemote) {\n try {\n await git.fetch(remoteName, baseBranch)\n } catch {\n // continue with local state\n }\n }\n\n // Ensure working tree is clean before checkout\n const status = await git.status()\n if (status.modified.length > 0 || status.not_added.length > 0 || status.created.length > 0) {\n throw new Error('Working tree has uncommitted changes. Commit or stash them before completing the transaction.')\n }\n\n await git.checkout(baseBranch)\n\n try {\n await git.merge([branch, '--no-edit'])\n } catch {\n // Merge conflict — abort and let the user resolve manually.\n try {\n await git.merge(['--abort'])\n } catch {\n // abort may fail if not in merge state\n }\n throw new Error(\n `Merge conflict when merging branch \"${branch}\" into \"${baseBranch}\". `\n + `The branch still exists with your changes intact. `\n + `Resolve the conflict manually, or delete the branch and retry.`,\n )\n }\n\n if (hasRemote) {\n try {\n await git.push(remoteName, baseBranch)\n } catch {\n // push may fail, branch is merged locally\n }\n }\n\n // Update context.json on base branch after successful merge\n if (contextUpdate) {\n try {\n await writeContext(projectRoot, contextUpdate)\n await git.add('.contentrain/context.json')\n await git.commit('[contentrain] update context', { '--allow-empty': null, '--author': `${authorName} <${authorEmail}>` })\n } catch {\n // Context update is best-effort — don't fail the transaction\n }\n }\n\n return { action: 'auto-merged', commit: commitHash }\n },\n\n async cleanup() {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n },\n }\n}\n\nexport function buildBranchName(scope: string, target: string, locale?: string): string {\n const ts = branchTimestamp()\n const parts = ['contentrain', scope, target]\n if (locale) parts.push(locale)\n parts.push(ts)\n return parts.join('/')\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"transaction-BhSvWfSV.mjs","names":[],"sources":["../src/git/transaction.ts"],"sourcesContent":["import { simpleGit } from 'simple-git'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { readConfig } from '../core/config.js'\nimport { writeContext } from '../core/context.js'\nimport { branchTimestamp } from '../util/id.js'\n\nexport interface ContextUpdate {\n tool: string\n model: string\n locale?: string\n entries?: string[]\n}\n\nexport interface GitTransaction {\n worktree: string\n branch: string\n write(callback: (worktreePath: string) => Promise<void>): Promise<void>\n commit(message: string): Promise<string>\n complete(contextUpdate?: ContextUpdate): Promise<{ action: 'auto-merged' | 'pending-review'; commit: string }>\n cleanup(): Promise<void>\n}\n\nexport async function createTransaction(\n projectRoot: string,\n branchName: string,\n options?: { workflowOverride?: 'review' | 'auto-merge' },\n): Promise<GitTransaction> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n const workflow = options?.workflowOverride ?? config?.workflow ?? 'auto-merge'\n\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n // Smart base branch detection: env → config → current branch → 'main'\n let baseBranch = process.env['CONTENTRAIN_BRANCH'] ?? config?.repository?.default_branch ?? ''\n if (!baseBranch) {\n try {\n baseBranch = (await git.raw(['branch', '--show-current'])).trim()\n } catch {\n // fallback below\n }\n }\n if (!baseBranch) {\n baseBranch = 'main'\n }\n\n // Detect remote\n let hasRemote = false\n try {\n const remotes = await git.getRemotes()\n hasRemote = remotes.some(r => r.name === remoteName)\n } catch {\n hasRemote = false\n }\n\n // Fetch latest if remote exists\n if (hasRemote) {\n try {\n await git.fetch(remoteName, baseBranch)\n } catch {\n // fetch may fail (no network), continue with local state\n }\n }\n\n const worktreePath = join(tmpdir(), `cr-${randomUUID()}`)\n const branch = branchName\n\n // Create worktree with new branch from base\n const baseRef = hasRemote ? `${remoteName}/${baseBranch}` : baseBranch\n await git.raw(['worktree', 'add', worktreePath, '-b', branch, baseRef])\n\n const wtGit = simpleGit(worktreePath)\n\n // Configure author\n const authorName = process.env['CONTENTRAIN_AUTHOR_NAME'] ?? 'Contentrain'\n const authorEmail = process.env['CONTENTRAIN_AUTHOR_EMAIL'] ?? 'mcp@contentrain.io'\n await wtGit.addConfig('user.name', authorName)\n await wtGit.addConfig('user.email', authorEmail)\n\n let commitHash = ''\n\n return {\n worktree: worktreePath,\n branch,\n\n async write(callback) {\n await callback(worktreePath)\n },\n\n async commit(message) {\n await wtGit.add('.')\n // Exclude context.json from branch commits — it contains timestamps\n // that cause merge conflicts on sequential operations. Context is\n // updated separately on the base branch after merge.\n try {\n await wtGit.raw(['reset', 'HEAD', '--', '.contentrain/context.json'])\n } catch {\n // context.json may not be staged — that's fine\n }\n const result = await wtGit.commit(message, { '--allow-empty': null })\n commitHash = result.commit || ''\n return commitHash\n },\n\n async complete(contextUpdate?: ContextUpdate) {\n if (workflow === 'review') {\n if (hasRemote) {\n await git.push(remoteName, branch)\n }\n return { action: 'pending-review', commit: commitHash }\n }\n\n // auto-merge: merge branch into base\n if (hasRemote) {\n try {\n await git.fetch(remoteName, baseBranch)\n } catch {\n // continue with local state\n }\n }\n\n // Ensure working tree is clean before checkout\n const status = await git.status()\n if (status.modified.length > 0 || status.not_added.length > 0 || status.created.length > 0) {\n throw new Error('Working tree has uncommitted changes. Commit or stash them before completing the transaction.')\n }\n\n await git.checkout(baseBranch)\n\n try {\n await git.merge([branch, '--no-edit'])\n } catch {\n // Merge conflict — abort and let the user resolve manually.\n try {\n await git.merge(['--abort'])\n } catch {\n // abort may fail if not in merge state\n }\n throw new Error(\n `Merge conflict when merging branch \"${branch}\" into \"${baseBranch}\". `\n + `The branch still exists with your changes intact. `\n + `Resolve the conflict manually, or delete the branch and retry.`,\n )\n }\n\n if (hasRemote) {\n try {\n await git.push(remoteName, baseBranch)\n } catch {\n // push may fail, branch is merged locally\n }\n }\n\n // Update context.json on base branch after successful merge\n if (contextUpdate) {\n try {\n await writeContext(projectRoot, contextUpdate)\n await git.add('.contentrain/context.json')\n await git.commit('[contentrain] update context', { '--allow-empty': null, '--author': `${authorName} <${authorEmail}>` })\n } catch {\n // Context update is best-effort — don't fail the transaction\n }\n }\n\n return { action: 'auto-merged', commit: commitHash }\n },\n\n async cleanup() {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n },\n }\n}\n\nexport function buildBranchName(scope: string, target: string, locale?: string): string {\n const ts = branchTimestamp()\n const parts = ['contentrain', scope, target]\n if (locale) parts.push(locale)\n parts.push(ts)\n return parts.join('/')\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
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
2
  import { t as readConfig } from "./config-CPlhDwsv.mjs";
3
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-BcWWu5l2.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";
5
5
  import { join } from "node:path";
6
6
  import { rm } from "node:fs/promises";
7
7
  //#region src/core/validator.ts
@@ -752,4 +752,4 @@ async function validateProject(projectRoot, options) {
752
752
  //#endregion
753
753
  export { validateProject as t };
754
754
 
755
- //# sourceMappingURL=validator-ku84AyFr.mjs.map
755
+ //# sourceMappingURL=validator-CyckZAVr.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"validator-ku84AyFr.mjs","names":[],"sources":["../src/core/validator.ts"],"sourcesContent":["import type { ValidationError, ModelDefinition, ContentrainConfig, FieldDef, EntryMeta } 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// ─── Secret detection patterns ───\n\nconst SECRET_PATTERNS = [\n /sk_/,\n /pk_/,\n /api_key/i,\n /apikey/i,\n /ghp_/,\n /gho_/,\n /Bearer /,\n /postgres:\\/\\//,\n /mysql:\\/\\//,\n /mongodb:\\/\\//,\n /-----BEGIN/,\n /AKIA/,\n /aws_secret/i,\n]\n\n// ─── Validation helpers ───\n\nfunction checkSecret(value: unknown): boolean {\n if (typeof value !== 'string') return false\n return SECRET_PATTERNS.some(p => p.test(value))\n}\n\nfunction fieldTypeMatches(value: unknown, fieldDef: FieldDef): boolean {\n if (value === null || value === undefined) return true\n\n switch (fieldDef.type) {\n case 'string':\n case 'text':\n case 'email':\n case 'url':\n case 'slug':\n case 'color':\n case 'phone':\n case 'code':\n case 'icon':\n case 'markdown':\n case 'richtext':\n case 'date':\n case 'datetime':\n case 'image':\n case 'video':\n case 'file':\n case 'select':\n return typeof value === 'string'\n\n case 'number':\n case 'integer':\n case 'decimal':\n case 'percent':\n case 'rating':\n return typeof value === 'number'\n\n case 'boolean':\n return typeof value === 'boolean'\n\n case 'relation':\n return typeof value === 'string'\n\n case 'relations':\n case 'array':\n return Array.isArray(value)\n\n case 'object':\n return typeof value === 'object' && !Array.isArray(value)\n\n default:\n return true\n }\n}\n\nfunction checkMinMax(value: unknown, fieldDef: FieldDef): string | null {\n if (fieldDef.min !== undefined) {\n if (typeof value === 'number' && value < fieldDef.min) {\n return `Value ${value} is below minimum ${fieldDef.min}`\n }\n if (typeof value === 'string' && value.length < fieldDef.min) {\n return `String length ${value.length} is below minimum ${fieldDef.min}`\n }\n if (Array.isArray(value) && value.length < fieldDef.min) {\n return `Array length ${value.length} is below minimum ${fieldDef.min}`\n }\n }\n if (fieldDef.max !== undefined) {\n if (typeof value === 'number' && value > fieldDef.max) {\n return `Value ${value} is above maximum ${fieldDef.max}`\n }\n if (typeof value === 'string' && value.length > fieldDef.max) {\n return `String length ${value.length} is above maximum ${fieldDef.max}`\n }\n if (Array.isArray(value) && value.length > fieldDef.max) {\n return `Array length ${value.length} is above maximum ${fieldDef.max}`\n }\n }\n return null\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 (checkSecret(value)) {\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 // Required check\n if (fieldDef.required && (value === undefined || value === null || value === '')) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n entry: entryId,\n field: fieldName,\n message: `Required field \"${fieldName}\" is missing`,\n })\n }\n\n // Type mismatch\n if (value !== undefined && value !== null && !fieldTypeMatches(value, fieldDef)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n entry: entryId,\n field: fieldName,\n message: `Type mismatch: expected ${fieldDef.type}, got ${typeof value}`,\n })\n }\n\n // Min/max\n if (value !== undefined && value !== null) {\n const minMaxErr = checkMinMax(value, fieldDef)\n if (minMaxErr) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n entry: entryId,\n field: fieldName,\n message: minMaxErr,\n })\n }\n }\n\n // Pattern regex check\n if (fieldDef.pattern && typeof value === 'string') {\n try {\n const regex = new RegExp(fieldDef.pattern)\n if (!regex.test(value)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n entry: entryId,\n field: fieldName,\n message: `Value \"${value}\" does not match pattern /${fieldDef.pattern}/`,\n })\n }\n } catch {\n // Invalid regex in schema — report as warning\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n entry: entryId,\n field: fieldName,\n message: `Invalid pattern regex: ${fieldDef.pattern}`,\n })\n }\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 (checkSecret(value)) {\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 if (fieldDef.required && (value === undefined || value === null || value === '')) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: fieldName,\n message: `Required field \"${fieldName}\" is missing`,\n })\n }\n\n if (value !== undefined && value !== null && !fieldTypeMatches(value, fieldDef)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: fieldName,\n message: `Type mismatch: expected ${fieldDef.type}, got ${typeof value}`,\n })\n }\n\n if (value !== undefined && value !== null) {\n const minMaxErr = checkMinMax(value, fieldDef)\n if (minMaxErr) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: fieldName,\n message: minMaxErr,\n })\n }\n }\n\n // Pattern regex check\n if (fieldDef.pattern && typeof value === 'string') {\n try {\n const regex = new RegExp(fieldDef.pattern)\n if (!regex.test(value)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: fieldName,\n message: `Value \"${value}\" does not match pattern /${fieldDef.pattern}/`,\n })\n }\n } catch {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n field: fieldName,\n message: `Invalid pattern regex: ${fieldDef.pattern}`,\n })\n }\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 (checkSecret(value)) {\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 (checkSecret(value)) {\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 (checkSecret(body)) {\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 if (fieldDef.required && (value === undefined || value === null || value === '')) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: fieldName,\n message: `Required field \"${fieldName}\" is missing`,\n })\n }\n\n if (value !== undefined && value !== null && !fieldTypeMatches(value, fieldDef)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: fieldName,\n message: `Type mismatch: expected ${fieldDef.type}, got ${typeof value}`,\n })\n }\n\n if (value !== undefined && value !== null) {\n const minMaxErr = checkMinMax(value, fieldDef)\n if (minMaxErr) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: fieldName,\n message: minMaxErr,\n })\n }\n }\n\n // Pattern regex check\n if (fieldDef.pattern && typeof value === 'string') {\n try {\n const regex = new RegExp(fieldDef.pattern)\n if (!regex.test(value)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: fieldName,\n message: `Value \"${value}\" does not match pattern /${fieldDef.pattern}/`,\n })\n }\n } catch {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n slug,\n field: fieldName,\n message: `Invalid pattern regex: ${fieldDef.pattern}`,\n })\n }\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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"}
1
+ {"version":3,"file":"validator-CyckZAVr.mjs","names":[],"sources":["../src/core/validator.ts"],"sourcesContent":["import type { ValidationError, ModelDefinition, ContentrainConfig, FieldDef, EntryMeta } 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// ─── Secret detection patterns ───\n\nconst SECRET_PATTERNS = [\n /sk_/,\n /pk_/,\n /api_key/i,\n /apikey/i,\n /ghp_/,\n /gho_/,\n /Bearer /,\n /postgres:\\/\\//,\n /mysql:\\/\\//,\n /mongodb:\\/\\//,\n /-----BEGIN/,\n /AKIA/,\n /aws_secret/i,\n]\n\n// ─── Validation helpers ───\n\nfunction checkSecret(value: unknown): boolean {\n if (typeof value !== 'string') return false\n return SECRET_PATTERNS.some(p => p.test(value))\n}\n\nfunction fieldTypeMatches(value: unknown, fieldDef: FieldDef): boolean {\n if (value === null || value === undefined) return true\n\n switch (fieldDef.type) {\n case 'string':\n case 'text':\n case 'email':\n case 'url':\n case 'slug':\n case 'color':\n case 'phone':\n case 'code':\n case 'icon':\n case 'markdown':\n case 'richtext':\n case 'date':\n case 'datetime':\n case 'image':\n case 'video':\n case 'file':\n case 'select':\n return typeof value === 'string'\n\n case 'number':\n case 'integer':\n case 'decimal':\n case 'percent':\n case 'rating':\n return typeof value === 'number'\n\n case 'boolean':\n return typeof value === 'boolean'\n\n case 'relation':\n return typeof value === 'string'\n\n case 'relations':\n case 'array':\n return Array.isArray(value)\n\n case 'object':\n return typeof value === 'object' && !Array.isArray(value)\n\n default:\n return true\n }\n}\n\nfunction checkMinMax(value: unknown, fieldDef: FieldDef): string | null {\n if (fieldDef.min !== undefined) {\n if (typeof value === 'number' && value < fieldDef.min) {\n return `Value ${value} is below minimum ${fieldDef.min}`\n }\n if (typeof value === 'string' && value.length < fieldDef.min) {\n return `String length ${value.length} is below minimum ${fieldDef.min}`\n }\n if (Array.isArray(value) && value.length < fieldDef.min) {\n return `Array length ${value.length} is below minimum ${fieldDef.min}`\n }\n }\n if (fieldDef.max !== undefined) {\n if (typeof value === 'number' && value > fieldDef.max) {\n return `Value ${value} is above maximum ${fieldDef.max}`\n }\n if (typeof value === 'string' && value.length > fieldDef.max) {\n return `String length ${value.length} is above maximum ${fieldDef.max}`\n }\n if (Array.isArray(value) && value.length > fieldDef.max) {\n return `Array length ${value.length} is above maximum ${fieldDef.max}`\n }\n }\n return null\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 (checkSecret(value)) {\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 // Required check\n if (fieldDef.required && (value === undefined || value === null || value === '')) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n entry: entryId,\n field: fieldName,\n message: `Required field \"${fieldName}\" is missing`,\n })\n }\n\n // Type mismatch\n if (value !== undefined && value !== null && !fieldTypeMatches(value, fieldDef)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n entry: entryId,\n field: fieldName,\n message: `Type mismatch: expected ${fieldDef.type}, got ${typeof value}`,\n })\n }\n\n // Min/max\n if (value !== undefined && value !== null) {\n const minMaxErr = checkMinMax(value, fieldDef)\n if (minMaxErr) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n entry: entryId,\n field: fieldName,\n message: minMaxErr,\n })\n }\n }\n\n // Pattern regex check\n if (fieldDef.pattern && typeof value === 'string') {\n try {\n const regex = new RegExp(fieldDef.pattern)\n if (!regex.test(value)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n entry: entryId,\n field: fieldName,\n message: `Value \"${value}\" does not match pattern /${fieldDef.pattern}/`,\n })\n }\n } catch {\n // Invalid regex in schema — report as warning\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n entry: entryId,\n field: fieldName,\n message: `Invalid pattern regex: ${fieldDef.pattern}`,\n })\n }\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 (checkSecret(value)) {\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 if (fieldDef.required && (value === undefined || value === null || value === '')) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: fieldName,\n message: `Required field \"${fieldName}\" is missing`,\n })\n }\n\n if (value !== undefined && value !== null && !fieldTypeMatches(value, fieldDef)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: fieldName,\n message: `Type mismatch: expected ${fieldDef.type}, got ${typeof value}`,\n })\n }\n\n if (value !== undefined && value !== null) {\n const minMaxErr = checkMinMax(value, fieldDef)\n if (minMaxErr) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: fieldName,\n message: minMaxErr,\n })\n }\n }\n\n // Pattern regex check\n if (fieldDef.pattern && typeof value === 'string') {\n try {\n const regex = new RegExp(fieldDef.pattern)\n if (!regex.test(value)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: fieldName,\n message: `Value \"${value}\" does not match pattern /${fieldDef.pattern}/`,\n })\n }\n } catch {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n field: fieldName,\n message: `Invalid pattern regex: ${fieldDef.pattern}`,\n })\n }\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 (checkSecret(value)) {\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 (checkSecret(value)) {\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 (checkSecret(body)) {\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 if (fieldDef.required && (value === undefined || value === null || value === '')) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: fieldName,\n message: `Required field \"${fieldName}\" is missing`,\n })\n }\n\n if (value !== undefined && value !== null && !fieldTypeMatches(value, fieldDef)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: fieldName,\n message: `Type mismatch: expected ${fieldDef.type}, got ${typeof value}`,\n })\n }\n\n if (value !== undefined && value !== null) {\n const minMaxErr = checkMinMax(value, fieldDef)\n if (minMaxErr) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: fieldName,\n message: minMaxErr,\n })\n }\n }\n\n // Pattern regex check\n if (fieldDef.pattern && typeof value === 'string') {\n try {\n const regex = new RegExp(fieldDef.pattern)\n if (!regex.test(value)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: fieldName,\n message: `Value \"${value}\" does not match pattern /${fieldDef.pattern}/`,\n })\n }\n } catch {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n slug,\n field: fieldName,\n message: `Invalid pattern regex: ${fieldDef.pattern}`,\n })\n }\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,26 +1,32 @@
1
1
  {
2
2
  "name": "@contentrain/mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.4",
4
4
  "license": "MIT",
5
- "description": "MCP tools for AI-powered content management with Contentrain",
5
+ "description": "Local-first MCP server for AI-generated content governance — 13 deterministic tools for any platform",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/contentrain/contentrain-ai.git",
9
+ "url": "git+https://github.com/Contentrain/ai.git",
10
10
  "directory": "packages/mcp"
11
11
  },
12
- "homepage": "https://github.com/contentrain/contentrain-ai/tree/main/packages/mcp",
12
+ "homepage": "https://github.com/Contentrain/ai/tree/main/packages/mcp",
13
13
  "bugs": {
14
- "url": "https://github.com/contentrain/contentrain-ai/issues"
14
+ "url": "https://github.com/Contentrain/ai/issues"
15
15
  },
16
16
  "keywords": [
17
17
  "contentrain",
18
18
  "mcp",
19
+ "model-context-protocol",
19
20
  "ai",
20
21
  "content-governance",
21
22
  "git",
22
23
  "i18n",
23
- "local-first"
24
+ "normalize",
25
+ "local-first",
26
+ "platform-independent",
27
+ "claude",
28
+ "cursor",
29
+ "windsurf"
24
30
  ],
25
31
  "publishConfig": {
26
32
  "access": "public"
@@ -100,7 +106,7 @@
100
106
  "simple-git": "^3.27.0",
101
107
  "typescript": "^5.7.0",
102
108
  "zod": "^3.24.0",
103
- "@contentrain/types": "0.1.0"
109
+ "@contentrain/types": "0.2.0"
104
110
  },
105
111
  "optionalDependencies": {
106
112
  "@astrojs/compiler": "^2.10.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"apply-manager-CaTuu7Lj.mjs","names":[],"sources":["../src/core/apply-manager.ts"],"sourcesContent":["import type { ModelDefinition, FieldDef } from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText, writeText, pathExists } from '../util/fs.js'\nimport { readModel, writeModel, listModels, validateModelDefinition } from './model-manager.js'\nimport { writeContent, resolveContentDir, resolveJsonFilePath, resolveMdFilePath, type ContentEntry } from './content-manager.js'\nimport { readConfig } from './config.js'\nimport { writeContext } from './context.js'\nimport { createTransaction, buildBranchName } from '../git/transaction.js'\nimport { checkBranchHealth } from '../git/branch-lifecycle.js'\n\n// ─── Types ───\n\nexport interface ExtractionEntry {\n model: string\n kind: 'singleton' | 'collection' | 'dictionary' | 'document'\n domain: string\n i18n?: boolean\n fields?: Record<string, FieldDef>\n entries: Array<{\n locale?: string\n slug?: string\n data: Record<string, unknown>\n source?: { file: string; line: number; value: string }\n sources?: Array<{ file: string; line: number; key: string; value: string }>\n }>\n}\n\nexport interface ExtractionInput {\n extractions: ExtractionEntry[]\n dry_run?: boolean\n}\n\nexport interface ExtractionPreview {\n models_to_create: string[]\n models_to_update: string[]\n total_entries: number\n content_files: string[]\n}\n\nexport interface ExtractionResult {\n dry_run: boolean\n preview?: ExtractionPreview\n error?: string\n validation_errors?: string[]\n results?: {\n models_created: string[]\n models_updated: string[]\n entries_written: number\n source_map: Array<{ model: string; locale: string; value: string; file: string; line: number }>\n }\n git?: { branch: string; action: string; commit: string }\n context_updated?: boolean\n next_steps: string[]\n}\n\nexport interface PatchEntry {\n file: string\n line: number\n old_value: string\n new_expression: string\n import_statement?: string\n}\n\nexport interface ReuseInput {\n scope: { model?: string; domain?: string }\n patches: PatchEntry[]\n dry_run?: boolean\n}\n\nexport interface SyntaxError {\n file: string\n error: string\n}\n\nexport interface ReuseResult {\n dry_run: boolean\n error?: string\n scope_warnings?: string[]\n preview?: {\n files_to_modify: string[]\n patches_count: number\n imports_to_add: number\n }\n results?: {\n files_modified: string[]\n patches_applied: number\n patches_skipped: Array<{ file: string; line: number; reason: string }>\n imports_added: number\n framework_warnings?: Array<{ file: string; warning: string }>\n syntax_errors?: SyntaxError[]\n }\n git?: { branch: string; action: string; commit: string }\n next_steps: string[]\n}\n\n// ─── Constants ───\n\nconst MAX_PATCHES = 100\n\n/** File extensions allowed for patching — scannable source files only */\nexport const PATCHABLE_EXTENSIONS = new Set([\n '.vue', '.tsx', '.jsx', '.ts', '.js', '.mjs', '.astro', '.svelte',\n])\n\n/** Directories that must never be patched */\nconst FORBIDDEN_PATH_SEGMENTS = new Set([\n '.contentrain', 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt',\n])\n\n// ─── Framework Detection (Guardrail #2) ───\n\nexport type FileFramework = 'vue' | 'svelte' | 'jsx' | 'astro' | 'script'\n\nexport function detectFileFramework(filePath: string): FileFramework {\n const ext = extname(filePath).toLowerCase()\n switch (ext) {\n case '.vue': return 'vue'\n case '.svelte': return 'svelte'\n case '.tsx':\n case '.jsx': return 'jsx'\n case '.astro': return 'astro'\n case '.ts':\n case '.js':\n case '.mjs': return 'script'\n default: return 'script'\n }\n}\n\n/**\n * Validate that a replacement expression uses the correct template syntax\n * for the target file's framework. Returns a warning string or null.\n */\nexport function validateFrameworkExpression(\n filePath: string,\n newExpression: string,\n context: 'tag_text' | 'other',\n): string | null {\n if (context !== 'tag_text') return null\n\n const framework = detectFileFramework(filePath)\n\n switch (framework) {\n case 'vue':\n if (!newExpression.includes('{{')) {\n return `Vue file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{{\" — expected Vue template syntax like {{ $t('key') }}`\n }\n break\n case 'jsx':\n if (!newExpression.includes('{')) {\n return `JSX file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected JSX syntax like {t('key')}`\n }\n break\n case 'svelte':\n if (!newExpression.includes('{')) {\n return `Svelte file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected Svelte syntax like {$t('key')}`\n }\n break\n case 'astro':\n if (!newExpression.includes('{')) {\n return `Astro file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected Astro syntax like {t('key')}`\n }\n break\n case 'script':\n // Script files don't have template interpolation — warn if attempting tag text replacement\n return `Script file \"${filePath}\": tag text replacement not applicable for .ts/.js files`\n }\n\n return null\n}\n\n// ─── Scope Validation (Guardrail #1) ───\n\n/**\n * Validate that a patch file path is safe and within allowed scope.\n * Returns an error message or null if valid.\n */\nexport function validatePatchPath(filePath: string): string | null {\n const normalizedPath = filePath.replace(/\\\\/g, '/')\n\n // Reject path traversal\n if (normalizedPath.includes('..')) {\n return `Path traversal detected: \"${filePath}\"`\n }\n\n // Reject absolute paths\n if (normalizedPath.startsWith('/')) {\n return `Absolute path not allowed: \"${filePath}\"`\n }\n\n // Reject forbidden directories\n const segments = normalizedPath.split('/')\n for (const seg of segments) {\n if (FORBIDDEN_PATH_SEGMENTS.has(seg)) {\n return `Patching files inside \"${seg}/\" is not allowed: \"${filePath}\"`\n }\n }\n\n // Reject non-scannable extensions\n const ext = extname(filePath).toLowerCase()\n if (!PATCHABLE_EXTENSIONS.has(ext)) {\n return `File extension \"${ext}\" is not patchable. Allowed: ${[...PATCHABLE_EXTENSIONS].join(', ')}. Path: \"${filePath}\"`\n }\n\n return null\n}\n\n// ─── Syntax Check (Guardrail #5) ───\n\n/**\n * Perform a basic syntax check on a patched file.\n * Returns an error message or null if syntax appears valid.\n */\nexport function checkSyntax(filePath: string, content: string): string | null {\n const ext = extname(filePath).toLowerCase()\n\n switch (ext) {\n case '.ts':\n case '.tsx':\n case '.js':\n case '.jsx':\n case '.mjs':\n return checkJsSyntax(content)\n case '.vue':\n return checkVueSyntax(content)\n case '.svelte':\n case '.astro':\n return checkTagBalance(content)\n default:\n return null\n }\n}\n\n/**\n * Basic JS/TS syntax check: bracket/paren/brace balance + string literal closure.\n * This is intentionally conservative — it catches obvious breakage without\n * requiring a full parser.\n */\nfunction checkJsSyntax(content: string): string | null {\n const stack: string[] = []\n const pairs: Record<string, string> = { ')': '(', ']': '[', '}': '{' }\n let inString: string | null = null\n let escaped = false\n let inLineComment = false\n let inBlockComment = false\n\n for (let i = 0; i < content.length; i++) {\n const ch = content[i]!\n const next = content[i + 1]\n\n // Handle escape sequences inside strings\n if (escaped) {\n escaped = false\n continue\n }\n\n if (ch === '\\\\' && inString !== null) {\n escaped = true\n continue\n }\n\n // Line comment\n if (!inString && !inBlockComment && ch === '/' && next === '/') {\n inLineComment = true\n continue\n }\n if (inLineComment) {\n if (ch === '\\n') inLineComment = false\n continue\n }\n\n // Block comment\n if (!inString && !inBlockComment && ch === '/' && next === '*') {\n inBlockComment = true\n i++ // skip *\n continue\n }\n if (inBlockComment) {\n if (ch === '*' && next === '/') {\n inBlockComment = false\n i++ // skip /\n }\n continue\n }\n\n // String handling\n if (inString !== null) {\n if (ch === inString) {\n // Template literal allows multi-line, others don't\n inString = null\n } else if (inString !== '`' && ch === '\\n') {\n return `Unterminated string literal near offset ${i}`\n }\n continue\n }\n\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch\n continue\n }\n\n // Bracket matching\n if (ch === '(' || ch === '[' || ch === '{') {\n stack.push(ch)\n } else if (ch === ')' || ch === ']' || ch === '}') {\n const expected = pairs[ch]!\n if (stack.length === 0) {\n return `Unmatched closing \"${ch}\" near offset ${i}`\n }\n const top = stack.pop()!\n if (top !== expected) {\n return `Mismatched bracket: expected closing for \"${top}\" but found \"${ch}\" near offset ${i}`\n }\n }\n }\n\n if (inString !== null) {\n return `Unterminated string literal (opened with ${inString})`\n }\n\n if (stack.length > 0) {\n return `Unclosed bracket \"${stack[stack.length - 1]}\" — ${stack.length} unclosed bracket(s)`\n }\n\n return null\n}\n\n/**\n * Vue SFC syntax check: ensure <template>, <script>, <style> tags are balanced.\n */\nfunction checkVueSyntax(content: string): string | null {\n const tagBalance = checkTagBalance(content)\n if (tagBalance) return tagBalance\n\n // Vue-specific: check that SFC root tags are present and balanced\n for (const tag of ['template', 'script']) {\n const openRe = new RegExp(`<${tag}[\\\\s>]`, 'g')\n const closeRe = new RegExp(`</${tag}>`, 'g')\n const opens = content.match(openRe)?.length ?? 0\n const closes = content.match(closeRe)?.length ?? 0\n if (opens !== closes) {\n return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`\n }\n }\n\n return null\n}\n\n/**\n * Basic tag balance check for HTML-like files.\n * Checks that self-closing tags are handled and major structural tags are balanced.\n */\nfunction checkTagBalance(content: string): string | null {\n // Check for common structural tags balance\n const structuralTags = ['div', 'section', 'main', 'header', 'footer', 'nav', 'article', 'aside', 'ul', 'ol', 'table']\n\n for (const tag of structuralTags) {\n const openRe = new RegExp(`<${tag}[\\\\s>]`, 'g')\n const closeRe = new RegExp(`</${tag}>`, 'g')\n const opens = content.match(openRe)?.length ?? 0\n const closes = content.match(closeRe)?.length ?? 0\n if (opens !== closes) {\n return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`\n }\n }\n\n return null\n}\n\n// ─── Extract Mode ───\n\nexport async function applyExtract(\n projectRoot: string,\n input: ExtractionInput,\n): Promise<ExtractionResult> {\n const config = await readConfig(projectRoot)\n if (!config) throw new Error('Project not initialized. Run contentrain_init first.')\n\n const { extractions, dry_run } = input\n\n // Analyze what will happen\n const existingModels = await listModels(projectRoot)\n const existingIds = new Set(existingModels.map(m => m.id))\n\n const modelsToCreate: string[] = []\n const modelsToUpdate: string[] = []\n const contentFiles: string[] = []\n let totalEntries = 0\n\n const validationErrors: string[] = []\n\n for (const ext of extractions) {\n // Validate model definition with same rules as model_save\n const modelErrors = validateModelDefinition({\n id: ext.model,\n kind: ext.kind,\n fields: ext.fields as Record<string, unknown> | undefined,\n })\n if (modelErrors.length > 0) {\n validationErrors.push(...modelErrors.map(e => `[${ext.model}] ${e}`))\n }\n\n for (const entry of ext.entries) {\n if (ext.kind === 'dictionary') {\n if (entry.data['id'] !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Dictionary entries should not have id or slug`)\n }\n for (const [key, val] of Object.entries(entry.data)) {\n if (typeof val !== 'string') {\n validationErrors.push(`[${ext.model}] Dictionary entry value for key \"${key}\" must be a string, got ${typeof val}`)\n }\n }\n } else if (ext.kind === 'document') {\n if (!entry.slug && !entry.data['slug']) {\n validationErrors.push(`[${ext.model}] Document entries must have a slug`)\n }\n } else if (ext.kind === 'collection') {\n if (entry.slug !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Collection entries should not have slug`)\n }\n } else if (ext.kind === 'singleton') {\n if (entry.data['id'] !== undefined || entry.slug !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Singleton entries should not have id or slug`)\n }\n }\n }\n\n if (existingIds.has(ext.model)) {\n modelsToUpdate.push(ext.model)\n } else {\n modelsToCreate.push(ext.model)\n }\n totalEntries += ext.entries.length\n\n // Guardrail #3: Preview-Execute Parity — use real model metadata if it exists\n let previewModel: ModelDefinition\n if (existingIds.has(ext.model)) {\n const real = await readModel(projectRoot, ext.model)\n if (real) {\n previewModel = real\n } else {\n previewModel = { id: ext.model, kind: ext.kind, domain: ext.domain, i18n: ext.i18n ?? true } as ModelDefinition\n }\n } else {\n previewModel = { id: ext.model, kind: ext.kind, domain: ext.domain, i18n: ext.i18n ?? true } as ModelDefinition\n }\n\n const cDir = resolveContentDir(projectRoot, previewModel)\n for (const entry of ext.entries) {\n const locale = entry.locale ?? config.locales.default\n if (ext.kind === 'document' && entry.slug) {\n contentFiles.push(resolveMdFilePath(cDir, previewModel, locale, entry.slug))\n } else {\n contentFiles.push(resolveJsonFilePath(cDir, previewModel, locale))\n }\n }\n }\n\n const preview: ExtractionPreview = {\n models_to_create: modelsToCreate,\n models_to_update: modelsToUpdate,\n total_entries: totalEntries,\n content_files: [...new Set(contentFiles)],\n }\n\n // Dry run — return preview only (include validation errors if any)\n if (dry_run !== false) {\n return {\n dry_run: true,\n preview,\n ...(validationErrors.length > 0 ? { validation_errors: validationErrors } : {}),\n next_steps: [\n ...(validationErrors.length > 0\n ? [`WARNING: ${validationErrors.length} validation error(s) found — fix before executing`]\n : []),\n 'Review the preview above',\n 'Call contentrain_apply with mode:extract and dry_run:false to execute',\n ],\n }\n }\n\n // Block execute if validation errors exist\n if (validationErrors.length > 0) {\n return {\n dry_run: false,\n error: 'Model validation failed — cannot execute extract with invalid model definitions',\n validation_errors: validationErrors,\n next_steps: ['Fix the validation errors and retry'],\n }\n }\n\n // Branch health gate\n const health = await checkBranchHealth(projectRoot)\n if (health.blocked) {\n return {\n error: health.message,\n action: 'blocked' as const,\n hint: 'Merge or delete old contentrain/* branches before executing normalize.',\n } as unknown as ExtractionResult\n }\n\n // Execute — git transaction (always review mode for normalize)\n const branchName = buildBranchName('normalize', 'extract')\n const tx = await createTransaction(projectRoot, branchName, { workflowOverride: 'review' })\n const sourceMap: Array<{ model: string; locale: string; value: string; file: string; line: number }> = []\n const modelsCreated: string[] = []\n const modelsUpdated: string[] = []\n let entriesWritten = 0\n\n try {\n await tx.write(async (wt) => {\n for (const ext of extractions) {\n // Create or merge model\n const existing = await readModel(wt, ext.model)\n if (existing) {\n // Merge fields: add new, keep existing\n if (ext.fields) {\n const merged = { ...existing.fields, ...ext.fields }\n // Only overwrite if new fields were actually added\n const newFieldNames = Object.keys(ext.fields).filter(k => !(k in (existing.fields ?? {})))\n if (newFieldNames.length > 0) {\n existing.fields = merged\n await writeModel(wt, existing)\n modelsUpdated.push(ext.model)\n }\n }\n } else {\n // Create new model\n const newModel: ModelDefinition = {\n id: ext.model,\n name: ext.model.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),\n kind: ext.kind,\n domain: ext.domain,\n i18n: ext.i18n ?? true,\n fields: ext.fields,\n }\n await writeModel(wt, newModel)\n modelsCreated.push(ext.model)\n }\n\n // Write content entries\n const model = (await readModel(wt, ext.model))!\n const entries: ContentEntry[] = ext.entries.map(e => ({\n locale: e.locale,\n slug: e.slug,\n data: e.data,\n }))\n\n const wtConfig = await readConfig(wt) ?? config\n await writeContent(wt, model, entries, wtConfig)\n entriesWritten += entries.length\n\n // Track source map\n for (const entry of ext.entries) {\n // For dictionary entries with per-key source tracking\n if (ext.kind === 'dictionary' && entry.sources) {\n for (const s of entry.sources) {\n sourceMap.push({\n model: ext.model,\n locale: entry.locale ?? config.locales.default,\n value: s.value,\n file: s.file,\n line: s.line,\n })\n }\n } else if (entry.source) {\n sourceMap.push({\n model: ext.model,\n locale: entry.locale ?? config.locales.default,\n value: entry.source.value,\n file: entry.source.file,\n line: entry.source.line,\n })\n }\n }\n }\n\n // Write source map for reuse scope enforcement\n if (sourceMap.length > 0) {\n const sourcesByModel: Record<string, { source_files: string[]; entry_count: number }> = {}\n for (const s of sourceMap) {\n if (!sourcesByModel[s.model]) {\n sourcesByModel[s.model] = { source_files: [], entry_count: 0 }\n }\n const modelEntry = sourcesByModel[s.model]!\n if (!modelEntry.source_files.includes(s.file)) {\n modelEntry.source_files.push(s.file)\n }\n modelEntry.entry_count++\n }\n const sourcesJson = JSON.stringify({\n version: 1,\n created_at: new Date().toISOString(),\n models: sourcesByModel,\n }, null, 2) + '\\n'\n // Write to worktree only — merge brings it to main\n // Phase 2 (reuse) must wait for extract branch to be merged first\n await writeText(join(wt, '.contentrain', 'normalize-sources.json'), sourcesJson)\n }\n\n // Update context\n await writeContext(wt, {\n tool: 'contentrain_apply',\n model: extractions.map(e => e.model).join(','),\n locale: config.locales.default,\n entries: extractions.flatMap(e => e.entries.map(en => en.slug ?? 'entry')),\n })\n })\n\n const commitMsg = `[contentrain] normalize: extract ${entriesWritten} entries to ${extractions.length} models`\n await tx.commit(commitMsg)\n\n const gitResult = { branch: branchName, action: 'pending-review', commit: '' }\n try {\n const completed = await tx.complete()\n gitResult.action = completed.action\n gitResult.commit = completed.commit\n } catch {\n gitResult.action = 'pending-review'\n } finally {\n await tx.cleanup()\n }\n\n return {\n dry_run: false,\n results: {\n models_created: modelsCreated,\n models_updated: modelsUpdated,\n entries_written: entriesWritten,\n source_map: sourceMap,\n },\n git: gitResult,\n context_updated: true,\n next_steps: [\n 'Run contentrain_validate to check the extracted content',\n 'Run contentrain_submit to push the branch for review',\n 'After review, proceed with mode:reuse to patch source files',\n ],\n }\n } catch (error) {\n await tx.cleanup()\n throw error\n }\n}\n\n// ─── Reuse Mode ───\n\nexport async function applyReuse(\n projectRoot: string,\n input: ReuseInput,\n): Promise<ReuseResult> {\n const config = await readConfig(projectRoot)\n if (!config) throw new Error('Project not initialized. Run contentrain_init first.')\n\n const { scope, patches, dry_run } = input\n\n // Validate scope\n if (!scope.model && !scope.domain) {\n throw new Error('Scope required: provide model or domain. Whole-project patching is not allowed.')\n }\n\n // Validate patch count\n if (patches.length > MAX_PATCHES) {\n throw new Error(`Too many patches (${patches.length}). Maximum ${MAX_PATCHES} per operation. Split into multiple calls.`)\n }\n\n // Check content exists for scope (soft warning)\n if (scope.model) {\n const model = await readModel(projectRoot, scope.model)\n if (!model) {\n throw new Error(`Model \"${scope.model}\" not found. Run extract phase first.`)\n }\n }\n\n const scopeWarnings: string[] = []\n\n // Guardrail #1: Scope Real Enforcement\n // Step 1: Path safety — every patch must target a valid, patchable source file\n for (const patch of patches) {\n const pathError = validatePatchPath(patch.file)\n if (pathError) {\n throw new Error(`Invalid patch path: ${pathError}`)\n }\n }\n\n // Step 2: Semantic scope — verify scope model/domain exists and cross-check patch files\n if (scope.model || scope.domain) {\n const models = await listModels(projectRoot)\n const scopeModels = scope.model\n ? models.filter(m => m.id === scope.model)\n : scope.domain\n ? models.filter(m => m.domain === scope.domain)\n : models\n\n if (scopeModels.length === 0) {\n throw new Error(`No models found for scope ${scope.model ? `model=\"${scope.model}\"` : `domain=\"${scope.domain}\"`}`)\n }\n\n // Step 3: Verify patch files are source files (not content/config/meta files)\n // and belong to detectable source directories (not random locations)\n const { autoDetectSourceDirs } = await import('./scan-config.js')\n const sourceDirs = await autoDetectSourceDirs(projectRoot)\n\n // Build allowed file prefixes from source dirs\n const allowedPrefixes = sourceDirs.map(d => d === '.' ? '' : d + '/')\n\n for (const patch of patches) {\n const normalizedPath = patch.file.replace(/\\\\/g, '/')\n\n // Reject patches targeting .contentrain/ directory (content files should never be patched by reuse)\n if (normalizedPath.startsWith('.contentrain/') || normalizedPath.includes('/.contentrain/')) {\n throw new Error(`Cannot patch content/config files directly: \"${patch.file}\". Reuse patches source files only.`)\n }\n\n // If source dirs were detected (not just \".\"), verify patch files are within them\n if (sourceDirs.length > 0 && sourceDirs[0] !== '.') {\n const inSourceDir = allowedPrefixes.some(prefix =>\n prefix === '' || normalizedPath.startsWith(prefix),\n )\n if (!inSourceDir) {\n throw new Error(\n `Patch file \"${patch.file}\" is outside detected source directories (${sourceDirs.join(', ')}). ` +\n `Reuse patches must target source files within the project's source tree.`,\n )\n }\n }\n }\n\n // Step 4: Semantic source→model cross-check via normalize-sources.json\n // Source map is written by extract into the review branch worktree.\n // It only exists on base after extract branch is merged.\n // If missing: dry_run proceeds with warning, execute is blocked.\n if (scope.model || scope.domain) {\n const sourcesPath = join(projectRoot, '.contentrain', 'normalize-sources.json')\n const sourcesRaw = await readText(sourcesPath)\n\n if (!sourcesRaw) {\n // Source map not found — check if scoped model is a dictionary\n // Dictionaries store all keys in one entry, so per-file source tracking\n // is not available. Allow reuse with a warning instead of blocking.\n const scopedModel = scopeModels.find(m => m.id === scope.model)\n const isDictionary = scopedModel?.kind === 'dictionary'\n\n if (isDictionary) {\n scopeWarnings.push(\n 'normalize-sources.json not found. Dictionary models do not generate per-file source maps. ' +\n 'Scope enforcement is based on source-tree locality only.',\n )\n } else if (dry_run !== false) {\n scopeWarnings.push(\n 'normalize-sources.json not found. Semantic scope enforcement is unavailable. ' +\n 'Merge the extract branch first, then reuse will have full scope protection.',\n )\n } else {\n throw new Error(\n 'Cannot execute reuse: normalize-sources.json not found on base branch. ' +\n 'The extract branch must be merged before reuse can execute. ' +\n 'This ensures semantic scope enforcement protects against out-of-scope patching.',\n )\n }\n } else {\n const sourcesData = JSON.parse(sourcesRaw) as {\n models?: Record<string, { source_files?: string[] }>\n }\n\n // Collect all allowed source files for the scope\n let allowedSourceFiles: string[] = []\n let scopeLabel = ''\n\n if (scope.model) {\n const modelSources = sourcesData.models?.[scope.model]?.source_files\n if (modelSources) allowedSourceFiles = modelSources\n scopeLabel = `model \"${scope.model}\"`\n } else if (scope.domain) {\n const domainModelIds = scopeModels.map(m => m.id)\n for (const modelId of domainModelIds) {\n const modelSources = sourcesData.models?.[modelId]?.source_files\n if (modelSources) allowedSourceFiles.push(...modelSources)\n }\n allowedSourceFiles = [...new Set(allowedSourceFiles)]\n scopeLabel = `domain \"${scope.domain}\"`\n }\n\n if (allowedSourceFiles.length > 0) {\n const outOfScopePatches: string[] = []\n for (const patch of patches) {\n const normalizedPath = patch.file.replace(/\\\\/g, '/')\n if (!allowedSourceFiles.includes(normalizedPath)) {\n outOfScopePatches.push(patch.file)\n }\n }\n if (outOfScopePatches.length > 0) {\n throw new Error(\n `Scope enforcement: ${outOfScopePatches.length} patch file(s) are not associated with ${scopeLabel}. ` +\n `Out-of-scope files: ${outOfScopePatches.join(', ')}. ` +\n `Known source files: ${allowedSourceFiles.join(', ')}.`,\n )\n }\n }\n }\n }\n }\n\n // Group patches by file\n const patchesByFile = new Map<string, PatchEntry[]>()\n for (const patch of patches) {\n if (!patchesByFile.has(patch.file)) {\n patchesByFile.set(patch.file, [])\n }\n patchesByFile.get(patch.file)!.push(patch)\n }\n\n const filesToModify = [...patchesByFile.keys()]\n const importsToAdd = patches.filter(p => p.import_statement).length\n\n // Dry run — return preview only\n if (dry_run !== false) {\n return {\n dry_run: true,\n preview: {\n files_to_modify: filesToModify,\n patches_count: patches.length,\n imports_to_add: importsToAdd,\n },\n ...(scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}),\n next_steps: [\n ...(scopeWarnings.length > 0 ? [`WARNING: ${scopeWarnings.length} patch file(s) not in extract source map — verify intent`] : []),\n 'Review the files and patches above',\n 'Call contentrain_apply with mode:reuse and dry_run:false to execute',\n ],\n }\n }\n\n // Branch health gate\n const reuseHealth = await checkBranchHealth(projectRoot)\n if (reuseHealth.blocked) {\n return {\n dry_run: false,\n error: `Branch blocked: ${reuseHealth.message}`,\n next_steps: ['Merge or delete old contentrain/* branches before executing reuse.'],\n }\n }\n\n // Execute — git transaction\n const scopeTarget = scope.model ?? scope.domain!\n const branchName = buildBranchName('normalize/reuse', scopeTarget)\n const tx = await createTransaction(projectRoot, branchName, { workflowOverride: 'review' })\n\n const filesModified: string[] = []\n let patchesApplied = 0\n let importsAdded = 0\n const patchesSkipped: Array<{ file: string; line: number; reason: string }> = []\n const frameworkWarnings: Array<{ file: string; warning: string }> = []\n const syntaxErrors: SyntaxError[] = []\n\n try {\n await tx.write(async (wt) => {\n for (const [relFile, filePatches] of patchesByFile) {\n const absPath = join(wt, relFile)\n\n if (!(await pathExists(absPath))) {\n for (const p of filePatches) {\n patchesSkipped.push({ file: relFile, line: p.line, reason: 'file not found' })\n }\n continue\n }\n\n const content = await readText(absPath)\n if (content === null) {\n for (const p of filePatches) {\n patchesSkipped.push({ file: relFile, line: p.line, reason: 'file unreadable' })\n }\n continue\n }\n\n // Sort patches by line DESC (bottom-up to avoid line shifts)\n const sorted = [...filePatches].toSorted((a, b) => b.line - a.line)\n\n const lines = content.split('\\n')\n let fileModified = false\n\n for (const patch of sorted) {\n // Determine replacement context before applying\n const targetLine = lines[patch.line - 1] ?? ''\n const isTagTextContext = targetLine.includes(`>${patch.old_value}<`)\n const replacementContext = isTagTextContext ? 'tag_text' as const : 'other' as const\n\n // Guardrail #2: Framework-aware validation — only warn for tag text replacements\n if (isTagTextContext) {\n const fwWarning = validateFrameworkExpression(relFile, patch.new_expression, replacementContext)\n if (fwWarning) {\n frameworkWarnings.push({ file: relFile, warning: fwWarning })\n }\n }\n\n const applied = applyPatchToLines(lines, patch)\n if (applied) {\n patchesApplied++\n fileModified = true\n } else {\n patchesSkipped.push({ file: relFile, line: patch.line, reason: 'old_value not found at or near specified line' })\n }\n }\n\n // Add imports (deduplicate)\n const importStatements = new Set(\n filePatches\n .filter(p => p.import_statement)\n .map(p => p.import_statement!),\n )\n\n if (importStatements.size > 0) {\n const added = addImportsToLines(lines, importStatements)\n importsAdded += added\n if (added > 0) fileModified = true\n }\n\n if (fileModified) {\n const newContent = lines.join('\\n')\n await writeText(absPath, newContent)\n filesModified.push(relFile)\n\n // Guardrail #5: Syntax check after patching\n const syntaxError = checkSyntax(relFile, newContent)\n if (syntaxError) {\n syntaxErrors.push({ file: relFile, error: syntaxError })\n }\n }\n }\n\n // Update context\n await writeContext(wt, {\n tool: 'contentrain_apply',\n model: scopeTarget,\n locale: config.locales.default,\n })\n })\n\n if (filesModified.length === 0) {\n await tx.cleanup()\n return {\n dry_run: false,\n results: {\n files_modified: [],\n patches_applied: 0,\n patches_skipped: patchesSkipped,\n imports_added: 0,\n framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : undefined,\n },\n next_steps: ['No files were modified. Check patch definitions and try again.'],\n }\n }\n\n const commitMsg = `[contentrain] normalize: reuse ${scopeTarget} — patch ${filesModified.length} files (${patchesApplied} replacements)`\n await tx.commit(commitMsg)\n\n const gitResult = { branch: branchName, action: 'pending-review', commit: '' }\n try {\n const completed = await tx.complete()\n gitResult.action = completed.action\n gitResult.commit = completed.commit\n } catch {\n gitResult.action = 'pending-review'\n } finally {\n await tx.cleanup()\n }\n\n return {\n dry_run: false,\n results: {\n files_modified: filesModified,\n patches_applied: patchesApplied,\n patches_skipped: patchesSkipped,\n imports_added: importsAdded,\n framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : undefined,\n syntax_errors: syntaxErrors.length > 0 ? syntaxErrors : undefined,\n },\n ...(scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}),\n git: gitResult,\n next_steps: [\n 'Run contentrain_validate to verify the patched files',\n patchesSkipped.length > 0 ? `${patchesSkipped.length} patches were skipped — review and retry if needed` : '',\n syntaxErrors.length > 0 ? `WARNING: ${syntaxErrors.length} file(s) may have syntax errors after patching — review manually` : '',\n scopeWarnings.length > 0 ? `NOTE: ${scopeWarnings.length} patch file(s) not in extract source map` : '',\n 'Run contentrain_submit to push the branch for review',\n ].filter(Boolean),\n }\n } catch (error) {\n await tx.cleanup()\n throw error\n }\n}\n\n// ─── Patch Helpers ───\n\n/**\n * Apply a single patch to a lines array. Mutates lines in place.\n * Uses line hint for proximity matching — searches ±10 lines from hint.\n */\nfunction applyPatchToLines(lines: string[], patch: PatchEntry): boolean {\n const { line, old_value, new_expression } = patch\n const lineIdx = line - 1 // 0-based\n\n // Search range: ±10 lines from hint\n const searchStart = Math.max(0, lineIdx - 10)\n const searchEnd = Math.min(lines.length, lineIdx + 11)\n\n // First pass: exact line match\n if (lineIdx >= 0 && lineIdx < lines.length) {\n const replaced = replaceInLine(lines[lineIdx]!, old_value, new_expression)\n if (replaced !== null) {\n lines[lineIdx] = replaced\n return true\n }\n }\n\n // Second pass: proximity search\n for (let i = searchStart; i < searchEnd; i++) {\n if (i === lineIdx) continue // already tried\n const replaced = replaceInLine(lines[i]!, old_value, new_expression)\n if (replaced !== null) {\n lines[i] = replaced\n return true\n }\n }\n\n return false\n}\n\n/**\n * Replace old_value with new_expression in a single line.\n * Matches the string literal (quoted or unquoted tag text).\n * Returns the modified line, or null if not found.\n *\n * Guardrail #4: Safer patch matching — word boundary awareness and\n * ambiguity rejection for plain text fallback.\n */\nexport function replaceInLine(line: string, oldValue: string, newExpression: string): string | null {\n // Try exact match of the value in quoted strings\n // Match: \"old_value\", 'old_value', `old_value`\n for (const quote of ['\"', \"'\", '`']) {\n const quoted = `${quote}${oldValue}${quote}`\n if (line.includes(quoted)) {\n return line.replace(quoted, newExpression)\n }\n }\n\n // Try unquoted tag text match: >old_value<\n // The agent provides the complete new_expression with correct framework syntax:\n // JSX: {t('key')}\n // Vue: {{ $t('key') }}\n // Svelte: {$t('key')}\n // So we insert the expression as-is between > and <, without wrapping in braces.\n if (line.includes(`>${oldValue}<`)) {\n return line.replace(`>${oldValue}<`, `>${newExpression}<`)\n }\n\n // Guardrail #4: Safer plain text fallback\n // Only match if old_value appears at a word boundary and is unambiguous\n if (line.includes(oldValue)) {\n // Count occurrences — if multiple, it's ambiguous\n const occurrences = countOccurrences(line, oldValue)\n if (occurrences > 1) {\n return null // ambiguous — let proximity search handle it\n }\n\n // Word boundary check: don't replace \"Submit\" inside \"SubmitButton\"\n // Reject if the old_value is a substring of a larger word — check if\n // adjacent characters are word characters that extend the token.\n const idx = line.indexOf(oldValue)\n const charBefore = idx > 0 ? line[idx - 1]! : ''\n const charAfter = idx + oldValue.length < line.length ? line[idx + oldValue.length]! : ''\n\n const oldStartsWithWord = oldValue.length > 0 && isWordChar(oldValue[0]!)\n const oldEndsWithWord = oldValue.length > 0 && isWordChar(oldValue[oldValue.length - 1]!)\n\n // If old_value starts with a word char and char before is also word char, it's a partial match\n if (oldStartsWithWord && isWordChar(charBefore)) {\n return null\n }\n // If old_value ends with a word char and char after is also word char, it's a partial match\n if (oldEndsWithWord && isWordChar(charAfter)) {\n return null\n }\n\n return line.replace(oldValue, newExpression)\n }\n\n return null\n}\n\n/** Count non-overlapping occurrences of a substring */\nfunction countOccurrences(str: string, sub: string): number {\n let count = 0\n let pos = 0\n while (pos <= str.length - sub.length) {\n const idx = str.indexOf(sub, pos)\n if (idx === -1) break\n count++\n pos = idx + sub.length\n }\n return count\n}\n\n/** Check if a character is a word character (letter, digit, underscore) */\nfunction isWordChar(ch: string): boolean {\n return /\\w/.test(ch)\n}\n\n/**\n * Add import statements to the top of a file (after existing imports).\n * Deduplicates — won't add if the import already exists.\n * Returns number of imports actually added.\n */\nfunction addImportsToLines(lines: string[], imports: Set<string>): number {\n let added = 0\n const existingContent = lines.join('\\n')\n\n // Find the last import line position (handles multi-line imports)\n let lastImportIdx = -1\n let inMultiLineImport = false\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim()\n if (inMultiLineImport) {\n lastImportIdx = i\n if (trimmed.includes('}')) inMultiLineImport = false\n continue\n }\n if (trimmed.startsWith('import ') || trimmed.startsWith('import{')) {\n lastImportIdx = i\n // Check if this is a multi-line import (has { but no closing })\n if (trimmed.includes('{') && !trimmed.includes('}')) {\n inMultiLineImport = true\n }\n continue\n }\n // Stop searching after a non-import, non-empty, non-comment line following imports\n if (lastImportIdx >= 0 && trimmed.length > 0 && !trimmed.startsWith('//') && !trimmed.startsWith('/*') && !trimmed.startsWith('*')) {\n break\n }\n }\n\n let insertAt: number\n if (lastImportIdx >= 0) {\n insertAt = lastImportIdx + 1\n } else {\n // No existing imports found — insert after shebang and directive lines\n insertAt = 0\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim()\n if (i === 0 && trimmed.startsWith('#!')) {\n insertAt = i + 1\n continue\n }\n if (trimmed === \"'use client'\" || trimmed === '\"use client\"'\n || trimmed === \"'use server'\" || trimmed === '\"use server\"'\n || trimmed === \"'use client';\" || trimmed === '\"use client\";'\n || trimmed === \"'use server';\" || trimmed === '\"use server\";') {\n insertAt = i + 1\n continue\n }\n if (insertAt > 0 && trimmed.length > 0) break\n if (insertAt === 0 && trimmed.length > 0) break\n }\n }\n const toInsert: string[] = []\n\n for (const imp of imports) {\n // Check if this import already exists (by checking the from clause)\n if (!existingContent.includes(imp)) {\n toInsert.push(imp)\n added++\n }\n }\n\n if (toInsert.length > 0) {\n lines.splice(insertAt, 0, ...toInsert)\n }\n\n return added\n}\n"],"mappings":"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"}