ucn 5.3.6 → 5.3.8

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.
@@ -41,6 +41,12 @@ three. The text block is the whole response on every surface.
41
41
 
42
42
  Persistent indexes live in a per-user, project-keyed cache rather than the analyzed repository. Set `UCN_CACHE_DIR` to override the cache root; CLI `--no-cache` bypasses persistence and `--clear-cache` removes the current project's cache. Legacy `<project>/.ucn-cache` directories are migrated on first use. Concurrent invocations on a cold cache share one build: the first process takes a build lock, the others wait for its cache (`Waiting for another ucn process building the index...` on stderr) instead of rebuilding; a lock left by a dead process is broken automatically.
43
43
 
44
+ Files named `*.min.js`, `*.bundle.js`, and `*.map` are disclosed as skipped
45
+ bundled sources; they make observed-text completeness partial. Pass
46
+ `--include-bundled` (MCP `include_bundled=true`) to index the JavaScript files.
47
+ This option bypasses the shared cache and still respects user exclusions.
48
+ Source maps remain disclosed but unindexed; inspect them with text tools.
49
+
44
50
  Supported source families are JavaScript/TypeScript/TSX, Python, Go, Rust, Java, C, C++, C#, and HTML inline JavaScript/event handlers. C/C++ uses `compile_commands.json` when available to classify headers and resolve include paths. Recoverable preprocessor branches contribute AST-proven source facts, so a single selected configuration does not silently erase definitions or calls; disagreeing conditional macro identities stay visible as unverified. C++ resolution uses namespace ownership, static overload shape (including arrays), and macro-parameter requalification. C# resolution uses declared property/field receiver types plus overload and hiding discipline. This is portable AST analysis, not a compiler build; macros, templates, generated code, reflection, and external dependency semantics can remain unverified.
45
51
 
46
52
  `repo` refines its HOT list exactly for up to 400 candidate definitions per
@@ -78,6 +84,7 @@ counts them; `CALL SITES` stays call-shaped.
78
84
  Target-less `impact` and `check` diff the working tree against `HEAD` AND
79
85
  include untracked, non-ignored source files as whole-file additions, so new
80
86
  modules are checked before `git add`. `--staged` keeps its index-only meaning.
87
+ The changed-path note also counts untracked documentation and configuration.
81
88
 
82
89
  An observed-text zero is not semantic zero or safe-delete proof. Numeric evidence values are ordinal ranking weights, not probabilities.
83
90
 
@@ -119,7 +126,7 @@ answers:
119
126
  ucn find handleRequest --lines # path:line:signature # kind
120
127
  ucn show handleRequest --lines # callers as path:line:text; unverified ones end in "\t# unverified: <reason>"
121
128
  ucn show handleRequest --lines --sections=callees
122
- ucn usages handleRequest --lines # every literal-name line; non-call kinds tagged "# import" / "# definition"
129
+ ucn usages handleRequest --lines # every literal-name occurrence; non-call kinds tagged "# import" / "# definition"
123
130
  ucn search 'retry(' --lines # grep -n output, code-aware scope
124
131
  ucn impact handleRequest --lines
125
132
  ucn source handleRequest --raw # the code and nothing else, ready for an exact-string edit
@@ -129,6 +136,12 @@ ucn source src/server.js:40-80 --raw
129
136
  Records go to stdout; the `ACCOUNT` / `CONTRACT` lines, notes, and the
130
137
  same-name disambiguation go to stderr prefixed `# ` (MCP keeps them in the one
131
138
  text block, and `--raw` appends its note as one trailing `# ` line there).
139
+ `usages` emits one record per occurrence, so a source line may repeat; deduplicate
140
+ `path:line` values for line counts. Definition handles start at decorators when
141
+ present, while usages point at token lines (`nameLine` identifies the declaration
142
+ token when it differs from `startLine`). Structural `search --unused` keeps its
143
+ safety note and decorator tags in shell output; runtime registrations can appear
144
+ and zero call edges do not prove a symbol is safe to delete.
132
145
  `--lines` lists the whole band without default row/character caps, so pipe through
133
146
  `grep -v '# unverified'` for the confirmed tier or `cut -d: -f1 | sort | uniq -c`
134
147
  for callers per file. Nothing to list prints nothing and exits 1, grep's
@@ -143,6 +156,11 @@ characters (backslash/tab/CR/LF) are escaped; use JSON for exact filenames.
143
156
  languages; use `ucn ... --lines` when the question is a symbol, a caller, or a
144
157
  definition, and `--raw` when the next step is an edit.
145
158
 
159
+ Command errors exit 2 in ordinary text mode as well. CLI JSON preserves its
160
+ own contract: successful empty results exit 0; command errors exit 1 with
161
+ `meta.ok: false` and an `error` field. A target-less `check` exits 1 when
162
+ `TRUST` is `BLOCKED`, 0 otherwise; a check that could not run exits 2.
163
+
146
164
  ## Breaking-change protocol
147
165
 
148
166
  1. Pin the exact definition with `find`.
@@ -183,6 +201,12 @@ rejected, and unsupported advanced syntax should be handed to ripgrep.
183
201
  `find` activity counts are definition-pinned: confirmed plus visible unverified
