@contentrain/mcp 3.10.1 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/dist/{apply-manager-B-O3eAbz.mjs → apply-manager-DfLr1Q-T.mjs} +2 -2
- package/dist/{apply-manager-B-O3eAbz.mjs.map → apply-manager-DfLr1Q-T.mjs.map} +1 -1
- package/dist/core/apply-manager.mjs +2 -2
- package/dist/core/ops/index.mjs +2 -2
- package/dist/core/validator/index.d.mts.map +1 -1
- package/dist/core/validator/index.mjs +1 -1
- package/dist/{errors-De8Noahk.mjs → errors-Cp6_Hlws.mjs} +2 -2
- package/dist/{errors-De8Noahk.mjs.map → errors-Cp6_Hlws.mjs.map} +1 -1
- package/dist/git/errors.mjs +1 -1
- package/dist/git/reconcile.d.mts.map +1 -1
- package/dist/git/reconcile.mjs +4 -4
- package/dist/git/transaction.d.mts +4 -0
- package/dist/git/transaction.d.mts.map +1 -1
- package/dist/git/transaction.mjs +1 -1
- package/dist/index.mjs +8 -8
- package/dist/{local-CBtRYFH4.mjs → local-CCVjLKy2.mjs} +6 -5
- package/dist/local-CCVjLKy2.mjs.map +1 -0
- package/dist/providers/local/index.d.mts.map +1 -1
- package/dist/providers/local/index.mjs +4 -4
- package/dist/{reconcile-DSflMfhj.mjs → reconcile-B7w5gow1.mjs} +4 -4
- package/dist/reconcile-B7w5gow1.mjs.map +1 -0
- package/dist/{reconcile-J-iorvSa.mjs → reconcile-CYmKsXxx.mjs} +2 -2
- package/dist/{reconcile-J-iorvSa.mjs.map → reconcile-CYmKsXxx.mjs.map} +1 -1
- package/dist/server/http/index.d.mts +1 -1
- package/dist/server/http/index.mjs +8 -8
- package/dist/{server-24UHN9Ll.d.mts → server-CQKCQ8H_.d.mts} +2 -2
- package/dist/{server-24UHN9Ll.d.mts.map → server-CQKCQ8H_.d.mts.map} +1 -1
- package/dist/{server-DvWJelCh.mjs → server-tPKMBPdS.mjs} +38 -23
- package/dist/server-tPKMBPdS.mjs.map +1 -0
- package/dist/server.d.mts +1 -1
- package/dist/server.mjs +8 -8
- package/dist/{transaction-DnrUATJF.mjs → transaction-DCDf0y0T.mjs} +82 -16
- package/dist/transaction-DCDf0y0T.mjs.map +1 -0
- package/dist/{validator-S19GHR4V.mjs → validator-mfCY3eea.mjs} +27 -2
- package/dist/validator-mfCY3eea.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/local-CBtRYFH4.mjs.map +0 -1
- package/dist/reconcile-DSflMfhj.mjs.map +0 -1
- package/dist/server-DvWJelCh.mjs.map +0 -1
- package/dist/transaction-DnrUATJF.mjs.map +0 -1
- package/dist/validator-S19GHR4V.mjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"transaction-DnrUATJF.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 { type SimpleGit } from 'simple-git'\nimport { createGit } from '../git/identity.js'\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 { deleteRemoteBranch, networkGit, type RemoteDeleteResult } from './branch-lifecycle.js'\nimport { authorConfig } from './identity.js'\nimport { branchTimestamp } from '../util/id.js'\nimport { migrateLegacyBranches } from '../providers/local/migration.js'\nimport { resolveBaseBranch } from './base-branch.js'\nimport { pushPolicy } from './push-policy.js'\n\n// Re-exported so hosts of mergeBranch (CLI serve) word local mode the same way.\nexport { LOCAL_MODE_NOTE } from './push-policy.js'\nimport type { BaseAdvance, RemotePush, SyncResult, WorkflowMode } from '@contentrain/types'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\n\n/**\n * Block-timeout for fetch/push inside transactions. These calls inherit the\n * host's credential setup (see networkGit); without a timeout a hung SSH\n * passphrase prompt would hang the MCP call with it.\n */\nexport const NETWORK_TIMEOUT_MS = 10_000\n\nexport interface ContextUpdate {\n tool: string\n model: string\n locale?: string\n entries?: string[]\n}\n\n/**\n * Result of completing a write transaction.\n *\n * A diverged base branch is NOT an error: the content is committed and merged\n * into the contentrain branch either way — only the base fast-forward is\n * pending. `base_advance: 'blocked_diverged'` reports that truthfully instead\n * of failing a write that in fact landed.\n */\nexport interface CompleteResult {\n action: 'auto-merged' | 'pending-review'\n commit: string\n sync?: SyncResult\n warning?: string\n /** Present on `auto-merged`: did the base branch advance to the contentrain tip? */\n base_advance?: BaseAdvance\n /** Present on `auto-merged`: outcome of pushing the contentrain branch. */\n remote_push?: RemotePush\n}\n\n/** Result of landing a feature branch via {@link mergeBranch}. */\nexport interface MergeBranchResult {\n action: 'merged'\n commit: string\n sync: SyncResult\n base_advance: BaseAdvance\n remote_push: RemotePush\n warning?: string\n remote?: RemoteDeleteResult\n}\n\nexport interface GitTransaction {\n worktree: string\n branch: string\n /** The resolved base branch this transaction advances. */\n baseBranch: string\n /** Whether the developer has the base branch checked out. */\n baseCheckedOut: boolean\n write(callback: (worktreePath: string) => Promise<void>): Promise<void>\n commit(message: string, contextUpdate?: ContextUpdate): Promise<string>\n complete(): Promise<CompleteResult>\n cleanup(): Promise<void>\n}\n\nexport async function ensureContentBranch(projectRoot: string): Promise<void> {\n const git = createGit(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 = await resolveBaseBranch(git, config)\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 — unless pushing is off (local mode).\n if (!pushPolicy(config).push) return\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\nexport async function selectiveSync(\n projectRoot: string,\n _worktreePath: string,\n contentrainTip: string,\n _previousBaseRef?: string,\n dirtyFilesBeforeUpdate?: Set<string>,\n): Promise<SyncResult> {\n const git = createGit(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 // Which changed files still exist in contentrainTip (HEAD after update-ref)?\n // ONE `ls-tree` over the changed paths lists exactly the survivors, instead\n // of a `cat-file -e` spawn per file. Falls back to per-file probing if\n // ls-tree fails so behavior is preserved on any edge.\n const filesInTip = new Set<string>()\n try {\n const lsOutput = await git.raw(['ls-tree', '-r', '--name-only', contentrainTip, '--', ...changedFiles])\n for (const f of lsOutput.split('\\n')) {\n const trimmed = f.trim()\n if (trimmed) filesInTip.add(trimmed)\n }\n } catch {\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\n // Partition: dirty developer files are skipped; survivors get checked out\n // from HEAD; the rest were deleted in the new HEAD and are removed on disk.\n const toCheckout: string[] = []\n const toRemove: string[] = []\n for (const file of changedFiles) {\n if (dirtyFiles.has(file)) skipped.push(file)\n else if (filesInTip.has(file)) toCheckout.push(file)\n else toRemove.push(file)\n }\n\n // ONE `git checkout HEAD -- f1 f2 …` restores every clean survivor at once.\n // On failure, fall back to per-file so a single unresolvable path still\n // yields precise skip accounting (dirty files were already excluded).\n if (toCheckout.length > 0) {\n try {\n await git.checkout(['HEAD', '--', ...toCheckout])\n synced.push(...toCheckout)\n } catch {\n for (const file of toCheckout) {\n try {\n await git.checkout(['HEAD', '--', file])\n synced.push(file)\n } catch {\n skipped.push(file)\n }\n }\n }\n }\n\n // Deletions are working-tree fs removals — no git spawn, safe to parallelize.\n await Promise.all(toRemove.map(async (file) => {\n try {\n await removeFile(join(projectRoot, file), { force: true })\n synced.push(file)\n } catch {\n skipped.push(file)\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 = createGit(projectRoot)\n const config = await readConfig(projectRoot)\n const workflow = options?.workflowOverride ?? config?.workflow ?? 'auto-merge'\n const push = pushPolicy(config).push\n\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n // Detect current branch + remote in ONE batch, then the base branch.\n const [branchResult, remotes] = await Promise.all([\n git.raw(['branch', '--show-current']).catch(() => ''),\n git.getRemotes().catch(() => []),\n ])\n const currentBranch = branchResult.trim()\n const baseBranch = await resolveBaseBranch(git, config, { remoteName, currentBranch })\n const hasRemote = (remotes as { name: string }[]).some(r => r.name === remoteName)\n // The developer's HEAD moves with an advance only when the base is what\n // they have checked out. On any other branch their tree stays exactly as\n // it is — no index refresh, no selective sync.\n const baseCheckedOut = currentBranch === baseBranch\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). Hardened:\n // no interactive credential prompt, block-timeout on hangs.\n if (hasRemote) {\n const net = networkGit(projectRoot, NETWORK_TIMEOUT_MS)\n await Promise.all([\n net.fetch(remoteName, baseBranch).catch(() => {}),\n net.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 // Commit identity comes from `-c user.*` config (see authorConfig) — passed\n // as args, never via `.env()`, so simple-git's block-unsafe guard is never\n // triggered by an inherited EDITOR/GIT_ASKPASS/etc.\n const wtGit = createGit(worktreePath, { config: authorConfig() })\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 // Set once the feature branch has been merged into the contentrain branch,\n // which is what makes deleting it in cleanup() safe — the work is reachable\n // from contentrain at that point.\n let mergedIntoContentrain = false\n let savedContextUpdate: ContextUpdate | undefined\n\n return {\n worktree: worktreePath,\n branch,\n baseBranch,\n baseCheckedOut,\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 // Mark the branch as surviving BEFORE attempting the push. The commit\n // already exists at this point, so a push failure (no write access,\n // offline, expired credentials) must not make cleanup() treat this as\n // a failed transaction and delete the branch — that silently discards\n // committed work. Pushing is a convenience here; contentrain_submit is\n // the documented way to publish a review branch.\n pendingReview = true\n\n let warning: string | undefined\n if (hasRemote && push) {\n try {\n await networkGit(projectRoot, NETWORK_TIMEOUT_MS).push(remoteName, branch)\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error)\n warning = `Changes are committed locally on \"${branch}\", but pushing to \"${remoteName}\" failed: ${detail.trim()}. `\n + `The branch is intact — run contentrain_submit once you have push access, or merge it locally with contentrain_merge.`\n }\n }\n\n return { action: 'pending-review', commit: commitHash, warning, ...(push ? {} : { remote_push: 'disabled' as const }) }\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 // The commit is now reachable from contentrain, so cleanup() may prune\n // the feature branch. Anything that fails after this point still leaves\n // the work safe.\n mergedIntoContentrain = true\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 // Fast-forward check: baseBranch must be an ancestor of contentrainTip.\n // `rev-list --count` instead of `merge-base --is-ancestor`: the latter\n // signals via exit code with empty stderr, which simple-git reports as\n // success — the check would silently pass on divergence.\n //\n // Divergence is NOT an error here. The content is already committed and\n // merged into contentrain (the CDN's source of truth) — only the base\n // fast-forward is impossible. Throwing at this point used to report a\n // failure for a write that had in fact landed, while cleanup() deleted\n // the cr/* branch. Report the truth instead.\n let sync: SyncResult = { synced: [], skipped: [] }\n let baseAdvance: BaseAdvance\n let divergedWarning: string | undefined\n if (await isAncestor(git, previousBaseRef, contentrainTip)) {\n baseAdvance = 'advanced'\n\n sync = await advanceBase(git, {\n projectRoot, worktreePath, baseBranch, currentBranch, baseCheckedOut,\n contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate,\n })\n } else {\n baseAdvance = 'blocked_diverged'\n divergedWarning = `Content is committed and merged into \"${CONTENTRAIN_BRANCH}\", but \"${baseBranch}\" has commits that are not in \"${CONTENTRAIN_BRANCH}\" — the branches have diverged, so \"${baseBranch}\" was not advanced. Run contentrain_reconcile (CLI: npx contentrain reconcile) to merge them and restore the fast-forward advance.`\n }\n\n // Push contentrain (with retry) and, when advanced, the base branch.\n let remotePush: RemotePush = push ? 'no-remote' : 'disabled'\n if (hasRemote && push) {\n remotePush = await pushContentBranches(projectRoot, worktreePath, wtGit, remoteName, baseBranch, baseAdvance === 'advanced')\n }\n\n const warnings = [divergedWarning, sync.warning].filter(Boolean)\n return {\n action: 'auto-merged' as const,\n commit: commitHash,\n sync,\n base_advance: baseAdvance,\n remote_push: remotePush,\n ...(warnings.length > 0 ? { warning: warnings.join(' ') } : {}),\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 only when doing so cannot lose work:\n // - pendingReview: must survive for a later contentrain_submit/merge\n // - commitHash === '': nothing was ever committed, safe to drop\n // - mergedIntoContentrain: the work is reachable from contentrain\n // Anything else means the branch holds a commit that is reachable from\n // nowhere else, and deleting it would silently discard the user's work\n // while the caller reports success. Keeping it may leave a stray cr/*\n // ref after a failure — that is recoverable; a deleted commit is not.\n const holdsUnreachableWork = commitHash !== '' && !mergedIntoContentrain\n if (!pendingReview && !holdsUnreachableWork) {\n await safeDeleteBranch(git, branch)\n }\n },\n }\n}\n\nexport async function mergeBranch(\n projectRoot: string,\n branchName: string,\n): Promise<MergeBranchResult> {\n const git = createGit(projectRoot)\n const config = await readConfig(projectRoot)\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n // Detect base branch\n const currentBranch = (await git.raw(['branch', '--show-current']).catch(() => '')).trim()\n const baseBranch = await resolveBaseBranch(git, config, { remoteName, currentBranch })\n const baseCheckedOut = currentBranch === baseBranch\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 // Fetch latest from remote before merging — an approve against stale\n // remote-tracking refs is how an already-reconciled divergence keeps\n // reporting itself. Hardened: no credential prompt, block-timeout.\n if (hasRemote) {\n const net = networkGit(projectRoot, NETWORK_TIMEOUT_MS)\n await Promise.all([\n net.fetch(remoteName, baseBranch).catch(() => {}),\n net.fetch(remoteName, CONTENTRAIN_BRANCH).catch(() => {}),\n ])\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 // Commit identity from `-c user.*` config (see authorConfig) — guard-safe,\n // no `.env()` spread.\n const wtGit = createGit(worktreePath, { config: authorConfig() })\n\n try {\n // Sync contentrain with the base branch (and remotes) BEFORE landing the\n // feature branch — the counterpart of the pre-fork sync in\n // createTransaction. Without it, one base commit that is not in\n // contentrain makes every merge report a diverged base forever. A\n // conflicting sync is aborted silently; the fast-forward check below then\n // reports the divergence truthfully instead of this call guessing at it.\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 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 // 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 // Fast-forward check: baseBranch must be an ancestor of contentrainTip.\n // (See complete() — merge-base --is-ancestor is unusable via simple-git.)\n // Divergence is NOT an error: the feature branch is already merged into\n // contentrain; only the base advance is pending until the branches are\n // reconciled. Throwing here used to strand the cr/* branch as a phantom\n // \"pending review\" while its content was already on contentrain.\n let sync: SyncResult = { synced: [], skipped: [] }\n let baseAdvance: BaseAdvance\n let divergedWarning: string | undefined\n if (await isAncestor(git, previousBaseRef, contentrainTip)) {\n baseAdvance = 'advanced'\n\n sync = await advanceBase(git, {\n projectRoot, worktreePath, baseBranch, currentBranch, baseCheckedOut,\n contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate,\n })\n } else {\n baseAdvance = 'blocked_diverged'\n divergedWarning = `Branch \"${branchName}\" is merged into \"${CONTENTRAIN_BRANCH}\", but \"${baseBranch}\" has commits that are not in \"${CONTENTRAIN_BRANCH}\" — the branches have diverged, so \"${baseBranch}\" was not advanced. Run contentrain_reconcile (CLI: npx contentrain reconcile) to merge them and restore the fast-forward advance.`\n }\n\n // Push contentrain (with retry) and, when advanced, the base branch.\n const push = pushPolicy(config).push\n let remotePush: RemotePush = push ? 'no-remote' : 'disabled'\n if (hasRemote && push) {\n remotePush = await pushContentBranches(projectRoot, worktreePath, wtGit, remoteName, baseBranch, baseAdvance === 'advanced')\n }\n\n // Prune the now-merged feature branch so merged cr/* refs don't accumulate.\n await safeDeleteBranch(git, branchName)\n\n // Delete the remote copy too (review-mode branches were pushed on save).\n // Best-effort and config-gated inside the helper: a failure surfaces as\n // `remote.warning`, never as a failed merge.\n let remote: RemoteDeleteResult | undefined\n if (hasRemote) {\n remote = await deleteRemoteBranch(projectRoot, branchName, { config })\n }\n\n const warnings = [divergedWarning, sync.warning].filter(Boolean)\n return {\n action: 'merged' as const,\n commit: contentrainTip,\n sync,\n base_advance: baseAdvance,\n remote_push: remotePush,\n ...(warnings.length > 0 ? { warning: warnings.join(' ') } : {}),\n ...(remote ? { remote } : {}),\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 * Fast-forward the base branch to the contentrain tip with `update-ref`, then\n * bring the developer's working tree along — but only when the base is the\n * branch they have checked out.\n *\n * When it is, `update-ref` moved their HEAD: the index is refreshed and the\n * changed files are synced (dirty files skipped). When it is not — a feature\n * branch is checked out — their HEAD did not move, so there is nothing to\n * refresh: `read-tree HEAD` would only drop what they have staged, and a\n * \"sync\" from HEAD would restore or delete files against the wrong branch.\n * Their tree is left exactly as it is, and the result says where the content\n * went.\n */\nasync function advanceBase(git: SimpleGit, input: {\n projectRoot: string\n worktreePath: string\n baseBranch: string\n currentBranch: string\n baseCheckedOut: boolean\n contentrainTip: string\n previousBaseRef: string\n dirtyFilesBeforeUpdate: Set<string>\n}): Promise<SyncResult> {\n await git.raw(['update-ref', `refs/heads/${input.baseBranch}`, input.contentrainTip])\n\n if (!input.baseCheckedOut) {\n const where = input.currentBranch ? `\"${input.currentBranch}\"` : 'a detached HEAD'\n return {\n synced: [],\n skipped: [],\n warning: `Content landed on \"${CONTENTRAIN_BRANCH}\" and \"${input.baseBranch}\". Your working tree is on ${where}, which was not touched — merge or rebase \"${input.baseBranch}\" to see the change there.`,\n }\n }\n\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 the changed files to the developer's working tree.\n // Only after an advance — when the base did not move, the developer's HEAD\n // did not change, and syncing files from the contentrain tip would desync\n // their working tree from their own HEAD.\n return selectiveSync(input.projectRoot, input.worktreePath, input.contentrainTip, input.previousBaseRef, input.dirtyFilesBeforeUpdate)\n}\n\n/**\n * True when `ancestor` is an ancestor of (or equal to) `descendant`.\n * Implemented with `rev-list --count` because `merge-base --is-ancestor`\n * signals via exit code with empty stderr — simple-git reports that as\n * success, so it cannot express a negative verdict.\n */\nexport async function isAncestor(git: SimpleGit, ancestor: string, descendant: string): Promise<boolean> {\n try {\n const count = Number((await git.raw(['rev-list', '--count', ancestor, `^${descendant}`])).trim())\n return count === 0\n } catch {\n return false\n }\n}\n\n/**\n * Push the contentrain branch (with one fetch-merge-retry) and, when the base\n * advanced, the base branch. Returns the push outcome for contentrain; the\n * base push is best-effort and never demotes the outcome, because the\n * contentrain branch is the content SSOT — it is what the CDN reads.\n *\n * The retry absorbs a concurrent remote advance (another writer pushed\n * contentrain first): fetch, merge the remote tip into the local worktree's\n * contentrain, push again. A conflicting retry-merge is aborted and reported\n * as `rejected` — that is a real divergence, not a race.\n */\nexport async function pushContentBranches(\n projectRoot: string,\n worktreePath: string,\n wtGit: SimpleGit,\n remoteName: string,\n baseBranch: string,\n baseAdvanced: boolean,\n): Promise<RemotePush> {\n const net = networkGit(projectRoot, NETWORK_TIMEOUT_MS)\n let outcome: RemotePush = 'pushed'\n try {\n await net.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n try {\n await networkGit(worktreePath, NETWORK_TIMEOUT_MS).fetch(remoteName, CONTENTRAIN_BRANCH)\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n await net.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n outcome = 'rejected'\n }\n }\n if (baseAdvanced) {\n try {\n await net.push(remoteName, baseBranch)\n } catch {\n // Best-effort: a failed base push alone does not demote the outcome.\n }\n }\n return outcome\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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"validator-S19GHR4V.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\n/** Bounds `items`-inside-`items` nesting; far above any real schema. */\nconst MAX_FIELD_DEPTH = 10\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 depth = 0,\n): ValidationError[] {\n const errors: ValidationError[] = []\n const errCtx = { model: modelId, locale, entry: entryId, field: fieldId }\n\n if (depth > MAX_FIELD_DEPTH) {\n return [{ severity: 'error', ...errCtx, message: `${fieldId} exceeds the maximum nesting depth of ${MAX_FIELD_DEPTH}` }]\n }\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 === '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 // Array items run through `validateField` itself rather than a parallel type\n // switch. The old switch knew 10 of the 27 field types and checked only\n // `typeof`, so `min`/`max`/`pattern`/`options` never reached an item and an\n // `items` given as a FieldDef with a non-object type matched no branch at all —\n // silently unvalidated, while the type emitter rendered it as a real constraint.\n // Recursing keeps one rule set for a type wherever it appears.\n if (def.type === 'array' && Array.isArray(value) && def.items) {\n const itemDef: FieldDef = typeof def.items === 'string'\n ? { type: def.items as FieldDef['type'] }\n : def.items\n for (let i = 0; i < value.length; i++) {\n // `ctx` is deliberately dropped: `unique` compares an entry against its\n // siblings, which has no meaning for positions inside one array.\n errors.push(...validateField(\n value[i], itemDef, modelId, locale, entryId, `${fieldId}[${i}]`, undefined, depth + 1,\n ))\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, ctx)\n // Qualify the nested path with the parent so an error names where it lives —\n // `meta.title`, or `blocks[0].title` when this object is an array item.\n for (const e of nested.errors) {\n errors.push({ ...e, field: e.field ? `${fieldId}.${e.field}` : fieldId })\n }\n }\n\n return errors\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, ModelLocaleScope } from '@contentrain/types'\nimport { describeModelLocaleScope, detectSecrets, MODEL_FIELD_ORDER, resolveModelLocales, validateModelLocales } 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 { inferTitleField, legacyFieldNames, listModels, readModel, titleFieldIssues, titleFieldOptions } from '../model-manager.js'\nimport { mergeEntryMeta, writeMeta } from '../meta-manager.js'\nimport { parseFrontmatter, resolveLocaleStrategy, serializeFrontmatter } 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// ─── Locale coverage ───\n\n/**\n * The clause every locale-coverage message ends with: which list the check was\n * evaluated against.\n *\n * Without it \"missing tr\" is unreadable. A model that legitimately covers two\n * of four project locales and one that has genuinely lost a translation produce\n * the same sentence, and the reader cannot tell which they are looking at\n * without opening the model definition and config.json side by side. Naming the\n * list turns the message into its own explanation — and, when the list is the\n * project-wide one, into the hint that `locales` is what narrows it.\n */\nfunction againstScope(scope: ModelLocaleScope): string {\n return ` (checked against ${describeModelLocaleScope(scope)})`\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 // An i18n model that declares `locales` covers that subset and no more — a\n // model that was never translated into a locale says so once, instead of\n // failing on that locale's missing file.\n const scope = resolveModelLocales(model, config)\n const locales = scope.locales\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${againstScope(scope)}`,\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). An entry one locale has and another lacks is a\n // translation that was never written — on a WPML or Polylang import the normal\n // state of a site, not a broken store — so it is a warning, as a document's\n // missing translation is. A locale with no file at all stays an error above.\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: 'warning',\n model: model.id,\n locale: locB,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locA} but missing in ${locB}${againstScope(scope)}`,\n })\n }\n }\n for (const id of idsB) {\n if (!idsA.has(id)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locA,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locB} but missing in ${locA}${againstScope(scope)}`,\n })\n }\n }\n }\n }\n\n // Consolidate a non-i18n model's meta layout BEFORE the orphan checks below.\n // The reverse orphan-content check fabricates a draft default-locale meta for\n // any content entry lacking meta at the default locale. If a stray holds the\n // real (e.g. published) record under a non-default locale, running that check\n // first would mint a draft default and let this pass then delete the real\n // record. Consolidating first preserves the authoritative record.\n const strayResult = await checkStrayNonI18nMeta(reader, projectRoot, model, config, issues, fix)\n fixed += strayResult.fixed\n\n // Orphan meta check\n for (const locale of locales) {\n const metaRelPath = metaFilePath(model, locale, config.locales.default)\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 // Skip fabrication while a non-i18n model's meta is stuck in an\n // unresolved stray: the content already has meta (misplaced), so minting\n // a draft default here would let the stray-consolidation pass then delete\n // the real record on a subsequent run. The mismatch warning already asks\n // the agent to resolve the layout by hand.\n if (fix && projectRoot && !strayResult.unresolved) {\n await writeMeta(projectRoot, model, { locale, entryId, defaultLocale: config.locales.default }, {\n // `source: 'import'` on purpose: this record is being fabricated\n // for content that arrived without one, not written by an agent.\n ...mergeEntryMeta(undefined),\n source: 'import',\n })\n fixed++\n }\n }\n }\n\n // Publish-state drift. Drafts sitting alongside published entries in the\n // same collection is the fingerprint of writes that reset status — the\n // shape that silently emptied collections on the CDN. Advisory only: a\n // genuine work-in-progress entry is indistinguishable, so this is a call\n // for the agent to make, not an error. Never auto-fixed — publishing is a\n // content decision, and MCP does not make those.\n if (metaData) {\n const entries = Object.entries(metaData)\n const draftIds = entries.filter(([, m]) => m.status === 'draft').map(([id]) => id)\n const publishedCount = entries.filter(([, m]) => m.status === 'published').length\n if (publishedCount > 0 && draftIds.length > 0) {\n issues.push({\n severity: 'notice',\n model: model.id,\n locale,\n message:\n `Publish-state drift: ${draftIds.length} draft entr${draftIds.length === 1 ? 'y' : 'ies'} `\n + `alongside ${publishedCount} published in the same collection — [${draftIds.join(', ')}]. `\n + 'If these were published before, restore them with contentrain_bulk update_status.',\n })\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\n/**\n * Flag — and, with `fix`, backfill — a model definition's `title_field`.\n *\n * This is the migration path for models authored before the property existed.\n * Reads are an unvalidated cast, so such a model loads happily with `title_field`\n * undefined in a slot the type system calls a string; nothing would ever say so.\n *\n * Only the *missing* case is repaired. A `title_field` that names an absent field\n * or an undisplayable type is an authoring mistake, not a legacy gap — filling in\n * a different answer there would be overruling a decision someone made, which is\n * the line `project.ts` draws everywhere else.\n *\n * Every repair emits a notice naming the field and the rule that chose it. The\n * doctrine that governs the rest of this file is that MCP does not make content\n * decisions; a schema backfill is not a content decision, but the principle\n * underneath — never exercise judgement invisibly — still binds. The notice and\n * the diff on `cr/fix/validate` are the two ways a human can overrule it.\n */\nasync function checkModelDefinition(\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<number> {\n // `locales` is reported, never repaired — not even under `fix`. A model that\n // does not declare one covers every project locale, and choosing a narrower\n // set for it would be deciding which translations are *supposed* to exist,\n // which is a content decision. The one MCP could compute — \"the locales that\n // happen to have files today\" — would write the current gaps into the schema\n // and silence the very errors that reveal them.\n for (const issue of validateModelLocales(model, config)) {\n issues.push({ ...issue, model: model.id })\n }\n\n // Field names that predate the snake_case rule. `contentrain_model_save`\n // keeps them (#116) — this is how an agent learns a model carries one before\n // it edits the model, not mid-write. A notice, not a warning: renaming means\n // renaming content keys in every locale and every consumer, so it is a\n // migration to plan, not a defect to clear.\n for (const path of legacyFieldNames(model.fields)) {\n issues.push({\n severity: 'notice',\n model: model.id,\n field: path,\n message: `Field \"${path}\" predates the snake_case rule. contentrain_model_save keeps it as-is; new fields must be snake_case. Renaming it also means renaming its content keys in every locale.`,\n })\n }\n\n const { errors, warnings } = titleFieldIssues(model)\n if (errors.length === 0 && warnings.length === 0) return 0\n\n const repairable = !model.title_field\n if (fix && projectRoot && repairable) {\n const pick = inferTitleField(model.kind, model.fields)\n if (pick) {\n model.title_field = pick.field\n // writeJson, not writeModel: writeModel also ensures the content and meta\n // directories, which for a model with `content_path` means creating\n // directories in the user's source tree. A validator does not do that.\n await writeJson(join(projectRoot, `.contentrain/models/${model.id}.json`), model, MODEL_FIELD_ORDER)\n issues.push({\n severity: 'notice',\n model: model.id,\n field: 'title_field',\n message: `title_field set to \"${pick.field}\" (rule: ${pick.rule}). If that is the wrong field, correct it with contentrain_model_save.`,\n })\n return 1\n }\n // Inference declined. Two different situations, said differently: a model\n // whose only displayable fields are slug/url/code has legal choices that a\n // human must make (#120); a model with none needs a field added.\n const legal = titleFieldOptions(model.kind, model.fields)\n issues.push({\n severity: 'error',\n model: model.id,\n field: 'title_field',\n message: legal.length > 0\n ? `Missing \"title_field\" and no field reads as a title — only ${legal.map(f => `\"${f}\"`).join(', ')} could legally hold one, and a slug, URL or code field, or a field inside an object, is not inferred. Set title_field explicitly with contentrain_model_save, or add a string field.`\n : `Missing \"title_field\" and no field can serve as one — add a string or text field, then re-run. A title cannot be an image, relation or number.`,\n })\n return 0\n }\n\n for (const message of errors) {\n issues.push({ severity: 'error', model: model.id, field: 'title_field', message })\n }\n for (const message of warnings) {\n issues.push({ severity: 'warning', model: model.id, field: 'title_field', message })\n }\n return 0\n}\n\n/**\n * Flag — and, with `fix`, remediate — meta files a non-i18n model should not have.\n *\n * Such a model keeps all content in one `data.json` and therefore exactly one\n * meta record, at the default locale. Earlier writes derived the meta path from\n * the caller's locale, so saving under a non-default locale left a second meta\n * file, and readers disagreed about which was authoritative.\n *\n * The `fix` remediation is deterministic and never decides a status:\n * - default-locale meta present → the strays are redundant, so delete them\n * (the default-locale record stays authoritative — no status is merged).\n * - default-locale meta absent, exactly one stray → that stray holds the only\n * record, so migrate it to the default path (move) rather than orphan the\n * content.\n * - default-locale meta absent, several strays → which is authoritative is\n * ambiguous, so leave the warning for the agent to resolve by hand.\n *\n * Returns the count of files remediated and whether strays remain unresolved.\n * `unresolved` gates the caller's orphan-content fabrication: while a non-i18n\n * model's meta still lives in a stray, the content is not truly orphaned, so\n * minting a draft default-locale record would both be wrong and set up a trap\n * (a later fix pass would then treat the real stray as redundant and delete it).\n */\nasync function checkStrayNonI18nMeta(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ fixed: number, unresolved: boolean }> {\n if (model.i18n) return { fixed: 0, unresolved: false }\n\n const metaDir = `.contentrain/meta/${model.id}`\n const expected = `${config.locales.default}.json`\n let files: string[]\n try {\n files = await reader.listDirectory(metaDir)\n } catch {\n return { fixed: 0, unresolved: false }\n }\n\n const strays = files.filter(f => f.endsWith('.json') && f !== expected)\n if (strays.length === 0) return { fixed: 0, unresolved: false }\n\n issues.push({\n severity: 'warning',\n model: model.id,\n message:\n `Meta layout mismatch: \"${model.id}\" has i18n disabled, so its content lives in a single data.json `\n + `and its meta belongs at ${expected} alone — but [${strays.join(', ')}] also exist. `\n + 'Readers may disagree about which file is authoritative. Run contentrain_validate fix:true to prune '\n + `the extras (the default-locale meta stays authoritative).`,\n })\n\n if (!fix || !projectRoot) return { fixed: 0, unresolved: true }\n\n const defaultMeta = await readJsonViaReader<Record<string, unknown>>(reader, `${metaDir}/${expected}`)\n\n if (defaultMeta === null) {\n // No authoritative default. A single stray holds the only record — keep it\n // by moving it to the default path. Several strays with no default is\n // ambiguous, so do not guess: leave the warning standing (unresolved).\n if (strays.length !== 1) return { fixed: 0, unresolved: true }\n const stray = strays[0]!\n const content = await readJsonViaReader<Record<string, unknown>>(reader, `${metaDir}/${stray}`)\n if (content === null) return { fixed: 0, unresolved: true }\n await writeJson(join(projectRoot, metaDir, expected), content)\n await rm(join(projectRoot, metaDir, stray), { force: true })\n return { fixed: 1, unresolved: false }\n }\n\n // Default-locale meta is authoritative → the strays are redundant. Each is a\n // distinct file, so removing them in parallel is safe.\n await Promise.all(strays.map(stray => rm(join(projectRoot, metaDir, stray), { force: true })))\n return { fixed: strays.length, unresolved: false }\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 const scope = resolveModelLocales(model, config)\n\n for (const locale of scope.locales) {\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${againstScope(scope)}`,\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, config.locales.default))\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 const scope = resolveModelLocales(model, config)\n\n for (const locale of scope.locales) {\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${againstScope(scope)}`,\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}${againstScope(scope)}`,\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}${againstScope(scope)}`,\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\n/** Meta-owned scheduling keys; see `EntrySchedule` in meta-manager. */\nconst SCHEDULE_KEYS = ['publish_at', 'expire_at'] as const\n\n/**\n * Scheduling keys a document's frontmatter carries that belong to its meta.\n *\n * Before #125, `contentrain_content_save` merged `publish_at`/`expire_at` into\n * the entry's data before planning the write, so the same value landed in the\n * frontmatter and in meta. A key counts as leaked only when three things hold:\n * it is in the frontmatter, the model does not declare a field by that name,\n * and the meta record holds the key too. The third condition is what separates\n * the tool's leftover from an author's own frontmatter field.\n */\nasync function leakedScheduleKeys(\n reader: RepoReader,\n model: ModelDefinition,\n locale: string,\n slug: string,\n frontmatter: Record<string, unknown>,\n defaultLocale: string,\n): Promise<string[]> {\n const present = SCHEDULE_KEYS.filter(k => k in frontmatter && !(model.fields && k in model.fields))\n if (present.length === 0) return []\n const meta = await readJsonViaReader<EntryMeta>(reader, metaFilePath(model, locale, defaultLocale, slug))\n if (!meta) return []\n return present.filter(k => meta[k] !== undefined)\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 // A declared `locales` narrows the i18n case — see validateCollectionModel.\n const scope = resolveModelLocales(model, config)\n const locales = scope.locales\n\n // Read every document once up front and keep its frontmatter per locale.\n // `unique` compares an entry against its siblings, so it needs the whole set —\n // validating one file at a time is why `unique` was silently a no-op for\n // documents, which is exactly where every shipped template declares it.\n // Keyed by `${slug}\\u0000${locale}`. The NUL separator can appear in neither\n // a slug nor a locale, so composite keys never collide. Written as the escape\n // `\\u0000` (not a raw NUL byte) so the source stays plain text — a literal NUL\n // makes tools classify this file as binary and breaks grep/diff.\n const rawByKey = new Map<string, string>()\n const frontmatterByLocale: Record<string, Record<string, Record<string, unknown>>> = {}\n for (const slug of slugs) {\n if (slug.startsWith('.')) continue\n for (const locale of locales) {\n const raw = await readTextViaReader(reader, documentFilePath(model, locale, slug))\n if (!raw) continue\n rawByKey.set(`${slug}\\u0000${locale}`, raw)\n const { frontmatter } = parseFrontmatter(raw)\n frontmatterByLocale[locale] ??= {}\n frontmatterByLocale[locale][slug] = frontmatter\n }\n }\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 = rawByKey.get(`${slug}\\u0000${locale}`) ?? null\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${againstScope(scope)}`,\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, preserved } = parseFrontmatter(raw)\n for (const block of preserved?.blocks ?? []) {\n if (block.key === undefined) continue\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n slug,\n field: block.key,\n message: `Frontmatter key \"${block.key}\" is in a shape Contentrain does not read (a nested map, a block value, or a name with a space or a non-ASCII letter). It is kept exactly as written when the document is saved, but it is not validated.`,\n })\n }\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 // Scheduling keys an older content_save copied into the frontmatter as\n // well as into meta (#125). They are meta's, not the author's — and since\n // documents merge frontmatter on save, no later save could remove them.\n // Recognised only when meta holds the same key: an undeclared `publish_at`\n // with no meta counterpart is the author's own field and is left alone.\n const leaked = await leakedScheduleKeys(reader, model, locale, slug, frontmatter, config.locales.default)\n if (leaked.length > 0) {\n const keys = leaked.map(k => `\"${k}\"`).join(', ')\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n slug,\n field: leaked[0],\n message: `Frontmatter carries scheduling key(s) ${keys} that belong in meta — copied there by an older content_save. The meta record already holds the value; run validate with fix:true to strip the frontmatter copy.`,\n })\n if (fix && projectRoot) {\n const cleaned = Object.fromEntries(Object.entries(frontmatter).filter(([k]) => !leaked.includes(k)))\n await writeText(join(projectRoot, filePath), serializeFrontmatter(cleaned, body, preserved))\n fixed++\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 undefined,\n // Siblings in the same locale, so `unique` has something to compare\n // against. Keyed by slug — a document's identity is its slug.\n { allEntries: frontmatterByLocale[locale] ?? {}, currentEntryId: slug },\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 // Ahead of the content checks: a broken model definition explains the content\n // errors below it, and the repair mutates `model` in place so the content\n // validators downstream see the fixed object.\n totalFixed += await checkModelDefinition(projectRoot, model, config, issues, fix)\n\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 resolveModelLocales(model, config).locales) {\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":"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"}
|