@omfalos/mokosh 0.5.0 → 0.5.2

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/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { F as FileNode, C as CallEdge, I as ImportEdge, P as ParseResult, S as StructuredTag } from './types-y90pqeDR.js';
2
- export { E as ExportedSymbol } from './types-y90pqeDR.js';
3
- import { N as NormalizedToken } from './tokenizer-DIbb0GLe.js';
4
- import { F as FileType, N as NodeCategory } from './parse-yamNoaoL.js';
5
- export { I as ImportType, T as TagKind } from './parse-yamNoaoL.js';
1
+ import { F as FileNode, C as CallEdge, I as ImportEdge, P as ParseResult, S as StructuredTag } from './types-B4jVIqMD.js';
2
+ export { E as ExportedSymbol } from './types-B4jVIqMD.js';
3
+ import { N as NormalizedToken } from './tokenizer-D9P-y7lH.js';
4
+ import { F as FileType, N as NodeCategory, T as TagKind } from './parse-yamNoaoL.js';
5
+ export { I as ImportType } from './parse-yamNoaoL.js';
6
6
 
7
7
  interface SerializedGraph {
8
8
  nodes: FileNode[];
@@ -17,6 +17,104 @@ interface TraversalOptions {
17
17
  direction?: "outgoing" | "incoming";
18
18
  }
19
19
 
20
+ /** Analyzes a dependency graph node map for unused files, export-usage hotspots, and circular import chains. */
21
+
22
+ /**
23
+ * Edge kinds that {@link GraphAnalyzer.findCycles} treats as *not* real import cycles and skips
24
+ * by default: `"samePackage"` for the synthetic JVM same-package sibling clique, `"docReference"`
25
+ * for Markdown link / code-span file references (ADR-009). Pass one via `includeKinds` to walk it
26
+ * anyway (e.g. a caller that genuinely wants to see doc cross-link loops).
27
+ */
28
+ type CycleEdgeKind = "docReference" | "samePackage";
29
+ /** Options for {@link GraphAnalyzer.findCycles}. */
30
+ interface FindCyclesOptions {
31
+ /** Edge kinds to include in the walk that are otherwise skipped (see {@link CycleEdgeKind}). */
32
+ includeKinds?: CycleEdgeKind[] | undefined;
33
+ /** When true, return every raw elementary cycle instead of collapsing cycles that share a
34
+ * strongly-connected component to one representative each. Default false. */
35
+ expandComponents?: boolean | undefined;
36
+ }
37
+ /**
38
+ * @description Utility for analyzing the dependency graph for cycles and unused files.
39
+ * Operates on the raw node map rather than a `Graph` instance so it can be used
40
+ * without the full traversal infrastructure.
41
+ */
42
+ declare class GraphAnalyzer {
43
+ private nodes;
44
+ /**
45
+ * @param {Map<string, FileNode>} nodes - The full node map of the graph to analyze, keyed by project-relative file path.
46
+ */
47
+ constructor(nodes: Map<string, FileNode>);
48
+ /**
49
+ * @description Returns files from `allFiles` that are absent from the graph — meaning nothing
50
+ * imports them directly or transitively from any entry point, making them deletion candidates.
51
+ * @param {string[]} allFiles - Complete list of project-relative file paths to test against the graph.
52
+ * @returns {string[]} Subset of `allFiles` whose paths do not appear as graph nodes.
53
+ */
54
+ findUnusedFiles(allFiles: string[]): string[];
55
+ /**
56
+ * @description Returns files whose highest single-edge export usage ratio meets or exceeds
57
+ * `threshold`, sorted descending by `maxExportUsage`. Useful for identifying files
58
+ * that consume a large fraction of one dependency's API surface.
59
+ * @param {number} threshold - Minimum `maxExportUsage` value (0–1) for a file to be included.
60
+ * @returns {Array<{ path: string; maxExportUsage: number; tightestDep: string }>} Entries sorted descending by `maxExportUsage`.
61
+ */
62
+ findHighExportUsage(threshold: number): Array<{
63
+ path: string;
64
+ maxExportUsage: number;
65
+ tightestDep: string;
66
+ }>;
67
+ /**
68
+ * @description The structural-import targets of `nodePath` — the outgoing edges a cycle walk
69
+ * follows. Excludes unresolved (`!toPath`) and external edges always, and by default the two
70
+ * non-`import` synthetic edge kinds: the JVM same-package sibling clique (`isSamePackage`) and
71
+ * Markdown link / code-span file references (`isDocReference`, ADR-009). Both are opt-in via
72
+ * `include`. Shared by {@link findCycles}' DFS and its strongly-connected-component pass so
73
+ * the two agree on exactly which edges exist.
74
+ * @param nodePath - The file whose outgoing edges to resolve.
75
+ * @param include - Edge kinds to walk that are otherwise skipped.
76
+ * @returns Deduplicated target paths, in first-seen order.
77
+ */
78
+ private traversableTargets;
79
+ /**
80
+ * @description Detects all circular import chains using DFS with a recursion-stack back-edge check.
81
+ * Each returned array is one cycle as an ordered list of file paths ending at the entry that closes the loop.
82
+ * Non-import edge kinds — the synthetic JVM same-package clique and Markdown doc references
83
+ * (ADR-009) — are skipped by default; pass `includeKinds` to walk them anyway.
84
+ *
85
+ * Cycles that share a strongly-connected component are collapsed to one representative each
86
+ * (the shortest, ties broken lexicographically). A single hub file cyclically bound to N
87
+ * siblings — e.g. `ItemList.tsx` importing four `*CellRenderer.tsx` that each import a type
88
+ * back from it — is one dependency knot to untangle, not N findings; the DFS otherwise emits
89
+ * one back-edge cycle per sibling. Pass `expandComponents` to get every raw elementary cycle
90
+ * instead.
91
+ * @param {FindCyclesOptions} [opts] - `includeKinds` opts specific edge kinds back into the
92
+ * walk; `expandComponents` disables the same-component collapse.
93
+ * @returns {string[][]} Array of cycles; each cycle is an ordered list of file paths forming a loop.
94
+ */
95
+ findCycles(opts?: FindCyclesOptions): string[][];
96
+ /**
97
+ * @description Collapses `cycles` so that every strongly-connected component contributes at
98
+ * most one — its shortest cycle, ties broken by the joined path string for determinism.
99
+ * Every elementary cycle lies wholly within one SCC, so cycles are grouped by the SCC id of
100
+ * any member; a graph with no repeated-SCC cycles passes through unchanged (order preserved
101
+ * by first appearance).
102
+ * @param cycles - Raw elementary cycles from the DFS.
103
+ * @param include - Edge kinds in scope, so the SCC pass walks the same edges the DFS did.
104
+ * @returns One representative cycle per SCC that had any, in first-appearance order.
105
+ */
106
+ private collapseByComponent;
107
+ /**
108
+ * @description Tarjan's strongly-connected-components over the cycle-walk edge set, restricted
109
+ * to nodes that sit in a component of size ≥ 2 (the only ones that can be on a cycle). Runs
110
+ * iteratively — the graphs this analyzes reach tens of thousands of nodes, past a safe
111
+ * recursion depth.
112
+ * @param include - Edge kinds to walk, matching {@link traversableTargets}.
113
+ * @returns Map from file path to a numeric component id, only for nodes in a non-trivial SCC.
114
+ */
115
+ private componentIds;
116
+ }
117
+
20
118
  /** Graph class wrapping the raw node map with DFS traversal, cycle detection, serialization, and reverse-edge helpers. */
21
119
 
22
120
  /**
@@ -112,9 +210,12 @@ declare class Graph {
112
210
  /**
113
211
  * @description Detects all circular import chains in the graph using DFS
114
212
  * with a back-edge check. Each returned array is one cycle as an ordered path.
213
+ * Synthetic JVM same-package edges and Markdown doc references are skipped by default —
214
+ * pass `opts.includeKinds` to walk them.
215
+ * @param {FindCyclesOptions} [opts] - Forwarded to {@link GraphAnalyzer.findCycles}.
115
216
  * @returns Array of cycles; each cycle is a list of file paths forming a loop.
116
217
  */
117
- findCycles(): string[][];
218
+ findCycles(opts?: FindCyclesOptions): string[][];
118
219
  }
119
220
 
120
221
  /** Configures whether/how `parseFile` calls are offloaded to a `piscina` worker pool. `false` always parses in-process. */
@@ -184,6 +285,16 @@ interface MokoshConfig {
184
285
  */
185
286
  frameworkOverrides?: Record<string, TagFramework>;
186
287
  };
288
+ /**
289
+ * Tunes which tags count as test-selection labels for `propose_tags`, `apply_tags`,
290
+ * `list_tags` (default view) and the `tag:` query filter. Both lists are case-insensitive.
291
+ */
292
+ tags?: {
293
+ /** Tag names to treat as noise, added to the built-in curated blocklist. */
294
+ blocklist?: string[];
295
+ /** Tag names to keep even when built-in- or user-blocked (overrides `blocklist`). */
296
+ allowlist?: string[];
297
+ };
187
298
  /** Path to the Istanbul/v8 `coverage-summary.json` file, relative to the project root. When set, `coveragePct` is populated on each node after the graph is built. */
188
299
  coverageReportPath?: string;
189
300
  /** Default line-coverage threshold (0–100) used by `find_uncovered`. Defaults to `80` when not specified. */
@@ -197,6 +308,39 @@ interface MokoshConfig {
197
308
  * in-process, or pass `{ minFiles, maxThreads }` to raise the threshold instead.
198
309
  */
199
310
  parallelParsing?: ParallelParsingOption;
311
+ /** Duplicate-detection (`find_duplicates` / `--find-duplicates`) tuning. */
312
+ duplication?: {
313
+ /**
314
+ * Extra generated / vendored file patterns to exclude from duplicate scanning, merged with
315
+ * the built-in list (protobuf output, `*.generated.*`, `generated/` dirs, `@generated`
316
+ * markers). Two shapes only — `** /name/**` (any path segment equals `name`) and `*.suffix`
317
+ * (basename ends with `.suffix`) — not full glob syntax.
318
+ */
319
+ ignoreGlobs?: string[];
320
+ /** When true, scan generated / vendored files too (default false). Matches involving one are
321
+ * tagged `signals: ["generated"]`. */
322
+ includeGenerated?: boolean;
323
+ /** When true, include matches where every occurrence is in a single file (default false) —
324
+ * a file's own naturally repetitive structure is usually not actionable copy-paste. Matches
325
+ * are always tagged `signals: ["same-file"]` regardless of this setting. */
326
+ includeSameFile?: boolean;
327
+ /** When true, include matches whose occurrences are all inline SVG / SVG-shaped JSX markup
328
+ * (default false) — two different icons sharing a literal-normalized skeleton (block matcher)
329
+ * or a byte-identical `<defs>`/`<filter>` block (`defKind: "jsxElement"`), neither of which is
330
+ * usually an authored clone. Matches are always tagged `signals: ["svg-markup"]` regardless
331
+ * of this setting. */
332
+ includeSvgMarkup?: boolean;
333
+ /** When true, include matches in the `markdown` family (default false) — mirrored prose
334
+ * docs (README ↔ *.mdx) are not code duplication. Matches are always tagged
335
+ * `signals: ["docs"]` regardless. */
336
+ includeDocs?: boolean;
337
+ /** Which duplicates to surface, by test-file involvement (default `"src"`). `"src"` drops
338
+ * every cluster with a test-file occurrence; `"tests"` returns only substantive test
339
+ * clusters (shared setup/mocks/assertions, not render/snapshot skeletons); `"all"` returns
340
+ * everything. The `--scope` CLI flag overrides this. Matches are always tagged
341
+ * `signals: ["test"]` regardless of this setting. */
342
+ scope?: "src" | "tests" | "all";
343
+ };
200
344
  }
201
345
  /**
202
346
  * @description Loads a mokosh config file, probing standard filenames in `rootDirOrPath` or reading an explicit path when `isExplicitPath` is true.
@@ -243,10 +387,40 @@ declare function configToGraphOptions(config: MokoshConfig | undefined): {
243
387
  declare const DEFAULT_CACHE_DIR = "mokosh-cache";
244
388
  /** Filename for the disk-persisted `find_duplicates` token cache within `DEFAULT_CACHE_DIR`. */
245
389
  declare const DEFAULT_DUPLICATION_TOKEN_CACHE_FILE = "duplication-tokens.json";
390
+ /** Filename for the disk-persisted `find_duplicates` *result* cache within `DEFAULT_CACHE_DIR`
391
+ * (or, on a monorepo, `<pkg-slug>-duplication-result.json` per package). Holds the full
392
+ * `{ groups, clusters }` from the last scan, keyed by a digest of every in-scope file's
393
+ * mtime/size plus the output-affecting scan params — a match lets a repeat call skip the scan
394
+ * entirely. See `src/graph/duplication/result-cache-store.ts`. */
395
+ declare const DEFAULT_DUPLICATION_RESULT_CACHE_FILE = "duplication-result.json";
246
396
  /** Filename for the disk-persisted graph cache within `DEFAULT_CACHE_DIR`. Written by the CLI
247
397
  * (`src/cli/graph-loader.ts`) after every build; read by the MCP server (`src/mcp/cache.ts`) to
248
398
  * seed a session's first `analyze` call so it reuses unchanged nodes instead of parsing cold. */
249
399
  declare const DEFAULT_GRAPH_CACHE_FILE = "graph.json";
