@contentrain/mcp 1.1.2 → 1.2.1

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 (34) hide show
  1. package/README.md +29 -20
  2. package/dist/{apply-manager-Cf5OWOJ7.mjs → apply-manager-BkPMfpsN.mjs} +8 -3
  3. package/dist/apply-manager-BkPMfpsN.mjs.map +1 -0
  4. package/dist/{astro-parser-RofKmKQi.mjs → astro-parser-BJ-54JGe.mjs} +3 -3
  5. package/dist/{astro-parser-RofKmKQi.mjs.map → astro-parser-BJ-54JGe.mjs.map} +1 -1
  6. package/dist/core/apply-manager.d.mts.map +1 -1
  7. package/dist/core/apply-manager.mjs +2 -2
  8. package/dist/core/scanner.d.mts +3 -9
  9. package/dist/core/scanner.d.mts.map +1 -1
  10. package/dist/core/scanner.mjs +3 -3
  11. package/dist/git/transaction.d.mts +6 -1
  12. package/dist/git/transaction.d.mts.map +1 -1
  13. package/dist/git/transaction.mjs +2 -2
  14. package/dist/index.mjs +5 -5
  15. package/dist/{scanner-DPrbU-oK.mjs → scanner-DUoXnmtI.mjs} +291 -306
  16. package/dist/scanner-DUoXnmtI.mjs.map +1 -0
  17. package/dist/{server-CjK63H6X.mjs → server-Dtq8vqbK.mjs} +172 -20
  18. package/dist/server-Dtq8vqbK.mjs.map +1 -0
  19. package/dist/server.mjs +5 -5
  20. package/dist/{svelte-parser-COTNG92w.mjs → svelte-parser-1bBcMx3T.mjs} +16 -6
  21. package/dist/{svelte-parser-COTNG92w.mjs.map → svelte-parser-1bBcMx3T.mjs.map} +1 -1
  22. package/dist/{transaction-B0V0_SE2.mjs → transaction-C_Pvux78.mjs} +102 -2
  23. package/dist/{transaction-B0V0_SE2.mjs.map → transaction-C_Pvux78.mjs.map} +1 -1
  24. package/dist/tsx-parser-BFLv292Y.mjs +2 -0
  25. package/dist/{tsx-parser-CYVyBWjn.mjs → tsx-parser-BWiMGAIR.mjs} +3 -2
  26. package/dist/tsx-parser-BWiMGAIR.mjs.map +1 -0
  27. package/dist/{vue-parser-CjS4YAoQ.mjs → vue-parser-DTPTN3Yb.mjs} +16 -5
  28. package/dist/{vue-parser-CjS4YAoQ.mjs.map → vue-parser-DTPTN3Yb.mjs.map} +1 -1
  29. package/package.json +2 -2
  30. package/dist/apply-manager-Cf5OWOJ7.mjs.map +0 -1
  31. package/dist/scanner-DPrbU-oK.mjs.map +0 -1
  32. package/dist/server-CjK63H6X.mjs.map +0 -1
  33. package/dist/tsx-parser-Bf8Firrq.mjs +0 -2
  34. package/dist/tsx-parser-CYVyBWjn.mjs.map +0 -1
@@ -1,73 +1,36 @@
1
1
  import { o as readText } from "./fs-BnIQ4dKj.mjs";
2
2
  import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-QgnJ1-52.mjs";
3
- import { t as parseTsx } from "./tsx-parser-CYVyBWjn.mjs";
3
+ import { t as parseTsx } from "./tsx-parser-BWiMGAIR.mjs";
4
4
  import { extname, join } from "node:path";
5
5
  //#region src/core/ast-scanner/pre-filter.ts
6
- const PURE_NUMBER_RE = /^\d+(\.\d+)?$/;
6
+ const PURE_NUMBER_RE = /^-?\d+(\.\d+)?$/;
7
7
  const HEX_COLOR_RE = /^#[0-9a-f]{3,8}$/i;
