@contentrain/mcp 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +7 -0
  2. package/dist/{apply-manager-CMAoS_nu.mjs → apply-manager-Cf5OWOJ7.mjs} +7 -7
  3. package/dist/{apply-manager-CMAoS_nu.mjs.map → apply-manager-Cf5OWOJ7.mjs.map} +1 -1
  4. package/dist/{branch-lifecycle-B7TgkxGr.mjs → branch-lifecycle-wGJg25LT.mjs} +3 -3
  5. package/dist/{branch-lifecycle-B7TgkxGr.mjs.map → branch-lifecycle-wGJg25LT.mjs.map} +1 -1
  6. package/dist/{config-CPlhDwsv.mjs → config-B5kc2oeV.mjs} +2 -2
  7. package/dist/{config-CPlhDwsv.mjs.map → config-B5kc2oeV.mjs.map} +1 -1
  8. package/dist/{context-CUFulSi2.mjs → context-DIDt_TxX.mjs} +4 -4
  9. package/dist/{context-CUFulSi2.mjs.map → context-DIDt_TxX.mjs.map} +1 -1
  10. package/dist/core/apply-manager.mjs +10 -5
  11. package/dist/core/config.mjs +3 -1
  12. package/dist/core/content-manager.d.mts +1 -9
  13. package/dist/core/content-manager.d.mts.map +1 -1
  14. package/dist/core/content-manager.mjs +4 -1
  15. package/dist/core/context.mjs +6 -2
  16. package/dist/core/graph-builder.mjs +4 -1
  17. package/dist/core/meta-manager.mjs +3 -1
  18. package/dist/core/model-manager.mjs +4 -1
  19. package/dist/core/scan-config.mjs +3 -1
  20. package/dist/core/scanner.mjs +4 -1
  21. package/dist/core/validator.d.mts.map +1 -1
  22. package/dist/core/validator.mjs +6 -2
  23. package/dist/{detect-C8XmJ4S6.mjs → detect-DinOQAmK.mjs} +2 -2
  24. package/dist/{detect-C8XmJ4S6.mjs.map → detect-DinOQAmK.mjs.map} +1 -1
  25. package/dist/{fs-BbuGnVcG.mjs → fs-BnIQ4dKj.mjs} +2 -2
  26. package/dist/{fs-BbuGnVcG.mjs.map → fs-BnIQ4dKj.mjs.map} +1 -1
  27. package/dist/git/branch-lifecycle.mjs +4 -1
  28. package/dist/git/transaction.mjs +8 -3
  29. package/dist/{graph-builder-fPrDDwnu.mjs → graph-builder-tM7zj-SX.mjs} +3 -3
  30. package/dist/{graph-builder-fPrDDwnu.mjs.map → graph-builder-tM7zj-SX.mjs.map} +1 -1
  31. package/dist/{id-BdD4HvMY.mjs → id-DV_T9Ic8.mjs} +3 -5
  32. package/dist/id-DV_T9Ic8.mjs.map +1 -0
  33. package/dist/index.mjs +16 -9
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/{meta-manager-CECrfsKm.mjs → meta-manager--0IwAPCl.mjs} +2 -2
  36. package/dist/{meta-manager-CECrfsKm.mjs.map → meta-manager--0IwAPCl.mjs.map} +1 -1
  37. package/dist/{model-manager-kl78AAMg.mjs → model-manager-DW_fOGRM.mjs} +16 -186
  38. package/dist/model-manager-DW_fOGRM.mjs.map +1 -0
  39. package/dist/{scan-config-AJGxCdNd.mjs → scan-config-QgnJ1-52.mjs} +2 -2
  40. package/dist/{scan-config-AJGxCdNd.mjs.map → scan-config-QgnJ1-52.mjs.map} +1 -1
  41. package/dist/{scanner-BdTiv8sw.mjs → scanner-DPrbU-oK.mjs} +3 -3
  42. package/dist/{scanner-BdTiv8sw.mjs.map → scanner-DPrbU-oK.mjs.map} +1 -1
  43. package/dist/serializer-CnHTtmc1.mjs +2 -0
  44. package/dist/{server-CDv6JNRi.mjs → server-CjK63H6X.mjs} +16 -16
  45. package/dist/{server-CDv6JNRi.mjs.map → server-CjK63H6X.mjs.map} +1 -1
  46. package/dist/server.mjs +16 -9
  47. package/dist/templates/index.mjs +1 -1
  48. package/dist/{templates-3HWi-PTu.mjs → templates-bRPzYq4M.mjs} +1 -1
  49. package/dist/{templates-3HWi-PTu.mjs.map → templates-bRPzYq4M.mjs.map} +1 -1
  50. package/dist/{transaction-CncRbpI9.mjs → transaction-B0V0_SE2.mjs} +6 -6
  51. package/dist/{transaction-CncRbpI9.mjs.map → transaction-B0V0_SE2.mjs.map} +1 -1
  52. package/dist/util/detect.mjs +3 -1
  53. package/dist/util/fs.mjs +2 -1
  54. package/dist/util/id.d.mts +2 -1
  55. package/dist/util/id.d.mts.map +1 -1
  56. package/dist/util/id.mjs +1 -1
  57. package/dist/util/serializer.d.mts +2 -14
  58. package/dist/util/serializer.mjs +2 -2
  59. package/dist/{validator-CyckZAVr.mjs → validator-SQYUV2JP.mjs} +23 -197
  60. package/dist/validator-SQYUV2JP.mjs.map +1 -0
  61. package/package.json +3 -3
  62. package/dist/id-BdD4HvMY.mjs.map +0 -1
  63. package/dist/model-manager-kl78AAMg.mjs.map +0 -1
  64. package/dist/serializer-Dueih-i2.mjs +0 -33
  65. package/dist/serializer-Dueih-i2.mjs.map +0 -1
  66. package/dist/util/serializer.d.mts.map +0 -1
  67. package/dist/validator-CyckZAVr.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-manager-DW_fOGRM.mjs","names":[],"sources":["../src/core/content-manager.ts","../src/core/model-manager.ts"],"sourcesContent":["import type { ModelDefinition, ContentrainConfig, EntryMeta, LocaleStrategy, DocumentEntry } from '@contentrain/types'\nimport { validateSlug, validateEntryId, validateLocale, generateEntryId, parseMarkdownFrontmatter, serializeMarkdownFrontmatter } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { contentrainDir, readDir, readJson, readText, writeJson, writeText } from '../util/fs.js'\nimport { writeMeta, deleteMeta } from './meta-manager.js'\nimport { readModel } from './model-manager.js'\n\n// Re-export for backward compatibility (MCP internal consumers)\nexport { validateSlug, validateEntryId, validateLocale }\nexport { parseMarkdownFrontmatter as parseFrontmatter, serializeMarkdownFrontmatter as serializeFrontmatter } from '@contentrain/types'\n\n// ─── Content paths ───\n\nexport function resolveContentDir(projectRoot: string, model: ModelDefinition): string {\n if (model.content_path) {\n return join(projectRoot, model.content_path)\n }\n return join(contentrainDir(projectRoot), 'content', model.domain, model.id)\n}\n\nexport function resolveLocaleStrategy(model: ModelDefinition): LocaleStrategy {\n return model.locale_strategy ?? 'file'\n}\n\n/** Build the file path for a JSON content file (singleton/collection/dictionary) */\nexport function resolveJsonFilePath(dir: string, model: ModelDefinition, locale: string): string {\n // When i18n is disabled, always use data.json (locale parameter is ignored)\n if (!model.i18n) return join(dir, 'data.json')\n\n switch (resolveLocaleStrategy(model)) {\n case 'suffix': return join(dir, `${model.id}.${locale}.json`)\n case 'directory': return join(dir, locale, `${model.id}.json`)\n case 'none': return join(dir, `${model.id}.json`)\n case 'file':\n default: return join(dir, `${locale}.json`)\n }\n}\n\n/** Build the file path for a markdown document */\nexport function resolveMdFilePath(dir: string, model: ModelDefinition, locale: string, slug: string): string {\n // When i18n is disabled, always use {slug}.md (locale parameter is ignored)\n if (!model.i18n) return join(dir, `${slug}.md`)\n\n switch (resolveLocaleStrategy(model)) {\n case 'suffix': return join(dir, `${slug}.${locale}.md`)\n case 'directory': return join(dir, locale, `${slug}.md`)\n case 'none': return join(dir, `${slug}.md`)\n case 'file':\n default: return join(dir, slug, `${locale}.md`)\n }\n}\n\n// ─── Types ───\n\nexport interface ContentEntry {\n id?: string\n slug?: string\n locale?: string\n data: Record<string, unknown>\n}\n\nexport interface WriteResult {\n action: 'created' | 'updated'\n id?: string\n slug?: string\n locale: string\n}\n\nexport interface DeleteOpts {\n id?: string\n slug?: string\n locale?: string\n keys?: string[]\n}\n\nexport interface ListOpts {\n locale?: string\n filter?: Record<string, unknown>\n resolve?: boolean\n limit?: number\n offset?: number\n}\n\n// ─── Default meta ───\n\nfunction defaultMeta(data?: Record<string, unknown>): EntryMeta {\n const meta: EntryMeta = {\n status: 'draft',\n source: 'agent',\n updated_by: 'contentrain-mcp',\n }\n if (data?.['publish_at'] !== undefined) {\n meta.publish_at = data['publish_at'] as string\n }\n if (data?.['expire_at'] !== undefined) {\n meta.expire_at = data['expire_at'] as string\n }\n return meta\n}\n\n// ─── writeContent ───\n\nexport async function writeContent(\n projectRoot: string,\n model: ModelDefinition,\n entries: ContentEntry[],\n config: ContentrainConfig,\n): Promise<WriteResult[]> {\n const results: WriteResult[] = []\n const defaultLocale = config.locales.default\n\n for (const entry of entries) {\n const locale = entry.locale ?? defaultLocale\n\n // Validate locale\n const localeErr = validateLocale(locale, config)\n if (localeErr) throw new Error(localeErr)\n\n // Validate entry ID if provided\n if (entry.id) {\n const idErr = validateEntryId(entry.id)\n if (idErr) throw new Error(idErr)\n }\n\n // Validate slug if provided\n if (entry.slug) {\n const slugErr = validateSlug(entry.slug)\n if (slugErr) throw new Error(slugErr)\n }\n\n switch (model.kind) {\n case 'singleton': {\n const filePath = resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale)\n await writeJson(filePath, entry.data)\n await writeMeta(projectRoot, model, { locale }, defaultMeta(entry.data))\n results.push({ action: 'updated', locale })\n break\n }\n\n case 'collection': {\n const isNew = !entry.id\n const id = entry.id ?? generateEntryId()\n const filePath = resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale)\n const existing = await readJson<Record<string, Record<string, unknown>>>(filePath) ?? {}\n\n const action: 'created' | 'updated' = (isNew || !(id in existing)) ? 'created' : 'updated'\n existing[id] = entry.data\n\n // Sort by entry ID for canonical output\n const sorted: Record<string, Record<string, unknown>> = {}\n for (const key of Object.keys(existing).toSorted()) {\n sorted[key] = existing[key]!\n }\n\n await writeJson(filePath, sorted)\n await writeMeta(projectRoot, model, { locale, entryId: id }, defaultMeta(entry.data))\n results.push({ action, id, locale })\n break\n }\n\n case 'document': {\n const slug = entry.slug ?? entry.data['slug'] as string\n if (!slug) throw new Error('Document entries require a slug')\n const slugErr = validateSlug(slug)\n if (slugErr) throw new Error(slugErr)\n\n const bodyContent = (entry.data['body'] as string) ?? ''\n const fmData = { ...entry.data }\n delete fmData['body']\n if (!fmData['slug']) fmData['slug'] = slug\n\n // Check if existing\n const docPath = resolveMdFilePath(resolveContentDir(projectRoot, model), model, locale, slug)\n const existingRaw = await readText(docPath)\n const action: 'created' | 'updated' = existingRaw ? 'updated' : 'created'\n\n const mdContent = serializeMarkdownFrontmatter(fmData, bodyContent)\n await writeText(docPath, mdContent)\n await writeMeta(projectRoot, model, { locale, slug }, defaultMeta(entry.data))\n results.push({ action, slug, locale })\n break\n }\n\n case 'dictionary': {\n const filePath = resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale)\n const existing = await readJson<Record<string, string>>(filePath) ?? {}\n const collisions: string[] = []\n for (const key of Object.keys(entry.data as Record<string, string>)) {\n if (key in existing && existing[key] !== (entry.data as Record<string, string>)[key]) {\n collisions.push(key)\n }\n }\n if (collisions.length > 0) {\n throw new Error(\n `Dictionary \"${model.id}\" (${locale}): ${collisions.length} key collision(s) — [${collisions.join(', ')}] already exist with different values. ` +\n `Read existing keys with contentrain_content_list first, or include all keys in a single save call.`,\n )\n }\n const merged = { ...existing, ...entry.data as Record<string, string> }\n await writeJson(filePath, merged)\n await writeMeta(projectRoot, model, { locale }, defaultMeta(entry.data))\n results.push({ action: 'updated', locale })\n break\n }\n }\n }\n\n return results\n}\n\n// ─── deleteContent ───\n\nexport async function deleteContent(\n projectRoot: string,\n model: ModelDefinition,\n opts: DeleteOpts,\n): Promise<string[]> {\n const removed: string[] = []\n const cDir = resolveContentDir(projectRoot, model)\n\n switch (model.kind) {\n case 'collection': {\n if (!opts.id) throw new Error('Collection delete requires an entry ID')\n const locales = opts.locale\n ? [opts.locale]\n : (await readDir(cDir)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', '').replace(`${model.id}.`, ''))\n\n for (const loc of locales) {\n const filePath = resolveJsonFilePath(cDir, model, loc)\n const data = await readJson<Record<string, unknown>>(filePath)\n if (data && opts.id in data) {\n delete data[opts.id]\n await writeJson(filePath, data)\n removed.push(`content/${model.domain}/${model.id}/${loc}.json#${opts.id}`)\n }\n }\n\n for (const loc of locales) {\n await deleteMeta(projectRoot, model, { locale: loc, entryId: opts.id })\n }\n break\n }\n\n case 'document': {\n if (!opts.slug) throw new Error('Document delete requires a slug')\n const slugDelErr = validateSlug(opts.slug)\n if (slugDelErr) throw new Error(slugDelErr)\n\n const strategy = resolveLocaleStrategy(model)\n if (!model.i18n) {\n // No i18n: single {slug}.md file\n await rm(join(cDir, `${opts.slug}.md`), { force: true })\n } else if (strategy === 'file') {\n // slug is a directory — remove entire slug directory\n await rm(join(cDir, opts.slug), { recursive: true, force: true })\n } else if (opts.locale) {\n // Remove specific locale file\n const filePath = resolveMdFilePath(cDir, model, opts.locale, opts.slug)\n await rm(filePath, { force: true })\n } else {\n // Remove all locale files for this slug\n const files = await readDir(strategy === 'directory' ? cDir : cDir)\n for (const f of files) {\n if (strategy === 'suffix' && f.startsWith(`${opts.slug}.`) && f.endsWith('.md')) {\n await rm(join(cDir, f), { force: true })\n } else if (strategy === 'directory') {\n // f is a locale dir — remove slug.md from it\n await rm(join(cDir, f, `${opts.slug}.md`), { force: true })\n } else if (strategy === 'none') {\n if (f === `${opts.slug}.md`) await rm(join(cDir, f), { force: true })\n }\n }\n }\n removed.push(`${model.content_path ?? `content/${model.domain}/${model.id}`}/${opts.slug}`)\n\n await deleteMeta(projectRoot, model, { slug: opts.slug, locale: opts.locale })\n break\n }\n\n case 'singleton': {\n if (model.i18n && !opts.locale) throw new Error('Singleton delete requires a locale when i18n is enabled')\n const locale = opts.locale ?? 'data'\n const filePath = resolveJsonFilePath(cDir, model, locale)\n await rm(filePath, { force: true })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n await deleteMeta(projectRoot, model, { locale: model.i18n ? locale : undefined })\n break\n }\n\n case 'dictionary': {\n if (model.i18n && !opts.locale) throw new Error('Dictionary delete requires a locale when i18n is enabled')\n const locale = opts.locale ?? 'data'\n const filePath = resolveJsonFilePath(cDir, model, locale)\n\n if (opts.keys?.length) {\n // Delete specific keys from dictionary\n const existing = await readJson<Record<string, string>>(filePath) ?? {}\n const notFound: string[] = []\n for (const key of opts.keys) {\n if (key in existing) {\n delete existing[key]\n } else {\n notFound.push(key)\n }\n }\n if (notFound.length > 0) {\n throw new Error(`Dictionary \"${model.id}\" (${locale}): keys not found — [${notFound.join(', ')}]`)\n }\n await writeJson(filePath, existing)\n removed.push(...opts.keys.map(k => `${model.id}/${locale}:${k}`))\n } else {\n // Delete entire locale file\n await rm(filePath, { force: true })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n await deleteMeta(projectRoot, model, { locale: model.i18n ? locale : undefined })\n }\n break\n }\n }\n\n return removed\n}\n\n// ─── listContent ───\n\nexport async function listContent(\n projectRoot: string,\n model: ModelDefinition,\n opts: ListOpts,\n config: ContentrainConfig,\n): Promise<unknown> {\n const cDir = resolveContentDir(projectRoot, model)\n const locale = opts.locale ?? config.locales.default\n\n switch (model.kind) {\n case 'singleton': {\n const data = await readJson<Record<string, unknown>>(resolveJsonFilePath(cDir, model, locale))\n return { kind: 'singleton', data: data ?? {}, locale }\n }\n\n case 'collection': {\n const data = await readJson<Record<string, Record<string, unknown>>>(resolveJsonFilePath(cDir, model, locale)) ?? {}\n let entries: Array<Record<string, unknown>> = Object.entries(data).map(([id, fields]) => {\n const entry: Record<string, unknown> = { id }\n Object.assign(entry, fields)\n return entry\n })\n\n // Filter\n if (opts.filter) {\n entries = entries.filter(entry => {\n for (const [key, value] of Object.entries(opts.filter!)) {\n if (entry[key] !== value) return false\n }\n return true\n })\n }\n\n const total = entries.length\n\n // Pagination\n const offset = opts.offset ?? 0\n const limit = opts.limit ?? entries.length\n entries = entries.slice(offset, offset + limit)\n\n // Resolve relations\n if (opts.resolve && model.fields) {\n entries = await resolveRelations(projectRoot, model, entries, locale)\n }\n\n return { kind: 'collection', data: entries, total, locale, offset, limit }\n }\n\n case 'document': {\n const entries: DocumentEntry[] = []\n const strategy = resolveLocaleStrategy(model)\n\n if (!model.i18n) {\n // No i18n: flat {slug}.md files, no locale in path\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else if (strategy === 'file') {\n // Each slug is a subdirectory containing locale.md files\n const slugDirs = await readDir(cDir)\n for (const slug of slugDirs) {\n const raw = await readText(join(cDir, slug, `${locale}.md`))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else if (strategy === 'suffix') {\n // Files like {slug}.{locale}.md in flat directory\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) {\n if (!f.endsWith(suffix)) continue\n const slug = f.slice(0, -suffix.length)\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else if (strategy === 'directory') {\n // Files in {locale}/ directory as {slug}.md\n const localeDir = join(cDir, locale)\n const files = await readDir(localeDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(localeDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else {\n // none — {slug}.md, no locale in filename\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n }\n\n const total = entries.length\n const offset = opts.offset ?? 0\n const limit = opts.limit ?? entries.length\n const paged = entries.slice(offset, offset + limit)\n\n return { kind: 'document', data: paged, total, locale, offset, limit }\n }\n\n case 'dictionary': {\n const data = await readJson<Record<string, string>>(resolveJsonFilePath(cDir, model, locale)) ?? {}\n return { kind: 'dictionary', data, total_keys: Object.keys(data).length, locale }\n }\n }\n}\n\n// ─── readContent (for external use / relation resolving) ───\n\nexport async function readContent(\n projectRoot: string,\n model: ModelDefinition,\n opts: { locale: string; entryId?: string; slug?: string },\n): Promise<unknown> {\n const cDir = resolveContentDir(projectRoot, model)\n\n switch (model.kind) {\n case 'singleton':\n return readJson<Record<string, unknown>>(resolveJsonFilePath(cDir, model, opts.locale))\n\n case 'collection': {\n if (!opts.entryId) return null\n const data = await readJson<Record<string, Record<string, unknown>>>(resolveJsonFilePath(cDir, model, opts.locale))\n return data?.[opts.entryId] ? { id: opts.entryId, ...data[opts.entryId] } : null\n }\n\n case 'document': {\n if (!opts.slug) return null\n const raw = await readText(resolveMdFilePath(cDir, model, opts.locale, opts.slug))\n if (!raw) return null\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n return { slug: opts.slug, ...frontmatter, body }\n }\n\n case 'dictionary':\n return readJson<Record<string, string>>(resolveJsonFilePath(cDir, model, opts.locale))\n }\n}\n\n// ─── Relation resolving ───\n\nasync function resolveRelations(\n projectRoot: string,\n model: ModelDefinition,\n entries: Array<Record<string, unknown>>,\n locale: string,\n): Promise<Array<Record<string, unknown>>> {\n if (!model.fields) return entries\n\n const relationFields: Array<{ name: string; targetModels: string[]; multi: boolean }> = []\n for (const [name, field] of Object.entries(model.fields)) {\n if (field.type === 'relation' || field.type === 'relations') {\n const targets = Array.isArray(field.model) ? field.model : field.model ? [field.model] : []\n relationFields.push({ name, targetModels: targets, multi: field.type === 'relations' })\n }\n }\n\n if (relationFields.length === 0) return entries\n\n // Load target model contents into cache (with circular reference protection)\n const targetCache: Record<string, Record<string, Record<string, unknown>>> = {}\n const visited = new Set<string>([model.id])\n\n for (const rf of relationFields) {\n for (const targetModelId of rf.targetModels) {\n if (targetCache[targetModelId] || visited.has(targetModelId)) continue\n visited.add(targetModelId)\n\n const targetModel = await readModel(projectRoot, targetModelId)\n if (!targetModel) continue\n\n if (targetModel.kind === 'collection') {\n const targetData = await readJson<Record<string, Record<string, unknown>>>(\n resolveJsonFilePath(resolveContentDir(projectRoot, targetModel), targetModel, locale),\n ) ?? {}\n targetCache[targetModelId] = targetData\n } else if (targetModel.kind === 'document') {\n // Build slug-keyed cache by listing document entries for this locale\n const docCache: Record<string, Record<string, unknown>> = {}\n const cDir = resolveContentDir(projectRoot, targetModel)\n const strategy = resolveLocaleStrategy(targetModel)\n\n if (!targetModel.i18n) {\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else if (strategy === 'file') {\n const slugDirs = await readDir(cDir)\n for (const slug of slugDirs) {\n const raw = await readText(join(cDir, slug, `${locale}.md`))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else if (strategy === 'suffix') {\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) {\n if (!f.endsWith(suffix)) continue\n const slug = f.slice(0, -suffix.length)\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else if (strategy === 'directory') {\n const localeDir = join(cDir, locale)\n const files = await readDir(localeDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(localeDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else {\n // 'none' strategy\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n }\n\n targetCache[targetModelId] = docCache\n }\n }\n }\n\n return entries.map(entry => {\n const resolved = { ...entry }\n for (const rf of relationFields) {\n const value = resolved[rf.name]\n if (!value) continue\n\n if (rf.multi && Array.isArray(value)) {\n resolved[rf.name] = value.map(id => {\n for (const targetModelId of rf.targetModels) {\n const cached = targetCache[targetModelId]?.[id as string]\n if (cached) return { id, ...cached }\n }\n return id\n })\n } else if (typeof value === 'string') {\n for (const targetModelId of rf.targetModels) {\n const cached = targetCache[targetModelId]?.[value]\n if (cached) {\n resolved[rf.name] = { id: value, ...cached }\n break\n }\n }\n }\n }\n return resolved\n })\n}\n","import type { ModelDefinition, ModelSummary } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { z } from 'zod'\nimport { contentrainDir, ensureDir, readDir, readJson, writeJson } from '../util/fs.js'\nimport { resolveContentDir, resolveLocaleStrategy } from './content-manager.js'\n\nexport type { ModelSummary } from '@contentrain/types'\n\nexport async function listModels(projectRoot: string): Promise<ModelSummary[]> {\n const modelsDir = join(contentrainDir(projectRoot), 'models')\n const files = await readDir(modelsDir)\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n\n const models = await Promise.all(\n jsonFiles.map(file => readJson<ModelDefinition>(join(modelsDir, file))),\n )\n\n return models\n .filter((m): m is ModelDefinition => m !== null && !!m.id)\n .map(model => ({\n id: model.id,\n kind: model.kind,\n domain: model.domain,\n i18n: model.i18n,\n fields: model.fields ? Object.keys(model.fields).length : 0,\n }))\n .toSorted((a, b) => a.id.localeCompare(b.id, 'en'))\n}\n\nexport async function readModel(projectRoot: string, modelId: string): Promise<ModelDefinition | null> {\n const filePath = join(contentrainDir(projectRoot), 'models', `${modelId}.json`)\n return readJson<ModelDefinition>(filePath)\n}\n\nasync function countDocumentFileStrategy(\n contentDir: string,\n entries: string[],\n): Promise<{ total: number; locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n let total = 0\n\n const results = await Promise.all(\n entries.map(async (entry) => {\n const localeFiles = await readDir(join(contentDir, entry))\n return localeFiles\n .map(lf => lf.replace(/\\.(json|md|mdx)$/, ''))\n .filter((locale, i) => locale !== localeFiles[i])\n }),\n )\n\n for (const entryLocales of results) {\n for (const locale of entryLocales) {\n locales[locale] = (locales[locale] ?? 0) + 1\n total++\n }\n }\n\n return { total, locales }\n}\n\nasync function countDocumentSuffixStrategy(\n _contentDir: string,\n files: string[],\n): Promise<{ total: number; locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n const slugsByLocale: Record<string, Set<string>> = {}\n\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n // Pattern: {slug}.{locale}.md\n const match = f.match(/^(.+)\\.([a-z]{2}(?:-[A-Z]{2})?)\\.md$/)\n if (!match) continue\n const locale = match[2]!\n if (!slugsByLocale[locale]) slugsByLocale[locale] = new Set()\n slugsByLocale[locale].add(match[1]!)\n }\n\n let total = 0\n for (const [locale, slugs] of Object.entries(slugsByLocale)) {\n locales[locale] = slugs.size\n total += slugs.size\n }\n\n return { total, locales }\n}\n\nasync function countDocumentDirectoryStrategy(\n contentDir: string,\n localeDirs: string[],\n): Promise<{ total: number; locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n let total = 0\n\n const results = await Promise.all(\n localeDirs.map(async (localeDir) => {\n const files = await readDir(join(contentDir, localeDir))\n const mdFiles = files.filter(f => f.endsWith('.md'))\n return { locale: localeDir, count: mdFiles.length }\n }),\n )\n\n for (const { locale, count } of results) {\n locales[locale] = count\n total += count\n }\n\n return { total, locales }\n}\n\nasync function countDocumentNoneStrategy(\n projectRoot: string,\n modelId: string,\n files: string[],\n i18n: boolean,\n): Promise<{ total: number; locales: Record<string, number> }> {\n const mdFiles = files.filter(f => f.endsWith('.md'))\n\n if (!i18n) {\n return { total: mdFiles.length, locales: { _: mdFiles.length } }\n }\n\n // With i18n + none strategy, content files have no locale info.\n // Use meta/{modelId}/{slug}/ directories to determine locale counts.\n const metaDir = join(contentrainDir(projectRoot), 'meta', modelId)\n const locales: Record<string, number> = {}\n let total = 0\n\n const slugs = mdFiles.map(f => f.replace('.md', ''))\n const results = await Promise.all(\n slugs.map(async (slug) => {\n const metaFiles = await readDir(join(metaDir, slug))\n return metaFiles\n .filter(f => f.endsWith('.json'))\n .map(f => f.replace('.json', ''))\n }),\n )\n\n for (const slugLocales of results) {\n for (const locale of slugLocales) {\n locales[locale] = (locales[locale] ?? 0) + 1\n total++\n }\n }\n\n return { total, locales }\n}\n\nasync function countCollectionEntries(\n contentDir: string,\n jsonFiles: string[],\n): Promise<{ total: number; locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n let total = 0\n\n const results = await Promise.all(\n jsonFiles.map(async (file) => {\n const locale = file.replace(/\\.json$/, '')\n const data = await readJson<Record<string, unknown>>(join(contentDir, file))\n return { locale, count: data ? Object.keys(data).length : 0 }\n }),\n )\n\n for (const { locale, count } of results) {\n locales[locale] = count\n total += count\n }\n\n return { total, locales }\n}\n\nexport async function countEntries(\n projectRoot: string,\n model: ModelDefinition,\n): Promise<{ total: number; locales: Record<string, number> }> {\n const cDir = resolveContentDir(projectRoot, model)\n const strategy = resolveLocaleStrategy(model)\n const files = await readDir(cDir)\n\n if (model.kind === 'document') {\n if (!model.i18n) {\n // No i18n: flat {slug}.md files\n return countDocumentNoneStrategy(projectRoot, model.id, files, false)\n }\n\n switch (strategy) {\n case 'file':\n return countDocumentFileStrategy(cDir, files)\n case 'suffix':\n return countDocumentSuffixStrategy(cDir, files)\n case 'directory':\n return countDocumentDirectoryStrategy(cDir, files)\n case 'none':\n return countDocumentNoneStrategy(projectRoot, model.id, files, true)\n }\n }\n\n if (model.kind === 'collection') {\n if (!model.i18n) {\n // Non-i18n: single data.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n return countCollectionEntries(cDir, jsonFiles)\n }\n switch (strategy) {\n case 'suffix': {\n // Files: {model}.{locale}.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n const locales: Record<string, number> = {}\n for (const f of jsonFiles) {\n const match = f.match(/^.+\\.([a-z]{2}(?:-[A-Z]{2})?)\\.json$/)\n if (match) {\n const data = await readJson<Record<string, unknown>>(join(cDir, f))\n locales[match[1]!] = data ? Object.keys(data).length : 0\n }\n }\n const total = Object.values(locales).reduce((a, b) => a + b, 0)\n return { total, locales }\n }\n case 'directory': {\n // Dirs: {locale}/... containing json files\n const locales: Record<string, number> = {}\n let total = 0\n for (const localeDir of files) {\n const subFiles = await readDir(join(cDir, localeDir))\n const jsonFile = subFiles.find(f => f.endsWith('.json'))\n if (jsonFile) {\n const data = await readJson<Record<string, unknown>>(join(cDir, localeDir, jsonFile))\n const count = data ? Object.keys(data).length : 0\n locales[localeDir] = count\n total += count\n }\n }\n return { total, locales }\n }\n case 'none': {\n // Single file: {model}.json — count entries inside\n const noneFile = files.find(f => f === `${model.id}.json`)\n if (!noneFile) return { total: 0, locales: {} }\n const data = await readJson<Record<string, unknown>>(join(cDir, noneFile))\n const count = data ? Object.keys(data).length : 0\n return { total: count, locales: { _: count } }\n }\n default: {\n // 'file': {locale}.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n return countCollectionEntries(cDir, jsonFiles)\n }\n }\n }\n\n // singleton / dictionary — one entry per locale file\n if (!model.i18n) {\n // Non-i18n: single data.json\n const hasData = files.some(f => f === 'data.json')\n return { total: hasData ? 1 : 0, locales: {} }\n }\n\n switch (strategy) {\n case 'suffix': {\n // Files: {model}.{locale}.json\n const locales: Record<string, number> = {}\n for (const f of files) {\n const match = f.match(/^.+\\.([a-z]{2}(?:-[A-Z]{2})?)\\.json$/)\n if (match) locales[match[1]!] = 1\n }\n return { total: Object.keys(locales).length, locales }\n }\n case 'directory': {\n // Dirs: {locale}/...\n const locales: Record<string, number> = {}\n for (const localeDir of files) {\n const subFiles = await readDir(join(cDir, localeDir))\n if (subFiles.some(f => f.endsWith('.json'))) {\n locales[localeDir] = 1\n }\n }\n return { total: Object.keys(locales).length, locales }\n }\n case 'none': {\n // Single file: {model}.json\n const hasFile = files.some(f => f === `${model.id}.json`)\n return { total: hasFile ? 1 : 0, locales: {} }\n }\n default: {\n // 'file': {locale}.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n const locales: Record<string, number> = {}\n for (const file of jsonFiles) {\n locales[file.replace(/\\.json$/, '')] = 1\n }\n return { total: jsonFiles.length, locales }\n }\n }\n}\n\nconst MODEL_FIELD_ORDER = ['id', 'name', 'kind', 'domain', 'i18n', 'description', 'content_path', 'locale_strategy', 'fields']\n\nexport async function writeModel(projectRoot: string, model: ModelDefinition): Promise<void> {\n const filePath = join(contentrainDir(projectRoot), 'models', `${model.id}.json`)\n await writeJson(filePath, model, MODEL_FIELD_ORDER)\n\n // Ensure content and meta directories exist (respects content_path)\n await ensureDir(resolveContentDir(projectRoot, model))\n await ensureDir(join(contentrainDir(projectRoot), 'meta', model.id))\n}\n\nexport async function deleteModel(projectRoot: string, modelId: string): Promise<string[]> {\n const model = await readModel(projectRoot, modelId)\n if (!model) return []\n\n const crDir = contentrainDir(projectRoot)\n const removed: string[] = []\n\n const modelPath = join(crDir, 'models', `${modelId}.json`)\n const contentPath = resolveContentDir(projectRoot, model)\n const metaPath = join(crDir, 'meta', modelId)\n\n await rm(modelPath, { force: true })\n removed.push(`models/${modelId}.json`)\n\n try {\n await rm(contentPath, { recursive: true, force: true })\n removed.push(model.content_path ?? `content/${model.domain}/${modelId}/`)\n } catch {\n // directory may not exist\n }\n\n try {\n await rm(metaPath, { recursive: true, force: true })\n removed.push(`meta/${modelId}/`)\n } catch {\n // directory may not exist\n }\n\n return removed\n}\n\nexport interface ModelReference {\n model: string\n field: string\n type: 'relation' | 'relations'\n}\n\nexport async function checkReferences(projectRoot: string, modelId: string): Promise<ModelReference[]> {\n const summaries = await listModels(projectRoot)\n const others = summaries.filter(s => s.id !== modelId)\n\n const models = await Promise.all(\n others.map(s => readModel(projectRoot, s.id)),\n )\n\n const refs: ModelReference[] = []\n for (const model of models) {\n if (!model?.fields) continue\n\n for (const [fieldName, fieldDef] of Object.entries(model.fields)) {\n if (fieldDef.type !== 'relation' && fieldDef.type !== 'relations') continue\n const targets = Array.isArray(fieldDef.model) ? fieldDef.model : [fieldDef.model]\n if (targets.includes(modelId)) {\n refs.push({ model: model.id, field: fieldName, type: fieldDef.type })\n }\n }\n }\n\n return refs\n}\n\n// ─── Shared Field Type Catalog ───\n\nexport const FIELD_TYPE_ENUM = [\n 'string', 'text', 'email', 'url', 'slug', 'color', 'phone', 'code', 'icon',\n 'markdown', 'richtext',\n 'number', 'integer', 'decimal', 'percent', 'rating',\n 'boolean', 'date', 'datetime',\n 'image', 'video', 'file',\n 'relation', 'relations',\n 'select', 'array', 'object',\n] as const\n\n/**\n * Shared Zod schema for field definitions.\n * Used by both model_save and normalize extract for full parity.\n */\nexport const fieldDefZodSchema: z.ZodType<Record<string, unknown>> = z.record(z.string(), z.object({\n type: z.enum(FIELD_TYPE_ENUM).describe('Field type from the 27-type catalog'),\n required: z.boolean().optional(),\n unique: z.boolean().optional(),\n default: z.unknown().optional(),\n min: z.number().optional(),\n max: z.number().optional(),\n pattern: z.string().optional(),\n options: z.array(z.string()).optional(),\n model: z.union([z.string(), z.array(z.string())]).optional(),\n items: z.union([z.string(), z.lazy(() => z.record(z.string(), z.unknown()))]).optional(),\n fields: z.lazy(() => z.record(z.string(), z.unknown())).optional(),\n accept: z.string().optional(),\n maxSize: z.number().optional(),\n description: z.string().optional(),\n}).refine(\n (f) => {\n if ((f.type === 'relation' || f.type === 'relations') && !f.model) return false\n if (f.type === 'select' && (!f.options || f.options.length === 0)) return false\n return true\n },\n { message: 'relation/relations requires \"model\", select requires non-empty \"options\"' },\n))\n\n// ─── Model Validation (shared between model_save and normalize extract) ───\n\nconst VALID_FIELD_TYPES = new Set<string>(FIELD_TYPE_ENUM)\n\n/**\n * Validate a model definition before writing.\n * Returns array of error messages (empty = valid).\n * Used by both model_save tool and normalize extract.\n */\nexport function validateModelDefinition(input: { id: string; kind: string; fields?: Record<string, unknown> }): string[] {\n const errors: string[] = []\n\n // ID format: kebab-case\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.id)) {\n errors.push(`Invalid model ID \"${input.id}\": must be kebab-case`)\n }\n\n // Dictionary models cannot have fields\n if (input.kind === 'dictionary' && input.fields && Object.keys(input.fields).length > 0) {\n errors.push('Dictionary models cannot have fields. Dictionaries store flat key-value pairs.')\n }\n\n // Fields validation\n if (input.fields) {\n for (const [fieldName, fieldDef] of Object.entries(input.fields)) {\n const def = fieldDef as { type?: string; model?: unknown; options?: unknown }\n\n // Field name format\n if (!/^[a-z][a-z0-9_]*$/.test(fieldName)) {\n errors.push(`Field \"${fieldName}\": invalid name — must be snake_case starting with letter`)\n }\n\n // Type check\n if (!def.type || !VALID_FIELD_TYPES.has(def.type)) {\n errors.push(`Field \"${fieldName}\": invalid type \"${def.type}\"`)\n }\n\n // Relation requires model\n if ((def.type === 'relation' || def.type === 'relations') && !def.model) {\n errors.push(`Field \"${fieldName}\": ${def.type} type requires \"model\" property`)\n }\n\n // Select requires options\n if (def.type === 'select' && (!def.options || !Array.isArray(def.options) || def.options.length === 0)) {\n errors.push(`Field \"${fieldName}\": select type requires non-empty \"options\" array`)\n }\n }\n }\n\n return errors\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { r as pathExists } from "./fs-BbuGnVcG.mjs";