184
202
  call candidates. Calls proved to belong to another same-name target are
185
203
  disclosed separately and excluded from the activity total.
204
+ Broad queries rank candidates by inexpensive approximate usage totals before
205
+ applying the row limit; only returned definitions receive caller adjudication.
206
+ The selection note discloses that approximation. `find`, text `search`,
207
+ `deadcode`, `api`, and `repo --sections=files` default to at most 500 results
208
+ (structural `search`: 50). Use an explicit `--limit=N` to request more;
209
+ `usages` and `--lines` have no default row cap.
186
210
 
187
211
  For `plan --rename-to`, the selected declaration is only the starting point.
188
212
  When the index proves the relationship, the rename unit closes over
@@ -33,7 +33,7 @@ structural or code-only search.
33
33
  |---|---|
34
34
  | `repo` | Repository orientation. Select `summary,files,stats,health` with `--sections`; `--deep` includes readiness evidence. Skipped unsupported source is listed with a grep/language-tool handoff. |
35
35
  | `deps <file>` | File dependency graph. Use `--direction=imports\|importers\|both`, `--detailed`, or `--cycles`. Cycles distinguish eager edges from function-local, Python typing-guarded, and TypeScript type-only edges. Complete cycle groups remain visible when enumeration is capped. |
36
- | `api [file]` | Static exported/public surface for a project or file. |
36
+ | `api [file]` | Static exported/public surface for a project or file. An exact file includes tests; broader scans exclude tests with a count. Use `--include-tests` to include them. |
37
37
  | `entrypoints` | Framework, route, task, test, and runtime entry points. |
38
38
  | `endpoints` | Server/client HTTP surface; `--bridge` adds advisory matching. |
39
39
 
@@ -49,6 +49,14 @@ structural or code-only search.
49
49
 
50
50
  Symbol-listing commands emit handles such as `src/api.ts:42:handler`. Pass the full handle to symbol commands. `path:line` also works. Handles prevent same-named definitions from being silently combined.
51
51
 
52
+ Definition handles and source spans start at the first decorator or annotation when present; literal usages point at the actual token line. Use a symbol's `nameLine` (when present, otherwise `startLine`) to compare declaration tokens with usages.
53
+
54
+ Structural `search --param` matches parameter names, types, and defaults; `--returns` matches return annotations. Both exclude AST comments and preserve string contents. `--unused` lists callable symbols without call edges, not safe-delete candidates; decorated runtime registrations may still appear. Its safety note and decorator tags are retained in `--lines` output. Use `deadcode` and `usages` before deletion.
55
+
56
+ `repo` summary/stats `buildTime` is the duration of the last index build (discovery, parsing, and graphs), retained in the cache. It excludes cache loading/saving and query execution, so it is not command wall time; `buildTimeNote` states this boundary.
57
+
58
+ `--lines` writes one record per output line. `usages` records occurrences, so multiple tokens on the same source line can produce repeated `path:line` values. Deduplicate those values when counting source lines.
59
+
52
60
  ## Common flags
53
61
 
54
62
  | Flag | Meaning |
@@ -58,6 +66,8 @@ Symbol-listing commands emit handles such as `src/api.ts:42:handler`. Pass the f
58
66
  | `--class-name=<name>` | Scope a member when no handle is available. |
59
67
  | `--in=<directory>` | Limit query scope to a directory. |
60
68
  | `--exclude=<patterns>` | Exclude matching paths. |
69
+ | `--limit=N` | Default maximum of 500 results for `find`, text `search`, `deadcode`, `api`, and `repo` files; structural `search` defaults to 50. Explicit limits override these caps; `usages` and `--lines` are uncapped by default. |
70
+ | `--include-bundled` | Include `*.min.js` and `*.bundle.js` in discovery, respecting user exclusions and bypassing the shared cache. By default these and `*.map` are disclosed as skipped sources and completeness is partial. Source maps remain unindexed. MCP: `include_bundled=true`. |
61
71
  | `--depth=N` | Set trace/dependency/test traversal depth. |
62
72
  | `--direction=<value>` | Select trace or dependency direction. |
63
73
  | `--all` | Lift result and formatter caps where supported. It is recommended only for commands that accept it. |
@@ -84,6 +94,13 @@ ucn [target] <command> [argument] [flags]
84
94
 
85
95
  Omit the target for the current project. A target may be a file, directory, or quoted glob such as `"src/**/*.py"`.
86
96
 
97
+ Ordinary text and shell command errors exit 2. JSON command errors retain
98
+ exit 1 with `meta.ok: false` and an `error` field; successful empty JSON
99
+ results exit 0. Target-less `check` exits 1 for `TRUST: BLOCKED`, 0 for other
100
+ completed checks, and 2 when the check could not run. Working-tree `impact`
101
+ and `check` count untracked documentation/configuration in their non-source
102
+ path note; `--staged` excludes all untracked paths.
103
+
87
104
  ## Language notes
88
105
 
