@contentrain/mcp 3.9.32 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +18 -1
  2. package/dist/{apply-manager-BenQNb_b.mjs → apply-manager-B-O3eAbz.mjs} +16 -8
  3. package/dist/apply-manager-B-O3eAbz.mjs.map +1 -0
  4. package/dist/{branch-lifecycle-CirYfTXu.d.mts → branch-lifecycle-4iZax7a7.d.mts} +3 -3
  5. package/dist/{branch-lifecycle-CirYfTXu.d.mts.map → branch-lifecycle-4iZax7a7.d.mts.map} +1 -1
  6. package/dist/{branch-lifecycle-CcsSyfSw.mjs → branch-lifecycle-D4DxPgQg.mjs} +53 -5
  7. package/dist/branch-lifecycle-D4DxPgQg.mjs.map +1 -0
  8. package/dist/{config-BLwZtwlI.mjs → config-BnNYu128.mjs} +3 -2
  9. package/dist/config-BnNYu128.mjs.map +1 -0
  10. package/dist/content-manager-2g0pkLFh.d.mts.map +1 -1
  11. package/dist/{context-Cp8b8Nxm.mjs → context-D9HXnnGj.mjs} +5 -5
  12. package/dist/{context-Cp8b8Nxm.mjs.map → context-D9HXnnGj.mjs.map} +1 -1
  13. package/dist/core/apply-manager.d.mts.map +1 -1
  14. package/dist/core/apply-manager.mjs +6 -6
  15. package/dist/core/config.d.mts.map +1 -1
  16. package/dist/core/config.mjs +1 -1
  17. package/dist/core/content-manager.mjs +1 -1
  18. package/dist/core/context.mjs +3 -3
  19. package/dist/core/doctor.mjs +4 -4
  20. package/dist/core/model-manager.mjs +1 -1
  21. package/dist/core/ops/index.mjs +6 -6
  22. package/dist/core/validator/index.d.mts.map +1 -1
  23. package/dist/core/validator/index.mjs +3 -3
  24. package/dist/{doctor-Qabcb-Z1.mjs → doctor-Br1NqEl2.mjs} +4 -4
  25. package/dist/{doctor-Qabcb-Z1.mjs.map → doctor-Br1NqEl2.mjs.map} +1 -1
  26. package/dist/git/branch-lifecycle.d.mts +1 -1
  27. package/dist/git/branch-lifecycle.mjs +2 -2
  28. package/dist/git/reconcile.d.mts.map +1 -1
  29. package/dist/git/reconcile.mjs +8 -8
  30. package/dist/git/transaction.d.mts +7 -3
  31. package/dist/git/transaction.d.mts.map +1 -1
  32. package/dist/git/transaction.mjs +6 -6
  33. package/dist/index.mjs +13 -13
  34. package/dist/{local-BQVdC9MM.mjs → local-CBtRYFH4.mjs} +7 -7
  35. package/dist/{local-BQVdC9MM.mjs.map → local-CBtRYFH4.mjs.map} +1 -1
  36. package/dist/{model-manager-BYF2z7qC.mjs → model-manager-D0VztE8M.mjs} +8 -5
  37. package/dist/model-manager-D0VztE8M.mjs.map +1 -0
  38. package/dist/{ops-CQ_IIedh.mjs → ops-CVI308jA.mjs} +9 -4
  39. package/dist/ops-CVI308jA.mjs.map +1 -0
  40. package/dist/providers/local/index.mjs +9 -9
  41. package/dist/{reconcile-CRAukgae.mjs → reconcile-DSflMfhj.mjs} +8 -7
  42. package/dist/reconcile-DSflMfhj.mjs.map +1 -0
  43. package/dist/{reconcile-C7AaUfCl.mjs → reconcile-J-iorvSa.mjs} +5 -4
  44. package/dist/reconcile-J-iorvSa.mjs.map +1 -0
  45. package/dist/server/http/index.mjs +13 -13
  46. package/dist/{server-D8oYrulE.mjs → server-CJbmjX48.mjs} +43 -14
  47. package/dist/server-CJbmjX48.mjs.map +1 -0
  48. package/dist/server.mjs +13 -13
  49. package/dist/{transaction-CcjGiVYg.mjs → transaction-DnrUATJF.mjs} +14 -10
  50. package/dist/transaction-DnrUATJF.mjs.map +1 -0
  51. package/dist/{validator-Bz3jnSnb.mjs → validator-S19GHR4V.mjs} +16 -5
  52. package/dist/{validator-Bz3jnSnb.mjs.map → validator-S19GHR4V.mjs.map} +1 -1
  53. package/package.json +2 -2
  54. package/dist/apply-manager-BenQNb_b.mjs.map +0 -1
  55. package/dist/branch-lifecycle-CcsSyfSw.mjs.map +0 -1
  56. package/dist/config-BLwZtwlI.mjs.map +0 -1
  57. package/dist/model-manager-BYF2z7qC.mjs.map +0 -1
  58. package/dist/ops-CQ_IIedh.mjs.map +0 -1
  59. package/dist/reconcile-C7AaUfCl.mjs.map +0 -1
  60. package/dist/reconcile-CRAukgae.mjs.map +0 -1
  61. package/dist/server-D8oYrulE.mjs.map +0 -1
  62. package/dist/transaction-CcjGiVYg.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"validator-Bz3jnSnb.mjs","names":[],"sources":["../src/core/validator/entry.ts","../src/core/validator/relation-integrity.ts","../src/core/validator/schedule.ts","../src/core/validator/project.ts"],"sourcesContent":["import type { FieldDef, ModelDefinition, ValidationError, ValidationResult } from '@contentrain/types'\nimport { detectSecrets, validateFieldValue } from '@contentrain/types'\n\n/**\n * Context for cross-entry validation (unique, relations, nested references).\n *\n * Shape aligned with Studio's `content-validation.ts:ValidationContext` so\n * Studio can swap its internal implementation for this module without\n * touching call sites.\n */\nexport interface ValidationContext {\n /** All entries in the collection (for unique-constraint checks). Keyed by entry ID. */\n allEntries?: Record<string, Record<string, unknown>>\n /** ID of the entry being validated (excluded from unique-constraint comparisons). */\n currentEntryId?: string\n /** All model definitions, if available (reserved for future cross-model checks). */\n models?: ModelDefinition[]\n}\n\n/**\n * Validate a single content entry against its model's field schema.\n *\n * Merges the rule sets from MCP's legacy `validator.ts` (secret detection,\n * schema validation, unique constraints) with Studio's `content-validation.ts`\n * (email/url heuristics, polymorphic relation structure, nested object and\n * array-of-object recursion). The union is the authoritative per-entry\n * validator — both MCP's project validator and Studio's save path should\n * converge on this function over time.\n *\n * Asynchronous relation-integrity checks (does the referenced entry exist?)\n * live in `relation-integrity.ts` because they require I/O.\n */\nexport function validateContent(\n data: Record<string, unknown>,\n fields: Record<string, FieldDef>,\n modelId: string,\n locale: string,\n entryId?: string,\n ctx?: ValidationContext,\n): ValidationResult {\n const errors: ValidationError[] = []\n\n for (const [fieldId, def] of Object.entries(fields)) {\n const value = data[fieldId]\n errors.push(...validateField(value, def, modelId, locale, entryId, fieldId, ctx))\n }\n\n return {\n valid: errors.filter(e => e.severity === 'error').length === 0,\n errors,\n }\n}\n\n/** Bounds `items`-inside-`items` nesting; far above any real schema. */\nconst MAX_FIELD_DEPTH = 10\n\nfunction validateField(\n value: unknown,\n def: FieldDef,\n modelId: string,\n locale: string,\n entryId: string | undefined,\n fieldId: string,\n ctx?: ValidationContext,\n depth = 0,\n): ValidationError[] {\n const errors: ValidationError[] = []\n const errCtx = { model: modelId, locale, entry: entryId, field: fieldId }\n\n if (depth > MAX_FIELD_DEPTH) {\n return [{ severity: 'error', ...errCtx, message: `${fieldId} exceeds the maximum nesting depth of ${MAX_FIELD_DEPTH}` }]\n }\n\n if (value !== null && value !== undefined && value !== '') {\n const secretErrors = detectSecrets(value)\n for (const e of secretErrors) {\n errors.push({ ...e, ...errCtx })\n }\n }\n\n const isRelationType = def.type === 'relation' || def.type === 'relations'\n if (!isRelationType) {\n const fieldErrors = validateFieldValue(value, def)\n if (fieldErrors.length > 0) {\n for (const e of fieldErrors) errors.push({ ...e, ...errCtx })\n if (fieldErrors.some(e => e.severity === 'error')) return errors\n }\n } else if (def.required && (value === null || value === undefined || value === '')) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} is required` })\n return errors\n }\n\n if (value === null || value === undefined) return errors\n\n if (def.unique && ctx?.allEntries) {\n // String() comparison — matches legacy validator.ts uniqueness semantics\n // (catches cross-type duplicates like 42 vs '42' that strict === would miss).\n const valueKey = String(value)\n for (const [otherId, otherEntry] of Object.entries(ctx.allEntries)) {\n if (otherId === ctx.currentEntryId) continue\n const otherValue = otherEntry[fieldId]\n if (otherValue !== null && otherValue !== undefined && String(otherValue) === valueKey) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} must be unique — \"${String(value)}\" already exists in entry ${otherId}`,\n })\n break\n }\n }\n }\n\n if (def.type === 'relation' && def.model) {\n const targets = Array.isArray(def.model) ? def.model : [def.model]\n if (targets.length > 1) {\n if (typeof value !== 'object' || value === null || !('model' in value) || !('ref' in value)) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} must be { model, ref } for polymorphic relation`,\n })\n } else {\n const polyVal = value as { model: string, ref: string }\n if (!targets.includes(polyVal.model)) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} target model \"${polyVal.model}\" must be one of: ${targets.join(', ')}`,\n })\n }\n }\n } else if (typeof value !== 'string') {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must be a string (entry ID or slug)` })\n }\n }\n\n if (def.type === 'relations') {\n if (!Array.isArray(value)) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must be an array` })\n } else {\n if (def.min !== undefined && value.length < def.min) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must have at least ${def.min} items` })\n }\n if (def.max !== undefined && value.length > def.max) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must have at most ${def.max} items` })\n }\n // Polymorphic multi-relations (model targets multiple kinds) store\n // { model, ref } items — mirror the single-relation rule so content that\n // conforms to the generated type isn't rejected. Single-target relations\n // store plain id/slug strings.\n const targets = Array.isArray(def.model) ? def.model : (def.model ? [def.model] : [])\n const polymorphic = targets.length > 1\n for (let i = 0; i < value.length; i++) {\n const item = value[i]\n const itemCtx = { ...errCtx, field: `${fieldId}[${i}]` }\n if (polymorphic) {\n if (typeof item !== 'object' || item === null || !('model' in item) || !('ref' in item)) {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] must be { model, ref } for polymorphic relations` })\n } else {\n const polyItem = item as { model: string, ref: string }\n if (!targets.includes(polyItem.model)) {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] target model \"${polyItem.model}\" must be one of: ${targets.join(', ')}` })\n }\n }\n } else if (typeof item !== 'string') {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] must be a string (entry ID or slug)` })\n }\n }\n }\n }\n\n // Array items run through `validateField` itself rather than a parallel type\n // switch. The old switch knew 10 of the 27 field types and checked only\n // `typeof`, so `min`/`max`/`pattern`/`options` never reached an item and an\n // `items` given as a FieldDef with a non-object type matched no branch at all —\n // silently unvalidated, while the type emitter rendered it as a real constraint.\n // Recursing keeps one rule set for a type wherever it appears.\n if (def.type === 'array' && Array.isArray(value) && def.items) {\n const itemDef: FieldDef = typeof def.items === 'string'\n ? { type: def.items as FieldDef['type'] }\n : def.items\n for (let i = 0; i < value.length; i++) {\n // `ctx` is deliberately dropped: `unique` compares an entry against its\n // siblings, which has no meaning for positions inside one array.\n errors.push(...validateField(\n value[i], itemDef, modelId, locale, entryId, `${fieldId}[${i}]`, undefined, depth + 1,\n ))\n }\n }\n\n if (def.type === 'object' && def.fields && typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const nested = validateContent(value as Record<string, unknown>, def.fields, modelId, locale, entryId, ctx)\n // Qualify the nested path with the parent so an error names where it lives —\n // `meta.title`, or `blocks[0].title` when this object is an array item.\n for (const e of nested.errors) {\n errors.push({ ...e, field: e.field ? `${fieldId}.${e.field}` : fieldId })\n }\n }\n\n return errors\n}\n","import type { FieldDef, ValidationError } from '@contentrain/types'\n\n/**\n * Resolution of a target model for a relation integrity check.\n *\n * - `{ exists: true, content: {...} }` — target model is known and its\n * content for the requested locale was loaded. The ref must appear\n * as a key in `content`.\n * - `{ exists: true, content: null }` — target model exists (e.g. a\n * singleton or dictionary) but per-entry key enforcement does not\n * apply. The checker skips the ref existence test for this target.\n * - `{ exists: false }` — the referenced model is not defined at all.\n * The checker emits a \"target model not found\" error.\n */\nexport interface ResolvedTarget {\n exists: boolean\n content?: Record<string, unknown> | null\n}\n\n/** Back-compat loader signature used by Studio. A null return means \"nothing to check\". */\nexport type LegacyLoadContent = (\n targetModelId: string,\n targetLocale: string,\n) => Promise<Record<string, unknown> | null>\n\nexport interface CheckRelationIntegrityOptions {\n /** Severity for broken-relation errors. Default: `warning` (Studio-compat). */\n severity?: 'error' | 'warning'\n /**\n * Richer target resolver — when provided, it can signal target-model\n * absence. MCP's project validator passes one so `contentrain_validate`\n * still emits \"target model not found\" errors.\n */\n resolveTarget?: (\n targetModelId: string,\n targetLocale: string,\n ) => Promise<ResolvedTarget>\n}\n\n/**\n * Verify that relation and relations fields reference targets that actually\n * exist. Two severities are supported — Studio's per-save flow emits\n * `warning` (the referenced entry may still be drafted), while MCP's\n * project-wide validator emits `error` and additionally flags missing\n * target models via `resolveTarget`.\n *\n * The loader abstraction keeps this function I/O-agnostic: MCP wires it\n * to filesystem reads through LocalReader, Studio wires it to a\n * `GitProvider.readFile`-backed loader, mocks pass in-memory maps.\n */\nexport async function checkRelationIntegrity(\n data: Record<string, unknown>,\n fields: Record<string, FieldDef>,\n modelId: string,\n locale: string,\n entryId: string | undefined,\n loadContent: LegacyLoadContent,\n opts: CheckRelationIntegrityOptions = {},\n): Promise<ValidationError[]> {\n const errors: ValidationError[] = []\n const severity = opts.severity ?? 'warning'\n\n const resolve = opts.resolveTarget\n ?? (async (id: string, loc: string): Promise<ResolvedTarget> => {\n const content = await loadContent(id, loc)\n return { exists: true, content }\n })\n\n for (const [fieldId, def] of Object.entries(fields)) {\n const value = data[fieldId]\n if (value === null || value === undefined) continue\n\n if (def.type === 'relation' && def.model) {\n const targets = Array.isArray(def.model) ? def.model : [def.model]\n\n if (targets.length > 1 && typeof value === 'object' && value !== null) {\n const polyVal = value as { model: string, ref: string }\n if (polyVal.model && polyVal.ref) {\n const resolved = await resolve(polyVal.model, locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${polyVal.model}\" not found`,\n })\n } else if (resolved.content && !(polyVal.ref in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${polyVal.ref}\" not found in ${polyVal.model}`,\n })\n }\n }\n } else if (typeof value === 'string' && targets[0]) {\n const resolved = await resolve(targets[0], locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${targets[0]}\" not found`,\n })\n } else if (resolved.content && !(value in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${value}\" not found in ${targets[0]}`,\n })\n }\n }\n }\n\n if (def.type === 'relations' && def.model && Array.isArray(value)) {\n const target = Array.isArray(def.model) ? def.model[0] : def.model\n if (target) {\n const resolved = await resolve(target, locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${target}\" not found`,\n })\n } else if (resolved.content) {\n for (const ref of value) {\n if (typeof ref === 'string' && !(ref in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${ref}\" not found in ${target}`,\n })\n }\n }\n }\n }\n }\n }\n\n return errors\n}\n","import type { EntryMeta, ValidationError } from '@contentrain/types'\n\n/**\n * Validate `publish_at` and `expire_at` meta fields.\n *\n * Rules (matches legacy `validator.ts:validateScheduleFields`):\n * - `publish_at` must parse as a valid Date\n * - `expire_at` must parse as a valid Date\n * - When both are present, `expire_at` must be strictly after `publish_at`\n */\nexport function validateScheduleFields(\n meta: EntryMeta,\n ctx: { model: string, locale: string, entry?: string, slug?: string },\n issues: ValidationError[],\n): void {\n if (meta.publish_at !== undefined) {\n const d = new Date(meta.publish_at)\n if (Number.isNaN(d.getTime())) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `Invalid publish_at date: \"${meta.publish_at}\". Must be a valid ISO 8601 date string.`,\n })\n }\n }\n if (meta.expire_at !== undefined) {\n const d = new Date(meta.expire_at)\n if (Number.isNaN(d.getTime())) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `Invalid expire_at date: \"${meta.expire_at}\". Must be a valid ISO 8601 date string.`,\n })\n }\n }\n if (meta.publish_at !== undefined && meta.expire_at !== undefined) {\n const pubDate = new Date(meta.publish_at)\n const expDate = new Date(meta.expire_at)\n if (!Number.isNaN(pubDate.getTime()) && !Number.isNaN(expDate.getTime()) && expDate <= pubDate) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `expire_at (\"${meta.expire_at}\") must be after publish_at (\"${meta.publish_at}\").`,\n })\n }\n }\n}\n","import type { ValidationError, ModelDefinition, ContentrainConfig, EntryMeta, ModelLocaleScope } from '@contentrain/types'\nimport { describeModelLocaleScope, detectSecrets, MODEL_FIELD_ORDER, resolveModelLocales, validateModelLocales } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { writeJson, writeText } from '../../util/fs.js'\nimport { readConfig } from '../config.js'\nimport { inferTitleField, legacyFieldNames, listModels, readModel, titleFieldIssues, titleFieldOptions } from '../model-manager.js'\nimport { mergeEntryMeta, writeMeta } from '../meta-manager.js'\nimport { parseFrontmatter, resolveLocaleStrategy, serializeFrontmatter } from '../content-manager.js'\nimport type { RepoReader } from '../contracts/index.js'\nimport { LocalReader } from '../../providers/local/reader.js'\nimport { contentDirPath, contentFilePath, documentFilePath, metaFilePath } from '../ops/paths.js'\nimport { validateContent } from './entry.js'\nimport { checkRelationIntegrity, type ResolvedTarget } from './relation-integrity.js'\nimport { validateScheduleFields } from './schedule.js'\n\n// ─── Types ───\n\nexport interface ValidateOptions {\n model?: string\n fix?: boolean\n}\n\nexport interface ValidateResult {\n valid: boolean\n summary: {\n errors: number\n warnings: number\n notices: number\n models_checked: number\n entries_checked: number\n }\n issues: ValidationError[]\n fixed: number\n}\n\n// ─── Reader-backed IO helpers ───\n\nasync function readJsonViaReader<T>(reader: RepoReader, path: string): Promise<T | null> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return null\n }\n}\n\nasync function readTextViaReader(reader: RepoReader, path: string): Promise<string | null> {\n try {\n return await reader.readFile(path)\n } catch {\n return null\n }\n}\n\n// ─── Shared field validation helpers ───\n\n/**\n * Build a `resolveTarget` adapter for `checkRelationIntegrity` that walks\n * the project's content store via the shared {@link RepoReader}. Mirrors\n * the target-resolution shape the legacy `checkRelation` used — collection\n * targets return their entry object-map, documents return a \"slug exists\"\n * marker map, singletons and dictionaries return null content so the\n * checker skips key enforcement for them.\n */\nfunction buildProjectTargetResolver(\n reader: RepoReader,\n config: ContentrainConfig,\n): (targetModelId: string, targetLocale: string) => Promise<ResolvedTarget> {\n return async (targetModelId, targetLocale) => {\n const targetModel = await readModel(reader, targetModelId)\n if (!targetModel) return { exists: false }\n\n if (targetModel.kind === 'document') {\n const docContentDir = contentDirPath(targetModel)\n const slugs = await discoverDocumentSlugs(reader, docContentDir, targetModel)\n return { exists: true, content: Object.fromEntries(slugs.map(s => [s, true])) }\n }\n if (targetModel.kind === 'singleton' || targetModel.kind === 'dictionary') {\n return { exists: true, content: null }\n }\n // Collection: resolve against the target model's own storage. i18n:false\n // targets are locale-agnostic (data.json). For i18n:true targets, fall back\n // to the default-locale file when the source locale lacks the entry — a\n // relation legitimately points at an entry that may be translated under\n // another locale, so the id set is merged to avoid phantom broken refs. #Y5\n const merged: Record<string, unknown> = {}\n const primary = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, targetLocale),\n )\n if (primary) Object.assign(merged, primary)\n if (targetModel.i18n && targetLocale !== config.locales.default) {\n const fallback = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, config.locales.default),\n )\n if (fallback) Object.assign(merged, fallback)\n }\n return { exists: true, content: merged }\n }\n}\n\n/**\n * Scan an entry's data fields for detected secrets in UNDECLARED keys —\n * the legacy validator also flagged stray/rogue fields that were not in\n * `model.fields`. `validateContent` only knows about declared fields, so\n * this complementary pass preserves that coverage.\n */\nfunction scanUndeclaredFieldsForSecrets(\n data: Record<string, unknown>,\n declared: Record<string, unknown> | undefined,\n ctx: { model: string, locale: string, entry?: string, slug?: string },\n issues: ValidationError[],\n): void {\n for (const [fieldName, value] of Object.entries(data)) {\n if (declared && fieldName in declared) continue\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n ...ctx,\n field: fieldName,\n message: `Potential secret detected in field \"${fieldName}\"`,\n })\n }\n }\n}\n\n// ─── Locale coverage ───\n\n/**\n * The clause every locale-coverage message ends with: which list the check was\n * evaluated against.\n *\n * Without it \"missing tr\" is unreadable. A model that legitimately covers two\n * of four project locales and one that has genuinely lost a translation produce\n * the same sentence, and the reader cannot tell which they are looking at\n * without opening the model definition and config.json side by side. Naming the\n * list turns the message into its own explanation — and, when the list is the\n * project-wide one, into the hint that `locales` is what narrows it.\n */\nfunction againstScope(scope: ModelLocaleScope): string {\n return ` (checked against ${describeModelLocaleScope(scope)})`\n}\n\n// ─── Per-model validators ───\n\nasync function validateCollectionModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n // An i18n model that declares `locales` covers that subset and no more — a\n // model that was never translated into a locale says so once, instead of\n // failing on that locale's missing file.\n const scope = resolveModelLocales(model, config)\n const locales = scope.locales\n\n // Collect all entry IDs per locale for parity check\n const localeEntryIds: Record<string, Set<string>> = {}\n const allEntryIds = new Set<string>()\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n\n for (const locale of locales) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, Record<string, unknown>>>(reader, filePath)\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json${againstScope(scope)}`,\n })\n\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n const ids = new Set(Object.keys(data))\n localeEntryIds[locale] = ids\n for (const id of ids) allEntryIds.add(id)\n\n // Check canonical sort\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, Record<string, unknown>> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate each entry — validateContent covers secret/field/unique/email-url/\n // polymorphic/nested on declared fields; supplementary scans below catch\n // undeclared-field secrets and async relation integrity.\n for (const [entryId, fields] of Object.entries(data)) {\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(\n fields,\n model.fields,\n { model: model.id, locale, entry: entryId },\n issues,\n )\n\n if (!model.fields) continue\n\n const entryResult = validateContent(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n { allEntries: data, currentEntryId: entryId },\n )\n issues.push(...entryResult.errors)\n\n const relationErrors = await checkRelationIntegrity(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\n }\n }\n\n // Entry parity check (i18n). An entry one locale has and another lacks is a\n // translation that was never written — on a WPML or Polylang import the normal\n // state of a site, not a broken store — so it is a warning, as a document's\n // missing translation is. A locale with no file at all stays an error above.\n if (model.i18n && Object.keys(localeEntryIds).length > 1) {\n const localeKeys = Object.keys(localeEntryIds)\n for (let i = 1; i < localeKeys.length; i++) {\n const locA = localeKeys[0]!\n const locB = localeKeys[i]!\n const idsA = localeEntryIds[locA]!\n const idsB = localeEntryIds[locB]!\n\n for (const id of idsA) {\n if (!idsB.has(id)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locB,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locA} but missing in ${locB}${againstScope(scope)}`,\n })\n }\n }\n for (const id of idsB) {\n if (!idsA.has(id)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locA,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locB} but missing in ${locA}${againstScope(scope)}`,\n })\n }\n }\n }\n }\n\n // Consolidate a non-i18n model's meta layout BEFORE the orphan checks below.\n // The reverse orphan-content check fabricates a draft default-locale meta for\n // any content entry lacking meta at the default locale. If a stray holds the\n // real (e.g. published) record under a non-default locale, running that check\n // first would mint a draft default and let this pass then delete the real\n // record. Consolidating first preserves the authoritative record.\n const strayResult = await checkStrayNonI18nMeta(reader, projectRoot, model, config, issues, fix)\n fixed += strayResult.fixed\n\n // Orphan meta check\n for (const locale of locales) {\n const metaRelPath = metaFilePath(model, locale, config.locales.default)\n const metaData = await readJsonViaReader<Record<string, EntryMeta>>(reader, metaRelPath)\n const contentData = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(model, locale),\n ) ?? {}\n\n // Forward check: meta entries without content + schedule validation\n if (metaData) {\n for (const metaEntryId of Object.keys(metaData)) {\n // Validate schedule fields on every meta entry\n validateScheduleFields(metaData[metaEntryId]!, { model: model.id, locale, entry: metaEntryId }, issues)\n\n if (!(metaEntryId in contentData)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n entry: metaEntryId,\n message: `Orphan meta: meta entry \"${metaEntryId}\" exists but content entry missing`,\n })\n if (fix && projectRoot) {\n delete metaData[metaEntryId]\n const metaAbs = join(projectRoot, metaRelPath)\n if (Object.keys(metaData).length > 0) {\n await writeJson(metaAbs, metaData)\n } else {\n await rm(metaAbs, { force: true })\n }\n fixed++\n }\n }\n }\n }\n\n // Reverse check: content entries without meta\n for (const entryId of Object.keys(contentData)) {\n if (!metaData || !(entryId in metaData)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n entry: entryId,\n message: `Orphan content: entry \"${entryId}\" has no metadata`,\n })\n // Skip fabrication while a non-i18n model's meta is stuck in an\n // unresolved stray: the content already has meta (misplaced), so minting\n // a draft default here would let the stray-consolidation pass then delete\n // the real record on a subsequent run. The mismatch warning already asks\n // the agent to resolve the layout by hand.\n if (fix && projectRoot && !strayResult.unresolved) {\n await writeMeta(projectRoot, model, { locale, entryId, defaultLocale: config.locales.default }, {\n // `source: 'import'` on purpose: this record is being fabricated\n // for content that arrived without one, not written by an agent.\n ...mergeEntryMeta(undefined),\n source: 'import',\n })\n fixed++\n }\n }\n }\n\n // Publish-state drift. Drafts sitting alongside published entries in the\n // same collection is the fingerprint of writes that reset status — the\n // shape that silently emptied collections on the CDN. Advisory only: a\n // genuine work-in-progress entry is indistinguishable, so this is a call\n // for the agent to make, not an error. Never auto-fixed — publishing is a\n // content decision, and MCP does not make those.\n if (metaData) {\n const entries = Object.entries(metaData)\n const draftIds = entries.filter(([, m]) => m.status === 'draft').map(([id]) => id)\n const publishedCount = entries.filter(([, m]) => m.status === 'published').length\n if (publishedCount > 0 && draftIds.length > 0) {\n issues.push({\n severity: 'notice',\n model: model.id,\n locale,\n message:\n `Publish-state drift: ${draftIds.length} draft entr${draftIds.length === 1 ? 'y' : 'ies'} `\n + `alongside ${publishedCount} published in the same collection — [${draftIds.join(', ')}]. `\n + 'If these were published before, restore them with contentrain_bulk update_status.',\n })\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\n/**\n * Flag — and, with `fix`, backfill — a model definition's `title_field`.\n *\n * This is the migration path for models authored before the property existed.\n * Reads are an unvalidated cast, so such a model loads happily with `title_field`\n * undefined in a slot the type system calls a string; nothing would ever say so.\n *\n * Only the *missing* case is repaired. A `title_field` that names an absent field\n * or an undisplayable type is an authoring mistake, not a legacy gap — filling in\n * a different answer there would be overruling a decision someone made, which is\n * the line `project.ts` draws everywhere else.\n *\n * Every repair emits a notice naming the field and the rule that chose it. The\n * doctrine that governs the rest of this file is that MCP does not make content\n * decisions; a schema backfill is not a content decision, but the principle\n * underneath — never exercise judgement invisibly — still binds. The notice and\n * the diff on `cr/fix/validate` are the two ways a human can overrule it.\n */\nasync function checkModelDefinition(\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<number> {\n // `locales` is reported, never repaired — not even under `fix`. A model that\n // does not declare one covers every project locale, and choosing a narrower\n // set for it would be deciding which translations are *supposed* to exist,\n // which is a content decision. The one MCP could compute — \"the locales that\n // happen to have files today\" — would write the current gaps into the schema\n // and silence the very errors that reveal them.\n for (const issue of validateModelLocales(model, config)) {\n issues.push({ ...issue, model: model.id })\n }\n\n // Field names that predate the snake_case rule. `contentrain_model_save`\n // keeps them (#116) — this is how an agent learns a model carries one before\n // it edits the model, not mid-write. A notice, not a warning: renaming means\n // renaming content keys in every locale and every consumer, so it is a\n // migration to plan, not a defect to clear.\n for (const path of legacyFieldNames(model.fields)) {\n issues.push({\n severity: 'notice',\n model: model.id,\n field: path,\n message: `Field \"${path}\" predates the snake_case rule. contentrain_model_save keeps it as-is; new fields must be snake_case. Renaming it also means renaming its content keys in every locale.`,\n })\n }\n\n const { errors, warnings } = titleFieldIssues(model)\n if (errors.length === 0 && warnings.length === 0) return 0\n\n const repairable = !model.title_field\n if (fix && projectRoot && repairable) {\n const pick = inferTitleField(model.kind, model.fields)\n if (pick) {\n model.title_field = pick.field\n // writeJson, not writeModel: writeModel also ensures the content and meta\n // directories, which for a model with `content_path` means creating\n // directories in the user's source tree. A validator does not do that.\n await writeJson(join(projectRoot, `.contentrain/models/${model.id}.json`), model, MODEL_FIELD_ORDER)\n issues.push({\n severity: 'notice',\n model: model.id,\n field: 'title_field',\n message: `title_field set to \"${pick.field}\" (rule: ${pick.rule}). If that is the wrong field, correct it with contentrain_model_save.`,\n })\n return 1\n }\n // Inference declined. Two different situations, said differently: a model\n // whose only displayable fields are slug/url/code has legal choices that a\n // human must make (#120); a model with none needs a field added.\n const legal = titleFieldOptions(model.kind, model.fields)\n issues.push({\n severity: 'error',\n model: model.id,\n field: 'title_field',\n message: legal.length > 0\n ? `Missing \"title_field\" and no field reads as a title — only ${legal.map(f => `\"${f}\"`).join(', ')} could legally hold one, and a slug, URL or code field, or a field inside an object, is not inferred. Set title_field explicitly with contentrain_model_save, or add a string field.`\n : `Missing \"title_field\" and no field can serve as one — add a string or text field, then re-run. A title cannot be an image, relation or number.`,\n })\n return 0\n }\n\n for (const message of errors) {\n issues.push({ severity: 'error', model: model.id, field: 'title_field', message })\n }\n for (const message of warnings) {\n issues.push({ severity: 'warning', model: model.id, field: 'title_field', message })\n }\n return 0\n}\n\n/**\n * Flag — and, with `fix`, remediate — meta files a non-i18n model should not have.\n *\n * Such a model keeps all content in one `data.json` and therefore exactly one\n * meta record, at the default locale. Earlier writes derived the meta path from\n * the caller's locale, so saving under a non-default locale left a second meta\n * file, and readers disagreed about which was authoritative.\n *\n * The `fix` remediation is deterministic and never decides a status:\n * - default-locale meta present → the strays are redundant, so delete them\n * (the default-locale record stays authoritative — no status is merged).\n * - default-locale meta absent, exactly one stray → that stray holds the only\n * record, so migrate it to the default path (move) rather than orphan the\n * content.\n * - default-locale meta absent, several strays → which is authoritative is\n * ambiguous, so leave the warning for the agent to resolve by hand.\n *\n * Returns the count of files remediated and whether strays remain unresolved.\n * `unresolved` gates the caller's orphan-content fabrication: while a non-i18n\n * model's meta still lives in a stray, the content is not truly orphaned, so\n * minting a draft default-locale record would both be wrong and set up a trap\n * (a later fix pass would then treat the real stray as redundant and delete it).\n */\nasync function checkStrayNonI18nMeta(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ fixed: number, unresolved: boolean }> {\n if (model.i18n) return { fixed: 0, unresolved: false }\n\n const metaDir = `.contentrain/meta/${model.id}`\n const expected = `${config.locales.default}.json`\n let files: string[]\n try {\n files = await reader.listDirectory(metaDir)\n } catch {\n return { fixed: 0, unresolved: false }\n }\n\n const strays = files.filter(f => f.endsWith('.json') && f !== expected)\n if (strays.length === 0) return { fixed: 0, unresolved: false }\n\n issues.push({\n severity: 'warning',\n model: model.id,\n message:\n `Meta layout mismatch: \"${model.id}\" has i18n disabled, so its content lives in a single data.json `\n + `and its meta belongs at ${expected} alone — but [${strays.join(', ')}] also exist. `\n + 'Readers may disagree about which file is authoritative. Run contentrain_validate fix:true to prune '\n + `the extras (the default-locale meta stays authoritative).`,\n })\n\n if (!fix || !projectRoot) return { fixed: 0, unresolved: true }\n\n const defaultMeta = await readJsonViaReader<Record<string, unknown>>(reader, `${metaDir}/${expected}`)\n\n if (defaultMeta === null) {\n // No authoritative default. A single stray holds the only record — keep it\n // by moving it to the default path. Several strays with no default is\n // ambiguous, so do not guess: leave the warning standing (unresolved).\n if (strays.length !== 1) return { fixed: 0, unresolved: true }\n const stray = strays[0]!\n const content = await readJsonViaReader<Record<string, unknown>>(reader, `${metaDir}/${stray}`)\n if (content === null) return { fixed: 0, unresolved: true }\n await writeJson(join(projectRoot, metaDir, expected), content)\n await rm(join(projectRoot, metaDir, stray), { force: true })\n return { fixed: 1, unresolved: false }\n }\n\n // Default-locale meta is authoritative → the strays are redundant. Each is a\n // distinct file, so removing them in parallel is safe.\n await Promise.all(strays.map(stray => rm(join(projectRoot, metaDir, stray), { force: true })))\n return { fixed: strays.length, unresolved: false }\n}\n\nasync function validateSingletonModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n const scope = resolveModelLocales(model, config)\n\n for (const locale of scope.locales) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, unknown>>(reader, filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json${againstScope(scope)}`,\n })\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(data, model.fields, { model: model.id, locale }, issues)\n\n if (model.fields) {\n const entryResult = validateContent(data, model.fields, model.id, locale)\n issues.push(...entryResult.errors)\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n const relationErrors = await checkRelationIntegrity(\n data,\n model.fields,\n model.id,\n locale,\n undefined,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, unknown> = {}\n for (const key of sorted) {\n resorted[key] = data[key]\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate schedule fields in singleton meta\n const singletonMetaData = await readJsonViaReader<EntryMeta>(reader, metaFilePath(model, locale, config.locales.default))\n if (singletonMetaData) {\n validateScheduleFields(singletonMetaData, { model: model.id, locale }, issues)\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function validateDictionaryModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n\n const localeKeys: Record<string, Set<string>> = {}\n const scope = resolveModelLocales(model, config)\n\n for (const locale of scope.locales) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, string>>(reader, filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json${againstScope(scope)}`,\n })\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n localeKeys[locale] = new Set(Object.keys(data))\n\n // Secret detection\n for (const [key, value] of Object.entries(data)) {\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: key,\n message: `Potential secret detected in key \"${key}\"`,\n })\n }\n }\n\n // Duplicate value detection\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n for (const [value, dupeKeys] of valueToKeys) {\n if (dupeKeys.length > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: `Duplicate value \"${truncated}\" mapped to ${dupeKeys.length} keys: [${dupeKeys.join(', ')}]`,\n })\n }\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, string> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n }\n\n // Key parity check\n if (model.i18n && Object.keys(localeKeys).length > 1) {\n const localeNames = Object.keys(localeKeys)\n for (let i = 1; i < localeNames.length; i++) {\n const locA = localeNames[0]!\n const locB = localeNames[i]!\n const keysA = localeKeys[locA]!\n const keysB = localeKeys[locB]!\n\n for (const k of keysA) {\n if (!keysB.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locB,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locA} but missing in ${locB}${againstScope(scope)}`,\n })\n }\n }\n for (const k of keysB) {\n if (!keysA.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locA,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locB} but missing in ${locA}${againstScope(scope)}`,\n })\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function discoverDocumentSlugs(\n reader: RepoReader,\n cDir: string,\n model: ModelDefinition,\n): Promise<string[]> {\n const strategy = resolveLocaleStrategy(model)\n const entries = await reader.listDirectory(cDir)\n\n // When i18n is disabled, documents are flat {slug}.md files\n if (!model.i18n) {\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n }\n\n if (strategy === 'file') {\n // Each slug is a subdirectory\n return entries.filter(e => !e.startsWith('.'))\n }\n\n if (strategy === 'suffix') {\n // Files like {slug}.{locale}.md — extract unique slugs\n const slugs = new Set<string>()\n for (const f of entries) {\n if (!f.endsWith('.md')) continue\n // Remove last two dot-separated segments (.locale.md)\n const parts = f.replace(/\\.md$/, '').split('.')\n if (parts.length >= 2) {\n parts.pop() // remove locale\n slugs.add(parts.join('.'))\n }\n }\n return [...slugs]\n }\n\n if (strategy === 'directory') {\n // Locale subdirectories contain {slug}.md files — collect slugs from all locale dirs\n const slugs = new Set<string>()\n const localeLists = await Promise.all(\n entries\n .filter(localeDir => !localeDir.startsWith('.'))\n .map(localeDir => reader.listDirectory(`${cDir}/${localeDir}`)),\n )\n for (const files of localeLists) {\n for (const f of files) {\n if (f.endsWith('.md')) slugs.add(f.replace(/\\.md$/, ''))\n }\n }\n return [...slugs]\n }\n\n // 'none' — {slug}.md files, no locale\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n}\n\n/** Meta-owned scheduling keys; see `EntrySchedule` in meta-manager. */\nconst SCHEDULE_KEYS = ['publish_at', 'expire_at'] as const\n\n/**\n * Scheduling keys a document's frontmatter carries that belong to its meta.\n *\n * Before #125, `contentrain_content_save` merged `publish_at`/`expire_at` into\n * the entry's data before planning the write, so the same value landed in the\n * frontmatter and in meta. A key counts as leaked only when three things hold:\n * it is in the frontmatter, the model does not declare a field by that name,\n * and the meta record holds the key too. The third condition is what separates\n * the tool's leftover from an author's own frontmatter field.\n */\nasync function leakedScheduleKeys(\n reader: RepoReader,\n model: ModelDefinition,\n locale: string,\n slug: string,\n frontmatter: Record<string, unknown>,\n defaultLocale: string,\n): Promise<string[]> {\n const present = SCHEDULE_KEYS.filter(k => k in frontmatter && !(model.fields && k in model.fields))\n if (present.length === 0) return []\n const meta = await readJsonViaReader<EntryMeta>(reader, metaFilePath(model, locale, defaultLocale, slug))\n if (!meta) return []\n return present.filter(k => meta[k] !== undefined)\n}\n\nasync function validateDocumentModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n const cDir = contentDirPath(model)\n\n if (!await reader.fileExists(cDir)) return { entries: 0, fixed: 0 }\n\n const slugs = await discoverDocumentSlugs(reader, cDir, model)\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n // A declared `locales` narrows the i18n case — see validateCollectionModel.\n const scope = resolveModelLocales(model, config)\n const locales = scope.locales\n\n // Read every document once up front and keep its frontmatter per locale.\n // `unique` compares an entry against its siblings, so it needs the whole set —\n // validating one file at a time is why `unique` was silently a no-op for\n // documents, which is exactly where every shipped template declares it.\n // Keyed by `${slug}\\u0000${locale}`. The NUL separator can appear in neither\n // a slug nor a locale, so composite keys never collide. Written as the escape\n // `\\u0000` (not a raw NUL byte) so the source stays plain text — a literal NUL\n // makes tools classify this file as binary and breaks grep/diff.\n const rawByKey = new Map<string, string>()\n const frontmatterByLocale: Record<string, Record<string, Record<string, unknown>>> = {}\n for (const slug of slugs) {\n if (slug.startsWith('.')) continue\n for (const locale of locales) {\n const raw = await readTextViaReader(reader, documentFilePath(model, locale, slug))\n if (!raw) continue\n rawByKey.set(`${slug}\\u0000${locale}`, raw)\n const { frontmatter } = parseFrontmatter(raw)\n frontmatterByLocale[locale] ??= {}\n frontmatterByLocale[locale][slug] = frontmatter\n }\n }\n\n for (const slug of slugs) {\n if (slug.startsWith('.')) continue\n\n for (const locale of locales) {\n const filePath = documentFilePath(model, locale, slug)\n const raw = rawByKey.get(`${slug}\\u0000${locale}`) ?? null\n\n if (!raw) {\n if (model.i18n) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n slug,\n message: `Missing translation: document \"${slug}\" missing ${locale} locale file${againstScope(scope)}`,\n })\n if (fix && projectRoot) {\n // Create empty template\n const template = `---\\nslug: ${slug}\\n---\\n`\n await writeText(join(projectRoot, filePath), template)\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n const { frontmatter, body } = 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 // Scheduling keys an older content_save copied into the frontmatter as\n // well as into meta (#125). They are meta's, not the author's — and since\n // documents merge frontmatter on save, no later save could remove them.\n // Recognised only when meta holds the same key: an undeclared `publish_at`\n // with no meta counterpart is the author's own field and is left alone.\n const leaked = await leakedScheduleKeys(reader, model, locale, slug, frontmatter, config.locales.default)\n if (leaked.length > 0) {\n const keys = leaked.map(k => `\"${k}\"`).join(', ')\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n slug,\n field: leaked[0],\n message: `Frontmatter carries scheduling key(s) ${keys} that belong in meta — copied there by an older content_save. The meta record already holds the value; run validate with fix:true to strip the frontmatter copy.`,\n })\n if (fix && projectRoot) {\n const cleaned = Object.fromEntries(Object.entries(frontmatter).filter(([k]) => !leaked.includes(k)))\n await writeText(join(projectRoot, filePath), serializeFrontmatter(cleaned, body))\n fixed++\n }\n }\n\n if (model.fields) {\n const fieldsWithoutBody = Object.fromEntries(\n Object.entries(model.fields).filter(([name]) => name !== 'body'),\n )\n const entryResult = validateContent(\n frontmatter,\n fieldsWithoutBody,\n model.id,\n locale,\n undefined,\n // Siblings in the same locale, so `unique` has something to compare\n // against. Keyed by slug — a document's identity is its slug.\n { allEntries: frontmatterByLocale[locale] ?? {}, currentEntryId: slug },\n )\n for (const err of entryResult.errors) {\n issues.push({ ...err, slug })\n }\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n const relationErrors = await checkRelationIntegrity(\n frontmatter,\n fieldsWithoutBody,\n model.id,\n locale,\n undefined,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n for (const err of relationErrors) {\n issues.push({ ...err, slug })\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\n// ─── Orphan content check ───\n\nasync function checkOrphanContent(\n reader: RepoReader,\n validModelIds: Set<string>,\n issues: ValidationError[],\n _fix: boolean,\n): Promise<number> {\n const fixed = 0\n const contentBase = '.contentrain/content'\n const domains = await reader.listDirectory(contentBase)\n\n const modelLists = await Promise.all(\n domains\n .filter(d => !d.startsWith('.'))\n .map(async d => ({ domain: d, dirs: await reader.listDirectory(`${contentBase}/${d}`) })),\n )\n\n for (const { dirs } of modelLists) {\n for (const modelDir of dirs) {\n if (modelDir.startsWith('.')) continue\n if (!validModelIds.has(modelDir)) {\n issues.push({\n severity: 'warning',\n model: modelDir,\n message: `Orphan content: content directory exists for deleted model \"${modelDir}\"`,\n })\n }\n }\n }\n\n return fixed\n}\n\n// ─── Main validation function ───\n\n/**\n * Validate a project's content against its model schemas.\n *\n * Two signatures:\n *\n * - `validateProject(projectRoot, options?)` — legacy disk-backed flow.\n * `options.fix: true` applies structural repairs (canonical sort,\n * orphan meta, missing locale files) directly on disk.\n * - `validateProject(reader, options?)` — reader-backed flow used by\n * HTTP/remote callers (e.g. GitHubProvider). `options.fix` is ignored\n * because remote flows cannot write to the local filesystem.\n *\n * Read-side behavior is identical across both signatures so Studio and\n * local CLIs see the same issue set for the same content state.\n */\nexport async function validateProject(projectRoot: string, options?: ValidateOptions): Promise<ValidateResult>\nexport async function validateProject(reader: RepoReader, options?: ValidateOptions): Promise<ValidateResult>\nexport async function validateProject(\n input: string | RepoReader,\n options?: ValidateOptions,\n): Promise<ValidateResult> {\n const reader: RepoReader = typeof input === 'string' ? new LocalReader(input) : input\n const projectRoot: string | undefined = typeof input === 'string' ? input : undefined\n const fix = Boolean(options?.fix) && projectRoot !== undefined\n\n const issues: ValidationError[] = []\n let totalEntries = 0\n let totalFixed = 0\n let modelsChecked = 0\n\n const config = await readConfig(reader)\n if (!config) {\n return {\n valid: false,\n summary: { errors: 1, warnings: 0, notices: 0, models_checked: 0, entries_checked: 0 },\n issues: [{ severity: 'error', message: 'Project not initialized: config.json missing' }],\n fixed: 0,\n }\n }\n\n const modelSummaries = await listModels(reader)\n const validModelIds = new Set(modelSummaries.map(m => m.id))\n const modelsToCheck = options?.model\n ? modelSummaries.filter(m => m.id === options.model)\n : modelSummaries\n\n for (const summary of modelsToCheck) {\n const model = await readModel(reader, summary.id)\n if (!model) continue\n\n modelsChecked++\n // Ahead of the content checks: a broken model definition explains the content\n // errors below it, and the repair mutates `model` in place so the content\n // validators downstream see the fixed object.\n totalFixed += await checkModelDefinition(projectRoot, model, config, issues, fix)\n\n let result: { entries: number; fixed: number }\n\n switch (model.kind) {\n case 'collection':\n result = await validateCollectionModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'singleton':\n result = await validateSingletonModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'dictionary':\n result = await validateDictionaryModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'document':\n result = await validateDocumentModel(reader, projectRoot, model, config, issues, fix)\n break\n default:\n result = { entries: 0, fixed: 0 }\n }\n\n totalEntries += result.entries\n totalFixed += result.fixed\n }\n\n // Orphan content check (only when checking all models)\n if (!options?.model) {\n totalFixed += await checkOrphanContent(reader, validModelIds, issues, fix)\n }\n\n // Cross-dictionary duplicate value detection (only when checking all models)\n if (!options?.model) {\n const dictModels = modelsToCheck.filter(m => m.kind === 'dictionary')\n if (dictModels.length > 1) {\n const globalValueMap: Record<string, Map<string, Array<{ model: string; key: string }>>> = {}\n\n for (const summary of dictModels) {\n const model = await readModel(reader, summary.id)\n if (!model) continue\n\n for (const locale of resolveModelLocales(model, config).locales) {\n if (!globalValueMap[locale]) globalValueMap[locale] = new Map()\n const data = await readJsonViaReader<Record<string, string>>(reader, contentFilePath(model, locale))\n if (!data) continue\n\n for (const [key, value] of Object.entries(data)) {\n const refs = globalValueMap[locale]!.get(value)\n if (refs) refs.push({ model: model.id, key })\n else globalValueMap[locale]!.set(value, [{ model: model.id, key }])\n }\n }\n }\n\n for (const [locale, valueMap] of Object.entries(globalValueMap)) {\n for (const [value, refs] of valueMap) {\n const uniqueModels = new Set(refs.map(r => r.model))\n if (uniqueModels.size > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'notice',\n locale,\n message: `Cross-model duplicate value \"${truncated}\" in ${refs.map(r => `${r.model}/${r.key}`).join(', ')}`,\n })\n }\n }\n }\n }\n }\n\n const errors = issues.filter(i => i.severity === 'error').length\n const warnings = issues.filter(i => i.severity === 'warning').length\n const notices = issues.filter(i => i.severity === 'notice').length\n\n return {\n valid: errors === 0,\n summary: {\n errors,\n warnings,\n notices,\n models_checked: modelsChecked,\n entries_checked: totalEntries,\n },\n issues,\n fixed: totalFixed,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;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;;;AAIH,MAAM,kBAAkB;AAExB,SAAS,cACP,OACA,KACA,SACA,QACA,SACA,SACA,KACA,QAAQ,GACW;CACnB,MAAM,SAA4B,EAAE;CACpC,MAAM,SAAS;EAAE,OAAO;EAAS;EAAQ,OAAO;EAAS,OAAO;EAAS;AAEzE,KAAI,QAAQ,gBACV,QAAO,CAAC;EAAE,UAAU;EAAS,GAAG;EAAQ,SAAS,GAAG,QAAQ,wCAAwC;EAAmB,CAAC;AAG1H,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,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;;;AAYvH,KAAI,IAAI,SAAS,WAAW,MAAM,QAAQ,MAAM,IAAI,IAAI,OAAO;EAC7D,MAAM,UAAoB,OAAO,IAAI,UAAU,WAC3C,EAAE,MAAM,IAAI,OAA2B,GACvC,IAAI;AACR,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAGhC,QAAO,KAAK,GAAG,cACb,MAAM,IAAI,SAAS,SAAS,QAAQ,SAAS,GAAG,QAAQ,GAAG,EAAE,IAAI,KAAA,GAAW,QAAQ,EACrF,CAAC;;AAIN,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,SAAS,IAAI;AAG3G,OAAK,MAAM,KAAK,OAAO,OACrB,QAAO,KAAK;GAAE,GAAG;GAAG,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,EAAE,UAAU;GAAS,CAAC;;AAI7E,QAAO;;;;;;;;;;;;;;;ACrJT,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;;;;;;;;;;;;;;AAkBR,SAAS,aAAa,OAAiC;AACrD,QAAO,qBAAqB,yBAAyB,MAAM,CAAC;;AAK9D,eAAe,wBACb,QACA,aACA,OACA,QACA,QACA,KAC6C;CAC7C,IAAI,iBAAiB;CACrB,IAAI,QAAQ;CAOZ,MAAM,QAAQ,oBAAoB,OAAO,OAAO;CAChD,MAAM,UAAU,MAAM;CAGtB,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,OAAO,aAAa,MAAM;KACnE,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;;;AAQlC,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,OAAO,aAAa,MAAM;IACpG,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,OAAO,aAAa,MAAM;IACpG,CAAC;;;CAYV,MAAM,cAAc,MAAM,sBAAsB,QAAQ,aAAa,OAAO,QAAQ,QAAQ,IAAI;AAChG,UAAS,YAAY;AAGrB,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,cAAc,aAAa,OAAO,QAAQ,OAAO,QAAQ,QAAQ;EACvE,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;AAMF,OAAI,OAAO,eAAe,CAAC,YAAY,YAAY;AACjD,UAAM,UAAU,aAAa,OAAO;KAAE;KAAQ;KAAS,eAAe,OAAO,QAAQ;KAAS,EAAE;KAG9F,GAAG,eAAe,KAAA,EAAU;KAC5B,QAAQ;KACT,CAAC;AACF;;;AAWN,MAAI,UAAU;GACZ,MAAM,UAAU,OAAO,QAAQ,SAAS;GACxC,MAAM,WAAW,QAAQ,QAAQ,GAAG,OAAO,EAAE,WAAW,QAAQ,CAAC,KAAK,CAAC,QAAQ,GAAG;GAClF,MAAM,iBAAiB,QAAQ,QAAQ,GAAG,OAAO,EAAE,WAAW,YAAY,CAAC;AAC3E,OAAI,iBAAiB,KAAK,SAAS,SAAS,EAC1C,QAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA,SACE,wBAAwB,SAAS,OAAO,aAAa,SAAS,WAAW,IAAI,MAAM,MAAM,aAC1E,eAAe,uCAAuC,SAAS,KAAK,KAAK,CAAC;IAE5F,CAAC;;;AAKR,QAAO;EAAE,SAAS;EAAgB;EAAO;;;;;;;;;;;;;;;;;;;;AAqB3C,eAAe,qBACb,aACA,OACA,QACA,QACA,KACiB;AAOjB,MAAK,MAAM,SAAS,qBAAqB,OAAO,OAAO,CACrD,QAAO,KAAK;EAAE,GAAG;EAAO,OAAO,MAAM;EAAI,CAAC;AAQ5C,MAAK,MAAM,QAAQ,iBAAiB,MAAM,OAAO,CAC/C,QAAO,KAAK;EACV,UAAU;EACV,OAAO,MAAM;EACb,OAAO;EACP,SAAS,UAAU,KAAK;EACzB,CAAC;CAGJ,MAAM,EAAE,QAAQ,aAAa,iBAAiB,MAAM;AACpD,KAAI,OAAO,WAAW,KAAK,SAAS,WAAW,EAAG,QAAO;CAEzD,MAAM,aAAa,CAAC,MAAM;AAC1B,KAAI,OAAO,eAAe,YAAY;EACpC,MAAM,OAAO,gBAAgB,MAAM,MAAM,MAAM,OAAO;AACtD,MAAI,MAAM;AACR,SAAM,cAAc,KAAK;AAIzB,SAAM,UAAU,KAAK,aAAa,uBAAuB,MAAM,GAAG,OAAO,EAAE,OAAO,kBAAkB;AACpG,UAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb,OAAO;IACP,SAAS,uBAAuB,KAAK,MAAM,WAAW,KAAK,KAAK;IACjE,CAAC;AACF,UAAO;;EAKT,MAAM,QAAQ,kBAAkB,MAAM,MAAM,MAAM,OAAO;AACzD,SAAO,KAAK;GACV,UAAU;GACV,OAAO,MAAM;GACb,OAAO;GACP,SAAS,MAAM,SAAS,IACpB,8DAA8D,MAAM,KAAI,MAAK,IAAI,EAAE,GAAG,CAAC,KAAK,KAAK,CAAC,wLAClG;GACL,CAAC;AACF,SAAO;;AAGT,MAAK,MAAM,WAAW,OACpB,QAAO,KAAK;EAAE,UAAU;EAAS,OAAO,MAAM;EAAI,OAAO;EAAe;EAAS,CAAC;AAEpF,MAAK,MAAM,WAAW,SACpB,QAAO,KAAK;EAAE,UAAU;EAAW,OAAO,MAAM;EAAI,OAAO;EAAe;EAAS,CAAC;AAEtF,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;AA0BT,eAAe,sBACb,QACA,aACA,OACA,QACA,QACA,KACiD;AACjD,KAAI,MAAM,KAAM,QAAO;EAAE,OAAO;EAAG,YAAY;EAAO;CAEtD,MAAM,UAAU,qBAAqB,MAAM;CAC3C,MAAM,WAAW,GAAG,OAAO,QAAQ,QAAQ;CAC3C,IAAI;AACJ,KAAI;AACF,UAAQ,MAAM,OAAO,cAAc,QAAQ;SACrC;AACN,SAAO;GAAE,OAAO;GAAG,YAAY;GAAO;;CAGxC,MAAM,SAAS,MAAM,QAAO,MAAK,EAAE,SAAS,QAAQ,IAAI,MAAM,SAAS;AACvE,KAAI,OAAO,WAAW,EAAG,QAAO;EAAE,OAAO;EAAG,YAAY;EAAO;AAE/D,QAAO,KAAK;EACV,UAAU;EACV,OAAO,MAAM;EACb,SACE,0BAA0B,MAAM,GAAG,0FACN,SAAS,gBAAgB,OAAO,KAAK,KAAK,CAAC;EAG3E,CAAC;AAEF,KAAI,CAAC,OAAO,CAAC,YAAa,QAAO;EAAE,OAAO;EAAG,YAAY;EAAM;AAI/D,KAFoB,MAAM,kBAA2C,QAAQ,GAAG,QAAQ,GAAG,WAAW,KAElF,MAAM;AAIxB,MAAI,OAAO,WAAW,EAAG,QAAO;GAAE,OAAO;GAAG,YAAY;GAAM;EAC9D,MAAM,QAAQ,OAAO;EACrB,MAAM,UAAU,MAAM,kBAA2C,QAAQ,GAAG,QAAQ,GAAG,QAAQ;AAC/F,MAAI,YAAY,KAAM,QAAO;GAAE,OAAO;GAAG,YAAY;GAAM;AAC3D,QAAM,UAAU,KAAK,aAAa,SAAS,SAAS,EAAE,QAAQ;AAC9D,QAAM,GAAG,KAAK,aAAa,SAAS,MAAM,EAAE,EAAE,OAAO,MAAM,CAAC;AAC5D,SAAO;GAAE,OAAO;GAAG,YAAY;GAAO;;AAKxC,OAAM,QAAQ,IAAI,OAAO,KAAI,UAAS,GAAG,KAAK,aAAa,SAAS,MAAM,EAAE,EAAE,OAAO,MAAM,CAAC,CAAC,CAAC;AAC9F,QAAO;EAAE,OAAO,OAAO;EAAQ,YAAY;EAAO;;AAGpD,eAAe,uBACb,QACA,aACA,OACA,QACA,QACA,KAC6C;CAC7C,IAAI,iBAAiB;CACrB,IAAI,QAAQ;CACZ,MAAM,QAAQ,oBAAoB,OAAO,OAAO;AAEhD,MAAK,MAAM,UAAU,MAAM,SAAS;EAClC,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,OAAO,aAAa,MAAM;KACnE,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,QAAQ,OAAO,QAAQ,QAAQ,CAAC;AACzH,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;CAClD,MAAM,QAAQ,oBAAoB,OAAO,OAAO;AAEhD,MAAK,MAAM,UAAU,MAAM,SAAS;EAClC,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,OAAO,aAAa,MAAM;KACnE,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,OAAO,aAAa,MAAM;IAC/F,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,OAAO,aAAa,MAAM;IAC/F,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;;;AAIhF,MAAM,gBAAgB,CAAC,cAAc,YAAY;;;;;;;;;;;AAYjD,eAAe,mBACb,QACA,OACA,QACA,MACA,aACA,eACmB;CACnB,MAAM,UAAU,cAAc,QAAO,MAAK,KAAK,eAAe,EAAE,MAAM,UAAU,KAAK,MAAM,QAAQ;AACnG,KAAI,QAAQ,WAAW,EAAG,QAAO,EAAE;CACnC,MAAM,OAAO,MAAM,kBAA6B,QAAQ,aAAa,OAAO,QAAQ,eAAe,KAAK,CAAC;AACzG,KAAI,CAAC,KAAM,QAAO,EAAE;AACpB,QAAO,QAAQ,QAAO,MAAK,KAAK,OAAO,KAAA,EAAU;;AAGnD,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;CAK9D,MAAM,QAAQ,oBAAoB,OAAO,OAAO;CAChD,MAAM,UAAU,MAAM;CAUtB,MAAM,2BAAW,IAAI,KAAqB;CAC1C,MAAM,sBAA+E,EAAE;AACvF,MAAK,MAAM,QAAQ,OAAO;AACxB,MAAI,KAAK,WAAW,IAAI,CAAE;AAC1B,OAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,MAAM,MAAM,kBAAkB,QAAQ,iBAAiB,OAAO,QAAQ,KAAK,CAAC;AAClF,OAAI,CAAC,IAAK;AACV,YAAS,IAAI,GAAG,KAAK,QAAQ,UAAU,IAAI;GAC3C,MAAM,EAAE,gBAAgB,iBAAiB,IAAI;AAC7C,uBAAoB,YAAY,EAAE;AAClC,uBAAoB,QAAQ,QAAQ;;;AAIxC,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,SAAS,IAAI,GAAG,KAAK,QAAQ,SAAS,IAAI;AAEtD,OAAI,CAAC,KAAK;AACR,QAAI,MAAM,MAAM;AACd,YAAO,KAAK;MACV,UAAU;MACV,OAAO,MAAM;MACb;MACA;MACA,SAAS,kCAAkC,KAAK,YAAY,OAAO,cAAc,aAAa,MAAM;MACrG,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;GAQJ,MAAM,SAAS,MAAM,mBAAmB,QAAQ,OAAO,QAAQ,MAAM,aAAa,OAAO,QAAQ,QAAQ;AACzG,OAAI,OAAO,SAAS,GAAG;IACrB,MAAM,OAAO,OAAO,KAAI,MAAK,IAAI,EAAE,GAAG,CAAC,KAAK,KAAK;AACjD,WAAO,KAAK;KACV,UAAU;KACV,OAAO,MAAM;KACb;KACA;KACA,OAAO,OAAO;KACd,SAAS,yCAAyC,KAAK;KACxD,CAAC;AACF,QAAI,OAAO,aAAa;KACtB,MAAM,UAAU,OAAO,YAAY,OAAO,QAAQ,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,SAAS,EAAE,CAAC,CAAC;AACpG,WAAM,UAAU,KAAK,aAAa,SAAS,EAAE,qBAAqB,SAAS,KAAK,CAAC;AACjF;;;AAIJ,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,QACA,KAAA,GAGA;KAAE,YAAY,oBAAoB,WAAW,EAAE;KAAE,gBAAgB;KAAM,CACxE;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;AAIA,gBAAc,MAAM,qBAAqB,aAAa,OAAO,QAAQ,QAAQ,IAAI;EAEjF,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,UAAU,oBAAoB,OAAO,OAAO,CAAC,SAAS;AAC/D,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-S19GHR4V.mjs","names":[],"sources":["../src/core/validator/entry.ts","../src/core/validator/relation-integrity.ts","../src/core/validator/schedule.ts","../src/core/validator/project.ts"],"sourcesContent":["import type { FieldDef, ModelDefinition, ValidationError, ValidationResult } from '@contentrain/types'\nimport { detectSecrets, validateFieldValue } from '@contentrain/types'\n\n/**\n * Context for cross-entry validation (unique, relations, nested references).\n *\n * Shape aligned with Studio's `content-validation.ts:ValidationContext` so\n * Studio can swap its internal implementation for this module without\n * touching call sites.\n */\nexport interface ValidationContext {\n /** All entries in the collection (for unique-constraint checks). Keyed by entry ID. */\n allEntries?: Record<string, Record<string, unknown>>\n /** ID of the entry being validated (excluded from unique-constraint comparisons). */\n currentEntryId?: string\n /** All model definitions, if available (reserved for future cross-model checks). */\n models?: ModelDefinition[]\n}\n\n/**\n * Validate a single content entry against its model's field schema.\n *\n * Merges the rule sets from MCP's legacy `validator.ts` (secret detection,\n * schema validation, unique constraints) with Studio's `content-validation.ts`\n * (email/url heuristics, polymorphic relation structure, nested object and\n * array-of-object recursion). The union is the authoritative per-entry\n * validator — both MCP's project validator and Studio's save path should\n * converge on this function over time.\n *\n * Asynchronous relation-integrity checks (does the referenced entry exist?)\n * live in `relation-integrity.ts` because they require I/O.\n */\nexport function validateContent(\n data: Record<string, unknown>,\n fields: Record<string, FieldDef>,\n modelId: string,\n locale: string,\n entryId?: string,\n ctx?: ValidationContext,\n): ValidationResult {\n const errors: ValidationError[] = []\n\n for (const [fieldId, def] of Object.entries(fields)) {\n const value = data[fieldId]\n errors.push(...validateField(value, def, modelId, locale, entryId, fieldId, ctx))\n }\n\n return {\n valid: errors.filter(e => e.severity === 'error').length === 0,\n errors,\n }\n}\n\n/** Bounds `items`-inside-`items` nesting; far above any real schema. */\nconst MAX_FIELD_DEPTH = 10\n\nfunction validateField(\n value: unknown,\n def: FieldDef,\n modelId: string,\n locale: string,\n entryId: string | undefined,\n fieldId: string,\n ctx?: ValidationContext,\n depth = 0,\n): ValidationError[] {\n const errors: ValidationError[] = []\n const errCtx = { model: modelId, locale, entry: entryId, field: fieldId }\n\n if (depth > MAX_FIELD_DEPTH) {\n return [{ severity: 'error', ...errCtx, message: `${fieldId} exceeds the maximum nesting depth of ${MAX_FIELD_DEPTH}` }]\n }\n\n if (value !== null && value !== undefined && value !== '') {\n const secretErrors = detectSecrets(value)\n for (const e of secretErrors) {\n errors.push({ ...e, ...errCtx })\n }\n }\n\n const isRelationType = def.type === 'relation' || def.type === 'relations'\n if (!isRelationType) {\n const fieldErrors = validateFieldValue(value, def)\n if (fieldErrors.length > 0) {\n for (const e of fieldErrors) errors.push({ ...e, ...errCtx })\n if (fieldErrors.some(e => e.severity === 'error')) return errors\n }\n } else if (def.required && (value === null || value === undefined || value === '')) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} is required` })\n return errors\n }\n\n if (value === null || value === undefined) return errors\n\n if (def.unique && ctx?.allEntries) {\n // String() comparison — matches legacy validator.ts uniqueness semantics\n // (catches cross-type duplicates like 42 vs '42' that strict === would miss).\n const valueKey = String(value)\n for (const [otherId, otherEntry] of Object.entries(ctx.allEntries)) {\n if (otherId === ctx.currentEntryId) continue\n const otherValue = otherEntry[fieldId]\n if (otherValue !== null && otherValue !== undefined && String(otherValue) === valueKey) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} must be unique — \"${String(value)}\" already exists in entry ${otherId}`,\n })\n break\n }\n }\n }\n\n if (def.type === 'relation' && def.model) {\n const targets = Array.isArray(def.model) ? def.model : [def.model]\n if (targets.length > 1) {\n if (typeof value !== 'object' || value === null || !('model' in value) || !('ref' in value)) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} must be { model, ref } for polymorphic relation`,\n })\n } else {\n const polyVal = value as { model: string, ref: string }\n if (!targets.includes(polyVal.model)) {\n errors.push({\n severity: 'error',\n ...errCtx,\n message: `${fieldId} target model \"${polyVal.model}\" must be one of: ${targets.join(', ')}`,\n })\n }\n }\n } else if (typeof value !== 'string') {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must be a string (entry ID or slug)` })\n }\n }\n\n if (def.type === 'relations') {\n if (!Array.isArray(value)) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must be an array` })\n } else {\n if (def.min !== undefined && value.length < def.min) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must have at least ${def.min} items` })\n }\n if (def.max !== undefined && value.length > def.max) {\n errors.push({ severity: 'error', ...errCtx, message: `${fieldId} must have at most ${def.max} items` })\n }\n // Polymorphic multi-relations (model targets multiple kinds) store\n // { model, ref } items — mirror the single-relation rule so content that\n // conforms to the generated type isn't rejected. Single-target relations\n // store plain id/slug strings.\n const targets = Array.isArray(def.model) ? def.model : (def.model ? [def.model] : [])\n const polymorphic = targets.length > 1\n for (let i = 0; i < value.length; i++) {\n const item = value[i]\n const itemCtx = { ...errCtx, field: `${fieldId}[${i}]` }\n if (polymorphic) {\n if (typeof item !== 'object' || item === null || !('model' in item) || !('ref' in item)) {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] must be { model, ref } for polymorphic relations` })\n } else {\n const polyItem = item as { model: string, ref: string }\n if (!targets.includes(polyItem.model)) {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] target model \"${polyItem.model}\" must be one of: ${targets.join(', ')}` })\n }\n }\n } else if (typeof item !== 'string') {\n errors.push({ severity: 'error', ...itemCtx, message: `${fieldId}[${i}] must be a string (entry ID or slug)` })\n }\n }\n }\n }\n\n // Array items run through `validateField` itself rather than a parallel type\n // switch. The old switch knew 10 of the 27 field types and checked only\n // `typeof`, so `min`/`max`/`pattern`/`options` never reached an item and an\n // `items` given as a FieldDef with a non-object type matched no branch at all —\n // silently unvalidated, while the type emitter rendered it as a real constraint.\n // Recursing keeps one rule set for a type wherever it appears.\n if (def.type === 'array' && Array.isArray(value) && def.items) {\n const itemDef: FieldDef = typeof def.items === 'string'\n ? { type: def.items as FieldDef['type'] }\n : def.items\n for (let i = 0; i < value.length; i++) {\n // `ctx` is deliberately dropped: `unique` compares an entry against its\n // siblings, which has no meaning for positions inside one array.\n errors.push(...validateField(\n value[i], itemDef, modelId, locale, entryId, `${fieldId}[${i}]`, undefined, depth + 1,\n ))\n }\n }\n\n if (def.type === 'object' && def.fields && typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const nested = validateContent(value as Record<string, unknown>, def.fields, modelId, locale, entryId, ctx)\n // Qualify the nested path with the parent so an error names where it lives —\n // `meta.title`, or `blocks[0].title` when this object is an array item.\n for (const e of nested.errors) {\n errors.push({ ...e, field: e.field ? `${fieldId}.${e.field}` : fieldId })\n }\n }\n\n return errors\n}\n","import type { FieldDef, ValidationError } from '@contentrain/types'\n\n/**\n * Resolution of a target model for a relation integrity check.\n *\n * - `{ exists: true, content: {...} }` — target model is known and its\n * content for the requested locale was loaded. The ref must appear\n * as a key in `content`.\n * - `{ exists: true, content: null }` — target model exists (e.g. a\n * singleton or dictionary) but per-entry key enforcement does not\n * apply. The checker skips the ref existence test for this target.\n * - `{ exists: false }` — the referenced model is not defined at all.\n * The checker emits a \"target model not found\" error.\n */\nexport interface ResolvedTarget {\n exists: boolean\n content?: Record<string, unknown> | null\n}\n\n/** Back-compat loader signature used by Studio. A null return means \"nothing to check\". */\nexport type LegacyLoadContent = (\n targetModelId: string,\n targetLocale: string,\n) => Promise<Record<string, unknown> | null>\n\nexport interface CheckRelationIntegrityOptions {\n /** Severity for broken-relation errors. Default: `warning` (Studio-compat). */\n severity?: 'error' | 'warning'\n /**\n * Richer target resolver — when provided, it can signal target-model\n * absence. MCP's project validator passes one so `contentrain_validate`\n * still emits \"target model not found\" errors.\n */\n resolveTarget?: (\n targetModelId: string,\n targetLocale: string,\n ) => Promise<ResolvedTarget>\n}\n\n/**\n * Verify that relation and relations fields reference targets that actually\n * exist. Two severities are supported — Studio's per-save flow emits\n * `warning` (the referenced entry may still be drafted), while MCP's\n * project-wide validator emits `error` and additionally flags missing\n * target models via `resolveTarget`.\n *\n * The loader abstraction keeps this function I/O-agnostic: MCP wires it\n * to filesystem reads through LocalReader, Studio wires it to a\n * `GitProvider.readFile`-backed loader, mocks pass in-memory maps.\n */\nexport async function checkRelationIntegrity(\n data: Record<string, unknown>,\n fields: Record<string, FieldDef>,\n modelId: string,\n locale: string,\n entryId: string | undefined,\n loadContent: LegacyLoadContent,\n opts: CheckRelationIntegrityOptions = {},\n): Promise<ValidationError[]> {\n const errors: ValidationError[] = []\n const severity = opts.severity ?? 'warning'\n\n const resolve = opts.resolveTarget\n ?? (async (id: string, loc: string): Promise<ResolvedTarget> => {\n const content = await loadContent(id, loc)\n return { exists: true, content }\n })\n\n for (const [fieldId, def] of Object.entries(fields)) {\n const value = data[fieldId]\n if (value === null || value === undefined) continue\n\n if (def.type === 'relation' && def.model) {\n const targets = Array.isArray(def.model) ? def.model : [def.model]\n\n if (targets.length > 1 && typeof value === 'object' && value !== null) {\n const polyVal = value as { model: string, ref: string }\n if (polyVal.model && polyVal.ref) {\n const resolved = await resolve(polyVal.model, locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${polyVal.model}\" not found`,\n })\n } else if (resolved.content && !(polyVal.ref in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${polyVal.ref}\" not found in ${polyVal.model}`,\n })\n }\n }\n } else if (typeof value === 'string' && targets[0]) {\n const resolved = await resolve(targets[0], locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${targets[0]}\" not found`,\n })\n } else if (resolved.content && !(value in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${value}\" not found in ${targets[0]}`,\n })\n }\n }\n }\n\n if (def.type === 'relations' && def.model && Array.isArray(value)) {\n const target = Array.isArray(def.model) ? def.model[0] : def.model\n if (target) {\n const resolved = await resolve(target, locale)\n if (!resolved.exists) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: target model \"${target}\" not found`,\n })\n } else if (resolved.content) {\n for (const ref of value) {\n if (typeof ref === 'string' && !(ref in resolved.content)) {\n errors.push({\n severity,\n model: modelId,\n locale,\n entry: entryId,\n field: fieldId,\n message: `Broken relation: \"${ref}\" not found in ${target}`,\n })\n }\n }\n }\n }\n }\n }\n\n return errors\n}\n","import type { EntryMeta, ValidationError } from '@contentrain/types'\n\n/**\n * Validate `publish_at` and `expire_at` meta fields.\n *\n * Rules (matches legacy `validator.ts:validateScheduleFields`):\n * - `publish_at` must parse as a valid Date\n * - `expire_at` must parse as a valid Date\n * - When both are present, `expire_at` must be strictly after `publish_at`\n */\nexport function validateScheduleFields(\n meta: EntryMeta,\n ctx: { model: string, locale: string, entry?: string, slug?: string },\n issues: ValidationError[],\n): void {\n if (meta.publish_at !== undefined) {\n const d = new Date(meta.publish_at)\n if (Number.isNaN(d.getTime())) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `Invalid publish_at date: \"${meta.publish_at}\". Must be a valid ISO 8601 date string.`,\n })\n }\n }\n if (meta.expire_at !== undefined) {\n const d = new Date(meta.expire_at)\n if (Number.isNaN(d.getTime())) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `Invalid expire_at date: \"${meta.expire_at}\". Must be a valid ISO 8601 date string.`,\n })\n }\n }\n if (meta.publish_at !== undefined && meta.expire_at !== undefined) {\n const pubDate = new Date(meta.publish_at)\n const expDate = new Date(meta.expire_at)\n if (!Number.isNaN(pubDate.getTime()) && !Number.isNaN(expDate.getTime()) && expDate <= pubDate) {\n issues.push({\n severity: 'error',\n ...ctx,\n message: `expire_at (\"${meta.expire_at}\") must be after publish_at (\"${meta.publish_at}\").`,\n })\n }\n }\n}\n","import type { ValidationError, ModelDefinition, ContentrainConfig, EntryMeta, ModelLocaleScope } from '@contentrain/types'\nimport { describeModelLocaleScope, detectSecrets, MODEL_FIELD_ORDER, resolveModelLocales, validateModelLocales } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { writeJson, writeText } from '../../util/fs.js'\nimport { readConfig } from '../config.js'\nimport { inferTitleField, legacyFieldNames, listModels, readModel, titleFieldIssues, titleFieldOptions } from '../model-manager.js'\nimport { mergeEntryMeta, writeMeta } from '../meta-manager.js'\nimport { parseFrontmatter, resolveLocaleStrategy, serializeFrontmatter } from '../content-manager.js'\nimport type { RepoReader } from '../contracts/index.js'\nimport { LocalReader } from '../../providers/local/reader.js'\nimport { contentDirPath, contentFilePath, documentFilePath, metaFilePath } from '../ops/paths.js'\nimport { validateContent } from './entry.js'\nimport { checkRelationIntegrity, type ResolvedTarget } from './relation-integrity.js'\nimport { validateScheduleFields } from './schedule.js'\n\n// ─── Types ───\n\nexport interface ValidateOptions {\n model?: string\n fix?: boolean\n}\n\nexport interface ValidateResult {\n valid: boolean\n summary: {\n errors: number\n warnings: number\n notices: number\n models_checked: number\n entries_checked: number\n }\n issues: ValidationError[]\n fixed: number\n}\n\n// ─── Reader-backed IO helpers ───\n\nasync function readJsonViaReader<T>(reader: RepoReader, path: string): Promise<T | null> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return null\n }\n}\n\nasync function readTextViaReader(reader: RepoReader, path: string): Promise<string | null> {\n try {\n return await reader.readFile(path)\n } catch {\n return null\n }\n}\n\n// ─── Shared field validation helpers ───\n\n/**\n * Build a `resolveTarget` adapter for `checkRelationIntegrity` that walks\n * the project's content store via the shared {@link RepoReader}. Mirrors\n * the target-resolution shape the legacy `checkRelation` used — collection\n * targets return their entry object-map, documents return a \"slug exists\"\n * marker map, singletons and dictionaries return null content so the\n * checker skips key enforcement for them.\n */\nfunction buildProjectTargetResolver(\n reader: RepoReader,\n config: ContentrainConfig,\n): (targetModelId: string, targetLocale: string) => Promise<ResolvedTarget> {\n return async (targetModelId, targetLocale) => {\n const targetModel = await readModel(reader, targetModelId)\n if (!targetModel) return { exists: false }\n\n if (targetModel.kind === 'document') {\n const docContentDir = contentDirPath(targetModel)\n const slugs = await discoverDocumentSlugs(reader, docContentDir, targetModel)\n return { exists: true, content: Object.fromEntries(slugs.map(s => [s, true])) }\n }\n if (targetModel.kind === 'singleton' || targetModel.kind === 'dictionary') {\n return { exists: true, content: null }\n }\n // Collection: resolve against the target model's own storage. i18n:false\n // targets are locale-agnostic (data.json). For i18n:true targets, fall back\n // to the default-locale file when the source locale lacks the entry — a\n // relation legitimately points at an entry that may be translated under\n // another locale, so the id set is merged to avoid phantom broken refs. #Y5\n const merged: Record<string, unknown> = {}\n const primary = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, targetLocale),\n )\n if (primary) Object.assign(merged, primary)\n if (targetModel.i18n && targetLocale !== config.locales.default) {\n const fallback = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, config.locales.default),\n )\n if (fallback) Object.assign(merged, fallback)\n }\n return { exists: true, content: merged }\n }\n}\n\n/**\n * Scan an entry's data fields for detected secrets in UNDECLARED keys —\n * the legacy validator also flagged stray/rogue fields that were not in\n * `model.fields`. `validateContent` only knows about declared fields, so\n * this complementary pass preserves that coverage.\n */\nfunction scanUndeclaredFieldsForSecrets(\n data: Record<string, unknown>,\n declared: Record<string, unknown> | undefined,\n ctx: { model: string, locale: string, entry?: string, slug?: string },\n issues: ValidationError[],\n): void {\n for (const [fieldName, value] of Object.entries(data)) {\n if (declared && fieldName in declared) continue\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n ...ctx,\n field: fieldName,\n message: `Potential secret detected in field \"${fieldName}\"`,\n })\n }\n }\n}\n\n// ─── Locale coverage ───\n\n/**\n * The clause every locale-coverage message ends with: which list the check was\n * evaluated against.\n *\n * Without it \"missing tr\" is unreadable. A model that legitimately covers two\n * of four project locales and one that has genuinely lost a translation produce\n * the same sentence, and the reader cannot tell which they are looking at\n * without opening the model definition and config.json side by side. Naming the\n * list turns the message into its own explanation — and, when the list is the\n * project-wide one, into the hint that `locales` is what narrows it.\n */\nfunction againstScope(scope: ModelLocaleScope): string {\n return ` (checked against ${describeModelLocaleScope(scope)})`\n}\n\n// ─── Per-model validators ───\n\nasync function validateCollectionModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n // An i18n model that declares `locales` covers that subset and no more — a\n // model that was never translated into a locale says so once, instead of\n // failing on that locale's missing file.\n const scope = resolveModelLocales(model, config)\n const locales = scope.locales\n\n // Collect all entry IDs per locale for parity check\n const localeEntryIds: Record<string, Set<string>> = {}\n const allEntryIds = new Set<string>()\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n\n for (const locale of locales) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, Record<string, unknown>>>(reader, filePath)\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json${againstScope(scope)}`,\n })\n\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n const ids = new Set(Object.keys(data))\n localeEntryIds[locale] = ids\n for (const id of ids) allEntryIds.add(id)\n\n // Check canonical sort\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, Record<string, unknown>> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate each entry — validateContent covers secret/field/unique/email-url/\n // polymorphic/nested on declared fields; supplementary scans below catch\n // undeclared-field secrets and async relation integrity.\n for (const [entryId, fields] of Object.entries(data)) {\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(\n fields,\n model.fields,\n { model: model.id, locale, entry: entryId },\n issues,\n )\n\n if (!model.fields) continue\n\n const entryResult = validateContent(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n { allEntries: data, currentEntryId: entryId },\n )\n issues.push(...entryResult.errors)\n\n const relationErrors = await checkRelationIntegrity(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\n }\n }\n\n // Entry parity check (i18n). An entry one locale has and another lacks is a\n // translation that was never written — on a WPML or Polylang import the normal\n // state of a site, not a broken store — so it is a warning, as a document's\n // missing translation is. A locale with no file at all stays an error above.\n if (model.i18n && Object.keys(localeEntryIds).length > 1) {\n const localeKeys = Object.keys(localeEntryIds)\n for (let i = 1; i < localeKeys.length; i++) {\n const locA = localeKeys[0]!\n const locB = localeKeys[i]!\n const idsA = localeEntryIds[locA]!\n const idsB = localeEntryIds[locB]!\n\n for (const id of idsA) {\n if (!idsB.has(id)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locB,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locA} but missing in ${locB}${againstScope(scope)}`,\n })\n }\n }\n for (const id of idsB) {\n if (!idsA.has(id)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locA,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locB} but missing in ${locA}${againstScope(scope)}`,\n })\n }\n }\n }\n }\n\n // Consolidate a non-i18n model's meta layout BEFORE the orphan checks below.\n // The reverse orphan-content check fabricates a draft default-locale meta for\n // any content entry lacking meta at the default locale. If a stray holds the\n // real (e.g. published) record under a non-default locale, running that check\n // first would mint a draft default and let this pass then delete the real\n // record. Consolidating first preserves the authoritative record.\n const strayResult = await checkStrayNonI18nMeta(reader, projectRoot, model, config, issues, fix)\n fixed += strayResult.fixed\n\n // Orphan meta check\n for (const locale of locales) {\n const metaRelPath = metaFilePath(model, locale, config.locales.default)\n const metaData = await readJsonViaReader<Record<string, EntryMeta>>(reader, metaRelPath)\n const contentData = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(model, locale),\n ) ?? {}\n\n // Forward check: meta entries without content + schedule validation\n if (metaData) {\n for (const metaEntryId of Object.keys(metaData)) {\n // Validate schedule fields on every meta entry\n validateScheduleFields(metaData[metaEntryId]!, { model: model.id, locale, entry: metaEntryId }, issues)\n\n if (!(metaEntryId in contentData)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n entry: metaEntryId,\n message: `Orphan meta: meta entry \"${metaEntryId}\" exists but content entry missing`,\n })\n if (fix && projectRoot) {\n delete metaData[metaEntryId]\n const metaAbs = join(projectRoot, metaRelPath)\n if (Object.keys(metaData).length > 0) {\n await writeJson(metaAbs, metaData)\n } else {\n await rm(metaAbs, { force: true })\n }\n fixed++\n }\n }\n }\n }\n\n // Reverse check: content entries without meta\n for (const entryId of Object.keys(contentData)) {\n if (!metaData || !(entryId in metaData)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n entry: entryId,\n message: `Orphan content: entry \"${entryId}\" has no metadata`,\n })\n // Skip fabrication while a non-i18n model's meta is stuck in an\n // unresolved stray: the content already has meta (misplaced), so minting\n // a draft default here would let the stray-consolidation pass then delete\n // the real record on a subsequent run. The mismatch warning already asks\n // the agent to resolve the layout by hand.\n if (fix && projectRoot && !strayResult.unresolved) {\n await writeMeta(projectRoot, model, { locale, entryId, defaultLocale: config.locales.default }, {\n // `source: 'import'` on purpose: this record is being fabricated\n // for content that arrived without one, not written by an agent.\n ...mergeEntryMeta(undefined),\n source: 'import',\n })\n fixed++\n }\n }\n }\n\n // Publish-state drift. Drafts sitting alongside published entries in the\n // same collection is the fingerprint of writes that reset status — the\n // shape that silently emptied collections on the CDN. Advisory only: a\n // genuine work-in-progress entry is indistinguishable, so this is a call\n // for the agent to make, not an error. Never auto-fixed — publishing is a\n // content decision, and MCP does not make those.\n if (metaData) {\n const entries = Object.entries(metaData)\n const draftIds = entries.filter(([, m]) => m.status === 'draft').map(([id]) => id)\n const publishedCount = entries.filter(([, m]) => m.status === 'published').length\n if (publishedCount > 0 && draftIds.length > 0) {\n issues.push({\n severity: 'notice',\n model: model.id,\n locale,\n message:\n `Publish-state drift: ${draftIds.length} draft entr${draftIds.length === 1 ? 'y' : 'ies'} `\n + `alongside ${publishedCount} published in the same collection — [${draftIds.join(', ')}]. `\n + 'If these were published before, restore them with contentrain_bulk update_status.',\n })\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\n/**\n * Flag — and, with `fix`, backfill — a model definition's `title_field`.\n *\n * This is the migration path for models authored before the property existed.\n * Reads are an unvalidated cast, so such a model loads happily with `title_field`\n * undefined in a slot the type system calls a string; nothing would ever say so.\n *\n * Only the *missing* case is repaired. A `title_field` that names an absent field\n * or an undisplayable type is an authoring mistake, not a legacy gap — filling in\n * a different answer there would be overruling a decision someone made, which is\n * the line `project.ts` draws everywhere else.\n *\n * Every repair emits a notice naming the field and the rule that chose it. The\n * doctrine that governs the rest of this file is that MCP does not make content\n * decisions; a schema backfill is not a content decision, but the principle\n * underneath — never exercise judgement invisibly — still binds. The notice and\n * the diff on `cr/fix/validate` are the two ways a human can overrule it.\n */\nasync function checkModelDefinition(\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<number> {\n // `locales` is reported, never repaired — not even under `fix`. A model that\n // does not declare one covers every project locale, and choosing a narrower\n // set for it would be deciding which translations are *supposed* to exist,\n // which is a content decision. The one MCP could compute — \"the locales that\n // happen to have files today\" — would write the current gaps into the schema\n // and silence the very errors that reveal them.\n for (const issue of validateModelLocales(model, config)) {\n issues.push({ ...issue, model: model.id })\n }\n\n // Field names that predate the snake_case rule. `contentrain_model_save`\n // keeps them (#116) — this is how an agent learns a model carries one before\n // it edits the model, not mid-write. A notice, not a warning: renaming means\n // renaming content keys in every locale and every consumer, so it is a\n // migration to plan, not a defect to clear.\n for (const path of legacyFieldNames(model.fields)) {\n issues.push({\n severity: 'notice',\n model: model.id,\n field: path,\n message: `Field \"${path}\" predates the snake_case rule. contentrain_model_save keeps it as-is; new fields must be snake_case. Renaming it also means renaming its content keys in every locale.`,\n })\n }\n\n const { errors, warnings } = titleFieldIssues(model)\n if (errors.length === 0 && warnings.length === 0) return 0\n\n const repairable = !model.title_field\n if (fix && projectRoot && repairable) {\n const pick = inferTitleField(model.kind, model.fields)\n if (pick) {\n model.title_field = pick.field\n // writeJson, not writeModel: writeModel also ensures the content and meta\n // directories, which for a model with `content_path` means creating\n // directories in the user's source tree. A validator does not do that.\n await writeJson(join(projectRoot, `.contentrain/models/${model.id}.json`), model, MODEL_FIELD_ORDER)\n issues.push({\n severity: 'notice',\n model: model.id,\n field: 'title_field',\n message: `title_field set to \"${pick.field}\" (rule: ${pick.rule}). If that is the wrong field, correct it with contentrain_model_save.`,\n })\n return 1\n }\n // Inference declined. Two different situations, said differently: a model\n // whose only displayable fields are slug/url/code has legal choices that a\n // human must make (#120); a model with none needs a field added.\n const legal = titleFieldOptions(model.kind, model.fields)\n issues.push({\n severity: 'error',\n model: model.id,\n field: 'title_field',\n message: legal.length > 0\n ? `Missing \"title_field\" and no field reads as a title — only ${legal.map(f => `\"${f}\"`).join(', ')} could legally hold one, and a slug, URL or code field, or a field inside an object, is not inferred. Set title_field explicitly with contentrain_model_save, or add a string field.`\n : `Missing \"title_field\" and no field can serve as one — add a string or text field, then re-run. A title cannot be an image, relation or number.`,\n })\n return 0\n }\n\n for (const message of errors) {\n issues.push({ severity: 'error', model: model.id, field: 'title_field', message })\n }\n for (const message of warnings) {\n issues.push({ severity: 'warning', model: model.id, field: 'title_field', message })\n }\n return 0\n}\n\n/**\n * Flag — and, with `fix`, remediate — meta files a non-i18n model should not have.\n *\n * Such a model keeps all content in one `data.json` and therefore exactly one\n * meta record, at the default locale. Earlier writes derived the meta path from\n * the caller's locale, so saving under a non-default locale left a second meta\n * file, and readers disagreed about which was authoritative.\n *\n * The `fix` remediation is deterministic and never decides a status:\n * - default-locale meta present → the strays are redundant, so delete them\n * (the default-locale record stays authoritative — no status is merged).\n * - default-locale meta absent, exactly one stray → that stray holds the only\n * record, so migrate it to the default path (move) rather than orphan the\n * content.\n * - default-locale meta absent, several strays → which is authoritative is\n * ambiguous, so leave the warning for the agent to resolve by hand.\n *\n * Returns the count of files remediated and whether strays remain unresolved.\n * `unresolved` gates the caller's orphan-content fabrication: while a non-i18n\n * model's meta still lives in a stray, the content is not truly orphaned, so\n * minting a draft default-locale record would both be wrong and set up a trap\n * (a later fix pass would then treat the real stray as redundant and delete it).\n */\nasync function checkStrayNonI18nMeta(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ fixed: number, unresolved: boolean }> {\n if (model.i18n) return { fixed: 0, unresolved: false }\n\n const metaDir = `.contentrain/meta/${model.id}`\n const expected = `${config.locales.default}.json`\n let files: string[]\n try {\n files = await reader.listDirectory(metaDir)\n } catch {\n return { fixed: 0, unresolved: false }\n }\n\n const strays = files.filter(f => f.endsWith('.json') && f !== expected)\n if (strays.length === 0) return { fixed: 0, unresolved: false }\n\n issues.push({\n severity: 'warning',\n model: model.id,\n message:\n `Meta layout mismatch: \"${model.id}\" has i18n disabled, so its content lives in a single data.json `\n + `and its meta belongs at ${expected} alone — but [${strays.join(', ')}] also exist. `\n + 'Readers may disagree about which file is authoritative. Run contentrain_validate fix:true to prune '\n + `the extras (the default-locale meta stays authoritative).`,\n })\n\n if (!fix || !projectRoot) return { fixed: 0, unresolved: true }\n\n const defaultMeta = await readJsonViaReader<Record<string, unknown>>(reader, `${metaDir}/${expected}`)\n\n if (defaultMeta === null) {\n // No authoritative default. A single stray holds the only record — keep it\n // by moving it to the default path. Several strays with no default is\n // ambiguous, so do not guess: leave the warning standing (unresolved).\n if (strays.length !== 1) return { fixed: 0, unresolved: true }\n const stray = strays[0]!\n const content = await readJsonViaReader<Record<string, unknown>>(reader, `${metaDir}/${stray}`)\n if (content === null) return { fixed: 0, unresolved: true }\n await writeJson(join(projectRoot, metaDir, expected), content)\n await rm(join(projectRoot, metaDir, stray), { force: true })\n return { fixed: 1, unresolved: false }\n }\n\n // Default-locale meta is authoritative → the strays are redundant. Each is a\n // distinct file, so removing them in parallel is safe.\n await Promise.all(strays.map(stray => rm(join(projectRoot, metaDir, stray), { force: true })))\n return { fixed: strays.length, unresolved: false }\n}\n\nasync function validateSingletonModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n const scope = resolveModelLocales(model, config)\n\n for (const locale of scope.locales) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, unknown>>(reader, filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json${againstScope(scope)}`,\n })\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(data, model.fields, { model: model.id, locale }, issues)\n\n if (model.fields) {\n const entryResult = validateContent(data, model.fields, model.id, locale)\n issues.push(...entryResult.errors)\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n const relationErrors = await checkRelationIntegrity(\n data,\n model.fields,\n model.id,\n locale,\n undefined,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, unknown> = {}\n for (const key of sorted) {\n resorted[key] = data[key]\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate schedule fields in singleton meta\n const singletonMetaData = await readJsonViaReader<EntryMeta>(reader, metaFilePath(model, locale, config.locales.default))\n if (singletonMetaData) {\n validateScheduleFields(singletonMetaData, { model: model.id, locale }, issues)\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function validateDictionaryModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n\n const localeKeys: Record<string, Set<string>> = {}\n const scope = resolveModelLocales(model, config)\n\n for (const locale of scope.locales) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, string>>(reader, filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json${againstScope(scope)}`,\n })\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n localeKeys[locale] = new Set(Object.keys(data))\n\n // Secret detection\n for (const [key, value] of Object.entries(data)) {\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: key,\n message: `Potential secret detected in key \"${key}\"`,\n })\n }\n }\n\n // Duplicate value detection\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n for (const [value, dupeKeys] of valueToKeys) {\n if (dupeKeys.length > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: `Duplicate value \"${truncated}\" mapped to ${dupeKeys.length} keys: [${dupeKeys.join(', ')}]`,\n })\n }\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, string> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n }\n\n // Key parity check\n if (model.i18n && Object.keys(localeKeys).length > 1) {\n const localeNames = Object.keys(localeKeys)\n for (let i = 1; i < localeNames.length; i++) {\n const locA = localeNames[0]!\n const locB = localeNames[i]!\n const keysA = localeKeys[locA]!\n const keysB = localeKeys[locB]!\n\n for (const k of keysA) {\n if (!keysB.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locB,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locA} but missing in ${locB}${againstScope(scope)}`,\n })\n }\n }\n for (const k of keysB) {\n if (!keysA.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locA,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locB} but missing in ${locA}${againstScope(scope)}`,\n })\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function discoverDocumentSlugs(\n reader: RepoReader,\n cDir: string,\n model: ModelDefinition,\n): Promise<string[]> {\n const strategy = resolveLocaleStrategy(model)\n const entries = await reader.listDirectory(cDir)\n\n // When i18n is disabled, documents are flat {slug}.md files\n if (!model.i18n) {\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n }\n\n if (strategy === 'file') {\n // Each slug is a subdirectory\n return entries.filter(e => !e.startsWith('.'))\n }\n\n if (strategy === 'suffix') {\n // Files like {slug}.{locale}.md — extract unique slugs\n const slugs = new Set<string>()\n for (const f of entries) {\n if (!f.endsWith('.md')) continue\n // Remove last two dot-separated segments (.locale.md)\n const parts = f.replace(/\\.md$/, '').split('.')\n if (parts.length >= 2) {\n parts.pop() // remove locale\n slugs.add(parts.join('.'))\n }\n }\n return [...slugs]\n }\n\n if (strategy === 'directory') {\n // Locale subdirectories contain {slug}.md files — collect slugs from all locale dirs\n const slugs = new Set<string>()\n const localeLists = await Promise.all(\n entries\n .filter(localeDir => !localeDir.startsWith('.'))\n .map(localeDir => reader.listDirectory(`${cDir}/${localeDir}`)),\n )\n for (const files of localeLists) {\n for (const f of files) {\n if (f.endsWith('.md')) slugs.add(f.replace(/\\.md$/, ''))\n }\n }\n return [...slugs]\n }\n\n // 'none' — {slug}.md files, no locale\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n}\n\n/** Meta-owned scheduling keys; see `EntrySchedule` in meta-manager. */\nconst SCHEDULE_KEYS = ['publish_at', 'expire_at'] as const\n\n/**\n * Scheduling keys a document's frontmatter carries that belong to its meta.\n *\n * Before #125, `contentrain_content_save` merged `publish_at`/`expire_at` into\n * the entry's data before planning the write, so the same value landed in the\n * frontmatter and in meta. A key counts as leaked only when three things hold:\n * it is in the frontmatter, the model does not declare a field by that name,\n * and the meta record holds the key too. The third condition is what separates\n * the tool's leftover from an author's own frontmatter field.\n */\nasync function leakedScheduleKeys(\n reader: RepoReader,\n model: ModelDefinition,\n locale: string,\n slug: string,\n frontmatter: Record<string, unknown>,\n defaultLocale: string,\n): Promise<string[]> {\n const present = SCHEDULE_KEYS.filter(k => k in frontmatter && !(model.fields && k in model.fields))\n if (present.length === 0) return []\n const meta = await readJsonViaReader<EntryMeta>(reader, metaFilePath(model, locale, defaultLocale, slug))\n if (!meta) return []\n return present.filter(k => meta[k] !== undefined)\n}\n\nasync function validateDocumentModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n const cDir = contentDirPath(model)\n\n if (!await reader.fileExists(cDir)) return { entries: 0, fixed: 0 }\n\n const slugs = await discoverDocumentSlugs(reader, cDir, model)\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n // A declared `locales` narrows the i18n case — see validateCollectionModel.\n const scope = resolveModelLocales(model, config)\n const locales = scope.locales\n\n // Read every document once up front and keep its frontmatter per locale.\n // `unique` compares an entry against its siblings, so it needs the whole set —\n // validating one file at a time is why `unique` was silently a no-op for\n // documents, which is exactly where every shipped template declares it.\n // Keyed by `${slug}\\u0000${locale}`. The NUL separator can appear in neither\n // a slug nor a locale, so composite keys never collide. Written as the escape\n // `\\u0000` (not a raw NUL byte) so the source stays plain text — a literal NUL\n // makes tools classify this file as binary and breaks grep/diff.\n const rawByKey = new Map<string, string>()\n const frontmatterByLocale: Record<string, Record<string, Record<string, unknown>>> = {}\n for (const slug of slugs) {\n if (slug.startsWith('.')) continue\n for (const locale of locales) {\n const raw = await readTextViaReader(reader, documentFilePath(model, locale, slug))\n if (!raw) continue\n rawByKey.set(`${slug}\\u0000${locale}`, raw)\n const { frontmatter } = parseFrontmatter(raw)\n frontmatterByLocale[locale] ??= {}\n frontmatterByLocale[locale][slug] = frontmatter\n }\n }\n\n for (const slug of slugs) {\n if (slug.startsWith('.')) continue\n\n for (const locale of locales) {\n const filePath = documentFilePath(model, locale, slug)\n const raw = rawByKey.get(`${slug}\\u0000${locale}`) ?? null\n\n if (!raw) {\n if (model.i18n) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n slug,\n message: `Missing translation: document \"${slug}\" missing ${locale} locale file${againstScope(scope)}`,\n })\n if (fix && projectRoot) {\n // Create empty template\n const template = `---\\nslug: ${slug}\\n---\\n`\n await writeText(join(projectRoot, filePath), template)\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n const { frontmatter, body, preserved } = parseFrontmatter(raw)\n for (const block of preserved?.blocks ?? []) {\n if (block.key === undefined) continue\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n slug,\n field: block.key,\n message: `Frontmatter key \"${block.key}\" is in a shape Contentrain does not read (a nested map, a block value, or a name with a space or a non-ASCII letter). It is kept exactly as written when the document is saved, but it is not validated.`,\n })\n }\n\n // Always scan undeclared frontmatter fields + body for secrets.\n scanUndeclaredFieldsForSecrets(\n frontmatter,\n // Treat `body` as a declared field so the undeclared-scan skips it; body\n // is scanned separately below.\n { ...model.fields, body: true } as Record<string, unknown>,\n { model: model.id, locale, slug },\n issues,\n )\n\n if (detectSecrets(body).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: 'body',\n message: 'Potential secret detected in document body',\n })\n }\n\n // Scheduling keys an older content_save copied into the frontmatter as\n // well as into meta (#125). They are meta's, not the author's — and since\n // documents merge frontmatter on save, no later save could remove them.\n // Recognised only when meta holds the same key: an undeclared `publish_at`\n // with no meta counterpart is the author's own field and is left alone.\n const leaked = await leakedScheduleKeys(reader, model, locale, slug, frontmatter, config.locales.default)\n if (leaked.length > 0) {\n const keys = leaked.map(k => `\"${k}\"`).join(', ')\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n slug,\n field: leaked[0],\n message: `Frontmatter carries scheduling key(s) ${keys} that belong in meta — copied there by an older content_save. The meta record already holds the value; run validate with fix:true to strip the frontmatter copy.`,\n })\n if (fix && projectRoot) {\n const cleaned = Object.fromEntries(Object.entries(frontmatter).filter(([k]) => !leaked.includes(k)))\n await writeText(join(projectRoot, filePath), serializeFrontmatter(cleaned, body, preserved))\n fixed++\n }\n }\n\n if (model.fields) {\n const fieldsWithoutBody = Object.fromEntries(\n Object.entries(model.fields).filter(([name]) => name !== 'body'),\n )\n const entryResult = validateContent(\n frontmatter,\n fieldsWithoutBody,\n model.id,\n locale,\n undefined,\n // Siblings in the same locale, so `unique` has something to compare\n // against. Keyed by slug — a document's identity is its slug.\n { allEntries: frontmatterByLocale[locale] ?? {}, currentEntryId: slug },\n )\n for (const err of entryResult.errors) {\n issues.push({ ...err, slug })\n }\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n const relationErrors = await checkRelationIntegrity(\n frontmatter,\n fieldsWithoutBody,\n model.id,\n locale,\n undefined,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n for (const err of relationErrors) {\n issues.push({ ...err, slug })\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\n// ─── Orphan content check ───\n\nasync function checkOrphanContent(\n reader: RepoReader,\n validModelIds: Set<string>,\n issues: ValidationError[],\n _fix: boolean,\n): Promise<number> {\n const fixed = 0\n const contentBase = '.contentrain/content'\n const domains = await reader.listDirectory(contentBase)\n\n const modelLists = await Promise.all(\n domains\n .filter(d => !d.startsWith('.'))\n .map(async d => ({ domain: d, dirs: await reader.listDirectory(`${contentBase}/${d}`) })),\n )\n\n for (const { dirs } of modelLists) {\n for (const modelDir of dirs) {\n if (modelDir.startsWith('.')) continue\n if (!validModelIds.has(modelDir)) {\n issues.push({\n severity: 'warning',\n model: modelDir,\n message: `Orphan content: content directory exists for deleted model \"${modelDir}\"`,\n })\n }\n }\n }\n\n return fixed\n}\n\n// ─── Main validation function ───\n\n/**\n * Validate a project's content against its model schemas.\n *\n * Two signatures:\n *\n * - `validateProject(projectRoot, options?)` — legacy disk-backed flow.\n * `options.fix: true` applies structural repairs (canonical sort,\n * orphan meta, missing locale files) directly on disk.\n * - `validateProject(reader, options?)` — reader-backed flow used by\n * HTTP/remote callers (e.g. GitHubProvider). `options.fix` is ignored\n * because remote flows cannot write to the local filesystem.\n *\n * Read-side behavior is identical across both signatures so Studio and\n * local CLIs see the same issue set for the same content state.\n */\nexport async function validateProject(projectRoot: string, options?: ValidateOptions): Promise<ValidateResult>\nexport async function validateProject(reader: RepoReader, options?: ValidateOptions): Promise<ValidateResult>\nexport async function validateProject(\n input: string | RepoReader,\n options?: ValidateOptions,\n): Promise<ValidateResult> {\n const reader: RepoReader = typeof input === 'string' ? new LocalReader(input) : input\n const projectRoot: string | undefined = typeof input === 'string' ? input : undefined\n const fix = Boolean(options?.fix) && projectRoot !== undefined\n\n const issues: ValidationError[] = []\n let totalEntries = 0\n let totalFixed = 0\n let modelsChecked = 0\n\n const config = await readConfig(reader)\n if (!config) {\n return {\n valid: false,\n summary: { errors: 1, warnings: 0, notices: 0, models_checked: 0, entries_checked: 0 },\n issues: [{ severity: 'error', message: 'Project not initialized: config.json missing' }],\n fixed: 0,\n }\n }\n\n const modelSummaries = await listModels(reader)\n const validModelIds = new Set(modelSummaries.map(m => m.id))\n const modelsToCheck = options?.model\n ? modelSummaries.filter(m => m.id === options.model)\n : modelSummaries\n\n for (const summary of modelsToCheck) {\n const model = await readModel(reader, summary.id)\n if (!model) continue\n\n modelsChecked++\n // Ahead of the content checks: a broken model definition explains the content\n // errors below it, and the repair mutates `model` in place so the content\n // validators downstream see the fixed object.\n totalFixed += await checkModelDefinition(projectRoot, model, config, issues, fix)\n\n let result: { entries: number; fixed: number }\n\n switch (model.kind) {\n case 'collection':\n result = await validateCollectionModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'singleton':\n result = await validateSingletonModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'dictionary':\n result = await validateDictionaryModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'document':\n result = await validateDocumentModel(reader, projectRoot, model, config, issues, fix)\n break\n default:\n result = { entries: 0, fixed: 0 }\n }\n\n totalEntries += result.entries\n totalFixed += result.fixed\n }\n\n // Orphan content check (only when checking all models)\n if (!options?.model) {\n totalFixed += await checkOrphanContent(reader, validModelIds, issues, fix)\n }\n\n // Cross-dictionary duplicate value detection (only when checking all models)\n if (!options?.model) {\n const dictModels = modelsToCheck.filter(m => m.kind === 'dictionary')\n if (dictModels.length > 1) {\n const globalValueMap: Record<string, Map<string, Array<{ model: string; key: string }>>> = {}\n\n for (const summary of dictModels) {\n const model = await readModel(reader, summary.id)\n if (!model) continue\n\n for (const locale of resolveModelLocales(model, config).locales) {\n if (!globalValueMap[locale]) globalValueMap[locale] = new Map()\n const data = await readJsonViaReader<Record<string, string>>(reader, contentFilePath(model, locale))\n if (!data) continue\n\n for (const [key, value] of Object.entries(data)) {\n const refs = globalValueMap[locale]!.get(value)\n if (refs) refs.push({ model: model.id, key })\n else globalValueMap[locale]!.set(value, [{ model: model.id, key }])\n }\n }\n }\n\n for (const [locale, valueMap] of Object.entries(globalValueMap)) {\n for (const [value, refs] of valueMap) {\n const uniqueModels = new Set(refs.map(r => r.model))\n if (uniqueModels.size > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'notice',\n locale,\n message: `Cross-model duplicate value \"${truncated}\" in ${refs.map(r => `${r.model}/${r.key}`).join(', ')}`,\n })\n }\n }\n }\n }\n }\n\n const errors = issues.filter(i => i.severity === 'error').length\n const warnings = issues.filter(i => i.severity === 'warning').length\n const notices = issues.filter(i => i.severity === 'notice').length\n\n return {\n valid: errors === 0,\n summary: {\n errors,\n warnings,\n notices,\n models_checked: modelsChecked,\n entries_checked: totalEntries,\n },\n issues,\n fixed: totalFixed,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;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;;;AAIH,MAAM,kBAAkB;AAExB,SAAS,cACP,OACA,KACA,SACA,QACA,SACA,SACA,KACA,QAAQ,GACW;CACnB,MAAM,SAA4B,EAAE;CACpC,MAAM,SAAS;EAAE,OAAO;EAAS;EAAQ,OAAO;EAAS,OAAO;EAAS;AAEzE,KAAI,QAAQ,gBACV,QAAO,CAAC;EAAE,UAAU;EAAS,GAAG;EAAQ,SAAS,GAAG,QAAQ,wCAAwC;EAAmB,CAAC;AAG1H,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,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;;;AAYvH,KAAI,IAAI,SAAS,WAAW,MAAM,QAAQ,MAAM,IAAI,IAAI,OAAO;EAC7D,MAAM,UAAoB,OAAO,IAAI,UAAU,WAC3C,EAAE,MAAM,IAAI,OAA2B,GACvC,IAAI;AACR,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAGhC,QAAO,KAAK,GAAG,cACb,MAAM,IAAI,SAAS,SAAS,QAAQ,SAAS,GAAG,QAAQ,GAAG,EAAE,IAAI,KAAA,GAAW,QAAQ,EACrF,CAAC;;AAIN,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,SAAS,IAAI;AAG3G,OAAK,MAAM,KAAK,OAAO,OACrB,QAAO,KAAK;GAAE,GAAG;GAAG,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,EAAE,UAAU;GAAS,CAAC;;AAI7E,QAAO;;;;;;;;;;;;;;;ACrJT,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;;;;;;;;;;;;;;AAkBR,SAAS,aAAa,OAAiC;AACrD,QAAO,qBAAqB,yBAAyB,MAAM,CAAC;;AAK9D,eAAe,wBACb,QACA,aACA,OACA,QACA,QACA,KAC6C;CAC7C,IAAI,iBAAiB;CACrB,IAAI,QAAQ;CAOZ,MAAM,QAAQ,oBAAoB,OAAO,OAAO;CAChD,MAAM,UAAU,MAAM;CAGtB,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,OAAO,aAAa,MAAM;KACnE,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;;;AAQlC,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,OAAO,aAAa,MAAM;IACpG,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,OAAO,aAAa,MAAM;IACpG,CAAC;;;CAYV,MAAM,cAAc,MAAM,sBAAsB,QAAQ,aAAa,OAAO,QAAQ,QAAQ,IAAI;AAChG,UAAS,YAAY;AAGrB,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,cAAc,aAAa,OAAO,QAAQ,OAAO,QAAQ,QAAQ;EACvE,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;AAMF,OAAI,OAAO,eAAe,CAAC,YAAY,YAAY;AACjD,UAAM,UAAU,aAAa,OAAO;KAAE;KAAQ;KAAS,eAAe,OAAO,QAAQ;KAAS,EAAE;KAG9F,GAAG,eAAe,KAAA,EAAU;KAC5B,QAAQ;KACT,CAAC;AACF;;;AAWN,MAAI,UAAU;GACZ,MAAM,UAAU,OAAO,QAAQ,SAAS;GACxC,MAAM,WAAW,QAAQ,QAAQ,GAAG,OAAO,EAAE,WAAW,QAAQ,CAAC,KAAK,CAAC,QAAQ,GAAG;GAClF,MAAM,iBAAiB,QAAQ,QAAQ,GAAG,OAAO,EAAE,WAAW,YAAY,CAAC;AAC3E,OAAI,iBAAiB,KAAK,SAAS,SAAS,EAC1C,QAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb;IACA,SACE,wBAAwB,SAAS,OAAO,aAAa,SAAS,WAAW,IAAI,MAAM,MAAM,aAC1E,eAAe,uCAAuC,SAAS,KAAK,KAAK,CAAC;IAE5F,CAAC;;;AAKR,QAAO;EAAE,SAAS;EAAgB;EAAO;;;;;;;;;;;;;;;;;;;;AAqB3C,eAAe,qBACb,aACA,OACA,QACA,QACA,KACiB;AAOjB,MAAK,MAAM,SAAS,qBAAqB,OAAO,OAAO,CACrD,QAAO,KAAK;EAAE,GAAG;EAAO,OAAO,MAAM;EAAI,CAAC;AAQ5C,MAAK,MAAM,QAAQ,iBAAiB,MAAM,OAAO,CAC/C,QAAO,KAAK;EACV,UAAU;EACV,OAAO,MAAM;EACb,OAAO;EACP,SAAS,UAAU,KAAK;EACzB,CAAC;CAGJ,MAAM,EAAE,QAAQ,aAAa,iBAAiB,MAAM;AACpD,KAAI,OAAO,WAAW,KAAK,SAAS,WAAW,EAAG,QAAO;CAEzD,MAAM,aAAa,CAAC,MAAM;AAC1B,KAAI,OAAO,eAAe,YAAY;EACpC,MAAM,OAAO,gBAAgB,MAAM,MAAM,MAAM,OAAO;AACtD,MAAI,MAAM;AACR,SAAM,cAAc,KAAK;AAIzB,SAAM,UAAU,KAAK,aAAa,uBAAuB,MAAM,GAAG,OAAO,EAAE,OAAO,kBAAkB;AACpG,UAAO,KAAK;IACV,UAAU;IACV,OAAO,MAAM;IACb,OAAO;IACP,SAAS,uBAAuB,KAAK,MAAM,WAAW,KAAK,KAAK;IACjE,CAAC;AACF,UAAO;;EAKT,MAAM,QAAQ,kBAAkB,MAAM,MAAM,MAAM,OAAO;AACzD,SAAO,KAAK;GACV,UAAU;GACV,OAAO,MAAM;GACb,OAAO;GACP,SAAS,MAAM,SAAS,IACpB,8DAA8D,MAAM,KAAI,MAAK,IAAI,EAAE,GAAG,CAAC,KAAK,KAAK,CAAC,wLAClG;GACL,CAAC;AACF,SAAO;;AAGT,MAAK,MAAM,WAAW,OACpB,QAAO,KAAK;EAAE,UAAU;EAAS,OAAO,MAAM;EAAI,OAAO;EAAe;EAAS,CAAC;AAEpF,MAAK,MAAM,WAAW,SACpB,QAAO,KAAK;EAAE,UAAU;EAAW,OAAO,MAAM;EAAI,OAAO;EAAe;EAAS,CAAC;AAEtF,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;AA0BT,eAAe,sBACb,QACA,aACA,OACA,QACA,QACA,KACiD;AACjD,KAAI,MAAM,KAAM,QAAO;EAAE,OAAO;EAAG,YAAY;EAAO;CAEtD,MAAM,UAAU,qBAAqB,MAAM;CAC3C,MAAM,WAAW,GAAG,OAAO,QAAQ,QAAQ;CAC3C,IAAI;AACJ,KAAI;AACF,UAAQ,MAAM,OAAO,cAAc,QAAQ;SACrC;AACN,SAAO;GAAE,OAAO;GAAG,YAAY;GAAO;;CAGxC,MAAM,SAAS,MAAM,QAAO,MAAK,EAAE,SAAS,QAAQ,IAAI,MAAM,SAAS;AACvE,KAAI,OAAO,WAAW,EAAG,QAAO;EAAE,OAAO;EAAG,YAAY;EAAO;AAE/D,QAAO,KAAK;EACV,UAAU;EACV,OAAO,MAAM;EACb,SACE,0BAA0B,MAAM,GAAG,0FACN,SAAS,gBAAgB,OAAO,KAAK,KAAK,CAAC;EAG3E,CAAC;AAEF,KAAI,CAAC,OAAO,CAAC,YAAa,QAAO;EAAE,OAAO;EAAG,YAAY;EAAM;AAI/D,KAFoB,MAAM,kBAA2C,QAAQ,GAAG,QAAQ,GAAG,WAAW,KAElF,MAAM;AAIxB,MAAI,OAAO,WAAW,EAAG,QAAO;GAAE,OAAO;GAAG,YAAY;GAAM;EAC9D,MAAM,QAAQ,OAAO;EACrB,MAAM,UAAU,MAAM,kBAA2C,QAAQ,GAAG,QAAQ,GAAG,QAAQ;AAC/F,MAAI,YAAY,KAAM,QAAO;GAAE,OAAO;GAAG,YAAY;GAAM;AAC3D,QAAM,UAAU,KAAK,aAAa,SAAS,SAAS,EAAE,QAAQ;AAC9D,QAAM,GAAG,KAAK,aAAa,SAAS,MAAM,EAAE,EAAE,OAAO,MAAM,CAAC;AAC5D,SAAO;GAAE,OAAO;GAAG,YAAY;GAAO;;AAKxC,OAAM,QAAQ,IAAI,OAAO,KAAI,UAAS,GAAG,KAAK,aAAa,SAAS,MAAM,EAAE,EAAE,OAAO,MAAM,CAAC,CAAC,CAAC;AAC9F,QAAO;EAAE,OAAO,OAAO;EAAQ,YAAY;EAAO;;AAGpD,eAAe,uBACb,QACA,aACA,OACA,QACA,QACA,KAC6C;CAC7C,IAAI,iBAAiB;CACrB,IAAI,QAAQ;CACZ,MAAM,QAAQ,oBAAoB,OAAO,OAAO;AAEhD,MAAK,MAAM,UAAU,MAAM,SAAS;EAClC,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,OAAO,aAAa,MAAM;KACnE,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,QAAQ,OAAO,QAAQ,QAAQ,CAAC;AACzH,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;CAClD,MAAM,QAAQ,oBAAoB,OAAO,OAAO;AAEhD,MAAK,MAAM,UAAU,MAAM,SAAS;EAClC,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,OAAO,aAAa,MAAM;KACnE,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,OAAO,aAAa,MAAM;IAC/F,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,OAAO,aAAa,MAAM;IAC/F,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;;;AAIhF,MAAM,gBAAgB,CAAC,cAAc,YAAY;;;;;;;;;;;AAYjD,eAAe,mBACb,QACA,OACA,QACA,MACA,aACA,eACmB;CACnB,MAAM,UAAU,cAAc,QAAO,MAAK,KAAK,eAAe,EAAE,MAAM,UAAU,KAAK,MAAM,QAAQ;AACnG,KAAI,QAAQ,WAAW,EAAG,QAAO,EAAE;CACnC,MAAM,OAAO,MAAM,kBAA6B,QAAQ,aAAa,OAAO,QAAQ,eAAe,KAAK,CAAC;AACzG,KAAI,CAAC,KAAM,QAAO,EAAE;AACpB,QAAO,QAAQ,QAAO,MAAK,KAAK,OAAO,KAAA,EAAU;;AAGnD,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;CAK9D,MAAM,QAAQ,oBAAoB,OAAO,OAAO;CAChD,MAAM,UAAU,MAAM;CAUtB,MAAM,2BAAW,IAAI,KAAqB;CAC1C,MAAM,sBAA+E,EAAE;AACvF,MAAK,MAAM,QAAQ,OAAO;AACxB,MAAI,KAAK,WAAW,IAAI,CAAE;AAC1B,OAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,MAAM,MAAM,kBAAkB,QAAQ,iBAAiB,OAAO,QAAQ,KAAK,CAAC;AAClF,OAAI,CAAC,IAAK;AACV,YAAS,IAAI,GAAG,KAAK,QAAQ,UAAU,IAAI;GAC3C,MAAM,EAAE,gBAAgB,iBAAiB,IAAI;AAC7C,uBAAoB,YAAY,EAAE;AAClC,uBAAoB,QAAQ,QAAQ;;;AAIxC,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,SAAS,IAAI,GAAG,KAAK,QAAQ,SAAS,IAAI;AAEtD,OAAI,CAAC,KAAK;AACR,QAAI,MAAM,MAAM;AACd,YAAO,KAAK;MACV,UAAU;MACV,OAAO,MAAM;MACb;MACA;MACA,SAAS,kCAAkC,KAAK,YAAY,OAAO,cAAc,aAAa,MAAM;MACrG,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,MAAM,cAAc,iBAAiB,IAAI;AAC9D,QAAK,MAAM,SAAS,WAAW,UAAU,EAAE,EAAE;AAC3C,QAAI,MAAM,QAAQ,KAAA,EAAW;AAC7B,WAAO,KAAK;KACV,UAAU;KACV,OAAO,MAAM;KACb;KACA;KACA,OAAO,MAAM;KACb,SAAS,oBAAoB,MAAM,IAAI;KACxC,CAAC;;AAIJ,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;GAQJ,MAAM,SAAS,MAAM,mBAAmB,QAAQ,OAAO,QAAQ,MAAM,aAAa,OAAO,QAAQ,QAAQ;AACzG,OAAI,OAAO,SAAS,GAAG;IACrB,MAAM,OAAO,OAAO,KAAI,MAAK,IAAI,EAAE,GAAG,CAAC,KAAK,KAAK;AACjD,WAAO,KAAK;KACV,UAAU;KACV,OAAO,MAAM;KACb;KACA;KACA,OAAO,OAAO;KACd,SAAS,yCAAyC,KAAK;KACxD,CAAC;AACF,QAAI,OAAO,aAAa;KACtB,MAAM,UAAU,OAAO,YAAY,OAAO,QAAQ,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,SAAS,EAAE,CAAC,CAAC;AACpG,WAAM,UAAU,KAAK,aAAa,SAAS,EAAE,qBAAqB,SAAS,MAAM,UAAU,CAAC;AAC5F;;;AAIJ,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,QACA,KAAA,GAGA;KAAE,YAAY,oBAAoB,WAAW,EAAE;KAAE,gBAAgB;KAAM,CACxE;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;AAIA,gBAAc,MAAM,qBAAqB,aAAa,OAAO,QAAQ,QAAQ,IAAI;EAEjF,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,UAAU,oBAAoB,OAAO,OAAO,CAAC,SAAS;AAC/D,SAAI,CAAC,eAAe,QAAS,gBAAe,0BAAU,IAAI,KAAK;KAC/D,MAAM,OAAO,MAAM,kBAA0C,QAAQ,gBAAgB,OAAO,OAAO,CAAC;AACpG,SAAI,CAAC,KAAM;AAEX,UAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,EAAE;MAC/C,MAAM,OAAO,eAAe,QAAS,IAAI,MAAM;AAC/C,UAAI,KAAM,MAAK,KAAK;OAAE,OAAO,MAAM;OAAI;OAAK,CAAC;UACxC,gBAAe,QAAS,IAAI,OAAO,CAAC;OAAE,OAAO,MAAM;OAAI;OAAK,CAAC,CAAC;;;;AAKzE,QAAK,MAAM,CAAC,QAAQ,aAAa,OAAO,QAAQ,eAAe,CAC7D,MAAK,MAAM,CAAC,OAAO,SAAS,SAE1B,KADqB,IAAI,IAAI,KAAK,KAAI,MAAK,EAAE,MAAM,CAAC,CACnC,OAAO,GAAG;IACzB,MAAM,YAAY,MAAM,SAAS,KAAK,GAAG,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO;AACnE,WAAO,KAAK;KACV,UAAU;KACV;KACA,SAAS,gCAAgC,UAAU,OAAO,KAAK,KAAI,MAAK,GAAG,EAAE,MAAM,GAAG,EAAE,MAAM,CAAC,KAAK,KAAK;KAC1G,CAAC;;;;CAOZ,MAAM,SAAS,OAAO,QAAO,MAAK,EAAE,aAAa,QAAQ,CAAC;CAC1D,MAAM,WAAW,OAAO,QAAO,MAAK,EAAE,aAAa,UAAU,CAAC;CAC9D,MAAM,UAAU,OAAO,QAAO,MAAK,EAAE,aAAa,SAAS,CAAC;AAE5D,QAAO;EACL,OAAO,WAAW;EAClB,SAAS;GACP;GACA;GACA;GACA,gBAAgB;GAChB,iBAAiB;GAClB;EACD;EACA,OAAO;EACR"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/mcp",
3
- "version": "3.9.32",
3
+ "version": "3.10.0",
4
4
  "mcpName": "io.github.Contentrain/contentrain",
5
5
  "license": "MIT",
6
6
  "description": "Local-first MCP server for AI-generated content governance — 27 deterministic tools (22 core + 5 media), stdio + HTTP transports, Local / GitHub / GitLab providers",
@@ -182,7 +182,7 @@
182
182
  "simple-git": "^3.36.0",
183
183
  "typescript": "^5.7.0",
184
184
  "zod": "^3.24.0",
185
- "@contentrain/types": "1.59.0"
185
+ "@contentrain/types": "1.61.0"
186
186
  },
187
187
  "optionalDependencies": {
188
188
  "@astrojs/compiler": "^2.10.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"apply-manager-BenQNb_b.mjs","names":[],"sources":["../src/core/apply-manager.ts"],"sourcesContent":["import type { ModelDefinition, FieldDef, FileFramework } from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText, writeText, pathExists } from '../util/fs.js'\nimport { collectFieldPaths, readModel, writeModel, listModels, validateModelDefinition, inferTitleField } from './model-manager.js'\nimport { writeContent, resolveContentDir, resolveJsonFilePath, resolveMdFilePath, type ContentEntry, type ContentFileModel, type ContentPathModel } from './content-manager.js'\nimport { readConfig } from './config.js'\nimport { writeContext } from './context.js'\nimport { createTransaction, buildBranchName } from '../git/transaction.js'\nimport { checkBranchHealth } from '../git/branch-lifecycle.js'\n\n// ─── Types ───\n\nexport interface ExtractionEntry {\n model: string\n kind: 'singleton' | 'collection' | 'dictionary' | 'document'\n domain: string\n i18n?: boolean\n /** Optional — inferred from `fields` when omitted, and reported in the dry run. */\n title_field?: string\n fields?: Record<string, FieldDef>\n entries: Array<{\n locale?: string\n slug?: string\n data: Record<string, unknown>\n source?: { file: string; line: number; value: string }\n sources?: Array<{ file: string; line: number; key: string; value: string }>\n }>\n}\n\nexport interface ExtractionInput {\n extractions: ExtractionEntry[]\n dry_run?: boolean\n}\n\nexport interface ExtractionPreview {\n models_to_create: string[]\n models_to_update: string[]\n total_entries: number\n content_files: string[]\n /**\n * Resolved `title_field` per model, keyed by model id. Surfaced in the dry run\n * because the extract envelope may omit it — without this the agent would be\n * approving a schema decision it never saw.\n */\n title_fields: Record<string, string>\n}\n\nexport interface ExtractionResult {\n dry_run: boolean\n preview?: ExtractionPreview\n error?: string\n validation_errors?: string[]\n results?: {\n models_created: string[]\n models_updated: string[]\n entries_written: number\n source_map: Array<{ model: string; locale: string; value: string; file: string; line: number }>\n }\n git?: { branch: string; action: string; commit: string; warning?: string }\n context_updated?: boolean\n next_steps: string[]\n}\n\nexport interface PatchEntry {\n file: string\n line: number\n old_value: string\n new_expression: string\n import_statement?: string\n}\n\nexport interface ReuseInput {\n scope: { model?: string; domain?: string }\n patches: PatchEntry[]\n dry_run?: boolean\n}\n\nexport interface SyntaxError {\n file: string\n error: string\n}\n\nexport interface ReuseResult {\n dry_run: boolean\n error?: string\n scope_warnings?: string[]\n preview?: {\n files_to_modify: string[]\n patches_count: number\n imports_to_add: number\n }\n results?: {\n files_modified: string[]\n patches_applied: number\n patches_skipped: Array<{ file: string; line: number; reason: string }>\n imports_added: number\n framework_warnings?: Array<{ file: string; warning: string }>\n syntax_errors?: SyntaxError[]\n }\n git?: { branch: string; action: string; commit: string; warning?: string }\n next_steps: string[]\n}\n\n// ─── Constants ───\n\nconst MAX_PATCHES = 100\n\n/** File extensions allowed for patching — scannable source files only */\nexport const PATCHABLE_EXTENSIONS = new Set([\n '.vue', '.tsx', '.jsx', '.ts', '.js', '.mjs', '.astro', '.svelte',\n])\n\n/** Directories that must never be patched */\nconst FORBIDDEN_PATH_SEGMENTS = new Set([\n '.contentrain', 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt',\n])\n\n// ─── Framework Detection (Guardrail #2) ───\n\nexport type { FileFramework } from '@contentrain/types'\n\nexport function detectFileFramework(filePath: string): FileFramework {\n const ext = extname(filePath).toLowerCase()\n switch (ext) {\n case '.vue': return 'vue'\n case '.svelte': return 'svelte'\n case '.tsx':\n case '.jsx': return 'jsx'\n case '.astro': return 'astro'\n case '.ts':\n case '.js':\n case '.mjs': return 'script'\n default: return 'script'\n }\n}\n\n/**\n * Validate that a replacement expression uses the correct template syntax\n * for the target file's framework. Returns a warning string or null.\n */\nexport function validateFrameworkExpression(\n filePath: string,\n newExpression: string,\n context: 'tag_text' | 'other',\n): string | null {\n if (context !== 'tag_text') return null\n\n const framework = detectFileFramework(filePath)\n\n switch (framework) {\n case 'vue':\n if (!newExpression.includes('{{')) {\n return `Vue file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{{\" — expected Vue template syntax like {{ $t('key') }}`\n }\n break\n case 'jsx':\n if (!newExpression.includes('{')) {\n return `JSX file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected JSX syntax like {t('key')}`\n }\n break\n case 'svelte':\n if (!newExpression.includes('{')) {\n return `Svelte file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected Svelte syntax like {$t('key')}`\n }\n break\n case 'astro':\n if (!newExpression.includes('{')) {\n return `Astro file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected Astro syntax like {t('key')}`\n }\n break\n case 'script':\n // Script files don't have template interpolation — warn if attempting tag text replacement\n return `Script file \"${filePath}\": tag text replacement not applicable for .ts/.js files`\n }\n\n return null\n}\n\n// ─── Scope Validation (Guardrail #1) ───\n\n/**\n * Validate that a patch file path is safe and within allowed scope.\n * Returns an error message or null if valid.\n */\nexport function validatePatchPath(filePath: string): string | null {\n const normalizedPath = filePath.replace(/\\\\/g, '/')\n\n // Reject path traversal\n if (normalizedPath.includes('..')) {\n return `Path traversal detected: \"${filePath}\"`\n }\n\n // Reject absolute paths\n if (normalizedPath.startsWith('/')) {\n return `Absolute path not allowed: \"${filePath}\"`\n }\n\n // Reject forbidden directories\n const segments = normalizedPath.split('/')\n for (const seg of segments) {\n if (FORBIDDEN_PATH_SEGMENTS.has(seg)) {\n return `Patching files inside \"${seg}/\" is not allowed: \"${filePath}\"`\n }\n }\n\n // Reject non-scannable extensions\n const ext = extname(filePath).toLowerCase()\n if (!PATCHABLE_EXTENSIONS.has(ext)) {\n return `File extension \"${ext}\" is not patchable. Allowed: ${[...PATCHABLE_EXTENSIONS].join(', ')}. Path: \"${filePath}\"`\n }\n\n return null\n}\n\n// ─── Syntax Check (Guardrail #5) ───\n\n/**\n * Perform a basic syntax check on a patched file.\n * Returns an error message or null if syntax appears valid.\n */\nexport function checkSyntax(filePath: string, content: string): string | null {\n const ext = extname(filePath).toLowerCase()\n\n switch (ext) {\n case '.ts':\n case '.tsx':\n case '.js':\n case '.jsx':\n case '.mjs':\n return checkJsSyntax(content)\n case '.vue':\n return checkVueSyntax(content)\n case '.svelte':\n case '.astro':\n return checkTagBalance(content)\n default:\n return null\n }\n}\n\n/**\n * Basic JS/TS syntax check: bracket/paren/brace balance + string literal closure.\n * This is intentionally conservative — it catches obvious breakage without\n * requiring a full parser.\n */\nfunction checkJsSyntax(content: string): string | null {\n const stack: string[] = []\n const pairs: Record<string, string> = { ')': '(', ']': '[', '}': '{' }\n let inString: string | null = null\n let escaped = false\n let inLineComment = false\n let inBlockComment = false\n\n for (let i = 0; i < content.length; i++) {\n const ch = content[i]!\n const next = content[i + 1]\n\n // Handle escape sequences inside strings\n if (escaped) {\n escaped = false\n continue\n }\n\n if (ch === '\\\\' && inString !== null) {\n escaped = true\n continue\n }\n\n // Line comment\n if (!inString && !inBlockComment && ch === '/' && next === '/') {\n inLineComment = true\n continue\n }\n if (inLineComment) {\n if (ch === '\\n') inLineComment = false\n continue\n }\n\n // Block comment\n if (!inString && !inBlockComment && ch === '/' && next === '*') {\n inBlockComment = true\n i++ // skip *\n continue\n }\n if (inBlockComment) {\n if (ch === '*' && next === '/') {\n inBlockComment = false\n i++ // skip /\n }\n continue\n }\n\n // String handling\n if (inString !== null) {\n if (ch === inString) {\n // Template literal allows multi-line, others don't\n inString = null\n } else if (inString !== '`' && ch === '\\n') {\n return `Unterminated string literal near offset ${i}`\n }\n continue\n }\n\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch\n continue\n }\n\n // Bracket matching\n if (ch === '(' || ch === '[' || ch === '{') {\n stack.push(ch)\n } else if (ch === ')' || ch === ']' || ch === '}') {\n const expected = pairs[ch]!\n if (stack.length === 0) {\n return `Unmatched closing \"${ch}\" near offset ${i}`\n }\n const top = stack.pop()!\n if (top !== expected) {\n return `Mismatched bracket: expected closing for \"${top}\" but found \"${ch}\" near offset ${i}`\n }\n }\n }\n\n if (inString !== null) {\n return `Unterminated string literal (opened with ${inString})`\n }\n\n if (stack.length > 0) {\n return `Unclosed bracket \"${stack[stack.length - 1]}\" — ${stack.length} unclosed bracket(s)`\n }\n\n return null\n}\n\n/**\n * Vue SFC syntax check: ensure <template>, <script>, <style> tags are balanced.\n */\nfunction checkVueSyntax(content: string): string | null {\n const tagBalance = checkTagBalance(content)\n if (tagBalance) return tagBalance\n\n // Vue-specific: check that SFC root tags are present and balanced\n for (const tag of ['template', 'script']) {\n const openRe = new RegExp(`<${tag}[\\\\s>]`, 'g')\n const closeRe = new RegExp(`</${tag}>`, 'g')\n const opens = content.match(openRe)?.length ?? 0\n const closes = content.match(closeRe)?.length ?? 0\n if (opens !== closes) {\n return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`\n }\n }\n\n return null\n}\n\n/**\n * Basic tag balance check for HTML-like files.\n * Checks that self-closing tags are handled and major structural tags are balanced.\n */\nfunction checkTagBalance(content: string): string | null {\n // Check for common structural tags balance\n const structuralTags = ['div', 'section', 'main', 'header', 'footer', 'nav', 'article', 'aside', 'ul', 'ol', 'table']\n\n for (const tag of structuralTags) {\n const openRe = new RegExp(`<${tag}[\\\\s>]`, 'g')\n const closeRe = new RegExp(`</${tag}>`, 'g')\n const opens = content.match(openRe)?.length ?? 0\n const closes = content.match(closeRe)?.length ?? 0\n if (opens !== closes) {\n return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`\n }\n }\n\n return null\n}\n\n// ─── Extract Mode ───\n\nexport async function applyExtract(\n projectRoot: string,\n input: ExtractionInput,\n): Promise<ExtractionResult> {\n const config = await readConfig(projectRoot)\n if (!config) throw new Error('Project not initialized. Run contentrain_init first.')\n\n const { extractions, dry_run } = input\n\n // Analyze what will happen\n const existingModels = await listModels(projectRoot)\n const existingIds = new Set(existingModels.map(m => m.id))\n\n const modelsToCreate: string[] = []\n const modelsToUpdate: string[] = []\n const contentFiles: string[] = []\n let totalEntries = 0\n\n const validationErrors: string[] = []\n const titleFields: Record<string, string> = {}\n\n for (const ext of extractions) {\n // `title_field` is optional on the extraction envelope for the same reason\n // `name` is absent from it: the agent describes content, and the derived\n // schema properties are resolved here and shown in the dry run.\n const titlePick = ext.title_field\n ? { field: ext.title_field, rule: 'explicit' as const }\n : inferTitleField(ext.kind, ext.fields as Record<string, unknown> | undefined)\n if (titlePick) titleFields[ext.model] = titlePick.field\n\n const existing = existingIds.has(ext.model) ? await readModel(projectRoot, ext.model) : null\n\n // Validate model definition with same rules as model_save — including the\n // grandfathering of field names the existing model already carries.\n const modelErrors = validateModelDefinition({\n id: ext.model,\n kind: ext.kind,\n title_field: titlePick?.field,\n fields: ext.fields as Record<string, unknown> | undefined,\n }, { existingFieldPaths: collectFieldPaths(existing?.fields) })\n if (modelErrors.errors.length > 0) {\n validationErrors.push(...modelErrors.errors.map(e => `[${ext.model}] ${e}`))\n }\n\n // A model that predates the requirement would be rewritten still-invalid by\n // the merge branch below. Refuse rather than backfill: `contentrain_validate\n // fix:true` is the one migration path, and normalize should not quietly\n // rewrite a schema it did not author.\n if (existing && !existing.title_field) {\n validationErrors.push(\n `[${ext.model}] Model is missing \"title_field\". Run contentrain_validate with fix:true before extracting into it.`,\n )\n }\n\n for (const entry of ext.entries) {\n if (ext.kind === 'dictionary') {\n if (entry.data['id'] !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Dictionary entries should not have id or slug`)\n }\n for (const [key, val] of Object.entries(entry.data)) {\n if (typeof val !== 'string') {\n validationErrors.push(`[${ext.model}] Dictionary entry value for key \"${key}\" must be a string, got ${typeof val}`)\n }\n }\n } else if (ext.kind === 'document') {\n if (!entry.slug && !entry.data['slug']) {\n validationErrors.push(`[${ext.model}] Document entries must have a slug`)\n }\n } else if (ext.kind === 'collection') {\n if (entry.slug !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Collection entries should not have slug`)\n }\n } else if (ext.kind === 'singleton') {\n if (entry.data['id'] !== undefined || entry.slug !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Singleton entries should not have id or slug`)\n }\n }\n }\n\n if (existingIds.has(ext.model)) {\n modelsToUpdate.push(ext.model)\n } else {\n modelsToCreate.push(ext.model)\n }\n totalEntries += ext.entries.length\n\n // Guardrail #3: Preview-Execute Parity — use real model metadata if it exists.\n // Typed as the path fields these resolvers actually read rather than a whole\n // ModelDefinition: the fallback genuinely has no name, fields or title_field,\n // and an `as ModelDefinition` cast here would assert otherwise — hiding a\n // missing required property from the compiler instead of reporting it.\n let previewModel: ContentPathModel & ContentFileModel\n if (existingIds.has(ext.model)) {\n const real = await readModel(projectRoot, ext.model)\n previewModel = real ?? { id: ext.model, domain: ext.domain, i18n: ext.i18n ?? true }\n } else {\n previewModel = { id: ext.model, domain: ext.domain, i18n: ext.i18n ?? true }\n }\n\n const cDir = resolveContentDir(projectRoot, previewModel)\n for (const entry of ext.entries) {\n const locale = entry.locale ?? config.locales.default\n if (ext.kind === 'document' && entry.slug) {\n contentFiles.push(resolveMdFilePath(cDir, previewModel, locale, entry.slug))\n } else {\n contentFiles.push(resolveJsonFilePath(cDir, previewModel, locale))\n }\n }\n }\n\n const preview: ExtractionPreview = {\n models_to_create: modelsToCreate,\n models_to_update: modelsToUpdate,\n total_entries: totalEntries,\n content_files: [...new Set(contentFiles)],\n title_fields: titleFields,\n }\n\n // Dry run — return preview only (include validation errors if any)\n if (dry_run !== false) {\n return {\n dry_run: true,\n preview,\n ...(validationErrors.length > 0 ? { validation_errors: validationErrors } : {}),\n next_steps: [\n ...(validationErrors.length > 0\n ? [`WARNING: ${validationErrors.length} validation error(s) found — fix before executing`]\n : []),\n 'Review the preview above',\n 'Call contentrain_apply with mode:extract and dry_run:false to execute',\n ],\n }\n }\n\n // Block execute if validation errors exist\n if (validationErrors.length > 0) {\n return {\n dry_run: false,\n error: 'Model validation failed — cannot execute extract with invalid model definitions',\n validation_errors: validationErrors,\n next_steps: ['Fix the validation errors and retry'],\n }\n }\n\n // Branch health gate\n const health = await checkBranchHealth(projectRoot)\n if (health.blocked) {\n return {\n error: health.message,\n action: 'blocked' as const,\n hint: 'Merge or delete old contentrain/* branches before executing normalize.',\n } as unknown as ExtractionResult\n }\n\n // Execute — git transaction (always review mode for normalize)\n const branchName = buildBranchName('normalize', 'extract')\n const tx = await createTransaction(projectRoot, branchName, { workflowOverride: 'review' })\n const sourceMap: Array<{ model: string; locale: string; value: string; file: string; line: number }> = []\n const modelsCreated: string[] = []\n const modelsUpdated: string[] = []\n let entriesWritten = 0\n\n try {\n await tx.write(async (wt) => {\n for (const ext of extractions) {\n // Create or merge model\n const existing = await readModel(wt, ext.model)\n if (existing) {\n // Merge fields: add new, keep existing\n if (ext.fields) {\n const merged = { ...existing.fields, ...ext.fields }\n // Only overwrite if new fields were actually added\n const newFieldNames = Object.keys(ext.fields).filter(k => !(k in (existing.fields ?? {})))\n if (newFieldNames.length > 0) {\n existing.fields = merged\n await writeModel(wt, existing)\n modelsUpdated.push(ext.model)\n }\n }\n } else {\n // Create new model. Like `name`, `title_field` is derived rather than\n // demanded of the extraction envelope — the dry run resolves and reports\n // it, so by the time we get here the choice has already been shown.\n const titlePick = ext.title_field\n ? { field: ext.title_field }\n : inferTitleField(ext.kind, ext.fields as Record<string, unknown> | undefined)\n if (!titlePick) {\n throw new Error(\n `Cannot create model \"${ext.model}\": no extracted field can serve as title_field. `\n + 'Add a string or text field to the extraction, or create the model with contentrain_model_save first.',\n )\n }\n const newModel: ModelDefinition = {\n id: ext.model,\n name: ext.model.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),\n kind: ext.kind,\n domain: ext.domain,\n i18n: ext.i18n ?? true,\n title_field: titlePick.field,\n fields: ext.fields,\n }\n await writeModel(wt, newModel)\n modelsCreated.push(ext.model)\n }\n\n // Write content entries\n const model = (await readModel(wt, ext.model))!\n const entries: ContentEntry[] = ext.entries.map(e => ({\n locale: e.locale,\n slug: e.slug,\n data: e.data,\n }))\n\n const wtConfig = await readConfig(wt) ?? config\n await writeContent(wt, model, entries, wtConfig)\n entriesWritten += entries.length\n\n // Track source map\n for (const entry of ext.entries) {\n // For dictionary entries with per-key source tracking\n if (ext.kind === 'dictionary' && entry.sources) {\n for (const s of entry.sources) {\n sourceMap.push({\n model: ext.model,\n locale: entry.locale ?? config.locales.default,\n value: s.value,\n file: s.file,\n line: s.line,\n })\n }\n } else if (entry.source) {\n sourceMap.push({\n model: ext.model,\n locale: entry.locale ?? config.locales.default,\n value: entry.source.value,\n file: entry.source.file,\n line: entry.source.line,\n })\n }\n }\n }\n\n // Write source map for reuse scope enforcement\n if (sourceMap.length > 0) {\n const sourcesByModel: Record<string, { source_files: string[]; entry_count: number }> = {}\n for (const s of sourceMap) {\n if (!sourcesByModel[s.model]) {\n sourcesByModel[s.model] = { source_files: [], entry_count: 0 }\n }\n const modelEntry = sourcesByModel[s.model]!\n if (!modelEntry.source_files.includes(s.file)) {\n modelEntry.source_files.push(s.file)\n }\n modelEntry.entry_count++\n }\n const sourcesJson = JSON.stringify({\n version: 1,\n created_at: new Date().toISOString(),\n models: sourcesByModel,\n }, null, 2) + '\\n'\n // Write to worktree only — merge brings it to main\n // Phase 2 (reuse) must wait for extract branch to be merged first\n await writeText(join(wt, '.contentrain', 'normalize-sources.json'), sourcesJson)\n }\n\n // Update context\n await writeContext(wt, {\n tool: 'contentrain_apply',\n model: extractions.map(e => e.model).join(','),\n locale: config.locales.default,\n entries: extractions.flatMap(e => e.entries.map(en => en.slug ?? 'entry')),\n })\n })\n\n const commitMsg = `[contentrain] normalize: extract ${entriesWritten} entries to ${extractions.length} models`\n await tx.commit(commitMsg)\n\n const gitResult: { branch: string; action: string; commit: string; warning?: string } = {\n branch: branchName,\n action: 'pending-review',\n commit: '',\n }\n try {\n const completed = await tx.complete()\n gitResult.action = completed.action\n gitResult.commit = completed.commit\n if (completed.warning !== undefined) gitResult.warning = completed.warning\n } catch (error) {\n // Never fall through to a value that mimics success. The commit landed\n // but publishing it did not, and 'pending-review' with an empty commit\n // hash is indistinguishable from a healthy review branch.\n gitResult.action = 'incomplete'\n gitResult.warning = `Content was committed to \"${branchName}\" but the transaction could not be completed: `\n + `${error instanceof Error ? error.message : String(error)}. `\n + `The branch is preserved — inspect it with contentrain_branch_list before retrying.`\n } finally {\n await tx.cleanup()\n }\n\n return {\n dry_run: false,\n results: {\n models_created: modelsCreated,\n models_updated: modelsUpdated,\n entries_written: entriesWritten,\n source_map: sourceMap,\n },\n git: gitResult,\n context_updated: true,\n next_steps: [\n 'Run contentrain_validate to check the extracted content',\n 'Run contentrain_submit to push the branch for review',\n 'For browser-based review: ensure `contentrain serve` is running, direct user to http://localhost:3333/normalize',\n 'For terminal workflow: use contentrain_merge to merge the branch locally',\n 'After merge, run `npx contentrain generate` to update SDK client',\n 'After review, proceed with mode:reuse to patch source files',\n ],\n }\n } catch (error) {\n await tx.cleanup()\n throw error\n }\n}\n\n// ─── Reuse Mode ───\n\nexport async function applyReuse(\n projectRoot: string,\n input: ReuseInput,\n): Promise<ReuseResult> {\n const config = await readConfig(projectRoot)\n if (!config) throw new Error('Project not initialized. Run contentrain_init first.')\n\n const { scope, patches, dry_run } = input\n\n // Validate scope\n if (!scope.model && !scope.domain) {\n throw new Error('Scope required: provide model or domain. Whole-project patching is not allowed.')\n }\n\n // Validate patch count\n if (patches.length > MAX_PATCHES) {\n throw new Error(`Too many patches (${patches.length}). Maximum ${MAX_PATCHES} per operation. Split into multiple calls.`)\n }\n\n // Check content exists for scope (soft warning)\n if (scope.model) {\n const model = await readModel(projectRoot, scope.model)\n if (!model) {\n throw new Error(`Model \"${scope.model}\" not found. Run extract phase first.`)\n }\n }\n\n const scopeWarnings: string[] = []\n\n // Guardrail #1: Scope Real Enforcement\n // Step 1: Path safety — every patch must target a valid, patchable source file\n for (const patch of patches) {\n const pathError = validatePatchPath(patch.file)\n if (pathError) {\n throw new Error(`Invalid patch path: ${pathError}`)\n }\n }\n\n // Step 2: Semantic scope — verify scope model/domain exists and cross-check patch files\n if (scope.model || scope.domain) {\n const models = await listModels(projectRoot)\n const scopeModels = scope.model\n ? models.filter(m => m.id === scope.model)\n : scope.domain\n ? models.filter(m => m.domain === scope.domain)\n : models\n\n if (scopeModels.length === 0) {\n throw new Error(`No models found for scope ${scope.model ? `model=\"${scope.model}\"` : `domain=\"${scope.domain}\"`}`)\n }\n\n // Step 3: Verify patch files are source files (not content/config/meta files)\n // and belong to detectable source directories (not random locations)\n const { autoDetectSourceDirs } = await import('./scan-config.js')\n const sourceDirs = await autoDetectSourceDirs(projectRoot)\n\n // Build allowed file prefixes from source dirs\n const allowedPrefixes = sourceDirs.map(d => d === '.' ? '' : d + '/')\n\n for (const patch of patches) {\n const normalizedPath = patch.file.replace(/\\\\/g, '/')\n\n // Reject patches targeting .contentrain/ directory (content files should never be patched by reuse)\n if (normalizedPath.startsWith('.contentrain/') || normalizedPath.includes('/.contentrain/')) {\n throw new Error(`Cannot patch content/config files directly: \"${patch.file}\". Reuse patches source files only.`)\n }\n\n // If source dirs were detected (not just \".\"), verify patch files are within them\n if (sourceDirs.length > 0 && sourceDirs[0] !== '.') {\n const inSourceDir = allowedPrefixes.some(prefix =>\n prefix === '' || normalizedPath.startsWith(prefix),\n )\n if (!inSourceDir) {\n throw new Error(\n `Patch file \"${patch.file}\" is outside detected source directories (${sourceDirs.join(', ')}). ` +\n `Reuse patches must target source files within the project's source tree.`,\n )\n }\n }\n }\n\n // Step 4: Semantic source→model cross-check via normalize-sources.json\n // Source map is written by extract into the review branch worktree.\n // It only exists on base after extract branch is merged.\n // If missing: dry_run proceeds with warning, execute is blocked.\n if (scope.model || scope.domain) {\n const sourcesPath = join(projectRoot, '.contentrain', 'normalize-sources.json')\n const sourcesRaw = await readText(sourcesPath)\n\n if (!sourcesRaw) {\n // Source map not found — check if scoped model is a dictionary\n // Dictionaries store all keys in one entry, so per-file source tracking\n // is not available. Allow reuse with a warning instead of blocking.\n const scopedModel = scopeModels.find(m => m.id === scope.model)\n const isDictionary = scopedModel?.kind === 'dictionary'\n\n if (isDictionary) {\n scopeWarnings.push(\n 'normalize-sources.json not found. Dictionary models do not generate per-file source maps. ' +\n 'Scope enforcement is based on source-tree locality only.',\n )\n } else if (dry_run !== false) {\n scopeWarnings.push(\n 'normalize-sources.json not found. Semantic scope enforcement is unavailable. ' +\n 'Merge the extract branch first, then reuse will have full scope protection.',\n )\n } else {\n throw new Error(\n 'Cannot execute reuse: normalize-sources.json not found on base branch. ' +\n 'The extract branch must be merged before reuse can execute. ' +\n 'This ensures semantic scope enforcement protects against out-of-scope patching.',\n )\n }\n } else {\n const sourcesData = JSON.parse(sourcesRaw) as {\n models?: Record<string, { source_files?: string[] }>\n }\n\n // Collect all allowed source files for the scope\n let allowedSourceFiles: string[] = []\n let scopeLabel = ''\n\n if (scope.model) {\n const modelSources = sourcesData.models?.[scope.model]?.source_files\n if (modelSources) allowedSourceFiles = modelSources\n scopeLabel = `model \"${scope.model}\"`\n } else if (scope.domain) {\n const domainModelIds = scopeModels.map(m => m.id)\n for (const modelId of domainModelIds) {\n const modelSources = sourcesData.models?.[modelId]?.source_files\n if (modelSources) allowedSourceFiles.push(...modelSources)\n }\n allowedSourceFiles = [...new Set(allowedSourceFiles)]\n scopeLabel = `domain \"${scope.domain}\"`\n }\n\n if (allowedSourceFiles.length > 0) {\n const outOfScopePatches: string[] = []\n for (const patch of patches) {\n const normalizedPath = patch.file.replace(/\\\\/g, '/')\n if (!allowedSourceFiles.includes(normalizedPath)) {\n outOfScopePatches.push(patch.file)\n }\n }\n if (outOfScopePatches.length > 0) {\n throw new Error(\n `Scope enforcement: ${outOfScopePatches.length} patch file(s) are not associated with ${scopeLabel}. ` +\n `Out-of-scope files: ${outOfScopePatches.join(', ')}. ` +\n `Known source files: ${allowedSourceFiles.join(', ')}.`,\n )\n }\n }\n }\n }\n }\n\n // Group patches by file\n const patchesByFile = new Map<string, PatchEntry[]>()\n for (const patch of patches) {\n if (!patchesByFile.has(patch.file)) {\n patchesByFile.set(patch.file, [])\n }\n patchesByFile.get(patch.file)!.push(patch)\n }\n\n const filesToModify = [...patchesByFile.keys()]\n const importsToAdd = patches.filter(p => p.import_statement).length\n\n // Dry run — return preview only\n if (dry_run !== false) {\n return {\n dry_run: true,\n preview: {\n files_to_modify: filesToModify,\n patches_count: patches.length,\n imports_to_add: importsToAdd,\n },\n ...(scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}),\n next_steps: [\n ...(scopeWarnings.length > 0 ? [`WARNING: ${scopeWarnings.length} patch file(s) not in extract source map — verify intent`] : []),\n 'Review the files and patches above',\n 'Call contentrain_apply with mode:reuse and dry_run:false to execute',\n ],\n }\n }\n\n // Branch health gate\n const reuseHealth = await checkBranchHealth(projectRoot)\n if (reuseHealth.blocked) {\n return {\n dry_run: false,\n error: `Branch blocked: ${reuseHealth.message}`,\n next_steps: ['Merge or delete old contentrain/* branches before executing reuse.'],\n }\n }\n\n // Execute — git transaction\n const scopeTarget = scope.model ?? scope.domain!\n const branchName = buildBranchName('normalize/reuse', scopeTarget)\n const tx = await createTransaction(projectRoot, branchName, { workflowOverride: 'review' })\n\n const filesModified: string[] = []\n let patchesApplied = 0\n let importsAdded = 0\n const patchesSkipped: Array<{ file: string; line: number; reason: string }> = []\n const frameworkWarnings: Array<{ file: string; warning: string }> = []\n const syntaxErrors: SyntaxError[] = []\n\n try {\n await tx.write(async (wt) => {\n for (const [relFile, filePatches] of patchesByFile) {\n const absPath = join(wt, relFile)\n\n if (!(await pathExists(absPath))) {\n for (const p of filePatches) {\n patchesSkipped.push({ file: relFile, line: p.line, reason: 'file not found' })\n }\n continue\n }\n\n const content = await readText(absPath)\n if (content === null) {\n for (const p of filePatches) {\n patchesSkipped.push({ file: relFile, line: p.line, reason: 'file unreadable' })\n }\n continue\n }\n\n // Sort patches by line DESC (bottom-up to avoid line shifts)\n const sorted = [...filePatches].toSorted((a, b) => b.line - a.line)\n\n const lines = content.split('\\n')\n let fileModified = false\n\n for (const patch of sorted) {\n // Determine replacement context before applying\n const targetLine = lines[patch.line - 1] ?? ''\n const isTagTextContext = targetLine.includes(`>${patch.old_value}<`)\n const replacementContext = isTagTextContext ? 'tag_text' as const : 'other' as const\n\n // Guardrail #2: Framework-aware validation — only warn for tag text replacements\n if (isTagTextContext) {\n const fwWarning = validateFrameworkExpression(relFile, patch.new_expression, replacementContext)\n if (fwWarning) {\n frameworkWarnings.push({ file: relFile, warning: fwWarning })\n }\n }\n\n const applied = applyPatchToLines(lines, patch)\n if (applied) {\n patchesApplied++\n fileModified = true\n } else {\n patchesSkipped.push({ file: relFile, line: patch.line, reason: 'old_value not found at or near specified line' })\n }\n }\n\n // Add imports (deduplicate)\n const importStatements = new Set(\n filePatches\n .filter(p => p.import_statement)\n .map(p => p.import_statement!),\n )\n\n if (importStatements.size > 0) {\n const added = addImportsToLines(lines, importStatements)\n importsAdded += added\n if (added > 0) fileModified = true\n }\n\n if (fileModified) {\n const newContent = lines.join('\\n')\n await writeText(absPath, newContent)\n filesModified.push(relFile)\n\n // Guardrail #5: Syntax check after patching\n const syntaxError = checkSyntax(relFile, newContent)\n if (syntaxError) {\n syntaxErrors.push({ file: relFile, error: syntaxError })\n }\n }\n }\n\n // Update context\n await writeContext(wt, {\n tool: 'contentrain_apply',\n model: scopeTarget,\n locale: config.locales.default,\n })\n })\n\n if (filesModified.length === 0) {\n await tx.cleanup()\n return {\n dry_run: false,\n results: {\n files_modified: [],\n patches_applied: 0,\n patches_skipped: patchesSkipped,\n imports_added: 0,\n framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : undefined,\n },\n next_steps: ['No files were modified. Check patch definitions and try again.'],\n }\n }\n\n const commitMsg = `[contentrain] normalize: reuse ${scopeTarget} — patch ${filesModified.length} files (${patchesApplied} replacements)`\n await tx.commit(commitMsg)\n\n const gitResult: { branch: string; action: string; commit: string; warning?: string } = {\n branch: branchName,\n action: 'pending-review',\n commit: '',\n }\n try {\n const completed = await tx.complete()\n gitResult.action = completed.action\n gitResult.commit = completed.commit\n if (completed.warning !== undefined) gitResult.warning = completed.warning\n } catch (error) {\n // Never fall through to a value that mimics success. The commit landed\n // but publishing it did not, and 'pending-review' with an empty commit\n // hash is indistinguishable from a healthy review branch.\n gitResult.action = 'incomplete'\n gitResult.warning = `Content was committed to \"${branchName}\" but the transaction could not be completed: `\n + `${error instanceof Error ? error.message : String(error)}. `\n + `The branch is preserved — inspect it with contentrain_branch_list before retrying.`\n } finally {\n await tx.cleanup()\n }\n\n return {\n dry_run: false,\n results: {\n files_modified: filesModified,\n patches_applied: patchesApplied,\n patches_skipped: patchesSkipped,\n imports_added: importsAdded,\n framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : undefined,\n syntax_errors: syntaxErrors.length > 0 ? syntaxErrors : undefined,\n },\n ...(scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}),\n git: gitResult,\n next_steps: [\n 'Run contentrain_validate to verify the patched files',\n patchesSkipped.length > 0 ? `${patchesSkipped.length} patches were skipped — review and retry if needed` : '',\n syntaxErrors.length > 0 ? `WARNING: ${syntaxErrors.length} file(s) may have syntax errors after patching — review manually` : '',\n scopeWarnings.length > 0 ? `NOTE: ${scopeWarnings.length} patch file(s) not in extract source map` : '',\n 'Run contentrain_submit to push the branch for review',\n 'For review: direct user to http://localhost:3333/branches or use contentrain_merge',\n 'After all reuse phases complete, run `npx contentrain generate` to update SDK types',\n ].filter(Boolean),\n }\n } catch (error) {\n await tx.cleanup()\n throw error\n }\n}\n\n// ─── Patch Helpers ───\n\n/**\n * Apply a single patch to a lines array. Mutates lines in place.\n * Uses line hint for proximity matching — searches ±10 lines from hint.\n */\nfunction applyPatchToLines(lines: string[], patch: PatchEntry): boolean {\n const { line, old_value, new_expression } = patch\n const lineIdx = line - 1 // 0-based\n\n // Search range: ±10 lines from hint\n const searchStart = Math.max(0, lineIdx - 10)\n const searchEnd = Math.min(lines.length, lineIdx + 11)\n\n // First pass: exact line match\n if (lineIdx >= 0 && lineIdx < lines.length) {\n const replaced = replaceInLine(lines[lineIdx]!, old_value, new_expression)\n if (replaced !== null) {\n lines[lineIdx] = replaced\n return true\n }\n }\n\n // Second pass: proximity search\n for (let i = searchStart; i < searchEnd; i++) {\n if (i === lineIdx) continue // already tried\n const replaced = replaceInLine(lines[i]!, old_value, new_expression)\n if (replaced !== null) {\n lines[i] = replaced\n return true\n }\n }\n\n return false\n}\n\n/**\n * Replace old_value with new_expression in a single line.\n * Matches the string literal (quoted or unquoted tag text).\n * Returns the modified line, or null if not found.\n *\n * Guardrail #4: Safer patch matching — word boundary awareness and\n * ambiguity rejection for plain text fallback.\n */\nexport function replaceInLine(line: string, oldValue: string, newExpression: string): string | null {\n // Try exact match of the value in quoted strings\n // Match: \"old_value\", 'old_value', `old_value`\n for (const quote of ['\"', \"'\", '`']) {\n const quoted = `${quote}${oldValue}${quote}`\n if (line.includes(quoted)) {\n return line.replace(quoted, newExpression)\n }\n }\n\n // Try unquoted tag text match: >old_value<\n // The agent provides the complete new_expression with correct framework syntax:\n // JSX: {t('key')}\n // Vue: {{ $t('key') }}\n // Svelte: {$t('key')}\n // So we insert the expression as-is between > and <, without wrapping in braces.\n if (line.includes(`>${oldValue}<`)) {\n return line.replace(`>${oldValue}<`, `>${newExpression}<`)\n }\n\n // Guardrail #4: Safer plain text fallback\n // Only match if old_value appears at a word boundary and is unambiguous\n if (line.includes(oldValue)) {\n // Count occurrences — if multiple, it's ambiguous\n const occurrences = countOccurrences(line, oldValue)\n if (occurrences > 1) {\n return null // ambiguous — let proximity search handle it\n }\n\n // Word boundary check: don't replace \"Submit\" inside \"SubmitButton\"\n // Reject if the old_value is a substring of a larger word — check if\n // adjacent characters are word characters that extend the token.\n const idx = line.indexOf(oldValue)\n const charBefore = idx > 0 ? line[idx - 1]! : ''\n const charAfter = idx + oldValue.length < line.length ? line[idx + oldValue.length]! : ''\n\n const oldStartsWithWord = oldValue.length > 0 && isWordChar(oldValue[0]!)\n const oldEndsWithWord = oldValue.length > 0 && isWordChar(oldValue[oldValue.length - 1]!)\n\n // If old_value starts with a word char and char before is also word char, it's a partial match\n if (oldStartsWithWord && isWordChar(charBefore)) {\n return null\n }\n // If old_value ends with a word char and char after is also word char, it's a partial match\n if (oldEndsWithWord && isWordChar(charAfter)) {\n return null\n }\n\n return line.replace(oldValue, newExpression)\n }\n\n return null\n}\n\n/** Count non-overlapping occurrences of a substring */\nfunction countOccurrences(str: string, sub: string): number {\n let count = 0\n let pos = 0\n while (pos <= str.length - sub.length) {\n const idx = str.indexOf(sub, pos)\n if (idx === -1) break\n count++\n pos = idx + sub.length\n }\n return count\n}\n\n/** Check if a character is a word character (letter, digit, underscore) */\nfunction isWordChar(ch: string): boolean {\n return /\\w/.test(ch)\n}\n\n/**\n * Add import statements to the top of a file (after existing imports).\n * Deduplicates — won't add if the import already exists.\n * Returns number of imports actually added.\n */\nfunction addImportsToLines(lines: string[], imports: Set<string>): number {\n let added = 0\n const existingContent = lines.join('\\n')\n\n // Find the last import line position (handles multi-line imports)\n let lastImportIdx = -1\n let inMultiLineImport = false\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim()\n if (inMultiLineImport) {\n lastImportIdx = i\n if (trimmed.includes('}')) inMultiLineImport = false\n continue\n }\n if (trimmed.startsWith('import ') || trimmed.startsWith('import{')) {\n lastImportIdx = i\n // Check if this is a multi-line import (has { but no closing })\n if (trimmed.includes('{') && !trimmed.includes('}')) {\n inMultiLineImport = true\n }\n continue\n }\n // Stop searching after a non-import, non-empty, non-comment line following imports\n if (lastImportIdx >= 0 && trimmed.length > 0 && !trimmed.startsWith('//') && !trimmed.startsWith('/*') && !trimmed.startsWith('*')) {\n break\n }\n }\n\n let insertAt: number\n if (lastImportIdx >= 0) {\n insertAt = lastImportIdx + 1\n } else {\n // No existing imports found — insert after shebang and directive lines\n insertAt = 0\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim()\n if (i === 0 && trimmed.startsWith('#!')) {\n insertAt = i + 1\n continue\n }\n if (trimmed === \"'use client'\" || trimmed === '\"use client\"'\n || trimmed === \"'use server'\" || trimmed === '\"use server\"'\n || trimmed === \"'use client';\" || trimmed === '\"use client\";'\n || trimmed === \"'use server';\" || trimmed === '\"use server\";') {\n insertAt = i + 1\n continue\n }\n if (insertAt > 0 && trimmed.length > 0) break\n if (insertAt === 0 && trimmed.length > 0) break\n }\n }\n const toInsert: string[] = []\n\n for (const imp of imports) {\n // Check if this import already exists (by checking the from clause)\n if (!existingContent.includes(imp)) {\n toInsert.push(imp)\n added++\n }\n }\n\n if (toInsert.length > 0) {\n lines.splice(insertAt, 0, ...toInsert)\n }\n\n return added\n}\n"],"mappings":"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"}