@danielsimonjr/repo-tools 2.2.7 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +60 -2
  3. package/dist/cli.js +1981 -1143
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@danielsimonjr/repo-tools",
5
- version: "2.2.7",
5
+ version: "2.4.0",
6
6
  description: "One CLI for repository analysis: dependency graphs creation and query with per-export API facts, file chunking, and context compression. TypeScript on Bun, compiled to a single executable.",
7
7
  type: "module",
8
8
  license: "MIT",
@@ -55,10 +55,10 @@ var package_default = {
55
55
 
56
56
  // src/check/index.ts
57
57
  import { readdirSync as readdirSync2, readFileSync as readFileSync17 } from "node:fs";
58
- import { join as join20, resolve as resolve5 } from "node:path";
58
+ import { join as join21, resolve as resolve6 } from "node:path";
59
59
 
60
60
  // src/config.ts
61
- import { existsSync as existsSync2, readFileSync } from "node:fs";
61
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
62
62
  import { posix, resolve, win32 } from "node:path";
63
63
 
64
64
  // src/depgraph/api-surface.ts
@@ -662,13 +662,231 @@ function buildApiSurfaceReport(surface, files, stabilityTags) {
662
662
  };
663
663
  }
664
664
 
665
- // src/depgraph/exclude.ts
666
- import { statSync } from "node:fs";
665
+ // src/depgraph/duplicates.ts
666
+ import { readFileSync } from "node:fs";
667
667
  import { join as join2 } from "node:path";
668
668
 
