@contentrain/mcp 3.6.1 → 3.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/{apply-manager-eZ6zA99Y.mjs → apply-manager-llmSyK5K.mjs} +7 -7
- package/dist/{apply-manager-eZ6zA99Y.mjs.map → apply-manager-llmSyK5K.mjs.map} +1 -1
- package/dist/branch-lifecycle-DGlgjnC7.d.mts.map +1 -1
- package/dist/{branch-lifecycle-DUOoyNWn.mjs → branch-lifecycle-f4gI_eGo.mjs} +71 -5
- package/dist/branch-lifecycle-f4gI_eGo.mjs.map +1 -0
- package/dist/{config-B8a4JBT1.mjs → config-BLwZtwlI.mjs} +2 -2
- package/dist/{config-B8a4JBT1.mjs.map → config-BLwZtwlI.mjs.map} +1 -1
- package/dist/{context-BHfO4WBv.mjs → context-fJCKH_qv.mjs} +6 -6
- package/dist/{context-BHfO4WBv.mjs.map → context-fJCKH_qv.mjs.map} +1 -1
- package/dist/core/apply-manager.mjs +8 -8
- package/dist/core/config.mjs +2 -2
- package/dist/core/content-manager.mjs +3 -3
- package/dist/core/context.mjs +5 -5
- package/dist/core/doctor.mjs +7 -7
- package/dist/core/graph-builder.mjs +3 -3
- package/dist/core/meta-manager.mjs +2 -2
- package/dist/core/model-manager.mjs +3 -3
- package/dist/core/ops/index.mjs +8 -8
- package/dist/core/scan-config.mjs +2 -2
- package/dist/core/scanner.mjs +3 -3
- package/dist/core/validator/index.mjs +5 -5
- package/dist/{detect-CI32ynpq.mjs → detect-DUYFv3-O.mjs} +2 -2
- package/dist/{detect-CI32ynpq.mjs.map → detect-DUYFv3-O.mjs.map} +1 -1
- package/dist/{doctor-VnOzDTbk.mjs → doctor-fIG4TtsO.mjs} +6 -6
- package/dist/{doctor-VnOzDTbk.mjs.map → doctor-fIG4TtsO.mjs.map} +1 -1
- package/dist/{fs-DJv5G8uL.mjs → fs-BFTuXbbw.mjs} +2 -2
- package/dist/{fs-DJv5G8uL.mjs.map → fs-BFTuXbbw.mjs.map} +1 -1
- package/dist/git/branch-lifecycle.mjs +3 -3
- package/dist/git/reconcile.d.mts.map +1 -1
- package/dist/git/reconcile.mjs +10 -10
- package/dist/git/transaction.d.mts +4 -0
- package/dist/git/transaction.d.mts.map +1 -1
- package/dist/git/transaction.mjs +7 -7
- package/dist/{graph-builder-C9NS7y-a.mjs → graph-builder-CNswWNtb.mjs} +3 -3
- package/dist/{graph-builder-C9NS7y-a.mjs.map → graph-builder-CNswWNtb.mjs.map} +1 -1
- package/dist/index.mjs +19 -19
- package/dist/{local-DgjDad1x.mjs → local-DkDTrGt_.mjs} +140 -13
- package/dist/local-DkDTrGt_.mjs.map +1 -0
- package/dist/{meta-manager-Cd6AfuGS.mjs → meta-manager-CSpKtjnP.mjs} +2 -2
- package/dist/{meta-manager-Cd6AfuGS.mjs.map → meta-manager-CSpKtjnP.mjs.map} +1 -1
- package/dist/{model-manager-CfwJWvz7.mjs → model-manager-DPXulg7z.mjs} +4 -4
- package/dist/{model-manager-CfwJWvz7.mjs.map → model-manager-DPXulg7z.mjs.map} +1 -1
- package/dist/{ops-PfMNR0nx.mjs → ops-DxhCCs_Z.mjs} +4 -4
- package/dist/{ops-PfMNR0nx.mjs.map → ops-DxhCCs_Z.mjs.map} +1 -1
- package/dist/providers/local/index.d.mts.map +1 -1
- package/dist/providers/local/index.mjs +11 -11
- package/dist/{reconcile-B55dZqhg.mjs → reconcile-CPDGFQ8H.mjs} +5 -5
- package/dist/{reconcile-B55dZqhg.mjs.map → reconcile-CPDGFQ8H.mjs.map} +1 -1
- package/dist/{reconcile-CwS4LUbo.mjs → reconcile-Cq60cldB.mjs} +11 -7
- package/dist/reconcile-Cq60cldB.mjs.map +1 -0
- package/dist/{scan-config-C5zzygeH.mjs → scan-config-DkRKu_ON.mjs} +2 -2
- package/dist/{scan-config-C5zzygeH.mjs.map → scan-config-DkRKu_ON.mjs.map} +1 -1
- package/dist/{scanner-CEiT8Tgx.mjs → scanner-BLo3ssiF.mjs} +3 -3
- package/dist/{scanner-CEiT8Tgx.mjs.map → scanner-BLo3ssiF.mjs.map} +1 -1
- package/dist/server/http/index.mjs +19 -19
- package/dist/{server-BRTztDnc.mjs → server-BRXP35b8.mjs} +21 -21
- package/dist/server-BRXP35b8.mjs.map +1 -0
- package/dist/server.mjs +19 -19
- package/dist/{transaction-BR2jg4uj.mjs → transaction-BWJt1KQb.mjs} +77 -39
- package/dist/transaction-BWJt1KQb.mjs.map +1 -0
- package/dist/util/detect.mjs +2 -2
- package/dist/util/fs.mjs +1 -1
- package/dist/{validator-BD0Z0O0_.mjs → validator-C08p1OpM.mjs} +6 -6
- package/dist/{validator-BD0Z0O0_.mjs.map → validator-C08p1OpM.mjs.map} +1 -1
- package/package.json +2 -2
- package/dist/branch-lifecycle-DUOoyNWn.mjs.map +0 -1
- package/dist/local-DgjDad1x.mjs.map +0 -1
- package/dist/reconcile-CwS4LUbo.mjs.map +0 -1
- package/dist/server-BRTztDnc.mjs.map +0 -1
- package/dist/transaction-BR2jg4uj.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ops-PfMNR0nx.mjs","names":["canonicalStringify","parseMarkdownFrontmatter","serializeMarkdownFrontmatter","canonicalStringify","canonicalStringify"],"sources":["../src/core/ops/content-delete.ts","../src/core/ops/content-save.ts","../src/core/ops/model-delete.ts","../src/core/ops/model-save.ts","../src/core/bind-ref.ts","../src/core/ops/vocabulary-save.ts"],"sourcesContent":["import type { EntryMeta, ModelDefinition } from '@contentrain/types'\nimport { validateSlug } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport { canonicalStringify } from '../serialization/index.js'\nimport { contentFilePath, documentFilePath, metaFilePath } from './paths.js'\nimport type { OpPlan } from './types.js'\n\nexport interface ContentDeleteInput {\n model: ModelDefinition\n id?: string\n slug?: string\n locale?: string\n keys?: string[]\n /**\n * `config.locales.default` — where a non-i18n model's single meta record\n * lives. Required to resolve meta paths; see `metaFilePath`.\n */\n defaultLocale: string\n}\n\nexport type ContentDeletePlan = OpPlan<string[]>\n\nfunction contentDir(model: Pick<ModelDefinition, 'domain' | 'id' | 'content_path'>): string {\n return model.content_path ?? `.contentrain/content/${model.domain}/${model.id}`\n}\n\nasync function tryReadJson<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 discoverCollectionLocales(\n reader: RepoReader,\n model: ModelDefinition,\n): Promise<string[]> {\n const dir = contentDir(model)\n const files = await reader.listDirectory(dir)\n const strategy = model.locale_strategy ?? 'file'\n if (strategy === 'directory') {\n return files\n }\n return files\n .filter(f => f.endsWith('.json'))\n .map(f => f.replace('.json', '').replace(`${model.id}.`, ''))\n}\n\n/**\n * Build the FileChange[] required to delete content. Pure — touches no disk.\n *\n * Semantics preserved from legacy `deleteContent`:\n * - Collection: remove the entry ID from each locale's content file and its\n * meta file. Meta file is deleted outright if the removal empties it.\n * - Singleton: delete the locale's content + meta file (locale-scoped when\n * i18n, non-i18n removes whatever meta lives under `.contentrain/meta/{id}/`).\n * - Dictionary: two modes — key-scoped removal updates the file in place,\n * or locale-scoped removes the content + meta pair.\n * - Document: strategy-aware delete (file/suffix/directory/none), plus\n * meta removal under `.contentrain/meta/{id}/{slug}/`.\n */\nexport async function planContentDelete(\n reader: RepoReader,\n input: ContentDeleteInput,\n): Promise<ContentDeletePlan> {\n const { model } = input\n\n // A non-i18n model stores content locale-agnostically in data.json and keeps\n // exactly one meta record, pinned to the default locale. A locale-scoped\n // delete is therefore meaningless — and actively destructive: the locale\n // silently maps onto data.json and the default-locale meta, so passing a\n // non-default locale deletes the shared content and the wrong meta while the\n // named locale's stray meta is never touched. Reject it; stray per-locale\n // meta is cleaned up by `contentrain_validate fix:true`.\n if (!model.i18n && input.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 changes: FileChange[] = []\n const removed: string[] = []\n\n switch (model.kind) {\n case 'collection': {\n if (!input.id) throw new Error('Collection delete requires an entry ID')\n\n const locales = input.locale\n ? [input.locale]\n : await discoverCollectionLocales(reader, model)\n\n for (const loc of locales) {\n const cPath = contentFilePath(model, loc)\n const data = await tryReadJson<Record<string, unknown>>(reader, cPath)\n if (!data || !(input.id in data)) continue\n delete data[input.id]\n const sorted: Record<string, unknown> = {}\n for (const k of Object.keys(data).toSorted()) sorted[k] = data[k]\n changes.push({ path: cPath, content: canonicalStringify(sorted) })\n removed.push(`content/${model.domain}/${model.id}/${loc}.json#${input.id}`)\n }\n\n for (const loc of locales) {\n // For a non-i18n model `loc` is the pseudo-locale \"data\" (discovered from\n // data.json); metaFilePath maps it onto the default locale regardless.\n const mPath = metaFilePath(model, loc, input.defaultLocale)\n const meta = await tryReadJson<Record<string, EntryMeta>>(reader, mPath)\n if (!meta || !(input.id in meta)) continue\n delete meta[input.id]\n if (Object.keys(meta).length > 0) {\n changes.push({ path: mPath, content: canonicalStringify(meta) })\n } else {\n changes.push({ path: mPath, content: null })\n }\n }\n break\n }\n\n case 'singleton': {\n if (model.i18n && !input.locale) {\n throw new Error('Singleton delete requires a locale when i18n is enabled')\n }\n const locale = input.locale ?? 'data'\n const cPath = contentFilePath(model, locale)\n changes.push({ path: cPath, content: null })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n\n if (model.i18n) {\n changes.push({ path: metaFilePath(model, locale, input.defaultLocale), content: null })\n } else {\n // Removing data.json leaves a non-i18n model with no content at all, so\n // every meta file under it goes — including strays left by older writes.\n await pushAllMetaForModel(reader, model.id, changes)\n }\n break\n }\n\n case 'dictionary': {\n if (model.i18n && !input.locale) {\n throw new Error('Dictionary delete requires a locale when i18n is enabled')\n }\n const locale = input.locale ?? 'data'\n const cPath = contentFilePath(model, locale)\n\n if (input.keys?.length) {\n const existing = (await tryReadJson<Record<string, string>>(reader, cPath)) ?? {}\n const notFound: string[] = []\n for (const key of input.keys) {\n if (key in existing) delete existing[key]\n else notFound.push(key)\n }\n if (notFound.length > 0) {\n throw new Error(`Dictionary \"${model.id}\" (${locale}): keys not found — [${notFound.join(', ')}]`)\n }\n changes.push({ path: cPath, content: canonicalStringify(existing) })\n removed.push(...input.keys.map(k => `${model.id}/${locale}:${k}`))\n } else {\n changes.push({ path: cPath, content: null })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n if (model.i18n) {\n changes.push({ path: metaFilePath(model, locale, input.defaultLocale), content: null })\n } else {\n await pushAllMetaForModel(reader, model.id, changes)\n }\n }\n break\n }\n\n case 'document': {\n if (!input.slug) throw new Error('Document delete requires a slug')\n const slugErr = validateSlug(input.slug)\n if (slugErr) throw new Error(slugErr)\n\n const dir = contentDir(model)\n const strategy = model.locale_strategy ?? 'file'\n\n if (!model.i18n) {\n changes.push({ path: `${dir}/${input.slug}.md`, content: null })\n } else if (strategy === 'file') {\n const slugDir = `${dir}/${input.slug}`\n const files = await reader.listDirectory(slugDir)\n for (const f of files) changes.push({ path: `${slugDir}/${f}`, content: null })\n } else if (input.locale) {\n changes.push({ path: documentFilePath(model, input.locale, input.slug), content: null })\n } else if (strategy === 'suffix') {\n const files = await reader.listDirectory(dir)\n for (const f of files) {\n if (f.startsWith(`${input.slug}.`) && f.endsWith('.md')) {\n changes.push({ path: `${dir}/${f}`, content: null })\n }\n }\n } else if (strategy === 'directory') {\n const subdirs = await reader.listDirectory(dir)\n for (const d of subdirs) {\n const filePath = `${dir}/${d}/${input.slug}.md`\n if (await reader.fileExists(filePath)) {\n changes.push({ path: filePath, content: null })\n }\n }\n } else if (strategy === 'none') {\n const files = await reader.listDirectory(dir)\n if (files.includes(`${input.slug}.md`)) {\n changes.push({ path: `${dir}/${input.slug}.md`, content: null })\n }\n }\n\n removed.push(`${model.content_path ?? `content/${model.domain}/${model.id}`}/${input.slug}`)\n\n const metaSlugDir = `.contentrain/meta/${model.id}/${input.slug}`\n if (input.locale) {\n changes.push({ path: `${metaSlugDir}/${input.locale}.json`, content: null })\n } else {\n const metaFiles = await reader.listDirectory(metaSlugDir)\n for (const f of metaFiles) changes.push({ path: `${metaSlugDir}/${f}`, content: null })\n }\n break\n }\n }\n\n changes.sort((a, b) => a.path.localeCompare(b.path))\n return { changes, result: removed, advisories: [] }\n}\n\nasync function pushAllMetaForModel(reader: RepoReader, modelId: string, changes: FileChange[]): Promise<void> {\n const metaDir = `.contentrain/meta/${modelId}`\n const files = await reader.listDirectory(metaDir)\n for (const f of files) changes.push({ path: `${metaDir}/${f}`, content: null })\n}\n","import type { ContentrainConfig, EntryMeta, ModelDefinition, Vocabulary } from '@contentrain/types'\nimport { generateEntryId, rewriteEntryMedia, rewriteMarkdownMedia, validateEntryId, validateLocale, validateSlug } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport type { ContentEntry } from '../content-manager.js'\nimport { canonicalStringify, parseMarkdownFrontmatter, serializeMarkdownFrontmatter } from '../serialization/index.js'\nimport type { ContentSaveEntryResult, ContentSavePlan } from './types.js'\nimport { contentFilePath, documentFilePath, metaFilePath } from './paths.js'\nimport { mergeEntryMeta } from '../meta-manager.js'\n\n/** Keys named individually in a replacement advisory before it collapses to a count. */\nconst MAX_REPORTED_KEYS = 5\n\ninterface PlanInput {\n model: ModelDefinition\n entries: ContentEntry[]\n config: ContentrainConfig\n vocabulary?: Vocabulary | null\n /**\n * Per-project public media delivery base (`RepoProvider.mediaBaseUrl`). When\n * set (hosted/cloud mode), relative `media/...` references in media fields\n * and markdown bodies are normalized to absolute delivery URLs before the\n * value is committed. Undefined in local mode — paths are kept verbatim.\n */\n mediaBaseUrl?: string\n}\n\n/**\n * Build the FileChange[] required to save a batch of content entries. The\n * plan is deterministic, pure, and does not touch disk — it reads existing\n * state through `reader` and returns changes for a single atomic commit.\n *\n * Grouping: multiple entries targeting the same (model, locale) share a\n * single content file and a single meta file. Reads are cached in-memory\n * so repeated reads of the same path during a plan are one IO.\n */\nexport async function planContentSave(reader: RepoReader, input: PlanInput): Promise<ContentSavePlan> {\n const { model, entries, config, vocabulary, mediaBaseUrl } = input\n const result: ContentSaveEntryResult[] = []\n const advisories: string[] = []\n\n // Hosted/cloud mode: normalize relative `media/...` references to absolute\n // delivery URLs so the committed value renders anywhere with no SDK. A no-op\n // in local mode (no base) or for field-less models (e.g. dictionaries).\n const normalizeMedia = (data: Record<string, unknown>): Record<string, unknown> =>\n mediaBaseUrl && model.fields ? rewriteEntryMedia(data, model.fields, mediaBaseUrl) : data\n\n // In-memory accumulators keyed by content-root-relative path.\n const contentByPath = new Map<string, unknown>()\n const metaByPath = new Map<string, unknown>()\n const markdownChanges = new Map<string, string>()\n\n async function readJsonOrEmpty<T>(path: string): Promise<T> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return {} as T\n }\n }\n\n /** Absent file and absent entry both mean \"no prior meta\" — hence undefined, not `{}`. */\n async function readJsonOrUndefined<T>(path: string): Promise<T | undefined> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return undefined\n }\n }\n\n /** Prior meta for a whole-file (non-collection) meta path, honouring the accumulator. */\n async function priorMeta(mPath: string): Promise<EntryMeta | undefined> {\n return (metaByPath.get(mPath) as EntryMeta | undefined)\n ?? await readJsonOrUndefined<EntryMeta>(mPath)\n }\n\n const defaultLocale = config.locales.default\n\n for (const entry of entries) {\n const locale = entry.locale ?? defaultLocale\n\n const localeErr = validateLocale(locale, config)\n if (localeErr) throw new Error(localeErr)\n if (entry.id) {\n const err = validateEntryId(entry.id)\n if (err) throw new Error(err)\n }\n if (entry.slug) {\n const err = validateSlug(entry.slug)\n if (err) throw new Error(err)\n }\n\n switch (model.kind) {\n case 'singleton': {\n const cPath = contentFilePath(model, locale)\n const mPath = metaFilePath(model, locale, defaultLocale)\n contentByPath.set(cPath, normalizeMedia(entry.data))\n metaByPath.set(mPath, mergeEntryMeta(await priorMeta(mPath), entry))\n result.push({ action: 'updated', locale })\n break\n }\n\n case 'collection': {\n const isNew = !entry.id\n const id = entry.id ?? generateEntryId()\n const cPath = contentFilePath(model, locale)\n const mPath = metaFilePath(model, locale, defaultLocale)\n\n const existing = (contentByPath.get(cPath) as Record<string, unknown> | undefined)\n ?? await readJsonOrEmpty<Record<string, unknown>>(cPath)\n\n const action: 'created' | 'updated' = isNew || !(id in existing) ? 'created' : 'updated'\n existing[id] = normalizeMedia(entry.data)\n\n const sorted: Record<string, unknown> = {}\n for (const key of Object.keys(existing).toSorted()) {\n sorted[key] = existing[key]\n }\n contentByPath.set(cPath, sorted)\n\n const existingMeta = (metaByPath.get(mPath) as Record<string, EntryMeta> | undefined)\n ?? await readJsonOrEmpty<Record<string, EntryMeta>>(mPath)\n existingMeta[id] = mergeEntryMeta(existingMeta[id], entry)\n metaByPath.set(mPath, existingMeta)\n\n result.push({ action, id, locale })\n break\n }\n\n case 'dictionary': {\n const cPath = contentFilePath(model, locale)\n const mPath = metaFilePath(model, locale, defaultLocale)\n\n const existing = (contentByPath.get(cPath) as Record<string, string> | undefined)\n ?? await readJsonOrEmpty<Record<string, string>>(cPath)\n\n const newData = entry.data as Record<string, string>\n const entryAdvisories: string[] = []\n\n // Overwriting a key used to be refused outright, which made the most\n // ordinary dictionary operation — correcting a translation — impossible:\n // the workaround was delete, merge, save, merge. Four operations and two\n // branches to fix one string. The refusal also advised \"include all keys\n // in a single save call\", which cannot work, because the check compares\n // values per key rather than counting them.\n //\n // Choosing a translation is a content decision, and MCP does not make\n // those. It reports them — the same way the duplicate-value case two\n // blocks below already does — and the branch diff shows the rest.\n const replaced = Object.keys(newData).filter(\n k => k in existing && existing[k] !== newData[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]}\" → \"${newData[k]}\"`).join(', ')\n const more = replaced.length > shown.length ? `, and ${replaced.length - shown.length} more` : ''\n entryAdvisories.push(\n `Dictionary \"${model.id}\" (${locale}): replaced ${replaced.length} existing value(s) — ${detail}${more}.`,\n )\n }\n\n const reverseMap = new Map<string, string>()\n for (const [k, v] of Object.entries(existing)) reverseMap.set(v, k)\n for (const [newKey, newValue] of Object.entries(newData)) {\n if (newKey in existing) continue\n const existingKey = reverseMap.get(newValue)\n if (existingKey && existingKey !== newKey) {\n entryAdvisories.push(\n `Value \"${newValue}\" already exists as key \"${existingKey}\". Consider reusing instead of creating \"${newKey}\".`,\n )\n }\n }\n\n if (vocabulary && Object.keys(vocabulary.terms).length > 0) {\n outer:\n for (const [newKey, newValue] of Object.entries(newData)) {\n if (newKey in existing) continue\n for (const translations of Object.values(vocabulary.terms)) {\n if (Object.values(translations).includes(newValue)) {\n entryAdvisories.push(\n `Value \"${newValue}\" matches a vocabulary term. Use the canonical form for consistency.`,\n )\n continue outer\n }\n }\n }\n }\n\n advisories.push(...entryAdvisories)\n\n contentByPath.set(cPath, { ...existing, ...newData })\n metaByPath.set(mPath, mergeEntryMeta(await priorMeta(mPath), entry))\n\n result.push({\n action: 'updated',\n locale,\n ...(entryAdvisories.length > 0 ? { advisories: entryAdvisories } : {}),\n })\n break\n }\n\n case 'document': {\n const entryAdvisories: string[] = []\n const slug = entry.slug ?? (entry.data['slug'] as string | undefined)\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 const dPath = documentFilePath(model, locale, slug)\n const mPath = metaFilePath(model, locale, defaultLocale, slug)\n\n // Accumulator before disk, as the collection branch does: two entries\n // touching one document in a single call must compose, not race.\n let existingRaw: string | null = markdownChanges.get(dPath) ?? null\n if (existingRaw === null) {\n try { existingRaw = await reader.readFile(dPath) }\n catch { /* not yet */ }\n }\n // Same rule the collection branch uses: anything already present —\n // on disk or produced earlier in this plan — makes this an update.\n const action: 'created' | 'updated' = existingRaw ? 'updated' : 'created'\n\n // Merge with what is on disk, the way collections and singletons above\n // already do. Documents were the only kind that replaced instead of\n // merging, so a save that touched one frontmatter field wrote a file\n // containing only that field — and an entirely empty body. Nothing\n // reported it: the response said `valid: true`, because validation runs\n // over the plan's own output, which was internally consistent and\n // wrong. Under auto-merge that reaches the default branch directly.\n const existing = existingRaw ? parseMarkdownFrontmatter(existingRaw) : null\n const fmData = { ...existing?.frontmatter, ...incomingFm }\n\n // Absent `body` means \"I am not editing the body\" — keep what is there.\n // A `body` that is present, even empty, is an instruction, and is\n // honoured; clearing real content that way is announced rather than\n // silent, because it is indistinguishable from a templating mistake.\n const bodyContent = bodySent ? incomingBody : (existing?.body ?? '')\n if (bodySent && !incomingBody.trim() && existing?.body.trim()) {\n const notice = `Document \"${slug}\" (${locale}): body cleared — an explicit empty \"body\" replaced ${existing.body.length} characters. Omit \"body\" to leave it untouched.`\n entryAdvisories.push(notice)\n advisories.push(notice)\n }\n\n // Frontmatter media fields go through the schema-guided rewrite; the\n // markdown body has its `media/...` image/link targets rewritten too.\n const normalizedBody = mediaBaseUrl ? rewriteMarkdownMedia(bodyContent, mediaBaseUrl) : bodyContent\n markdownChanges.set(dPath, serializeMarkdownFrontmatter(normalizeMedia(fmData), normalizedBody))\n metaByPath.set(mPath, mergeEntryMeta(await priorMeta(mPath), entry))\n\n result.push({\n action,\n slug,\n locale,\n ...(entryAdvisories.length > 0 ? { advisories: entryAdvisories } : {}),\n })\n break\n }\n }\n }\n\n const changes: FileChange[] = []\n for (const [path, data] of contentByPath) {\n changes.push({ path, content: canonicalStringify(data) })\n }\n for (const [path, meta] of metaByPath) {\n changes.push({ path, content: canonicalStringify(meta) })\n }\n for (const [path, content] of markdownChanges) {\n changes.push({ path, content })\n }\n changes.sort((a, b) => a.path.localeCompare(b.path))\n\n return { changes, result, advisories }\n}\n","import type { ModelDefinition } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport type { OpPlan } from './types.js'\n\nexport interface ModelDeleteInput {\n model: ModelDefinition\n}\n\nexport type ModelDeletePlan = OpPlan<string[]>\n\n/**\n * Build the FileChange[] required to delete a model and its associated\n * content/meta files.\n *\n * The schema is at most two directory levels deep (e.g. document meta at\n * `.contentrain/meta/{id}/{slug}/{locale}.json`). The helper below walks\n * that much, which is enough for every model kind + locale strategy and\n * keeps us free of a general-purpose recursive lister for now.\n *\n * `result` is the legacy three-entry summary:\n * - `models/{id}.json`\n * - `content/{domain}/{id}/` (only if any content files existed)\n * - `meta/{id}/` (only if any meta files existed)\n */\nexport async function planModelDelete(reader: RepoReader, input: ModelDeleteInput): Promise<ModelDeletePlan> {\n const { model } = input\n const changes: FileChange[] = []\n const removed: string[] = []\n\n changes.push({ path: `.contentrain/models/${model.id}.json`, content: null })\n removed.push(`models/${model.id}.json`)\n\n const contentDir = model.content_path ?? `.contentrain/content/${model.domain}/${model.id}`\n const contentFiles = await enumerateTwoLevels(reader, contentDir)\n for (const path of contentFiles) {\n changes.push({ path, content: null })\n }\n if (contentFiles.length > 0) {\n removed.push(model.content_path ?? `content/${model.domain}/${model.id}/`)\n }\n\n const metaDir = `.contentrain/meta/${model.id}`\n const metaFiles = await enumerateTwoLevels(reader, metaDir)\n for (const path of metaFiles) {\n changes.push({ path, content: null })\n }\n if (metaFiles.length > 0) {\n removed.push(`meta/${model.id}/`)\n }\n\n changes.sort((a, b) => a.path.localeCompare(b.path))\n return { changes, result: removed, advisories: [] }\n}\n\n/** List files up to two directory levels deep. Filters to `.json` and `.md`. */\nasync function enumerateTwoLevels(reader: RepoReader, dir: string): Promise<string[]> {\n const results: string[] = []\n const entries = await reader.listDirectory(dir)\n for (const entry of entries) {\n const path = `${dir}/${entry}`\n if (entry.endsWith('.json') || entry.endsWith('.md')) {\n results.push(path)\n continue\n }\n const subEntries = await reader.listDirectory(path)\n for (const sub of subEntries) {\n if (sub.endsWith('.json') || sub.endsWith('.md')) {\n results.push(`${path}/${sub}`)\n }\n }\n }\n return results\n}\n","import type { ModelDefinition } from '@contentrain/types'\nimport { MODEL_FIELD_ORDER } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport { canonicalStringify } from '../serialization/index.js'\nimport type { OpPlan } from './types.js'\n\nexport interface ModelSaveInput {\n model: ModelDefinition\n}\n\nexport interface ModelSaveResult {\n action: 'created' | 'updated'\n id: string\n}\n\nexport type ModelSavePlan = OpPlan<ModelSaveResult>\n\n/**\n * Build the FileChange[] required to create or update a model definition.\n *\n * The only disk change is the model JSON file itself. Content and meta\n * directories are created on-demand when their first write lands — git does\n * not track empty directories, and MCP's write helpers `mkdir -p` before\n * every file write, so there is no operational reason to emit an empty-dir\n * ensure step here.\n */\nexport async function planModelSave(reader: RepoReader, input: ModelSaveInput): Promise<ModelSavePlan> {\n const { model } = input\n const modelPath = `.contentrain/models/${model.id}.json`\n\n let action: 'created' | 'updated' = 'created'\n try {\n await reader.readFile(modelPath)\n action = 'updated'\n } catch { /* not yet exists */ }\n\n const changes: FileChange[] = [\n { path: modelPath, content: canonicalStringify(model, MODEL_FIELD_ORDER) },\n ]\n\n return {\n changes,\n result: { action, id: model.id },\n advisories: [],\n }\n}\n","import type { RepoReader } from './contracts/index.js'\n\n/**\n * Pin a `RepoReader` to one ref. The reconcile planner takes three of these\n * (merge-base / contentrain / base branch) and stays entirely ignorant of\n * refs — executors and tools decide what to bind, the planner just reads.\n *\n * For readers that honour the per-call `ref?` parameter (GitHub, GitLab)\n * this is the whole story; a reader that ignores it (LocalReader reads the\n * working tree) needs a ref-aware implementation instead — see\n * `git/ref-reader.ts`.\n */\nexport function bindRef(reader: RepoReader, ref: string): RepoReader {\n return {\n readFile: path => reader.readFile(path, ref),\n listDirectory: path => reader.listDirectory(path, ref),\n fileExists: path => reader.fileExists(path, ref),\n }\n}\n","import type { Vocabulary } from '@contentrain/types'\nimport { canonicalStringify, LOCALE_PATTERN, SLUG_PATTERN } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport type { OpPlan } from './types.js'\n\nconst VOCABULARY_PATH = '.contentrain/vocabulary.json'\n\n/** Terms named individually in an advisory before it collapses to a count. */\nconst MAX_REPORTED_TERMS = 5\n\nexport interface VocabularySaveInput {\n /**\n * Terms to add or update, keyed by term slug, each holding its translations\n * keyed by locale. Merged with what is already there — a term absent from\n * this map is untouched.\n */\n terms: Record<string, Record<string, string>>\n}\n\nexport interface VocabularyDeleteInput {\n /** Term slugs to remove. A slug that is not present is reported, not an error. */\n terms: string[]\n}\n\nexport interface VocabularySaveResult {\n added: string[]\n updated: string[]\n total: number\n}\n\nexport interface VocabularyDeleteResult {\n deleted: string[]\n missing: string[]\n total: number\n}\n\nexport type VocabularySavePlan = OpPlan<VocabularySaveResult>\nexport type VocabularyDeletePlan = OpPlan<VocabularyDeleteResult>\n\nasync function readVocabulary(reader: RepoReader): Promise<Vocabulary> {\n try {\n const parsed = JSON.parse(await reader.readFile(VOCABULARY_PATH)) as Partial<Vocabulary>\n return { version: parsed.version ?? 1, terms: parsed.terms ?? {} }\n } catch {\n return { version: 1, terms: {} }\n }\n}\n\n/**\n * Validate the shape callers get wrong.\n *\n * The nesting is `term -> locale -> translation`, and it reads identically the\n * other way round, so a caller that groups by locale at the top level produces\n * a structurally valid file that matches nothing. That is not a typo anyone\n * notices — the vocabulary simply stops having any effect. Checking that outer\n * keys are term slugs and inner keys are locale codes catches the inversion at\n * the point of writing.\n */\nfunction validateTerms(terms: Record<string, Record<string, string>>): string[] {\n const errors: string[] = []\n\n for (const [term, translations] of Object.entries(terms)) {\n if (!SLUG_PATTERN.test(term)) {\n errors.push(`Term \"${term}\": must be kebab-case. The outer key is the term, not a locale.`)\n }\n if (typeof translations !== 'object' || translations === null || Array.isArray(translations)) {\n errors.push(`Term \"${term}\": expected an object of locale → translation.`)\n continue\n }\n if (Object.keys(translations).length === 0) {\n errors.push(`Term \"${term}\": has no translations.`)\n }\n for (const [locale, value] of Object.entries(translations)) {\n if (!LOCALE_PATTERN.test(locale)) {\n errors.push(\n `Term \"${term}\": \"${locale}\" is not a locale code. Terms nest as { \"${term}\": { \"en\": \"…\" } } — one term, its translations inside.`,\n )\n }\n if (typeof value !== 'string') {\n errors.push(`Term \"${term}\" (${locale}): translation must be a string, got ${typeof value}.`)\n }\n }\n }\n\n return errors\n}\n\n/**\n * Build the FileChange[] to add or update vocabulary terms.\n *\n * Vocabulary had no write tool at all: the rules forbid editing `.contentrain/`\n * by hand, and there was no supported way to obey that. This closes it through\n * the same plan/apply path every other write uses, so it lands on a `cr/*`\n * branch and follows the project's workflow like anything else.\n */\nexport async function planVocabularySave(\n reader: RepoReader,\n input: VocabularySaveInput,\n): Promise<VocabularySavePlan> {\n const errors = validateTerms(input.terms)\n if (errors.length > 0) {\n throw new Error(`Vocabulary validation failed:\\n- ${errors.join('\\n- ')}`)\n }\n\n const existing = await readVocabulary(reader)\n const added: string[] = []\n const updated: string[] = []\n const advisories: string[] = []\n\n const merged: Record<string, Record<string, string>> = { ...existing.terms }\n for (const [term, translations] of Object.entries(input.terms)) {\n const prior = existing.terms[term]\n if (!prior) {\n added.push(term)\n merged[term] = { ...translations }\n continue\n }\n updated.push(term)\n // Merge per locale: adding a Turkish translation must not drop the English.\n merged[term] = { ...prior, ...translations }\n\n const changed = Object.entries(translations).filter(([l, v]) => l in prior && prior[l] !== v)\n if (changed.length > 0) {\n const shown = changed.slice(0, MAX_REPORTED_TERMS)\n const detail = shown.map(([l, v]) => `${l}: \"${prior[l]}\" → \"${v}\"`).join(', ')\n const more = changed.length > shown.length ? `, and ${changed.length - shown.length} more` : ''\n advisories.push(`Term \"${term}\": replaced ${changed.length} translation(s) — ${detail}${more}.`)\n }\n }\n\n // A canonical term whose value duplicates another term's is the whole thing\n // this file exists to prevent, so it is worth saying out loud.\n const byValue = new Map<string, string>()\n for (const [term, translations] of Object.entries(merged)) {\n for (const value of Object.values(translations)) {\n const owner = byValue.get(value)\n if (owner && owner !== term) {\n advisories.push(`Terms \"${owner}\" and \"${term}\" share the translation \"${value}\".`)\n } else {\n byValue.set(value, term)\n }\n }\n }\n\n const next: Vocabulary = { version: existing.version, terms: merged }\n const changes: FileChange[] = [\n { path: VOCABULARY_PATH, content: canonicalStringify(next) },\n ]\n\n return {\n changes,\n result: { added, updated, total: Object.keys(merged).length },\n advisories,\n }\n}\n\n/** Build the FileChange[] to remove vocabulary terms. */\nexport async function planVocabularyDelete(\n reader: RepoReader,\n input: VocabularyDeleteInput,\n): Promise<VocabularyDeletePlan> {\n const existing = await readVocabulary(reader)\n\n const deleted: string[] = []\n const missing: string[] = []\n const remaining = { ...existing.terms }\n\n for (const term of input.terms) {\n if (term in remaining) {\n delete remaining[term]\n deleted.push(term)\n } else {\n missing.push(term)\n }\n }\n\n const advisories = missing.length > 0\n ? [`Not in the vocabulary, so nothing to delete: ${missing.join(', ')}.`]\n : []\n\n // Nothing changed — emit no FileChange, so the caller can skip the commit\n // rather than create an empty branch.\n const changes: FileChange[] = deleted.length > 0\n ? [{ path: VOCABULARY_PATH, content: canonicalStringify({ version: existing.version, terms: remaining } satisfies Vocabulary) }]\n : []\n\n return {\n changes,\n result: { deleted, missing, total: Object.keys(remaining).length },\n advisories,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ops-DxhCCs_Z.mjs","names":["canonicalStringify","parseMarkdownFrontmatter","serializeMarkdownFrontmatter","canonicalStringify","canonicalStringify"],"sources":["../src/core/ops/content-delete.ts","../src/core/ops/content-save.ts","../src/core/ops/model-delete.ts","../src/core/ops/model-save.ts","../src/core/bind-ref.ts","../src/core/ops/vocabulary-save.ts"],"sourcesContent":["import type { EntryMeta, ModelDefinition } from '@contentrain/types'\nimport { validateSlug } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport { canonicalStringify } from '../serialization/index.js'\nimport { contentFilePath, documentFilePath, metaFilePath } from './paths.js'\nimport type { OpPlan } from './types.js'\n\nexport interface ContentDeleteInput {\n model: ModelDefinition\n id?: string\n slug?: string\n locale?: string\n keys?: string[]\n /**\n * `config.locales.default` — where a non-i18n model's single meta record\n * lives. Required to resolve meta paths; see `metaFilePath`.\n */\n defaultLocale: string\n}\n\nexport type ContentDeletePlan = OpPlan<string[]>\n\nfunction contentDir(model: Pick<ModelDefinition, 'domain' | 'id' | 'content_path'>): string {\n return model.content_path ?? `.contentrain/content/${model.domain}/${model.id}`\n}\n\nasync function tryReadJson<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 discoverCollectionLocales(\n reader: RepoReader,\n model: ModelDefinition,\n): Promise<string[]> {\n const dir = contentDir(model)\n const files = await reader.listDirectory(dir)\n const strategy = model.locale_strategy ?? 'file'\n if (strategy === 'directory') {\n return files\n }\n return files\n .filter(f => f.endsWith('.json'))\n .map(f => f.replace('.json', '').replace(`${model.id}.`, ''))\n}\n\n/**\n * Build the FileChange[] required to delete content. Pure — touches no disk.\n *\n * Semantics preserved from legacy `deleteContent`:\n * - Collection: remove the entry ID from each locale's content file and its\n * meta file. Meta file is deleted outright if the removal empties it.\n * - Singleton: delete the locale's content + meta file (locale-scoped when\n * i18n, non-i18n removes whatever meta lives under `.contentrain/meta/{id}/`).\n * - Dictionary: two modes — key-scoped removal updates the file in place,\n * or locale-scoped removes the content + meta pair.\n * - Document: strategy-aware delete (file/suffix/directory/none), plus\n * meta removal under `.contentrain/meta/{id}/{slug}/`.\n */\nexport async function planContentDelete(\n reader: RepoReader,\n input: ContentDeleteInput,\n): Promise<ContentDeletePlan> {\n const { model } = input\n\n // A non-i18n model stores content locale-agnostically in data.json and keeps\n // exactly one meta record, pinned to the default locale. A locale-scoped\n // delete is therefore meaningless — and actively destructive: the locale\n // silently maps onto data.json and the default-locale meta, so passing a\n // non-default locale deletes the shared content and the wrong meta while the\n // named locale's stray meta is never touched. Reject it; stray per-locale\n // meta is cleaned up by `contentrain_validate fix:true`.\n if (!model.i18n && input.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 changes: FileChange[] = []\n const removed: string[] = []\n\n switch (model.kind) {\n case 'collection': {\n if (!input.id) throw new Error('Collection delete requires an entry ID')\n\n const locales = input.locale\n ? [input.locale]\n : await discoverCollectionLocales(reader, model)\n\n for (const loc of locales) {\n const cPath = contentFilePath(model, loc)\n const data = await tryReadJson<Record<string, unknown>>(reader, cPath)\n if (!data || !(input.id in data)) continue\n delete data[input.id]\n const sorted: Record<string, unknown> = {}\n for (const k of Object.keys(data).toSorted()) sorted[k] = data[k]\n changes.push({ path: cPath, content: canonicalStringify(sorted) })\n removed.push(`content/${model.domain}/${model.id}/${loc}.json#${input.id}`)\n }\n\n for (const loc of locales) {\n // For a non-i18n model `loc` is the pseudo-locale \"data\" (discovered from\n // data.json); metaFilePath maps it onto the default locale regardless.\n const mPath = metaFilePath(model, loc, input.defaultLocale)\n const meta = await tryReadJson<Record<string, EntryMeta>>(reader, mPath)\n if (!meta || !(input.id in meta)) continue\n delete meta[input.id]\n if (Object.keys(meta).length > 0) {\n changes.push({ path: mPath, content: canonicalStringify(meta) })\n } else {\n changes.push({ path: mPath, content: null })\n }\n }\n break\n }\n\n case 'singleton': {\n if (model.i18n && !input.locale) {\n throw new Error('Singleton delete requires a locale when i18n is enabled')\n }\n const locale = input.locale ?? 'data'\n const cPath = contentFilePath(model, locale)\n changes.push({ path: cPath, content: null })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n\n if (model.i18n) {\n changes.push({ path: metaFilePath(model, locale, input.defaultLocale), content: null })\n } else {\n // Removing data.json leaves a non-i18n model with no content at all, so\n // every meta file under it goes — including strays left by older writes.\n await pushAllMetaForModel(reader, model.id, changes)\n }\n break\n }\n\n case 'dictionary': {\n if (model.i18n && !input.locale) {\n throw new Error('Dictionary delete requires a locale when i18n is enabled')\n }\n const locale = input.locale ?? 'data'\n const cPath = contentFilePath(model, locale)\n\n if (input.keys?.length) {\n const existing = (await tryReadJson<Record<string, string>>(reader, cPath)) ?? {}\n const notFound: string[] = []\n for (const key of input.keys) {\n if (key in existing) delete existing[key]\n else notFound.push(key)\n }\n if (notFound.length > 0) {\n throw new Error(`Dictionary \"${model.id}\" (${locale}): keys not found — [${notFound.join(', ')}]`)\n }\n changes.push({ path: cPath, content: canonicalStringify(existing) })\n removed.push(...input.keys.map(k => `${model.id}/${locale}:${k}`))\n } else {\n changes.push({ path: cPath, content: null })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n if (model.i18n) {\n changes.push({ path: metaFilePath(model, locale, input.defaultLocale), content: null })\n } else {\n await pushAllMetaForModel(reader, model.id, changes)\n }\n }\n break\n }\n\n case 'document': {\n if (!input.slug) throw new Error('Document delete requires a slug')\n const slugErr = validateSlug(input.slug)\n if (slugErr) throw new Error(slugErr)\n\n const dir = contentDir(model)\n const strategy = model.locale_strategy ?? 'file'\n\n if (!model.i18n) {\n changes.push({ path: `${dir}/${input.slug}.md`, content: null })\n } else if (strategy === 'file') {\n const slugDir = `${dir}/${input.slug}`\n const files = await reader.listDirectory(slugDir)\n for (const f of files) changes.push({ path: `${slugDir}/${f}`, content: null })\n } else if (input.locale) {\n changes.push({ path: documentFilePath(model, input.locale, input.slug), content: null })\n } else if (strategy === 'suffix') {\n const files = await reader.listDirectory(dir)\n for (const f of files) {\n if (f.startsWith(`${input.slug}.`) && f.endsWith('.md')) {\n changes.push({ path: `${dir}/${f}`, content: null })\n }\n }\n } else if (strategy === 'directory') {\n const subdirs = await reader.listDirectory(dir)\n for (const d of subdirs) {\n const filePath = `${dir}/${d}/${input.slug}.md`\n if (await reader.fileExists(filePath)) {\n changes.push({ path: filePath, content: null })\n }\n }\n } else if (strategy === 'none') {\n const files = await reader.listDirectory(dir)\n if (files.includes(`${input.slug}.md`)) {\n changes.push({ path: `${dir}/${input.slug}.md`, content: null })\n }\n }\n\n removed.push(`${model.content_path ?? `content/${model.domain}/${model.id}`}/${input.slug}`)\n\n const metaSlugDir = `.contentrain/meta/${model.id}/${input.slug}`\n if (input.locale) {\n changes.push({ path: `${metaSlugDir}/${input.locale}.json`, content: null })\n } else {\n const metaFiles = await reader.listDirectory(metaSlugDir)\n for (const f of metaFiles) changes.push({ path: `${metaSlugDir}/${f}`, content: null })\n }\n break\n }\n }\n\n changes.sort((a, b) => a.path.localeCompare(b.path))\n return { changes, result: removed, advisories: [] }\n}\n\nasync function pushAllMetaForModel(reader: RepoReader, modelId: string, changes: FileChange[]): Promise<void> {\n const metaDir = `.contentrain/meta/${modelId}`\n const files = await reader.listDirectory(metaDir)\n for (const f of files) changes.push({ path: `${metaDir}/${f}`, content: null })\n}\n","import type { ContentrainConfig, EntryMeta, ModelDefinition, Vocabulary } from '@contentrain/types'\nimport { generateEntryId, rewriteEntryMedia, rewriteMarkdownMedia, validateEntryId, validateLocale, validateSlug } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport type { ContentEntry } from '../content-manager.js'\nimport { canonicalStringify, parseMarkdownFrontmatter, serializeMarkdownFrontmatter } from '../serialization/index.js'\nimport type { ContentSaveEntryResult, ContentSavePlan } from './types.js'\nimport { contentFilePath, documentFilePath, metaFilePath } from './paths.js'\nimport { mergeEntryMeta } from '../meta-manager.js'\n\n/** Keys named individually in a replacement advisory before it collapses to a count. */\nconst MAX_REPORTED_KEYS = 5\n\ninterface PlanInput {\n model: ModelDefinition\n entries: ContentEntry[]\n config: ContentrainConfig\n vocabulary?: Vocabulary | null\n /**\n * Per-project public media delivery base (`RepoProvider.mediaBaseUrl`). When\n * set (hosted/cloud mode), relative `media/...` references in media fields\n * and markdown bodies are normalized to absolute delivery URLs before the\n * value is committed. Undefined in local mode — paths are kept verbatim.\n */\n mediaBaseUrl?: string\n}\n\n/**\n * Build the FileChange[] required to save a batch of content entries. The\n * plan is deterministic, pure, and does not touch disk — it reads existing\n * state through `reader` and returns changes for a single atomic commit.\n *\n * Grouping: multiple entries targeting the same (model, locale) share a\n * single content file and a single meta file. Reads are cached in-memory\n * so repeated reads of the same path during a plan are one IO.\n */\nexport async function planContentSave(reader: RepoReader, input: PlanInput): Promise<ContentSavePlan> {\n const { model, entries, config, vocabulary, mediaBaseUrl } = input\n const result: ContentSaveEntryResult[] = []\n const advisories: string[] = []\n\n // Hosted/cloud mode: normalize relative `media/...` references to absolute\n // delivery URLs so the committed value renders anywhere with no SDK. A no-op\n // in local mode (no base) or for field-less models (e.g. dictionaries).\n const normalizeMedia = (data: Record<string, unknown>): Record<string, unknown> =>\n mediaBaseUrl && model.fields ? rewriteEntryMedia(data, model.fields, mediaBaseUrl) : data\n\n // In-memory accumulators keyed by content-root-relative path.\n const contentByPath = new Map<string, unknown>()\n const metaByPath = new Map<string, unknown>()\n const markdownChanges = new Map<string, string>()\n\n async function readJsonOrEmpty<T>(path: string): Promise<T> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return {} as T\n }\n }\n\n /** Absent file and absent entry both mean \"no prior meta\" — hence undefined, not `{}`. */\n async function readJsonOrUndefined<T>(path: string): Promise<T | undefined> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return undefined\n }\n }\n\n /** Prior meta for a whole-file (non-collection) meta path, honouring the accumulator. */\n async function priorMeta(mPath: string): Promise<EntryMeta | undefined> {\n return (metaByPath.get(mPath) as EntryMeta | undefined)\n ?? await readJsonOrUndefined<EntryMeta>(mPath)\n }\n\n const defaultLocale = config.locales.default\n\n for (const entry of entries) {\n const locale = entry.locale ?? defaultLocale\n\n const localeErr = validateLocale(locale, config)\n if (localeErr) throw new Error(localeErr)\n if (entry.id) {\n const err = validateEntryId(entry.id)\n if (err) throw new Error(err)\n }\n if (entry.slug) {\n const err = validateSlug(entry.slug)\n if (err) throw new Error(err)\n }\n\n switch (model.kind) {\n case 'singleton': {\n const cPath = contentFilePath(model, locale)\n const mPath = metaFilePath(model, locale, defaultLocale)\n contentByPath.set(cPath, normalizeMedia(entry.data))\n metaByPath.set(mPath, mergeEntryMeta(await priorMeta(mPath), entry))\n result.push({ action: 'updated', locale })\n break\n }\n\n case 'collection': {\n const isNew = !entry.id\n const id = entry.id ?? generateEntryId()\n const cPath = contentFilePath(model, locale)\n const mPath = metaFilePath(model, locale, defaultLocale)\n\n const existing = (contentByPath.get(cPath) as Record<string, unknown> | undefined)\n ?? await readJsonOrEmpty<Record<string, unknown>>(cPath)\n\n const action: 'created' | 'updated' = isNew || !(id in existing) ? 'created' : 'updated'\n existing[id] = normalizeMedia(entry.data)\n\n const sorted: Record<string, unknown> = {}\n for (const key of Object.keys(existing).toSorted()) {\n sorted[key] = existing[key]\n }\n contentByPath.set(cPath, sorted)\n\n const existingMeta = (metaByPath.get(mPath) as Record<string, EntryMeta> | undefined)\n ?? await readJsonOrEmpty<Record<string, EntryMeta>>(mPath)\n existingMeta[id] = mergeEntryMeta(existingMeta[id], entry)\n metaByPath.set(mPath, existingMeta)\n\n result.push({ action, id, locale })\n break\n }\n\n case 'dictionary': {\n const cPath = contentFilePath(model, locale)\n const mPath = metaFilePath(model, locale, defaultLocale)\n\n const existing = (contentByPath.get(cPath) as Record<string, string> | undefined)\n ?? await readJsonOrEmpty<Record<string, string>>(cPath)\n\n const newData = entry.data as Record<string, string>\n const entryAdvisories: string[] = []\n\n // Overwriting a key used to be refused outright, which made the most\n // ordinary dictionary operation — correcting a translation — impossible:\n // the workaround was delete, merge, save, merge. Four operations and two\n // branches to fix one string. The refusal also advised \"include all keys\n // in a single save call\", which cannot work, because the check compares\n // values per key rather than counting them.\n //\n // Choosing a translation is a content decision, and MCP does not make\n // those. It reports them — the same way the duplicate-value case two\n // blocks below already does — and the branch diff shows the rest.\n const replaced = Object.keys(newData).filter(\n k => k in existing && existing[k] !== newData[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]}\" → \"${newData[k]}\"`).join(', ')\n const more = replaced.length > shown.length ? `, and ${replaced.length - shown.length} more` : ''\n entryAdvisories.push(\n `Dictionary \"${model.id}\" (${locale}): replaced ${replaced.length} existing value(s) — ${detail}${more}.`,\n )\n }\n\n const reverseMap = new Map<string, string>()\n for (const [k, v] of Object.entries(existing)) reverseMap.set(v, k)\n for (const [newKey, newValue] of Object.entries(newData)) {\n if (newKey in existing) continue\n const existingKey = reverseMap.get(newValue)\n if (existingKey && existingKey !== newKey) {\n entryAdvisories.push(\n `Value \"${newValue}\" already exists as key \"${existingKey}\". Consider reusing instead of creating \"${newKey}\".`,\n )\n }\n }\n\n if (vocabulary && Object.keys(vocabulary.terms).length > 0) {\n outer:\n for (const [newKey, newValue] of Object.entries(newData)) {\n if (newKey in existing) continue\n for (const translations of Object.values(vocabulary.terms)) {\n if (Object.values(translations).includes(newValue)) {\n entryAdvisories.push(\n `Value \"${newValue}\" matches a vocabulary term. Use the canonical form for consistency.`,\n )\n continue outer\n }\n }\n }\n }\n\n advisories.push(...entryAdvisories)\n\n contentByPath.set(cPath, { ...existing, ...newData })\n metaByPath.set(mPath, mergeEntryMeta(await priorMeta(mPath), entry))\n\n result.push({\n action: 'updated',\n locale,\n ...(entryAdvisories.length > 0 ? { advisories: entryAdvisories } : {}),\n })\n break\n }\n\n case 'document': {\n const entryAdvisories: string[] = []\n const slug = entry.slug ?? (entry.data['slug'] as string | undefined)\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 const dPath = documentFilePath(model, locale, slug)\n const mPath = metaFilePath(model, locale, defaultLocale, slug)\n\n // Accumulator before disk, as the collection branch does: two entries\n // touching one document in a single call must compose, not race.\n let existingRaw: string | null = markdownChanges.get(dPath) ?? null\n if (existingRaw === null) {\n try { existingRaw = await reader.readFile(dPath) }\n catch { /* not yet */ }\n }\n // Same rule the collection branch uses: anything already present —\n // on disk or produced earlier in this plan — makes this an update.\n const action: 'created' | 'updated' = existingRaw ? 'updated' : 'created'\n\n // Merge with what is on disk, the way collections and singletons above\n // already do. Documents were the only kind that replaced instead of\n // merging, so a save that touched one frontmatter field wrote a file\n // containing only that field — and an entirely empty body. Nothing\n // reported it: the response said `valid: true`, because validation runs\n // over the plan's own output, which was internally consistent and\n // wrong. Under auto-merge that reaches the default branch directly.\n const existing = existingRaw ? parseMarkdownFrontmatter(existingRaw) : null\n const fmData = { ...existing?.frontmatter, ...incomingFm }\n\n // Absent `body` means \"I am not editing the body\" — keep what is there.\n // A `body` that is present, even empty, is an instruction, and is\n // honoured; clearing real content that way is announced rather than\n // silent, because it is indistinguishable from a templating mistake.\n const bodyContent = bodySent ? incomingBody : (existing?.body ?? '')\n if (bodySent && !incomingBody.trim() && existing?.body.trim()) {\n const notice = `Document \"${slug}\" (${locale}): body cleared — an explicit empty \"body\" replaced ${existing.body.length} characters. Omit \"body\" to leave it untouched.`\n entryAdvisories.push(notice)\n advisories.push(notice)\n }\n\n // Frontmatter media fields go through the schema-guided rewrite; the\n // markdown body has its `media/...` image/link targets rewritten too.\n const normalizedBody = mediaBaseUrl ? rewriteMarkdownMedia(bodyContent, mediaBaseUrl) : bodyContent\n markdownChanges.set(dPath, serializeMarkdownFrontmatter(normalizeMedia(fmData), normalizedBody))\n metaByPath.set(mPath, mergeEntryMeta(await priorMeta(mPath), entry))\n\n result.push({\n action,\n slug,\n locale,\n ...(entryAdvisories.length > 0 ? { advisories: entryAdvisories } : {}),\n })\n break\n }\n }\n }\n\n const changes: FileChange[] = []\n for (const [path, data] of contentByPath) {\n changes.push({ path, content: canonicalStringify(data) })\n }\n for (const [path, meta] of metaByPath) {\n changes.push({ path, content: canonicalStringify(meta) })\n }\n for (const [path, content] of markdownChanges) {\n changes.push({ path, content })\n }\n changes.sort((a, b) => a.path.localeCompare(b.path))\n\n return { changes, result, advisories }\n}\n","import type { ModelDefinition } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport type { OpPlan } from './types.js'\n\nexport interface ModelDeleteInput {\n model: ModelDefinition\n}\n\nexport type ModelDeletePlan = OpPlan<string[]>\n\n/**\n * Build the FileChange[] required to delete a model and its associated\n * content/meta files.\n *\n * The schema is at most two directory levels deep (e.g. document meta at\n * `.contentrain/meta/{id}/{slug}/{locale}.json`). The helper below walks\n * that much, which is enough for every model kind + locale strategy and\n * keeps us free of a general-purpose recursive lister for now.\n *\n * `result` is the legacy three-entry summary:\n * - `models/{id}.json`\n * - `content/{domain}/{id}/` (only if any content files existed)\n * - `meta/{id}/` (only if any meta files existed)\n */\nexport async function planModelDelete(reader: RepoReader, input: ModelDeleteInput): Promise<ModelDeletePlan> {\n const { model } = input\n const changes: FileChange[] = []\n const removed: string[] = []\n\n changes.push({ path: `.contentrain/models/${model.id}.json`, content: null })\n removed.push(`models/${model.id}.json`)\n\n const contentDir = model.content_path ?? `.contentrain/content/${model.domain}/${model.id}`\n const contentFiles = await enumerateTwoLevels(reader, contentDir)\n for (const path of contentFiles) {\n changes.push({ path, content: null })\n }\n if (contentFiles.length > 0) {\n removed.push(model.content_path ?? `content/${model.domain}/${model.id}/`)\n }\n\n const metaDir = `.contentrain/meta/${model.id}`\n const metaFiles = await enumerateTwoLevels(reader, metaDir)\n for (const path of metaFiles) {\n changes.push({ path, content: null })\n }\n if (metaFiles.length > 0) {\n removed.push(`meta/${model.id}/`)\n }\n\n changes.sort((a, b) => a.path.localeCompare(b.path))\n return { changes, result: removed, advisories: [] }\n}\n\n/** List files up to two directory levels deep. Filters to `.json` and `.md`. */\nasync function enumerateTwoLevels(reader: RepoReader, dir: string): Promise<string[]> {\n const results: string[] = []\n const entries = await reader.listDirectory(dir)\n for (const entry of entries) {\n const path = `${dir}/${entry}`\n if (entry.endsWith('.json') || entry.endsWith('.md')) {\n results.push(path)\n continue\n }\n const subEntries = await reader.listDirectory(path)\n for (const sub of subEntries) {\n if (sub.endsWith('.json') || sub.endsWith('.md')) {\n results.push(`${path}/${sub}`)\n }\n }\n }\n return results\n}\n","import type { ModelDefinition } from '@contentrain/types'\nimport { MODEL_FIELD_ORDER } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport { canonicalStringify } from '../serialization/index.js'\nimport type { OpPlan } from './types.js'\n\nexport interface ModelSaveInput {\n model: ModelDefinition\n}\n\nexport interface ModelSaveResult {\n action: 'created' | 'updated'\n id: string\n}\n\nexport type ModelSavePlan = OpPlan<ModelSaveResult>\n\n/**\n * Build the FileChange[] required to create or update a model definition.\n *\n * The only disk change is the model JSON file itself. Content and meta\n * directories are created on-demand when their first write lands — git does\n * not track empty directories, and MCP's write helpers `mkdir -p` before\n * every file write, so there is no operational reason to emit an empty-dir\n * ensure step here.\n */\nexport async function planModelSave(reader: RepoReader, input: ModelSaveInput): Promise<ModelSavePlan> {\n const { model } = input\n const modelPath = `.contentrain/models/${model.id}.json`\n\n let action: 'created' | 'updated' = 'created'\n try {\n await reader.readFile(modelPath)\n action = 'updated'\n } catch { /* not yet exists */ }\n\n const changes: FileChange[] = [\n { path: modelPath, content: canonicalStringify(model, MODEL_FIELD_ORDER) },\n ]\n\n return {\n changes,\n result: { action, id: model.id },\n advisories: [],\n }\n}\n","import type { RepoReader } from './contracts/index.js'\n\n/**\n * Pin a `RepoReader` to one ref. The reconcile planner takes three of these\n * (merge-base / contentrain / base branch) and stays entirely ignorant of\n * refs — executors and tools decide what to bind, the planner just reads.\n *\n * For readers that honour the per-call `ref?` parameter (GitHub, GitLab)\n * this is the whole story; a reader that ignores it (LocalReader reads the\n * working tree) needs a ref-aware implementation instead — see\n * `git/ref-reader.ts`.\n */\nexport function bindRef(reader: RepoReader, ref: string): RepoReader {\n return {\n readFile: path => reader.readFile(path, ref),\n listDirectory: path => reader.listDirectory(path, ref),\n fileExists: path => reader.fileExists(path, ref),\n }\n}\n","import type { Vocabulary } from '@contentrain/types'\nimport { canonicalStringify, LOCALE_PATTERN, SLUG_PATTERN } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport type { OpPlan } from './types.js'\n\nconst VOCABULARY_PATH = '.contentrain/vocabulary.json'\n\n/** Terms named individually in an advisory before it collapses to a count. */\nconst MAX_REPORTED_TERMS = 5\n\nexport interface VocabularySaveInput {\n /**\n * Terms to add or update, keyed by term slug, each holding its translations\n * keyed by locale. Merged with what is already there — a term absent from\n * this map is untouched.\n */\n terms: Record<string, Record<string, string>>\n}\n\nexport interface VocabularyDeleteInput {\n /** Term slugs to remove. A slug that is not present is reported, not an error. */\n terms: string[]\n}\n\nexport interface VocabularySaveResult {\n added: string[]\n updated: string[]\n total: number\n}\n\nexport interface VocabularyDeleteResult {\n deleted: string[]\n missing: string[]\n total: number\n}\n\nexport type VocabularySavePlan = OpPlan<VocabularySaveResult>\nexport type VocabularyDeletePlan = OpPlan<VocabularyDeleteResult>\n\nasync function readVocabulary(reader: RepoReader): Promise<Vocabulary> {\n try {\n const parsed = JSON.parse(await reader.readFile(VOCABULARY_PATH)) as Partial<Vocabulary>\n return { version: parsed.version ?? 1, terms: parsed.terms ?? {} }\n } catch {\n return { version: 1, terms: {} }\n }\n}\n\n/**\n * Validate the shape callers get wrong.\n *\n * The nesting is `term -> locale -> translation`, and it reads identically the\n * other way round, so a caller that groups by locale at the top level produces\n * a structurally valid file that matches nothing. That is not a typo anyone\n * notices — the vocabulary simply stops having any effect. Checking that outer\n * keys are term slugs and inner keys are locale codes catches the inversion at\n * the point of writing.\n */\nfunction validateTerms(terms: Record<string, Record<string, string>>): string[] {\n const errors: string[] = []\n\n for (const [term, translations] of Object.entries(terms)) {\n if (!SLUG_PATTERN.test(term)) {\n errors.push(`Term \"${term}\": must be kebab-case. The outer key is the term, not a locale.`)\n }\n if (typeof translations !== 'object' || translations === null || Array.isArray(translations)) {\n errors.push(`Term \"${term}\": expected an object of locale → translation.`)\n continue\n }\n if (Object.keys(translations).length === 0) {\n errors.push(`Term \"${term}\": has no translations.`)\n }\n for (const [locale, value] of Object.entries(translations)) {\n if (!LOCALE_PATTERN.test(locale)) {\n errors.push(\n `Term \"${term}\": \"${locale}\" is not a locale code. Terms nest as { \"${term}\": { \"en\": \"…\" } } — one term, its translations inside.`,\n )\n }\n if (typeof value !== 'string') {\n errors.push(`Term \"${term}\" (${locale}): translation must be a string, got ${typeof value}.`)\n }\n }\n }\n\n return errors\n}\n\n/**\n * Build the FileChange[] to add or update vocabulary terms.\n *\n * Vocabulary had no write tool at all: the rules forbid editing `.contentrain/`\n * by hand, and there was no supported way to obey that. This closes it through\n * the same plan/apply path every other write uses, so it lands on a `cr/*`\n * branch and follows the project's workflow like anything else.\n */\nexport async function planVocabularySave(\n reader: RepoReader,\n input: VocabularySaveInput,\n): Promise<VocabularySavePlan> {\n const errors = validateTerms(input.terms)\n if (errors.length > 0) {\n throw new Error(`Vocabulary validation failed:\\n- ${errors.join('\\n- ')}`)\n }\n\n const existing = await readVocabulary(reader)\n const added: string[] = []\n const updated: string[] = []\n const advisories: string[] = []\n\n const merged: Record<string, Record<string, string>> = { ...existing.terms }\n for (const [term, translations] of Object.entries(input.terms)) {\n const prior = existing.terms[term]\n if (!prior) {\n added.push(term)\n merged[term] = { ...translations }\n continue\n }\n updated.push(term)\n // Merge per locale: adding a Turkish translation must not drop the English.\n merged[term] = { ...prior, ...translations }\n\n const changed = Object.entries(translations).filter(([l, v]) => l in prior && prior[l] !== v)\n if (changed.length > 0) {\n const shown = changed.slice(0, MAX_REPORTED_TERMS)\n const detail = shown.map(([l, v]) => `${l}: \"${prior[l]}\" → \"${v}\"`).join(', ')\n const more = changed.length > shown.length ? `, and ${changed.length - shown.length} more` : ''\n advisories.push(`Term \"${term}\": replaced ${changed.length} translation(s) — ${detail}${more}.`)\n }\n }\n\n // A canonical term whose value duplicates another term's is the whole thing\n // this file exists to prevent, so it is worth saying out loud.\n const byValue = new Map<string, string>()\n for (const [term, translations] of Object.entries(merged)) {\n for (const value of Object.values(translations)) {\n const owner = byValue.get(value)\n if (owner && owner !== term) {\n advisories.push(`Terms \"${owner}\" and \"${term}\" share the translation \"${value}\".`)\n } else {\n byValue.set(value, term)\n }\n }\n }\n\n const next: Vocabulary = { version: existing.version, terms: merged }\n const changes: FileChange[] = [\n { path: VOCABULARY_PATH, content: canonicalStringify(next) },\n ]\n\n return {\n changes,\n result: { added, updated, total: Object.keys(merged).length },\n advisories,\n }\n}\n\n/** Build the FileChange[] to remove vocabulary terms. */\nexport async function planVocabularyDelete(\n reader: RepoReader,\n input: VocabularyDeleteInput,\n): Promise<VocabularyDeletePlan> {\n const existing = await readVocabulary(reader)\n\n const deleted: string[] = []\n const missing: string[] = []\n const remaining = { ...existing.terms }\n\n for (const term of input.terms) {\n if (term in remaining) {\n delete remaining[term]\n deleted.push(term)\n } else {\n missing.push(term)\n }\n }\n\n const advisories = missing.length > 0\n ? [`Not in the vocabulary, so nothing to delete: ${missing.join(', ')}.`]\n : []\n\n // Nothing changed — emit no FileChange, so the caller can skip the commit\n // rather than create an empty branch.\n const changes: FileChange[] = deleted.length > 0\n ? [{ path: VOCABULARY_PATH, content: canonicalStringify({ version: existing.version, terms: remaining } satisfies Vocabulary) }]\n : []\n\n return {\n changes,\n result: { deleted, missing, total: Object.keys(remaining).length },\n advisories,\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/providers/local/types.ts","../../../src/providers/local/provider.ts","../../../src/providers/local/reader.ts"],"mappings":";;;;;UAIiB,kBAAA;EACf,IAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;AAYF;;;;UAAiB,mBAAA;EAYL;EAVV,MAAA;EAY+B;EAV/B,OAAA,EAAS,UAAA;EAFT;EAIA,OAAA;EAFS;EAIT,MAAA,GAAS,YAAA;EAAT;EAEA,IAAA;EAAA;EAEA,OAAA,GAAU,kBAAA;EAAA;EAEV,gBAAA,GAAmB,YAAA;AAAA;AAAA,UAGJ,gBAAA,SAAyB,QAAA;EAHT;AAGjC;;;;;EAOE,cAAA;EAPwC;EASxC,IAAA,GAAO,UAAA;EATuC;EAW9C,OAAA;EAJA;;;;;EAUA,YAAA,GAAe,WAAA;EAEf;EAAA,WAAA,GAAc,UAAA;AAAA;;;;;AApDhB;;;;;;;;;;AAgBA;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/providers/local/types.ts","../../../src/providers/local/provider.ts","../../../src/providers/local/reader.ts"],"mappings":";;;;;UAIiB,kBAAA;EACf,IAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;AAYF;;;;UAAiB,mBAAA;EAYL;EAVV,MAAA;EAY+B;EAV/B,OAAA,EAAS,UAAA;EAFT;EAIA,OAAA;EAFS;EAIT,MAAA,GAAS,YAAA;EAAT;EAEA,IAAA;EAAA;EAEA,OAAA,GAAU,kBAAA;EAAA;EAEV,gBAAA,GAAmB,YAAA;AAAA;AAAA,UAGJ,gBAAA,SAAyB,QAAA;EAHT;AAGjC;;;;;EAOE,cAAA;EAPwC;EASxC,IAAA,GAAO,UAAA;EATuC;EAW9C,OAAA;EAJA;;;;;EAUA,YAAA,GAAe,WAAA;EAEf;EAAA,WAAA,GAAc,UAAA;AAAA;;;;;AApDhB;;;;;;;;;;AAgBA;;;cCgCa,aAAA,YAAyB,cAAA;EAAA,SAIR,WAAA;EAAA,SAHnB,YAAA,EAAc,sBAAA;EAAA,iBACN,MAAA;cAEW,WAAA;EAI5B,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAItC,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAI3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;ED5C/B;;;;;ECqDH,mBAAA,CAAA,GAAuB,OAAA,CAAQ,cAAA;EAO/B,SAAA,CAAU,KAAA,EAAO,mBAAA,GAAsB,OAAA,CAAQ,gBAAA;EA8CrD,YAAA,CAAa,MAAA,YAAkB,OAAA,CAAQ,MAAA;EAIjC,YAAA,CAAa,IAAA,UAAc,OAAA,YAAmB,OAAA;EAKpD,YAAA,CAAa,IAAA,WAAe,OAAA;EAI5B,aAAA,CAAc,MAAA,UAAgB,IAAA,YAAgB,OAAA,CAAQ,QAAA;EAKtD,WAAA,CAAY,MAAA,UAAgB,IAAA,WAAe,OAAA,CAAQ,WAAA;EAInD,QAAA,CAAS,MAAA,UAAgB,IAAA,YAAgB,OAAA;EAKzC,gBAAA,CAAA,GAAoB,OAAA;EAId,YAAA,CAAa,IAAA,UAAc,IAAA,WAAe,OAAA;ED3GjC;;;;;;;EC2HT,iBAAA,CAAkB,KAAA;IACtB,MAAA;IACA,IAAA;IACA,MAAA;IACA,OAAA,EAAS,UAAA;IACT,OAAA;IACA,MAAA,EAAQ,YAAA;EAAA,IACN,OAAA,CAAQ,QAAA;AAAA;;;;;;ADpLd;;;;;;;;cEWa,WAAA,YAAuB,UAAA;EAAA,SACN,WAAA;cAAA,WAAA;EAEtB,QAAA,CAAS,IAAA,UAAc,IAAA,YAAgB,OAAA;EAIvC,aAAA,CAAc,IAAA,UAAc,IAAA,YAAgB,OAAA;EAQ5C,UAAA,CAAW,IAAA,UAAc,IAAA,YAAgB,OAAA;AAAA"}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import "../../contracts-DfL0BfrD.mjs";
|
|
2
|
-
import "../../reconcile-
|
|
3
|
-
import "../../context-
|
|
4
|
-
import { k as LocalReader } from "../../model-manager-
|
|
5
|
-
import "../../ops-
|
|
6
|
-
import "../../fs-
|
|
7
|
-
import "../../meta-manager-
|
|
8
|
-
import "../../config-
|
|
9
|
-
import "../../validator-
|
|
2
|
+
import "../../reconcile-CPDGFQ8H.mjs";
|
|
3
|
+
import "../../context-fJCKH_qv.mjs";
|
|
4
|
+
import { k as LocalReader } from "../../model-manager-DPXulg7z.mjs";
|
|
5
|
+
import "../../ops-DxhCCs_Z.mjs";
|
|
6
|
+
import "../../fs-BFTuXbbw.mjs";
|
|
7
|
+
import "../../meta-manager-CSpKtjnP.mjs";
|
|
8
|
+
import "../../config-BLwZtwlI.mjs";
|
|
9
|
+
import "../../validator-C08p1OpM.mjs";
|
|
10
10
|
import "../../identity-CxAB9Qb8.mjs";
|
|
11
|
-
import "../../branch-lifecycle-
|
|
11
|
+
import "../../branch-lifecycle-f4gI_eGo.mjs";
|
|
12
12
|
import "../../id-CjGMPP1k.mjs";
|
|
13
|
-
import "../../transaction-
|
|
14
|
-
import { t as LocalProvider } from "../../local-
|
|
13
|
+
import "../../transaction-BWJt1KQb.mjs";
|
|
14
|
+
import { t as LocalProvider } from "../../local-DkDTrGt_.mjs";
|
|
15
15
|
export { LocalProvider, LocalReader };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { t as buildContextChange } from "./context-
|
|
2
|
-
import { A as contentDirPath, M as documentFilePath, j as contentFilePath } from "./model-manager-
|
|
1
|
+
import { t as buildContextChange } from "./context-fJCKH_qv.mjs";
|
|
2
|
+
import { A as contentDirPath, M as documentFilePath, j as contentFilePath } from "./model-manager-DPXulg7z.mjs";
|
|
3
3
|
import { t as OverlayReader } from "./overlay-reader-Bx1YxGXq.mjs";
|
|
4
|
-
import { t as validateProject } from "./validator-
|
|
4
|
+
import { t as validateProject } from "./validator-C08p1OpM.mjs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
-
import { MODEL_FIELD_ORDER, canonicalStringify, conflictId, parseMarkdownFrontmatter, serializeMarkdownFrontmatter, sortKeys, stableHash } from "@contentrain/types";
|
|
7
6
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { MODEL_FIELD_ORDER, canonicalStringify, conflictId, parseMarkdownFrontmatter, serializeMarkdownFrontmatter, sortKeys, stableHash } from "@contentrain/types";
|
|
8
8
|
//#region src/core/ops/apply-to-worktree.ts
|
|
9
9
|
/**
|
|
10
10
|
* Phase 2 shim — applies a `FileChange[]` to a local worktree directory.
|
|
@@ -1724,4 +1724,4 @@ function parseJson(raw) {
|
|
|
1724
1724
|
//#endregion
|
|
1725
1725
|
export { applyChangesToWorktree as n, planReconcile as t };
|
|
1726
1726
|
|
|
1727
|
-
//# sourceMappingURL=reconcile-
|
|
1727
|
+
//# sourceMappingURL=reconcile-CPDGFQ8H.mjs.map
|