@contentrain/mcp 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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-CQOxARUU.mjs} +275 -308
  16. package/dist/scanner-CQOxARUU.mjs.map +1 -0
  17. package/dist/{server-BXguCuJb.mjs → server-DfWRpzdP.mjs} +172 -20
  18. package/dist/server-DfWRpzdP.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-BXguCuJb.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,25 @@
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",
18
- "_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"
65
- ]);
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
+ function isURLLike(str) {
17
+ if (/^(https?|ftp|file|mailto|data):/.test(str)) return true;
18
+ if (/^(\.\.?\/|\/|[A-Za-z]:\\)/.test(str)) return true;
19
+ if (/^['"]?[@a-z][\w-]*/.test(str.toLowerCase()) && !str.includes(" ") && (str.includes("/") || str.includes("."))) return true;
20
+ return false;
21
+ }
66
22
  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
23
  function isCssClassList(value) {
72
24
  const segments = value.trim().split(/\s+/);
73
25
  if (segments.length < 2) return false;
@@ -75,116 +27,216 @@ function isCssClassList(value) {
75
27
  for (const seg of segments) if (TAILWIND_SEGMENT_RE.test(seg)) matched++;
76
28
  return matched / segments.length >= .5;
77
29
  }
78
- /**
79
- * Returns true if the string is a single CSS utility token (e.g. "bg-blue-500").
80
- */
81
30
  function isSingleCssUtility(value) {
82
31
  const trimmed = value.trim();
83
32
  if (trimmed.includes(" ")) return false;
84
33
  return TAILWIND_SEGMENT_RE.test(trimmed);
85
34
  }
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"
35
+ const SVG_TECHNICAL_ATTRIBUTES = new Set([
36
+ "d",
37
+ "viewBox",
38
+ "points",
39
+ "transform",
40
+ "pathLength",
41
+ "xmlns",
42
+ "preserveAspectRatio",
43
+ "stroke-linecap",
44
+ "stroke-linejoin",
45
+ "stroke-width",
46
+ "stroke-dasharray",
47
+ "stroke-dashoffset",
48
+ "stroke-miterlimit",
49
+ "fill-rule",
50
+ "clip-rule"
51
+ ]);
52
+ const SVG_GRAPHIC_ELEMENTS = new Set([
53
+ "svg",
54
+ "path",
55
+ "circle",
56
+ "rect",
57
+ "line",
58
+ "polyline",
59
+ "polygon",
60
+ "ellipse",
61
+ "g",
62
+ "defs",
63
+ "use",
64
+ "symbol",
65
+ "clipPath",
66
+ "mask",
67
+ "pattern",
68
+ "linearGradient",
69
+ "radialGradient",
70
+ "stop",
71
+ "marker",
72
+ "animate",
73
+ "animateTransform",
74
+ "image"
75
+ ]);
76
+ const I18N_FUNCTIONS = new Set([
77
+ "t",
78
+ "$t",
79
+ "i18n",
80
+ "translate",
81
+ "formatMessage",
82
+ "msg"
83
+ ]);
84
+ const EMIT_FUNCTIONS = new Set(["emit", "$emit"]);
85
+ const TRANSLATABLE_ATTRIBUTES = new Set([
86
+ "title",
87
+ "alt",
88
+ "placeholder",
89
+ "label",
90
+ "summary",
91
+ "caption",
92
+ "abbr",
93
+ "accesskey",
94
+ "content",
95
+ "description",
96
+ "aria-label",
97
+ "aria-description",
98
+ "aria-placeholder",
99
+ "aria-roledescription",
100
+ "aria-valuetext",
101
+ "accessibilityLabel",
102
+ "accessibilityHint",
103
+ "accessibilityValue",
104
+ "heading",
105
+ "subheading",
106
+ "message",
107
+ "hint",
108
+ "tooltip",
109
+ "helper-text",
110
+ "error-message",
111
+ "success-message",
112
+ "confirm-text",
113
+ "cancel-text",
114
+ "empty-text",
115
+ "loading-text",
116
+ "no-data-text",
117
+ "no-results-text"
118
+ ]);
119
+ const TRANSLATABLE_PROPERTIES = new Set([
120
+ "label",
121
+ "title",
122
+ "description",
123
+ "text",
124
+ "message",
125
+ "placeholder",
126
+ "caption",
127
+ "summary",
128
+ "heading",
129
+ "subheading",
130
+ "subtitle",
131
+ "tooltip",
132
+ "hint",
133
+ "helpText",
134
+ "errorMessage",
135
+ "successMessage",
136
+ "name"
137
+ ]);
138
+ /**
139
+ * Determines if a string is definitely NOT user-visible content.
140
+ * Returns skip reason if it should be filtered, null if it should proceed to scoring.
141
+ *
142
+ * Conservative for template_text/jsx_text (tag-between text is almost always content).
143
+ * Aggressive for everything else (technical tokens, config values, framework artifacts).
144
+ */
145
+ function shouldSkip(str) {
146
+ if (str.context === "import_path") return "import_path";
147
+ if (str.context === "type_annotation") return "type_annotation";
148
+ if (str.context === "css_class") return "css_class";
149
+ if (str.context === "css_utility_call") return "css_utility_call";
150
+ if (str.context === "console_call") return "console_call";
151
+ if (str.context === "test_assertion") return "test_assertion";
152
+ if (str.context === "switch_case") return "switch_case";
153
+ const v = str.value;
154
+ if (v.length <= 1) return "single_char";
155
+ if (/^\s+$/.test(v)) return "whitespace";
156
+ if (PURE_NUMBER_RE.test(v)) return "pure_number";
157
+ if (HEX_COLOR_RE.test(v)) return "hex_color";
158
+ if (FILE_EXT_RE.test(v)) return "file_extension";
159
+ if (v.startsWith("--")) return "cli_flag";
160
+ if (I18N_KEY_RE.test(v)) return "i18n_key";
161
+ if (isURLLike(v)) return "url_path";
162
+ if (isCssClassList(v)) return "css_class_list";
163
+ if (isSingleCssUtility(v)) return "css_utility_token";
164
+ if (v.length > 3 && SVG_PATH_DATA_RE.test(v)) return "svg_path_data";
165
+ if (SVG_VIEWBOX_RE.test(v)) return "svg_viewbox";
166
+ if (str.parentProperty !== void 0 && SVG_TECHNICAL_ATTRIBUTES.has(str.parentProperty)) return "svg_technical_attr";
167
+ if (str.context === "template_attribute" && SVG_GRAPHIC_ELEMENTS.has(str.parent)) return "svg_element_attr";
168
+ if (v.startsWith("update:")) return "vue_emit_event";
169
+ if (PLACEHOLDER_RE.test(v)) return "placeholder";
170
+ if (str.context === "function_argument" && I18N_FUNCTIONS.has(str.parent)) {
171
+ if (/^[a-z][a-z0-9_.-]*$/.test(v)) return "i18n_function_arg";
172
+ }
173
+ if (str.context === "function_argument" && EMIT_FUNCTIONS.has(str.parent)) return "emit_event_arg";
174
+ if (str.context !== "template_text" && str.context !== "jsx_text") {
175
+ if (TECHNICAL_IDENTIFIER_RE.test(v) && v.length < 30) return "technical_identifier";
153
176
  }
154
- ];
177
+ if (ERROR_CODE_RE.test(v) && v.includes("_") && v.length > 3) return "error_code";
178
+ return null;
179
+ }
155
180
  /**
156
- * Structural pre-filter: removes strings that are 100% NOT content.
181
+ * Calculates a content confidence score (0-1) for a string that passed shouldSkip.
182
+ * Uses AST context metadata (our advantage over offset-based tools) combined with
183
+ * value-based signals proven by i18next-cli.
157
184
  *
158
- * Conservative — when in doubt, INCLUDE the string.
159
- * Returns candidates (passed), count of filtered, and reason breakdown.
185
+ * Base score: 0.5. Boosted/penalized by context and value characteristics.
160
186
  */
161
- function applyPreFilter(strings) {
187
+ function calculateContentScore(str) {
188
+ let score = .5;
189
+ if (str.context === "template_text" || str.context === "jsx_text") score += .3;
190
+ if (str.context === "template_attribute" || str.context === "jsx_attribute") if (str.parentProperty && TRANSLATABLE_ATTRIBUTES.has(str.parentProperty)) score += .2;
191
+ else score -= .2;
192
+ if (str.context === "object_property") {
193
+ if (str.parentProperty && TRANSLATABLE_PROPERTIES.has(str.parentProperty)) score += .25;
194
+ }
195
+ const wordCount = str.value.split(/\s+/).length;
196
+ if (wordCount >= 3) score += .2;
197
+ else if (wordCount === 2) score += .1;
198
+ if (/[.!?:;]$/.test(str.value)) score += .1;
199
+ if (/[\u0080-\uFFFF]/.test(str.value)) score += .15;
200
+ if (/^[A-Z]/.test(str.value) && /[a-z]/.test(str.value)) score += .1;
201
+ if (CAMEL_CASE_RE.test(str.value)) score -= .3;
202
+ if (str.value.includes("/") && !str.value.includes(" ")) score -= .2;
203
+ return Math.max(0, Math.min(1, score));
204
+ }
205
+ /**
206
+ * Two-phase pre-filter:
207
+ * 1. shouldSkip(): Binary removal of definite non-content
208
+ * 2. calculateContentScore(): 0-1 confidence scoring for ambiguous strings
209
+ *
210
+ * Returns candidates that passed both phases, with content scores attached.
211
+ */
212
+ function applyPreFilter(strings, minScore = .4) {
162
213
  const candidates = [];
163
- const filterReasons = {};
164
- let filtered = 0;
214
+ const skipReasons = {};
215
+ let skipped = 0;
216
+ let lowConfidence = 0;
165
217
  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);
218
+ const skipReason = shouldSkip(str);
219
+ if (skipReason) {
220
+ skipped++;
221
+ skipReasons[skipReason] = (skipReasons[skipReason] ?? 0) + 1;
222
+ continue;
223
+ }
224
+ const contentScore = calculateContentScore(str);
225
+ if (contentScore < minScore) {
226
+ lowConfidence++;
227
+ skipReasons["low_confidence"] = (skipReasons["low_confidence"] ?? 0) + 1;
228
+ continue;
229
+ }
230
+ str.contentScore = contentScore;
231
+ candidates.push(str);
171
232
  }
172
233
  return {
173
234
  candidates,
174
- filtered,
175
- filterReasons
235
+ skipped,
236
+ lowConfidence,
237
+ skipReasons
176
238
  };
177
239
  }
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
240
  //#endregion
189
241
  //#region src/core/ast-scanner/index.ts
190
242
  const TSX_EXTENSIONS = new Set([
@@ -203,7 +255,7 @@ const ASTRO_EXTENSIONS = new Set([".astro"]);
203
255
  */
204
256
  async function loadVueParser() {
205
257
  try {
206
- return (await import("./vue-parser-CjS4YAoQ.mjs")).parseVue;
258
+ return (await import("./vue-parser-DTPTN3Yb.mjs")).parseVue;
207
259
  } catch {
208
260
  return;
209
261
  }
@@ -214,7 +266,7 @@ async function loadVueParser() {
214
266
  */
215
267
  async function loadSvelteParser() {
216
268
  try {
217
- return (await import("./svelte-parser-COTNG92w.mjs")).parseSvelte;
269
+ return (await import("./svelte-parser-1bBcMx3T.mjs")).parseSvelte;
218
270
  } catch {
219
271
  return;
220
272
  }
@@ -225,7 +277,7 @@ async function loadSvelteParser() {
225
277
  */
226
278
  async function loadAstroParser() {
227
279
  try {
228
- return (await import("./astro-parser-RofKmKQi.mjs")).parseAstro;
280
+ return (await import("./astro-parser-BJ-54JGe.mjs")).parseAstro;
229
281
  } catch {
230
282
  return;
231
283
  }
@@ -343,6 +395,7 @@ const DEFAULT_LIMIT = 50;
343
395
  const DEFAULT_OFFSET = 0;
344
396
  const DEFAULT_MIN_LENGTH = 2;
345
397
  const DEFAULT_MAX_LENGTH = 500;
398
+ const DEFAULT_MIN_SCORE = .4;
346
399
  const SUMMARY_SAMPLE_SIZE = 10;
347
400
  const CONTEXT_MAP = {
348
401
  "template_text": "template_text",
@@ -364,142 +417,17 @@ const CONTEXT_MAP = {
364
417
  "console_call": "other",
365
418
  "test_assertion": "other"
366
419
  };
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
420
  async function scanCandidates(projectRoot, options) {
491
421
  const limit = options?.limit ?? DEFAULT_LIMIT;
492
422
  const offset = options?.offset ?? DEFAULT_OFFSET;
493
423
  const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH;
494
424
  const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH;
425
+ const minScore = options?.min_score ?? DEFAULT_MIN_SCORE;
495
426
  const files = await discoverFiles(projectRoot, {
496
427
  paths: options?.paths ?? await autoDetectSourceDirs(projectRoot),
497
428
  include: options?.include,
498
429
  exclude: options?.exclude
499
430
  });
500
- const allCandidates = [];
501
- const dupeMap = /* @__PURE__ */ new Map();
502
- let rawStringsFound = 0;
503
431
  const filePromises = files.map(async (relPath) => {
504
432
  const filePath = join(projectRoot, relPath);
505
433
  const content = await readText(filePath);
@@ -513,51 +441,89 @@ async function scanCandidates(projectRoot, options) {
513
441
  };
514
442
  });
515
443
  const fileResults = await Promise.all(filePromises);
444
+ let rawStringsFound = 0;
445
+ let skippedCount = 0;
446
+ let lowConfidenceCount = 0;
447
+ const skipReasons = {};
448
+ const uniqueMap = /* @__PURE__ */ new Map();
449
+ const dupeMap = /* @__PURE__ */ new Map();
516
450
  for (const { relPath, extractions } of fileResults) {
517
451
  rawStringsFound += extractions.length;
518
452
  for (const extraction of extractions) {
519
- if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue;
453
+ if (extraction.value.length < minLength || extraction.value.length > maxLength) {
454
+ skippedCount++;
455
+ skipReasons["length_filter"] = (skipReasons["length_filter"] ?? 0) + 1;
456
+ continue;
457
+ }
458
+ const contentScore = extraction.contentScore ?? calculateContentScore(extraction);
459
+ if (contentScore < minScore) {
460
+ lowConfidenceCount++;
461
+ skipReasons["low_confidence"] = (skipReasons["low_confidence"] ?? 0) + 1;
462
+ continue;
463
+ }
520
464
  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({
465
+ const loc = {
533
466
  file: relPath,
534
467
  line: extraction.line
468
+ };
469
+ if (!dupeMap.has(extraction.value)) dupeMap.set(extraction.value, []);
470
+ dupeMap.get(extraction.value).push(loc);
471
+ if (!uniqueMap.has(extraction.value)) uniqueMap.set(extraction.value, {
472
+ candidate: {
473
+ file: relPath,
474
+ line: extraction.line,
475
+ column: extraction.column,
476
+ value: extraction.value,
477
+ context: mappedContext,
478
+ surrounding: extraction.surrounding,
479
+ contentScore,
480
+ occurrences: [loc]
481
+ },
482
+ maxScore: contentScore
535
483
  });
484
+ else {
485
+ const entry = uniqueMap.get(extraction.value);
486
+ entry.candidate.occurrences.push(loc);
487
+ if (contentScore > entry.maxScore) {
488
+ entry.maxScore = contentScore;
489
+ entry.candidate.contentScore = contentScore;
490
+ entry.candidate.file = relPath;
491
+ entry.candidate.line = extraction.line;
492
+ entry.candidate.column = extraction.column;
493
+ entry.candidate.context = mappedContext;
494
+ entry.candidate.surrounding = extraction.surrounding;
495
+ }
496
+ }
536
497
  }
537
498
  }
499
+ const allUniqueCandidates = [...uniqueMap.values()].map((e) => e.candidate).toSorted((a, b) => b.contentScore - a.contentScore);
538
500
  const duplicates = [...dupeMap.entries()].filter(([, occurrences]) => occurrences.length >= 2).map(([value, occurrences]) => ({
539
501
  value,
540
502
  count: occurrences.length,
541
503
  occurrences
542
504
  })).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;
505
+ const uniqueCount = allUniqueCandidates.length;
506
+ const paginated = allUniqueCandidates.slice(offset, offset + limit);
507
+ const hasMore = uniqueCount > offset + limit;
546
508
  return {
547
509
  candidates: paginated,
548
510
  duplicates,
549
511
  stats: {
550
512
  files_scanned: files.length,
551
513
  raw_strings_found: rawStringsFound,
552
- after_filtering: afterFiltering,
514
+ skipped: skippedCount,
515
+ low_confidence: lowConfidenceCount,
516
+ unique_candidates: uniqueCount,
553
517
  candidates_returned: paginated.length,
554
- has_more: hasMore
518
+ has_more: hasMore,
519
+ skip_reasons: skipReasons
555
520
  }
556
521
  };
557
522
  }
558
523
  async function scanSummary(projectRoot, options) {
559
524
  const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH;
560
525
  const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH;
526
+ const minScore = options?.min_score ?? DEFAULT_MIN_SCORE;
561
527
  const files = await discoverFiles(projectRoot, {
562
528
  paths: options?.paths ?? await autoDetectSourceDirs(projectRoot),
563
529
  include: options?.include,
@@ -587,7 +553,8 @@ async function scanSummary(projectRoot, options) {
587
553
  });
588
554
  const sampleResults = await Promise.all(samplePromises);
589
555
  for (const extractions of sampleResults) for (const extraction of extractions) {
590
- if (applySecondaryFilter(extraction.value, extraction.surrounding, minLength, maxLength)) continue;
556
+ if (extraction.value.length < minLength || extraction.value.length > maxLength) continue;
557
+ if ((extraction.contentScore ?? calculateContentScore(extraction)) < minScore) continue;
591
558
  sampleCandidates++;
592
559
  const prev = freqMap.get(extraction.value) ?? 0;
593
560
  freqMap.set(extraction.value, prev + 1);
@@ -614,6 +581,6 @@ async function scanSummary(projectRoot, options) {
614
581
  };
615
582
  }
616
583
  //#endregion
617
- export { scanCandidates as n, scanSummary as r, isNonContent as t };
584
+ export { scanSummary as n, scanCandidates as t };
618
585
 
619
- //# sourceMappingURL=scanner-DPrbU-oK.mjs.map
586
+ //# sourceMappingURL=scanner-CQOxARUU.mjs.map