400
+ /** Legacy filename for the single-blob disk-persisted *workspace* (monorepo) graph cache within
401
+ * `DEFAULT_CACHE_DIR`. Superseded by `DEFAULT_WORKSPACE_CACHE_SUBDIR` (a manifest plus one file
402
+ * per package) — retained only so `src/graph/workspace/disk-cache.ts` can unlink a stale copy
403
+ * left by an older mokosh. A 190 MB+ single file here was `JSON.parse`d whole and OOM-killed the
404
+ * MCP server; see the manifest layout below. */
405
+ declare const DEFAULT_WORKSPACE_GRAPH_CACHE_FILE = "workspace-graph.json";
406
+ /** Subdirectory of `DEFAULT_CACHE_DIR` holding the workspace graph cache as a `manifest.json`
407
+ * ("the map file") plus one `<pkg-slug>.json` per package. Each package file is parsed on its
408
+ * own, so peak memory on hydrate is bounded by the largest single package rather than the whole
409
+ * serialized workspace. Written/read by `src/graph/workspace/disk-cache.ts`, driven from
410
+ * `src/mcp/cache.ts`. */
411
+ declare const DEFAULT_WORKSPACE_CACHE_SUBDIR = "workspace";
412
+ /** Filename of the workspace cache manifest within `DEFAULT_WORKSPACE_CACHE_SUBDIR`: the small
413
+ * index that carries the layout, the root (non-package-owned) source digest, and one entry per
414
+ * package (name, relative root, entry points, per-package digest, node count, cache filename). */
415
+ declare const WORKSPACE_MANIFEST_FILE = "manifest.json";
416
+ /** Schema version stamped into the workspace cache manifest. A mismatch on read discards the
417
+ * whole cache (full rebuild) — bump this whenever the serialized `FileNode` shape or the
418
+ * manifest structure changes incompatibly. */
419
+ declare const WORKSPACE_CACHE_VERSION = 1;
420
+ /** A single package whose serialized node array exceeds this is not written to the workspace
421
+ * cache (and never read back) — that package always rebuilds. Keeps any one per-package
422
+ * `JSON.parse` on hydrate within a safe memory envelope. */
423
+ declare const MAX_PACKAGE_CACHE_BYTES: number;
250
424
  /** Subdirectory of `DEFAULT_CACHE_DIR` holding one JSON file per commit sha — graphs built for
251
425
  * the "other" ref in a `compareBranches` call (`src/graph/branch-graph-cache.ts`). Keyed by sha
252
426
  * rather than branch name so entries are immutable and never need invalidation. */
@@ -315,6 +489,57 @@ interface ApiSurface {
315
489
  */
316
490
  testFiles: string[];
317
491
  }
492
+ /**
493
+ * A compact, token-bounded projection of an {@link ApiSurface}, suitable as the default
494
+ * response of a "what's the public API?" query. Every unbounded list in `ApiSurface` is
495
+ * replaced by a count; `publicExports` is reduced to a capped `{ name, kind }` sample plus a
496
+ * `byKind` histogram over the full set. `unreachableFromEntry` — the one directly actionable
497
+ * signal (missed entry points / dead code) — keeps its list, capped.
498
+ */
499
+ interface ApiSurfaceSummary {
500
+ /** Entry points, capped (see `entryPointsTruncated`). */
501
+ entryPoints: string[];
502
+ /** True number of entry points. */
503
+ entryPointCount: number;
504
+ /** Present and `true` when `entryPoints` was truncated. */
505
+ entryPointsTruncated?: true;
506
+ /** True number of accessible public exports. */
507
+ publicExportCount: number;
508
+ /** Capped `{ name, kind }` sample of the public exports, alphabetical. */
509
+ publicExports: Array<{
510
+ name: string;
511
+ kind: ExportKind;
512
+ }>;
513
+ /** Present and `true` when `publicExports` was truncated. */
514
+ publicExportsTruncated?: true;
515
+ /** Count of public exports per {@link ExportKind}, over the full (un-capped) set. */
516
+ byKind: Partial<Record<ExportKind, number>>;
517
+ /** Number of non-test implementation files reachable from an entry point. */
518
+ internalFileCount: number;
519
+ /** Number of non-test files not reachable from any entry point. */
520
+ unreachableFromEntryCount: number;
521
+ /** The unreachable non-test files, capped (see `unreachableFromEntryTruncated`). */
522
+ unreachableFromEntry: string[];
523
+ /** Present and `true` when `unreachableFromEntry` was truncated. */
524
+ unreachableFromEntryTruncated?: true;
525
+ /** Number of unreachable test files. */
526
+ testFileCount: number;
527
+ /** How to obtain the full lists this summary omits. */
528
+ hint: string;
529
+ }
530
+ /**
531
+ * Reduces a full {@link ApiSurface} to a token-bounded {@link ApiSurfaceSummary}.
532
+ *
533
+ * @param {ApiSurface} surface - The full surface report from {@link buildApiSurface}.
534
+ * @param {{ maxExports?: number; maxUnreachable?: number }} [opts] - `maxExports` caps the
535
+ * `publicExports` sample (default 30; pass `Infinity` to keep all names); `maxUnreachable`
536
+ * caps the `unreachableFromEntry` list (default 50).
537
+ * @returns {ApiSurfaceSummary} The compact projection.
538
+ */
539
+ declare function summarizeApiSurface(surface: ApiSurface, opts?: {
540
+ maxExports?: number;
541
+ maxUnreachable?: number;
542
+ }): ApiSurfaceSummary;
318
543
  /**
319
544
  * Attempts to auto-detect the primary public entry point by reading `package.json` from `root`.
320
545
  * Handles modern conditional-exports objects as well as plain `main`/`module` fields.
@@ -468,17 +693,27 @@ declare function loadChangeImpactCache(cachePath: string): ChangeImpactCache | n
468
693
 
469
694
  /**
470
695
  * Language-family partitioning for duplicate detection. `findDuplicates` never compares files
471
- * across a family boundary — see docs/adr-013-duplicate-detection-noise-reduction.md. The
472
- * immediate driver is CSS-family declarations: they share a small, finite vocabulary (property
696
+ * across a family boundary — see docs/adr-013-duplicate-detection-noise-reduction.md.
697
+ *
698
+ * The original driver was CSS: style declarations share a small, finite vocabulary (property
473
699
  * names, common value keywords) that the shared `KEYWORDS` denylist in `tokenizer.ts` doesn't
474
- * cover, so unrelated style rules with the same declaration *shape* but different selectors/
475
- * properties/values were hashing identically to unrelated TS/JS/Python shapes too. Splitting by
476
- * family also lets later phases tune `windowSize`/`minLines`/vocabulary per family without one
477
- * family's tuning affecting another's.
700
+ * cover, so unrelated rules with the same declaration *shape* hashed identically to unrelated
701
+ * code shapes. The same argument applies, more weakly, *within* code: Java, Go, and TS/JS all use
702
+ * C-style `if (…) { }` / `for (…; …; …)` blocks, so a `for` loop that pushes onto a list
703
+ * tokenizes almost identically in all three once identifiers collapse to `ID` — a polyglot repo
704
+ * then reports phantom control-flow "duplicates" across languages that share nothing. So code is
705
+ * split by paradigm too, not lumped into one family.
706
+ *
707
+ * Grouping (not one family per `FileType`) is deliberate: JavaScript↔TypeScript and
708
+ * CoffeeScript/LiveScript→JavaScript ports are real, common, cross-`FileType` duplication worth
709
+ * detecting, so those languages share the `"js"` family. The JVM languages interoperate and share
710
+ * idioms, so they share `"jvm"` — and, being their own family, never match against `"js"`, which
711
+ * is the case that motivated this split. Splitting by family also lets later phases tune
712
+ * `windowSize`/`minLines`/vocabulary per family without one family's tuning affecting another's.
478
713
  */
479
714
 
480
715
  /** One partition of `findDuplicates` matching — files in different families are never compared. */
481
- type DuplicateFamily = "style" | "code";
716
+ type DuplicateFamily = "style" | "js" | "jvm" | "python" | "go" | "lua" | "markdown" | "gherkin" | "other";
482
717
 
483
718
  /**
484
719
  * Sliding-window (shingle) hashing over a normalized token stream, plus chain-merging of
@@ -496,7 +731,32 @@ interface DuplicateOccurrence {
496
731
  file: string;
497
732
  startLine: number;
498
733
  endLine: number;
499
- }
734
+ /** The variable/type name declared at this location. Set only on `kind: "definition"` groups
735
+ * (`style-vars.ts`, `type-defs.ts`) — block matches have no single declared name. */
736
+ name?: string | undefined;
737
+ /** This occurrence's own normalized value. Set only on `defKind: "cssVar"` groups tagged
738
+ * `signals: ["value-drift"]`, where occurrences disagree and the value itself is the point. */
739
+ value?: string | undefined;
740
+ }
741
+ /**
742
+ * Advisory tags on a {@link DuplicateGroup} that help a caller (and issue 6's query layer) judge
743
+ * whether a match is worth acting on: `"same-file"` — every occurrence is in one file (an
744
+ * internally repeated block); `"generated"` — at least one occurrence is in a file only scanned
745
+ * because `includeGenerated: true` was passed; `"value-drift"` — a `defKind: "cssVar"` group
746
+ * where the same variable *name* holds different values across files (as opposed to the default
747
+ * consolidation case: different names, same value); `"svg-markup"` — every occurrence's source
748
+ * span is predominantly inline SVG / SVG-shaped JSX markup (a block match where two *different*
749
+ * icons share a literal-normalized skeleton, or a `defKind: "jsxElement"` match on an identical
750
+ * `<defs>`/`<filter>` block — see `svg-markup.ts`); `"test"` — at least one occurrence is in a
751
+ * test file (`__tests__/`, `__mocks__/`, `*.test.*`, `*.spec.*`, `__snapshots__/`); `"docs"` —
752
+ * the group is in the `markdown` family (mirrored prose docs, `README.md` ↔ `*.mdx`).
753
+ * `"same-file"`, `"svg-markup"` and `"docs"` groups are excluded from `findDuplicates`' results
754
+ * by default (opt back in with `includeSameFile` / `includeSvgMarkup` / `includeDocs`); `"test"`
755
+ * groups are filtered by the `scope` option (see `findDuplicates`), which separates substantive
756
+ * shared test logic from render/snapshot skeletons. The tag is always attached regardless of any
757
+ * filter. Designed to grow (issue 7 adds more).
758
+ */
759
+ type DuplicateSignal = "same-file" | "generated" | "value-drift" | "svg-markup" | "test" | "docs";
500
760
  interface DuplicateGroup {
501
761
  /** Every location sharing this duplicated block (two or more) — locations that pairwise
502
762
  * chain-match are clustered into one group instead of being reported once per pair, so a
@@ -504,14 +764,105 @@ interface DuplicateGroup {
504
764
  occurrences: DuplicateOccurrence[];
505
765
  /** Line span of the largest single pairwise match clustered into this group (each pair's own
506
766
  * span is the shorter of its two occurrences) — the best-verified size for this block, not an
507
- * average or a value shrunk by a more weakly-matching cluster member. */
767
+ * average or a value shrunk by a more weakly-matching cluster member. For `kind: "definition"`
768
+ * groups this is the declaration's own line span instead (1 for a single-line `cssVar` decl). */
508
769
  lines: number;
509
- /** Token-window length backing this block, after chain-merging adjacent windows. */
770
+ /** Token-window length backing this block, after chain-merging adjacent windows. For
771
+ * `kind: "definition"` groups this is the member/field count instead (1 for `cssVar`). */
510
772
  tokens: number;
773
+ /** Count of *logic-bearing* tokens (keywords + operators, not `ID`/`NUM`/`STR`/punctuation —
774
+ * see {@link isSignificantToken}) in the verified span. `findDuplicates` ranks and (with
775
+ * `minScore`) filters on this so a large low-information block — a Flow `type Props`, a JSX
776
+ * icon wrapper, a schema object literal — no longer outranks real shared logic. Set on
777
+ * `kind: "block"` groups by `suffix-duplicates.ts`; absent on `kind: "definition"` groups
778
+ * (already content-verified) — those are ranked by `lines` instead. See
779
+ * docs/adr-019-logic-token-scoring.md. */
780
+ score?: number | undefined;
511
781
  /** Which language family both occurrences belong to (set by `findDuplicates`, which never
512
782
  * matches across families — see docs/adr-013-duplicate-detection-noise-reduction.md).
513
783
  * Absent when called directly with a token stream that isn't family-scoped, e.g. in tests. */
514
784
  family?: DuplicateFamily | undefined;
785
+ /** Advisory tags for filtering — see {@link DuplicateSignal}. Absent (not `[]`) when no
786
+ * signal applies. */
787
+ signals?: DuplicateSignal[] | undefined;
788
+ /** `"block"` (the default, and absent on every group predating this field) is a token/structural
789
+ * match over a span of code; `"definition"` is a declaration-level match — see `defKind`. */
790
+ kind?: "block" | "definition" | undefined;
791
+ /** What kind of declaration a `kind: "definition"` group matched on. Absent for `kind: "block"`
792
+ * (or absent-`kind`) groups. */
793
+ defKind?: "cssVar" | "interface" | "type" | "objectLiteral" | "jsxElement" | undefined;
794
+ }
795
+
796
+ /**
797
+ * Groups `DuplicateGroup`s that share the exact same set of files into one `DuplicateCluster`,
798
+ * addressing the "window-splitting" noise class `applyDominanceFilter` (suffix-duplicates.ts)
799
+ * deliberately doesn't touch: two files that share several genuinely non-nested matches (different
800
+ * start positions, no containment relationship — see
801
+ * docs/adr-015-suffix-array-duplicate-detection.md's addendum) still surface as one `DuplicateGroup`
802
+ * per match, so a single real duplication between two files can legitimately report a dozen-plus
803
+ * rows. A file pair matched by 14 separate groups (same two files, non-nested spans) becomes one
804
+ * cluster with `matchCount: 14` instead of 14 rows a caller has to manually recognize as "the same
805
+ * underlying duplication" — the `ContentExplorer.tsx`/`ContentPicker.js` case from the dogfooding
806
+ * report this responds to.
807
+ *
808
+ * **This module previously clustered by connected file-component (union-find: any two files that
809
+ * co-occur in some group's occurrences are connected, transitively) instead of exact file-set
810
+ * equality, and that was wrong — caught by a follow-up dogfood pass before anyone relied on it.**
811
+ * Connected-component clustering is single-linkage clustering, and single-linkage is notorious for
812
+ * "chaining": on a real repo, one file sharing a single incidental block each with dozens of
813
+ * otherwise-unrelated files is enough to transitively merge all of them into one cluster, since
814
+ * connectivity only ever needs one bridge edge, never a strong one. Confirmed in practice on a
815
+ * production monorepo: 803 files and 4,853 groups collapsed into a single "cluster" — a regression,
816
+ * not a summary, and strictly less useful than the flat `groups` list it was meant to compress.
817
+ * Exact file-set equality can't chain this way: a group between `{A, B}` and a group between
818
+ * `{B, C}` (B is a genuine bridge — it duplicates code with both, but that doesn't mean A and C
819
+ * relate to each other) land in two separate clusters, not one, because their occurrence sets
820
+ * differ. The trade-off this accepts: a true N-way clone family reported as several exact-N-way
821
+ * groups already merges correctly (same set every time); one reported as several *different-sized*
822
+ * subsets (a 3-file match here, a 2-file subset of it there) does not merge into one cluster. That
823
+ * residual case is deliberately left alone rather than resolved with more graph theory — a
824
+ * conservative under-merge is a far safer default than the chaining failure mode above.
825
+ *
826
+ * **Coverage.** `matchCount: 14` still reads as "14 things," not "one relationship" — the same
827
+ * gap SonarQube's duplication check closes by reporting a merged duplication-*density* percentage
828
+ * per file instead of an enumerated match list. {@link DuplicateCluster.coverage} does the same
829
+ * here: union each cluster's occurrence spans per file (so overlapping/adjacent sub-matches count
830
+ * once, not once each) and divide by that file's total line count — turning
831
+ * `matchCount: 14, files: ["ContentExplorer.tsx", "ContentPicker.js"]` into
832
+ * `"ContentExplorer.tsx: 62% (312/501 lines) duplicated with ContentPicker.js"`, which is a much
833
+ * more decisive triage signal than the match count alone: high coverage on both sides says "these
834
+ * are near-duplicate files," low coverage says "one shared helper embedded in two otherwise-
835
+ * unrelated files."
836
+ */
837
+
838
+ interface DuplicateClusterFileCoverage {
839
+ file: string;
840
+ /** Total lines this cluster's occurrences cover in `file`, after merging overlapping/adjacent
841
+ * spans across every group in the cluster — never double-counts a line two groups both touch. */
842
+ coveredLines: number;
843
+ /** `file`'s total line count, when known (the caller passed `fileLineCounts` and had an entry
844
+ * for it) — `undefined` otherwise, e.g. a cache-hit file scanned without re-reading its source. */
845
+ totalLines: number | undefined;
846
+ /** `coveredLines / totalLines * 100`, rounded to one decimal place — `undefined` when
847
+ * `totalLines` isn't known or is 0. */
848
+ coveragePct: number | undefined;
849
+ }
850
+ interface DuplicateCluster {
851
+ /** The exact set of files every group in this cluster shares occurrences in, sorted. */
852
+ files: string[];
853
+ /** This cluster's member groups, largest-`lines`-first — the same `DuplicateGroup` objects
854
+ * `findDuplicates` would otherwise report standalone, unmodified. */
855
+ groups: DuplicateGroup[];
856
+ /** `groups.length` — how many separate matches this cluster bundles. A cluster with
857
+ * `matchCount: 1` is just a single group reported through the same shape for consistency. */
858
+ matchCount: number;
859
+ /** The largest `lines` value across this cluster's groups — the single best-verified size for
860
+ * "how big is this duplication," independent of how many rows it fragmented into. */
861
+ longestMatch: number;
862
+ /** Per-file duplication coverage, same order as `files` — see this module's top-of-file
863
+ * comment. Every entry has `coveredLines`; `totalLines`/`coveragePct` are only present when
864
+ * `buildDuplicateClusters` was given a line count for that file. */
865
+ coverage: DuplicateClusterFileCoverage[];
515
866
  }
