ucn 3.8.17 → 3.8.19

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/core/execute.js CHANGED
@@ -96,6 +96,7 @@ function buildCallerOptions(p) {
96
96
  className: p.className,
97
97
  includeMethods: p.includeMethods,
98
98
  includeUncertain: p.includeUncertain || false,
99
+ includeTests: p.includeTests,
99
100
  exclude: toExcludeArray(p.exclude),
100
101
  minConfidence: num(p.minConfidence, 0),
101
102
  };
@@ -592,11 +593,27 @@ const HANDLERS = {
592
593
  const err = requireName(p.name);
593
594
  if (err) return { ok: false, error: err };
594
595
  applyClassMethodSyntax(p);
596
+ if (p.file) {
597
+ const fileErr = checkFilePatternMatch(index, p.file);
598
+ if (fileErr) return { ok: false, error: fileErr };
599
+ // Validate that the symbol exists in the target file
600
+ const defs = index.find(p.name, { exact: true, file: p.file, className: p.className });
601
+ if (defs.length === 0) {
602
+ const allDefs = index.find(p.name, { exact: true });
603
+ if (allDefs.length > 0) {
604
+ const files = allDefs.map(d => d.relativePath).join(', ');
605
+ return { ok: false, error: `Symbol "${p.name}" not found in files matching "${p.file}". Defined in: ${files}` };
606
+ }
607
+ return { ok: false, error: `Symbol "${p.name}" not found.` };
608
+ }
609
+ }
595
610
  const classErr = validateClassName(index, p.name, p.className);
596
611
  if (classErr) return { ok: false, error: classErr };
597
612
  const result = index.tests(p.name, {
598
613
  callsOnly: p.callsOnly || false,
599
614
  className: p.className,
615
+ file: p.file,
616
+ exclude: toExcludeArray(p.exclude),
600
617
  });
601
618
  return { ok: true, result };
602
619
  },
