@contentrain/mcp 3.9.32 → 3.10.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 +18 -1
- package/dist/{apply-manager-BenQNb_b.mjs → apply-manager-B-O3eAbz.mjs} +16 -8
- package/dist/apply-manager-B-O3eAbz.mjs.map +1 -0
- package/dist/{branch-lifecycle-CirYfTXu.d.mts → branch-lifecycle-4iZax7a7.d.mts} +3 -3
- package/dist/{branch-lifecycle-CirYfTXu.d.mts.map → branch-lifecycle-4iZax7a7.d.mts.map} +1 -1
- package/dist/{branch-lifecycle-CcsSyfSw.mjs → branch-lifecycle-D4DxPgQg.mjs} +53 -5
- package/dist/branch-lifecycle-D4DxPgQg.mjs.map +1 -0
- package/dist/{config-BLwZtwlI.mjs → config-BnNYu128.mjs} +3 -2
- package/dist/config-BnNYu128.mjs.map +1 -0
- package/dist/content-manager-2g0pkLFh.d.mts.map +1 -1
- package/dist/{context-Cp8b8Nxm.mjs → context-D9HXnnGj.mjs} +5 -5
- package/dist/{context-Cp8b8Nxm.mjs.map → context-D9HXnnGj.mjs.map} +1 -1
- package/dist/core/apply-manager.d.mts.map +1 -1
- package/dist/core/apply-manager.mjs +6 -6
- package/dist/core/config.d.mts.map +1 -1
- package/dist/core/config.mjs +1 -1
- package/dist/core/content-manager.mjs +1 -1
- package/dist/core/context.mjs +3 -3
- package/dist/core/doctor.mjs +4 -4
- package/dist/core/model-manager.mjs +1 -1
- package/dist/core/ops/index.mjs +6 -6
- package/dist/core/validator/index.d.mts.map +1 -1
- package/dist/core/validator/index.mjs +3 -3
- package/dist/{doctor-Qabcb-Z1.mjs → doctor-Br1NqEl2.mjs} +4 -4
- package/dist/{doctor-Qabcb-Z1.mjs.map → doctor-Br1NqEl2.mjs.map} +1 -1
- package/dist/git/branch-lifecycle.d.mts +1 -1
- package/dist/git/branch-lifecycle.mjs +2 -2
- package/dist/git/reconcile.d.mts.map +1 -1
- package/dist/git/reconcile.mjs +8 -8
- package/dist/git/transaction.d.mts +7 -3
- package/dist/git/transaction.d.mts.map +1 -1
- package/dist/git/transaction.mjs +6 -6
- package/dist/index.mjs +13 -13
- package/dist/{local-BQVdC9MM.mjs → local-CBtRYFH4.mjs} +7 -7
- package/dist/{local-BQVdC9MM.mjs.map → local-CBtRYFH4.mjs.map} +1 -1
- package/dist/{model-manager-BYF2z7qC.mjs → model-manager-D0VztE8M.mjs} +8 -5
- package/dist/model-manager-D0VztE8M.mjs.map +1 -0
- package/dist/{ops-CQ_IIedh.mjs → ops-CVI308jA.mjs} +9 -4
- package/dist/ops-CVI308jA.mjs.map +1 -0
- package/dist/providers/local/index.mjs +9 -9
- package/dist/{reconcile-CRAukgae.mjs → reconcile-DSflMfhj.mjs} +8 -7
- package/dist/reconcile-DSflMfhj.mjs.map +1 -0
- package/dist/{reconcile-C7AaUfCl.mjs → reconcile-J-iorvSa.mjs} +5 -4
- package/dist/reconcile-J-iorvSa.mjs.map +1 -0
- package/dist/server/http/index.mjs +13 -13
- package/dist/{server-D8oYrulE.mjs → server-CJbmjX48.mjs} +43 -14
- package/dist/server-CJbmjX48.mjs.map +1 -0
- package/dist/server.mjs +13 -13
- package/dist/{transaction-CcjGiVYg.mjs → transaction-DnrUATJF.mjs} +14 -10
- package/dist/transaction-DnrUATJF.mjs.map +1 -0
- package/dist/{validator-Bz3jnSnb.mjs → validator-S19GHR4V.mjs} +16 -5
- package/dist/{validator-Bz3jnSnb.mjs.map → validator-S19GHR4V.mjs.map} +1 -1
- package/package.json +2 -2
- package/dist/apply-manager-BenQNb_b.mjs.map +0 -1
- package/dist/branch-lifecycle-CcsSyfSw.mjs.map +0 -1
- package/dist/config-BLwZtwlI.mjs.map +0 -1
- package/dist/model-manager-BYF2z7qC.mjs.map +0 -1
- package/dist/ops-CQ_IIedh.mjs.map +0 -1
- package/dist/reconcile-C7AaUfCl.mjs.map +0 -1
- package/dist/reconcile-CRAukgae.mjs.map +0 -1
- package/dist/server-D8oYrulE.mjs.map +0 -1
- package/dist/transaction-CcjGiVYg.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-BQVdC9MM.mjs","names":["mergeBranchOp","canonicalStringify","sortKeys","listBranchesOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp","getDefaultBranchOp"],"sources":["../src/providers/local/branch-ops.ts","../src/git/ref-snapshot.ts","../src/providers/local/content-view.ts","../src/providers/local/rebase-changes.ts","../src/providers/local/provider.ts"],"sourcesContent":["import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport { createGit } from '../../git/identity.js'\nimport type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport { readConfig } from '../../core/config.js'\nimport { resolveBaseBranch } from '../../git/base-branch.js'\nimport { classifyMergedBranches, deleteRemoteBranch } from '../../git/branch-lifecycle.js'\nimport { mergeBranch as mergeBranchOp } from '../../git/transaction.js'\n\n/**\n * Branch/merge/diff helpers backed by the local simple-git worktree flow.\n *\n * Pure functions composed into `LocalProvider` — mirroring the shape of\n * `providers/github/branch-ops.ts` so the two providers share the same\n * surface at the `RepoProvider` boundary.\n */\n\nexport async function getDefaultBranch(projectRoot: string): Promise<string> {\n return resolveBaseBranch(createGit(projectRoot), await readConfig(projectRoot))\n}\n\nexport async function listBranches(\n projectRoot: string,\n prefix?: string,\n): Promise<Branch[]> {\n const git = createGit(projectRoot)\n const summary = await git.branchLocal()\n const names = prefix\n ? summary.all.filter(n => n.startsWith(prefix))\n : summary.all\n const branches: Branch[] = []\n for (const name of names) {\n const info = summary.branches[name]\n branches.push({ name, sha: info?.commit ?? '' })\n }\n return branches\n}\n\nexport async function createBranch(\n projectRoot: string,\n name: string,\n fromRef: string,\n): Promise<void> {\n const git = createGit(projectRoot)\n await git.raw(['branch', name, fromRef])\n}\n\nexport async function deleteBranch(\n projectRoot: string,\n name: string,\n): Promise<void> {\n const git = createGit(projectRoot)\n await git.deleteLocalBranch(name, true)\n // Parity with the remote-API providers, whose deleteBranch removes the\n // remote ref: best-effort, config-gated inside the helper, never throws.\n await deleteRemoteBranch(projectRoot, name)\n}\n\nexport async function getBranchDiff(\n projectRoot: string,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const git = createGit(projectRoot)\n const raw = await git.raw(['diff', '--name-status', `${base}...${branch}`])\n const diffs: FileDiff[] = []\n for (const line of raw.split('\\n')) {\n if (!line.trim()) continue\n const [code, ...pathParts] = line.split('\\t')\n const path = pathParts[pathParts.length - 1]\n if (!code || !path) continue\n const status: FileDiff['status'] = code.startsWith('A')\n ? 'added'\n : code.startsWith('D')\n ? 'removed'\n : 'modified'\n diffs.push({ path, status, before: null, after: null })\n }\n return diffs\n}\n\nexport async function mergeBranch(\n projectRoot: string,\n branch: string,\n into: string,\n): Promise<MergeResult> {\n if (into !== CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `LocalProvider.mergeBranch only supports merging into \"${CONTENTRAIN_BRANCH}\" (got \"${into}\"). `\n + `The local flow merges feature branches into the content-tracking branch and fast-forwards the base branch via update-ref.`,\n ), {\n code: 'UNSUPPORTED_MERGE_TARGET',\n agent_hint: `Pass \"${CONTENTRAIN_BRANCH}\" as the merge target, or use a non-local provider that supports arbitrary targets.`,\n developer_action: `Merge \"${branch}\" into \"${CONTENTRAIN_BRANCH}\" instead.`,\n })\n }\n const result = await mergeBranchOp(projectRoot, branch)\n return {\n merged: true,\n sha: result.commit,\n pullRequestUrl: null,\n sync: result.sync,\n ...(result.remote ? { remote: result.remote } : {}),\n }\n}\n\nexport async function isMerged(\n projectRoot: string,\n branch: string,\n into: string,\n): Promise<boolean> {\n try {\n const merged = await classifyMergedBranches(projectRoot, [branch], into)\n return merged.has(branch)\n } catch {\n return false\n }\n}\n","import { spawn } from 'node:child_process'\nimport type { RepoReader } from '../core/contracts/index.js'\nimport { gitBinary } from './identity.js'\n\n/**\n * RefSnapshotReader — a `RepoReader` over one subtree of one commit, read\n * with two git spawns in total however many files the caller touches.\n *\n * `GitRefReader` spawns `git show` per file, which is right for a planner\n * reading a handful of paths and wrong for a tool that walks every model and\n * every locale file (`content_list`, `status`, validation): a few hundred\n * spawns per call (#229). Here one `ls-tree -r` lists the subtree — which\n * answers `listDirectory` and `fileExists` from memory — and the first\n * `readFile` pulls every content blob under it through a single\n * `cat-file --batch`. Only content is batched ({@link batched}): media under\n * `assets_path`, and any large or non-text file, is read on demand, one\n * `cat-file blob` each, so a list or a save never loads the project's images.\n *\n * Bound to a commit sha, not a branch name, so a snapshot never changes\n * under its reader; {@link loadRefSnapshot} caches one per (repo, sha).\n *\n * Paths are repo-relative with `/` separators; the caller maps its own paths\n * (see LocalProvider). Only paths under `root` are in the snapshot — anything\n * else reads as missing.\n */\nexport class RefSnapshotReader implements RepoReader {\n private blobs: Promise<Map<string, string>> | null = null\n private readonly single = new Map<string, Promise<string>>()\n\n constructor(\n private readonly repoDir: string,\n public readonly commit: string,\n public readonly root: string,\n /** repo-relative file path → blob oid and size in bytes */\n private readonly files: ReadonlyMap<string, SnapshotFile>,\n /** repo-relative directory path → child names */\n private readonly dirs: ReadonlyMap<string, readonly string[]>,\n ) {}\n\n async readFile(path: string, _ref?: string): Promise<string> {\n const p = trim(path)\n const file = this.files.get(p)\n if (!file) throw Object.assign(new Error(`ENOENT: ${path} is not in ${this.commit.slice(0, 8)}:${this.root}`), { code: 'ENOENT' })\n if (!batched(p, file.size)) {\n let text = this.single.get(file.oid)\n if (!text) {\n text = runGit(this.repoDir, ['cat-file', 'blob', file.oid]).then(buf => buf.toString('utf-8'))\n text.catch(() => this.single.delete(file.oid))\n this.single.set(file.oid, text)\n }\n return text\n }\n this.blobs ??= readBlobs(this.repoDir, [...new Set([...this.files].filter(([f, { size }]) => batched(f, size)).map(([, { oid }]) => oid))])\n const text = (await this.blobs).get(file.oid)\n if (text === undefined) throw new Error(`blob ${file.oid} for ${path} could not be read`)\n return text\n }\n\n async listDirectory(path: string, _ref?: string): Promise<string[]> {\n return [...(this.dirs.get(trim(path)) ?? [])]\n }\n\n async fileExists(path: string, _ref?: string): Promise<boolean> {\n const p = trim(path)\n return this.files.has(p) || this.dirs.has(p)\n }\n}\n\ninterface SnapshotFile { oid: string, size: number }\n\n/** Largest blob the batch loads; a content file is far smaller. */\nconst BATCH_MAX_BYTES = 1024 * 1024\n\n/**\n * Whether the first `readFile` loads this file with the rest: a content file —\n * JSON (config, models, content, meta) or Markdown (documents) — no larger\n * than {@link BATCH_MAX_BYTES}. Media never matches by extension, wherever\n * `assets_path` points.\n */\nfunction batched(path: string, size: number): boolean {\n return size <= BATCH_MAX_BYTES && /\\.(?:json|md|mdx)$/i.test(path)\n}\n\nconst snapshots = new Map<string, Promise<RefSnapshotReader>>()\nconst MAX_SNAPSHOTS = 8\n\n/**\n * The snapshot of `root` at `commit`, listed once per (repo, commit) for the\n * life of the process: a commit's tree never changes, so a later tool call on\n * an unmoved branch costs no spawn at all.\n */\nexport function loadRefSnapshot(repoDir: string, commit: string, root: string): Promise<RefSnapshotReader> {\n const key = `${repoDir}\\0${commit}\\0${root}`\n let hit = snapshots.get(key)\n if (!hit) {\n hit = listTree(repoDir, commit, root)\n hit.catch(() => snapshots.delete(key))\n snapshots.set(key, hit)\n // Oldest-first eviction; a long-lived server sees a new tip per write.\n if (snapshots.size > MAX_SNAPSHOTS) snapshots.delete(snapshots.keys().next().value!)\n }\n return hit\n}\n\nasync function listTree(repoDir: string, commit: string, root: string): Promise<RefSnapshotReader> {\n const raw = await runGit(repoDir, ['ls-tree', '-r', '-l', '-z', '--full-tree', commit, '--', root])\n const files = new Map<string, SnapshotFile>()\n const children = new Map<string, Set<string>>()\n for (const record of raw.toString('utf-8').split('\\0')) {\n if (!record) continue\n // \"<mode> <type> <oid> <size, space-padded>\\t<path>\"\n const tab = record.indexOf('\\t')\n const [, type, oid, size] = record.slice(0, tab).split(/ +/)\n if (type !== 'blob' || !oid) continue\n const path = record.slice(tab + 1)\n files.set(path, { oid, size: Number(size) })\n // Register every ancestor directory down from the repo root.\n const parts = path.split('/')\n for (let i = 0; i < parts.length; i++) {\n const dir = parts.slice(0, i).join('/')\n let set = children.get(dir)\n if (!set) children.set(dir, set = new Set())\n set.add(parts[i]!)\n }\n }\n const dirs = new Map([...children].map(([dir, names]) => [dir, [...names].toSorted()] as const))\n return new RefSnapshotReader(repoDir, commit, root, files, dirs)\n}\n\n/** The given blobs in one `cat-file --batch` process. */\nasync function readBlobs(repoDir: string, oids: string[]): Promise<Map<string, string>> {\n const out = new Map<string, string>()\n if (oids.length === 0) return out\n const buf = await runGit(repoDir, ['cat-file', '--batch'], `${oids.join('\\n')}\\n`)\n // Each record: \"<oid> <type> <size>\\n<size bytes>\\n\"\n let at = 0\n while (at < buf.length) {\n const eol = buf.indexOf(0x0A, at)\n if (eol < 0) break\n const [oid, , size] = buf.subarray(at, eol).toString('utf-8').split(' ')\n const bytes = Number(size)\n if (!oid || !Number.isFinite(bytes)) break\n out.set(oid, buf.subarray(eol + 1, eol + 1 + bytes).toString('utf-8'))\n at = eol + 1 + bytes + 1\n }\n return out\n}\n\nfunction runGit(cwd: string, args: string[], stdin?: string): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const child = spawn(gitBinary(), args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] })\n const chunks: Buffer[] = []\n let stderr = ''\n child.stdout.on('data', (c: Buffer) => chunks.push(c))\n child.stderr.on('data', (c: Buffer) => { stderr += c.toString() })\n child.on('error', reject)\n // git may exit before reading all of stdin (a bad ref, a missing object): the write then fails\n // with EPIPE. The exit code below reports that failure; unhandled, it would crash the process.\n child.stdin.on('error', () => {})\n child.on('close', (code) => {\n if (code === 0) resolve(Buffer.concat(chunks))\n else reject(new Error(`git ${args[0]} exited ${code}: ${stderr.trim()}`))\n })\n child.stdin.end(stdin ?? '')\n })\n}\n\nfunction trim(path: string): string {\n let p = path.replaceAll('\\\\', '/')\n while (p.startsWith('./')) p = p.slice(2)\n while (p.endsWith('/')) p = p.slice(0, -1)\n return p === '.' ? '' : p\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\nimport { readFile, stat } from 'node:fs/promises'\nimport { join, resolve } from 'node:path'\nimport { CONTENTRAIN_BRANCH, type ContentReadSource } from '@contentrain/types'\nimport { readConfig } from '../../core/config.js'\nimport { resolveBaseBranch } from '../../git/base-branch.js'\nimport { createGit } from '../../git/identity.js'\nimport { loadRefSnapshot, type RefSnapshotReader } from '../../git/ref-snapshot.js'\n\n/**\n * Where a LocalProvider reads `.contentrain/` from (#229).\n *\n * Content writes land on `contentrain` and the base branch; a checked-out\n * feature branch is deliberately left alone (#227). Its working tree is then\n * behind every write, so reading `.contentrain/` from it plans the next write\n * from a stale value and shows read tools stale content. On such a branch the\n * content is read from the `contentrain` ref instead. Everything else — source\n * files, scan, normalize — still comes from the working tree.\n *\n * Working tree, unchanged from before, when:\n * - the base branch is checked out (writes advance it, so it is current once\n * pulled — #226's carry-over and `CONTENT_WORKING_TREE_STALE` still apply);\n * - HEAD is detached (a CI checkout validates the commit it checked out);\n * - HEAD is `contentrain` itself or a `cr/*` review branch (their tree IS\n * the content under review);\n * - there is no `contentrain` branch yet, or git cannot answer.\n */\nexport type ContentView =\n | { source: 'working_tree' }\n | { source: 'ref', ref: string, commit: string, branch: string, reader: RefSnapshotReader, prefix: string }\n\n/** The `.contentrain` paths that are never committed: always the working tree. */\nconst UNTRACKED = ['.contentrain/.cache', '.contentrain/client']\n\nconst scope = new AsyncLocalStorage<Map<string, Promise<ContentView>>>()\n\n/**\n * Run `fn` with one content view per project for its whole duration: every\n * read inside answers from the same snapshot, resolved once. The MCP server\n * wraps each tool call in this, so a call costs a fixed handful of spawns\n * however many files it reads.\n */\nexport function withReadScope<T>(fn: () => T): T {\n return scope.run(new Map(), fn)\n}\n\nexport function contentView(projectRoot: string): Promise<ContentView> {\n const memo = scope.getStore()\n if (!memo) return resolveView(projectRoot)\n let view = memo.get(projectRoot)\n if (!view) {\n view = resolveView(projectRoot)\n memo.set(projectRoot, view)\n }\n return view\n}\n\nexport function describeSource(view: ContentView): ContentReadSource {\n return view.source === 'working_tree'\n ? { source: 'working_tree' }\n : { source: 'ref', ref: view.ref, commit: view.commit, checked_out: view.branch }\n}\n\n/**\n * The repo-relative path a snapshot answers for, or null when `path` must come\n * from the working tree (outside `.contentrain/`, or never committed).\n */\nexport function refPath(view: ContentView & { source: 'ref' }, projectRoot: string, path: string): string | null {\n const abs = resolve(projectRoot, path)\n const root = resolve(projectRoot)\n if (abs !== root && !abs.startsWith(`${root}/`) && !abs.startsWith(`${root}\\\\`)) return null\n const rel = abs.slice(root.length + 1).replaceAll('\\\\', '/')\n if (rel !== '.contentrain' && !rel.startsWith('.contentrain/')) return null\n if (UNTRACKED.some(p => rel === p || rel.startsWith(`${p}/`))) return null\n return view.prefix + rel\n}\n\nasync function resolveView(projectRoot: string): Promise<ContentView> {\n try {\n const head = await headBranch(projectRoot)\n if (!head || head === CONTENTRAIN_BRANCH || head.startsWith('cr/')) return { source: 'working_tree' }\n const git = createGit(projectRoot)\n const base = await resolveBaseBranch(git, await readConfig(projectRoot), { currentBranch: head })\n if (head === base) return { source: 'working_tree' }\n const [prefix, commit] = (await git.raw(['rev-parse', '--show-prefix', `refs/heads/${CONTENTRAIN_BRANCH}^{commit}`]))\n .split('\\n').map(line => line.trim())\n if (!commit) return { source: 'working_tree' }\n const root = `${prefix ?? ''}.contentrain`\n const reader = await loadRefSnapshot(projectRoot, commit, root)\n return { source: 'ref', ref: CONTENTRAIN_BRANCH, commit, branch: head, reader, prefix: prefix ?? '' }\n } catch {\n return { source: 'working_tree' }\n }\n}\n\n/**\n * The checked-out branch, or null when HEAD is detached. Read from the HEAD\n * file when `.git` sits at the project root (no spawn — this runs once per\n * tool call on every local project); asked of git otherwise (a linked\n * worktree, or a project in a monorepo subdirectory).\n */\nasync function headBranch(projectRoot: string): Promise<string | null> {\n const dotGit = join(projectRoot, '.git')\n try {\n let gitDir = dotGit\n if ((await stat(dotGit)).isFile()) {\n const pointer = (await readFile(dotGit, 'utf-8')).match(/^gitdir:\\s*(.+)$/m)?.[1]?.trim()\n if (!pointer) throw new Error('unreadable .git file')\n gitDir = resolve(projectRoot, pointer)\n }\n const head = (await readFile(join(gitDir, 'HEAD'), 'utf-8')).trim()\n return head.startsWith('ref: refs/heads/') ? head.slice('ref: refs/heads/'.length) : null\n } catch {\n const out = await createGit(projectRoot).raw(['symbolic-ref', '-q', '--short', 'HEAD']).catch(() => '')\n return out.trim() || null\n }\n}\n","import { MODEL_FIELD_ORDER } from '@contentrain/types'\nimport type { FileChange } from '../../core/contracts/index.js'\nimport { canonicalStringify, sortKeys } from '../../core/serialization/index.js'\n\n/**\n * Carry a plan's full-file changes over to the tree they are committed on.\n *\n * The local provider plans against the developer's working tree, but commits\n * in a worktree on the `contentrain` branch after fetching and merging the\n * remote. Those two trees differ whenever another writer (Studio, a teammate,\n * CI) reached `contentrain` since the developer last pulled. A plan is a set\n * of whole files, so writing it as-is replaced every file it touched with a\n * copy built from the stale tree — silently deleting whatever the other\n * writer had added (#226).\n *\n * Each change is a three-way merge instead:\n * basis — the file as the planner read it (working tree, `null` if absent)\n * planned — the change's content (`null` for a delete)\n * current — the file in the worktree the commit is built on\n *\n * `current === basis` is the common case and writes the plan verbatim. When\n * they differ, JSON object files (collection / dictionary / singleton content,\n * meta, model definitions) are merged key by key: what the plan changed\n * relative to its basis is applied on top of `current`, recursively through\n * nested objects. Anything that cannot be merged that way — both sides\n * changed the same value, a markdown document, a delete of a file that\n * gained entries — is a conflict. Conflicts are never resolved by guessing:\n * the caller refuses the write.\n *\n * One exception, in meta files only: the write stamp (`updated_at`,\n * `updated_by`, `source`). Every write re-stamps the entries it touches\n * (`mergeEntryMeta`, `applyStatusChange`), so two writes to the same entry\n * always disagree there. The stamp describes the latest write, which is this\n * one — so when this write re-stamped an entry, its whole stamp wins, as a\n * unit (never this write's `updated_at` with the other writer's\n * `updated_by`). `status` and scheduling still merge or conflict normally.\n */\nexport type RebaseResult =\n | { ok: true, change: FileChange }\n | { ok: false, path: string, reason: string }\n\nexport function rebaseChange(change: FileChange, basis: string | null, current: string | null): RebaseResult {\n if (current === basis || current === change.content) return { ok: true, change }\n\n const reject = (reason: string): RebaseResult => ({ ok: false, path: change.path, reason })\n\n if (basis !== null && current === null) {\n return reject('the file was deleted on the contentrain branch since it was read')\n }\n if (!change.path.endsWith('.json')) {\n return reject('the file changed on the contentrain branch since it was read, and only JSON files can be merged')\n }\n\n const base = basis === null ? {} : parseObject(basis)\n const theirs = current === null ? {} : parseObject(current)\n const ours = change.content === null ? {} : parseObject(change.content)\n if (!base || !theirs || !ours) {\n return reject('the file changed on the contentrain branch since it was read, and is not a JSON object')\n }\n\n const stampWins = change.path.startsWith('.contentrain/meta/')\n const merged = merge3(base, ours, theirs, [], stampWins)\n if (!merged.ok) {\n return reject(`${describeKey(merged.at)} was changed both by this write and on the contentrain branch`)\n }\n const value = merged.value === ABSENT ? {} : merged.value as Record<string, unknown>\n\n if (change.content === null) {\n // A delete merges to \"the keys only the other writer has\". Writing those\n // back would keep a file this operation meant to remove (a deleted model's\n // content, say) — refuse instead and let the caller decide after a pull.\n return Object.keys(value).length === 0\n ? { ok: true, change }\n : reject('the file gained entries on the contentrain branch since it was read, so deleting it would drop them')\n }\n\n const fieldOrder = change.path.startsWith('.contentrain/models/') ? MODEL_FIELD_ORDER : undefined\n return { ok: true, change: { path: change.path, content: canonicalStringify(value, fieldOrder) } }\n}\n\nconst ABSENT = Symbol('absent')\ntype Value = unknown | typeof ABSENT\n\ntype MergeResult = { ok: true, value: Value } | { ok: false, at: string[] }\n\n/** Meta keys that record who wrote last and when — see the module comment. */\nconst WRITE_STAMP = new Set(['updated_at', 'updated_by', 'source'])\n\nfunction merge3(base: Value, ours: Value, theirs: Value, at: string[], stampWins: boolean): MergeResult {\n if (same(ours, base)) return { ok: true, value: theirs }\n if (same(theirs, base)) return { ok: true, value: ours }\n if (same(ours, theirs)) return { ok: true, value: ours }\n if (!isPlainObject(base) || !isPlainObject(ours) || !isPlainObject(theirs)) {\n return { ok: false, at }\n }\n\n const restamped = stampWins\n && [...WRITE_STAMP].some(key => !same(field(ours, key), field(base, key)))\n const out: Record<string, unknown> = {}\n const keys = new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])\n for (const key of keys) {\n if (restamped && WRITE_STAMP.has(key)) {\n const value = field(ours, key)\n if (value !== ABSENT) out[key] = value\n continue\n }\n const result = merge3(field(base, key), field(ours, key), field(theirs, key), [...at, key], stampWins)\n if (!result.ok) return result\n if (result.value !== ABSENT) out[key] = result.value\n }\n return { ok: true, value: out }\n}\n\nfunction field(obj: Record<string, unknown>, key: string): Value {\n return Object.hasOwn(obj, key) && obj[key] !== null && obj[key] !== undefined ? obj[key] : ABSENT\n}\n\n/** Canonical equality — the same normalisation the serializer applies (sorted keys, nulls dropped). */\nfunction same(a: Value, b: Value): boolean {\n if (a === ABSENT || b === ABSENT) return a === b\n return JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b))\n}\n\nfunction isPlainObject(value: Value): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction parseObject(raw: string): Record<string, unknown> | undefined {\n try {\n const parsed: unknown = JSON.parse(raw)\n return isPlainObject(parsed) ? parsed : undefined\n } catch {\n return undefined\n }\n}\n\nfunction describeKey(at: string[]): string {\n if (at.length === 0) return 'the file'\n if (at.length === 1) return `\"${at[0]}\"`\n return `\"${at[0]}\" (field ${at.slice(1).map(k => `\"${k}\"`).join(' → ')})`\n}\n","import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type {\n Branch,\n Commit,\n CommitAuthor,\n ContentReadSource,\n FileChange,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n WriteReadiness,\n} from '../../core/contracts/index.js'\nimport { LOCAL_CAPABILITIES } from '../../core/contracts/index.js'\nimport { applyChangesToWorktree } from '../../core/ops/index.js'\nimport { checkBranchHealth } from '../../git/branch-lifecycle.js'\nimport { createGit } from '../../git/identity.js'\nimport { createTransaction } from '../../git/transaction.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { contentView, describeSource, refPath, type ContentView } from './content-view.js'\nimport { LocalReader } from './reader.js'\nimport { rebaseChange } from './rebase-changes.js'\nimport type { LocalApplyPlanInput, LocalApplyResult } from './types.js'\n\nconst DEFAULT_AUTHOR_NAME = 'Contentrain'\nconst DEFAULT_AUTHOR_EMAIL = 'ai@contentrain.io'\n\n/**\n * LocalProvider — the local-filesystem, worktree-backed content provider.\n *\n * Implements the full `RepoProvider` surface:\n * - Reader methods delegate to `LocalReader`.\n * - `applyPlan` wraps `createTransaction` and returns `LocalApplyResult`\n * (a superset of `Commit` carrying workflow action + selective sync).\n * - Branch ops mirror `GitHubProvider` — thin wrappers over the local\n * simple-git helpers in `./branch-ops.ts`.\n *\n * `mergeBranch` only supports merging into the singleton\n * `CONTENTRAIN_BRANCH`; the local flow advances the base branch via\n * `update-ref` in `transaction.mergeBranch`, so arbitrary merge targets\n * would bypass that invariant.\n */\nexport class LocalProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = LOCAL_CAPABILITIES\n private readonly reader: LocalReader\n\n constructor(public readonly projectRoot: string) {\n this.reader = new LocalReader(projectRoot)\n }\n\n // Reads: `.contentrain/**` from the content view (the `contentrain` ref\n // while a feature branch is checked out — #229), everything else from the\n // working tree. See content-view.ts.\n\n async readFile(path: string, ref?: string): Promise<string> {\n const at = await this.routed(path)\n return at ? at.view.reader.readFile(at.path) : this.reader.readFile(path, ref)\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const at = await this.routed(path)\n return at ? at.view.reader.listDirectory(at.path) : this.reader.listDirectory(path, ref)\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const at = await this.routed(path)\n return at ? at.view.reader.fileExists(at.path) : this.reader.fileExists(path, ref)\n }\n\n async contentSource(): Promise<ContentReadSource> {\n return describeSource(await contentView(this.projectRoot))\n }\n\n private async routed(path: string): Promise<{ view: ContentView & { source: 'ref' }, path: string } | null> {\n const view = await contentView(this.projectRoot)\n if (view.source !== 'ref') return null\n const inRef = refPath(view, this.projectRoot, path)\n return inRef === null ? null : { view, path: inRef }\n }\n\n /**\n * Branch pressure gate. Unmerged `cr/*` branches accumulate in a local repo\n * and eventually have to stop new writes; hosted providers have no such\n * limit, so they omit this method entirely.\n */\n async checkWriteReadiness(): Promise<WriteReadiness> {\n const health = await checkBranchHealth(this.projectRoot)\n return health.blocked\n ? { blocked: true, message: health.message ?? 'Too many active contentrain branches.' }\n : { blocked: false }\n }\n\n async applyPlan(input: LocalApplyPlanInput): Promise<LocalApplyResult> {\n // What the planner read: the content view — the working tree, or the\n // `contentrain` snapshot on a feature branch (#229). Captured before the\n // transaction fetches, so it is the tree the plan was computed from — not\n // whatever the remote says now.\n const view = await contentView(this.projectRoot)\n const basis = await Promise.all(input.changes.map(c => this.readBasis(view, c.path)))\n const tx = await createTransaction(this.projectRoot, input.branch, {\n workflowOverride: input.workflowOverride,\n })\n try {\n await tx.write(async (wt) => {\n // The worktree is the contentrain tip after fetch + merge — the tree\n // this commit is built on. Carry the plan over to it instead of\n // overwriting it with files built from a possibly stale working tree\n // (#226). See rebaseChange.\n const current = await Promise.all(input.changes.map(c => readOrNull(join(wt, c.path))))\n const results = input.changes.map((change, i) => rebaseChange(change, basis[i]!, current[i]!))\n const conflicts = results.filter(r => !r.ok)\n if (conflicts.length > 0) {\n throw Object.assign(new Error(\n `Nothing was written: the content branch moved since your working tree was last updated, and this change cannot be applied on top of it. `\n + conflicts.map(c => `${c.path}: ${c.reason}.`).join(' '),\n ), staleTreeGuidance(tx.baseBranch, tx.baseCheckedOut, view.source === 'ref'))\n }\n await applyChangesToWorktree(wt, results.map(r => (r as Extract<typeof r, { ok: true }>).change))\n })\n await tx.commit(input.message, input.context)\n const gitResult = await tx.complete()\n return {\n sha: gitResult.commit,\n message: input.message,\n author: input.author ?? {\n name: process.env['CONTENTRAIN_AUTHOR_NAME'] ?? DEFAULT_AUTHOR_NAME,\n email: process.env['CONTENTRAIN_AUTHOR_EMAIL'] ?? DEFAULT_AUTHOR_EMAIL,\n },\n timestamp: new Date().toISOString(),\n workflowAction: gitResult.action,\n sync: gitResult.sync,\n warning: gitResult.warning,\n base_advance: gitResult.base_advance,\n remote_push: gitResult.remote_push,\n }\n } finally {\n await tx.cleanup()\n }\n }\n\n private async readBasis(view: ContentView, path: string): Promise<string | null> {\n if (view.source === 'ref') {\n const inRef = refPath(view, this.projectRoot, path)\n if (inRef !== null) return view.reader.readFile(inRef).catch(() => null)\n }\n return readOrNull(join(this.projectRoot, path))\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.projectRoot, prefix)\n }\n\n async createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? CONTENTRAIN_BRANCH\n await createBranchOp(this.projectRoot, name, resolved)\n }\n\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.projectRoot, name)\n }\n\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? CONTENTRAIN_BRANCH\n return getBranchDiffOp(this.projectRoot, branch, resolved)\n }\n\n mergeBranch(branch: string, into: string): Promise<MergeResult> {\n return mergeBranchOp(this.projectRoot, branch, into)\n }\n\n isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? CONTENTRAIN_BRANCH\n return isMergedOp(this.projectRoot, branch, resolved)\n }\n\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.projectRoot)\n }\n\n async getMergeBase(refA: string, refB: string): Promise<string | null> {\n const git = createGit(this.projectRoot)\n try {\n return (await git.raw(['merge-base', refA, refB])).trim() || null\n } catch {\n return null\n }\n }\n\n /**\n * Two-parent merge commit in a temp worktree. The real `git merge`\n * resolves everything outside the plan; the plan's changes then\n * overwrite every content-owned path (planner output is authoritative,\n * never git's textual auto-merge). Leftover unmerged paths abort — they\n * are outside the content planner's scope by construction.\n */\n async createMergeCommit(input: {\n branch: string\n ours: string\n theirs: string\n changes: FileChange[]\n message: string\n author: CommitAuthor\n }): Promise<Commit> {\n const git = createGit(this.projectRoot)\n const tip = (await git.raw(['rev-parse', input.branch])).trim()\n if (tip !== input.ours) {\n throw Object.assign(new Error(\n `createMergeCommit: \"${input.branch}\" is at ${tip.slice(0, 8)}, not the expected ${input.ours.slice(0, 8)} — the branch moved since planning.`,\n ), {\n code: 'RECONCILE_STALE_OURS',\n agent_hint: 'Re-run the reconcile plan against the current tip.',\n })\n }\n\n const worktreePath = join(tmpdir(), `cr-merge-commit-${randomUUID()}`)\n await git.raw(['worktree', 'add', worktreePath, input.branch])\n const wtGit = createGit(worktreePath, {\n config: [\n `user.name=${input.author.name}`,\n `user.email=${input.author.email}`,\n ],\n })\n try {\n try {\n await wtGit.merge([input.theirs, '--no-commit', '--no-ff'])\n } catch {\n // Conflicts are expected — the plan overwrites content paths next.\n }\n await applyChangesToWorktree(worktreePath, input.changes)\n await wtGit.raw(['add', '-A'])\n const unmerged = (await wtGit.raw(['ls-files', '-u'])).trim()\n if (unmerged) {\n const paths = [...new Set(unmerged.split('\\n').map(l => l.split('\\t')[1]).filter(Boolean))]\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `createMergeCommit: ${paths.length} non-content file(s) conflict: ${paths.join(', ')}.`,\n ), { code: 'RECONCILE_SOURCE_CONFLICT' })\n }\n await wtGit.commit(input.message, { '--no-verify': null })\n const sha = (await wtGit.raw(['rev-parse', 'HEAD'])).trim()\n return {\n sha,\n message: input.message,\n author: input.author,\n timestamp: new Date().toISOString(),\n }\n } finally {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n }\n }\n}\n\nasync function readOrNull(path: string): Promise<string | null> {\n try {\n return await readFile(path, 'utf-8')\n } catch {\n return null\n }\n}\n\n/**\n * How to bring the planner's tree up to date. On the base branch that is a\n * pull. On a feature branch the content never reaches the working tree (the\n * advance leaves a non-base checkout alone), so a pull fetches nothing\n * useful — merging the base is what brings it in.\n */\nfunction staleTreeGuidance(baseBranch: string, baseCheckedOut: boolean, readFromRef: boolean): {\n code: string\n agent_hint: string\n developer_action?: string\n} {\n // Planned from the contentrain snapshot (#229): nothing on the developer's\n // side is stale — another writer moved contentrain between the read and\n // this write. Re-reading is the whole fix.\n if (readFromRef) {\n return {\n code: 'CONTENT_WORKING_TREE_STALE',\n agent_hint: `Another writer (Studio, a teammate, CI) changed the same value on the contentrain branch after this write read it. Re-read the content and retry — do not retry unchanged. No git action is needed.`,\n }\n }\n const update = baseCheckedOut\n ? 'Ask the developer to update their working tree (git pull)'\n : `The developer is on a branch other than \"${baseBranch}\", which content writes do not update. Ask them to merge \"${baseBranch}\" into it (or rebase onto it)`\n return {\n code: 'CONTENT_WORKING_TREE_STALE',\n agent_hint: `The content on the contentrain branch differs from the working tree this write was planned from — another writer (Studio, a teammate, CI) or an earlier write changed the same value. ${update}, then re-read the content and retry — do not retry unchanged.`,\n developer_action: baseCheckedOut ? 'git pull' : `git merge ${baseBranch}`,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"local-CBtRYFH4.mjs","names":["mergeBranchOp","canonicalStringify","sortKeys","listBranchesOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp","getDefaultBranchOp"],"sources":["../src/providers/local/branch-ops.ts","../src/git/ref-snapshot.ts","../src/providers/local/content-view.ts","../src/providers/local/rebase-changes.ts","../src/providers/local/provider.ts"],"sourcesContent":["import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport { createGit } from '../../git/identity.js'\nimport type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport { readConfig } from '../../core/config.js'\nimport { resolveBaseBranch } from '../../git/base-branch.js'\nimport { classifyMergedBranches, deleteRemoteBranch } from '../../git/branch-lifecycle.js'\nimport { mergeBranch as mergeBranchOp } from '../../git/transaction.js'\n\n/**\n * Branch/merge/diff helpers backed by the local simple-git worktree flow.\n *\n * Pure functions composed into `LocalProvider` — mirroring the shape of\n * `providers/github/branch-ops.ts` so the two providers share the same\n * surface at the `RepoProvider` boundary.\n */\n\nexport async function getDefaultBranch(projectRoot: string): Promise<string> {\n return resolveBaseBranch(createGit(projectRoot), await readConfig(projectRoot))\n}\n\nexport async function listBranches(\n projectRoot: string,\n prefix?: string,\n): Promise<Branch[]> {\n const git = createGit(projectRoot)\n const summary = await git.branchLocal()\n const names = prefix\n ? summary.all.filter(n => n.startsWith(prefix))\n : summary.all\n const branches: Branch[] = []\n for (const name of names) {\n const info = summary.branches[name]\n branches.push({ name, sha: info?.commit ?? '' })\n }\n return branches\n}\n\nexport async function createBranch(\n projectRoot: string,\n name: string,\n fromRef: string,\n): Promise<void> {\n const git = createGit(projectRoot)\n await git.raw(['branch', name, fromRef])\n}\n\nexport async function deleteBranch(\n projectRoot: string,\n name: string,\n): Promise<void> {\n const git = createGit(projectRoot)\n await git.deleteLocalBranch(name, true)\n // Parity with the remote-API providers, whose deleteBranch removes the\n // remote ref: best-effort, config-gated inside the helper, never throws.\n await deleteRemoteBranch(projectRoot, name)\n}\n\nexport async function getBranchDiff(\n projectRoot: string,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const git = createGit(projectRoot)\n const raw = await git.raw(['diff', '--name-status', `${base}...${branch}`])\n const diffs: FileDiff[] = []\n for (const line of raw.split('\\n')) {\n if (!line.trim()) continue\n const [code, ...pathParts] = line.split('\\t')\n const path = pathParts[pathParts.length - 1]\n if (!code || !path) continue\n const status: FileDiff['status'] = code.startsWith('A')\n ? 'added'\n : code.startsWith('D')\n ? 'removed'\n : 'modified'\n diffs.push({ path, status, before: null, after: null })\n }\n return diffs\n}\n\nexport async function mergeBranch(\n projectRoot: string,\n branch: string,\n into: string,\n): Promise<MergeResult> {\n if (into !== CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `LocalProvider.mergeBranch only supports merging into \"${CONTENTRAIN_BRANCH}\" (got \"${into}\"). `\n + `The local flow merges feature branches into the content-tracking branch and fast-forwards the base branch via update-ref.`,\n ), {\n code: 'UNSUPPORTED_MERGE_TARGET',\n agent_hint: `Pass \"${CONTENTRAIN_BRANCH}\" as the merge target, or use a non-local provider that supports arbitrary targets.`,\n developer_action: `Merge \"${branch}\" into \"${CONTENTRAIN_BRANCH}\" instead.`,\n })\n }\n const result = await mergeBranchOp(projectRoot, branch)\n return {\n merged: true,\n sha: result.commit,\n pullRequestUrl: null,\n sync: result.sync,\n ...(result.remote ? { remote: result.remote } : {}),\n }\n}\n\nexport async function isMerged(\n projectRoot: string,\n branch: string,\n into: string,\n): Promise<boolean> {\n try {\n const merged = await classifyMergedBranches(projectRoot, [branch], into)\n return merged.has(branch)\n } catch {\n return false\n }\n}\n","import { spawn } from 'node:child_process'\nimport type { RepoReader } from '../core/contracts/index.js'\nimport { gitBinary } from './identity.js'\n\n/**\n * RefSnapshotReader — a `RepoReader` over one subtree of one commit, read\n * with two git spawns in total however many files the caller touches.\n *\n * `GitRefReader` spawns `git show` per file, which is right for a planner\n * reading a handful of paths and wrong for a tool that walks every model and\n * every locale file (`content_list`, `status`, validation): a few hundred\n * spawns per call (#229). Here one `ls-tree -r` lists the subtree — which\n * answers `listDirectory` and `fileExists` from memory — and the first\n * `readFile` pulls every content blob under it through a single\n * `cat-file --batch`. Only content is batched ({@link batched}): media under\n * `assets_path`, and any large or non-text file, is read on demand, one\n * `cat-file blob` each, so a list or a save never loads the project's images.\n *\n * Bound to a commit sha, not a branch name, so a snapshot never changes\n * under its reader; {@link loadRefSnapshot} caches one per (repo, sha).\n *\n * Paths are repo-relative with `/` separators; the caller maps its own paths\n * (see LocalProvider). Only paths under `root` are in the snapshot — anything\n * else reads as missing.\n */\nexport class RefSnapshotReader implements RepoReader {\n private blobs: Promise<Map<string, string>> | null = null\n private readonly single = new Map<string, Promise<string>>()\n\n constructor(\n private readonly repoDir: string,\n public readonly commit: string,\n public readonly root: string,\n /** repo-relative file path → blob oid and size in bytes */\n private readonly files: ReadonlyMap<string, SnapshotFile>,\n /** repo-relative directory path → child names */\n private readonly dirs: ReadonlyMap<string, readonly string[]>,\n ) {}\n\n async readFile(path: string, _ref?: string): Promise<string> {\n const p = trim(path)\n const file = this.files.get(p)\n if (!file) throw Object.assign(new Error(`ENOENT: ${path} is not in ${this.commit.slice(0, 8)}:${this.root}`), { code: 'ENOENT' })\n if (!batched(p, file.size)) {\n let text = this.single.get(file.oid)\n if (!text) {\n text = runGit(this.repoDir, ['cat-file', 'blob', file.oid]).then(buf => buf.toString('utf-8'))\n text.catch(() => this.single.delete(file.oid))\n this.single.set(file.oid, text)\n }\n return text\n }\n this.blobs ??= readBlobs(this.repoDir, [...new Set([...this.files].filter(([f, { size }]) => batched(f, size)).map(([, { oid }]) => oid))])\n const text = (await this.blobs).get(file.oid)\n if (text === undefined) throw new Error(`blob ${file.oid} for ${path} could not be read`)\n return text\n }\n\n async listDirectory(path: string, _ref?: string): Promise<string[]> {\n return [...(this.dirs.get(trim(path)) ?? [])]\n }\n\n async fileExists(path: string, _ref?: string): Promise<boolean> {\n const p = trim(path)\n return this.files.has(p) || this.dirs.has(p)\n }\n}\n\ninterface SnapshotFile { oid: string, size: number }\n\n/** Largest blob the batch loads; a content file is far smaller. */\nconst BATCH_MAX_BYTES = 1024 * 1024\n\n/**\n * Whether the first `readFile` loads this file with the rest: a content file —\n * JSON (config, models, content, meta) or Markdown (documents) — no larger\n * than {@link BATCH_MAX_BYTES}. Media never matches by extension, wherever\n * `assets_path` points.\n */\nfunction batched(path: string, size: number): boolean {\n return size <= BATCH_MAX_BYTES && /\\.(?:json|md|mdx)$/i.test(path)\n}\n\nconst snapshots = new Map<string, Promise<RefSnapshotReader>>()\nconst MAX_SNAPSHOTS = 8\n\n/**\n * The snapshot of `root` at `commit`, listed once per (repo, commit) for the\n * life of the process: a commit's tree never changes, so a later tool call on\n * an unmoved branch costs no spawn at all.\n */\nexport function loadRefSnapshot(repoDir: string, commit: string, root: string): Promise<RefSnapshotReader> {\n const key = `${repoDir}\\0${commit}\\0${root}`\n let hit = snapshots.get(key)\n if (!hit) {\n hit = listTree(repoDir, commit, root)\n hit.catch(() => snapshots.delete(key))\n snapshots.set(key, hit)\n // Oldest-first eviction; a long-lived server sees a new tip per write.\n if (snapshots.size > MAX_SNAPSHOTS) snapshots.delete(snapshots.keys().next().value!)\n }\n return hit\n}\n\nasync function listTree(repoDir: string, commit: string, root: string): Promise<RefSnapshotReader> {\n const raw = await runGit(repoDir, ['ls-tree', '-r', '-l', '-z', '--full-tree', commit, '--', root])\n const files = new Map<string, SnapshotFile>()\n const children = new Map<string, Set<string>>()\n for (const record of raw.toString('utf-8').split('\\0')) {\n if (!record) continue\n // \"<mode> <type> <oid> <size, space-padded>\\t<path>\"\n const tab = record.indexOf('\\t')\n const [, type, oid, size] = record.slice(0, tab).split(/ +/)\n if (type !== 'blob' || !oid) continue\n const path = record.slice(tab + 1)\n files.set(path, { oid, size: Number(size) })\n // Register every ancestor directory down from the repo root.\n const parts = path.split('/')\n for (let i = 0; i < parts.length; i++) {\n const dir = parts.slice(0, i).join('/')\n let set = children.get(dir)\n if (!set) children.set(dir, set = new Set())\n set.add(parts[i]!)\n }\n }\n const dirs = new Map([...children].map(([dir, names]) => [dir, [...names].toSorted()] as const))\n return new RefSnapshotReader(repoDir, commit, root, files, dirs)\n}\n\n/** The given blobs in one `cat-file --batch` process. */\nasync function readBlobs(repoDir: string, oids: string[]): Promise<Map<string, string>> {\n const out = new Map<string, string>()\n if (oids.length === 0) return out\n const buf = await runGit(repoDir, ['cat-file', '--batch'], `${oids.join('\\n')}\\n`)\n // Each record: \"<oid> <type> <size>\\n<size bytes>\\n\"\n let at = 0\n while (at < buf.length) {\n const eol = buf.indexOf(0x0A, at)\n if (eol < 0) break\n const [oid, , size] = buf.subarray(at, eol).toString('utf-8').split(' ')\n const bytes = Number(size)\n if (!oid || !Number.isFinite(bytes)) break\n out.set(oid, buf.subarray(eol + 1, eol + 1 + bytes).toString('utf-8'))\n at = eol + 1 + bytes + 1\n }\n return out\n}\n\nfunction runGit(cwd: string, args: string[], stdin?: string): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const child = spawn(gitBinary(), args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] })\n const chunks: Buffer[] = []\n let stderr = ''\n child.stdout.on('data', (c: Buffer) => chunks.push(c))\n child.stderr.on('data', (c: Buffer) => { stderr += c.toString() })\n child.on('error', reject)\n // git may exit before reading all of stdin (a bad ref, a missing object): the write then fails\n // with EPIPE. The exit code below reports that failure; unhandled, it would crash the process.\n child.stdin.on('error', () => {})\n child.on('close', (code) => {\n if (code === 0) resolve(Buffer.concat(chunks))\n else reject(new Error(`git ${args[0]} exited ${code}: ${stderr.trim()}`))\n })\n child.stdin.end(stdin ?? '')\n })\n}\n\nfunction trim(path: string): string {\n let p = path.replaceAll('\\\\', '/')\n while (p.startsWith('./')) p = p.slice(2)\n while (p.endsWith('/')) p = p.slice(0, -1)\n return p === '.' ? '' : p\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\nimport { readFile, stat } from 'node:fs/promises'\nimport { join, resolve } from 'node:path'\nimport { CONTENTRAIN_BRANCH, type ContentReadSource } from '@contentrain/types'\nimport { readConfig } from '../../core/config.js'\nimport { resolveBaseBranch } from '../../git/base-branch.js'\nimport { createGit } from '../../git/identity.js'\nimport { loadRefSnapshot, type RefSnapshotReader } from '../../git/ref-snapshot.js'\n\n/**\n * Where a LocalProvider reads `.contentrain/` from (#229).\n *\n * Content writes land on `contentrain` and the base branch; a checked-out\n * feature branch is deliberately left alone (#227). Its working tree is then\n * behind every write, so reading `.contentrain/` from it plans the next write\n * from a stale value and shows read tools stale content. On such a branch the\n * content is read from the `contentrain` ref instead. Everything else — source\n * files, scan, normalize — still comes from the working tree.\n *\n * Working tree, unchanged from before, when:\n * - the base branch is checked out (writes advance it, so it is current once\n * pulled — #226's carry-over and `CONTENT_WORKING_TREE_STALE` still apply);\n * - HEAD is detached (a CI checkout validates the commit it checked out);\n * - HEAD is `contentrain` itself or a `cr/*` review branch (their tree IS\n * the content under review);\n * - there is no `contentrain` branch yet, or git cannot answer.\n */\nexport type ContentView =\n | { source: 'working_tree' }\n | { source: 'ref', ref: string, commit: string, branch: string, reader: RefSnapshotReader, prefix: string }\n\n/** The `.contentrain` paths that are never committed: always the working tree. */\nconst UNTRACKED = ['.contentrain/.cache', '.contentrain/client']\n\nconst scope = new AsyncLocalStorage<Map<string, Promise<ContentView>>>()\n\n/**\n * Run `fn` with one content view per project for its whole duration: every\n * read inside answers from the same snapshot, resolved once. The MCP server\n * wraps each tool call in this, so a call costs a fixed handful of spawns\n * however many files it reads.\n */\nexport function withReadScope<T>(fn: () => T): T {\n return scope.run(new Map(), fn)\n}\n\nexport function contentView(projectRoot: string): Promise<ContentView> {\n const memo = scope.getStore()\n if (!memo) return resolveView(projectRoot)\n let view = memo.get(projectRoot)\n if (!view) {\n view = resolveView(projectRoot)\n memo.set(projectRoot, view)\n }\n return view\n}\n\nexport function describeSource(view: ContentView): ContentReadSource {\n return view.source === 'working_tree'\n ? { source: 'working_tree' }\n : { source: 'ref', ref: view.ref, commit: view.commit, checked_out: view.branch }\n}\n\n/**\n * The repo-relative path a snapshot answers for, or null when `path` must come\n * from the working tree (outside `.contentrain/`, or never committed).\n */\nexport function refPath(view: ContentView & { source: 'ref' }, projectRoot: string, path: string): string | null {\n const abs = resolve(projectRoot, path)\n const root = resolve(projectRoot)\n if (abs !== root && !abs.startsWith(`${root}/`) && !abs.startsWith(`${root}\\\\`)) return null\n const rel = abs.slice(root.length + 1).replaceAll('\\\\', '/')\n if (rel !== '.contentrain' && !rel.startsWith('.contentrain/')) return null\n if (UNTRACKED.some(p => rel === p || rel.startsWith(`${p}/`))) return null\n return view.prefix + rel\n}\n\nasync function resolveView(projectRoot: string): Promise<ContentView> {\n try {\n const head = await headBranch(projectRoot)\n if (!head || head === CONTENTRAIN_BRANCH || head.startsWith('cr/')) return { source: 'working_tree' }\n const git = createGit(projectRoot)\n const base = await resolveBaseBranch(git, await readConfig(projectRoot), { currentBranch: head })\n if (head === base) return { source: 'working_tree' }\n const [prefix, commit] = (await git.raw(['rev-parse', '--show-prefix', `refs/heads/${CONTENTRAIN_BRANCH}^{commit}`]))\n .split('\\n').map(line => line.trim())\n if (!commit) return { source: 'working_tree' }\n const root = `${prefix ?? ''}.contentrain`\n const reader = await loadRefSnapshot(projectRoot, commit, root)\n return { source: 'ref', ref: CONTENTRAIN_BRANCH, commit, branch: head, reader, prefix: prefix ?? '' }\n } catch {\n return { source: 'working_tree' }\n }\n}\n\n/**\n * The checked-out branch, or null when HEAD is detached. Read from the HEAD\n * file when `.git` sits at the project root (no spawn — this runs once per\n * tool call on every local project); asked of git otherwise (a linked\n * worktree, or a project in a monorepo subdirectory).\n */\nasync function headBranch(projectRoot: string): Promise<string | null> {\n const dotGit = join(projectRoot, '.git')\n try {\n let gitDir = dotGit\n if ((await stat(dotGit)).isFile()) {\n const pointer = (await readFile(dotGit, 'utf-8')).match(/^gitdir:\\s*(.+)$/m)?.[1]?.trim()\n if (!pointer) throw new Error('unreadable .git file')\n gitDir = resolve(projectRoot, pointer)\n }\n const head = (await readFile(join(gitDir, 'HEAD'), 'utf-8')).trim()\n return head.startsWith('ref: refs/heads/') ? head.slice('ref: refs/heads/'.length) : null\n } catch {\n const out = await createGit(projectRoot).raw(['symbolic-ref', '-q', '--short', 'HEAD']).catch(() => '')\n return out.trim() || null\n }\n}\n","import { MODEL_FIELD_ORDER } from '@contentrain/types'\nimport type { FileChange } from '../../core/contracts/index.js'\nimport { canonicalStringify, sortKeys } from '../../core/serialization/index.js'\n\n/**\n * Carry a plan's full-file changes over to the tree they are committed on.\n *\n * The local provider plans against the developer's working tree, but commits\n * in a worktree on the `contentrain` branch after fetching and merging the\n * remote. Those two trees differ whenever another writer (Studio, a teammate,\n * CI) reached `contentrain` since the developer last pulled. A plan is a set\n * of whole files, so writing it as-is replaced every file it touched with a\n * copy built from the stale tree — silently deleting whatever the other\n * writer had added (#226).\n *\n * Each change is a three-way merge instead:\n * basis — the file as the planner read it (working tree, `null` if absent)\n * planned — the change's content (`null` for a delete)\n * current — the file in the worktree the commit is built on\n *\n * `current === basis` is the common case and writes the plan verbatim. When\n * they differ, JSON object files (collection / dictionary / singleton content,\n * meta, model definitions) are merged key by key: what the plan changed\n * relative to its basis is applied on top of `current`, recursively through\n * nested objects. Anything that cannot be merged that way — both sides\n * changed the same value, a markdown document, a delete of a file that\n * gained entries — is a conflict. Conflicts are never resolved by guessing:\n * the caller refuses the write.\n *\n * One exception, in meta files only: the write stamp (`updated_at`,\n * `updated_by`, `source`). Every write re-stamps the entries it touches\n * (`mergeEntryMeta`, `applyStatusChange`), so two writes to the same entry\n * always disagree there. The stamp describes the latest write, which is this\n * one — so when this write re-stamped an entry, its whole stamp wins, as a\n * unit (never this write's `updated_at` with the other writer's\n * `updated_by`). `status` and scheduling still merge or conflict normally.\n */\nexport type RebaseResult =\n | { ok: true, change: FileChange }\n | { ok: false, path: string, reason: string }\n\nexport function rebaseChange(change: FileChange, basis: string | null, current: string | null): RebaseResult {\n if (current === basis || current === change.content) return { ok: true, change }\n\n const reject = (reason: string): RebaseResult => ({ ok: false, path: change.path, reason })\n\n if (basis !== null && current === null) {\n return reject('the file was deleted on the contentrain branch since it was read')\n }\n if (!change.path.endsWith('.json')) {\n return reject('the file changed on the contentrain branch since it was read, and only JSON files can be merged')\n }\n\n const base = basis === null ? {} : parseObject(basis)\n const theirs = current === null ? {} : parseObject(current)\n const ours = change.content === null ? {} : parseObject(change.content)\n if (!base || !theirs || !ours) {\n return reject('the file changed on the contentrain branch since it was read, and is not a JSON object')\n }\n\n const stampWins = change.path.startsWith('.contentrain/meta/')\n const merged = merge3(base, ours, theirs, [], stampWins)\n if (!merged.ok) {\n return reject(`${describeKey(merged.at)} was changed both by this write and on the contentrain branch`)\n }\n const value = merged.value === ABSENT ? {} : merged.value as Record<string, unknown>\n\n if (change.content === null) {\n // A delete merges to \"the keys only the other writer has\". Writing those\n // back would keep a file this operation meant to remove (a deleted model's\n // content, say) — refuse instead and let the caller decide after a pull.\n return Object.keys(value).length === 0\n ? { ok: true, change }\n : reject('the file gained entries on the contentrain branch since it was read, so deleting it would drop them')\n }\n\n const fieldOrder = change.path.startsWith('.contentrain/models/') ? MODEL_FIELD_ORDER : undefined\n return { ok: true, change: { path: change.path, content: canonicalStringify(value, fieldOrder) } }\n}\n\nconst ABSENT = Symbol('absent')\ntype Value = unknown | typeof ABSENT\n\ntype MergeResult = { ok: true, value: Value } | { ok: false, at: string[] }\n\n/** Meta keys that record who wrote last and when — see the module comment. */\nconst WRITE_STAMP = new Set(['updated_at', 'updated_by', 'source'])\n\nfunction merge3(base: Value, ours: Value, theirs: Value, at: string[], stampWins: boolean): MergeResult {\n if (same(ours, base)) return { ok: true, value: theirs }\n if (same(theirs, base)) return { ok: true, value: ours }\n if (same(ours, theirs)) return { ok: true, value: ours }\n if (!isPlainObject(base) || !isPlainObject(ours) || !isPlainObject(theirs)) {\n return { ok: false, at }\n }\n\n const restamped = stampWins\n && [...WRITE_STAMP].some(key => !same(field(ours, key), field(base, key)))\n const out: Record<string, unknown> = {}\n const keys = new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])\n for (const key of keys) {\n if (restamped && WRITE_STAMP.has(key)) {\n const value = field(ours, key)\n if (value !== ABSENT) out[key] = value\n continue\n }\n const result = merge3(field(base, key), field(ours, key), field(theirs, key), [...at, key], stampWins)\n if (!result.ok) return result\n if (result.value !== ABSENT) out[key] = result.value\n }\n return { ok: true, value: out }\n}\n\nfunction field(obj: Record<string, unknown>, key: string): Value {\n return Object.hasOwn(obj, key) && obj[key] !== null && obj[key] !== undefined ? obj[key] : ABSENT\n}\n\n/** Canonical equality — the same normalisation the serializer applies (sorted keys, nulls dropped). */\nfunction same(a: Value, b: Value): boolean {\n if (a === ABSENT || b === ABSENT) return a === b\n return JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b))\n}\n\nfunction isPlainObject(value: Value): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction parseObject(raw: string): Record<string, unknown> | undefined {\n try {\n const parsed: unknown = JSON.parse(raw)\n return isPlainObject(parsed) ? parsed : undefined\n } catch {\n return undefined\n }\n}\n\nfunction describeKey(at: string[]): string {\n if (at.length === 0) return 'the file'\n if (at.length === 1) return `\"${at[0]}\"`\n return `\"${at[0]}\" (field ${at.slice(1).map(k => `\"${k}\"`).join(' → ')})`\n}\n","import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type {\n Branch,\n Commit,\n CommitAuthor,\n ContentReadSource,\n FileChange,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n WriteReadiness,\n} from '../../core/contracts/index.js'\nimport { LOCAL_CAPABILITIES } from '../../core/contracts/index.js'\nimport { applyChangesToWorktree } from '../../core/ops/index.js'\nimport { checkBranchHealth } from '../../git/branch-lifecycle.js'\nimport { createGit } from '../../git/identity.js'\nimport { createTransaction } from '../../git/transaction.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { contentView, describeSource, refPath, type ContentView } from './content-view.js'\nimport { LocalReader } from './reader.js'\nimport { rebaseChange } from './rebase-changes.js'\nimport type { LocalApplyPlanInput, LocalApplyResult } from './types.js'\n\nconst DEFAULT_AUTHOR_NAME = 'Contentrain'\nconst DEFAULT_AUTHOR_EMAIL = 'ai@contentrain.io'\n\n/**\n * LocalProvider — the local-filesystem, worktree-backed content provider.\n *\n * Implements the full `RepoProvider` surface:\n * - Reader methods delegate to `LocalReader`.\n * - `applyPlan` wraps `createTransaction` and returns `LocalApplyResult`\n * (a superset of `Commit` carrying workflow action + selective sync).\n * - Branch ops mirror `GitHubProvider` — thin wrappers over the local\n * simple-git helpers in `./branch-ops.ts`.\n *\n * `mergeBranch` only supports merging into the singleton\n * `CONTENTRAIN_BRANCH`; the local flow advances the base branch via\n * `update-ref` in `transaction.mergeBranch`, so arbitrary merge targets\n * would bypass that invariant.\n */\nexport class LocalProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = LOCAL_CAPABILITIES\n private readonly reader: LocalReader\n\n constructor(public readonly projectRoot: string) {\n this.reader = new LocalReader(projectRoot)\n }\n\n // Reads: `.contentrain/**` from the content view (the `contentrain` ref\n // while a feature branch is checked out — #229), everything else from the\n // working tree. See content-view.ts.\n\n async readFile(path: string, ref?: string): Promise<string> {\n const at = await this.routed(path)\n return at ? at.view.reader.readFile(at.path) : this.reader.readFile(path, ref)\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const at = await this.routed(path)\n return at ? at.view.reader.listDirectory(at.path) : this.reader.listDirectory(path, ref)\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const at = await this.routed(path)\n return at ? at.view.reader.fileExists(at.path) : this.reader.fileExists(path, ref)\n }\n\n async contentSource(): Promise<ContentReadSource> {\n return describeSource(await contentView(this.projectRoot))\n }\n\n private async routed(path: string): Promise<{ view: ContentView & { source: 'ref' }, path: string } | null> {\n const view = await contentView(this.projectRoot)\n if (view.source !== 'ref') return null\n const inRef = refPath(view, this.projectRoot, path)\n return inRef === null ? null : { view, path: inRef }\n }\n\n /**\n * Branch pressure gate. Unmerged `cr/*` branches accumulate in a local repo\n * and eventually have to stop new writes; hosted providers have no such\n * limit, so they omit this method entirely.\n */\n async checkWriteReadiness(): Promise<WriteReadiness> {\n const health = await checkBranchHealth(this.projectRoot)\n return health.blocked\n ? { blocked: true, message: health.message ?? 'Too many active contentrain branches.' }\n : { blocked: false }\n }\n\n async applyPlan(input: LocalApplyPlanInput): Promise<LocalApplyResult> {\n // What the planner read: the content view — the working tree, or the\n // `contentrain` snapshot on a feature branch (#229). Captured before the\n // transaction fetches, so it is the tree the plan was computed from — not\n // whatever the remote says now.\n const view = await contentView(this.projectRoot)\n const basis = await Promise.all(input.changes.map(c => this.readBasis(view, c.path)))\n const tx = await createTransaction(this.projectRoot, input.branch, {\n workflowOverride: input.workflowOverride,\n })\n try {\n await tx.write(async (wt) => {\n // The worktree is the contentrain tip after fetch + merge — the tree\n // this commit is built on. Carry the plan over to it instead of\n // overwriting it with files built from a possibly stale working tree\n // (#226). See rebaseChange.\n const current = await Promise.all(input.changes.map(c => readOrNull(join(wt, c.path))))\n const results = input.changes.map((change, i) => rebaseChange(change, basis[i]!, current[i]!))\n const conflicts = results.filter(r => !r.ok)\n if (conflicts.length > 0) {\n throw Object.assign(new Error(\n `Nothing was written: the content branch moved since your working tree was last updated, and this change cannot be applied on top of it. `\n + conflicts.map(c => `${c.path}: ${c.reason}.`).join(' '),\n ), staleTreeGuidance(tx.baseBranch, tx.baseCheckedOut, view.source === 'ref'))\n }\n await applyChangesToWorktree(wt, results.map(r => (r as Extract<typeof r, { ok: true }>).change))\n })\n await tx.commit(input.message, input.context)\n const gitResult = await tx.complete()\n return {\n sha: gitResult.commit,\n message: input.message,\n author: input.author ?? {\n name: process.env['CONTENTRAIN_AUTHOR_NAME'] ?? DEFAULT_AUTHOR_NAME,\n email: process.env['CONTENTRAIN_AUTHOR_EMAIL'] ?? DEFAULT_AUTHOR_EMAIL,\n },\n timestamp: new Date().toISOString(),\n workflowAction: gitResult.action,\n sync: gitResult.sync,\n warning: gitResult.warning,\n base_advance: gitResult.base_advance,\n remote_push: gitResult.remote_push,\n }\n } finally {\n await tx.cleanup()\n }\n }\n\n private async readBasis(view: ContentView, path: string): Promise<string | null> {\n if (view.source === 'ref') {\n const inRef = refPath(view, this.projectRoot, path)\n if (inRef !== null) return view.reader.readFile(inRef).catch(() => null)\n }\n return readOrNull(join(this.projectRoot, path))\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.projectRoot, prefix)\n }\n\n async createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? CONTENTRAIN_BRANCH\n await createBranchOp(this.projectRoot, name, resolved)\n }\n\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.projectRoot, name)\n }\n\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? CONTENTRAIN_BRANCH\n return getBranchDiffOp(this.projectRoot, branch, resolved)\n }\n\n mergeBranch(branch: string, into: string): Promise<MergeResult> {\n return mergeBranchOp(this.projectRoot, branch, into)\n }\n\n isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? CONTENTRAIN_BRANCH\n return isMergedOp(this.projectRoot, branch, resolved)\n }\n\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.projectRoot)\n }\n\n async getMergeBase(refA: string, refB: string): Promise<string | null> {\n const git = createGit(this.projectRoot)\n try {\n return (await git.raw(['merge-base', refA, refB])).trim() || null\n } catch {\n return null\n }\n }\n\n /**\n * Two-parent merge commit in a temp worktree. The real `git merge`\n * resolves everything outside the plan; the plan's changes then\n * overwrite every content-owned path (planner output is authoritative,\n * never git's textual auto-merge). Leftover unmerged paths abort — they\n * are outside the content planner's scope by construction.\n */\n async createMergeCommit(input: {\n branch: string\n ours: string\n theirs: string\n changes: FileChange[]\n message: string\n author: CommitAuthor\n }): Promise<Commit> {\n const git = createGit(this.projectRoot)\n const tip = (await git.raw(['rev-parse', input.branch])).trim()\n if (tip !== input.ours) {\n throw Object.assign(new Error(\n `createMergeCommit: \"${input.branch}\" is at ${tip.slice(0, 8)}, not the expected ${input.ours.slice(0, 8)} — the branch moved since planning.`,\n ), {\n code: 'RECONCILE_STALE_OURS',\n agent_hint: 'Re-run the reconcile plan against the current tip.',\n })\n }\n\n const worktreePath = join(tmpdir(), `cr-merge-commit-${randomUUID()}`)\n await git.raw(['worktree', 'add', worktreePath, input.branch])\n const wtGit = createGit(worktreePath, {\n config: [\n `user.name=${input.author.name}`,\n `user.email=${input.author.email}`,\n ],\n })\n try {\n try {\n await wtGit.merge([input.theirs, '--no-commit', '--no-ff'])\n } catch {\n // Conflicts are expected — the plan overwrites content paths next.\n }\n await applyChangesToWorktree(worktreePath, input.changes)\n await wtGit.raw(['add', '-A'])\n const unmerged = (await wtGit.raw(['ls-files', '-u'])).trim()\n if (unmerged) {\n const paths = [...new Set(unmerged.split('\\n').map(l => l.split('\\t')[1]).filter(Boolean))]\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `createMergeCommit: ${paths.length} non-content file(s) conflict: ${paths.join(', ')}.`,\n ), { code: 'RECONCILE_SOURCE_CONFLICT' })\n }\n await wtGit.commit(input.message, { '--no-verify': null })\n const sha = (await wtGit.raw(['rev-parse', 'HEAD'])).trim()\n return {\n sha,\n message: input.message,\n author: input.author,\n timestamp: new Date().toISOString(),\n }\n } finally {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n }\n }\n}\n\nasync function readOrNull(path: string): Promise<string | null> {\n try {\n return await readFile(path, 'utf-8')\n } catch {\n return null\n }\n}\n\n/**\n * How to bring the planner's tree up to date. On the base branch that is a\n * pull. On a feature branch the content never reaches the working tree (the\n * advance leaves a non-base checkout alone), so a pull fetches nothing\n * useful — merging the base is what brings it in.\n */\nfunction staleTreeGuidance(baseBranch: string, baseCheckedOut: boolean, readFromRef: boolean): {\n code: string\n agent_hint: string\n developer_action?: string\n} {\n // Planned from the contentrain snapshot (#229): nothing on the developer's\n // side is stale — another writer moved contentrain between the read and\n // this write. Re-reading is the whole fix.\n if (readFromRef) {\n return {\n code: 'CONTENT_WORKING_TREE_STALE',\n agent_hint: `Another writer (Studio, a teammate, CI) changed the same value on the contentrain branch after this write read it. Re-read the content and retry — do not retry unchanged. No git action is needed.`,\n }\n }\n const update = baseCheckedOut\n ? 'Ask the developer to update their working tree (git pull)'\n : `The developer is on a branch other than \"${baseBranch}\", which content writes do not update. Ask them to merge \"${baseBranch}\" into it (or rebase onto it)`\n return {\n code: 'CONTENT_WORKING_TREE_STALE',\n agent_hint: `The content on the contentrain branch differs from the working tree this write was planned from — another writer (Studio, a teammate, CI) or an earlier write changed the same value. ${update}, then re-read the content and retry — do not retry unchanged.`,\n developer_action: baseCheckedOut ? 'git pull' : `git merge ${baseBranch}`,\n }\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { a as readJson, c as writeText, i as readDir, n as ensureDir, o as readT
|
|
|
2
2
|
import { a as writeMeta, i as readMeta, n as deleteMeta, r as mergeEntryMeta } from "./meta-manager-CSpKtjnP.mjs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { access, readFile, readdir, rm } from "node:fs/promises";
|
|
5
|
-
import { DICTIONARY_TITLE_FIELD, LOCALE_PATTERN, MODEL_FIELD_ORDER, MODEL_FIELD_ORDER as MODEL_FIELD_ORDER$1, TITLE_FIELD_TYPES, generateEntryId, isTitleFieldType, parseMarkdownFrontmatter, parseMarkdownFrontmatter as parseFrontmatter, serializeMarkdownFrontmatter, serializeMarkdownFrontmatter as serializeFrontmatter, titleFieldTarget, validateEntryId as validateEntryId$1, validateLocale as validateLocale$1, validateSlug as validateSlug$1 } from "@contentrain/types";
|
|
5
|
+
import { DICTIONARY_TITLE_FIELD, LOCALE_PATTERN, MODEL_FIELD_ORDER, MODEL_FIELD_ORDER as MODEL_FIELD_ORDER$1, TITLE_FIELD_TYPES, generateEntryId, isTitleFieldType, parseMarkdownFrontmatter, parseMarkdownFrontmatter as parseFrontmatter, preservedKeyConflicts, serializeMarkdownFrontmatter, serializeMarkdownFrontmatter as serializeFrontmatter, titleFieldTarget, validateEntryId as validateEntryId$1, validateLocale as validateLocale$1, validateSlug as validateSlug$1 } from "@contentrain/types";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
//#region src/core/ops/paths.ts
|
|
8
8
|
/**
|
|
@@ -204,10 +204,11 @@ async function writeContent(projectRoot, model, entries, config, vocabulary) {
|
|
|
204
204
|
const existingRaw = await readText(docPath);
|
|
205
205
|
const action = existingRaw ? "updated" : "created";
|
|
206
206
|
const existingDoc = existingRaw ? parseMarkdownFrontmatter(existingRaw) : null;
|
|
207
|
-
|
|
207
|
+
const fmData = {
|
|
208
208
|
...existingDoc?.frontmatter,
|
|
209
209
|
...incomingFm
|
|
210
|
-
}
|
|
210
|
+
};
|
|
211
|
+
await writeText(docPath, serializeMarkdownFrontmatter(fmData, bodySent ? incomingBody : existingDoc?.body ?? "", existingDoc?.preserved));
|
|
211
212
|
const prevMeta = await readMeta(projectRoot, model, {
|
|
212
213
|
locale,
|
|
213
214
|
slug,
|
|
@@ -218,10 +219,12 @@ async function writeContent(projectRoot, model, entries, config, vocabulary) {
|
|
|
218
219
|
slug,
|
|
219
220
|
defaultLocale
|
|
220
221
|
}, mergeEntryMeta(prevMeta ?? void 0, entry));
|
|
222
|
+
const replaced = preservedKeyConflicts(fmData, existingDoc?.preserved).map((key) => `Document "${slug}" (${locale}): the saved "${key}" replaced a "${key}" block the file held in a shape Contentrain does not read.`);
|
|
221
223
|
results.push({
|
|
222
224
|
action,
|
|
223
225
|
slug,
|
|
224
|
-
locale
|
|
226
|
+
locale,
|
|
227
|
+
...replaced.length > 0 ? { advisories: replaced } : {}
|
|
225
228
|
});
|
|
226
229
|
break;
|
|
227
230
|
}
|
|
@@ -1449,4 +1452,4 @@ function validateModelDefinition(input, opts = {}) {
|
|
|
1449
1452
|
//#endregion
|
|
1450
1453
|
export { contentDirPath as A, resolveMdFilePath as C, validateSlug$1 as D, validateLocale$1 as E, documentFilePath as M, metaFilePath as N, writeContent as O, resolveLocaleStrategy as S, validateEntryId$1 as T, listContent as _, countEntries as a, resolveContentDir as b, inferTitleField as c, readModel as d, titleFieldIssues as f, deleteContent as g, writeModel as h, collectFieldPaths as i, contentFilePath as j, LocalReader as k, legacyFieldNames as l, validateModelDefinition as m, MODEL_FIELD_ORDER$1 as n, deleteModel as o, titleFieldOptions as p, checkReferences as r, fieldDefZodSchema as s, FIELD_TYPE_ENUM as t, listModels as u, parseFrontmatter as v, serializeFrontmatter as w, resolveJsonFilePath as x, readContent as y };
|
|
1451
1454
|
|
|
1452
|
-
//# sourceMappingURL=model-manager-
|
|
1455
|
+
//# sourceMappingURL=model-manager-D0VztE8M.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-manager-D0VztE8M.mjs","names":["validateLocale","validateEntryId","validateSlug","tryReadJsonViaReader"],"sources":["../src/core/ops/paths.ts","../src/providers/local/reader.ts","../src/core/content-manager.ts","../src/core/model-manager.ts"],"sourcesContent":["import type { ModelDefinition } from '@contentrain/types'\n\n/**\n * Content-root-relative path helpers for plan-API consumers.\n *\n * These mirror the logic inside `core/content-manager.ts:resolveJsonFilePath`\n * but return forward-slash relative strings suitable for `FileChange.path`.\n * Providers are responsible for resolving these against their backing store\n * (worktree on disk for LocalProvider, git tree path for GitHubProvider, ...).\n *\n * Kept inline to `core/ops/` for now; when Faz 2 finishes migrating every op\n * the legacy absolute-path helpers in `core/content-manager.ts` and\n * `core/meta-manager.ts` will call into these (or be replaced entirely).\n */\n\n/**\n * Content-root-relative content directory for a model (forward-slash paths).\n * Mirrors `resolveContentDir` with an empty `projectRoot`, so reader-backed\n * callers and plan-API consumers share the same path resolution.\n */\nexport function contentDirPath(\n model: Pick<ModelDefinition, 'domain' | 'id' | 'content_path'>,\n): string {\n if (model.content_path) return model.content_path\n return `.contentrain/content/${model.domain}/${model.id}`\n}\n\nfunction contentDir(model: Pick<ModelDefinition, 'domain' | 'id' | 'content_path'>): string {\n return contentDirPath(model)\n}\n\nexport function contentFilePath(\n model: Pick<ModelDefinition, 'id' | 'kind' | 'domain' | 'i18n' | 'content_path' | 'locale_strategy'>,\n locale: string,\n): string {\n const dir = contentDir(model)\n if (!model.i18n) return `${dir}/data.json`\n const strategy = model.locale_strategy ?? 'file'\n switch (strategy) {\n case 'suffix': return `${dir}/${model.id}.${locale}.json`\n case 'directory': return `${dir}/${locale}/${model.id}.json`\n case 'none': return `${dir}/${model.id}.json`\n default: return `${dir}/${locale}.json`\n }\n}\n\nexport function documentFilePath(\n model: Pick<ModelDefinition, 'id' | 'domain' | 'i18n' | 'content_path' | 'locale_strategy'>,\n locale: string,\n slug: string,\n): string {\n const dir = contentDir(model)\n if (!model.i18n) return `${dir}/${slug}.md`\n const strategy = model.locale_strategy ?? 'file'\n switch (strategy) {\n case 'suffix': return `${dir}/${slug}.${locale}.md`\n case 'directory': return `${dir}/${locale}/${slug}.md`\n case 'none': return `${dir}/${slug}.md`\n default: return `${dir}/${slug}/${locale}.md`\n }\n}\n\n/**\n * Content-root-relative meta path.\n *\n * `i18n` is in the Pick and `defaultLocale` is required on purpose. A non-i18n\n * model keeps all its content in one `data.json`, so it has exactly one meta\n * record — pinned to the default locale. Passing the caller's locale straight\n * through used to fan meta out across locales for that single content file\n * (`meta/{id}/tr.json` *and* `meta/{id}/en.json` for one `data.json`), leaving\n * every reader to guess which file was authoritative.\n */\nexport function metaFilePath(\n model: Pick<ModelDefinition, 'id' | 'kind' | 'i18n'>,\n locale: string,\n defaultLocale: string,\n slug?: string,\n): string {\n const base = `.contentrain/meta/${model.id}`\n const effective = model.i18n ? locale : defaultLocale\n if (model.kind === 'document' && slug) {\n return `${base}/${slug}/${effective}.json`\n }\n return `${base}/${effective}.json`\n}\n","import { access, readdir, readFile } from 'node:fs/promises'\nimport { resolve } from 'node:path'\nimport type { RepoReader } from '../../core/contracts/index.js'\n\n/**\n * LocalReader — `RepoReader` backed by the local filesystem.\n *\n * Paths may be absolute or relative to `projectRoot` (`node:path/resolve`\n * handles both). The `ref` parameter is accepted for interface compatibility\n * but ignored because LocalReader always reads from the working tree.\n *\n * Phase 1: provided as plumbing; core ops still use direct `fs` calls.\n * Phase 2 routes ops through this reader (and through GitHubProvider's reader\n * in Phase 5) so the same op surface works on every backing store.\n */\nexport class LocalReader implements RepoReader {\n constructor(public readonly projectRoot: string) {}\n\n async readFile(path: string, _ref?: string): Promise<string> {\n return readFile(resolve(this.projectRoot, path), 'utf-8')\n }\n\n async listDirectory(path: string, _ref?: string): Promise<string[]> {\n try {\n return await readdir(resolve(this.projectRoot, path))\n } catch {\n return []\n }\n }\n\n async fileExists(path: string, _ref?: string): Promise<boolean> {\n try {\n await access(resolve(this.projectRoot, path))\n return true\n } catch {\n return false\n }\n }\n}\n","import type { ModelDefinition, ContentrainConfig, EntryMeta, LocaleStrategy, DocumentEntry, Vocabulary } from '@contentrain/types'\nimport { validateSlug, validateEntryId, validateLocale, generateEntryId, parseMarkdownFrontmatter, preservedKeyConflicts, serializeMarkdownFrontmatter } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { contentrainDir, readDir, readJson, readText, writeJson, writeText } from '../util/fs.js'\nimport { readMeta, writeMeta, deleteMeta, mergeEntryMeta } from './meta-manager.js'\nimport { readModel } from './model-manager.js'\nimport type { RepoReader } from './contracts/index.js'\nimport { contentDirPath, contentFilePath, documentFilePath } from './ops/paths.js'\n\n// Re-export for backward compatibility (MCP internal consumers)\nexport { validateSlug, validateEntryId, validateLocale }\nexport { parseMarkdownFrontmatter as parseFrontmatter, serializeMarkdownFrontmatter as serializeFrontmatter } from '@contentrain/types'\n\n/** Keys named individually in a replacement advisory before it collapses to a count. */\nconst MAX_REPORTED_KEYS = 5\n\n// ─── Content paths ───\n//\n// These take the narrowest `Pick<ModelDefinition, …>` each one actually reads,\n// mirroring `ops/paths.ts`. Path resolution has nothing to do with a model's\n// title or schema, and demanding a whole `ModelDefinition` pushed callers that\n// only have a path-shaped fragment into `as ModelDefinition` casts — which then\n// hide a genuinely missing required property from the compiler.\n\nexport type ContentPathModel = Pick<ModelDefinition, 'id' | 'domain' | 'content_path'>\nexport type LocaleStrategyModel = Pick<ModelDefinition, 'locale_strategy'>\nexport type ContentFileModel = Pick<ModelDefinition, 'id' | 'i18n' | 'locale_strategy'>\n\nexport function resolveContentDir(projectRoot: string, model: ContentPathModel): string {\n if (model.content_path) {\n return join(projectRoot, model.content_path)\n }\n return join(contentrainDir(projectRoot), 'content', model.domain, model.id)\n}\n\nexport function resolveLocaleStrategy(model: LocaleStrategyModel): LocaleStrategy {\n return model.locale_strategy ?? 'file'\n}\n\n/** Build the file path for a JSON content file (singleton/collection/dictionary) */\nexport function resolveJsonFilePath(dir: string, model: ContentFileModel, locale: string): string {\n // When i18n is disabled, always use data.json (locale parameter is ignored)\n if (!model.i18n) return join(dir, 'data.json')\n\n switch (resolveLocaleStrategy(model)) {\n case 'suffix': return join(dir, `${model.id}.${locale}.json`)\n case 'directory': return join(dir, locale, `${model.id}.json`)\n case 'none': return join(dir, `${model.id}.json`)\n case 'file':\n default: return join(dir, `${locale}.json`)\n }\n}\n\n/** Build the file path for a markdown document */\nexport function resolveMdFilePath(dir: string, model: Pick<ModelDefinition, 'i18n' | 'locale_strategy'>, locale: string, slug: string): string {\n // When i18n is disabled, always use {slug}.md (locale parameter is ignored)\n if (!model.i18n) return join(dir, `${slug}.md`)\n\n switch (resolveLocaleStrategy(model)) {\n case 'suffix': return join(dir, `${slug}.${locale}.md`)\n case 'directory': return join(dir, locale, `${slug}.md`)\n case 'none': return join(dir, `${slug}.md`)\n case 'file':\n default: return join(dir, slug, `${locale}.md`)\n }\n}\n\n// ─── Types ───\n\nexport interface ContentEntry {\n id?: string\n slug?: string\n locale?: string\n data: Record<string, unknown>\n /**\n * Scheduled publish date, ISO 8601. Meta only — never written to the content\n * file — and it never changes `status`. A string sets it, `null` clears it,\n * an absent key leaves it untouched. See `EntrySchedule` in meta-manager.\n */\n publish_at?: string | null\n /** Scheduled expiry, ISO 8601, with the same set/clear/leave semantics. */\n expire_at?: string | null\n}\n\nexport interface WriteResult {\n action: 'created' | 'updated'\n id?: string\n slug?: string\n locale: string\n advisories?: string[]\n}\n\nexport interface DeleteOpts {\n id?: string\n slug?: string\n locale?: string\n keys?: string[]\n /** `config.locales.default` — places a non-i18n model's single meta record. */\n defaultLocale: string\n}\n\nexport interface ListOpts {\n locale?: string\n filter?: Record<string, unknown>\n resolve?: boolean\n limit?: number\n offset?: number\n}\n\n// ─── writeContent ───\n\nexport async function writeContent(\n projectRoot: string,\n model: ModelDefinition,\n entries: ContentEntry[],\n config: ContentrainConfig,\n vocabulary?: Vocabulary | null,\n): Promise<WriteResult[]> {\n const results: WriteResult[] = []\n const defaultLocale = config.locales.default\n\n for (const entry of entries) {\n const locale = entry.locale ?? defaultLocale\n\n // Validate locale\n const localeErr = validateLocale(locale, config)\n if (localeErr) throw new Error(localeErr)\n\n // Validate entry ID if provided\n if (entry.id) {\n const idErr = validateEntryId(entry.id)\n if (idErr) throw new Error(idErr)\n }\n\n // Validate slug if provided\n if (entry.slug) {\n const slugErr = validateSlug(entry.slug)\n if (slugErr) throw new Error(slugErr)\n }\n\n switch (model.kind) {\n case 'singleton': {\n const filePath = resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale)\n await writeJson(filePath, entry.data)\n const prevMeta = await readMeta(projectRoot, model, { locale, defaultLocale }) as EntryMeta | null\n await writeMeta(projectRoot, model, { locale, defaultLocale }, mergeEntryMeta(prevMeta ?? undefined, entry))\n results.push({ action: 'updated', locale })\n break\n }\n\n case 'collection': {\n const isNew = !entry.id\n const id = entry.id ?? generateEntryId()\n const filePath = resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale)\n const existing = await readJson<Record<string, Record<string, unknown>>>(filePath) ?? {}\n\n const action: 'created' | 'updated' = (isNew || !(id in existing)) ? 'created' : 'updated'\n existing[id] = entry.data\n\n // Sort by entry ID for canonical output\n const sorted: Record<string, Record<string, unknown>> = {}\n for (const key of Object.keys(existing).toSorted()) {\n sorted[key] = existing[key]!\n }\n\n await writeJson(filePath, sorted)\n const prevMetaMap = await readMeta(projectRoot, model, { locale, defaultLocale }) as Record<string, EntryMeta> | null\n await writeMeta(projectRoot, model, { locale, entryId: id, defaultLocale }, mergeEntryMeta(prevMetaMap?.[id], entry))\n results.push({ action, id, locale })\n break\n }\n\n case 'document': {\n const slug = entry.slug ?? entry.data['slug'] as string\n if (!slug) throw new Error('Document entries require a slug')\n const slugErr = validateSlug(slug)\n if (slugErr) throw new Error(slugErr)\n\n const incomingFm = { ...entry.data }\n const bodySent = 'body' in incomingFm\n const incomingBody = (incomingFm['body'] as string | undefined) ?? ''\n delete incomingFm['body']\n if (!incomingFm['slug']) incomingFm['slug'] = slug\n\n // Check if existing\n const docPath = resolveMdFilePath(resolveContentDir(projectRoot, model), model, locale, slug)\n const existingRaw = await readText(docPath)\n const action: 'created' | 'updated' = existingRaw ? 'updated' : 'created'\n\n // Merge with what is there, matching `planContentSave`. This path is\n // reached by scaffold and by normalize extract; extracting into a model\n // that already has documents would otherwise replace them wholesale.\n const existingDoc = existingRaw ? parseMarkdownFrontmatter(existingRaw) : null\n const fmData = { ...existingDoc?.frontmatter, ...incomingFm }\n const bodyContent = bodySent ? incomingBody : (existingDoc?.body ?? '')\n\n const mdContent = serializeMarkdownFrontmatter(fmData, bodyContent, existingDoc?.preserved)\n await writeText(docPath, mdContent)\n const prevMeta = await readMeta(projectRoot, model, { locale, slug, defaultLocale }) as EntryMeta | null\n await writeMeta(projectRoot, model, { locale, slug, defaultLocale }, mergeEntryMeta(prevMeta ?? undefined, entry))\n // Same advisory as planContentSave: a saved value for a key the file held in an unreadable shape replaces it.\n const replaced = preservedKeyConflicts(fmData, existingDoc?.preserved)\n .map(key => `Document \"${slug}\" (${locale}): the saved \"${key}\" replaced a \"${key}\" block the file held in a shape Contentrain does not read.`)\n results.push({ action, slug, locale, ...(replaced.length > 0 ? { advisories: replaced } : {}) })\n break\n }\n\n case 'dictionary': {\n const filePath = resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale)\n const existing = await readJson<Record<string, string>>(filePath) ?? {}\n // Duplicate value advisory: warn when a new key maps to a value that already exists\n const advisories: string[] = []\n\n // Replacing a value is reported, not refused — see the same block in\n // `planContentSave`. Correcting a translation is a content decision.\n const incoming = entry.data as Record<string, string>\n const replaced = Object.keys(incoming).filter(\n k => k in existing && existing[k] !== incoming[k],\n )\n if (replaced.length > 0) {\n const shown = replaced.slice(0, MAX_REPORTED_KEYS)\n const detail = shown.map(k => `\"${k}\": \"${existing[k]}\" → \"${incoming[k]}\"`).join(', ')\n const more = replaced.length > shown.length ? `, and ${replaced.length - shown.length} more` : ''\n advisories.push(\n `Dictionary \"${model.id}\" (${locale}): replaced ${replaced.length} existing value(s) — ${detail}${more}.`,\n )\n }\n const reverseMap = new Map<string, string>()\n for (const [k, v] of Object.entries(existing)) {\n reverseMap.set(v, k)\n }\n for (const [newKey, newValue] of Object.entries(entry.data as Record<string, string>)) {\n if (newKey in existing) continue\n const existingKey = reverseMap.get(newValue as string)\n if (existingKey && existingKey !== newKey) {\n advisories.push(\n `Value \"${newValue}\" already exists as key \"${existingKey}\". Consider reusing instead of creating \"${newKey}\".`,\n )\n }\n }\n\n // Vocabulary cross-reference: warn when a value matches a known vocabulary term\n if (vocabulary && Object.keys(vocabulary.terms).length > 0) {\n for (const [newKey, newValue] of Object.entries(entry.data as Record<string, string>)) {\n if (newKey in existing) continue\n for (const [, translations] of Object.entries(vocabulary.terms)) {\n if (Object.values(translations).includes(newValue as string)) {\n advisories.push(\n `Value \"${newValue}\" matches a vocabulary term. Use the canonical form for consistency.`,\n )\n break\n }\n }\n }\n }\n\n const merged = { ...existing, ...entry.data as Record<string, string> }\n await writeJson(filePath, merged)\n const prevMeta = await readMeta(projectRoot, model, { locale, defaultLocale }) as EntryMeta | null\n await writeMeta(projectRoot, model, { locale, defaultLocale }, mergeEntryMeta(prevMeta ?? undefined, entry))\n results.push({ action: 'updated', locale, ...(advisories.length > 0 ? { advisories } : {}) })\n break\n }\n }\n }\n\n return results\n}\n\n// ─── deleteContent ───\n\nexport async function deleteContent(\n projectRoot: string,\n model: ModelDefinition,\n opts: DeleteOpts,\n): Promise<string[]> {\n // A non-i18n model stores content locale-agnostically in data.json with a\n // single meta record at the default locale, so a locale-scoped delete is\n // meaningless and destructive (the locale maps onto data.json + the default\n // meta, deleting the shared content and the wrong meta). Reject it; mirrors\n // the same guard in the plan API (`planContentDelete`).\n if (!model.i18n && opts.locale) {\n throw new Error(\n `Model \"${model.id}\" has i18n disabled — its content is locale-agnostic (stored in data.json), `\n + 'so a locale-scoped delete is invalid. Omit \"locale\" to delete the entry, '\n + 'or run contentrain_validate fix:true to remove stray per-locale meta files.',\n )\n }\n\n const removed: string[] = []\n const cDir = resolveContentDir(projectRoot, model)\n\n switch (model.kind) {\n case 'collection': {\n if (!opts.id) throw new Error('Collection delete requires an entry ID')\n const locales = opts.locale\n ? [opts.locale]\n : (await readDir(cDir)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', '').replace(`${model.id}.`, ''))\n\n for (const loc of locales) {\n const filePath = resolveJsonFilePath(cDir, model, loc)\n const data = await readJson<Record<string, unknown>>(filePath)\n if (data && opts.id in data) {\n delete data[opts.id]\n await writeJson(filePath, data)\n removed.push(`content/${model.domain}/${model.id}/${loc}.json#${opts.id}`)\n }\n }\n\n for (const loc of locales) {\n await deleteMeta(projectRoot, model, { locale: loc, entryId: opts.id, defaultLocale: opts.defaultLocale })\n }\n break\n }\n\n case 'document': {\n if (!opts.slug) throw new Error('Document delete requires a slug')\n const slugDelErr = validateSlug(opts.slug)\n if (slugDelErr) throw new Error(slugDelErr)\n\n const strategy = resolveLocaleStrategy(model)\n if (!model.i18n) {\n // No i18n: single {slug}.md file\n await rm(join(cDir, `${opts.slug}.md`), { force: true })\n } else if (strategy === 'file') {\n // slug is a directory — remove entire slug directory\n await rm(join(cDir, opts.slug), { recursive: true, force: true })\n } else if (opts.locale) {\n // Remove specific locale file\n const filePath = resolveMdFilePath(cDir, model, opts.locale, opts.slug)\n await rm(filePath, { force: true })\n } else {\n // Remove all locale files for this slug\n const files = await readDir(strategy === 'directory' ? cDir : cDir)\n for (const f of files) {\n if (strategy === 'suffix' && f.startsWith(`${opts.slug}.`) && f.endsWith('.md')) {\n await rm(join(cDir, f), { force: true })\n } else if (strategy === 'directory') {\n // f is a locale dir — remove slug.md from it\n await rm(join(cDir, f, `${opts.slug}.md`), { force: true })\n } else if (strategy === 'none') {\n if (f === `${opts.slug}.md`) await rm(join(cDir, f), { force: true })\n }\n }\n }\n removed.push(`${model.content_path ?? `content/${model.domain}/${model.id}`}/${opts.slug}`)\n\n await deleteMeta(projectRoot, model, { slug: opts.slug, locale: opts.locale, defaultLocale: opts.defaultLocale })\n break\n }\n\n case 'singleton': {\n if (model.i18n && !opts.locale) throw new Error('Singleton delete requires a locale when i18n is enabled')\n const locale = opts.locale ?? 'data'\n const filePath = resolveJsonFilePath(cDir, model, locale)\n await rm(filePath, { force: true })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n await deleteMeta(projectRoot, model, { locale: model.i18n ? locale : undefined, defaultLocale: opts.defaultLocale })\n break\n }\n\n case 'dictionary': {\n if (model.i18n && !opts.locale) throw new Error('Dictionary delete requires a locale when i18n is enabled')\n const locale = opts.locale ?? 'data'\n const filePath = resolveJsonFilePath(cDir, model, locale)\n\n if (opts.keys?.length) {\n // Delete specific keys from dictionary\n const existing = await readJson<Record<string, string>>(filePath) ?? {}\n const notFound: string[] = []\n for (const key of opts.keys) {\n if (key in existing) {\n delete existing[key]\n } else {\n notFound.push(key)\n }\n }\n if (notFound.length > 0) {\n throw new Error(`Dictionary \"${model.id}\" (${locale}): keys not found — [${notFound.join(', ')}]`)\n }\n await writeJson(filePath, existing)\n removed.push(...opts.keys.map(k => `${model.id}/${locale}:${k}`))\n } else {\n // Delete entire locale file\n await rm(filePath, { force: true })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n await deleteMeta(projectRoot, model, { locale: model.i18n ? locale : undefined, defaultLocale: opts.defaultLocale })\n }\n break\n }\n }\n\n return removed\n}\n\n// ─── listContent ───\n\n/**\n * List content entries for a model. Dual signature:\n *\n * - `listContent(projectRoot, model, opts, config)` — legacy local flow,\n * uses direct filesystem reads and supports `opts.resolve` for\n * cross-model relation hydration.\n * - `listContent(reader, model, opts, config)` — reader-backed flow for\n * remote providers. Basic list works across all four model kinds.\n * `opts.resolve: true` is rejected with an error on remote readers\n * because the cross-model walk requires local filesystem access.\n *\n * The reader-based function lives in {@link listContentViaReader}; the\n * projectRoot-based entry point continues to call the legacy body to\n * preserve bit-for-bit behaviour for every existing caller.\n */\nexport function listContent(\n projectRoot: string,\n model: ModelDefinition,\n opts: ListOpts,\n config: ContentrainConfig,\n): Promise<unknown>\nexport function listContent(\n reader: RepoReader,\n model: ModelDefinition,\n opts: ListOpts,\n config: ContentrainConfig,\n): Promise<unknown>\nexport async function listContent(\n input: string | RepoReader,\n model: ModelDefinition,\n opts: ListOpts,\n config: ContentrainConfig,\n): Promise<unknown> {\n if (typeof input !== 'string') {\n return listContentViaReader(input, model, opts, config)\n }\n return listContentLocal(input, model, opts, config)\n}\n\nasync function listContentLocal(\n projectRoot: string,\n model: ModelDefinition,\n opts: ListOpts,\n config: ContentrainConfig,\n): Promise<unknown> {\n const cDir = resolveContentDir(projectRoot, model)\n const locale = opts.locale ?? config.locales.default\n\n switch (model.kind) {\n case 'singleton': {\n const data = await readJson<Record<string, unknown>>(resolveJsonFilePath(cDir, model, locale))\n return { kind: 'singleton', data: data ?? {}, locale }\n }\n\n case 'collection': {\n const data = await readJson<Record<string, Record<string, unknown>>>(resolveJsonFilePath(cDir, model, locale)) ?? {}\n let entries: Array<Record<string, unknown>> = Object.entries(data).map(([id, fields]) => {\n const entry: Record<string, unknown> = { id }\n Object.assign(entry, fields)\n return entry\n })\n\n // Filter\n if (opts.filter) {\n entries = entries.filter(entry => {\n for (const [key, value] of Object.entries(opts.filter!)) {\n if (entry[key] !== value) return false\n }\n return true\n })\n }\n\n const total = entries.length\n\n // Pagination\n const offset = opts.offset ?? 0\n const limit = opts.limit ?? entries.length\n entries = entries.slice(offset, offset + limit)\n\n // Resolve relations\n if (opts.resolve && model.fields) {\n entries = await resolveRelations(projectRoot, model, entries, locale)\n }\n\n return { kind: 'collection', data: entries, total, locale, offset, limit }\n }\n\n case 'document': {\n const entries: DocumentEntry[] = []\n const strategy = resolveLocaleStrategy(model)\n\n if (!model.i18n) {\n // No i18n: flat {slug}.md files, no locale in path\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else if (strategy === 'file') {\n // Each slug is a subdirectory containing locale.md files\n const slugDirs = await readDir(cDir)\n for (const slug of slugDirs) {\n const raw = await readText(join(cDir, slug, `${locale}.md`))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else if (strategy === 'suffix') {\n // Files like {slug}.{locale}.md in flat directory\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) {\n if (!f.endsWith(suffix)) continue\n const slug = f.slice(0, -suffix.length)\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else if (strategy === 'directory') {\n // Files in {locale}/ directory as {slug}.md\n const localeDir = join(cDir, locale)\n const files = await readDir(localeDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(localeDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else {\n // none — {slug}.md, no locale in filename\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n }\n\n const total = entries.length\n const offset = opts.offset ?? 0\n const limit = opts.limit ?? entries.length\n const paged = entries.slice(offset, offset + limit)\n\n return { kind: 'document', data: paged, total, locale, offset, limit }\n }\n\n case 'dictionary': {\n const data = await readJson<Record<string, string>>(resolveJsonFilePath(cDir, model, locale)) ?? {}\n return { kind: 'dictionary', data, total_keys: Object.keys(data).length, locale }\n }\n }\n}\n\nasync function tryReadJsonViaReader<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 tryReadTextViaReader(reader: RepoReader, path: string): Promise<string | null> {\n try {\n return await reader.readFile(path)\n } catch {\n return null\n }\n}\n\nasync function listContentViaReader(\n reader: RepoReader,\n model: ModelDefinition,\n opts: ListOpts,\n config: ContentrainConfig,\n): Promise<unknown> {\n if (opts.resolve) {\n throw new Error(\n 'contentrain_content_list with resolve:true requires local filesystem access. '\n + 'Use a LocalProvider (stdio or HTTP+LocalProvider) or omit resolve:true.',\n )\n }\n\n const cDir = contentDirPath(model)\n const locale = opts.locale ?? config.locales.default\n\n switch (model.kind) {\n case 'singleton': {\n const data = await tryReadJsonViaReader<Record<string, unknown>>(reader, contentFilePath(model, locale))\n return { kind: 'singleton', data: data ?? {}, locale }\n }\n\n case 'collection': {\n const data = await tryReadJsonViaReader<Record<string, Record<string, unknown>>>(\n reader,\n contentFilePath(model, locale),\n ) ?? {}\n let entries: Array<Record<string, unknown>> = Object.entries(data).map(([id, fields]) => {\n const entry: Record<string, unknown> = { id }\n Object.assign(entry, fields)\n return entry\n })\n\n if (opts.filter) {\n entries = entries.filter(entry => {\n for (const [key, value] of Object.entries(opts.filter!)) {\n if (entry[key] !== value) return false\n }\n return true\n })\n }\n\n const total = entries.length\n const offset = opts.offset ?? 0\n const limit = opts.limit ?? entries.length\n entries = entries.slice(offset, offset + limit)\n\n return { kind: 'collection', data: entries, total, locale, offset, limit }\n }\n\n case 'document': {\n const entries: DocumentEntry[] = []\n const strategy = resolveLocaleStrategy(model)\n\n const collectEntry = async (relPath: string, slug: string): Promise<void> => {\n const raw = await tryReadTextViaReader(reader, relPath)\n if (!raw) return\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n\n if (!model.i18n) {\n const files = await reader.listDirectory(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n await collectEntry(documentFilePath(model, locale, f.replace(/\\.md$/u, '')), f.replace(/\\.md$/u, ''))\n }\n } else if (strategy === 'file') {\n const slugDirs = await reader.listDirectory(cDir)\n for (const slug of slugDirs) {\n await collectEntry(documentFilePath(model, locale, slug), slug)\n }\n } else if (strategy === 'suffix') {\n const files = await reader.listDirectory(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) {\n if (!f.endsWith(suffix)) continue\n const slug = f.slice(0, -suffix.length)\n await collectEntry(documentFilePath(model, locale, slug), slug)\n }\n } else if (strategy === 'directory') {\n const files = await reader.listDirectory(`${cDir}/${locale}`)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace(/\\.md$/u, '')\n await collectEntry(documentFilePath(model, locale, slug), slug)\n }\n } else {\n const files = await reader.listDirectory(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace(/\\.md$/u, '')\n await collectEntry(documentFilePath(model, locale, slug), slug)\n }\n }\n\n const total = entries.length\n const offset = opts.offset ?? 0\n const limit = opts.limit ?? entries.length\n const paged = entries.slice(offset, offset + limit)\n\n return { kind: 'document', data: paged, total, locale, offset, limit }\n }\n\n case 'dictionary': {\n const data = await tryReadJsonViaReader<Record<string, string>>(reader, contentFilePath(model, locale)) ?? {}\n return { kind: 'dictionary', data, total_keys: Object.keys(data).length, locale }\n }\n }\n}\n\n// ─── readContent (for external use / relation resolving) ───\n\nexport async function readContent(\n projectRoot: string,\n model: ModelDefinition,\n opts: { locale: string; entryId?: string; slug?: string },\n): Promise<unknown> {\n const cDir = resolveContentDir(projectRoot, model)\n\n switch (model.kind) {\n case 'singleton':\n return readJson<Record<string, unknown>>(resolveJsonFilePath(cDir, model, opts.locale))\n\n case 'collection': {\n if (!opts.entryId) return null\n const data = await readJson<Record<string, Record<string, unknown>>>(resolveJsonFilePath(cDir, model, opts.locale))\n return data?.[opts.entryId] ? { id: opts.entryId, ...data[opts.entryId] } : null\n }\n\n case 'document': {\n if (!opts.slug) return null\n const raw = await readText(resolveMdFilePath(cDir, model, opts.locale, opts.slug))\n if (!raw) return null\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n return { slug: opts.slug, ...frontmatter, body }\n }\n\n case 'dictionary':\n return readJson<Record<string, string>>(resolveJsonFilePath(cDir, model, opts.locale))\n }\n}\n\n// ─── Relation resolving ───\n\nasync function resolveRelations(\n projectRoot: string,\n model: ModelDefinition,\n entries: Array<Record<string, unknown>>,\n locale: string,\n): Promise<Array<Record<string, unknown>>> {\n if (!model.fields) return entries\n\n const relationFields: Array<{ name: string; targetModels: string[]; multi: boolean }> = []\n for (const [name, field] of Object.entries(model.fields)) {\n if (field.type === 'relation' || field.type === 'relations') {\n const targets = Array.isArray(field.model) ? field.model : field.model ? [field.model] : []\n relationFields.push({ name, targetModels: targets, multi: field.type === 'relations' })\n }\n }\n\n if (relationFields.length === 0) return entries\n\n // Load target model contents into cache (with circular reference protection)\n const targetCache: Record<string, Record<string, Record<string, unknown>>> = {}\n const visited = new Set<string>([model.id])\n\n for (const rf of relationFields) {\n for (const targetModelId of rf.targetModels) {\n if (targetCache[targetModelId] || visited.has(targetModelId)) continue\n visited.add(targetModelId)\n\n const targetModel = await readModel(projectRoot, targetModelId)\n if (!targetModel) continue\n\n if (targetModel.kind === 'collection') {\n const targetData = await readJson<Record<string, Record<string, unknown>>>(\n resolveJsonFilePath(resolveContentDir(projectRoot, targetModel), targetModel, locale),\n ) ?? {}\n targetCache[targetModelId] = targetData\n } else if (targetModel.kind === 'document') {\n // Build slug-keyed cache by listing document entries for this locale\n const docCache: Record<string, Record<string, unknown>> = {}\n const cDir = resolveContentDir(projectRoot, targetModel)\n const strategy = resolveLocaleStrategy(targetModel)\n\n if (!targetModel.i18n) {\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else if (strategy === 'file') {\n const slugDirs = await readDir(cDir)\n for (const slug of slugDirs) {\n const raw = await readText(join(cDir, slug, `${locale}.md`))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else if (strategy === 'suffix') {\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) {\n if (!f.endsWith(suffix)) continue\n const slug = f.slice(0, -suffix.length)\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else if (strategy === 'directory') {\n const localeDir = join(cDir, locale)\n const files = await readDir(localeDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(localeDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else {\n // 'none' strategy\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n }\n\n targetCache[targetModelId] = docCache\n }\n }\n }\n\n return entries.map(entry => {\n const resolved = { ...entry }\n for (const rf of relationFields) {\n const value = resolved[rf.name]\n if (!value) continue\n\n if (rf.multi && Array.isArray(value)) {\n resolved[rf.name] = value.map(id => {\n for (const targetModelId of rf.targetModels) {\n const cached = targetCache[targetModelId]?.[id as string]\n if (cached) return { id, ...cached }\n }\n return id\n })\n } else if (typeof value === 'string') {\n for (const targetModelId of rf.targetModels) {\n const cached = targetCache[targetModelId]?.[value]\n if (cached) {\n resolved[rf.name] = { id: value, ...cached }\n break\n }\n }\n }\n }\n return resolved\n })\n}\n","import type { FieldDef, FieldType, ModelDefinition, ModelSummary } from '@contentrain/types'\nimport { DICTIONARY_TITLE_FIELD, isTitleFieldType, LOCALE_PATTERN, MODEL_FIELD_ORDER, TITLE_FIELD_TYPES, titleFieldTarget } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { z } from 'zod'\nimport { contentrainDir, ensureDir, readDir, readJson, writeJson } from '../util/fs.js'\nimport type { RepoReader } from './contracts/index.js'\nimport { LocalReader } from '../providers/local/reader.js'\nimport { resolveContentDir, resolveLocaleStrategy } from './content-manager.js'\nimport { contentDirPath } from './ops/paths.js'\n\nexport type { ModelSummary } from '@contentrain/types'\n// Re-exported so `packages/rules` and `packages/skills` — which devDep\n// @contentrain/mcp but not @contentrain/types — can assert docs parity\n// against the canonical key list without taking a new dependency.\nexport { MODEL_FIELD_ORDER } from '@contentrain/types'\n\nconst MODELS_DIR_PATH = '.contentrain/models'\n\nasync function tryReadJsonViaReader<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\n/**\n * List every model defined under `.contentrain/models/*.json`.\n *\n * Accepts either a legacy `projectRoot` string (filesystem walk) or any\n * `RepoReader` — HTTP-hosted callers pass their `GitHubProvider` so the\n * same helper resolves model metadata over the Git Data API.\n */\nexport function listModels(projectRoot: string): Promise<ModelSummary[]>\nexport function listModels(reader: RepoReader): Promise<ModelSummary[]>\nexport async function listModels(input: string | RepoReader): Promise<ModelSummary[]> {\n let files: string[]\n let load: (file: string) => Promise<ModelDefinition | null>\n\n if (typeof input === 'string') {\n const modelsDir = join(contentrainDir(input), 'models')\n files = await readDir(modelsDir)\n load = file => readJson<ModelDefinition>(join(modelsDir, file))\n } else {\n files = await input.listDirectory(MODELS_DIR_PATH)\n load = file => tryReadJsonViaReader<ModelDefinition>(input, `${MODELS_DIR_PATH}/${file}`)\n }\n\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n const models = await Promise.all(jsonFiles.map(load))\n\n return models\n .filter((m): m is ModelDefinition => m !== null && !!m.id)\n .map(model => ({\n id: model.id,\n kind: model.kind,\n domain: model.domain,\n i18n: model.i18n,\n fields: model.fields ? Object.keys(model.fields).length : 0,\n }))\n .toSorted((a, b) => a.id.localeCompare(b.id, 'en'))\n}\n\n/**\n * Read a single model definition. Same dual signature as {@link listModels}.\n */\nexport function readModel(projectRoot: string, modelId: string): Promise<ModelDefinition | null>\nexport function readModel(reader: RepoReader, modelId: string): Promise<ModelDefinition | null>\nexport async function readModel(input: string | RepoReader, modelId: string): Promise<ModelDefinition | null> {\n if (typeof input === 'string') {\n const filePath = join(contentrainDir(input), 'models', `${modelId}.json`)\n return readJson<ModelDefinition>(filePath)\n }\n return tryReadJsonViaReader<ModelDefinition>(input, `${MODELS_DIR_PATH}/${modelId}.json`)\n}\n\nasync function countDocumentFileStrategy(\n reader: RepoReader,\n contentDir: string,\n entries: string[],\n): Promise<{ total: number, locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n let total = 0\n\n const results = await Promise.all(\n entries.map(async (entry) => {\n const localeFiles = await reader.listDirectory(`${contentDir}/${entry}`)\n return localeFiles\n .map(lf => lf.replace(/\\.(json|md|mdx)$/, ''))\n .filter((locale, i) => locale !== localeFiles[i])\n }),\n )\n\n for (const entryLocales of results) {\n for (const locale of entryLocales) {\n locales[locale] = (locales[locale] ?? 0) + 1\n total++\n }\n }\n\n return { total, locales }\n}\n\nasync function countDocumentSuffixStrategy(\n _reader: RepoReader,\n _contentDir: string,\n files: string[],\n): Promise<{ total: number, locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n const slugsByLocale: Record<string, Set<string>> = {}\n\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n // Pattern: {slug}.{locale}.md\n const match = f.match(/^(.+)\\.([a-z]{2}(?:-[A-Z]{2})?)\\.md$/)\n if (!match) continue\n const locale = match[2]!\n if (!slugsByLocale[locale]) slugsByLocale[locale] = new Set()\n slugsByLocale[locale].add(match[1]!)\n }\n\n let total = 0\n for (const [locale, slugs] of Object.entries(slugsByLocale)) {\n locales[locale] = slugs.size\n total += slugs.size\n }\n\n return { total, locales }\n}\n\nasync function countDocumentDirectoryStrategy(\n reader: RepoReader,\n contentDir: string,\n localeDirs: string[],\n): Promise<{ total: number, locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n let total = 0\n\n const results = await Promise.all(\n localeDirs.map(async (localeDir) => {\n const files = await reader.listDirectory(`${contentDir}/${localeDir}`)\n const mdFiles = files.filter(f => f.endsWith('.md'))\n return { locale: localeDir, count: mdFiles.length }\n }),\n )\n\n for (const { locale, count } of results) {\n locales[locale] = count\n total += count\n }\n\n return { total, locales }\n}\n\nasync function countDocumentNoneStrategy(\n reader: RepoReader,\n modelId: string,\n files: string[],\n i18n: boolean,\n): Promise<{ total: number, locales: Record<string, number> }> {\n const mdFiles = files.filter(f => f.endsWith('.md'))\n\n if (!i18n) {\n return { total: mdFiles.length, locales: { _: mdFiles.length } }\n }\n\n // With i18n + none strategy, content files have no locale info.\n // Use meta/{modelId}/{slug}/ directories to determine locale counts.\n const metaDir = `.contentrain/meta/${modelId}`\n const locales: Record<string, number> = {}\n let total = 0\n\n const slugs = mdFiles.map(f => f.replace('.md', ''))\n const results = await Promise.all(\n slugs.map(async (slug) => {\n const metaFiles = await reader.listDirectory(`${metaDir}/${slug}`)\n return metaFiles\n .filter(f => f.endsWith('.json'))\n .map(f => f.replace('.json', ''))\n }),\n )\n\n for (const slugLocales of results) {\n for (const locale of slugLocales) {\n locales[locale] = (locales[locale] ?? 0) + 1\n total++\n }\n }\n\n return { total, locales }\n}\n\nasync function countCollectionEntries(\n reader: RepoReader,\n contentDir: string,\n jsonFiles: string[],\n): Promise<{ total: number, locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n let total = 0\n\n const results = await Promise.all(\n jsonFiles.map(async (file) => {\n const locale = file.replace(/\\.json$/, '')\n const data = await tryReadJsonViaReader<Record<string, unknown>>(reader, `${contentDir}/${file}`)\n return { locale, count: data ? Object.keys(data).length : 0 }\n }),\n )\n\n for (const { locale, count } of results) {\n locales[locale] = count\n total += count\n }\n\n return { total, locales }\n}\n\n/**\n * Count content entries across locales for a model.\n *\n * Accepts either a legacy `projectRoot` string (wraps LocalReader internally)\n * or any `RepoReader` — so remote providers (GitHubProvider) feed the same\n * counting logic via the Git Data API.\n */\nexport function countEntries(\n projectRoot: string,\n model: ModelDefinition,\n): Promise<{ total: number, locales: Record<string, number> }>\nexport function countEntries(\n reader: RepoReader,\n model: ModelDefinition,\n): Promise<{ total: number, locales: Record<string, number> }>\nexport async function countEntries(\n input: string | RepoReader,\n model: ModelDefinition,\n): Promise<{ total: number, locales: Record<string, number> }> {\n const reader: RepoReader = typeof input === 'string' ? new LocalReader(input) : input\n const cDir = contentDirPath(model)\n const strategy = resolveLocaleStrategy(model)\n const files = await reader.listDirectory(cDir)\n\n if (model.kind === 'document') {\n if (!model.i18n) {\n // No i18n: flat {slug}.md files\n return countDocumentNoneStrategy(reader, model.id, files, false)\n }\n\n switch (strategy) {\n case 'file':\n return countDocumentFileStrategy(reader, cDir, files)\n case 'suffix':\n return countDocumentSuffixStrategy(reader, cDir, files)\n case 'directory':\n return countDocumentDirectoryStrategy(reader, cDir, files)\n case 'none':\n return countDocumentNoneStrategy(reader, model.id, files, true)\n }\n }\n\n if (model.kind === 'collection') {\n if (!model.i18n) {\n // Non-i18n: single data.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n return countCollectionEntries(reader, cDir, jsonFiles)\n }\n switch (strategy) {\n case 'suffix': {\n // Files: {model}.{locale}.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n const locales: Record<string, number> = {}\n for (const f of jsonFiles) {\n const match = f.match(/^.+\\.([a-z]{2}(?:-[A-Z]{2})?)\\.json$/)\n if (match) {\n const data = await tryReadJsonViaReader<Record<string, unknown>>(reader, `${cDir}/${f}`)\n locales[match[1]!] = data ? Object.keys(data).length : 0\n }\n }\n const total = Object.values(locales).reduce((a, b) => a + b, 0)\n return { total, locales }\n }\n case 'directory': {\n // Dirs: {locale}/... containing json files\n const locales: Record<string, number> = {}\n let total = 0\n for (const localeDir of files) {\n const subFiles = await reader.listDirectory(`${cDir}/${localeDir}`)\n const jsonFile = subFiles.find(f => f.endsWith('.json'))\n if (jsonFile) {\n const data = await tryReadJsonViaReader<Record<string, unknown>>(reader, `${cDir}/${localeDir}/${jsonFile}`)\n const count = data ? Object.keys(data).length : 0\n locales[localeDir] = count\n total += count\n }\n }\n return { total, locales }\n }\n case 'none': {\n // Single file: {model}.json — count entries inside\n const noneFile = files.find(f => f === `${model.id}.json`)\n if (!noneFile) return { total: 0, locales: {} }\n const data = await tryReadJsonViaReader<Record<string, unknown>>(reader, `${cDir}/${noneFile}`)\n const count = data ? Object.keys(data).length : 0\n return { total: count, locales: { _: count } }\n }\n default: {\n // 'file': {locale}.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n return countCollectionEntries(reader, cDir, jsonFiles)\n }\n }\n }\n\n // singleton / dictionary — one entry per locale file\n if (!model.i18n) {\n // Non-i18n: single data.json\n const hasData = files.some(f => f === 'data.json')\n return { total: hasData ? 1 : 0, locales: {} }\n }\n\n switch (strategy) {\n case 'suffix': {\n // Files: {model}.{locale}.json\n const locales: Record<string, number> = {}\n for (const f of files) {\n const match = f.match(/^.+\\.([a-z]{2}(?:-[A-Z]{2})?)\\.json$/)\n if (match) locales[match[1]!] = 1\n }\n return { total: Object.keys(locales).length, locales }\n }\n case 'directory': {\n // Dirs: {locale}/...\n const locales: Record<string, number> = {}\n for (const localeDir of files) {\n const subFiles = await reader.listDirectory(`${cDir}/${localeDir}`)\n if (subFiles.some(f => f.endsWith('.json'))) {\n locales[localeDir] = 1\n }\n }\n return { total: Object.keys(locales).length, locales }\n }\n case 'none': {\n // Single file: {model}.json\n const hasFile = files.some(f => f === `${model.id}.json`)\n return { total: hasFile ? 1 : 0, locales: {} }\n }\n default: {\n // 'file': {locale}.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n const locales: Record<string, number> = {}\n for (const file of jsonFiles) {\n locales[file.replace(/\\.json$/, '')] = 1\n }\n return { total: jsonFiles.length, locales }\n }\n }\n}\n\nexport async function writeModel(projectRoot: string, model: ModelDefinition): Promise<void> {\n const filePath = join(contentrainDir(projectRoot), 'models', `${model.id}.json`)\n await writeJson(filePath, model, MODEL_FIELD_ORDER)\n\n // Ensure content and meta directories exist (respects content_path)\n await ensureDir(resolveContentDir(projectRoot, model))\n await ensureDir(join(contentrainDir(projectRoot), 'meta', model.id))\n}\n\nexport async function deleteModel(projectRoot: string, modelId: string): Promise<string[]> {\n const model = await readModel(projectRoot, modelId)\n if (!model) return []\n\n const crDir = contentrainDir(projectRoot)\n const removed: string[] = []\n\n const modelPath = join(crDir, 'models', `${modelId}.json`)\n const contentPath = resolveContentDir(projectRoot, model)\n const metaPath = join(crDir, 'meta', modelId)\n\n await rm(modelPath, { force: true })\n removed.push(`models/${modelId}.json`)\n\n try {\n await rm(contentPath, { recursive: true, force: true })\n removed.push(model.content_path ?? `content/${model.domain}/${modelId}/`)\n } catch {\n // directory may not exist\n }\n\n try {\n await rm(metaPath, { recursive: true, force: true })\n removed.push(`meta/${modelId}/`)\n } catch {\n // directory may not exist\n }\n\n return removed\n}\n\nexport interface ModelReference {\n model: string\n field: string\n type: 'relation' | 'relations'\n}\n\n/**\n * Enumerate models that reference `modelId` through relation or relations\n * fields. Used by `model_delete` to block destructive deletes when other\n * models still depend on the target.\n *\n * Accepts either a legacy `projectRoot` string (LocalReader wraps internally)\n * or any `RepoReader` — remote providers get the same pre-check.\n */\nexport function checkReferences(projectRoot: string, modelId: string): Promise<ModelReference[]>\nexport function checkReferences(reader: RepoReader, modelId: string): Promise<ModelReference[]>\nexport async function checkReferences(\n input: string | RepoReader,\n modelId: string,\n): Promise<ModelReference[]> {\n const summaries = typeof input === 'string' ? await listModels(input) : await listModels(input)\n const others = summaries.filter(s => s.id !== modelId)\n\n const models = await Promise.all(\n others.map(s => typeof input === 'string' ? readModel(input, s.id) : readModel(input, s.id)),\n )\n\n const refs: ModelReference[] = []\n for (const model of models) {\n if (!model?.fields) continue\n\n for (const [fieldName, fieldDef] of Object.entries(model.fields)) {\n if (fieldDef.type !== 'relation' && fieldDef.type !== 'relations') continue\n const targets = Array.isArray(fieldDef.model) ? fieldDef.model : [fieldDef.model]\n if (targets.includes(modelId)) {\n refs.push({ model: model.id, field: fieldName, type: fieldDef.type })\n }\n }\n }\n\n return refs\n}\n\n// ─── Shared Field Type Catalog ───\n\nexport const FIELD_TYPE_ENUM = [\n 'string', 'text', 'email', 'url', 'slug', 'color', 'phone', 'code', 'icon',\n 'markdown', 'richtext',\n 'number', 'integer', 'decimal', 'percent', 'rating',\n 'boolean', 'date', 'datetime',\n 'image', 'video', 'file',\n 'relation', 'relations',\n 'select', 'array', 'object',\n] as const\n\n/**\n * Shared Zod schema for field definitions.\n * Used by both model_save and normalize extract for full parity.\n *\n * `.strict()` is load-bearing: the default `z.object` *strips* unknown keys, so a\n * typo'd constraint (`requird: true`) used to vanish without a word and the field\n * silently lost the rule its author thought they had declared.\n */\nexport const fieldDefZodSchema: z.ZodType<Record<string, unknown>> = z.record(z.string(), z.object({\n type: z.enum(FIELD_TYPE_ENUM).describe('Field type from the 27-type catalog'),\n required: z.boolean().optional(),\n unique: z.boolean().optional(),\n default: z.unknown().optional(),\n min: z.number().optional(),\n max: z.number().optional(),\n pattern: z.string().optional(),\n options: z.array(z.string()).optional(),\n model: z.union([z.string(), z.array(z.string())]).optional(),\n items: z.union([z.string(), z.lazy(() => z.record(z.string(), z.unknown()))]).optional(),\n fields: z.lazy(() => z.record(z.string(), z.unknown())).optional(),\n accept: z.string().optional(),\n maxSize: z.number().optional(),\n description: z.string().optional(),\n label: z.union([z.string(), z.record(z.string(), z.string())]).optional()\n .describe('Editor label. A string is one label for all locales; an object keyed by locale carries a translation each.'),\n order: z.number().optional()\n .describe('Display position, ascending. Fields without one sort last, alphabetically. Fractional values allowed.'),\n}).strict().refine(\n (f) => {\n if ((f.type === 'relation' || f.type === 'relations') && !f.model) return false\n if (f.type === 'select' && (!f.options || f.options.length === 0)) return false\n return true\n },\n { message: 'relation/relations requires \"model\", select requires non-empty \"options\"' },\n))\n\n// ─── Model Validation (shared between model_save and normalize extract) ───\n\nconst VALID_FIELD_TYPES = new Set<string>(FIELD_TYPE_ENUM)\n\n/** Field types whose value is a path/URL to a media asset. */\nconst MEDIA_FIELD_TYPES = new Set<string>(['image', 'video', 'file'])\n/** Bounds `items`/`fields` nesting; far above any real schema. */\nconst MAX_SCHEMA_DEPTH = 10\n/** Field names are snake_case: a letter, then letters, digits and underscores. */\nconst FIELD_NAME_PATTERN = /^[a-z][a-z0-9_]*$/\n\nexport interface ModelDefinitionIssues {\n /** Block the write. */\n errors: string[]\n /** Surface to the caller; the write proceeds. */\n warnings: string[]\n}\n\nexport interface ModelDefinitionOptions {\n /**\n * Field paths already declared by the model being updated — `title`,\n * `seo.title`, `tags.items.label` — as {@link collectFieldPaths} spells them.\n *\n * A name in this set that is not snake_case is grandfathered. It was accepted\n * before the rule existed, and refusing it now would make the whole model\n * read-only through MCP: no edit could be saved, not even one that never\n * touches the field. A one-line `title_field` correction on a model with one\n * legacy `creativeWork` relation should not require renaming the field, its\n * content keys in every locale, and every consuming component first.\n *\n * Only *new* fields must be snake_case. The legacy name is reported as a\n * warning so the caller knows what was tolerated.\n */\n existingFieldPaths?: ReadonlySet<string>\n}\n\n/**\n * Every field path a schema declares, nested paths dotted the way\n * `validateModelDefinition` reports them: `seo.title` for an object's member,\n * `tags.items.label` for a member of an array's item object.\n */\nexport function collectFieldPaths(fields: Record<string, unknown> | undefined, prefix = ''): Set<string> {\n const out = new Set<string>()\n if (!fields) return out\n for (const [name, raw] of Object.entries(fields)) {\n const path = `${prefix}${name}`\n out.add(path)\n const def = raw as RawFieldDef | null\n if (typeof def !== 'object' || def === null) continue\n if (typeof def.fields === 'object' && def.fields !== null) {\n for (const nested of collectFieldPaths(def.fields as Record<string, unknown>, `${path}.`)) out.add(nested)\n }\n const items = def.items as RawFieldDef | string | undefined\n if (typeof items === 'object' && items !== null && typeof items.fields === 'object' && items.fields !== null) {\n for (const nested of collectFieldPaths(items.fields as Record<string, unknown>, `${path}.items.`)) out.add(nested)\n }\n }\n return out\n}\n\n/**\n * Declared field paths whose name predates the snake_case rule.\n *\n * The project validator reports these so an agent learns a model carries\n * legacy names *before* it edits the model, rather than discovering them\n * mid-operation. `model_save` tolerates them; see {@link ModelDefinitionOptions}.\n */\nexport function legacyFieldNames(fields: Record<string, unknown> | undefined): string[] {\n return [...collectFieldPaths(fields)].filter(path => !FIELD_NAME_PATTERN.test(path.slice(path.lastIndexOf('.') + 1)))\n}\n\n/**\n * A field's name is snake_case, or it is a legacy name the model already has.\n * Anything else is an error. See {@link ModelDefinitionOptions.existingFieldPaths}.\n */\nfunction checkFieldName(\n path: string,\n opts: ModelDefinitionOptions,\n errors: string[],\n warnings: string[],\n): void {\n const name = path.slice(path.lastIndexOf('.') + 1)\n if (FIELD_NAME_PATTERN.test(name)) return\n if (opts.existingFieldPaths?.has(path)) {\n warnings.push(\n `Field \"${path}\": legacy name is not snake_case — kept because the model already has it. `\n + 'New fields must be snake_case. Renaming it also means renaming its content keys in every locale.',\n )\n return\n }\n errors.push(`Field \"${path}\": invalid name — must be snake_case starting with letter`)\n}\n\ninterface RawFieldDef {\n type?: string\n required?: unknown\n unique?: unknown\n default?: unknown\n min?: unknown\n max?: unknown\n pattern?: unknown\n options?: unknown\n model?: unknown\n items?: unknown\n fields?: unknown\n accept?: unknown\n maxSize?: unknown\n label?: unknown\n order?: unknown\n}\n\n/**\n * Check one field definition, recursing into `fields` and `items`.\n *\n * The governing rule: **do not accept a constraint that will not be enforced.**\n * A constraint that silently does nothing is worse than no constraint, because\n * the author stops looking. So a property declared where it cannot apply is an\n * error, and a property we genuinely cannot enforce says so out loud.\n */\nfunction checkFieldDef(\n raw: unknown,\n path: string,\n modelKind: string,\n errors: string[],\n warnings: string[],\n depth: number,\n opts: ModelDefinitionOptions,\n): void {\n if (typeof raw !== 'object' || raw === null) {\n errors.push(`Field \"${path}\": must be an object`)\n return\n }\n if (depth > MAX_SCHEMA_DEPTH) {\n errors.push(`Field \"${path}\": exceeds the maximum nesting depth of ${MAX_SCHEMA_DEPTH}`)\n return\n }\n\n const def = raw as RawFieldDef\n const type = def.type\n\n if (!type || !VALID_FIELD_TYPES.has(type)) {\n errors.push(`Field \"${path}\": invalid type \"${type}\"`)\n return\n }\n\n // Presentation. Wrong here is not fatal to a write, but a locale-keyed label\n // whose keys are not locales is the same inversion the vocabulary hits, and\n // it silently resolves to nothing.\n if (def.label !== undefined) {\n if (typeof def.label === 'string') {\n if (def.label.trim() === '') errors.push(`Field \"${path}\": \"label\" is empty`)\n } else if (typeof def.label === 'object' && def.label !== null && !Array.isArray(def.label)) {\n const entries = Object.entries(def.label as Record<string, unknown>)\n if (entries.length === 0) errors.push(`Field \"${path}\": \"label\" has no translations`)\n for (const [locale, value] of entries) {\n if (!LOCALE_PATTERN.test(locale)) {\n errors.push(`Field \"${path}\": \"label\" key \"${locale}\" is not a locale code. A per-locale label nests as { \"en\": \"…\", \"tr\": \"…\" }.`)\n }\n if (typeof value !== 'string' || value.trim() === '') {\n errors.push(`Field \"${path}\": \"label\" for \"${locale}\" must be a non-empty string`)\n }\n }\n } else {\n errors.push(`Field \"${path}\": \"label\" must be a string, or an object keyed by locale`)\n }\n }\n if (def.order !== undefined && (typeof def.order !== 'number' || !Number.isFinite(def.order))) {\n errors.push(`Field \"${path}\": \"order\" must be a finite number`)\n }\n\n if ((type === 'relation' || type === 'relations') && !def.model) {\n errors.push(`Field \"${path}\": ${type} type requires \"model\" property`)\n }\n if (type === 'select' && (!Array.isArray(def.options) || def.options.length === 0)) {\n errors.push(`Field \"${path}\": select type requires non-empty \"options\" array`)\n }\n\n // An object with no shape is a field nothing can validate and no editor can\n // render — it shows as an empty frame. A warning, not an error: it is a\n // legitimate intermediate state while a schema is being designed.\n if (type === 'object' && (!def.fields || typeof def.fields !== 'object' || Object.keys(def.fields as Record<string, unknown>).length === 0)) {\n warnings.push(`Field \"${path}\": \"object\" with no \"fields\" — nothing validates it and no editor can render it. Declare its shape, or use a plain type.`)\n }\n // The reverse direction was never checked, so `options` on a string field was\n // accepted and then silently ignored at validation time.\n if (def.options !== undefined && type !== 'select') {\n errors.push(`Field \"${path}\": \"options\" only applies to select fields — it is ignored on \"${type}\"`)\n }\n if (def.items !== undefined && type !== 'array') {\n errors.push(`Field \"${path}\": \"items\" only applies to array fields — it is ignored on \"${type}\"`)\n }\n if (def.fields !== undefined && type !== 'object') {\n errors.push(`Field \"${path}\": \"fields\" only applies to object fields — it is ignored on \"${type}\"`)\n }\n if (def.accept !== undefined && !MEDIA_FIELD_TYPES.has(type)) {\n errors.push(`Field \"${path}\": \"accept\" only applies to image/video/file fields — it is ignored on \"${type}\"`)\n }\n if (def.maxSize !== undefined && !MEDIA_FIELD_TYPES.has(type)) {\n errors.push(`Field \"${path}\": \"maxSize\" only applies to image/video/file fields — it is ignored on \"${type}\"`)\n }\n\n // A singleton holds one record per locale, so there is nothing for a value to\n // be unique against.\n if (def.unique === true && modelKind === 'singleton') {\n errors.push(`Field \"${path}\": \"unique\" has no meaning on a singleton — the model holds a single record per locale`)\n }\n\n if (typeof def.min === 'number' && typeof def.max === 'number' && def.min > def.max) {\n errors.push(`Field \"${path}\": min (${def.min}) is greater than max (${def.max})`)\n }\n\n // Compile the regex here rather than let it fail once per entry at validation\n // time, where it degrades to a warning and silently disables the constraint.\n if (typeof def.pattern === 'string') {\n try {\n // eslint-disable-next-line no-new\n new RegExp(def.pattern)\n } catch {\n errors.push(`Field \"${path}\": \"pattern\" is not a valid regular expression — /${def.pattern}/`)\n }\n }\n\n if (def.default !== undefined) {\n checkDefaultCoherence(def, type, path, errors)\n }\n\n // `max` on a media field measures the length of the stored path string, not\n // the file — almost certainly not what the author meant.\n if (typeof def.max === 'number' && MEDIA_FIELD_TYPES.has(type)) {\n warnings.push(\n `Field \"${path}\": \"max\" on a ${type} field limits the length of the stored path string, not the file size. Use \"maxSize\" for bytes.`,\n )\n }\n\n // Said plainly rather than accepted in silence: MCP holds a path, never the\n // bytes, so it cannot check this. The provider owns the policy at ingest.\n if (def.maxSize !== undefined && MEDIA_FIELD_TYPES.has(type)) {\n warnings.push(\n `Field \"${path}\": \"maxSize\" is not enforced by MCP — it has no access to the file. Your media provider enforces it when the asset is ingested.`,\n )\n }\n\n if (def.fields !== undefined && typeof def.fields === 'object' && def.fields !== null) {\n for (const [nested, nestedDef] of Object.entries(def.fields as Record<string, unknown>)) {\n checkFieldName(`${path}.${nested}`, opts, errors, warnings)\n checkFieldDef(nestedDef, `${path}.${nested}`, modelKind, errors, warnings, depth + 1, opts)\n }\n }\n\n if (typeof def.items === 'string') {\n if (!VALID_FIELD_TYPES.has(def.items)) {\n errors.push(`Field \"${path}.items\": invalid type \"${def.items}\"`)\n }\n } else if (def.items !== undefined) {\n checkFieldDef(def.items, `${path}.items`, modelKind, errors, warnings, depth + 1, opts)\n }\n}\n\n/** A default that its own field would reject is a schema bug, not a content one. */\nfunction checkDefaultCoherence(def: RawFieldDef, type: string, path: string, errors: string[]): void {\n const value = def.default\n const isString = typeof value === 'string'\n const isNumber = typeof value === 'number'\n\n if (type === 'select' && Array.isArray(def.options) && isString && !def.options.includes(value)) {\n errors.push(`Field \"${path}\": default \"${value}\" is not one of its own options [${(def.options as string[]).join(', ')}]`)\n return\n }\n const wantsNumber = ['number', 'integer', 'decimal', 'percent', 'rating'].includes(type)\n const wantsBoolean = type === 'boolean'\n const wantsArray = type === 'array'\n\n if (wantsNumber && !isNumber) {\n errors.push(`Field \"${path}\": default must be a number for type \"${type}\"`)\n } else if (wantsBoolean && typeof value !== 'boolean') {\n errors.push(`Field \"${path}\": default must be a boolean for type \"${type}\"`)\n } else if (wantsArray && !Array.isArray(value)) {\n errors.push(`Field \"${path}\": default must be an array for type \"${type}\"`)\n }\n}\n\n// ─── title_field ───\n\n/** Field names that announce themselves as a title, most explicit first. */\nconst TITLE_NAME_HINTS = ['title', 'name', 'label', 'heading'] as const\n\n/**\n * Preference among displayable types, short scalars first. A title is a label,\n * not a body: given both `question: string` and `answer: text`, the string wins.\n * Without this, \"first required text field\" resolves alphabetically and a FAQ\n * model gets titled by its answers. The three at the end only ever break a tie\n * on the name-match rung — see {@link NOT_INFERRED_AS_TITLE}.\n */\nconst TITLE_TYPE_PRIORITY: readonly FieldType[] = [\n 'string', 'email', 'text', 'markdown', 'richtext', 'slug', 'url', 'code',\n]\n\n/**\n * Legal `title_field` types that inference never falls back to.\n *\n * An author may deliberately title a model by its slug, URL or code snippet, and\n * `title_field` accepts that. *Guessed*, the same choice is the failure this\n * contract exists to prevent: a settings singleton whose only displayable field\n * is `whatsapp_url` was backfilled with it, and every Studio row read as a\n * WhatsApp link. Studio's runtime resolver (`entry-title.ts`) skips the same\n * three types on its fallback rung, so the writer here and the reader there\n * agree on what a fallback title is — before, the CLI wrote exactly what Studio\n * is written to avoid.\n */\nconst NOT_INFERRED_AS_TITLE: ReadonlySet<FieldType> = new Set<FieldType>(['slug', 'url', 'code'])\n\nexport interface TitleFieldPick {\n field: string\n /** Which rule chose it — surfaced to the user so an inferred title can be overruled. */\n rule: 'dictionary' | 'name-match' | 'required-displayable' | 'displayable'\n}\n\ninterface TitleCandidate {\n field: string\n type: FieldType\n required: boolean\n}\n\n/** Fields that could carry a title at all. Everything else is out of the running. */\nfunction titleCandidates(fields?: Record<string, unknown>): TitleCandidate[] {\n if (!fields) return []\n const out: TitleCandidate[] = []\n for (const [field, raw] of Object.entries(fields)) {\n const def = raw as { type?: unknown; required?: unknown } | null\n const type = def?.type\n if (typeof type !== 'string' || !isTitleFieldType(type as FieldType)) continue\n out.push({ field, type: type as FieldType, required: def?.required === true })\n }\n return out\n}\n\nfunction byTypeThenName(a: TitleCandidate, b: TitleCandidate): number {\n const byType = TITLE_TYPE_PRIORITY.indexOf(a.type) - TITLE_TYPE_PRIORITY.indexOf(b.type)\n return byType !== 0 ? byType : a.field.localeCompare(b.field, 'en')\n}\n\n/**\n * Required first. A required field always renders; an optional one may be empty,\n * and an entry titled by an empty field has no title. `authors` with an optional\n * `title` (the job title) and a required `name` is titled by `name`.\n */\nfunction byRequired(a: TitleCandidate, b: TitleCandidate): number {\n return Number(b.required) - Number(a.required)\n}\n\n/**\n * Fields a model may legally declare as its `title_field`.\n *\n * The picker's list, and the hint when {@link inferTitleField} declines to\n * guess: \"no field reads as a title\" is more useful with the legal choices\n * spelled out next to it.\n */\nexport function titleFieldOptions(kind: string, fields?: Record<string, unknown>): string[] {\n if (kind === 'dictionary') return [DICTIONARY_TITLE_FIELD]\n const top = titleCandidates(fields).map(c => c.field)\n // One level into object fields: a page singleton built from sections has no\n // top-level text, and `hero.heading` is its legal title. Never inferred.\n const nested = Object.entries(fields ?? {}).flatMap(([name, raw]) => {\n const def = raw as { type?: unknown; fields?: Record<string, unknown> } | null\n return def?.type === 'object' ? titleCandidates(def.fields).map(c => `${name}.${c.field}`) : []\n })\n return [...top, ...nested]\n}\n\n/**\n * How strongly a field name reads as a title. Exact matches outrank snake_case\n * token matches, so `name` beats `brand_name`, and `filename` matches nothing —\n * it tokenizes to `[\"filename\"]`, not `[\"file\", \"name\"]`.\n */\nfunction titleNameRank(field: string): number {\n const exact = (TITLE_NAME_HINTS as readonly string[]).indexOf(field)\n if (exact !== -1) return exact\n\n const tokens = field.split('_')\n const token = TITLE_NAME_HINTS.findIndex(hint => tokens.includes(hint))\n return token === -1 ? Number.POSITIVE_INFINITY : TITLE_NAME_HINTS.length + token\n}\n\n/**\n * Pick the field a model should title its entries by.\n *\n * Migration aid only — it exists to backfill models authored before `title_field`\n * was required, and `contentrain validate --fix` is its one caller. New models\n * declare the field; nothing here runs on the write path.\n *\n * Every rung filters by `isTitleFieldType` first. That filter is the whole point:\n * an unfiltered name match hands the title to a field called `name` that happens\n * to be typed `icon`, which is the exact bug this contract replaces.\n *\n * The rungs, in order — the same chain Studio's `resolveTitleFieldId` walks, with\n * requiredness as the tiebreak on each:\n *\n * 1. a required name-like field (`title`, `name`, `label`, `heading`)\n * 2. an optional name-like field\n * 3. a required prose field (string, email, text, markdown, richtext)\n * 4. any prose field\n *\n * A slug, URL or code field is never reached by inference — see\n * {@link NOT_INFERRED_AS_TITLE} — except as a tiebreak among name-like fields,\n * where the name has already made the case.\n *\n * Returns `null` when nothing qualifies. That is a real answer, not a failure —\n * guessing `image`, `relation` or `whatsapp_url` here would re-create the bug,\n * so the validation error stands and a human picks.\n */\nexport function inferTitleField(kind: string, fields?: Record<string, unknown>): TitleFieldPick | null {\n // A dictionary has no fields to point at — its key is the title.\n if (kind === 'dictionary') return { field: DICTIONARY_TITLE_FIELD, rule: 'dictionary' }\n\n const candidates = titleCandidates(fields)\n if (candidates.length === 0) return null\n\n const named = candidates.filter(c => Number.isFinite(titleNameRank(c.field)))\n if (named.length > 0) {\n const best = named.toSorted((a, b) =>\n byRequired(a, b)\n || titleNameRank(a.field) - titleNameRank(b.field)\n || byTypeThenName(a, b),\n )[0]!\n return { field: best.field, rule: 'name-match' }\n }\n\n const prose = candidates.filter(c => !NOT_INFERRED_AS_TITLE.has(c.type))\n const required = prose.filter(c => c.required)\n if (required.length > 0) {\n return { field: required.toSorted(byTypeThenName)[0]!.field, rule: 'required-displayable' }\n }\n if (prose.length > 0) {\n return { field: prose.toSorted(byTypeThenName)[0]!.field, rule: 'displayable' }\n }\n\n // Only slug/url/code fields are left. Each is a legal title_field, but naming\n // one is the author's decision — the caller reports the options instead.\n return null\n}\n\n/**\n * The `title_field` rule table.\n *\n * Extracted so `validateModelDefinition` (write path) and the project validator\n * (read path) report the same wording from one place, rather than one of them\n * sniffing the other's strings.\n *\n * Chained so a model gets one title_field error, not a cascade of five.\n */\nexport function titleFieldIssues(\n input: { kind: string; title_field?: unknown; fields?: Record<string, unknown> },\n): ModelDefinitionIssues {\n const errors: string[] = []\n const warnings: string[] = []\n const titleField = input.title_field\n\n if (titleField === undefined || titleField === null || titleField === '') {\n errors.push('Missing \"title_field\". Every model must name the field shown as its title. Dictionary models use \"key\".')\n } else if (typeof titleField !== 'string') {\n errors.push(`Invalid \"title_field\": must be a string, got ${typeof titleField}.`)\n } else if (input.kind === 'dictionary') {\n if (titleField !== DICTIONARY_TITLE_FIELD) {\n errors.push(`Dictionary models must use title_field: \"${DICTIONARY_TITLE_FIELD}\". Dictionaries have no fields — the key is the title. Got \"${titleField}\".`)\n }\n } else if (!input.fields || Object.keys(input.fields).length === 0) {\n errors.push(`Invalid \"title_field\": \"${titleField}\" cannot resolve — the model declares no fields.`)\n } else {\n // A dotted path (`hero.heading`) reaches one level into an object field — a\n // page singleton built from sections is titled by a section's heading.\n const target = titleFieldTarget(input.fields as Record<string, FieldDef>, titleField)\n if (target.kind === 'too-deep') {\n errors.push(`Invalid \"title_field\": \"${titleField}\" reaches more than one level deep. A title is a field, or a field of an object field (\"hero.heading\").`)\n } else if (target.kind === 'not-object') {\n errors.push(`Invalid \"title_field\": \"${titleField}\" goes through \"${target.at}\", which has type \"${target.type}\" — a dotted title reaches into an object field only.`)\n } else if (target.kind === 'missing') {\n // A non-dictionary model may legitimately own a field named \"key\" — the\n // sentinel only collides where there are no fields at all.\n errors.push(\n `Invalid \"title_field\": field \"${titleField}\" is not defined in fields.`\n + (titleField === DICTIONARY_TITLE_FIELD ? ` \"${DICTIONARY_TITLE_FIELD}\" is reserved for dictionary models.` : ''),\n )\n } else {\n const { def, parent } = target\n const type: unknown = def.type\n if (typeof type === 'string' && !isTitleFieldType(type as FieldType)) {\n errors.push(`Invalid \"title_field\": field \"${titleField}\" has type \"${type}\" — a title must be one of: ${TITLE_FIELD_TYPES.join(', ')}.`)\n } else if (def.required !== true || (parent && parent.required !== true)) {\n warnings.push(`\"title_field\" points at optional field \"${titleField}\" — entries may render with an empty title. Consider required: true${parent ? ' on the object and its field' : ''}.`)\n }\n // A non-string `type` falls through: the per-field loop already reports it,\n // and repeating it here would double-count one mistake.\n }\n }\n\n return { errors, warnings }\n}\n\n/**\n * Validate a model definition before writing.\n * Used by both the model_save tool and normalize extract.\n */\nexport function validateModelDefinition(\n // `title_field` is `unknown`, not `string?`: a model read back off disk is an\n // unvalidated cast, so the runtime check has to see a value the type system\n // already believes is a string.\n input: { id: string; kind: string; title_field?: unknown; fields?: Record<string, unknown> },\n opts: ModelDefinitionOptions = {},\n): ModelDefinitionIssues {\n const errors: string[] = []\n const warnings: string[] = []\n\n // ID format: kebab-case\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.id)) {\n errors.push(`Invalid model ID \"${input.id}\": must be kebab-case`)\n }\n\n // Dictionary models cannot have fields\n if (input.kind === 'dictionary' && input.fields && Object.keys(input.fields).length > 0) {\n errors.push('Dictionary models cannot have fields. Dictionaries store flat key-value pairs.')\n }\n\n // Reported above the per-field loop: a broken title_field explains the model,\n // the field errors only explain one field.\n const titleIssues = titleFieldIssues(input)\n errors.push(...titleIssues.errors)\n warnings.push(...titleIssues.warnings)\n\n if (input.fields) {\n for (const [fieldName, fieldDef] of Object.entries(input.fields)) {\n checkFieldName(fieldName, opts, errors, warnings)\n checkFieldDef(fieldDef, fieldName, input.kind, errors, warnings, 0, opts)\n }\n }\n\n return { errors, warnings }\n}\n"],"mappings":"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"}
|