669
- // src/depgraph/dirlist.ts
670
- import { existsSync, lstatSync, readdirSync } from "node:fs";
671
- import { join } from "node:path";
669
+ // src/mask.ts
670
+ var REGEX_KEYWORDS2 = new Set([
671
+ "return",
672
+ "typeof",
673
+ "instanceof",
674
+ "in",
675
+ "of",
676
+ "new",
677
+ "delete",
678
+ "void",
679
+ "throw",
680
+ "case",
681
+ "do",
682
+ "else",
683
+ "yield",
684
+ "await"
685
+ ]);
686
+ function regexCanStart(src, prev, afterStmtParen) {
687
+ if (prev < 0)
688
+ return true;
689
+ const c = src[prev] ?? "";
690
+ if (c === ")")
691
+ return afterStmtParen;
692
+ if (c === ">")
693
+ return src[prev - 1] === "=";
694
+ if ((c === "+" || c === "-") && src[prev - 1] === c)
695
+ return false;
696
+ if (/[([{},;:?=!&|+\-*%^~]/.test(c))
697
+ return true;
698
+ if (!/[\w$]/.test(c))
699
+ return false;
700
+ const word = /[\w$]+$/.exec(src.slice(Math.max(0, prev - 15), prev + 1))?.[0] ?? "";
701
+ return REGEX_KEYWORDS2.has(word) && !/[\w$.]/.test(src[prev - word.length] ?? "");
702
+ }
703
+ function regexBodyEnd(src, start2) {
704
+ let inClass = false;
705
+ for (let j = start2 + 1;j < src.length; j++) {
706
+ const c = src[j];
707
+ if (c === `
708
+ ` || c === "\r")
709
+ return -1;
710
+ if (c === "\\")
711
+ j += 1;
712
+ else if (c === "[")
713
+ inClass = true;
714
+ else if (c === "]")
715
+ inClass = false;
716
+ else if (c === "/" && !inClass)
717
+ return j;
718
+ }
719
+ return -1;
720
+ }
721
+ function scan(src) {
722
+ const out2 = [];
723
+ const push = (kind, start2, end) => {
724
+ if (end <= start2)
725
+ return;
726
+ const last = out2[out2.length - 1];
727
+ if (last && last.kind === kind && last.end === start2)
728
+ last.end = end;
729
+ else
730
+ out2.push({ kind, start: start2, end });
731
+ };
732
+ const templates = [];
733
+ let i2 = 0;
734
+ const n = src.length;
735
+ const readTemplateBody = () => {
736
+ const start2 = i2;
737
+ while (i2 < n) {
738
+ const c = src[i2];
739
+ if (c === "\\") {
740
+ i2 += 2;
741
+ continue;
742
+ }
743
+ if (c === "`") {
744
+ push("string", start2, i2);
745
+ push("code", i2, i2 + 1);
746
+ i2 += 1;
747
+ return;
748
+ }
749
+ if (c === "$" && src[i2 + 1] === "{") {
750
+ push("string", start2, i2);
751
+ push("code", i2, i2 + 2);
752
+ i2 += 2;
753
+ templates.push(0);
754
+ return;
755
+ }
756
+ i2 += 1;
757
+ }
758
+ push("string", start2, n);
759
+ };
760
+ let codeStart = 0;
761
+ let prev = -1;
762
+ const parenStmt = [];
763
+ let closedStmt = false;
764
+ const wordEndingAt = (index) => {
765
+ let end = index;
766
+ while (end > 0) {
767
+ const prev = src[end - 1] ?? "";
768
+ if (/\s/.test(prev)) {
769
+ end -= 1;
770
+ continue;
771
+ }
772
+ if (prev === "/" && src[end - 2] === "*") {
773
+ const open = src.lastIndexOf("/*", end - 2);
774
+ if (open === -1)
775
+ break;
776
+ end = open;
777
+ continue;
778
+ }
779
+ break;
780
+ }
781
+ return /[\w$]+$/.exec(src.slice(Math.max(0, end - 16), end))?.[0] ?? "";
782
+ };
783
+ while (i2 < n) {
784
+ const c = src[i2];
785
+ const next = src[i2 + 1];
786
+ if (c === "/" && next !== "/" && next !== "*" && regexCanStart(src, prev, closedStmt)) {
787
+ const close = regexBodyEnd(src, i2);
788
+ if (close !== -1) {
789
+ push("code", codeStart, i2 + 1);
790
+ push("string", i2 + 1, close);
791
+ i2 = close + 1;
792
+ while (i2 < n && /[a-z]/i.test(src[i2] ?? ""))
793
+ i2 += 1;
794
+ push("code", close, i2);
795
+ prev = i2 - 1;
796
+ closedStmt = false;
797
+ codeStart = i2;
798
+ continue;
799
+ }
800
+ }
801
+ if (c === "/" && next === "/") {
802
+ push("code", codeStart, i2);
803
+ const eol = src.indexOf(`
804
+ `, i2);
805
+ const end = eol === -1 ? n : eol;
806
+ push("comment", i2, end);
807
+ i2 = end;
808
+ codeStart = i2;
809
+ continue;
810
+ }
811
+ if (c === "/" && next === "*") {
812
+ push("code", codeStart, i2);
813
+ const close = src.indexOf("*/", i2 + 2);
814
+ const end = close === -1 ? n : close + 2;
815
+ push("comment", i2, end);
816
+ i2 = end;
817
+ codeStart = i2;
818
+ continue;
819
+ }
820
+ if (c === "'" || c === '"') {
821
+ push("code", codeStart, i2 + 1);
822
+ let j = i2 + 1;
823
+ while (j < n && src[j] !== c && src[j] !== `
824
+ `)
825
+ j += src[j] === "\\" ? 2 : 1;
826
+ const end = Math.min(j, n);
827
+ push("string", i2 + 1, end);
828
+ i2 = end < n && src[end] === c ? end + 1 : end;
829
+ push("code", end, i2);
830
+ prev = i2 - 1;
831
+ closedStmt = false;
832
+ codeStart = i2;
833
+ continue;
834
+ }
835
+ if (c === "`") {
836
+ push("code", codeStart, i2 + 1);
837
+ i2 += 1;
838
+ readTemplateBody();
839
+ prev = i2 - 1;
840
+ closedStmt = false;
841
+ codeStart = i2;
842
+ continue;
843
+ }
844
+ if (!/\s/.test(c ?? "")) {
845
+ if (c === "(") {
846
+ const word = wordEndingAt(i2);
847
+ parenStmt.push(word === "if" || word === "while" || word === "for" || word === "with");
848
+ closedStmt = false;
849
+ } else if (c === ")")
850
+ closedStmt = parenStmt.pop() === true;
851
+ else
852
+ closedStmt = false;
853
+ prev = i2;
854
+ }
855
+ if (templates.length > 0) {
856
+ const depth = templates.length - 1;
857
+ if (c === "{")
858
+ templates[depth] = (templates[depth] ?? 0) + 1;
859
+ else if (c === "}") {
860
+ if (templates[depth] === 0) {
861
+ templates.pop();
862
+ push("code", codeStart, i2 + 1);
863
+ i2 += 1;
864
+ readTemplateBody();
865
+ prev = i2 - 1;
866
+ closedStmt = false;
867
+ codeStart = i2;
868
+ continue;
869
+ }
870
+ templates[depth] = (templates[depth] ?? 1) - 1;
871
+ }
872
+ }
873
+ i2 += 1;
874
+ }
875
+ push("code", codeStart, n);
876
+ return out2;
877
+ }
878
+ function blank(text) {
879
+ return text.replace(/[^\r\n]/g, " ");
880
+ }
881
+ function blankCommentsAndStrings(src) {
882
+ return scan(src).map((s) => {
883
+ const text = src.slice(s.start, s.end);
884
+ return s.kind === "code" ? text : blank(text);
885
+ }).join("");
886
+ }
887
+ function stripComments(src) {
888
+ return scan(src).filter((s) => s.kind !== "comment").map((s) => src.slice(s.start, s.end)).join("");
889
+ }
672
890
 
673
891
  // src/sort.ts
674
892
  function compareCodeUnits(a, b) {
@@ -680,79 +898,17 @@ function sortCodeUnits(values) {
680
898
  return [...values].sort(compareCodeUnits);
681
899
  }
682
900
 
683
- // src/depgraph/dirlist.ts
684
- var readdirHook = {
685
- names: (dir) => readdirSync(dir),
686
- entries: (dir) => readdirSync(dir, { withFileTypes: true })
687
- };
688
- function listNames(dir) {
689
- return sortCodeUnits(readdirHook.names(dir));
901
+ // src/depgraph/paths.ts
902
+ import { join, relative } from "node:path";
903
+ var OUTPUT_SUBDIR = "docs/architecture";
904
+ function withoutTsExtension(path) {
905
+ return path.replace(/\.tsx?$/, "");
690
906
  }
691
- function listEntries(dir) {
692
- return [...readdirHook.entries(dir)].sort((a, b) => compareCodeUnits(a.name, b.name));
907
+ function toPosix(path) {
908
+ return path.replace(/\\/g, "/");
693
909
  }
694
- var linkLog = { skipped: new Set };
695
- function isLink(path) {
696
- let link;
697
- try {
698
- link = lstatSync(path).isSymbolicLink();
699
- } catch {
700
- return false;
701
- }
702
- if (link)
703
- linkLog.skipped.add(path);
704
- return link;
705
- }
706
- function isLinkEntry(dir, entry) {
707
- if (!entry.isSymbolicLink())
708
- return false;
709
- linkLog.skipped.add(join(dir, entry.name));
710
- return true;
711
- }
712
- function isWalkable(dir) {
713
- return existsSync(dir) && !isLink(dir);
714
- }
715
-
716
- // src/depgraph/exclude.ts
717
- var DEFAULT_EXCLUDE = [
718
- "node_modules",
719
- "dist",
720
- "build",
721
- "coverage",
722
- ".git"
723
- ];
724
- var TEST_DIR_NAMES = ["test", "tests"];
725
- function isSourceInput(name2) {
726
- return (name2.endsWith(".ts") || name2.endsWith(".tsx")) && !name2.endsWith(".d.ts") && !/\.(test|spec)\.tsx?$/.test(name2);
727
- }
728
- function sourceTsFilesUnder(dir, skip = new Set(DEFAULT_EXCLUDE), files = []) {
729
- if (!isWalkable(dir))
730
- return files;
731
- for (const entry of listNames(dir)) {
732
- if (skip.has(entry))
733
- continue;
734
- const fullPath = join2(dir, entry);
735
- if (isLink(fullPath))
736
- continue;
737
- if (statSync(fullPath).isDirectory())
738
- sourceTsFilesUnder(fullPath, skip, files);
739
- else if (isSourceInput(entry))
740
- files.push(fullPath);
741
- }
742
- return files;
743
- }
744
-
745
- // src/depgraph/paths.ts
746
- import { join as join3, relative } from "node:path";
747
- var OUTPUT_SUBDIR = "docs/architecture";
748
- function withoutTsExtension(path) {
749
- return path.replace(/\.tsx?$/, "");
750
- }
751
- function toPosix(path) {
752
- return path.replace(/\\/g, "/");
753
- }
754
- function isSrcIndex(path) {
755
- return path === "src/index.ts" || path.endsWith("/src/index.ts");
910
+ function isSrcIndex(path) {
911
+ return path === "src/index.ts" || path.endsWith("/src/index.ts");
756
912
  }
757
913
  function relativePosix(root, path) {
758
914
  return toPosix(relative(root, path));
@@ -761,864 +917,761 @@ function maskRoot(text, root) {
761
917
  return text.split(root).join("<root>").split(toPosix(root)).join("<root>");
762
918
  }
763
919
  function outputDirOf(root) {
764
- return join3(root, "docs", "architecture");
920
+ return join(root, "docs", "architecture");
765
921
  }
766
922
 
767
- // src/depgraph/reporters/banner.ts
768
- var VERIFICATION_MARKER = "<!-- repo-map:no-verification -->";
769
- var DEFAULT_REGENERATE_COMMAND = "repo-tools depgraph";
770
- function bannerFor(options) {
771
- const command = options.command ?? DEFAULT_REGENERATE_COMMAND;
772
- const marker = options.marker === undefined ? VERIFICATION_MARKER : options.marker;
773
- const comment = `<!-- GENERATED FILE -- do not edit by hand.
774
- Regenerate with \`${command}\`. -->
775
-
776
- `;
777
- return marker === null ? comment : `${marker}
778
- ${comment}`;
923
+ // src/depgraph/duplicates.ts
924
+ var RUNTIME_DUP_CATEGORIES = [
925
+ { cat: "function", key: "functions" },
926
+ { cat: "constant", key: "constants" },
927
+ { cat: "class", key: "classes" }
928
+ ];
929
+ var TYPE_DUP_CATEGORIES = [
930
+ { cat: "interface", key: "interfaces" },
931
+ { cat: "type", key: "types" },
932
+ { cat: "enum", key: "enums" }
933
+ ];
934
+ var DEFAULT_DISPATCH_CALLS = ["mathTyped"];
935
+ function describeDispatchCalls(calls, code = false) {
936
+ const items = calls.map((call) => code ? `\`${call}(...)\`` : `${call}(...)`);
937
+ if (items.length <= 1)
938
+ return items.join("");
939
+ return `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`;
940
+ }
941
+ function duplicateSymbolsNote(dispatchCalls = DEFAULT_DISPATCH_CALLS) {
942
+ const dispatch = dispatchCalls.length === 0 ? "DISPATCH_VARIANT (never set: no dispatch call is configured), " : `DISPATCH_VARIANT (>=2 ${describeDispatchCalls(dispatchCalls)} registrations of the ` + "same public name — distinct dispatch surfaces, not copy-paste bodies), ";
943
+ return "This report groups names by OWN definition, not by call graph, then classifies each " + "flagged name (see DupEntryTag): TRUE_DUPLICATE (the actionable merge targets), " + dispatch + "ALIAS_DELEGATION (a const-alias forward to an imported symbol, not an independent body — " + "excluded once fewer than 2 real bodies remain), and ALLOWLISTED (matches " + "duplicate-allowlist.json: for example hot-path is* guards, mirrors in another language, " + "per-package VERSION strings). NOT detected: same-file typed-dispatch overload polymorphism " + "for different argument shapes within one registration — a human still triages " + "TRUE_DUPLICATE entries using the defining files + public flags before merging anything.";
944
+ }
945
+ var DUPLICATE_SYMBOLS_NOTE = duplicateSymbolsNote();
946
+ var DUP_ENTRY_TAG_SORT_ORDER = {
947
+ TRUE_DUPLICATE: 0,
948
+ DISPATCH_VARIANT: 1,
949
+ ALIAS_DELEGATION: 2,
950
+ ALLOWLISTED: 3
951
+ };
952
+ function loadDuplicateAllowlist(path) {
953
+ try {
954
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
955
+ return parsed.entries ?? [];
956
+ } catch {
957
+ return [];
958
+ }
779
959
  }
780
- function withBanner(body2, options = {}) {
781
- return bannerFor(options) + body2;
960
+ function globMatchSingle(pattern, value) {
961
+ if (pattern === "*")
962
+ return true;
963
+ if (pattern.endsWith("/**"))
964
+ return value.startsWith(pattern.slice(0, -2));
965
+ if (pattern.endsWith("*"))
966
+ return value.startsWith(pattern.slice(0, -1));
967
+ return value === pattern;
782
968
  }
783
-
784
- // src/config.ts
785
- var CONFIG_FILE = "repo-tools.config.json";
786
- var DEFAULT_API_ENTRY = "src/index.ts";
787
- var ABSOLUTE_PATH_TEXT = "holds an absolute path; pass a path relative to the root";
788
- function isAbsolutePath(path) {
789
- return posix.isAbsolute(path) || win32.isAbsolute(path);
969
+ function findAllowlistMatch(allowlist, name2, filePath) {
970
+ return allowlist.find((e) => e.names.some((n) => globMatchSingle(n, name2)) && e.filesGlob.some((f) => globMatchSingle(f, filePath)));
790
971
  }
791
- function resolveUnderRoot(root, path) {
792
- return resolve(root, path);
972
+ function escapeRegExpLiteral(s) {
973
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
793
974
  }
794
- var DEPGRAPH_KEYS = {
795
- src: "src",
796
- tests: "paths",
797
- out: "path",
798
- exclude: "names",
799
- alsoExclude: "names",
800
- strictOrphans: "boolean",
801
- duplicateAllowlist: "path",
802
- duplicateBaseline: "path",
803
- coveragePolicy: "path",
804
- regenerateCommand: "string",
805
- verificationMarker: "string-or-null",
806
- extensions: "paths"
807
- };
808
- var API_SURFACE_KEYS = {
809
- out: "path-or-null",
810
- entry: "path",
811
- stabilityTags: "names"
812
- };
813
- function isObject(value) {
814
- return typeof value === "object" && value !== null && !Array.isArray(value);
975
+ function isDispatchVariantBody(code, name2, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
976
+ if (dispatchCalls.length === 0)
977
+ return false;
978
+ const calls = dispatchCalls.map(escapeRegExpLiteral).join("|");
979
+ const re = new RegExp(`export\\s+const\\s+${escapeRegExpLiteral(name2)}\\s*(?::[^=]+)?=\\s*(?:${calls})\\s*(?:<[^>]*>)?\\s*\\(`);
980
+ return re.test(code);
815
981
  }
816
- function checkValue(key, kind, value, fail) {
817
- const text = (v, what) => {
818
- if (typeof v !== "string" || v === "")
819
- throw fail(`'${key}' must be ${what}`);
820
- return v;
821
- };
822
- const relPath = (v, what) => {
823
- const p = text(v, what);
824
- if (isAbsolutePath(p))
825
- throw fail(`'${key}' ${ABSOLUTE_PATH_TEXT}`);
826
- return p;
827
- };
828
- const list = (v, item, what) => {
829
- if (!Array.isArray(v))
830
- throw fail(`'${key}' must be ${what}`);
831
- return v.map(item);
832
- };
833
- switch (kind) {
834
- case "path":
835
- return relPath(value, "a non-empty relative path");
836
- case "path-or-null":
837
- return value === null ? null : relPath(value, "a non-empty relative path or null");
838
- case "paths":
839
- return list(value, (x) => relPath(x, "a list of non-empty relative paths"), "a list of non-empty relative paths");
840
- case "src":
841
- if (value === "auto")
842
- return value;
843
- return list(value, (x) => relPath(x, '"auto" or a list of non-empty relative paths'), '"auto" or a list of non-empty relative paths');
844
- case "names":
845
- return list(value, (x) => text(x, "a list of non-empty strings"), "a list of non-empty strings");
846
- case "boolean":
847
- if (typeof value !== "boolean")
848
- throw fail(`'${key}' must be true or false`);
849
- return value;
850
- case "string":
851
- return text(value, "a non-empty string");
852
- case "string-or-null":
853
- return value === null ? null : text(value, "a non-empty string or null");
982
+ function collectImportedLocalNames(code) {
983
+ const names = new Set;
984
+ const importRegex = /import\s+(?:type\s+)?(?:(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+))(?:\s*,\s*(?:\{([^}]+)\}|(\w+)))?)\s+from\s+['"][^'"]+['"]/g;
985
+ for (const m of code.matchAll(importRegex)) {
986
+ const named = m[1] || m[4] || "";
987
+ const def = m[2] || m[5] || "";
988
+ const ns = m[3] || "";
989
+ if (named) {
990
+ for (const item of named.split(",")) {
991
+ const trimmed = item.trim().replace(/^type\s+/, "");
992
+ if (!trimmed)
993
+ continue;
994
+ const parts2 = trimmed.split(/\s+as\s+/);
995
+ const local = (parts2[parts2.length - 1] ?? "").trim();
996
+ if (local)
997
+ names.add(local);
998
+ }
999
+ }
1000
+ if (def)
1001
+ names.add(def);
1002
+ if (ns)
1003
+ names.add(ns);
854
1004
  }
1005
+ return names;
855
1006
  }
856
- function checkSection(prefix, object, keys, fail) {
857
- const out2 = {};
858
- for (const [name2, value] of Object.entries(object)) {
859
- const kind = keys[name2];
860
- if (kind === undefined)
861
- throw fail(`unknown key '${prefix}${name2}'`);
862
- out2[name2] = checkValue(`${prefix}${name2}`, kind, value, fail);
863
- }
864
- return out2;
1007
+ function isAliasDelegationBody(code, name2, importedLocalNames) {
1008
+ const re = new RegExp(`export\\s+const\\s+${escapeRegExpLiteral(name2)}\\s*(?::[^=]+)?=\\s*([A-Za-z_$][\\w$]*)\\s*;`);
1009
+ const m = code.match(re);
1010
+ return !!m && importedLocalNames.has(m[1] ?? "");
865
1011
  }
866
- var QUERY_KEYS = {
867
- out: "path",
868
- nodeRuntimes: "names"
869
- };
870
- function parseConfigSections(parsed, label) {
871
- const fail = (message) => new Error(`config ${label}: ${message}`);
872
- if (!isObject(parsed))
873
- throw fail("the file must hold a JSON object");
874
- for (const key of Object.keys(parsed)) {
875
- if (key === "$schema")
876
- continue;
877
- if (key !== "depgraph" && key !== "map" && key !== "query") {
878
- throw fail(`unknown key '${key}'`);
1012
+ function classifyDefiner(file, name2, category, allowlist, readRaw, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
1013
+ const allowMatch = findAllowlistMatch(allowlist, name2, file.path);
1014
+ if (allowMatch)
1015
+ return { tag: "ALLOWLISTED", reason: allowMatch.reason };
1016
+ if (category === "constant") {
1017
+ const raw = readRaw(file.path);
1018
+ if (raw) {
1019
+ const code = stripComments(raw);
1020
+ if (isDispatchVariantBody(code, name2, dispatchCalls))
1021
+ return { tag: "DISPATCH_VARIANT" };
1022
+ if (isAliasDelegationBody(code, name2, collectImportedLocalNames(code))) {
1023
+ return { tag: "ALIAS_DELEGATION" };
1024
+ }
879
1025
  }
880
1026
  }
881
- if (parsed.map !== undefined && parsed.depgraph !== undefined) {
882
- throw fail("use the section 'map' or its old name 'depgraph', not both");
883
- }
884
- const sectionName = parsed.map !== undefined ? "map" : "depgraph";
885
- const sections = { depgraph: {}, query: {} };
886
- const section = parsed[sectionName];
887
- if (section !== undefined) {
888
- if (!isObject(section))
889
- throw fail(`'${sectionName}' must be an object`);
890
- const { apiSurface, ...rest } = section;
891
- sections.depgraph = checkSection(`${sectionName}.`, rest, DEPGRAPH_KEYS, fail);
892
- if (apiSurface !== undefined) {
893
- if (!isObject(apiSurface))
894
- throw fail(`'${sectionName}.apiSurface' must be an object`);
895
- sections.depgraph.apiSurface = checkSection(`${sectionName}.apiSurface.`, apiSurface, API_SURFACE_KEYS, fail);
1027
+ return { tag: "PLAIN" };
1028
+ }
1029
+ function collectOwnDefiners(files, categories) {
1030
+ const byName = new Map;
1031
+ for (const file of files) {
1032
+ const reExported = new Set(file.exports.reExported);
1033
+ for (const { cat, key } of categories) {
1034
+ for (const name2 of file.exports[key]) {
1035
+ if (reExported.has(name2))
1036
+ continue;
1037
+ if (key === "types" && file.exports.interfaces.includes(name2))
1038
+ continue;
1039
+ const list = byName.get(name2) ?? [];
1040
+ list.push({ file, category: cat });
1041
+ byName.set(name2, list);
1042
+ }
896
1043
  }
897
1044
  }
898
- const query = parsed.query;
899
- if (query !== undefined) {
900
- if (!isObject(query))
901
- throw fail("'query' must be an object");
902
- sections.query = checkSection("query.", query, QUERY_KEYS, fail);
903
- }
904
- return sections;
1045
+ return byName;
905
1046
  }
906
- function loadConfigSections(root, configPath) {
907
- if (configPath !== undefined && isAbsolutePath(configPath)) {
908
- throw new Error(`flag --config ${ABSOLUTE_PATH_TEXT}`);
909
- }
910
- const rel = configPath ?? CONFIG_FILE;
911
- const label = `<root>/${rel.replace(/\\/g, "/")}`;
912
- const path = resolveUnderRoot(root, rel);
913
- if (configPath === undefined && !existsSync2(path))
914
- return { depgraph: {}, query: {} };
915
- let text;
916
- try {
917
- text = readFileSync(path, "utf8");
918
- } catch {
919
- throw new Error(`config ${label}: the file cannot be read`);
920
- }
921
- let parsed;
922
- try {
923
- parsed = JSON.parse(text);
924
- } catch {
925
- throw new Error(`config ${label}: the file is not valid JSON`);
926
- }
927
- return parseConfigSections(parsed, label);
1047
+ function finalizeDuplicateEntry(name2, categories, definers, tag) {
1048
+ const publicDefiners = definers.filter((d) => d.public);
1049
+ const canonicalHint = publicDefiners.length === 1 ? publicDefiners[0]?.file ?? "" : publicDefiners.length > 1 ? "AMBIGUOUS" : "internal-only";
1050
+ return { name: name2, category: [...categories].sort().join("+"), definers, canonicalHint, tag };
928
1051
  }
929
- function loadConfigFile(root, configPath) {
930
- return loadConfigSections(root, configPath).depgraph;
1052
+ function buildDuplicateEntries(byName, publicSurface, allowlist, readRaw, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
1053
+ const isFilePublic = (file, name2) => publicSurface.publicWildcardFiles.has(file.path) || publicSurface.publicNamed.has(`${file.path}::${name2}`);
1054
+ const entries = [];
1055
+ for (const [name2, defs] of byName) {
1056
+ const byFile = new Map;
1057
+ for (const d of defs)
1058
+ if (!byFile.has(d.file.path))
1059
+ byFile.set(d.file.path, d);
1060
+ if (byFile.size < 2)
1061
+ continue;
1062
+ const categories = new Set;
1063
+ const definers = [];
1064
+ for (const { file, category } of byFile.values()) {
1065
+ categories.add(category);
1066
+ const { tag, reason } = classifyDefiner(file, name2, category, allowlist, readRaw, dispatchCalls);
1067
+ definers.push({
1068
+ file: file.path,
1069
+ package: file.packageName ?? "unknown",
1070
+ public: isFilePublic(file, name2),
1071
+ tag,
1072
+ ...reason ? { reason } : {}
1073
+ });
1074
+ }
1075
+ definers.sort((a, b) => compareCodeUnits(a.file, b.file));
1076
+ const nonAlias = definers.filter((d) => d.tag !== "ALIAS_DELEGATION");
1077
+ if (nonAlias.length < 2) {
1078
+ entries.push(finalizeDuplicateEntry(name2, categories, definers, "ALIAS_DELEGATION"));
1079
+ continue;
1080
+ }
1081
+ const nonAllowlisted = nonAlias.filter((d) => d.tag !== "ALLOWLISTED");
1082
+ let entryTag;
1083
+ if (nonAllowlisted.length < 2) {
1084
+ entryTag = "ALLOWLISTED";
1085
+ } else {
1086
+ const dispatchCount = nonAllowlisted.filter((d) => d.tag === "DISPATCH_VARIANT").length;
1087
+ entryTag = dispatchCount >= 2 ? "DISPATCH_VARIANT" : "TRUE_DUPLICATE";
1088
+ }
1089
+ entries.push(finalizeDuplicateEntry(name2, categories, definers, entryTag));
1090
+ }
1091
+ entries.sort((a, b) => DUP_ENTRY_TAG_SORT_ORDER[a.tag] - DUP_ENTRY_TAG_SORT_ORDER[b.tag] || b.definers.length - a.definers.length || compareCodeUnits(a.name, b.name));
1092
+ return entries;
931
1093
  }
932
- function mergeQueryConfig(cli, file) {
933
- return {
934
- out: cli.out ?? file.query.out ?? file.depgraph.out ?? OUTPUT_SUBDIR,
935
- nodeRuntimes: cli.nodeRuntimes ?? file.query.nodeRuntimes ?? []
1094
+ function tallyByTag(entries) {
1095
+ const tally = {
1096
+ TRUE_DUPLICATE: 0,
1097
+ DISPATCH_VARIANT: 0,
1098
+ ALIAS_DELEGATION: 0,
1099
+ ALLOWLISTED: 0
936
1100
  };
1101
+ for (const e of entries)
1102
+ tally[e.tag]++;
1103
+ return tally;
937
1104
  }
938
- var MAP_REGENERATE_COMMAND = "repo-tools map";
939
- function mergeMapConfig(cli, file) {
940
- const merged = mergeDepgraphConfig(cli, file);
941
- const inDocs = (name2) => `${OUTPUT_SUBDIR}/${name2}`;
1105
+ function detectDuplicateSymbols(files, publicSurface, root, allowlistPath = join2(outputDirOf(root), "duplicate-allowlist.json"), dispatchCalls = DEFAULT_DISPATCH_CALLS) {
1106
+ const allowlist = loadDuplicateAllowlist(allowlistPath);
1107
+ const cache = new Map;
1108
+ const readRaw = (relPath) => {
1109
+ let content = cache.get(relPath);
1110
+ if (content === undefined) {
1111
+ try {
1112
+ content = readFileSync(join2(root, relPath), "utf-8");
1113
+ } catch {
1114
+ content = "";
1115
+ }
1116
+ cache.set(relPath, content);
1117
+ }
1118
+ return content;
1119
+ };
942
1120
  return {
943
- ...merged,
944
- duplicateAllowlist: cli.duplicateAllowlist ?? file.duplicateAllowlist ?? inDocs("duplicate-allowlist.json"),
945
- duplicateBaseline: cli.duplicateBaseline ?? file.duplicateBaseline ?? inDocs("duplicate-baseline.json"),
946
- coveragePolicy: cli.coveragePolicy ?? file.coveragePolicy ?? inDocs("coverage-policy.json"),
947
- regenerateCommand: cli.regenerateCommand ?? file.regenerateCommand ?? MAP_REGENERATE_COMMAND
1121
+ runtime: buildDuplicateEntries(collectOwnDefiners(files, RUNTIME_DUP_CATEGORIES), publicSurface, allowlist, readRaw, dispatchCalls),
1122
+ types: buildDuplicateEntries(collectOwnDefiners(files, TYPE_DUP_CATEGORIES), publicSurface, allowlist, readRaw, dispatchCalls)
948
1123
  };
949
1124
  }
950
- function mergeDepgraphConfig(cli, file) {
951
- const out2 = cli.out ?? file.out ?? OUTPUT_SUBDIR;
952
- const inOut = (name2) => `${out2.replace(/[\\/]+$/, "")}/${name2}`;
1125
+ function buildDuplicateReport(dup, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
1126
+ const runtimeByTag = tallyByTag(dup.runtime);
1127
+ const typeByTag = tallyByTag(dup.types);
953
1128
  return {
954
- src: cli.src ?? file.src ?? "auto",
955
- tests: cli.tests ?? file.tests ?? [...TEST_DIR_NAMES],
956
- out: out2,
957
- exclude: cli.exclude ?? file.exclude ?? [...DEFAULT_EXCLUDE],
958
- alsoExclude: cli.alsoExclude ?? file.alsoExclude ?? [],
959
- strictOrphans: cli.strictOrphans ?? file.strictOrphans ?? false,
960
- duplicateAllowlist: cli.duplicateAllowlist ?? file.duplicateAllowlist ?? inOut("duplicate-allowlist.json"),
961
- duplicateBaseline: cli.duplicateBaseline ?? file.duplicateBaseline ?? inOut("duplicate-baseline.json"),
962
- coveragePolicy: cli.coveragePolicy ?? file.coveragePolicy ?? inOut("coverage-policy.json"),
963
- regenerateCommand: cli.regenerateCommand ?? file.regenerateCommand ?? DEFAULT_REGENERATE_COMMAND,
964
- verificationMarker: cli.verificationMarker !== undefined ? cli.verificationMarker : file.verificationMarker !== undefined ? file.verificationMarker : VERIFICATION_MARKER,
965
- apiSurface: {
966
- out: cli.apiSurface?.out !== undefined ? cli.apiSurface.out : file.apiSurface?.out !== undefined ? file.apiSurface.out : null,
967
- entry: cli.apiSurface?.entry ?? file.apiSurface?.entry ?? DEFAULT_API_ENTRY,
968
- stabilityTags: cli.apiSurface?.stabilityTags ?? file.apiSurface?.stabilityTags ?? [...DEFAULT_STABILITY_TAGS]
1129
+ note: duplicateSymbolsNote(dispatchCalls),
1130
+ summary: {
1131
+ runtimeDuplicates: runtimeByTag.TRUE_DUPLICATE,
1132
+ typeDuplicates: typeByTag.TRUE_DUPLICATE,
1133
+ runtimeByTag,
1134
+ typeByTag
969
1135
  },
970
- extensions: cli.extensions ?? file.extensions ?? []
1136
+ runtime: dup.runtime,
1137
+ types: dup.types
971
1138
  };
972
1139
  }
973
-
974
- // src/depgraph/args.ts
975
- var SCAN_SCOPE_FLAGS = {
976
- "--src": "the census finds the source files itself; the area of a file comes from its path",
977
- "--tests": "a test file is found by its name or by its test/ or tests/ folder",
978
- "--exclude": "the census reads the files that git tracks; list a file in .gitignore instead",
979
- "--also-exclude": "the census reads the files that git tracks; list a file in .gitignore instead",
980
- "--all": "the graph always holds every file; the disposition of each file shows reachability",
981
- "-a": "the graph always holds every file; the disposition of each file shows reachability",
982
- "--reachable-only": "the graph always holds every file; the disposition shows reachability",
983
- "--include-tests": "the test coverage reports are always written",
984
- "-t": "the test coverage reports are always written"
985
- };
986
- function scanScopeText(flag) {
987
- return `flag ${flag} has no effect in 2.0.0: ${SCAN_SCOPE_FLAGS[flag]}`;
1140
+ var DUPLICATE_BASELINE_NOTE = "The accepted TRUE_DUPLICATE names of each kind, with their defining files. " + "`repo-tools depgraph --check-duplicates` fails only on a TRUE_DUPLICATE name that this file " + "does not hold. After a reviewed change, write it again with " + "`repo-tools depgraph --write-duplicate-baseline`.";
1141
+ function trueDuplicateNames(entries) {
1142
+ const names = {};
1143
+ const found = entries.filter((e) => e.tag === "TRUE_DUPLICATE").sort((a, b) => compareCodeUnits(a.name, b.name));
1144
+ for (const e of found)
1145
+ names[e.name] = e.definers.map((d) => d.file).sort(compareCodeUnits);
1146
+ return names;
988
1147
  }
989
- var BOOLEAN_FLAGS = {
990
- "--all": "all",
991
- "-a": "all",
992
- "--include-tests": "includeTests",
993
- "-t": "includeTests",
994
- "--reachable-only": "reachableOnly",
995
- "--strict-orphans": "strictOrphans",
996
- "--strict-census": "strictCensus",
997
- "--check-census": "checkCensus",
998
- "--check-duplicates": "checkDuplicates",
999
- "--no-regen": "noRegen",
1000
- "--write-duplicate-baseline": "writeDuplicateBaseline",
1001
- "--no-extensions": "noExtensions",
1002
- "--help": "help",
1003
- "-h": "help"
1004
- };
1005
- var VALUE_FLAGS = {
1006
- "--root": (_o, value, setRoot) => setRoot(value),
1007
- "--config": (o, value) => {
1008
- o.config = relativePath("--config", value);
1009
- },
1010
- "--src": (o, value) => {
1011
- o.settings.src = value === "auto" ? "auto" : pathList("--src", value);
1012
- },
1013
- "--tests": (o, value) => {
1014
- o.settings.tests = pathList("--tests", value);
1015
- },
1016
- "--out": (o, value) => {
1017
- o.settings.out = relativePath("--out", value);
1018
- },
1019
- "--exclude": (o, value) => {
1020
- o.settings.exclude = list("--exclude", value);
1021
- },
1022
- "--also-exclude": (o, value) => {
1023
- o.settings.alsoExclude = list("--also-exclude", value);
1024
- },
1025
- "--api-surface": (o, value) => {
1026
- o.settings.apiSurface = { ...o.settings.apiSurface, out: relativePath("--api-surface", value) };
1027
- },
1028
- "--api-entry": (o, value) => {
1029
- o.settings.apiSurface = { ...o.settings.apiSurface, entry: relativePath("--api-entry", value) };
1030
- },
1031
- "--stability-tags": (o, value) => {
1032
- o.settings.apiSurface = {
1033
- ...o.settings.apiSurface,
1034
- stabilityTags: list("--stability-tags", value)
1035
- };
1148
+ function buildDuplicateBaseline(report) {
1149
+ return {
1150
+ note: DUPLICATE_BASELINE_NOTE,
1151
+ runtime: trueDuplicateNames(report.runtime),
1152
+ types: trueDuplicateNames(report.types)
1153
+ };
1154
+ }
1155
+ function findNewDuplicates(report, baseline) {
1156
+ const found = [];
1157
+ for (const kind of ["runtime", "types"]) {
1158
+ const held = baseline[kind];
1159
+ for (const [name2, files] of Object.entries(trueDuplicateNames(report[kind]))) {
1160
+ if (!Object.hasOwn(held, name2))
1161
+ found.push({ kind, name: name2, files });
1162
+ }
1036
1163
  }
1037
- };
1038
- function list(flag, value) {
1039
- const items = value.split(",");
1040
- if (items.some((item) => item === ""))
1041
- throw new Error(`flag ${flag} holds an empty item`);
1042
- return items;
1164
+ return found;
1043
1165
  }
1044
- function pathList(flag, value) {
1045
- return list(flag, value).map((item) => relativePath(flag, item));
1166
+ function isRecord(value) {
1167
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1046
1168
  }
1047
- function relativePath(flag, value) {
1048
- if (isAbsolutePath(value)) {
1049
- throw new Error(`flag ${flag} ${ABSOLUTE_PATH_TEXT}`);
1169
+ function asDuplicateEntries(parsed) {
1170
+ if (!isRecord(parsed) || !Array.isArray(parsed.runtime) || !Array.isArray(parsed.types)) {
1171
+ return null;
1050
1172
  }
1051
- return value;
1173
+ const valid = (e) => isRecord(e) && typeof e.name === "string" && typeof e.tag === "string" && Array.isArray(e.definers) && e.definers.every((d) => isRecord(d) && typeof d.file === "string");
1174
+ if (!parsed.runtime.every(valid) || !parsed.types.every(valid))
1175
+ return null;
1176
+ return parsed;
1052
1177
  }
1053
- function parseDepgraphArgs(argv, cwd, command) {
1054
- const options = {
1055
- root: cwd,
1056
- settings: {},
1057
- includeTests: false,
1058
- all: false,
1059
- reachableOnly: false,
1060
- strictOrphans: false,
1061
- strictCensus: false,
1062
- checkCensus: false,
1063
- checkDuplicates: false,
1064
- noRegen: false,
1065
- writeDuplicateBaseline: false,
1066
- noExtensions: false,
1067
- help: false,
1068
- ignoredFlags: []
1069
- };
1070
- let rootSet = false;
1071
- const setRoot = (value) => {
1072
- if (rootSet)
1073
- throw new Error("the root is set twice (use --root=<path> or one positional path)");
1074
- rootSet = true;
1075
- options.root = value;
1076
- };
1077
- for (const arg of argv) {
1078
- if (!arg.startsWith("-")) {
1079
- setRoot(arg);
1080
- continue;
1081
- }
1082
- const eq = arg.indexOf("=");
1083
- const name2 = eq === -1 ? arg : arg.slice(0, eq);
1084
- const value = eq === -1 ? undefined : arg.slice(eq + 1);
1085
- if (command !== undefined && name2 in SCAN_SCOPE_FLAGS) {
1086
- if (command === "map")
1087
- throw new Error(scanScopeText(name2));
1088
- const takesValue = name2 in VALUE_FLAGS;
1089
- if (!takesValue && value !== undefined)
1090
- throw new Error(`flag ${name2} takes no value`);
1091
- if (takesValue && (value === undefined || value === "")) {
1092
- throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
1093
- }
1094
- options.ignoredFlags.push(name2);
1095
- continue;
1096
- }
1097
- const booleanKey = BOOLEAN_FLAGS[name2];
1098
- if (booleanKey !== undefined) {
1099
- if (value !== undefined)
1100
- throw new Error(`flag ${name2} takes no value`);
1101
- options[booleanKey] = true;
1102
- continue;
1103
- }
1104
- const apply = VALUE_FLAGS[name2];
1105
- if (apply !== undefined) {
1106
- if (value === undefined || value === "") {
1107
- throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
1108
- }
1109
- apply(options, value, setRoot);
1110
- continue;
1111
- }
1112
- throw new Error(`unknown flag '${name2}' (see repo-tools ${command ?? "depgraph"} --help)`);
1113
- }
1114
- checkModes(options);
1115
- return options;
1116
- }
1117
- function checkModes(options) {
1118
- if (options.noRegen && !options.checkDuplicates) {
1119
- throw new Error("flag --no-regen applies with --check-duplicates only");
1120
- }
1121
- const modes = [
1122
- options.checkCensus && "--check-census",
1123
- options.checkDuplicates && "--check-duplicates",
1124
- options.writeDuplicateBaseline && "--write-duplicate-baseline"
1125
- ].filter((mode) => mode !== false);
1126
- if (modes.length > 1) {
1127
- throw new Error(`flags ${modes.join(" and ")} are two modes; use one of them in a run`);
1128
- }
1129
- }
1130
- function wantsHelp(argv) {
1131
- return argv.includes("--help") || argv.includes("-h");
1178
+ function asBaselineNames(parsed) {
1179
+ if (!isRecord(parsed))
1180
+ return null;
1181
+ const kind = (value) => value === undefined ? {} : isRecord(value) ? value : null;
1182
+ const runtime = kind(parsed.runtime);
1183
+ const types = kind(parsed.types);
1184
+ return runtime && types ? { runtime, types } : null;
1132
1185
  }
1133
1186
 
1134
- // src/map/command.ts
1135
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync15, statSync as statSync6 } from "node:fs";
1136
- import { resolve as resolve4 } from "node:path";
1137
-
1138
- // src/depgraph/duplicates.ts
1139
- import { readFileSync as readFileSync2 } from "node:fs";
1187
+ // src/depgraph/exclude.ts
1188
+ import { statSync } from "node:fs";
1140
1189
  import { join as join4 } from "node:path";
1141
1190
 
1142
- // src/mask.ts
1143
- var REGEX_KEYWORDS2 = new Set([
1144
- "return",
1145
- "typeof",
1146
- "instanceof",
1147
- "in",
1148
- "of",
1149
- "new",
1150
- "delete",
1151
- "void",
1152
- "throw",
1153
- "case",
1154
- "do",
1155
- "else",
1156
- "yield",
1157
- "await"
1158
- ]);
1159
- function regexCanStart(src, prev, afterStmtParen) {
1160
- if (prev < 0)
1161
- return true;
1162
- const c = src[prev] ?? "";
1163
- if (c === ")")
1164
- return afterStmtParen;
1165
- if (c === ">")
1166
- return src[prev - 1] === "=";
1167
- if ((c === "+" || c === "-") && src[prev - 1] === c)
1191
+ // src/depgraph/dirlist.ts
1192
+ import { existsSync, lstatSync, readdirSync } from "node:fs";
1193
+ import { join as join3 } from "node:path";
1194
+ var readdirHook = {
1195
+ names: (dir) => readdirSync(dir),
1196
+ entries: (dir) => readdirSync(dir, { withFileTypes: true })
1197
+ };
1198
+ function listNames(dir) {
1199
+ return sortCodeUnits(readdirHook.names(dir));
1200
+ }
1201
+ function listEntries(dir) {
1202
+ return [...readdirHook.entries(dir)].sort((a, b) => compareCodeUnits(a.name, b.name));
1203
+ }
1204
+ var linkLog = { skipped: new Set };
1205
+ function isLink(path) {
1206
+ let link;
1207
+ try {
1208
+ link = lstatSync(path).isSymbolicLink();
1209
+ } catch {
1168
1210
  return false;
1169
- if (/[([{},;:?=!&|+\-*%^~]/.test(c))
1170
- return true;
1171
- if (!/[\w$]/.test(c))
1211
+ }
1212
+ if (link)
1213
+ linkLog.skipped.add(path);
1214
+ return link;
1215
+ }
1216
+ function isLinkEntry(dir, entry) {
1217
+ if (!entry.isSymbolicLink())
1172
1218
  return false;
1173
- const word = /[\w$]+$/.exec(src.slice(Math.max(0, prev - 15), prev + 1))?.[0] ?? "";
1174
- return REGEX_KEYWORDS2.has(word) && !/[\w$.]/.test(src[prev - word.length] ?? "");
1219
+ linkLog.skipped.add(join3(dir, entry.name));
1220
+ return true;
1175
1221
  }
1176
- function regexBodyEnd(src, start2) {
1177
- let inClass = false;
1178
- for (let j = start2 + 1;j < src.length; j++) {
1179
- const c = src[j];
1180
- if (c === `
1181
- ` || c === "\r")
1182
- return -1;
1183
- if (c === "\\")
1184
- j += 1;
1185
- else if (c === "[")
1186
- inClass = true;
1187
- else if (c === "]")
1188
- inClass = false;
1189
- else if (c === "/" && !inClass)
1190
- return j;
1222
+ function isWalkable(dir) {
1223
+ return existsSync(dir) && !isLink(dir);
1224
+ }
1225
+
1226
+ // src/depgraph/exclude.ts
1227
+ var DEFAULT_EXCLUDE = [
1228
+ "node_modules",
1229
+ "dist",
1230
+ "build",
1231
+ "coverage",
1232
+ ".git"
1233
+ ];
1234
+ var TEST_DIR_NAMES = ["test", "tests"];
1235
+ function isSourceInput(name2) {
1236
+ return (name2.endsWith(".ts") || name2.endsWith(".tsx")) && !name2.endsWith(".d.ts") && !/\.(test|spec)\.tsx?$/.test(name2);
1237
+ }
1238
+ function sourceTsFilesUnder(dir, skip = new Set(DEFAULT_EXCLUDE), files = []) {
1239
+ if (!isWalkable(dir))
1240
+ return files;
1241
+ for (const entry of listNames(dir)) {
1242
+ if (skip.has(entry))
1243
+ continue;
1244
+ const fullPath = join4(dir, entry);
1245
+ if (isLink(fullPath))
1246
+ continue;
1247
+ if (statSync(fullPath).isDirectory())
1248
+ sourceTsFilesUnder(fullPath, skip, files);
1249
+ else if (isSourceInput(entry))
1250
+ files.push(fullPath);
1191
1251
  }
1192
- return -1;
1252
+ return files;
1193
1253
  }
1194
- function scan(src) {
1195
- const out2 = [];
1196
- const push = (kind, start2, end) => {
1197
- if (end <= start2)
1198
- return;
1199
- const last = out2[out2.length - 1];
1200
- if (last && last.kind === kind && last.end === start2)
1201
- last.end = end;
1202
- else
1203
- out2.push({ kind, start: start2, end });
1254
+
1255
+ // src/depgraph/reporters/banner.ts
1256
+ var VERIFICATION_MARKER = "<!-- repo-map:no-verification -->";
1257
+ var DEFAULT_REGENERATE_COMMAND = "repo-tools depgraph";
1258
+ function bannerFor(options) {
1259
+ const command = options.command ?? DEFAULT_REGENERATE_COMMAND;
1260
+ const marker = options.marker === undefined ? VERIFICATION_MARKER : options.marker;
1261
+ const comment = `<!-- GENERATED FILE -- do not edit by hand.
1262
+ Regenerate with \`${command}\`. -->
1263
+
1264
+ `;
1265
+ return marker === null ? comment : `${marker}
1266
+ ${comment}`;
1267
+ }
1268
+ function withBanner(body2, options = {}) {
1269
+ return bannerFor(options) + body2;
1270
+ }
1271
+
1272
+ // src/config.ts
1273
+ var CONFIG_FILE = "repo-tools.config.json";
1274
+ var DEFAULT_API_ENTRY = "src/index.ts";
1275
+ var ABSOLUTE_PATH_TEXT = "holds an absolute path; pass a path relative to the root";
1276
+ function isAbsolutePath(path) {
1277
+ return posix.isAbsolute(path) || win32.isAbsolute(path);
1278
+ }
1279
+ function resolveUnderRoot(root, path) {
1280
+ return resolve(root, path);
1281
+ }
1282
+ var DEPGRAPH_KEYS = {
1283
+ src: "src",
1284
+ tests: "paths",
1285
+ out: "path",
1286
+ exclude: "names",
1287
+ alsoExclude: "names",
1288
+ strictOrphans: "boolean",
1289
+ dispatchCalls: "calls",
1290
+ duplicateAllowlist: "path",
1291
+ duplicateBaseline: "path",
1292
+ coveragePolicy: "path",
1293
+ regenerateCommand: "string",
1294
+ verificationMarker: "string-or-null",
1295
+ extensions: "paths"
1296
+ };
1297
+ var API_SURFACE_KEYS = {
1298
+ out: "path-or-null",
1299
+ entry: "path",
1300
+ stabilityTags: "names"
1301
+ };
1302
+ var CALL_NAME = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
1303
+ function isObject(value) {
1304
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1305
+ }
1306
+ function checkValue(key, kind, value, fail) {
1307
+ const text = (v, what) => {
1308
+ if (typeof v !== "string" || v === "")
1309
+ throw fail(`'${key}' must be ${what}`);
1310
+ return v;
1204
1311
  };
1205
- const templates = [];
1206
- let i2 = 0;
1207
- const n = src.length;
1208
- const readTemplateBody = () => {
1209
- const start2 = i2;
1210
- while (i2 < n) {
1211
- const c = src[i2];
1212
- if (c === "\\") {
1213
- i2 += 2;
1214
- continue;
1215
- }
1216
- if (c === "`") {
1217
- push("string", start2, i2);
1218
- push("code", i2, i2 + 1);
1219
- i2 += 1;
1220
- return;
1221
- }
1222
- if (c === "$" && src[i2 + 1] === "{") {
1223
- push("string", start2, i2);
1224
- push("code", i2, i2 + 2);
1225
- i2 += 2;
1226
- templates.push(0);
1227
- return;
1228
- }
1229
- i2 += 1;
1230
- }
1231
- push("string", start2, n);
1312
+ const relPath = (v, what) => {
1313
+ const p = text(v, what);
1314
+ if (isAbsolutePath(p))
1315
+ throw fail(`'${key}' ${ABSOLUTE_PATH_TEXT}`);
1316
+ return p;
1232
1317
  };
1233
- let codeStart = 0;
1234
- let prev = -1;
1235
- const parenStmt = [];
1236
- let closedStmt = false;
1237
- const wordEndingAt = (index) => {
1238
- let end = index;
1239
- while (end > 0) {
1240
- const prev = src[end - 1] ?? "";
1241
- if (/\s/.test(prev)) {
1242
- end -= 1;
1243
- continue;
1244
- }
1245
- if (prev === "/" && src[end - 2] === "*") {
1246
- const open = src.lastIndexOf("/*", end - 2);
1247
- if (open === -1)
1248
- break;
1249
- end = open;
1250
- continue;
1251
- }
1252
- break;
1253
- }
1254
- return /[\w$]+$/.exec(src.slice(Math.max(0, end - 16), end))?.[0] ?? "";
1318
+ const list = (v, item, what) => {
1319
+ if (!Array.isArray(v))
1320
+ throw fail(`'${key}' must be ${what}`);
1321
+ return v.map(item);
1255
1322
  };
1256
- while (i2 < n) {
1257
- const c = src[i2];
1258
- const next = src[i2 + 1];
1259
- if (c === "/" && next !== "/" && next !== "*" && regexCanStart(src, prev, closedStmt)) {
1260
- const close = regexBodyEnd(src, i2);
1261
- if (close !== -1) {
1262
- push("code", codeStart, i2 + 1);
1263
- push("string", i2 + 1, close);
1264
- i2 = close + 1;
1265
- while (i2 < n && /[a-z]/i.test(src[i2] ?? ""))
1266
- i2 += 1;
1267
- push("code", close, i2);
1268
- prev = i2 - 1;
1269
- closedStmt = false;
1270
- codeStart = i2;
1271
- continue;
1272
- }
1273
- }
1274
- if (c === "/" && next === "/") {
1275
- push("code", codeStart, i2);
1276
- const eol = src.indexOf(`
1277
- `, i2);
1278
- const end = eol === -1 ? n : eol;
1279
- push("comment", i2, end);
1280
- i2 = end;
1281
- codeStart = i2;
1282
- continue;
1283
- }
1284
- if (c === "/" && next === "*") {
1285
- push("code", codeStart, i2);
1286
- const close = src.indexOf("*/", i2 + 2);
1287
- const end = close === -1 ? n : close + 2;
1288
- push("comment", i2, end);
1289
- i2 = end;
1290
- codeStart = i2;
1291
- continue;
1292
- }
1293
- if (c === "'" || c === '"') {
1294
- push("code", codeStart, i2 + 1);
1295
- let j = i2 + 1;
1296
- while (j < n && src[j] !== c && src[j] !== `
1297
- `)
1298
- j += src[j] === "\\" ? 2 : 1;
1299
- const end = Math.min(j, n);
1300
- push("string", i2 + 1, end);
1301
- i2 = end < n && src[end] === c ? end + 1 : end;
1302
- push("code", end, i2);
1303
- prev = i2 - 1;
1304
- closedStmt = false;
1305
- codeStart = i2;
1306
- continue;
1307
- }
1308
- if (c === "`") {
1309
- push("code", codeStart, i2 + 1);
1310
- i2 += 1;
1311
- readTemplateBody();
1312
- prev = i2 - 1;
1313
- closedStmt = false;
1314
- codeStart = i2;
1315
- continue;
1316
- }
1317
- if (!/\s/.test(c ?? "")) {
1318
- if (c === "(") {
1319
- const word = wordEndingAt(i2);
1320
- parenStmt.push(word === "if" || word === "while" || word === "for" || word === "with");
1321
- closedStmt = false;
1322
- } else if (c === ")")
1323
- closedStmt = parenStmt.pop() === true;
1324
- else
1325
- closedStmt = false;
1326
- prev = i2;
1327
- }
1328
- if (templates.length > 0) {
1329
- const depth = templates.length - 1;
1330
- if (c === "{")
1331
- templates[depth] = (templates[depth] ?? 0) + 1;
1332
- else if (c === "}") {
1333
- if (templates[depth] === 0) {
1334
- templates.pop();
1335
- push("code", codeStart, i2 + 1);
1336
- i2 += 1;
1337
- readTemplateBody();
1338
- prev = i2 - 1;
1339
- closedStmt = false;
1340
- codeStart = i2;
1341
- continue;
1323
+ switch (kind) {
1324
+ case "path":
1325
+ return relPath(value, "a non-empty relative path");
1326
+ case "path-or-null":
1327
+ return value === null ? null : relPath(value, "a non-empty relative path or null");
1328
+ case "paths":
1329
+ return list(value, (x) => relPath(x, "a list of non-empty relative paths"), "a list of non-empty relative paths");
1330
+ case "src":
1331
+ if (value === "auto")
1332
+ return value;
1333
+ return list(value, (x) => relPath(x, '"auto" or a list of non-empty relative paths'), '"auto" or a list of non-empty relative paths');
1334
+ case "names":
1335
+ return list(value, (x) => text(x, "a list of non-empty strings"), "a list of non-empty strings");
1336
+ case "calls":
1337
+ return list(value, (x) => {
1338
+ if (typeof x !== "string" || !CALL_NAME.test(x)) {
1339
+ throw fail(`'${key}' must be a list of call names (identifiers joined by dots)`);
1342
1340
  }
1343
- templates[depth] = (templates[depth] ?? 1) - 1;
1341
+ return x;
1342
+ }, "a list of call names (identifiers joined by dots)");
1343
+ case "boolean":
1344
+ if (typeof value !== "boolean")
1345
+ throw fail(`'${key}' must be true or false`);
1346
+ return value;
1347
+ case "string":
1348
+ return text(value, "a non-empty string");
1349
+ case "string-or-null":
1350
+ return value === null ? null : text(value, "a non-empty string or null");
1351
+ case "count":
1352
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
1353
+ throw fail(`'${key}' must be a whole number of 1 or more`);
1344
1354
  }
1345
- }
1346
- i2 += 1;
1355
+ return value;
1356
+ case "fraction":
1357
+ if (typeof value !== "number" || !(value >= 0 && value <= 1)) {
1358
+ throw fail(`'${key}' must be a number from 0 to 1`);
1359
+ }
1360
+ return value;
1347
1361
  }
1348
- push("code", codeStart, n);
1349
- return out2;
1350
1362
  }
1351
- function blank(text) {
1352
- return text.replace(/[^\r\n]/g, " ");
1363
+ function checkSection(prefix, object, keys, fail) {
1364
+ const out2 = {};
1365
+ for (const [name2, value] of Object.entries(object)) {
1366
+ const kind = keys[name2];
1367
+ if (kind === undefined)
1368
+ throw fail(`unknown key '${prefix}${name2}'`);
1369
+ out2[name2] = checkValue(`${prefix}${name2}`, kind, value, fail);
1370
+ }
1371
+ return out2;
1353
1372
  }
1354
- function blankCommentsAndStrings(src) {
1355
- return scan(src).map((s) => {
1356
- const text = src.slice(s.start, s.end);
1357
- return s.kind === "code" ? text : blank(text);
1358
- }).join("");
1373
+ var QUERY_KEYS = {
1374
+ out: "path",
1375
+ nodeRuntimes: "names"
1376
+ };
1377
+ var CLONES_KEYS = {
1378
+ src: "src",
1379
+ out: "path-or-null",
1380
+ minNodes: "count",
1381
+ shingleLength: "count",
1382
+ nearThreshold: "fraction",
1383
+ containmentThreshold: "fraction",
1384
+ containmentMinShingles: "count",
1385
+ rareShingleLimit: "count",
1386
+ minSharedShingles: "count",
1387
+ parseErrorList: "count"
1388
+ };
1389
+ function checkClonesFlag(label, key, value) {
1390
+ return checkValue(label, CLONES_KEYS[key], value, (message) => new Error(message));
1359
1391
  }
1360
- function stripComments(src) {
1361
- return scan(src).filter((s) => s.kind !== "comment").map((s) => src.slice(s.start, s.end)).join("");
1392
+ function parseConfigSections(parsed, label) {
1393
+ const fail = (message) => new Error(`config ${label}: ${message}`);
1394
+ if (!isObject(parsed))
1395
+ throw fail("the file must hold a JSON object");
1396
+ for (const key of Object.keys(parsed)) {
1397
+ if (key === "$schema")
1398
+ continue;
1399
+ if (key !== "depgraph" && key !== "map" && key !== "query" && key !== "clones") {
1400
+ throw fail(`unknown key '${key}'`);
1401
+ }
1402
+ }
1403
+ if (parsed.map !== undefined && parsed.depgraph !== undefined) {
1404
+ throw fail("use the section 'map' or its old name 'depgraph', not both");
1405
+ }
1406
+ const sectionName = parsed.map !== undefined ? "map" : "depgraph";
1407
+ const sections = { depgraph: {}, query: {}, clones: {} };
1408
+ const section = parsed[sectionName];
1409
+ if (section !== undefined) {
1410
+ if (!isObject(section))
1411
+ throw fail(`'${sectionName}' must be an object`);
1412
+ const { apiSurface, ...rest } = section;
1413
+ sections.depgraph = checkSection(`${sectionName}.`, rest, DEPGRAPH_KEYS, fail);
1414
+ if (apiSurface !== undefined) {
1415
+ if (!isObject(apiSurface))
1416
+ throw fail(`'${sectionName}.apiSurface' must be an object`);
1417
+ sections.depgraph.apiSurface = checkSection(`${sectionName}.apiSurface.`, apiSurface, API_SURFACE_KEYS, fail);
1418
+ }
1419
+ }
1420
+ const query = parsed.query;
1421
+ if (query !== undefined) {
1422
+ if (!isObject(query))
1423
+ throw fail("'query' must be an object");
1424
+ sections.query = checkSection("query.", query, QUERY_KEYS, fail);
1425
+ }
1426
+ const clones = parsed.clones;
1427
+ if (clones !== undefined) {
1428
+ if (!isObject(clones))
1429
+ throw fail("'clones' must be an object");
1430
+ sections.clones = checkSection("clones.", clones, CLONES_KEYS, fail);
1431
+ }
1432
+ return sections;
1362
1433
  }
1363
-
1364
- // src/depgraph/duplicates.ts
1365
- var RUNTIME_DUP_CATEGORIES = [
1366
- { cat: "function", key: "functions" },
1367
- { cat: "constant", key: "constants" },
1368
- { cat: "class", key: "classes" }
1369
- ];
1370
- var TYPE_DUP_CATEGORIES = [
1371
- { cat: "interface", key: "interfaces" },
1372
- { cat: "type", key: "types" },
1373
- { cat: "enum", key: "enums" }
1374
- ];
1375
- var DUPLICATE_SYMBOLS_NOTE = "This report groups names by OWN definition, not by call graph, then classifies each " + "flagged name (see DupEntryTag): TRUE_DUPLICATE (the actionable merge targets), " + "DISPATCH_VARIANT (>=2 mathTyped(...) registrations of the same public name — distinct " + "dispatch surfaces, not copy-paste bodies), " + "ALIAS_DELEGATION (a const-alias forward to an imported symbol, not an independent body — " + "excluded once fewer than 2 real bodies remain), and ALLOWLISTED (matches " + "duplicate-allowlist.json: for example hot-path is* guards, mirrors in another language, " + "per-package VERSION strings). NOT detected: same-file typed-dispatch overload polymorphism for " + "different argument shapes within one registration — a human still triages TRUE_DUPLICATE " + "entries using the defining files + public flags before merging anything.";
1376
- var DUP_ENTRY_TAG_SORT_ORDER = {
1377
- TRUE_DUPLICATE: 0,
1378
- DISPATCH_VARIANT: 1,
1379
- ALIAS_DELEGATION: 2,
1380
- ALLOWLISTED: 3
1381
- };
1382
- function loadDuplicateAllowlist(path) {
1434
+ function loadConfigSections(root, configPath) {
1435
+ if (configPath !== undefined && isAbsolutePath(configPath)) {
1436
+ throw new Error(`flag --config ${ABSOLUTE_PATH_TEXT}`);
1437
+ }
1438
+ const rel = configPath ?? CONFIG_FILE;
1439
+ const label = `<root>/${rel.replace(/\\/g, "/")}`;
1440
+ const path = resolveUnderRoot(root, rel);
1441
+ if (configPath === undefined && !existsSync2(path))
1442
+ return { depgraph: {}, query: {}, clones: {} };
1443
+ let text;
1444
+ try {
1445
+ text = readFileSync2(path, "utf8");
1446
+ } catch {
1447
+ throw new Error(`config ${label}: the file cannot be read`);
1448
+ }
1449
+ let parsed;
1383
1450
  try {
1384
- const parsed = JSON.parse(readFileSync2(path, "utf-8"));
1385
- return parsed.entries ?? [];
1451
+ parsed = JSON.parse(text);
1386
1452
  } catch {
1387
- return [];
1453
+ throw new Error(`config ${label}: the file is not valid JSON`);
1388
1454
  }
1455
+ return parseConfigSections(parsed, label);
1389
1456
  }
1390
- function globMatchSingle(pattern, value) {
1391
- if (pattern === "*")
1392
- return true;
1393
- if (pattern.endsWith("/**"))
1394
- return value.startsWith(pattern.slice(0, -2));
1395
- if (pattern.endsWith("*"))
1396
- return value.startsWith(pattern.slice(0, -1));
1397
- return value === pattern;
1398
- }
1399
- function findAllowlistMatch(allowlist, name2, filePath) {
1400
- return allowlist.find((e) => e.names.some((n) => globMatchSingle(n, name2)) && e.filesGlob.some((f) => globMatchSingle(f, filePath)));
1457
+ function loadConfigFile(root, configPath) {
1458
+ return loadConfigSections(root, configPath).depgraph;
1401
1459
  }
1402
- function escapeRegExpLiteral(s) {
1403
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1460
+ function mergeQueryConfig(cli, file) {
1461
+ return {
1462
+ out: cli.out ?? file.query.out ?? file.depgraph.out ?? OUTPUT_SUBDIR,
1463
+ nodeRuntimes: cli.nodeRuntimes ?? file.query.nodeRuntimes ?? []
1464
+ };
1404
1465
  }
1405
- function isDispatchVariantBody(code, name2) {
1406
- const re = new RegExp(`export\\s+const\\s+${escapeRegExpLiteral(name2)}\\s*(?::[^=]+)?=\\s*mathTyped\\s*(?:<[^>]*>)?\\s*\\(`);
1407
- return re.test(code);
1466
+ var MAP_REGENERATE_COMMAND = "repo-tools map";
1467
+ function mergeMapConfig(cli, file) {
1468
+ const merged = mergeDepgraphConfig(cli, file);
1469
+ const inDocs = (name2) => `${OUTPUT_SUBDIR}/${name2}`;
1470
+ return {
1471
+ ...merged,
1472
+ duplicateAllowlist: cli.duplicateAllowlist ?? file.duplicateAllowlist ?? inDocs("duplicate-allowlist.json"),
1473
+ duplicateBaseline: cli.duplicateBaseline ?? file.duplicateBaseline ?? inDocs("duplicate-baseline.json"),
1474
+ coveragePolicy: cli.coveragePolicy ?? file.coveragePolicy ?? inDocs("coverage-policy.json"),
1475
+ regenerateCommand: cli.regenerateCommand ?? file.regenerateCommand ?? MAP_REGENERATE_COMMAND
1476
+ };
1408
1477
  }
1409
- function collectImportedLocalNames(code) {
1410
- const names = new Set;
1411
- const importRegex = /import\s+(?:type\s+)?(?:(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+))(?:\s*,\s*(?:\{([^}]+)\}|(\w+)))?)\s+from\s+['"][^'"]+['"]/g;
1412
- for (const m of code.matchAll(importRegex)) {
1413
- const named = m[1] || m[4] || "";
1414
- const def = m[2] || m[5] || "";
1415
- const ns = m[3] || "";
1416
- if (named) {
1417
- for (const item of named.split(",")) {
1418
- const trimmed = item.trim().replace(/^type\s+/, "");
1419
- if (!trimmed)
1420
- continue;
1421
- const parts2 = trimmed.split(/\s+as\s+/);
1422
- const local = (parts2[parts2.length - 1] ?? "").trim();
1423
- if (local)
1424
- names.add(local);
1425
- }
1426
- }
1427
- if (def)
1428
- names.add(def);
1429
- if (ns)
1430
- names.add(ns);
1431
- }
1432
- return names;
1478
+ function mergeDepgraphConfig(cli, file) {
1479
+ const out2 = cli.out ?? file.out ?? OUTPUT_SUBDIR;
1480
+ const inOut = (name2) => `${out2.replace(/[\\/]+$/, "")}/${name2}`;
1481
+ return {
1482
+ src: cli.src ?? file.src ?? "auto",
1483
+ tests: cli.tests ?? file.tests ?? [...TEST_DIR_NAMES],
1484
+ out: out2,
1485
+ exclude: cli.exclude ?? file.exclude ?? [...DEFAULT_EXCLUDE],
1486
+ alsoExclude: cli.alsoExclude ?? file.alsoExclude ?? [],
1487
+ strictOrphans: cli.strictOrphans ?? file.strictOrphans ?? false,
1488
+ dispatchCalls: cli.dispatchCalls ?? file.dispatchCalls ?? [...DEFAULT_DISPATCH_CALLS],
1489
+ duplicateAllowlist: cli.duplicateAllowlist ?? file.duplicateAllowlist ?? inOut("duplicate-allowlist.json"),
1490
+ duplicateBaseline: cli.duplicateBaseline ?? file.duplicateBaseline ?? inOut("duplicate-baseline.json"),
1491
+ coveragePolicy: cli.coveragePolicy ?? file.coveragePolicy ?? inOut("coverage-policy.json"),
1492
+ regenerateCommand: cli.regenerateCommand ?? file.regenerateCommand ?? DEFAULT_REGENERATE_COMMAND,
1493
+ verificationMarker: cli.verificationMarker !== undefined ? cli.verificationMarker : file.verificationMarker !== undefined ? file.verificationMarker : VERIFICATION_MARKER,
1494
+ apiSurface: {
1495
+ out: cli.apiSurface?.out !== undefined ? cli.apiSurface.out : file.apiSurface?.out !== undefined ? file.apiSurface.out : null,
1496
+ entry: cli.apiSurface?.entry ?? file.apiSurface?.entry ?? DEFAULT_API_ENTRY,
1497
+ stabilityTags: cli.apiSurface?.stabilityTags ?? file.apiSurface?.stabilityTags ?? [...DEFAULT_STABILITY_TAGS]
1498
+ },
1499
+ extensions: cli.extensions ?? file.extensions ?? []
1500
+ };
1433
1501
  }
1434
- function isAliasDelegationBody(code, name2, importedLocalNames) {
1435
- const re = new RegExp(`export\\s+const\\s+${escapeRegExpLiteral(name2)}\\s*(?::[^=]+)?=\\s*([A-Za-z_$][\\w$]*)\\s*;`);
1436
- const m = code.match(re);
1437
- return !!m && importedLocalNames.has(m[1] ?? "");
1502
+
1503
+ // src/depgraph/args.ts
1504
+ var SCAN_SCOPE_FLAGS = {
1505
+ "--src": "the census finds the source files itself; the area of a file comes from its path",
1506
+ "--tests": "a test file is found by its name or by its test/ or tests/ folder",
1507
+ "--exclude": "the census reads the files that git tracks; list a file in .gitignore instead",
1508
+ "--also-exclude": "the census reads the files that git tracks; list a file in .gitignore instead",
1509
+ "--all": "the graph always holds every file; the disposition of each file shows reachability",
1510
+ "-a": "the graph always holds every file; the disposition of each file shows reachability",
1511
+ "--reachable-only": "the graph always holds every file; the disposition shows reachability",
1512
+ "--include-tests": "the test coverage reports are always written",
1513
+ "-t": "the test coverage reports are always written"
1514
+ };
1515
+ function scanScopeText(flag) {
1516
+ return `flag ${flag} has no effect in 2.0.0: ${SCAN_SCOPE_FLAGS[flag]}`;
1438
1517
  }
1439
- function classifyDefiner(file, name2, category, allowlist, readRaw) {
1440
- const allowMatch = findAllowlistMatch(allowlist, name2, file.path);
1441
- if (allowMatch)
1442
- return { tag: "ALLOWLISTED", reason: allowMatch.reason };
1443
- if (category === "constant") {
1444
- const raw = readRaw(file.path);
1445
- if (raw) {
1446
- const code = stripComments(raw);
1447
- if (isDispatchVariantBody(code, name2))
1448
- return { tag: "DISPATCH_VARIANT" };
1449
- if (isAliasDelegationBody(code, name2, collectImportedLocalNames(code))) {
1450
- return { tag: "ALIAS_DELEGATION" };
1451
- }
1452
- }
1518
+ var BOOLEAN_FLAGS = {
1519
+ "--all": "all",
1520
+ "-a": "all",
1521
+ "--include-tests": "includeTests",
1522
+ "-t": "includeTests",
1523
+ "--reachable-only": "reachableOnly",
1524
+ "--strict-orphans": "strictOrphans",
1525
+ "--strict-census": "strictCensus",
1526
+ "--check-census": "checkCensus",
1527
+ "--check-duplicates": "checkDuplicates",
1528
+ "--no-regen": "noRegen",
1529
+ "--write-duplicate-baseline": "writeDuplicateBaseline",
1530
+ "--no-extensions": "noExtensions",
1531
+ "--help": "help",
1532
+ "-h": "help"
1533
+ };
1534
+ var VALUE_FLAGS = {
1535
+ "--root": (_o, value, setRoot) => setRoot(value),
1536
+ "--config": (o, value) => {
1537
+ o.config = relativePath("--config", value);
1538
+ },
1539
+ "--src": (o, value) => {
1540
+ o.settings.src = value === "auto" ? "auto" : pathList("--src", value);
1541
+ },
1542
+ "--tests": (o, value) => {
1543
+ o.settings.tests = pathList("--tests", value);
1544
+ },
1545
+ "--out": (o, value) => {
1546
+ o.settings.out = relativePath("--out", value);
1547
+ },
1548
+ "--exclude": (o, value) => {
1549
+ o.settings.exclude = list("--exclude", value);
1550
+ },
1551
+ "--also-exclude": (o, value) => {
1552
+ o.settings.alsoExclude = list("--also-exclude", value);
1553
+ },
1554
+ "--api-surface": (o, value) => {
1555
+ o.settings.apiSurface = { ...o.settings.apiSurface, out: relativePath("--api-surface", value) };
1556
+ },
1557
+ "--api-entry": (o, value) => {
1558
+ o.settings.apiSurface = { ...o.settings.apiSurface, entry: relativePath("--api-entry", value) };
1559
+ },
1560
+ "--stability-tags": (o, value) => {
1561
+ o.settings.apiSurface = {
1562
+ ...o.settings.apiSurface,
1563
+ stabilityTags: list("--stability-tags", value)
1564
+ };
1453
1565
  }
1454
- return { tag: "PLAIN" };
1566
+ };
1567
+ function list(flag, value) {
1568
+ const items = value.split(",");
1569
+ if (items.some((item) => item === ""))
1570
+ throw new Error(`flag ${flag} holds an empty item`);
1571
+ return items;
1455
1572
  }
1456
- function collectOwnDefiners(files, categories) {
1457
- const byName = new Map;
1458
- for (const file of files) {
1459
- const reExported = new Set(file.exports.reExported);
1460
- for (const { cat, key } of categories) {
1461
- for (const name2 of file.exports[key]) {
1462
- if (reExported.has(name2))
1463
- continue;
1464
- if (key === "types" && file.exports.interfaces.includes(name2))
1465
- continue;
1466
- const list = byName.get(name2) ?? [];
1467
- list.push({ file, category: cat });
1468
- byName.set(name2, list);
1469
- }
1470
- }
1471
- }
1472
- return byName;
1573
+ function pathList(flag, value) {
1574
+ return list(flag, value).map((item) => relativePath(flag, item));
1473
1575
  }
1474
- function finalizeDuplicateEntry(name2, categories, definers, tag) {
1475
- const publicDefiners = definers.filter((d) => d.public);
1476
- const canonicalHint = publicDefiners.length === 1 ? publicDefiners[0]?.file ?? "" : publicDefiners.length > 1 ? "AMBIGUOUS" : "internal-only";
1477
- return { name: name2, category: [...categories].sort().join("+"), definers, canonicalHint, tag };
1576
+ function relativePath(flag, value) {
1577
+ if (isAbsolutePath(value)) {
1578
+ throw new Error(`flag ${flag} ${ABSOLUTE_PATH_TEXT}`);
1579
+ }
1580
+ return value;
1478
1581
  }
1479
- function buildDuplicateEntries(byName, publicSurface, allowlist, readRaw) {
1480
- const isFilePublic = (file, name2) => publicSurface.publicWildcardFiles.has(file.path) || publicSurface.publicNamed.has(`${file.path}::${name2}`);
1481
- const entries = [];
1482
- for (const [name2, defs] of byName) {
1483
- const byFile = new Map;
1484
- for (const d of defs)
1485
- if (!byFile.has(d.file.path))
1486
- byFile.set(d.file.path, d);
1487
- if (byFile.size < 2)
1582
+ function parseDepgraphArgs(argv, cwd, command) {
1583
+ const options = {
1584
+ root: cwd,
1585
+ settings: {},
1586
+ includeTests: false,
1587
+ all: false,
1588
+ reachableOnly: false,
1589
+ strictOrphans: false,
1590
+ strictCensus: false,
1591
+ checkCensus: false,
1592
+ checkDuplicates: false,
1593
+ noRegen: false,
1594
+ writeDuplicateBaseline: false,
1595
+ noExtensions: false,
1596
+ help: false,
1597
+ ignoredFlags: []
1598
+ };
1599
+ let rootSet = false;
1600
+ const setRoot = (value) => {
1601
+ if (rootSet)
1602
+ throw new Error("the root is set twice (use --root=<path> or one positional path)");
1603
+ rootSet = true;
1604
+ options.root = value;
1605
+ };
1606
+ for (const arg of argv) {
1607
+ if (!arg.startsWith("-")) {
1608
+ setRoot(arg);
1488
1609
  continue;
1489
- const categories = new Set;
1490
- const definers = [];
1491
- for (const { file, category } of byFile.values()) {
1492
- categories.add(category);
1493
- const { tag, reason } = classifyDefiner(file, name2, category, allowlist, readRaw);
1494
- definers.push({
1495
- file: file.path,
1496
- package: file.packageName ?? "unknown",
1497
- public: isFilePublic(file, name2),
1498
- tag,
1499
- ...reason ? { reason } : {}
1500
- });
1501
1610
  }
1502
- definers.sort((a, b) => compareCodeUnits(a.file, b.file));
1503
- const nonAlias = definers.filter((d) => d.tag !== "ALIAS_DELEGATION");
1504
- if (nonAlias.length < 2) {
1505
- entries.push(finalizeDuplicateEntry(name2, categories, definers, "ALIAS_DELEGATION"));
1611
+ const eq = arg.indexOf("=");
1612
+ const name2 = eq === -1 ? arg : arg.slice(0, eq);
1613
+ const value = eq === -1 ? undefined : arg.slice(eq + 1);
1614
+ if (command !== undefined && name2 in SCAN_SCOPE_FLAGS) {
1615
+ if (command === "map")
1616
+ throw new Error(scanScopeText(name2));
1617
+ const takesValue = name2 in VALUE_FLAGS;
1618
+ if (!takesValue && value !== undefined)
1619
+ throw new Error(`flag ${name2} takes no value`);
1620
+ if (takesValue && (value === undefined || value === "")) {
1621
+ throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
1622
+ }
1623
+ options.ignoredFlags.push(name2);
1506
1624
  continue;
1507
1625
  }
1508
- const nonAllowlisted = nonAlias.filter((d) => d.tag !== "ALLOWLISTED");
1509
- let entryTag;
1510
- if (nonAllowlisted.length < 2) {
1511
- entryTag = "ALLOWLISTED";
1512
- } else {
1513
- const dispatchCount = nonAllowlisted.filter((d) => d.tag === "DISPATCH_VARIANT").length;
1514
- entryTag = dispatchCount >= 2 ? "DISPATCH_VARIANT" : "TRUE_DUPLICATE";
1626
+ const booleanKey = BOOLEAN_FLAGS[name2];
1627
+ if (booleanKey !== undefined) {
1628
+ if (value !== undefined)
1629
+ throw new Error(`flag ${name2} takes no value`);
1630
+ options[booleanKey] = true;
1631
+ continue;
1515
1632
  }
1516
- entries.push(finalizeDuplicateEntry(name2, categories, definers, entryTag));
1517
- }
1518
- entries.sort((a, b) => DUP_ENTRY_TAG_SORT_ORDER[a.tag] - DUP_ENTRY_TAG_SORT_ORDER[b.tag] || b.definers.length - a.definers.length || compareCodeUnits(a.name, b.name));
1519
- return entries;
1520
- }
1521
- function tallyByTag(entries) {
1522
- const tally = {
1523
- TRUE_DUPLICATE: 0,
1524
- DISPATCH_VARIANT: 0,
1525
- ALIAS_DELEGATION: 0,
1526
- ALLOWLISTED: 0
1527
- };
1528
- for (const e of entries)
1529
- tally[e.tag]++;
1530
- return tally;
1531
- }
1532
- function detectDuplicateSymbols(files, publicSurface, root, allowlistPath = join4(outputDirOf(root), "duplicate-allowlist.json")) {
1533
- const allowlist = loadDuplicateAllowlist(allowlistPath);
1534
- const cache = new Map;
1535
- const readRaw = (relPath) => {
1536
- let content = cache.get(relPath);
1537
- if (content === undefined) {
1538
- try {
1539
- content = readFileSync2(join4(root, relPath), "utf-8");
1540
- } catch {
1541
- content = "";
1633
+ const apply = VALUE_FLAGS[name2];
1634
+ if (apply !== undefined) {
1635
+ if (value === undefined || value === "") {
1636
+ throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
1542
1637
  }
1543
- cache.set(relPath, content);
1544
- }
1545
- return content;
1546
- };
1547
- return {
1548
- runtime: buildDuplicateEntries(collectOwnDefiners(files, RUNTIME_DUP_CATEGORIES), publicSurface, allowlist, readRaw),
1549
- types: buildDuplicateEntries(collectOwnDefiners(files, TYPE_DUP_CATEGORIES), publicSurface, allowlist, readRaw)
1550
- };
1551
- }
1552
- function buildDuplicateReport(dup) {
1553
- const runtimeByTag = tallyByTag(dup.runtime);
1554
- const typeByTag = tallyByTag(dup.types);
1555
- return {
1556
- note: DUPLICATE_SYMBOLS_NOTE,
1557
- summary: {
1558
- runtimeDuplicates: runtimeByTag.TRUE_DUPLICATE,
1559
- typeDuplicates: typeByTag.TRUE_DUPLICATE,
1560
- runtimeByTag,
1561
- typeByTag
1562
- },
1563
- runtime: dup.runtime,
1564
- types: dup.types
1565
- };
1566
- }
1567
- var DUPLICATE_BASELINE_NOTE = "The accepted TRUE_DUPLICATE names of each kind, with their defining files. " + "`repo-tools depgraph --check-duplicates` fails only on a TRUE_DUPLICATE name that this file " + "does not hold. After a reviewed change, write it again with " + "`repo-tools depgraph --write-duplicate-baseline`.";
1568
- function trueDuplicateNames(entries) {
1569
- const names = {};
1570
- const found = entries.filter((e) => e.tag === "TRUE_DUPLICATE").sort((a, b) => compareCodeUnits(a.name, b.name));
1571
- for (const e of found)
1572
- names[e.name] = e.definers.map((d) => d.file).sort(compareCodeUnits);
1573
- return names;
1574
- }
1575
- function buildDuplicateBaseline(report) {
1576
- return {
1577
- note: DUPLICATE_BASELINE_NOTE,
1578
- runtime: trueDuplicateNames(report.runtime),
1579
- types: trueDuplicateNames(report.types)
1580
- };
1581
- }
1582
- function findNewDuplicates(report, baseline) {
1583
- const found = [];
1584
- for (const kind of ["runtime", "types"]) {
1585
- const held = baseline[kind];
1586
- for (const [name2, files] of Object.entries(trueDuplicateNames(report[kind]))) {
1587
- if (!Object.hasOwn(held, name2))
1588
- found.push({ kind, name: name2, files });
1638
+ apply(options, value, setRoot);
1639
+ continue;
1589
1640
  }
1641
+ throw new Error(`unknown flag '${name2}' (see repo-tools ${command ?? "depgraph"} --help)`);
1590
1642
  }
1591
- return found;
1592
- }
1593
- function isRecord(value) {
1594
- return typeof value === "object" && value !== null && !Array.isArray(value);
1643
+ checkModes(options);
1644
+ return options;
1595
1645
  }
1596
- function asDuplicateEntries(parsed) {
1597
- if (!isRecord(parsed) || !Array.isArray(parsed.runtime) || !Array.isArray(parsed.types)) {
1598
- return null;
1646
+ function checkModes(options) {
1647
+ if (options.noRegen && !options.checkDuplicates) {
1648
+ throw new Error("flag --no-regen applies with --check-duplicates only");
1649
+ }
1650
+ const modes = [
1651
+ options.checkCensus && "--check-census",
1652
+ options.checkDuplicates && "--check-duplicates",
1653
+ options.writeDuplicateBaseline && "--write-duplicate-baseline"
1654
+ ].filter((mode) => mode !== false);
1655
+ if (modes.length > 1) {
1656
+ throw new Error(`flags ${modes.join(" and ")} are two modes; use one of them in a run`);
1599
1657
  }
1600
- const valid = (e) => isRecord(e) && typeof e.name === "string" && typeof e.tag === "string" && Array.isArray(e.definers) && e.definers.every((d) => isRecord(d) && typeof d.file === "string");
1601
- if (!parsed.runtime.every(valid) || !parsed.types.every(valid))
1602
- return null;
1603
- return parsed;
1604
1658
  }
1605
- function asBaselineNames(parsed) {
1606
- if (!isRecord(parsed))
1607
- return null;
1608
- const kind = (value) => value === undefined ? {} : isRecord(value) ? value : null;
1609
- const runtime = kind(parsed.runtime);
1610
- const types = kind(parsed.types);
1611
- return runtime && types ? { runtime, types } : null;
1659
+ function wantsHelp(argv) {
1660
+ return argv.includes("--help") || argv.includes("-h");
1612
1661
  }
1613
1662
 
1663
+ // src/map/command.ts
1664
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync15, statSync as statSync6 } from "node:fs";
1665
+ import { resolve as resolve5 } from "node:path";
1666
+
1614
1667
  // src/depgraph/extensions.ts
1615
- import { existsSync as existsSync3, lstatSync as lstatSync2, realpathSync, statSync as statSync2 } from "node:fs";
1616
- import { basename, dirname as dirname2, isAbsolute, join as join5, relative as relative2, resolve as resolve2, sep } from "node:path";
1668
+ import { existsSync as existsSync3, statSync as statSync2 } from "node:fs";
1669
+ import { isAbsolute, relative as relative2, resolve as resolve3, sep } from "node:path";
1617
1670
  import { pathToFileURL } from "node:url";
1618
1671
 
1619
1672
  // src/io.ts
1620
- import { mkdirSync, writeFileSync } from "node:fs";
1621
- import { dirname } from "node:path";
1673
+ import { lstatSync as lstatSync2, mkdirSync, realpathSync, writeFileSync } from "node:fs";
1674
+ import { basename, dirname, join as join5, resolve as resolve2 } from "node:path";
1622
1675
  function writeLf(path, text) {
1623
1676
  mkdirSync(dirname(path), { recursive: true });
1624
1677
  writeFileSync(path, text.replace(/\r\n?/g, `
@@ -1636,6 +1689,22 @@ function toJson(value) {
1636
1689
  return `${JSON.stringify(value, null, 2)}
1637
1690
  `;
1638
1691
  }
1692
+ function realPath(path) {
1693
+ let existing = resolve2(path);
1694
+ const rest = [];
1695
+ while (!lstatSync2(existing, { throwIfNoEntry: false })) {
1696
+ const up = dirname(existing);
1697
+ if (up === existing)
1698
+ return;
1699
+ rest.unshift(basename(existing));
1700
+ existing = up;
1701
+ }
1702
+ try {
1703
+ return join5(realpathSync.native(existing), ...rest);
1704
+ } catch {
1705
+ return;
1706
+ }
1707
+ }
1639
1708
 
1640
1709
  // src/depgraph/extensions.ts
1641
1710
  var MASK = Object.freeze({ blankCommentsAndStrings, stripComments });
@@ -1716,27 +1785,11 @@ function outsideText(folder, path) {
1716
1785
  const rel = relative2(folder, path);
1717
1786
  return rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || rel.startsWith("../") || isAbsolute(rel);
1718
1787
  }
1719
- function realPath(path) {
1720
- let existing = resolve2(path);
1721
- const rest = [];
1722
- while (!lstatSync2(existing, { throwIfNoEntry: false })) {
1723
- const up = dirname2(existing);
1724
- if (up === existing)
1725
- return;
1726
- rest.unshift(basename(existing));
1727
- existing = up;
1728
- }
1729
- try {
1730
- return join5(realpathSync.native(existing), ...rest);
1731
- } catch {
1732
- return;
1733
- }
1734
- }
1735
1788
  function outputPath(outputDir, relPath) {
1736
1789
  if (typeof relPath !== "string" || relPath === "" || isAbsolutePath(relPath)) {
1737
1790
  throw new Error("write: the path must be relative to the output folder");
1738
1791
  }
1739
- const path = resolve2(outputDir, relPath);
1792
+ const path = resolve3(outputDir, relPath);
1740
1793
  if (outsideText(outputDir, path)) {
1741
1794
  throw new Error("write: the path must stay in the output folder");
1742
1795
  }
@@ -1749,8 +1802,8 @@ function outputPath(outputDir, relPath) {
1749
1802
  }
1750
1803
 
1751
1804
  // src/map/artifacts.ts
1752
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, statSync as statSync5, writeFileSync as writeFileSync2 } from "node:fs";
1753
- import { basename as basename4, dirname as dirname6, join as join12 } from "node:path";
1805
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync10, statSync as statSync5, writeFileSync as writeFileSync2 } from "node:fs";
1806
+ import { basename as basename4, dirname as dirname5, join as join13 } from "node:path";
1754
1807
 
1755
1808
  // src/map/cycles.ts
1756
1809
  function sccContaining(start2, eligible, edges, reverseEdges) {
@@ -1890,7 +1943,7 @@ function stronglyConnectedComponents(graph) {
1890
1943
  }
1891
1944
 
1892
1945
  // src/depgraph/resolver.ts
1893
- import { dirname as dirname3, join as join6 } from "node:path";
1946
+ import { dirname as dirname2, join as join6 } from "node:path";
1894
1947
  function distToSrc(path) {
1895
1948
  const m = /(^|\/)dist\/(?:src\/)?/.exec(path);
1896
1949
  if (!m)
@@ -1901,7 +1954,7 @@ function distToSrc(path) {
1901
1954
  return `${before}${m[1]}src/${path.slice(m.index + m[0].length)}`;
1902
1955
  }
1903
1956
  function resolveCandidates(fromPath, spec) {
1904
- const base = toPosix(join6(dirname3(fromPath), spec));
1957
+ const base = toPosix(join6(dirname2(fromPath), spec));
1905
1958
  let candidates;
1906
1959
  if (base.endsWith(".js")) {
1907
1960
  const stem = base.slice(0, -3);
@@ -2035,7 +2088,7 @@ function filesInCycles(components) {
2035
2088
 
2036
2089
  // src/depgraph/roots.ts
2037
2090
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
2038
- import { dirname as dirname4, join as join7 } from "node:path";
2091
+ import { dirname as dirname3, join as join7 } from "node:path";
2039
2092
  function isJsonObject(value) {
2040
2093
  return typeof value === "object" && value !== null && !Array.isArray(value);
2041
2094
  }
@@ -2118,7 +2171,7 @@ function seedTsconfigEntries(root, pkgDir, cfgRel, add) {
2118
2171
  } catch {
2119
2172
  return;
2120
2173
  }
2121
- const cfgDir = dirname4(join7(pkgDir, cfgRel));
2174
+ const cfgDir = dirname3(join7(pkgDir, cfgRel));
2122
2175
  const seedPath = (p) => {
2123
2176
  if (p.includes("*")) {
2124
2177
  const base = join7(root, cfgDir, p.replace(/\/?\*.*$/, ""));
@@ -2312,7 +2365,7 @@ function htmlReferencedEntries(root) {
2312
2365
  const src = m[1] ?? "";
2313
2366
  if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(src) || !/\.[cm]?[jt]sx?$/.test(src))
2314
2367
  continue;
2315
- const fromPage = toPosix(join7(dirname4(page), src)).replace(/^\.\//, "");
2368
+ const fromPage = toPosix(join7(dirname3(page), src)).replace(/^\.\//, "");
2316
2369
  const candidates = src.startsWith("/") ? [src.slice(1), fromPage] : [fromPage];
2317
2370
  const found = candidates.find((c) => existsSync4(join7(root, c)));
2318
2371
  if (found !== undefined && !out2.includes(found))
@@ -2332,7 +2385,7 @@ function runtimeLaunchedEntries(root, parsed) {
2332
2385
  continue;
2333
2386
  }
2334
2387
  for (const m of code.matchAll(/new\s+URL\(\s*['"`](\.[^'"`]*?\.m?js)['"`]\s*,\s*import\.meta\.url/g)) {
2335
- const sibling = toPosix(join7(dirname4(file.path), m[1] ?? ""));
2388
+ const sibling = toPosix(join7(dirname3(file.path), m[1] ?? ""));
2336
2389
  const tsPath = sibling.replace(/\.m?js$/, ".ts");
2337
2390
  if (known.has(tsPath))
2338
2391
  out2.add(tsPath);
@@ -6198,7 +6251,7 @@ import { readFileSync as readFileSync5 } from "node:fs";
6198
6251
  import { join as join9, posix as posix2 } from "node:path";
6199
6252
 
6200
6253
  // src/depgraph/description.ts
6201
- import { basename as basename2, dirname as dirname5 } from "node:path";
6254
+ import { basename as basename2, dirname as dirname4 } from "node:path";
6202
6255
  var RULE_LINE = /^[=\-*~#_]{3,}$/;
6203
6256
  function extractDescription(content) {
6204
6257
  const jsdocMatch = content.match(/\/\*\*\s*\n([^*]*(?:\*(?!\/)[^*]*)*)\*\//);
@@ -6233,7 +6286,7 @@ function generateFallbackDescription(file) {
6233
6286
  const fileName = withoutTsExtension(basename2(file.path));
6234
6287
  if (fileName === "index") {
6235
6288
  if (file.exports.reExported.length > 0) {
6236
- const pkgName = file.packageName || dirname5(file.path).split("/").pop() || "";
6289
+ const pkgName = file.packageName || dirname4(file.path).split("/").pop() || "";
6237
6290
  return `Package entry point for ${pkgName || "module"} (re-exports ${file.exports.reExported.length} symbols)`;
6238
6291
  }
6239
6292
  if (file.exports.named.length > 0) {
@@ -6447,6 +6500,26 @@ function isReparsePoint(path) {
6447
6500
  return false;
6448
6501
  }
6449
6502
  }
6503
+ function rglobParts(root, suffix) {
6504
+ const found = [];
6505
+ const go = (dir, parts2) => {
6506
+ let entries;
6507
+ try {
6508
+ entries = readdirHook.entries(dir);
6509
+ } catch {
6510
+ return;
6511
+ }
6512
+ for (const e of entries) {
6513
+ const full = join10(dir, e.name);
6514
+ if (e.isDirectory() && !isReparsePoint(full))
6515
+ go(full, [...parts2, e.name]);
6516
+ else if (e.name.endsWith(suffix))
6517
+ found.push([...parts2, e.name]);
6518
+ }
6519
+ };
6520
+ go(root, []);
6521
+ return found.sort((a, b) => compareCodeUnits(a.join("/"), b.join("/")));
6522
+ }
6450
6523
  function entries(dir) {
6451
6524
  try {
6452
6525
  return readdirHook.entries(dir);
@@ -6668,8 +6741,8 @@ function discover(root, language = detectLanguage(root), skippedLinks) {
6668
6741
  }
6669
6742
 
6670
6743
  // src/map/graph.ts
6671
- import { readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
6672
- import { basename as basename3, join as join11, resolve as resolve3 } from "node:path";
6744
+ import { readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
6745
+ import { basename as basename3, join as join12, resolve as resolve4 } from "node:path";
6673
6746
 
6674
6747
  // node_modules/smol-toml/dist/error.js
6675
6748
  /*!
@@ -10977,6 +11050,20 @@ function parserFor(name2) {
10977
11050
  return parser;
10978
11051
  }
10979
11052
 
11053
+ // src/map/package-field.ts
11054
+ import { readFileSync as readFileSync8 } from "node:fs";
11055
+ import { join as join11 } from "node:path";
11056
+ function packageField(root, key) {
11057
+ let pkg = null;
11058
+ try {
11059
+ pkg = JSON.parse(readFileSync8(join11(root, "package.json"), "utf8"));
11060
+ } catch {}
11061
+ if (typeof pkg !== "object" || pkg === null || Array.isArray(pkg))
11062
+ return null;
11063
+ const value = pkg[key];
11064
+ return typeof value === "string" && value.length > 0 ? value : null;
11065
+ }
11066
+
10980
11067
  // src/map/parsing.ts
10981
11068
  function emptyModule() {
10982
11069
  return {
@@ -11044,6 +11131,7 @@ function importClauseNames(clause) {
11044
11131
  return names;
11045
11132
  }
11046
11133
  var hasChild = (node, type) => node.children.some((c) => c.type === type);
11134
+ var allInlineTyped = (specifiers) => specifiers.length > 0 && specifiers.every((s) => hasChild(s, "type"));
11047
11135
  function isTypeOnlyImport(stmt, clause) {
11048
11136
  if (hasChild(stmt, "type"))
11049
11137
  return true;
@@ -11052,10 +11140,7 @@ function isTypeOnlyImport(stmt, clause) {
11052
11140
  const kids = clause.children.filter((c) => c.type !== ",");
11053
11141
  if (kids.length !== 1 || kids[0]?.type !== "named_imports")
11054
11142
  return false;
11055
- const specifiers = kids[0].children.filter((c) => c.type === "import_specifier");
11056
- if (specifiers.length === 0)
11057
- return false;
11058
- return specifiers.every((s) => hasChild(s, "type"));
11143
+ return allInlineTyped(kids[0].children.filter((c) => c.type === "import_specifier"));
11059
11144
  }
11060
11145
  function defaultExportKindAndLocal(stmt) {
11061
11146
  for (const c of stmt.children) {
@@ -11120,11 +11205,10 @@ function exportClauseSourceNames(clause) {
11120
11205
  function isTypeOnlyExport(stmt, clause) {
11121
11206
  if (hasChild(stmt, "type"))
11122
11207
  return true;
11123
- const specifiers = clause.children.filter((c) => c.type === "export_specifier");
11124
- if (specifiers.length === 0)
11125
- return false;
11126
- return specifiers.every((s) => hasChild(s, "type"));
11208
+ return allInlineTyped(clause.children.filter((c) => c.type === "export_specifier"));
11127
11209
  }
11210
+ var GAP = String.raw`(?:\s|/\*[\s\S]*?\*/)`;
11211
+ var EXPORT_TYPE_STAR = new RegExp(String.raw`^export${GAP}+type${GAP}*\*`, "u");
11128
11212
  function stripChars(text, chars) {
11129
11213
  let a = 0;
11130
11214
  let b = text.length;
@@ -11243,6 +11327,7 @@ function parseTs(source, path = "") {
11243
11327
  const nsExport = node.children.find((c) => c.type === "namespace_export");
11244
11328
  let names = [];
11245
11329
  let typeOnly = false;
11330
+ const typeStar = hasChild(node, "type") || EXPORT_TYPE_STAR.test(node.text);
11246
11331
  if (clause) {
11247
11332
  names = exportClauseSourceNames(clause);
11248
11333
  typeOnly = isTypeOnlyExport(node, clause);
@@ -11250,11 +11335,12 @@ function parseTs(source, path = "") {
11250
11335
  mod.reExports.push(name2);
11251
11336
  } else if (nsExport) {
11252
11337
  names = ["*"];
11338
+ typeOnly = typeStar;
11253
11339
  for (const [name2] of exportEntries(node))
11254
11340
  mod.reExports.push(name2);
11255
11341
  } else {
11256
- const typeStar = hasChild(node, "type") ? "type " : "";
11257
- mod.reExports.push(`${typeStar}* from ${specifierOf(spec)}`);
11342
+ typeOnly = typeStar;
11343
+ mod.reExports.push(`${typeStar ? "type " : ""}* from ${specifierOf(spec)}`);
11258
11344
  }
11259
11345
  mod.imports.push({ specifier: specifierOf(spec), names, typeOnly, reExport: true });
11260
11346
  }
@@ -12362,7 +12448,7 @@ var STRING_LITERAL = /['"]([^'"\n]*)['"]/g;
12362
12448
  var PATH_SEGMENT = new RegExp(`^(?:${W}|[.-])+$`, "u");
12363
12449
  var lstripDotSlash = (text) => text.replace(/^[./]+/, "");
12364
12450
  function launcherEntry(root, spec, known) {
12365
- const path = join11(root, lstripDotSlash(spec.replaceAll("\\", "/")));
12451
+ const path = join12(root, lstripDotSlash(spec.replaceAll("\\", "/")));
12366
12452
  let text;
12367
12453
  try {
12368
12454
  const st = statSync4(path);
@@ -12410,7 +12496,7 @@ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
12410
12496
  function packageJsonRoots(root, known) {
12411
12497
  let pkg;
12412
12498
  try {
12413
- pkg = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readFileSync8(join11(root, "package.json"))));
12499
+ pkg = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readFileSync9(join12(root, "package.json"))));
12414
12500
  } catch {
12415
12501
  return [[], []];
12416
12502
  }
@@ -12458,7 +12544,7 @@ var PYTHON_FALLBACK_ROOTS = [
12458
12544
  ];
12459
12545
  function pythonRoots(root, known) {
12460
12546
  const warnings = [];
12461
- const pyproject = join11(root, "pyproject.toml");
12547
+ const pyproject = join12(root, "pyproject.toml");
12462
12548
  let isFile = false;
12463
12549
  try {
12464
12550
  isFile = statSync4(pyproject).isFile();
@@ -12509,31 +12595,11 @@ function pythonRoots(root, known) {
12509
12595
  }
12510
12596
  var CSHARP_MAIN = new RegExp(`${B}static${S}+(?:async${S}+)?[^${SPACE_BODY}(){};]+${S}+Main${S}*\\(`, "u");
12511
12597
  var CSPROJ_EXE = new RegExp(`<OutputType>${S}*(Exe|WinExe)${S}*</OutputType>`, "iu");
12512
- function rglobParts(root, suffix) {
12513
- const found = [];
12514
- const go = (dir, parts2) => {
12515
- let entries;
12516
- try {
12517
- entries = readdirHook.entries(dir);
12518
- } catch {
12519
- return;
12520
- }
12521
- for (const e of entries) {
12522
- const full = join11(dir, e.name);
12523
- if (e.isDirectory() && !isReparsePoint(full))
12524
- go(full, [...parts2, e.name]);
12525
- else if (e.name.endsWith(suffix))
12526
- found.push([...parts2, e.name]);
12527
- }
12528
- };
12529
- go(root, []);
12530
- return found.sort((a, b) => compareCodeUnits(a.join("/"), b.join("/")));
12531
- }
12532
12598
  function csharpRoots(root, known) {
12533
12599
  const warnings = [];
12534
12600
  const declaresMain = (rel) => {
12535
12601
  try {
12536
- return CSHARP_MAIN.test(readSource(join11(root, rel)));
12602
+ return CSHARP_MAIN.test(readSource(join12(root, rel)));
12537
12603
  } catch {
12538
12604
  return false;
12539
12605
  }
@@ -12544,7 +12610,7 @@ function csharpRoots(root, known) {
12544
12610
  continue;
12545
12611
  let text;
12546
12612
  try {
12547
- text = readSource(join11(root, ...parts2));
12613
+ text = readSource(join12(root, ...parts2));
12548
12614
  } catch (error) {
12549
12615
  warnings.push(`could not read ${parts2.join("/")} (${error instanceof Error ? error.message : String(error)})`);
12550
12616
  continue;
@@ -12566,7 +12632,7 @@ function csharpRoots(root, known) {
12566
12632
  if (program.length > 0)
12567
12633
  roots.push(program[0]);
12568
12634
  else {
12569
- warnings.push(`${projDir || basename3(resolve3(root))} declares OutputType Exe but no file in it declares ` + "Main and none is named Program.cs; it contributes no root");
12635
+ warnings.push(`${projDir || basename3(resolve4(root))} declares OutputType Exe but no file in it declares ` + "Main and none is named Program.cs; it contributes no root");
12570
12636
  }
12571
12637
  }
12572
12638
  if (roots.length > 0)
@@ -12602,7 +12668,7 @@ function workspaceRoots(root, known) {
12602
12668
  continue;
12603
12669
  const prefix = `${ws.directory}/`;
12604
12670
  const local = new Set([...known].filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length)));
12605
- const [found, pkgWarnings] = packageJsonRoots(join11(root, ws.directory), local);
12671
+ const [found, pkgWarnings] = packageJsonRoots(join12(root, ws.directory), local);
12606
12672
  const pkgRoots = found.map((p) => prefix + p);
12607
12673
  for (const entry of ws.extraEntries)
12608
12674
  if (known.has(entry))
@@ -12651,7 +12717,7 @@ async function buildGraph(root) {
12651
12717
  const parsed = new Map;
12652
12718
  const sources = new Map;
12653
12719
  for (const f of found) {
12654
- const text = readSource(join11(root, f.path)).replace(/\r\n?/g, `
12720
+ const text = readSource(join12(root, f.path)).replace(/\r\n?/g, `
12655
12721
  `);
12656
12722
  sources.set(f.path, text);
12657
12723
  parsed.set(f.path, read(text, f.path));
@@ -12781,7 +12847,7 @@ async function buildGraph(root) {
12781
12847
  warnings.push("could not determine any entry-point roots: no package.json main/module/bin/exports resolved " + "to a known source file, and no src/index.* fallback was found -- every 'src' file will show " + "as orphan or test-only rather than build-entry");
12782
12848
  }
12783
12849
  const graph = newRepoGraph({
12784
- name: basename3(resolve3(root)),
12850
+ name: packageField(root, "name") ?? basename3(resolve4(root)),
12785
12851
  files,
12786
12852
  roots,
12787
12853
  warnings,
@@ -12885,7 +12951,7 @@ function pyJsonDumps(data) {
12885
12951
  return out2;
12886
12952
  }
12887
12953
  function writeJson(path, data) {
12888
- mkdirSync2(dirname6(path), { recursive: true });
12954
+ mkdirSync2(dirname5(path), { recursive: true });
12889
12955
  writeFileSync2(path, `${pyJsonDumps(data)}
12890
12956
  `);
12891
12957
  return path;
@@ -12968,11 +13034,11 @@ function emitDependencyGraph(graph, outDir, options = {}) {
12968
13034
  ...graph.language === "typescript" ? exportKindStats(graph) : {}
12969
13035
  });
12970
13036
  data.warnings = [...graph.warnings];
12971
- return writeJson(join12(outDir, "dependency-graph.json"), data);
13037
+ return writeJson(join13(outDir, "dependency-graph.json"), data);
12972
13038
  }
12973
13039
  function readJson(path) {
12974
13040
  try {
12975
- return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readFileSync9(path)));
13041
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readFileSync10(path)));
12976
13042
  } catch {
12977
13043
  return null;
12978
13044
  }
@@ -12982,7 +13048,7 @@ var PNPM_ITEM = new RegExp(`(?<=^|\\n)${S}*-${S}*['"]?([^'"${SPACE_BODY}#]+)['"]
12982
13048
  function pnpmWorkspacePatterns(root) {
12983
13049
  let text;
12984
13050
  try {
12985
- text = readSource(join12(root, "pnpm-workspace.yaml")).replace(/\r\n?/g, `
13051
+ text = readSource(join13(root, "pnpm-workspace.yaml")).replace(/\r\n?/g, `
12986
13052
  `);
12987
13053
  } catch {
12988
13054
  return [];
@@ -13012,29 +13078,9 @@ function normalizeWorkspacePatterns(raw) {
13012
13078
  ]
13013
13079
  ];
13014
13080
  }
13015
- function csprojParts(root) {
13016
- const found = [];
13017
- const go = (dir, parts2) => {
13018
- let entries;
13019
- try {
13020
- entries = readdirHook.entries(dir);
13021
- } catch {
13022
- return;
13023
- }
13024
- for (const e of entries) {
13025
- const full = join12(dir, e.name);
13026
- if (e.isDirectory() && !isReparsePoint(full))
13027
- go(full, [...parts2, e.name]);
13028
- else if (e.name.endsWith(".csproj"))
13029
- found.push([...parts2, e.name]);
13030
- }
13031
- };
13032
- go(root, []);
13033
- return found.sort((a, b) => compareCodeUnits(a.join("/"), b.join("/")));
13034
- }
13035
13081
  function csprojMap(root) {
13036
13082
  const projects = new Map;
13037
- for (const parts2 of csprojParts(root)) {
13083
+ for (const parts2 of rglobParts(root, ".csproj")) {
13038
13084
  if (parts2.some((p) => p === "obj" || p === "bin"))
13039
13085
  continue;
13040
13086
  const name2 = parts2.at(-1);
@@ -13052,13 +13098,13 @@ function csprojMap(root) {
13052
13098
  }
13053
13099
  function subdirs(dir) {
13054
13100
  try {
13055
- return readdirHook.entries(dir).filter((e) => e.isDirectory() || e.isSymbolicLink() && statSync5(join12(dir, e.name)).isDirectory()).map((e) => e.name).sort(compareCodeUnits);
13101
+ return readdirHook.entries(dir).filter((e) => e.isDirectory() || e.isSymbolicLink() && statSync5(join13(dir, e.name)).isDirectory()).map((e) => e.name).sort(compareCodeUnits);
13056
13102
  } catch {
13057
13103
  return [];
13058
13104
  }
13059
13105
  }
13060
13106
  function workspaceMap(root) {
13061
- const pkg = readJson(join12(root, "package.json"));
13107
+ const pkg = readJson(join13(root, "package.json"));
13062
13108
  if (!isObject3(pkg)) {
13063
13109
  return [
13064
13110
  new Map,
@@ -13092,18 +13138,18 @@ function workspaceMap(root) {
13092
13138
  const parent = pattern.slice(0, -2);
13093
13139
  let isDir = false;
13094
13140
  try {
13095
- isDir = statSync5(join12(root, parent)).isDirectory();
13141
+ isDir = statSync5(join13(root, parent)).isDirectory();
13096
13142
  } catch {
13097
13143
  isDir = false;
13098
13144
  }
13099
13145
  if (!isDir)
13100
13146
  continue;
13101
- candidates = subdirs(join12(root, parent)).map((d) => parent ? `${parent}/${d}` : d);
13147
+ candidates = subdirs(join13(root, parent)).map((d) => parent ? `${parent}/${d}` : d);
13102
13148
  } else {
13103
13149
  candidates = [pattern];
13104
13150
  }
13105
13151
  for (const relCand of candidates) {
13106
- const sub = readJson(join12(root, relCand, "package.json"));
13152
+ const sub = readJson(join13(root, relCand, "package.json"));
13107
13153
  if (!isObject3(sub)) {
13108
13154
  warnings.push(`emit_file_inventory: workspace candidate '${relCand}' (matched by '${pattern}') has no readable package.json -- skipped, its files will report package '(root)'`);
13109
13155
  continue;
@@ -13163,7 +13209,7 @@ function emitFileInventory(graph, outDir) {
13163
13209
  }));
13164
13210
  const byDisposition = Object.fromEntries(DISPOSITIONS.map((d) => [d, 0]));
13165
13211
  Object.assign(byDisposition, counter(files.map((f) => f.disposition)));
13166
- return writeJson(join12(outDir, "file-inventory.json"), {
13212
+ return writeJson(join13(outDir, "file-inventory.json"), {
13167
13213
  totalFiles: files.length,
13168
13214
  byDisposition,
13169
13215
  byArea: counter(files.map((f) => f.area)),
@@ -13197,7 +13243,7 @@ function readSourceText(rootPath, rel) {
13197
13243
  if (rootPath === null)
13198
13244
  return null;
13199
13245
  try {
13200
- return readSource(join12(rootPath, rel)).replace(/\r\n?/g, `
13246
+ return readSource(join13(rootPath, rel)).replace(/\r\n?/g, `
13201
13247
  `);
13202
13248
  } catch {
13203
13249
  return null;
@@ -13352,7 +13398,7 @@ function emitDuplicateSymbols(graph, outDir, options = {}) {
13352
13398
  duplicates[sym] = paths;
13353
13399
  }
13354
13400
  const summary = { duplicateCount: Object.keys(duplicates).length, totalSymbols: owners.size };
13355
- const path = join12(outDir, "duplicate-symbols.json");
13401
+ const path = join13(outDir, "duplicate-symbols.json");
13356
13402
  const root = graph.rootPath;
13357
13403
  if (graph.language !== "typescript" || root === null) {
13358
13404
  const reason = root === null ? "This graph has no root folder to read the definitions from." : `This repository reads as ${graph.language}.`;
@@ -13368,7 +13414,7 @@ function emitDuplicateSymbols(graph, outDir, options = {}) {
13368
13414
  const rootEntries = workspaces.size > 0 ? [] : rootPackageEntries(root);
13369
13415
  const surface = computePublicSurface(records, root, workspaces, rootEntries);
13370
13416
  const definers = records.filter((r) => !/\.d\.[cm]?ts$/.test(r.path));
13371
- const report = buildDuplicateReport(detectDuplicateSymbols(definers, surface, root, options.allowlistPath ?? join12(root, "docs", "architecture", "duplicate-allowlist.json")));
13417
+ const report = buildDuplicateReport(detectDuplicateSymbols(definers, surface, root, options.allowlistPath ?? join13(root, "docs", "architecture", "duplicate-allowlist.json"), options.dispatchCalls), options.dispatchCalls);
13372
13418
  return writeJson(path, {
13373
13419
  note: DUPLICATES_CLASSIFIED_NOTE,
13374
13420
  classificationNote: report.note,
@@ -13384,7 +13430,7 @@ function emitUnusedAnalysis(graph, outDir) {
13384
13430
  const [unreferenced, referencedInModule, unclassified] = classifyUnusedExports(graph);
13385
13431
  const notes = unreferencedAnywhereNotes(graph, unreferenced);
13386
13432
  const noImporter = noImporterFiles(graph);
13387
- return writeJson(join12(outDir, "unused-analysis.json"), {
13433
+ return writeJson(join13(outDir, "unused-analysis.json"), {
13388
13434
  caveats: caveatsFor(graph),
13389
13435
  summary: {
13390
13436
  unusedExportCount: countAll(unreferenced) + countAll(referencedInModule) + countAll(unclassified),
@@ -13402,16 +13448,16 @@ function emitUnusedAnalysis(graph, outDir) {
13402
13448
  }
13403
13449
 
13404
13450
  // src/map/coverage.ts
13405
- import { join as join16 } from "node:path";
13451
+ import { join as join17 } from "node:path";
13406
13452
 
13407
13453
  // src/depgraph/coverage.ts
13408
- import { existsSync as existsSync6, readFileSync as readFileSync10 } from "node:fs";
13409
- import { join as join13 } from "node:path";
13454
+ import { existsSync as existsSync6, readFileSync as readFileSync11 } from "node:fs";
13455
+ import { join as join14 } from "node:path";
13410
13456
  function loadCoveragePolicy(policyPath) {
13411
13457
  if (!existsSync6(policyPath))
13412
13458
  return null;
13413
13459
  try {
13414
- const parsed = JSON.parse(readFileSync10(policyPath, "utf8"));
13460
+ const parsed = JSON.parse(readFileSync11(policyPath, "utf8"));
13415
13461
  if (!parsed.categories || typeof parsed.categories !== "object")
13416
13462
  return null;
13417
13463
  return parsed;
@@ -13511,7 +13557,7 @@ function traceReExports(importedPath, reExportMap) {
13511
13557
  result.add(source);
13512
13558
  return result;
13513
13559
  }
13514
- function analyzeTestCoverage(graphFiles, testFiles, root, policyPath = join13(outputDirOf(root), "coverage-policy.json")) {
13560
+ function analyzeTestCoverage(graphFiles, testFiles, root, policyPath = join14(outputDirOf(root), "coverage-policy.json")) {
13515
13561
  const sourceFiles = graphFiles.filter((f) => !f.path.endsWith(".d.ts"));
13516
13562
  const sourceFilePaths = new Set(sourceFiles.map((f) => f.path));
13517
13563
  const coverageMap = new Map;
@@ -13650,10 +13696,10 @@ function generateTestCoverageMarkdown(coverage, options = {}) {
13650
13696
  }
13651
13697
  lines.push("---");
13652
13698
  lines.push("");
13653
- lines.push("## Source Files Without Test Coverage");
13699
+ lines.push("## Source Files Not Imported By A Test");
13654
13700
  lines.push("");
13655
13701
  if (coverage.untestedFiles.length === 0) {
13656
- lines.push("**All source files have test coverage!** \uD83C\uDF89");
13702
+ lines.push("**Every source file is imported by a test file.** \uD83C\uDF89");
13657
13703
  } else {
13658
13704
  lines.push(`The following ${coverage.untestedFiles.length} source files are not directly imported by any test file:`);
13659
13705
  lines.push("");
@@ -13677,7 +13723,7 @@ function generateTestCoverageMarkdown(coverage, options = {}) {
13677
13723
  }
13678
13724
  lines.push("---");
13679
13725
  lines.push("");
13680
- lines.push("## Source Files With Test Coverage");
13726
+ lines.push("## Source Files Imported By A Test");
13681
13727
  lines.push("");
13682
13728
  lines.push("| Source File | Test Files |");
13683
13729
  lines.push("|-------------|------------|");
@@ -13741,7 +13787,7 @@ function generateTestCoverageJson(coverage, options = {}) {
13741
13787
 
13742
13788
  // src/map/reports.ts
13743
13789
  import { readFileSync as readFileSync12 } from "node:fs";
13744
- import { join as join15 } from "node:path";
13790
+ import { join as join16 } from "node:path";
13745
13791
 
13746
13792
  // src/depgraph/reporters/json.ts
13747
13793
  function modulesJsonOf(modules) {
@@ -13958,10 +14004,14 @@ function fileSection(lines, path, file) {
13958
14004
  lines.push("---");
13959
14005
  lines.push("");
13960
14006
  }
13961
- function summaryRowsOf(stats) {
14007
+ var DEPGRAPH_LABELS = {
14008
+ files: "Total TypeScript Files",
14009
+ modules: "Total Modules"
14010
+ };
14011
+ function summaryRowsOf(stats, labels = DEPGRAPH_LABELS) {
13962
14012
  const rows = [
13963
- ["Total TypeScript Files", stats.totalTypeScriptFiles],
13964
- ["Total Modules", stats.totalModules],
14013
+ [labels.files, stats.totalTypeScriptFiles],
14014
+ [labels.modules, stats.totalModules],
13965
14015
  ["Total Lines of Code", stats.totalLinesOfCode],
13966
14016
  ["Total Exports", stats.totalExports],
13967
14017
  ["Total Re-exports", stats.totalReExports],
@@ -14104,20 +14154,12 @@ function generateYaml(json, dumpFn = dump) {
14104
14154
  }
14105
14155
 
14106
14156
  // src/map/layers.ts
14107
- import { readFileSync as readFileSync11 } from "node:fs";
14108
- import { join as join14 } from "node:path";
14157
+ import { join as join15 } from "node:path";
14109
14158
  function projectIdentity(root, graph) {
14110
- let pkg = null;
14111
- try {
14112
- pkg = JSON.parse(readFileSync11(join14(root, "package.json"), "utf8"));
14113
- } catch {}
14114
- const field = (key) => {
14115
- if (typeof pkg !== "object" || pkg === null || Array.isArray(pkg))
14116
- return null;
14117
- const value = pkg[key];
14118
- return typeof value === "string" && value.length > 0 ? value : null;
14159
+ return {
14160
+ name: packageField(root, "name") ?? graph.name,
14161
+ version: packageField(root, "version") ?? "unknown"
14119
14162
  };
14120
- return { name: field("name") ?? graph.name, version: field("version") ?? "unknown" };
14121
14163
  }
14122
14164
  function subsystemView(graph, root, warn = () => {}) {
14123
14165
  const records = toParsedFiles(graph, root);
@@ -14129,7 +14171,7 @@ function subsystemView(graph, root, warn = () => {}) {
14129
14171
  }
14130
14172
  function emitDependencyLayers(graph, root, outDir, view = subsystemView(graph, root)) {
14131
14173
  const identity = projectIdentity(root, graph);
14132
- return writeJson(join14(outDir, "dependency-layers.json"), {
14174
+ return writeJson(join15(outDir, "dependency-layers.json"), {
14133
14175
  metadata: {
14134
14176
  name: identity.name,
14135
14177
  version: identity.version,
@@ -14168,33 +14210,14 @@ function statisticsFromCore(core, subsystems) {
14168
14210
  unusedExportsCount: num("unusedExportsCount")
14169
14211
  };
14170
14212
  }
14171
- function summaryRows(stats) {
14172
- const rows = [
14173
- ["Total Source Files", stats.totalTypeScriptFiles],
14174
- ["Subsystems", stats.totalModules],
14175
- ["Total Lines of Code", stats.totalLinesOfCode],
14176
- ["Total Exports", stats.totalExports],
14177
- ["Total Re-exports", stats.totalReExports],
14178
- ["Total Classes", stats.totalClasses],
14179
- ["Total Interfaces", stats.totalInterfaces],
14180
- ["Total Functions", stats.totalFunctions],
14181
- ["Total Type Guards", stats.totalTypeGuards],
14182
- ["Total Enums", stats.totalEnums],
14183
- ["Type-only Imports", stats.totalTypeOnlyImports],
14184
- ["Runtime Cyclic Components", stats.runtimeCyclicComponents],
14185
- ["Type-only Cyclic Components", stats.typeOnlyCyclicComponents],
14186
- ["Files in Runtime Cycles", stats.runtimeFilesInCycles],
14187
- ["Files in Type-only Cycles", stats.typeOnlyFilesInCycles]
14188
- ];
14189
- return rows.filter((r) => r[1] !== undefined);
14190
- }
14213
+ var SUMMARY_LABELS = { files: "Total Source Files", modules: "Subsystems" };
14191
14214
  function emitSubsystemReports(graph, root, outDir, corePath, options = {}) {
14192
14215
  const view = options.view ?? subsystemView(graph, root);
14193
14216
  const core = JSON.parse(readFileSync12(corePath, "utf8"));
14194
14217
  const stats = statisticsFromCore(core.statistics, Object.keys(view.modules).length);
14195
14218
  const identity = projectIdentity(root, graph);
14196
14219
  const banner = { command: MAP_REGENERATE_COMMAND, ...options.banner };
14197
- const markdown = generateMarkdown(view.records, view.modules, stats, view.cycles, buildDependencyMatrix(view.records), identity, summaryRows(stats));
14220
+ const markdown = generateMarkdown(view.records, view.modules, stats, view.cycles, buildDependencyMatrix(view.records), identity, summaryRowsOf(stats, SUMMARY_LABELS));
14198
14221
  const written = [
14199
14222
  ["DEPENDENCY_GRAPH.md", withBanner(markdown, banner)],
14200
14223
  ["dependency-graph.yaml", generateYaml(core)],
@@ -14204,7 +14227,7 @@ function emitSubsystemReports(graph, root, outDir, corePath, options = {}) {
14204
14227
  ]
14205
14228
  ];
14206
14229
  return written.map(([name2, text]) => {
14207
- const path = join15(outDir, name2);
14230
+ const path = join16(outDir, name2);
14208
14231
  writeReport(path, text);
14209
14232
  return path;
14210
14233
  });
@@ -14222,9 +14245,9 @@ function isTestFile(path, language, area) {
14222
14245
  function emitTestCoverage(graph, root, outDir, options = {}) {
14223
14246
  const sources = toParsedFiles(graph, root);
14224
14247
  const tests = toParsedFiles(graph, root, { allAreas: true, loadEdges: true }).filter((r) => isTestFile(r.path, graph.language, graph.files.get(r.path)?.area ?? ""));
14225
- const coverage = analyzeTestCoverage(sources, tests, root, options.policyPath ?? join16(root, "docs", "architecture", "coverage-policy.json"));
14226
- const mdPath = join16(outDir, "TEST_COVERAGE.md");
14227
- const jsonPath = join16(outDir, "test-coverage.json");
14248
+ const coverage = analyzeTestCoverage(sources, tests, root, options.policyPath ?? join17(root, "docs", "architecture", "coverage-policy.json"));
14249
+ const mdPath = join17(outDir, "TEST_COVERAGE.md");
14250
+ const jsonPath = join17(outDir, "test-coverage.json");
14228
14251
  const namespaceImportBasis = !importsNameSymbols(graph.language);
14229
14252
  const markdown = generateTestCoverageMarkdown(coverage, {
14230
14253
  expectedTestHint: graph.language === "typescript" && tests.some((t) => t.path.startsWith("tests/unit/")),
@@ -14238,7 +14261,7 @@ function emitTestCoverage(graph, root, outDir, options = {}) {
14238
14261
 
14239
14262
  // src/map/markdown.ts
14240
14263
  import { readFileSync as readFileSync13 } from "node:fs";
14241
- import { join as join17 } from "node:path";
14264
+ import { join as join18 } from "node:path";
14242
14265
 
14243
14266
  // src/depgraph/reporters/duplicates.ts
14244
14267
  function summaryTable(byTag, total) {
@@ -14278,7 +14301,7 @@ function renderTaggedSection(title, entries, tag) {
14278
14301
 
14279
14302
  ${renderTable(entries.filter((e) => e.tag === tag))}`;
14280
14303
  }
14281
- function generateDuplicateSymbolsMarkdown(report) {
14304
+ function generateDuplicateSymbolsMarkdown(report, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
14282
14305
  let md = `# Duplicate Symbols
14283
14306
 
14284
14307
  `;
@@ -14286,9 +14309,14 @@ function generateDuplicateSymbolsMarkdown(report) {
14286
14309
  md += "A file that only re-exports the name does not count. ";
14287
14310
  md += "Each name carries one tag (see `DupEntryTag`), so the actionable subset is clear:\n\n";
14288
14311
  md += "- `TRUE_DUPLICATE`: a real merge target.\n";
14289
- md += "- `DISPATCH_VARIANT`: two or more `mathTyped(...)` registrations of the same public name. ";
14290
- md += `The registrations are distinct dispatch surfaces, not copy-paste bodies.
14312
+ if (dispatchCalls.length === 0) {
14313
+ md += "- `DISPATCH_VARIANT`: never set, because no dispatch call is configured.\n";
14314
+ } else {
14315
+ md += `- \`DISPATCH_VARIANT\`: two or more ${describeDispatchCalls(dispatchCalls, true)} `;
14316
+ md += "registrations of the same public name. ";
14317
+ md += `The registrations are distinct dispatch surfaces, not copy-paste bodies.
14291
14318
  `;
14319
+ }
14292
14320
  md += "- `ALIAS_DELEGATION`: a `const X = importedY` forward. ";
14293
14321
  md += `The entry is excluded once fewer than two real bodies remain.
14294
14322
  `;
@@ -14383,7 +14411,7 @@ function inventoryMarkdown(inv, language) {
14383
14411
  return lines.join(`
14384
14412
  `);
14385
14413
  }
14386
- function duplicatesMarkdown(dup) {
14414
+ function duplicatesMarkdown(dup, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
14387
14415
  if (Array.isArray(dup.runtime) && Array.isArray(dup.types)) {
14388
14416
  const report = {
14389
14417
  note: dup.classificationNote,
@@ -14396,7 +14424,7 @@ function duplicatesMarkdown(dup) {
14396
14424
  runtime: dup.runtime,
14397
14425
  types: dup.types
14398
14426
  };
14399
- return generateDuplicateSymbolsMarkdown(report);
14427
+ return generateDuplicateSymbolsMarkdown(report, dispatchCalls);
14400
14428
  }
14401
14429
  const lines = ["# Duplicate Symbols", ""];
14402
14430
  lines.push(`> **Note:** ${dup.note}`, "", `> ${dup.classificationNote}`, "");
@@ -14467,17 +14495,17 @@ function unusedMarkdown(unused, graph) {
14467
14495
  return lines.join(`
14468
14496
  `);
14469
14497
  }
14470
- function emitMarkdownReports(outDir, banner = {}) {
14471
- const read = (name2) => JSON.parse(readFileSync13(join17(outDir, name2), "utf8"));
14498
+ function emitMarkdownReports(outDir, banner = {}, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
14499
+ const read = (name2) => JSON.parse(readFileSync13(join18(outDir, name2), "utf8"));
14472
14500
  const graph = read("dependency-graph.json");
14473
14501
  const bannerOptions = { command: MAP_REGENERATE_COMMAND, ...banner };
14474
14502
  const reports = [
14475
14503
  ["FILE_INVENTORY.md", inventoryMarkdown(read("file-inventory.json"), graph.metadata.language)],
14476
- ["duplicate-symbols.md", duplicatesMarkdown(read("duplicate-symbols.json"))],
14504
+ ["duplicate-symbols.md", duplicatesMarkdown(read("duplicate-symbols.json"), dispatchCalls)],
14477
14505
  ["unused-analysis.md", unusedMarkdown(read("unused-analysis.json"), graph)]
14478
14506
  ];
14479
14507
  return reports.map(([name2, body2]) => {
14480
- const path = join17(outDir, name2);
14508
+ const path = join18(outDir, name2);
14481
14509
  writeReport(path, withBanner(body2, bannerOptions));
14482
14510
  return path;
14483
14511
  });
@@ -14485,7 +14513,7 @@ function emitMarkdownReports(outDir, banner = {}) {
14485
14513
 
14486
14514
  // src/map/surfaces.ts
14487
14515
  import { readFileSync as readFileSync14 } from "node:fs";
14488
- import { join as join18 } from "node:path";
14516
+ import { join as join19 } from "node:path";
14489
14517
 
14490
14518
  // src/depgraph/reporters/surfaces.ts
14491
14519
  function packageKeyOf(moduleKey) {
@@ -14520,7 +14548,7 @@ function emitApiSurface(graph, root, outPath, entry, stabilityTags) {
14520
14548
  if (graph.language === "typescript") {
14521
14549
  const load = (p) => {
14522
14550
  try {
14523
- return readFileSync14(join18(root, p), "utf8");
14551
+ return readFileSync14(join19(root, p), "utf8");
14524
14552
  } catch {
14525
14553
  return null;
14526
14554
  }
@@ -14566,7 +14594,7 @@ function sortedSurfaces(map) {
14566
14594
  return out2;
14567
14595
  }
14568
14596
  function emitExportSurfaces(graph, root, outDir) {
14569
- const path = join18(outDir, "package-export-surfaces.json");
14597
+ const path = join19(outDir, "package-export-surfaces.json");
14570
14598
  if (graph.language === "typescript") {
14571
14599
  const records = toParsedFiles(graph, root);
14572
14600
  const workspaces = detectWorkspaces(root);
@@ -14694,7 +14722,7 @@ async function runMap(argv, io, command = "map") {
14694
14722
  `);
14695
14723
  if (options.strictOrphans)
14696
14724
  options.settings.strictOrphans = true;
14697
- root = resolve4(options.root);
14725
+ root = resolve5(options.root);
14698
14726
  if (!isDirectory(root))
14699
14727
  throw new Error("the root <root> is not an existing directory");
14700
14728
  const fileSettings = loadConfigFile(root, options.config);
@@ -14761,13 +14789,14 @@ async function runPipeline(root, options, config, io) {
14761
14789
  const corePath = emitDependencyGraph(graph, outDir);
14762
14790
  written.push(corePath, emitFileInventory(graph, outDir));
14763
14791
  written.push(emitDuplicateSymbols(graph, outDir, {
14764
- allowlistPath: resolveUnderRoot(root, config.duplicateAllowlist)
14792
+ allowlistPath: resolveUnderRoot(root, config.duplicateAllowlist),
14793
+ dispatchCalls: config.dispatchCalls
14765
14794
  }));
14766
14795
  written.push(emitUnusedAnalysis(graph, outDir));
14767
14796
  const view = subsystemView(graph, root);
14768
14797
  written.push(emitDependencyLayers(graph, root, outDir, view));
14769
14798
  written.push(...emitSubsystemReports(graph, root, outDir, corePath, { view, banner }));
14770
- written.push(...emitMarkdownReports(outDir, banner));
14799
+ written.push(...emitMarkdownReports(outDir, banner, config.dispatchCalls));
14771
14800
  written.push(...emitTestCoverage(graph, root, outDir, {
14772
14801
  policyPath: resolveUnderRoot(root, config.coveragePolicy),
14773
14802
  policyNote: config.coveragePolicy.replace(/\\/g, "/"),
@@ -14971,7 +15000,7 @@ function parseClaims(markdown) {
14971
15000
  // src/check/metrics.ts
14972
15001
  import { mkdtempSync, readFileSync as readFileSync16, rmSync } from "node:fs";
14973
15002
  import { tmpdir } from "node:os";
14974
- import { join as join19 } from "node:path";
15003
+ import { join as join20 } from "node:path";
14975
15004
  var readReport = (path) => JSON.parse(readFileSync16(path, "utf8"));
14976
15005
  function section(report, key) {
14977
15006
  const value = report[key];
@@ -14989,11 +15018,14 @@ function addScalars(into, source) {
14989
15018
  }
14990
15019
  async function collectMetrics(root, options = {}) {
14991
15020
  const graph = await graphOf(root);
14992
- const scratch = mkdtempSync(join19(options.scratchBase ?? tmpdir(), "repo-tools-check-"));
15021
+ const scratch = mkdtempSync(join20(options.scratchBase ?? tmpdir(), "repo-tools-check-"));
14993
15022
  try {
14994
15023
  const dep = readReport(emitDependencyGraph(graph, scratch));
14995
15024
  const inventory = readReport(emitFileInventory(graph, scratch));
14996
- const duplicates = readReport(emitDuplicateSymbols(graph, scratch, options.allowlistPath === undefined ? {} : { allowlistPath: options.allowlistPath }));
15025
+ const duplicates = readReport(emitDuplicateSymbols(graph, scratch, {
15026
+ ...options.allowlistPath === undefined ? {} : { allowlistPath: options.allowlistPath },
15027
+ ...options.dispatchCalls === undefined ? {} : { dispatchCalls: options.dispatchCalls }
15028
+ }));
14997
15029
  const unused = readReport(emitUnusedAnalysis(graph, scratch));
14998
15030
  const metrics = new Map;
14999
15031
  addScalars(metrics, section(dep, "metadata"));
@@ -15191,7 +15223,7 @@ function readDocs(dir, label) {
15191
15223
  return names.map((name2) => {
15192
15224
  let bytes;
15193
15225
  try {
15194
- bytes = readFileSync17(join20(dir, name2));
15226
+ bytes = readFileSync17(join21(dir, name2));
15195
15227
  } catch {
15196
15228
  throw new Error(`cannot read ${label}/${name2}`);
15197
15229
  }
@@ -15218,16 +15250,17 @@ async function run2(argv, io) {
15218
15250
  const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
15219
15251
  try {
15220
15252
  const options = parseCheckArgs(argv, process.cwd());
15221
- root = resolve5(options.root);
15253
+ root = resolve6(options.root);
15222
15254
  if (!isDirectory(root))
15223
15255
  throw new Error("the root <root> is not an existing directory");
15224
15256
  if (options.docs === undefined)
15225
15257
  throw new Error("flag --docs is required: --docs=<dir>");
15226
15258
  const config = mergeMapConfig({}, loadConfigFile(root, options.config));
15227
15259
  const label = isAbsolutePath(options.docs) ? "<docs>" : `<root>/${options.docs.replace(/\\/g, "/")}`;
15228
- const docs = readDocs(resolve5(root, options.docs), label);
15260
+ const docs = readDocs(resolve6(root, options.docs), label);
15229
15261
  const measured = await collectMetrics(root, {
15230
- allowlistPath: resolve5(root, config.duplicateAllowlist)
15262
+ allowlistPath: resolve6(root, config.duplicateAllowlist),
15263
+ dispatchCalls: config.dispatchCalls
15231
15264
  });
15232
15265
  const markers = [NO_VERIFICATION_MARKER];
15233
15266
  if (config.verificationMarker !== null && !markers.includes(config.verificationMarker)) {
@@ -15259,12 +15292,12 @@ import {
15259
15292
  statSync as statSync7,
15260
15293
  writeFileSync as writeFileSync3
15261
15294
  } from "node:fs";
15262
- import { basename as basename7, dirname as dirname7, extname as extname2, isAbsolute as isAbsolute3, join as join21, relative as relative4, resolve as resolve7, sep as sep3 } from "node:path";
15295
+ import { basename as basename7, dirname as dirname6, extname as extname2, isAbsolute as isAbsolute3, join as join22, relative as relative4, resolve as resolve8, sep as sep3 } from "node:path";
15263
15296
 
15264
15297
  // src/chunk/manifest.ts
15265
15298
  import { createHash } from "node:crypto";
15266
15299
  import { readFileSync as readFileSync18 } from "node:fs";
15267
- import { posix as posix3, relative as relative3, resolve as resolve6, sep as sep2, win32 as win322 } from "node:path";
15300
+ import { posix as posix3, relative as relative3, resolve as resolve7, sep as sep2, win32 as win322 } from "node:path";
15268
15301
 
15269
15302
  // src/chunk/splitters.ts
15270
15303
  import { extname } from "node:path";
@@ -15895,7 +15928,7 @@ function relativeSource(manifestDir, sourcePath) {
15895
15928
  return relative3(manifestDir, sourcePath).split(sep2).join("/");
15896
15929
  }
15897
15930
  function resolveSource(manifestDir, sourceFile) {
15898
- return resolve6(manifestDir, sourceFile);
15931
+ return resolve7(manifestDir, sourceFile);
15899
15932
  }
15900
15933
  function readManifest(path) {
15901
15934
  return validateManifest(JSON.parse(readFileSync18(path, "utf8")));
@@ -16114,34 +16147,18 @@ function isOutsideText(folder, path) {
16114
16147
  const rel = relative4(folder, path);
16115
16148
  return rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute3(rel);
16116
16149
  }
16117
- function realPath2(path) {
16118
- let existing = resolve7(path);
16119
- const rest = [];
16120
- while (!lstatSync4(existing, { throwIfNoEntry: false })) {
16121
- const up = dirname7(existing);
16122
- if (up === existing)
16123
- return;
16124
- rest.unshift(basename7(existing));
16125
- existing = up;
16126
- }
16127
- try {
16128
- return join21(realpathSync2.native(existing), ...rest);
16129
- } catch {
16130
- return;
16131
- }
16132
- }
16133
16150
  function isOutside(folder, path) {
16134
16151
  if (isOutsideText(folder, path))
16135
16152
  return true;
16136
- const realFolder = realPath2(folder);
16137
- const realTarget = realPath2(path);
16153
+ const realFolder = realPath(folder);
16154
+ const realTarget = realPath(path);
16138
16155
  return realFolder === undefined || realTarget === undefined || isOutsideText(realFolder, realTarget);
16139
16156
  }
16140
16157
  function isInside(folder, path) {
16141
16158
  if (!isOutsideText(folder, path))
16142
16159
  return true;
16143
- const realFolder = realPath2(folder);
16144
- const realTarget = realPath2(path);
16160
+ const realFolder = realPath(folder);
16161
+ const realTarget = realPath(path);
16145
16162
  return realFolder !== undefined && realTarget !== undefined && !isOutsideText(realFolder, realTarget);
16146
16163
  }
16147
16164
  function chunkFolderError(chunksDir, paths) {
@@ -16154,11 +16171,11 @@ function chunkFolderError(chunksDir, paths) {
16154
16171
  return;
16155
16172
  }
16156
16173
  function outsideError(sourcePath, chunksDir, remedy) {
16157
- return `Error: the source file ${sourcePath} is outside ${dirname7(chunksDir)}, the parent folder of the chunk folder. ${remedy}
16174
+ return `Error: the source file ${sourcePath} is outside ${dirname6(chunksDir)}, the parent folder of the chunk folder. ${remedy}
16158
16175
  `;
16159
16176
  }
16160
16177
  function readChunk(chunksDir, filename) {
16161
- const chunkPath = join21(chunksDir, filename);
16178
+ const chunkPath = join22(chunksDir, filename);
16162
16179
  const stat = lstatSync4(chunkPath, { throwIfNoEntry: false });
16163
16180
  if (!stat)
16164
16181
  return;
@@ -16192,7 +16209,7 @@ function split(inputFile, options, io) {
16192
16209
  const log = lineWriter(io);
16193
16210
  const maxLines = options.maxLines ?? 500;
16194
16211
  const dryRun = options.dryRun ?? false;
16195
- const absoluteInput = resolve7(inputFile);
16212
+ const absoluteInput = resolve8(inputFile);
16196
16213
  if (!existsSync8(absoluteInput)) {
16197
16214
  io.stderr(`Error: File not found: ${absoluteInput}
16198
16215
  `);
@@ -16209,7 +16226,7 @@ function split(inputFile, options, io) {
16209
16226
  const content = readFileSync19(absoluteInput, "utf8");
16210
16227
  const sourceHash = contentHash(content);
16211
16228
  const baseName = basename7(inputFile, extname2(inputFile));
16212
- const outputDir = options.output ? resolve7(options.output) : join21(dirname7(absoluteInput), `${baseName}_chunks`);
16229
+ const outputDir = options.output ? resolve8(options.output) : join22(dirname6(absoluteInput), `${baseName}_chunks`);
16213
16230
  const sourceFile = relativeSource(outputDir, absoluteInput);
16214
16231
  if (isAbsoluteAnywhere(sourceFile)) {
16215
16232
  io.stderr(`Error: the chunk folder ${outputDir} is on another volume than ${absoluteInput}. Nothing was written. Put the chunk folder on the same volume as the source file.
@@ -16251,7 +16268,7 @@ chunker - Splitting File`);
16251
16268
  const large = lineCount > maxLines ? " ⚠️ [LARGE]" : "";
16252
16269
  log(` ${String(i2 + 1).padStart(2)}. [${levelIndicator(fileType, section.level).padEnd(4)}] ${section.title.substring(0, 40).padEnd(40)} ${String(lineCount).padStart(4)} lines${large}`);
16253
16270
  if (!dryRun)
16254
- writeLf(join21(outputDir, filename), section.content);
16271
+ writeLf(join22(outputDir, filename), section.content);
16255
16272
  return {
16256
16273
  index: i2 + 1,
16257
16274
  filename,
@@ -16275,7 +16292,7 @@ chunker - Splitting File`);
16275
16292
  ...lineEnding === "crlf" ? { lineEnding } : {},
16276
16293
  ...lineEnding === "mixed" ? { lineBreaks: lineBreakRuns(content) } : {}
16277
16294
  };
16278
- const manifestPath = join21(outputDir, MANIFEST_NAME);
16295
+ const manifestPath = join22(outputDir, MANIFEST_NAME);
16279
16296
  if (!dryRun) {
16280
16297
  writeManifest(manifestPath, manifest);
16281
16298
  log(`
@@ -16304,7 +16321,7 @@ To edit: Modify individual chunk files in the directory`);
16304
16321
  function merge(manifestFile, options, io) {
16305
16322
  const log = lineWriter(io);
16306
16323
  const dryRun = options.dryRun ?? false;
16307
- const absoluteManifest = resolve7(manifestFile);
16324
+ const absoluteManifest = resolve8(manifestFile);
16308
16325
  if (!existsSync8(absoluteManifest)) {
16309
16326
  io.stderr(`Error: Manifest not found: ${absoluteManifest}
16310
16327
  `);
@@ -16317,17 +16334,17 @@ function merge(manifestFile, options, io) {
16317
16334
  return 1;
16318
16335
  }
16319
16336
  const manifest = readManifest(absoluteManifest);
16320
- const chunksDir = dirname7(absoluteManifest);
16337
+ const chunksDir = dirname6(absoluteManifest);
16321
16338
  const sourcePath = resolveSource(chunksDir, manifest.sourceFile);
16322
16339
  const selfError = chunkFolderError(chunksDir, {
16323
16340
  "source file": sourcePath,
16324
- ...options.output ? { "output file": resolve7(options.output) } : {}
16341
+ ...options.output ? { "output file": resolve8(options.output) } : {}
16325
16342
  });
16326
16343
  if (selfError) {
16327
16344
  io.stderr(selfError);
16328
16345
  return 1;
16329
16346
  }
16330
- const outside = isOutside(dirname7(chunksDir), sourcePath);
16347
+ const outside = isOutside(dirname6(chunksDir), sourcePath);
16331
16348
  if (outside && !options.output && !options.yes) {
16332
16349
  io.stderr(outsideError(sourcePath, chunksDir, "Use -o <file> to name the target, or --yes to confirm."));
16333
16350
  return 1;
@@ -16350,7 +16367,7 @@ chunker - Merging Chunks`);
16350
16367
  for (const chunk of manifest.chunks) {
16351
16368
  const content = readChunk(chunksDir, chunk.filename);
16352
16369
  if (content === undefined) {
16353
- io.stderr(`Error: Missing chunk file: ${join21(chunksDir, chunk.filename)}
16370
+ io.stderr(`Error: Missing chunk file: ${join22(chunksDir, chunk.filename)}
16354
16371
  `);
16355
16372
  return 1;
16356
16373
  }
@@ -16383,7 +16400,7 @@ Modified chunks: ${modifiedCount} of ${manifest.chunks.length}`);
16383
16400
  NOTE: the number of lines changed. All line breaks use the most common original one.`);
16384
16401
  }
16385
16402
  }
16386
- const outputPath = options.output ? resolve7(options.output) : sourcePath;
16403
+ const outputPath = options.output ? resolve8(options.output) : sourcePath;
16387
16404
  if (readSource && existsSync8(sourcePath)) {
16388
16405
  const currentSourceHash = hash(readFileSync19(sourcePath, "utf8"));
16389
16406
  if (currentSourceHash !== manifest.sourceHash) {
@@ -16424,7 +16441,7 @@ Merged file written: ${outputPath}`);
16424
16441
  }
16425
16442
  function status(manifestFile, options, io) {
16426
16443
  const log = lineWriter(io);
16427
- const absoluteManifest = resolve7(manifestFile);
16444
+ const absoluteManifest = resolve8(manifestFile);
16428
16445
  if (!existsSync8(absoluteManifest)) {
16429
16446
  io.stderr(`Error: Manifest not found: ${absoluteManifest}
16430
16447
  `);
@@ -16437,14 +16454,14 @@ function status(manifestFile, options, io) {
16437
16454
  return 1;
16438
16455
  }
16439
16456
  const manifest = readManifest(absoluteManifest);
16440
- const chunksDir = dirname7(absoluteManifest);
16457
+ const chunksDir = dirname6(absoluteManifest);
16441
16458
  const sourcePath = resolveSource(chunksDir, manifest.sourceFile);
16442
16459
  const selfError = chunkFolderError(chunksDir, { "source file": sourcePath });
16443
16460
  if (selfError) {
16444
16461
  io.stderr(selfError);
16445
16462
  return 1;
16446
16463
  }
16447
- if (isOutside(dirname7(chunksDir), sourcePath) && !options.yes) {
16464
+ if (isOutside(dirname6(chunksDir), sourcePath) && !options.yes) {
16448
16465
  io.stderr(outsideError(sourcePath, chunksDir, "Use --yes to read it."));
16449
16466
  return 1;
16450
16467
  }
@@ -16564,9 +16581,668 @@ ${CHUNK_HELP}`);
16564
16581
  }
16565
16582
  }
16566
16583
 
16584
+ // src/clones/index.ts
16585
+ import { join as join23, resolve as resolve9 } from "node:path";
16586
+
16587
+ // src/clones/detect.ts
16588
+ function fnv1a(text) {
16589
+ let h = 2166136261;
16590
+ for (let i2 = 0;i2 < text.length; i2++) {
16591
+ h ^= text.charCodeAt(i2);
16592
+ h = Math.imul(h, 16777619);
16593
+ }
16594
+ return h >>> 0;
16595
+ }
16596
+ function groupsBy(units, key) {
16597
+ const groups = new Map;
16598
+ units.forEach((unit, id) => {
16599
+ const ids = groups.get(key(unit));
16600
+ if (ids)
16601
+ ids.push(id);
16602
+ else
16603
+ groups.set(key(unit), [id]);
16604
+ });
16605
+ return [...groups.values()].filter((ids) => ids.length > 1);
16606
+ }
16607
+ function shinglesOf(labels, length) {
16608
+ const set = new Set;
16609
+ for (let i2 = 0;i2 + length <= labels.length; i2++) {
16610
+ set.add(fnv1a(labels.slice(i2, i2 + length).join("|")));
16611
+ }
16612
+ return set;
16613
+ }
16614
+ function nested(a, b) {
16615
+ if (a.file !== b.file)
16616
+ return false;
16617
+ const aInB = b.startIndex <= a.startIndex && a.endIndex <= b.endIndex;
16618
+ const bInA = a.startIndex <= b.startIndex && b.endIndex <= a.endIndex;
16619
+ return aInB || bInA;
16620
+ }
16621
+ function overlap(small, big) {
16622
+ let shared = 0;
16623
+ for (const item of small)
16624
+ if (big.has(item))
16625
+ shared++;
16626
+ return shared;
16627
+ }
16628
+ function round3(value) {
16629
+ return Number(value.toFixed(3));
16630
+ }
16631
+ function detect(units, params) {
16632
+ const exactGroups = groupsBy(units, (u) => u.exactHash);
16633
+ const textGroups = groupsBy(units, (u) => u.textHash);
16634
+ const shingles = units.map((u) => shinglesOf(u.labels, params.shingleLength));
16635
+ const index = new Map;
16636
+ shingles.forEach((set, id) => {
16637
+ for (const shingle of set) {
16638
+ const ids = index.get(shingle);
16639
+ if (ids)
16640
+ ids.push(id);
16641
+ else
16642
+ index.set(shingle, [id]);
16643
+ }
16644
+ });
16645
+ const near = [];
16646
+ units.forEach((unit, id) => {
16647
+ const mine = shingles[id];
16648
+ const shared = new Map;
16649
+ for (const shingle of mine) {
16650
+ const ids = index.get(shingle);
16651
+ if (ids.length > params.rareShingleLimit)
16652
+ continue;
16653
+ for (const other of ids)
16654
+ if (other > id)
16655
+ shared.set(other, (shared.get(other) ?? 0) + 1);
16656
+ }
16657
+ for (const [other, count] of shared) {
16658
+ if (count < params.minSharedShingles)
16659
+ continue;
16660
+ const peer = units[other];
16661
+ if (nested(unit, peer))
16662
+ continue;
16663
+ const theirs = shingles[other];
16664
+ const [small, big] = mine.size <= theirs.size ? [mine, theirs] : [theirs, mine];
16665
+ const inter = overlap(small, big);
16666
+ const jaccard = inter / (mine.size + theirs.size - inter);
16667
+ const contain = inter / small.size;
16668
+ const byContainment = contain >= params.containmentThreshold && small.size >= params.containmentMinShingles;
16669
+ if (jaccard < params.nearThreshold && !byContainment)
16670
+ continue;
16671
+ const common = overlap(unit.names, peer.names);
16672
+ const union = unit.names.size + peer.names.size - common;
16673
+ near.push({
16674
+ a: id,
16675
+ b: other,
16676
+ jStruct: round3(jaccard),
16677
+ contain: round3(contain),
16678
+ jNames: round3(common / (union || 1))
16679
+ });
16680
+ }
16681
+ });
16682
+ near.sort((x, y) => y.jStruct - x.jStruct || x.a - y.a || x.b - y.b);
16683
+ return { exactGroups, textGroups, near };
16684
+ }
16685
+
16686
+ // src/clones/report.ts
16687
+ function inventoryRecord(unit, id) {
16688
+ return {
16689
+ id,
16690
+ file: unit.file,
16691
+ name: unit.name,
16692
+ container: unit.container,
16693
+ kind: unit.kind,
16694
+ exported: unit.exported,
16695
+ startLine: unit.startLine,
16696
+ endLine: unit.endLine,
16697
+ loc: unit.loc,
16698
+ params: unit.params,
16699
+ async: unit.async,
16700
+ nodes: unit.nodes,
16701
+ doc: unit.doc,
16702
+ exactHash: unit.exactHash,
16703
+ textHash: unit.textHash
16704
+ };
16705
+ }
16706
+ function inventoryJsonl(units) {
16707
+ return units.map((unit, id) => `${JSON.stringify(inventoryRecord(unit, id))}
16708
+ `).join("");
16709
+ }
16710
+ function listJson(items) {
16711
+ if (items.length === 0)
16712
+ return "[]";
16713
+ return `[
16714
+ ${items.map((item) => ` ${JSON.stringify(item)}`).join(`,
16715
+ `)}
16716
+ ]`;
16717
+ }
16718
+ function clonesJson(config, detection) {
16719
+ const { out: _out, parseErrorList: _list, ...params } = config;
16720
+ return [
16721
+ "{",
16722
+ ` "params": ${JSON.stringify(params)},`,
16723
+ ` "exactGroups": ${listJson(detection.exactGroups)},`,
16724
+ ` "textGroups": ${listJson(detection.textGroups)},`,
16725
+ ` "near": ${listJson(detection.near)}`,
16726
+ "}",
16727
+ ""
16728
+ ].join(`
16729
+ `);
16730
+ }
16731
+ var SUMMARY_BANDS = [
16732
+ ["0.9 or more", 0.9, Number.POSITIVE_INFINITY],
16733
+ ["0.8 to 0.9", 0.8, 0.9],
16734
+ ["0.7 to 0.8", 0.7, 0.8],
16735
+ ["0.6 to 0.7", 0.6, 0.7],
16736
+ ["below 0.6 (containment)", 0, 0.6]
16737
+ ];
16738
+ function folderOf(file) {
16739
+ const parts2 = file.split("/").slice(0, -1);
16740
+ return parts2.length === 0 ? "." : parts2.slice(0, 2).join("/");
16741
+ }
16742
+ function percentile(sorted, p) {
16743
+ return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))] ?? 0;
16744
+ }
16745
+ function summaryText(config, stats, units, detection) {
16746
+ const sizes = units.map((u) => u.nodes).sort((a, b) => a - b);
16747
+ const inGroups = detection.exactGroups.reduce((sum, group) => sum + group.length, 0);
16748
+ const bands = SUMMARY_BANDS.map(([label, lo, hi]) => `${label}: ${detection.near.filter((p) => p.jStruct >= lo && p.jStruct < hi).length}`);
16749
+ const byFolder = new Map;
16750
+ for (const unit of units) {
16751
+ const key = folderOf(unit.file);
16752
+ byFolder.set(key, (byFolder.get(key) ?? 0) + 1);
16753
+ }
16754
+ const folders = [...byFolder.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, count]) => `${key}=${count}`).join(" ");
16755
+ const failed = stats.parseErrorFiles.length;
16756
+ const paths = [...stats.parseErrorFiles].sort((a, b) => a < b ? -1 : a > b ? 1 : 0).slice(0, config.parseErrorList);
16757
+ const errorList = failed === 0 ? [] : [
16758
+ `files with a parse error (${paths.length < failed ? "first " : ""}${paths.length} of ${failed}):`,
16759
+ ...paths.map((path) => ` ${path}`)
16760
+ ];
16761
+ const lines = [
16762
+ `files=${stats.files} parseErrors=${failed} units(minNodes=${config.minNodes})=${units.length}`,
16763
+ ...errorList,
16764
+ `nodes p50=${percentile(sizes, 0.5)} p90=${percentile(sizes, 0.9)} p99=${percentile(sizes, 0.99)} max=${sizes[sizes.length - 1] ?? 0}`,
16765
+ `exact groups=${detection.exactGroups.length} (units in them=${inGroups}) text-identical groups=${detection.textGroups.length}`,
16766
+ `near pairs=${detection.near.length} by jStruct: ${bands.join(", ")}`,
16767
+ `units by folder: ${folders}`
16768
+ ];
16769
+ return `${lines.join(`
16770
+ `)}
16771
+ `;
16772
+ }
16773
+
16774
+ // src/clones/settings.ts
16775
+ var CLONES_DEFAULTS = Object.freeze({
16776
+ src: "auto",
16777
+ out: null,
16778
+ minNodes: 50,
16779
+ shingleLength: 6,
16780
+ nearThreshold: 0.6,
16781
+ containmentThreshold: 0.9,
16782
+ containmentMinShingles: 20,
16783
+ rareShingleLimit: 60,
16784
+ minSharedShingles: 3,
16785
+ parseErrorList: 20
16786
+ });
16787
+ function mergeClonesConfig(cli, file) {
16788
+ const d = CLONES_DEFAULTS;
16789
+ const src = cli.src ?? file.src ?? d.src;
16790
+ return {
16791
+ src: src === "auto" ? src : [...src],
16792
+ out: cli.out !== undefined ? cli.out : file.out !== undefined ? file.out : d.out,
16793
+ minNodes: cli.minNodes ?? file.minNodes ?? d.minNodes,
16794
+ shingleLength: cli.shingleLength ?? file.shingleLength ?? d.shingleLength,
16795
+ nearThreshold: cli.nearThreshold ?? file.nearThreshold ?? d.nearThreshold,
16796
+ containmentThreshold: cli.containmentThreshold ?? file.containmentThreshold ?? d.containmentThreshold,
16797
+ containmentMinShingles: cli.containmentMinShingles ?? file.containmentMinShingles ?? d.containmentMinShingles,
16798
+ rareShingleLimit: cli.rareShingleLimit ?? file.rareShingleLimit ?? d.rareShingleLimit,
16799
+ minSharedShingles: cli.minSharedShingles ?? file.minSharedShingles ?? d.minSharedShingles,
16800
+ parseErrorList: cli.parseErrorList ?? file.parseErrorList ?? d.parseErrorList
16801
+ };
16802
+ }
16803
+
16804
+ // src/clones/units.ts
16805
+ import { createHash as createHash2 } from "node:crypto";
16806
+
16807
+ // src/clones/normalise.ts
16808
+ var IDENTIFIERS = new Set([
16809
+ "identifier",
16810
+ "property_identifier",
16811
+ "shorthand_property_identifier",
16812
+ "shorthand_property_identifier_pattern",
16813
+ "private_property_identifier",
16814
+ "statement_identifier",
16815
+ "undefined"
16816
+ ]);
16817
+ var LITERALS = new Set(["string", "number", "regex", "true", "false", "null"]);
16818
+ var TYPE_SYNTAX = new Set([
16819
+ "type_annotation",
16820
+ "type_arguments",
16821
+ "type_parameters",
16822
+ "type_alias_declaration",
16823
+ "interface_declaration",
16824
+ "enum_declaration",
16825
+ "ambient_declaration",
16826
+ "asserts_annotation",
16827
+ "opting_type_annotation",
16828
+ "omitting_type_annotation",
16829
+ "type_predicate_annotation",
16830
+ "accessibility_modifier",
16831
+ "override_modifier",
16832
+ "decorator",
16833
+ "comment"
16834
+ ]);
16835
+ var TRANSPARENT = new Set([
16836
+ "parenthesized_expression",
16837
+ "as_expression",
16838
+ "satisfies_expression",
16839
+ "non_null_expression",
16840
+ "type_assertion",
16841
+ "instantiation_expression"
16842
+ ]);
16843
+ var TEMPLATE_TEXT = new Set(["string_fragment", "escape_sequence"]);
16844
+ function wrapped(node) {
16845
+ const children = node.namedChildren;
16846
+ return (node.type === "type_assertion" ? children[children.length - 1] : children[0]) ?? null;
16847
+ }
16848
+ function walk2(node, out2) {
16849
+ if (!node.isNamed)
16850
+ return;
16851
+ const type = node.type;
16852
+ if (TYPE_SYNTAX.has(type))
16853
+ return;
16854
+ if (TRANSPARENT.has(type)) {
16855
+ const inner = wrapped(node);
16856
+ if (inner)
16857
+ walk2(inner, out2);
16858
+ return;
16859
+ }
16860
+ if (IDENTIFIERS.has(type)) {
16861
+ out2.labels.push("ID");
16862
+ out2.names.add(node.text.toLowerCase());
16863
+ return;
16864
+ }
16865
+ if (LITERALS.has(type)) {
16866
+ out2.labels.push("LIT");
16867
+ return;
16868
+ }
16869
+ if (TEMPLATE_TEXT.has(type)) {
16870
+ out2.labels.push("TPL");
16871
+ return;
16872
+ }
16873
+ if (type !== "template_substitution") {
16874
+ const operator = node.childForFieldName("operator");
16875
+ out2.labels.push(operator ? `${type}:${operator.type}` : type);
16876
+ }
16877
+ for (const child of node.children)
16878
+ walk2(child, out2);
16879
+ }
16880
+ function normalise(body2) {
16881
+ const out2 = { labels: [], names: new Set };
16882
+ walk2(body2, out2);
16883
+ return out2;
16884
+ }
16885
+
16886
+ // src/clones/units.ts
16887
+ var DOC_LIMIT = 160;
16888
+ var FUNCTION_VALUES = new Set([
16889
+ "arrow_function",
16890
+ "function_expression",
16891
+ "function",
16892
+ "generator_function"
16893
+ ]);
16894
+ var CLASSES = new Set(["class_declaration", "abstract_class_declaration", "class"]);
16895
+ var ANONYMOUS_CLASS = "(class)";
16896
+ function sha1(text) {
16897
+ return createHash2("sha1").update(text).digest("hex").slice(0, 16);
16898
+ }
16899
+ function keyName(key) {
16900
+ if (!key)
16901
+ return null;
16902
+ return key.type === "string" ? key.text.slice(1, -1) : key.text;
16903
+ }
16904
+ function hasToken(node, token) {
16905
+ return node.children.some((c) => !c.isNamed && c.type === token);
16906
+ }
16907
+ function anchorOf(node) {
16908
+ let anchor = node;
16909
+ if (node.type === "variable_declarator" && node.parent)
16910
+ anchor = node.parent;
16911
+ if (anchor.parent?.type === "export_statement")
16912
+ anchor = anchor.parent;
16913
+ return anchor;
16914
+ }
16915
+ function declared(node, container) {
16916
+ switch (node.type) {
16917
+ case "function_declaration":
16918
+ case "generator_function_declaration": {
16919
+ const name2 = keyName(node.childForFieldName("name"));
16920
+ if (!name2)
16921
+ return null;
16922
+ return { fn: node, name: name2, kind: container ? "local" : "function", anchor: anchorOf(node) };
16923
+ }
16924
+ case "method_definition": {
16925
+ const name2 = keyName(node.childForFieldName("name"));
16926
+ if (!name2)
16927
+ return null;
16928
+ const inClass = node.parent?.type === "class_body";
16929
+ let kind = "prop";
16930
+ if (inClass) {
16931
+ if (name2 === "constructor")
16932
+ kind = "ctor";
16933
+ else if (hasToken(node, "get"))
16934
+ kind = "get";
16935
+ else if (hasToken(node, "set"))
16936
+ kind = "set";
16937
+ else
16938
+ kind = "method";
16939
+ }
16940
+ return { fn: node, name: name2, kind, anchor: node };
16941
+ }
16942
+ case "public_field_definition": {
16943
+ const value = node.childForFieldName("value");
16944
+ const name2 = keyName(node.childForFieldName("name"));
16945
+ if (!value || !name2 || !FUNCTION_VALUES.has(value.type))
16946
+ return null;
16947
+ return { fn: value, name: name2, kind: "field", anchor: node };
16948
+ }
16949
+ case "variable_declarator": {
16950
+ const value = node.childForFieldName("value");
16951
+ const id = node.childForFieldName("name");
16952
+ if (!value || !id || id.type !== "identifier" || !FUNCTION_VALUES.has(value.type)) {
16953
+ return null;
16954
+ }
16955
+ return {
16956
+ fn: value,
16957
+ name: id.text,
16958
+ kind: container ? "local" : "function",
16959
+ anchor: anchorOf(node)
16960
+ };
16961
+ }
16962
+ case "pair": {
16963
+ const value = node.childForFieldName("value");
16964
+ const name2 = keyName(node.childForFieldName("key"));
16965
+ if (!value || !name2 || !FUNCTION_VALUES.has(value.type))
16966
+ return null;
16967
+ return { fn: value, name: name2, kind: "prop", anchor: node };
16968
+ }
16969
+ default:
16970
+ return null;
16971
+ }
16972
+ }
16973
+ function paramCount(fn) {
16974
+ const list = fn.childForFieldName("parameters");
16975
+ if (list)
16976
+ return list.namedChildren.filter((c) => c.type !== "comment").length;
16977
+ return fn.childForFieldName("parameter") ? 1 : 0;
16978
+ }
16979
+ function docOf(anchor, source) {
16980
+ const comment = anchor.previousSibling;
16981
+ if (comment?.type !== "comment" || !comment.text.startsWith("/**"))
16982
+ return "";
16983
+ if (source.slice(comment.endIndex, anchor.startIndex).trim() !== "")
16984
+ return "";
16985
+ const lines = comment.text.slice(3, -2).split(`
16986
+ `).map((line) => line.replace(/^\s*\*\s?/, "").trim());
16987
+ const tag = lines.findIndex((line) => line.startsWith("@"));
16988
+ const text = (tag === -1 ? lines : lines.slice(0, tag)).filter((line) => line !== "");
16989
+ return text.join(" ").slice(0, DOC_LIMIT);
16990
+ }
16991
+ function buildUnit(d, container, source, file, minNodes) {
16992
+ const body2 = d.fn.childForFieldName("body");
16993
+ if (!body2)
16994
+ return null;
16995
+ const { labels, names } = normalise(body2);
16996
+ if (labels.length < minNodes)
16997
+ return null;
16998
+ const startLine = d.anchor.startPosition.row + 1;
16999
+ const endLine = d.fn.endPosition.row + 1;
17000
+ return {
17001
+ file,
17002
+ name: d.name,
17003
+ container,
17004
+ kind: d.kind,
17005
+ exported: d.anchor.type === "export_statement",
17006
+ startLine,
17007
+ endLine,
17008
+ loc: endLine - startLine + 1,
17009
+ params: paramCount(d.fn),
17010
+ async: hasToken(d.fn, "async"),
17011
+ nodes: labels.length,
17012
+ doc: docOf(d.anchor, source),
17013
+ exactHash: sha1(labels.join(" ")),
17014
+ textHash: sha1(source.slice(body2.startIndex, body2.endIndex).replace(/\s+/g, "")),
17015
+ startIndex: d.fn.startIndex,
17016
+ endIndex: d.fn.endIndex,
17017
+ labels,
17018
+ names
17019
+ };
17020
+ }
17021
+ function unitsOf(root, source, file, minNodes) {
17022
+ const units = [];
17023
+ const visit = (node, container) => {
17024
+ const found = declared(node, container);
17025
+ if (found) {
17026
+ const unit = buildUnit(found, container, source, file, minNodes);
17027
+ if (unit)
17028
+ units.push(unit);
17029
+ }
17030
+ let inner = container;
17031
+ if (CLASSES.has(node.type)) {
17032
+ inner = keyName(node.childForFieldName("name")) ?? (container || ANONYMOUS_CLASS);
17033
+ }
17034
+ for (const child of node.namedChildren)
17035
+ visit(child, inner);
17036
+ };
17037
+ visit(root, "");
17038
+ return units;
17039
+ }
17040
+
17041
+ // src/clones/index.ts
17042
+ var CLONES_HELP = `Usage: repo-tools clones [root] [--out=<dir>] [--config=<path>] [--src=<path>...]
17043
+ [--min-nodes=<n>] [--shingle=<n>] [--near=<f>]
17044
+ [--containment=<f>] [--containment-min=<n>]
17045
+ [--rare=<n>] [--min-shared=<n>] [--list-errors=<n>]
17046
+
17047
+ List the functions of a TypeScript or JavaScript repository and find the functions
17048
+ that copy each other. The command only reads the repository. The result is a list
17049
+ of candidates: read both bodies of a pair before you change anything.
17050
+
17051
+ A function is a unit when it has a name and its body has at least --min-nodes
17052
+ labels. A label is one node of the body, with names, literals, types and comments
17053
+ erased. Units with the same label sequence are an exact group. Two units whose
17054
+ shingles (runs of --shingle labels) overlap enough are a near pair.
17055
+
17056
+ --out=<dir> Write inventory.jsonl and clones.json to this folder.
17057
+ Without it the command writes no file.
17058
+ --config=<path> The config file. The default is repo-tools.config.json at
17059
+ the root. The settings are in its "clones" section.
17060
+ --src=<path>... The folders to read. The word "auto" (the default) reads
17061
+ the files of the src area. The flag takes every argument
17062
+ up to the next flag.
17063
+ --min-nodes=<n> The smallest number of labels of a unit. Default 50.
17064
+ --shingle=<n> The labels in one shingle. Default 6.
17065
+ --near=<f> The smallest Jaccard score of a near pair. Default 0.6.
17066
+ --containment=<f> The smallest containment of a near pair. Default 0.9.
17067
+ --containment-min=<n> The smallest shingle count of the smaller unit for the
17068
+ containment rule. Default 20.
17069
+ --rare=<n> A shingle in more units than this does not pick candidate
17070
+ pairs. Default 60.
17071
+ --min-shared=<n> The rare shingles that a candidate pair must share.
17072
+ Default 3.
17073
+ --list-errors=<n> The most paths of files with a parse error that the
17074
+ summary lists. Default 20.
17075
+ --root=<path> The root. The default is the current folder.
17076
+ --help, -h Show this help.
17077
+
17078
+ A flag overrides the config file, and the config file overrides the default.
17079
+
17080
+ Exit codes: 0 when the run finished. 1 when the root holds no source file to read.
17081
+ 2 on a usage error, a bad config file or a root that is not a folder.
17082
+ `;
17083
+
17084
+ class NothingToRead extends Error {
17085
+ }
17086
+ var VALUE_FLAGS2 = {
17087
+ "--out": "out",
17088
+ "--min-nodes": "minNodes",
17089
+ "--shingle": "shingleLength",
17090
+ "--near": "nearThreshold",
17091
+ "--containment": "containmentThreshold",
17092
+ "--containment-min": "containmentMinShingles",
17093
+ "--rare": "rareShingleLimit",
17094
+ "--min-shared": "minSharedShingles",
17095
+ "--list-errors": "parseErrorList"
17096
+ };
17097
+ var NUMERIC = new Set([
17098
+ "minNodes",
17099
+ "shingleLength",
17100
+ "nearThreshold",
17101
+ "containmentThreshold",
17102
+ "containmentMinShingles",
17103
+ "rareShingleLimit",
17104
+ "minSharedShingles",
17105
+ "parseErrorList"
17106
+ ]);
17107
+ function parseArgs2(argv) {
17108
+ const options = { root: ".", config: undefined, settings: {} };
17109
+ const seen = new Set;
17110
+ let rootSet = false;
17111
+ const setRoot = (value) => {
17112
+ if (rootSet)
17113
+ throw new Error("the root is set twice (use --root=<path> or one positional path)");
17114
+ rootSet = true;
17115
+ options.root = value;
17116
+ };
17117
+ const once = (flag) => {
17118
+ if (seen.has(flag))
17119
+ throw new Error(`flag ${flag} is set twice`);
17120
+ seen.add(flag);
17121
+ };
17122
+ for (let i2 = 0;i2 < argv.length; i2++) {
17123
+ const arg = argv[i2] ?? "";
17124
+ if (!arg.startsWith("-")) {
17125
+ setRoot(arg);
17126
+ continue;
17127
+ }
17128
+ const eq = arg.indexOf("=");
17129
+ const name2 = eq === -1 ? arg : arg.slice(0, eq);
17130
+ if (name2 === "--src") {
17131
+ once(name2);
17132
+ const paths = [];
17133
+ if (eq !== -1)
17134
+ paths.push(arg.slice(eq + 1));
17135
+ else
17136
+ while (i2 + 1 < argv.length && !(argv[i2 + 1] ?? "").startsWith("-")) {
17137
+ paths.push(argv[++i2] ?? "");
17138
+ }
17139
+ if (paths.length === 0 || paths.some((p) => p === "")) {
17140
+ throw new Error("flag --src needs a value: --src=<path>");
17141
+ }
17142
+ const only = paths.length === 1 ? paths[0] : undefined;
17143
+ options.settings.src = checkClonesFlag(name2, "src", only === "auto" ? "auto" : paths);
17144
+ continue;
17145
+ }
17146
+ if (name2 !== "--root" && name2 !== "--config" && !Object.hasOwn(VALUE_FLAGS2, name2)) {
17147
+ throw new Error(`unknown flag '${name2}' (see repo-tools clones --help)`);
17148
+ }
17149
+ once(name2);
17150
+ let value = eq === -1 ? undefined : arg.slice(eq + 1);
17151
+ if (eq === -1 && i2 + 1 < argv.length && !(argv[i2 + 1] ?? "").startsWith("-")) {
17152
+ value = argv[++i2];
17153
+ }
17154
+ if (value === undefined || value === "") {
17155
+ throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
17156
+ }
17157
+ if (name2 === "--root")
17158
+ setRoot(value);
17159
+ else if (name2 === "--config")
17160
+ options.config = value;
17161
+ else {
17162
+ const key = VALUE_FLAGS2[name2];
17163
+ const raw = NUMERIC.has(key) ? /^\d+(\.\d+)?$/.test(value) ? Number(value) : NaN : value;
17164
+ Object.assign(options.settings, { [key]: checkClonesFlag(name2, key, raw) });
17165
+ }
17166
+ }
17167
+ return options;
17168
+ }
17169
+ function folderPrefix(path) {
17170
+ const posix = path.replace(/\\/g, "/").replace(/^(\.\/)+/, "").replace(/\/+$/, "");
17171
+ return posix === "." ? "" : posix;
17172
+ }
17173
+ function selectFiles(root, src) {
17174
+ const found = discover(root, "typescript").filter((f) => !f.path.endsWith(".d.ts"));
17175
+ if (src === "auto")
17176
+ return found.filter((f) => f.area === "src").map((f) => f.path);
17177
+ const folders = src.map(folderPrefix);
17178
+ return found.filter((f) => folders.some((d) => d === "" || f.path === d || f.path.startsWith(`${d}/`))).map((f) => f.path);
17179
+ }
17180
+ async function readUnits(root, files, minNodes) {
17181
+ const units = [];
17182
+ const parseErrorFiles = [];
17183
+ for (const file of files) {
17184
+ const source = readSource(join23(root, file)).replace(/^\uFEFF/, "");
17185
+ const grammar = grammarFor(file);
17186
+ await loadGrammar(grammar);
17187
+ const tree = parserFor(grammar).parse(source);
17188
+ if (!tree) {
17189
+ parseErrorFiles.push(file);
17190
+ continue;
17191
+ }
17192
+ try {
17193
+ if (tree.rootNode.hasError)
17194
+ parseErrorFiles.push(file);
17195
+ units.push(...unitsOf(tree.rootNode, source, file, minNodes));
17196
+ } finally {
17197
+ tree.delete();
17198
+ }
17199
+ }
17200
+ return { units, parseErrorFiles };
17201
+ }
17202
+ async function execute(root, config, io) {
17203
+ const files = selectFiles(root, config.src);
17204
+ if (files.length === 0) {
17205
+ throw new NothingToRead(config.src === "auto" ? "no TypeScript or JavaScript file in the src area (set clones.src or pass --src=<path>)" : "no TypeScript or JavaScript file below the listed folders");
17206
+ }
17207
+ const { units, parseErrorFiles } = await readUnits(root, files, config.minNodes);
17208
+ const detection = detect(units, config);
17209
+ io.stdout(summaryText(config, { files: files.length, parseErrorFiles }, units, detection));
17210
+ if (config.out !== null) {
17211
+ const out2 = resolve9(root, config.out);
17212
+ try {
17213
+ writeLf(join23(out2, "inventory.jsonl"), inventoryJsonl(units));
17214
+ writeLf(join23(out2, "clones.json"), clonesJson(config, detection));
17215
+ } catch {
17216
+ throw new Error(`cannot write the output folder '${config.out}'`);
17217
+ }
17218
+ const shown = config.out.replace(/\\/g, "/").replace(/\/+$/, "");
17219
+ io.stdout(`wrote ${shown}/inventory.jsonl
17220
+ wrote ${shown}/clones.json
17221
+ `);
17222
+ }
17223
+ return 0;
17224
+ }
17225
+ async function run4(argv, io) {
17226
+ let root;
17227
+ const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
17228
+ try {
17229
+ const options = parseArgs2(argv);
17230
+ root = resolve9(options.root);
17231
+ if (!isDirectory(root))
17232
+ throw new Error("the root <root> is not an existing directory");
17233
+ const file = loadConfigSections(root, options.config).clones;
17234
+ return await execute(root, mergeClonesConfig(options.settings, file), io);
17235
+ } catch (error) {
17236
+ const message = error instanceof Error ? error.message : String(error);
17237
+ stderr(`repo-tools clones: ${message}
17238
+ `);
17239
+ return error instanceof NothingToRead ? 1 : 2;
17240
+ }
17241
+ }
17242
+
16567
17243
  // src/compress/index.ts
16568
17244
  import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync20, statSync as statSync8, writeFileSync as writeFileSync4 } from "node:fs";
16569
- import { basename as basename8, dirname as dirname8, extname as extname4, join as join22 } from "node:path";
17245
+ import { basename as basename8, dirname as dirname7, extname as extname4, join as join24 } from "node:path";
16570
17246
 
16571
17247
  // src/compress/formats.ts
16572
17248
  import { extname as extname3 } from "node:path";
@@ -17199,7 +17875,7 @@ a value, a missing input file and two inputs without --batch are errors. No file
17199
17875
  var defaultDeps = {
17200
17876
  readdir: (dir) => readdirSync3(dir, { withFileTypes: true })
17201
17877
  };
17202
- function parseArgs2(args2) {
17878
+ function parseArgs3(args2) {
17203
17879
  const o = {
17204
17880
  input: "",
17205
17881
  inputs: [],
@@ -17322,7 +17998,7 @@ function findFiles(dir, pattern, recursive, deps = defaultDeps) {
17322
17998
  const scan = (current) => {
17323
17999
  const entries = [...deps.readdir(current)].sort((a, b) => compareCodeUnits(a.name, b.name));
17324
18000
  for (const entry of entries) {
17325
- const full = join22(current, entry.name);
18001
+ const full = join24(current, entry.name);
17326
18002
  if (entry.isDirectory()) {
17327
18003
  if (recursive && !entry.name.startsWith(".") && entry.name !== "node_modules")
17328
18004
  scan(full);
@@ -17339,16 +18015,16 @@ function isDirectory2(path) {
17339
18015
  }
17340
18016
  function compactName(file) {
17341
18017
  const ext = extname4(file);
17342
- return join22(dirname8(file), `${basename8(file, ext)}.compact${ext}`);
18018
+ return join24(dirname7(file), `${basename8(file, ext)}.compact${ext}`);
17343
18019
  }
17344
18020
  function restoredName(file) {
17345
18021
  const base = basename8(file);
17346
18022
  const stripped = base.replace(/\.compact(?=\.[^.]*$|$)/i, "");
17347
- const output = stripped === base ? file : join22(dirname8(file), stripped);
18023
+ const output = stripped === base ? file : join24(dirname7(file), stripped);
17348
18024
  if (output !== file)
17349
18025
  return output;
17350
18026
  const ext = extname4(file);
17351
- return join22(dirname8(file), `${basename8(file, ext)}.restored${ext}`);
18027
+ return join24(dirname7(file), `${basename8(file, ext)}.restored${ext}`);
17352
18028
  }
17353
18029
  function assertCanRestore(output, o) {
17354
18030
  if (!o.dryRun && !o.yes && existsSync9(output)) {
@@ -17508,10 +18184,10 @@ Level: ${o.level}
17508
18184
  }
17509
18185
  return 0;
17510
18186
  }
17511
- async function run4(argv, io, deps = defaultDeps) {
18187
+ async function run5(argv, io, deps = defaultDeps) {
17512
18188
  let o;
17513
18189
  try {
17514
- o = parseArgs2(argv);
18190
+ o = parseArgs3(argv);
17515
18191
  } catch (error) {
17516
18192
  if (!(error instanceof UsageError))
17517
18193
  throw error;
@@ -17562,16 +18238,16 @@ async function run4(argv, io, deps = defaultDeps) {
17562
18238
 
17563
18239
  // src/docs/index.ts
17564
18240
  import { lstatSync as lstatSync5, readFileSync as readFileSync23, realpathSync as realpathSync3, writeFileSync as writeFileSync5 } from "node:fs";
17565
- import { basename as basename9, isAbsolute as isAbsolute4, join as join26, relative as relative5, resolve as resolve8, sep as sep4 } from "node:path";
18241
+ import { basename as basename9, isAbsolute as isAbsolute4, join as join28, relative as relative5, resolve as resolve10, sep as sep4 } from "node:path";
17566
18242
 
17567
18243
  // src/docs/analyse.ts
17568
18244
  import { readFileSync as readFileSync22 } from "node:fs";
17569
- import { join as join24 } from "node:path";
18245
+ import { join as join26 } from "node:path";
17570
18246
 
17571
18247
  // src/docs/discovery.ts
17572
18248
  import { spawnSync as spawnSync2 } from "node:child_process";
17573
18249
  import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync21, statSync as statSync9 } from "node:fs";
17574
- import { join as join23 } from "node:path";
18250
+ import { join as join25 } from "node:path";
17575
18251
  var PYTHON_SUFFIXES2 = new Set([".py"]);
17576
18252
  var TYPESCRIPT_SUFFIXES = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
17577
18253
  var SKIP_DIRS2 = new Set([
@@ -17655,8 +18331,8 @@ function walkSource(root) {
17655
18331
  const rel = `${prefix}${entry.name}`;
17656
18332
  if (entry.isDirectory()) {
17657
18333
  if (!SKIP_DIRS2.has(entry.name))
17658
- go(join23(dir, entry.name), `${rel}/`);
17659
- } else if (isSource(rel) && statOk(join23(dir, entry.name))) {
18334
+ go(join25(dir, entry.name), `${rel}/`);
18335
+ } else if (isSource(rel) && statOk(join25(dir, entry.name))) {
17660
18336
  found.push(rel);
17661
18337
  }
17662
18338
  }
@@ -17672,7 +18348,7 @@ function statOk(path) {
17672
18348
  }
17673
18349
  }
17674
18350
  function loadExclusions(root) {
17675
- const file = join23(root, CONFIG_NAME);
18351
+ const file = join25(root, CONFIG_NAME);
17676
18352
  if (!existsSync10(file) || !statOk(file))
17677
18353
  return [];
17678
18354
  let data;
@@ -18280,13 +18956,13 @@ var DECLARATIONS = {
18280
18956
  var DECLARATION_TYPES = Object.keys(DECLARATIONS);
18281
18957
  var SPACE_CLASS = "[ \\t\\n\\r\\f\\v]";
18282
18958
  var ABSTRACT_PROPERTY = new RegExp(`\\babstract(${SPACE_CLASS}*\\??${SPACE_CLASS}*:)`, "g");
18283
- var EXPORT_TYPE_STAR = new RegExp(`(\\bexport${SPACE_CLASS}+)type(${SPACE_CLASS}*\\*)`, "g");
18959
+ var EXPORT_TYPE_STAR2 = new RegExp(`(\\bexport${SPACE_CLASS}+)type(${SPACE_CLASS}*\\*)`, "g");
18284
18960
  function sanitise(source) {
18285
- return source.replace(ABSTRACT_PROPERTY, "abstrac_$1").replace(EXPORT_TYPE_STAR, "$1 $2");
18961
+ return source.replace(ABSTRACT_PROPERTY, "abstrac_$1").replace(EXPORT_TYPE_STAR2, "$1 $2");
18286
18962
  }
18287
- var IDENTIFIERS = new Set(["identifier", "shorthand_property_identifier_pattern"]);
18963
+ var IDENTIFIERS2 = new Set(["identifier", "shorthand_property_identifier_pattern"]);
18288
18964
  function boundIdentifiers(node, text) {
18289
- if (IDENTIFIERS.has(node.type))
18965
+ if (IDENTIFIERS2.has(node.type))
18290
18966
  return [text(node)];
18291
18967
  const pattern = node.childForFieldName("pattern");
18292
18968
  if (pattern)
@@ -18304,7 +18980,7 @@ function boundIdentifiers(node, text) {
18304
18980
  case "rest_pattern":
18305
18981
  return into(node.namedChildren[0] ?? null);
18306
18982
  default: {
18307
- const first = node.namedChildren.find((c) => IDENTIFIERS.has(c.type));
18983
+ const first = node.namedChildren.find((c) => IDENTIFIERS2.has(c.type));
18308
18984
  return first ? [text(first)] : [];
18309
18985
  }
18310
18986
  }
@@ -18313,7 +18989,7 @@ function paramsOf2(node, text) {
18313
18989
  const list = node.childForFieldName("parameters");
18314
18990
  return (list?.namedChildren ?? []).flatMap((param) => boundIdentifiers(param, text));
18315
18991
  }
18316
- function anchorOf(node) {
18992
+ function anchorOf2(node) {
18317
18993
  let target = node;
18318
18994
  while (target.parent?.type === "export_statement")
18319
18995
  target = target.parent;
@@ -18470,7 +19146,7 @@ async function analyseTypeScript(path, source) {
18470
19146
  const text = (node) => source.slice(node.startIndex, node.endIndex);
18471
19147
  for (const node of tree.rootNode.descendantsOfType(DECLARATION_TYPES)) {
18472
19148
  const kind = DECLARATIONS[node.type];
18473
- const anchor = anchorOf(node);
19149
+ const anchor = anchorOf2(node);
18474
19150
  const lead = stubAnchor(anchor, source, text);
18475
19151
  const doc = leadingDoc(anchor, node, text);
18476
19152
  const dialect = dialectOf(doc);
@@ -18527,7 +19203,7 @@ async function analyseFile(root, rel) {
18527
19203
  const language = languageOf(rel);
18528
19204
  let source;
18529
19205
  try {
18530
- source = readSourceText2(join24(root, rel));
19206
+ source = readSourceText2(join26(root, rel));
18531
19207
  } catch (error) {
18532
19208
  const report = newReport(rel, language);
18533
19209
  report.error = `unreadable: ${readFailure(error)}`;
@@ -18544,7 +19220,7 @@ async function analyseRepo(root) {
18544
19220
  }
18545
19221
 
18546
19222
  // src/docs/report.ts
18547
- import { join as join25 } from "node:path";
19223
+ import { join as join27 } from "node:path";
18548
19224
  function percent3(part, whole) {
18549
19225
  if (whole === 0)
18550
19226
  return 100;
@@ -18656,8 +19332,8 @@ function symbolJson(s) {
18656
19332
  };
18657
19333
  }
18658
19334
  function writeOutputs(outDir, reports, stats, provenance, rootName) {
18659
- const markdown = join25(outDir, "COVERAGE.md");
18660
- const json = join25(outDir, "coverage.json");
19335
+ const markdown = join27(outDir, "COVERAGE.md");
19336
+ const json = join27(outDir, "coverage.json");
18661
19337
  writeReport(markdown, renderMarkdown(reports, stats, provenance, rootName));
18662
19338
  const payload = {
18663
19339
  summary: statsJson(stats),
@@ -18861,7 +19537,7 @@ Exit codes: 0 when the action succeeded and the gate passed. 1 when the gate
18861
19537
  failed, or when the root holds no source file to measure. 2 on a usage error,
18862
19538
  when the root is not a folder, or when ${CONFIG_NAME} is malformed.
18863
19539
  `;
18864
- var VALUE_FLAGS2 = {
19540
+ var VALUE_FLAGS3 = {
18865
19541
  "--root": "all",
18866
19542
  "--out": "scan",
18867
19543
  "--path": "stub",
@@ -18914,8 +19590,8 @@ function parseDocsArgs(argv) {
18914
19590
  while (i2 + 1 < argv.length && !(argv[i2 + 1] ?? "").startsWith("-"))
18915
19591
  options.paths.push(argv[++i2] ?? "");
18916
19592
  }
18917
- } else if (Object.hasOwn(VALUE_FLAGS2, name2)) {
18918
- forbid(name2, VALUE_FLAGS2[name2]);
19593
+ } else if (Object.hasOwn(VALUE_FLAGS3, name2)) {
19594
+ forbid(name2, VALUE_FLAGS3[name2]);
18919
19595
  let value = eq === -1 ? undefined : arg.slice(eq + 1);
18920
19596
  if (eq === -1 && i2 + 1 < argv.length && !(argv[i2 + 1] ?? "").startsWith("-"))
18921
19597
  value = argv[++i2];
@@ -19009,7 +19685,7 @@ async function runScan(root, options, io) {
19009
19685
  if (reports.length === 0)
19010
19686
  throw new NothingToMeasure(provenance);
19011
19687
  const stats = summarise(reports);
19012
- const outDir = resolve8(options.out ?? join26(root, "docs", "code-docs"));
19688
+ const outDir = resolve10(options.out ?? join28(root, "docs", "code-docs"));
19013
19689
  const written = writeOutputs(outDir, reports, stats, provenance, basename9(root));
19014
19690
  io.stdout(`scanned ${stats.filesScanned} file(s) via ${provenance}
19015
19691
  `);
@@ -19049,7 +19725,7 @@ async function runStub(root, options, io) {
19049
19725
  const targets = report.symbols.filter((s) => s.issues.some((i2) => i2.rule === "M1"));
19050
19726
  if (targets.length === 0)
19051
19727
  continue;
19052
- const file = join26(root, report.path);
19728
+ const file = join28(root, report.path);
19053
19729
  if (leavesRoot(root, file)) {
19054
19730
  io.stdout(` SKIPPED ${report.path}: the path is a link, or it resolves outside the root
19055
19731
  `);
@@ -19175,7 +19851,7 @@ async function runCheck(root, options, io) {
19175
19851
  `);
19176
19852
  return 1;
19177
19853
  }
19178
- async function run5(argv, io) {
19854
+ async function run6(argv, io) {
19179
19855
  if (wantsHelp(argv)) {
19180
19856
  io.stdout(DOCS_HELP);
19181
19857
  return 0;
@@ -19184,7 +19860,7 @@ async function run5(argv, io) {
19184
19860
  const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
19185
19861
  try {
19186
19862
  const options = parseDocsArgs(argv);
19187
- root = resolve8(options.root);
19863
+ root = resolve10(options.root);
19188
19864
  if (!isDirectory(root))
19189
19865
  throw new Error("the root <root> is not an existing directory");
19190
19866
  assertReadableLanguage(root);
@@ -19203,14 +19879,14 @@ async function run5(argv, io) {
19203
19879
 
19204
19880
  // src/query/index.ts
19205
19881
  import { existsSync as existsSync12, statSync as statSync10 } from "node:fs";
19206
- import { resolve as resolve9 } from "node:path";
19882
+ import { resolve as resolve11 } from "node:path";
19207
19883
 
19208
19884
  // src/query/args.ts
19209
19885
  var MODE_FLAGS = {
19210
19886
  "--emit": { name: "emit" },
19211
19887
  "--check-browser-safety": { name: "check-browser-safety" }
19212
19888
  };
19213
- var VALUE_FLAGS3 = new Set(["--root", "--config", "--out", "--node-runtime"]);
19889
+ var VALUE_FLAGS4 = new Set(["--root", "--config", "--out", "--node-runtime"]);
19214
19890
  var COMMAND_USAGE = {
19215
19891
  dependents: "<file>",
19216
19892
  "symbol-users": "<symbol>",
@@ -19291,7 +19967,7 @@ function parseQueryArgs(argv, cwd) {
19291
19967
  mode = modeCommand;
19292
19968
  continue;
19293
19969
  }
19294
- if (!VALUE_FLAGS3.has(name2)) {
19970
+ if (!VALUE_FLAGS4.has(name2)) {
19295
19971
  throw new Error(`unknown flag '${name2}' (see repo-tools query --help)`);
19296
19972
  }
19297
19973
  if (value === undefined || value === "") {
@@ -19780,12 +20456,12 @@ report, a path that is not a file of the graph, a browser-safety command on a
19780
20456
  graph that is not TypeScript/JavaScript, or a browser-safety leak with
19781
20457
  --check-browser-safety.
19782
20458
  `;
19783
- async function run6(argv, io) {
20459
+ async function run7(argv, io) {
19784
20460
  let root;
19785
20461
  const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
19786
20462
  try {
19787
20463
  const options = parseQueryArgs(argv, process.cwd());
19788
- root = resolve9(options.root);
20464
+ root = resolve11(options.root);
19789
20465
  if (!(existsSync12(root) && statSync10(root).isDirectory())) {
19790
20466
  throw new Error("the root <root> is not an existing directory");
19791
20467
  }
@@ -19821,7 +20497,7 @@ async function run6(argv, io) {
19821
20497
 
19822
20498
  // src/ste/index.ts
19823
20499
  import { existsSync as existsSync13, lstatSync as lstatSync6, readdirSync as readdirSync5, readFileSync as readFileSync26, statSync as statSync11 } from "node:fs";
19824
- import { basename as basename10, join as join28 } from "node:path";
20500
+ import { basename as basename11, join as join30, resolve as resolve13 } from "node:path";
19825
20501
 
19826
20502
  // src/ste/markdown.ts
19827
20503
  function stripNoise(text) {
@@ -19876,7 +20552,12 @@ function wordCount(sentence) {
19876
20552
  const s = sentence.replace(CODE_SPAN, "X").replace(LINK, "$1").replace(EMPHASIS_CHARS, "").replace(LIST_MARKER, "");
19877
20553
  return pySplit(s).filter((w) => Array.from(w).some(isAlnum)).length;
19878
20554
  }
19879
- function join27(parts2, start2) {
20555
+ var LIST_JOINS = new Set(["and", "or"]);
20556
+ function isIdentifierList(sentence) {
20557
+ const s = sentence.replace(CODE_SPAN, " ").replace(LINK, "$1").replace(LIST_MARKER, "");
20558
+ return pySplit(s).map((w) => Array.from(w).filter(isAlnum).join("").toLowerCase()).filter((w) => w !== "").every((w) => LIST_JOINS.has(w));
20559
+ }
20560
+ function join29(parts2, start2) {
19880
20561
  const chunks = [];
19881
20562
  const lineMap = [];
19882
20563
  parts2.forEach(([n, body2], i2) => {
@@ -19906,7 +20587,7 @@ function paragraphs(text) {
19906
20587
  for (const [n, line] of stripNoise(text)) {
19907
20588
  const startsItem = LIST_ITEM.test(line);
19908
20589
  if ((n !== prevLine + 1 || startsItem) && para.length > 0) {
19909
- blocks.push(join27(para, paraStart));
20590
+ blocks.push(join29(para, paraStart));
19910
20591
  para = [];
19911
20592
  }
19912
20593
  if (para.length === 0)
@@ -19915,42 +20596,112 @@ function paragraphs(text) {
19915
20596
  prevLine = n;
19916
20597
  }
19917
20598
  if (para.length > 0)
19918
- blocks.push(join27(para, paraStart));
20599
+ blocks.push(join29(para, paraStart));
19919
20600
  return blocks;
19920
20601
  }
19921
- function checkMarkdown(name2, text) {
19922
- const problems = [];
20602
+ function checkMarkdownFindings(name2, text) {
20603
+ const found = [];
19923
20604
  const blocks = paragraphs(text);
19924
- for (const [start2, block] of blocks) {
20605
+ for (const [start2, block, lineMap] of blocks) {
19925
20606
  const procedural = PROCEDURAL_STARTS.some((p) => pyLstrip(block).startsWith(p));
19926
20607
  const limit = procedural ? MAX_WORDS_PROCEDURAL : MAX_WORDS_DESCRIPTIVE;
20608
+ const last = lineMap.length > 0 ? lineMap[lineMap.length - 1] : start2;
19927
20609
  for (const sentence of sentences(block)) {
19928
20610
  const n = wordCount(sentence);
19929
- if (n > limit) {
20611
+ if (n > limit && !isIdentifierList(sentence)) {
19930
20612
  const tier = procedural ? "procedural" : "descriptive";
19931
- problems.push(`${name2}:~${start2}: ${n} words (>${limit} ${tier}): ${headCodePoints(pyStrip(sentence), 90)}`);
20613
+ found.push({
20614
+ text: `${name2}:~${start2}: ${n} words (>${limit} ${tier}): ${headCodePoints(pyStrip(sentence), 90)}`,
20615
+ first: start2,
20616
+ last
20617
+ });
19932
20618
  }
19933
20619
  }
19934
20620
  }
19935
20621
  for (const [, block, lineMap] of blocks) {
19936
20622
  const bare = proseOnly(block);
20623
+ const at = (index, rest) => {
20624
+ const line = lineAt2(index, lineMap);
20625
+ return { text: `${name2}:${line}: ${rest}`, first: line, last: line };
20626
+ };
19937
20627
  for (const [phrase, better, re] of WORDY_RULES) {
19938
20628
  for (const m of bare.matchAll(re)) {
19939
- problems.push(`${name2}:${lineAt2(m.index, lineMap)}: wordy: ${pyRepr(phrase)} - prefer ${pyRepr(better)}`);
20629
+ found.push(at(m.index, `wordy: ${pyRepr(phrase)} - prefer ${pyRepr(better)}`));
19940
20630
  }
19941
20631
  }
19942
20632
  for (const m of bare.matchAll(AMBIGUOUS2)) {
19943
- problems.push(`${name2}:${lineAt2(m.index, lineMap)}: ambiguous reference: ${pyRepr(pyStrip(m[0]))}`);
20633
+ found.push(at(m.index, `ambiguous reference: ${pyRepr(pyStrip(m[0]))}`));
19944
20634
  }
19945
20635
  for (const m of bare.matchAll(PASSIVE2)) {
19946
- problems.push(`${name2}:${lineAt2(m.index, lineMap)}: passive voice: ${pyRepr(pyStrip(m[0]))}`);
20636
+ found.push(at(m.index, `passive voice: ${pyRepr(pyStrip(m[0]))}`));
19947
20637
  }
19948
20638
  }
19949
- return problems;
20639
+ return found;
20640
+ }
20641
+
20642
+ // src/ste/since.ts
20643
+ import { spawnSync as spawnSync3 } from "node:child_process";
20644
+ import { basename as basename10, dirname as dirname8, resolve as resolve12 } from "node:path";
20645
+ var GIT_TIMEOUT_MS = 60000;
20646
+
20647
+ class SinceError extends Error {
20648
+ }
20649
+ function git(dir, args2) {
20650
+ return spawnSync3("git", ["-C", dir, ...args2], {
20651
+ stdio: ["ignore", "pipe", "pipe"],
20652
+ timeout: GIT_TIMEOUT_MS,
20653
+ maxBuffer: 1 << 28
20654
+ });
20655
+ }
20656
+ var checked = new Set;
20657
+ function checkRevision(dir, rev) {
20658
+ const key = `${dir}\x00${rev}`;
20659
+ if (checked.has(key))
20660
+ return;
20661
+ const tree = git(dir, ["rev-parse", "--is-inside-work-tree"]);
20662
+ if (tree.error)
20663
+ throw new SinceError(`--since needs git, and git cannot run: ${tree.error.message}`);
20664
+ if (tree.status !== 0)
20665
+ throw new SinceError(`--since needs a git work tree: ${dir}`);
20666
+ const commit = git(dir, ["rev-parse", "--verify", "--quiet", `${rev}^{commit}`]);
20667
+ if (commit.status !== 0)
20668
+ throw new SinceError(`cannot resolve revision '${rev}' in ${dir}`);
20669
+ checked.add(key);
20670
+ }
20671
+ var HUNK = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gmu;
20672
+ function addedLinesSince(file, rev) {
20673
+ const full = resolve12(file);
20674
+ const dir = dirname8(full);
20675
+ const name2 = basename10(full);
20676
+ checkRevision(dir, rev);
20677
+ if (git(dir, ["cat-file", "-e", `${rev}:./${name2}`]).status !== 0)
20678
+ return "all";
20679
+ const diff = git(dir, ["diff", "-U0", "--no-color", "--no-ext-diff", rev, "--", name2]);
20680
+ if (diff.status !== 0) {
20681
+ throw new SinceError(`git diff failed for ${name2}: ${diff.stderr.toString().trim()}`);
20682
+ }
20683
+ const ranges = [];
20684
+ const cuts = [];
20685
+ for (const m of diff.stdout.toString().matchAll(HUNK)) {
20686
+ const first = Number(m[1]);
20687
+ const count = m[2] === undefined ? 1 : Number(m[2]);
20688
+ if (count > 0)
20689
+ ranges.push([first, first + count - 1]);
20690
+ else
20691
+ cuts.push(first);
20692
+ }
20693
+ return { ranges, cuts };
20694
+ }
20695
+ function touchesAdded(added, first, last) {
20696
+ if (added === "all")
20697
+ return true;
20698
+ if (added.ranges.some(([from, to]) => from <= last && first <= to))
20699
+ return true;
20700
+ return added.cuts.some((line) => first <= line && line + 1 <= last);
19950
20701
  }
19951
20702
 
19952
20703
  // src/ste/index.ts
19953
- var STE_HELP = `Usage: repo-tools ste <file-or-directory> [...]
20704
+ var STE_HELP = `Usage: repo-tools ste <file-or-directory> [...] [--require=<path>] [--since=<rev>]
19954
20705
  repo-tools ste --prose <file>
19955
20706
 
19956
20707
  Check prose against the mechanically decidable part of ASD-STE100 Simplified
@@ -19960,11 +20711,25 @@ cannot judge the approved dictionary.
19960
20711
 
19961
20712
  Markdown mode (the default) checks each Markdown file, or each .md file below a
19962
20713
  directory, in code-unit order of the path. A descriptive sentence may have 25
19963
- words; a procedural one (a numbered step or a check box) may have 20. Code
19964
- fences, tables, headings, quotes, comments, frontmatter, inline code and
19965
- identifiers are not checked. A file whose first five lines hold
20714
+ words; a procedural one (a numbered step or a check box) may have 20. A list of
20715
+ identifiers with no prose word is not a sentence, so the word limit does not
20716
+ apply to it. Code fences, tables, headings, quotes, comments and frontmatter are
20717
+ not checked. Inline code and identifiers are not checked by the wordy, ambiguous
20718
+ and passive rules, and each one counts as a word. A file whose first five lines
20719
+ hold
19966
20720
  "<!-- ste:historical-record -->" is skipped, and the skip is counted.
19967
20721
 
20722
+ --require=<path> The file must exist, and the run must examine it. Repeat the
20723
+ option for more files. A document that someone deletes inside
20724
+ a folder gives no finding, so this option makes the deletion
20725
+ an error. A file that the run skips as a historical record
20726
+ counts as examined.
20727
+ --since=<rev> Report only the findings on the lines that the working tree
20728
+ adds or changes since the git revision <rev> (committed,
20729
+ staged and unstaged edits). A sentence-length finding counts
20730
+ when any line of its paragraph changed. A file that is not in
20731
+ <rev> counts in full. The totals line gives the number of
20732
+ older findings that the option hides.
19968
20733
  --prose <file> Check the file as one docstring. Tag lines (@param), section
19969
20734
  headers, doctest lines and code fences are removed first. Each
19970
20735
  finding prints as "RULE<TAB>detail". The findings are advice:
@@ -19973,7 +20738,9 @@ identifiers are not checked. A file whose first five lines hold
19973
20738
 
19974
20739
  Exit codes: 0 when no finding (Markdown mode) or always (--prose mode). 1 when a
19975
20740
  Markdown finding exists. 2 on a usage error, when a path does not exist, when no
19976
- Markdown file was examined, or when a file is not UTF-8.
20741
+ Markdown file was examined, when a required file does not exist or was not
20742
+ examined, when --since cannot use the revision or the work tree, or when a file
20743
+ is not UTF-8.
19977
20744
  `;
19978
20745
  var HISTORICAL_MARKER = "<!-- ste:historical-record -->";
19979
20746
  function readText(path) {
@@ -20002,7 +20769,7 @@ function targetsFor(path) {
20002
20769
  return;
20003
20770
  }
20004
20771
  for (const entry of entries) {
20005
- const full = join28(dir, entry.name);
20772
+ const full = join30(dir, entry.name);
20006
20773
  if (entry.isDirectory()) {
20007
20774
  if (entry.name !== ".pytest_cache")
20008
20775
  walk(full, [...parts2, entry.name]);
@@ -20015,7 +20782,7 @@ function targetsFor(path) {
20015
20782
  };
20016
20783
  if (existsSync13(path) && lstatSync6(path).isDirectory())
20017
20784
  walk(path, []);
20018
- return found.sort(comparePaths).map((parts2) => join28(path, ...parts2));
20785
+ return found.sort(comparePaths).map((parts2) => join30(path, ...parts2));
20019
20786
  }
20020
20787
  function isHistoricalRecord(path) {
20021
20788
  let text;
@@ -20037,7 +20804,7 @@ function runProse(args2, io) {
20037
20804
  try {
20038
20805
  text = readText(file);
20039
20806
  } catch {
20040
- io.stderr(`repo-tools ste: cannot read ${basename10(file)} as UTF-8 text
20807
+ io.stderr(`repo-tools ste: cannot read ${basename11(file)} as UTF-8 text
20041
20808
  `);
20042
20809
  return 2;
20043
20810
  }
@@ -20046,10 +20813,41 @@ function runProse(args2, io) {
20046
20813
  `);
20047
20814
  return 0;
20048
20815
  }
20049
- async function run7(argv, io) {
20816
+ function parseMarkdownArgs(argv) {
20817
+ const args2 = { paths: [], required: [], since: null };
20818
+ for (const arg of argv) {
20819
+ if (arg === "--require" || arg.startsWith("--require=")) {
20820
+ const value = arg.slice("--require=".length);
20821
+ if (arg === "--require" || value === "")
20822
+ return "--require needs a path (--require=<path>)";
20823
+ args2.required.push(value);
20824
+ } else if (arg === "--since" || arg.startsWith("--since=")) {
20825
+ const value = arg.slice("--since=".length);
20826
+ if (arg === "--since" || value === "" || value.startsWith("-")) {
20827
+ return "--since needs a revision (--since=<rev>)";
20828
+ }
20829
+ args2.since = value;
20830
+ } else {
20831
+ args2.paths.push(arg);
20832
+ }
20833
+ }
20834
+ return args2;
20835
+ }
20836
+ function pathKey(path) {
20837
+ const full = resolve13(path);
20838
+ return process.platform === "win32" ? full.toLowerCase() : full;
20839
+ }
20840
+ async function run8(argv, io) {
20050
20841
  if (argv[0] === "--prose")
20051
20842
  return runProse(argv.slice(1), io);
20052
- if (argv.length === 0) {
20843
+ const parsed = parseMarkdownArgs(argv);
20844
+ if (typeof parsed === "string") {
20845
+ io.stderr(`repo-tools ste: ${parsed}
20846
+ `);
20847
+ return 2;
20848
+ }
20849
+ const { paths, required, since } = parsed;
20850
+ if (paths.length === 0) {
20053
20851
  io.stdout(`usage: repo-tools ste <file-or-directory> [...]
20054
20852
  `);
20055
20853
  return 2;
@@ -20057,12 +20855,15 @@ async function run7(argv, io) {
20057
20855
  let total = 0;
20058
20856
  let checked = 0;
20059
20857
  let skipped = 0;
20858
+ let hidden = 0;
20060
20859
  const missing = [];
20061
- for (const arg of argv) {
20860
+ const examined = new Set;
20861
+ for (const arg of paths) {
20062
20862
  if (!existsSync13(arg))
20063
20863
  missing.push(arg);
20064
20864
  for (const target of targetsFor(arg)) {
20065
- const name2 = basename10(target);
20865
+ const name2 = basename11(target);
20866
+ examined.add(pathKey(target));
20066
20867
  if (isHistoricalRecord(target)) {
20067
20868
  skipped += 1;
20068
20869
  io.stdout(`
@@ -20078,30 +20879,62 @@ async function run7(argv, io) {
20078
20879
  `);
20079
20880
  return 2;
20080
20881
  }
20081
- const problems = checkMarkdown(name2, text);
20882
+ let findings = checkMarkdownFindings(name2, text);
20883
+ if (since !== null) {
20884
+ try {
20885
+ const added = addedLinesSince(target, since);
20886
+ const shown = findings.filter((f) => touchesAdded(added, f.first, f.last));
20887
+ hidden += findings.length - shown.length;
20888
+ findings = shown;
20889
+ } catch (error) {
20890
+ if (!(error instanceof SinceError))
20891
+ throw error;
20892
+ io.stderr(`repo-tools ste: ${error.message}
20893
+ `);
20894
+ return 2;
20895
+ }
20896
+ }
20082
20897
  checked += 1;
20083
- total += problems.length;
20084
- const status = problems.length === 0 ? "OK" : `${problems.length} problem(s)`;
20898
+ total += findings.length;
20899
+ const status = findings.length === 0 ? "OK" : `${findings.length} problem(s)`;
20085
20900
  io.stdout(`
20086
20901
  === ${name2}: ${status} ===
20087
20902
  `);
20088
- for (const p of problems)
20089
- io.stdout(` ${p}
20903
+ for (const f of findings)
20904
+ io.stdout(` ${f.text}
20090
20905
  `);
20091
20906
  }
20092
20907
  }
20093
- const tail = skipped > 0 ? `; ${skipped} skipped as historical record(s)` : "";
20908
+ const notes = [];
20909
+ if (skipped > 0)
20910
+ notes.push(`${skipped} skipped as historical record(s)`);
20911
+ if (since !== null && hidden > 0) {
20912
+ notes.push(`${hidden} older finding(s) hidden by --since=${since}`);
20913
+ }
20914
+ const tail = notes.length > 0 ? `; ${notes.join("; ")}` : "";
20094
20915
  io.stdout(`
20095
20916
  TOTAL: ${total} problem(s) in ${checked} file(s)${tail}
20096
20917
  `);
20097
20918
  for (const path of missing)
20098
20919
  io.stderr(`ERROR: path does not exist: ${path}
20099
20920
  `);
20921
+ let requiredFailed = false;
20922
+ for (const path of required) {
20923
+ if (!existsSync13(path) || !statSync11(path).isFile()) {
20924
+ io.stderr(`ERROR: required file does not exist: ${path}
20925
+ `);
20926
+ requiredFailed = true;
20927
+ } else if (!examined.has(pathKey(path))) {
20928
+ io.stderr(`ERROR: required file was not examined: ${path}
20929
+ `);
20930
+ requiredFailed = true;
20931
+ }
20932
+ }
20100
20933
  if (checked === 0) {
20101
20934
  io.stderr(`ERROR: no Markdown file was examined. This is a failure, not a clean result -- check the path(s) given.
20102
20935
  `);
20103
20936
  }
20104
- if (missing.length > 0 || checked === 0)
20937
+ if (missing.length > 0 || checked === 0 || requiredFailed)
20105
20938
  return 2;
20106
20939
  return total > 0 ? 1 : 0;
20107
20940
  }
@@ -20128,7 +20961,7 @@ ${MAP_HELP}`,
20128
20961
  docs: {
20129
20962
  summary: "Measure, stub and gate the doc comments of TypeScript, JavaScript and Python source.",
20130
20963
  help: DOCS_HELP,
20131
- run: run5
20964
+ run: run6
20132
20965
  },
20133
20966
  chunk: {
20134
20967
  summary: "Split a large file into chunks, merge the chunks back, or show changed chunks.",
@@ -20138,17 +20971,22 @@ ${MAP_HELP}`,
20138
20971
  compress: {
20139
20972
  summary: "Write a compact copy of a file for a model context, or restore it.",
20140
20973
  help: HELP,
20141
- run: run4
20974
+ run: run5
20142
20975
  },
20143
20976
  query: {
20144
20977
  summary: "Answer structural questions from the depgraph reports, or write derived reports.",
20145
20978
  help: QUERY_HELP,
20146
- run: run6
20979
+ run: run7
20147
20980
  },
20148
20981
  ste: {
20149
20982
  summary: "Check Markdown or docstring prose against Simplified Technical English.",
20150
20983
  help: STE_HELP,
20151
- run: run7
20984
+ run: run8
20985
+ },
20986
+ clones: {
20987
+ summary: "List the functions of a repository and find the functions that copy each other.",
20988
+ help: CLONES_HELP,
20989
+ run: run4
20152
20990
  }
20153
20991
  };
20154
20992
  var SUBCOMMANDS = Object.keys(REGISTRY);