@contentrain/mcp 1.6.0 → 1.8.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.
Files changed (66) hide show
  1. package/README.md +8 -7
  2. package/dist/{apply-manager-BJ3nKEls.mjs → apply-manager-BRmB27H5.mjs} +5 -5
  3. package/dist/{apply-manager-BJ3nKEls.mjs.map → apply-manager-BRmB27H5.mjs.map} +1 -1
  4. package/dist/branch-lifecycle-7dQoF22r.d.mts +141 -0
  5. package/dist/branch-lifecycle-7dQoF22r.d.mts.map +1 -0
  6. package/dist/branch-lifecycle-D7tXOE4f.mjs +444 -0
  7. package/dist/branch-lifecycle-D7tXOE4f.mjs.map +1 -0
  8. package/dist/{config-Lw7vpK9n.mjs → config-DlFcTxFG.mjs} +3 -2
  9. package/dist/config-DlFcTxFG.mjs.map +1 -0
  10. package/dist/{context-DS40WOdw.mjs → context-CYN__o3Q.mjs} +24 -11
  11. package/dist/context-CYN__o3Q.mjs.map +1 -0
  12. package/dist/core/apply-manager.mjs +6 -6
  13. package/dist/core/config.d.mts.map +1 -1
  14. package/dist/core/config.mjs +1 -1
  15. package/dist/core/context.d.mts +20 -2
  16. package/dist/core/context.d.mts.map +1 -1
  17. package/dist/core/context.mjs +2 -2
  18. package/dist/core/doctor.mjs +3 -3
  19. package/dist/core/validator/index.mjs +2 -2
  20. package/dist/{detect-DL9mFmaS.mjs → detect-eWS6lvoH.mjs} +1 -1
  21. package/dist/{detect-DL9mFmaS.mjs.map → detect-eWS6lvoH.mjs.map} +1 -1
  22. package/dist/{doctor-Dxu_SzZy.mjs → doctor-qiUKCy9b.mjs} +34 -15
  23. package/dist/doctor-qiUKCy9b.mjs.map +1 -0
  24. package/dist/git/branch-lifecycle.d.mts +2 -57
  25. package/dist/git/branch-lifecycle.mjs +3 -3
  26. package/dist/git/transaction.d.mts +2 -0
  27. package/dist/git/transaction.d.mts.map +1 -1
  28. package/dist/git/transaction.mjs +5 -4
  29. package/dist/{id-CIO9s0x_.mjs → id-DV_T9Ic8.mjs} +1 -1
  30. package/dist/{id-CIO9s0x_.mjs.map → id-DV_T9Ic8.mjs.map} +1 -1
  31. package/dist/index.mjs +11 -11
  32. package/dist/{local-D56D9e5T.mjs → local-BZEUWBoC.mjs} +8 -11
  33. package/dist/local-BZEUWBoC.mjs.map +1 -0
  34. package/dist/providers/github/index.d.mts +23 -2
  35. package/dist/providers/github/index.d.mts.map +1 -1
  36. package/dist/providers/github/index.mjs +112 -51
  37. package/dist/providers/github/index.mjs.map +1 -1
  38. package/dist/providers/gitlab/index.d.mts +3 -1
  39. package/dist/providers/gitlab/index.d.mts.map +1 -1
  40. package/dist/providers/gitlab/index.mjs +21 -5
  41. package/dist/providers/gitlab/index.mjs.map +1 -1
  42. package/dist/providers/local/index.mjs +6 -5
  43. package/dist/server/http/index.d.mts +1 -1
  44. package/dist/server/http/index.mjs +11 -11
  45. package/dist/{server-WqGx6ait.d.mts → server-DZBcZWhE.d.mts} +1 -1
  46. package/dist/{server-WqGx6ait.d.mts.map → server-DZBcZWhE.d.mts.map} +1 -1
  47. package/dist/{server-CufhOunD.mjs → server-tTuinKHD.mjs} +56 -22
  48. package/dist/server-tTuinKHD.mjs.map +1 -0
  49. package/dist/server.d.mts +1 -1
  50. package/dist/server.mjs +11 -11
  51. package/dist/{transaction-LiTjan9Q.mjs → transaction-qliKvBRL.mjs} +104 -60
  52. package/dist/transaction-qliKvBRL.mjs.map +1 -0
  53. package/dist/util/detect.mjs +1 -1
  54. package/dist/util/id.mjs +1 -1
  55. package/dist/{validator-Bo06QPVB.mjs → validator-Z3DGNWQ6.mjs} +2 -2
  56. package/dist/{validator-Bo06QPVB.mjs.map → validator-Z3DGNWQ6.mjs.map} +1 -1
  57. package/package.json +2 -2
  58. package/dist/branch-lifecycle-DjX3MXNG.mjs +0 -149
  59. package/dist/branch-lifecycle-DjX3MXNG.mjs.map +0 -1
  60. package/dist/config-Lw7vpK9n.mjs.map +0 -1
  61. package/dist/context-DS40WOdw.mjs.map +0 -1
  62. package/dist/doctor-Dxu_SzZy.mjs.map +0 -1
  63. package/dist/git/branch-lifecycle.d.mts.map +0 -1
  64. package/dist/local-D56D9e5T.mjs.map +0 -1
  65. package/dist/server-CufhOunD.mjs.map +0 -1
  66. package/dist/transaction-LiTjan9Q.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"validator-Bo06QPVB.mjs","names":[],"sources":["../src/core/validator/entry.ts","../src/core/validator/relation-integrity.ts","../src/core/validator/schedule.ts","../src/core/validator/project.ts"],"sourcesContent":["import type { FieldDef, ModelDefinition, ValidationError, ValidationResult } from '@contentrain/types'\nimport { detectSecrets, validateFieldValue } from '@contentrain/types'\n\n/**\n * Context for cross-entry validation (unique, relations, nested references).\n *\n * Shape aligned with Studio's `content-validation.ts:ValidationContext` so\n * Studio can swap its internal implementation for this module without\n * touching call sites.\n */\nexport interface ValidationContext {\n /** All entries in the collection (for unique-constraint checks). Keyed by entry ID. */\n allEntries?: Record<string, Record<string, unknown>>\n /** ID of the entry being validated (excluded from unique-constraint comparisons). */\n currentEntryId?: string\n /** All model definitions, if available (reserved for future cross-model checks). */\n models?: ModelDefinition[]\n}\n\n/**\n * Validate a single content entry against its model's field schema.\n *\n * Merges the rule sets from MCP's legacy `validator.ts` (secret detection,\n * schema validation, unique constraints) with Studio's `content-validation.ts`\n * (email/url heuristics, polymorphic relation structure, nested object and\n * array-of-object recursion). The union is the authoritative per-entry\n * validator — both MCP's project validator and Studio's save path should\n * converge on this function over time.\n *\n * Asynchronous relation-integrity checks (does the referenced entry exist?)\n * live in `relation-integrity.ts` because they require I/O.\n */\nexport function validateContent(\n data: Record<string, unknown>,\n fields: Record<string, FieldDef>,\n modelId: string,\n locale: string,\n entryId?: string,\n ctx?: ValidationContext,\n): ValidationResult {\n const errors: ValidationError[] = []\n\n for (const [fieldId, def] of Object.entries(fields)) {\n const value = data[fieldId]\n errors.push(...validateField(value, def, modelId, locale, entryId, fieldId, ctx))\n }\n\n return {\n valid: errors.filter(e => e.severity === 'error').length === 0,\n errors,\n }\n}\n\nfunction validateField(\n value: unknown,\n def: FieldDef,\n modelId: string,\n locale: string,\n entryId: string | undefined,\n fieldId: string,\n ctx?: ValidationContext,\n): ValidationError[] {\n const errors: ValidationError[] = []\n const errCtx = { model: modelId, locale, entry: entryId, field: fieldId }\n\n if (value !== null && value !== undefined && value !== '') {\n const secretErrors = detectSecrets(value)\n for (const e of secretErrors) {\n errors.push({ ...e, ...errCtx })\n }\n }\n\n const isRelationType = def.type === 'relation' || def.type === 'relations'\n if (!isRelationType) {\n const fieldErrors = validateFieldValue(value, def)\n if (fieldErrors.length > 0) {\n for (const e of fieldErrors) errors.push({ ...e, ...errCtx })\n if (fieldErrors.some(e => e.severity === 'error')) return errors\n }\n } else if (def.required && (value === null || value === undefined || value === '')) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} is required` })\n return errors\n }\n\n if (value === null || value === undefined) return errors\n\n if (def.unique && ctx?.allEntries) {\n // String() comparison — matches legacy validator.ts uniqueness semantics\n // (catches cross-type duplicates like 42 vs '42' that strict === would miss).\n const valueKey = String(value)\n for (const [otherId, otherEntry] of Object.entries(ctx.allEntries)) {\n if (otherId === ctx.currentEntryId) continue\n const otherValue = otherEntry[fieldId]\n if (otherValue !== null && otherValue !== undefined && String(otherValue) === valueKey) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} must be unique — \"${String(value)}\" already exists in entry ${otherId}`,\n })\n break\n }\n }\n }\n\n if (def.type === 'email' && typeof value === 'string' && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {\n errors.push({ severity: 'warning', ...errCtx, message: `${fieldId} may not be a valid email` })\n }\n if (def.type === 'url' && typeof value === 'string' && !/^https?:\\/\\/.+/.test(value) && !value.startsWith('/')) {\n errors.push({ severity: 'warning', ...errCtx, message: `${fieldId} may not be a valid URL` })\n }\n\n if (def.type === 'relation' && def.model) {\n const targets = Array.isArray(def.model) ? def.model : [def.model]\n if (targets.length > 1) {\n if (typeof value !== 'object' || value === null || !('model' in value) || !('ref' in value)) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} must be { model, ref } for polymorphic relation`,\n })\n } else {\n const polyVal = value as { model: string, ref: string }\n if (!targets.includes(polyVal.model)) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} target model \"${polyVal.model}\" must be one of: ${targets.join(', ')}`,\n })\n }\n }\n } else if (typeof value !== 'string') {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must be a string (entry ID or slug)` })\n }\n }\n\n if (def.type === 'relations') {\n if (!Array.isArray(value)) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must be an array` })\n } else {\n if (def.min !== undefined && value.length < def.min) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must have at least ${def.min} items` })\n }\n if (def.max !== undefined && value.length > def.max) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must have at most ${def.max} items` })\n }\n // Polymorphic multi-relations (model targets multiple kinds) store\n // { model, ref } items — mirror the single-relation rule so content that\n // conforms to the generated type isn't rejected. Single-target relations\n // store plain id/slug strings.\n const targets = Array.isArray(def.model) ? def.model : (def.model ? [def.model] : [])\n const polymorphic = targets.length > 1\n for (let i = 0; i < value.length; i++) {\n const item = value[i]\n const itemCtx = { ...errCtx, field: `${fieldId}[${i}]` }\n if (polymorphic) {\n if (typeof item !== 'object' || item === null || !('model' in item) || !('ref' in item)) {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] must be { model, ref } for polymorphic relations` })\n } else {\n const polyItem = item as { model: string, ref: string }\n if (!targets.includes(polyItem.model)) {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] target model \"${polyItem.model}\" must be one of: ${targets.join(', ')}` })\n }\n }\n } else if (typeof item !== 'string') {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] must be a string (entry ID or slug)` })\n }\n }\n }\n }\n\n if (def.type === 'array' && Array.isArray(value) && def.items && typeof def.items === 'string') {\n for (let i = 0; i < value.length; i++) {\n errors.push(...validateArrayItemType(value[i], def.items, errCtx, `${fieldId}[${i}]`))\n }\n }\n\n if (def.type === 'object' && def.fields && typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const nested = validateContent(value as Record<string, unknown>, def.fields, modelId, locale, entryId)\n errors.push(...nested.errors)\n }\n\n if (\n def.type === 'array'\n && Array.isArray(value)\n && def.items\n && typeof def.items === 'object'\n && def.items.type === 'object'\n && def.items.fields\n ) {\n for (let i = 0; i < value.length; i++) {\n if (typeof value[i] === 'object' && value[i] !== null) {\n const nested = validateContent(value[i] as Record<string, unknown>, def.items.fields, modelId, locale, entryId)\n for (const e of nested.errors) {\n errors.push({ ...e, field: `${fieldId}[${i}].${e.field}` })\n }\n }\n }\n }\n\n return errors\n}\n\nfunction validateArrayItemType(\n value: unknown,\n itemType: string,\n errCtx: { model: string, locale: string, entry: string | undefined, field: string },\n fieldPath: string,\n): ValidationError[] {\n const ctx = { ...errCtx, field: fieldPath }\n switch (itemType) {\n case 'string':\n case 'email':\n case 'url':\n case 'slug':\n case 'image':\n case 'video':\n case 'file':\n if (typeof value !== 'string') return [{ severity: 'error', ...ctx, message: `${fieldPath} must be a string` }]\n break\n case 'number':\n case 'integer':\n case 'decimal':\n if (typeof value !== 'number') return [{ severity: 'error', ...ctx, message: `${fieldPath} must be a number` }]\n if (itemType === 'integer' && !Number.isInteger(value)) {\n return [{ severity: 'error', ...ctx, message: `${fieldPath} must be an integer` }]\n }\n break\n case 'boolean':\n if (typeof value !== 'boolean') return [{ severity: 'error', ...ctx, message: `${fieldPath} must be a boolean` }]\n break\n }\n return []\n}\n","import type { FieldDef, ValidationError } from '@contentrain/types'\n\n/**\n * Resolution of a target model for a relation integrity check.\n *\n * - `{ exists: true, content: {...} }` — target model is known and its\n * content for the requested locale was loaded. The ref must appear\n * as a key in `content`.\n * - `{ exists: true, content: null }` — target model exists (e.g. a\n * singleton or dictionary) but per-entry key enforcement does not\n * apply. The checker skips the ref existence test for this target.\n * - `{ exists: false }` — the referenced model is not defined at all.\n * The checker emits a \"target model not found\" error.\n */\nexport interface ResolvedTarget {\n exists: boolean\n content?: Record<string, unknown> | null\n}\n\n/** Back-compat loader signature used by Studio. A null return means \"nothing to check\". */\nexport type LegacyLoadContent = (\n targetModelId: string,\n targetLocale: string,\n) => Promise<Record<string, unknown> | null>\n\nexport interface CheckRelationIntegrityOptions {\n /** Severity for broken-relation errors. Default: `warning` (Studio-compat). */\n severity?: 'error' | 'warning'\n /**\n * Richer target resolver — when provided, it can signal target-model\n * absence. MCP's project validator passes one so `contentrain_validate`\n * still emits \"target model not found\" errors.\n */\n resolveTarget?: (\n targetModelId: string,\n targetLocale: string,\n ) => Promise<ResolvedTarget>\n}\n\n/**\n * Verify that relation and relations fields reference targets that actually\n * exist. Two severities are supported — Studio's per-save flow emits\n * `warning` (the referenced entry may still be drafted), while MCP's\n * project-wide validator emits `error` and additionally flags missing\n * target models via `resolveTarget`.\n *\n * The loader abstraction keeps this function I/O-agnostic: MCP wires it\n * to filesystem reads through LocalReader, Studio wires it to a\n * `GitProvider.readFile`-backed loader, mocks pass in-memory maps.\n */\nexport async function checkRelationIntegrity(\n data: Record<string, unknown>,\n fields: Record<string, FieldDef>,\n modelId: string,\n locale: string,\n entryId: string | undefined,\n loadContent: LegacyLoadContent,\n opts: CheckRelationIntegrityOptions = {},\n): Promise<ValidationError[]> {\n const errors: ValidationError[] = []\n const severity = opts.severity ?? 'warning'\n\n const resolve = opts.resolveTarget\n ?? (async (id: string, loc: string): Promise<ResolvedTarget> => {\n const content = await loadContent(id, loc)\n return { exists: true, content }\n })\n\n for (const [fieldId, def] of Object.entries(fields)) {\n const value = data[fieldId]\n if (value === null || value === undefined) continue\n\n if (def.type === 'relation' && def.model) {\n const targets = Array.isArray(def.model) ? def.model : [def.model]\n\n if (targets.length > 1 && typeof value === 'object' && value !== null) {\n const polyVal = value as { model: string, ref: string }\n if (polyVal.model && polyVal.ref) {\n const resolved = await resolve(polyVal.model, locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${polyVal.model}\" not found`,\n })\n } else if (resolved.content && !(polyVal.ref in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${polyVal.ref}\" not found in ${polyVal.model}`,\n })\n }\n }\n } else if (typeof value === 'string' && targets[0]) {\n const resolved = await resolve(targets[0], locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${targets[0]}\" not found`,\n })\n } else if (resolved.content && !(value in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${value}\" not found in ${targets[0]}`,\n })\n }\n }\n }\n\n if (def.type === 'relations' && def.model && Array.isArray(value)) {\n const target = Array.isArray(def.model) ? def.model[0] : def.model\n if (target) {\n const resolved = await resolve(target, locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${target}\" not found`,\n })\n } else if (resolved.content) {\n for (const ref of value) {\n if (typeof ref === 'string' && !(ref in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${ref}\" not found in ${target}`,\n })\n }\n }\n }\n }\n }\n }\n\n return errors\n}\n","import type { EntryMeta, ValidationError } from '@contentrain/types'\n\n/**\n * Validate `publish_at` and `expire_at` meta fields.\n *\n * Rules (matches legacy `validator.ts:validateScheduleFields`):\n * - `publish_at` must parse as a valid Date\n * - `expire_at` must parse as a valid Date\n * - When both are present, `expire_at` must be strictly after `publish_at`\n */\nexport function validateScheduleFields(\n meta: EntryMeta,\n ctx: { model: string, locale: string, entry?: string, slug?: string },\n issues: ValidationError[],\n): void {\n if (meta.publish_at !== undefined) {\n const d = new Date(meta.publish_at)\n if (Number.isNaN(d.getTime())) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `Invalid publish_at date: \"${meta.publish_at}\". Must be a valid ISO 8601 date string.`,\n })\n }\n }\n if (meta.expire_at !== undefined) {\n const d = new Date(meta.expire_at)\n if (Number.isNaN(d.getTime())) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `Invalid expire_at date: \"${meta.expire_at}\". Must be a valid ISO 8601 date string.`,\n })\n }\n }\n if (meta.publish_at !== undefined && meta.expire_at !== undefined) {\n const pubDate = new Date(meta.publish_at)\n const expDate = new Date(meta.expire_at)\n if (!Number.isNaN(pubDate.getTime()) && !Number.isNaN(expDate.getTime()) && expDate <= pubDate) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `expire_at (\"${meta.expire_at}\") must be after publish_at (\"${meta.publish_at}\").`,\n })\n }\n }\n}\n","import type { ValidationError, ModelDefinition, ContentrainConfig, EntryMeta } from '@contentrain/types'\nimport { detectSecrets } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { writeJson, writeText } from '../../util/fs.js'\nimport { readConfig } from '../config.js'\nimport { listModels, readModel } from '../model-manager.js'\nimport { writeMeta } from '../meta-manager.js'\nimport { parseFrontmatter, resolveLocaleStrategy } from '../content-manager.js'\nimport type { RepoReader } from '../contracts/index.js'\nimport { LocalReader } from '../../providers/local/reader.js'\nimport { contentDirPath, contentFilePath, documentFilePath, metaFilePath } from '../ops/paths.js'\nimport { validateContent } from './entry.js'\nimport { checkRelationIntegrity, type ResolvedTarget } from './relation-integrity.js'\nimport { validateScheduleFields } from './schedule.js'\n\n// ─── Types ───\n\nexport interface ValidateOptions {\n model?: string\n fix?: boolean\n}\n\nexport interface ValidateResult {\n valid: boolean\n summary: {\n errors: number\n warnings: number\n notices: number\n models_checked: number\n entries_checked: number\n }\n issues: ValidationError[]\n fixed: number\n}\n\n// ─── Reader-backed IO helpers ───\n\nasync function readJsonViaReader<T>(reader: RepoReader, path: string): Promise<T | null> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return null\n }\n}\n\nasync function readTextViaReader(reader: RepoReader, path: string): Promise<string | null> {\n try {\n return await reader.readFile(path)\n } catch {\n return null\n }\n}\n\n// ─── Shared field validation helpers ───\n\n/**\n * Build a `resolveTarget` adapter for `checkRelationIntegrity` that walks\n * the project's content store via the shared {@link RepoReader}. Mirrors\n * the target-resolution shape the legacy `checkRelation` used — collection\n * targets return their entry object-map, documents return a \"slug exists\"\n * marker map, singletons and dictionaries return null content so the\n * checker skips key enforcement for them.\n */\nfunction buildProjectTargetResolver(\n reader: RepoReader,\n config: ContentrainConfig,\n): (targetModelId: string, targetLocale: string) => Promise<ResolvedTarget> {\n return async (targetModelId, targetLocale) => {\n const targetModel = await readModel(reader, targetModelId)\n if (!targetModel) return { exists: false }\n\n if (targetModel.kind === 'document') {\n const docContentDir = contentDirPath(targetModel)\n const slugs = await discoverDocumentSlugs(reader, docContentDir, targetModel)\n return { exists: true, content: Object.fromEntries(slugs.map(s => [s, true])) }\n }\n if (targetModel.kind === 'singleton' || targetModel.kind === 'dictionary') {\n return { exists: true, content: null }\n }\n // Collection: resolve against the target model's own storage. i18n:false\n // targets are locale-agnostic (data.json). For i18n:true targets, fall back\n // to the default-locale file when the source locale lacks the entry — a\n // relation legitimately points at an entry that may be translated under\n // another locale, so the id set is merged to avoid phantom broken refs. #Y5\n const merged: Record<string, unknown> = {}\n const primary = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, targetLocale),\n )\n if (primary) Object.assign(merged, primary)\n if (targetModel.i18n && targetLocale !== config.locales.default) {\n const fallback = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, config.locales.default),\n )\n if (fallback) Object.assign(merged, fallback)\n }\n return { exists: true, content: merged }\n }\n}\n\n/**\n * Scan an entry's data fields for detected secrets in UNDECLARED keys —\n * the legacy validator also flagged stray/rogue fields that were not in\n * `model.fields`. `validateContent` only knows about declared fields, so\n * this complementary pass preserves that coverage.\n */\nfunction scanUndeclaredFieldsForSecrets(\n data: Record<string, unknown>,\n declared: Record<string, unknown> | undefined,\n ctx: { model: string, locale: string, entry?: string, slug?: string },\n issues: ValidationError[],\n): void {\n for (const [fieldName, value] of Object.entries(data)) {\n if (declared && fieldName in declared) continue\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n ...ctx,\n field: fieldName,\n message: `Potential secret detected in field \"${fieldName}\"`,\n })\n }\n }\n}\n\n// ─── Per-model validators ───\n\nasync function validateCollectionModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n const locales = model.i18n ? config.locales.supported : [config.locales.default]\n\n // Collect all entry IDs per locale for parity check\n const localeEntryIds: Record<string, Set<string>> = {}\n const allEntryIds = new Set<string>()\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n\n for (const locale of locales) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, Record<string, unknown>>>(reader, filePath)\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n const ids = new Set(Object.keys(data))\n localeEntryIds[locale] = ids\n for (const id of ids) allEntryIds.add(id)\n\n // Check canonical sort\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, Record<string, unknown>> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate each entry — validateContent covers secret/field/unique/email-url/\n // polymorphic/nested on declared fields; supplementary scans below catch\n // undeclared-field secrets and async relation integrity.\n for (const [entryId, fields] of Object.entries(data)) {\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(\n fields,\n model.fields,\n { model: model.id, locale, entry: entryId },\n issues,\n )\n\n if (!model.fields) continue\n\n const entryResult = validateContent(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n { allEntries: data, currentEntryId: entryId },\n )\n issues.push(...entryResult.errors)\n\n const relationErrors = await checkRelationIntegrity(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\n }\n }\n\n // Entry parity check (i18n)\n if (model.i18n && Object.keys(localeEntryIds).length > 1) {\n const localeKeys = Object.keys(localeEntryIds)\n for (let i = 1; i < localeKeys.length; i++) {\n const locA = localeKeys[0]!\n const locB = localeKeys[i]!\n const idsA = localeEntryIds[locA]!\n const idsB = localeEntryIds[locB]!\n\n for (const id of idsA) {\n if (!idsB.has(id)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale: locB,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locA} but missing in ${locB}`,\n })\n }\n }\n for (const id of idsB) {\n if (!idsA.has(id)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale: locA,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locB} but missing in ${locA}`,\n })\n }\n }\n }\n }\n\n // Orphan meta check\n for (const locale of locales) {\n const metaRelPath = metaFilePath(model, locale)\n const metaData = await readJsonViaReader<Record<string, EntryMeta>>(reader, metaRelPath)\n const contentData = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(model, locale),\n ) ?? {}\n\n // Forward check: meta entries without content + schedule validation\n if (metaData) {\n for (const metaEntryId of Object.keys(metaData)) {\n // Validate schedule fields on every meta entry\n validateScheduleFields(metaData[metaEntryId]!, { model: model.id, locale, entry: metaEntryId }, issues)\n\n if (!(metaEntryId in contentData)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n entry: metaEntryId,\n message: `Orphan meta: meta entry \"${metaEntryId}\" exists but content entry missing`,\n })\n if (fix && projectRoot) {\n delete metaData[metaEntryId]\n const metaAbs = join(projectRoot, metaRelPath)\n if (Object.keys(metaData).length > 0) {\n await writeJson(metaAbs, metaData)\n } else {\n await rm(metaAbs, { force: true })\n }\n fixed++\n }\n }\n }\n }\n\n // Reverse check: content entries without meta\n for (const entryId of Object.keys(contentData)) {\n if (!metaData || !(entryId in metaData)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n entry: entryId,\n message: `Orphan content: entry \"${entryId}\" has no metadata`,\n })\n if (fix && projectRoot) {\n await writeMeta(projectRoot, model, { locale, entryId }, {\n status: 'draft',\n source: 'import',\n updated_by: 'contentrain-mcp',\n })\n fixed++\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function validateSingletonModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, unknown>>(reader, filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(data, model.fields, { model: model.id, locale }, issues)\n\n if (model.fields) {\n const entryResult = validateContent(data, model.fields, model.id, locale)\n issues.push(...entryResult.errors)\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n const relationErrors = await checkRelationIntegrity(\n data,\n model.fields,\n model.id,\n locale,\n undefined,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, unknown> = {}\n for (const key of sorted) {\n resorted[key] = data[key]\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate schedule fields in singleton meta\n const singletonMetaData = await readJsonViaReader<EntryMeta>(reader, metaFilePath(model, locale))\n if (singletonMetaData) {\n validateScheduleFields(singletonMetaData, { model: model.id, locale }, issues)\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function validateDictionaryModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n\n const localeKeys: Record<string, Set<string>> = {}\n\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, string>>(reader, filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n localeKeys[locale] = new Set(Object.keys(data))\n\n // Secret detection\n for (const [key, value] of Object.entries(data)) {\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: key,\n message: `Potential secret detected in key \"${key}\"`,\n })\n }\n }\n\n // Duplicate value detection\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n for (const [value, dupeKeys] of valueToKeys) {\n if (dupeKeys.length > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: `Duplicate value \"${truncated}\" mapped to ${dupeKeys.length} keys: [${dupeKeys.join(', ')}]`,\n })\n }\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, string> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n }\n\n // Key parity check\n if (model.i18n && Object.keys(localeKeys).length > 1) {\n const localeNames = Object.keys(localeKeys)\n for (let i = 1; i < localeNames.length; i++) {\n const locA = localeNames[0]!\n const locB = localeNames[i]!\n const keysA = localeKeys[locA]!\n const keysB = localeKeys[locB]!\n\n for (const k of keysA) {\n if (!keysB.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locB,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locA} but missing in ${locB}`,\n })\n }\n }\n for (const k of keysB) {\n if (!keysA.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locA,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locB} but missing in ${locA}`,\n })\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function discoverDocumentSlugs(\n reader: RepoReader,\n cDir: string,\n model: ModelDefinition,\n): Promise<string[]> {\n const strategy = resolveLocaleStrategy(model)\n const entries = await reader.listDirectory(cDir)\n\n // When i18n is disabled, documents are flat {slug}.md files\n if (!model.i18n) {\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n }\n\n if (strategy === 'file') {\n // Each slug is a subdirectory\n return entries.filter(e => !e.startsWith('.'))\n }\n\n if (strategy === 'suffix') {\n // Files like {slug}.{locale}.md — extract unique slugs\n const slugs = new Set<string>()\n for (const f of entries) {\n if (!f.endsWith('.md')) continue\n // Remove last two dot-separated segments (.locale.md)\n const parts = f.replace(/\\.md$/, '').split('.')\n if (parts.length >= 2) {\n parts.pop() // remove locale\n slugs.add(parts.join('.'))\n }\n }\n return [...slugs]\n }\n\n if (strategy === 'directory') {\n // Locale subdirectories contain {slug}.md files — collect slugs from all locale dirs\n const slugs = new Set<string>()\n const localeLists = await Promise.all(\n entries\n .filter(localeDir => !localeDir.startsWith('.'))\n .map(localeDir => reader.listDirectory(`${cDir}/${localeDir}`)),\n )\n for (const files of localeLists) {\n for (const f of files) {\n if (f.endsWith('.md')) slugs.add(f.replace(/\\.md$/, ''))\n }\n }\n return [...slugs]\n }\n\n // 'none' — {slug}.md files, no locale\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n}\n\nasync function validateDocumentModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n const cDir = contentDirPath(model)\n\n if (!await reader.fileExists(cDir)) return { entries: 0, fixed: 0 }\n\n const slugs = await discoverDocumentSlugs(reader, cDir, model)\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n const locales = model.i18n ? config.locales.supported : [config.locales.default]\n\n for (const slug of slugs) {\n if (slug.startsWith('.')) continue\n\n for (const locale of locales) {\n const filePath = documentFilePath(model, locale, slug)\n const raw = await readTextViaReader(reader, filePath)\n\n if (!raw) {\n if (model.i18n) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n slug,\n message: `Missing translation: document \"${slug}\" missing ${locale} locale file`,\n })\n if (fix && projectRoot) {\n // Create empty template\n const template = `---\\nslug: ${slug}\\n---\\n`\n await writeText(join(projectRoot, filePath), template)\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n const { frontmatter, body } = parseFrontmatter(raw)\n\n // Always scan undeclared frontmatter fields + body for secrets.\n scanUndeclaredFieldsForSecrets(\n frontmatter,\n // Treat `body` as a declared field so the undeclared-scan skips it; body\n // is scanned separately below.\n { ...model.fields, body: true } as Record<string, unknown>,\n { model: model.id, locale, slug },\n issues,\n )\n\n if (detectSecrets(body).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: 'body',\n message: 'Potential secret detected in document body',\n })\n }\n\n if (model.fields) {\n const fieldsWithoutBody = Object.fromEntries(\n Object.entries(model.fields).filter(([name]) => name !== 'body'),\n )\n const entryResult = validateContent(\n frontmatter,\n fieldsWithoutBody,\n model.id,\n locale,\n )\n for (const err of entryResult.errors) {\n issues.push({ ...err, slug })\n }\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n const relationErrors = await checkRelationIntegrity(\n frontmatter,\n fieldsWithoutBody,\n model.id,\n locale,\n undefined,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n for (const err of relationErrors) {\n issues.push({ ...err, slug })\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\n// ─── Orphan content check ───\n\nasync function checkOrphanContent(\n reader: RepoReader,\n validModelIds: Set<string>,\n issues: ValidationError[],\n _fix: boolean,\n): Promise<number> {\n const fixed = 0\n const contentBase = '.contentrain/content'\n const domains = await reader.listDirectory(contentBase)\n\n const modelLists = await Promise.all(\n domains\n .filter(d => !d.startsWith('.'))\n .map(async d => ({ domain: d, dirs: await reader.listDirectory(`${contentBase}/${d}`) })),\n )\n\n for (const { dirs } of modelLists) {\n for (const modelDir of dirs) {\n if (modelDir.startsWith('.')) continue\n if (!validModelIds.has(modelDir)) {\n issues.push({\n severity: 'warning',\n model: modelDir,\n message: `Orphan content: content directory exists for deleted model \"${modelDir}\"`,\n })\n }\n }\n }\n\n return fixed\n}\n\n// ─── Main validation function ───\n\n/**\n * Validate a project's content against its model schemas.\n *\n * Two signatures:\n *\n * - `validateProject(projectRoot, options?)` — legacy disk-backed flow.\n * `options.fix: true` applies structural repairs (canonical sort,\n * orphan meta, missing locale files) directly on disk.\n * - `validateProject(reader, options?)` — reader-backed flow used by\n * HTTP/remote callers (e.g. GitHubProvider). `options.fix` is ignored\n * because remote flows cannot write to the local filesystem.\n *\n * Read-side behavior is identical across both signatures so Studio and\n * local CLIs see the same issue set for the same content state.\n */\nexport async function validateProject(projectRoot: string, options?: ValidateOptions): Promise<ValidateResult>\nexport async function validateProject(reader: RepoReader, options?: ValidateOptions): Promise<ValidateResult>\nexport async function validateProject(\n input: string | RepoReader,\n options?: ValidateOptions,\n): Promise<ValidateResult> {\n const reader: RepoReader = typeof input === 'string' ? new LocalReader(input) : input\n const projectRoot: string | undefined = typeof input === 'string' ? input : undefined\n const fix = Boolean(options?.fix) && projectRoot !== undefined\n\n const issues: ValidationError[] = []\n let totalEntries = 0\n let totalFixed = 0\n let modelsChecked = 0\n\n const config = await readConfig(reader)\n if (!config) {\n return {\n valid: false,\n summary: { errors: 1, warnings: 0, notices: 0, models_checked: 0, entries_checked: 0 },\n issues: [{ severity: 'error', message: 'Project not initialized: config.json missing' }],\n fixed: 0,\n }\n }\n\n const modelSummaries = await listModels(reader)\n const validModelIds = new Set(modelSummaries.map(m => m.id))\n const modelsToCheck = options?.model\n ? modelSummaries.filter(m => m.id === options.model)\n : modelSummaries\n\n for (const summary of modelsToCheck) {\n const model = await readModel(reader, summary.id)\n if (!model) continue\n\n modelsChecked++\n let result: { entries: number; fixed: number }\n\n switch (model.kind) {\n case 'collection':\n result = await validateCollectionModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'singleton':\n result = await validateSingletonModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'dictionary':\n result = await validateDictionaryModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'document':\n result = await validateDocumentModel(reader, projectRoot, model, config, issues, fix)\n break\n default:\n result = { entries: 0, fixed: 0 }\n }\n\n totalEntries += result.entries\n totalFixed += result.fixed\n }\n\n // Orphan content check (only when checking all models)\n if (!options?.model) {\n totalFixed += await checkOrphanContent(reader, validModelIds, issues, fix)\n }\n\n // Cross-dictionary duplicate value detection (only when checking all models)\n if (!options?.model) {\n const dictModels = modelsToCheck.filter(m => m.kind === 'dictionary')\n if (dictModels.length > 1) {\n const globalValueMap: Record<string, Map<string, Array<{ model: string; key: string }>>> = {}\n\n for (const summary of dictModels) {\n const model = await readModel(reader, summary.id)\n if (!model) continue\n\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n if (!globalValueMap[locale]) globalValueMap[locale] = new Map()\n const data = await readJsonViaReader<Record<string, string>>(reader, contentFilePath(model, locale))\n if (!data) continue\n\n for (const [key, value] of Object.entries(data)) {\n const refs = globalValueMap[locale]!.get(value)\n if (refs) refs.push({ model: model.id, key })\n else globalValueMap[locale]!.set(value, [{ model: model.id, key }])\n }\n }\n }\n\n for (const [locale, valueMap] of Object.entries(globalValueMap)) {\n for (const [value, refs] of valueMap) {\n const uniqueModels = new Set(refs.map(r => r.model))\n if (uniqueModels.size > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'notice',\n locale,\n message: `Cross-model duplicate value \"${truncated}\" in ${refs.map(r => `${r.model}/${r.key}`).join(', ')}`,\n })\n }\n }\n }\n }\n }\n\n const errors = issues.filter(i => i.severity === 'error').length\n const warnings = issues.filter(i => i.severity === 'warning').length\n const notices = issues.filter(i => i.severity === 'notice').length\n\n return {\n valid: errors === 0,\n summary: {\n errors,\n warnings,\n notices,\n models_checked: modelsChecked,\n entries_checked: totalEntries,\n },\n issues,\n fixed: totalFixed,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,gBACd,MACA,QACA,SACA,QACA,SACA,KACkB;CAClB,MAAM,SAA4B,EAAE;AAEpC,MAAK,MAAM,CAAC,SAAS,QAAQ,OAAO,QAAQ,OAAO,EAAE;EACnD,MAAM,QAAQ,KAAK;AACnB,SAAO,KAAK,GAAG,cAAc,OAAO,KAAK,SAAS,QAAQ,SAAS,SAAS,IAAI,CAAC;;AAGnF,QAAO;EACL,OAAO,OAAO,QAAO,MAAK,EAAE,aAAa,QAAQ,CAAC,WAAW;EAC7D;EACD;;AAGH,SAAS,cACP,OACA,KACA,SACA,QACA,SACA,SACA,KACmB;CACnB,MAAM,SAA4B,EAAE;CACpC,MAAM,SAAS;EAAE,OAAO;EAAS;EAAQ,OAAO;EAAS,OAAO;EAAS;AAEzE,KAAI,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,IAAI;EACzD,MAAM,eAAe,cAAc,MAAM;AACzC,OAAK,MAAM,KAAK,aACd,QAAO,KAAK;GAAE,GAAG;GAAG,GAAG;GAAQ,CAAC;;AAKpC,KAAI,EADmB,IAAI,SAAS,cAAc,IAAI,SAAS,cAC1C;EACnB,MAAM,cAAc,mBAAmB,OAAO,IAAI;AAClD,MAAI,YAAY,SAAS,GAAG;AAC1B,QAAK,MAAM,KAAK,YAAa,QAAO,KAAK;IAAE,GAAG;IAAG,GAAG;IAAQ,CAAC;AAC7D,OAAI,YAAY,MAAK,MAAK,EAAE,aAAa,QAAQ,CAAE,QAAO;;YAEnD,IAAI,aAAa,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,KAAK;AAClF,SAAO,KAAK;GAAE,UAAU;GAAS,GAAG;GAAQ,SAAS,GAAG,QAAQ;GAAe,CAAC;AAChF,SAAO;;AAGT,KAAI,UAAU,QAAQ,UAAU,KAAA,EAAW,QAAO;AAElD,KAAI,IAAI,UAAU,KAAK,YAAY;EAGjC,MAAM,WAAW,OAAO,MAAM;AAC9B,OAAK,MAAM,CAAC,SAAS,eAAe,OAAO,QAAQ,IAAI,WAAW,EAAE;AAClE,OAAI,YAAY,IAAI,eAAgB;GACpC,MAAM,aAAa,WAAW;AAC9B,OAAI,eAAe,QAAQ,eAAe,KAAA,KAAa,OAAO,WAAW,KAAK,UAAU;AACtF,WAAO,KAAK;KACV,UAAU;KACV,GAAG;KACH,SAAS,GAAG,QAAQ,qBAAqB,OAAO,MAAM,CAAC,4BAA4B;KACpF,CAAC;AACF;;;;AAKN,KAAI,IAAI,SAAS,WAAW,OAAO,UAAU,YAAY,CAAC,6BAA6B,KAAK,MAAM,CAChG,QAAO,KAAK;EAAE,UAAU;EAAW,GAAG;EAAQ,SAAS,GAAG,QAAQ;EAA4B,CAAC;AAEjG,KAAI,IAAI,SAAS,SAAS,OAAO,UAAU,YAAY,CAAC,iBAAiB,KAAK,MAAM,IAAI,CAAC,MAAM,WAAW,IAAI,CAC5G,QAAO,KAAK;EAAE,UAAU;EAAW,GAAG;EAAQ,SAAS,GAAG,QAAQ;EAA0B,CAAC;AAG/F,KAAI,IAAI,SAAS,cAAc,IAAI,OAAO;EACxC,MAAM,UAAU,MAAM,QAAQ,IAAI,MAAM,GAAG,IAAI,QAAQ,CAAC,IAAI,MAAM;AAClE,MAAI,QAAQ,SAAS,EACnB,KAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,EAAE,WAAW,UAAU,EAAE,SAAS,OACnF,QAAO,KAAK;GACV,UAAU;GACV,GAAG;GACH,SAAS,GAAG,QAAQ;GACrB,CAAC;OACG;GACL,MAAM,UAAU;AAChB,OAAI,CAAC,QAAQ,SAAS,QAAQ,MAAM,CAClC,QAAO,KAAK;IACV,UAAU;IACV,GAAG;IACH,SAAS,GAAG,QAAQ,iBAAiB,QAAQ,MAAM,oBAAoB,QAAQ,KAAK,KAAK;IAC1F,CAAC;;WAGG,OAAO,UAAU,SAC1B,QAAO,KAAK;GAAE,UAAU;GAAS,GAAG;GAAQ,SAAS,GAAG,QAAQ;GAAuC,CAAC;;AAI5G,KAAI,IAAI,SAAS,YACf,KAAI,CAAC,MAAM,QAAQ,MAAM,CACvB,QAAO,KAAK;EAAE,UAAU;EAAS,GAAG;EAAQ,SAAS,GAAG,QAAQ;EAAoB,CAAC;MAChF;AACL,MAAI,IAAI,QAAQ,KAAA,KAAa,MAAM,SAAS,IAAI,IAC9C,QAAO,KAAK;GAAE,UAAU;GAAS,GAAG;GAAQ,SAAS,GAAG,QAAQ,sBAAsB,IAAI,IAAI;GAAS,CAAC;AAE1G,MAAI,IAAI,QAAQ,KAAA,KAAa,MAAM,SAAS,IAAI,IAC9C,QAAO,KAAK;GAAE,UAAU;GAAS,GAAG;GAAQ,SAAS,GAAG,QAAQ,qBAAqB,IAAI,IAAI;GAAS,CAAC;EAMzG,MAAM,UAAU,MAAM,QAAQ,IAAI,MAAM,GAAG,IAAI,QAAS,IAAI,QAAQ,CAAC,IAAI,MAAM,GAAG,EAAE;EACpF,MAAM,cAAc,QAAQ,SAAS;AACrC,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACrC,MAAM,OAAO,MAAM;GACnB,MAAM,UAAU;IAAE,GAAG;IAAQ,OAAO,GAAG,QAAQ,GAAG,EAAE;IAAI;AACxD,OAAI,YACF,KAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,EAAE,WAAW,SAAS,EAAE,SAAS,MAChF,QAAO,KAAK;IAAE,UAAU;IAAS,GAAG;IAAS,SAAS,GAAG,QAAQ,GAAG,EAAE;IAAqD,CAAC;QACvH;IACL,MAAM,WAAW;AACjB,QAAI,CAAC,QAAQ,SAAS,SAAS,MAAM,CACnC,QAAO,KAAK;KAAE,UAAU;KAAS,GAAG;KAAS,SAAS,GAAG,QAAQ,GAAG,EAAE,kBAAkB,SAAS,MAAM,oBAAoB,QAAQ,KAAK,KAAK;KAAI,CAAC;;YAG7I,OAAO,SAAS,SACzB,QAAO,KAAK;IAAE,UAAU;IAAS,GAAG;IAAS,SAAS,GAAG,QAAQ,GAAG,EAAE;IAAwC,CAAC;;;AAMvH,KAAI,IAAI,SAAS,WAAW,MAAM,QAAQ,MAAM,IAAI,IAAI,SAAS,OAAO,IAAI,UAAU,SACpF,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAChC,QAAO,KAAK,GAAG,sBAAsB,MAAM,IAAI,IAAI,OAAO,QAAQ,GAAG,QAAQ,GAAG,EAAE,GAAG,CAAC;AAI1F,KAAI,IAAI,SAAS,YAAY,IAAI,UAAU,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,MAAM,EAAE;EAC/G,MAAM,SAAS,gBAAgB,OAAkC,IAAI,QAAQ,SAAS,QAAQ,QAAQ;AACtG,SAAO,KAAK,GAAG,OAAO,OAAO;;AAG/B,KACE,IAAI,SAAS,WACV,MAAM,QAAQ,MAAM,IACpB,IAAI,SACJ,OAAO,IAAI,UAAU,YACrB,IAAI,MAAM,SAAS,YACnB,IAAI,MAAM;OAER,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAChC,KAAI,OAAO,MAAM,OAAO,YAAY,MAAM,OAAO,MAAM;GACrD,MAAM,SAAS,gBAAgB,MAAM,IAA+B,IAAI,MAAM,QAAQ,SAAS,QAAQ,QAAQ;AAC/G,QAAK,MAAM,KAAK,OAAO,OACrB,QAAO,KAAK;IAAE,GAAG;IAAG,OAAO,GAAG,QAAQ,GAAG,EAAE,IAAI,EAAE;IAAS,CAAC;;;AAMnE,QAAO;;AAGT,SAAS,sBACP,OACA,UACA,QACA,WACmB;CACnB,MAAM,MAAM;EAAE,GAAG;EAAQ,OAAO;EAAW;AAC3C,SAAQ,UAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;AACH,OAAI,OAAO,UAAU,SAAU,QAAO,CAAC;IAAE,UAAU;IAAS,GAAG;IAAK,SAAS,GAAG,UAAU;IAAoB,CAAC;AAC/G;EACF,KAAK;EACL,KAAK;EACL,KAAK;AACH,OAAI,OAAO,UAAU,SAAU,QAAO,CAAC;IAAE,UAAU;IAAS,GAAG;IAAK,SAAS,GAAG,UAAU;IAAoB,CAAC;AAC/G,OAAI,aAAa,aAAa,CAAC,OAAO,UAAU,MAAM,CACpD,QAAO,CAAC;IAAE,UAAU;IAAS,GAAG;IAAK,SAAS,GAAG,UAAU;IAAsB,CAAC;AAEpF;EACF,KAAK;AACH,OAAI,OAAO,UAAU,UAAW,QAAO,CAAC;IAAE,UAAU;IAAS,GAAG;IAAK,SAAS,GAAG,UAAU;IAAqB,CAAC;AACjH;;AAEJ,QAAO,EAAE;;;;;;;;;;;;;;;ACrLX,eAAsB,uBACpB,MACA,QACA,SACA,QACA,SACA,aACA,OAAsC,EAAE,EACZ;CAC5B,MAAM,SAA4B,EAAE;CACpC,MAAM,WAAW,KAAK,YAAY;CAElC,MAAM,UAAU,KAAK,kBACf,OAAO,IAAY,QAAyC;AAE9D,SAAO;GAAE,QAAQ;GAAM,SADP,MAAM,YAAY,IAAI,IAAI;GACV;;AAGpC,MAAK,MAAM,CAAC,SAAS,QAAQ,OAAO,QAAQ,OAAO,EAAE;EACnD,MAAM,QAAQ,KAAK;AACnB,MAAI,UAAU,QAAQ,UAAU,KAAA,EAAW;AAE3C,MAAI,IAAI,SAAS,cAAc,IAAI,OAAO;GACxC,MAAM,UAAU,MAAM,QAAQ,IAAI,MAAM,GAAG,IAAI,QAAQ,CAAC,IAAI,MAAM;AAElE,OAAI,QAAQ,SAAS,KAAK,OAAO,UAAU,YAAY,UAAU,MAAM;IACrE,MAAM,UAAU;AAChB,QAAI,QAAQ,SAAS,QAAQ,KAAK;KAChC,MAAM,WAAW,MAAM,QAAQ,QAAQ,OAAO,OAAO;AACrD,SAAI,CAAC,SAAS,OACZ,QAAO,KAAK;MACV;MACA,OAAO;MACP;MACA,OAAO;MACP,OAAO;MACP,SAAS,kCAAkC,QAAQ,MAAM;MAC1D,CAAC;cACO,SAAS,WAAW,EAAE,QAAQ,OAAO,SAAS,SACvD,QAAO,KAAK;MACV;MACA,OAAO;MACP;MACA,OAAO;MACP,OAAO;MACP,SAAS,qBAAqB,QAAQ,IAAI,iBAAiB,QAAQ;MACpE,CAAC;;cAGG,OAAO,UAAU,YAAY,QAAQ,IAAI;IAClD,MAAM,WAAW,MAAM,QAAQ,QAAQ,IAAI,OAAO;AAClD,QAAI,CAAC,SAAS,OACZ,QAAO,KAAK;KACV;KACA,OAAO;KACP;KACA,OAAO;KACP,OAAO;KACP,SAAS,kCAAkC,QAAQ,GAAG;KACvD,CAAC;aACO,SAAS,WAAW,EAAE,SAAS,SAAS,SACjD,QAAO,KAAK;KACV;KACA,OAAO;KACP;KACA,OAAO;KACP,OAAO;KACP,SAAS,qBAAqB,MAAM,iBAAiB,QAAQ;KAC9D,CAAC;;;AAKR,MAAI,IAAI,SAAS,eAAe,IAAI,SAAS,MAAM,QAAQ,MAAM,EAAE;GACjE,MAAM,SAAS,MAAM,QAAQ,IAAI,MAAM,GAAG,IAAI,MAAM,KAAK,IAAI;AAC7D,OAAI,QAAQ;IACV,MAAM,WAAW,MAAM,QAAQ,QAAQ,OAAO;AAC9C,QAAI,CAAC,SAAS,OACZ,QAAO,KAAK;KACV;KACA,OAAO;KACP;KACA,OAAO;KACP,OAAO;KACP,SAAS,kCAAkC,OAAO;KACnD,CAAC;aACO,SAAS;UACb,MAAM,OAAO,MAChB,KAAI,OAAO,QAAQ,YAAY,EAAE,OAAO,SAAS,SAC/C,QAAO,KAAK;MACV;MACA,OAAO;MACP;MACA,OAAO;MACP,OAAO;MACP,SAAS,qBAAqB,IAAI,iBAAiB;MACpD,CAAC;;;;;AAQd,QAAO;;;;;;;;;;;;AChJT,SAAgB,uBACd,MACA,KACA,QACM;AACN,KAAI,KAAK,eAAe,KAAA,GAAW;EACjC,MAAM,IAAI,IAAI,KAAK,KAAK,WAAW;AACnC,MAAI,OAAO,MAAM,EAAE,SAAS,CAAC,CAC3B,QAAO,KAAK;GACV,UAAU;GACV,GAAG;GACH,SAAS,6BAA6B,KAAK,WAAW;GACvD,CAAC;;AAGN,KAAI,KAAK,cAAc,KAAA,GAAW;EAChC,MAAM,IAAI,IAAI,KAAK,KAAK,UAAU;AAClC,MAAI,OAAO,MAAM,EAAE,SAAS,CAAC,CAC3B,QAAO,KAAK;GACV,UAAU;GACV,GAAG;GACH,SAAS,4BAA4B,KAAK,UAAU;GACrD,CAAC;;AAGN,KAAI,KAAK,eAAe,KAAA,KAAa,KAAK,cAAc,KAAA,GAAW;EACjE,MAAM,UAAU,IAAI,KAAK,KAAK,WAAW;EACzC,MAAM,UAAU,IAAI,KAAK,KAAK,UAAU;AACxC,MAAI,CAAC,OAAO,MAAM,QAAQ,SAAS,CAAC,IAAI,CAAC,OAAO,MAAM,QAAQ,SAAS,CAAC,IAAI,WAAW,QACrF,QAAO,KAAK;GACV,UAAU;GACV,GAAG;GACH,SAAS,eAAe,KAAK,UAAU,gCAAgC,KAAK,WAAW;GACxF,CAAC;;;;;ACLR,eAAe,kBAAqB,QAAoB,MAAiC;AACvF,KAAI;AACF,SAAO,KAAK,MAAM,MAAM,OAAO,SAAS,KAAK,CAAC;SACxC;AACN,SAAO;;;AAIX,eAAe,kBAAkB,QAAoB,MAAsC;AACzF,KAAI;AACF,SAAO,MAAM,OAAO,SAAS,KAAK;SAC5B;AACN,SAAO;;;;;;;;;;;AAcX,SAAS,2BACP,QACA,QAC0E;AAC1E,QAAO,OAAO,eAAe,iBAAiB;EAC5C,MAAM,cAAc,MAAM,UAAU,QAAQ,cAAc;AAC1D,MAAI,CAAC,YAAa,QAAO,EAAE,QAAQ,OAAO;AAE1C,MAAI,YAAY,SAAS,YAAY;GAEnC,MAAM,QAAQ,MAAM,sBAAsB,QADpB,eAAe,YAAY,EACgB,YAAY;AAC7E,UAAO;IAAE,QAAQ;IAAM,SAAS,OAAO,YAAY,MAAM,KAAI,MAAK,CAAC,GAAG,KAAK,CAAC,CAAC;IAAE;;AAEjF,MAAI,YAAY,SAAS,eAAe,YAAY,SAAS,aAC3D,QAAO;GAAE,QAAQ;GAAM,SAAS;GAAM;EAOxC,MAAM,SAAkC,EAAE;EAC1C,MAAM,UAAU,MAAM,kBACpB,QACA,gBAAgB,aAAa,aAAa,CAC3C;AACD,MAAI,QAAS,QAAO,OAAO,QAAQ,QAAQ;AAC3C,MAAI,YAAY,QAAQ,iBAAiB,OAAO,QAAQ,SAAS;GAC/D,MAAM,WAAW,MAAM,kBACrB,QACA,gBAAgB,aAAa,OAAO,QAAQ,QAAQ,CACrD;AACD,OAAI,SAAU,QAAO,OAAO,QAAQ,SAAS;;AAE/C,SAAO;GAAE,QAAQ;GAAM,SAAS;GAAQ;;;;;;;;;AAU5C,SAAS,+BACP,MACA,UACA,KACA,QACM;AACN,MAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,KAAK,EAAE;AACrD,MAAI,YAAY,aAAa,SAAU;AACvC,MAAI,cAAc,MAAM,CAAC,SAAS,EAChC,QAAO,KAAK;GACV,UAAU;GACV,GAAG;GACH,OAAO;GACP,SAAS,uCAAuC,UAAU;GAC3D,CAAC;;;AAOR,eAAe,wBACb,QACA,aACA,OACA,QACA,QACA,KAC6C;CAC7C,IAAI,iBAAiB;CACrB,IAAI,QAAQ;CAIZ,MAAM,UAAU,MAAM,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,QAAQ,QAAQ;CAGhF,MAAM,iBAA8C,EAAE;CACtD,MAAM,8BAAc,IAAI,KAAa;CAErC,MAAM,gBAAgB,2BAA2B,QAAQ,OAAO;AAEhE,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,WAAW,gBAAgB,OAAO,OAAO;EAC/C,MAAM,OAAO,MAAM,kBAA2D,QAAQ,SAAS;AAC/F,MAAI,CAAC,MAAM;AACT,OAAI,MAAM,MAAM;AACd,WAAO,KAAK;KACV,UAAU;KACV,OAAO,MAAM;KACb;KACA,SAAS,wBAAwB,OAAO;KACzC,CAAC;AAEF,QAAI,OAAO,aAAa;AACtB,WAAM,UAAU,KAAK,aAAa,SAAS,EAAE,EAAE,CAAC;AAChD;;;AAGJ;;EAGF,MAAM,MAAM,IAAI,IAAI,OAAO,KAAK,KAAK,CAAC;AACtC,iBAAe,UAAU;AACzB,OAAK,MAAM,MAAM,IAAK,aAAY,IAAI,GAAG;EAGzC,MAAM,OAAO,OAAO,KAAK,KAAK;EAC9B,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,UAAU;AACnC,MAAI,KAAK,KAAK,IAAI,KAAK,OAAO,KAAK,IAAI,EAAE;AACvC,UAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA,SAAS;IACV,CAAC;AACF,OAAI,OAAO,aAAa;IACtB,MAAM,WAAoD,EAAE;AAC5D,SAAK,MAAM,OAAO,OAChB,UAAS,OAAO,KAAK;AAEvB,UAAM,UAAU,KAAK,aAAa,SAAS,EAAE,SAAS;AACtD;;;AAOJ,OAAK,MAAM,CAAC,SAAS,WAAW,OAAO,QAAQ,KAAK,EAAE;AACpD;AAEA,kCACE,QACA,MAAM,QACN;IAAE,OAAO,MAAM;IAAI;IAAQ,OAAO;IAAS,EAC3C,OACD;AAED,OAAI,CAAC,MAAM,OAAQ;GAEnB,MAAM,cAAc,gBAClB,QACA,MAAM,QACN,MAAM,IACN,QACA,SACA;IAAE,YAAY;IAAM,gBAAgB;IAAS,CAC9C;AACD,UAAO,KAAK,GAAG,YAAY,OAAO;GAElC,MAAM,iBAAiB,MAAM,uBAC3B,QACA,MAAM,QACN,MAAM,IACN,QACA,SACA,YAAY,MACZ;IAAE,UAAU;IAAS;IAAe,CACrC;AACD,UAAO,KAAK,GAAG,eAAe;;;AAKlC,KAAI,MAAM,QAAQ,OAAO,KAAK,eAAe,CAAC,SAAS,GAAG;EACxD,MAAM,aAAa,OAAO,KAAK,eAAe;AAC9C,OAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;GAC1C,MAAM,OAAO,WAAW;GACxB,MAAM,OAAO,WAAW;GACxB,MAAM,OAAO,eAAe;GAC5B,MAAM,OAAO,eAAe;AAE5B,QAAK,MAAM,MAAM,KACf,KAAI,CAAC,KAAK,IAAI,GAAG,CACf,QAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb,QAAQ;IACR,OAAO;IACP,SAAS,wBAAwB,GAAG,cAAc,KAAK,kBAAkB;IAC1E,CAAC;AAGN,QAAK,MAAM,MAAM,KACf,KAAI,CAAC,KAAK,IAAI,GAAG,CACf,QAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb,QAAQ;IACR,OAAO;IACP,SAAS,wBAAwB,GAAG,cAAc,KAAK,kBAAkB;IAC1E,CAAC;;;AAOV,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,cAAc,aAAa,OAAO,OAAO;EAC/C,MAAM,WAAW,MAAM,kBAA6C,QAAQ,YAAY;EACxF,MAAM,cAAc,MAAM,kBACxB,QACA,gBAAgB,OAAO,OAAO,CAC/B,IAAI,EAAE;AAGP,MAAI,SACF,MAAK,MAAM,eAAe,OAAO,KAAK,SAAS,EAAE;AAE/C,0BAAuB,SAAS,cAAe;IAAE,OAAO,MAAM;IAAI;IAAQ,OAAO;IAAa,EAAE,OAAO;AAEvG,OAAI,EAAE,eAAe,cAAc;AACjC,WAAO,KAAK;KACV,UAAU;KACV,OAAO,MAAM;KACb;KACA,OAAO;KACP,SAAS,4BAA4B,YAAY;KAClD,CAAC;AACF,QAAI,OAAO,aAAa;AACtB,YAAO,SAAS;KAChB,MAAM,UAAU,KAAK,aAAa,YAAY;AAC9C,SAAI,OAAO,KAAK,SAAS,CAAC,SAAS,EACjC,OAAM,UAAU,SAAS,SAAS;SAElC,OAAM,GAAG,SAAS,EAAE,OAAO,MAAM,CAAC;AAEpC;;;;AAOR,OAAK,MAAM,WAAW,OAAO,KAAK,YAAY,CAC5C,KAAI,CAAC,YAAY,EAAE,WAAW,WAAW;AACvC,UAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA,OAAO;IACP,SAAS,0BAA0B,QAAQ;IAC5C,CAAC;AACF,OAAI,OAAO,aAAa;AACtB,UAAM,UAAU,aAAa,OAAO;KAAE;KAAQ;KAAS,EAAE;KACvD,QAAQ;KACR,QAAQ;KACR,YAAY;KACb,CAAC;AACF;;;;AAMR,QAAO;EAAE,SAAS;EAAgB;EAAO;;AAG3C,eAAe,uBACb,QACA,aACA,OACA,QACA,QACA,KAC6C;CAC7C,IAAI,iBAAiB;CACrB,IAAI,QAAQ;AAEZ,MAAK,MAAM,UAAW,MAAM,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,QAAQ,QAAQ,EAAG;EACvF,MAAM,WAAW,gBAAgB,OAAO,OAAO;EAC/C,MAAM,OAAO,MAAM,kBAA2C,QAAQ,SAAS;AAE/E,MAAI,CAAC,MAAM;AACT,OAAI,MAAM,MAAM;AACd,WAAO,KAAK;KACV,UAAU;KACV,OAAO,MAAM;KACb;KACA,SAAS,wBAAwB,OAAO;KACzC,CAAC;AACF,QAAI,OAAO,aAAa;AACtB,WAAM,UAAU,KAAK,aAAa,SAAS,EAAE,EAAE,CAAC;AAChD;;;AAGJ;;AAGF;AAEA,iCAA+B,MAAM,MAAM,QAAQ;GAAE,OAAO,MAAM;GAAI;GAAQ,EAAE,OAAO;AAEvF,MAAI,MAAM,QAAQ;GAChB,MAAM,cAAc,gBAAgB,MAAM,MAAM,QAAQ,MAAM,IAAI,OAAO;AACzE,UAAO,KAAK,GAAG,YAAY,OAAO;GAElC,MAAM,gBAAgB,2BAA2B,QAAQ,OAAO;GAChE,MAAM,iBAAiB,MAAM,uBAC3B,MACA,MAAM,QACN,MAAM,IACN,QACA,KAAA,GACA,YAAY,MACZ;IAAE,UAAU;IAAS;IAAe,CACrC;AACD,UAAO,KAAK,GAAG,eAAe;;EAIhC,MAAM,OAAO,OAAO,KAAK,KAAK;EAC9B,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,UAAU;AACnC,MAAI,KAAK,KAAK,IAAI,KAAK,OAAO,KAAK,IAAI,EAAE;AACvC,UAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA,SAAS;IACV,CAAC;AACF,OAAI,OAAO,aAAa;IACtB,MAAM,WAAoC,EAAE;AAC5C,SAAK,MAAM,OAAO,OAChB,UAAS,OAAO,KAAK;AAEvB,UAAM,UAAU,KAAK,aAAa,SAAS,EAAE,SAAS;AACtD;;;EAKJ,MAAM,oBAAoB,MAAM,kBAA6B,QAAQ,aAAa,OAAO,OAAO,CAAC;AACjG,MAAI,kBACF,wBAAuB,mBAAmB;GAAE,OAAO,MAAM;GAAI;GAAQ,EAAE,OAAO;;AAIlF,QAAO;EAAE,SAAS;EAAgB;EAAO;;AAG3C,eAAe,wBACb,QACA,aACA,OACA,QACA,QACA,KAC6C;CAC7C,IAAI,iBAAiB;CACrB,IAAI,QAAQ;CAEZ,MAAM,aAA0C,EAAE;AAElD,MAAK,MAAM,UAAW,MAAM,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,QAAQ,QAAQ,EAAG;EACvF,MAAM,WAAW,gBAAgB,OAAO,OAAO;EAC/C,MAAM,OAAO,MAAM,kBAA0C,QAAQ,SAAS;AAE9E,MAAI,CAAC,MAAM;AACT,OAAI,MAAM,MAAM;AACd,WAAO,KAAK;KACV,UAAU;KACV,OAAO,MAAM;KACb;KACA,SAAS,wBAAwB,OAAO;KACzC,CAAC;AACF,QAAI,OAAO,aAAa;AACtB,WAAM,UAAU,KAAK,aAAa,SAAS,EAAE,EAAE,CAAC;AAChD;;;AAGJ;;AAGF;AACA,aAAW,UAAU,IAAI,IAAI,OAAO,KAAK,KAAK,CAAC;AAG/C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,CAC7C,KAAI,cAAc,MAAM,CAAC,SAAS,EAChC,QAAO,KAAK;GACV,UAAU;GACV,OAAO,MAAM;GACb;GACA,OAAO;GACP,SAAS,qCAAqC,IAAI;GACnD,CAAC;EAKN,MAAM,8BAAc,IAAI,KAAuB;AAC/C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,EAAE;GAC/C,MAAM,MAAM,YAAY,IAAI,MAAM;AAClC,OAAI,IAAK,KAAI,KAAK,IAAI;OACjB,aAAY,IAAI,OAAO,CAAC,IAAI,CAAC;;AAEpC,OAAK,MAAM,CAAC,OAAO,aAAa,YAC9B,KAAI,SAAS,SAAS,GAAG;GACvB,MAAM,YAAY,MAAM,SAAS,KAAK,GAAG,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO;AACnE,UAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA,SAAS,oBAAoB,UAAU,cAAc,SAAS,OAAO,UAAU,SAAS,KAAK,KAAK,CAAC;IACpG,CAAC;;EAKN,MAAM,OAAO,OAAO,KAAK,KAAK;EAC9B,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,UAAU;AACnC,MAAI,KAAK,KAAK,IAAI,KAAK,OAAO,KAAK,IAAI,EAAE;AACvC,UAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA,SAAS;IACV,CAAC;AACF,OAAI,OAAO,aAAa;IACtB,MAAM,WAAmC,EAAE;AAC3C,SAAK,MAAM,OAAO,OAChB,UAAS,OAAO,KAAK;AAEvB,UAAM,UAAU,KAAK,aAAa,SAAS,EAAE,SAAS;AACtD;;;;AAMN,KAAI,MAAM,QAAQ,OAAO,KAAK,WAAW,CAAC,SAAS,GAAG;EACpD,MAAM,cAAc,OAAO,KAAK,WAAW;AAC3C,OAAK,IAAI,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;GAC3C,MAAM,OAAO,YAAY;GACzB,MAAM,OAAO,YAAY;GACzB,MAAM,QAAQ,WAAW;GACzB,MAAM,QAAQ,WAAW;AAEzB,QAAK,MAAM,KAAK,MACd,KAAI,CAAC,MAAM,IAAI,EAAE,CACf,QAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb,QAAQ;IACR,OAAO;IACP,SAAS,oBAAoB,EAAE,cAAc,KAAK,kBAAkB;IACrE,CAAC;AAGN,QAAK,MAAM,KAAK,MACd,KAAI,CAAC,MAAM,IAAI,EAAE,CACf,QAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb,QAAQ;IACR,OAAO;IACP,SAAS,oBAAoB,EAAE,cAAc,KAAK,kBAAkB;IACrE,CAAC;;;AAMV,QAAO;EAAE,SAAS;EAAgB;EAAO;;AAG3C,eAAe,sBACb,QACA,MACA,OACmB;CACnB,MAAM,WAAW,sBAAsB,MAAM;CAC7C,MAAM,UAAU,MAAM,OAAO,cAAc,KAAK;AAGhD,KAAI,CAAC,MAAM,KACT,QAAO,QAAQ,QAAO,MAAK,EAAE,SAAS,MAAM,CAAC,CAAC,KAAI,MAAK,EAAE,QAAQ,SAAS,GAAG,CAAC;AAGhF,KAAI,aAAa,OAEf,QAAO,QAAQ,QAAO,MAAK,CAAC,EAAE,WAAW,IAAI,CAAC;AAGhD,KAAI,aAAa,UAAU;EAEzB,MAAM,wBAAQ,IAAI,KAAa;AAC/B,OAAK,MAAM,KAAK,SAAS;AACvB,OAAI,CAAC,EAAE,SAAS,MAAM,CAAE;GAExB,MAAM,QAAQ,EAAE,QAAQ,SAAS,GAAG,CAAC,MAAM,IAAI;AAC/C,OAAI,MAAM,UAAU,GAAG;AACrB,UAAM,KAAK;AACX,UAAM,IAAI,MAAM,KAAK,IAAI,CAAC;;;AAG9B,SAAO,CAAC,GAAG,MAAM;;AAGnB,KAAI,aAAa,aAAa;EAE5B,MAAM,wBAAQ,IAAI,KAAa;EAC/B,MAAM,cAAc,MAAM,QAAQ,IAChC,QACG,QAAO,cAAa,CAAC,UAAU,WAAW,IAAI,CAAC,CAC/C,KAAI,cAAa,OAAO,cAAc,GAAG,KAAK,GAAG,YAAY,CAAC,CAClE;AACD,OAAK,MAAM,SAAS,YAClB,MAAK,MAAM,KAAK,MACd,KAAI,EAAE,SAAS,MAAM,CAAE,OAAM,IAAI,EAAE,QAAQ,SAAS,GAAG,CAAC;AAG5D,SAAO,CAAC,GAAG,MAAM;;AAInB,QAAO,QAAQ,QAAO,MAAK,EAAE,SAAS,MAAM,CAAC,CAAC,KAAI,MAAK,EAAE,QAAQ,SAAS,GAAG,CAAC;;AAGhF,eAAe,sBACb,QACA,aACA,OACA,QACA,QACA,KAC6C;CAC7C,IAAI,iBAAiB;CACrB,IAAI,QAAQ;CACZ,MAAM,OAAO,eAAe,MAAM;AAElC,KAAI,CAAC,MAAM,OAAO,WAAW,KAAK,CAAE,QAAO;EAAE,SAAS;EAAG,OAAO;EAAG;CAEnE,MAAM,QAAQ,MAAM,sBAAsB,QAAQ,MAAM,MAAM;CAI9D,MAAM,UAAU,MAAM,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,QAAQ,QAAQ;AAEhF,MAAK,MAAM,QAAQ,OAAO;AACxB,MAAI,KAAK,WAAW,IAAI,CAAE;AAE1B,OAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,WAAW,iBAAiB,OAAO,QAAQ,KAAK;GACtD,MAAM,MAAM,MAAM,kBAAkB,QAAQ,SAAS;AAErD,OAAI,CAAC,KAAK;AACR,QAAI,MAAM,MAAM;AACd,YAAO,KAAK;MACV,UAAU;MACV,OAAO,MAAM;MACb;MACA;MACA,SAAS,kCAAkC,KAAK,YAAY,OAAO;MACpE,CAAC;AACF,SAAI,OAAO,aAAa;MAEtB,MAAM,WAAW,cAAc,KAAK;AACpC,YAAM,UAAU,KAAK,aAAa,SAAS,EAAE,SAAS;AACtD;;;AAGJ;;AAGF;GACA,MAAM,EAAE,aAAa,SAAS,iBAAiB,IAAI;AAGnD,kCACE,aAGA;IAAE,GAAG,MAAM;IAAQ,MAAM;IAAM,EAC/B;IAAE,OAAO,MAAM;IAAI;IAAQ;IAAM,EACjC,OACD;AAED,OAAI,cAAc,KAAK,CAAC,SAAS,EAC/B,QAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA;IACA,OAAO;IACP,SAAS;IACV,CAAC;AAGJ,OAAI,MAAM,QAAQ;IAChB,MAAM,oBAAoB,OAAO,YAC/B,OAAO,QAAQ,MAAM,OAAO,CAAC,QAAQ,CAAC,UAAU,SAAS,OAAO,CACjE;IACD,MAAM,cAAc,gBAClB,aACA,mBACA,MAAM,IACN,OACD;AACD,SAAK,MAAM,OAAO,YAAY,OAC5B,QAAO,KAAK;KAAE,GAAG;KAAK;KAAM,CAAC;IAG/B,MAAM,gBAAgB,2BAA2B,QAAQ,OAAO;IAChE,MAAM,iBAAiB,MAAM,uBAC3B,aACA,mBACA,MAAM,IACN,QACA,KAAA,GACA,YAAY,MACZ;KAAE,UAAU;KAAS;KAAe,CACrC;AACD,SAAK,MAAM,OAAO,eAChB,QAAO,KAAK;KAAE,GAAG;KAAK;KAAM,CAAC;;;;AAMrC,QAAO;EAAE,SAAS;EAAgB;EAAO;;AAK3C,eAAe,mBACb,QACA,eACA,QACA,MACiB;CACjB,MAAM,QAAQ;CACd,MAAM,cAAc;CACpB,MAAM,UAAU,MAAM,OAAO,cAAc,YAAY;CAEvD,MAAM,aAAa,MAAM,QAAQ,IAC/B,QACG,QAAO,MAAK,CAAC,EAAE,WAAW,IAAI,CAAC,CAC/B,IAAI,OAAM,OAAM;EAAE,QAAQ;EAAG,MAAM,MAAM,OAAO,cAAc,GAAG,YAAY,GAAG,IAAI;EAAE,EAAE,CAC5F;AAED,MAAK,MAAM,EAAE,UAAU,WACrB,MAAK,MAAM,YAAY,MAAM;AAC3B,MAAI,SAAS,WAAW,IAAI,CAAE;AAC9B,MAAI,CAAC,cAAc,IAAI,SAAS,CAC9B,QAAO,KAAK;GACV,UAAU;GACV,OAAO;GACP,SAAS,+DAA+D,SAAS;GAClF,CAAC;;AAKR,QAAO;;AAsBT,eAAsB,gBACpB,OACA,SACyB;CACzB,MAAM,SAAqB,OAAO,UAAU,WAAW,IAAI,YAAY,MAAM,GAAG;CAChF,MAAM,cAAkC,OAAO,UAAU,WAAW,QAAQ,KAAA;CAC5E,MAAM,MAAM,QAAQ,SAAS,IAAI,IAAI,gBAAgB,KAAA;CAErD,MAAM,SAA4B,EAAE;CACpC,IAAI,eAAe;CACnB,IAAI,aAAa;CACjB,IAAI,gBAAgB;CAEpB,MAAM,SAAS,MAAM,WAAW,OAAO;AACvC,KAAI,CAAC,OACH,QAAO;EACL,OAAO;EACP,SAAS;GAAE,QAAQ;GAAG,UAAU;GAAG,SAAS;GAAG,gBAAgB;GAAG,iBAAiB;GAAG;EACtF,QAAQ,CAAC;GAAE,UAAU;GAAS,SAAS;GAAgD,CAAC;EACxF,OAAO;EACR;CAGH,MAAM,iBAAiB,MAAM,WAAW,OAAO;CAC/C,MAAM,gBAAgB,IAAI,IAAI,eAAe,KAAI,MAAK,EAAE,GAAG,CAAC;CAC5D,MAAM,gBAAgB,SAAS,QAC3B,eAAe,QAAO,MAAK,EAAE,OAAO,QAAQ,MAAM,GAClD;AAEJ,MAAK,MAAM,WAAW,eAAe;EACnC,MAAM,QAAQ,MAAM,UAAU,QAAQ,QAAQ,GAAG;AACjD,MAAI,CAAC,MAAO;AAEZ;EACA,IAAI;AAEJ,UAAQ,MAAM,MAAd;GACE,KAAK;AACH,aAAS,MAAM,wBAAwB,QAAQ,aAAa,OAAO,QAAQ,QAAQ,IAAI;AACvF;GACF,KAAK;AACH,aAAS,MAAM,uBAAuB,QAAQ,aAAa,OAAO,QAAQ,QAAQ,IAAI;AACtF;GACF,KAAK;AACH,aAAS,MAAM,wBAAwB,QAAQ,aAAa,OAAO,QAAQ,QAAQ,IAAI;AACvF;GACF,KAAK;AACH,aAAS,MAAM,sBAAsB,QAAQ,aAAa,OAAO,QAAQ,QAAQ,IAAI;AACrF;GACF,QACE,UAAS;IAAE,SAAS;IAAG,OAAO;IAAG;;AAGrC,kBAAgB,OAAO;AACvB,gBAAc,OAAO;;AAIvB,KAAI,CAAC,SAAS,MACZ,eAAc,MAAM,mBAAmB,QAAQ,eAAe,QAAQ,IAAI;AAI5E,KAAI,CAAC,SAAS,OAAO;EACnB,MAAM,aAAa,cAAc,QAAO,MAAK,EAAE,SAAS,aAAa;AACrE,MAAI,WAAW,SAAS,GAAG;GACzB,MAAM,iBAAqF,EAAE;AAE7F,QAAK,MAAM,WAAW,YAAY;IAChC,MAAM,QAAQ,MAAM,UAAU,QAAQ,QAAQ,GAAG;AACjD,QAAI,CAAC,MAAO;AAEZ,SAAK,MAAM,UAAW,MAAM,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,QAAQ,QAAQ,EAAG;AACvF,SAAI,CAAC,eAAe,QAAS,gBAAe,0BAAU,IAAI,KAAK;KAC/D,MAAM,OAAO,MAAM,kBAA0C,QAAQ,gBAAgB,OAAO,OAAO,CAAC;AACpG,SAAI,CAAC,KAAM;AAEX,UAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,EAAE;MAC/C,MAAM,OAAO,eAAe,QAAS,IAAI,MAAM;AAC/C,UAAI,KAAM,MAAK,KAAK;OAAE,OAAO,MAAM;OAAI;OAAK,CAAC;UACxC,gBAAe,QAAS,IAAI,OAAO,CAAC;OAAE,OAAO,MAAM;OAAI;OAAK,CAAC,CAAC;;;;AAKzE,QAAK,MAAM,CAAC,QAAQ,aAAa,OAAO,QAAQ,eAAe,CAC7D,MAAK,MAAM,CAAC,OAAO,SAAS,SAE1B,KADqB,IAAI,IAAI,KAAK,KAAI,MAAK,EAAE,MAAM,CAAC,CACnC,OAAO,GAAG;IACzB,MAAM,YAAY,MAAM,SAAS,KAAK,GAAG,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO;AACnE,WAAO,KAAK;KACV,UAAU;KACV;KACA,SAAS,gCAAgC,UAAU,OAAO,KAAK,KAAI,MAAK,GAAG,EAAE,MAAM,GAAG,EAAE,MAAM,CAAC,KAAK,KAAK;KAC1G,CAAC;;;;CAOZ,MAAM,SAAS,OAAO,QAAO,MAAK,EAAE,aAAa,QAAQ,CAAC;CAC1D,MAAM,WAAW,OAAO,QAAO,MAAK,EAAE,aAAa,UAAU,CAAC;CAC9D,MAAM,UAAU,OAAO,QAAO,MAAK,EAAE,aAAa,SAAS,CAAC;AAE5D,QAAO;EACL,OAAO,WAAW;EAClB,SAAS;GACP;GACA;GACA;GACA,gBAAgB;GAChB,iBAAiB;GAClB;EACD;EACA,OAAO;EACR"}