89
106
  Supported source families are JavaScript/TypeScript/TSX, Python, Go, Rust, Java, C, C++, C#, and HTML inline JavaScript/event handlers. C/C++ consumes `compile_commands.json` when present for header-language and include-path context, and retains AST-proven facts across recoverable preprocessor branches. C++ call identity uses namespace ownership, static overload shape (including arrays), and macro requalification; disagreeing conditional macro definitions remain visible as unverified. C# uses declared property/field receiver types and overload/hiding discipline. UCN remains portable AST analysis: it does not run a compiler, preprocessor, Roslyn, or an LSP during normal queries, and it does not assert which conditional branch a build activates.
package/README.md CHANGED
@@ -213,6 +213,23 @@ and a shell-mode character budget fails before writing partial output.
213
213
  `source --raw` extracts complete functions and classes unless an explicit
214
214
  line limit is requested; any resulting truncation is reported on stderr.
215
215
 
216
+ Ordinary text-mode command errors also exit 2. JSON keeps exit 0 for successful
217
+ empty results and exit 1 for command errors (`meta.ok: false` plus `error`).
218
+ Target-less `check` exits 1 when `TRUST` is `BLOCKED`, 0 for other completed
219
+ checks, and 2 if it could not run.
220
+
221
+ Outside `--lines`, `find`, text `search`, `deadcode`, `api`, and
222
+ `repo --sections=files` default to a maximum of 500 results. Use `--limit=N`
223
+ to request more; `usages` lists every site unless a limit is given. Broad `find` queries select candidates by approximate usage
224
+ totals before calculating definition-pinned caller activity, and disclose
225
+ that selection when limited.
226
+
227
+ Files named `*.min.js`, `*.bundle.js`, and `*.map` are reported as skipped
228
+ sources and make completeness partial. `--include-bundled` (MCP
229
+ `include_bundled=true`) indexes the JavaScript bundles while respecting user
230
+ exclusions; it bypasses the shared cache. Source maps remain disclosed but
231
+ unindexed.
232
+
216
233
  ## AI setup
217
234
 
218
235
  One tool, 18 commands, compact source-linked answers that keep their trust
package/cli/index.js CHANGED
@@ -48,7 +48,7 @@ let activeCanonicalCommand = null;
48
48
  class CommandError extends Error { constructor() { super(); } }
49
49
 
50
50
  // Thrown by validateNumericFlags when a numeric flag has a bad value.
51
- // The CLI top-level catches this, prints the message, and exits 1. Interactive
51
+ // The CLI top-level catches this, prints the message, and exits 2 (JSON: 1). Interactive
52
52
  // mode catches it inside its REPL try/catch and continues the session.
