@contentrain/mcp 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{apply-manager-B7BrL-ZW.mjs → apply-manager-SLCRLHN_.mjs} +1 -1
- package/dist/{apply-manager-B7BrL-ZW.mjs.map → apply-manager-SLCRLHN_.mjs.map} +1 -1
- package/dist/{astro-parser-BVoGjA6M.mjs → astro-parser-LNjEspnf.mjs} +3 -3
- package/dist/{astro-parser-BVoGjA6M.mjs.map → astro-parser-LNjEspnf.mjs.map} +1 -1
- package/dist/{branch-lifecycle-AwBJIheA.d.mts → branch-lifecycle-D4W0WIs4.d.mts} +1 -1
- package/dist/{branch-lifecycle-AwBJIheA.d.mts.map → branch-lifecycle-D4W0WIs4.d.mts.map} +1 -1
- package/dist/{compiler-BZXAN4Ir.mjs → compiler-CNzm2Y2I.mjs} +2 -2
- package/dist/{compiler-BZXAN4Ir.mjs.map → compiler-CNzm2Y2I.mjs.map} +1 -1
- package/dist/{compiler-sfc.cjs-tFlmtjrJ.mjs → compiler-sfc.cjs-y0B0L3MH.mjs} +2 -2
- package/dist/{compiler-sfc.cjs-tFlmtjrJ.mjs.map → compiler-sfc.cjs-y0B0L3MH.mjs.map} +1 -1
- package/dist/{content-manager-BgxF3dHu.d.mts → content-manager-DWo3G64y.d.mts} +2 -2
- package/dist/{content-manager-BgxF3dHu.d.mts.map → content-manager-DWo3G64y.d.mts.map} +1 -1
- package/dist/core/apply-manager.mjs +1 -1
- package/dist/core/config.d.mts +1 -1
- package/dist/core/content-manager.d.mts +1 -1
- package/dist/core/context.d.mts +1 -1
- package/dist/core/contracts/index.d.mts +1 -1
- package/dist/core/doctor.mjs +2 -2
- package/dist/core/graph-builder.mjs +2 -2
- package/dist/core/model-manager.d.mts +1 -1
- package/dist/core/ops/index.d.mts +2 -2
- package/dist/core/overlay-reader.d.mts +1 -1
- package/dist/core/overlay-reader.mjs +1 -1
- package/dist/core/scan-config.mjs +1 -1
- package/dist/core/scanner.mjs +3 -3
- package/dist/core/validator/index.d.mts +1 -1
- package/dist/core/validator/index.mjs +1 -1
- package/dist/{doctor-pw8EWRFR.mjs → doctor-BwJ_nmqS.mjs} +2 -2
- package/dist/{doctor-pw8EWRFR.mjs.map → doctor-BwJ_nmqS.mjs.map} +1 -1
- package/dist/errors-e0YdjooK.mjs +126 -0
- package/dist/errors-e0YdjooK.mjs.map +1 -0
- package/dist/git/branch-lifecycle.d.mts +1 -1
- package/dist/git/transaction.d.mts +1 -1
- package/dist/{graph-builder-CRUX_8mA.mjs → graph-builder-DK4Mh8Tn.mjs} +2 -2
- package/dist/{graph-builder-CRUX_8mA.mjs.map → graph-builder-DK4Mh8Tn.mjs.map} +1 -1
- package/dist/index.mjs +8 -8
- package/dist/{node-wC-z3NoZ.mjs → node-BxujauDw.mjs} +1 -1
- package/dist/{node-wC-z3NoZ.mjs.map → node-BxujauDw.mjs.map} +1 -1
- package/dist/{overlay-reader-BOb105gS.mjs → overlay-reader-DNaVsgiS.mjs} +1 -1
- package/dist/{overlay-reader-BOb105gS.mjs.map → overlay-reader-DNaVsgiS.mjs.map} +1 -1
- package/dist/paths-enT2coeX.mjs +27 -0
- package/dist/paths-enT2coeX.mjs.map +1 -0
- package/dist/providers/github/index.d.mts +1 -1
- package/dist/providers/github/index.mjs +2 -1
- package/dist/providers/github/index.mjs.map +1 -1
- package/dist/providers/gitlab/index.d.mts +1 -1
- package/dist/providers/gitlab/index.mjs +2 -1
- package/dist/providers/gitlab/index.mjs.map +1 -1
- package/dist/providers/local/index.d.mts +1 -1
- package/dist/{scan-config-BGUflS8t.mjs → scan-config-BlNLRCMx.mjs} +1 -1
- package/dist/{scan-config-BGUflS8t.mjs.map → scan-config-BlNLRCMx.mjs.map} +1 -1
- package/dist/{scanner-VOwrKLGC.mjs → scanner-CGWhmpDz.mjs} +6 -6
- package/dist/{scanner-VOwrKLGC.mjs.map → scanner-CGWhmpDz.mjs.map} +1 -1
- package/dist/server/http/index.d.mts +1 -1
- package/dist/server/http/index.mjs +8 -8
- package/dist/{server-vA1tM7DA.d.mts → server-DLauBiu2.d.mts} +2 -2
- package/dist/{server-vA1tM7DA.d.mts.map → server-DLauBiu2.d.mts.map} +1 -1
- package/dist/{server-RWxCofdl.mjs → server-I6sqKvO8.mjs} +19 -8
- package/dist/server-I6sqKvO8.mjs.map +1 -0
- package/dist/server.d.mts +1 -1
- package/dist/server.mjs +8 -8
- package/dist/{source-map-generator-BIEiz3If.mjs → source-map-generator-Bvbu-kM5.mjs} +1 -1
- package/dist/{source-map-generator-BIEiz3If.mjs.map → source-map-generator-Bvbu-kM5.mjs.map} +1 -1
- package/dist/{svelte-parser-Bll-1SK0.mjs → svelte-parser-DaFK9iY0.mjs} +3 -3
- package/dist/{svelte-parser-Bll-1SK0.mjs.map → svelte-parser-DaFK9iY0.mjs.map} +1 -1
- package/dist/tsx-parser-C3XsIrwU.mjs +2 -0
- package/dist/{tsx-parser-B_aI_C2r.mjs → tsx-parser-md1N0Niu.mjs} +1 -1
- package/dist/{tsx-parser-B_aI_C2r.mjs.map → tsx-parser-md1N0Niu.mjs.map} +1 -1
- package/dist/{validator-D5VncJ8M.mjs → validator-ChiYk6ap.mjs} +1 -1
- package/dist/{validator-D5VncJ8M.mjs.map → validator-ChiYk6ap.mjs.map} +1 -1
- package/dist/{vue-parser-COig4Y1a.mjs → vue-parser-CHxDohd8.mjs} +3 -3
- package/dist/{vue-parser-COig4Y1a.mjs.map → vue-parser-CHxDohd8.mjs.map} +1 -1
- package/package.json +1 -1
- package/dist/paths-BU6E-oDs.mjs +0 -52
- package/dist/paths-BU6E-oDs.mjs.map +0 -1
- package/dist/server-RWxCofdl.mjs.map +0 -1
- package/dist/tsx-parser-ChduVwKJ.mjs +0 -2
- /package/dist/{index-DDX-qYNw.d.mts → index-w8QHThNS.d.mts} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator-D5VncJ8M.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 } from '@contentrain/types'\nimport { detectSecrets } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { writeJson, writeText } from '../../util/fs.js'\nimport { readConfig } from '../config.js'\nimport { listModels, readModel } from '../model-manager.js'\nimport { writeMeta } from '../meta-manager.js'\nimport { parseFrontmatter, resolveLocaleStrategy } from '../content-manager.js'\nimport type { RepoReader } from '../contracts/index.js'\nimport { LocalReader } from '../../providers/local/reader.js'\nimport { contentDirPath, contentFilePath, documentFilePath, metaFilePath } from '../ops/paths.js'\nimport { validateContent } from './entry.js'\nimport { checkRelationIntegrity, type ResolvedTarget } from './relation-integrity.js'\nimport { validateScheduleFields } from './schedule.js'\n\n// ─── Types ───\n\nexport interface ValidateOptions {\n model?: string\n fix?: boolean\n}\n\nexport interface ValidateResult {\n valid: boolean\n summary: {\n errors: number\n warnings: number\n notices: number\n models_checked: number\n entries_checked: number\n }\n issues: ValidationError[]\n fixed: number\n}\n\n// ─── Reader-backed IO helpers ───\n\nasync function readJsonViaReader<T>(reader: RepoReader, path: string): Promise<T | null> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return null\n }\n}\n\nasync function readTextViaReader(reader: RepoReader, path: string): Promise<string | null> {\n try {\n return await reader.readFile(path)\n } catch {\n return null\n }\n}\n\n// ─── Shared field validation helpers ───\n\n/**\n * Build a `resolveTarget` adapter for `checkRelationIntegrity` that walks\n * the project's content store via the shared {@link RepoReader}. Mirrors\n * the target-resolution shape the legacy `checkRelation` used — collection\n * targets return their entry object-map, documents return a \"slug exists\"\n * marker map, singletons and dictionaries return null content so the\n * checker skips key enforcement for them.\n */\nfunction buildProjectTargetResolver(\n reader: RepoReader,\n config: ContentrainConfig,\n): (targetModelId: string, targetLocale: string) => Promise<ResolvedTarget> {\n return async (targetModelId, targetLocale) => {\n const targetModel = await readModel(reader, targetModelId)\n if (!targetModel) return { exists: false }\n\n if (targetModel.kind === 'document') {\n const docContentDir = contentDirPath(targetModel)\n const slugs = await discoverDocumentSlugs(reader, docContentDir, targetModel)\n return { exists: true, content: Object.fromEntries(slugs.map(s => [s, true])) }\n }\n if (targetModel.kind === 'singleton' || targetModel.kind === 'dictionary') {\n return { exists: true, content: null }\n }\n // Collection: resolve against the target model's own storage. i18n:false\n // targets are locale-agnostic (data.json). For i18n:true targets, fall back\n // to the default-locale file when the source locale lacks the entry — a\n // relation legitimately points at an entry that may be translated under\n // another locale, so the id set is merged to avoid phantom broken refs. #Y5\n const merged: Record<string, unknown> = {}\n const primary = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, targetLocale),\n )\n if (primary) Object.assign(merged, primary)\n if (targetModel.i18n && targetLocale !== config.locales.default) {\n const fallback = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, config.locales.default),\n )\n if (fallback) Object.assign(merged, fallback)\n }\n return { exists: true, content: merged }\n }\n}\n\n/**\n * Scan an entry's data fields for detected secrets in UNDECLARED keys —\n * the legacy validator also flagged stray/rogue fields that were not in\n * `model.fields`. `validateContent` only knows about declared fields, so\n * this complementary pass preserves that coverage.\n */\nfunction scanUndeclaredFieldsForSecrets(\n data: Record<string, unknown>,\n declared: Record<string, unknown> | undefined,\n ctx: { model: string, locale: string, entry?: string, slug?: string },\n issues: ValidationError[],\n): void {\n for (const [fieldName, value] of Object.entries(data)) {\n if (declared && fieldName in declared) continue\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n ...ctx,\n field: fieldName,\n message: `Potential secret detected in field \"${fieldName}\"`,\n })\n }\n }\n}\n\n// ─── Per-model validators ───\n\nasync function validateCollectionModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n const locales = model.i18n ? config.locales.supported : [config.locales.default]\n\n // Collect all entry IDs per locale for parity check\n const localeEntryIds: Record<string, Set<string>> = {}\n const allEntryIds = new Set<string>()\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n\n for (const locale of locales) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, Record<string, unknown>>>(reader, filePath)\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n const ids = new Set(Object.keys(data))\n localeEntryIds[locale] = ids\n for (const id of ids) allEntryIds.add(id)\n\n // Check canonical sort\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, Record<string, unknown>> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate each entry — validateContent covers secret/field/unique/email-url/\n // polymorphic/nested on declared fields; supplementary scans below catch\n // undeclared-field secrets and async relation integrity.\n for (const [entryId, fields] of Object.entries(data)) {\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(\n fields,\n model.fields,\n { model: model.id, locale, entry: entryId },\n issues,\n )\n\n if (!model.fields) continue\n\n const entryResult = validateContent(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n { allEntries: data, currentEntryId: entryId },\n )\n issues.push(...entryResult.errors)\n\n const relationErrors = await checkRelationIntegrity(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\n }\n }\n\n // Entry parity check (i18n)\n if (model.i18n && Object.keys(localeEntryIds).length > 1) {\n const localeKeys = Object.keys(localeEntryIds)\n for (let i = 1; i < localeKeys.length; i++) {\n const locA = localeKeys[0]!\n const locB = localeKeys[i]!\n const idsA = localeEntryIds[locA]!\n const idsB = localeEntryIds[locB]!\n\n for (const id of idsA) {\n if (!idsB.has(id)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale: locB,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locA} but missing in ${locB}`,\n })\n }\n }\n for (const id of idsB) {\n if (!idsA.has(id)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale: locA,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locB} but missing in ${locA}`,\n })\n }\n }\n }\n }\n\n // 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 status: 'draft',\n source: 'import',\n updated_by: 'contentrain-mcp',\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`, 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\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, unknown>>(reader, filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(data, model.fields, { model: model.id, locale }, issues)\n\n if (model.fields) {\n const entryResult = validateContent(data, model.fields, model.id, locale)\n issues.push(...entryResult.errors)\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n const relationErrors = await checkRelationIntegrity(\n data,\n model.fields,\n model.id,\n locale,\n undefined,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, unknown> = {}\n for (const key of sorted) {\n resorted[key] = data[key]\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate schedule fields in singleton meta\n const singletonMetaData = await readJsonViaReader<EntryMeta>(reader, metaFilePath(model, locale, 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\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, string>>(reader, filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n localeKeys[locale] = new Set(Object.keys(data))\n\n // Secret detection\n for (const [key, value] of Object.entries(data)) {\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: key,\n message: `Potential secret detected in key \"${key}\"`,\n })\n }\n }\n\n // Duplicate value detection\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n for (const [value, dupeKeys] of valueToKeys) {\n if (dupeKeys.length > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: `Duplicate value \"${truncated}\" mapped to ${dupeKeys.length} keys: [${dupeKeys.join(', ')}]`,\n })\n }\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, string> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n }\n\n // Key parity check\n if (model.i18n && Object.keys(localeKeys).length > 1) {\n const localeNames = Object.keys(localeKeys)\n for (let i = 1; i < localeNames.length; i++) {\n const locA = localeNames[0]!\n const locB = localeNames[i]!\n const keysA = localeKeys[locA]!\n const keysB = localeKeys[locB]!\n\n for (const k of keysA) {\n if (!keysB.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locB,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locA} but missing in ${locB}`,\n })\n }\n }\n for (const k of keysB) {\n if (!keysA.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locA,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locB} but missing in ${locA}`,\n })\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function discoverDocumentSlugs(\n reader: RepoReader,\n cDir: string,\n model: ModelDefinition,\n): Promise<string[]> {\n const strategy = resolveLocaleStrategy(model)\n const entries = await reader.listDirectory(cDir)\n\n // When i18n is disabled, documents are flat {slug}.md files\n if (!model.i18n) {\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n }\n\n if (strategy === 'file') {\n // Each slug is a subdirectory\n return entries.filter(e => !e.startsWith('.'))\n }\n\n if (strategy === 'suffix') {\n // Files like {slug}.{locale}.md — extract unique slugs\n const slugs = new Set<string>()\n for (const f of entries) {\n if (!f.endsWith('.md')) continue\n // Remove last two dot-separated segments (.locale.md)\n const parts = f.replace(/\\.md$/, '').split('.')\n if (parts.length >= 2) {\n parts.pop() // remove locale\n slugs.add(parts.join('.'))\n }\n }\n return [...slugs]\n }\n\n if (strategy === 'directory') {\n // Locale subdirectories contain {slug}.md files — collect slugs from all locale dirs\n const slugs = new Set<string>()\n const localeLists = await Promise.all(\n entries\n .filter(localeDir => !localeDir.startsWith('.'))\n .map(localeDir => reader.listDirectory(`${cDir}/${localeDir}`)),\n )\n for (const files of localeLists) {\n for (const f of files) {\n if (f.endsWith('.md')) slugs.add(f.replace(/\\.md$/, ''))\n }\n }\n return [...slugs]\n }\n\n // 'none' — {slug}.md files, no locale\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n}\n\nasync function validateDocumentModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n const cDir = contentDirPath(model)\n\n if (!await reader.fileExists(cDir)) return { entries: 0, fixed: 0 }\n\n const slugs = await discoverDocumentSlugs(reader, cDir, model)\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n const locales = model.i18n ? config.locales.supported : [config.locales.default]\n\n // 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`,\n })\n if (fix && projectRoot) {\n // Create empty template\n const template = `---\\nslug: ${slug}\\n---\\n`\n await writeText(join(projectRoot, filePath), template)\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n const { frontmatter, body } = parseFrontmatter(raw)\n\n // Always scan undeclared frontmatter fields + body for secrets.\n scanUndeclaredFieldsForSecrets(\n frontmatter,\n // Treat `body` as a declared field so the undeclared-scan skips it; body\n // is scanned separately below.\n { ...model.fields, body: true } as Record<string, unknown>,\n { model: model.id, locale, slug },\n issues,\n )\n\n if (detectSecrets(body).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: 'body',\n message: 'Potential secret detected in document body',\n })\n }\n\n if (model.fields) {\n const fieldsWithoutBody = Object.fromEntries(\n Object.entries(model.fields).filter(([name]) => name !== 'body'),\n )\n const entryResult = validateContent(\n frontmatter,\n fieldsWithoutBody,\n model.id,\n locale,\n 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 let result: { entries: number; fixed: number }\n\n switch (model.kind) {\n case 'collection':\n result = await validateCollectionModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'singleton':\n result = await validateSingletonModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'dictionary':\n result = await validateDictionaryModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'document':\n result = await validateDocumentModel(reader, projectRoot, model, config, issues, fix)\n break\n default:\n result = { entries: 0, fixed: 0 }\n }\n\n totalEntries += result.entries\n totalFixed += result.fixed\n }\n\n // Orphan content check (only when checking all models)\n if (!options?.model) {\n totalFixed += await checkOrphanContent(reader, validModelIds, issues, fix)\n }\n\n // Cross-dictionary duplicate value detection (only when checking all models)\n if (!options?.model) {\n const dictModels = modelsToCheck.filter(m => m.kind === 'dictionary')\n if (dictModels.length > 1) {\n const globalValueMap: Record<string, Map<string, Array<{ model: string; key: string }>>> = {}\n\n for (const summary of dictModels) {\n const model = await readModel(reader, summary.id)\n if (!model) continue\n\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n if (!globalValueMap[locale]) globalValueMap[locale] = new Map()\n const data = await readJsonViaReader<Record<string, string>>(reader, contentFilePath(model, locale))\n if (!data) continue\n\n for (const [key, value] of Object.entries(data)) {\n const refs = globalValueMap[locale]!.get(value)\n if (refs) refs.push({ model: model.id, key })\n else globalValueMap[locale]!.set(value, [{ model: model.id, key }])\n }\n }\n }\n\n for (const [locale, valueMap] of Object.entries(globalValueMap)) {\n for (const [value, refs] of valueMap) {\n const uniqueModels = new Set(refs.map(r => r.model))\n if (uniqueModels.size > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'notice',\n locale,\n message: `Cross-model duplicate value \"${truncated}\" in ${refs.map(r => `${r.model}/${r.key}`).join(', ')}`,\n })\n }\n }\n }\n }\n }\n\n const errors = issues.filter(i => i.severity === 'error').length\n const warnings = issues.filter(i => i.severity === 'warning').length\n const notices = issues.filter(i => i.severity === 'notice').length\n\n return {\n valid: errors === 0,\n summary: {\n errors,\n warnings,\n notices,\n models_checked: modelsChecked,\n entries_checked: totalEntries,\n },\n issues,\n fixed: totalFixed,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validator-ChiYk6ap.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 } from '@contentrain/types'\nimport { detectSecrets } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { writeJson, writeText } from '../../util/fs.js'\nimport { readConfig } from '../config.js'\nimport { listModels, readModel } from '../model-manager.js'\nimport { writeMeta } from '../meta-manager.js'\nimport { parseFrontmatter, resolveLocaleStrategy } from '../content-manager.js'\nimport type { RepoReader } from '../contracts/index.js'\nimport { LocalReader } from '../../providers/local/reader.js'\nimport { contentDirPath, contentFilePath, documentFilePath, metaFilePath } from '../ops/paths.js'\nimport { validateContent } from './entry.js'\nimport { checkRelationIntegrity, type ResolvedTarget } from './relation-integrity.js'\nimport { validateScheduleFields } from './schedule.js'\n\n// ─── Types ───\n\nexport interface ValidateOptions {\n model?: string\n fix?: boolean\n}\n\nexport interface ValidateResult {\n valid: boolean\n summary: {\n errors: number\n warnings: number\n notices: number\n models_checked: number\n entries_checked: number\n }\n issues: ValidationError[]\n fixed: number\n}\n\n// ─── Reader-backed IO helpers ───\n\nasync function readJsonViaReader<T>(reader: RepoReader, path: string): Promise<T | null> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return null\n }\n}\n\nasync function readTextViaReader(reader: RepoReader, path: string): Promise<string | null> {\n try {\n return await reader.readFile(path)\n } catch {\n return null\n }\n}\n\n// ─── Shared field validation helpers ───\n\n/**\n * Build a `resolveTarget` adapter for `checkRelationIntegrity` that walks\n * the project's content store via the shared {@link RepoReader}. Mirrors\n * the target-resolution shape the legacy `checkRelation` used — collection\n * targets return their entry object-map, documents return a \"slug exists\"\n * marker map, singletons and dictionaries return null content so the\n * checker skips key enforcement for them.\n */\nfunction buildProjectTargetResolver(\n reader: RepoReader,\n config: ContentrainConfig,\n): (targetModelId: string, targetLocale: string) => Promise<ResolvedTarget> {\n return async (targetModelId, targetLocale) => {\n const targetModel = await readModel(reader, targetModelId)\n if (!targetModel) return { exists: false }\n\n if (targetModel.kind === 'document') {\n const docContentDir = contentDirPath(targetModel)\n const slugs = await discoverDocumentSlugs(reader, docContentDir, targetModel)\n return { exists: true, content: Object.fromEntries(slugs.map(s => [s, true])) }\n }\n if (targetModel.kind === 'singleton' || targetModel.kind === 'dictionary') {\n return { exists: true, content: null }\n }\n // Collection: resolve against the target model's own storage. i18n:false\n // targets are locale-agnostic (data.json). For i18n:true targets, fall back\n // to the default-locale file when the source locale lacks the entry — a\n // relation legitimately points at an entry that may be translated under\n // another locale, so the id set is merged to avoid phantom broken refs. #Y5\n const merged: Record<string, unknown> = {}\n const primary = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, targetLocale),\n )\n if (primary) Object.assign(merged, primary)\n if (targetModel.i18n && targetLocale !== config.locales.default) {\n const fallback = await readJsonViaReader<Record<string, unknown>>(\n reader,\n contentFilePath(targetModel, config.locales.default),\n )\n if (fallback) Object.assign(merged, fallback)\n }\n return { exists: true, content: merged }\n }\n}\n\n/**\n * Scan an entry's data fields for detected secrets in UNDECLARED keys —\n * the legacy validator also flagged stray/rogue fields that were not in\n * `model.fields`. `validateContent` only knows about declared fields, so\n * this complementary pass preserves that coverage.\n */\nfunction scanUndeclaredFieldsForSecrets(\n data: Record<string, unknown>,\n declared: Record<string, unknown> | undefined,\n ctx: { model: string, locale: string, entry?: string, slug?: string },\n issues: ValidationError[],\n): void {\n for (const [fieldName, value] of Object.entries(data)) {\n if (declared && fieldName in declared) continue\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n ...ctx,\n field: fieldName,\n message: `Potential secret detected in field \"${fieldName}\"`,\n })\n }\n }\n}\n\n// ─── Per-model validators ───\n\nasync function validateCollectionModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n const locales = model.i18n ? config.locales.supported : [config.locales.default]\n\n // Collect all entry IDs per locale for parity check\n const localeEntryIds: Record<string, Set<string>> = {}\n const allEntryIds = new Set<string>()\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n\n for (const locale of locales) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, Record<string, unknown>>>(reader, filePath)\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n const ids = new Set(Object.keys(data))\n localeEntryIds[locale] = ids\n for (const id of ids) allEntryIds.add(id)\n\n // Check canonical sort\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, Record<string, unknown>> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate each entry — validateContent covers secret/field/unique/email-url/\n // polymorphic/nested on declared fields; supplementary scans below catch\n // undeclared-field secrets and async relation integrity.\n for (const [entryId, fields] of Object.entries(data)) {\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(\n fields,\n model.fields,\n { model: model.id, locale, entry: entryId },\n issues,\n )\n\n if (!model.fields) continue\n\n const entryResult = validateContent(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n { allEntries: data, currentEntryId: entryId },\n )\n issues.push(...entryResult.errors)\n\n const relationErrors = await checkRelationIntegrity(\n fields,\n model.fields,\n model.id,\n locale,\n entryId,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\n }\n }\n\n // Entry parity check (i18n)\n if (model.i18n && Object.keys(localeEntryIds).length > 1) {\n const localeKeys = Object.keys(localeEntryIds)\n for (let i = 1; i < localeKeys.length; i++) {\n const locA = localeKeys[0]!\n const locB = localeKeys[i]!\n const idsA = localeEntryIds[locA]!\n const idsB = localeEntryIds[locB]!\n\n for (const id of idsA) {\n if (!idsB.has(id)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale: locB,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locA} but missing in ${locB}`,\n })\n }\n }\n for (const id of idsB) {\n if (!idsA.has(id)) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale: locA,\n entry: id,\n message: `Entry parity: entry \"${id}\" exists in ${locB} but missing in ${locA}`,\n })\n }\n }\n }\n }\n\n // 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 status: 'draft',\n source: 'import',\n updated_by: 'contentrain-mcp',\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`, 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\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, unknown>>(reader, filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n\n scanUndeclaredFieldsForSecrets(data, model.fields, { model: model.id, locale }, issues)\n\n if (model.fields) {\n const entryResult = validateContent(data, model.fields, model.id, locale)\n issues.push(...entryResult.errors)\n\n const resolveTarget = buildProjectTargetResolver(reader, config)\n const relationErrors = await checkRelationIntegrity(\n data,\n model.fields,\n model.id,\n locale,\n undefined,\n async () => null,\n { severity: 'error', resolveTarget },\n )\n issues.push(...relationErrors)\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, unknown> = {}\n for (const key of sorted) {\n resorted[key] = data[key]\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n\n // Validate schedule fields in singleton meta\n const singletonMetaData = await readJsonViaReader<EntryMeta>(reader, metaFilePath(model, locale, 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\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n const filePath = contentFilePath(model, locale)\n const data = await readJsonViaReader<Record<string, string>>(reader, filePath)\n\n if (!data) {\n if (model.i18n) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n message: `Locale file missing: ${locale}.json`,\n })\n if (fix && projectRoot) {\n await writeJson(join(projectRoot, filePath), {})\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n localeKeys[locale] = new Set(Object.keys(data))\n\n // Secret detection\n for (const [key, value] of Object.entries(data)) {\n if (detectSecrets(value).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n field: key,\n message: `Potential secret detected in key \"${key}\"`,\n })\n }\n }\n\n // Duplicate value detection\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n for (const [value, dupeKeys] of valueToKeys) {\n if (dupeKeys.length > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: `Duplicate value \"${truncated}\" mapped to ${dupeKeys.length} keys: [${dupeKeys.join(', ')}]`,\n })\n }\n }\n\n // Canonical sort check\n const keys = Object.keys(data)\n const sorted = [...keys].toSorted()\n if (keys.join(',') !== sorted.join(',')) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale,\n message: 'Content file keys not in canonical order',\n })\n if (fix && projectRoot) {\n const resorted: Record<string, string> = {}\n for (const key of sorted) {\n resorted[key] = data[key]!\n }\n await writeJson(join(projectRoot, filePath), resorted)\n fixed++\n }\n }\n }\n\n // Key parity check\n if (model.i18n && Object.keys(localeKeys).length > 1) {\n const localeNames = Object.keys(localeKeys)\n for (let i = 1; i < localeNames.length; i++) {\n const locA = localeNames[0]!\n const locB = localeNames[i]!\n const keysA = localeKeys[locA]!\n const keysB = localeKeys[locB]!\n\n for (const k of keysA) {\n if (!keysB.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locB,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locA} but missing in ${locB}`,\n })\n }\n }\n for (const k of keysB) {\n if (!keysA.has(k)) {\n issues.push({\n severity: 'warning',\n model: model.id,\n locale: locA,\n field: k,\n message: `Key parity: key \"${k}\" exists in ${locB} but missing in ${locA}`,\n })\n }\n }\n }\n }\n\n return { entries: entriesChecked, fixed }\n}\n\nasync function discoverDocumentSlugs(\n reader: RepoReader,\n cDir: string,\n model: ModelDefinition,\n): Promise<string[]> {\n const strategy = resolveLocaleStrategy(model)\n const entries = await reader.listDirectory(cDir)\n\n // When i18n is disabled, documents are flat {slug}.md files\n if (!model.i18n) {\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n }\n\n if (strategy === 'file') {\n // Each slug is a subdirectory\n return entries.filter(e => !e.startsWith('.'))\n }\n\n if (strategy === 'suffix') {\n // Files like {slug}.{locale}.md — extract unique slugs\n const slugs = new Set<string>()\n for (const f of entries) {\n if (!f.endsWith('.md')) continue\n // Remove last two dot-separated segments (.locale.md)\n const parts = f.replace(/\\.md$/, '').split('.')\n if (parts.length >= 2) {\n parts.pop() // remove locale\n slugs.add(parts.join('.'))\n }\n }\n return [...slugs]\n }\n\n if (strategy === 'directory') {\n // Locale subdirectories contain {slug}.md files — collect slugs from all locale dirs\n const slugs = new Set<string>()\n const localeLists = await Promise.all(\n entries\n .filter(localeDir => !localeDir.startsWith('.'))\n .map(localeDir => reader.listDirectory(`${cDir}/${localeDir}`)),\n )\n for (const files of localeLists) {\n for (const f of files) {\n if (f.endsWith('.md')) slugs.add(f.replace(/\\.md$/, ''))\n }\n }\n return [...slugs]\n }\n\n // 'none' — {slug}.md files, no locale\n return entries.filter(f => f.endsWith('.md')).map(f => f.replace(/\\.md$/, ''))\n}\n\nasync function validateDocumentModel(\n reader: RepoReader,\n projectRoot: string | undefined,\n model: ModelDefinition,\n config: ContentrainConfig,\n issues: ValidationError[],\n fix: boolean,\n): Promise<{ entries: number; fixed: number }> {\n let entriesChecked = 0\n let fixed = 0\n const cDir = contentDirPath(model)\n\n if (!await reader.fileExists(cDir)) return { entries: 0, fixed: 0 }\n\n const slugs = await discoverDocumentSlugs(reader, cDir, model)\n // Non-i18n models store content + meta locale-agnostically (data.json, a\n // single meta set), so iterating every supported locale produces phantom\n // per-locale orphan/parity warnings. Iterate just the default locale. #8/Y3\n const locales = model.i18n ? config.locales.supported : [config.locales.default]\n\n // 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`,\n })\n if (fix && projectRoot) {\n // Create empty template\n const template = `---\\nslug: ${slug}\\n---\\n`\n await writeText(join(projectRoot, filePath), template)\n fixed++\n }\n }\n continue\n }\n\n entriesChecked++\n const { frontmatter, body } = parseFrontmatter(raw)\n\n // Always scan undeclared frontmatter fields + body for secrets.\n scanUndeclaredFieldsForSecrets(\n frontmatter,\n // Treat `body` as a declared field so the undeclared-scan skips it; body\n // is scanned separately below.\n { ...model.fields, body: true } as Record<string, unknown>,\n { model: model.id, locale, slug },\n issues,\n )\n\n if (detectSecrets(body).length > 0) {\n issues.push({\n severity: 'error',\n model: model.id,\n locale,\n slug,\n field: 'body',\n message: 'Potential secret detected in document body',\n })\n }\n\n if (model.fields) {\n const fieldsWithoutBody = Object.fromEntries(\n Object.entries(model.fields).filter(([name]) => name !== 'body'),\n )\n const entryResult = validateContent(\n frontmatter,\n fieldsWithoutBody,\n model.id,\n locale,\n 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 let result: { entries: number; fixed: number }\n\n switch (model.kind) {\n case 'collection':\n result = await validateCollectionModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'singleton':\n result = await validateSingletonModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'dictionary':\n result = await validateDictionaryModel(reader, projectRoot, model, config, issues, fix)\n break\n case 'document':\n result = await validateDocumentModel(reader, projectRoot, model, config, issues, fix)\n break\n default:\n result = { entries: 0, fixed: 0 }\n }\n\n totalEntries += result.entries\n totalFixed += result.fixed\n }\n\n // Orphan content check (only when checking all models)\n if (!options?.model) {\n totalFixed += await checkOrphanContent(reader, validModelIds, issues, fix)\n }\n\n // Cross-dictionary duplicate value detection (only when checking all models)\n if (!options?.model) {\n const dictModels = modelsToCheck.filter(m => m.kind === 'dictionary')\n if (dictModels.length > 1) {\n const globalValueMap: Record<string, Map<string, Array<{ model: string; key: string }>>> = {}\n\n for (const summary of dictModels) {\n const model = await readModel(reader, summary.id)\n if (!model) continue\n\n for (const locale of (model.i18n ? config.locales.supported : [config.locales.default])) {\n if (!globalValueMap[locale]) globalValueMap[locale] = new Map()\n const data = await readJsonViaReader<Record<string, string>>(reader, contentFilePath(model, locale))\n if (!data) continue\n\n for (const [key, value] of Object.entries(data)) {\n const refs = globalValueMap[locale]!.get(value)\n if (refs) refs.push({ model: model.id, key })\n else globalValueMap[locale]!.set(value, [{ model: model.id, key }])\n }\n }\n }\n\n for (const [locale, valueMap] of Object.entries(globalValueMap)) {\n for (const [value, refs] of valueMap) {\n const uniqueModels = new Set(refs.map(r => r.model))\n if (uniqueModels.size > 1) {\n const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value\n issues.push({\n severity: 'notice',\n locale,\n message: `Cross-model duplicate value \"${truncated}\" in ${refs.map(r => `${r.model}/${r.key}`).join(', ')}`,\n })\n }\n }\n }\n }\n }\n\n const errors = issues.filter(i => i.severity === 'error').length\n const warnings = issues.filter(i => i.severity === 'warning').length\n const notices = issues.filter(i => i.severity === 'notice').length\n\n return {\n valid: errors === 0,\n summary: {\n errors,\n warnings,\n notices,\n models_checked: modelsChecked,\n entries_checked: totalEntries,\n },\n issues,\n fixed: totalFixed,\n }\n}\n"],"mappings":"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"}
|
|
@@ -21,7 +21,7 @@ let _compiler = null;
|
|
|
21
21
|
async function loadCompiler() {
|
|
22
22
|
if (_compiler) return _compiler;
|
|
23
23
|
try {
|
|
24
|
-
_compiler = await import("./compiler-sfc.cjs-
|
|
24
|
+
_compiler = await import("./compiler-sfc.cjs-y0B0L3MH.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
|
|
25
25
|
return _compiler;
|
|
26
26
|
} catch {
|
|
27
27
|
throw new Error("@vue/compiler-sfc is required to parse .vue files. Install it with: pnpm add -D @vue/compiler-sfc");
|
|
@@ -178,7 +178,7 @@ let _tsxParser = null;
|
|
|
178
178
|
async function loadTsxParser() {
|
|
179
179
|
if (_tsxParser) return _tsxParser;
|
|
180
180
|
try {
|
|
181
|
-
_tsxParser = (await import("./tsx-parser-
|
|
181
|
+
_tsxParser = (await import("./tsx-parser-C3XsIrwU.mjs")).parseTsx;
|
|
182
182
|
return _tsxParser;
|
|
183
183
|
} catch {
|
|
184
184
|
return null;
|
|
@@ -233,4 +233,4 @@ async function parseVue(content, fileName) {
|
|
|
233
233
|
//#endregion
|
|
234
234
|
export { parseVue };
|
|
235
235
|
|
|
236
|
-
//# sourceMappingURL=vue-parser-
|
|
236
|
+
//# sourceMappingURL=vue-parser-CHxDohd8.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-parser-COig4Y1a.mjs","names":[],"sources":["../src/core/ast-scanner/vue-parser.ts"],"sourcesContent":["// ─── Vue SFC Parser for Scanner v2 ───\n// Parses .vue Single File Components using @vue/compiler-sfc.\n// Extracts ALL strings with structural context metadata.\n// Scanner does NOT classify — agent does. When in doubt, INCLUDE.\n\n// ─── Types ───\n\nimport type { ExtractedString } from './types.js'\nexport type { ExtractedString }\n\n// ─── Lazy-loaded @vue/compiler-sfc ───\n\ninterface VueCompilerSFC {\n parse: (source: string, options?: { filename?: string }) => {\n descriptor: {\n template: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n ast?: VueTemplateNode\n } | null\n script: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n lang?: string\n } | null\n scriptSetup: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n lang?: string\n } | null\n }\n }\n compileTemplate: (options: {\n source: string\n filename: string\n id: string\n }) => {\n ast?: VueTemplateNode\n }\n}\n\n// Vue template AST node types\nconst NodeTypes = {\n ROOT: 0,\n ELEMENT: 1,\n TEXT: 2,\n COMMENT: 3,\n SIMPLE_EXPRESSION: 4,\n INTERPOLATION: 5,\n ATTRIBUTE: 6,\n DIRECTIVE: 7,\n COMPOUND_EXPRESSION: 8,\n IF: 9,\n IF_BRANCH: 10,\n FOR: 11,\n TEXT_CALL: 12,\n VNODE_CALL: 13,\n JS_CALL_EXPRESSION: 14,\n} as const\n\ninterface VueTemplateLoc {\n start: { line: number; column: number; offset: number }\n end: { line: number; column: number; offset: number }\n source: string\n}\n\ninterface VueTemplateNode {\n type: number\n loc: VueTemplateLoc\n children?: VueTemplateNode[]\n tag?: string // for ELEMENT\n props?: VueTemplateProp[]\n content?: VueTemplateNode | string // for INTERPOLATION, TEXT, SIMPLE_EXPRESSION\n branches?: VueTemplateNode[] // for IF\n}\n\ninterface VueTemplateProp {\n type: number // ATTRIBUTE (6) or DIRECTIVE (7)\n name: string\n loc: VueTemplateLoc\n value?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n exp?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n arg?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n}\n\nlet _compiler: VueCompilerSFC | null = null\n\nasync function loadCompiler(): Promise<VueCompilerSFC> {\n if (_compiler) return _compiler\n try {\n // Dynamic import — @vue/compiler-sfc is optional\n const mod = await import('@vue/compiler-sfc')\n _compiler = mod as unknown as VueCompilerSFC\n return _compiler\n } catch {\n throw new Error(\n '@vue/compiler-sfc is required to parse .vue files. '\n + 'Install it with: pnpm add -D @vue/compiler-sfc',\n )\n }\n}\n\n// ─── Directives that contain code expressions (not user content) ───\n\nconst CODE_DIRECTIVES = new Set([\n 'if', 'else-if', 'else', 'show',\n 'for',\n 'on', // @click, v-on:click\n 'model',\n 'memo',\n 'once',\n 'pre',\n 'cloak',\n 'is',\n 'slot',\n 'key',\n])\n\n// Directive args that are code/binding expressions, not content\nconst CODE_DIRECTIVE_ARGS = new Set([\n 'class', 'style', // :class, :style → CSS bindings\n 'key',\n 'ref',\n 'is',\n])\n\n// Static attributes whose values are CSS, not content\nconst CSS_ATTRIBUTES = new Set([\n 'class', 'style',\n])\n\n// ─── Surrounding text helper ───\n\nconst SURROUNDING_MAX = 120\n\nfunction _getSurrounding(content: string, offset: number): string {\n const lines = content.split('\\n')\n let charCount = 0\n for (let i = 0; i < lines.length; i++) {\n const lineLen = (lines[i]?.length ?? 0) + 1 // +1 for newline\n if (charCount + lineLen > offset) {\n return (lines[i] ?? '').slice(0, SURROUNDING_MAX)\n }\n charCount += lineLen\n }\n return ''\n}\n\nfunction getSurroundingByLine(content: string, line: number): string {\n const lines = content.split('\\n')\n const idx = line - 1\n if (idx >= 0 && idx < lines.length) {\n return (lines[idx] ?? '').slice(0, SURROUNDING_MAX)\n }\n return ''\n}\n\n// ─── Template AST Walker ───\n\nfunction walkTemplate(\n node: VueTemplateNode,\n templateLineOffset: number,\n sfcContent: string,\n results: ExtractedString[],\n parentTag: string = '',\n): void {\n switch (node.type) {\n case NodeTypes.TEXT: {\n // Static text between tags\n const text = typeof node.content === 'string'\n ? node.content\n : (node.loc?.source ?? '')\n const trimmed = text.trim()\n if (trimmed.length > 0 && /\\S/.test(trimmed)) {\n results.push({\n value: trimmed,\n line: node.loc.start.line + templateLineOffset - 1,\n column: node.loc.start.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1),\n })\n }\n break\n }\n\n case NodeTypes.INTERPOLATION: {\n // {{ expression }} — extract string literals from within\n if (node.content && typeof node.content !== 'string' && node.content.type === NodeTypes.SIMPLE_EXPRESSION) {\n const expr = typeof node.content.content === 'string'\n ? node.content.content\n : ''\n // Only extract if the expression itself is a string literal\n // e.g., {{ 'Hello' }} or {{ \"World\" }}\n const stringMatch = expr.match(/^(['\"`])(.+)\\1$/)\n if (stringMatch?.[2]) {\n results.push({\n value: stringMatch[2],\n line: node.loc.start.line + templateLineOffset - 1,\n column: node.loc.start.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1),\n })\n }\n // If it's a variable reference like {{ greeting }}, skip — that's code\n }\n break\n }\n\n case NodeTypes.ELEMENT: {\n const tag = node.tag ?? ''\n\n // Process props/attributes\n if (node.props) {\n for (const prop of node.props) {\n processProp(prop, tag, templateLineOffset, sfcContent, results)\n }\n }\n\n // Recurse into children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, tag)\n }\n }\n break\n }\n\n case NodeTypes.IF: {\n // v-if creates branches — walk each branch's children\n if (node.branches) {\n for (const branch of node.branches) {\n if (branch.children) {\n for (const child of branch.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n }\n }\n break\n }\n\n case NodeTypes.IF_BRANCH: {\n // Walk children of if branch\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.FOR: {\n // v-for node — walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.ROOT: {\n // Root node — walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.COMPOUND_EXPRESSION: {\n // Compound expression — walk children\n if (node.children) {\n for (const child of node.children) {\n if (typeof child !== 'string') {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n }\n break\n }\n\n case NodeTypes.TEXT_CALL: {\n // Text call node (wrapper for text in v-if, etc.) — walk content\n if (node.content && typeof node.content !== 'string') {\n walkTemplate(node.content, templateLineOffset, sfcContent, results, parentTag)\n }\n // Also walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n default: {\n // For any unknown node type, try to walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n }\n}\n\nfunction processProp(\n prop: VueTemplateProp,\n parentTag: string,\n templateLineOffset: number,\n sfcContent: string,\n results: ExtractedString[],\n): void {\n if (prop.type === NodeTypes.ATTRIBUTE) {\n // Static attribute: title=\"Hello\"\n if (!prop.value) return\n\n const attrName = prop.name\n const attrValue = prop.value.content\n\n if (!attrValue || attrValue.trim().length === 0) return\n\n // CSS attributes get css_class context\n if (CSS_ATTRIBUTES.has(attrName)) {\n results.push({\n value: attrValue,\n line: prop.value.loc.start.line + templateLineOffset - 1,\n column: prop.value.loc.start.column,\n context: 'css_class',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1),\n })\n return\n }\n\n results.push({\n value: attrValue,\n line: prop.value.loc.start.line + templateLineOffset - 1,\n column: prop.value.loc.start.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1),\n })\n } else if (prop.type === NodeTypes.DIRECTIVE) {\n // Dynamic directive: :title=\"expr\", v-bind:title=\"expr\", @click=\"handler\"\n const directiveName = prop.name // 'bind', 'on', 'if', 'for', etc.\n\n // Skip code directives entirely (v-if, v-for, @click, etc.)\n if (CODE_DIRECTIVES.has(directiveName)) return\n\n // For v-bind (:attr=\"expr\"), check if the arg is a code binding\n if (directiveName === 'bind' && prop.arg) {\n const argName = typeof prop.arg.content === 'string' ? prop.arg.content : ''\n if (CODE_DIRECTIVE_ARGS.has(argName)) return\n }\n\n // Extract string literals from directive expressions\n if (prop.exp) {\n const expr = typeof prop.exp.content === 'string' ? prop.exp.content : ''\n const argName = prop.arg && typeof prop.arg.content === 'string' ? prop.arg.content : ''\n\n // Check if expression is a simple string literal: 'text' or \"text\"\n const stringMatch = expr.match(/^(['\"`])(.+)\\1$/)\n if (stringMatch?.[2]) {\n results.push({\n value: stringMatch[2],\n line: prop.exp.loc.start.line + templateLineOffset - 1,\n column: prop.exp.loc.start.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: argName || directiveName,\n surrounding: getSurroundingByLine(sfcContent, prop.exp.loc.start.line + templateLineOffset - 1),\n })\n }\n // If it's a variable or complex expression, skip — scanner doesn't interpret code\n }\n }\n}\n\n// ─── Script Block Parsing ───\n\n// tsx-parser is being built in parallel; define the interface we expect\ntype TsxParserFn = (content: string, fileName: string) => ExtractedString[]\n\nlet _tsxParser: TsxParserFn | null = null\n\nasync function loadTsxParser(): Promise<TsxParserFn | null> {\n if (_tsxParser) return _tsxParser\n try {\n const mod = await import('./tsx-parser.js')\n _tsxParser = mod.parseTsx\n return _tsxParser\n } catch {\n // tsx-parser not yet available — fall back to no script parsing\n return null\n }\n}\n\n/**\n * Map script block lang to a file extension that tsx-parser's getScriptKind understands.\n * Without this, .vue files default to ScriptKind.JS — causing TypeScript type annotations\n * (e.g. defineEmits<{ 'update:modelValue': [...] }>) to be misclassified as object literals.\n */\nfunction resolveScriptFileName(vueFileName: string, lang?: string): string {\n if (lang === 'tsx') return vueFileName.replace(/\\.vue$/, '.tsx')\n if (lang === 'ts') return vueFileName.replace(/\\.vue$/, '.ts')\n if (lang === 'jsx') return vueFileName.replace(/\\.vue$/, '.jsx')\n return vueFileName.replace(/\\.vue$/, '.js')\n}\n\nfunction parseScriptBlock(\n scriptContent: string,\n scriptStartLine: number,\n fileName: string,\n parseTsx: TsxParserFn,\n lang?: string,\n): ExtractedString[] {\n // Resolve filename with correct extension for TypeScript parser\n const resolvedFileName = resolveScriptFileName(fileName, lang)\n const scriptResults = parseTsx(scriptContent, resolvedFileName)\n\n // Adjust line numbers by script block offset\n return scriptResults.map(r => {\n r.line = r.line + scriptStartLine - 1\n r.scope = 'script'\n return r\n })\n}\n\n// ─── Main Export ───\n\nexport async function parseVue(content: string, fileName: string): Promise<ExtractedString[]> {\n const compiler = await loadCompiler()\n const results: ExtractedString[] = []\n\n const { descriptor } = compiler.parse(content, { filename: fileName })\n\n // ─── Template Block ───\n if (descriptor.template) {\n const templateLineOffset = descriptor.template.loc.start.line\n const templateAst = descriptor.template.ast\n\n if (templateAst) {\n walkTemplate(templateAst, templateLineOffset, content, results)\n } else {\n // Fallback: compile template to get AST\n try {\n const compiled = compiler.compileTemplate({\n source: descriptor.template.content,\n filename: fileName,\n id: 'ast-scanner',\n })\n if (compiled.ast) {\n walkTemplate(compiled.ast, templateLineOffset, content, results)\n }\n } catch {\n // If template compilation fails, we skip template extraction\n // This is acceptable — malformed templates shouldn't block scanning\n }\n }\n }\n\n // ─── Script Block ───\n const scriptBlock = descriptor.scriptSetup ?? descriptor.script\n if (scriptBlock) {\n const parseTsx = await loadTsxParser()\n if (parseTsx) {\n const scriptStartLine = scriptBlock.loc.start.line\n const scriptResults = parseScriptBlock(\n scriptBlock.content,\n scriptStartLine,\n fileName,\n parseTsx,\n scriptBlock.lang,\n )\n results.push(...scriptResults)\n }\n }\n\n // Style blocks are intentionally skipped — no content strings in CSS\n\n return results\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vue-parser-CHxDohd8.mjs","names":[],"sources":["../src/core/ast-scanner/vue-parser.ts"],"sourcesContent":["// ─── Vue SFC Parser for Scanner v2 ───\n// Parses .vue Single File Components using @vue/compiler-sfc.\n// Extracts ALL strings with structural context metadata.\n// Scanner does NOT classify — agent does. When in doubt, INCLUDE.\n\n// ─── Types ───\n\nimport type { ExtractedString } from './types.js'\nexport type { ExtractedString }\n\n// ─── Lazy-loaded @vue/compiler-sfc ───\n\ninterface VueCompilerSFC {\n parse: (source: string, options?: { filename?: string }) => {\n descriptor: {\n template: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n ast?: VueTemplateNode\n } | null\n script: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n lang?: string\n } | null\n scriptSetup: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n lang?: string\n } | null\n }\n }\n compileTemplate: (options: {\n source: string\n filename: string\n id: string\n }) => {\n ast?: VueTemplateNode\n }\n}\n\n// Vue template AST node types\nconst NodeTypes = {\n ROOT: 0,\n ELEMENT: 1,\n TEXT: 2,\n COMMENT: 3,\n SIMPLE_EXPRESSION: 4,\n INTERPOLATION: 5,\n ATTRIBUTE: 6,\n DIRECTIVE: 7,\n COMPOUND_EXPRESSION: 8,\n IF: 9,\n IF_BRANCH: 10,\n FOR: 11,\n TEXT_CALL: 12,\n VNODE_CALL: 13,\n JS_CALL_EXPRESSION: 14,\n} as const\n\ninterface VueTemplateLoc {\n start: { line: number; column: number; offset: number }\n end: { line: number; column: number; offset: number }\n source: string\n}\n\ninterface VueTemplateNode {\n type: number\n loc: VueTemplateLoc\n children?: VueTemplateNode[]\n tag?: string // for ELEMENT\n props?: VueTemplateProp[]\n content?: VueTemplateNode | string // for INTERPOLATION, TEXT, SIMPLE_EXPRESSION\n branches?: VueTemplateNode[] // for IF\n}\n\ninterface VueTemplateProp {\n type: number // ATTRIBUTE (6) or DIRECTIVE (7)\n name: string\n loc: VueTemplateLoc\n value?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n exp?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n arg?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n}\n\nlet _compiler: VueCompilerSFC | null = null\n\nasync function loadCompiler(): Promise<VueCompilerSFC> {\n if (_compiler) return _compiler\n try {\n // Dynamic import — @vue/compiler-sfc is optional\n const mod = await import('@vue/compiler-sfc')\n _compiler = mod as unknown as VueCompilerSFC\n return _compiler\n } catch {\n throw new Error(\n '@vue/compiler-sfc is required to parse .vue files. '\n + 'Install it with: pnpm add -D @vue/compiler-sfc',\n )\n }\n}\n\n// ─── Directives that contain code expressions (not user content) ───\n\nconst CODE_DIRECTIVES = new Set([\n 'if', 'else-if', 'else', 'show',\n 'for',\n 'on', // @click, v-on:click\n 'model',\n 'memo',\n 'once',\n 'pre',\n 'cloak',\n 'is',\n 'slot',\n 'key',\n])\n\n// Directive args that are code/binding expressions, not content\nconst CODE_DIRECTIVE_ARGS = new Set([\n 'class', 'style', // :class, :style → CSS bindings\n 'key',\n 'ref',\n 'is',\n])\n\n// Static attributes whose values are CSS, not content\nconst CSS_ATTRIBUTES = new Set([\n 'class', 'style',\n])\n\n// ─── Surrounding text helper ───\n\nconst SURROUNDING_MAX = 120\n\nfunction _getSurrounding(content: string, offset: number): string {\n const lines = content.split('\\n')\n let charCount = 0\n for (let i = 0; i < lines.length; i++) {\n const lineLen = (lines[i]?.length ?? 0) + 1 // +1 for newline\n if (charCount + lineLen > offset) {\n return (lines[i] ?? '').slice(0, SURROUNDING_MAX)\n }\n charCount += lineLen\n }\n return ''\n}\n\nfunction getSurroundingByLine(content: string, line: number): string {\n const lines = content.split('\\n')\n const idx = line - 1\n if (idx >= 0 && idx < lines.length) {\n return (lines[idx] ?? '').slice(0, SURROUNDING_MAX)\n }\n return ''\n}\n\n// ─── Template AST Walker ───\n\nfunction walkTemplate(\n node: VueTemplateNode,\n templateLineOffset: number,\n sfcContent: string,\n results: ExtractedString[],\n parentTag: string = '',\n): void {\n switch (node.type) {\n case NodeTypes.TEXT: {\n // Static text between tags\n const text = typeof node.content === 'string'\n ? node.content\n : (node.loc?.source ?? '')\n const trimmed = text.trim()\n if (trimmed.length > 0 && /\\S/.test(trimmed)) {\n results.push({\n value: trimmed,\n line: node.loc.start.line + templateLineOffset - 1,\n column: node.loc.start.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1),\n })\n }\n break\n }\n\n case NodeTypes.INTERPOLATION: {\n // {{ expression }} — extract string literals from within\n if (node.content && typeof node.content !== 'string' && node.content.type === NodeTypes.SIMPLE_EXPRESSION) {\n const expr = typeof node.content.content === 'string'\n ? node.content.content\n : ''\n // Only extract if the expression itself is a string literal\n // e.g., {{ 'Hello' }} or {{ \"World\" }}\n const stringMatch = expr.match(/^(['\"`])(.+)\\1$/)\n if (stringMatch?.[2]) {\n results.push({\n value: stringMatch[2],\n line: node.loc.start.line + templateLineOffset - 1,\n column: node.loc.start.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1),\n })\n }\n // If it's a variable reference like {{ greeting }}, skip — that's code\n }\n break\n }\n\n case NodeTypes.ELEMENT: {\n const tag = node.tag ?? ''\n\n // Process props/attributes\n if (node.props) {\n for (const prop of node.props) {\n processProp(prop, tag, templateLineOffset, sfcContent, results)\n }\n }\n\n // Recurse into children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, tag)\n }\n }\n break\n }\n\n case NodeTypes.IF: {\n // v-if creates branches — walk each branch's children\n if (node.branches) {\n for (const branch of node.branches) {\n if (branch.children) {\n for (const child of branch.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n }\n }\n break\n }\n\n case NodeTypes.IF_BRANCH: {\n // Walk children of if branch\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.FOR: {\n // v-for node — walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.ROOT: {\n // Root node — walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.COMPOUND_EXPRESSION: {\n // Compound expression — walk children\n if (node.children) {\n for (const child of node.children) {\n if (typeof child !== 'string') {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n }\n break\n }\n\n case NodeTypes.TEXT_CALL: {\n // Text call node (wrapper for text in v-if, etc.) — walk content\n if (node.content && typeof node.content !== 'string') {\n walkTemplate(node.content, templateLineOffset, sfcContent, results, parentTag)\n }\n // Also walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n default: {\n // For any unknown node type, try to walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n }\n}\n\nfunction processProp(\n prop: VueTemplateProp,\n parentTag: string,\n templateLineOffset: number,\n sfcContent: string,\n results: ExtractedString[],\n): void {\n if (prop.type === NodeTypes.ATTRIBUTE) {\n // Static attribute: title=\"Hello\"\n if (!prop.value) return\n\n const attrName = prop.name\n const attrValue = prop.value.content\n\n if (!attrValue || attrValue.trim().length === 0) return\n\n // CSS attributes get css_class context\n if (CSS_ATTRIBUTES.has(attrName)) {\n results.push({\n value: attrValue,\n line: prop.value.loc.start.line + templateLineOffset - 1,\n column: prop.value.loc.start.column,\n context: 'css_class',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1),\n })\n return\n }\n\n results.push({\n value: attrValue,\n line: prop.value.loc.start.line + templateLineOffset - 1,\n column: prop.value.loc.start.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1),\n })\n } else if (prop.type === NodeTypes.DIRECTIVE) {\n // Dynamic directive: :title=\"expr\", v-bind:title=\"expr\", @click=\"handler\"\n const directiveName = prop.name // 'bind', 'on', 'if', 'for', etc.\n\n // Skip code directives entirely (v-if, v-for, @click, etc.)\n if (CODE_DIRECTIVES.has(directiveName)) return\n\n // For v-bind (:attr=\"expr\"), check if the arg is a code binding\n if (directiveName === 'bind' && prop.arg) {\n const argName = typeof prop.arg.content === 'string' ? prop.arg.content : ''\n if (CODE_DIRECTIVE_ARGS.has(argName)) return\n }\n\n // Extract string literals from directive expressions\n if (prop.exp) {\n const expr = typeof prop.exp.content === 'string' ? prop.exp.content : ''\n const argName = prop.arg && typeof prop.arg.content === 'string' ? prop.arg.content : ''\n\n // Check if expression is a simple string literal: 'text' or \"text\"\n const stringMatch = expr.match(/^(['\"`])(.+)\\1$/)\n if (stringMatch?.[2]) {\n results.push({\n value: stringMatch[2],\n line: prop.exp.loc.start.line + templateLineOffset - 1,\n column: prop.exp.loc.start.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: argName || directiveName,\n surrounding: getSurroundingByLine(sfcContent, prop.exp.loc.start.line + templateLineOffset - 1),\n })\n }\n // If it's a variable or complex expression, skip — scanner doesn't interpret code\n }\n }\n}\n\n// ─── Script Block Parsing ───\n\n// tsx-parser is being built in parallel; define the interface we expect\ntype TsxParserFn = (content: string, fileName: string) => ExtractedString[]\n\nlet _tsxParser: TsxParserFn | null = null\n\nasync function loadTsxParser(): Promise<TsxParserFn | null> {\n if (_tsxParser) return _tsxParser\n try {\n const mod = await import('./tsx-parser.js')\n _tsxParser = mod.parseTsx\n return _tsxParser\n } catch {\n // tsx-parser not yet available — fall back to no script parsing\n return null\n }\n}\n\n/**\n * Map script block lang to a file extension that tsx-parser's getScriptKind understands.\n * Without this, .vue files default to ScriptKind.JS — causing TypeScript type annotations\n * (e.g. defineEmits<{ 'update:modelValue': [...] }>) to be misclassified as object literals.\n */\nfunction resolveScriptFileName(vueFileName: string, lang?: string): string {\n if (lang === 'tsx') return vueFileName.replace(/\\.vue$/, '.tsx')\n if (lang === 'ts') return vueFileName.replace(/\\.vue$/, '.ts')\n if (lang === 'jsx') return vueFileName.replace(/\\.vue$/, '.jsx')\n return vueFileName.replace(/\\.vue$/, '.js')\n}\n\nfunction parseScriptBlock(\n scriptContent: string,\n scriptStartLine: number,\n fileName: string,\n parseTsx: TsxParserFn,\n lang?: string,\n): ExtractedString[] {\n // Resolve filename with correct extension for TypeScript parser\n const resolvedFileName = resolveScriptFileName(fileName, lang)\n const scriptResults = parseTsx(scriptContent, resolvedFileName)\n\n // Adjust line numbers by script block offset\n return scriptResults.map(r => {\n r.line = r.line + scriptStartLine - 1\n r.scope = 'script'\n return r\n })\n}\n\n// ─── Main Export ───\n\nexport async function parseVue(content: string, fileName: string): Promise<ExtractedString[]> {\n const compiler = await loadCompiler()\n const results: ExtractedString[] = []\n\n const { descriptor } = compiler.parse(content, { filename: fileName })\n\n // ─── Template Block ───\n if (descriptor.template) {\n const templateLineOffset = descriptor.template.loc.start.line\n const templateAst = descriptor.template.ast\n\n if (templateAst) {\n walkTemplate(templateAst, templateLineOffset, content, results)\n } else {\n // Fallback: compile template to get AST\n try {\n const compiled = compiler.compileTemplate({\n source: descriptor.template.content,\n filename: fileName,\n id: 'ast-scanner',\n })\n if (compiled.ast) {\n walkTemplate(compiled.ast, templateLineOffset, content, results)\n }\n } catch {\n // If template compilation fails, we skip template extraction\n // This is acceptable — malformed templates shouldn't block scanning\n }\n }\n }\n\n // ─── Script Block ───\n const scriptBlock = descriptor.scriptSetup ?? descriptor.script\n if (scriptBlock) {\n const parseTsx = await loadTsxParser()\n if (parseTsx) {\n const scriptStartLine = scriptBlock.loc.start.line\n const scriptResults = parseScriptBlock(\n scriptBlock.content,\n scriptStartLine,\n fileName,\n parseTsx,\n scriptBlock.lang,\n )\n results.push(...scriptResults)\n }\n }\n\n // Style blocks are intentionally skipped — no content strings in CSS\n\n return results\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentrain/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"mcpName": "io.github.Contentrain/contentrain",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Local-first MCP server for AI-generated content governance — 24 deterministic tools (19 core + 5 media), stdio + HTTP transports, Local / GitHub / GitLab providers",
|
package/dist/paths-BU6E-oDs.mjs
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
//#region src/providers/shared/errors.ts
|
|
2
|
-
/**
|
|
3
|
-
* Unified "is this a 404?" helper for API-backed providers.
|
|
4
|
-
*
|
|
5
|
-
* Two common error shapes in the provider SDKs we use:
|
|
6
|
-
*
|
|
7
|
-
* - **Octokit** (`@octokit/rest`) — rejects with an `Error` that has a
|
|
8
|
-
* top-level `.status` number set to the HTTP status code.
|
|
9
|
-
* - **Gitbeaker** (`@gitbeaker/rest`) — rejects with a plain `Error` whose
|
|
10
|
-
* `.cause` includes `{ response: { status } }`.
|
|
11
|
-
*
|
|
12
|
-
* Both forms converge on `404` meaning "resource missing", so we check
|
|
13
|
-
* both shapes strictly. We deliberately do NOT fall back to substring
|
|
14
|
-
* matching on the error message — that leniency can silently mask other
|
|
15
|
-
* 404-like errors (forbidden repo, deleted project, rate limits) and
|
|
16
|
-
* produce the wrong answer. If either SDK ever stops populating the
|
|
17
|
-
* status field on a legitimate 404, the regression will surface in tests
|
|
18
|
-
* rather than being papered over at the reader layer.
|
|
19
|
-
*/
|
|
20
|
-
function isNotFoundError(error) {
|
|
21
|
-
if (typeof error !== "object" || error === null) return false;
|
|
22
|
-
if (error.status === 404) return true;
|
|
23
|
-
return error.cause?.response?.status === 404;
|
|
24
|
-
}
|
|
25
|
-
//#endregion
|
|
26
|
-
//#region src/providers/shared/paths.ts
|
|
27
|
-
/**
|
|
28
|
-
* Normalise an optional contentRoot — strip leading/trailing slashes,
|
|
29
|
-
* treat `''`, `/` and `undefined` as "no prefix". Used by API-backed
|
|
30
|
-
* providers (GitHub, GitLab, future Bitbucket) to anchor content-relative
|
|
31
|
-
* paths against a repo subdirectory when Contentrain lives under a
|
|
32
|
-
* monorepo path like `apps/web/.contentrain/`.
|
|
33
|
-
*/
|
|
34
|
-
function normaliseContentRoot(raw) {
|
|
35
|
-
if (!raw || raw === "/" || raw === "") return "";
|
|
36
|
-
return raw.replace(/^\/+|\/+$/g, "");
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Resolve a content-root-relative path to a repo-relative path. The result
|
|
40
|
-
* always uses forward slashes and has no leading slash — the form every
|
|
41
|
-
* REST git API consumes for `file_path` / `path` query parameters and the
|
|
42
|
-
* Git Data API tree entries.
|
|
43
|
-
*/
|
|
44
|
-
function resolveRepoPath(contentRoot, relativePath) {
|
|
45
|
-
const prefix = normaliseContentRoot(contentRoot);
|
|
46
|
-
const cleanPath = relativePath.replace(/^\/+/, "");
|
|
47
|
-
return prefix ? `${prefix}/${cleanPath}` : cleanPath;
|
|
48
|
-
}
|
|
49
|
-
//#endregion
|
|
50
|
-
export { isNotFoundError as n, resolveRepoPath as t };
|
|
51
|
-
|
|
52
|
-
//# sourceMappingURL=paths-BU6E-oDs.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"paths-BU6E-oDs.mjs","names":[],"sources":["../src/providers/shared/errors.ts","../src/providers/shared/paths.ts"],"sourcesContent":["/**\n * Unified \"is this a 404?\" helper for API-backed providers.\n *\n * Two common error shapes in the provider SDKs we use:\n *\n * - **Octokit** (`@octokit/rest`) — rejects with an `Error` that has a\n * top-level `.status` number set to the HTTP status code.\n * - **Gitbeaker** (`@gitbeaker/rest`) — rejects with a plain `Error` whose\n * `.cause` includes `{ response: { status } }`.\n *\n * Both forms converge on `404` meaning \"resource missing\", so we check\n * both shapes strictly. We deliberately do NOT fall back to substring\n * matching on the error message — that leniency can silently mask other\n * 404-like errors (forbidden repo, deleted project, rate limits) and\n * produce the wrong answer. If either SDK ever stops populating the\n * status field on a legitimate 404, the regression will surface in tests\n * rather than being papered over at the reader layer.\n */\nexport function isNotFoundError(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) return false\n const direct = (error as { status?: number }).status\n if (direct === 404) return true\n const nested = (error as { cause?: { response?: { status?: number } } }).cause?.response?.status\n return nested === 404\n}\n","/**\n * Normalise an optional contentRoot — strip leading/trailing slashes,\n * treat `''`, `/` and `undefined` as \"no prefix\". Used by API-backed\n * providers (GitHub, GitLab, future Bitbucket) to anchor content-relative\n * paths against a repo subdirectory when Contentrain lives under a\n * monorepo path like `apps/web/.contentrain/`.\n */\nexport function normaliseContentRoot(raw?: string): string {\n if (!raw || raw === '/' || raw === '') return ''\n return raw.replace(/^\\/+|\\/+$/g, '')\n}\n\n/**\n * Resolve a content-root-relative path to a repo-relative path. The result\n * always uses forward slashes and has no leading slash — the form every\n * REST git API consumes for `file_path` / `path` query parameters and the\n * Git Data API tree entries.\n */\nexport function resolveRepoPath(contentRoot: string | undefined, relativePath: string): string {\n const prefix = normaliseContentRoot(contentRoot)\n const cleanPath = relativePath.replace(/^\\/+/, '')\n return prefix ? `${prefix}/${cleanPath}` : cleanPath\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkBA,SAAgB,gBAAgB,OAAyB;AACvD,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AAExD,KADgB,MAA8B,WAC/B,IAAK,QAAO;AAE3B,QADgB,MAAyD,OAAO,UAAU,WACxE;;;;;;;;;;;AChBpB,SAAgB,qBAAqB,KAAsB;AACzD,KAAI,CAAC,OAAO,QAAQ,OAAO,QAAQ,GAAI,QAAO;AAC9C,QAAO,IAAI,QAAQ,cAAc,GAAG;;;;;;;;AAStC,SAAgB,gBAAgB,aAAiC,cAA8B;CAC7F,MAAM,SAAS,qBAAqB,YAAY;CAChD,MAAM,YAAY,aAAa,QAAQ,QAAQ,GAAG;AAClD,QAAO,SAAS,GAAG,OAAO,GAAG,cAAc"}
|