1
+ {"version":3,"file":"validator-Z3DGNWQ6.mjs","names":[],"sources":["../src/core/validator/entry.ts","../src/core/validator/relation-integrity.ts","../src/core/validator/schedule.ts","../src/core/validator/project.ts"],"sourcesContent":["import type { FieldDef, ModelDefinition, ValidationError, ValidationResult } from '@contentrain/types'\nimport { detectSecrets, validateFieldValue } from '@contentrain/types'\n\n/**\n * Context for cross-entry validation (unique, relations, nested references).\n *\n * Shape aligned with Studio's `content-validation.ts:ValidationContext` so\n * Studio can swap its internal implementation for this module without\n * touching call sites.\n */\nexport interface ValidationContext {\n /** All entries in the collection (for unique-constraint checks). Keyed by entry ID. */\n allEntries?: Record<string, Record<string, unknown>>\n /** ID of the entry being validated (excluded from unique-constraint comparisons). */\n currentEntryId?: string\n /** All model definitions, if available (reserved for future cross-model checks). */\n models?: ModelDefinition[]\n}\n\n/**\n * Validate a single content entry against its model's field schema.\n *\n * Merges the rule sets from MCP's legacy `validator.ts` (secret detection,\n * schema validation, unique constraints) with Studio's `content-validation.ts`\n * (email/url heuristics, polymorphic relation structure, nested object and\n * array-of-object recursion). The union is the authoritative per-entry\n * validator — both MCP's project validator and Studio's save path should\n * converge on this function over time.\n *\n * Asynchronous relation-integrity checks (does the referenced entry exist?)\n * live in `relation-integrity.ts` because they require I/O.\n */\nexport function validateContent(\n data: Record<string, unknown>,\n fields: Record<string, FieldDef>,\n modelId: string,\n locale: string,\n entryId?: string,\n ctx?: ValidationContext,\n): ValidationResult {\n const errors: ValidationError[] = []\n\n for (const [fieldId, def] of Object.entries(fields)) {\n const value = data[fieldId]\n errors.push(...validateField(value, def, modelId, locale, entryId, fieldId, ctx))\n }\n\n return {\n valid: errors.filter(e => e.severity === 'error').length === 0,\n errors,\n }\n}\n\nfunction validateField(\n value: unknown,\n def: FieldDef,\n modelId: string,\n locale: string,\n entryId: string | undefined,\n fieldId: string,\n ctx?: ValidationContext,\n): ValidationError[] {\n const errors: ValidationError[] = []\n const errCtx = { model: modelId, locale, entry: entryId, field: fieldId }\n\n if (value !== null && value !== undefined && value !== '') {\n const secretErrors = detectSecrets(value)\n for (const e of secretErrors) {\n errors.push({ ...e, ...errCtx })\n }\n }\n\n const isRelationType = def.type === 'relation' || def.type === 'relations'\n if (!isRelationType) {\n const fieldErrors = validateFieldValue(value, def)\n if (fieldErrors.length > 0) {\n for (const e of fieldErrors) errors.push({ ...e, ...errCtx })\n if (fieldErrors.some(e => e.severity === 'error')) return errors\n }\n } else if (def.required && (value === null || value === undefined || value === '')) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} is required` })\n return errors\n }\n\n if (value === null || value === undefined) return errors\n\n if (def.unique && ctx?.allEntries) {\n // String() comparison — matches legacy validator.ts uniqueness semantics\n // (catches cross-type duplicates like 42 vs '42' that strict === would miss).\n const valueKey = String(value)\n for (const [otherId, otherEntry] of Object.entries(ctx.allEntries)) {\n if (otherId === ctx.currentEntryId) continue\n const otherValue = otherEntry[fieldId]\n if (otherValue !== null && otherValue !== undefined && String(otherValue) === valueKey) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} must be unique — \"${String(value)}\" already exists in entry ${otherId}`,\n })\n break\n }\n }\n }\n\n if (def.type === 'email' && typeof value === 'string' && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {\n errors.push({ severity: 'warning', ...errCtx, message: `${fieldId} may not be a valid email` })\n }\n if (def.type === 'url' && typeof value === 'string' && !/^https?:\\/\\/.+/.test(value) && !value.startsWith('/')) {\n errors.push({ severity: 'warning', ...errCtx, message: `${fieldId} may not be a valid URL` })\n }\n\n if (def.type === 'relation' && def.model) {\n const targets = Array.isArray(def.model) ? def.model : [def.model]\n if (targets.length > 1) {\n if (typeof value !== 'object' || value === null || !('model' in value) || !('ref' in value)) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} must be { model, ref } for polymorphic relation`,\n })\n } else {\n const polyVal = value as { model: string, ref: string }\n if (!targets.includes(polyVal.model)) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} target model \"${polyVal.model}\" must be one of: ${targets.join(', ')}`,\n })\n }\n }\n } else if (typeof value !== 'string') {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must be a string (entry ID or slug)` })\n }\n }\n\n if (def.type === 'relations') {\n if (!Array.isArray(value)) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must be an array` })\n } else {\n if (def.min !== undefined && value.length < def.min) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must have at least ${def.min} items` })\n }\n if (def.max !== undefined && value.length > def.max) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must have at most ${def.max} items` })\n }\n // Polymorphic multi-relations (model targets multiple kinds) store\n // { model, ref } items — mirror the single-relation rule so content that\n // conforms to the generated type isn't rejected. Single-target relations\n // store plain id/slug strings.\n const targets = Array.isArray(def.model) ? def.model : (def.model ? [def.model] : [])\n const polymorphic = targets.length > 1\n for (let i = 0; i < value.length; i++) {\n const item = value[i]\n const itemCtx = { ...errCtx, field: `${fieldId}[${i}]` }\n if (polymorphic) {\n if (typeof item !== 'object' || item === null || !('model' in item) || !('ref' in item)) {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] must be { model, ref } for polymorphic relations` })\n } else {\n const polyItem = item as { model: string, ref: string }\n if (!targets.includes(polyItem.model)) {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] target model \"${polyItem.model}\" must be one of: ${targets.join(', ')}` })\n }\n }\n } else if (typeof item !== 'string') {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] must be a string (entry ID or slug)` })\n }\n }\n }\n }\n\n if (def.type === 'array' && Array.isArray(value) && def.items && typeof def.items === 'string') {\n for (let i = 0; i < value.length; i++) {\n errors.push(...validateArrayItemType(value[i], def.items, errCtx, `${fieldId}[${i}]`))\n }\n }\n\n if (def.type === 'object' && def.fields && typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const nested = validateContent(value as Record<string, unknown>, def.fields, modelId, locale, entryId)\n errors.push(...nested.errors)\n }\n\n if (\n def.type === 'array'\n && Array.isArray(value)\n && def.items\n && typeof def.items === 'object'\n && def.items.type === 'object'\n && def.items.fields\n ) {\n for (let i = 0; i < value.length; i++) {\n if (typeof value[i] === 'object' && value[i] !== null) {\n const nested = validateContent(value[i] as Record<string, unknown>, def.items.fields, modelId, locale, entryId)\n for (const e of nested.errors) {\n errors.push({ ...e, field: `${fieldId}[${i}].${e.field}` })\n }\n }\n }\n }\n\n return errors\n}\n\nfunction validateArrayItemType(\n value: unknown,\n itemType: string,\n errCtx: { model: string, locale: string, entry: string | undefined, field: string },\n fieldPath: string,\n): ValidationError[] {\n const ctx = { ...errCtx, field: fieldPath }\n switch (itemType) {\n case 'string':\n case 'email':\n case 'url':\n case 'slug':\n case 'image':\n case 'video':\n case 'file':\n if (typeof value !== 'string') return [{ severity: 'error', ...ctx, message: `${fieldPath} must be a string` }]\n break\n case 'number':\n case 'integer':\n case 'decimal':\n if (typeof value !== 'number') return [{ severity: 'error', ...ctx, message: `${fieldPath} must be a number` }]\n if (itemType === 'integer' && !Number.isInteger(value)) {\n return [{ severity: 'error', ...ctx, message: `${fieldPath} must be an integer` }]\n }\n break\n case 'boolean':\n if (typeof value !== 'boolean') return [{ severity: 'error', ...ctx, message: `${fieldPath} must be a boolean` }]\n break\n }\n return []\n}\n","import type { FieldDef, ValidationError } from '@contentrain/types'\n\n/**\n * Resolution of a target model for a relation integrity check.\n *\n * - `{ exists: true, content: {...} }` — target model is known and its\n * content for the requested locale was loaded. The ref must appear\n * as a key in `content`.\n * - `{ exists: true, content: null }` — target model exists (e.g. a\n * singleton or dictionary) but per-entry key enforcement does not\n * apply. The checker skips the ref existence test for this target.\n * - `{ exists: false }` — the referenced model is not defined at all.\n * The checker emits a \"target model not found\" error.\n */\nexport interface ResolvedTarget {\n exists: boolean\n content?: Record<string, unknown> | null\n}\n\n/** Back-compat loader signature used by Studio. A null return means \"nothing to check\". */\nexport type LegacyLoadContent = (\n targetModelId: string,\n targetLocale: string,\n) => Promise<Record<string, unknown> | null>\n\nexport interface CheckRelationIntegrityOptions {\n /** Severity for broken-relation errors. Default: `warning` (Studio-compat). */\n severity?: 'error' | 'warning'\n /**\n * Richer target resolver — when provided, it can signal target-model\n * absence. MCP's project validator passes one so `contentrain_validate`\n * still emits \"target model not found\" errors.\n */\n resolveTarget?: (\n targetModelId: string,\n targetLocale: string,\n ) => Promise<ResolvedTarget>\n}\n\n/**\n * Verify that relation and relations fields reference targets that actually\n * exist. Two severities are supported — Studio's per-save flow emits\n * `warning` (the referenced entry may still be drafted), while MCP's\n * project-wide validator emits `error` and additionally flags missing\n * target models via `resolveTarget`.\n *\n * The loader abstraction keeps this function I/O-agnostic: MCP wires it\n * to filesystem reads through LocalReader, Studio wires it to a\n * `GitProvider.readFile`-backed loader, mocks pass in-memory maps.\n */\nexport async function checkRelationIntegrity(\n data: Record<string, unknown>,\n fields: Record<string, FieldDef>,\n modelId: string,\n locale: string,\n entryId: string | undefined,\n loadContent: LegacyLoadContent,\n opts: CheckRelationIntegrityOptions = {},\n): Promise<ValidationError[]> {\n const errors: ValidationError[] = []\n const severity = opts.severity ?? 'warning'\n\n const resolve = opts.resolveTarget\n ?? (async (id: string, loc: string): Promise<ResolvedTarget> => {\n const content = await loadContent(id, loc)\n return { exists: true, content }\n })\n\n for (const [fieldId, def] of Object.entries(fields)) {\n const value = data[fieldId]\n if (value === null || value === undefined) continue\n\n if (def.type === 'relation' && def.model) {\n const targets = Array.isArray(def.model) ? def.model : [def.model]\n\n if (targets.length > 1 && typeof value === 'object' && value !== null) {\n const polyVal = value as { model: string, ref: string }\n if (polyVal.model && polyVal.ref) {\n const resolved = await resolve(polyVal.model, locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${polyVal.model}\" not found`,\n })\n } else if (resolved.content && !(polyVal.ref in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${polyVal.ref}\" not found in ${polyVal.model}`,\n })\n }\n }\n } else if (typeof value === 'string' && targets[0]) {\n const resolved = await resolve(targets[0], locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${targets[0]}\" not found`,\n })\n } else if (resolved.content && !(value in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${value}\" not found in ${targets[0]}`,\n })\n }\n }\n }\n\n if (def.type === 'relations' && def.model && Array.isArray(value)) {\n const target = Array.isArray(def.model) ? def.model[0] : def.model\n if (target) {\n const resolved = await resolve(target, locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${target}\" not found`,\n })\n } else if (resolved.content) {\n for (const ref of value) {\n if (typeof ref === 'string' && !(ref in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${ref}\" not found in ${target}`,\n })\n }\n }\n }\n }\n }\n }\n\n return errors\n}\n","import type { EntryMeta, ValidationError } from '@contentrain/types'\n\n/**\n * Validate `publish_at` and `expire_at` meta fields.\n *\n * Rules (matches legacy `validator.ts:validateScheduleFields`):\n * - `publish_at` must parse as a valid Date\n * - `expire_at` must parse as a valid Date\n * - When both are present, `expire_at` must be strictly after `publish_at`\n */\nexport function validateScheduleFields(\n meta: EntryMeta,\n ctx: { model: string, locale: string, entry?: string, slug?: string },\n issues: ValidationError[],\n): void {\n if (meta.publish_at !== undefined) {\n const d = new Date(meta.publish_at)\n if (Number.isNaN(d.getTime())) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `Invalid publish_at date: \"${meta.publish_at}\". Must be a valid ISO 8601 date string.`,\n })\n }\n }\n if (meta.expire_at !== undefined) {\n const d = new Date(meta.expire_at)\n if (Number.isNaN(d.getTime())) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `Invalid expire_at date: \"${meta.expire_at}\". Must be a valid ISO 8601 date string.`,\n })\n }\n }\n if (meta.publish_at !== undefined && meta.expire_at !== undefined) {\n const pubDate = new Date(meta.publish_at)\n const expDate = new Date(meta.expire_at)\n if (!Number.isNaN(pubDate.getTime()) && !Number.isNaN(expDate.getTime()) && expDate <= pubDate) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `expire_at (\"${meta.expire_at}\") must be after publish_at (\"${meta.publish_at}\").`,\n })\n }\n }\n}\n","import type { ValidationError, ModelDefinition, ContentrainConfig, EntryMeta } from '@contentrain/types'\nimport { detectSecrets } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { writeJson, writeText } from '../../util/fs.js'\nimport { readConfig } from '../config.js'\nimport { listModels, readModel } from '../model-manager.js'\nimport { writeMeta } from '../meta-manager.js'\nimport { parseFrontmatter, resolveLocaleStrategy } from '../content-manager.js'\nimport type { RepoReader } from '../contracts/index.js'\nimport { LocalReader } from '../../providers/local/reader.js'\nimport { contentDirPath, contentFilePath, documentFilePath, metaFilePath } from '../ops/paths.js'\nimport { validateContent } from './entry.js'\nimport { checkRelationIntegrity, type ResolvedTarget } from './relation-integrity.js'\nimport { validateScheduleFields } from './schedule.js'\n\n// ─── Types ───\n\nexport interface ValidateOptions {\n model?: string\n fix?: boolean\n}\n\nexport interface ValidateResult {\n valid: boolean\n summary: {\n errors: number\n warnings: number\n notices: number\n models_checked: number\n entries_checked: number\n }\n issues: ValidationError[]\n fixed: number\n}\n\n// ─── Reader-backed IO helpers ───\n\nasync function readJsonViaReader<T>(reader: RepoReader, path: string): Promise<T | null> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return null\n }\n}\n\nasync function readTextViaReader(reader: RepoReader, path: string): Promise<string | null> {\n try {\n return await reader.readFile(path)\n } catch {\n return null\n }\n}\n\n// ─── Shared field validation helpers ───\n\n/**\n * Build a `resolveTarget` adapter for `checkRelationIntegrity` that walks\n * the project's content store via the shared {@link RepoReader}. Mirrors\n * the target-resolution shape the legacy `checkRelation` used — collection\n * targets return their entry object-map, documents return a \"slug exists\"\n * marker map, singletons and dictionaries return null content so the\n * checker skips key enforcement for them.\n */\nfunction buildProjectTargetResolver(\n reader: RepoReader,\n config: ContentrainConfig,\n): (targetModelId: string, targetLocale: string) => Promise<ResolvedTarget> {\n return async (targetModelId, targetLocale) => {\n const targetModel = await readModel(reader, targetModelId)\n if (!targetModel) return { exists: false }\n\n if (targetModel.kind === 'document') {\n const docContentDir = contentDirPath(targetModel)\n const slugs = await discoverDocumentSlugs(reader, docContentDir, targetModel)\n return { exists: true, content: Object.fromEntries(slugs.map(s => [s, true])) }\n }\n if (targetModel.kind === 'singleton' || targetModel.kind === 'dictionary') {\n return { exists: true, content: null }\n }\n // Collection: resolve against the target model's own storage. i18n:false\n // targets are locale-agnostic (data.json). For i18n:true targets, fall back\n // to the default-locale file when the source locale lacks the entry — a\n // relation legitimately points at an entry that may be translated under\n // another locale, so the id set is merged to avoid phantom broken refs. #Y5\n const merged: Record<string, unknown> = {}\n const primary = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, targetLocale),\n )\n if (primary) Object.assign(merged, primary)\n if (targetModel.i18n && targetLocale !== config.locales.default) {\n const fallback = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, config.locales.default),\n )\n if (fallback) Object.assign(merged, fallback)\n }\n return { exists: true, content: merged }\n }\n}\n\n/**\n * Scan an entry's data fields for detected secrets in UNDECLARED keys —\n * the legacy validator also flagged stray/rogue fields that were not in\n * `model.fields`. `validateContent` only knows about declared fields, so\n * this complementary pass preserves that coverage.\n */\nfunction scanUndeclaredFieldsForSecrets(\n data: Record<string, unknown>,\n declared: Record<string, unknown> | undefined,\n ctx: { model: string, locale: string, entry?: string, slug?: string },\n issues: ValidationError[],\n): void {\n for (const [fieldName, value] of Object.entries(data)) {\n if (declared && fieldName in declared) continue\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n ...ctx,\n field: fieldName,\n message: `Potential secret detected in field \"${fieldName}\"`,\n })\n }\n }\n}\n\n// ─── Per-model validators ───\n\nasync function validateCollectionModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n const locales = model.i18n ? config.locales.supported : [config.locales.default]\n\n // Collect all entry IDs per locale for parity check\n const localeEntryIds: Record<string, Set<string>> = {}\n const allEntryIds = new Set<string>()\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n\n for (const locale of locales) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, Record<string, unknown>>>(reader, filePath)\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n const ids = new Set(Object.keys(data))\n localeEntryIds[locale] = ids\n for (const id of ids) allEntryIds.add(id)\n\n // Check canonical sort\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, Record<string, unknown>> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate each entry — validateContent covers secret/field/unique/email-url/\n // polymorphic/nested on declared fields; supplementary scans below catch\n // undeclared-field secrets and async relation integrity.\n for (const [entryId, fields] of Object.entries(data)) {\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(\n fields,\n model.fields,\n { model: model.id, locale, entry: entryId },\n issues,\n )\n\n if (!model.fields) continue\n\n const entryResult = validateContent(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n { allEntries: data, currentEntryId: entryId },\n )\n issues.push(...entryResult.errors)\n\n const relationErrors = await checkRelationIntegrity(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\n }\n }\n\n // Entry parity check (i18n)\n if (model.i18n && Object.keys(localeEntryIds).length > 1) {\n const localeKeys = Object.keys(localeEntryIds)\n for (let i = 1; i < localeKeys.length; i++) {\n const locA = localeKeys[0]!\n const locB = localeKeys[i]!\n const idsA = localeEntryIds[locA]!\n const idsB = localeEntryIds[locB]!\n\n for (const id of idsA) {\n if (!idsB.has(id)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale: locB,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locA} but missing in ${locB}`,\n })\n }\n }\n for (const id of idsB) {\n if (!idsA.has(id)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale: locA,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locB} but missing in ${locA}`,\n })\n }\n }\n }\n }\n\n // Orphan meta check\n for (const locale of locales) {\n const metaRelPath = metaFilePath(model, locale)\n const metaData = await readJsonViaReader<Record<string, EntryMeta>>(reader, metaRelPath)\n const contentData = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(model, locale),\n ) ?? {}\n\n // Forward check: meta entries without content + schedule validation\n if (metaData) {\n for (const metaEntryId of Object.keys(metaData)) {\n // Validate schedule fields on every meta entry\n validateScheduleFields(metaData[metaEntryId]!, { model: model.id, locale, entry: metaEntryId }, issues)\n\n if (!(metaEntryId in contentData)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n entry: metaEntryId,\n message: `Orphan meta: meta entry \"${metaEntryId}\" exists but content entry missing`,\n })\n if (fix && projectRoot) {\n delete metaData[metaEntryId]\n const metaAbs = join(projectRoot, metaRelPath)\n if (Object.keys(metaData).length > 0) {\n await writeJson(metaAbs, metaData)\n } else {\n await rm(metaAbs, { force: true })\n }\n fixed++\n }\n }\n }\n }\n\n // Reverse check: content entries without meta\n for (const entryId of Object.keys(contentData)) {\n if (!metaData || !(entryId in metaData)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n entry: entryId,\n message: `Orphan content: entry \"${entryId}\" has no metadata`,\n })\n if (fix && projectRoot) {\n await writeMeta(projectRoot, model, { locale, entryId }, {\n status: 'draft',\n source: 'import',\n updated_by: 'contentrain-mcp',\n })\n fixed++\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function validateSingletonModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, unknown>>(reader, filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(data, model.fields, { model: model.id, locale }, issues)\n\n if (model.fields) {\n const entryResult = validateContent(data, model.fields, model.id, locale)\n issues.push(...entryResult.errors)\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n const relationErrors = await checkRelationIntegrity(\n data,\n model.fields,\n model.id,\n locale,\n undefined,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, unknown> = {}\n for (const key of sorted) {\n resorted[key] = data[key]\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate schedule fields in singleton meta\n const singletonMetaData = await readJsonViaReader<EntryMeta>(reader, metaFilePath(model, locale))\n if (singletonMetaData) {\n validateScheduleFields(singletonMetaData, { model: model.id, locale }, issues)\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function validateDictionaryModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n\n const localeKeys: Record<string, Set<string>> = {}\n\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, string>>(reader, filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n localeKeys[locale] = new Set(Object.keys(data))\n\n // Secret detection\n for (const [key, value] of Object.entries(data)) {\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: key,\n message: `Potential secret detected in key \"${key}\"`,\n })\n }\n }\n\n // Duplicate value detection\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n for (const [value, dupeKeys] of valueToKeys) {\n if (dupeKeys.length > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: `Duplicate value \"${truncated}\" mapped to ${dupeKeys.length} keys: [${dupeKeys.join(', ')}]`,\n })\n }\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, string> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n }\n\n // Key parity check\n if (model.i18n && Object.keys(localeKeys).length > 1) {\n const localeNames = Object.keys(localeKeys)\n for (let i = 1; i < localeNames.length; i++) {\n const locA = localeNames[0]!\n const locB = localeNames[i]!\n const keysA = localeKeys[locA]!\n const keysB = localeKeys[locB]!\n\n for (const k of keysA) {\n if (!keysB.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locB,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locA} but missing in ${locB}`,\n })\n }\n }\n for (const k of keysB) {\n if (!keysA.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locA,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locB} but missing in ${locA}`,\n })\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function discoverDocumentSlugs(\n reader: RepoReader,\n cDir: string,\n model: ModelDefinition,\n): Promise<string[]> {\n const strategy = resolveLocaleStrategy(model)\n const entries = await reader.listDirectory(cDir)\n\n // When i18n is disabled, documents are flat {slug}.md files\n if (!model.i18n) {\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n }\n\n if (strategy === 'file') {\n // Each slug is a subdirectory\n return entries.filter(e => !e.startsWith('.'))\n }\n\n if (strategy === 'suffix') {\n // Files like {slug}.{locale}.md — extract unique slugs\n const slugs = new Set<string>()\n for (const f of entries) {\n if (!f.endsWith('.md')) continue\n // Remove last two dot-separated segments (.locale.md)\n const parts = f.replace(/\\.md$/, '').split('.')\n if (parts.length >= 2) {\n parts.pop() // remove locale\n slugs.add(parts.join('.'))\n }\n }\n return [...slugs]\n }\n\n if (strategy === 'directory') {\n // Locale subdirectories contain {slug}.md files — collect slugs from all locale dirs\n const slugs = new Set<string>()\n const localeLists = await Promise.all(\n entries\n .filter(localeDir => !localeDir.startsWith('.'))\n .map(localeDir => reader.listDirectory(`${cDir}/${localeDir}`)),\n )\n for (const files of localeLists) {\n for (const f of files) {\n if (f.endsWith('.md')) slugs.add(f.replace(/\\.md$/, ''))\n }\n }\n return [...slugs]\n }\n\n // 'none' — {slug}.md files, no locale\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n}\n\nasync function validateDocumentModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n const cDir = contentDirPath(model)\n\n if (!await reader.fileExists(cDir)) return { entries: 0, fixed: 0 }\n\n const slugs = await discoverDocumentSlugs(reader, cDir, model)\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n const locales = model.i18n ? config.locales.supported : [config.locales.default]\n\n for (const slug of slugs) {\n if (slug.startsWith('.')) continue\n\n for (const locale of locales) {\n const filePath = documentFilePath(model, locale, slug)\n const raw = await readTextViaReader(reader, filePath)\n\n if (!raw) {\n if (model.i18n) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n slug,\n message: `Missing translation: document \"${slug}\" missing ${locale} locale file`,\n })\n if (fix && projectRoot) {\n // Create empty template\n const template = `---\\nslug: ${slug}\\n---\\n`\n await writeText(join(projectRoot, filePath), template)\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n const { frontmatter, body } = parseFrontmatter(raw)\n\n // Always scan undeclared frontmatter fields + body for secrets.\n scanUndeclaredFieldsForSecrets(\n frontmatter,\n // Treat `body` as a declared field so the undeclared-scan skips it; body\n // is scanned separately below.\n { ...model.fields, body: true } as Record<string, unknown>,\n { model: model.id, locale, slug },\n issues,\n )\n\n if (detectSecrets(body).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: 'body',\n message: 'Potential secret detected in document body',\n })\n }\n\n if (model.fields) {\n const fieldsWithoutBody = Object.fromEntries(\n Object.entries(model.fields).filter(([name]) => name !== 'body'),\n )\n const entryResult = validateContent(\n frontmatter,\n fieldsWithoutBody,\n model.id,\n locale,\n )\n for (const err of entryResult.errors) {\n issues.push({ ...err, slug })\n }\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n const relationErrors = await checkRelationIntegrity(\n frontmatter,\n fieldsWithoutBody,\n model.id,\n locale,\n undefined,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n for (const err of relationErrors) {\n issues.push({ ...err, slug })\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\n// ─── Orphan content check ───\n\nasync function checkOrphanContent(\n reader: RepoReader,\n validModelIds: Set<string>,\n issues: ValidationError[],\n _fix: boolean,\n): Promise<number> {\n const fixed = 0\n const contentBase = '.contentrain/content'\n const domains = await reader.listDirectory(contentBase)\n\n const modelLists = await Promise.all(\n domains\n .filter(d => !d.startsWith('.'))\n .map(async d => ({ domain: d, dirs: await reader.listDirectory(`${contentBase}/${d}`) })),\n )\n\n for (const { dirs } of modelLists) {\n for (const modelDir of dirs) {\n if (modelDir.startsWith('.')) continue\n if (!validModelIds.has(modelDir)) {\n issues.push({\n severity: 'warning',\n model: modelDir,\n message: `Orphan content: content directory exists for deleted model \"${modelDir}\"`,\n })\n }\n }\n }\n\n return fixed\n}\n\n// ─── Main validation function ───\n\n/**\n * Validate a project's content against its model schemas.\n *\n * Two signatures:\n *\n * - `validateProject(projectRoot, options?)` — legacy disk-backed flow.\n * `options.fix: true` applies structural repairs (canonical sort,\n * orphan meta, missing locale files) directly on disk.\n * - `validateProject(reader, options?)` — reader-backed flow used by\n * HTTP/remote callers (e.g. GitHubProvider). `options.fix` is ignored\n * because remote flows cannot write to the local filesystem.\n *\n * Read-side behavior is identical across both signatures so Studio and\n * local CLIs see the same issue set for the same content state.\n */\nexport async function validateProject(projectRoot: string, options?: ValidateOptions): Promise<ValidateResult>\nexport async function validateProject(reader: RepoReader, options?: ValidateOptions): Promise<ValidateResult>\nexport async function validateProject(\n input: string | RepoReader,\n options?: ValidateOptions,\n): Promise<ValidateResult> {\n const reader: RepoReader = typeof input === 'string' ? new LocalReader(input) : input\n const projectRoot: string | undefined = typeof input === 'string' ? input : undefined\n const fix = Boolean(options?.fix) && projectRoot !== undefined\n\n const issues: ValidationError[] = []\n let totalEntries = 0\n let totalFixed = 0\n let modelsChecked = 0\n\n const config = await readConfig(reader)\n if (!config) {\n return {\n valid: false,\n summary: { errors: 1, warnings: 0, notices: 0, models_checked: 0, entries_checked: 0 },\n issues: [{ severity: 'error', message: 'Project not initialized: config.json missing' }],\n fixed: 0,\n }\n }\n\n const modelSummaries = await listModels(reader)\n const validModelIds = new Set(modelSummaries.map(m => m.id))\n const modelsToCheck = options?.model\n ? modelSummaries.filter(m => m.id === options.model)\n : modelSummaries\n\n for (const summary of modelsToCheck) {\n const model = await readModel(reader, summary.id)\n if (!model) continue\n\n modelsChecked++\n let result: { entries: number; fixed: number }\n\n switch (model.kind) {\n case 'collection':\n result = await validateCollectionModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'singleton':\n result = await validateSingletonModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'dictionary':\n result = await validateDictionaryModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'document':\n result = await validateDocumentModel(reader, projectRoot, model, config, issues, fix)\n break\n default:\n result = { entries: 0, fixed: 0 }\n }\n\n totalEntries += result.entries\n totalFixed += result.fixed\n }\n\n // Orphan content check (only when checking all models)\n if (!options?.model) {\n totalFixed += await checkOrphanContent(reader, validModelIds, issues, fix)\n }\n\n // Cross-dictionary duplicate value detection (only when checking all models)\n if (!options?.model) {\n const dictModels = modelsToCheck.filter(m => m.kind === 'dictionary')\n if (dictModels.length > 1) {\n const globalValueMap: Record<string, Map<string, Array<{ model: string; key: string }>>> = {}\n\n for (const summary of dictModels) {\n const model = await readModel(reader, summary.id)\n if (!model) continue\n\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n if (!globalValueMap[locale]) globalValueMap[locale] = new Map()\n const data = await readJsonViaReader<Record<string, string>>(reader, contentFilePath(model, locale))\n if (!data) continue\n\n for (const [key, value] of Object.entries(data)) {\n const refs = globalValueMap[locale]!.get(value)\n if (refs) refs.push({ model: model.id, key })\n else globalValueMap[locale]!.set(value, [{ model: model.id, key }])\n }\n }\n }\n\n for (const [locale, valueMap] of Object.entries(globalValueMap)) {\n for (const [value, refs] of valueMap) {\n const uniqueModels = new Set(refs.map(r => r.model))\n if (uniqueModels.size > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'notice',\n locale,\n message: `Cross-model duplicate value \"${truncated}\" in ${refs.map(r => `${r.model}/${r.key}`).join(', ')}`,\n })\n }\n }\n }\n }\n }\n\n const errors = issues.filter(i => i.severity === 'error').length\n const warnings = issues.filter(i => i.severity === 'warning').length\n const notices = issues.filter(i => i.severity === 'notice').length\n\n return {\n valid: errors === 0,\n summary: {\n errors,\n warnings,\n notices,\n models_checked: modelsChecked,\n entries_checked: totalEntries,\n },\n issues,\n fixed: totalFixed,\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/mcp",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "mcpName": "io.github.Contentrain/contentrain",
5
5
  "license": "MIT",
6
6
  "description": "Local-first MCP server for AI-generated content governance — 17 deterministic tools, stdio + HTTP transports, Local / GitHub / GitLab providers",
@@ -152,7 +152,7 @@
152
152
  "simple-git": "^3.27.0",
153
153
  "typescript": "^5.7.0",
154
154
  "zod": "^3.24.0",
155
- "@contentrain/types": "0.6.0"
155
+ "@contentrain/types": "0.7.0"
156
156
  },
157
157
  "optionalDependencies": {
158
158
  "@astrojs/compiler": "^2.10.0",
@@ -1,149 +0,0 @@
1
- import { t as readConfig } from "./config-Lw7vpK9n.mjs";
2
- import { CONTENTRAIN_BRANCH } from "@contentrain/types";
3
- import { simpleGit } from "simple-git";
4
- //#region src/git/branch-lifecycle.ts
5
- /**
6
- * Lists all local contentrain/* branches, deletes those already merged
7
- * into the base branch, and returns the count of remaining unmerged ones.
8
- */
9
- async function cleanupMergedBranches(projectRoot) {
10
- const git = simpleGit(projectRoot);
11
- const config = await readConfig(projectRoot);
12
- const baseBranch = config?.repository?.default_branch ?? process.env["CONTENTRAIN_BRANCH"] ?? ((await git.raw(["branch", "--show-current"])).trim() || "main");
13
- const contentrainBranches = (await git.branchLocal()).all.filter((b) => b.startsWith("cr/")).filter((b) => b !== CONTENTRAIN_BRANCH);
14
- if (contentrainBranches.length === 0) return {
15
- deleted: 0,
16
- remaining: 0,
17
- deletedBranches: []
18
- };
19
- let mergedRaw = "";
20
- try {
21
- mergedRaw = await git.raw([
22
- "branch",
23
- "--merged",
24
- CONTENTRAIN_BRANCH
25
- ]);
26
- } catch {
27
- try {
28
- mergedRaw = await git.raw([
29
- "branch",
30
- "--merged",
31
- baseBranch
32
- ]);
33
- } catch {
34
- return {
35
- deleted: 0,
36
- remaining: contentrainBranches.length,
37
- deletedBranches: []
38
- };
39
- }
40
- }
41
- const mergedSet = new Set(mergedRaw.split("\n").map((b) => b.replace(/^\*?\s+/, "").trim()).filter(Boolean));
42
- const mergedContentrain = contentrainBranches.filter((b) => mergedSet.has(b));
43
- const deletedBranches = [];
44
- const retentionMs = (config?.branchRetention ?? 30) * 24 * 60 * 60 * 1e3;
45
- const now = Date.now();
46
- for (const branch of mergedContentrain) try {
47
- const timestampRaw = (await git.raw([
48
- "log",
49
- "-1",
50
- "--format=%ct",
51
- branch
52
- ])).trim();
53
- if (now - Number(timestampRaw) * 1e3 < retentionMs) continue;
54
- await git.raw([
55
- "branch",
56
- "-d",
57
- branch
58
- ]);
59
- deletedBranches.push(branch);
60
- } catch {}
61
- const remaining = contentrainBranches.length - deletedBranches.length;
62
- return {
63
- deleted: deletedBranches.length,
64
- remaining,
65
- deletedBranches
66
- };
67
- }
68
- /**
69
- * Check branch health: count contentrain/* branches and return warning/blocked status.
70
- * - 50+ branches: warning
71
- * - 80+ branches: blocked
72
- */
73
- async function checkBranchHealth(projectRoot) {
74
- const git = simpleGit(projectRoot);
75
- const config = await readConfig(projectRoot);
76
- const baseBranch = config?.repository?.default_branch ?? process.env["CONTENTRAIN_BRANCH"] ?? ((await git.raw(["branch", "--show-current"])).trim() || "main");
77
- const contentrainBranches = (await git.branchLocal()).all.filter((b) => b.startsWith("cr/")).filter((b) => b !== CONTENTRAIN_BRANCH);
78
- const total = contentrainBranches.length;
79
- let mergedCount = 0;
80
- try {
81
- const mergedRaw = await git.raw([
82
- "branch",
83
- "--merged",
84
- CONTENTRAIN_BRANCH
85
- ]);
86
- const mergedSet = new Set(mergedRaw.split("\n").map((b) => b.replace(/^\*?\s+/, "").trim()).filter(Boolean));
87
- mergedCount = contentrainBranches.filter((b) => mergedSet.has(b)).length;
88
- } catch {
89
- try {
90
- const mergedRaw = await git.raw([
91
- "branch",
92
- "--merged",
93
- baseBranch
94
- ]);
95
- const mergedSet = new Set(mergedRaw.split("\n").map((b) => b.replace(/^\*?\s+/, "").trim()).filter(Boolean));
96
- mergedCount = contentrainBranches.filter((b) => mergedSet.has(b)).length;
97
- } catch {}
98
- }
99
- const warnLimit = config?.branchWarnLimit ?? 50;
100
- const blockLimit = config?.branchBlockLimit ?? 80;
101
- const unmerged = total - mergedCount;
102
- const warning = unmerged >= warnLimit;
103
- const blocked = unmerged >= blockLimit;
104
- let message;
105
- if (blocked) message = `BLOCKED: ${unmerged} active contentrain branches (limit: ${blockLimit}). Run cleanup or merge/delete old branches before creating new ones.`;
106
- else if (warning) message = `WARNING: ${unmerged} active contentrain branches. Consider merging or deleting old branches (warning at ${warnLimit}, blocked at ${blockLimit}).`;
107
- return {
108
- total,
109
- merged: mergedCount,
110
- unmerged,
111
- warning,
112
- blocked,
113
- message
114
- };
115
- }
116
- /**
117
- * Compute the diff between a feature branch and its base.
118
- *
119
- * Defaults `base` to `CONTENTRAIN_BRANCH` — the singleton content-
120
- * tracking branch every feature branch forks from. Passing the repo's
121
- * default branch (e.g. `main`) is almost always a bug: when
122
- * `contentrain` is ahead of `main`, the diff picks up unrelated
123
- * historical content changes that the feature branch did not produce.
124
- *
125
- * Used by `contentrain serve` (branch detail view), the `contentrain
126
- * diff` CLI command, and any Studio-side driver that needs to preview
127
- * a feature branch before approving it.
128
- */
129
- async function branchDiff(projectRoot, opts) {
130
- const git = simpleGit(projectRoot);
131
- const base = opts.base ?? CONTENTRAIN_BRANCH;
132
- const range = `${base}...${opts.branch}`;
133
- const [stat, patch, summary] = await Promise.all([
134
- git.diff([range, "--stat"]),
135
- git.diff([range]),
136
- git.diffSummary([range])
137
- ]);
138
- return {
139
- branch: opts.branch,
140
- base,
141
- stat,
142
- patch,
143
- filesChanged: summary.changed
144
- };
145
- }
146
- //#endregion
147
- export { checkBranchHealth as n, cleanupMergedBranches as r, branchDiff as t };
148
-
149
- //# sourceMappingURL=branch-lifecycle-DjX3MXNG.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"branch-lifecycle-DjX3MXNG.mjs","names":[],"sources":["../src/git/branch-lifecycle.ts"],"sourcesContent":["import { simpleGit } from 'simple-git'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport { readConfig } from '../core/config.js'\n\nexport interface CleanupResult {\n deleted: number\n remaining: number\n deletedBranches: string[]\n}\n\nexport interface BranchHealthCheck {\n total: number\n merged: number\n unmerged: number\n warning: boolean\n blocked: boolean\n message?: string\n}\n\n/**\n * Lists all local contentrain/* branches, deletes those already merged\n * into the base branch, and returns the count of remaining unmerged ones.\n */\nexport async function cleanupMergedBranches(projectRoot: string): Promise<CleanupResult> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n\n // Determine base branch\n const baseBranch = config?.repository?.default_branch\n ?? process.env['CONTENTRAIN_BRANCH']\n ?? ((await git.raw(['branch', '--show-current'])).trim() || 'main')\n\n // Get all local branches (exclude the dedicated contentrain branch itself)\n const branchSummary = await git.branchLocal()\n const contentrainBranches = branchSummary.all\n .filter(b => b.startsWith('cr/'))\n .filter(b => b !== CONTENTRAIN_BRANCH)\n\n if (contentrainBranches.length === 0) {\n return { deleted: 0, remaining: 0, deletedBranches: [] }\n }\n\n // Check merged into the dedicated contentrain branch, falling back to baseBranch\n let mergedRaw = ''\n try {\n mergedRaw = await git.raw(['branch', '--merged', CONTENTRAIN_BRANCH])\n } catch {\n // Contentrain branch may not exist yet (pre-init); fall back to baseBranch\n try {\n mergedRaw = await git.raw(['branch', '--merged', baseBranch])\n } catch {\n // Base branch may not exist either; nothing is merged\n return { deleted: 0, remaining: contentrainBranches.length, deletedBranches: [] }\n }\n }\n\n const mergedSet = new Set(\n mergedRaw.split('\\n').map(b => b.replace(/^\\*?\\s+/, '').trim()).filter(Boolean),\n )\n\n const mergedContentrain = contentrainBranches.filter(b => mergedSet.has(b))\n const deletedBranches: string[] = []\n\n // Determine retention period (days). Default: 7\n const retentionDays = config?.branchRetention ?? 30\n const retentionMs = retentionDays * 24 * 60 * 60 * 1000\n const now = Date.now()\n\n // Delete merged branches only if older than retention period\n for (const branch of mergedContentrain) {\n try {\n const timestampRaw = (await git.raw(['log', '-1', '--format=%ct', branch])).trim()\n const commitTimestamp = Number(timestampRaw) * 1000\n if (now - commitTimestamp < retentionMs) {\n continue // Branch is within retention period — keep it\n }\n await git.raw(['branch', '-d', branch])\n deletedBranches.push(branch)\n } catch {\n // Branch may be checked out, locked, or log failed — skip\n }\n }\n\n const remaining = contentrainBranches.length - deletedBranches.length\n return { deleted: deletedBranches.length, remaining, deletedBranches }\n}\n\n/**\n * Check branch health: count contentrain/* branches and return warning/blocked status.\n * - 50+ branches: warning\n * - 80+ branches: blocked\n */\nexport async function checkBranchHealth(projectRoot: string): Promise<BranchHealthCheck> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n\n const baseBranch = config?.repository?.default_branch\n ?? process.env['CONTENTRAIN_BRANCH']\n ?? ((await git.raw(['branch', '--show-current'])).trim() || 'main')\n\n const branchSummary = await git.branchLocal()\n const contentrainBranches = branchSummary.all\n .filter(b => b.startsWith('cr/'))\n .filter(b => b !== CONTENTRAIN_BRANCH)\n const total = contentrainBranches.length\n\n // Count merged into contentrain branch, falling back to baseBranch\n let mergedCount = 0\n try {\n const mergedRaw = await git.raw(['branch', '--merged', CONTENTRAIN_BRANCH])\n const mergedSet = new Set(\n mergedRaw.split('\\n').map(b => b.replace(/^\\*?\\s+/, '').trim()).filter(Boolean),\n )\n mergedCount = contentrainBranches.filter(b => mergedSet.has(b)).length\n } catch {\n // Contentrain branch may not exist yet (pre-init); fall back to baseBranch\n try {\n const mergedRaw = await git.raw(['branch', '--merged', baseBranch])\n const mergedSet = new Set(\n mergedRaw.split('\\n').map(b => b.replace(/^\\*?\\s+/, '').trim()).filter(Boolean),\n )\n mergedCount = contentrainBranches.filter(b => mergedSet.has(b)).length\n } catch {\n // ignore — neither branch exists\n }\n }\n\n const warnLimit = config?.branchWarnLimit ?? 50\n const blockLimit = config?.branchBlockLimit ?? 80\n const unmerged = total - mergedCount\n const warning = unmerged >= warnLimit\n const blocked = unmerged >= blockLimit\n\n let message: string | undefined\n if (blocked) {\n message = `BLOCKED: ${unmerged} active contentrain branches (limit: ${blockLimit}). Run cleanup or merge/delete old branches before creating new ones.`\n } else if (warning) {\n message = `WARNING: ${unmerged} active contentrain branches. Consider merging or deleting old branches (warning at ${warnLimit}, blocked at ${blockLimit}).`\n }\n\n return { total, merged: mergedCount, unmerged, warning, blocked, message }\n}\n\nexport interface BranchDiffResult {\n /** The feature branch the diff was computed from. */\n branch: string\n /** The base ref the diff was computed against. Defaults to the `contentrain` branch. */\n base: string\n /** `git diff --stat` output — human-readable summary. */\n stat: string\n /** Raw unified diff. */\n patch: string\n /** Number of files touched in the diff. */\n filesChanged: number\n}\n\n/**\n * Compute the diff between a feature branch and its base.\n *\n * Defaults `base` to `CONTENTRAIN_BRANCH` — the singleton content-\n * tracking branch every feature branch forks from. Passing the repo's\n * default branch (e.g. `main`) is almost always a bug: when\n * `contentrain` is ahead of `main`, the diff picks up unrelated\n * historical content changes that the feature branch did not produce.\n *\n * Used by `contentrain serve` (branch detail view), the `contentrain\n * diff` CLI command, and any Studio-side driver that needs to preview\n * a feature branch before approving it.\n */\nexport async function branchDiff(\n projectRoot: string,\n opts: { branch: string, base?: string },\n): Promise<BranchDiffResult> {\n const git = simpleGit(projectRoot)\n const base = opts.base ?? CONTENTRAIN_BRANCH\n const range = `${base}...${opts.branch}`\n\n const [stat, patch, summary] = await Promise.all([\n git.diff([range, '--stat']),\n git.diff([range]),\n git.diffSummary([range]),\n ])\n\n return {\n branch: opts.branch,\n base,\n stat,\n patch,\n filesChanged: summary.changed,\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"config-Lw7vpK9n.mjs","names":[],"sources":["../src/core/config.ts"],"sourcesContent":["import type { ContentrainConfig, Vocabulary } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { contentrainDir, readJson } from '../util/fs.js'\nimport type { RepoReader } from './contracts/index.js'\n\nconst CONFIG_PATH = '.contentrain/config.json'\nconst VOCABULARY_PATH = '.contentrain/vocabulary.json'\n\nasync function readJsonViaReader<T>(reader: RepoReader, path: string): Promise<T | null> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return null\n }\n}\n\nfunction normaliseConfig(raw: Partial<ContentrainConfig> | null): ContentrainConfig | null {\n if (!raw) return null\n return {\n version: raw.version ?? 1,\n stack: raw.stack ?? 'other',\n workflow: raw.workflow ?? 'auto-merge',\n locales: {\n default: raw.locales?.default ?? 'en',\n supported: raw.locales?.supported ?? [raw.locales?.default ?? 'en'],\n },\n domains: raw.domains ?? [],\n repository: raw.repository,\n assets_path: raw.assets_path,\n branchRetention: raw.branchRetention,\n branchWarnLimit: raw.branchWarnLimit,\n branchBlockLimit: raw.branchBlockLimit,\n }\n}\n\n/**\n * Read `.contentrain/config.json` and normalise it with safe defaults.\n *\n * Accepts either a legacy `projectRoot` string (LocalReader is constructed\n * internally) or any `RepoReader` — tool handlers pass their provider so\n * the same logic resolves against local fs or the GitHub Git Data API.\n */\nexport function readConfig(projectRoot: string): Promise<ContentrainConfig | null>\nexport function readConfig(reader: RepoReader): Promise<ContentrainConfig | null>\nexport async function readConfig(input: string | RepoReader): Promise<ContentrainConfig | null> {\n if (typeof input === 'string') {\n const raw = await readJson<Partial<ContentrainConfig>>(join(contentrainDir(input), 'config.json'))\n return normaliseConfig(raw)\n }\n const raw = await readJsonViaReader<Partial<ContentrainConfig>>(input, CONFIG_PATH)\n return normaliseConfig(raw)\n}\n\n/**\n * Read `.contentrain/vocabulary.json`. Same dual signature as {@link readConfig}.\n */\nexport function readVocabulary(projectRoot: string): Promise<Vocabulary | null>\nexport function readVocabulary(reader: RepoReader): Promise<Vocabulary | null>\nexport async function readVocabulary(input: string | RepoReader): Promise<Vocabulary | null> {\n if (typeof input === 'string') {\n return readJson<Vocabulary>(join(contentrainDir(input), 'vocabulary.json'))\n }\n return readJsonViaReader<Vocabulary>(input, VOCABULARY_PATH)\n}\n"],"mappings":";;;AAKA,MAAM,cAAc;AACpB,MAAM,kBAAkB;AAExB,eAAe,kBAAqB,QAAoB,MAAiC;AACvF,KAAI;AACF,SAAO,KAAK,MAAM,MAAM,OAAO,SAAS,KAAK,CAAC;SACxC;AACN,SAAO;;;AAIX,SAAS,gBAAgB,KAAkE;AACzF,KAAI,CAAC,IAAK,QAAO;AACjB,QAAO;EACL,SAAS,IAAI,WAAW;EACxB,OAAO,IAAI,SAAS;EACpB,UAAU,IAAI,YAAY;EAC1B,SAAS;GACP,SAAS,IAAI,SAAS,WAAW;GACjC,WAAW,IAAI,SAAS,aAAa,CAAC,IAAI,SAAS,WAAW,KAAK;GACpE;EACD,SAAS,IAAI,WAAW,EAAE;EAC1B,YAAY,IAAI;EAChB,aAAa,IAAI;EACjB,iBAAiB,IAAI;EACrB,iBAAiB,IAAI;EACrB,kBAAkB,IAAI;EACvB;;AAYH,eAAsB,WAAW,OAA+D;AAC9F,KAAI,OAAO,UAAU,SAEnB,QAAO,gBADK,MAAM,SAAqC,KAAK,eAAe,MAAM,EAAE,cAAc,CAAC,CACvE;AAG7B,QAAO,gBADK,MAAM,kBAA8C,OAAO,YAAY,CACxD;;AAQ7B,eAAsB,eAAe,OAAwD;AAC3F,KAAI,OAAO,UAAU,SACnB,QAAO,SAAqB,KAAK,eAAe,MAAM,EAAE,kBAAkB,CAAC;AAE7E,QAAO,kBAA8B,OAAO,gBAAgB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"context-DS40WOdw.mjs","names":[],"sources":["../src/core/context.ts"],"sourcesContent":["import type { ContextJson, ContextSource } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { contentrainDir, readJson, writeJson } from '../util/fs.js'\nimport type { FileChange, RepoReader } from './contracts/index.js'\nimport { canonicalStringify } from './serialization/index.js'\nimport { listModels, readModel, countEntries } from './model-manager.js'\nimport { readConfig } from './config.js'\n\nconst CONTEXT_PATH = '.contentrain/context.json'\n\n/**\n * Read the committed `.contentrain/context.json` payload written by the\n * most recent content or model operation. Returns `null` when the file\n * does not exist (fresh project, or reader lookup failure).\n *\n * Dual signature — the local flow passes a `projectRoot` string, remote\n * flows (GitHubProvider, GitLabProvider, any custom `RepoReader`) pass\n * the reader directly so `contentrain_status` over HTTP can still\n * report the last operation + stats.\n */\nexport function readContext(projectRoot: string): Promise<ContextJson | null>\nexport function readContext(reader: RepoReader): Promise<ContextJson | null>\nexport async function readContext(input: string | RepoReader): Promise<ContextJson | null> {\n if (typeof input === 'string') {\n return readJson<ContextJson>(join(contentrainDir(input), 'context.json'))\n }\n try {\n const raw = await input.readFile(CONTEXT_PATH)\n return JSON.parse(raw) as ContextJson\n } catch {\n return null\n }\n}\n\nfunction resolveSource(explicit?: ContextSource): ContextSource {\n if (explicit) return explicit\n return process.env['CONTENTRAIN_SOURCE'] === 'mcp-studio' ? 'mcp-studio' : 'mcp-local'\n}\n\nasync function computeEntriesCount(projectRoot: string): Promise<number | null> {\n try {\n const models = await listModels(projectRoot)\n const fullModels = await Promise.all(models.map(m => readModel(projectRoot, m.id)))\n const counts = await Promise.all(\n fullModels\n .filter((m): m is NonNullable<typeof m> => m !== null)\n .map(m => countEntries(projectRoot, m)),\n )\n return counts.reduce((acc, c) => acc + c.total, 0)\n } catch {\n return null\n }\n}\n\nexport async function writeContext(\n projectRoot: string,\n operation: { tool: string, model: string, locale?: string, entries?: string[] },\n): Promise<void> {\n const models = await listModels(projectRoot)\n const config = await readConfig(projectRoot)\n const locales = config?.locales.supported ?? ['en']\n const totalEntries = await computeEntriesCount(projectRoot)\n const source = resolveSource()\n\n const context: ContextJson = {\n version: '1',\n lastOperation: {\n tool: operation.tool,\n model: operation.model,\n locale: operation.locale ?? config?.locales.default ?? 'en',\n entries: operation.entries,\n timestamp: new Date().toISOString(),\n source,\n },\n stats: {\n models: models.length,\n entries: totalEntries as number,\n locales,\n lastSync: new Date().toISOString(),\n },\n }\n\n await writeJson(join(contentrainDir(projectRoot), 'context.json'), context)\n}\n\n/**\n * Build a FileChange for `.contentrain/context.json` that remote providers\n * (GitHubProvider over HTTP, for example) can slot into their plan. The\n * local write path still uses {@link writeContext} directly because its\n * transaction layer writes into the post-apply worktree with real stats.\n *\n * Remote providers (Phase 5.5+) now also get accurate entry counts: the\n * reader-based {@link countEntries} walks each model over the provider's\n * read surface. GitHubProvider pays an extra round trip per model; the\n * payoff is a context.json that matches what the local write path emits,\n * so cross-provider merges stay deterministic.\n */\nexport async function buildContextChange(\n reader: RepoReader,\n operation: { tool: string, model: string, locale?: string, entries?: string[] },\n source?: ContextSource,\n): Promise<FileChange> {\n const config = await readConfig(reader)\n const models = await listModels(reader)\n const locales = config?.locales.supported ?? ['en']\n\n let totalEntries: number | null = null\n try {\n const fullModels = await Promise.all(models.map(m => readModel(reader, m.id)))\n const counts = await Promise.all(\n fullModels\n .filter((m): m is NonNullable<typeof m> => m !== null)\n .map(m => countEntries(reader, m)),\n )\n totalEntries = counts.reduce((acc, c) => acc + c.total, 0)\n } catch {\n totalEntries = null\n }\n\n const context: ContextJson = {\n version: '1',\n lastOperation: {\n tool: operation.tool,\n model: operation.model,\n locale: operation.locale ?? config?.locales.default ?? 'en',\n entries: operation.entries,\n timestamp: new Date().toISOString(),\n source: resolveSource(source),\n },\n stats: {\n models: models.length,\n entries: totalEntries as number,\n locales,\n lastSync: new Date().toISOString(),\n },\n }\n\n return {\n path: CONTEXT_PATH,\n content: canonicalStringify(context),\n }\n}\n"],"mappings":";;;;;;AAQA,MAAM,eAAe;AAcrB,eAAsB,YAAY,OAAyD;AACzF,KAAI,OAAO,UAAU,SACnB,QAAO,SAAsB,KAAK,eAAe,MAAM,EAAE,eAAe,CAAC;AAE3E,KAAI;EACF,MAAM,MAAM,MAAM,MAAM,SAAS,aAAa;AAC9C,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,SAAO;;;AAIX,SAAS,cAAc,UAAyC;AAC9D,KAAI,SAAU,QAAO;AACrB,QAAO,QAAQ,IAAI,0BAA0B,eAAe,eAAe;;AAG7E,eAAe,oBAAoB,aAA6C;AAC9E,KAAI;EACF,MAAM,SAAS,MAAM,WAAW,YAAY;EAC5C,MAAM,aAAa,MAAM,QAAQ,IAAI,OAAO,KAAI,MAAK,UAAU,aAAa,EAAE,GAAG,CAAC,CAAC;AAMnF,UALe,MAAM,QAAQ,IAC3B,WACG,QAAQ,MAAkC,MAAM,KAAK,CACrD,KAAI,MAAK,aAAa,aAAa,EAAE,CAAC,CAC1C,EACa,QAAQ,KAAK,MAAM,MAAM,EAAE,OAAO,EAAE;SAC5C;AACN,SAAO;;;AAIX,eAAsB,aACpB,aACA,WACe;CACf,MAAM,SAAS,MAAM,WAAW,YAAY;CAC5C,MAAM,SAAS,MAAM,WAAW,YAAY;CAC5C,MAAM,UAAU,QAAQ,QAAQ,aAAa,CAAC,KAAK;CACnD,MAAM,eAAe,MAAM,oBAAoB,YAAY;CAC3D,MAAM,SAAS,eAAe;CAE9B,MAAM,UAAuB;EAC3B,SAAS;EACT,eAAe;GACb,MAAM,UAAU;GAChB,OAAO,UAAU;GACjB,QAAQ,UAAU,UAAU,QAAQ,QAAQ,WAAW;GACvD,SAAS,UAAU;GACnB,4BAAW,IAAI,MAAM,EAAC,aAAa;GACnC;GACD;EACD,OAAO;GACL,QAAQ,OAAO;GACf,SAAS;GACT;GACA,2BAAU,IAAI,MAAM,EAAC,aAAa;GACnC;EACF;AAED,OAAM,UAAU,KAAK,eAAe,YAAY,EAAE,eAAe,EAAE,QAAQ;;;;;;;;;;;;;;AAe7E,eAAsB,mBACpB,QACA,WACA,QACqB;CACrB,MAAM,SAAS,MAAM,WAAW,OAAO;CACvC,MAAM,SAAS,MAAM,WAAW,OAAO;CACvC,MAAM,UAAU,QAAQ,QAAQ,aAAa,CAAC,KAAK;CAEnD,IAAI,eAA8B;AAClC,KAAI;EACF,MAAM,aAAa,MAAM,QAAQ,IAAI,OAAO,KAAI,MAAK,UAAU,QAAQ,EAAE,GAAG,CAAC,CAAC;AAM9E,kBALe,MAAM,QAAQ,IAC3B,WACG,QAAQ,MAAkC,MAAM,KAAK,CACrD,KAAI,MAAK,aAAa,QAAQ,EAAE,CAAC,CACrC,EACqB,QAAQ,KAAK,MAAM,MAAM,EAAE,OAAO,EAAE;SACpD;AACN,iBAAe;;AAqBjB,QAAO;EACL,MAAM;EACN,SAAS,mBApBkB;GAC3B,SAAS;GACT,eAAe;IACb,MAAM,UAAU;IAChB,OAAO,UAAU;IACjB,QAAQ,UAAU,UAAU,QAAQ,QAAQ,WAAW;IACvD,SAAS,UAAU;IACnB,4BAAW,IAAI,MAAM,EAAC,aAAa;IACnC,QAAQ,cAAc,OAAO;IAC9B;GACD,OAAO;IACL,QAAQ,OAAO;IACf,SAAS;IACT;IACA,2BAAU,IAAI,MAAM,EAAC,aAAa;IACnC;GACF,CAIqC;EACrC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"doctor-Dxu_SzZy.mjs","names":[],"sources":["../src/core/doctor.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { stat } from 'node:fs/promises'\nimport { simpleGit } from 'simple-git'\nimport type { ContentrainConfig, ModelDefinition } from '@contentrain/types'\nimport { readConfig } from './config.js'\nimport { listModels, readModel } from './model-manager.js'\nimport { resolveContentDir, resolveJsonFilePath, resolveLocaleStrategy } from './content-manager.js'\nimport { autoDetectSourceDirs, discoverFiles } from './scan-config.js'\nimport { checkBranchHealth } from '../git/branch-lifecycle.js'\nimport { contentrainDir, pathExists, readDir, readJson, readText } from '../util/fs.js'\n\n/**\n * Doctor — project health report.\n *\n * The public entry point is `runDoctor(projectRoot, { usage? })`. It is\n * inherently local-filesystem work (Node version, git install, file\n * mtimes, orphan directory detection), so the MCP tool surface gates\n * it behind the `localWorktree` capability — same pattern as\n * `contentrain_setup` and normalize.\n *\n * The report is structured JSON so three consumers can share it:\n *\n * - The `contentrain doctor` CLI command pretty-prints the checks.\n * - The Serve UI `/api/doctor` route returns the report to the\n * Dashboard's Doctor panel.\n * - Automation (CI, Studio) gets a deterministic JSON shape it can\n * assert against.\n *\n * Usage analysis (`--usage`) is a heavier, opt-in branch — it scans\n * every source file in the repo for content-key references. Kept\n * behind the flag so the default doctor run stays fast.\n */\n\nexport type CheckSeverity = 'error' | 'warning' | 'info'\n\nexport interface DoctorCheck {\n name: string\n pass: boolean\n detail: string\n /**\n * `error` — default for failing checks. Blocks a clean bill of health.\n * `warning` — failing-but-not-blocking (e.g. pending branches above\n * threshold, stale SDK client).\n * `info` — passed check; pure informational.\n */\n severity?: CheckSeverity\n}\n\nexport interface UnusedKeyEntry {\n model: string\n kind: string\n key: string\n locale: string\n}\n\nexport interface DuplicateValueEntry {\n model: string\n locale: string\n value: string\n keys: string[]\n}\n\nexport interface MissingLocaleEntry {\n model: string\n key: string\n missingIn: string\n}\n\nexport interface DoctorUsageAnalysis {\n unusedKeys: UnusedKeyEntry[]\n duplicateValues: DuplicateValueEntry[]\n missingLocaleKeys: MissingLocaleEntry[]\n}\n\nexport interface DoctorReport {\n checks: DoctorCheck[]\n summary: {\n total: number\n passed: number\n failed: number\n warnings: number\n }\n /** Present only when `options.usage === true`. */\n usage?: DoctorUsageAnalysis\n}\n\nexport interface RunDoctorOptions {\n /** Run heavier `--usage` analysis (unused keys, duplicates, locale gaps). */\n usage?: boolean\n}\n\nexport async function runDoctor(\n projectRoot: string,\n options: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n const checks: DoctorCheck[] = []\n\n // ─── 1. Git installed ───\n try {\n const git = simpleGit(projectRoot)\n const version = await git.version()\n checks.push({\n name: 'Git',\n pass: true,\n detail: `v${version.major}.${version.minor}.${version.patch}`,\n })\n } catch {\n checks.push({ name: 'Git', pass: false, detail: 'Not installed or not in PATH', severity: 'error' })\n }\n\n // ─── 2. Git repo initialized ───\n const hasGit = await pathExists(join(projectRoot, '.git'))\n checks.push({\n name: 'Git repository',\n pass: hasGit,\n detail: hasGit ? projectRoot : 'No .git directory found',\n severity: hasGit ? undefined : 'error',\n })\n\n // ─── 3. Node version ───\n const nodeVersion = process.versions.node\n const [major] = nodeVersion.split('.').map(Number)\n const nodePass = (major ?? 0) >= 22\n checks.push({\n name: 'Node.js',\n pass: nodePass,\n detail: `v${nodeVersion}${nodePass ? '' : ' (requires ≥22)'}`,\n severity: nodePass ? undefined : 'error',\n })\n\n // ─── 4. .contentrain/ structure ───\n const crDir = contentrainDir(projectRoot)\n const hasCrDir = await pathExists(crDir)\n const hasConfig = await pathExists(join(crDir, 'config.json'))\n const hasModels = await pathExists(join(crDir, 'models'))\n const hasContent = await pathExists(join(crDir, 'content'))\n const structurePass = hasCrDir && hasConfig && hasModels && hasContent\n\n checks.push({\n name: '.contentrain/ structure',\n pass: structurePass,\n detail: !hasCrDir\n ? 'Not initialized — run `contentrain init`'\n : [\n hasConfig ? null : 'missing config.json',\n hasModels ? null : 'missing models/',\n hasContent ? null : 'missing content/',\n ].filter(Boolean).join(', ') || 'OK',\n severity: structurePass ? undefined : 'error',\n })\n\n // ─── 5. Config parseable ───\n let config: ContentrainConfig | null = null\n if (hasConfig) {\n config = await readConfig(projectRoot)\n checks.push({\n name: 'Config',\n pass: config !== null,\n detail: config\n ? `stack: ${config.stack}, locales: ${config.locales.supported.join(', ')}`\n : 'Failed to parse config.json',\n severity: config ? undefined : 'error',\n })\n }\n\n // ─── 6. Models all parseable ───\n if (hasCrDir) {\n try {\n const models = await listModels(projectRoot)\n const parseResults = await Promise.all(models.map(m => readModel(projectRoot, m.id)))\n const allParseable = parseResults.every(r => r !== null)\n checks.push({\n name: 'Models',\n pass: allParseable,\n detail: `${models.length} model(s)${allParseable ? ', all valid' : ', some failed to parse'}`,\n severity: allParseable ? undefined : 'error',\n })\n } catch {\n checks.push({ name: 'Models', pass: false, detail: 'Failed to read models', severity: 'error' })\n }\n }\n\n // ─── 7. Orphan content ───\n if (hasCrDir) {\n const orphans = await findOrphanContent(projectRoot)\n checks.push({\n name: 'Orphan content',\n pass: orphans.length === 0,\n detail: orphans.length === 0 ? 'None' : `Found: ${orphans.join(', ')}`,\n severity: orphans.length === 0 ? undefined : 'warning',\n })\n }\n\n // ─── 8. Stale contentrain branches ───\n if (hasGit) {\n try {\n const health = await checkBranchHealth(projectRoot)\n checks.push({\n name: 'Pending branches',\n pass: !health.blocked && !health.warning,\n detail: health.message\n ?? (health.unmerged === 0 ? 'None' : `${health.unmerged} active cr/* branch(es)`),\n severity: health.blocked ? 'error' : health.warning ? 'warning' : undefined,\n })\n } catch {\n checks.push({ name: 'Pending branches', pass: true, detail: 'Could not check' })\n }\n }\n\n // ─── 9. SDK client freshness ───\n const clientDir = join(crDir, 'client')\n const modelsDir = join(crDir, 'models')\n if (await pathExists(clientDir) && await pathExists(modelsDir)) {\n try {\n const [clientStat, modelsStat] = await Promise.all([stat(clientDir), stat(modelsDir)])\n const fresh = clientStat.mtimeMs >= modelsStat.mtimeMs\n checks.push({\n name: 'SDK client',\n pass: fresh,\n detail: fresh ? 'Up to date' : 'Stale — run `contentrain generate`',\n severity: fresh ? undefined : 'warning',\n })\n } catch {\n checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' })\n }\n }\n\n // ─── 10–12. Usage analysis (optional) ───\n let usage: DoctorUsageAnalysis | undefined\n if (options.usage && hasCrDir && config) {\n const [unusedKeys, duplicateValues, missingLocaleKeys] = await Promise.all([\n analyzeUnusedKeys(projectRoot, config),\n analyzeDuplicateValues(projectRoot, config),\n analyzeMissingLocaleKeys(projectRoot, config),\n ])\n usage = { unusedKeys, duplicateValues, missingLocaleKeys }\n\n checks.push({\n name: 'Unused content keys',\n pass: unusedKeys.length === 0,\n detail: unusedKeys.length === 0\n ? 'All keys referenced in source'\n : `${unusedKeys.length} key(s) not referenced in source code`,\n severity: unusedKeys.length === 0 ? undefined : 'warning',\n })\n\n checks.push({\n name: 'Duplicate dictionary values',\n pass: duplicateValues.length === 0,\n detail: duplicateValues.length === 0\n ? 'No duplicate values'\n : `${duplicateValues.length} value(s) mapped to multiple keys`,\n severity: duplicateValues.length === 0 ? undefined : 'warning',\n })\n\n checks.push({\n name: 'Locale key coverage',\n pass: missingLocaleKeys.length === 0,\n detail: missingLocaleKeys.length === 0\n ? 'All locales have matching keys'\n : `${missingLocaleKeys.length} key(s) missing in some locales`,\n severity: missingLocaleKeys.length === 0 ? undefined : 'warning',\n })\n }\n\n const passed = checks.filter(c => c.pass).length\n const failed = checks.length - passed\n const warnings = checks.filter(c => !c.pass && c.severity === 'warning').length\n\n const report: DoctorReport = {\n checks,\n summary: { total: checks.length, passed, failed, warnings },\n }\n if (usage) report.usage = usage\n return report\n}\n\nasync function findOrphanContent(projectRoot: string): Promise<string[]> {\n const crDir = contentrainDir(projectRoot)\n const models = await listModels(projectRoot)\n const orphans: string[] = []\n\n const knownContentDirs = new Set<string>()\n for (const m of models) {\n const full = await readModel(projectRoot, m.id)\n const modelForPath = full\n ? {\n ...full,\n content_path: full.content_path ?? (m as { content_path?: string }).content_path,\n }\n : {\n id: m.id,\n name: m.id,\n kind: m.kind,\n domain: m.domain,\n i18n: m.i18n,\n fields: {},\n content_path: (m as { content_path?: string }).content_path,\n }\n knownContentDirs.add(resolveContentDir(projectRoot, modelForPath))\n }\n\n const contentDir = join(crDir, 'content')\n if (await pathExists(contentDir)) {\n const domains = await readDir(contentDir)\n for (const domain of domains) {\n const domainDir = join(contentDir, domain)\n const entries = await readDir(domainDir)\n for (const entry of entries) {\n if (entry === '.gitkeep') continue\n const entryDir = join(domainDir, entry)\n if (!knownContentDirs.has(entryDir)) {\n orphans.push(`${domain}/${entry}`)\n }\n }\n }\n }\n\n for (const dir of knownContentDirs) {\n if (dir.startsWith(contentDir)) continue\n if (!await pathExists(dir)) {\n orphans.push(`(missing custom path) ${dir}`)\n continue\n }\n await readDir(dir)\n }\n\n return orphans\n}\n\nasync function analyzeUnusedKeys(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<UnusedKeyEntry[]> {\n const sourceDirs = await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, { paths: sourceDirs })\n if (files.length === 0) return []\n\n const chunks = await Promise.all(\n files.map(async (relPath) => {\n const content = await readText(join(projectRoot, relPath))\n return content ?? ''\n }),\n )\n const allSource = chunks.join('\\n')\n\n const models = await listModels(projectRoot)\n const defaultLocale = config.locales.default\n const unused: UnusedKeyEntry[] = []\n\n for (const m of models) {\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const keys = await extractContentKeys(projectRoot, fullModel, defaultLocale)\n for (const key of keys) {\n if (!allSource.includes(key)) {\n unused.push({ model: m.id, kind: m.kind, key, locale: defaultLocale })\n }\n }\n }\n\n return unused\n}\n\nasync function extractContentKeys(\n projectRoot: string,\n model: ModelDefinition,\n locale: string,\n): Promise<string[]> {\n const cDir = resolveContentDir(projectRoot, model)\n if (!await pathExists(cDir)) return []\n\n switch (model.kind) {\n case 'dictionary': {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, string>>(filePath)\n return data ? Object.keys(data) : []\n }\n case 'collection': {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, Record<string, unknown>>>(filePath)\n return data ? Object.keys(data) : []\n }\n case 'document': {\n const strategy = resolveLocaleStrategy(model)\n const slugs: string[] = []\n if (!model.i18n) {\n const files = await readDir(cDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n } else if (strategy === 'file') {\n const dirs = await readDir(cDir)\n for (const d of dirs) if (!d.startsWith('.')) slugs.push(d)\n } else if (strategy === 'suffix') {\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) if (f.endsWith(suffix)) slugs.push(f.slice(0, -suffix.length))\n } else if (strategy === 'directory') {\n const localeDir = join(cDir, locale)\n if (await pathExists(localeDir)) {\n const files = await readDir(localeDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n }\n } else {\n const files = await readDir(cDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n }\n return slugs\n }\n case 'singleton':\n return []\n default:\n return []\n }\n}\n\nasync function analyzeDuplicateValues(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<DuplicateValueEntry[]> {\n const models = await listModels(projectRoot)\n const result: DuplicateValueEntry[] = []\n\n for (const m of models) {\n if (m.kind !== 'dictionary') continue\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const cDir = resolveContentDir(projectRoot, fullModel)\n for (const locale of config.locales.supported) {\n const filePath = resolveJsonFilePath(cDir, fullModel, locale)\n const data = await readJson<Record<string, string>>(filePath)\n if (!data) continue\n\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n\n for (const [value, keys] of valueToKeys) {\n if (keys.length > 1) {\n result.push({ model: m.id, locale, value, keys })\n }\n }\n }\n }\n\n return result\n}\n\nasync function analyzeMissingLocaleKeys(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<MissingLocaleEntry[]> {\n if (config.locales.supported.length < 2) return []\n\n const models = await listModels(projectRoot)\n const result: MissingLocaleEntry[] = []\n const defaultLocale = config.locales.default\n const otherLocales = config.locales.supported.filter(l => l !== defaultLocale)\n\n for (const m of models) {\n if (m.kind !== 'dictionary' && m.kind !== 'collection') continue\n if (!m.i18n) continue\n\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const cDir = resolveContentDir(projectRoot, fullModel)\n const defaultPath = resolveJsonFilePath(cDir, fullModel, defaultLocale)\n const defaultData = await readJson<Record<string, unknown>>(defaultPath)\n if (!defaultData) continue\n const defaultKeys = new Set(Object.keys(defaultData))\n\n for (const locale of otherLocales) {\n const localePath = resolveJsonFilePath(cDir, fullModel, locale)\n const localeData = await readJson<Record<string, unknown>>(localePath)\n const localeKeys = localeData ? new Set(Object.keys(localeData)) : new Set<string>()\n\n for (const key of defaultKeys) {\n if (!localeKeys.has(key)) {\n result.push({ model: m.id, key, missingIn: locale })\n }\n }\n }\n }\n\n return result\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"branch-lifecycle.d.mts","names":[],"sources":["../../src/git/branch-lifecycle.ts"],"mappings":";UAIiB,aAAA;EACf,OAAA;EACA,SAAA;EACA,eAAA;AAAA;AAAA,UAGe,iBAAA;EACf,KAAA;EACA,MAAA;EACA,QAAA;EACA,OAAA;EACA,OAAA;EACA,OAAA;AAAA;;;;;iBAOoB,qBAAA,CAAsB,WAAA,WAAsB,OAAA,CAAQ,aAAA;;;;;AAA1E;iBAqEsB,iBAAA,CAAkB,WAAA,WAAsB,OAAA,CAAQ,iBAAA;AAAA,UAmDrD,gBAAA;EAxHwD;EA0HvE,MAAA;EA1HgE;EA4HhE,IAAA;EA5HqF;EA8HrF,IAAA;EAzDoB;EA2DpB,KAAA;;EAEA,YAAA;AAAA;;;;;AAVF;;;;;;;;;iBA0BsB,UAAA,CACpB,WAAA,UACA,IAAA;EAAQ,MAAA;EAAgB,IAAA;AAAA,IACvB,OAAA,CAAQ,gBAAA"}