516
867
 
517
868
  /** One file's cached tokenize result, fingerprinted by `mtime`/`size`/`ignoreLiterals` — any
@@ -522,6 +873,10 @@ interface CachedFileTokens {
522
873
  mtime: number;
523
874
  size: number;
524
875
  ignoreLiterals: boolean;
876
+ /** Whether this file is generated / vendored (path heuristic or first-bytes marker) — cached
877
+ * so a `includeGenerated: false` scan that gets a cache hit can still exclude it, and a
878
+ * `includeGenerated: true` scan can still tag matches, without re-reading the file. */
879
+ generated: boolean;
525
880
  tokens: NormalizedToken[];
526
881
  }
527
882
  /** Caller-owned cache, keyed by project-relative path, reused across repeated `findDuplicates`
@@ -550,6 +905,174 @@ declare function loadTokenCacheFromDisk(cachePath: string): DuplicationTokenCach
550
905
  */
551
906
  declare function saveTokenCacheToDisk(cache: DuplicationTokenCache, cachePath: string): void;
552
907
 
908
+ /**
909
+ * @description Whether a file's path alone marks it as generated — a known codegen basename
910
+ * suffix, a generated-output directory segment, or a caller-supplied `ignoreGlobs` match.
911
+ * Cheap and synchronous, so it runs before the file is read.
912
+ * @param relPath - Project-relative file path.
913
+ * @param ignoreGlobs - Extra patterns from `MokoshConfig.duplication.ignoreGlobs`.
914
+ * @returns Whether `relPath` should be treated as generated.
915
+ */
916
+ declare function isGeneratedPath(relPath: string, ignoreGlobs?: readonly string[]): boolean;
917
+ /**
918
+ * @description Whether the start of a file's source carries a generated-by marker
919
+ * (`@generated`, `DO NOT EDIT`, `Code generated by …`, etc.). Only the first
920
+ * {@link MARKER_SCAN_BYTES} characters are checked — codegen tools put the banner at the top.
921
+ * @param source - Full file source.
922
+ * @returns Whether a generated marker is present near the top of `source`.
923
+ */
924
+ declare function hasGeneratedMarker(source: string): boolean;
925
+
926
+ /** The output-affecting `findDuplicates` knobs — everything that changes which groups/clusters
927
+ * the scan produces. Folded into the cache key via {@link duplicationResultCacheKey}; a
928
+ * mismatch is a cache miss. `filter` is deliberately absent — the cache only ever holds an
929
+ * unfiltered scan. Any new output-affecting option wired into `findDuplicates` (e.g. a future
930
+ * `minScore`) must be added here too, or a run that changes it will read a stale result. */
931
+ interface DuplicationResultParams {
932
+ minLines: number;
933
+ ignoreLiterals: boolean;
934
+ maxPunctuationRatio: number;
935
+ /** The per-scan cap passed to `findDuplicates` — the stored result holds at most this many
936
+ * groups per package, so a later run wanting more must re-scan. */
937
+ limit: number;
938
+ scope: string | undefined;
939
+ includeGenerated: boolean;
940
+ includeSameFile: boolean;
941
+ includeSvgMarkup: boolean;
942
+ includeDocs: boolean;
943
+ ignoreDirs: string[];
944
+ ignoreGlobs: string[];
945
+ }
946
+ interface CachedDuplicationResult {
947
+ /** {@link duplicationDigest} of the in-scope files at write time. */
948
+ digest: string;
949
+ /** {@link duplicationResultCacheKey} of the scan params at write time. */
950
+ paramsKey: string;
951
+ groups: DuplicateGroup[];
952
+ clusters: DuplicateCluster[];
953
+ }
954
+ /**
955
+ * @description One `path\0mtime\0size` line per node, sha256'd over the sorted set — the same
956
+ * unit `computeWorkspaceSourceDigest` (`src/index.ts`) hashes, but sourced from the in-memory
957
+ * graph (every `FileNode` already carries `mtime`/`size`) so no `fs.stat` walk is needed.
958
+ * Passing the whole node set (a superset of what `findDuplicates` actually scans) is
959
+ * intentional: an out-of-scope change over-invalidates, which is safe.
960
+ * @param {Iterable<{ path: string; mtime: number; size: number }>} nodes - Graph file nodes.
961
+ * @returns {string} Hex sha-256 digest.
962
+ */
963
+ declare function duplicationDigest(nodes: Iterable<{
964
+ path: string;
965
+ mtime: number;
966
+ size: number;
967
+ }>): string;
968
+ /**
969
+ * @description Stable string form of the scan params for the cache key — keys emitted in a fixed
970
+ * order, arrays sorted — so equivalent params always produce the same string regardless of how
971
+ * the caller assembled them.
972
+ * @param {DuplicationResultParams} params - The output-affecting scan knobs.
973
+ * @returns {string} A canonical JSON string to compare byte-for-byte.
974
+ */
975
+ declare function duplicationResultCacheKey(params: DuplicationResultParams): string;
976
+ /**
977
+ * @description Reads a `CachedDuplicationResult` written by {@link saveDuplicationResult} and
978
+ * returns its `{ groups, clusters }` only when the stored `digest` and `paramsKey` both still
979
+ * match — i.e. no in-scope file changed and the scan params are identical. Any other outcome
980
+ * (missing file, corrupt JSON, wrong shape, stale digest, different params) returns `null`,
981
+ * which the caller treats as "run the scan". Never throws.
982
+ * @param {string} cachePath - Path to the JSON file written by `saveDuplicationResult`.
983
+ * @param {string} digest - The current {@link duplicationDigest} to validate against.
984
+ * @param {string} paramsKey - The current {@link duplicationResultCacheKey} to validate against.
985
+ * @returns {{ groups: DuplicateGroup[]; clusters: DuplicateCluster[] } | null} The cached result,
986
+ * or `null` on any miss.
987
+ */
988
+ declare function loadDuplicationResult(cachePath: string, digest: string, paramsKey: string): {
989
+ groups: DuplicateGroup[];
990
+ clusters: DuplicateCluster[];
991
+ } | null;
992
+ /**
993
+ * @description Serializes the full scan result to `cachePath` (creating parent dirs), stamped
994
+ * with the `digest` / `paramsKey` it is valid for. A write failure is logged to stderr and
995
+ * otherwise swallowed — the result cache is pure acceleration and must never fail the call
996
+ * that produced it.
997
+ * @param {string} cachePath - Destination path; parent directories are created automatically.
998
+ * @param {string} digest - The {@link duplicationDigest} this result was computed against.
999
+ * @param {string} paramsKey - The {@link duplicationResultCacheKey} this result was computed
1000
+ * against.
1001
+ * @param {readonly DuplicateGroup[]} groups - The full pre-`limit` group list from the scan.
1002
+ * @param {readonly DuplicateCluster[]} clusters - The full cluster list from the scan.
1003
+ * @returns {void}
1004
+ */
1005
+ declare function saveDuplicationResult(cachePath: string, digest: string, paramsKey: string, groups: readonly DuplicateGroup[], clusters: readonly DuplicateCluster[]): void;
1006
+
1007
+ /**
1008
+ * Response-shaping helpers shared by the `find_duplicates` MCP handler and the `--find-duplicates`
1009
+ * CLI command: a triage-first `summary` block and the compact ("slim") group/cluster projections.
1010
+ * Kept separate from `index.ts` (the scan) and `dup-filter.ts` (the predicate) so both consumers
1011
+ * present duplicate results identically.
1012
+ */
1013
+
1014
+ /** First path segment of a project-relative path (`"packages/app/x.ts"` -> `"packages"`), or
1015
+ * `"."` when the file sits at the repo root — the bucket key for `summary.byTopDir`. */
1016
+ declare function topDir(relPath: string): string;
1017
+ /** The triage-first `summary` block. */
1018
+ interface DuplicatesSummary {
1019
+ /** How many groups matched (post-`filter`, before any response `limit`). A lower bound when a
1020
+ * per-package scan cap clipped on a very large monorepo; exact otherwise. */
1021
+ matched: number;
1022
+ /** Group count per language `family`. */
1023
+ byFamily: Record<string, number>;
1024
+ /** Group count per top-level directory — a group touching two dirs counts in both. */
1025
+ byTopDir: Record<string, number>;
1026
+ /** Occurrence-signal frequency across the matched groups. */
1027
+ bySignal: Record<string, number>;
1028
+ /** Largest `lines` value among the matched groups. */
1029
+ largestLines: number;
1030
+ /** Only set on a `view: "full"` response: how many groups were dropped from the returned
1031
+ * `groups` list because a returned multi-member cluster already represents them (see
1032
+ * {@link dedupeGroupsAgainstClusters}). */
1033
+ clusteredGroups?: number;
1034
+ }
1035
+ /**
1036
+ * @description Drops from `groups` every group that is a member of a multi-member cluster in
1037
+ * `clusters` — that cluster already represents it (with a better signal: per-file `coverage`),
1038
+ * so returning both is redundant. Groups whose file pair matched only once (no multi-member
1039
+ * cluster) are kept. Used only for the `view: "full"` response, the one path that returns both
1040
+ * lists.
1041
+ * @param {readonly DuplicateGroup[]} groups - The ordered, pre-`limit` group list.
1042
+ * @param {readonly DuplicateCluster[]} clusters - The clusters that will be returned alongside.
1043
+ * @returns {DuplicateGroup[]} `groups` minus the ones folded into a returned multi-member cluster.
1044
+ */
1045
+ declare function dedupeGroupsAgainstClusters(groups: readonly DuplicateGroup[], clusters: readonly DuplicateCluster[]): DuplicateGroup[];
1046
+ /**
1047
+ * @description Builds the {@link DuplicatesSummary} for a `find_duplicates` response — the counts
1048
+ * a caller reads once to decide which targeted `filter` call to make next, without pulling the
1049
+ * full group list.
1050
+ * @param {readonly DuplicateGroup[]} groups - Every group that survived the `filter` predicate,
1051
+ * before the response `limit` truncates the list.
1052
+ * @returns {DuplicatesSummary} The aggregate counts.
1053
+ */
1054
+ declare function summarizeDuplicates(groups: readonly DuplicateGroup[]): DuplicatesSummary;
1055
+ /** Compact `["path:startLine-endLine", …]` rendering of a group's occurrences. */
1056
+ declare function slimOccurrences(group: DuplicateGroup): string[];
1057
+ /**
1058
+ * @description Slim per-group projection: size / score / classification / signals plus
1059
+ * `"path:start-end"` occurrence strings — no duplicated source text, no per-occurrence
1060
+ * metadata objects. Preserves a `package` field when `findDuplicates`' caller tagged one.
1061
+ * @param {DuplicateGroup} group - A finalized duplicate group.
1062
+ * @returns {Record<string, unknown>} The compact object.
1063
+ */
1064
+ declare function slimDupGroup(group: DuplicateGroup): Record<string, unknown>;
1065
+ /**
1066
+ * @description Slim per-cluster projection: the file set, match count, best-verified size,
1067
+ * per-file coverage, and the `"path:start-end"` span of the cluster's longest match — without
1068
+ * the nested member-group bodies. `longestMatchAt` is what makes a slim cluster actionable on
1069
+ * its own: it answers "where do I look" without the caller also pulling the `groups` list.
1070
+ * @param {DuplicateCluster} cluster - A built cluster. `cluster.groups` is largest-`lines`-first
1071
+ * (see `buildDuplicateClusters`), so `groups[0]` is the longest match.
1072
+ * @returns {Record<string, unknown>} The compact object.
1073
+ */
1074
+ declare function slimDupCluster(cluster: DuplicateCluster): Record<string, unknown>;
1075
+
553
1076
  /** Configures whether/how tokenizing is offloaded to a `piscina` worker pool. `false` always
554
1077
  * tokenizes in-process. */