1
+ import { r as pathExists } from "./fs-BnIQ4dKj.mjs";
2
2
  import { basename, extname, join } from "node:path";
3
3
  import { readdir } from "node:fs/promises";
4
4
  //#region src/core/scan-config.ts
@@ -156,4 +156,4 @@ async function discoverFiles(projectRoot, options) {
156
156
  //#endregion
157
157
  export { classifyFile as a, autoDetectSourceDirs as i, SCAN_EXTENSIONS as n, discoverFiles as o, SCAN_IGNORE_DIRS as r, MAX_SCAN_FILES as t };
158
158
 
159
- //# sourceMappingURL=scan-config-AJGxCdNd.mjs.map
159
+ //# sourceMappingURL=scan-config-QgnJ1-52.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"scan-config-AJGxCdNd.mjs","names":[],"sources":["../src/core/scan-config.ts"],"sourcesContent":["import { readdir } from 'node:fs/promises'\nimport { join, extname, basename } from 'node:path'\nimport { pathExists } from '../util/fs.js'\n\n// ─── Shared Scan Constants ───\n\n/** File extensions to scan across all JS/TS ecosystem projects */\nexport const SCAN_EXTENSIONS = new Set([\n '.tsx', '.jsx', '.vue', '.ts', '.js', '.mjs', '.astro', '.svelte',\n])\n\n/** Directory names to always exclude from scanning */\nexport const SCAN_IGNORE_DIRS = new Set([\n // Package managers / deps\n 'node_modules', '.pnpm',\n // Build outputs\n 'dist', 'build', 'out', '.output',\n // Framework caches\n '.nuxt', '.next', '.svelte-kit', '.expo', '.turbo', '.parcel-cache', '.vercel', '.netlify',\n // Test / coverage\n 'coverage', '__tests__', '__mocks__',\n // VCS / IDE\n '.git', '.vscode', '.idea',\n // Contentrain\n '.contentrain',\n])\n\n/** Max files per scan operation */\nexport const MAX_SCAN_FILES = 500\n\n/** File patterns to skip regardless of extension */\nconst SKIP_FILE_RE = /\\.(test|spec)\\.[^.]+$|\\.d\\.ts$|\\.min\\.[^.]+$/\n\n// ─── File Classification ───\n// Covers: React, Next.js, Nuxt, Vue, Astro, SvelteKit, Remix,\n// React Native/Expo, NestJS, Express, Fastify, Koa, Hapi\n\n/** Directories that represent entry points / pages / screens */\nconst PAGE_DIR_NAMES = new Set([\n // Frontend routing\n 'pages', 'routes', 'screens', 'views',\n // Backend entry points\n 'controllers', 'handlers', 'resolvers',\n])\n\n/** Directories that represent reusable components / modules */\nconst COMPONENT_DIR_NAMES = new Set([\n // UI components\n 'components', 'ui', 'widgets', 'elements',\n // Feature modules\n 'features', 'modules',\n // Backend services\n 'services', 'providers',\n // Shared / common\n 'shared', 'common',\n])\n\n/** Directories that represent layouts / templates */\nconst LAYOUT_DIR_NAMES = new Set([\n 'layouts', 'templates',\n])\n\n/** Next.js App Router special files that are page-like */\nconst NEXTJS_PAGE_FILES = new Set([\n 'page.tsx', 'page.jsx', 'page.ts', 'page.js',\n 'layout.tsx', 'layout.jsx', 'layout.ts', 'layout.js',\n 'error.tsx', 'error.jsx',\n 'loading.tsx', 'loading.jsx',\n 'not-found.tsx', 'not-found.jsx',\n])\n\n/** Classify a file into page/component/layout/other based on path heuristics */\nexport function classifyFile(relPath: string): 'page' | 'component' | 'layout' | 'other' {\n const parts = relPath.split('/')\n const fileName = parts[parts.length - 1] ?? ''\n\n // Next.js App Router: files in app/ with special names are pages\n if (parts.includes('app') && NEXTJS_PAGE_FILES.has(fileName)) {\n return fileName.startsWith('layout') ? 'layout' : 'page'\n }\n\n // Check directory names in path (check layout first — more specific)\n for (const part of parts) {\n const lower = part.toLowerCase()\n if (LAYOUT_DIR_NAMES.has(lower)) return 'layout'\n if (PAGE_DIR_NAMES.has(lower)) return 'page'\n if (COMPONENT_DIR_NAMES.has(lower)) return 'component'\n }\n\n return 'other'\n}\n\n// ─── Source Directory Detection ───\n\n/** Common source directories across all JS/TS project types */\nconst AUTO_DETECT_DIRS = [\n // Standard source\n 'src', 'app', 'lib',\n // Frontend specific\n 'pages', 'components', 'layouts', 'views',\n // Mobile\n 'screens',\n // Backend\n 'modules', 'routes', 'controllers', 'services',\n // Shared\n 'features', 'shared', 'common',\n // Hooks / composables\n 'hooks', 'composables',\n // Stores\n 'stores',\n]\n\n/** Auto-detect which source directories exist in the project */\nexport async function autoDetectSourceDirs(projectRoot: string): Promise<string[]> {\n const found: string[] = []\n for (const dir of AUTO_DETECT_DIRS) {\n if (await pathExists(join(projectRoot, dir))) {\n found.push(dir)\n }\n }\n return found.length > 0 ? found : ['.']\n}\n\n// ─── File Discovery ───\n\nexport interface DiscoverFilesOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n}\n\n/**\n * Discover source files matching scan criteria.\n * Returns relative paths (relative to projectRoot).\n */\nexport async function discoverFiles(\n projectRoot: string,\n options?: DiscoverFilesOptions,\n): Promise<string[]> {\n const extensions = options?.include\n ? new Set(options.include.map(e => e.startsWith('.') ? e : `.${e}`))\n : SCAN_EXTENSIONS\n const extraExcludes = new Set(options?.exclude ?? [])\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n\n const files: string[] = []\n\n for (const dir of scanDirs) {\n const absDir = join(projectRoot, dir)\n if (!(await pathExists(absDir))) continue\n\n let entries: string[]\n try {\n entries = await readdir(absDir, { recursive: true }) as unknown as string[]\n } catch {\n continue\n }\n\n for (const entry of entries) {\n if (files.length >= MAX_SCAN_FILES) break\n\n const fileName = basename(entry)\n\n // Check if any path segment is excluded\n const pathSegments = entry.split('/')\n if (pathSegments.some(seg => SCAN_IGNORE_DIRS.has(seg) || extraExcludes.has(seg))) continue\n\n // Check extension\n if (!extensions.has(extname(fileName))) continue\n\n // Skip test/spec/declaration/minified files\n if (SKIP_FILE_RE.test(fileName)) continue\n\n files.push(join(dir, entry))\n }\n\n if (files.length >= MAX_SCAN_FILES) break\n }\n\n return files.toSorted((a, b) => a.localeCompare(b)).slice(0, MAX_SCAN_FILES)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scan-config-QgnJ1-52.mjs","names":[],"sources":["../src/core/scan-config.ts"],"sourcesContent":["import { readdir } from 'node:fs/promises'\nimport { join, extname, basename } from 'node:path'\nimport { pathExists } from '../util/fs.js'\n\n// ─── Shared Scan Constants ───\n\n/** File extensions to scan across all JS/TS ecosystem projects */\nexport const SCAN_EXTENSIONS = new Set([\n '.tsx', '.jsx', '.vue', '.ts', '.js', '.mjs', '.astro', '.svelte',\n])\n\n/** Directory names to always exclude from scanning */\nexport const SCAN_IGNORE_DIRS = new Set([\n // Package managers / deps\n 'node_modules', '.pnpm',\n // Build outputs\n 'dist', 'build', 'out', '.output',\n // Framework caches\n '.nuxt', '.next', '.svelte-kit', '.expo', '.turbo', '.parcel-cache', '.vercel', '.netlify',\n // Test / coverage\n 'coverage', '__tests__', '__mocks__',\n // VCS / IDE\n '.git', '.vscode', '.idea',\n // Contentrain\n '.contentrain',\n])\n\n/** Max files per scan operation */\nexport const MAX_SCAN_FILES = 500\n\n/** File patterns to skip regardless of extension */\nconst SKIP_FILE_RE = /\\.(test|spec)\\.[^.]+$|\\.d\\.ts$|\\.min\\.[^.]+$/\n\n// ─── File Classification ───\n// Covers: React, Next.js, Nuxt, Vue, Astro, SvelteKit, Remix,\n// React Native/Expo, NestJS, Express, Fastify, Koa, Hapi\n\n/** Directories that represent entry points / pages / screens */\nconst PAGE_DIR_NAMES = new Set([\n // Frontend routing\n 'pages', 'routes', 'screens', 'views',\n // Backend entry points\n 'controllers', 'handlers', 'resolvers',\n])\n\n/** Directories that represent reusable components / modules */\nconst COMPONENT_DIR_NAMES = new Set([\n // UI components\n 'components', 'ui', 'widgets', 'elements',\n // Feature modules\n 'features', 'modules',\n // Backend services\n 'services', 'providers',\n // Shared / common\n 'shared', 'common',\n])\n\n/** Directories that represent layouts / templates */\nconst LAYOUT_DIR_NAMES = new Set([\n 'layouts', 'templates',\n])\n\n/** Next.js App Router special files that are page-like */\nconst NEXTJS_PAGE_FILES = new Set([\n 'page.tsx', 'page.jsx', 'page.ts', 'page.js',\n 'layout.tsx', 'layout.jsx', 'layout.ts', 'layout.js',\n 'error.tsx', 'error.jsx',\n 'loading.tsx', 'loading.jsx',\n 'not-found.tsx', 'not-found.jsx',\n])\n\n/** Classify a file into page/component/layout/other based on path heuristics */\nexport function classifyFile(relPath: string): 'page' | 'component' | 'layout' | 'other' {\n const parts = relPath.split('/')\n const fileName = parts[parts.length - 1] ?? ''\n\n // Next.js App Router: files in app/ with special names are pages\n if (parts.includes('app') && NEXTJS_PAGE_FILES.has(fileName)) {\n return fileName.startsWith('layout') ? 'layout' : 'page'\n }\n\n // Check directory names in path (check layout first — more specific)\n for (const part of parts) {\n const lower = part.toLowerCase()\n if (LAYOUT_DIR_NAMES.has(lower)) return 'layout'\n if (PAGE_DIR_NAMES.has(lower)) return 'page'\n if (COMPONENT_DIR_NAMES.has(lower)) return 'component'\n }\n\n return 'other'\n}\n\n// ─── Source Directory Detection ───\n\n/** Common source directories across all JS/TS project types */\nconst AUTO_DETECT_DIRS = [\n // Standard source\n 'src', 'app', 'lib',\n // Frontend specific\n 'pages', 'components', 'layouts', 'views',\n // Mobile\n 'screens',\n // Backend\n 'modules', 'routes', 'controllers', 'services',\n // Shared\n 'features', 'shared', 'common',\n // Hooks / composables\n 'hooks', 'composables',\n // Stores\n 'stores',\n]\n\n/** Auto-detect which source directories exist in the project */\nexport async function autoDetectSourceDirs(projectRoot: string): Promise<string[]> {\n const found: string[] = []\n for (const dir of AUTO_DETECT_DIRS) {\n if (await pathExists(join(projectRoot, dir))) {\n found.push(dir)\n }\n }\n return found.length > 0 ? found : ['.']\n}\n\n// ─── File Discovery ───\n\nexport interface DiscoverFilesOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n}\n\n/**\n * Discover source files matching scan criteria.\n * Returns relative paths (relative to projectRoot).\n */\nexport async function discoverFiles(\n projectRoot: string,\n options?: DiscoverFilesOptions,\n): Promise<string[]> {\n const extensions = options?.include\n ? new Set(options.include.map(e => e.startsWith('.') ? e : `.${e}`))\n : SCAN_EXTENSIONS\n const extraExcludes = new Set(options?.exclude ?? [])\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n\n const files: string[] = []\n\n for (const dir of scanDirs) {\n const absDir = join(projectRoot, dir)\n if (!(await pathExists(absDir))) continue\n\n let entries: string[]\n try {\n entries = await readdir(absDir, { recursive: true }) as unknown as string[]\n } catch {\n continue\n }\n\n for (const entry of entries) {\n if (files.length >= MAX_SCAN_FILES) break\n\n const fileName = basename(entry)\n\n // Check if any path segment is excluded\n const pathSegments = entry.split('/')\n if (pathSegments.some(seg => SCAN_IGNORE_DIRS.has(seg) || extraExcludes.has(seg))) continue\n\n // Check extension\n if (!extensions.has(extname(fileName))) continue\n\n // Skip test/spec/declaration/minified files\n if (SKIP_FILE_RE.test(fileName)) continue\n\n files.push(join(dir, entry))\n }\n\n if (files.length >= MAX_SCAN_FILES) break\n }\n\n return files.toSorted((a, b) => a.localeCompare(b)).slice(0, MAX_SCAN_FILES)\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { o as readText } from "./fs-BbuGnVcG.mjs";
2
- import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-AJGxCdNd.mjs";
1
+ import { o as readText } from "./fs-BnIQ4dKj.mjs";
2
+ import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-QgnJ1-52.mjs";
3
3
  import { t as parseTsx } from "./tsx-parser-CYVyBWjn.mjs";
4
4
  import { extname, join } from "node:path";
5
5
  //#region src/core/ast-scanner/pre-filter.ts
@@ -616,4 +616,4 @@ async function scanSummary(projectRoot, options) {
616
616
  //#endregion
617
617
  export { scanCandidates as n, scanSummary as r, isNonContent as t };
618
618
 
619
- //# sourceMappingURL=scanner-BdTiv8sw.mjs.map
619
+ //# sourceMappingURL=scanner-DPrbU-oK.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"scanner-BdTiv8sw.mjs","names":["FILE_EXT_RE"],"sources":["../src/core/ast-scanner/pre-filter.ts","../src/core/ast-scanner/index.ts","../src/core/scanner.ts"],"sourcesContent":["import type { ExtractedString, PreFilterRule } from './types.js'\n\n// ─── Pre-filter Result ───\n\nexport interface PreFilterResult {\n /** Strings that passed the pre-filter (candidates for agent) */\n candidates: ExtractedString[]\n /** Total number of strings removed by the pre-filter */\n filtered: number\n /** Breakdown: reason string -> count of strings filtered for that reason */\n filterReasons: Record<string, number>\n}\n\n// ─── Value-based patterns (deterministic, no intelligence) ───\n\nconst PURE_NUMBER_RE = /^\\d+(\\.\\d+)?$/\nconst HEX_COLOR_RE = /^#[0-9a-f]{3,8}$/i\nconst FILE_EXT_RE = /\\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i\n\n// ─── HTML / component prop technical values ───\n\nconst HTML_PROP_VALUES = new Set([\n // CSS / variant keywords\n 'class', 'variant', 'secondary', 'outline', 'ghost', 'destructive', 'default', 'primary',\n // Link / target\n '_blank', 'noopener', 'noreferrer',\n // Input / button types\n 'button', 'submit', 'reset', 'text', 'numeric', 'password', 'email', 'checkbox', 'radio',\n // Layout / display\n 'hidden', 'none', 'auto', 'inherit', 'initial',\n // Size tokens\n 'sm', 'md', 'lg', 'xl', 'xs', '2xl', '3xl',\n // Icon variants\n 'icon', 'icon-sm', 'icon-lg',\n])\n\n// ─── Slot / event technical names (single lowercase word) ───\n\nconst SLOT_EVENT_NAMES = new Set([\n 'header', 'footer', 'default', 'trigger', 'content',\n 'sidebar', 'overlay', 'body', 'actions',\n 'click', 'change', 'input', 'focus', 'blur', 'submit',\n 'mounted', 'unmounted', 'updated',\n])\n\n// ─── Tailwind / CSS utility detection ───\n\nconst TAILWIND_SEGMENT_RE = /^(?:bg-|text-|border-|flex|grid|p-|px-|py-|pt-|pb-|pl-|pr-|m-|mx-|my-|mt-|mb-|ml-|mr-|rounded|shadow|w-|h-|min-|max-|gap-|space-|items-|justify-|self-|overflow-|z-|opacity-|transition|duration-|ease-|animate-|font-|leading-|tracking-|decoration-|underline|line-through|uppercase|lowercase|capitalize|truncate|whitespace-|break-|sr-only|not-sr-only|hover:|focus:|active:|disabled:|dark:|sm:|md:|lg:|xl:|2xl:|group-|peer-|ring-|outline-|divide-|table-|col-|row-|aspect-|object-|inset-|top-|right-|bottom-|left-|translate-|rotate-|scale-|skew-|origin-|cursor-|select-|resize-|fill-|stroke-|block|inline|absolute|relative|fixed|sticky|static|float-|clear-|isolate|visible|invisible|grow|shrink|basis-|order-|place-)/\n\n/**\n * Returns true if the string looks like a CSS class list (Tailwind or similar).\n * Requires 2+ space-separated segments with majority matching utility patterns.\n */\nfunction isCssClassList(value: string): boolean {\n const segments = value.trim().split(/\\s+/)\n if (segments.length < 2) return false\n let matched = 0\n for (const seg of segments) {\n if (TAILWIND_SEGMENT_RE.test(seg)) matched++\n }\n return matched / segments.length >= 0.5\n}\n\n/**\n * Returns true if the string is a single CSS utility token (e.g. \"bg-blue-500\").\n */\nfunction isSingleCssUtility(value: string): boolean {\n const trimmed = value.trim()\n // Must be a single token, no spaces\n if (trimmed.includes(' ')) return false\n return TAILWIND_SEGMENT_RE.test(trimmed)\n}\n\n// ─── Pre-filter Rules ───\n\nconst PRE_FILTER_RULES: PreFilterRule[] = [\n // Context-based rules (AST-determined, 100% accurate)\n { context: 'import_path', reason: 'import_path' },\n { context: 'type_annotation', reason: 'type_annotation' },\n { context: 'css_class', reason: 'css_class' },\n { context: 'css_utility_call', reason: 'css_utility_call' },\n { context: 'console_call', reason: 'console_call' },\n { context: 'test_assertion', reason: 'test_assertion' },\n { context: 'switch_case', reason: 'switch_case' },\n\n // Value-based rules (structural, not heuristic)\n { condition: (v) => v.length <= 1, reason: 'single_char' },\n { condition: (v) => PURE_NUMBER_RE.test(v), reason: 'pure_number' },\n { condition: (v) => v.startsWith('--'), reason: 'cli_flag' },\n { condition: (v) => HEX_COLOR_RE.test(v), reason: 'hex_color' },\n { condition: (v) => FILE_EXT_RE.test(v), reason: 'file_extension' },\n\n // HTML / component prop technical values (exact match, case-insensitive)\n { condition: (v) => HTML_PROP_VALUES.has(v.toLowerCase()), reason: 'html_prop_value' },\n\n // CSS class lists (Tailwind-style multi-segment strings)\n { condition: (v) => isCssClassList(v), reason: 'css_class_list' },\n\n // Single CSS utility token (e.g. \"bg-blue-500\", \"rounded-lg\")\n { condition: (v) => isSingleCssUtility(v), reason: 'css_utility_token' },\n\n // Slot / event technical names (single lowercase word only)\n { condition: (v) => {\n const lower = v.toLowerCase()\n return v === lower && !v.includes(' ') && SLOT_EVENT_NAMES.has(lower)\n }, reason: 'slot_event_name' },\n]\n\n// ─── Public API ───\n\n/**\n * Structural pre-filter: removes strings that are 100% NOT content.\n *\n * Conservative — when in doubt, INCLUDE the string.\n * Returns candidates (passed), count of filtered, and reason breakdown.\n */\nexport function applyPreFilter(strings: ExtractedString[]): PreFilterResult {\n const candidates: ExtractedString[] = []\n const filterReasons: Record<string, number> = {}\n let filtered = 0\n\n for (const str of strings) {\n const matchedRule = findMatchingRule(str)\n\n if (matchedRule) {\n filtered++\n filterReasons[matchedRule.reason] = (filterReasons[matchedRule.reason] ?? 0) + 1\n } else {\n candidates.push(str)\n }\n }\n\n return { candidates, filtered, filterReasons }\n}\n\n/**\n * Find the first pre-filter rule that matches this string.\n * Returns the rule if matched, undefined if the string should pass through.\n */\nfunction findMatchingRule(str: ExtractedString): PreFilterRule | undefined {\n for (const rule of PRE_FILTER_RULES) {\n if (rule.context !== undefined && str.context === rule.context) {\n return rule\n }\n if (rule.condition !== undefined && rule.condition(str.value)) {\n return rule\n }\n }\n return undefined\n}\n","import type { ExtractedString } from './types.js'\nimport { applyPreFilter } from './pre-filter.js'\nimport { parseTsx } from './tsx-parser.js'\n\n// ─── Extension sets ───\n\nconst TSX_EXTENSIONS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs'])\nconst VUE_EXTENSIONS = new Set(['.vue'])\nconst SVELTE_EXTENSIONS = new Set(['.svelte'])\nconst ASTRO_EXTENSIONS = new Set(['.astro'])\n\n// ─── Lazy-loaded parsers ───\n\n/**\n * Lazily import vue-parser. Returns undefined if not available\n * (@vue/compiler-sfc is an optional dependency).\n */\nasync function loadVueParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]> | ExtractedString[]) | undefined> {\n try {\n const mod = await import('./vue-parser.js')\n return mod.parseVue\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import svelte-parser. Returns undefined if not available\n * (svelte is an optional dependency).\n */\nasync function loadSvelteParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./svelte-parser.js')\n return mod.parseSvelte\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import astro-parser. Returns undefined if not available\n * (@astrojs/compiler is an optional dependency).\n */\nasync function loadAstroParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./astro-parser.js')\n return mod.parseAstro\n } catch {\n return undefined\n }\n}\n\n// ─── Regex fallback for unknown extensions ───\n\nconst SURROUNDING_MAX = 120\n\n/**\n * Minimal regex-based extractor for file types without AST parsers.\n * Extracts quoted strings and tag text — conservative, low accuracy.\n * Used as fallback when dedicated parsers are unavailable.\n */\nfunction extractWithRegex(content: string, _filePath: string): ExtractedString[] {\n const lines = content.split('\\n')\n const results: ExtractedString[] = []\n\n // Simple quoted string extraction\n const stringRe = /(['\"`])(?:(?!\\1|\\\\).|\\\\.)*?\\1/g\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!\n const trimmed = line.trim()\n\n // Skip comments, imports\n if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) continue\n if (/^\\s*(import|export)\\s/.test(line)) continue\n\n let match: RegExpExecArray | null\n stringRe.lastIndex = 0\n\n while ((match = stringRe.exec(line)) !== null) {\n // Remove surrounding quotes\n const raw = match[0]\n const value = raw.slice(1, -1)\n if (value.length === 0) continue\n\n const surrounding = buildSurrounding(lines, i)\n\n results.push({\n value,\n line: i + 1,\n column: match.index + 1,\n context: 'other',\n scope: 'script',\n parent: '',\n surrounding,\n })\n }\n\n // Tag text extraction: >text<\n const tagTextRe = />([^<>{]+)</g\n let tagMatch: RegExpExecArray | null\n tagTextRe.lastIndex = 0\n\n while ((tagMatch = tagTextRe.exec(line)) !== null) {\n const text = tagMatch[1]!.trim()\n if (text.length === 0) continue\n if (/^[\\s\\W]*$/.test(text) && !/[a-zA-Z]/.test(text)) continue\n\n results.push({\n value: text,\n line: i + 1,\n column: tagMatch.index + 1,\n context: 'template_text',\n scope: 'template',\n parent: '',\n surrounding: buildSurrounding(lines, i),\n })\n }\n }\n\n return results\n}\n\nfunction buildSurrounding(lines: string[], lineIdx: number): string {\n const start = Math.max(0, lineIdx - 1)\n const end = Math.min(lines.length - 1, lineIdx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const line = lines[i]\n if (line !== undefined) {\n parts.push(line)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n\n// ─── Public API ───\n\n/**\n * Extract strings from a source file with structural context.\n *\n * Routes to the correct parser based on file extension:\n * - .tsx/.jsx/.ts/.js/.mjs -> tsx-parser (AST-based)\n * - .vue -> vue-parser (lazy-loaded, AST-based)\n * - .svelte -> svelte-parser (lazy-loaded, AST-based)\n * - .astro -> astro-parser (lazy-loaded, AST-based)\n * - unknown -> empty array\n *\n * Falls back to regex extraction when the dedicated parser's\n * optional dependency is not installed.\n *\n * Applies structural pre-filter to remove 100% non-content strings.\n * Returns only candidates that should be sent to the agent.\n */\nexport async function extractStrings(\n filePath: string,\n content: string,\n ext: string,\n): Promise<ExtractedString[]> {\n const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`\n let rawStrings: ExtractedString[]\n\n if (TSX_EXTENSIONS.has(normalizedExt)) {\n rawStrings = parseTsx(content, filePath)\n } else if (VUE_EXTENSIONS.has(normalizedExt)) {\n const parseVue = await loadVueParser()\n if (parseVue) {\n rawStrings = await parseVue(content, filePath)\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (SVELTE_EXTENSIONS.has(normalizedExt)) {\n const parseSvelte = await loadSvelteParser()\n if (parseSvelte) {\n try {\n rawStrings = await parseSvelte(content, filePath)\n } catch {\n // svelte/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (ASTRO_EXTENSIONS.has(normalizedExt)) {\n const parseAstro = await loadAstroParser()\n if (parseAstro) {\n try {\n rawStrings = await parseAstro(content, filePath)\n } catch {\n // @astrojs/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n return []\n }\n\n // Apply structural pre-filter\n const { candidates } = applyPreFilter(rawStrings)\n return candidates\n}\n\n// Re-export types for convenience\nexport type { ExtractedString, StructuralContext, PreFilterRule } from './types.js'\nexport { applyPreFilter } from './pre-filter.js'\nexport type { PreFilterResult } from './pre-filter.js'\n","import type {\n StringContext,\n ScanCandidate,\n DuplicateGroup,\n ScanCandidatesResult,\n ScanSummaryResult,\n} from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText } from '../util/fs.js'\nimport {\n autoDetectSourceDirs,\n discoverFiles,\n} from './scan-config.js'\nimport { extractStrings } from './ast-scanner/index.js'\nimport type { StructuralContext } from './ast-scanner/types.js'\n\n// ─── Options ───\n\nexport interface ScanOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n limit?: number\n offset?: number\n min_length?: number\n max_length?: number\n}\n\n// ─── Constants ───\n\nconst DEFAULT_LIMIT = 50\nconst DEFAULT_OFFSET = 0\nconst DEFAULT_MIN_LENGTH = 2\nconst DEFAULT_MAX_LENGTH = 500\nconst SUMMARY_SAMPLE_SIZE = 10\n\n// ─── Context mapping: StructuralContext → StringContext ───\n\nconst CONTEXT_MAP: Record<StructuralContext, StringContext> = {\n 'template_text': 'template_text',\n 'template_attribute': 'template_attribute',\n 'jsx_text': 'jsx_text',\n 'jsx_attribute': 'jsx_attribute',\n 'variable_assignment': 'variable_assignment',\n 'object_property': 'object_value',\n 'function_argument': 'function_argument',\n 'array_element': 'other',\n 'enum_value': 'other',\n 'template_literal': 'other',\n 'switch_case': 'other',\n 'other': 'other',\n // Pre-filtered contexts should not reach here, but map them just in case\n 'import_path': 'other',\n 'type_annotation': 'other',\n 'css_class': 'other',\n 'css_utility_call': 'other',\n 'console_call': 'other',\n 'test_assertion': 'other',\n}\n\n// ─── Legacy Pre-filter (kept exported for backward compatibility) ───\n\nconst IMPORT_PATH_RE = /^(\\.\\/|\\.\\.\\/|@\\/|@[a-z]|[a-z][a-z0-9-]*\\/)/\nconst CSS_CLASS_PREFIX_RE = /^(flex|grid|block|inline|hidden|absolute|relative|fixed|sticky|p-|m-|w-|h-|bg-|text-|border-|rounded|shadow|gap-|space-|font-|leading-|tracking-|opacity-|z-|overflow-|cursor-|transition|transform|animate-|hover:|focus:|active:|dark:|sm:|md:|lg:|xl:)/\nconst URL_ROUTE_RE = /^(https?:\\/\\/|\\/api\\/|\\/v[0-9]|#\\/|mailto:|tel:)/\nconst ROUTE_LIKE_RE = /^\\/[a-z0-9/:_-]*$/i\nconst COLOR_RE = /^(#[0-9a-f]{3,8}|rgba?\\(|hsla?\\(|transparent|inherit|currentColor)$/i\nconst CAMEL_CASE_RE = /^[a-z][a-zA-Z0-9]*$/\nconst SCREAMING_SNAKE_RE = /^[A-Z][A-Z0-9_]+$/\nconst KEBAB_TECH_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/\nconst FILE_EXT_RE = /\\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i\nconst REGEX_LIKE_RE = /^\\^|^\\(.*\\)$|\\$$|\\\\d|\\\\w|\\\\s|\\.\\*|\\.\\+/\nconst CONSOLE_RE = /console\\.(log|warn|error|debug|info|trace)\\s*\\(/\n\n// Common content words that should NOT be filtered when they appear in kebab-case\nconst CONTENT_WORDS = new Set([\n 'sign', 'up', 'log', 'in', 'out', 'get', 'started', 'learn', 'more',\n 'read', 'click', 'here', 'go', 'back', 'next', 'prev', 'previous',\n 'see', 'all', 'view', 'show', 'hide', 'open', 'close', 'add', 'new',\n 'save', 'edit', 'delete', 'remove', 'submit', 'cancel', 'confirm',\n 'welcome', 'hello', 'thank', 'you', 'good', 'bye', 'hi', 'hey',\n 'no', 'yes', 'ok', 'sorry', 'please', 'help', 'about', 'contact',\n 'home', 'page', 'not', 'found', 'error', 'success', 'loading',\n])\n\n/**\n * Determines if a string is non-content (technical) and should be filtered out.\n * Pure function — no side effects, no semantic decisions.\n * Conservative: if in doubt, returns false (keeps the string).\n *\n * @deprecated Kept for backward compatibility. Scanner v2 uses structural pre-filter instead.\n */\nexport function isNonContent(value: string, lineContext: string, minLength: number = DEFAULT_MIN_LENGTH, maxLength: number = DEFAULT_MAX_LENGTH): boolean {\n // Length checks\n if (value.length < minLength) return true\n if (value.length > maxLength) return true\n\n // Import/require paths (no spaces, looks like module path)\n if (IMPORT_PATH_RE.test(value) && !value.includes(' ')) return true\n\n // CSS class strings — single class prefix\n if (CSS_CLASS_PREFIX_RE.test(value) && !value.includes(' ')) return true\n\n // CSS class strings — multiple space-separated tokens that all look like utility classes\n if (value.includes(' ') && looksLikeCssClasses(value)) return true\n\n // URL/route patterns\n if (URL_ROUTE_RE.test(value)) return true\n if (value.startsWith('/') && ROUTE_LIKE_RE.test(value)) return true\n\n // Color codes\n if (COLOR_RE.test(value)) return true\n\n // Technical identifiers (includes kebab-case with content-word awareness)\n if (isTechnicalIdentifier(value)) return true\n\n // File paths with extensions\n if (FILE_EXT_RE.test(value)) return true\n\n // RegExp-like patterns\n if (REGEX_LIKE_RE.test(value)) return true\n\n // Console/log context\n if (CONSOLE_RE.test(lineContext)) return true\n\n return false\n}\n\nfunction isTechnicalIdentifier(value: string): boolean {\n // No spaces allowed in identifiers\n if (value.includes(' ')) return false\n\n // camelCase: starts lowercase, has at least one uppercase letter, no spaces\n if (CAMEL_CASE_RE.test(value) && /[A-Z]/.test(value)) return true\n\n // SCREAMING_SNAKE_CASE — only filter if it contains underscores (true SCREAMING_SNAKE like API_KEY, MAX_COUNT).\n // Short uppercase words without underscores (OK, FAQ, GPS) are likely real UI labels.\n if (SCREAMING_SNAKE_RE.test(value) && value.includes('_')) return true\n\n // kebab-case: filter only if NO content words are present\n if (KEBAB_TECH_RE.test(value)) {\n const parts = value.split('-')\n const hasContentWord = parts.some(p => CONTENT_WORDS.has(p))\n if (!hasContentWord) return true\n }\n\n // Bare npm-like package names: all lowercase, no dashes with content words,\n // no spaces, < 40 chars. Single lowercase words are kept (could be content).\n if (/^[a-z][a-z0-9-]*$/.test(value) && value.length < 40 && !value.includes(' ')) {\n // Only filter if it has a slash (scoped package) or looks like a known package pattern\n // Single words or content-word kebab phrases pass through\n if (value.includes('/')) return true\n }\n\n return false\n}\n\nfunction looksLikeCssClasses(value: string): boolean {\n const tokens = value.split(/\\s+/)\n if (tokens.length < 2) return false\n\n // If ALL tokens look like CSS utilities, filter\n const cssLikeCount = tokens.filter(t => isCssClassToken(t)).length\n return cssLikeCount === tokens.length\n}\n\nfunction isCssClassToken(token: string): boolean {\n // Tailwind-like: prefix- or modifier: patterns\n if (CSS_CLASS_PREFIX_RE.test(token)) return true\n // Common standalone classes\n if (/^(flex|grid|block|inline|hidden|container|relative|absolute|fixed|sticky|static|isolate|overflow-hidden|overflow-auto|overflow-scroll|underline|italic|uppercase|lowercase|capitalize|truncate|break-words|break-all|whitespace-nowrap|sr-only|not-sr-only)$/.test(token)) return true\n // Arbitrary Tailwind with brackets: w-[100px]\n if (/^[a-z]+-\\[.+\\]$/.test(token)) return true\n // Responsive/state prefixes: sm:text-lg, hover:bg-blue-500\n if (/^(sm|md|lg|xl|2xl|hover|focus|active|disabled|first|last|odd|even|dark|group-hover|peer-checked):/.test(token)) return true\n return false\n}\n\n// ─── Secondary filter for v2 candidates ───\n\n/**\n * Apply legacy isNonContent filter as a secondary pass on v2 candidates.\n * This catches heuristic patterns (URLs, regex, camelCase identifiers, etc.)\n * that the structural pre-filter does not cover.\n */\nfunction applySecondaryFilter(\n value: string,\n _surrounding: string,\n minLength: number,\n maxLength: number,\n): boolean {\n // Pass empty lineContext — v2 pre-filter handles context-based detection (console, test, etc.)\n // Using surrounding would cause false positives because it spans +-1 lines\n return isNonContent(value, '', minLength, maxLength)\n}\n\n// ─── Main: scanCandidates ───\n\nexport async function scanCandidates(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanCandidatesResult> {\n const limit = options?.limit ?? DEFAULT_LIMIT\n const offset = options?.offset ?? DEFAULT_OFFSET\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n const allCandidates: ScanCandidate[] = []\n const dupeMap = new Map<string, Array<{ file: string; line: number }>>()\n let rawStringsFound = 0\n\n const filePromises = files.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return { relPath, extractions: [] }\n\n const ext = extname(filePath)\n const extractions = await extractStrings(filePath, content, ext)\n return { relPath, extractions }\n })\n\n const fileResults = await Promise.all(filePromises)\n\n for (const { relPath, extractions } of fileResults) {\n rawStringsFound += extractions.length\n\n for (const extraction of extractions) {\n // Apply secondary (legacy) filter for heuristic patterns\n if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue\n\n const mappedContext = CONTEXT_MAP[extraction.context]\n\n const candidate: ScanCandidate = {\n file: relPath,\n line: extraction.line,\n column: extraction.column,\n value: extraction.value,\n context: mappedContext,\n surrounding: extraction.surrounding,\n }\n allCandidates.push(candidate)\n\n // Track duplicates\n const key = extraction.value\n if (!dupeMap.has(key)) {\n dupeMap.set(key, [])\n }\n dupeMap.get(key)!.push({ file: relPath, line: extraction.line })\n }\n }\n\n // Build duplicate groups (only count >= 2), sorted by count descending\n const duplicates: DuplicateGroup[] = [...dupeMap.entries()]\n .filter(([, occurrences]) => occurrences.length >= 2)\n .map(([value, occurrences]) => ({ value, count: occurrences.length, occurrences }))\n .toSorted((a, b) => b.count - a.count)\n\n // Pagination\n const afterFiltering = allCandidates.length\n const paginated = allCandidates.slice(offset, offset + limit)\n const hasMore = afterFiltering > offset + limit\n\n return {\n candidates: paginated,\n duplicates,\n stats: {\n files_scanned: files.length,\n raw_strings_found: rawStringsFound,\n after_filtering: afterFiltering,\n candidates_returned: paginated.length,\n has_more: hasMore,\n },\n }\n}\n\n// ─── Main: scanSummary ───\n\nexport async function scanSummary(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanSummaryResult> {\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // Group files by directory\n const dirFiles = new Map<string, string[]>()\n const fileTypes: Record<string, number> = {}\n\n for (const relPath of files) {\n const parts = relPath.split('/')\n const dir = parts.slice(0, -1).join('/') || '.'\n const ext = extname(relPath)\n\n if (!dirFiles.has(dir)) dirFiles.set(dir, [])\n dirFiles.get(dir)!.push(relPath)\n\n fileTypes[ext] = (fileTypes[ext] ?? 0) + 1\n }\n\n // Sample files per directory and count candidates\n const byDirectory: Record<string, { files: number; candidates: number }> = {}\n const freqMap = new Map<string, number>()\n let totalCandidatesEstimate = 0\n\n for (const [dir, dirFileList] of dirFiles) {\n const totalInDir = dirFileList.length\n const sampleFiles = dirFileList.slice(0, SUMMARY_SAMPLE_SIZE)\n let sampleCandidates = 0\n\n const samplePromises = sampleFiles.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return []\n\n const ext = extname(filePath)\n return extractStrings(filePath, content, ext)\n })\n\n const sampleResults = await Promise.all(samplePromises)\n\n for (const extractions of sampleResults) {\n for (const extraction of extractions) {\n if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue\n sampleCandidates++\n\n // Track frequencies for top_repeated\n const prev = freqMap.get(extraction.value) ?? 0\n freqMap.set(extraction.value, prev + 1)\n }\n }\n\n // Estimate for full directory\n const avgPerFile = sampleFiles.length > 0 ? sampleCandidates / sampleFiles.length : 0\n const estimatedCandidates = Math.round(avgPerFile * totalInDir)\n\n byDirectory[dir] = {\n files: totalInDir,\n candidates: estimatedCandidates,\n }\n\n totalCandidatesEstimate += estimatedCandidates\n }\n\n // Top repeated strings\n const topRepeated = [...freqMap.entries()]\n .filter(([, count]) => count >= 2)\n .toSorted((a, b) => b[1] - a[1])\n .slice(0, 20)\n .map(([value, count]) => ({ value, count }))\n\n return {\n total_files: files.length,\n total_candidates_estimate: totalCandidatesEstimate,\n by_directory: byDirectory,\n top_repeated: topRepeated,\n sampling_note: `Based on first ${SUMMARY_SAMPLE_SIZE} files per directory. Counts are from sampled subset, not project-wide.`,\n file_types: fileTypes,\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scanner-DPrbU-oK.mjs","names":["FILE_EXT_RE"],"sources":["../src/core/ast-scanner/pre-filter.ts","../src/core/ast-scanner/index.ts","../src/core/scanner.ts"],"sourcesContent":["import type { ExtractedString, PreFilterRule } from './types.js'\n\n// ─── Pre-filter Result ───\n\nexport interface PreFilterResult {\n /** Strings that passed the pre-filter (candidates for agent) */\n candidates: ExtractedString[]\n /** Total number of strings removed by the pre-filter */\n filtered: number\n /** Breakdown: reason string -> count of strings filtered for that reason */\n filterReasons: Record<string, number>\n}\n\n// ─── Value-based patterns (deterministic, no intelligence) ───\n\nconst PURE_NUMBER_RE = /^\\d+(\\.\\d+)?$/\nconst HEX_COLOR_RE = /^#[0-9a-f]{3,8}$/i\nconst FILE_EXT_RE = /\\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i\n\n// ─── HTML / component prop technical values ───\n\nconst HTML_PROP_VALUES = new Set([\n // CSS / variant keywords\n 'class', 'variant', 'secondary', 'outline', 'ghost', 'destructive', 'default', 'primary',\n // Link / target\n '_blank', 'noopener', 'noreferrer',\n // Input / button types\n 'button', 'submit', 'reset', 'text', 'numeric', 'password', 'email', 'checkbox', 'radio',\n // Layout / display\n 'hidden', 'none', 'auto', 'inherit', 'initial',\n // Size tokens\n 'sm', 'md', 'lg', 'xl', 'xs', '2xl', '3xl',\n // Icon variants\n 'icon', 'icon-sm', 'icon-lg',\n])\n\n// ─── Slot / event technical names (single lowercase word) ───\n\nconst SLOT_EVENT_NAMES = new Set([\n 'header', 'footer', 'default', 'trigger', 'content',\n 'sidebar', 'overlay', 'body', 'actions',\n 'click', 'change', 'input', 'focus', 'blur', 'submit',\n 'mounted', 'unmounted', 'updated',\n])\n\n// ─── Tailwind / CSS utility detection ───\n\nconst TAILWIND_SEGMENT_RE = /^(?:bg-|text-|border-|flex|grid|p-|px-|py-|pt-|pb-|pl-|pr-|m-|mx-|my-|mt-|mb-|ml-|mr-|rounded|shadow|w-|h-|min-|max-|gap-|space-|items-|justify-|self-|overflow-|z-|opacity-|transition|duration-|ease-|animate-|font-|leading-|tracking-|decoration-|underline|line-through|uppercase|lowercase|capitalize|truncate|whitespace-|break-|sr-only|not-sr-only|hover:|focus:|active:|disabled:|dark:|sm:|md:|lg:|xl:|2xl:|group-|peer-|ring-|outline-|divide-|table-|col-|row-|aspect-|object-|inset-|top-|right-|bottom-|left-|translate-|rotate-|scale-|skew-|origin-|cursor-|select-|resize-|fill-|stroke-|block|inline|absolute|relative|fixed|sticky|static|float-|clear-|isolate|visible|invisible|grow|shrink|basis-|order-|place-)/\n\n/**\n * Returns true if the string looks like a CSS class list (Tailwind or similar).\n * Requires 2+ space-separated segments with majority matching utility patterns.\n */\nfunction isCssClassList(value: string): boolean {\n const segments = value.trim().split(/\\s+/)\n if (segments.length < 2) return false\n let matched = 0\n for (const seg of segments) {\n if (TAILWIND_SEGMENT_RE.test(seg)) matched++\n }\n return matched / segments.length >= 0.5\n}\n\n/**\n * Returns true if the string is a single CSS utility token (e.g. \"bg-blue-500\").\n */\nfunction isSingleCssUtility(value: string): boolean {\n const trimmed = value.trim()\n // Must be a single token, no spaces\n if (trimmed.includes(' ')) return false\n return TAILWIND_SEGMENT_RE.test(trimmed)\n}\n\n// ─── Pre-filter Rules ───\n\nconst PRE_FILTER_RULES: PreFilterRule[] = [\n // Context-based rules (AST-determined, 100% accurate)\n { context: 'import_path', reason: 'import_path' },\n { context: 'type_annotation', reason: 'type_annotation' },\n { context: 'css_class', reason: 'css_class' },\n { context: 'css_utility_call', reason: 'css_utility_call' },\n { context: 'console_call', reason: 'console_call' },\n { context: 'test_assertion', reason: 'test_assertion' },\n { context: 'switch_case', reason: 'switch_case' },\n\n // Value-based rules (structural, not heuristic)\n { condition: (v) => v.length <= 1, reason: 'single_char' },\n { condition: (v) => PURE_NUMBER_RE.test(v), reason: 'pure_number' },\n { condition: (v) => v.startsWith('--'), reason: 'cli_flag' },\n { condition: (v) => HEX_COLOR_RE.test(v), reason: 'hex_color' },\n { condition: (v) => FILE_EXT_RE.test(v), reason: 'file_extension' },\n\n // HTML / component prop technical values (exact match, case-insensitive)\n { condition: (v) => HTML_PROP_VALUES.has(v.toLowerCase()), reason: 'html_prop_value' },\n\n // CSS class lists (Tailwind-style multi-segment strings)\n { condition: (v) => isCssClassList(v), reason: 'css_class_list' },\n\n // Single CSS utility token (e.g. \"bg-blue-500\", \"rounded-lg\")\n { condition: (v) => isSingleCssUtility(v), reason: 'css_utility_token' },\n\n // Slot / event technical names (single lowercase word only)\n { condition: (v) => {\n const lower = v.toLowerCase()\n return v === lower && !v.includes(' ') && SLOT_EVENT_NAMES.has(lower)\n }, reason: 'slot_event_name' },\n]\n\n// ─── Public API ───\n\n/**\n * Structural pre-filter: removes strings that are 100% NOT content.\n *\n * Conservative — when in doubt, INCLUDE the string.\n * Returns candidates (passed), count of filtered, and reason breakdown.\n */\nexport function applyPreFilter(strings: ExtractedString[]): PreFilterResult {\n const candidates: ExtractedString[] = []\n const filterReasons: Record<string, number> = {}\n let filtered = 0\n\n for (const str of strings) {\n const matchedRule = findMatchingRule(str)\n\n if (matchedRule) {\n filtered++\n filterReasons[matchedRule.reason] = (filterReasons[matchedRule.reason] ?? 0) + 1\n } else {\n candidates.push(str)\n }\n }\n\n return { candidates, filtered, filterReasons }\n}\n\n/**\n * Find the first pre-filter rule that matches this string.\n * Returns the rule if matched, undefined if the string should pass through.\n */\nfunction findMatchingRule(str: ExtractedString): PreFilterRule | undefined {\n for (const rule of PRE_FILTER_RULES) {\n if (rule.context !== undefined && str.context === rule.context) {\n return rule\n }\n if (rule.condition !== undefined && rule.condition(str.value)) {\n return rule\n }\n }\n return undefined\n}\n","import type { ExtractedString } from './types.js'\nimport { applyPreFilter } from './pre-filter.js'\nimport { parseTsx } from './tsx-parser.js'\n\n// ─── Extension sets ───\n\nconst TSX_EXTENSIONS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs'])\nconst VUE_EXTENSIONS = new Set(['.vue'])\nconst SVELTE_EXTENSIONS = new Set(['.svelte'])\nconst ASTRO_EXTENSIONS = new Set(['.astro'])\n\n// ─── Lazy-loaded parsers ───\n\n/**\n * Lazily import vue-parser. Returns undefined if not available\n * (@vue/compiler-sfc is an optional dependency).\n */\nasync function loadVueParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]> | ExtractedString[]) | undefined> {\n try {\n const mod = await import('./vue-parser.js')\n return mod.parseVue\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import svelte-parser. Returns undefined if not available\n * (svelte is an optional dependency).\n */\nasync function loadSvelteParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./svelte-parser.js')\n return mod.parseSvelte\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import astro-parser. Returns undefined if not available\n * (@astrojs/compiler is an optional dependency).\n */\nasync function loadAstroParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./astro-parser.js')\n return mod.parseAstro\n } catch {\n return undefined\n }\n}\n\n// ─── Regex fallback for unknown extensions ───\n\nconst SURROUNDING_MAX = 120\n\n/**\n * Minimal regex-based extractor for file types without AST parsers.\n * Extracts quoted strings and tag text — conservative, low accuracy.\n * Used as fallback when dedicated parsers are unavailable.\n */\nfunction extractWithRegex(content: string, _filePath: string): ExtractedString[] {\n const lines = content.split('\\n')\n const results: ExtractedString[] = []\n\n // Simple quoted string extraction\n const stringRe = /(['\"`])(?:(?!\\1|\\\\).|\\\\.)*?\\1/g\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!\n const trimmed = line.trim()\n\n // Skip comments, imports\n if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) continue\n if (/^\\s*(import|export)\\s/.test(line)) continue\n\n let match: RegExpExecArray | null\n stringRe.lastIndex = 0\n\n while ((match = stringRe.exec(line)) !== null) {\n // Remove surrounding quotes\n const raw = match[0]\n const value = raw.slice(1, -1)\n if (value.length === 0) continue\n\n const surrounding = buildSurrounding(lines, i)\n\n results.push({\n value,\n line: i + 1,\n column: match.index + 1,\n context: 'other',\n scope: 'script',\n parent: '',\n surrounding,\n })\n }\n\n // Tag text extraction: >text<\n const tagTextRe = />([^<>{]+)</g\n let tagMatch: RegExpExecArray | null\n tagTextRe.lastIndex = 0\n\n while ((tagMatch = tagTextRe.exec(line)) !== null) {\n const text = tagMatch[1]!.trim()\n if (text.length === 0) continue\n if (/^[\\s\\W]*$/.test(text) && !/[a-zA-Z]/.test(text)) continue\n\n results.push({\n value: text,\n line: i + 1,\n column: tagMatch.index + 1,\n context: 'template_text',\n scope: 'template',\n parent: '',\n surrounding: buildSurrounding(lines, i),\n })\n }\n }\n\n return results\n}\n\nfunction buildSurrounding(lines: string[], lineIdx: number): string {\n const start = Math.max(0, lineIdx - 1)\n const end = Math.min(lines.length - 1, lineIdx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const line = lines[i]\n if (line !== undefined) {\n parts.push(line)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n\n// ─── Public API ───\n\n/**\n * Extract strings from a source file with structural context.\n *\n * Routes to the correct parser based on file extension:\n * - .tsx/.jsx/.ts/.js/.mjs -> tsx-parser (AST-based)\n * - .vue -> vue-parser (lazy-loaded, AST-based)\n * - .svelte -> svelte-parser (lazy-loaded, AST-based)\n * - .astro -> astro-parser (lazy-loaded, AST-based)\n * - unknown -> empty array\n *\n * Falls back to regex extraction when the dedicated parser's\n * optional dependency is not installed.\n *\n * Applies structural pre-filter to remove 100% non-content strings.\n * Returns only candidates that should be sent to the agent.\n */\nexport async function extractStrings(\n filePath: string,\n content: string,\n ext: string,\n): Promise<ExtractedString[]> {\n const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`\n let rawStrings: ExtractedString[]\n\n if (TSX_EXTENSIONS.has(normalizedExt)) {\n rawStrings = parseTsx(content, filePath)\n } else if (VUE_EXTENSIONS.has(normalizedExt)) {\n const parseVue = await loadVueParser()\n if (parseVue) {\n rawStrings = await parseVue(content, filePath)\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (SVELTE_EXTENSIONS.has(normalizedExt)) {\n const parseSvelte = await loadSvelteParser()\n if (parseSvelte) {\n try {\n rawStrings = await parseSvelte(content, filePath)\n } catch {\n // svelte/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (ASTRO_EXTENSIONS.has(normalizedExt)) {\n const parseAstro = await loadAstroParser()\n if (parseAstro) {\n try {\n rawStrings = await parseAstro(content, filePath)\n } catch {\n // @astrojs/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n return []\n }\n\n // Apply structural pre-filter\n const { candidates } = applyPreFilter(rawStrings)\n return candidates\n}\n\n// Re-export types for convenience\nexport type { ExtractedString, StructuralContext, PreFilterRule } from './types.js'\nexport { applyPreFilter } from './pre-filter.js'\nexport type { PreFilterResult } from './pre-filter.js'\n","import type {\n StringContext,\n ScanCandidate,\n DuplicateGroup,\n ScanCandidatesResult,\n ScanSummaryResult,\n} from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText } from '../util/fs.js'\nimport {\n autoDetectSourceDirs,\n discoverFiles,\n} from './scan-config.js'\nimport { extractStrings } from './ast-scanner/index.js'\nimport type { StructuralContext } from './ast-scanner/types.js'\n\n// ─── Options ───\n\nexport interface ScanOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n limit?: number\n offset?: number\n min_length?: number\n max_length?: number\n}\n\n// ─── Constants ───\n\nconst DEFAULT_LIMIT = 50\nconst DEFAULT_OFFSET = 0\nconst DEFAULT_MIN_LENGTH = 2\nconst DEFAULT_MAX_LENGTH = 500\nconst SUMMARY_SAMPLE_SIZE = 10\n\n// ─── Context mapping: StructuralContext → StringContext ───\n\nconst CONTEXT_MAP: Record<StructuralContext, StringContext> = {\n 'template_text': 'template_text',\n 'template_attribute': 'template_attribute',\n 'jsx_text': 'jsx_text',\n 'jsx_attribute': 'jsx_attribute',\n 'variable_assignment': 'variable_assignment',\n 'object_property': 'object_value',\n 'function_argument': 'function_argument',\n 'array_element': 'other',\n 'enum_value': 'other',\n 'template_literal': 'other',\n 'switch_case': 'other',\n 'other': 'other',\n // Pre-filtered contexts should not reach here, but map them just in case\n 'import_path': 'other',\n 'type_annotation': 'other',\n 'css_class': 'other',\n 'css_utility_call': 'other',\n 'console_call': 'other',\n 'test_assertion': 'other',\n}\n\n// ─── Legacy Pre-filter (kept exported for backward compatibility) ───\n\nconst IMPORT_PATH_RE = /^(\\.\\/|\\.\\.\\/|@\\/|@[a-z]|[a-z][a-z0-9-]*\\/)/\nconst CSS_CLASS_PREFIX_RE = /^(flex|grid|block|inline|hidden|absolute|relative|fixed|sticky|p-|m-|w-|h-|bg-|text-|border-|rounded|shadow|gap-|space-|font-|leading-|tracking-|opacity-|z-|overflow-|cursor-|transition|transform|animate-|hover:|focus:|active:|dark:|sm:|md:|lg:|xl:)/\nconst URL_ROUTE_RE = /^(https?:\\/\\/|\\/api\\/|\\/v[0-9]|#\\/|mailto:|tel:)/\nconst ROUTE_LIKE_RE = /^\\/[a-z0-9/:_-]*$/i\nconst COLOR_RE = /^(#[0-9a-f]{3,8}|rgba?\\(|hsla?\\(|transparent|inherit|currentColor)$/i\nconst CAMEL_CASE_RE = /^[a-z][a-zA-Z0-9]*$/\nconst SCREAMING_SNAKE_RE = /^[A-Z][A-Z0-9_]+$/\nconst KEBAB_TECH_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/\nconst FILE_EXT_RE = /\\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i\nconst REGEX_LIKE_RE = /^\\^|^\\(.*\\)$|\\$$|\\\\d|\\\\w|\\\\s|\\.\\*|\\.\\+/\nconst CONSOLE_RE = /console\\.(log|warn|error|debug|info|trace)\\s*\\(/\n\n// Common content words that should NOT be filtered when they appear in kebab-case\nconst CONTENT_WORDS = new Set([\n 'sign', 'up', 'log', 'in', 'out', 'get', 'started', 'learn', 'more',\n 'read', 'click', 'here', 'go', 'back', 'next', 'prev', 'previous',\n 'see', 'all', 'view', 'show', 'hide', 'open', 'close', 'add', 'new',\n 'save', 'edit', 'delete', 'remove', 'submit', 'cancel', 'confirm',\n 'welcome', 'hello', 'thank', 'you', 'good', 'bye', 'hi', 'hey',\n 'no', 'yes', 'ok', 'sorry', 'please', 'help', 'about', 'contact',\n 'home', 'page', 'not', 'found', 'error', 'success', 'loading',\n])\n\n/**\n * Determines if a string is non-content (technical) and should be filtered out.\n * Pure function — no side effects, no semantic decisions.\n * Conservative: if in doubt, returns false (keeps the string).\n *\n * @deprecated Kept for backward compatibility. Scanner v2 uses structural pre-filter instead.\n */\nexport function isNonContent(value: string, lineContext: string, minLength: number = DEFAULT_MIN_LENGTH, maxLength: number = DEFAULT_MAX_LENGTH): boolean {\n // Length checks\n if (value.length < minLength) return true\n if (value.length > maxLength) return true\n\n // Import/require paths (no spaces, looks like module path)\n if (IMPORT_PATH_RE.test(value) && !value.includes(' ')) return true\n\n // CSS class strings — single class prefix\n if (CSS_CLASS_PREFIX_RE.test(value) && !value.includes(' ')) return true\n\n // CSS class strings — multiple space-separated tokens that all look like utility classes\n if (value.includes(' ') && looksLikeCssClasses(value)) return true\n\n // URL/route patterns\n if (URL_ROUTE_RE.test(value)) return true\n if (value.startsWith('/') && ROUTE_LIKE_RE.test(value)) return true\n\n // Color codes\n if (COLOR_RE.test(value)) return true\n\n // Technical identifiers (includes kebab-case with content-word awareness)\n if (isTechnicalIdentifier(value)) return true\n\n // File paths with extensions\n if (FILE_EXT_RE.test(value)) return true\n\n // RegExp-like patterns\n if (REGEX_LIKE_RE.test(value)) return true\n\n // Console/log context\n if (CONSOLE_RE.test(lineContext)) return true\n\n return false\n}\n\nfunction isTechnicalIdentifier(value: string): boolean {\n // No spaces allowed in identifiers\n if (value.includes(' ')) return false\n\n // camelCase: starts lowercase, has at least one uppercase letter, no spaces\n if (CAMEL_CASE_RE.test(value) && /[A-Z]/.test(value)) return true\n\n // SCREAMING_SNAKE_CASE — only filter if it contains underscores (true SCREAMING_SNAKE like API_KEY, MAX_COUNT).\n // Short uppercase words without underscores (OK, FAQ, GPS) are likely real UI labels.\n if (SCREAMING_SNAKE_RE.test(value) && value.includes('_')) return true\n\n // kebab-case: filter only if NO content words are present\n if (KEBAB_TECH_RE.test(value)) {\n const parts = value.split('-')\n const hasContentWord = parts.some(p => CONTENT_WORDS.has(p))\n if (!hasContentWord) return true\n }\n\n // Bare npm-like package names: all lowercase, no dashes with content words,\n // no spaces, < 40 chars. Single lowercase words are kept (could be content).\n if (/^[a-z][a-z0-9-]*$/.test(value) && value.length < 40 && !value.includes(' ')) {\n // Only filter if it has a slash (scoped package) or looks like a known package pattern\n // Single words or content-word kebab phrases pass through\n if (value.includes('/')) return true\n }\n\n return false\n}\n\nfunction looksLikeCssClasses(value: string): boolean {\n const tokens = value.split(/\\s+/)\n if (tokens.length < 2) return false\n\n // If ALL tokens look like CSS utilities, filter\n const cssLikeCount = tokens.filter(t => isCssClassToken(t)).length\n return cssLikeCount === tokens.length\n}\n\nfunction isCssClassToken(token: string): boolean {\n // Tailwind-like: prefix- or modifier: patterns\n if (CSS_CLASS_PREFIX_RE.test(token)) return true\n // Common standalone classes\n if (/^(flex|grid|block|inline|hidden|container|relative|absolute|fixed|sticky|static|isolate|overflow-hidden|overflow-auto|overflow-scroll|underline|italic|uppercase|lowercase|capitalize|truncate|break-words|break-all|whitespace-nowrap|sr-only|not-sr-only)$/.test(token)) return true\n // Arbitrary Tailwind with brackets: w-[100px]\n if (/^[a-z]+-\\[.+\\]$/.test(token)) return true\n // Responsive/state prefixes: sm:text-lg, hover:bg-blue-500\n if (/^(sm|md|lg|xl|2xl|hover|focus|active|disabled|first|last|odd|even|dark|group-hover|peer-checked):/.test(token)) return true\n return false\n}\n\n// ─── Secondary filter for v2 candidates ───\n\n/**\n * Apply legacy isNonContent filter as a secondary pass on v2 candidates.\n * This catches heuristic patterns (URLs, regex, camelCase identifiers, etc.)\n * that the structural pre-filter does not cover.\n */\nfunction applySecondaryFilter(\n value: string,\n _surrounding: string,\n minLength: number,\n maxLength: number,\n): boolean {\n // Pass empty lineContext — v2 pre-filter handles context-based detection (console, test, etc.)\n // Using surrounding would cause false positives because it spans +-1 lines\n return isNonContent(value, '', minLength, maxLength)\n}\n\n// ─── Main: scanCandidates ───\n\nexport async function scanCandidates(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanCandidatesResult> {\n const limit = options?.limit ?? DEFAULT_LIMIT\n const offset = options?.offset ?? DEFAULT_OFFSET\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n const allCandidates: ScanCandidate[] = []\n const dupeMap = new Map<string, Array<{ file: string; line: number }>>()\n let rawStringsFound = 0\n\n const filePromises = files.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return { relPath, extractions: [] }\n\n const ext = extname(filePath)\n const extractions = await extractStrings(filePath, content, ext)\n return { relPath, extractions }\n })\n\n const fileResults = await Promise.all(filePromises)\n\n for (const { relPath, extractions } of fileResults) {\n rawStringsFound += extractions.length\n\n for (const extraction of extractions) {\n // Apply secondary (legacy) filter for heuristic patterns\n if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue\n\n const mappedContext = CONTEXT_MAP[extraction.context]\n\n const candidate: ScanCandidate = {\n file: relPath,\n line: extraction.line,\n column: extraction.column,\n value: extraction.value,\n context: mappedContext,\n surrounding: extraction.surrounding,\n }\n allCandidates.push(candidate)\n\n // Track duplicates\n const key = extraction.value\n if (!dupeMap.has(key)) {\n dupeMap.set(key, [])\n }\n dupeMap.get(key)!.push({ file: relPath, line: extraction.line })\n }\n }\n\n // Build duplicate groups (only count >= 2), sorted by count descending\n const duplicates: DuplicateGroup[] = [...dupeMap.entries()]\n .filter(([, occurrences]) => occurrences.length >= 2)\n .map(([value, occurrences]) => ({ value, count: occurrences.length, occurrences }))\n .toSorted((a, b) => b.count - a.count)\n\n // Pagination\n const afterFiltering = allCandidates.length\n const paginated = allCandidates.slice(offset, offset + limit)\n const hasMore = afterFiltering > offset + limit\n\n return {\n candidates: paginated,\n duplicates,\n stats: {\n files_scanned: files.length,\n raw_strings_found: rawStringsFound,\n after_filtering: afterFiltering,\n candidates_returned: paginated.length,\n has_more: hasMore,\n },\n }\n}\n\n// ─── Main: scanSummary ───\n\nexport async function scanSummary(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanSummaryResult> {\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // Group files by directory\n const dirFiles = new Map<string, string[]>()\n const fileTypes: Record<string, number> = {}\n\n for (const relPath of files) {\n const parts = relPath.split('/')\n const dir = parts.slice(0, -1).join('/') || '.'\n const ext = extname(relPath)\n\n if (!dirFiles.has(dir)) dirFiles.set(dir, [])\n dirFiles.get(dir)!.push(relPath)\n\n fileTypes[ext] = (fileTypes[ext] ?? 0) + 1\n }\n\n // Sample files per directory and count candidates\n const byDirectory: Record<string, { files: number; candidates: number }> = {}\n const freqMap = new Map<string, number>()\n let totalCandidatesEstimate = 0\n\n for (const [dir, dirFileList] of dirFiles) {\n const totalInDir = dirFileList.length\n const sampleFiles = dirFileList.slice(0, SUMMARY_SAMPLE_SIZE)\n let sampleCandidates = 0\n\n const samplePromises = sampleFiles.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return []\n\n const ext = extname(filePath)\n return extractStrings(filePath, content, ext)\n })\n\n const sampleResults = await Promise.all(samplePromises)\n\n for (const extractions of sampleResults) {\n for (const extraction of extractions) {\n if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue\n sampleCandidates++\n\n // Track frequencies for top_repeated\n const prev = freqMap.get(extraction.value) ?? 0\n freqMap.set(extraction.value, prev + 1)\n }\n }\n\n // Estimate for full directory\n const avgPerFile = sampleFiles.length > 0 ? sampleCandidates / sampleFiles.length : 0\n const estimatedCandidates = Math.round(avgPerFile * totalInDir)\n\n byDirectory[dir] = {\n files: totalInDir,\n candidates: estimatedCandidates,\n }\n\n totalCandidatesEstimate += estimatedCandidates\n }\n\n // Top repeated strings\n const topRepeated = [...freqMap.entries()]\n .filter(([, count]) => count >= 2)\n .toSorted((a, b) => b[1] - a[1])\n .slice(0, 20)\n .map(([value, count]) => ({ value, count }))\n\n return {\n total_files: files.length,\n total_candidates_estimate: totalCandidatesEstimate,\n by_directory: byDirectory,\n top_repeated: topRepeated,\n sampling_note: `Based on first ${SUMMARY_SAMPLE_SIZE} files per directory. Counts are from sampled subset, not project-wide.`,\n file_types: fileTypes,\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { canonicalStringify, sortKeys } from "@contentrain/types";
2
+ export { sortKeys as n, canonicalStringify as t };
@@ -1,22 +1,22 @@
1
- import { a as readJson, n as ensureDir, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-BbuGnVcG.mjs";
2
- import { n as readVocabulary, t as readConfig } from "./config-CPlhDwsv.mjs";
3
- import { n as readMeta, r as writeMeta } from "./meta-manager-CECrfsKm.mjs";
4
- import { S as writeContent, _ as resolveMdFilePath, a as fieldDefZodSchema, c as validateModelDefinition, d as listContent, g as resolveLocaleStrategy, h as resolveJsonFilePath, i as deleteModel, l as writeModel, m as resolveContentDir, n as checkReferences, o as listModels, r as countEntries, s as readModel, u as deleteContent } from "./model-manager-kl78AAMg.mjs";
5
- import { t as readContext } from "./context-CUFulSi2.mjs";
6
- import { t as detectStack } from "./detect-C8XmJ4S6.mjs";
7
- import { n as cleanupMergedBranches, t as checkBranchHealth } from "./branch-lifecycle-B7TgkxGr.mjs";
8
- import { n as listTemplates, t as getTemplate } from "./templates-3HWi-PTu.mjs";
9
- import { n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "./transaction-CncRbpI9.mjs";
10
- import { t as validateProject } from "./validator-CyckZAVr.mjs";
11
- import { t as buildGraph } from "./graph-builder-fPrDDwnu.mjs";
12
- import { n as scanCandidates, r as scanSummary } from "./scanner-BdTiv8sw.mjs";
13
- import { n as applyExtract, r as applyReuse } from "./apply-manager-CMAoS_nu.mjs";
1
+ import { a as readJson, n as ensureDir, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-BnIQ4dKj.mjs";
2
+ import { n as readVocabulary, t as readConfig } from "./config-B5kc2oeV.mjs";
3
+ import { n as readMeta, r as writeMeta } from "./meta-manager--0IwAPCl.mjs";
4
+ import { S as writeContent, _ as resolveMdFilePath, a as fieldDefZodSchema, c as validateModelDefinition, d as listContent, g as resolveLocaleStrategy, h as resolveJsonFilePath, i as deleteModel, l as writeModel, m as resolveContentDir, n as checkReferences, o as listModels, r as countEntries, s as readModel, u as deleteContent } from "./model-manager-DW_fOGRM.mjs";
5
+ import { t as readContext } from "./context-DIDt_TxX.mjs";
6
+ import { t as detectStack } from "./detect-DinOQAmK.mjs";
7
+ import { n as cleanupMergedBranches, t as checkBranchHealth } from "./branch-lifecycle-wGJg25LT.mjs";
8
+ import { n as listTemplates, t as getTemplate } from "./templates-bRPzYq4M.mjs";
9
+ import { n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "./transaction-B0V0_SE2.mjs";
10
+ import { t as validateProject } from "./validator-SQYUV2JP.mjs";
11
+ import { t as buildGraph } from "./graph-builder-tM7zj-SX.mjs";
12
+ import { n as scanCandidates, r as scanSummary } from "./scanner-DPrbU-oK.mjs";
13
+ import { n as applyExtract, r as applyReuse } from "./apply-manager-Cf5OWOJ7.mjs";
14
14
  import { join } from "node:path";
15
15
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
16
  import { z } from "zod";
17
17
  import { appendFile, readFile, writeFile } from "node:fs/promises";
18
- import { simpleGit } from "simple-git";
19
18
  import { CONTENTRAIN_BRANCH } from "@contentrain/types";
19
+ import { simpleGit } from "simple-git";
20
20
  //#region src/tools/context.ts
21
21
  function registerContextTools(server, projectRoot) {
22
22
  server.tool("contentrain_status", "Get full project status (read-only). Returns config, models, context. Do NOT manually edit .contentrain/ based on this output.", {}, async () => {
@@ -1860,7 +1860,7 @@ function registerBulkTools(server, projectRoot) {
1860
1860
  }
1861
1861
  //#endregion
1862
1862
  //#region package.json
1863
- var version = "1.1.0";
1863
+ var version = "1.1.2";
1864
1864
  //#endregion
1865
1865
  //#region src/server.ts
1866
1866
  function createServer(projectRoot) {
@@ -1880,4 +1880,4 @@ function createServer(projectRoot) {
1880
1880
  //#endregion
1881
1881
  export { createServer as t };
1882
1882
 
1883
- //# sourceMappingURL=server-CDv6JNRi.mjs.map
1883
+ //# sourceMappingURL=server-CjK63H6X.mjs.map