@@ -80,7 +80,7 @@ function formatContextJson(context) {
80
80
  function formatContext(ctx, options = {}) {
81
81
  if (!ctx) return { text: 'Symbol not found.', expandable: [] };
82
82
 
83
- const expandHint = options.expandHint || 'Use ucn_expand with item number to see code for any item.';
83
+ const expandHint = options.expandHint != null ? options.expandHint : 'Use ucn_expand with item number to see code for any item.';
84
84
  const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded. Use include_methods=true to include them.';
85
85
 
86
86
  const lines = [];
package/core/project.js CHANGED
@@ -158,28 +158,35 @@ class ProjectIndex {
158
158
  const startTime = Date.now();
159
159
  const quiet = options.quiet !== false;
160
160
 
161
- if (!pattern) {
162
- pattern = detectProjectPattern(this.root);
163
- }
161
+ // Accept pre-expanded file array (glob mode) or a pattern string
162
+ let files;
163
+ if (Array.isArray(pattern)) {
164
+ files = pattern;
165
+ } else {
166
+ if (!pattern) {
167
+ pattern = detectProjectPattern(this.root);
168
+ }
164
169
 
165
- const globOpts = {
166
- root: this.root,
167
- maxFiles: options.maxFiles || this.config.maxFiles || 50000,
168
- followSymlinks: options.followSymlinks
169
- };
170
+ const globOpts = {
171
+ root: this.root,
172
+ maxFiles: options.maxFiles || this.config.maxFiles || 50000,
173
+ followSymlinks: options.followSymlinks
174
+ };
170
175
 
171
- // Merge .gitignore and .ucn.json exclude into file discovery
172
- const gitignorePatterns = parseGitignore(this.root);
173
- const configExclude = this.config.exclude || [];
174
- if (gitignorePatterns.length > 0 || configExclude.length > 0) {
175
- globOpts.ignores = [...DEFAULT_IGNORES, ...gitignorePatterns, ...configExclude];
176
- }
176
+ // Merge .gitignore and .ucn.json exclude into file discovery
177
+ const gitignorePatterns = parseGitignore(this.root);
178
+ const configExclude = this.config.exclude || [];
179
+ if (gitignorePatterns.length > 0 || configExclude.length > 0) {
180
+ globOpts.ignores = [...DEFAULT_IGNORES, ...gitignorePatterns, ...configExclude];
181
+ }
177
182
 
178
- const files = expandGlob(pattern, globOpts);
183
+ files = expandGlob(pattern, globOpts);
184
+ }
179
185
 
180
186
  // Track if files were truncated by maxFiles limit
181
- if (files.length >= globOpts.maxFiles) {
182
- this.truncated = { indexed: files.length, maxFiles: globOpts.maxFiles };
187
+ const maxFiles = options.maxFiles || this.config.maxFiles || 50000;
188
+ if (!Array.isArray(pattern) && files.length >= maxFiles) {
189
+ this.truncated = { indexed: files.length, maxFiles };
183
190
  } else {
184
191
  this.truncated = null;
185
192
  }
package/core/registry.js CHANGED
@@ -92,11 +92,11 @@ const PARAM_MAP = {
92
92
  // ============================================================================
93
93
 
94
94
  // Per-command list of accepted flag names (camelCase). Source of truth for help text,
95
- // MCP schema validation, and architecture guards.
95
+ // MCP param stripping, CLI inapplicable-flag warnings, and architecture guards.
96
96
  // file* = file is the command subject (required), not a filter pattern.
97
97
  const FLAG_APPLICABILITY = {
98
98
  // Understanding code
99
- about: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence'],
99
+ about: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence'],
100
100
  context: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'minConfidence', 'showConfidence'],
101
101
  impact: ['file', 'exclude', 'className', 'top'],
102
102
  blast: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
@@ -110,7 +110,7 @@ const FLAG_APPLICABILITY = {
110
110
  usages: ['file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in'],
111
111
  toc: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in'],
112
112
  search: ['file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused'],
113
- tests: ['className', 'callsOnly'],
113
+ tests: ['file', 'exclude', 'className', 'callsOnly'],
114
114
  affectedTests:['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'minConfidence'],
115
115
  deadcode: ['file', 'exclude', 'includeTests', 'includeExported', 'includeDecorated', 'limit', 'in'],
116
116
  entrypoints: ['file', 'exclude', 'includeTests', 'limit', 'type', 'framework'],
@@ -123,7 +123,7 @@ const FLAG_APPLICABILITY = {
123
123
  imports: ['file'],
124
124
  exporters: ['file'],
125
125
  fileExports: ['file'],
126
- graph: ['file', 'depth', 'direction'],
126
+ graph: ['file', 'depth', 'direction', 'all'],
127
127
  circularDeps: ['file', 'exclude'],
128
128
  // Refactoring
129
129
  verify: ['file', 'className'],
@@ -224,11 +224,43 @@ function toCliName(canonical) {
224
224
  return canonical.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
225
225
  }
226
226
 
227
+ /**
228
+ * Build a reverse map: camelCase → snake_case from PARAM_MAP.
229
+ * Flags not in PARAM_MAP are already snake_case-safe (single words).
230
+ */
231
+ function buildReverseParamMap() {
232
+ const rev = {};
233
+ for (const [snake, camel] of Object.entries(PARAM_MAP)) {
234
+ rev[camel] = snake;
235
+ }
236
+ return rev;
237
+ }
238
+
239
+ const REVERSE_PARAM_MAP = buildReverseParamMap();
240
+
241
+ /**
242
+ * Generate per-command parameter listing for the MCP tool description.
243
+ * Maps camelCase flags back to snake_case for MCP clients.
244
+ * One line per command: `about: file, exclude, class_name, ...`
245
+ */
246
+ function generateMcpParamSection() {
247
+ const lines = ['', 'ACCEPTED FLAGS PER COMMAND (name, term, stack, range, base, staged, max_chars always accepted; flags not listed below are ignored):'];
248
+ for (const cmd of CANONICAL_COMMANDS) {
249
+ const flags = FLAG_APPLICABILITY[cmd];
250
+ if (!flags || flags.length === 0) continue;
251
+ const mcpCmd = toMcpName(cmd);
252
+ const mcpFlags = flags.map(f => REVERSE_PARAM_MAP[f] || f);
253
+ lines.push(` ${mcpCmd}: ${mcpFlags.join(', ')}`);
254
+ }
255
+ return lines.join('\n');
256
+ }
257
+
227
258
  module.exports = {
228
259
  CANONICAL_COMMANDS,
229
260
  CLI_ALIASES,
230
261
  MCP_ALIASES,
231
262
  PARAM_MAP,
263
+ REVERSE_PARAM_MAP,
232
264
  FLAG_APPLICABILITY,
233
265
  BROAD_COMMANDS,
234
266
  resolveCommand,
@@ -237,4 +269,5 @@ module.exports = {
237
269
  getMcpCommandEnum,
238
270
  toMcpName,
239
271
  toCliName,
272
+ generateMcpParamSection,
240
273
  };