@contentrain/mcp 1.4.0 → 1.5.1

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 (46) hide show
  1. package/README.md +6 -4
  2. package/dist/{annotations-Clhbw2lh.mjs → annotations-BFJbfSKe.mjs} +13 -1
  3. package/dist/{annotations-Clhbw2lh.mjs.map → annotations-BFJbfSKe.mjs.map} +1 -1
  4. package/dist/{apply-manager-D5ym5Jba.mjs → apply-manager-CDss8ZIr.mjs} +5 -5
  5. package/dist/{apply-manager-D5ym5Jba.mjs.map → apply-manager-CDss8ZIr.mjs.map} +1 -1
  6. package/dist/{branch-lifecycle-CFVpTpNT.mjs → branch-lifecycle-DjX3MXNG.mjs} +10 -7
  7. package/dist/{branch-lifecycle-CFVpTpNT.mjs.map → branch-lifecycle-DjX3MXNG.mjs.map} +1 -1
  8. package/dist/{config-BIl5Ybj0.mjs → config-Lw7vpK9n.mjs} +4 -2
  9. package/dist/config-Lw7vpK9n.mjs.map +1 -0
  10. package/dist/{context-J0OU9Nfp.mjs → context-DS40WOdw.mjs} +2 -2
  11. package/dist/{context-J0OU9Nfp.mjs.map → context-DS40WOdw.mjs.map} +1 -1
  12. package/dist/core/apply-manager.mjs +5 -5
  13. package/dist/core/config.d.mts.map +1 -1
  14. package/dist/core/config.mjs +1 -1
  15. package/dist/core/context.mjs +2 -2
  16. package/dist/core/doctor.mjs +3 -3
  17. package/dist/core/overlay-reader.mjs +1 -1
  18. package/dist/core/validator/index.d.mts.map +1 -1
  19. package/dist/core/validator/index.mjs +2 -2
  20. package/dist/{doctor-CgBoT6h1.mjs → doctor-Dxu_SzZy.mjs} +3 -3
  21. package/dist/{doctor-CgBoT6h1.mjs.map → doctor-Dxu_SzZy.mjs.map} +1 -1
  22. package/dist/git/branch-lifecycle.d.mts.map +1 -1
  23. package/dist/git/branch-lifecycle.mjs +2 -2
  24. package/dist/git/transaction.d.mts.map +1 -1
  25. package/dist/git/transaction.mjs +3 -3
  26. package/dist/index.mjs +10 -10
  27. package/dist/{local-DvP4Y4T-.mjs → local-BEl5L7p5.mjs} +4 -4
  28. package/dist/{local-DvP4Y4T-.mjs.map → local-BEl5L7p5.mjs.map} +1 -1
  29. package/dist/{overlay-reader-DOdyDRIv.mjs → overlay-reader-CUs9jgqk.mjs} +1 -1
  30. package/dist/{overlay-reader-DOdyDRIv.mjs.map → overlay-reader-CUs9jgqk.mjs.map} +1 -1
  31. package/dist/providers/local/index.mjs +4 -4
  32. package/dist/server/http/index.mjs +10 -10
  33. package/dist/{server-BkhhtEE2.mjs → server-BpQvNZke.mjs} +266 -55
  34. package/dist/server-BpQvNZke.mjs.map +1 -0
  35. package/dist/server.mjs +10 -10
  36. package/dist/tools/annotations.d.mts.map +1 -1
  37. package/dist/tools/annotations.mjs +1 -1
  38. package/dist/{transaction-DgkNgWSI.mjs → transaction-0D_edyh0.mjs} +48 -5
  39. package/dist/transaction-0D_edyh0.mjs.map +1 -0
  40. package/dist/{validator-B0nNRFeU.mjs → validator-Bo06QPVB.mjs} +46 -17
  41. package/dist/validator-Bo06QPVB.mjs.map +1 -0
  42. package/package.json +2 -2
  43. package/dist/config-BIl5Ybj0.mjs.map +0 -1
  44. package/dist/server-BkhhtEE2.mjs.map +0 -1
  45. package/dist/transaction-DgkNgWSI.mjs.map +0 -1
  46. package/dist/validator-B0nNRFeU.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction-0D_edyh0.mjs","names":["removeFile"],"sources":["../src/providers/local/migration.ts","../src/git/transaction.ts"],"sourcesContent":["import type { SimpleGit } from 'simple-git'\n\n/**\n * Migration: the first MCP release used `contentrain/*` feature branches.\n * Once we introduced the singleton `contentrain` branch (tracking the\n * committed content state), those old feature branches became a ref-\n * namespace conflict — git cannot hold both `contentrain` (a leaf ref)\n * and `contentrain/foo` (implying a directory) simultaneously.\n *\n * `migrateLegacyBranches` removes the old-prefix branches so the\n * singleton `contentrain` ref can be created. It is idempotent and\n * safe to call before every `ensureContentBranch` run.\n *\n * Strategy:\n * 1. Delete merged `contentrain/*` branches first (`-d`). Their commits\n * are already on the base branch via the old auto-merge flow.\n * 2. Force-delete whatever remains (`-D`). Any unmerged leftover is\n * from an abandoned or partially-committed legacy branch — content\n * on `main` always wins, and the singleton `contentrain` branch is\n * about to be created from `main`/`baseBranch` anyway.\n *\n * Returns the number of branches that were deleted. Callers may log it;\n * the git transaction layer does not need the count for correctness.\n */\nexport async function migrateLegacyBranches(\n git: SimpleGit,\n baseBranch: string,\n): Promise<number> {\n const branches = await git.branchLocal()\n const oldPrefixBranches = branches.all.filter(b => b.startsWith('contentrain/'))\n if (oldPrefixBranches.length === 0) return 0\n\n let deleted = 0\n\n // 1) Delete merged legacy branches first — the safe path.\n let mergedLegacy: string[] = []\n try {\n const mergedOutput = await git.raw(['branch', '--merged', baseBranch])\n mergedLegacy = mergedOutput.split('\\n')\n .map(b => b.trim().replace(/^\\*\\s*/, ''))\n .filter(b => b.startsWith('contentrain/'))\n } catch {\n // `branch --merged` fails before baseBranch exists — fall through.\n }\n\n for (const b of mergedLegacy) {\n try {\n await git.raw(['branch', '-d', b])\n deleted++\n } catch {\n // Branch may be protected or already gone — safe to skip.\n }\n }\n\n // 2) Force-delete any unmerged legacy branches still present.\n const remaining = (await git.branchLocal()).all.filter(b => b.startsWith('contentrain/'))\n for (const b of remaining) {\n try {\n await git.raw(['branch', '-D', b])\n deleted++\n } catch {\n // Skip — best-effort cleanup.\n }\n }\n\n return deleted\n}\n","import { simpleGit, type SimpleGit } from 'simple-git'\nimport { join } from 'node:path'\nimport { rm as removeFile } from 'node:fs/promises'\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'\nimport { migrateLegacyBranches } from '../providers/local/migration.js'\nimport type { SyncResult, WorkflowMode } from '@contentrain/types'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\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, contextUpdate?: ContextUpdate): Promise<string>\n complete(): Promise<{ action: 'auto-merged' | 'pending-review'; commit: string; sync?: SyncResult; warning?: string }>\n cleanup(): Promise<void>\n}\n\nexport async function ensureContentBranch(projectRoot: string): Promise<void> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n\n // Check if contentrain branch exists locally\n const branches = await git.branchLocal()\n if (branches.all.includes(CONTENTRAIN_BRANCH)) return\n\n // Detect base branch\n const baseBranch = config?.repository?.default_branch\n || (await git.raw(['branch', '--show-current'])).trim()\n || 'main'\n\n // Clean up legacy `contentrain/*` feature branches so the singleton\n // `contentrain` ref can be created. Idempotent — safe to call even\n // when no legacy branches exist.\n await migrateLegacyBranches(git, baseBranch)\n\n // Create contentrain branch from base\n await git.branch([CONTENTRAIN_BRANCH, baseBranch])\n\n // Push to remote if exists\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n try {\n const remotes = await git.getRemotes()\n if (remotes.some(r => r.name === remoteName)) {\n await git.push(['-u', remoteName, CONTENTRAIN_BRANCH])\n }\n } catch {\n // Remote push is best-effort\n }\n}\n\nasync function selectiveSync(\n projectRoot: string,\n _worktreePath: string,\n contentrainTip: string,\n _previousBaseRef?: string,\n dirtyFilesBeforeUpdate?: Set<string>,\n): Promise<SyncResult> {\n const git = simpleGit(projectRoot)\n const synced: string[] = []\n const skipped: string[] = []\n\n // Use git plumbing to find ALL files that differ between old and new commits.\n // diff-tree is fast and ignores working tree / index state entirely.\n // Not limited to .contentrain/ — some ops also modify .gitignore, etc.\n const compareRef = _previousBaseRef ?? contentrainTip\n let changedFiles: string[] = []\n try {\n const diffOutput = await git.raw([\n 'diff-tree', '--name-only', '-r', '--no-commit-id',\n compareRef, contentrainTip,\n ])\n changedFiles = diffOutput.split('\\n').filter(f => f.trim().length > 0)\n } catch {\n // Fallback: list .contentrain/ files from the contentrainTip commit\n try {\n const lsOutput = await git.raw(['ls-tree', '-r', '--name-only', contentrainTip, '.contentrain/'])\n changedFiles = lsOutput.split('\\n').filter(f => f.trim().length > 0)\n } catch {\n return { synced, skipped }\n }\n }\n\n if (changedFiles.length === 0) return { synced, skipped }\n\n // Use pre-captured dirty files (before update-ref) to avoid false positives.\n // After update-ref, files appear as \"modified\" in status even though the developer\n // didn't touch them. We use the pre-update state to know what was truly dirty.\n const dirtyFiles = dirtyFilesBeforeUpdate ?? new Set<string>()\n\n // Determine which changed files still exist in contentrainTip (HEAD after update-ref)\n // Check each changed file individually — `ls-tree` on specific paths is efficient\n const filesInTip = new Set<string>()\n for (const file of changedFiles) {\n try {\n await git.raw(['cat-file', '-e', `${contentrainTip}:${file}`])\n filesInTip.add(file)\n } catch {\n // File does not exist in tip (was deleted)\n }\n }\n\n for (const file of changedFiles) {\n if (dirtyFiles.has(file)) {\n skipped.push(file)\n } else if (filesInTip.has(file)) {\n // File exists in new HEAD — checkout from HEAD\n try {\n await git.checkout(['HEAD', '--', file])\n synced.push(file)\n } catch {\n skipped.push(file)\n }\n } else {\n // File was deleted in new HEAD — remove from working tree\n try {\n const filePath = join(projectRoot, file)\n await removeFile(filePath, { force: true })\n synced.push(file)\n } catch {\n skipped.push(file)\n }\n }\n }\n\n const warning = skipped.length > 0\n ? `${skipped.length} file(s) skipped due to local changes: ${skipped.join(', ')}. Commit your changes, then run: git checkout HEAD -- ${skipped.join(' ')}`\n : undefined\n\n return { synced, skipped, warning }\n}\n\nexport async function createTransaction(\n projectRoot: string,\n branchName: string,\n options?: { workflowOverride?: WorkflowMode },\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 // Detect base branch + current branch + remote in ONE batch\n // (reduces subprocess spawns from 4 to 2)\n let baseBranch = process.env['CONTENTRAIN_BRANCH'] ?? config?.repository?.default_branch ?? ''\n let currentBranch = ''\n let hasRemote = false\n\n const [branchResult, remotes] = await Promise.all([\n git.raw(['branch', '--show-current']).catch(() => ''),\n git.getRemotes().catch(() => []),\n ])\n currentBranch = branchResult.trim()\n if (!baseBranch) baseBranch = currentBranch || 'main'\n hasRemote = (remotes as { name: string }[]).some(r => r.name === remoteName)\n\n // Check if developer is on contentrain branch\n if (currentBranch === CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `The '${CONTENTRAIN_BRANCH}' branch is checked out in your working directory. `\n + `Contentrain manages this branch automatically. `\n + `Switch to your working branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_CHECKED_OUT',\n agent_hint: 'Ask the developer to switch to their working branch (e.g., main or a feature branch), then retry the operation.',\n developer_action: `git checkout ${baseBranch}`,\n })\n }\n\n // Ensure contentrain branch exists (with migration for old contentrain/* branches)\n await ensureContentBranch(projectRoot)\n\n // Fetch latest from remote (parallel fetch for both branches)\n if (hasRemote) {\n await Promise.all([\n git.fetch(remoteName, baseBranch).catch(() => {}),\n git.fetch(remoteName, CONTENTRAIN_BRANCH).catch(() => {}),\n ])\n }\n\n const worktreePath = join(tmpdir(), `cr-${randomUUID()}`)\n const branch = branchName\n\n // Create worktree on contentrain branch\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\n\n const wtGit = simpleGit(worktreePath)\n\n // Configure author (sequential — git config uses lock file)\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 // Sync contentrain with base branch (bring main changes into contentrain)\n try {\n await wtGit.merge([baseBranch, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n if (hasRemote) {\n try {\n await wtGit.merge([`${remoteName}/${baseBranch}`, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* ignore */ }\n }\n }\n }\n\n // Sync with remote contentrain if exists\n if (hasRemote) {\n try {\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* ignore */ }\n }\n }\n\n // Create feature branch from contentrain\n await wtGit.checkout(['-b', branch])\n\n let commitHash = ''\n let pendingReview = false\n let savedContextUpdate: ContextUpdate | undefined\n\n return {\n worktree: worktreePath,\n branch,\n\n async write(callback) {\n await callback(worktreePath)\n },\n\n async commit(message, contextUpdate?) {\n // context.json is intentionally NOT committed on the feature branch — it\n // is regenerated on the contentrain branch after the merge (see\n // complete()). Committing it per-branch caused cross-branch merge\n // conflicts on a single mutable file. `--no-verify` keeps the repo's\n // commit-msg / pre-commit hooks (commitlint, lefthook, husky) from\n // rejecting these machine-generated infra commits.\n savedContextUpdate = contextUpdate\n await wtGit.add('.')\n const result = await wtGit.commit(message, { '--allow-empty': null, '--no-verify': null })\n commitHash = result.commit || ''\n return commitHash\n },\n\n async complete() {\n if (workflow === 'review') {\n if (hasRemote) {\n await git.push(remoteName, branch)\n }\n // Pending-review branches must survive for a later contentrain_merge.\n pendingReview = true\n return { action: 'pending-review', commit: commitHash }\n }\n\n // auto-merge: merge feature branch into contentrain, then advance base\n\n // Switch to contentrain branch in worktree\n await wtGit.checkout(CONTENTRAIN_BRANCH)\n\n // Merge feature branch into contentrain\n try {\n await wtGit.merge([branch, '--no-edit'])\n } catch {\n try {\n await wtGit.merge(['--abort'])\n } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Merge conflict when merging branch \"${branch}\" into \"${CONTENTRAIN_BRANCH}\". `\n + `The branch still exists with your changes intact. `\n + `Resolve the conflict manually, or delete the branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_MERGE_CONFLICT',\n agent_hint: 'The feature branch could not be merged into the contentrain branch. Ask the developer to resolve the conflict.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${branch}`,\n })\n }\n\n // Regenerate context.json on the contentrain branch (post-merge,\n // single-threaded) and fold it into the tip before advancing the base.\n if (savedContextUpdate) {\n await regenerateContextOnContentrain(wtGit, worktreePath, savedContextUpdate)\n }\n\n // Get contentrain tip + old base ref + dirty files in parallel\n const [contentrainTip, previousBaseRef, statusBeforeUpdate] = await Promise.all([\n wtGit.raw(['rev-parse', 'HEAD']).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n git.status(),\n ])\n const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map(f => f.path))\n\n // Verify fast-forward: baseBranch must be an ancestor of contentrainTip\n // (guaranteed by the merge above, but verify for safety)\n try {\n await git.raw(['merge-base', '--is-ancestor', previousBaseRef, contentrainTip])\n } catch {\n throw Object.assign(new Error(\n `Cannot fast-forward \"${baseBranch}\" to contentrain tip. `\n + `The base branch has diverged. Merge \"${baseBranch}\" into \"${CONTENTRAIN_BRANCH}\" first.`,\n ), {\n code: 'BASE_UPDATE_FAILED',\n agent_hint: `The base branch has commits not in contentrain. Merge ${baseBranch} into ${CONTENTRAIN_BRANCH} first.`,\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${baseBranch} && git checkout ${baseBranch}`,\n })\n }\n\n // Advance base branch to contentrain tip via update-ref\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n\n // Refresh index to match new HEAD.\n // update-ref moves the branch pointer but leaves the index stale.\n // read-tree updates the index to match HEAD without touching the working tree.\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n // fallback: try reset for older git versions\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n\n // Selective sync: copy .contentrain/ files to developer's working tree\n const sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate)\n\n // Push to remote (best-effort with retry)\n if (hasRemote) {\n // Push contentrain branch\n try {\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n // Retry: fetch, merge, push\n try {\n await wtGit.fetch(remoteName, CONTENTRAIN_BRANCH)\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n // Push failed after retry — continue, local state is fine\n }\n }\n\n // Push base branch\n try {\n await git.push(remoteName, baseBranch)\n } catch {\n // push may fail, local merge succeeded\n }\n }\n\n return {\n action: 'auto-merged' as const,\n commit: commitHash,\n sync,\n ...(sync.warning ? { warning: sync.warning } : {}),\n }\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 // Prune the feature branch unless it is a pending-review branch that must\n // survive for a later contentrain_merge. Auto-merged branches (already in\n // contentrain) and failed/empty branches are both safe to delete, so\n // failed saves and merged saves no longer leak dangling cr/* refs.\n if (!pendingReview) {\n await safeDeleteBranch(git, branch)\n }\n },\n }\n}\n\nexport async function mergeBranch(\n projectRoot: string,\n branchName: string,\n): Promise<{ action: 'merged'; commit: string; sync: SyncResult }> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n // Detect base branch\n const baseBranch = process.env['CONTENTRAIN_BRANCH']\n ?? config?.repository?.default_branch\n ?? ((await git.raw(['branch', '--show-current'])).trim() || 'main')\n\n // Ensure contentrain branch exists\n await ensureContentBranch(projectRoot)\n\n // Check 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 // Create temp worktree on contentrain branch\n const worktreePath = join(tmpdir(), `cr-merge-${randomUUID()}`)\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\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 try {\n // Merge the feature branch into contentrain\n try {\n await wtGit.merge([branchName, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Merge conflict when merging branch \"${branchName}\" into \"${CONTENTRAIN_BRANCH}\". `\n + `The branch still exists with your changes intact. `\n + `Resolve the conflict manually, or delete the branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_MERGE_CONFLICT',\n agent_hint: 'The feature branch could not be merged into the contentrain branch. Ask the developer to resolve the conflict.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${branchName}`,\n })\n }\n\n // Regenerate context.json on contentrain post-merge (deterministic,\n // single-threaded) so review-mode branches — which carry no context.json —\n // still produce up-to-date stats once landed.\n await regenerateContextOnContentrain(wtGit, worktreePath, { tool: 'contentrain_merge', model: '*' })\n\n // Get contentrain tip + old base ref + dirty files in parallel\n const [contentrainTip, previousBaseRef, statusBeforeUpdate] = await Promise.all([\n wtGit.raw(['rev-parse', 'HEAD']).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n git.status(),\n ])\n const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map(f => f.path))\n\n // Verify fast-forward: baseBranch must be an ancestor of contentrainTip\n try {\n await git.raw(['merge-base', '--is-ancestor', previousBaseRef, contentrainTip])\n } catch {\n throw Object.assign(new Error(\n `Cannot fast-forward \"${baseBranch}\" to contentrain tip. `\n + `The base branch has diverged. Merge \"${baseBranch}\" into \"${CONTENTRAIN_BRANCH}\" first.`,\n ), {\n code: 'BASE_UPDATE_FAILED',\n agent_hint: `The base branch has commits not in contentrain. Merge ${baseBranch} into ${CONTENTRAIN_BRANCH} first.`,\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${baseBranch} && git checkout ${baseBranch}`,\n })\n }\n\n // Advance base branch to contentrain tip via update-ref\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n\n // Refresh index to match new HEAD\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n\n // Selective sync: copy .contentrain/ files to developer's working tree\n const sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate)\n\n // Push to remote (best-effort)\n if (hasRemote) {\n try {\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n try {\n await wtGit.fetch(remoteName, CONTENTRAIN_BRANCH)\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n // Push failed after retry — continue, local state is fine\n }\n }\n\n try {\n await git.push(remoteName, baseBranch)\n } catch {\n // push may fail, local merge succeeded\n }\n }\n\n // Prune the now-merged feature branch so merged cr/* refs don't accumulate.\n await safeDeleteBranch(git, branchName)\n\n return {\n action: 'merged' as const,\n commit: contentrainTip,\n sync,\n }\n } finally {\n // Cleanup worktree\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n }\n}\n\nexport function buildBranchName(scope: string, target: string, locale?: string): string {\n const ts = branchTimestamp()\n const parts = ['cr', scope, target]\n if (locale) parts.push(locale)\n parts.push(ts)\n return parts.join('/')\n}\n\n/**\n * Force-delete a local branch, swallowing all errors. Never deletes the\n * singleton `contentrain` branch. Used to prune feature branches after they\n * are merged (auto-merge / contentrain_merge) or when a transaction fails\n * before completing — so failed/merged `cr/*` refs do not accumulate.\n */\nasync function safeDeleteBranch(git: SimpleGit, branch: string): Promise<void> {\n if (!branch || branch === CONTENTRAIN_BRANCH) return\n try {\n await git.raw(['branch', '-D', branch])\n } catch {\n // Branch may not exist, be checked out, or already be deleted — ignore.\n }\n}\n\n/**\n * Regenerate `.contentrain/context.json` deterministically inside a worktree\n * that is currently on the `contentrain` branch, then commit it (hooks\n * bypassed). Called AFTER a feature branch is merged so context.json is only\n * ever written on `contentrain`, single-threaded — eliminating the per-branch\n * merge conflicts that came from committing it on every feature branch.\n */\nasync function regenerateContextOnContentrain(\n wtGit: SimpleGit,\n worktreePath: string,\n contextUpdate: ContextUpdate,\n): Promise<void> {\n await writeContext(worktreePath, contextUpdate)\n await wtGit.add('.contentrain/context.json')\n try {\n await wtGit.commit('[contentrain] context: update', { '--no-verify': null })\n } catch {\n // Nothing staged (context.json unchanged) — fine.\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { i as metaFilePath, n as contentFilePath, r as documentFilePath, t as contentDirPath } from "./paths-CYl5vCBD.mjs";
2
2
  import { c as writeText, s as writeJson } from "./fs-DLbVB-Ek.mjs";
3
- import { t as readConfig } from "./config-BIl5Ybj0.mjs";
3
+ import { t as readConfig } from "./config-Lw7vpK9n.mjs";
4
4
  import { C as LocalReader, f as parseFrontmatter, g as resolveLocaleStrategy, o as listModels, s as readModel } from "./model-manager-BhLsUgaB.mjs";
5
5
  import { r as writeMeta } from "./meta-manager-C900rEyG.mjs";
6
6
  import { join } from "node:path";
@@ -126,11 +126,33 @@ function validateField(value, def, modelId, locale, entryId, fieldId, ctx) {
126
126
  ...errCtx,
127
127
  message: `${fieldId} must have at most ${def.max} items`
128
128
  });