53
53
  class FlagValidationError extends Error {
54
54
  constructor(msg) { super(msg); this.name = 'FlagValidationError'; }
@@ -363,6 +363,7 @@ function parseFlags(tokens) {
363
363
  hideUncertain: tokens.includes('--hide-uncertain') || tokens.includes('--no-uncertain') || undefined,
364
364
  stack: getValueFlag('--stack'),
365
365
  workersRaw: getValueFlag('--workers'),
366
+ includeBundled: tokens.includes('--include-bundled') || undefined,
366
367
  workers: (() => {
367
368
  const v = getValueFlag('--workers');
368
369
  if (v === null) return undefined;
@@ -376,7 +377,7 @@ function parseFlags(tokens) {
376
377
  const flags = parseFlags(args);
377
378
  flags.json = args.includes('--json');
378
379
  flags.quiet = !args.includes('--verbose') && !args.includes('--no-quiet');
379
- flags.cache = !args.includes('--no-cache');
380
+ flags.cache = !args.includes('--no-cache') && !flags.includeBundled;
380
381
  flags.clearCache = args.includes('--clear-cache');
381
382
  flags.interactive = args.includes('--interactive') || args.includes('-i');
382
383
  flags.followSymlinks = !args.includes('--no-follow-symlinks');
@@ -408,7 +409,7 @@ if (unknownFlags.length > 0) {
408
409
  emitCliError(
409
410
  `Unknown flag(s): ${unknownFlags.join(', ')}. Use --help to see available flags.`,
410
411
  );
411
- process.exit(flags.lines || flags.raw ? 2 : 1);
412
+ process.exit(flags.json ? 1 : 2);
412
413
  }
413
414
 
414
415
  // Validate numeric flag values up front so bad input fails before we build
@@ -419,7 +420,7 @@ try {
419
420
  } catch (e) {
420
421
  if (e instanceof FlagValidationError) {
421
422
  emitCliError(e.message);
422
- process.exit(flags.lines || flags.raw ? 2 : 1);
423
+ process.exit(flags.json ? 1 : 2);
423
424
  }
424
425
  throw e;
425
426
  }
@@ -607,7 +608,7 @@ function main() {
607
608
  if (!(e instanceof CommandError)) {
608
609
  emitCliError(`Error: ${e.message}`);
609
610
  }
610
- process.exitCode = flags.lines || flags.raw ? 2 : 1;
611
+ process.exitCode = flags.json ? 1 : 2;
611
612
  }
612
613
  }
613
614
 
@@ -625,7 +626,7 @@ function printTieredNoOpNotes(canonical, flags, print) {
625
626
  }
626
627
 
627
628
  const GLOBAL_FLAG_KEYS = new Set([
628
- 'json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles',
629
+ 'json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'includeBundled', 'maxFiles',
629
630
  'verbose', 'interactive', '_fileFromFileMode', 'topRaw',
630
631
  'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'depthRaw', 'contextRaw',
631
632
  'workers', 'workersRaw', 'lineRaw', 'maxChars', 'maxCharsRaw', 'minConfidenceRaw',
@@ -752,7 +753,7 @@ function runProjectCommand(rootDir, command, arg) {
752
753
  if (flags.cache && !flags.clearCache) {
753
754
  const loaded = index.loadCache();
754
755
  cacheWasLoaded = !!loaded;
755
- if (loaded && !flags.maxFiles) {
756
+ if (loaded && !index.includeBundled && !flags.maxFiles) {
756
757
  if (!index.isCacheStale()) {
757
758
  usedCache = true;
758
759
  if (!flags.quiet) {
@@ -766,7 +767,7 @@ function runProjectCommand(rootDir, command, arg) {
766
767
  // If cache was loaded but stale, force rebuild to avoid duplicates
767
768
  let needsCacheSave = false;
768
769
  if (!usedCache) {
769
- const buildOpts = { quiet: flags.quiet, forceRebuild: cacheWasLoaded, followSymlinks: flags.followSymlinks, maxFiles: flags.maxFiles, workers: flags.workers };
770
+ const buildOpts = { quiet: flags.quiet, forceRebuild: cacheWasLoaded, followSymlinks: flags.followSymlinks, includeBundled: flags.includeBundled, maxFiles: flags.maxFiles, workers: flags.workers };
770
771
  if (flags.cache && !flags.maxFiles) {
771
772
  // Cross-process build lock (fix #354): concurrent cold-cache
772
773
  // invocations share one build instead of each rebuilding.
@@ -813,7 +814,7 @@ function runProjectCommand(rootDir, command, arg) {
813
814
  if (!(e instanceof CommandError)) {
814
815
  emitCliError(`Error: ${e.message}`);
815
816
  }
816
- process.exitCode = flags.lines || flags.raw ? 2 : 1;
817
+ process.exitCode = flags.json ? 1 : 2;
817
818
  } finally {
818
819
  // Save cache after command execution so callsCache populated
819
820
  // by findCallers/findCallees gets persisted to disk.
@@ -834,7 +835,11 @@ function runProjectCommand(rootDir, command, arg) {
834
835
  // ============================================================================
835
836
 
836
837
  function runGlobCommand(pattern, command, arg) {
837
- const files = expandGlob(pattern);
838
+ const discoveryIssues = [];
839
+ const files = expandGlob(pattern, {
840
+ includeBundled: flags.includeBundled,
841
+ onDiscoveryIssue: issue => discoveryIssues.push(issue),
842
+ });
838
843
 
839
844
  if (files.length === 0) {
840
845
  fail(`No files match pattern: ${pattern}`, command);
@@ -849,6 +854,9 @@ function runGlobCommand(pattern, command, arg) {
849
854
  const rootDir = findProjectRoot(path.dirname(files[0]));
850
855
  const index = new ProjectIndex(rootDir);
851
856
  index.build(files, { quiet: true });
857
+ index.discoveryIssues.push(...discoveryIssues.map(issue => ({
858
+ ...issue, relativePath: path.relative(index.root, issue.path), path: undefined,
859
+ })));
852
860
 
853
861
  if (!isPublicCommand(canonical)) {
854
862
  fail(unknownCommandMessage(command));
@@ -909,7 +917,8 @@ Commands:
909
917
  deps <file> File graph; --direction=imports|importers|both
910
918
  --detailed Include import declarations
911
919
  deps --cycles Report circular dependencies (no file target)
912
- api [file] Project or file public API
920
+ api [file] Project or file public API (exact files include tests;
921
+ broader scans exclude tests unless --include-tests)
913
922
  check [symbol] Signature check; without symbol, precommit check
914
923
  plan <symbol> Preview rename or parameter edits
915
924
  entrypoints Runtime and framework entry points
@@ -925,7 +934,8 @@ Common flags:
925
934
  --base=REF --staged --no-cache --clear-cache [--all] --max-files=N --workers=N
926
935
  --max-chars=N (text output; default 10K targeted / 3K broad, ceiling 100K)
927
936
  --lines find/usages/search/show/impact: grep -n shape, one path:line:text
928
- record per line (tags after a tab: # unverified: <reason>, # import,
937
+ record per output line; usages may repeat a source line per occurrence
938
+ (tags after a tab: # unverified: <reason>, # import,
929
939
  # callee); accounting and notes go to stderr as "# " lines; exit 1
930
940
  when nothing matched; exit 2 on errors. No default result cap.
931
941
  show defaults to callers; --sections=callers,callees selects bands.
@@ -938,14 +948,14 @@ ${perCommandFlags}
938
948
 
939
949
  Global/build/output flags:
940
950
  --help -h --version -v --mcp --json --verbose --no-quiet --quiet
941
- --interactive -i --no-cache --clear-cache --no-follow-symlinks
951
+ --interactive -i --no-cache --clear-cache --no-follow-symlinks --include-bundled
942
952
  --max-files=N --max-chars=N --workers=N
943
953
  --clear-cache --all clears every bounded per-user UCN project cache.
944
954
 
945
955
  Exit codes:
946
956
  0 Command completed successfully; check found no blocking issues.
947
- 1 Findings, unsafe changes, validation failures, or invalid user input.
948
- 2 Command could not run (operational/environment failure).
957
+ 1 Empty --lines listing, blocking check findings, or JSON command error.
958
+ 2 Text/shell command error, or a check that could not run.
949
959
 
950
960
  Boolean aliases:
951
961
  --no-include-methods --no-regex --show-confidence --hide-confidence
@@ -995,14 +1005,14 @@ function runInteractive(rootDir) {
995
1005
  let iCacheFresh;
996
1006
  if (flags.cache) {
997
1007
  const loaded = !flags.clearCache && index.loadCache();
998
- iCacheFresh = loaded && !index.isCacheStale();
1008
+ iCacheFresh = loaded && !index.includeBundled && !index.isCacheStale();
999
1009
  if (!iCacheFresh) {
1000
1010
  index.buildCached({ quiet: true, forceRebuild: !!loaded, workers: flags.workers }, {
1001
1011
  onWait: () => console.log('Waiting for another ucn process building the index...'),
1002
1012
  });
1003
1013
  }
1004
1014
  } else {
1005
- index.build(null, { quiet: true, workers: flags.workers });
1015
+ index.build(null, { quiet: true, workers: flags.workers, includeBundled: flags.includeBundled });
1006
1016
  }
1007
1017
  console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} unique symbol names`);
1008
1018
  console.log('Type commands (e.g., "find parseFile", "show main", "repo")');
@@ -1118,16 +1128,18 @@ Flags can be added per-command: show myFunc --sections=source,callers
1118
1128
  // refreshes individual call records lazily; without this matching
1119
1129
  // rebuild a newly added definition was invisible while its calls
1120
1130
  // appeared in neighbouring answers (UCN5-044).
1121
- if (index.isCacheStale()) {
1131
+ const includeBundled = !!(iflags.includeBundled || flags.includeBundled);
1132
+ if (!!index.includeBundled !== includeBundled || index.isCacheStale()) {
1122
1133
  console.log('Source changed; rebuilding index...');
1123
1134
  const rebuildOpts = {
1124
1135
  quiet: true,
1125
1136
  forceRebuild: true,
1126
1137
  followSymlinks: flags.followSymlinks,
1138
+ includeBundled,
1127
1139
  maxFiles: flags.maxFiles,
1128
1140
  workers: flags.workers,
1129
1141
  };
1130
- if (flags.cache && !flags.maxFiles) {
1142
+ if (flags.cache && !flags.maxFiles && !includeBundled) {
1131
1143
  index.buildCached(rebuildOpts, {
1132
1144
  onWait: () => console.log('Waiting for another ucn process building the index...'),
1133
1145
  });
package/core/analysis.js CHANGED
@@ -2052,7 +2052,14 @@ function diffImpact(index, options = {}) {
2052
2052
  if (known.has(localRel)) continue;
2053
2053
  const filePath = path.join(index.root, localRel);
2054
2054
  const fileEntry = index.files.get(filePath);
2055
- if (!fileEntry || !detectLanguage(filePath)) continue;
2055
+ if (!fileEntry || !detectLanguage(filePath)) {
2056
+ changes.push({
2057
+ filePath, relativePath: localRel,
2058
+ gitRelativePath: projectPrefix ? `${projectPrefix}/${localRel}` : localRel,
2059
+ addedLines: [], deletedLines: [], untracked: true,
2060
+ });
2061
+ continue;
2062
+ }
2056
2063
  let lineCount = fileEntry.lines;
2057
2064
  if (!Number.isFinite(lineCount)) {
2058
2065
  try { lineCount = fs.readFileSync(filePath, 'utf-8').split('\n').length; } catch (_) { continue; }
@@ -2096,7 +2103,10 @@ function diffImpact(index, options = {}) {
2096
2103
 
2097
2104
  for (const change of changes) {
2098
2105
  const lang = detectLanguage(change.filePath);
2099
- if (!lang) { nonSourcePaths++; continue; }
2106
+ if (!lang || (change.untracked && !index.files.has(change.filePath))) {
2107
+ nonSourcePaths++;
2108
+ continue;
2109
+ }
2100
2110
 
2101
2111
  const fileEntry = index.files.get(change.filePath);
2102
2112
 
package/core/cache.js CHANGED
@@ -711,7 +711,9 @@ function clearAllCaches() {
711
711
  // v224 (#355 recovery): Go declaration origins; Rust aliases, wrapper patterns,
712
712
  // copied bindings and qualified macro receivers; TS indexed array evidence.
713
713
  // v225 (fix #357): Rust `use path::name as local` bindings record the original name with a paired `renames` alias.
714
- const CACHE_FORMAT_VERSION = 225;
714
+ // v226: bundled/minified filename exclusions are disclosed in discoveryIssues.
715
+ // v227: signature parameter/return text excludes AST comments in every language.
716
+ const CACHE_FORMAT_VERSION = 227;
715
717
  const USAGE_CACHE_FILE = 'usage-results.json';
716
718
 
717
719
  /**
@@ -935,6 +937,7 @@ function saveCache(index, cachePath) {
935
937
  unsupportedFiles: Array.isArray(index.unsupportedFiles)
936
938
  ? index.unsupportedFiles
937
939
  : [],
940
+ includeBundled: index.includeBundled === true,
938
941
  discoveryIssues: Array.isArray(index.discoveryIssues)
939
942
  ? index.discoveryIssues
940
943
  : [],
@@ -1164,6 +1167,7 @@ function loadCache(index, cachePath) {
1164
1167
  index.unsupportedFiles = Array.isArray(cacheData.unsupportedFiles)
1165
1168
  ? cacheData.unsupportedFiles
1166
1169
  : [];
1170
+ index.includeBundled = cacheData.includeBundled === true;
1167
1171
  index.discoveryIssues = Array.isArray(cacheData.discoveryIssues)
1168
1172
  ? cacheData.discoveryIssues
1169
1173
  : [];
@@ -1302,8 +1306,13 @@ function isCacheStale(index) {
1302
1306
  // Only reached when all cached files are unchanged.
1303
1307
  const pattern = detectProjectPattern(index.root);
1304
1308
  const currentUnsupported = [];
1309
+ const currentBundled = [];
1305
1310
  const globOpts = {
1306
1311
  root: index.root,
1312
+ includeBundled: index.includeBundled === true,
1313
+ onDiscoveryIssue: issue => {
1314
+ if (issue.reason === 'bundled') currentBundled.push(path.relative(index.root, issue.path));
1315
+ },
1307
1316
  onSkippedFile: (filePath) => {
1308
1317
  const kind = classifyUnsupportedSourceFile(filePath);
1309
1318
  if (!kind) return;
@@ -1321,6 +1330,11 @@ function isCacheStale(index) {
1321
1330
  globOpts.ignores = [...DEFAULT_IGNORES, ...configExclude];
1322
1331
  }
1323
1332
  const currentFiles = expandGlob(pattern, globOpts);
1333
+ const cachedBundled = (index.discoveryIssues || [])
1334
+ .filter(issue => issue.reason === 'bundled').map(issue => issue.relativePath).sort();
1335
+ currentBundled.sort();
1336
+ if (cachedBundled.length !== currentBundled.length ||
1337
+ cachedBundled.some((value, i) => value !== currentBundled[i])) return true;
1324
1338
  const cachedPaths = new Set(index.files.keys());
1325
1339
  const currentPaths = new Set(currentFiles);
1326
1340
 
package/core/check.js CHANGED
@@ -73,6 +73,11 @@ function check(index, options = {}) {
73
73
  const modified = (dr && Array.isArray(dr.functions)) ? dr.functions : [];
74
74
  const added = (dr && Array.isArray(dr.newFunctions)) ? dr.newFunctions : [];
75
75
  const deleted = (dr && Array.isArray(dr.deletedFunctions)) ? dr.deletedFunctions : [];
76
+ const pathCounts = {
77
+ changedPaths: dr?.changedPaths || 0,
78
+ nonSourcePaths: dr?.nonSourcePaths || 0,
79
+ untrackedPaths: dr?.untrackedPaths || 0,
80
+ };
76
81
 
77
82
  const allChanged = [
78
83
  ...modified.map(f => ({ ...f, _kind: 'modified' })),
@@ -99,6 +104,7 @@ function check(index, options = {}) {
99
104
  status: 'clean',
100
105
  empty: true,
101
106
  reason,
107
+ ...pathCounts,
102
108
  };
103
109
  }
104
110
 
@@ -293,6 +299,7 @@ function check(index, options = {}) {
293
299
  staged: !!options.staged,
294
300
  ok: true,
295
301
  status: 'checked',
302
+ ...pathCounts,
296
303
  changed: items,
297
304
  totalChanged: allChanged.length + deleted.length,
298
305
  truncated: !!(limit && allChanged.length > limit),
package/core/discovery.js CHANGED
@@ -52,16 +52,15 @@ const DEFAULT_IGNORES = [
52
52
  '.pytest_cache',
53
53
  '.mypy_cache',
54
54
 
55
- // Bundled/minified
56
- '*.min.js',
57
- '*.bundle.js',
58
- '*.map',
59
-
60
55
  // System
61
56
  '.DS_Store',
62
57
  '.ucn-cache'
63
58
  ];
64
59
 
60
+ // These can contain user code: disclose their exclusion, unlike dependency
61
+ // and VCS directories. Source maps are data and cannot be parsed as code.
62
+ const BUNDLED_PATTERNS = ['*.min.js', '*.bundle.js', '*.map'];
63
+
65
64
  // Conditional ignores - only ignore when marker file exists in same directory
66
65
  // Maps directory name -> array of marker files that indicate it's a vendor dir
67
66
  const CONDITIONAL_IGNORES = {
@@ -340,6 +339,7 @@ function expandGlob(pattern, options = {}) {
340
339
  maxDepth,
341
340
  maxFileSize,
342
341
  followSymlinks,
342
+ includeBundled: options.includeBundled === true,
343
343
  // Anchored gitignore patterns ('/name') apply only to entries directly
344
344
  // under the project root — the .gitignore's own directory (fix #226).
345
345
  anchorRoot: root,
@@ -530,6 +530,16 @@ function walkDir(dir, options, depth = 0, visited = new Set()) {
530
530
  walkDir(fullPath, options, depth + 1, visited);
531
531
  }
532
532
  } else if (isFile) {
533
+ if (shouldIgnore(entry.name, BUNDLED_PATTERNS) &&
534
+ (!options.includeBundled || entry.name.endsWith('.map'))) {
535
+ options.onDiscoveryIssue?.({
536
+ path: fullPath, kind: 'file', reason: 'bundled',
537
+ detail: entry.name.endsWith('.map')
538
+ ? 'source maps are not supported source files'
539
+ : 'bundled/minified filename; use --include-bundled to index',
540
+ });
541
+ continue;
542
+ }
533
543
  if (options.filePattern.test(entry.name)) {
534
544
  let size;
535
545
  try { size = fs.statSync(fullPath).size; } catch (e) {
package/core/execute.js CHANGED
@@ -292,6 +292,11 @@ function num(val, fallback) {
292
292
  return isNaN(n) ? fallback : n;
293
293
  }
294
294
 
295
+ /** Shared listing bound; shell listings remain uncapped unless requested. */
296
+ function listingLimit(p) {
297
+ return num(p.limit, undefined) || (p.lines || p.all ? undefined : 500);
298
+ }
299
+
295
300
  /**
296
301
  * Apply limit to an array result.
297
302
  * Returns { items, total, limited } where limited is true if truncated.
@@ -771,7 +776,7 @@ const HANDLERS = {
771
776
  ...p,
772
777
  // repo's public limit caps the file result set. Direct toc's
773
778
  // legacy symbol-list cap is intentionally not composed here.
774
- top: p.top || p.limit,
779
+ top: p.top || listingLimit(p),
775
780
  limit: undefined,
776
781
  });
777
782
  if (!failure && selected.has('stats')) failure = collect('stats', 'stats', p);
@@ -1147,10 +1152,13 @@ const HANDLERS = {
1147
1152
  if (!p.withSource && Array.isArray(response.result)) {
1148
1153
  response.result = response.result.map(({ code, ...item }) => item);
1149
1154
  }
1150
- const limit = num(p.limit, undefined);
1155
+ const limit = listingLimit(p);
1151
1156
  if (limit && limit > 0 && Array.isArray(response.result)) {
1152
1157
  const { items, total, limited } = applyLimit(response.result, limit);
1153
1158
  response.result = items;
1159
+ Object.defineProperty(items, 'findInfo', {
1160
+ value: { total, shown: items.length }, enumerable: false,
1161
+ });
1154
1162
  if (limited) {
1155
1163
  const note = limitNote(limit, total);
1156
1164
  response.note = response.note ? `${response.note}\n${note}` : note;
@@ -1184,9 +1192,9 @@ const HANDLERS = {
1184
1192
  file: p.file,
1185
1193
  className: p.className,
1186
1194
  exact: p.exact || false,
1187
- // A shell definition listing renders no activity counts. Avoid a
1188
- // full pinned caller query for every definition in a wildcard.
1189
- skipCounts: !!p.lines,
1195
+ // Filter and bound the inventory before resolving any callers.
1196
+ // Shell listings do not need the activity pass at all.
1197
+ skipCounts: true,
1190
1198
  exclude,
1191
1199
  in: p.in,
1192
1200
  });
@@ -1204,9 +1212,6 @@ const HANDLERS = {
1204
1212
  const kinds = kindGroups[p.type] || new Set([p.type]);
1205
1213
  result = result.filter(item => kinds.has(item.type));
1206
1214
  }
1207
- if (p.withSource) {
1208
- result = result.map(item => ({ ...item, code: readAndExtract(item) }));
1209
- }
1210
1215
  const fullFindCount = result.length;
1211
1216
  const nameWideDefinitionCounts = Object.fromEntries(
1212
1217
  [...new Set(result.map(item => item.name))].map(name => [
@@ -1234,11 +1239,21 @@ const HANDLERS = {
1234
1239
  }
1235
1240
  }
1236
1241
  // Apply limit
1237
- const limit = num(p.limit, undefined);
1238
- if (limit && limit > 0) {
1239
- const { items, total, limited } = applyLimit(result, limit);
1240
- if (limited) notes.push(limitNote(limit, total));
1241
- result = items;
1242
+ const limit = listingLimit(p);
1243
+ if (!p.lines) {
1244
+ index._beginOp();
1245
+ try {
1246
+ result = index._applyFindFilters(result, { limit });
1247
+ } finally { index._endOp(); }
1248
+ } else {
1249
+ result = applyLimit(result, limit).items;
1250
+ }
1251
+ if (result.length < fullFindCount) {
1252
+ notes.push(limitNote(result.length, fullFindCount));
1253
+ if (!p.lines) notes.push('Selection ranked by approximate usage totals; returned activity counts use definition-pinned caller evidence.');
1254
+ }
1255
+ if (p.withSource) {
1256
+ result = result.map(item => ({ ...item, code: readAndExtract(item) }));
1242
1257
  }
1243
1258
  Object.defineProperty(result, 'findInfo', {
1244
1259
  value: {
@@ -1299,12 +1314,18 @@ const HANDLERS = {
1299
1314
  const hidden = unfiltered.length - result.length;
1300
1315
  if (hidden > 0) notes.push(`${hidden} test-file usage(s) hidden by default — pass --include-tests to include them.`);
1301
1316
  }
1302
- // Apply limit to total usages (result is a flat array)
1317
+ // Apply limit to total usages (result is a flat array). usages is the
1318
+ // escape-hatch listing (fix #284): only an EXPLICIT limit caps it; the
1319
+ // release command-surface gate reads every oracle reference from it.
1303
1320
  const limit = num(p.limit, undefined);
1304
1321
  let limited = result;
1305
1322
  if (limit && limit > 0 && Array.isArray(result) && result.length > limit) {
1306
1323
  notes.push(limitNote(limit, result.length));
1307
1324
  limited = result.slice(0, limit);
1325
+ Object.defineProperty(limited, 'limitInfo', {
1326
+ value: { total: result.length, shown: limited.length },
1327
+ enumerable: false,
1328
+ });
1308
1329
  // Summary counts describe the FULL result set — the limit applies
1309
1330
  // to listed entries only (fix #237: the header claimed '0 calls'
1310
1331
  // for a called function whenever the definition filled the limit).
@@ -1640,7 +1661,7 @@ const HANDLERS = {
1640
1661
  file: p.file,
1641
1662
  });
1642
1663
  // Apply limit to dead code results (result is an array with custom properties)
1643
- const limit = num(p.limit, undefined);
1664
+ const limit = listingLimit(p);
1644
1665
  let note;
1645
1666
  if (limit && limit > 0 && Array.isArray(result) && result.length > limit) {
1646
1667
  note = limitNote(limit, result.length);
@@ -2309,7 +2330,7 @@ const HANDLERS = {
2309
2330
  },
2310
2331
 
2311
2332
  api: (index, p) => {
2312
- if (p.file) {
2333
+ if (p.file && typeof index.resolveFilePathForQuery(p.file) !== 'string') {
2313
2334
  const fileErr = checkFilePatternMatch(index, p.file);
2314
2335
  if (fileErr) return { ok: false, error: fileErr };
2315
2336
  }
@@ -2325,13 +2346,13 @@ const HANDLERS = {
2325
2346
  return { ok: false, error: `No files matched the 'in' directory filter '${p.in}'.` };
2326
2347
  }
2327
2348
  }
2328
- let result = index.api(p.file, { in: p.in });
2349
+ let result = index.api(p.file, { in: p.in, includeTests: p.includeTests });
2329
2350
  if (p.file) {
2330
2351
  const fileErr = checkFileError(result, p.file, index);
2331
2352
  if (fileErr) return { ok: false, error: fileErr };
2332
2353
  }
2333
2354
  // Apply limit to api results (api returns an array)
2334
- const limit = num(p.limit, undefined);
2355
+ const limit = listingLimit(p);
2335
2356
  let note;
2336
2357
  if (limit && limit > 0 && Array.isArray(result)) {
2337
2358
  const { items, total, limited } = applyLimit(result, limit);
@@ -2353,6 +2374,10 @@ const HANDLERS = {
2353
2374
  }
2354
2375
  result = items;
2355
2376
  }
2377
+ if (result.apiInfo?.excludedTestFiles > 0) {
2378
+ const excluded = `${result.apiInfo.excludedTestFiles} test file(s) excluded from API; use --include-tests to include them, or name an exact file.`;
2379
+ note = note ? `${note}\n${excluded}` : excluded;
2380
+ }
2356
2381
  return { ok: true, result, note };
2357
2382
  },
2358
2383
 
@@ -2494,7 +2519,18 @@ function execute(index, command, params = {}) {
2494
2519
  }
2495
2520
  }
2496
2521
  }
2497
- return handler(index, params);
2522
+ const response = handler(index, params);
2523
+ const bundled = (index.discoveryIssues || []).filter(issue => issue.reason === 'bundled');
2524
+ if (bundled.length > 0) {
2525
+ const files = bundled.slice(0, 5).map(issue => issue.relativePath).join(', ');
2526
+ const note = `UCN skipped ${bundled.length} bundled/minified or source-map file(s): ${files}` +
2527
+ (bundled.length > 5 ? ', ...' : '') +
2528
+ '. This is not a repository-wide semantic zero; use --include-bundled for JavaScript bundles ' +
2529
+ '(source maps remain unindexed), or grep/ripgrep to inspect skipped files.';
2530
+ if (response.ok) response.note = combineNotes([response.note, note]);
2531
+ else response.error = `${response.error}\n${note}`;
2532
+ }
2533
+ return response;
2498
2534
  } catch (e) {
2499
2535
  return { ok: false, error: e.message };
2500
2536
  }