@contentrain/mcp 0.3.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +147 -238
- package/dist/apply-manager-CaTuu7Lj.mjs +751 -0
- package/dist/apply-manager-CaTuu7Lj.mjs.map +1 -0
- package/dist/astro-parser-RofKmKQi.mjs +166 -0
- package/dist/astro-parser-RofKmKQi.mjs.map +1 -0
- package/dist/branch-lifecycle-DB6sgeJi.mjs +97 -0
- package/dist/branch-lifecycle-DB6sgeJi.mjs.map +1 -0
- package/dist/chunk-BEJ448es.mjs +35 -0
- package/dist/compiler-rIC4ETHv.mjs +44509 -0
- package/dist/compiler-rIC4ETHv.mjs.map +1 -0
- package/dist/compiler-sfc.cjs-BbD04rn_.mjs +50279 -0
- package/dist/compiler-sfc.cjs-BbD04rn_.mjs.map +1 -0
- package/dist/config-CPlhDwsv.mjs +27 -0
- package/dist/config-CPlhDwsv.mjs.map +1 -0
- package/dist/context-yh3cuw5N.mjs +46 -0
- package/dist/context-yh3cuw5N.mjs.map +1 -0
- package/dist/core/apply-manager.d.mts +144 -0
- package/dist/core/apply-manager.d.mts.map +1 -0
- package/dist/core/apply-manager.mjs +6 -0
- package/dist/core/config.d.mts +8 -0
- package/dist/core/config.d.mts.map +1 -0
- package/dist/core/config.mjs +2 -0
- package/dist/core/content-manager.d.mts +53 -0
- package/dist/core/content-manager.d.mts.map +1 -0
- package/dist/core/content-manager.mjs +2 -0
- package/dist/core/context.d.mts +13 -0
- package/dist/core/context.d.mts.map +1 -0
- package/dist/core/context.mjs +3 -0
- package/dist/core/graph-builder.d.mts +12 -0
- package/dist/core/graph-builder.d.mts.map +1 -0
- package/dist/core/graph-builder.mjs +2 -0
- package/dist/core/meta-manager.d.mts +14 -0
- package/dist/core/meta-manager.d.mts.map +1 -0
- package/dist/core/meta-manager.mjs +2 -0
- package/dist/core/model-manager.d.mts +44 -0
- package/dist/core/model-manager.d.mts.map +1 -0
- package/dist/core/model-manager.mjs +2 -0
- package/dist/core/scan-config.d.mts +24 -0
- package/dist/core/scan-config.d.mts.map +1 -0
- package/dist/core/scan-config.mjs +2 -0
- package/dist/core/scanner.d.mts +25 -0
- package/dist/core/scanner.d.mts.map +1 -0
- package/dist/core/scanner.mjs +3 -0
- package/dist/core/validator.d.mts +23 -0
- package/dist/core/validator.d.mts.map +1 -0
- package/dist/core/validator.mjs +3 -0
- package/dist/detect-C8XmJ4S6.mjs +362 -0
- package/dist/detect-C8XmJ4S6.mjs.map +1 -0
- package/dist/fs-BbuGnVcG.mjs +52 -0
- package/dist/fs-BbuGnVcG.mjs.map +1 -0
- package/dist/git/branch-lifecycle.d.mts +28 -0
- package/dist/git/branch-lifecycle.d.mts.map +1 -0
- package/dist/git/branch-lifecycle.mjs +2 -0
- package/dist/git/transaction.d.mts +25 -0
- package/dist/git/transaction.d.mts.map +1 -0
- package/dist/git/transaction.mjs +4 -0
- package/dist/graph-builder-fPrDDwnu.mjs +162 -0
- package/dist/graph-builder-fPrDDwnu.mjs.map +1 -0
- package/dist/id-BdD4HvMY.mjs +12 -0
- package/dist/id-BdD4HvMY.mjs.map +1 -0
- package/dist/index.d.mts +1 -359
- package/dist/index.mjs +15 -2161
- package/dist/index.mjs.map +1 -1
- package/dist/meta-manager-CECrfsKm.mjs +53 -0
- package/dist/meta-manager-CECrfsKm.mjs.map +1 -0
- package/dist/model-manager-BcWWu5l2.mjs +963 -0
- package/dist/model-manager-BcWWu5l2.mjs.map +1 -0
- package/dist/node-Cbq5eR5i.mjs +300 -0
- package/dist/node-Cbq5eR5i.mjs.map +1 -0
- package/dist/scan-config-AJGxCdNd.mjs +159 -0
- package/dist/scan-config-AJGxCdNd.mjs.map +1 -0
- package/dist/scanner-BdTiv8sw.mjs +619 -0
- package/dist/scanner-BdTiv8sw.mjs.map +1 -0
- package/dist/serializer-Dueih-i2.mjs +33 -0
- package/dist/serializer-Dueih-i2.mjs.map +1 -0
- package/dist/server-CTcDpxwW.mjs +1861 -0
- package/dist/server-CTcDpxwW.mjs.map +1 -0
- package/dist/server.d.mts +7 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.mjs +11 -0
- package/dist/source-map-generator-DpO-ejrt.mjs +893 -0
- package/dist/source-map-generator-DpO-ejrt.mjs.map +1 -0
- package/dist/svelte-parser-COTNG92w.mjs +225 -0
- package/dist/svelte-parser-COTNG92w.mjs.map +1 -0
- package/dist/templates/index.d.mts +8 -0
- package/dist/templates/index.d.mts.map +1 -0
- package/dist/templates/index.mjs +2 -0
- package/dist/templates-3HWi-PTu.mjs +824 -0
- package/dist/templates-3HWi-PTu.mjs.map +1 -0
- package/dist/transaction-BsECnmL5.mjs +128 -0
- package/dist/transaction-BsECnmL5.mjs.map +1 -0
- package/dist/tsx-parser-Bf8Firrq.mjs +2 -0
- package/dist/tsx-parser-CYVyBWjn.mjs +234 -0
- package/dist/tsx-parser-CYVyBWjn.mjs.map +1 -0
- package/dist/util/detect.d.mts +20 -0
- package/dist/util/detect.d.mts.map +1 -0
- package/dist/util/detect.mjs +2 -0
- package/dist/util/fs.d.mts +12 -0
- package/dist/util/fs.d.mts.map +1 -0
- package/dist/util/fs.mjs +2 -0
- package/dist/util/id.d.mts +6 -0
- package/dist/util/id.d.mts.map +1 -0
- package/dist/util/id.mjs +2 -0
- package/dist/util/serializer.d.mts +14 -0
- package/dist/util/serializer.d.mts.map +1 -0
- package/dist/util/serializer.mjs +2 -0
- package/dist/validator-ku84AyFr.mjs +755 -0
- package/dist/validator-ku84AyFr.mjs.map +1 -0
- package/dist/vue-parser-CjS4YAoQ.mjs +225 -0
- package/dist/vue-parser-CjS4YAoQ.mjs.map +1 -0
- package/package.json +107 -43
- package/dist/fs-D83-Ii_y.mjs +0 -75
- package/dist/fs-D83-Ii_y.mjs.map +0 -1
- package/dist/index.d.mts.map +0 -1
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/util/serializer.ts
|
|
2
|
+
/**
|
|
3
|
+
* Canonical JSON serialization — deterministic output for git diff stability.
|
|
4
|
+
*
|
|
5
|
+
* Rules (spec Section 14.10):
|
|
6
|
+
* - Top-level keys: lexicographic sort
|
|
7
|
+
* - Entry fields: follow fieldOrder if provided, else lexicographic
|
|
8
|
+
* - 2-space indent, UTF-8, trailing newline
|
|
9
|
+
* - Null/undefined values omitted
|
|
10
|
+
*/
|
|
11
|
+
function sortKeys(obj, fieldOrder) {
|
|
12
|
+
if (obj === null || obj === void 0) return void 0;
|
|
13
|
+
if (Array.isArray(obj)) return obj.map((item) => sortKeys(item, fieldOrder));
|
|
14
|
+
if (typeof obj !== "object") return obj;
|
|
15
|
+
const record = obj;
|
|
16
|
+
const sorted = {};
|
|
17
|
+
const keys = fieldOrder ? [...new Set([...fieldOrder, ...Object.keys(record).toSorted()])] : Object.keys(record).toSorted();
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
if (!(key in record)) continue;
|
|
20
|
+
const val = record[key];
|
|
21
|
+
if (val === null || val === void 0) continue;
|
|
22
|
+
sorted[key] = sortKeys(val);
|
|
23
|
+
}
|
|
24
|
+
return sorted;
|
|
25
|
+
}
|
|
26
|
+
function canonicalStringify(data, fieldOrder) {
|
|
27
|
+
const sorted = sortKeys(data, fieldOrder);
|
|
28
|
+
return JSON.stringify(sorted, null, 2) + "\n";
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { canonicalStringify as t };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=serializer-Dueih-i2.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializer-Dueih-i2.mjs","names":[],"sources":["../src/util/serializer.ts"],"sourcesContent":["/**\n * Canonical JSON serialization — deterministic output for git diff stability.\n *\n * Rules (spec Section 14.10):\n * - Top-level keys: lexicographic sort\n * - Entry fields: follow fieldOrder if provided, else lexicographic\n * - 2-space indent, UTF-8, trailing newline\n * - Null/undefined values omitted\n */\n\nfunction sortKeys(obj: unknown, fieldOrder?: string[]): unknown {\n if (obj === null || obj === undefined) return undefined\n if (Array.isArray(obj)) return obj.map(item => sortKeys(item, fieldOrder))\n if (typeof obj !== 'object') return obj\n\n const record = obj as Record<string, unknown>\n const sorted: Record<string, unknown> = {}\n\n const keys = fieldOrder\n ? [...new Set([...fieldOrder, ...Object.keys(record).toSorted()])]\n : Object.keys(record).toSorted()\n\n for (const key of keys) {\n if (!(key in record)) continue\n const val = record[key]\n if (val === null || val === undefined) continue\n sorted[key] = sortKeys(val)\n }\n\n return sorted\n}\n\nexport function canonicalStringify(data: unknown, fieldOrder?: string[]): string {\n const sorted = sortKeys(data, fieldOrder)\n return JSON.stringify(sorted, null, 2) + '\\n'\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAS,SAAS,KAAc,YAAgC;AAC9D,KAAI,QAAQ,QAAQ,QAAQ,KAAA,EAAW,QAAO,KAAA;AAC9C,KAAI,MAAM,QAAQ,IAAI,CAAE,QAAO,IAAI,KAAI,SAAQ,SAAS,MAAM,WAAW,CAAC;AAC1E,KAAI,OAAO,QAAQ,SAAU,QAAO;CAEpC,MAAM,SAAS;CACf,MAAM,SAAkC,EAAE;CAE1C,MAAM,OAAO,aACT,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,YAAY,GAAG,OAAO,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAChE,OAAO,KAAK,OAAO,CAAC,UAAU;AAElC,MAAK,MAAM,OAAO,MAAM;AACtB,MAAI,EAAE,OAAO,QAAS;EACtB,MAAM,MAAM,OAAO;AACnB,MAAI,QAAQ,QAAQ,QAAQ,KAAA,EAAW;AACvC,SAAO,OAAO,SAAS,IAAI;;AAG7B,QAAO;;AAGT,SAAgB,mBAAmB,MAAe,YAA+B;CAC/E,MAAM,SAAS,SAAS,MAAM,WAAW;AACzC,QAAO,KAAK,UAAU,QAAQ,MAAM,EAAE,GAAG"}
|