8
- const FILE_EXT_RE$1 = /\.(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;
9
- const HTML_PROP_VALUES = new Set([
10
- "class",
11
- "variant",
12
- "secondary",
13
- "outline",
14
- "ghost",
15
- "destructive",
16
- "default",
17
- "primary",
8
+ const 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;
9
+ const SVG_PATH_DATA_RE = /^[Mm][\d\s.,LHVCSQTAZlhvcsqtazmMzZ-]+$/;
10
+ const SVG_VIEWBOX_RE = /^\d+(\.\d+)?\s+\d+(\.\d+)?\s+\d+(\.\d+)?\s+\d+(\.\d+)?$/;
11
+ const I18N_KEY_RE = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
12
+ const TECHNICAL_IDENTIFIER_RE = /^[_a-z][a-z0-9_-]*$/;
13
+ const ERROR_CODE_RE = /^[A-Z][A-Z0-9_]+$/;
14
+ const PLACEHOLDER_RE = /^\{\d+\}$|^\.{2,}$/;
15
+ const CAMEL_CASE_RE = /^[a-z]+[A-Z]/;
16
+ const LOCALE_CODE_RE = /^[a-z]{2}[-_][A-Z]{2}$/;
17
+ const DIMENSION_RE = /^\d+[x×]\d+$/;
18
+ const REPEAT_CHAR_RE = /^(.)\1{3,}$/;
19
+ const MIME_TYPE_RE = /^(application|text|image|audio|video|multipart|font)\/[\w.+-]+$/;
20
+ const PASCAL_CASE_RE = /^[A-Z][a-z]+[A-Z]/;
21
+ const HTML_TARGETS = new Set([
18
22
  "_blank",
19
- "noopener",
20
- "noreferrer",
21
- "button",
22
- "submit",
23
- "reset",
24
- "text",
25
- "numeric",
26
- "password",
27
- "email",
28
- "checkbox",
29
- "radio",
30
- "hidden",
31
- "none",
32
- "auto",
33
- "inherit",
34
- "initial",
35
- "sm",
36
- "md",
37
- "lg",
38
- "xl",
39
- "xs",
40
- "2xl",
41
- "3xl",
42
- "icon",
43
- "icon-sm",
44
- "icon-lg"
45
- ]);
46
- const SLOT_EVENT_NAMES = new Set([
47
- "header",
48
- "footer",
49
- "default",
50
- "trigger",
51
- "content",
52
- "sidebar",
53
- "overlay",
54
- "body",
55
- "actions",
56
- "click",
57
- "change",
58
- "input",
59
- "focus",
60
- "blur",
61
- "submit",
62
- "mounted",
63
- "unmounted",
64
- "updated"
23
+ "_self",
24
+ "_parent",
25
+ "_top"
65
26
  ]);
27
+ function isURLLike(str) {
28
+ if (/^(https?|ftp|file|mailto|data):/.test(str)) return true;
29
+ if (/^(\.\.?\/|\/|[A-Za-z]:\\)/.test(str)) return true;
30
+ if (/^['"]?[@a-z][\w-]*/.test(str.toLowerCase()) && !str.includes(" ") && (str.includes("/") || str.includes("."))) return true;
31
+ return false;
32
+ }
66
33
  const 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-)/;
67
- /**
68
- * Returns true if the string looks like a CSS class list (Tailwind or similar).
69
- * Requires 2+ space-separated segments with majority matching utility patterns.
70
- */
71
34
  function isCssClassList(value) {
72
35
  const segments = value.trim().split(/\s+/);
73
36
  if (segments.length < 2) return false;
@@ -75,116 +38,223 @@ function isCssClassList(value) {
75
38
  for (const seg of segments) if (TAILWIND_SEGMENT_RE.test(seg)) matched++;
76
39
  return matched / segments.length >= .5;
77
40
  }
78
- /**
79
- * Returns true if the string is a single CSS utility token (e.g. "bg-blue-500").
80
- */
81
41
  function isSingleCssUtility(value) {
82
42
  const trimmed = value.trim();
83
43
  if (trimmed.includes(" ")) return false;
84
44
  return TAILWIND_SEGMENT_RE.test(trimmed);
85
45
  }
86
- const PRE_FILTER_RULES = [
87
- {
88
- context: "import_path",
89
- reason: "import_path"
90
- },
91
- {
92
- context: "type_annotation",
93
- reason: "type_annotation"
94
- },
95
- {
96
- context: "css_class",
97
- reason: "css_class"
98
- },
99
- {
100
- context: "css_utility_call",
101
- reason: "css_utility_call"
102
- },
103
- {
104
- context: "console_call",
105
- reason: "console_call"
106
- },
107
- {
108
- context: "test_assertion",
109
- reason: "test_assertion"
110
- },
111
- {
112
- context: "switch_case",
113
- reason: "switch_case"
114
- },
115
- {
116
- condition: (v) => v.length <= 1,
117
- reason: "single_char"
118
- },
119
- {
120
- condition: (v) => PURE_NUMBER_RE.test(v),
121
- reason: "pure_number"
122
- },
123
- {
124
- condition: (v) => v.startsWith("--"),
125
- reason: "cli_flag"
126
- },
127
- {
128
- condition: (v) => HEX_COLOR_RE.test(v),
129
- reason: "hex_color"
130
- },
131
- {
132
- condition: (v) => FILE_EXT_RE$1.test(v),
133
- reason: "file_extension"
134
- },
135
- {
136
- condition: (v) => HTML_PROP_VALUES.has(v.toLowerCase()),
137
- reason: "html_prop_value"
138
- },
139
- {
140
- condition: (v) => isCssClassList(v),
141
- reason: "css_class_list"
142
- },
143
- {
144
- condition: (v) => isSingleCssUtility(v),
145
- reason: "css_utility_token"
146
- },
147
- {
148
- condition: (v) => {
149
- const lower = v.toLowerCase();
150
- return v === lower && !v.includes(" ") && SLOT_EVENT_NAMES.has(lower);
151
- },
152
- reason: "slot_event_name"
46
+ const SVG_TECHNICAL_ATTRIBUTES = new Set([
47
+ "d",
48
+ "viewBox",
49
+ "points",
50
+ "transform",
51
+ "pathLength",
52
+ "xmlns",
53
+ "preserveAspectRatio",
54
+ "stroke-linecap",
55
+ "stroke-linejoin",
56
+ "stroke-width",
57
+ "stroke-dasharray",
58
+ "stroke-dashoffset",
59
+ "stroke-miterlimit",
60
+ "fill-rule",
61
+ "clip-rule"
62
+ ]);
63
+ const SVG_GRAPHIC_ELEMENTS = new Set([
64
+ "svg",
65
+ "path",
66
+ "circle",
67
+ "rect",
68
+ "line",
69
+ "polyline",
70
+ "polygon",
71
+ "ellipse",
72
+ "g",
73
+ "defs",
74
+ "use",
75
+ "symbol",
76
+ "clipPath",
77
+ "mask",
78
+ "pattern",
79
+ "linearGradient",
80
+ "radialGradient",
81
+ "stop",
82
+ "marker",
83
+ "animate",
84
+ "animateTransform",
85
+ "image"
86
+ ]);
87
+ const I18N_FUNCTIONS = new Set([
88
+ "t",
89
+ "$t",
90
+ "i18n",
91
+ "translate",
92
+ "formatMessage",
93
+ "msg"
94
+ ]);
95
+ const EMIT_FUNCTIONS = new Set(["emit", "$emit"]);
96
+ const TRANSLATABLE_ATTRIBUTES = new Set([
97
+ "title",
98
+ "alt",
99
+ "placeholder",
100
+ "label",
101
+ "summary",
102
+ "caption",
103
+ "abbr",
104
+ "accesskey",
105
+ "content",
106
+ "description",
107
+ "aria-label",
108
+ "aria-description",
109
+ "aria-placeholder",
110
+ "aria-roledescription",
111
+ "aria-valuetext",
112
+ "accessibilityLabel",
113
+ "accessibilityHint",
114
+ "accessibilityValue",
115
+ "heading",
116
+ "subheading",
117
+ "message",
118
+ "hint",
119
+ "tooltip",
120
+ "helper-text",
121
+ "error-message",
122
+ "success-message",
123
+ "confirm-text",
124
+ "cancel-text",
125
+ "empty-text",
126
+ "loading-text",
127
+ "no-data-text",
128
+ "no-results-text"
129
+ ]);
130
+ const TRANSLATABLE_PROPERTIES = new Set([
131
+ "label",
132
+ "title",
133
+ "description",
134
+ "text",
135
+ "message",
136
+ "placeholder",
137
+ "caption",
138
+ "summary",
139
+ "heading",
140
+ "subheading",
141
+ "subtitle",
142
+ "tooltip",
143
+ "hint",
144
+ "helpText",
145
+ "errorMessage",
146
+ "successMessage",
147
+ "name"
148
+ ]);
149
+ /**
150
+ * Determines if a string is definitely NOT user-visible content.
151
+ * Returns skip reason if it should be filtered, null if it should proceed to scoring.
152
+ *
153
+ * Conservative for template_text/jsx_text (tag-between text is almost always content).
154
+ * Aggressive for everything else (technical tokens, config values, framework artifacts).
155
+ */
156
+ function shouldSkip(str) {
157
+ if (str.context === "import_path") return "import_path";
158
+ if (str.context === "type_annotation") return "type_annotation";
159
+ if (str.context === "css_class") return "css_class";
160
+ if (str.context === "css_utility_call") return "css_utility_call";
161
+ if (str.context === "console_call") return "console_call";
162
+ if (str.context === "test_assertion") return "test_assertion";
163
+ if (str.context === "switch_case") return "switch_case";
164
+ const v = str.value;
165
+ if (v.length <= 1) return "single_char";
166
+ if (/^\s+$/.test(v)) return "whitespace";
167
+ if (PURE_NUMBER_RE.test(v)) return "pure_number";
168
+ if (HEX_COLOR_RE.test(v)) return "hex_color";
169
+ if (FILE_EXT_RE.test(v)) return "file_extension";
170
+ if (v.startsWith("--")) return "cli_flag";
171
+ if (I18N_KEY_RE.test(v)) return "i18n_key";
172
+ if (MIME_TYPE_RE.test(v)) return "mime_type";
173
+ if (isURLLike(v)) return "url_path";
174
+ if (isCssClassList(v)) return "css_class_list";
175
+ if (isSingleCssUtility(v)) return "css_utility_token";
176
+ if (v.length > 3 && SVG_PATH_DATA_RE.test(v)) return "svg_path_data";
177
+ if (SVG_VIEWBOX_RE.test(v)) return "svg_viewbox";
178
+ if (str.parentProperty !== void 0 && SVG_TECHNICAL_ATTRIBUTES.has(str.parentProperty)) return "svg_technical_attr";
179
+ if (str.context === "template_attribute" && SVG_GRAPHIC_ELEMENTS.has(str.parent)) return "svg_element_attr";
180
+ if (v.startsWith("update:")) return "vue_emit_event";
181
+ if (PLACEHOLDER_RE.test(v)) return "placeholder";
182
+ if (LOCALE_CODE_RE.test(v)) return "locale_code";
183
+ if (DIMENSION_RE.test(v)) return "dimension";
184
+ if (REPEAT_CHAR_RE.test(v)) return "repeat_chars";
185
+ if (HTML_TARGETS.has(v)) return "html_target";
186
+ if (str.context === "function_argument" && I18N_FUNCTIONS.has(str.parent)) {
187
+ if (/^[a-z][a-z0-9_.-]*$/.test(v)) return "i18n_function_arg";
188
+ }
189
+ if (str.context === "function_argument" && EMIT_FUNCTIONS.has(str.parent)) return "emit_event_arg";
190
+ if (str.context !== "template_text" && str.context !== "jsx_text") {
191
+ if (TECHNICAL_IDENTIFIER_RE.test(v) && v.length < 30) return "technical_identifier";
192
+ }
193
+ if (ERROR_CODE_RE.test(v) && v.includes("_") && v.length > 3) return "error_code";
194
+ return null;
195
+ }
196
+ /**
197
+ * Calculates a content confidence score (0-1) for a string that passed shouldSkip.
198
+ * Uses AST context metadata (our advantage over offset-based tools) combined with
199
+ * value-based signals proven by i18next-cli.
200
+ *
201
+ * Base score: 0.5. Boosted/penalized by context and value characteristics.
202
+ */
203
+ function calculateContentScore(str) {
204
+ let score = .5;
205
+ if (str.context === "template_text" || str.context === "jsx_text") score += .3;
206
+ if (str.context === "template_attribute" || str.context === "jsx_attribute") if (str.parentProperty && TRANSLATABLE_ATTRIBUTES.has(str.parentProperty)) score += .2;
207
+ else score -= .2;
208
+ if (str.context === "object_property") {
209
+ if (str.parentProperty && TRANSLATABLE_PROPERTIES.has(str.parentProperty)) score += .25;
153
210
  }
154
- ];
211
+ const wordCount = str.value.split(/\s+/).length;
212
+ if (wordCount >= 3) score += .2;
213
+ else if (wordCount === 2) score += .1;
214
+ if (/[.!?:;]$/.test(str.value)) score += .1;
215
+ if (/[\u0080-\uFFFF]/.test(str.value)) score += .15;
216
+ if (/^[A-Z]/.test(str.value) && /[a-z]/.test(str.value)) score += .1;
217
+ if (CAMEL_CASE_RE.test(str.value)) score -= .3;
218
+ if (PASCAL_CASE_RE.test(str.value) && !str.value.includes(" ")) score -= .25;
219
+ if (/^[A-Z]{2,5}$/.test(str.value)) score -= .15;
220
+ if (str.value.includes("/") && !str.value.includes(" ")) score -= .2;
221
+ return Math.max(0, Math.min(1, score));
222
+ }
155
223
  /**
156
- * Structural pre-filter: removes strings that are 100% NOT content.
224
+ * Two-phase pre-filter:
225
+ * 1. shouldSkip(): Binary removal of definite non-content
226
+ * 2. calculateContentScore(): 0-1 confidence scoring for ambiguous strings
157
227
  *
158
- * Conservative — when in doubt, INCLUDE the string.
159
- * Returns candidates (passed), count of filtered, and reason breakdown.
228
+ * Returns candidates that passed both phases, with content scores attached.
160
229
  */
161
- function applyPreFilter(strings) {
230
+ function applyPreFilter(strings, minScore = .4) {
162
231
  const candidates = [];
163
- const filterReasons = {};
164
- let filtered = 0;
232
+ const skipReasons = {};
233
+ let skipped = 0;
234
+ let lowConfidence = 0;
165
235
  for (const str of strings) {
166
- const matchedRule = findMatchingRule(str);
167
- if (matchedRule) {
168
- filtered++;
169
- filterReasons[matchedRule.reason] = (filterReasons[matchedRule.reason] ?? 0) + 1;
170
- } else candidates.push(str);
236
+ const skipReason = shouldSkip(str);
237
+ if (skipReason) {
238
+ skipped++;
239
+ skipReasons[skipReason] = (skipReasons[skipReason] ?? 0) + 1;
240
+ continue;
241
+ }
242
+ const contentScore = calculateContentScore(str);
243
+ if (contentScore < minScore) {
244
+ lowConfidence++;
245
+ skipReasons["low_confidence"] = (skipReasons["low_confidence"] ?? 0) + 1;
246
+ continue;
247
+ }
248
+ str.contentScore = contentScore;
249
+ candidates.push(str);
171
250
  }
172
251
  return {
173
252
  candidates,
174
- filtered,
175
- filterReasons
253
+ skipped,
254
+ lowConfidence,
255
+ skipReasons
176
256
  };
177
257
  }
178
- /**
179
- * Find the first pre-filter rule that matches this string.
180
- * Returns the rule if matched, undefined if the string should pass through.
181
- */
182
- function findMatchingRule(str) {
183
- for (const rule of PRE_FILTER_RULES) {
184
- if (rule.context !== void 0 && str.context === rule.context) return rule;
185
- if (rule.condition !== void 0 && rule.condition(str.value)) return rule;
186
- }
187
- }
188
258
  //#endregion
189
259
  //#region src/core/ast-scanner/index.ts
190
260
  const TSX_EXTENSIONS = new Set([
@@ -203,7 +273,7 @@ const ASTRO_EXTENSIONS = new Set([".astro"]);
203
273
  */
204
274
  async function loadVueParser() {
205
275
  try {
206
- return (await import("./vue-parser-CjS4YAoQ.mjs")).parseVue;
276
+ return (await import("./vue-parser-DTPTN3Yb.mjs")).parseVue;
207
277
  } catch {
208
278
  return;
209
279
  }
@@ -214,7 +284,7 @@ async function loadVueParser() {
214
284
  */
215
285
  async function loadSvelteParser() {
216
286
  try {
217
- return (await import("./svelte-parser-COTNG92w.mjs")).parseSvelte;
287
+ return (await import("./svelte-parser-1bBcMx3T.mjs")).parseSvelte;
218
288
  } catch {
219
289
  return;
220
290
  }
@@ -225,7 +295,7 @@ async function loadSvelteParser() {
225
295
  */
226
296
  async function loadAstroParser() {
227
297
  try {
228
- return (await import("./astro-parser-RofKmKQi.mjs")).parseAstro;
298
+ return (await import("./astro-parser-BJ-54JGe.mjs")).parseAstro;
229
299
  } catch {
230
300
  return;
231
301
  }
@@ -343,6 +413,7 @@ const DEFAULT_LIMIT = 50;
343
413
  const DEFAULT_OFFSET = 0;
344
414
  const DEFAULT_MIN_LENGTH = 2;
345
415
  const DEFAULT_MAX_LENGTH = 500;
416
+ const DEFAULT_MIN_SCORE = .4;
346
417
  const SUMMARY_SAMPLE_SIZE = 10;
347
418
  const CONTEXT_MAP = {
348
419
  "template_text": "template_text",
@@ -364,142 +435,17 @@ const CONTEXT_MAP = {
364
435
  "console_call": "other",
365
436
  "test_assertion": "other"
366
437
  };
367
- const IMPORT_PATH_RE = /^(\.\/|\.\.\/|@\/|@[a-z]|[a-z][a-z0-9-]*\/)/;
368
- const 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:)/;
369
- const URL_ROUTE_RE = /^(https?:\/\/|\/api\/|\/v[0-9]|#\/|mailto:|tel:)/;
370
- const ROUTE_LIKE_RE = /^\/[a-z0-9/:_-]*$/i;
371
- const COLOR_RE = /^(#[0-9a-f]{3,8}|rgba?\(|hsla?\(|transparent|inherit|currentColor)$/i;
372
- const CAMEL_CASE_RE = /^[a-z][a-zA-Z0-9]*$/;
373
- const SCREAMING_SNAKE_RE = /^[A-Z][A-Z0-9_]+$/;
374
- const KEBAB_TECH_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
375
- const 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;
376
- const REGEX_LIKE_RE = /^\^|^\(.*\)$|\$$|\\d|\\w|\\s|\.\*|\.\+/;
377
- const CONSOLE_RE = /console\.(log|warn|error|debug|info|trace)\s*\(/;
378
- const CONTENT_WORDS = new Set([
379
- "sign",
380
- "up",
381
- "log",
382
- "in",
383
- "out",
384
- "get",
385
- "started",
386
- "learn",
387
- "more",
388
- "read",
389
- "click",
390
- "here",
391
- "go",
392
- "back",
393
- "next",
394
- "prev",
395
- "previous",
396
- "see",
397
- "all",
398
- "view",
399
- "show",
400
- "hide",
401
- "open",
402
- "close",
403
- "add",
404
- "new",
405
- "save",
406
- "edit",
407
- "delete",
408
- "remove",
409
- "submit",
410
- "cancel",
411
- "confirm",
412
- "welcome",
413
- "hello",
414
- "thank",
415
- "you",
416
- "good",
417
- "bye",
418
- "hi",
419
- "hey",
420
- "no",
421
- "yes",
422
- "ok",
423
- "sorry",
424
- "please",
425
- "help",
426
- "about",
427
- "contact",
428
- "home",
429
- "page",
430
- "not",
431
- "found",
432
- "error",
433
- "success",
434
- "loading"
435
- ]);
436
- /**
437
- * Determines if a string is non-content (technical) and should be filtered out.
438
- * Pure function — no side effects, no semantic decisions.
439
- * Conservative: if in doubt, returns false (keeps the string).
440
- *
441
- * @deprecated Kept for backward compatibility. Scanner v2 uses structural pre-filter instead.
442
- */
443
- function isNonContent(value, lineContext, minLength = DEFAULT_MIN_LENGTH, maxLength = DEFAULT_MAX_LENGTH) {
444
- if (value.length < minLength) return true;
445
- if (value.length > maxLength) return true;
446
- if (IMPORT_PATH_RE.test(value) && !value.includes(" ")) return true;
447
- if (CSS_CLASS_PREFIX_RE.test(value) && !value.includes(" ")) return true;
448
- if (value.includes(" ") && looksLikeCssClasses(value)) return true;
449
- if (URL_ROUTE_RE.test(value)) return true;
450
- if (value.startsWith("/") && ROUTE_LIKE_RE.test(value)) return true;
451
- if (COLOR_RE.test(value)) return true;
452
- if (isTechnicalIdentifier(value)) return true;
453
- if (FILE_EXT_RE.test(value)) return true;
454
- if (REGEX_LIKE_RE.test(value)) return true;
455
- if (CONSOLE_RE.test(lineContext)) return true;
456
- return false;
457
- }
458
- function isTechnicalIdentifier(value) {
459
- if (value.includes(" ")) return false;
460
- if (CAMEL_CASE_RE.test(value) && /[A-Z]/.test(value)) return true;
461
- if (SCREAMING_SNAKE_RE.test(value) && value.includes("_")) return true;
462
- if (KEBAB_TECH_RE.test(value)) {
463
- if (!value.split("-").some((p) => CONTENT_WORDS.has(p))) return true;
464
- }
465
- if (/^[a-z][a-z0-9-]*$/.test(value) && value.length < 40 && !value.includes(" ")) {
466
- if (value.includes("/")) return true;
467
- }
468
- return false;
469
- }
470
- function looksLikeCssClasses(value) {
471
- const tokens = value.split(/\s+/);
472
- if (tokens.length < 2) return false;
473
- return tokens.filter((t) => isCssClassToken(t)).length === tokens.length;
474
- }
475
- function isCssClassToken(token) {
476
- if (CSS_CLASS_PREFIX_RE.test(token)) return true;
477
- 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;
478
- if (/^[a-z]+-\[.+\]$/.test(token)) return true;
479
- if (/^(sm|md|lg|xl|2xl|hover|focus|active|disabled|first|last|odd|even|dark|group-hover|peer-checked):/.test(token)) return true;
480
- return false;
481
- }
482
- /**
483
- * Apply legacy isNonContent filter as a secondary pass on v2 candidates.
484
- * This catches heuristic patterns (URLs, regex, camelCase identifiers, etc.)
485
- * that the structural pre-filter does not cover.
486
- */
487
- function applySecondaryFilter(value, _surrounding, minLength, maxLength) {
488
- return isNonContent(value, "", minLength, maxLength);
489
- }
490
438
  async function scanCandidates(projectRoot, options) {
491
439
  const limit = options?.limit ?? DEFAULT_LIMIT;
492
440
  const offset = options?.offset ?? DEFAULT_OFFSET;
493
441
  const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH;
494
442
  const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH;
443
+ const minScore = options?.min_score ?? DEFAULT_MIN_SCORE;
495
444
  const files = await discoverFiles(projectRoot, {
496
445
  paths: options?.paths ?? await autoDetectSourceDirs(projectRoot),
497
446
  include: options?.include,
498
447
  exclude: options?.exclude
499
448
  });
500
- const allCandidates = [];
501
- const dupeMap = /* @__PURE__ */ new Map();
502
- let rawStringsFound = 0;
503
449
  const filePromises = files.map(async (relPath) => {
504
450
  const filePath = join(projectRoot, relPath);
505
451
  const content = await readText(filePath);
@@ -513,51 +459,89 @@ async function scanCandidates(projectRoot, options) {
513
459
  };
514
460
  });
515
461
  const fileResults = await Promise.all(filePromises);
462
+ let rawStringsFound = 0;
463
+ let skippedCount = 0;
464
+ let lowConfidenceCount = 0;
465
+ const skipReasons = {};
466
+ const uniqueMap = /* @__PURE__ */ new Map();
467
+ const dupeMap = /* @__PURE__ */ new Map();
516
468
  for (const { relPath, extractions } of fileResults) {
517
469
  rawStringsFound += extractions.length;
518
470
  for (const extraction of extractions) {
519
- if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue;
471
+ if (extraction.value.length < minLength || extraction.value.length > maxLength) {
472
+ skippedCount++;
473
+ skipReasons["length_filter"] = (skipReasons["length_filter"] ?? 0) + 1;
474
+ continue;
475
+ }
476
+ const contentScore = extraction.contentScore ?? calculateContentScore(extraction);
477
+ if (contentScore < minScore) {
478
+ lowConfidenceCount++;
479
+ skipReasons["low_confidence"] = (skipReasons["low_confidence"] ?? 0) + 1;
480
+ continue;
481
+ }
520
482
  const mappedContext = CONTEXT_MAP[extraction.context];
521
- const candidate = {
522
- file: relPath,
523
- line: extraction.line,
524
- column: extraction.column,
525
- value: extraction.value,
526
- context: mappedContext,
527
- surrounding: extraction.surrounding
528
- };
529
- allCandidates.push(candidate);
530
- const key = extraction.value;
531
- if (!dupeMap.has(key)) dupeMap.set(key, []);
532
- dupeMap.get(key).push({
483
+ const loc = {
533
484
  file: relPath,
534
485
  line: extraction.line
486
+ };
487
+ if (!dupeMap.has(extraction.value)) dupeMap.set(extraction.value, []);
488
+ dupeMap.get(extraction.value).push(loc);
489
+ if (!uniqueMap.has(extraction.value)) uniqueMap.set(extraction.value, {
490
+ candidate: {
491
+ file: relPath,
492
+ line: extraction.line,
493
+ column: extraction.column,
494
+ value: extraction.value,
495
+ context: mappedContext,
496
+ surrounding: extraction.surrounding,
497
+ contentScore,
498
+ occurrences: [loc]
499
+ },
500
+ maxScore: contentScore
535
501
  });
502
+ else {
503
+ const entry = uniqueMap.get(extraction.value);
504
+ entry.candidate.occurrences.push(loc);
505
+ if (contentScore > entry.maxScore) {
506
+ entry.maxScore = contentScore;
507
+ entry.candidate.contentScore = contentScore;
508
+ entry.candidate.file = relPath;
509
+ entry.candidate.line = extraction.line;
510
+ entry.candidate.column = extraction.column;
511
+ entry.candidate.context = mappedContext;
512
+ entry.candidate.surrounding = extraction.surrounding;
513
+ }
514
+ }
536
515
  }
537
516
  }
517
+ const allUniqueCandidates = [...uniqueMap.values()].map((e) => e.candidate).toSorted((a, b) => b.contentScore - a.contentScore);
538
518
  const duplicates = [...dupeMap.entries()].filter(([, occurrences]) => occurrences.length >= 2).map(([value, occurrences]) => ({
539
519
  value,
540
520
  count: occurrences.length,
541
521
  occurrences
542
522
  })).toSorted((a, b) => b.count - a.count);
543
- const afterFiltering = allCandidates.length;
544
- const paginated = allCandidates.slice(offset, offset + limit);
545
- const hasMore = afterFiltering > offset + limit;
523
+ const uniqueCount = allUniqueCandidates.length;
524
+ const paginated = allUniqueCandidates.slice(offset, offset + limit);
525
+ const hasMore = uniqueCount > offset + limit;
546
526
  return {
547
527
  candidates: paginated,
548
528
  duplicates,
549
529
  stats: {
550
530
  files_scanned: files.length,
551
531
  raw_strings_found: rawStringsFound,
552
- after_filtering: afterFiltering,
532
+ skipped: skippedCount,
533
+ low_confidence: lowConfidenceCount,
534
+ unique_candidates: uniqueCount,
553
535
  candidates_returned: paginated.length,
554
- has_more: hasMore
536
+ has_more: hasMore,
537
+ skip_reasons: skipReasons
555
538
  }
556
539
  };
557
540
  }
558
541
  async function scanSummary(projectRoot, options) {
559
542
  const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH;
560
543
  const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH;
544
+ const minScore = options?.min_score ?? DEFAULT_MIN_SCORE;
561
545
  const files = await discoverFiles(projectRoot, {
562
546
  paths: options?.paths ?? await autoDetectSourceDirs(projectRoot),
563
547
  include: options?.include,
@@ -587,7 +571,8 @@ async function scanSummary(projectRoot, options) {
587
571
  });
588
572
  const sampleResults = await Promise.all(samplePromises);
589
573
  for (const extractions of sampleResults) for (const extraction of extractions) {
590
- if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue;
574
+ if (extraction.value.length < minLength || extraction.value.length > maxLength) continue;
575
+ if ((extraction.contentScore ?? calculateContentScore(extraction)) < minScore) continue;
591
576
  sampleCandidates++;
592
577
  const prev = freqMap.get(extraction.value) ?? 0;
593
578
  freqMap.set(extraction.value, prev + 1);
@@ -614,6 +599,6 @@ async function scanSummary(projectRoot, options) {
614
599
  };
615
600
  }
616
601
  //#endregion
617
- export { scanCandidates as n, scanSummary as r, isNonContent as t };
602
+ export { scanSummary as n, scanCandidates as t };
618
603
 
619
- //# sourceMappingURL=scanner-DPrbU-oK.mjs.map
604
+ //# sourceMappingURL=scanner-DUoXnmtI.mjs.map