129
- for (let i = 0; i < value.length; i++) if (typeof value[i] !== "string") errors.push({
130
- severity: "error",
131
- ...errCtx,
132
- message: `${fieldId}[${i}] must be a string (entry ID or slug)`
133
- });
129
+ const targets = Array.isArray(def.model) ? def.model : def.model ? [def.model] : [];
130
+ const polymorphic = targets.length > 1;
131
+ for (let i = 0; i < value.length; i++) {
132
+ const item = value[i];
133
+ const itemCtx = {
134
+ ...errCtx,
135
+ field: `${fieldId}[${i}]`
136
+ };
137
+ if (polymorphic) if (typeof item !== "object" || item === null || !("model" in item) || !("ref" in item)) errors.push({
138
+ severity: "error",
139
+ ...itemCtx,
140
+ message: `${fieldId}[${i}] must be { model, ref } for polymorphic relations`
141
+ });
142
+ else {
143
+ const polyItem = item;
144
+ if (!targets.includes(polyItem.model)) errors.push({
145
+ severity: "error",
146
+ ...itemCtx,
147
+ message: `${fieldId}[${i}] target model "${polyItem.model}" must be one of: ${targets.join(", ")}`
148
+ });
149
+ }
150
+ else if (typeof item !== "string") errors.push({
151
+ severity: "error",
152
+ ...itemCtx,
153
+ message: `${fieldId}[${i}] must be a string (entry ID or slug)`
154
+ });
155
+ }
134
156
  }
135
157
  if (def.type === "array" && Array.isArray(value) && def.items && typeof def.items === "string") for (let i = 0; i < value.length; i++) errors.push(...validateArrayItemType(value[i], def.items, errCtx, `${fieldId}[${i}]`));