555
1078
  type ParallelTokenizingOption = boolean | {
@@ -577,6 +1100,62 @@ interface FindDuplicatesOptions {
577
1100
  /** Directory names to exclude, matched against any path segment (default `DEFAULT_IGNORE_DIRS`
578
1101
  * — `node_modules`, `dist`, `.git`, `mokosh-cache`, `coverage`, etc.). Pass `[]` to disable. */
579
1102
  ignoreDirs?: readonly string[] | undefined;
1103
+ /** When false (default), skip generated / vendored files — protobuf output, `*.generated.*`,
1104
+ * codegen basenames, files under a `generated/` segment, and files whose first ~500 bytes
1105
+ * carry a `@generated` / `DO NOT EDIT` / `Code generated by` marker. Their repetition is not
1106
+ * actionable copy-paste. Set true to scan them anyway; matches involving one are tagged
1107
+ * `signals: ["generated"]`. See docs/adr-013-duplicate-detection-noise-reduction.md. */
1108
+ includeGenerated?: boolean | undefined;
1109
+ /** When false (default), skip matches where every occurrence is in a single file — a file's
1110
+ * own naturally repetitive structure (a long class with many similar methods, a Markdown doc
1111
+ * table, a big object literal) shows up as "duplicating itself" far more often than it
1112
+ * represents an actionable copy-paste bug. Dogfooding found this was the single largest noise
1113
+ * class in `groups` (32% in one measurement). Set true to see them anyway; they're always
1114
+ * tagged `signals: ["same-file"]` regardless of this flag, so a caller already relying on that
1115
+ * tag to filter manually is unaffected either way. Mirrors `includeGenerated`'s
1116
+ * default-off/opt-in-back-in shape. */
1117
+ includeSameFile?: boolean | undefined;
1118
+ /** When false (default), skip matches where every occurrence's source span is predominantly
1119
+ * inline SVG / SVG-shaped JSX markup. Two mechanisms produce these: the token-shingle block
1120
+ * matcher (under the default `ignoreLiterals: true` the `d=` path string and filter constants
1121
+ * that actually tell two icons apart normalize to a placeholder, so *different* icons
1122
+ * token-match on their shared skeleton), and the `defKind: "jsxElement"` detector (two icons
1123
+ * sharing a byte-identical `<defs>`/`<filter>` block — boilerplate, not an authored clone).
1124
+ * Both are excluded here. Set true to see them anyway; they're always tagged
1125
+ * `signals: ["svg-markup"]` regardless. Mirrors `includeSameFile`'s default-off/opt-in shape.
1126
+ * See `svg-markup.ts`. */
1127
+ includeSvgMarkup?: boolean | undefined;
1128
+ /** When false (default), skip matches in the `markdown` family — prose docs (`README.md` ↔
1129
+ * `*.mdx`, `*.md` ↔ `*.md`) that mirror each other are not code duplication, and their
1130
+ * fenced code blocks tokenize densely enough to top the score-ranked list. Set true to see
1131
+ * them anyway; they're always tagged `signals: ["docs"]` regardless. Mirrors
1132
+ * `includeSameFile`'s default-off/opt-in shape. */
1133
+ includeDocs?: boolean | undefined;
1134
+ /** Minimum logic-token score (keywords + operators in the verified span — see
1135
+ * {@link significantTokenCount}) for a `kind: "block"` match to be reported. Default `0`
1136
+ * (off): every match is still returned, but `groups` and `clusters` are now *ranked* by score
1137
+ * regardless. A positive value drops boilerplate-shaped blocks — a Flow `type Props`, a JSX
1138
+ * icon-component wrapper, a schema object literal all score in the single digits; a real
1139
+ * 30-line function scores 25–40. Never filters `kind: "definition"` groups (already
1140
+ * content-verified). See docs/adr-019-logic-token-scoring.md. */
1141
+ minScore?: number | undefined;
1142
+ /** Which duplicates to surface, by whether test files are involved (default `"src"`). A group
1143
+ * with any occurrence in a test file (`__tests__/`, `__mocks__/`, `*.test.*`, `*.spec.*`,
1144
+ * `__snapshots__/`) is always tagged `signals: ["test"]`; this option decides which of those
1145
+ * reach `groups`/`clusters`:
1146
+ * - `"src"` (default) — drop every cluster that has a test-file occurrence. Auditing product
1147
+ * code shouldn't be buried under `render(<Icon/>); expect(…).toMatchSnapshot()` repeated
1148
+ * across hundreds of icon tests.
1149
+ * - `"tests"` — surface *only* test clusters, and only the substantive ones: a cluster with
1150
+ * ≥ {@link TEST_SUBSTANTIVE_BLOCKS} distinct shared blocks across ≤ {@link
1151
+ * TEST_SUBSTANTIVE_MAX_FILES} files, or one block ≥ {@link TEST_SUBSTANTIVE_TOKENS} tokens —
1152
+ * i.e. shared setup / mocks / assertions worth a helper, not a one-block render skeleton.
1153
+ * - `"all"` — every cluster, test or not, subject only to the other filters. */
1154
+ scope?: "src" | "tests" | "all" | undefined;
1155
+ /** Extra generated-file patterns merged with the built-in list (from
1156
+ * `MokoshConfig.duplication.ignoreGlobs`). Two shapes only — `**​/name/**` (path segment) and
1157
+ * `*.suffix` (basename) — not full glob syntax. */
1158
+ ignoreGlobs?: readonly string[] | undefined;
580
1159
  /** Controls worker-pool offloading of per-file tokenizing (default `true`): offloads once the
581
1160
  * candidate file count reaches `minFiles` (default 20, matching `GraphBuilder`'s parse pool);
582
1161
  * `false` always tokenizes in-process; an object overrides `minFiles`/`maxThreads`. See
@@ -587,10 +1166,33 @@ interface FindDuplicatesOptions {
587
1166
  * call's) skip tokenizing entirely. Mutated in place; omit for one-shot callers (e.g. the CLI).
588
1167
  * See {@link DuplicationTokenCache} and docs/adr-014-duplicate-detection-scale.md. */
589
1168
  tokenCache?: DuplicationTokenCache | undefined;
1169
+ /** Optional `key:value` result filter (see `src/query/dup-parser.ts` / `DuplicateQuery`).
1170
+ * Applied as a per-group predicate *before* clustering, so `clusters` stay consistent with the
1171
+ * narrowed `groups`. Only the predicate keys act here — the DSL's `sort`/`limit` are the
1172
+ * caller's to apply (via `sortLimitDupGroups`), since on a monorepo `findDuplicates` runs
1173
+ * once per package and a global order/cap can only be decided after the per-package results
1174
+ * merge. A parse error in the string is thrown, not swallowed. */
1175
+ filter?: string | undefined;
590
1176
  }
591
1177
  interface FindDuplicatesResult {
592
1178
  /** Duplicate blocks, largest-first, capped at `limit`. */
593
1179
  groups: DuplicateGroup[];
1180
+ /** `groups` (before `limit` truncation) bucketed by *exact* occurrence file set — every group
1181
+ * is merged into one cluster with every other group whose occurrences touch the identical set
1182
+ * of files, so N separate non-nested matches between the same two files (the "window-splitting"
1183
+ * case, see docs/known_issues/09-duplicate-clone-family-noise.md) read as one `matchCount: N`
1184
+ * entry instead of N rows a caller has to manually recognize as the same underlying
1185
+ * duplication. Deliberately *not* transitive across partially-overlapping file sets (a group
1186
+ * over `{A, B}` and one over `{B, C}` land in separate clusters) — an earlier connected-
1187
+ * component version was chaining unrelated files through shared bridge files into one
1188
+ * incomprehensible supercluster on real repos; see `src/graph/duplication/clusters.ts`'s
1189
+ * top-of-file comment. Largest-`longestMatch`-first, capped at `limit` clusters (each cluster's
1190
+ * own `groups` are never truncated). No group is dropped or altered to build this — every group
1191
+ * in `groups` (subject to `limit`) also appears inside exactly one cluster here. Each cluster
1192
+ * also carries per-file duplication `coverage` — merged occurrence spans divided by that
1193
+ * file's total line count — so `matchCount: 14` reads as "62% of this file" rather than just
1194
+ * a row count; see `src/graph/duplication/clusters.ts`'s `DuplicateClusterFileCoverage`. */
1195
+ clusters: DuplicateCluster[];
594
1196
  }
595
1197
  /**
596
1198
  * @description Scans every file already present in `graph` for cross-file (and within-file)
@@ -602,8 +1204,11 @@ interface FindDuplicatesResult {
602
1204
  * identifiers (and, by default, literals) are normalized to placeholders so renamed-variable
603
1205
  * copies still match, then a sliding token window is hashed and chain-merged into contiguous
604
1206
  * blocks. Token-shingled files are additionally partitioned into language families
605
- * ({@link getDuplicateFamily} — `"style"` for Stylus, `"code"` for everything else) so
606
- * matching never crosses that boundary see docs/adr-013-duplicate-detection-noise-reduction.md.
1207
+ * ({@link getDuplicateFamily} — `"js"` for JS/TS/Coffee/LS, `"jvm"` for Java/Kotlin/Scala/
1208
+ * Groovy, `"style"` for Stylus, and one family each for Python, Go, Lua, Markdown, Gherkin) so
1209
+ * matching never crosses a family boundary — a `for` loop tokenizes almost identically in Java,
1210
+ * Go and TS, so a single `"code"` family produced phantom cross-language matches on polyglot
1211
+ * repos. See docs/adr-013-duplicate-detection-noise-reduction.md.
607
1212
  * @param graph - The graph to scan; its node paths (already ignore-rule-filtered) are the file
608
1213
  * list, re-read from disk since duplication data isn't cached on `FileNode`.
609
1214
  * @param rootDir - Absolute project root that graph paths are relative to.
@@ -612,13 +1217,24 @@ interface FindDuplicatesResult {
612
1217
  * token-shingle path only (CSS/Less/SCSS always match on literal declaration content);
613
1218
  * `maxPunctuationRatio` gates out token-shingle blocks that are mostly object/array-literal
614
1219
  * structural punctuation (e.g. schema/object-literal boilerplate) rather than substantive
615
- * shared logic; `ignoreDirs` excludes files under matching directory names; `limit` caps
616
- * results; `parallelTokenizing` offloads per-file tokenizing to a worker pool once the
617
- * candidate file count is large enough to be worth it. Lock files are always excluded,
618
- * independent of `ignoreDirs`.
1220
+ * shared logic; `ignoreDirs` excludes files under matching directory names; `includeSameFile`
1221
+ * controls whether same-file-only matches are returned (excluded by default — see its doc
1222
+ * comment); `includeSvgMarkup` likewise controls whether inline-SVG-markup matches are returned
1223
+ * (excluded by default — two different icons share a literal-normalized skeleton); `scope`
1224
+ * filters whole clusters by test-file involvement — `"src"` (default) drops every test cluster,
1225
+ * `"tests"` returns only the substantive ones, `"all"` returns everything; `filter` is an
1226
+ * optional `key:value` DSL string (see `DuplicateQuery`) applied as a per-group predicate
1227
+ * before clustering; `limit` caps results; `parallelTokenizing` offloads per-file tokenizing
1228
+ * to a worker pool once the candidate file count is large enough to be worth it. Lock files and
1229
+ * `type: "unknown"` nodes (non-code assets like `.svg`/`.json` pulled in via an explicit
1230
+ * `import`) are always excluded, independent of `ignoreDirs`.
619
1231
  * @returns `groups` — duplicate blocks (each tagged with its `family`), two or more occurrences
620
1232
  * per block, every block that pairwise chain-matches another clustered into one group instead
621
- * of one per pair, sorted largest-first across all families.
1233
+ * of one per pair, sorted largest-first across all families (same-file-only and svg-markup
1234
+ * matches excluded unless `includeSameFile` / `includeSvgMarkup`; test matches filtered per
1235
+ * `scope`). `clusters` — the same groups
1236
+ * bucketed by exact file set, with
1237
+ * per-file duplication coverage, see {@link FindDuplicatesResult.clusters}.
622
1238
  */
623
1239
  declare function findDuplicates(graph: Graph, rootDir: string, options?: FindDuplicatesOptions): Promise<FindDuplicatesResult>;
624
1240
 
@@ -920,19 +1536,48 @@ interface MonorepoDetector {
920
1536
  */
921
1537
  declare function registerMonorepoDetector(detector: MonorepoDetector): void;
922
1538
 
923
- /**
924
- * @description Runs all registered monorepo detectors against `rootDir` and merges
925
- * their results into a single `MonorepoLayout`. All matching detectors contribute
926
- * their `type` string and packages — so a Turborepo + pnpm repo will have
927
- * `types: ["turborepo", "pnpm"]` and packages from the pnpm detector.
928
- *
929
- * Packages are deduplicated by name: the first detector to emit a package name wins.
930
- * Returns `type: "none"` when no detector fires.
931
- */
932
- declare function detectMonorepo(rootDir: string, detectors?: readonly MonorepoDetector[]): MonorepoLayout;
933
-
934
1539
  /** WorkspaceGraph holds one per-package Graph for a monorepo and exposes cross-package blast-radius queries. */
935
1540
 
1541
+ /**
1542
+ * @description Whether `relPath` falls under `pkg`'s own `relativeRoot` — i.e. the package owns
1543
+ * that file, as opposed to merely importing it across a package boundary. Each per-package
1544
+ * `Graph` also carries "borrowed" nodes for cross-package files it imports (so outward
1545
+ * traversal reaches them); this predicate is how callers that iterate every package's graph
1546
+ * avoid double-counting a file that two packages both import.
1547
+ * @param {string} relPath - A monorepo-root-relative file path.
1548
+ * @param {Pick<WorkspacePackage, "relativeRoot">} pkg - The package to test ownership against.
1549
+ * @returns {boolean} `true` if `relPath` is `pkg.relativeRoot` or sits beneath it.
1550
+ */
1551
+ declare function packageOwnsFile(relPath: string, pkg: Pick<WorkspacePackage, "relativeRoot">): boolean;
1552
+ /** @description A whole-workspace `Graph` (one namespace, all packages' own nodes, cross-package
1553
+ * edges intact) paired with a path → owning-package-name lookup. Produced by
1554
+ * {@link WorkspaceGraph.flatten}. */
1555
+ interface FlatWorkspaceGraph {
1556
+ graph: Graph;
1557
+ packageOf: Map<string, string>;
1558
+ }
1559
+ /** @description Compact, capped cross-package blast radius — the response shape of
1560
+ * {@link WorkspaceGraph.summarizeAffectedAcrossPackages}. */
1561
+ interface WorkspaceAffectedSummary {
1562
+ /** The changed file the blast radius was computed for. */
1563
+ file: string;
1564
+ /** Total affected files across every package (the real count, never capped). */
1565
+ totalAffected: number;
1566
+ /** Number of distinct packages with at least one affected file. */
1567
+ packageCount: number;
1568
+ /** Per-package breakdown, sorted by `count` descending. */
1569
+ byPackage: Array<{
1570
+ package: string;
1571
+ /** Real affected-file count for this package. */
1572
+ count: number;
1573
+ /** Up to `maxFilesPerPackage` example paths. */
1574
+ sample: string[];
1575
+ /** `count - sample.length` — files omitted from `sample`. */
1576
+ more: number;
1577
+ }>;
1578
+ /** `true` if any package's `sample` was capped (`more > 0` somewhere). */
1579
+ truncated: boolean;
1580
+ }
936
1581
  /** @description JSON-safe snapshot of a `WorkspaceGraph`, suitable for writing to disk and restoring via `WorkspaceGraph.deserialize`. */
937
1582
  interface SerializedWorkspaceGraph {
938
1583
  monorepoRoot: string;
@@ -955,6 +1600,9 @@ declare class WorkspaceGraph {
955
1600
  graph: Graph;
956
1601
  pkg: WorkspacePackage;
957
1602
  }>;
1603
+ /** Lazily-built merged view; see {@link flatten}. Never invalidated — a rebuild replaces the
1604
+ * whole `WorkspaceGraph` instance rather than mutating this one. */
1605
+ private flattened?;
958
1606
  /**
959
1607
  * @param {string} monorepoRoot - Absolute path to the monorepo root directory.
960
1608
  * @param {string} type - Primary detected monorepo tool (e.g. `"turborepo"`, `"pnpm"`), or `"none"`.
@@ -977,6 +1625,19 @@ declare class WorkspaceGraph {
977
1625
  * Call once after all packages are registered.
978
1626
  */
979
1627
  annotateCrossPackageEdges(): void;
1628
+ /**
1629
+ * @description Merges every package's own nodes into a single `Graph` sharing one path
1630
+ * namespace, plus a `path → package name` lookup. "Borrowed" cross-package nodes are
1631
+ * dropped (each is contributed by its owning package instead), so no file appears twice.
1632
+ * Cross-package import edges already carry real monorepo-root-relative `toPath`s, so
1633
+ * `Graph.traverse` and `Graph.findCycles` span package boundaries on the returned graph
1634
+ * with no special-casing — this is the basis for whole-workspace blast radius, call
1635
+ * graphs, symbol search and queries. The graph is read-only: `FileNode`s are shared by
1636
+ * reference with the per-package graphs, never cloned.
1637
+ * Result is memoized for the life of this `WorkspaceGraph`.
1638
+ * @returns {FlatWorkspaceGraph} The merged graph and its package lookup.
1639
+ */
1640
+ flatten(): FlatWorkspaceGraph;
980
1641
  /**
981
1642
  * @description Returns the workspace package whose `relativeRoot` is a path prefix of `relPath`.
982
1643
  * @param {string} relPath - A monorepo-root-relative file path to look up.
@@ -991,9 +1652,10 @@ declare class WorkspaceGraph {
991
1652
  getPackageDependencies(): Map<string, string[]>;
992
1653
  /**
993
1654
  * @description Cross-package blast-radius analysis. Returns every file (with its package name)
994
- * that could be affected if the given monorepo-root-relative path changes.
995
- * Step 1: traverses incoming edges within the owning package graph for intra-package dependents.
996
- * Step 2: surfaces files in other packages that hold workspace import edges pointing at the owner.
1655
+ * transitively affected if the given monorepo-root-relative path changes — a full incoming
1656
+ * traversal over the flattened whole-workspace graph, so it follows real import edges across
1657
+ * package boundaries. The full, unbounded list: on a large monorepo this can be most of the
1658
+ * repo, so prefer {@link summarizeAffectedAcrossPackages} for an agent-facing response.
997
1659
  * @param {string} relPath - Monorepo-root-relative path of the changed file.
998
1660
  * @returns {Array<{ file: string; package: string }>} Each affected file paired with its package name.
999
1661
  */
@@ -1001,6 +1663,19 @@ declare class WorkspaceGraph {
1001
1663
  file: string;
1002
1664
  package: string;
1003
1665
  }>;
1666
+ /**
1667
+ * @description Agent-friendly form of {@link getAffectedAcrossPackages}: the blast radius
1668
+ * grouped by owning package, with each package's file list capped so the response stays
1669
+ * small even when thousands of files are affected. Packages are sorted by affected count
1670
+ * (descending).
1671
+ * @param {string} relPath - Monorepo-root-relative path of the changed file.
1672
+ * @param {{ maxFilesPerPackage?: number }} [opts] - `maxFilesPerPackage` caps each package's
1673
+ * `sample` list (default 10; `0` = counts only, no file lists).
1674
+ * @returns {WorkspaceAffectedSummary} Totals plus a capped per-package breakdown.
1675
+ */
1676
+ summarizeAffectedAcrossPackages(relPath: string, opts?: {
1677
+ maxFilesPerPackage?: number;
1678
+ }): WorkspaceAffectedSummary;
1004
1679
  /**
1005
1680
  * @description Serializes the workspace graph to a plain JSON-safe object.
1006
1681
  * `root` is omitted from package entries as it is not needed after build time.
@@ -1016,6 +1691,47 @@ declare class WorkspaceGraph {
1016
1691
  static deserialize(data: SerializedWorkspaceGraph): WorkspaceGraph;
1017
1692
  }
1018
1693
 
1694
+ interface WorkspaceLayoutPackageSummary {
1695
+ name: string;
1696
+ relativeRoot: string;
1697
+ /** Sibling workspace packages this one depends on. Exact when `dependsOnResolved`, best-effort otherwise. */
1698
+ dependsOn: string[];
1699
+ /** Present only when a built workspace graph was available. */
1700
+ nodeCount?: number;
1701
+ }
1702
+ interface WorkspaceLayoutSummary {
1703
+ monorepoType: string;
1704
+ monorepoTypes: string[];
1705
+ packageCount: number;
1706
+ packages: WorkspaceLayoutPackageSummary[];
1707
+ /** `true` when `dependsOn` came from a built graph or every package exposed a manifest. */
1708
+ dependsOnResolved: boolean;
1709
+ /** `true` when per-package `nodeCount` is populated (a built graph was available). */
1710
+ nodeCountsResolved: boolean;
1711
+ note?: string;
1712
+ }
1713
+ /**
1714
+ * @description Summarizes a monorepo from its detected layout alone — package names, relative
1715
+ * roots, and best-effort `dependsOn` from `package.json` manifests — without building any
1716
+ * dependency graph. When a built `WorkspaceGraph` is supplied, its exact per-package node
1717
+ * counts and cross-package edges are used instead.
1718
+ * @param layout - The result of `detectMonorepo`. Must not be `type: "none"`.
1719
+ * @param builtGraph - An already-built workspace graph for `layout.root`, if one is cached.
1720
+ * @returns A layout summary suitable for the `get_workspace_packages` response.
1721
+ */
1722
+ declare function summarizeWorkspaceLayout(layout: MonorepoLayout, builtGraph?: WorkspaceGraph): WorkspaceLayoutSummary;
1723
+
1724
+ /**
1725
+ * @description Runs all registered monorepo detectors against `rootDir` and merges
1726
+ * their results into a single `MonorepoLayout`. All matching detectors contribute
1727
+ * their `type` string and packages — so a Turborepo + pnpm repo will have
1728
+ * `types: ["turborepo", "pnpm"]` and packages from the pnpm detector.
1729
+ *
1730
+ * Packages are deduplicated by name: the first detector to emit a package name wins.
1731
+ * Returns `type: "none"` when no detector fires.
1732
+ */
1733
+ declare function detectMonorepo(rootDir: string, detectors?: readonly MonorepoDetector[]): MonorepoLayout;
1734
+
1019
1735
  /**
1020
1736
  * @description Contract for graph serializers. Implement this to add a new export format
1021
1737
  * (e.g. Graphviz DOT, JSON, SVG) without touching the core graph model.
@@ -1136,6 +1852,7 @@ interface SlimNode {
1136
1852
  exports: string[];
1137
1853
  tags: string[];
1138
1854
  importsFiles: string[];
1855
+ package?: string;
1139
1856
  description?: string;
1140
1857
  testedBy?: string[];
1141
1858
  coveragePct?: number;
@@ -1182,6 +1899,14 @@ declare function hasCoverageData(graph: Graph): boolean;
1182
1899
  * @returns Whether any node has a defined `commitCount90d`.
1183
1900
  */
1184
1901
  declare function hasChurnData(graph: Graph): boolean;
1902
+ /**
1903
+ * @description Returns `true` if at least one node has a last-commit timestamp — i.e. `analyze`
1904
+ * ran with `gitStats` enabled. `check_doc_drift` needs this: without it `enrichDocDrift`
1905
+ * populates no `staleFor`, so an empty result would be indistinguishable from "no drift".
1906
+ * @param graph - The graph to check.
1907
+ * @returns Whether any node has a defined `lastCommitAt`.
1908
+ */
1909
+ declare function hasGitTimestampData(graph: Graph): boolean;
1185
1910
  interface RiskHotspotEntry {
1186
1911
  file: string;
1187
1912
  name: string;
@@ -1228,6 +1953,13 @@ interface BuildGraphAtRefOptions {
1228
1953
  pathAliases?: Record<string, string[]> | undefined;
1229
1954
  /** Extra directory names to skip during test/doc discovery, on top of the built-in list. Sourced from `MokoshConfig.ignoreDirs`. */
1230
1955
  additionalIgnoreDirs?: string[] | undefined;
1956
+ /** Monorepo roots only. When set, the base-ref graph is built by calling this with the
1957
+ * worktree directory — the caller builds a whole `WorkspaceGraph` there and returns its
1958
+ * flattened `Graph`, so cross-package edges survive. Bypasses the entry-point-seeded
1959
+ * `GraphBuilder` path (which has no `workspaceMap` and would miss `@org/*` imports).
1960
+ * Injected rather than imported to keep `compare.ts` free of the `createWorkspaceGraph`
1961
+ * dependency cycle. */
1962
+ workspaceBuilder?: ((worktreeDir: string) => Promise<Graph>) | undefined;
1231
1963
  }
1232
1964
  /**
1233
1965
  * @description Builds the dependency graph as it existed at `ref`, rather than the current
@@ -1412,12 +2144,89 @@ declare const EXPORT_TRACKING_TYPES: ReadonlySet<FileType>;
1412
2144
  declare const IMPORT_SYMBOL_TYPES: ReadonlySet<FileType>;
1413
2145
  /** File types whose parser records function-level call edges (`FileNode.callEdges`). */
1414
2146
  declare const CALL_EDGE_TYPES: ReadonlySet<FileType>;
2147
+ /** File types whose parser populates the per-function complexity breakdown (`FileNode.functions`)
2148
+ * plus file-level `complexity` / `cognitiveComplexity`. */
2149
+ declare const FUNCTION_COMPLEXITY_TYPES: ReadonlySet<FileType>;
2150
+ /** File types with a dedicated test-tag strategy (`src/tags/strategies/`) — a framework-aware
2151
+ * applier, not the generic path-glob fallback. */
2152
+ declare const TEST_TAG_STRATEGY_TYPES: ReadonlySet<FileType>;
2153
+ /** File types the type graph (`buildTypeGraph`) can extract interfaces/classes/enums/aliases from. */
2154
+ declare const TYPE_GRAPH_TYPES: ReadonlySet<FileType>;
2155
+ /** A graph-analysis capability that only some languages' parsers feed. */
2156
+ type LanguageFeature = "callEdges" | "functionComplexity" | "typeGraph";
2157
+ /**
2158
+ * @description Returns an explanatory note when none of the graphs contain a language whose
2159
+ * parser feeds `feature` — so an empty tool result (`count: 0`) reads as "language not
2160
+ * supported" rather than "nothing found". Returns `undefined` when at least one supported-
2161
+ * language file is present (the empty result is then genuine).
2162
+ * @param graphs - One graph, or the per-package graphs of a workspace.
2163
+ * @param feature - The capability the calling tool depends on.
2164
+ * @returns A one-sentence note, or `undefined`.
2165
+ */
2166
+ declare function languageSupportNote(graphs: Graph | Graph[], feature: LanguageFeature): string | undefined;
2167
+ /** How completely a single analysis axis is implemented for one language:
2168
+ * - `"full"` — implemented with language-aware handling; results are call/symbol-level accurate.
2169
+ * - `"partial"` — implemented but known-lossy (index-based resolution, module-level exports,
2170
+ * constructor-only call edges, heuristic categories, the generic token duplicate pipeline).
2171
+ * - `"none"` — not implemented, or not applicable to this language. */
2172
+ type FidelityLevel = "full" | "partial" | "none";
2173
+ /** Per-language fidelity across every precision-relevant analysis axis. The four axes backed by
2174
+ * a `*_TYPES` set above are kept in exact sync with it by a test; the rest are a maintained
2175
+ * judgement, cross-checked against `docs/language-support.md`. */
2176
+ interface LanguageFidelity {
2177
+ /** Raw import/require specifiers → resolved graph edges. */
2178
+ importResolution: FidelityLevel;
2179
+ /** `FileNode.exports` populated with named symbols. Backed by {@link EXPORT_TRACKING_TYPES}. */
2180
+ exportSymbols: FidelityLevel;
2181
+ /** `ImportEdge.symbols` — which names each import pulls in. Backed by {@link IMPORT_SYMBOL_TYPES}. */
2182
+ importSymbols: FidelityLevel;
2183
+ /** Function-level call edges (`FileNode.callEdges`). Backed by {@link CALL_EDGE_TYPES}. */
2184
+ callEdges: FidelityLevel;
2185
+ /** Per-function + file-level complexity. Backed by {@link FUNCTION_COMPLEXITY_TYPES}. */
2186
+ complexity: FidelityLevel;
2187
+ /** Accuracy of the `logic`/`ui`/`test`/`config`/… category classification. */
2188
+ category: FidelityLevel;
2189
+ /** Duplicate detection: `"full"` = language-aware structural comparator (CSS family),
2190
+ * `"partial"` = the generic cross-language token pipeline (the norm — works, no language
2191
+ * semantics), `"none"` = not scanned. */
2192
+ duplication: FidelityLevel;
2193
+ /** A framework-aware test-tag strategy. Backed by {@link TEST_TAG_STRATEGY_TYPES}. */
2194
+ testTags: FidelityLevel;
2195
+ }
2196
+ /**
2197
+ * Authoritative per-language fidelity matrix — see `docs/language-support.md` for the same table
2198
+ * with per-language known-limitations prose and ADR links, and `docs/known_issues/08-cross-language-reliability.md`
2199
+ * for the plan this closes the first slice of. Every {@link FileType} has an entry.
2200
+ */
2201
+ declare const LANGUAGE_FIDELITY: Record<FileType, LanguageFidelity>;
2202
+ /**
2203
+ * @description Returns one advisory sentence per language present in `graphs` whose `axis`
2204
+ * fidelity is `"partial"` or `"none"` — so a tool whose result is real but *lossy* for the
2205
+ * languages in play (e.g. `get_call_graph` on a Java repo: constructors only) can say so
2206
+ * instead of returning a confident-looking result. Complements {@link languageSupportNote},
2207
+ * which only fires when the result is fully empty *and* no supported-language file is present.
2208
+ * @param graphs - One graph, or the per-package graphs of a workspace.
2209
+ * @param axis - The fidelity axis the calling tool depends on.
2210
+ * @returns Sorted, de-duplicated caveat sentences; empty when every present language is `"full"`
2211
+ * for `axis` (or the only files are markdown/unknown).
2212
+ */
2213
+ declare function languageCaveats(graphs: Graph | Graph[], axis: keyof LanguageFidelity): string[];
2214
+ /**
2215
+ * @description Aggregates {@link languageCaveats} across the precision-relevant axes
2216
+ * ({@link SUMMARY_AXES}) for an `analyze` response, so a caller sees upfront — from the first
2217
+ * call — where results for this repo's languages will be lossy.
2218
+ * @param graphs - One graph, or the per-package graphs of a workspace.
2219
+ * @returns Sorted, de-duplicated caveat sentences; empty for an all-`full` (e.g. all-TS) repo.
2220
+ */
2221
+ declare function languageCaveatsSummary(graphs: Graph | Graph[]): string[];
1415
2222
  interface LanguageCoverage {
1416
2223
  type: FileType;
1417
2224
  fileCount: number;
1418
2225
  exportsTracked: boolean;
1419
2226
  importSymbolsTracked: boolean;
1420
2227
  callEdgesTracked: boolean;
2228
+ /** The full per-axis fidelity for this language — see {@link LANGUAGE_FIDELITY}. */
2229
+ fidelity: LanguageFidelity;
1421
2230
  }
1422
2231
  /**
1423
2232
  * @description Reports which precision-relevant data mokosh actually tracks for each language
@@ -1464,6 +2273,90 @@ interface SymbolMatch {
1464
2273
  */
1465
2274
  declare function findSymbol(graph: Graph, name: string): SymbolMatch[];
1466
2275
 
2276
+ /** Tag kinds worth surfacing for `tag:<name>` querying by default — the shared
2277
+ * {@link SELECTION_TAG_KINDS} (`comment-marker` + `import`). The other kinds (`function` /
2278
+ * `variable` / `library`) are declaration/dependency names, not test-selection labels. */
2279
+ declare const SUMMARY_TAG_KINDS: readonly TagKind[];
2280
+ /** Default `minCount` floor — drops the single-occurrence long tail. */
2281
+ declare const DEFAULT_TAG_MIN_COUNT = 2;
2282
+ /** Default `limit` when the caller gives none. 50 count-ranked tags ≈ ~1K tokens and covers
2283
+ * the discovery need; the tail past it is uniformly low-count. */
2284
+ declare const DEFAULT_TAG_LIMIT = 50;
2285
+ /** Absolute ceiling on the returned `tags` array. `limit` is clamped to this; no argument
2286
+ * combination can exceed it. ~250 `{name,count,kinds}` entries ≈ ~1.5K tokens. */
2287
+ declare const TAG_RESPONSE_HARD_CAP = 250;
2288
+ /** One distinct tag name, aggregated across every kind and node it appears on. */
2289
+ interface TagAggregateEntry {
2290
+ name: string;
2291
+ /** Total node occurrences, summed across every kind this name appears with. */
2292
+ count: number;
2293
+ /** Distinct kinds seen for this name, in `TagKind` declaration order. */
2294
+ kinds: TagKind[];
2295
+ }
2296
+ /** The full tag inventory for one or more graphs, before any filtering. */
2297
+ interface TagInventory {
2298
+ /** name → aggregate, first-seen insertion order. */
2299
+ entries: Map<string, TagAggregateEntry>;
2300
+ /** Distinct tag *names* per kind (a name carrying two kinds counts once under each). */
2301
+ byKind: Partial<Record<TagKind, number>>;
2302
+ /** `entries.size` — distinct tag names across all kinds. */
2303
+ totalDistinct: number;
2304
+ }
2305
+ /** Knobs for {@link summarizeTagInventory}. Each may be `undefined` (unset) so callers can
2306
+ * forward optional args straight through under `exactOptionalPropertyTypes`. */
2307
+ interface TagSummaryOptions {
2308
+ /** Restrict to one kind, or `"all"` for every kind. Default: {@link SUMMARY_TAG_KINDS}. */
2309
+ kind?: TagKind | "all" | undefined;
2310
+ /** Case-insensitive substring match on the tag name. */
2311
+ prefix?: string | undefined;
2312
+ /** Minimum node count for a tag to appear. Default: {@link DEFAULT_TAG_MIN_COUNT}. */
2313
+ minCount?: number | undefined;
2314
+ /** Max tags returned. Default: {@link DEFAULT_TAG_LIMIT}; hard-capped at
2315
+ * {@link TAG_RESPONSE_HARD_CAP}. */
2316
+ limit?: number | undefined;
2317
+ }
2318
+ /** The bounded `list_tags` response. */
2319
+ interface TagSummary {
2320
+ /** Filtered, sorted (count desc, then name asc), capped. */
2321
+ tags: Array<{
2322
+ name: string;
2323
+ count: number;
2324
+ kinds: TagKind[];
2325
+ }>;
2326
+ /** `tags.length` after the cap. */
2327
+ count: number;
2328
+ /** Names passing the `kind` + `prefix` + `minCount` filter, before the cap. */
2329
+ matched: number;
2330
+ /** Distinct tag names across ALL kinds, before any filter. */
2331
+ totalDistinct: number;
2332
+ /** Distinct names per kind over the FULL inventory (always every kind). */
2333
+ byKind: Partial<Record<TagKind, number>>;
2334
+ /** Set when `matched > count` (the cap or `limit` clipped the list). */
2335
+ truncated?: true;
2336
+ /** How to narrow further. */
2337
+ hint: string;
2338
+ }
2339
+ /**
2340
+ * @description Aggregates every tag on every node of the given graphs into one inventory:
2341
+ * per-name occurrence count, the set of kinds each name carries, and a `byKind` histogram
2342
+ * of distinct names per kind. Replaces the ad-hoc count loop the MCP handler and CLI command
2343
+ * each used to run.
2344
+ * @param {readonly Graph[]} graphs - Graphs to scan (one for a single package, many for a
2345
+ * workspace fan-out).
2346
+ * @returns {TagInventory} The full, unfiltered inventory.
2347
+ */
2348
+ declare function buildTagInventory(graphs: readonly Graph[]): TagInventory;
2349
+ /**
2350
+ * @description Filters, sorts and caps a {@link TagInventory} into the bounded `list_tags`
2351
+ * response. The returned `tags` array never exceeds {@link TAG_RESPONSE_HARD_CAP}, whatever
2352
+ * the options; `byKind` and `totalDistinct` always describe the full inventory so a caller
2353
+ * can see what the cap hid.
2354
+ * @param {TagInventory} inventory - Output of {@link buildTagInventory}.
2355
+ * @param {TagSummaryOptions} [opts] - `kind` / `prefix` / `minCount` / `limit` knobs.
2356
+ * @returns {TagSummary} The bounded response object.
2357
+ */
2358
+ declare function summarizeTagInventory(inventory: TagInventory, opts?: TagSummaryOptions): TagSummary;
2359
+
1467
2360
  /** A config matcher: substring, regex, or predicate tested against the lowercase basename. */
1468
2361
  type ConfigMatcher = string | RegExp | ((baseName: string) => boolean);
1469
2362
  /**
@@ -1535,6 +2428,10 @@ interface NodeQuery {
1535
2428
  /** AND match — all entries must be present (use `tag:a+b` syntax in query strings). */
1536
2429
  allTags?: string[];
1537
2430
  path?: string;
2431
+ /** Exact match on the node's owning workspace package name. Prefix with `"!"` to negate.
2432
+ * Only meaningful when a `packageOf` lookup is supplied to `filterGraph`/`matchNode`
2433
+ * (i.e. querying a flattened workspace graph); ignored otherwise. */
2434
+ package?: string;
1538
2435
  isExternal?: boolean;
1539
2436
  /** Substring match on `imp.toPath` — node must import a file whose path contains this string. */
1540
2437
  importsFile?: string;
@@ -1578,23 +2475,202 @@ interface NodeQuery {
1578
2475
  /** OR-group: node matches if it satisfies ANY sub-query in this array, ANDed with all other top-level fields on this NodeQuery. Populated by `any(key:val|key:val)` syntax in query strings. */
1579
2476
  any?: NodeQuery[];
1580
2477
  }
2478
+ /** Sortable axes for `find_duplicates` results (`DuplicateQuery.sort`). */
2479
+ type DupSortField = "lines" | "score" | "occurrences";
2480
+ /**
2481
+ * Criteria for filtering `find_duplicates` results — the duplicate-group/cluster analogue of
2482
+ * {@link NodeQuery}. Parsed from a `"key:value,key:value"` string by `parseDupQuery`
2483
+ * (AND across keys); applied per group by `matchDupGroup`. String fields support a `"!"` prefix
2484
+ * for negation. A group's occurrences never span more than one language family or `FileType`
2485
+ * (matching never crosses a family boundary), so `family`/`type` are effectively group-level.
2486
+ */
2487
+ interface DuplicateQuery {
2488
+ /** Substring match on occurrence paths — matches when *at least one* occurrence's path
2489
+ * contains this. `"!substr"` matches when *no* occurrence's path contains it. */
2490
+ path?: string;
2491
+ /** Substring match — matches only when *every* occurrence's path contains this (an
2492
+ * entirely-within-one-module duplicate). `"!substr"` negates (no occurrence contains it). */
2493
+ allPaths?: string;
2494
+ /** Exact match on the group's `family` (`js`, `jvm`, `style`, `python`, `go`, `lua`,
2495
+ * `markdown`, `gherkin`, `other`). `"!"` to negate. */
2496
+ family?: string;
2497
+ /** Exact match on the `FileType` shared by every occurrence (derived from each occurrence's
2498
+ * path). `"!lang"` matches when no occurrence is of that type. */
2499
+ type?: string;
2500
+ /** `"block"` (a span match) or `"definition"` (a declaration-level match). */
2501
+ kind?: string;
2502
+ /** Exact match on a definition group's `defKind` (`cssVar` | `interface` | `type` |
2503
+ * `objectLiteral` | `jsxElement`). `"!"` to negate. */
2504
+ defKind?: string;
2505
+ /** Minimum / maximum duplicated block size in lines (`DuplicateGroup.lines`). */
2506
+ minLines?: number;
2507
+ maxLines?: number;
2508
+ /** Minimum / maximum logic-token score (`DuplicateGroup.score`, falling back to `lines` for
2509
+ * definition groups, which carry no score). */
2510
+ minScore?: number;
2511
+ maxScore?: number;
2512
+ /** Minimum number of occurrences (`DuplicateGroup.occurrences.length`). */
2513
+ minOccurrences?: number;
2514
+ /** `true` = occurrences span ≥2 distinct files; `false` = every occurrence is in one file. */
2515
+ crossFile?: boolean;
2516
+ /** OR match across positive entries (`DuplicateSignal` names); `"!name"` entries are
2517
+ * mandatory exclusions, evaluated independently. */
2518
+ signals?: string[];
2519
+ /** Result ordering for the flat `groups` list. */
2520
+ sort?: DupSortField;
2521
+ /** Direction for `sort`. Defaults to `"desc"`. */
2522
+ sortDir?: "asc" | "desc";
2523
+ /** Cap on the number of results. */
2524
+ limit?: number;
2525
+ }
2526
+
2527
+ /** Applies DuplicateQuery predicates to `find_duplicates` groups: path, family, type, size, score,
2528
+ * occurrence count, cross-file, and signal filters, plus DSL-driven sort/limit. */
2529
+
2530
+ /**
2531
+ * @description Whether a single {@link DuplicateGroup} satisfies every clause in `query` (AND
2532
+ * across keys). An empty query matches every group.
2533
+ * @param {DuplicateGroup} group - The group to test.
2534
+ * @param {DuplicateQuery} query - Parsed filter criteria.
2535
+ * @returns {boolean} `true` when the group passes all set criteria.
2536
+ */
2537
+ declare function matchDupGroup(group: DuplicateGroup, query: DuplicateQuery): boolean;
2538
+ /**
2539
+ * @description Applies the DSL's `sort` / `sortDir` / `limit` to an already-filtered group list.
2540
+ * When no `sort` is given the input order is preserved (callers pass groups already ranked by
2541
+ * `findDuplicates`). Pure — returns a new array. Split from {@link applyDupQuery} so consumers
2542
+ * that received predicate-filtered groups from `findDuplicates` can re-order/cap them without a
2543
+ * redundant second predicate pass.
2544
+ * @param {DuplicateGroup[]} groups - Groups to order and cap.
2545
+ * @param {DuplicateQuery} query - Parsed criteria; only `sort`/`sortDir`/`limit` are read.
2546
+ * @returns {DuplicateGroup[]} The groups, ordered and capped per the query.
2547
+ */
2548
+ declare function sortLimitDupGroups(groups: DuplicateGroup[], query: DuplicateQuery): DuplicateGroup[];
2549
+ /**
2550
+ * @description Filters `groups` by `query`, then orders and caps them via
2551
+ * {@link sortLimitDupGroups}. Pure — returns a new array.
2552
+ * @param {DuplicateGroup[]} groups - Groups to filter and shape.
2553
+ * @param {DuplicateQuery} query - Parsed filter criteria.
2554
+ * @returns {DuplicateGroup[]} The matching groups, ordered and capped per the query.
2555
+ */
2556
+ declare function applyDupQuery(groups: DuplicateGroup[], query: DuplicateQuery): DuplicateGroup[];
2557
+
2558
+ /** Parses a `key:value` filter string for `find_duplicates` results into a structured DuplicateQuery. */
2559
+
2560
+ /**
2561
+ * @description Parses a `"key:value,key:value"` filter string (AND across keys) into a
2562
+ * structured {@link DuplicateQuery} for use with `matchDupGroup` / `applyDupQuery`. An empty
2563
+ * or whitespace-only string yields an empty query (matches everything).
2564
+ * @param {string} queryString - Comma-separated `key:value` pairs, e.g.
2565
+ * `"crossFile:true,type:typescript,path:!test"`.
2566
+ * @returns {DuplicateQuery} The structured query object.
2567
+ * @throws {Error} On an unknown key, a malformed clause, a non-numeric numeric value, or an
2568
+ * invalid `sort` value — surfaced to the caller rather than silently ignored, so a typo'd
2569
+ * filter fails loudly instead of returning zero groups.
2570
+ */
2571
+ declare function parseDupQuery(queryString: string): DuplicateQuery;
1581
2572
 
1582
2573
  /** Filters a graph by applying NodeQuery predicates: category, type, tag, path, imports, coverage, and more. */
1583
2574
 
2575
+ /** @description Optional extras for {@link filterGraph}. */
2576
+ interface FilterGraphOptions {
2577
+ /** Path → owning-package-name lookup, from a flattened `WorkspaceGraph`. When supplied,
2578
+ * enables the `package:` query key and stamps each result node with its `package`. */
2579
+ packageOf?: Map<string, string> | undefined;
2580
+ }
1584
2581
  /**
1585
2582
  * @description Filters a serialized graph to only nodes matching all criteria in `query`,
1586
2583
  * then trims each node's import list to edges whose target is also in the result set.
1587
2584
  * Optionally sorts the result and applies a `limit`.
1588
2585
  * @param {SerializedGraph} graph - The serialized graph to filter.
1589
2586
  * @param {NodeQuery} query - Filter criteria; omitted fields are treated as wildcards.
2587
+ * @param {FilterGraphOptions} [options] - Optional `packageOf` lookup for workspace-scoped queries.
1590
2588
  * @returns {SerializedGraph} A new `SerializedGraph` containing only the matching subgraph.
1591
2589
  */
1592
- declare function filterGraph(graph: SerializedGraph, query: NodeQuery): SerializedGraph;
2590
+ declare function filterGraph(graph: SerializedGraph, query: NodeQuery, options?: FilterGraphOptions): SerializedGraph;
1593
2591
 
1594
2592
  /** Parses a key:value query string into a structured NodeQuery for use with filterGraph. */
1595
2593
 
1596
2594
  declare function parseQuery(queryString: string): NodeQuery;
1597
2595
 
2596
+ /**
2597
+ * Shared definition of a **test-selection tag** — a tag name that is a plausible
2598
+ * `vitest --grep` / native-framework-tag term for "given this source change, which tests run?".
2599
+ *
2600
+ * Graph tags are produced generously (every top-level declaration name, every imported symbol,
2601
+ * every `@word` in a test title, every third-party package). Most of those are noise for test
2602
+ * selection: a `library` tag like `vitest` matches every test under grep, a `function` tag is
2603
+ * the name of a helper in the test file itself, `test`/`barrel` are on every node of their
2604
+ * kind. This module is the one place that decides which tags survive, reused by
2605
+ * `propose_tags` (`src/tags/proposer.ts`), `apply_tags` (`src/tags/applier.ts`),
2606
+ * `list_tags` (`src/graph/tag-inventory.ts`) and the `tag:` query filter
2607
+ * (`src/query/matchers.ts`).
2608
+ *
2609
+ * The blocklist is configured from `mokosh.config` via {@link configureTagQuality}, called by
2610
+ * `applyConfig` — the same reset-then-apply lifecycle as the classify registries.
2611
+ */
2612
+
2613
+ /** Tag kinds that can denote a test-selection label. `function` / `variable` are declaration
2614
+ * names (a helper in the test file, or a symbol on a source file — neither is a grep term);
2615
+ * `library` is a third-party package name (present on nearly every test). `class` / `type`
2616
+ * are never produced. */
2617
+ declare const SELECTION_TAG_KINDS: readonly TagKind[];
2618
+ /** Tag names that are just a file's `category` echoed as a tag — present on every node of that
2619
+ * category, so they have zero selectivity. */
2620
+ declare const CATEGORY_MARKER_TAGS: ReadonlySet<string>;
2621
+ /** A tag name must be a bare identifier: no `@`, `:`, `/`, spaces. (Moved here from
2622
+ * `src/tags/applier.ts`.) */
2623
+ declare const VALID_TAG_NAME_RE: RegExp;
2624
+ /**
2625
+ * Curated generic names that appear in almost every codebase and carry no domain signal, so
2626
+ * they never make a useful test-selection tag. Deliberately **conservative** — real subsystem
2627
+ * names (`graph`, `parser`, `resolver`, `cache`, …) are left queryable; a project narrows
2628
+ * further with `mokosh.config` `tags.blocklist`. Mirrors the `rename-singles` denylist
2629
+ * philosophy: small, evidence-driven, extended only when a name is shown to be noise.
2630
+ *
2631
+ * Seeded from `applier.ts`'s former `GENERIC_TAG_BLOCKLIST` plus the generic offenders seen
2632
+ * dominating `list_tags` on real repos (structural filenames + common code words).
2633
+ */
2634
+ declare const DEFAULT_TAG_BLOCKLIST: ReadonlySet<string>;
2635
+ /** User overrides from `mokosh.config` `tags`. */
2636
+ interface TagQualityConfig {
2637
+ /** Names added to {@link DEFAULT_TAG_BLOCKLIST} (case-insensitive). */
2638
+ blocklist?: string[];
2639
+ /** Names removed from the effective blocklist — kept even if built-in or user-blocked. */
2640
+ allowlist?: string[];
2641
+ }
2642
+ /**
2643
+ * @description Recomputes the process-wide effective tag blocklist:
2644
+ * `(DEFAULT_TAG_BLOCKLIST ∪ cfg.blocklist) \ cfg.allowlist`, all lower-cased. Called by
2645
+ * `applyConfig` after `resetTagQuality`, so each analyzed root fully replaces the previous
2646
+ * config rather than accumulating.
2647
+ * @param {TagQualityConfig} [cfg] - The `tags` section of the loaded `mokosh.config`.
2648
+ */
2649
+ declare function configureTagQuality(cfg?: TagQualityConfig): void;
2650
+ /** @description Restores the built-in blocklist, dropping any prior {@link configureTagQuality}. */
2651
+ declare function resetTagQuality(): void;
2652
+ /**
2653
+ * @description The kind-independent half of {@link isSelectionTag}: a bare identifier, not a
2654
+ * `category` echo (`test`/`barrel`), and not blocklisted. Use when you already have just a
2655
+ * tag name (e.g. the `list_tags` inventory aggregates names across kinds).
2656
+ * @param {string} name - The tag name to test.
2657
+ * @returns {boolean} `true` when the name is not noise.
2658
+ */
2659
+ declare function isSelectionTagName(name: string): boolean;
2660
+ /**
2661
+ * @description Whether a tag is usable as a test-selection label: a selection kind, plus
2662
+ * {@link isSelectionTagName}.
2663
+ * @param {StructuredTag} tag - The structured tag to test.
2664
+ * @returns {boolean} `true` when the tag should be surfaced for test selection.
2665
+ */
2666
+ declare function isSelectionTag(tag: StructuredTag): boolean;
2667
+ /**
2668
+ * @description Distinct names of the selection-quality tags on a node, in first-seen order.
2669
+ * @param {readonly StructuredTag[]} tags - A node's `tags` array.
2670
+ * @returns {string[]} Deduplicated selection tag names.
2671
+ */
2672
+ declare function selectionTagNames(tags: readonly StructuredTag[]): string[];
2673
+
1598
2674
  /**
1599
2675
  * @description Result for a single file processed by {@link applyTagsToFile}.
1600
2676
  */
@@ -1620,10 +2696,12 @@ interface ApplyTagsResult {
1620
2696
  files: ApplyTagsFileResult[];
1621
2697
  }
1622
2698
  /**
1623
- * @description Iterates every test node in the graph, extracts `"import"` kind tags that pass
1624
- * a name validity check and generic-name blocklist, then delegates writing to the strategy
1625
- * selected by `mokosh.config.*` (`tagApplier.framework`, default `"vitest"`). Non-test nodes
1626
- * are skipped.
2699
+ * @description Iterates every test node in the graph, keeps its selection-quality tags
2700
+ * (see {@link isSelectionTag} `import` filename/symbol tags and deliberate `comment-marker`
2701
+ * markers, minus declaration/library names, `test`/`barrel`, and blocklisted generics), then
2702
+ * delegates writing to the strategy selected by `mokosh.config.*` (`tagApplier.framework`,
2703
+ * default `"vitest"`). Non-test nodes are skipped. Assumes `applyConfig` has already run for
2704
+ * this root so any `tags.blocklist` / `tags.allowlist` is active.
1627
2705
  * @param {Graph} graph - The fully-enriched dependency graph.
1628
2706
  * @param {string} rootDir - Absolute path to the project root.
1629
2707
  * @param {{ dryRun: boolean }} options - Pass `dryRun: true` to preview changes without disk writes.
@@ -1661,9 +2739,10 @@ interface ProposeTagsOptions {
1661
2739
  * @description Proposes Vitest tags to run based on which files changed.
1662
2740
  *
1663
2741
  * Traverses the incoming dependency graph from each changed file. Test nodes
1664
- * that can reach the changed file contribute their tags. Feature hubs act as
1665
- * boundaries: the hub's tag is emitted and traversal stops there, preventing
1666
- * combinatorial blowup in large graphs.
2742
+ * that can reach the changed file contribute their **selection-quality** tags
2743
+ * (see {@link selectionTagNames} drops declaration/library names, `test`/`barrel`,
2744
+ * and blocklisted generics). Feature hubs act as boundaries: the hub's tag is
2745
+ * emitted and traversal stops there, preventing combinatorial blowup in large graphs.
1667
2746
  * @param {Graph} graph - The full project dependency graph.
1668
2747
  * @param {string[]} changedFiles - Relative paths of files that were modified (e.g. from git diff).
1669
2748
  * @param {ProposeTagsOptions} [options] - Optional: custom test identifier and feature-detection settings.
@@ -1705,20 +2784,57 @@ declare function createImportMap(rootDir: string, entryPoints: string[], previou
1705
2784
  parallelParsing?: ParallelParsingOption | undefined;
1706
2785
  pathAliases?: Record<string, string[]> | undefined;
1707
2786
  additionalIgnoreDirs?: string[] | undefined;
2787
+ docFiles?: string[] | null | undefined;
1708
2788
  }): Promise<Graph>;
2789
+ /**
2790
+ * @description Computes a digest of every source file under `rootDir` (path + mtime + size),
2791
+ * used to decide whether a disk-persisted workspace graph is still valid. Changing, adding, or
2792
+ * removing any file changes the digest.
2793
+ * @param rootDir - Absolute monorepo root.
2794
+ * @returns A hex sha-256 digest, and the relative file list it was computed from (reused by the
2795
+ * caller so the directory tree is walked only once).
2796
+ */
2797
+ declare function computeWorkspaceSourceDigest(rootDir: string): {
2798
+ digest: string;
2799
+ files: string[];
2800
+ };
2801
+ /**
2802
+ * @description Buckets the monorepo's source files by owning package and digests each bucket
2803
+ * independently — plus a `rootDigest` over files owned by no package (top-level configs,
2804
+ * lockfiles, root docs). Lets the workspace disk cache (`src/graph/workspace/disk-cache.ts`)
2805
+ * tell which individual packages are still current, instead of one whole-tree digest that any
2806
+ * single edit invalidates.
2807
+ * @param rootDir - Absolute monorepo root.
2808
+ * @param packages - Packages to bucket into; each needs `name` and `relativeRoot`.
2809
+ * @param files - A pre-computed `getAllProjectFiles(rootDir)` result (e.g. from
2810
+ * `computeWorkspaceSourceDigest`), reused so the tree is walked only once.
2811
+ * @returns `rootDigest` and a `packageDigests` map keyed by package name (every package in
2812
+ * `packages` gets an entry, even if it owns no files).
2813
+ */
2814
+ declare function computeWorkspacePackageDigests(rootDir: string, packages: ReadonlyArray<Pick<WorkspacePackage, "name" | "relativeRoot">>, files: string[]): {
2815
+ rootDigest: string;
2816
+ packageDigests: Map<string, string>;
2817
+ };
1709
2818
  /**
1710
2819
  * @description Auto-detects the monorepo layout under `rootDir` and builds a per-package
1711
2820
  * dependency graph, stitching them together into a single WorkspaceGraph.
1712
2821
  * @param rootDir - Absolute path to the monorepo root.
1713
- * @param options - `packages` filters to a named subset of packages; `silent` suppresses progress; `gitStats` attaches git churn data per file; `parallelParsing` controls worker-pool offloading of file parsing per package (see {@link ParallelParsingOption}); `pathAliases` overrides/extends tsconfig path-alias resolution for every package (see `MokoshConfig.pathAliases`).
2822
+ * @param options - `packages` filters to a named subset of packages; `silent` suppresses progress; `gitStats` attaches git churn data per file; `parallelParsing` controls worker-pool offloading of file parsing per package (see {@link ParallelParsingOption}); `pathAliases` overrides/extends tsconfig path-alias resolution for every package (see `MokoshConfig.pathAliases`); `layout` supplies a pre-computed `detectMonorepo` result so detection is not repeated by callers that already ran it.
1714
2823
  * @returns A WorkspaceGraph where each package has its own Graph and cross-package edges are resolved.
1715
2824
  */
1716
2825
  declare function createWorkspaceGraph(rootDir: string, options?: {
1717
- packages?: string[];
2826
+ packages?: string[] | undefined;
1718
2827
  silent?: boolean;
1719
2828
  gitStats?: boolean;
1720
2829
  parallelParsing?: ParallelParsingOption | undefined;
1721
2830
  pathAliases?: Record<string, string[]> | undefined;
2831
+ additionalIgnoreDirs?: string[] | undefined;
2832
+ layout?: MonorepoLayout | undefined;
2833
+ /** Pre-computed `getAllProjectFiles(rootDir)` result, so the caller's digest walk isn't repeated. */
2834
+ projectFiles?: string[] | undefined;
2835
+ /** A previously built workspace graph; each package reuses its prior graph as an incremental
2836
+ * base so unchanged files are not re-parsed (mtime+size match). */
2837
+ previousWorkspace?: WorkspaceGraph | undefined;
1722
2838
  }): Promise<WorkspaceGraph>;
1723
2839
  /**
1724
2840
  * @description Recursively walks `rootDir` and returns paths of every file whose extension
@@ -1729,4 +2845,4 @@ declare function createWorkspaceGraph(rootDir: string, options?: {
1729
2845
  */
1730
2846
  declare function getAllProjectFiles(rootDir: string, options?: ScanOptions): string[];
1731
2847
 
1732
- export { type ApiSurface, type ApplyTagsFileResult, type ApplyTagsResult, type BranchComparison, type BranchComparisonSummary, type BuildGraphAtRefOptions, CALL_EDGE_TYPES, type CachedFileTokens, CallEdge, type CalleeEntry, type CallerEntry$1 as CallerEntry, type ChangeImpactCache, type CompareBranchesOptions, type ComplexFunctionEntry, type ComplexityDelta, type CoverageDelta, DEFAULT_BRANCH_GRAPH_CACHE_DIR, DEFAULT_CACHE_DIR, DEFAULT_DUPLICATION_TOKEN_CACHE_FILE, DEFAULT_EXTENSIONS, DEFAULT_GRAPH_CACHE_FILE, DEFAULT_IGNORE_DIRS, type DependencyGraph, type DocDriftDelta, type DuplicateFamily, type DuplicateGroup, type DuplicateOccurrence, type DuplicationDelta, type DuplicationTokenCache, EXPORT_TRACKING_TYPES, type ExportKind, type FeatureDetectionOptions, type FeatureDomain, type FeatureGraph, type FeatureGraphOptions, type FeatureInfo, type FileDiff, FileNode, FileType, type FindComplexFunctionsOptions, type FindDuplicatesOptions, type FindDuplicatesResult, type FindRiskHotspotsOptions, type FunctionCallInfo, type GetAffectedOptions, type GetCallersOptions, Graph, type CallerEntry as GraphCallerEntry, type GraphExporter, IMPORT_SYMBOL_TYPES, ImportEdge, type LanguageCoverage, MermaidExporter, type ModuleResponsibility, type ModuleRole, type MokoshConfig, type MonorepoDetector, type MonorepoLayout, NodeCategory, type NodeMeta, type NodeQuery, type ParallelParsingOption, type PathWithSymbols, type ProposeTagsOptions, type PublicExport, type ResponsibilityGraph, type RiskHotspotEntry, type RiskHotspotsResult, type ScanOptions, type SerializedGraph, type SerializedWorkspaceGraph, type SlimNode, type SlimSerializedGraph, type StaleReference, StructuredTag, type SummarizeOptions, type SymbolCaller, type SymbolImporter, type SymbolMatch, type SymbolPrecision, SymbolTraversalContext, type TestNodeIdentifier, type TraversalOptions, type TraversalVisitor, type TypeEdge, type TypeGraph, type TypeKind, type TypeNode, type TypeQueryResult, WorkspaceGraph, type WorkspacePackage, type WorkspacePackageSummary, type WorkspacePackagesSummary, applyConfig, applyTags, buildApiSurface, buildChangeImpactCache, buildFeatureGraph, buildGraphAtRef, buildResponsibilityGraph, buildTypeGraph, compareBranches, computeGraphHash, configToGraphOptions, createImportMap, createWorkspaceGraph, detectAllEntryPoints, detectEntryPoint, detectFeatures, detectMonorepo, filterGraph, findComplexFunctions, findDuplicates, findRiskHotspots, findSymbol, getAffected, getAllProjectFiles, getCallers, getDependencies, getDependents, getLanguageCoverage, getNodeMeta, hasChurnData, hasCoverageData, isChangeImpactCacheValid, loadChangeImpactCache, loadCoverageMap, loadMokoshConfig, loadTokenCacheFromDisk, parseQuery, proposeAffectedTests, proposeTags, queryCallGraph, queryChangeImpact, queryTypeGraph, registerConfigMatcher, registerMonorepoDetector, registerParser, registerTestLibrary, registerTestPattern, resetClassifyRegistries, saveChangeImpactCache, saveTokenCacheToDisk, slimSerialize, summarizeBranchComparison, summarizeWorkspacePackages, toMermaid };
2848
+ export { type ApiSurface, type ApiSurfaceSummary, type ApplyTagsFileResult, type ApplyTagsResult, type BranchComparison, type BranchComparisonSummary, type BuildGraphAtRefOptions, CALL_EDGE_TYPES, CATEGORY_MARKER_TAGS, type CachedDuplicationResult, type CachedFileTokens, CallEdge, type CalleeEntry, type CallerEntry$1 as CallerEntry, type ChangeImpactCache, type CompareBranchesOptions, type ComplexFunctionEntry, type ComplexityDelta, type CoverageDelta, type CycleEdgeKind, DEFAULT_BRANCH_GRAPH_CACHE_DIR, DEFAULT_CACHE_DIR, DEFAULT_DUPLICATION_RESULT_CACHE_FILE, DEFAULT_DUPLICATION_TOKEN_CACHE_FILE, DEFAULT_EXTENSIONS, DEFAULT_GRAPH_CACHE_FILE, DEFAULT_IGNORE_DIRS, DEFAULT_TAG_BLOCKLIST, DEFAULT_TAG_LIMIT, DEFAULT_TAG_MIN_COUNT, DEFAULT_WORKSPACE_CACHE_SUBDIR, DEFAULT_WORKSPACE_GRAPH_CACHE_FILE, type DependencyGraph, type DocDriftDelta, type DuplicateCluster, type DuplicateClusterFileCoverage, type DuplicateFamily, type DuplicateGroup, type DuplicateOccurrence, type DuplicateQuery, type DuplicateSignal, type DuplicatesSummary, type DuplicationDelta, type DuplicationResultParams, type DuplicationTokenCache, EXPORT_TRACKING_TYPES, type ExportKind, FUNCTION_COMPLEXITY_TYPES, type FeatureDetectionOptions, type FeatureDomain, type FeatureGraph, type FeatureGraphOptions, type FeatureInfo, type FidelityLevel, type FileDiff, FileNode, FileType, type FindComplexFunctionsOptions, type FindCyclesOptions, type FindDuplicatesOptions, type FindDuplicatesResult, type FindRiskHotspotsOptions, type FlatWorkspaceGraph, type FunctionCallInfo, type GetAffectedOptions, type GetCallersOptions, Graph, GraphAnalyzer, type CallerEntry as GraphCallerEntry, type GraphExporter, IMPORT_SYMBOL_TYPES, ImportEdge, LANGUAGE_FIDELITY, type LanguageCoverage, type LanguageFeature, type LanguageFidelity, MAX_PACKAGE_CACHE_BYTES, MermaidExporter, type ModuleResponsibility, type ModuleRole, type MokoshConfig, type MonorepoDetector, type MonorepoLayout, NodeCategory, type NodeMeta, type NodeQuery, type ParallelParsingOption, type PathWithSymbols, type ProposeTagsOptions, type PublicExport, type ResponsibilityGraph, type RiskHotspotEntry, type RiskHotspotsResult, SELECTION_TAG_KINDS, SUMMARY_TAG_KINDS, type ScanOptions, type SerializedGraph, type SerializedWorkspaceGraph, type SlimNode, type SlimSerializedGraph, type StaleReference, StructuredTag, type SummarizeOptions, type SymbolCaller, type SymbolImporter, type SymbolMatch, type SymbolPrecision, SymbolTraversalContext, TAG_RESPONSE_HARD_CAP, TEST_TAG_STRATEGY_TYPES, TYPE_GRAPH_TYPES, type TagAggregateEntry, type TagInventory, TagKind, type TagQualityConfig, type TagSummary, type TagSummaryOptions, type TestNodeIdentifier, type TraversalOptions, type TraversalVisitor, type TypeEdge, type TypeGraph, type TypeKind, type TypeNode, type TypeQueryResult, VALID_TAG_NAME_RE, WORKSPACE_CACHE_VERSION, WORKSPACE_MANIFEST_FILE, type WorkspaceAffectedSummary, WorkspaceGraph, type WorkspaceLayoutPackageSummary, type WorkspaceLayoutSummary, type WorkspacePackage, type WorkspacePackageSummary, type WorkspacePackagesSummary, applyConfig, applyDupQuery, applyTags, buildApiSurface, buildChangeImpactCache, buildFeatureGraph, buildGraphAtRef, buildResponsibilityGraph, buildTagInventory, buildTypeGraph, compareBranches, computeGraphHash, computeWorkspacePackageDigests, computeWorkspaceSourceDigest, configToGraphOptions, configureTagQuality, createImportMap, createWorkspaceGraph, dedupeGroupsAgainstClusters, detectAllEntryPoints, detectEntryPoint, detectFeatures, detectMonorepo, duplicationDigest, duplicationResultCacheKey, filterGraph, findComplexFunctions, findDuplicates, findRiskHotspots, findSymbol, getAffected, getAllProjectFiles, getCallers, getDependencies, getDependents, getLanguageCoverage, getNodeMeta, hasChurnData, hasCoverageData, hasGeneratedMarker, hasGitTimestampData, isChangeImpactCacheValid, isGeneratedPath, isSelectionTag, isSelectionTagName, languageCaveats, languageCaveatsSummary, languageSupportNote, loadChangeImpactCache, loadCoverageMap, loadDuplicationResult, loadMokoshConfig, loadTokenCacheFromDisk, matchDupGroup, packageOwnsFile, parseDupQuery, parseQuery, proposeAffectedTests, proposeTags, queryCallGraph, queryChangeImpact, queryTypeGraph, registerConfigMatcher, registerMonorepoDetector, registerParser, registerTestLibrary, registerTestPattern, resetClassifyRegistries, resetTagQuality, saveChangeImpactCache, saveDuplicationResult, saveTokenCacheToDisk, selectionTagNames, slimDupCluster, slimDupGroup, slimOccurrences, slimSerialize, sortLimitDupGroups, summarizeApiSurface, summarizeBranchComparison, summarizeDuplicates, summarizeTagInventory, summarizeWorkspaceLayout, summarizeWorkspacePackages, toMermaid, topDir };