136
158
  if (def.type === "object" && def.fields && typeof value === "object" && value !== null && !Array.isArray(value)) {
@@ -347,7 +369,7 @@ async function readTextViaReader(reader, path) {
347
369
  * marker map, singletons and dictionaries return null content so the
348
370
  * checker skips key enforcement for them.
349
371
  */
350
- function buildProjectTargetResolver(reader) {
372
+ function buildProjectTargetResolver(reader, config) {
351
373
  return async (targetModelId, targetLocale) => {
352
374
  const targetModel = await readModel(reader, targetModelId);
353
375
  if (!targetModel) return { exists: false };
@@ -362,9 +384,16 @@ function buildProjectTargetResolver(reader) {
362
384
  exists: true,
363
385
  content: null
364
386
  };
387
+ const merged = {};
388
+ const primary = await readJsonViaReader(reader, contentFilePath(targetModel, targetLocale));
389
+ if (primary) Object.assign(merged, primary);
390
+ if (targetModel.i18n && targetLocale !== config.locales.default) {
391
+ const fallback = await readJsonViaReader(reader, contentFilePath(targetModel, config.locales.default));
392
+ if (fallback) Object.assign(merged, fallback);
393
+ }
365
394
  return {
366
395
  exists: true,
367
- content: await readJsonViaReader(reader, contentFilePath(targetModel, targetLocale)) ?? {}
396
+ content: merged
368
397
  };
369
398
  };
370
399
  }
@@ -388,10 +417,10 @@ function scanUndeclaredFieldsForSecrets(data, declared, ctx, issues) {
388
417
  async function validateCollectionModel(reader, projectRoot, model, config, issues, fix) {
389
418
  let entriesChecked = 0;
390
419
  let fixed = 0;
391
- const locales = config.locales.supported;
420
+ const locales = model.i18n ? config.locales.supported : [config.locales.default];
392
421
  const localeEntryIds = {};
393
422
  const allEntryIds = /* @__PURE__ */ new Set();
394
- const resolveTarget = buildProjectTargetResolver(reader);
423
+ const resolveTarget = buildProjectTargetResolver(reader, config);
395
424
  for (const locale of locales) {
396
425
  const filePath = contentFilePath(model, locale);
397
426
  const data = await readJsonViaReader(reader, filePath);
@@ -528,7 +557,7 @@ async function validateCollectionModel(reader, projectRoot, model, config, issue
528
557
  async function validateSingletonModel(reader, projectRoot, model, config, issues, fix) {
529
558
  let entriesChecked = 0;
530
559
  let fixed = 0;
531
- for (const locale of config.locales.supported) {
560
+ for (const locale of model.i18n ? config.locales.supported : [config.locales.default]) {
532
561
  const filePath = contentFilePath(model, locale);
533
562
  const data = await readJsonViaReader(reader, filePath);
534
563
  if (!data) {
@@ -554,7 +583,7 @@ async function validateSingletonModel(reader, projectRoot, model, config, issues
554
583
  if (model.fields) {
555
584
  const entryResult = validateContent(data, model.fields, model.id, locale);
556
585
  issues.push(...entryResult.errors);
557
- const resolveTarget = buildProjectTargetResolver(reader);
586
+ const resolveTarget = buildProjectTargetResolver(reader, config);
558
587
  const relationErrors = await checkRelationIntegrity(data, model.fields, model.id, locale, void 0, async () => null, {
559
588
  severity: "error",
560
589
  resolveTarget
@@ -592,7 +621,7 @@ async function validateDictionaryModel(reader, projectRoot, model, config, issue
592
621
  let entriesChecked = 0;
593
622
  let fixed = 0;
594
623
  const localeKeys = {};
595
- for (const locale of config.locales.supported) {
624
+ for (const locale of model.i18n ? config.locales.supported : [config.locales.default]) {
596
625
  const filePath = contentFilePath(model, locale);
597
626
  const data = await readJsonViaReader(reader, filePath);
598
627
  if (!data) {
@@ -713,7 +742,7 @@ async function validateDocumentModel(reader, projectRoot, model, config, issues,
713
742
  fixed: 0
714
743
  };
715
744
  const slugs = await discoverDocumentSlugs(reader, cDir, model);
716
- const locales = config.locales.supported;
745
+ const locales = model.i18n ? config.locales.supported : [config.locales.default];
717
746
  for (const slug of slugs) {
718
747
  if (slug.startsWith(".")) continue;
719
748
  for (const locale of locales) {
@@ -761,7 +790,7 @@ async function validateDocumentModel(reader, projectRoot, model, config, issues,
761
790
  ...err,
762
791
  slug
763
792
  });
764
- const resolveTarget = buildProjectTargetResolver(reader);
793
+ const resolveTarget = buildProjectTargetResolver(reader, config);
765
794
  const relationErrors = await checkRelationIntegrity(frontmatter, fieldsWithoutBody, model.id, locale, void 0, async () => null, {
766
795
  severity: "error",
767
796
  resolveTarget
@@ -857,7 +886,7 @@ async function validateProject(input, options) {
857
886
  for (const summary of dictModels) {
858
887
  const model = await readModel(reader, summary.id);
859
888
  if (!model) continue;
860
- for (const locale of config.locales.supported) {
889
+ for (const locale of model.i18n ? config.locales.supported : [config.locales.default]) {
861
890
  if (!globalValueMap[locale]) globalValueMap[locale] = /* @__PURE__ */ new Map();
862
891
  const data = await readJsonViaReader(reader, contentFilePath(model, locale));
863
892
  if (!data) continue;
@@ -903,4 +932,4 @@ async function validateProject(input, options) {
903
932
  //#endregion
904
933
  export { validateContent as i, validateScheduleFields as n, checkRelationIntegrity as r, validateProject as t };
905
934
 
906
- //# sourceMappingURL=validator-B0nNRFeU.mjs.map
935
+ //# sourceMappingURL=validator-Bo06QPVB.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator-Bo06QPVB.mjs","names":[],"sources":["../src/core/validator/entry.ts","../src/core/validator/relation-integrity.ts","../src/core/validator/schedule.ts","../src/core/validator/project.ts"],"sourcesContent":["import type { FieldDef, ModelDefinition, ValidationError, ValidationResult } from '@contentrain/types'\nimport { detectSecrets, validateFieldValue } from '@contentrain/types'\n\n/**\n * Context for cross-entry validation (unique, relations, nested references).\n *\n * Shape aligned with Studio's `content-validation.ts:ValidationContext` so\n * Studio can swap its internal implementation for this module without\n * touching call sites.\n */\nexport interface ValidationContext {\n /** All entries in the collection (for unique-constraint checks). Keyed by entry ID. */\n allEntries?: Record<string, Record<string, unknown>>\n /** ID of the entry being validated (excluded from unique-constraint comparisons). */\n currentEntryId?: string\n /** All model definitions, if available (reserved for future cross-model checks). */\n models?: ModelDefinition[]\n}\n\n/**\n * Validate a single content entry against its model's field schema.\n *\n * Merges the rule sets from MCP's legacy `validator.ts` (secret detection,\n * schema validation, unique constraints) with Studio's `content-validation.ts`\n * (email/url heuristics, polymorphic relation structure, nested object and\n * array-of-object recursion). The union is the authoritative per-entry\n * validator — both MCP's project validator and Studio's save path should\n * converge on this function over time.\n *\n * Asynchronous relation-integrity checks (does the referenced entry exist?)\n * live in `relation-integrity.ts` because they require I/O.\n */\nexport function validateContent(\n data: Record<string, unknown>,\n fields: Record<string, FieldDef>,\n modelId: string,\n locale: string,\n entryId?: string,\n ctx?: ValidationContext,\n): ValidationResult {\n const errors: ValidationError[] = []\n\n for (const [fieldId, def] of Object.entries(fields)) {\n const value = data[fieldId]\n errors.push(...validateField(value, def, modelId, locale, entryId, fieldId, ctx))\n }\n\n return {\n valid: errors.filter(e => e.severity === 'error').length === 0,\n errors,\n }\n}\n\nfunction validateField(\n value: unknown,\n def: FieldDef,\n modelId: string,\n locale: string,\n entryId: string | undefined,\n fieldId: string,\n ctx?: ValidationContext,\n): ValidationError[] {\n const errors: ValidationError[] = []\n const errCtx = { model: modelId, locale, entry: entryId, field: fieldId }\n\n if (value !== null && value !== undefined && value !== '') {\n const secretErrors = detectSecrets(value)\n for (const e of secretErrors) {\n errors.push({ ...e, ...errCtx })\n }\n }\n\n const isRelationType = def.type === 'relation' || def.type === 'relations'\n if (!isRelationType) {\n const fieldErrors = validateFieldValue(value, def)\n if (fieldErrors.length > 0) {\n for (const e of fieldErrors) errors.push({ ...e, ...errCtx })\n if (fieldErrors.some(e => e.severity === 'error')) return errors\n }\n } else if (def.required && (value === null || value === undefined || value === '')) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} is required` })\n return errors\n }\n\n if (value === null || value === undefined) return errors\n\n if (def.unique && ctx?.allEntries) {\n // String() comparison — matches legacy validator.ts uniqueness semantics\n // (catches cross-type duplicates like 42 vs '42' that strict === would miss).\n const valueKey = String(value)\n for (const [otherId, otherEntry] of Object.entries(ctx.allEntries)) {\n if (otherId === ctx.currentEntryId) continue\n const otherValue = otherEntry[fieldId]\n if (otherValue !== null && otherValue !== undefined && String(otherValue) === valueKey) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} must be unique — \"${String(value)}\" already exists in entry ${otherId}`,\n })\n break\n }\n }\n }\n\n if (def.type === 'email' && typeof value === 'string' && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {\n errors.push({ severity: 'warning', ...errCtx, message: `${fieldId} may not be a valid email` })\n }\n if (def.type === 'url' && typeof value === 'string' && !/^https?:\\/\\/.+/.test(value) && !value.startsWith('/')) {\n errors.push({ severity: 'warning', ...errCtx, message: `${fieldId} may not be a valid URL` })\n }\n\n if (def.type === 'relation' && def.model) {\n const targets = Array.isArray(def.model) ? def.model : [def.model]\n if (targets.length > 1) {\n if (typeof value !== 'object' || value === null || !('model' in value) || !('ref' in value)) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} must be { model, ref } for polymorphic relation`,\n })\n } else {\n const polyVal = value as { model: string, ref: string }\n if (!targets.includes(polyVal.model)) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} target model \"${polyVal.model}\" must be one of: ${targets.join(', ')}`,\n })\n }\n }\n } else if (typeof value !== 'string') {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must be a string (entry ID or slug)` })\n }\n }\n\n if (def.type === 'relations') {\n if (!Array.isArray(value)) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must be an array` })\n } else {\n if (def.min !== undefined && value.length < def.min) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must have at least ${def.min} items` })\n }\n if (def.max !== undefined && value.length > def.max) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must have at most ${def.max} items` })\n }\n // Polymorphic multi-relations (model targets multiple kinds) store\n // { model, ref } items — mirror the single-relation rule so content that\n // conforms to the generated type isn't rejected. Single-target relations\n // store plain id/slug strings.\n const targets = Array.isArray(def.model) ? def.model : (def.model ? [def.model] : [])\n const polymorphic = targets.length > 1\n for (let i = 0; i < value.length; i++) {\n const item = value[i]\n const itemCtx = { ...errCtx, field: `${fieldId}[${i}]` }\n if (polymorphic) {\n if (typeof item !== 'object' || item === null || !('model' in item) || !('ref' in item)) {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] must be { model, ref } for polymorphic relations` })\n } else {\n const polyItem = item as { model: string, ref: string }\n if (!targets.includes(polyItem.model)) {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] target model \"${polyItem.model}\" must be one of: ${targets.join(', ')}` })\n }\n }\n } else if (typeof item !== 'string') {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] must be a string (entry ID or slug)` })\n }\n }\n }\n }\n\n if (def.type === 'array' && Array.isArray(value) && def.items && typeof def.items === 'string') {\n for (let i = 0; i < value.length; i++) {\n errors.push(...validateArrayItemType(value[i], def.items, errCtx, `${fieldId}[${i}]`))\n }\n }\n\n if (def.type === 'object' && def.fields && typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const nested = validateContent(value as Record<string, unknown>, def.fields, modelId, locale, entryId)\n errors.push(...nested.errors)\n }\n\n if (\n def.type === 'array'\n && Array.isArray(value)\n && def.items\n && typeof def.items === 'object'\n && def.items.type === 'object'\n && def.items.fields\n ) {\n for (let i = 0; i < value.length; i++) {\n if (typeof value[i] === 'object' && value[i] !== null) {\n const nested = validateContent(value[i] as Record<string, unknown>, def.items.fields, modelId, locale, entryId)\n for (const e of nested.errors) {\n errors.push({ ...e, field: `${fieldId}[${i}].${e.field}` })\n }\n }\n }\n }\n\n return errors\n}\n\nfunction validateArrayItemType(\n value: unknown,\n itemType: string,\n errCtx: { model: string, locale: string, entry: string | undefined, field: string },\n fieldPath: string,\n): ValidationError[] {\n const ctx = { ...errCtx, field: fieldPath }\n switch (itemType) {\n case 'string':\n case 'email':\n case 'url':\n case 'slug':\n case 'image':\n case 'video':\n case 'file':\n if (typeof value !== 'string') return [{ severity: 'error', ...ctx, message: `${fieldPath} must be a string` }]\n break\n case 'number':\n case 'integer':\n case 'decimal':\n if (typeof value !== 'number') return [{ severity: 'error', ...ctx, message: `${fieldPath} must be a number` }]\n if (itemType === 'integer' && !Number.isInteger(value)) {\n return [{ severity: 'error', ...ctx, message: `${fieldPath} must be an integer` }]\n }\n break\n case 'boolean':\n if (typeof value !== 'boolean') return [{ severity: 'error', ...ctx, message: `${fieldPath} must be a boolean` }]\n break\n }\n return []\n}\n","import type { FieldDef, ValidationError } from '@contentrain/types'\n\n/**\n * Resolution of a target model for a relation integrity check.\n *\n * - `{ exists: true, content: {...} }` — target model is known and its\n * content for the requested locale was loaded. The ref must appear\n * as a key in `content`.\n * - `{ exists: true, content: null }` — target model exists (e.g. a\n * singleton or dictionary) but per-entry key enforcement does not\n * apply. The checker skips the ref existence test for this target.\n * - `{ exists: false }` — the referenced model is not defined at all.\n * The checker emits a \"target model not found\" error.\n */\nexport interface ResolvedTarget {\n exists: boolean\n content?: Record<string, unknown> | null\n}\n\n/** Back-compat loader signature used by Studio. A null return means \"nothing to check\". */\nexport type LegacyLoadContent = (\n targetModelId: string,\n targetLocale: string,\n) => Promise<Record<string, unknown> | null>\n\nexport interface CheckRelationIntegrityOptions {\n /** Severity for broken-relation errors. Default: `warning` (Studio-compat). */\n severity?: 'error' | 'warning'\n /**\n * Richer target resolver — when provided, it can signal target-model\n * absence. MCP's project validator passes one so `contentrain_validate`\n * still emits \"target model not found\" errors.\n */\n resolveTarget?: (\n targetModelId: string,\n targetLocale: string,\n ) => Promise<ResolvedTarget>\n}\n\n/**\n * Verify that relation and relations fields reference targets that actually\n * exist. Two severities are supported — Studio's per-save flow emits\n * `warning` (the referenced entry may still be drafted), while MCP's\n * project-wide validator emits `error` and additionally flags missing\n * target models via `resolveTarget`.\n *\n * The loader abstraction keeps this function I/O-agnostic: MCP wires it\n * to filesystem reads through LocalReader, Studio wires it to a\n * `GitProvider.readFile`-backed loader, mocks pass in-memory maps.\n */\nexport async function checkRelationIntegrity(\n data: Record<string, unknown>,\n fields: Record<string, FieldDef>,\n modelId: string,\n locale: string,\n entryId: string | undefined,\n loadContent: LegacyLoadContent,\n opts: CheckRelationIntegrityOptions = {},\n): Promise<ValidationError[]> {\n const errors: ValidationError[] = []\n const severity = opts.severity ?? 'warning'\n\n const resolve = opts.resolveTarget\n ?? (async (id: string, loc: string): Promise<ResolvedTarget> => {\n const content = await loadContent(id, loc)\n return { exists: true, content }\n })\n\n for (const [fieldId, def] of Object.entries(fields)) {\n const value = data[fieldId]\n if (value === null || value === undefined) continue\n\n if (def.type === 'relation' && def.model) {\n const targets = Array.isArray(def.model) ? def.model : [def.model]\n\n if (targets.length > 1 && typeof value === 'object' && value !== null) {\n const polyVal = value as { model: string, ref: string }\n if (polyVal.model && polyVal.ref) {\n const resolved = await resolve(polyVal.model, locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${polyVal.model}\" not found`,\n })\n } else if (resolved.content && !(polyVal.ref in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${polyVal.ref}\" not found in ${polyVal.model}`,\n })\n }\n }\n } else if (typeof value === 'string' && targets[0]) {\n const resolved = await resolve(targets[0], locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${targets[0]}\" not found`,\n })\n } else if (resolved.content && !(value in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${value}\" not found in ${targets[0]}`,\n })\n }\n }\n }\n\n if (def.type === 'relations' && def.model && Array.isArray(value)) {\n const target = Array.isArray(def.model) ? def.model[0] : def.model\n if (target) {\n const resolved = await resolve(target, locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${target}\" not found`,\n })\n } else if (resolved.content) {\n for (const ref of value) {\n if (typeof ref === 'string' && !(ref in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${ref}\" not found in ${target}`,\n })\n }\n }\n }\n }\n }\n }\n\n return errors\n}\n","import type { EntryMeta, ValidationError } from '@contentrain/types'\n\n/**\n * Validate `publish_at` and `expire_at` meta fields.\n *\n * Rules (matches legacy `validator.ts:validateScheduleFields`):\n * - `publish_at` must parse as a valid Date\n * - `expire_at` must parse as a valid Date\n * - When both are present, `expire_at` must be strictly after `publish_at`\n */\nexport function 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","import type { ValidationError, ModelDefinition, ContentrainConfig, EntryMeta } from '@contentrain/types'\nimport { detectSecrets } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { writeJson, writeText } from '../../util/fs.js'\nimport { readConfig } from '../config.js'\nimport { listModels, readModel } from '../model-manager.js'\nimport { writeMeta } from '../meta-manager.js'\nimport { parseFrontmatter, resolveLocaleStrategy } from '../content-manager.js'\nimport type { RepoReader } from '../contracts/index.js'\nimport { LocalReader } from '../../providers/local/reader.js'\nimport { contentDirPath, contentFilePath, documentFilePath, metaFilePath } from '../ops/paths.js'\nimport { validateContent } from './entry.js'\nimport { checkRelationIntegrity, type ResolvedTarget } from './relation-integrity.js'\nimport { validateScheduleFields } from './schedule.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// ─── Reader-backed IO helpers ───\n\nasync function readJsonViaReader<T>(reader: RepoReader, path: string): Promise<T | null> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return null\n }\n}\n\nasync function readTextViaReader(reader: RepoReader, path: string): Promise<string | null> {\n try {\n return await reader.readFile(path)\n } catch {\n return null\n }\n}\n\n// ─── Shared field validation helpers ───\n\n/**\n * Build a `resolveTarget` adapter for `checkRelationIntegrity` that walks\n * the project's content store via the shared {@link RepoReader}. Mirrors\n * the target-resolution shape the legacy `checkRelation` used — collection\n * targets return their entry object-map, documents return a \"slug exists\"\n * marker map, singletons and dictionaries return null content so the\n * checker skips key enforcement for them.\n */\nfunction buildProjectTargetResolver(\n reader: RepoReader,\n config: ContentrainConfig,\n): (targetModelId: string, targetLocale: string) => Promise<ResolvedTarget> {\n return async (targetModelId, targetLocale) => {\n const targetModel = await readModel(reader, targetModelId)\n if (!targetModel) return { exists: false }\n\n if (targetModel.kind === 'document') {\n const docContentDir = contentDirPath(targetModel)\n const slugs = await discoverDocumentSlugs(reader, docContentDir, targetModel)\n return { exists: true, content: Object.fromEntries(slugs.map(s => [s, true])) }\n }\n if (targetModel.kind === 'singleton' || targetModel.kind === 'dictionary') {\n return { exists: true, content: null }\n }\n // Collection: resolve against the target model's own storage. i18n:false\n // targets are locale-agnostic (data.json). For i18n:true targets, fall back\n // to the default-locale file when the source locale lacks the entry — a\n // relation legitimately points at an entry that may be translated under\n // another locale, so the id set is merged to avoid phantom broken refs. #Y5\n const merged: Record<string, unknown> = {}\n const primary = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, targetLocale),\n )\n if (primary) Object.assign(merged, primary)\n if (targetModel.i18n && targetLocale !== config.locales.default) {\n const fallback = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, config.locales.default),\n )\n if (fallback) Object.assign(merged, fallback)\n }\n return { exists: true, content: merged }\n }\n}\n\n/**\n * Scan an entry's data fields for detected secrets in UNDECLARED keys —\n * the legacy validator also flagged stray/rogue fields that were not in\n * `model.fields`. `validateContent` only knows about declared fields, so\n * this complementary pass preserves that coverage.\n */\nfunction scanUndeclaredFieldsForSecrets(\n data: Record<string, unknown>,\n declared: Record<string, unknown> | undefined,\n ctx: { model: string, locale: string, entry?: string, slug?: string },\n issues: ValidationError[],\n): void {\n for (const [fieldName, value] of Object.entries(data)) {\n if (declared && fieldName in declared) continue\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n ...ctx,\n field: fieldName,\n message: `Potential secret detected in field \"${fieldName}\"`,\n })\n }\n }\n}\n\n// ─── Per-model validators ───\n\nasync function validateCollectionModel(\n reader: RepoReader,\n projectRoot: string | undefined,\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 // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n const locales = model.i18n ? config.locales.supported : [config.locales.default]\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 const resolveTarget = buildProjectTargetResolver(reader, config)\n\n for (const locale of locales) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, Record<string, unknown>>>(reader, 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 && projectRoot) {\n await writeJson(join(projectRoot, 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 && projectRoot) {\n const resorted: Record<string, Record<string, unknown>> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate each entry — validateContent covers secret/field/unique/email-url/\n // polymorphic/nested on declared fields; supplementary scans below catch\n // undeclared-field secrets and async relation integrity.\n for (const [entryId, fields] of Object.entries(data)) {\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(\n fields,\n model.fields,\n { model: model.id, locale, entry: entryId },\n issues,\n )\n\n if (!model.fields) continue\n\n const entryResult = validateContent(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n { allEntries: data, currentEntryId: entryId },\n )\n issues.push(...entryResult.errors)\n\n const relationErrors = await checkRelationIntegrity(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\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 metaRelPath = metaFilePath(model, locale)\n const metaData = await readJsonViaReader<Record<string, EntryMeta>>(reader, metaRelPath)\n const contentData = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(model, locale),\n ) ?? {}\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 && projectRoot) {\n delete metaData[metaEntryId]\n const metaAbs = join(projectRoot, metaRelPath)\n if (Object.keys(metaData).length > 0) {\n await writeJson(metaAbs, metaData)\n } else {\n await rm(metaAbs, { 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 && projectRoot) {\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 reader: RepoReader,\n projectRoot: string | undefined,\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\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, unknown>>(reader, 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 && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(data, model.fields, { model: model.id, locale }, issues)\n\n if (model.fields) {\n const entryResult = validateContent(data, model.fields, model.id, locale)\n issues.push(...entryResult.errors)\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n const relationErrors = await checkRelationIntegrity(\n data,\n model.fields,\n model.id,\n locale,\n undefined,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\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 && projectRoot) {\n const resorted: Record<string, unknown> = {}\n for (const key of sorted) {\n resorted[key] = data[key]\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate schedule fields in singleton meta\n const singletonMetaData = await readJsonViaReader<EntryMeta>(reader, metaFilePath(model, locale))\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 reader: RepoReader,\n projectRoot: string | undefined,\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\n const localeKeys: Record<string, Set<string>> = {}\n\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, string>>(reader, 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 && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n localeKeys[locale] = new Set(Object.keys(data))\n\n // Secret detection\n for (const [key, value] of Object.entries(data)) {\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: key,\n message: `Potential secret detected in key \"${key}\"`,\n })\n }\n }\n\n // Duplicate value detection\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n for (const [value, dupeKeys] of valueToKeys) {\n if (dupeKeys.length > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: `Duplicate value \"${truncated}\" mapped to ${dupeKeys.length} keys: [${dupeKeys.join(', ')}]`,\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 && projectRoot) {\n const resorted: Record<string, string> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, 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 reader: RepoReader,\n cDir: string,\n model: ModelDefinition,\n): Promise<string[]> {\n const strategy = resolveLocaleStrategy(model)\n const entries = await reader.listDirectory(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 const localeLists = await Promise.all(\n entries\n .filter(localeDir => !localeDir.startsWith('.'))\n .map(localeDir => reader.listDirectory(`${cDir}/${localeDir}`)),\n )\n for (const files of localeLists) {\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 reader: RepoReader,\n projectRoot: string | undefined,\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 = contentDirPath(model)\n\n if (!await reader.fileExists(cDir)) return { entries: 0, fixed: 0 }\n\n const slugs = await discoverDocumentSlugs(reader, cDir, model)\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n const locales = model.i18n ? config.locales.supported : [config.locales.default]\n\n for (const slug of slugs) {\n if (slug.startsWith('.')) continue\n\n for (const locale of locales) {\n const filePath = documentFilePath(model, locale, slug)\n const raw = await readTextViaReader(reader, 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 && projectRoot) {\n // Create empty template\n const template = `---\\nslug: ${slug}\\n---\\n`\n await writeText(join(projectRoot, filePath), template)\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n const { frontmatter, body } = parseFrontmatter(raw)\n\n // Always scan undeclared frontmatter fields + body for secrets.\n scanUndeclaredFieldsForSecrets(\n frontmatter,\n // Treat `body` as a declared field so the undeclared-scan skips it; body\n // is scanned separately below.\n { ...model.fields, body: true } as Record<string, unknown>,\n { model: model.id, locale, slug },\n issues,\n )\n\n if (detectSecrets(body).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: 'body',\n message: 'Potential secret detected in document body',\n })\n }\n\n if (model.fields) {\n const fieldsWithoutBody = Object.fromEntries(\n Object.entries(model.fields).filter(([name]) => name !== 'body'),\n )\n const entryResult = validateContent(\n frontmatter,\n fieldsWithoutBody,\n model.id,\n locale,\n )\n for (const err of entryResult.errors) {\n issues.push({ ...err, slug })\n }\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n const relationErrors = await checkRelationIntegrity(\n frontmatter,\n fieldsWithoutBody,\n model.id,\n locale,\n undefined,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n for (const err of relationErrors) {\n issues.push({ ...err, slug })\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\n// ─── Orphan content check ───\n\nasync function checkOrphanContent(\n reader: RepoReader,\n validModelIds: Set<string>,\n issues: ValidationError[],\n _fix: boolean,\n): Promise<number> {\n const fixed = 0\n const contentBase = '.contentrain/content'\n const domains = await reader.listDirectory(contentBase)\n\n const modelLists = await Promise.all(\n domains\n .filter(d => !d.startsWith('.'))\n .map(async d => ({ domain: d, dirs: await reader.listDirectory(`${contentBase}/${d}`) })),\n )\n\n for (const { dirs } of modelLists) {\n for (const modelDir of dirs) {\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\n/**\n * Validate a project's content against its model schemas.\n *\n * Two signatures:\n *\n * - `validateProject(projectRoot, options?)` — legacy disk-backed flow.\n * `options.fix: true` applies structural repairs (canonical sort,\n * orphan meta, missing locale files) directly on disk.\n * - `validateProject(reader, options?)` — reader-backed flow used by\n * HTTP/remote callers (e.g. GitHubProvider). `options.fix` is ignored\n * because remote flows cannot write to the local filesystem.\n *\n * Read-side behavior is identical across both signatures so Studio and\n * local CLIs see the same issue set for the same content state.\n */\nexport async function validateProject(projectRoot: string, options?: ValidateOptions): Promise<ValidateResult>\nexport async function validateProject(reader: RepoReader, options?: ValidateOptions): Promise<ValidateResult>\nexport async function validateProject(\n input: string | RepoReader,\n options?: ValidateOptions,\n): Promise<ValidateResult> {\n const reader: RepoReader = typeof input === 'string' ? new LocalReader(input) : input\n const projectRoot: string | undefined = typeof input === 'string' ? input : undefined\n const fix = Boolean(options?.fix) && projectRoot !== undefined\n\n const issues: ValidationError[] = []\n let totalEntries = 0\n let totalFixed = 0\n let modelsChecked = 0\n\n const config = await readConfig(reader)\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(reader)\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(reader, 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(reader, projectRoot, model, config, issues, fix)\n break\n case 'singleton':\n result = await validateSingletonModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'dictionary':\n result = await validateDictionaryModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'document':\n result = await validateDocumentModel(reader, 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(reader, validModelIds, issues, fix)\n }\n\n // Cross-dictionary duplicate value detection (only when checking all models)\n if (!options?.model) {\n const dictModels = modelsToCheck.filter(m => m.kind === 'dictionary')\n if (dictModels.length > 1) {\n const globalValueMap: Record<string, Map<string, Array<{ model: string; key: string }>>> = {}\n\n for (const summary of dictModels) {\n const model = await readModel(reader, summary.id)\n if (!model) continue\n\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n if (!globalValueMap[locale]) globalValueMap[locale] = new Map()\n const data = await readJsonViaReader<Record<string, string>>(reader, contentFilePath(model, locale))\n if (!data) continue\n\n for (const [key, value] of Object.entries(data)) {\n const refs = globalValueMap[locale]!.get(value)\n if (refs) refs.push({ model: model.id, key })\n else globalValueMap[locale]!.set(value, [{ model: model.id, key }])\n }\n }\n }\n\n for (const [locale, valueMap] of Object.entries(globalValueMap)) {\n for (const [value, refs] of valueMap) {\n const uniqueModels = new Set(refs.map(r => r.model))\n if (uniqueModels.size > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'notice',\n locale,\n message: `Cross-model duplicate value \"${truncated}\" in ${refs.map(r => `${r.model}/${r.key}`).join(', ')}`,\n })\n }\n }\n }\n }\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":";;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,gBACd,MACA,QACA,SACA,QACA,SACA,KACkB;CAClB,MAAM,SAA4B,EAAE;AAEpC,MAAK,MAAM,CAAC,SAAS,QAAQ,OAAO,QAAQ,OAAO,EAAE;EACnD,MAAM,QAAQ,KAAK;AACnB,SAAO,KAAK,GAAG,cAAc,OAAO,KAAK,SAAS,QAAQ,SAAS,SAAS,IAAI,CAAC;;AAGnF,QAAO;EACL,OAAO,OAAO,QAAO,MAAK,EAAE,aAAa,QAAQ,CAAC,WAAW;EAC7D;EACD;;AAGH,SAAS,cACP,OACA,KACA,SACA,QACA,SACA,SACA,KACmB;CACnB,MAAM,SAA4B,EAAE;CACpC,MAAM,SAAS;EAAE,OAAO;EAAS;EAAQ,OAAO;EAAS,OAAO;EAAS;AAEzE,KAAI,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,IAAI;EACzD,MAAM,eAAe,cAAc,MAAM;AACzC,OAAK,MAAM,KAAK,aACd,QAAO,KAAK;GAAE,GAAG;GAAG,GAAG;GAAQ,CAAC;;AAKpC,KAAI,EADmB,IAAI,SAAS,cAAc,IAAI,SAAS,cAC1C;EACnB,MAAM,cAAc,mBAAmB,OAAO,IAAI;AAClD,MAAI,YAAY,SAAS,GAAG;AAC1B,QAAK,MAAM,KAAK,YAAa,QAAO,KAAK;IAAE,GAAG;IAAG,GAAG;IAAQ,CAAC;AAC7D,OAAI,YAAY,MAAK,MAAK,EAAE,aAAa,QAAQ,CAAE,QAAO;;YAEnD,IAAI,aAAa,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,KAAK;AAClF,SAAO,KAAK;GAAE,UAAU;GAAS,GAAG;GAAQ,SAAS,GAAG,QAAQ;GAAe,CAAC;AAChF,SAAO;;AAGT,KAAI,UAAU,QAAQ,UAAU,KAAA,EAAW,QAAO;AAElD,KAAI,IAAI,UAAU,KAAK,YAAY;EAGjC,MAAM,WAAW,OAAO,MAAM;AAC9B,OAAK,MAAM,CAAC,SAAS,eAAe,OAAO,QAAQ,IAAI,WAAW,EAAE;AAClE,OAAI,YAAY,IAAI,eAAgB;GACpC,MAAM,aAAa,WAAW;AAC9B,OAAI,eAAe,QAAQ,eAAe,KAAA,KAAa,OAAO,WAAW,KAAK,UAAU;AACtF,WAAO,KAAK;KACV,UAAU;KACV,GAAG;KACH,SAAS,GAAG,QAAQ,qBAAqB,OAAO,MAAM,CAAC,4BAA4B;KACpF,CAAC;AACF;;;;AAKN,KAAI,IAAI,SAAS,WAAW,OAAO,UAAU,YAAY,CAAC,6BAA6B,KAAK,MAAM,CAChG,QAAO,KAAK;EAAE,UAAU;EAAW,GAAG;EAAQ,SAAS,GAAG,QAAQ;EAA4B,CAAC;AAEjG,KAAI,IAAI,SAAS,SAAS,OAAO,UAAU,YAAY,CAAC,iBAAiB,KAAK,MAAM,IAAI,CAAC,MAAM,WAAW,IAAI,CAC5G,QAAO,KAAK;EAAE,UAAU;EAAW,GAAG;EAAQ,SAAS,GAAG,QAAQ;EAA0B,CAAC;AAG/F,KAAI,IAAI,SAAS,cAAc,IAAI,OAAO;EACxC,MAAM,UAAU,MAAM,QAAQ,IAAI,MAAM,GAAG,IAAI,QAAQ,CAAC,IAAI,MAAM;AAClE,MAAI,QAAQ,SAAS,EACnB,KAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,EAAE,WAAW,UAAU,EAAE,SAAS,OACnF,QAAO,KAAK;GACV,UAAU;GACV,GAAG;GACH,SAAS,GAAG,QAAQ;GACrB,CAAC;OACG;GACL,MAAM,UAAU;AAChB,OAAI,CAAC,QAAQ,SAAS,QAAQ,MAAM,CAClC,QAAO,KAAK;IACV,UAAU;IACV,GAAG;IACH,SAAS,GAAG,QAAQ,iBAAiB,QAAQ,MAAM,oBAAoB,QAAQ,KAAK,KAAK;IAC1F,CAAC;;WAGG,OAAO,UAAU,SAC1B,QAAO,KAAK;GAAE,UAAU;GAAS,GAAG;GAAQ,SAAS,GAAG,QAAQ;GAAuC,CAAC;;AAI5G,KAAI,IAAI,SAAS,YACf,KAAI,CAAC,MAAM,QAAQ,MAAM,CACvB,QAAO,KAAK;EAAE,UAAU;EAAS,GAAG;EAAQ,SAAS,GAAG,QAAQ;EAAoB,CAAC;MAChF;AACL,MAAI,IAAI,QAAQ,KAAA,KAAa,MAAM,SAAS,IAAI,IAC9C,QAAO,KAAK;GAAE,UAAU;GAAS,GAAG;GAAQ,SAAS,GAAG,QAAQ,sBAAsB,IAAI,IAAI;GAAS,CAAC;AAE1G,MAAI,IAAI,QAAQ,KAAA,KAAa,MAAM,SAAS,IAAI,IAC9C,QAAO,KAAK;GAAE,UAAU;GAAS,GAAG;GAAQ,SAAS,GAAG,QAAQ,qBAAqB,IAAI,IAAI;GAAS,CAAC;EAMzG,MAAM,UAAU,MAAM,QAAQ,IAAI,MAAM,GAAG,IAAI,QAAS,IAAI,QAAQ,CAAC,IAAI,MAAM,GAAG,EAAE;EACpF,MAAM,cAAc,QAAQ,SAAS;AACrC,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACrC,MAAM,OAAO,MAAM;GACnB,MAAM,UAAU;IAAE,GAAG;IAAQ,OAAO,GAAG,QAAQ,GAAG,EAAE;IAAI;AACxD,OAAI,YACF,KAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,EAAE,WAAW,SAAS,EAAE,SAAS,MAChF,QAAO,KAAK;IAAE,UAAU;IAAS,GAAG;IAAS,SAAS,GAAG,QAAQ,GAAG,EAAE;IAAqD,CAAC;QACvH;IACL,MAAM,WAAW;AACjB,QAAI,CAAC,QAAQ,SAAS,SAAS,MAAM,CACnC,QAAO,KAAK;KAAE,UAAU;KAAS,GAAG;KAAS,SAAS,GAAG,QAAQ,GAAG,EAAE,kBAAkB,SAAS,MAAM,oBAAoB,QAAQ,KAAK,KAAK;KAAI,CAAC;;YAG7I,OAAO,SAAS,SACzB,QAAO,KAAK;IAAE,UAAU;IAAS,GAAG;IAAS,SAAS,GAAG,QAAQ,GAAG,EAAE;IAAwC,CAAC;;;AAMvH,KAAI,IAAI,SAAS,WAAW,MAAM,QAAQ,MAAM,IAAI,IAAI,SAAS,OAAO,IAAI,UAAU,SACpF,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAChC,QAAO,KAAK,GAAG,sBAAsB,MAAM,IAAI,IAAI,OAAO,QAAQ,GAAG,QAAQ,GAAG,EAAE,GAAG,CAAC;AAI1F,KAAI,IAAI,SAAS,YAAY,IAAI,UAAU,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,MAAM,EAAE;EAC/G,MAAM,SAAS,gBAAgB,OAAkC,IAAI,QAAQ,SAAS,QAAQ,QAAQ;AACtG,SAAO,KAAK,GAAG,OAAO,OAAO;;AAG/B,KACE,IAAI,SAAS,WACV,MAAM,QAAQ,MAAM,IACpB,IAAI,SACJ,OAAO,IAAI,UAAU,YACrB,IAAI,MAAM,SAAS,YACnB,IAAI,MAAM;OAER,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAChC,KAAI,OAAO,MAAM,OAAO,YAAY,MAAM,OAAO,MAAM;GACrD,MAAM,SAAS,gBAAgB,MAAM,IAA+B,IAAI,MAAM,QAAQ,SAAS,QAAQ,QAAQ;AAC/G,QAAK,MAAM,KAAK,OAAO,OACrB,QAAO,KAAK;IAAE,GAAG;IAAG,OAAO,GAAG,QAAQ,GAAG,EAAE,IAAI,EAAE;IAAS,CAAC;;;AAMnE,QAAO;;AAGT,SAAS,sBACP,OACA,UACA,QACA,WACmB;CACnB,MAAM,MAAM;EAAE,GAAG;EAAQ,OAAO;EAAW;AAC3C,SAAQ,UAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;AACH,OAAI,OAAO,UAAU,SAAU,QAAO,CAAC;IAAE,UAAU;IAAS,GAAG;IAAK,SAAS,GAAG,UAAU;IAAoB,CAAC;AAC/G;EACF,KAAK;EACL,KAAK;EACL,KAAK;AACH,OAAI,OAAO,UAAU,SAAU,QAAO,CAAC;IAAE,UAAU;IAAS,GAAG;IAAK,SAAS,GAAG,UAAU;IAAoB,CAAC;AAC/G,OAAI,aAAa,aAAa,CAAC,OAAO,UAAU,MAAM,CACpD,QAAO,CAAC;IAAE,UAAU;IAAS,GAAG;IAAK,SAAS,GAAG,UAAU;IAAsB,CAAC;AAEpF;EACF,KAAK;AACH,OAAI,OAAO,UAAU,UAAW,QAAO,CAAC;IAAE,UAAU;IAAS,GAAG;IAAK,SAAS,GAAG,UAAU;IAAqB,CAAC;AACjH;;AAEJ,QAAO,EAAE;;;;;;;;;;;;;;;ACrLX,eAAsB,uBACpB,MACA,QACA,SACA,QACA,SACA,aACA,OAAsC,EAAE,EACZ;CAC5B,MAAM,SAA4B,EAAE;CACpC,MAAM,WAAW,KAAK,YAAY;CAElC,MAAM,UAAU,KAAK,kBACf,OAAO,IAAY,QAAyC;AAE9D,SAAO;GAAE,QAAQ;GAAM,SADP,MAAM,YAAY,IAAI,IAAI;GACV;;AAGpC,MAAK,MAAM,CAAC,SAAS,QAAQ,OAAO,QAAQ,OAAO,EAAE;EACnD,MAAM,QAAQ,KAAK;AACnB,MAAI,UAAU,QAAQ,UAAU,KAAA,EAAW;AAE3C,MAAI,IAAI,SAAS,cAAc,IAAI,OAAO;GACxC,MAAM,UAAU,MAAM,QAAQ,IAAI,MAAM,GAAG,IAAI,QAAQ,CAAC,IAAI,MAAM;AAElE,OAAI,QAAQ,SAAS,KAAK,OAAO,UAAU,YAAY,UAAU,MAAM;IACrE,MAAM,UAAU;AAChB,QAAI,QAAQ,SAAS,QAAQ,KAAK;KAChC,MAAM,WAAW,MAAM,QAAQ,QAAQ,OAAO,OAAO;AACrD,SAAI,CAAC,SAAS,OACZ,QAAO,KAAK;MACV;MACA,OAAO;MACP;MACA,OAAO;MACP,OAAO;MACP,SAAS,kCAAkC,QAAQ,MAAM;MAC1D,CAAC;cACO,SAAS,WAAW,EAAE,QAAQ,OAAO,SAAS,SACvD,QAAO,KAAK;MACV;MACA,OAAO;MACP;MACA,OAAO;MACP,OAAO;MACP,SAAS,qBAAqB,QAAQ,IAAI,iBAAiB,QAAQ;MACpE,CAAC;;cAGG,OAAO,UAAU,YAAY,QAAQ,IAAI;IAClD,MAAM,WAAW,MAAM,QAAQ,QAAQ,IAAI,OAAO;AAClD,QAAI,CAAC,SAAS,OACZ,QAAO,KAAK;KACV;KACA,OAAO;KACP;KACA,OAAO;KACP,OAAO;KACP,SAAS,kCAAkC,QAAQ,GAAG;KACvD,CAAC;aACO,SAAS,WAAW,EAAE,SAAS,SAAS,SACjD,QAAO,KAAK;KACV;KACA,OAAO;KACP;KACA,OAAO;KACP,OAAO;KACP,SAAS,qBAAqB,MAAM,iBAAiB,QAAQ;KAC9D,CAAC;;;AAKR,MAAI,IAAI,SAAS,eAAe,IAAI,SAAS,MAAM,QAAQ,MAAM,EAAE;GACjE,MAAM,SAAS,MAAM,QAAQ,IAAI,MAAM,GAAG,IAAI,MAAM,KAAK,IAAI;AAC7D,OAAI,QAAQ;IACV,MAAM,WAAW,MAAM,QAAQ,QAAQ,OAAO;AAC9C,QAAI,CAAC,SAAS,OACZ,QAAO,KAAK;KACV;KACA,OAAO;KACP;KACA,OAAO;KACP,OAAO;KACP,SAAS,kCAAkC,OAAO;KACnD,CAAC;aACO,SAAS;UACb,MAAM,OAAO,MAChB,KAAI,OAAO,QAAQ,YAAY,EAAE,OAAO,SAAS,SAC/C,QAAO,KAAK;MACV;MACA,OAAO;MACP;MACA,OAAO;MACP,OAAO;MACP,SAAS,qBAAqB,IAAI,iBAAiB;MACpD,CAAC;;;;;AAQd,QAAO;;;;;;;;;;;;AChJT,SAAgB,uBACd,MACA,KACA,QACM;AACN,KAAI,KAAK,eAAe,KAAA,GAAW;EACjC,MAAM,IAAI,IAAI,KAAK,KAAK,WAAW;AACnC,MAAI,OAAO,MAAM,EAAE,SAAS,CAAC,CAC3B,QAAO,KAAK;GACV,UAAU;GACV,GAAG;GACH,SAAS,6BAA6B,KAAK,WAAW;GACvD,CAAC;;AAGN,KAAI,KAAK,cAAc,KAAA,GAAW;EAChC,MAAM,IAAI,IAAI,KAAK,KAAK,UAAU;AAClC,MAAI,OAAO,MAAM,EAAE,SAAS,CAAC,CAC3B,QAAO,KAAK;GACV,UAAU;GACV,GAAG;GACH,SAAS,4BAA4B,KAAK,UAAU;GACrD,CAAC;;AAGN,KAAI,KAAK,eAAe,KAAA,KAAa,KAAK,cAAc,KAAA,GAAW;EACjE,MAAM,UAAU,IAAI,KAAK,KAAK,WAAW;EACzC,MAAM,UAAU,IAAI,KAAK,KAAK,UAAU;AACxC,MAAI,CAAC,OAAO,MAAM,QAAQ,SAAS,CAAC,IAAI,CAAC,OAAO,MAAM,QAAQ,SAAS,CAAC,IAAI,WAAW,QACrF,QAAO,KAAK;GACV,UAAU;GACV,GAAG;GACH,SAAS,eAAe,KAAK,UAAU,gCAAgC,KAAK,WAAW;GACxF,CAAC;;;;;ACLR,eAAe,kBAAqB,QAAoB,MAAiC;AACvF,KAAI;AACF,SAAO,KAAK,MAAM,MAAM,OAAO,SAAS,KAAK,CAAC;SACxC;AACN,SAAO;;;AAIX,eAAe,kBAAkB,QAAoB,MAAsC;AACzF,KAAI;AACF,SAAO,MAAM,OAAO,SAAS,KAAK;SAC5B;AACN,SAAO;;;;;;;;;;;AAcX,SAAS,2BACP,QACA,QAC0E;AAC1E,QAAO,OAAO,eAAe,iBAAiB;EAC5C,MAAM,cAAc,MAAM,UAAU,QAAQ,cAAc;AAC1D,MAAI,CAAC,YAAa,QAAO,EAAE,QAAQ,OAAO;AAE1C,MAAI,YAAY,SAAS,YAAY;GAEnC,MAAM,QAAQ,MAAM,sBAAsB,QADpB,eAAe,YAAY,EACgB,YAAY;AAC7E,UAAO;IAAE,QAAQ;IAAM,SAAS,OAAO,YAAY,MAAM,KAAI,MAAK,CAAC,GAAG,KAAK,CAAC,CAAC;IAAE;;AAEjF,MAAI,YAAY,SAAS,eAAe,YAAY,SAAS,aAC3D,QAAO;GAAE,QAAQ;GAAM,SAAS;GAAM;EAOxC,MAAM,SAAkC,EAAE;EAC1C,MAAM,UAAU,MAAM,kBACpB,QACA,gBAAgB,aAAa,aAAa,CAC3C;AACD,MAAI,QAAS,QAAO,OAAO,QAAQ,QAAQ;AAC3C,MAAI,YAAY,QAAQ,iBAAiB,OAAO,QAAQ,SAAS;GAC/D,MAAM,WAAW,MAAM,kBACrB,QACA,gBAAgB,aAAa,OAAO,QAAQ,QAAQ,CACrD;AACD,OAAI,SAAU,QAAO,OAAO,QAAQ,SAAS;;AAE/C,SAAO;GAAE,QAAQ;GAAM,SAAS;GAAQ;;;;;;;;;AAU5C,SAAS,+BACP,MACA,UACA,KACA,QACM;AACN,MAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,KAAK,EAAE;AACrD,MAAI,YAAY,aAAa,SAAU;AACvC,MAAI,cAAc,MAAM,CAAC,SAAS,EAChC,QAAO,KAAK;GACV,UAAU;GACV,GAAG;GACH,OAAO;GACP,SAAS,uCAAuC,UAAU;GAC3D,CAAC;;;AAOR,eAAe,wBACb,QACA,aACA,OACA,QACA,QACA,KAC6C;CAC7C,IAAI,iBAAiB;CACrB,IAAI,QAAQ;CAIZ,MAAM,UAAU,MAAM,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,QAAQ,QAAQ;CAGhF,MAAM,iBAA8C,EAAE;CACtD,MAAM,8BAAc,IAAI,KAAa;CAErC,MAAM,gBAAgB,2BAA2B,QAAQ,OAAO;AAEhE,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,WAAW,gBAAgB,OAAO,OAAO;EAC/C,MAAM,OAAO,MAAM,kBAA2D,QAAQ,SAAS;AAC/F,MAAI,CAAC,MAAM;AACT,OAAI,MAAM,MAAM;AACd,WAAO,KAAK;KACV,UAAU;KACV,OAAO,MAAM;KACb;KACA,SAAS,wBAAwB,OAAO;KACzC,CAAC;AAEF,QAAI,OAAO,aAAa;AACtB,WAAM,UAAU,KAAK,aAAa,SAAS,EAAE,EAAE,CAAC;AAChD;;;AAGJ;;EAGF,MAAM,MAAM,IAAI,IAAI,OAAO,KAAK,KAAK,CAAC;AACtC,iBAAe,UAAU;AACzB,OAAK,MAAM,MAAM,IAAK,aAAY,IAAI,GAAG;EAGzC,MAAM,OAAO,OAAO,KAAK,KAAK;EAC9B,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,UAAU;AACnC,MAAI,KAAK,KAAK,IAAI,KAAK,OAAO,KAAK,IAAI,EAAE;AACvC,UAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA,SAAS;IACV,CAAC;AACF,OAAI,OAAO,aAAa;IACtB,MAAM,WAAoD,EAAE;AAC5D,SAAK,MAAM,OAAO,OAChB,UAAS,OAAO,KAAK;AAEvB,UAAM,UAAU,KAAK,aAAa,SAAS,EAAE,SAAS;AACtD;;;AAOJ,OAAK,MAAM,CAAC,SAAS,WAAW,OAAO,QAAQ,KAAK,EAAE;AACpD;AAEA,kCACE,QACA,MAAM,QACN;IAAE,OAAO,MAAM;IAAI;IAAQ,OAAO;IAAS,EAC3C,OACD;AAED,OAAI,CAAC,MAAM,OAAQ;GAEnB,MAAM,cAAc,gBAClB,QACA,MAAM,QACN,MAAM,IACN,QACA,SACA;IAAE,YAAY;IAAM,gBAAgB;IAAS,CAC9C;AACD,UAAO,KAAK,GAAG,YAAY,OAAO;GAElC,MAAM,iBAAiB,MAAM,uBAC3B,QACA,MAAM,QACN,MAAM,IACN,QACA,SACA,YAAY,MACZ;IAAE,UAAU;IAAS;IAAe,CACrC;AACD,UAAO,KAAK,GAAG,eAAe;;;AAKlC,KAAI,MAAM,QAAQ,OAAO,KAAK,eAAe,CAAC,SAAS,GAAG;EACxD,MAAM,aAAa,OAAO,KAAK,eAAe;AAC9C,OAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;GAC1C,MAAM,OAAO,WAAW;GACxB,MAAM,OAAO,WAAW;GACxB,MAAM,OAAO,eAAe;GAC5B,MAAM,OAAO,eAAe;AAE5B,QAAK,MAAM,MAAM,KACf,KAAI,CAAC,KAAK,IAAI,GAAG,CACf,QAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb,QAAQ;IACR,OAAO;IACP,SAAS,wBAAwB,GAAG,cAAc,KAAK,kBAAkB;IAC1E,CAAC;AAGN,QAAK,MAAM,MAAM,KACf,KAAI,CAAC,KAAK,IAAI,GAAG,CACf,QAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb,QAAQ;IACR,OAAO;IACP,SAAS,wBAAwB,GAAG,cAAc,KAAK,kBAAkB;IAC1E,CAAC;;;AAOV,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,cAAc,aAAa,OAAO,OAAO;EAC/C,MAAM,WAAW,MAAM,kBAA6C,QAAQ,YAAY;EACxF,MAAM,cAAc,MAAM,kBACxB,QACA,gBAAgB,OAAO,OAAO,CAC/B,IAAI,EAAE;AAGP,MAAI,SACF,MAAK,MAAM,eAAe,OAAO,KAAK,SAAS,EAAE;AAE/C,0BAAuB,SAAS,cAAe;IAAE,OAAO,MAAM;IAAI;IAAQ,OAAO;IAAa,EAAE,OAAO;AAEvG,OAAI,EAAE,eAAe,cAAc;AACjC,WAAO,KAAK;KACV,UAAU;KACV,OAAO,MAAM;KACb;KACA,OAAO;KACP,SAAS,4BAA4B,YAAY;KAClD,CAAC;AACF,QAAI,OAAO,aAAa;AACtB,YAAO,SAAS;KAChB,MAAM,UAAU,KAAK,aAAa,YAAY;AAC9C,SAAI,OAAO,KAAK,SAAS,CAAC,SAAS,EACjC,OAAM,UAAU,SAAS,SAAS;SAElC,OAAM,GAAG,SAAS,EAAE,OAAO,MAAM,CAAC;AAEpC;;;;AAOR,OAAK,MAAM,WAAW,OAAO,KAAK,YAAY,CAC5C,KAAI,CAAC,YAAY,EAAE,WAAW,WAAW;AACvC,UAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA,OAAO;IACP,SAAS,0BAA0B,QAAQ;IAC5C,CAAC;AACF,OAAI,OAAO,aAAa;AACtB,UAAM,UAAU,aAAa,OAAO;KAAE;KAAQ;KAAS,EAAE;KACvD,QAAQ;KACR,QAAQ;KACR,YAAY;KACb,CAAC;AACF;;;;AAMR,QAAO;EAAE,SAAS;EAAgB;EAAO;;AAG3C,eAAe,uBACb,QACA,aACA,OACA,QACA,QACA,KAC6C;CAC7C,IAAI,iBAAiB;CACrB,IAAI,QAAQ;AAEZ,MAAK,MAAM,UAAW,MAAM,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,QAAQ,QAAQ,EAAG;EACvF,MAAM,WAAW,gBAAgB,OAAO,OAAO;EAC/C,MAAM,OAAO,MAAM,kBAA2C,QAAQ,SAAS;AAE/E,MAAI,CAAC,MAAM;AACT,OAAI,MAAM,MAAM;AACd,WAAO,KAAK;KACV,UAAU;KACV,OAAO,MAAM;KACb;KACA,SAAS,wBAAwB,OAAO;KACzC,CAAC;AACF,QAAI,OAAO,aAAa;AACtB,WAAM,UAAU,KAAK,aAAa,SAAS,EAAE,EAAE,CAAC;AAChD;;;AAGJ;;AAGF;AAEA,iCAA+B,MAAM,MAAM,QAAQ;GAAE,OAAO,MAAM;GAAI;GAAQ,EAAE,OAAO;AAEvF,MAAI,MAAM,QAAQ;GAChB,MAAM,cAAc,gBAAgB,MAAM,MAAM,QAAQ,MAAM,IAAI,OAAO;AACzE,UAAO,KAAK,GAAG,YAAY,OAAO;GAElC,MAAM,gBAAgB,2BAA2B,QAAQ,OAAO;GAChE,MAAM,iBAAiB,MAAM,uBAC3B,MACA,MAAM,QACN,MAAM,IACN,QACA,KAAA,GACA,YAAY,MACZ;IAAE,UAAU;IAAS;IAAe,CACrC;AACD,UAAO,KAAK,GAAG,eAAe;;EAIhC,MAAM,OAAO,OAAO,KAAK,KAAK;EAC9B,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,UAAU;AACnC,MAAI,KAAK,KAAK,IAAI,KAAK,OAAO,KAAK,IAAI,EAAE;AACvC,UAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA,SAAS;IACV,CAAC;AACF,OAAI,OAAO,aAAa;IACtB,MAAM,WAAoC,EAAE;AAC5C,SAAK,MAAM,OAAO,OAChB,UAAS,OAAO,KAAK;AAEvB,UAAM,UAAU,KAAK,aAAa,SAAS,EAAE,SAAS;AACtD;;;EAKJ,MAAM,oBAAoB,MAAM,kBAA6B,QAAQ,aAAa,OAAO,OAAO,CAAC;AACjG,MAAI,kBACF,wBAAuB,mBAAmB;GAAE,OAAO,MAAM;GAAI;GAAQ,EAAE,OAAO;;AAIlF,QAAO;EAAE,SAAS;EAAgB;EAAO;;AAG3C,eAAe,wBACb,QACA,aACA,OACA,QACA,QACA,KAC6C;CAC7C,IAAI,iBAAiB;CACrB,IAAI,QAAQ;CAEZ,MAAM,aAA0C,EAAE;AAElD,MAAK,MAAM,UAAW,MAAM,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,QAAQ,QAAQ,EAAG;EACvF,MAAM,WAAW,gBAAgB,OAAO,OAAO;EAC/C,MAAM,OAAO,MAAM,kBAA0C,QAAQ,SAAS;AAE9E,MAAI,CAAC,MAAM;AACT,OAAI,MAAM,MAAM;AACd,WAAO,KAAK;KACV,UAAU;KACV,OAAO,MAAM;KACb;KACA,SAAS,wBAAwB,OAAO;KACzC,CAAC;AACF,QAAI,OAAO,aAAa;AACtB,WAAM,UAAU,KAAK,aAAa,SAAS,EAAE,EAAE,CAAC;AAChD;;;AAGJ;;AAGF;AACA,aAAW,UAAU,IAAI,IAAI,OAAO,KAAK,KAAK,CAAC;AAG/C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,CAC7C,KAAI,cAAc,MAAM,CAAC,SAAS,EAChC,QAAO,KAAK;GACV,UAAU;GACV,OAAO,MAAM;GACb;GACA,OAAO;GACP,SAAS,qCAAqC,IAAI;GACnD,CAAC;EAKN,MAAM,8BAAc,IAAI,KAAuB;AAC/C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,EAAE;GAC/C,MAAM,MAAM,YAAY,IAAI,MAAM;AAClC,OAAI,IAAK,KAAI,KAAK,IAAI;OACjB,aAAY,IAAI,OAAO,CAAC,IAAI,CAAC;;AAEpC,OAAK,MAAM,CAAC,OAAO,aAAa,YAC9B,KAAI,SAAS,SAAS,GAAG;GACvB,MAAM,YAAY,MAAM,SAAS,KAAK,GAAG,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO;AACnE,UAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA,SAAS,oBAAoB,UAAU,cAAc,SAAS,OAAO,UAAU,SAAS,KAAK,KAAK,CAAC;IACpG,CAAC;;EAKN,MAAM,OAAO,OAAO,KAAK,KAAK;EAC9B,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,UAAU;AACnC,MAAI,KAAK,KAAK,IAAI,KAAK,OAAO,KAAK,IAAI,EAAE;AACvC,UAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA,SAAS;IACV,CAAC;AACF,OAAI,OAAO,aAAa;IACtB,MAAM,WAAmC,EAAE;AAC3C,SAAK,MAAM,OAAO,OAChB,UAAS,OAAO,KAAK;AAEvB,UAAM,UAAU,KAAK,aAAa,SAAS,EAAE,SAAS;AACtD;;;;AAMN,KAAI,MAAM,QAAQ,OAAO,KAAK,WAAW,CAAC,SAAS,GAAG;EACpD,MAAM,cAAc,OAAO,KAAK,WAAW;AAC3C,OAAK,IAAI,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;GAC3C,MAAM,OAAO,YAAY;GACzB,MAAM,OAAO,YAAY;GACzB,MAAM,QAAQ,WAAW;GACzB,MAAM,QAAQ,WAAW;AAEzB,QAAK,MAAM,KAAK,MACd,KAAI,CAAC,MAAM,IAAI,EAAE,CACf,QAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb,QAAQ;IACR,OAAO;IACP,SAAS,oBAAoB,EAAE,cAAc,KAAK,kBAAkB;IACrE,CAAC;AAGN,QAAK,MAAM,KAAK,MACd,KAAI,CAAC,MAAM,IAAI,EAAE,CACf,QAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb,QAAQ;IACR,OAAO;IACP,SAAS,oBAAoB,EAAE,cAAc,KAAK,kBAAkB;IACrE,CAAC;;;AAMV,QAAO;EAAE,SAAS;EAAgB;EAAO;;AAG3C,eAAe,sBACb,QACA,MACA,OACmB;CACnB,MAAM,WAAW,sBAAsB,MAAM;CAC7C,MAAM,UAAU,MAAM,OAAO,cAAc,KAAK;AAGhD,KAAI,CAAC,MAAM,KACT,QAAO,QAAQ,QAAO,MAAK,EAAE,SAAS,MAAM,CAAC,CAAC,KAAI,MAAK,EAAE,QAAQ,SAAS,GAAG,CAAC;AAGhF,KAAI,aAAa,OAEf,QAAO,QAAQ,QAAO,MAAK,CAAC,EAAE,WAAW,IAAI,CAAC;AAGhD,KAAI,aAAa,UAAU;EAEzB,MAAM,wBAAQ,IAAI,KAAa;AAC/B,OAAK,MAAM,KAAK,SAAS;AACvB,OAAI,CAAC,EAAE,SAAS,MAAM,CAAE;GAExB,MAAM,QAAQ,EAAE,QAAQ,SAAS,GAAG,CAAC,MAAM,IAAI;AAC/C,OAAI,MAAM,UAAU,GAAG;AACrB,UAAM,KAAK;AACX,UAAM,IAAI,MAAM,KAAK,IAAI,CAAC;;;AAG9B,SAAO,CAAC,GAAG,MAAM;;AAGnB,KAAI,aAAa,aAAa;EAE5B,MAAM,wBAAQ,IAAI,KAAa;EAC/B,MAAM,cAAc,MAAM,QAAQ,IAChC,QACG,QAAO,cAAa,CAAC,UAAU,WAAW,IAAI,CAAC,CAC/C,KAAI,cAAa,OAAO,cAAc,GAAG,KAAK,GAAG,YAAY,CAAC,CAClE;AACD,OAAK,MAAM,SAAS,YAClB,MAAK,MAAM,KAAK,MACd,KAAI,EAAE,SAAS,MAAM,CAAE,OAAM,IAAI,EAAE,QAAQ,SAAS,GAAG,CAAC;AAG5D,SAAO,CAAC,GAAG,MAAM;;AAInB,QAAO,QAAQ,QAAO,MAAK,EAAE,SAAS,MAAM,CAAC,CAAC,KAAI,MAAK,EAAE,QAAQ,SAAS,GAAG,CAAC;;AAGhF,eAAe,sBACb,QACA,aACA,OACA,QACA,QACA,KAC6C;CAC7C,IAAI,iBAAiB;CACrB,IAAI,QAAQ;CACZ,MAAM,OAAO,eAAe,MAAM;AAElC,KAAI,CAAC,MAAM,OAAO,WAAW,KAAK,CAAE,QAAO;EAAE,SAAS;EAAG,OAAO;EAAG;CAEnE,MAAM,QAAQ,MAAM,sBAAsB,QAAQ,MAAM,MAAM;CAI9D,MAAM,UAAU,MAAM,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,QAAQ,QAAQ;AAEhF,MAAK,MAAM,QAAQ,OAAO;AACxB,MAAI,KAAK,WAAW,IAAI,CAAE;AAE1B,OAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,WAAW,iBAAiB,OAAO,QAAQ,KAAK;GACtD,MAAM,MAAM,MAAM,kBAAkB,QAAQ,SAAS;AAErD,OAAI,CAAC,KAAK;AACR,QAAI,MAAM,MAAM;AACd,YAAO,KAAK;MACV,UAAU;MACV,OAAO,MAAM;MACb;MACA;MACA,SAAS,kCAAkC,KAAK,YAAY,OAAO;MACpE,CAAC;AACF,SAAI,OAAO,aAAa;MAEtB,MAAM,WAAW,cAAc,KAAK;AACpC,YAAM,UAAU,KAAK,aAAa,SAAS,EAAE,SAAS;AACtD;;;AAGJ;;AAGF;GACA,MAAM,EAAE,aAAa,SAAS,iBAAiB,IAAI;AAGnD,kCACE,aAGA;IAAE,GAAG,MAAM;IAAQ,MAAM;IAAM,EAC/B;IAAE,OAAO,MAAM;IAAI;IAAQ;IAAM,EACjC,OACD;AAED,OAAI,cAAc,KAAK,CAAC,SAAS,EAC/B,QAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA;IACA,OAAO;IACP,SAAS;IACV,CAAC;AAGJ,OAAI,MAAM,QAAQ;IAChB,MAAM,oBAAoB,OAAO,YAC/B,OAAO,QAAQ,MAAM,OAAO,CAAC,QAAQ,CAAC,UAAU,SAAS,OAAO,CACjE;IACD,MAAM,cAAc,gBAClB,aACA,mBACA,MAAM,IACN,OACD;AACD,SAAK,MAAM,OAAO,YAAY,OAC5B,QAAO,KAAK;KAAE,GAAG;KAAK;KAAM,CAAC;IAG/B,MAAM,gBAAgB,2BAA2B,QAAQ,OAAO;IAChE,MAAM,iBAAiB,MAAM,uBAC3B,aACA,mBACA,MAAM,IACN,QACA,KAAA,GACA,YAAY,MACZ;KAAE,UAAU;KAAS;KAAe,CACrC;AACD,SAAK,MAAM,OAAO,eAChB,QAAO,KAAK;KAAE,GAAG;KAAK;KAAM,CAAC;;;;AAMrC,QAAO;EAAE,SAAS;EAAgB;EAAO;;AAK3C,eAAe,mBACb,QACA,eACA,QACA,MACiB;CACjB,MAAM,QAAQ;CACd,MAAM,cAAc;CACpB,MAAM,UAAU,MAAM,OAAO,cAAc,YAAY;CAEvD,MAAM,aAAa,MAAM,QAAQ,IAC/B,QACG,QAAO,MAAK,CAAC,EAAE,WAAW,IAAI,CAAC,CAC/B,IAAI,OAAM,OAAM;EAAE,QAAQ;EAAG,MAAM,MAAM,OAAO,cAAc,GAAG,YAAY,GAAG,IAAI;EAAE,EAAE,CAC5F;AAED,MAAK,MAAM,EAAE,UAAU,WACrB,MAAK,MAAM,YAAY,MAAM;AAC3B,MAAI,SAAS,WAAW,IAAI,CAAE;AAC9B,MAAI,CAAC,cAAc,IAAI,SAAS,CAC9B,QAAO,KAAK;GACV,UAAU;GACV,OAAO;GACP,SAAS,+DAA+D,SAAS;GAClF,CAAC;;AAKR,QAAO;;AAsBT,eAAsB,gBACpB,OACA,SACyB;CACzB,MAAM,SAAqB,OAAO,UAAU,WAAW,IAAI,YAAY,MAAM,GAAG;CAChF,MAAM,cAAkC,OAAO,UAAU,WAAW,QAAQ,KAAA;CAC5E,MAAM,MAAM,QAAQ,SAAS,IAAI,IAAI,gBAAgB,KAAA;CAErD,MAAM,SAA4B,EAAE;CACpC,IAAI,eAAe;CACnB,IAAI,aAAa;CACjB,IAAI,gBAAgB;CAEpB,MAAM,SAAS,MAAM,WAAW,OAAO;AACvC,KAAI,CAAC,OACH,QAAO;EACL,OAAO;EACP,SAAS;GAAE,QAAQ;GAAG,UAAU;GAAG,SAAS;GAAG,gBAAgB;GAAG,iBAAiB;GAAG;EACtF,QAAQ,CAAC;GAAE,UAAU;GAAS,SAAS;GAAgD,CAAC;EACxF,OAAO;EACR;CAGH,MAAM,iBAAiB,MAAM,WAAW,OAAO;CAC/C,MAAM,gBAAgB,IAAI,IAAI,eAAe,KAAI,MAAK,EAAE,GAAG,CAAC;CAC5D,MAAM,gBAAgB,SAAS,QAC3B,eAAe,QAAO,MAAK,EAAE,OAAO,QAAQ,MAAM,GAClD;AAEJ,MAAK,MAAM,WAAW,eAAe;EACnC,MAAM,QAAQ,MAAM,UAAU,QAAQ,QAAQ,GAAG;AACjD,MAAI,CAAC,MAAO;AAEZ;EACA,IAAI;AAEJ,UAAQ,MAAM,MAAd;GACE,KAAK;AACH,aAAS,MAAM,wBAAwB,QAAQ,aAAa,OAAO,QAAQ,QAAQ,IAAI;AACvF;GACF,KAAK;AACH,aAAS,MAAM,uBAAuB,QAAQ,aAAa,OAAO,QAAQ,QAAQ,IAAI;AACtF;GACF,KAAK;AACH,aAAS,MAAM,wBAAwB,QAAQ,aAAa,OAAO,QAAQ,QAAQ,IAAI;AACvF;GACF,KAAK;AACH,aAAS,MAAM,sBAAsB,QAAQ,aAAa,OAAO,QAAQ,QAAQ,IAAI;AACrF;GACF,QACE,UAAS;IAAE,SAAS;IAAG,OAAO;IAAG;;AAGrC,kBAAgB,OAAO;AACvB,gBAAc,OAAO;;AAIvB,KAAI,CAAC,SAAS,MACZ,eAAc,MAAM,mBAAmB,QAAQ,eAAe,QAAQ,IAAI;AAI5E,KAAI,CAAC,SAAS,OAAO;EACnB,MAAM,aAAa,cAAc,QAAO,MAAK,EAAE,SAAS,aAAa;AACrE,MAAI,WAAW,SAAS,GAAG;GACzB,MAAM,iBAAqF,EAAE;AAE7F,QAAK,MAAM,WAAW,YAAY;IAChC,MAAM,QAAQ,MAAM,UAAU,QAAQ,QAAQ,GAAG;AACjD,QAAI,CAAC,MAAO;AAEZ,SAAK,MAAM,UAAW,MAAM,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,QAAQ,QAAQ,EAAG;AACvF,SAAI,CAAC,eAAe,QAAS,gBAAe,0BAAU,IAAI,KAAK;KAC/D,MAAM,OAAO,MAAM,kBAA0C,QAAQ,gBAAgB,OAAO,OAAO,CAAC;AACpG,SAAI,CAAC,KAAM;AAEX,UAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,EAAE;MAC/C,MAAM,OAAO,eAAe,QAAS,IAAI,MAAM;AAC/C,UAAI,KAAM,MAAK,KAAK;OAAE,OAAO,MAAM;OAAI;OAAK,CAAC;UACxC,gBAAe,QAAS,IAAI,OAAO,CAAC;OAAE,OAAO,MAAM;OAAI;OAAK,CAAC,CAAC;;;;AAKzE,QAAK,MAAM,CAAC,QAAQ,aAAa,OAAO,QAAQ,eAAe,CAC7D,MAAK,MAAM,CAAC,OAAO,SAAS,SAE1B,KADqB,IAAI,IAAI,KAAK,KAAI,MAAK,EAAE,MAAM,CAAC,CACnC,OAAO,GAAG;IACzB,MAAM,YAAY,MAAM,SAAS,KAAK,GAAG,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO;AACnE,WAAO,KAAK;KACV,UAAU;KACV;KACA,SAAS,gCAAgC,UAAU,OAAO,KAAK,KAAI,MAAK,GAAG,EAAE,MAAM,GAAG,EAAE,MAAM,CAAC,KAAK,KAAK;KAC1G,CAAC;;;;CAOZ,MAAM,SAAS,OAAO,QAAO,MAAK,EAAE,aAAa,QAAQ,CAAC;CAC1D,MAAM,WAAW,OAAO,QAAO,MAAK,EAAE,aAAa,UAAU,CAAC;CAC9D,MAAM,UAAU,OAAO,QAAO,MAAK,EAAE,aAAa,SAAS,CAAC;AAE5D,QAAO;EACL,OAAO,WAAW;EAClB,SAAS;GACP;GACA;GACA;GACA,gBAAgB;GAChB,iBAAiB;GAClB;EACD;EACA,OAAO;EACR"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/mcp",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "mcpName": "io.github.Contentrain/contentrain",
5
5
  "license": "MIT",
6
6
  "description": "Local-first MCP server for AI-generated content governance — 17 deterministic tools, stdio + HTTP transports, Local / GitHub / GitLab providers",
@@ -152,7 +152,7 @@
152
152
  "simple-git": "^3.27.0",
153
153
  "typescript": "^5.7.0",
154
154
  "zod": "^3.24.0",
155
- "@contentrain/types": "0.5.0"
155
+ "@contentrain/types": "0.5.1"
156
156
  },
157
157
  "optionalDependencies": {
158
158
  "@astrojs/compiler": "^2.10.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"config-BIl5Ybj0.mjs","names":[],"sources":["../src/core/config.ts"],"sourcesContent":["import type { ContentrainConfig, Vocabulary } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { contentrainDir, readJson } from '../util/fs.js'\nimport type { RepoReader } from './contracts/index.js'\n\nconst CONFIG_PATH = '.contentrain/config.json'\nconst VOCABULARY_PATH = '.contentrain/vocabulary.json'\n\nasync function readJsonViaReader<T>(reader: RepoReader, path: string): Promise<T | null> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return null\n }\n}\n\nfunction normaliseConfig(raw: Partial<ContentrainConfig> | null): ContentrainConfig | null {\n if (!raw) return null\n return {\n version: raw.version ?? 1,\n stack: raw.stack ?? 'other',\n workflow: raw.workflow ?? 'auto-merge',\n locales: {\n default: raw.locales?.default ?? 'en',\n supported: raw.locales?.supported ?? [raw.locales?.default ?? 'en'],\n },\n domains: raw.domains ?? [],\n repository: raw.repository,\n assets_path: raw.assets_path,\n branchRetention: raw.branchRetention,\n }\n}\n\n/**\n * Read `.contentrain/config.json` and normalise it with safe defaults.\n *\n * Accepts either a legacy `projectRoot` string (LocalReader is constructed\n * internally) or any `RepoReader` — tool handlers pass their provider so\n * the same logic resolves against local fs or the GitHub Git Data API.\n */\nexport function readConfig(projectRoot: string): Promise<ContentrainConfig | null>\nexport function readConfig(reader: RepoReader): Promise<ContentrainConfig | null>\nexport async function readConfig(input: string | RepoReader): Promise<ContentrainConfig | null> {\n if (typeof input === 'string') {\n const raw = await readJson<Partial<ContentrainConfig>>(join(contentrainDir(input), 'config.json'))\n return normaliseConfig(raw)\n }\n const raw = await readJsonViaReader<Partial<ContentrainConfig>>(input, CONFIG_PATH)\n return normaliseConfig(raw)\n}\n\n/**\n * Read `.contentrain/vocabulary.json`. Same dual signature as {@link readConfig}.\n */\nexport function readVocabulary(projectRoot: string): Promise<Vocabulary | null>\nexport function readVocabulary(reader: RepoReader): Promise<Vocabulary | null>\nexport async function readVocabulary(input: string | RepoReader): Promise<Vocabulary | null> {\n if (typeof input === 'string') {\n return readJson<Vocabulary>(join(contentrainDir(input), 'vocabulary.json'))\n }\n return readJsonViaReader<Vocabulary>(input, VOCABULARY_PATH)\n}\n"],"mappings":";;;AAKA,MAAM,cAAc;AACpB,MAAM,kBAAkB;AAExB,eAAe,kBAAqB,QAAoB,MAAiC;AACvF,KAAI;AACF,SAAO,KAAK,MAAM,MAAM,OAAO,SAAS,KAAK,CAAC;SACxC;AACN,SAAO;;;AAIX,SAAS,gBAAgB,KAAkE;AACzF,KAAI,CAAC,IAAK,QAAO;AACjB,QAAO;EACL,SAAS,IAAI,WAAW;EACxB,OAAO,IAAI,SAAS;EACpB,UAAU,IAAI,YAAY;EAC1B,SAAS;GACP,SAAS,IAAI,SAAS,WAAW;GACjC,WAAW,IAAI,SAAS,aAAa,CAAC,IAAI,SAAS,WAAW,KAAK;GACpE;EACD,SAAS,IAAI,WAAW,EAAE;EAC1B,YAAY,IAAI;EAChB,aAAa,IAAI;EACjB,iBAAiB,IAAI;EACtB;;AAYH,eAAsB,WAAW,OAA+D;AAC9F,KAAI,OAAO,UAAU,SAEnB,QAAO,gBADK,MAAM,SAAqC,KAAK,eAAe,MAAM,EAAE,cAAc,CAAC,CACvE;AAG7B,QAAO,gBADK,MAAM,kBAA8C,OAAO,YAAY,CACxD;;AAQ7B,eAAsB,eAAe,OAAwD;AAC3F,KAAI,OAAO,UAAU,SACnB,QAAO,SAAqB,KAAK,eAAe,MAAM,EAAE,kBAAkB,CAAC;AAE7E,QAAO,kBAA8B,OAAO,gBAAgB"}