@danielsimonjr/repo-tools 2.3.0 → 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 +79 -0
  2. package/README.md +26 -3
  3. package/dist/cli.js +1331 -1174
  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.3.0",
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,894 +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");
854
- case "count":
855
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
856
- throw fail(`'${key}' must be a whole number of 1 or more`);
857
- }
858
- return value;
859
- case "fraction":
860
- if (typeof value !== "number" || !(value >= 0 && value <= 1)) {
861
- throw fail(`'${key}' must be a number from 0 to 1`);
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);
862
998
  }
863
- return value;
864
- }
865
- }
866
- function checkSection(prefix, object, keys, fail) {
867
- const out2 = {};
868
- for (const [name2, value] of Object.entries(object)) {
869
- const kind = keys[name2];
870
- if (kind === undefined)
871
- throw fail(`unknown key '${prefix}${name2}'`);
872
- out2[name2] = checkValue(`${prefix}${name2}`, kind, value, fail);
999
+ }
1000
+ if (def)
1001
+ names.add(def);
1002
+ if (ns)
1003
+ names.add(ns);
873
1004
  }
874
- return out2;
1005
+ return names;
875
1006
  }
876
- var QUERY_KEYS = {
877
- out: "path",
878
- nodeRuntimes: "names"
879
- };
880
- var CLONES_KEYS = {
881
- src: "src",
882
- out: "path-or-null",
883
- minNodes: "count",
884
- shingleLength: "count",
885
- nearThreshold: "fraction",
886
- containmentThreshold: "fraction",
887
- containmentMinShingles: "count",
888
- rareShingleLimit: "count",
889
- minSharedShingles: "count"
890
- };
891
- function checkClonesFlag(label, key, value) {
892
- return checkValue(label, CLONES_KEYS[key], value, (message) => new Error(message));
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] ?? "");
893
1011
  }
894
- function parseConfigSections(parsed, label) {
895
- const fail = (message) => new Error(`config ${label}: ${message}`);
896
- if (!isObject(parsed))
897
- throw fail("the file must hold a JSON object");
898
- for (const key of Object.keys(parsed)) {
899
- if (key === "$schema")
900
- continue;
901
- if (key !== "depgraph" && key !== "map" && key !== "query" && key !== "clones") {
902
- 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
+ }
903
1025
  }
904
1026
  }
905
- if (parsed.map !== undefined && parsed.depgraph !== undefined) {
906
- throw fail("use the section 'map' or its old name 'depgraph', not both");
907
- }
908
- const sectionName = parsed.map !== undefined ? "map" : "depgraph";
909
- const sections = { depgraph: {}, query: {}, clones: {} };
910
- const section = parsed[sectionName];
911
- if (section !== undefined) {
912
- if (!isObject(section))
913
- throw fail(`'${sectionName}' must be an object`);
914
- const { apiSurface, ...rest } = section;
915
- sections.depgraph = checkSection(`${sectionName}.`, rest, DEPGRAPH_KEYS, fail);
916
- if (apiSurface !== undefined) {
917
- if (!isObject(apiSurface))
918
- throw fail(`'${sectionName}.apiSurface' must be an object`);
919
- 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
+ }
920
1043
  }
921
1044
  }
922
- const query = parsed.query;
923
- if (query !== undefined) {
924
- if (!isObject(query))
925
- throw fail("'query' must be an object");
926
- sections.query = checkSection("query.", query, QUERY_KEYS, fail);
927
- }
928
- const clones = parsed.clones;
929
- if (clones !== undefined) {
930
- if (!isObject(clones))
931
- throw fail("'clones' must be an object");
932
- sections.clones = checkSection("clones.", clones, CLONES_KEYS, fail);
933
- }
934
- return sections;
1045
+ return byName;
935
1046
  }
936
- function loadConfigSections(root, configPath) {
937
- if (configPath !== undefined && isAbsolutePath(configPath)) {
938
- throw new Error(`flag --config ${ABSOLUTE_PATH_TEXT}`);
939
- }
940
- const rel = configPath ?? CONFIG_FILE;
941
- const label = `<root>/${rel.replace(/\\/g, "/")}`;
942
- const path = resolveUnderRoot(root, rel);
943
- if (configPath === undefined && !existsSync2(path))
944
- return { depgraph: {}, query: {}, clones: {} };
945
- let text;
946
- try {
947
- text = readFileSync(path, "utf8");
948
- } catch {
949
- throw new Error(`config ${label}: the file cannot be read`);
950
- }
951
- let parsed;
952
- try {
953
- parsed = JSON.parse(text);
954
- } catch {
955
- throw new Error(`config ${label}: the file is not valid JSON`);
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 };
1051
+ }
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));
956
1090
  }
957
- return parseConfigSections(parsed, label);
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;
958
1093
  }
959
- function loadConfigFile(root, configPath) {
960
- return loadConfigSections(root, configPath).depgraph;
1094
+ function tallyByTag(entries) {
1095
+ const tally = {
1096
+ TRUE_DUPLICATE: 0,
1097
+ DISPATCH_VARIANT: 0,
1098
+ ALIAS_DELEGATION: 0,
1099
+ ALLOWLISTED: 0
1100
+ };
1101
+ for (const e of entries)
1102
+ tally[e.tag]++;
1103
+ return tally;
961
1104
  }
962
- function mergeQueryConfig(cli, file) {
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
+ };
963
1120
  return {
964
- out: cli.out ?? file.query.out ?? file.depgraph.out ?? OUTPUT_SUBDIR,
965
- nodeRuntimes: cli.nodeRuntimes ?? file.query.nodeRuntimes ?? []
1121
+ runtime: buildDuplicateEntries(collectOwnDefiners(files, RUNTIME_DUP_CATEGORIES), publicSurface, allowlist, readRaw, dispatchCalls),
1122
+ types: buildDuplicateEntries(collectOwnDefiners(files, TYPE_DUP_CATEGORIES), publicSurface, allowlist, readRaw, dispatchCalls)
966
1123
  };
967
1124
  }
968
- var MAP_REGENERATE_COMMAND = "repo-tools map";
969
- function mergeMapConfig(cli, file) {
970
- const merged = mergeDepgraphConfig(cli, file);
971
- const inDocs = (name2) => `${OUTPUT_SUBDIR}/${name2}`;
1125
+ function buildDuplicateReport(dup, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
1126
+ const runtimeByTag = tallyByTag(dup.runtime);
1127
+ const typeByTag = tallyByTag(dup.types);
972
1128
  return {
973
- ...merged,
974
- duplicateAllowlist: cli.duplicateAllowlist ?? file.duplicateAllowlist ?? inDocs("duplicate-allowlist.json"),
975
- duplicateBaseline: cli.duplicateBaseline ?? file.duplicateBaseline ?? inDocs("duplicate-baseline.json"),
976
- coveragePolicy: cli.coveragePolicy ?? file.coveragePolicy ?? inDocs("coverage-policy.json"),
977
- regenerateCommand: cli.regenerateCommand ?? file.regenerateCommand ?? MAP_REGENERATE_COMMAND
1129
+ note: duplicateSymbolsNote(dispatchCalls),
1130
+ summary: {
1131
+ runtimeDuplicates: runtimeByTag.TRUE_DUPLICATE,
1132
+ typeDuplicates: typeByTag.TRUE_DUPLICATE,
1133
+ runtimeByTag,
1134
+ typeByTag
1135
+ },
1136
+ runtime: dup.runtime,
1137
+ types: dup.types
978
1138
  };
979
1139
  }
980
- function mergeDepgraphConfig(cli, file) {
981
- const out2 = cli.out ?? file.out ?? OUTPUT_SUBDIR;
982
- const inOut = (name2) => `${out2.replace(/[\\/]+$/, "")}/${name2}`;
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;
1147
+ }
1148
+ function buildDuplicateBaseline(report) {
983
1149
  return {
984
- src: cli.src ?? file.src ?? "auto",
985
- tests: cli.tests ?? file.tests ?? [...TEST_DIR_NAMES],
986
- out: out2,
987
- exclude: cli.exclude ?? file.exclude ?? [...DEFAULT_EXCLUDE],
988
- alsoExclude: cli.alsoExclude ?? file.alsoExclude ?? [],
989
- strictOrphans: cli.strictOrphans ?? file.strictOrphans ?? false,
990
- duplicateAllowlist: cli.duplicateAllowlist ?? file.duplicateAllowlist ?? inOut("duplicate-allowlist.json"),
991
- duplicateBaseline: cli.duplicateBaseline ?? file.duplicateBaseline ?? inOut("duplicate-baseline.json"),
992
- coveragePolicy: cli.coveragePolicy ?? file.coveragePolicy ?? inOut("coverage-policy.json"),
993
- regenerateCommand: cli.regenerateCommand ?? file.regenerateCommand ?? DEFAULT_REGENERATE_COMMAND,
994
- verificationMarker: cli.verificationMarker !== undefined ? cli.verificationMarker : file.verificationMarker !== undefined ? file.verificationMarker : VERIFICATION_MARKER,
995
- apiSurface: {
996
- out: cli.apiSurface?.out !== undefined ? cli.apiSurface.out : file.apiSurface?.out !== undefined ? file.apiSurface.out : null,
997
- entry: cli.apiSurface?.entry ?? file.apiSurface?.entry ?? DEFAULT_API_ENTRY,
998
- stabilityTags: cli.apiSurface?.stabilityTags ?? file.apiSurface?.stabilityTags ?? [...DEFAULT_STABILITY_TAGS]
999
- },
1000
- extensions: cli.extensions ?? file.extensions ?? []
1150
+ note: DUPLICATE_BASELINE_NOTE,
1151
+ runtime: trueDuplicateNames(report.runtime),
1152
+ types: trueDuplicateNames(report.types)
1001
1153
  };
1002
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
+ }
1163
+ }
1164
+ return found;
1165
+ }
1166
+ function isRecord(value) {
1167
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1168
+ }
1169
+ function asDuplicateEntries(parsed) {
1170
+ if (!isRecord(parsed) || !Array.isArray(parsed.runtime) || !Array.isArray(parsed.types)) {
1171
+ return null;
1172
+ }
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;
1177
+ }
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;
1185
+ }
1003
1186
 
1004
- // src/depgraph/args.ts
1005
- var SCAN_SCOPE_FLAGS = {
1006
- "--src": "the census finds the source files itself; the area of a file comes from its path",
1007
- "--tests": "a test file is found by its name or by its test/ or tests/ folder",
1008
- "--exclude": "the census reads the files that git tracks; list a file in .gitignore instead",
1009
- "--also-exclude": "the census reads the files that git tracks; list a file in .gitignore instead",
1010
- "--all": "the graph always holds every file; the disposition of each file shows reachability",
1011
- "-a": "the graph always holds every file; the disposition of each file shows reachability",
1012
- "--reachable-only": "the graph always holds every file; the disposition shows reachability",
1013
- "--include-tests": "the test coverage reports are always written",
1014
- "-t": "the test coverage reports are always written"
1187
+ // src/depgraph/exclude.ts
1188
+ import { statSync } from "node:fs";
1189
+ import { join as join4 } from "node:path";
1190
+
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 })
1015
1197
  };
1016
- function scanScopeText(flag) {
1017
- return `flag ${flag} has no effect in 2.0.0: ${SCAN_SCOPE_FLAGS[flag]}`;
1198
+ function listNames(dir) {
1199
+ return sortCodeUnits(readdirHook.names(dir));
1018
1200
  }
1019
- var BOOLEAN_FLAGS = {
1020
- "--all": "all",
1021
- "-a": "all",
1022
- "--include-tests": "includeTests",
1023
- "-t": "includeTests",
1024
- "--reachable-only": "reachableOnly",
1025
- "--strict-orphans": "strictOrphans",
1026
- "--strict-census": "strictCensus",
1027
- "--check-census": "checkCensus",
1028
- "--check-duplicates": "checkDuplicates",
1029
- "--no-regen": "noRegen",
1030
- "--write-duplicate-baseline": "writeDuplicateBaseline",
1031
- "--no-extensions": "noExtensions",
1032
- "--help": "help",
1033
- "-h": "help"
1034
- };
1035
- var VALUE_FLAGS = {
1036
- "--root": (_o, value, setRoot) => setRoot(value),
1037
- "--config": (o, value) => {
1038
- o.config = relativePath("--config", value);
1039
- },
1040
- "--src": (o, value) => {
1041
- o.settings.src = value === "auto" ? "auto" : pathList("--src", value);
1042
- },
1043
- "--tests": (o, value) => {
1044
- o.settings.tests = pathList("--tests", value);
1045
- },
1046
- "--out": (o, value) => {
1047
- o.settings.out = relativePath("--out", value);
1048
- },
1049
- "--exclude": (o, value) => {
1050
- o.settings.exclude = list("--exclude", value);
1051
- },
1052
- "--also-exclude": (o, value) => {
1053
- o.settings.alsoExclude = list("--also-exclude", value);
1054
- },
1055
- "--api-surface": (o, value) => {
1056
- o.settings.apiSurface = { ...o.settings.apiSurface, out: relativePath("--api-surface", value) };
1057
- },
1058
- "--api-entry": (o, value) => {
1059
- o.settings.apiSurface = { ...o.settings.apiSurface, entry: relativePath("--api-entry", value) };
1060
- },
1061
- "--stability-tags": (o, value) => {
1062
- o.settings.apiSurface = {
1063
- ...o.settings.apiSurface,
1064
- stabilityTags: list("--stability-tags", value)
1065
- };
1066
- }
1067
- };
1068
- function list(flag, value) {
1069
- const items = value.split(",");
1070
- if (items.some((item) => item === ""))
1071
- throw new Error(`flag ${flag} holds an empty item`);
1072
- return items;
1073
- }
1074
- function pathList(flag, value) {
1075
- return list(flag, value).map((item) => relativePath(flag, item));
1076
- }
1077
- function relativePath(flag, value) {
1078
- if (isAbsolutePath(value)) {
1079
- throw new Error(`flag ${flag} ${ABSOLUTE_PATH_TEXT}`);
1080
- }
1081
- return value;
1082
- }
1083
- function parseDepgraphArgs(argv, cwd, command) {
1084
- const options = {
1085
- root: cwd,
1086
- settings: {},
1087
- includeTests: false,
1088
- all: false,
1089
- reachableOnly: false,
1090
- strictOrphans: false,
1091
- strictCensus: false,
1092
- checkCensus: false,
1093
- checkDuplicates: false,
1094
- noRegen: false,
1095
- writeDuplicateBaseline: false,
1096
- noExtensions: false,
1097
- help: false,
1098
- ignoredFlags: []
1099
- };
1100
- let rootSet = false;
1101
- const setRoot = (value) => {
1102
- if (rootSet)
1103
- throw new Error("the root is set twice (use --root=<path> or one positional path)");
1104
- rootSet = true;
1105
- options.root = value;
1106
- };
1107
- for (const arg of argv) {
1108
- if (!arg.startsWith("-")) {
1109
- setRoot(arg);
1110
- continue;
1111
- }
1112
- const eq = arg.indexOf("=");
1113
- const name2 = eq === -1 ? arg : arg.slice(0, eq);
1114
- const value = eq === -1 ? undefined : arg.slice(eq + 1);
1115
- if (command !== undefined && name2 in SCAN_SCOPE_FLAGS) {
1116
- if (command === "map")
1117
- throw new Error(scanScopeText(name2));
1118
- const takesValue = name2 in VALUE_FLAGS;
1119
- if (!takesValue && value !== undefined)
1120
- throw new Error(`flag ${name2} takes no value`);
1121
- if (takesValue && (value === undefined || value === "")) {
1122
- throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
1123
- }
1124
- options.ignoredFlags.push(name2);
1125
- continue;
1126
- }
1127
- const booleanKey = BOOLEAN_FLAGS[name2];
1128
- if (booleanKey !== undefined) {
1129
- if (value !== undefined)
1130
- throw new Error(`flag ${name2} takes no value`);
1131
- options[booleanKey] = true;
1132
- continue;
1133
- }
1134
- const apply = VALUE_FLAGS[name2];
1135
- if (apply !== undefined) {
1136
- if (value === undefined || value === "") {
1137
- throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
1138
- }
1139
- apply(options, value, setRoot);
1140
- continue;
1141
- }
1142
- throw new Error(`unknown flag '${name2}' (see repo-tools ${command ?? "depgraph"} --help)`);
1143
- }
1144
- checkModes(options);
1145
- return options;
1146
- }
1147
- function checkModes(options) {
1148
- if (options.noRegen && !options.checkDuplicates) {
1149
- throw new Error("flag --no-regen applies with --check-duplicates only");
1150
- }
1151
- const modes = [
1152
- options.checkCensus && "--check-census",
1153
- options.checkDuplicates && "--check-duplicates",
1154
- options.writeDuplicateBaseline && "--write-duplicate-baseline"
1155
- ].filter((mode) => mode !== false);
1156
- if (modes.length > 1) {
1157
- throw new Error(`flags ${modes.join(" and ")} are two modes; use one of them in a run`);
1158
- }
1159
- }
1160
- function wantsHelp(argv) {
1161
- return argv.includes("--help") || argv.includes("-h");
1201
+ function listEntries(dir) {
1202
+ return [...readdirHook.entries(dir)].sort((a, b) => compareCodeUnits(a.name, b.name));
1162
1203
  }
1163
-
1164
- // src/map/command.ts
1165
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync15, statSync as statSync6 } from "node:fs";
1166
- import { resolve as resolve4 } from "node:path";
1167
-
1168
- // src/depgraph/duplicates.ts
1169
- import { readFileSync as readFileSync2 } from "node:fs";
1170
- import { join as join4 } from "node:path";
1171
-
1172
- // src/mask.ts
1173
- var REGEX_KEYWORDS2 = new Set([
1174
- "return",
1175
- "typeof",
1176
- "instanceof",
1177
- "in",
1178
- "of",
1179
- "new",
1180
- "delete",
1181
- "void",
1182
- "throw",
1183
- "case",
1184
- "do",
1185
- "else",
1186
- "yield",
1187
- "await"
1188
- ]);
1189
- function regexCanStart(src, prev, afterStmtParen) {
1190
- if (prev < 0)
1191
- return true;
1192
- const c = src[prev] ?? "";
1193
- if (c === ")")
1194
- return afterStmtParen;
1195
- if (c === ">")
1196
- return src[prev - 1] === "=";
1197
- if ((c === "+" || c === "-") && src[prev - 1] === c)
1198
- return false;
1199
- if (/[([{},;:?=!&|+\-*%^~]/.test(c))
1200
- return true;
1201
- if (!/[\w$]/.test(c))
1204
+ var linkLog = { skipped: new Set };
1205
+ function isLink(path) {
1206
+ let link;
1207
+ try {
1208
+ link = lstatSync(path).isSymbolicLink();
1209
+ } catch {
1202
1210
  return false;
1203
- const word = /[\w$]+$/.exec(src.slice(Math.max(0, prev - 15), prev + 1))?.[0] ?? "";
1204
- return REGEX_KEYWORDS2.has(word) && !/[\w$.]/.test(src[prev - word.length] ?? "");
1205
- }
1206
- function regexBodyEnd(src, start2) {
1207
- let inClass = false;
1208
- for (let j = start2 + 1;j < src.length; j++) {
1209
- const c = src[j];
1210
- if (c === `
1211
- ` || c === "\r")
1212
- return -1;
1213
- if (c === "\\")
1214
- j += 1;
1215
- else if (c === "[")
1216
- inClass = true;
1217
- else if (c === "]")
1218
- inClass = false;
1219
- else if (c === "/" && !inClass)
1220
- return j;
1221
- }
1222
- return -1;
1223
- }
1224
- function scan(src) {
1225
- const out2 = [];
1226
- const push = (kind, start2, end) => {
1227
- if (end <= start2)
1228
- return;
1229
- const last = out2[out2.length - 1];
1230
- if (last && last.kind === kind && last.end === start2)
1231
- last.end = end;
1232
- else
1233
- out2.push({ kind, start: start2, end });
1234
- };
1235
- const templates = [];
1236
- let i2 = 0;
1237
- const n = src.length;
1238
- const readTemplateBody = () => {
1239
- const start2 = i2;
1240
- while (i2 < n) {
1241
- const c = src[i2];
1242
- if (c === "\\") {
1243
- i2 += 2;
1244
- continue;
1245
- }
1246
- if (c === "`") {
1247
- push("string", start2, i2);
1248
- push("code", i2, i2 + 1);
1249
- i2 += 1;
1250
- return;
1251
- }
1252
- if (c === "$" && src[i2 + 1] === "{") {
1253
- push("string", start2, i2);
1254
- push("code", i2, i2 + 2);
1255
- i2 += 2;
1256
- templates.push(0);
1257
- return;
1258
- }
1259
- i2 += 1;
1260
- }
1261
- push("string", start2, n);
1262
- };
1263
- let codeStart = 0;
1264
- let prev = -1;
1265
- const parenStmt = [];
1266
- let closedStmt = false;
1267
- const wordEndingAt = (index) => {
1268
- let end = index;
1269
- while (end > 0) {
1270
- const prev = src[end - 1] ?? "";
1271
- if (/\s/.test(prev)) {
1272
- end -= 1;
1273
- continue;
1274
- }
1275
- if (prev === "/" && src[end - 2] === "*") {
1276
- const open = src.lastIndexOf("/*", end - 2);
1277
- if (open === -1)
1278
- break;
1279
- end = open;
1280
- continue;
1281
- }
1282
- break;
1283
- }
1284
- return /[\w$]+$/.exec(src.slice(Math.max(0, end - 16), end))?.[0] ?? "";
1285
- };
1286
- while (i2 < n) {
1287
- const c = src[i2];
1288
- const next = src[i2 + 1];
1289
- if (c === "/" && next !== "/" && next !== "*" && regexCanStart(src, prev, closedStmt)) {
1290
- const close = regexBodyEnd(src, i2);
1291
- if (close !== -1) {
1292
- push("code", codeStart, i2 + 1);
1293
- push("string", i2 + 1, close);
1294
- i2 = close + 1;
1295
- while (i2 < n && /[a-z]/i.test(src[i2] ?? ""))
1296
- i2 += 1;
1297
- push("code", close, i2);
1298
- prev = i2 - 1;
1299
- closedStmt = false;
1300
- codeStart = i2;
1301
- continue;
1302
- }
1303
- }
1304
- if (c === "/" && next === "/") {
1305
- push("code", codeStart, i2);
1306
- const eol = src.indexOf(`
1307
- `, i2);
1308
- const end = eol === -1 ? n : eol;
1309
- push("comment", i2, end);
1310
- i2 = end;
1311
- codeStart = i2;
1312
- continue;
1313
- }
1314
- if (c === "/" && next === "*") {
1315
- push("code", codeStart, i2);
1316
- const close = src.indexOf("*/", i2 + 2);
1317
- const end = close === -1 ? n : close + 2;
1318
- push("comment", i2, end);
1319
- i2 = end;
1320
- codeStart = i2;
1321
- continue;
1322
- }
1323
- if (c === "'" || c === '"') {
1324
- push("code", codeStart, i2 + 1);
1325
- let j = i2 + 1;
1326
- while (j < n && src[j] !== c && src[j] !== `
1327
- `)
1328
- j += src[j] === "\\" ? 2 : 1;
1329
- const end = Math.min(j, n);
1330
- push("string", i2 + 1, end);
1331
- i2 = end < n && src[end] === c ? end + 1 : end;
1332
- push("code", end, i2);
1333
- prev = i2 - 1;
1334
- closedStmt = false;
1335
- codeStart = i2;
1336
- continue;
1337
- }
1338
- if (c === "`") {
1339
- push("code", codeStart, i2 + 1);
1340
- i2 += 1;
1341
- readTemplateBody();
1342
- prev = i2 - 1;
1343
- closedStmt = false;
1344
- codeStart = i2;
1345
- continue;
1346
- }
1347
- if (!/\s/.test(c ?? "")) {
1348
- if (c === "(") {
1349
- const word = wordEndingAt(i2);
1350
- parenStmt.push(word === "if" || word === "while" || word === "for" || word === "with");
1351
- closedStmt = false;
1352
- } else if (c === ")")
1353
- closedStmt = parenStmt.pop() === true;
1354
- else
1355
- closedStmt = false;
1356
- prev = i2;
1357
- }
1358
- if (templates.length > 0) {
1359
- const depth = templates.length - 1;
1360
- if (c === "{")
1361
- templates[depth] = (templates[depth] ?? 0) + 1;
1362
- else if (c === "}") {
1363
- if (templates[depth] === 0) {
1364
- templates.pop();
1365
- push("code", codeStart, i2 + 1);
1366
- i2 += 1;
1367
- readTemplateBody();
1368
- prev = i2 - 1;
1369
- closedStmt = false;
1370
- codeStart = i2;
1371
- continue;
1372
- }
1373
- templates[depth] = (templates[depth] ?? 1) - 1;
1374
- }
1375
- }
1376
- i2 += 1;
1377
1211
  }
1378
- push("code", codeStart, n);
1379
- return out2;
1380
- }
1381
- function blank(text) {
1382
- return text.replace(/[^\r\n]/g, " ");
1212
+ if (link)
1213
+ linkLog.skipped.add(path);
1214
+ return link;
1383
1215
  }
1384
- function blankCommentsAndStrings(src) {
1385
- return scan(src).map((s) => {
1386
- const text = src.slice(s.start, s.end);
1387
- return s.kind === "code" ? text : blank(text);
1388
- }).join("");
1216
+ function isLinkEntry(dir, entry) {
1217
+ if (!entry.isSymbolicLink())
1218
+ return false;
1219
+ linkLog.skipped.add(join3(dir, entry.name));
1220
+ return true;
1389
1221
  }
1390
- function stripComments(src) {
1391
- return scan(src).filter((s) => s.kind !== "comment").map((s) => src.slice(s.start, s.end)).join("");
1222
+ function isWalkable(dir) {
1223
+ return existsSync(dir) && !isLink(dir);
1392
1224
  }
1393
1225
 
1394
- // src/depgraph/duplicates.ts
1395
- var RUNTIME_DUP_CATEGORIES = [
1396
- { cat: "function", key: "functions" },
1397
- { cat: "constant", key: "constants" },
1398
- { cat: "class", key: "classes" }
1399
- ];
1400
- var TYPE_DUP_CATEGORIES = [
1401
- { cat: "interface", key: "interfaces" },
1402
- { cat: "type", key: "types" },
1403
- { cat: "enum", key: "enums" }
1226
+ // src/depgraph/exclude.ts
1227
+ var DEFAULT_EXCLUDE = [
1228
+ "node_modules",
1229
+ "dist",
1230
+ "build",
1231
+ "coverage",
1232
+ ".git"
1404
1233
  ];
1405
- 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.";
1406
- var DUP_ENTRY_TAG_SORT_ORDER = {
1407
- TRUE_DUPLICATE: 0,
1408
- DISPATCH_VARIANT: 1,
1409
- ALIAS_DELEGATION: 2,
1410
- ALLOWLISTED: 3
1411
- };
1412
- function loadDuplicateAllowlist(path) {
1413
- try {
1414
- const parsed = JSON.parse(readFileSync2(path, "utf-8"));
1415
- return parsed.entries ?? [];
1416
- } catch {
1417
- return [];
1418
- }
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);
1419
1237
  }
1420
- function globMatchSingle(pattern, value) {
1421
- if (pattern === "*")
1422
- return true;
1423
- if (pattern.endsWith("/**"))
1424
- return value.startsWith(pattern.slice(0, -2));
1425
- if (pattern.endsWith("*"))
1426
- return value.startsWith(pattern.slice(0, -1));
1427
- return value === pattern;
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);
1251
+ }
1252
+ return files;
1428
1253
  }
1429
- function findAllowlistMatch(allowlist, name2, filePath) {
1430
- return allowlist.find((e) => e.names.some((n) => globMatchSingle(n, name2)) && e.filesGlob.some((f) => globMatchSingle(f, filePath)));
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}`;
1431
1267
  }
1432
- function escapeRegExpLiteral(s) {
1433
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1268
+ function withBanner(body2, options = {}) {
1269
+ return bannerFor(options) + body2;
1434
1270
  }
1435
- function isDispatchVariantBody(code, name2) {
1436
- const re = new RegExp(`export\\s+const\\s+${escapeRegExpLiteral(name2)}\\s*(?::[^=]+)?=\\s*mathTyped\\s*(?:<[^>]*>)?\\s*\\(`);
1437
- return re.test(code);
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);
1438
1278
  }
1439
- function collectImportedLocalNames(code) {
1440
- const names = new Set;
1441
- const importRegex = /import\s+(?:type\s+)?(?:(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+))(?:\s*,\s*(?:\{([^}]+)\}|(\w+)))?)\s+from\s+['"][^'"]+['"]/g;
1442
- for (const m of code.matchAll(importRegex)) {
1443
- const named = m[1] || m[4] || "";
1444
- const def = m[2] || m[5] || "";
1445
- const ns = m[3] || "";
1446
- if (named) {
1447
- for (const item of named.split(",")) {
1448
- const trimmed = item.trim().replace(/^type\s+/, "");
1449
- if (!trimmed)
1450
- continue;
1451
- const parts2 = trimmed.split(/\s+as\s+/);
1452
- const local = (parts2[parts2.length - 1] ?? "").trim();
1453
- if (local)
1454
- names.add(local);
1455
- }
1456
- }
1457
- if (def)
1458
- names.add(def);
1459
- if (ns)
1460
- names.add(ns);
1461
- }
1462
- return names;
1279
+ function resolveUnderRoot(root, path) {
1280
+ return resolve(root, path);
1463
1281
  }
1464
- function isAliasDelegationBody(code, name2, importedLocalNames) {
1465
- const re = new RegExp(`export\\s+const\\s+${escapeRegExpLiteral(name2)}\\s*(?::[^=]+)?=\\s*([A-Za-z_$][\\w$]*)\\s*;`);
1466
- const m = code.match(re);
1467
- return !!m && importedLocalNames.has(m[1] ?? "");
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);
1468
1305
  }
1469
- function classifyDefiner(file, name2, category, allowlist, readRaw) {
1470
- const allowMatch = findAllowlistMatch(allowlist, name2, file.path);
1471
- if (allowMatch)
1472
- return { tag: "ALLOWLISTED", reason: allowMatch.reason };
1473
- if (category === "constant") {
1474
- const raw = readRaw(file.path);
1475
- if (raw) {
1476
- const code = stripComments(raw);
1477
- if (isDispatchVariantBody(code, name2))
1478
- return { tag: "DISPATCH_VARIANT" };
1479
- if (isAliasDelegationBody(code, name2, collectImportedLocalNames(code))) {
1480
- return { tag: "ALIAS_DELEGATION" };
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;
1311
+ };
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;
1317
+ };
1318
+ const list = (v, item, what) => {
1319
+ if (!Array.isArray(v))
1320
+ throw fail(`'${key}' must be ${what}`);
1321
+ return v.map(item);
1322
+ };
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)`);
1340
+ }
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`);
1481
1354
  }
1482
- }
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;
1483
1361
  }
1484
- return { tag: "PLAIN" };
1485
1362
  }
1486
- function collectOwnDefiners(files, categories) {
1487
- const byName = new Map;
1488
- for (const file of files) {
1489
- const reExported = new Set(file.exports.reExported);
1490
- for (const { cat, key } of categories) {
1491
- for (const name2 of file.exports[key]) {
1492
- if (reExported.has(name2))
1493
- continue;
1494
- if (key === "types" && file.exports.interfaces.includes(name2))
1495
- continue;
1496
- const list = byName.get(name2) ?? [];
1497
- list.push({ file, category: cat });
1498
- byName.set(name2, list);
1499
- }
1500
- }
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);
1501
1370
  }
1502
- return byName;
1371
+ return out2;
1503
1372
  }
1504
- function finalizeDuplicateEntry(name2, categories, definers, tag) {
1505
- const publicDefiners = definers.filter((d) => d.public);
1506
- const canonicalHint = publicDefiners.length === 1 ? publicDefiners[0]?.file ?? "" : publicDefiners.length > 1 ? "AMBIGUOUS" : "internal-only";
1507
- return { name: name2, category: [...categories].sort().join("+"), definers, canonicalHint, tag };
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));
1508
1391
  }
1509
- function buildDuplicateEntries(byName, publicSurface, allowlist, readRaw) {
1510
- const isFilePublic = (file, name2) => publicSurface.publicWildcardFiles.has(file.path) || publicSurface.publicNamed.has(`${file.path}::${name2}`);
1511
- const entries = [];
1512
- for (const [name2, defs] of byName) {
1513
- const byFile = new Map;
1514
- for (const d of defs)
1515
- if (!byFile.has(d.file.path))
1516
- byFile.set(d.file.path, d);
1517
- if (byFile.size < 2)
1518
- continue;
1519
- const categories = new Set;
1520
- const definers = [];
1521
- for (const { file, category } of byFile.values()) {
1522
- categories.add(category);
1523
- const { tag, reason } = classifyDefiner(file, name2, category, allowlist, readRaw);
1524
- definers.push({
1525
- file: file.path,
1526
- package: file.packageName ?? "unknown",
1527
- public: isFilePublic(file, name2),
1528
- tag,
1529
- ...reason ? { reason } : {}
1530
- });
1531
- }
1532
- definers.sort((a, b) => compareCodeUnits(a.file, b.file));
1533
- const nonAlias = definers.filter((d) => d.tag !== "ALIAS_DELEGATION");
1534
- if (nonAlias.length < 2) {
1535
- entries.push(finalizeDuplicateEntry(name2, categories, definers, "ALIAS_DELEGATION"));
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")
1536
1398
  continue;
1399
+ if (key !== "depgraph" && key !== "map" && key !== "query" && key !== "clones") {
1400
+ throw fail(`unknown key '${key}'`);
1537
1401
  }
1538
- const nonAllowlisted = nonAlias.filter((d) => d.tag !== "ALLOWLISTED");
1539
- let entryTag;
1540
- if (nonAllowlisted.length < 2) {
1541
- entryTag = "ALLOWLISTED";
1542
- } else {
1543
- const dispatchCount = nonAllowlisted.filter((d) => d.tag === "DISPATCH_VARIANT").length;
1544
- entryTag = dispatchCount >= 2 ? "DISPATCH_VARIANT" : "TRUE_DUPLICATE";
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);
1545
1418
  }
1546
- entries.push(finalizeDuplicateEntry(name2, categories, definers, entryTag));
1547
1419
  }
1548
- 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));
1549
- return entries;
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;
1550
1433
  }
1551
- function tallyByTag(entries) {
1552
- const tally = {
1553
- TRUE_DUPLICATE: 0,
1554
- DISPATCH_VARIANT: 0,
1555
- ALIAS_DELEGATION: 0,
1556
- ALLOWLISTED: 0
1557
- };
1558
- for (const e of entries)
1559
- tally[e.tag]++;
1560
- return tally;
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;
1450
+ try {
1451
+ parsed = JSON.parse(text);
1452
+ } catch {
1453
+ throw new Error(`config ${label}: the file is not valid JSON`);
1454
+ }
1455
+ return parseConfigSections(parsed, label);
1561
1456
  }
1562
- function detectDuplicateSymbols(files, publicSurface, root, allowlistPath = join4(outputDirOf(root), "duplicate-allowlist.json")) {
1563
- const allowlist = loadDuplicateAllowlist(allowlistPath);
1564
- const cache = new Map;
1565
- const readRaw = (relPath) => {
1566
- let content = cache.get(relPath);
1567
- if (content === undefined) {
1568
- try {
1569
- content = readFileSync2(join4(root, relPath), "utf-8");
1570
- } catch {
1571
- content = "";
1572
- }
1573
- cache.set(relPath, content);
1574
- }
1575
- return content;
1457
+ function loadConfigFile(root, configPath) {
1458
+ return loadConfigSections(root, configPath).depgraph;
1459
+ }
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 ?? []
1576
1464
  };
1465
+ }
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}`;
1577
1470
  return {
1578
- runtime: buildDuplicateEntries(collectOwnDefiners(files, RUNTIME_DUP_CATEGORIES), publicSurface, allowlist, readRaw),
1579
- types: buildDuplicateEntries(collectOwnDefiners(files, TYPE_DUP_CATEGORIES), publicSurface, allowlist, readRaw)
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
1580
1476
  };
1581
1477
  }
1582
- function buildDuplicateReport(dup) {
1583
- const runtimeByTag = tallyByTag(dup.runtime);
1584
- const typeByTag = tallyByTag(dup.types);
1478
+ function mergeDepgraphConfig(cli, file) {
1479
+ const out2 = cli.out ?? file.out ?? OUTPUT_SUBDIR;
1480
+ const inOut = (name2) => `${out2.replace(/[\\/]+$/, "")}/${name2}`;
1585
1481
  return {
1586
- note: DUPLICATE_SYMBOLS_NOTE,
1587
- summary: {
1588
- runtimeDuplicates: runtimeByTag.TRUE_DUPLICATE,
1589
- typeDuplicates: typeByTag.TRUE_DUPLICATE,
1590
- runtimeByTag,
1591
- typeByTag
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]
1592
1498
  },
1593
- runtime: dup.runtime,
1594
- types: dup.types
1499
+ extensions: cli.extensions ?? file.extensions ?? []
1595
1500
  };
1596
1501
  }
1597
- 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`.";
1598
- function trueDuplicateNames(entries) {
1599
- const names = {};
1600
- const found = entries.filter((e) => e.tag === "TRUE_DUPLICATE").sort((a, b) => compareCodeUnits(a.name, b.name));
1601
- for (const e of found)
1602
- names[e.name] = e.definers.map((d) => d.file).sort(compareCodeUnits);
1603
- return names;
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]}`;
1604
1517
  }
1605
- function buildDuplicateBaseline(report) {
1606
- return {
1607
- note: DUPLICATE_BASELINE_NOTE,
1608
- runtime: trueDuplicateNames(report.runtime),
1609
- types: trueDuplicateNames(report.types)
1610
- };
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
+ };
1565
+ }
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;
1611
1572
  }
1612
- function findNewDuplicates(report, baseline) {
1613
- const found = [];
1614
- for (const kind of ["runtime", "types"]) {
1615
- const held = baseline[kind];
1616
- for (const [name2, files] of Object.entries(trueDuplicateNames(report[kind]))) {
1617
- if (!Object.hasOwn(held, name2))
1618
- found.push({ kind, name: name2, files });
1573
+ function pathList(flag, value) {
1574
+ return list(flag, value).map((item) => relativePath(flag, item));
1575
+ }
1576
+ function relativePath(flag, value) {
1577
+ if (isAbsolutePath(value)) {
1578
+ throw new Error(`flag ${flag} ${ABSOLUTE_PATH_TEXT}`);
1579
+ }
1580
+ return value;
1581
+ }
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);
1609
+ continue;
1610
+ }
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);
1624
+ continue;
1625
+ }
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;
1632
+ }
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>`);
1637
+ }
1638
+ apply(options, value, setRoot);
1639
+ continue;
1619
1640
  }
1641
+ throw new Error(`unknown flag '${name2}' (see repo-tools ${command ?? "depgraph"} --help)`);
1620
1642
  }
1621
- return found;
1622
- }
1623
- function isRecord(value) {
1624
- return typeof value === "object" && value !== null && !Array.isArray(value);
1643
+ checkModes(options);
1644
+ return options;
1625
1645
  }
1626
- function asDuplicateEntries(parsed) {
1627
- if (!isRecord(parsed) || !Array.isArray(parsed.runtime) || !Array.isArray(parsed.types)) {
1628
- 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`);
1629
1657
  }
1630
- 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");
1631
- if (!parsed.runtime.every(valid) || !parsed.types.every(valid))
1632
- return null;
1633
- return parsed;
1634
1658
  }
1635
- function asBaselineNames(parsed) {
1636
- if (!isRecord(parsed))
1637
- return null;
1638
- const kind = (value) => value === undefined ? {} : isRecord(value) ? value : null;
1639
- const runtime = kind(parsed.runtime);
1640
- const types = kind(parsed.types);
1641
- return runtime && types ? { runtime, types } : null;
1659
+ function wantsHelp(argv) {
1660
+ return argv.includes("--help") || argv.includes("-h");
1642
1661
  }
1643
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
+
1644
1667
  // src/depgraph/extensions.ts
1645
- import { existsSync as existsSync3, lstatSync as lstatSync2, realpathSync, statSync as statSync2 } from "node:fs";
1646
- 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";
1647
1670
  import { pathToFileURL } from "node:url";
1648
1671
 
1649
1672
  // src/io.ts
1650
- import { mkdirSync, writeFileSync } from "node:fs";
1651
- 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";
1652
1675
  function writeLf(path, text) {
1653
1676
  mkdirSync(dirname(path), { recursive: true });
1654
1677
  writeFileSync(path, text.replace(/\r\n?/g, `
@@ -1666,6 +1689,22 @@ function toJson(value) {
1666
1689
  return `${JSON.stringify(value, null, 2)}
1667
1690
  `;
1668
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
+ }
1669
1708
 
1670
1709
  // src/depgraph/extensions.ts
1671
1710
  var MASK = Object.freeze({ blankCommentsAndStrings, stripComments });
@@ -1746,27 +1785,11 @@ function outsideText(folder, path) {
1746
1785
  const rel = relative2(folder, path);
1747
1786
  return rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || rel.startsWith("../") || isAbsolute(rel);
1748
1787
  }
1749
- function realPath(path) {
1750
- let existing = resolve2(path);
1751
- const rest = [];
1752
- while (!lstatSync2(existing, { throwIfNoEntry: false })) {
1753
- const up = dirname2(existing);
1754
- if (up === existing)
1755
- return;
1756
- rest.unshift(basename(existing));
1757
- existing = up;
1758
- }
1759
- try {
1760
- return join5(realpathSync.native(existing), ...rest);
1761
- } catch {
1762
- return;
1763
- }
1764
- }
1765
1788
  function outputPath(outputDir, relPath) {
1766
1789
  if (typeof relPath !== "string" || relPath === "" || isAbsolutePath(relPath)) {
1767
1790
  throw new Error("write: the path must be relative to the output folder");
1768
1791
  }
1769
- const path = resolve2(outputDir, relPath);
1792
+ const path = resolve3(outputDir, relPath);
1770
1793
  if (outsideText(outputDir, path)) {
1771
1794
  throw new Error("write: the path must stay in the output folder");
1772
1795
  }
@@ -1779,8 +1802,8 @@ function outputPath(outputDir, relPath) {
1779
1802
  }
1780
1803
 
1781
1804
  // src/map/artifacts.ts
1782
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, statSync as statSync5, writeFileSync as writeFileSync2 } from "node:fs";
1783
- 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";
1784
1807
 
1785
1808
  // src/map/cycles.ts
1786
1809
  function sccContaining(start2, eligible, edges, reverseEdges) {
@@ -1920,7 +1943,7 @@ function stronglyConnectedComponents(graph) {
1920
1943
  }
1921
1944
 
1922
1945
  // src/depgraph/resolver.ts
1923
- import { dirname as dirname3, join as join6 } from "node:path";
1946
+ import { dirname as dirname2, join as join6 } from "node:path";
1924
1947
  function distToSrc(path) {
1925
1948
  const m = /(^|\/)dist\/(?:src\/)?/.exec(path);
1926
1949
  if (!m)
@@ -1931,7 +1954,7 @@ function distToSrc(path) {
1931
1954
  return `${before}${m[1]}src/${path.slice(m.index + m[0].length)}`;
1932
1955
  }
1933
1956
  function resolveCandidates(fromPath, spec) {
1934
- const base = toPosix(join6(dirname3(fromPath), spec));
1957
+ const base = toPosix(join6(dirname2(fromPath), spec));
1935
1958
  let candidates;
1936
1959
  if (base.endsWith(".js")) {
1937
1960
  const stem = base.slice(0, -3);
@@ -2065,7 +2088,7 @@ function filesInCycles(components) {
2065
2088
 
2066
2089
  // src/depgraph/roots.ts
2067
2090
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
2068
- import { dirname as dirname4, join as join7 } from "node:path";
2091
+ import { dirname as dirname3, join as join7 } from "node:path";
2069
2092
  function isJsonObject(value) {
2070
2093
  return typeof value === "object" && value !== null && !Array.isArray(value);
2071
2094
  }
@@ -2148,7 +2171,7 @@ function seedTsconfigEntries(root, pkgDir, cfgRel, add) {
2148
2171
  } catch {
2149
2172
  return;
2150
2173
  }
2151
- const cfgDir = dirname4(join7(pkgDir, cfgRel));
2174
+ const cfgDir = dirname3(join7(pkgDir, cfgRel));
2152
2175
  const seedPath = (p) => {
2153
2176
  if (p.includes("*")) {
2154
2177
  const base = join7(root, cfgDir, p.replace(/\/?\*.*$/, ""));
@@ -2342,7 +2365,7 @@ function htmlReferencedEntries(root) {
2342
2365
  const src = m[1] ?? "";
2343
2366
  if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(src) || !/\.[cm]?[jt]sx?$/.test(src))
2344
2367
  continue;
2345
- const fromPage = toPosix(join7(dirname4(page), src)).replace(/^\.\//, "");
2368
+ const fromPage = toPosix(join7(dirname3(page), src)).replace(/^\.\//, "");
2346
2369
  const candidates = src.startsWith("/") ? [src.slice(1), fromPage] : [fromPage];
2347
2370
  const found = candidates.find((c) => existsSync4(join7(root, c)));
2348
2371
  if (found !== undefined && !out2.includes(found))
@@ -2362,7 +2385,7 @@ function runtimeLaunchedEntries(root, parsed) {
2362
2385
  continue;
2363
2386
  }
2364
2387
  for (const m of code.matchAll(/new\s+URL\(\s*['"`](\.[^'"`]*?\.m?js)['"`]\s*,\s*import\.meta\.url/g)) {
2365
- const sibling = toPosix(join7(dirname4(file.path), m[1] ?? ""));
2388
+ const sibling = toPosix(join7(dirname3(file.path), m[1] ?? ""));
2366
2389
  const tsPath = sibling.replace(/\.m?js$/, ".ts");
2367
2390
  if (known.has(tsPath))
2368
2391
  out2.add(tsPath);
@@ -6228,7 +6251,7 @@ import { readFileSync as readFileSync5 } from "node:fs";
6228
6251
  import { join as join9, posix as posix2 } from "node:path";
6229
6252
 
6230
6253
  // src/depgraph/description.ts
6231
- import { basename as basename2, dirname as dirname5 } from "node:path";
6254
+ import { basename as basename2, dirname as dirname4 } from "node:path";
6232
6255
  var RULE_LINE = /^[=\-*~#_]{3,}$/;
6233
6256
  function extractDescription(content) {
6234
6257
  const jsdocMatch = content.match(/\/\*\*\s*\n([^*]*(?:\*(?!\/)[^*]*)*)\*\//);
@@ -6263,7 +6286,7 @@ function generateFallbackDescription(file) {
6263
6286
  const fileName = withoutTsExtension(basename2(file.path));
6264
6287
  if (fileName === "index") {
6265
6288
  if (file.exports.reExported.length > 0) {
6266
- const pkgName = file.packageName || dirname5(file.path).split("/").pop() || "";
6289
+ const pkgName = file.packageName || dirname4(file.path).split("/").pop() || "";
6267
6290
  return `Package entry point for ${pkgName || "module"} (re-exports ${file.exports.reExported.length} symbols)`;
6268
6291
  }
6269
6292
  if (file.exports.named.length > 0) {
@@ -6477,6 +6500,26 @@ function isReparsePoint(path) {
6477
6500
  return false;
6478
6501
  }
6479
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
+ }
6480
6523
  function entries(dir) {
6481
6524
  try {
6482
6525
  return readdirHook.entries(dir);
@@ -6698,8 +6741,8 @@ function discover(root, language = detectLanguage(root), skippedLinks) {
6698
6741
  }
6699
6742
 
6700
6743
  // src/map/graph.ts
6701
- import { readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
6702
- 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";
6703
6746
 
6704
6747
  // node_modules/smol-toml/dist/error.js
6705
6748
  /*!
@@ -11007,6 +11050,20 @@ function parserFor(name2) {
11007
11050
  return parser;
11008
11051
  }
11009
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
+
11010
11067
  // src/map/parsing.ts
11011
11068
  function emptyModule() {
11012
11069
  return {
@@ -11074,6 +11131,7 @@ function importClauseNames(clause) {
11074
11131
  return names;
11075
11132
  }
11076
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"));
11077
11135
  function isTypeOnlyImport(stmt, clause) {
11078
11136
  if (hasChild(stmt, "type"))
11079
11137
  return true;
@@ -11082,10 +11140,7 @@ function isTypeOnlyImport(stmt, clause) {
11082
11140
  const kids = clause.children.filter((c) => c.type !== ",");
11083
11141
  if (kids.length !== 1 || kids[0]?.type !== "named_imports")
11084
11142
  return false;
11085
- const specifiers = kids[0].children.filter((c) => c.type === "import_specifier");
11086
- if (specifiers.length === 0)
11087
- return false;
11088
- return specifiers.every((s) => hasChild(s, "type"));
11143
+ return allInlineTyped(kids[0].children.filter((c) => c.type === "import_specifier"));
11089
11144
  }
11090
11145
  function defaultExportKindAndLocal(stmt) {
11091
11146
  for (const c of stmt.children) {
@@ -11150,11 +11205,10 @@ function exportClauseSourceNames(clause) {
11150
11205
  function isTypeOnlyExport(stmt, clause) {
11151
11206
  if (hasChild(stmt, "type"))
11152
11207
  return true;
11153
- const specifiers = clause.children.filter((c) => c.type === "export_specifier");
11154
- if (specifiers.length === 0)
11155
- return false;
11156
- return specifiers.every((s) => hasChild(s, "type"));
11208
+ return allInlineTyped(clause.children.filter((c) => c.type === "export_specifier"));
11157
11209
  }
11210
+ var GAP = String.raw`(?:\s|/\*[\s\S]*?\*/)`;
11211
+ var EXPORT_TYPE_STAR = new RegExp(String.raw`^export${GAP}+type${GAP}*\*`, "u");
11158
11212
  function stripChars(text, chars) {
11159
11213
  let a = 0;
11160
11214
  let b = text.length;
@@ -11273,6 +11327,7 @@ function parseTs(source, path = "") {
11273
11327
  const nsExport = node.children.find((c) => c.type === "namespace_export");
11274
11328
  let names = [];
11275
11329
  let typeOnly = false;
11330
+ const typeStar = hasChild(node, "type") || EXPORT_TYPE_STAR.test(node.text);
11276
11331
  if (clause) {
11277
11332
  names = exportClauseSourceNames(clause);
11278
11333
  typeOnly = isTypeOnlyExport(node, clause);
@@ -11280,11 +11335,12 @@ function parseTs(source, path = "") {
11280
11335
  mod.reExports.push(name2);
11281
11336
  } else if (nsExport) {
11282
11337
  names = ["*"];
11338
+ typeOnly = typeStar;
11283
11339
  for (const [name2] of exportEntries(node))
11284
11340
  mod.reExports.push(name2);
11285
11341
  } else {
11286
- const typeStar = hasChild(node, "type") ? "type " : "";
11287
- mod.reExports.push(`${typeStar}* from ${specifierOf(spec)}`);
11342
+ typeOnly = typeStar;
11343
+ mod.reExports.push(`${typeStar ? "type " : ""}* from ${specifierOf(spec)}`);
11288
11344
  }
11289
11345
  mod.imports.push({ specifier: specifierOf(spec), names, typeOnly, reExport: true });
11290
11346
  }
@@ -12392,7 +12448,7 @@ var STRING_LITERAL = /['"]([^'"\n]*)['"]/g;
12392
12448
  var PATH_SEGMENT = new RegExp(`^(?:${W}|[.-])+$`, "u");
12393
12449
  var lstripDotSlash = (text) => text.replace(/^[./]+/, "");
12394
12450
  function launcherEntry(root, spec, known) {
12395
- const path = join11(root, lstripDotSlash(spec.replaceAll("\\", "/")));
12451
+ const path = join12(root, lstripDotSlash(spec.replaceAll("\\", "/")));
12396
12452
  let text;
12397
12453
  try {
12398
12454
  const st = statSync4(path);
@@ -12440,7 +12496,7 @@ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
12440
12496
  function packageJsonRoots(root, known) {
12441
12497
  let pkg;
12442
12498
  try {
12443
- 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"))));
12444
12500
  } catch {
12445
12501
  return [[], []];
12446
12502
  }
@@ -12488,7 +12544,7 @@ var PYTHON_FALLBACK_ROOTS = [
12488
12544
  ];
12489
12545
  function pythonRoots(root, known) {
12490
12546
  const warnings = [];
12491
- const pyproject = join11(root, "pyproject.toml");
12547
+ const pyproject = join12(root, "pyproject.toml");
12492
12548
  let isFile = false;
12493
12549
  try {
12494
12550
  isFile = statSync4(pyproject).isFile();
@@ -12539,31 +12595,11 @@ function pythonRoots(root, known) {
12539
12595
  }
12540
12596
  var CSHARP_MAIN = new RegExp(`${B}static${S}+(?:async${S}+)?[^${SPACE_BODY}(){};]+${S}+Main${S}*\\(`, "u");
12541
12597
  var CSPROJ_EXE = new RegExp(`<OutputType>${S}*(Exe|WinExe)${S}*</OutputType>`, "iu");
12542
- function rglobParts(root, suffix) {
12543
- const found = [];
12544
- const go = (dir, parts2) => {
12545
- let entries;
12546
- try {
12547
- entries = readdirHook.entries(dir);
12548
- } catch {
12549
- return;
12550
- }
12551
- for (const e of entries) {
12552
- const full = join11(dir, e.name);
12553
- if (e.isDirectory() && !isReparsePoint(full))
12554
- go(full, [...parts2, e.name]);
12555
- else if (e.name.endsWith(suffix))
12556
- found.push([...parts2, e.name]);
12557
- }
12558
- };
12559
- go(root, []);
12560
- return found.sort((a, b) => compareCodeUnits(a.join("/"), b.join("/")));
12561
- }
12562
12598
  function csharpRoots(root, known) {
12563
12599
  const warnings = [];
12564
12600
  const declaresMain = (rel) => {
12565
12601
  try {
12566
- return CSHARP_MAIN.test(readSource(join11(root, rel)));
12602
+ return CSHARP_MAIN.test(readSource(join12(root, rel)));
12567
12603
  } catch {
12568
12604
  return false;
12569
12605
  }
@@ -12574,7 +12610,7 @@ function csharpRoots(root, known) {
12574
12610
  continue;
12575
12611
  let text;
12576
12612
  try {
12577
- text = readSource(join11(root, ...parts2));
12613
+ text = readSource(join12(root, ...parts2));
12578
12614
  } catch (error) {
12579
12615
  warnings.push(`could not read ${parts2.join("/")} (${error instanceof Error ? error.message : String(error)})`);
12580
12616
  continue;
@@ -12596,7 +12632,7 @@ function csharpRoots(root, known) {
12596
12632
  if (program.length > 0)
12597
12633
  roots.push(program[0]);
12598
12634
  else {
12599
- 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");
12600
12636
  }
12601
12637
  }
12602
12638
  if (roots.length > 0)
@@ -12632,7 +12668,7 @@ function workspaceRoots(root, known) {
12632
12668
  continue;
12633
12669
  const prefix = `${ws.directory}/`;
12634
12670
  const local = new Set([...known].filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length)));
12635
- const [found, pkgWarnings] = packageJsonRoots(join11(root, ws.directory), local);
12671
+ const [found, pkgWarnings] = packageJsonRoots(join12(root, ws.directory), local);
12636
12672
  const pkgRoots = found.map((p) => prefix + p);
12637
12673
  for (const entry of ws.extraEntries)
12638
12674
  if (known.has(entry))
@@ -12681,7 +12717,7 @@ async function buildGraph(root) {
12681
12717
  const parsed = new Map;
12682
12718
  const sources = new Map;
12683
12719
  for (const f of found) {
12684
- const text = readSource(join11(root, f.path)).replace(/\r\n?/g, `
12720
+ const text = readSource(join12(root, f.path)).replace(/\r\n?/g, `
12685
12721
  `);
12686
12722
  sources.set(f.path, text);
12687
12723
  parsed.set(f.path, read(text, f.path));
@@ -12811,7 +12847,7 @@ async function buildGraph(root) {
12811
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");
12812
12848
  }
12813
12849
  const graph = newRepoGraph({
12814
- name: basename3(resolve3(root)),
12850
+ name: packageField(root, "name") ?? basename3(resolve4(root)),
12815
12851
  files,
12816
12852
  roots,
12817
12853
  warnings,
@@ -12915,7 +12951,7 @@ function pyJsonDumps(data) {
12915
12951
  return out2;
12916
12952
  }
12917
12953
  function writeJson(path, data) {
12918
- mkdirSync2(dirname6(path), { recursive: true });
12954
+ mkdirSync2(dirname5(path), { recursive: true });
12919
12955
  writeFileSync2(path, `${pyJsonDumps(data)}
12920
12956
  `);
12921
12957
  return path;
@@ -12998,11 +13034,11 @@ function emitDependencyGraph(graph, outDir, options = {}) {
12998
13034
  ...graph.language === "typescript" ? exportKindStats(graph) : {}
12999
13035
  });
13000
13036
  data.warnings = [...graph.warnings];
13001
- return writeJson(join12(outDir, "dependency-graph.json"), data);
13037
+ return writeJson(join13(outDir, "dependency-graph.json"), data);
13002
13038
  }
13003
13039
  function readJson(path) {
13004
13040
  try {
13005
- 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)));
13006
13042
  } catch {
13007
13043
  return null;
13008
13044
  }
@@ -13012,7 +13048,7 @@ var PNPM_ITEM = new RegExp(`(?<=^|\\n)${S}*-${S}*['"]?([^'"${SPACE_BODY}#]+)['"]
13012
13048
  function pnpmWorkspacePatterns(root) {
13013
13049
  let text;
13014
13050
  try {
13015
- text = readSource(join12(root, "pnpm-workspace.yaml")).replace(/\r\n?/g, `
13051
+ text = readSource(join13(root, "pnpm-workspace.yaml")).replace(/\r\n?/g, `
13016
13052
  `);
13017
13053
  } catch {
13018
13054
  return [];
@@ -13042,29 +13078,9 @@ function normalizeWorkspacePatterns(raw) {
13042
13078
  ]
13043
13079
  ];
13044
13080
  }
13045
- function csprojParts(root) {
13046
- const found = [];
13047
- const go = (dir, parts2) => {
13048
- let entries;
13049
- try {
13050
- entries = readdirHook.entries(dir);
13051
- } catch {
13052
- return;
13053
- }
13054
- for (const e of entries) {
13055
- const full = join12(dir, e.name);
13056
- if (e.isDirectory() && !isReparsePoint(full))
13057
- go(full, [...parts2, e.name]);
13058
- else if (e.name.endsWith(".csproj"))
13059
- found.push([...parts2, e.name]);
13060
- }
13061
- };
13062
- go(root, []);
13063
- return found.sort((a, b) => compareCodeUnits(a.join("/"), b.join("/")));
13064
- }
13065
13081
  function csprojMap(root) {
13066
13082
  const projects = new Map;
13067
- for (const parts2 of csprojParts(root)) {
13083
+ for (const parts2 of rglobParts(root, ".csproj")) {
13068
13084
  if (parts2.some((p) => p === "obj" || p === "bin"))
13069
13085
  continue;
13070
13086
  const name2 = parts2.at(-1);
@@ -13082,13 +13098,13 @@ function csprojMap(root) {
13082
13098
  }
13083
13099
  function subdirs(dir) {
13084
13100
  try {
13085
- 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);
13086
13102
  } catch {
13087
13103
  return [];
13088
13104
  }
13089
13105
  }
13090
13106
  function workspaceMap(root) {
13091
- const pkg = readJson(join12(root, "package.json"));
13107
+ const pkg = readJson(join13(root, "package.json"));
13092
13108
  if (!isObject3(pkg)) {
13093
13109
  return [
13094
13110
  new Map,
@@ -13122,18 +13138,18 @@ function workspaceMap(root) {
13122
13138
  const parent = pattern.slice(0, -2);
13123
13139
  let isDir = false;
13124
13140
  try {
13125
- isDir = statSync5(join12(root, parent)).isDirectory();
13141
+ isDir = statSync5(join13(root, parent)).isDirectory();
13126
13142
  } catch {
13127
13143
  isDir = false;
13128
13144
  }
13129
13145
  if (!isDir)
13130
13146
  continue;
13131
- candidates = subdirs(join12(root, parent)).map((d) => parent ? `${parent}/${d}` : d);
13147
+ candidates = subdirs(join13(root, parent)).map((d) => parent ? `${parent}/${d}` : d);
13132
13148
  } else {
13133
13149
  candidates = [pattern];
13134
13150
  }
13135
13151
  for (const relCand of candidates) {
13136
- const sub = readJson(join12(root, relCand, "package.json"));
13152
+ const sub = readJson(join13(root, relCand, "package.json"));
13137
13153
  if (!isObject3(sub)) {
13138
13154
  warnings.push(`emit_file_inventory: workspace candidate '${relCand}' (matched by '${pattern}') has no readable package.json -- skipped, its files will report package '(root)'`);
13139
13155
  continue;
@@ -13193,7 +13209,7 @@ function emitFileInventory(graph, outDir) {
13193
13209
  }));
13194
13210
  const byDisposition = Object.fromEntries(DISPOSITIONS.map((d) => [d, 0]));
13195
13211
  Object.assign(byDisposition, counter(files.map((f) => f.disposition)));
13196
- return writeJson(join12(outDir, "file-inventory.json"), {
13212
+ return writeJson(join13(outDir, "file-inventory.json"), {
13197
13213
  totalFiles: files.length,
13198
13214
  byDisposition,
13199
13215
  byArea: counter(files.map((f) => f.area)),
@@ -13227,7 +13243,7 @@ function readSourceText(rootPath, rel) {
13227
13243
  if (rootPath === null)
13228
13244
  return null;
13229
13245
  try {
13230
- return readSource(join12(rootPath, rel)).replace(/\r\n?/g, `
13246
+ return readSource(join13(rootPath, rel)).replace(/\r\n?/g, `
13231
13247
  `);
13232
13248
  } catch {
13233
13249
  return null;
@@ -13382,7 +13398,7 @@ function emitDuplicateSymbols(graph, outDir, options = {}) {
13382
13398
  duplicates[sym] = paths;
13383
13399
  }
13384
13400
  const summary = { duplicateCount: Object.keys(duplicates).length, totalSymbols: owners.size };
13385
- const path = join12(outDir, "duplicate-symbols.json");
13401
+ const path = join13(outDir, "duplicate-symbols.json");
13386
13402
  const root = graph.rootPath;
13387
13403
  if (graph.language !== "typescript" || root === null) {
13388
13404
  const reason = root === null ? "This graph has no root folder to read the definitions from." : `This repository reads as ${graph.language}.`;
@@ -13398,7 +13414,7 @@ function emitDuplicateSymbols(graph, outDir, options = {}) {
13398
13414
  const rootEntries = workspaces.size > 0 ? [] : rootPackageEntries(root);
13399
13415
  const surface = computePublicSurface(records, root, workspaces, rootEntries);
13400
13416
  const definers = records.filter((r) => !/\.d\.[cm]?ts$/.test(r.path));
13401
- 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);
13402
13418
  return writeJson(path, {
13403
13419
  note: DUPLICATES_CLASSIFIED_NOTE,
13404
13420
  classificationNote: report.note,
@@ -13414,7 +13430,7 @@ function emitUnusedAnalysis(graph, outDir) {
13414
13430
  const [unreferenced, referencedInModule, unclassified] = classifyUnusedExports(graph);
13415
13431
  const notes = unreferencedAnywhereNotes(graph, unreferenced);
13416
13432
  const noImporter = noImporterFiles(graph);
13417
- return writeJson(join12(outDir, "unused-analysis.json"), {
13433
+ return writeJson(join13(outDir, "unused-analysis.json"), {
13418
13434
  caveats: caveatsFor(graph),
13419
13435
  summary: {
13420
13436
  unusedExportCount: countAll(unreferenced) + countAll(referencedInModule) + countAll(unclassified),
@@ -13432,16 +13448,16 @@ function emitUnusedAnalysis(graph, outDir) {
13432
13448
  }
13433
13449
 
13434
13450
  // src/map/coverage.ts
13435
- import { join as join16 } from "node:path";
13451
+ import { join as join17 } from "node:path";
13436
13452
 
13437
13453
  // src/depgraph/coverage.ts
13438
- import { existsSync as existsSync6, readFileSync as readFileSync10 } from "node:fs";
13439
- 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";
13440
13456
  function loadCoveragePolicy(policyPath) {
13441
13457
  if (!existsSync6(policyPath))
13442
13458
  return null;
13443
13459
  try {
13444
- const parsed = JSON.parse(readFileSync10(policyPath, "utf8"));
13460
+ const parsed = JSON.parse(readFileSync11(policyPath, "utf8"));
13445
13461
  if (!parsed.categories || typeof parsed.categories !== "object")
13446
13462
  return null;
13447
13463
  return parsed;
@@ -13541,7 +13557,7 @@ function traceReExports(importedPath, reExportMap) {
13541
13557
  result.add(source);
13542
13558
  return result;
13543
13559
  }
13544
- 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")) {
13545
13561
  const sourceFiles = graphFiles.filter((f) => !f.path.endsWith(".d.ts"));
13546
13562
  const sourceFilePaths = new Set(sourceFiles.map((f) => f.path));
13547
13563
  const coverageMap = new Map;
@@ -13680,10 +13696,10 @@ function generateTestCoverageMarkdown(coverage, options = {}) {
13680
13696
  }
13681
13697
  lines.push("---");
13682
13698
  lines.push("");
13683
- lines.push("## Source Files Without Test Coverage");
13699
+ lines.push("## Source Files Not Imported By A Test");
13684
13700
  lines.push("");
13685
13701
  if (coverage.untestedFiles.length === 0) {
13686
- lines.push("**All source files have test coverage!** \uD83C\uDF89");
13702
+ lines.push("**Every source file is imported by a test file.** \uD83C\uDF89");
13687
13703
  } else {
13688
13704
  lines.push(`The following ${coverage.untestedFiles.length} source files are not directly imported by any test file:`);
13689
13705
  lines.push("");
@@ -13707,7 +13723,7 @@ function generateTestCoverageMarkdown(coverage, options = {}) {
13707
13723
  }
13708
13724
  lines.push("---");
13709
13725
  lines.push("");
13710
- lines.push("## Source Files With Test Coverage");
13726
+ lines.push("## Source Files Imported By A Test");
13711
13727
  lines.push("");
13712
13728
  lines.push("| Source File | Test Files |");
13713
13729
  lines.push("|-------------|------------|");
@@ -13771,7 +13787,7 @@ function generateTestCoverageJson(coverage, options = {}) {
13771
13787
 
13772
13788
  // src/map/reports.ts
13773
13789
  import { readFileSync as readFileSync12 } from "node:fs";
13774
- import { join as join15 } from "node:path";
13790
+ import { join as join16 } from "node:path";
13775
13791
 
13776
13792
  // src/depgraph/reporters/json.ts
13777
13793
  function modulesJsonOf(modules) {
@@ -13988,10 +14004,14 @@ function fileSection(lines, path, file) {
13988
14004
  lines.push("---");
13989
14005
  lines.push("");
13990
14006
  }
13991
- function summaryRowsOf(stats) {
14007
+ var DEPGRAPH_LABELS = {
14008
+ files: "Total TypeScript Files",
14009
+ modules: "Total Modules"
14010
+ };
14011
+ function summaryRowsOf(stats, labels = DEPGRAPH_LABELS) {
13992
14012
  const rows = [
13993
- ["Total TypeScript Files", stats.totalTypeScriptFiles],
13994
- ["Total Modules", stats.totalModules],
14013
+ [labels.files, stats.totalTypeScriptFiles],
14014
+ [labels.modules, stats.totalModules],
13995
14015
  ["Total Lines of Code", stats.totalLinesOfCode],
13996
14016
  ["Total Exports", stats.totalExports],
13997
14017
  ["Total Re-exports", stats.totalReExports],
@@ -14134,20 +14154,12 @@ function generateYaml(json, dumpFn = dump) {
14134
14154
  }
14135
14155
 
14136
14156
  // src/map/layers.ts
14137
- import { readFileSync as readFileSync11 } from "node:fs";
14138
- import { join as join14 } from "node:path";
14157
+ import { join as join15 } from "node:path";
14139
14158
  function projectIdentity(root, graph) {
14140
- let pkg = null;
14141
- try {
14142
- pkg = JSON.parse(readFileSync11(join14(root, "package.json"), "utf8"));
14143
- } catch {}
14144
- const field = (key) => {
14145
- if (typeof pkg !== "object" || pkg === null || Array.isArray(pkg))
14146
- return null;
14147
- const value = pkg[key];
14148
- return typeof value === "string" && value.length > 0 ? value : null;
14159
+ return {
14160
+ name: packageField(root, "name") ?? graph.name,
14161
+ version: packageField(root, "version") ?? "unknown"
14149
14162
  };
14150
- return { name: field("name") ?? graph.name, version: field("version") ?? "unknown" };
14151
14163
  }
14152
14164
  function subsystemView(graph, root, warn = () => {}) {
14153
14165
  const records = toParsedFiles(graph, root);
@@ -14159,7 +14171,7 @@ function subsystemView(graph, root, warn = () => {}) {
14159
14171
  }
14160
14172
  function emitDependencyLayers(graph, root, outDir, view = subsystemView(graph, root)) {
14161
14173
  const identity = projectIdentity(root, graph);
14162
- return writeJson(join14(outDir, "dependency-layers.json"), {
14174
+ return writeJson(join15(outDir, "dependency-layers.json"), {
14163
14175
  metadata: {
14164
14176
  name: identity.name,
14165
14177
  version: identity.version,
@@ -14198,33 +14210,14 @@ function statisticsFromCore(core, subsystems) {
14198
14210
  unusedExportsCount: num("unusedExportsCount")
14199
14211
  };
14200
14212
  }
14201
- function summaryRows(stats) {
14202
- const rows = [
14203
- ["Total Source Files", stats.totalTypeScriptFiles],
14204
- ["Subsystems", stats.totalModules],
14205
- ["Total Lines of Code", stats.totalLinesOfCode],
14206
- ["Total Exports", stats.totalExports],
14207
- ["Total Re-exports", stats.totalReExports],
14208
- ["Total Classes", stats.totalClasses],
14209
- ["Total Interfaces", stats.totalInterfaces],
14210
- ["Total Functions", stats.totalFunctions],
14211
- ["Total Type Guards", stats.totalTypeGuards],
14212
- ["Total Enums", stats.totalEnums],
14213
- ["Type-only Imports", stats.totalTypeOnlyImports],
14214
- ["Runtime Cyclic Components", stats.runtimeCyclicComponents],
14215
- ["Type-only Cyclic Components", stats.typeOnlyCyclicComponents],
14216
- ["Files in Runtime Cycles", stats.runtimeFilesInCycles],
14217
- ["Files in Type-only Cycles", stats.typeOnlyFilesInCycles]
14218
- ];
14219
- return rows.filter((r) => r[1] !== undefined);
14220
- }
14213
+ var SUMMARY_LABELS = { files: "Total Source Files", modules: "Subsystems" };
14221
14214
  function emitSubsystemReports(graph, root, outDir, corePath, options = {}) {
14222
14215
  const view = options.view ?? subsystemView(graph, root);
14223
14216
  const core = JSON.parse(readFileSync12(corePath, "utf8"));
14224
14217
  const stats = statisticsFromCore(core.statistics, Object.keys(view.modules).length);
14225
14218
  const identity = projectIdentity(root, graph);
14226
14219
  const banner = { command: MAP_REGENERATE_COMMAND, ...options.banner };
14227
- 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));
14228
14221
  const written = [
14229
14222
  ["DEPENDENCY_GRAPH.md", withBanner(markdown, banner)],
14230
14223
  ["dependency-graph.yaml", generateYaml(core)],
@@ -14234,7 +14227,7 @@ function emitSubsystemReports(graph, root, outDir, corePath, options = {}) {
14234
14227
  ]
14235
14228
  ];
14236
14229
  return written.map(([name2, text]) => {
14237
- const path = join15(outDir, name2);
14230
+ const path = join16(outDir, name2);
14238
14231
  writeReport(path, text);
14239
14232
  return path;
14240
14233
  });
@@ -14252,9 +14245,9 @@ function isTestFile(path, language, area) {
14252
14245
  function emitTestCoverage(graph, root, outDir, options = {}) {
14253
14246
  const sources = toParsedFiles(graph, root);
14254
14247
  const tests = toParsedFiles(graph, root, { allAreas: true, loadEdges: true }).filter((r) => isTestFile(r.path, graph.language, graph.files.get(r.path)?.area ?? ""));
14255
- const coverage = analyzeTestCoverage(sources, tests, root, options.policyPath ?? join16(root, "docs", "architecture", "coverage-policy.json"));
14256
- const mdPath = join16(outDir, "TEST_COVERAGE.md");
14257
- 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");
14258
14251
  const namespaceImportBasis = !importsNameSymbols(graph.language);
14259
14252
  const markdown = generateTestCoverageMarkdown(coverage, {
14260
14253
  expectedTestHint: graph.language === "typescript" && tests.some((t) => t.path.startsWith("tests/unit/")),
@@ -14268,7 +14261,7 @@ function emitTestCoverage(graph, root, outDir, options = {}) {
14268
14261
 
14269
14262
  // src/map/markdown.ts
14270
14263
  import { readFileSync as readFileSync13 } from "node:fs";
14271
- import { join as join17 } from "node:path";
14264
+ import { join as join18 } from "node:path";
14272
14265
 
14273
14266
  // src/depgraph/reporters/duplicates.ts
14274
14267
  function summaryTable(byTag, total) {
@@ -14308,7 +14301,7 @@ function renderTaggedSection(title, entries, tag) {
14308
14301
 
14309
14302
  ${renderTable(entries.filter((e) => e.tag === tag))}`;
14310
14303
  }
14311
- function generateDuplicateSymbolsMarkdown(report) {
14304
+ function generateDuplicateSymbolsMarkdown(report, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
14312
14305
  let md = `# Duplicate Symbols
14313
14306
 
14314
14307
  `;
@@ -14316,9 +14309,14 @@ function generateDuplicateSymbolsMarkdown(report) {
14316
14309
  md += "A file that only re-exports the name does not count. ";
14317
14310
  md += "Each name carries one tag (see `DupEntryTag`), so the actionable subset is clear:\n\n";
14318
14311
  md += "- `TRUE_DUPLICATE`: a real merge target.\n";
14319
- md += "- `DISPATCH_VARIANT`: two or more `mathTyped(...)` registrations of the same public name. ";
14320
- 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.
14321
14318
  `;
14319
+ }
14322
14320
  md += "- `ALIAS_DELEGATION`: a `const X = importedY` forward. ";
14323
14321
  md += `The entry is excluded once fewer than two real bodies remain.
14324
14322
  `;
@@ -14413,7 +14411,7 @@ function inventoryMarkdown(inv, language) {
14413
14411
  return lines.join(`
14414
14412
  `);
14415
14413
  }
14416
- function duplicatesMarkdown(dup) {
14414
+ function duplicatesMarkdown(dup, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
14417
14415
  if (Array.isArray(dup.runtime) && Array.isArray(dup.types)) {
14418
14416
  const report = {
14419
14417
  note: dup.classificationNote,
@@ -14426,7 +14424,7 @@ function duplicatesMarkdown(dup) {
14426
14424
  runtime: dup.runtime,
14427
14425
  types: dup.types
14428
14426
  };
14429
- return generateDuplicateSymbolsMarkdown(report);
14427
+ return generateDuplicateSymbolsMarkdown(report, dispatchCalls);
14430
14428
  }
14431
14429
  const lines = ["# Duplicate Symbols", ""];
14432
14430
  lines.push(`> **Note:** ${dup.note}`, "", `> ${dup.classificationNote}`, "");
@@ -14497,17 +14495,17 @@ function unusedMarkdown(unused, graph) {
14497
14495
  return lines.join(`
14498
14496
  `);
14499
14497
  }
14500
- function emitMarkdownReports(outDir, banner = {}) {
14501
- 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"));
14502
14500
  const graph = read("dependency-graph.json");
14503
14501
  const bannerOptions = { command: MAP_REGENERATE_COMMAND, ...banner };
14504
14502
  const reports = [
14505
14503
  ["FILE_INVENTORY.md", inventoryMarkdown(read("file-inventory.json"), graph.metadata.language)],
14506
- ["duplicate-symbols.md", duplicatesMarkdown(read("duplicate-symbols.json"))],
14504
+ ["duplicate-symbols.md", duplicatesMarkdown(read("duplicate-symbols.json"), dispatchCalls)],
14507
14505
  ["unused-analysis.md", unusedMarkdown(read("unused-analysis.json"), graph)]
14508
14506
  ];
14509
14507
  return reports.map(([name2, body2]) => {
14510
- const path = join17(outDir, name2);
14508
+ const path = join18(outDir, name2);
14511
14509
  writeReport(path, withBanner(body2, bannerOptions));
14512
14510
  return path;
14513
14511
  });
@@ -14515,7 +14513,7 @@ function emitMarkdownReports(outDir, banner = {}) {
14515
14513
 
14516
14514
  // src/map/surfaces.ts
14517
14515
  import { readFileSync as readFileSync14 } from "node:fs";
14518
- import { join as join18 } from "node:path";
14516
+ import { join as join19 } from "node:path";
14519
14517
 
14520
14518
  // src/depgraph/reporters/surfaces.ts
14521
14519
  function packageKeyOf(moduleKey) {
@@ -14550,7 +14548,7 @@ function emitApiSurface(graph, root, outPath, entry, stabilityTags) {
14550
14548
  if (graph.language === "typescript") {
14551
14549
  const load = (p) => {
14552
14550
  try {
14553
- return readFileSync14(join18(root, p), "utf8");
14551
+ return readFileSync14(join19(root, p), "utf8");
14554
14552
  } catch {
14555
14553
  return null;
14556
14554
  }
@@ -14596,7 +14594,7 @@ function sortedSurfaces(map) {
14596
14594
  return out2;
14597
14595
  }
14598
14596
  function emitExportSurfaces(graph, root, outDir) {
14599
- const path = join18(outDir, "package-export-surfaces.json");
14597
+ const path = join19(outDir, "package-export-surfaces.json");
14600
14598
  if (graph.language === "typescript") {
14601
14599
  const records = toParsedFiles(graph, root);
14602
14600
  const workspaces = detectWorkspaces(root);
@@ -14724,7 +14722,7 @@ async function runMap(argv, io, command = "map") {
14724
14722
  `);
14725
14723
  if (options.strictOrphans)
14726
14724
  options.settings.strictOrphans = true;
14727
- root = resolve4(options.root);
14725
+ root = resolve5(options.root);
14728
14726
  if (!isDirectory(root))
14729
14727
  throw new Error("the root <root> is not an existing directory");
14730
14728
  const fileSettings = loadConfigFile(root, options.config);
@@ -14791,13 +14789,14 @@ async function runPipeline(root, options, config, io) {
14791
14789
  const corePath = emitDependencyGraph(graph, outDir);
14792
14790
  written.push(corePath, emitFileInventory(graph, outDir));
14793
14791
  written.push(emitDuplicateSymbols(graph, outDir, {
14794
- allowlistPath: resolveUnderRoot(root, config.duplicateAllowlist)
14792
+ allowlistPath: resolveUnderRoot(root, config.duplicateAllowlist),
14793
+ dispatchCalls: config.dispatchCalls
14795
14794
  }));
14796
14795
  written.push(emitUnusedAnalysis(graph, outDir));
14797
14796
  const view = subsystemView(graph, root);
14798
14797
  written.push(emitDependencyLayers(graph, root, outDir, view));
14799
14798
  written.push(...emitSubsystemReports(graph, root, outDir, corePath, { view, banner }));
14800
- written.push(...emitMarkdownReports(outDir, banner));
14799
+ written.push(...emitMarkdownReports(outDir, banner, config.dispatchCalls));
14801
14800
  written.push(...emitTestCoverage(graph, root, outDir, {
14802
14801
  policyPath: resolveUnderRoot(root, config.coveragePolicy),
14803
14802
  policyNote: config.coveragePolicy.replace(/\\/g, "/"),
@@ -15001,7 +15000,7 @@ function parseClaims(markdown) {
15001
15000
  // src/check/metrics.ts
15002
15001
  import { mkdtempSync, readFileSync as readFileSync16, rmSync } from "node:fs";
15003
15002
  import { tmpdir } from "node:os";
15004
- import { join as join19 } from "node:path";
15003
+ import { join as join20 } from "node:path";
15005
15004
  var readReport = (path) => JSON.parse(readFileSync16(path, "utf8"));
15006
15005
  function section(report, key) {
15007
15006
  const value = report[key];
@@ -15019,11 +15018,14 @@ function addScalars(into, source) {
15019
15018
  }
15020
15019
  async function collectMetrics(root, options = {}) {
15021
15020
  const graph = await graphOf(root);
15022
- const scratch = mkdtempSync(join19(options.scratchBase ?? tmpdir(), "repo-tools-check-"));
15021
+ const scratch = mkdtempSync(join20(options.scratchBase ?? tmpdir(), "repo-tools-check-"));
15023
15022
  try {
15024
15023
  const dep = readReport(emitDependencyGraph(graph, scratch));
15025
15024
  const inventory = readReport(emitFileInventory(graph, scratch));
15026
- 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
+ }));
15027
15029
  const unused = readReport(emitUnusedAnalysis(graph, scratch));
15028
15030
  const metrics = new Map;
15029
15031
  addScalars(metrics, section(dep, "metadata"));
@@ -15221,7 +15223,7 @@ function readDocs(dir, label) {
15221
15223
  return names.map((name2) => {
15222
15224
  let bytes;
15223
15225
  try {
15224
- bytes = readFileSync17(join20(dir, name2));
15226
+ bytes = readFileSync17(join21(dir, name2));
15225
15227
  } catch {
15226
15228
  throw new Error(`cannot read ${label}/${name2}`);
15227
15229
  }
@@ -15248,16 +15250,17 @@ async function run2(argv, io) {
15248
15250
  const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
15249
15251
  try {
15250
15252
  const options = parseCheckArgs(argv, process.cwd());
15251
- root = resolve5(options.root);
15253
+ root = resolve6(options.root);
15252
15254
  if (!isDirectory(root))
15253
15255
  throw new Error("the root <root> is not an existing directory");
15254
15256
  if (options.docs === undefined)
15255
15257
  throw new Error("flag --docs is required: --docs=<dir>");
15256
15258
  const config = mergeMapConfig({}, loadConfigFile(root, options.config));
15257
15259
  const label = isAbsolutePath(options.docs) ? "<docs>" : `<root>/${options.docs.replace(/\\/g, "/")}`;
15258
- const docs = readDocs(resolve5(root, options.docs), label);
15260
+ const docs = readDocs(resolve6(root, options.docs), label);
15259
15261
  const measured = await collectMetrics(root, {
15260
- allowlistPath: resolve5(root, config.duplicateAllowlist)
15262
+ allowlistPath: resolve6(root, config.duplicateAllowlist),
15263
+ dispatchCalls: config.dispatchCalls
15261
15264
  });
15262
15265
  const markers = [NO_VERIFICATION_MARKER];
15263
15266
  if (config.verificationMarker !== null && !markers.includes(config.verificationMarker)) {
@@ -15289,12 +15292,12 @@ import {
15289
15292
  statSync as statSync7,
15290
15293
  writeFileSync as writeFileSync3
15291
15294
  } from "node:fs";
15292
- 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";
15293
15296
 
15294
15297
  // src/chunk/manifest.ts
15295
15298
  import { createHash } from "node:crypto";
15296
15299
  import { readFileSync as readFileSync18 } from "node:fs";
15297
- 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";
15298
15301
 
15299
15302
  // src/chunk/splitters.ts
15300
15303
  import { extname } from "node:path";
@@ -15925,7 +15928,7 @@ function relativeSource(manifestDir, sourcePath) {
15925
15928
  return relative3(manifestDir, sourcePath).split(sep2).join("/");
15926
15929
  }
15927
15930
  function resolveSource(manifestDir, sourceFile) {
15928
- return resolve6(manifestDir, sourceFile);
15931
+ return resolve7(manifestDir, sourceFile);
15929
15932
  }
15930
15933
  function readManifest(path) {
15931
15934
  return validateManifest(JSON.parse(readFileSync18(path, "utf8")));
@@ -16144,34 +16147,18 @@ function isOutsideText(folder, path) {
16144
16147
  const rel = relative4(folder, path);
16145
16148
  return rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute3(rel);
16146
16149
  }
16147
- function realPath2(path) {
16148
- let existing = resolve7(path);
16149
- const rest = [];
16150
- while (!lstatSync4(existing, { throwIfNoEntry: false })) {
16151
- const up = dirname7(existing);
16152
- if (up === existing)
16153
- return;
16154
- rest.unshift(basename7(existing));
16155
- existing = up;
16156
- }
16157
- try {
16158
- return join21(realpathSync2.native(existing), ...rest);
16159
- } catch {
16160
- return;
16161
- }
16162
- }
16163
16150
  function isOutside(folder, path) {
16164
16151
  if (isOutsideText(folder, path))
16165
16152
  return true;
16166
- const realFolder = realPath2(folder);
16167
- const realTarget = realPath2(path);
16153
+ const realFolder = realPath(folder);
16154
+ const realTarget = realPath(path);
16168
16155
  return realFolder === undefined || realTarget === undefined || isOutsideText(realFolder, realTarget);
16169
16156
  }
16170
16157
  function isInside(folder, path) {
16171
16158
  if (!isOutsideText(folder, path))
16172
16159
  return true;
16173
- const realFolder = realPath2(folder);
16174
- const realTarget = realPath2(path);
16160
+ const realFolder = realPath(folder);
16161
+ const realTarget = realPath(path);
16175
16162
  return realFolder !== undefined && realTarget !== undefined && !isOutsideText(realFolder, realTarget);
16176
16163
  }
16177
16164
  function chunkFolderError(chunksDir, paths) {
@@ -16184,11 +16171,11 @@ function chunkFolderError(chunksDir, paths) {
16184
16171
  return;
16185
16172
  }
16186
16173
  function outsideError(sourcePath, chunksDir, remedy) {
16187
- 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}
16188
16175
  `;
16189
16176
  }
16190
16177
  function readChunk(chunksDir, filename) {
16191
- const chunkPath = join21(chunksDir, filename);
16178
+ const chunkPath = join22(chunksDir, filename);
16192
16179
  const stat = lstatSync4(chunkPath, { throwIfNoEntry: false });
16193
16180
  if (!stat)
16194
16181
  return;
@@ -16222,7 +16209,7 @@ function split(inputFile, options, io) {
16222
16209
  const log = lineWriter(io);
16223
16210
  const maxLines = options.maxLines ?? 500;
16224
16211
  const dryRun = options.dryRun ?? false;
16225
- const absoluteInput = resolve7(inputFile);
16212
+ const absoluteInput = resolve8(inputFile);
16226
16213
  if (!existsSync8(absoluteInput)) {
16227
16214
  io.stderr(`Error: File not found: ${absoluteInput}
16228
16215
  `);
@@ -16239,7 +16226,7 @@ function split(inputFile, options, io) {
16239
16226
  const content = readFileSync19(absoluteInput, "utf8");
16240
16227
  const sourceHash = contentHash(content);
16241
16228
  const baseName = basename7(inputFile, extname2(inputFile));
16242
- 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`);
16243
16230
  const sourceFile = relativeSource(outputDir, absoluteInput);
16244
16231
  if (isAbsoluteAnywhere(sourceFile)) {
16245
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.
@@ -16281,7 +16268,7 @@ chunker - Splitting File`);
16281
16268
  const large = lineCount > maxLines ? " ⚠️ [LARGE]" : "";
16282
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}`);
16283
16270
  if (!dryRun)
16284
- writeLf(join21(outputDir, filename), section.content);
16271
+ writeLf(join22(outputDir, filename), section.content);
16285
16272
  return {
16286
16273
  index: i2 + 1,
16287
16274
  filename,
@@ -16305,7 +16292,7 @@ chunker - Splitting File`);
16305
16292
  ...lineEnding === "crlf" ? { lineEnding } : {},
16306
16293
  ...lineEnding === "mixed" ? { lineBreaks: lineBreakRuns(content) } : {}
16307
16294
  };
16308
- const manifestPath = join21(outputDir, MANIFEST_NAME);
16295
+ const manifestPath = join22(outputDir, MANIFEST_NAME);
16309
16296
  if (!dryRun) {
16310
16297
  writeManifest(manifestPath, manifest);
16311
16298
  log(`
@@ -16334,7 +16321,7 @@ To edit: Modify individual chunk files in the directory`);
16334
16321
  function merge(manifestFile, options, io) {
16335
16322
  const log = lineWriter(io);
16336
16323
  const dryRun = options.dryRun ?? false;
16337
- const absoluteManifest = resolve7(manifestFile);
16324
+ const absoluteManifest = resolve8(manifestFile);
16338
16325
  if (!existsSync8(absoluteManifest)) {
16339
16326
  io.stderr(`Error: Manifest not found: ${absoluteManifest}
16340
16327
  `);
@@ -16347,17 +16334,17 @@ function merge(manifestFile, options, io) {
16347
16334
  return 1;
16348
16335
  }
16349
16336
  const manifest = readManifest(absoluteManifest);
16350
- const chunksDir = dirname7(absoluteManifest);
16337
+ const chunksDir = dirname6(absoluteManifest);
16351
16338
  const sourcePath = resolveSource(chunksDir, manifest.sourceFile);
16352
16339
  const selfError = chunkFolderError(chunksDir, {
16353
16340
  "source file": sourcePath,
16354
- ...options.output ? { "output file": resolve7(options.output) } : {}
16341
+ ...options.output ? { "output file": resolve8(options.output) } : {}
16355
16342
  });
16356
16343
  if (selfError) {
16357
16344
  io.stderr(selfError);
16358
16345
  return 1;
16359
16346
  }
16360
- const outside = isOutside(dirname7(chunksDir), sourcePath);
16347
+ const outside = isOutside(dirname6(chunksDir), sourcePath);
16361
16348
  if (outside && !options.output && !options.yes) {
16362
16349
  io.stderr(outsideError(sourcePath, chunksDir, "Use -o <file> to name the target, or --yes to confirm."));
16363
16350
  return 1;
@@ -16380,7 +16367,7 @@ chunker - Merging Chunks`);
16380
16367
  for (const chunk of manifest.chunks) {
16381
16368
  const content = readChunk(chunksDir, chunk.filename);
16382
16369
  if (content === undefined) {
16383
- io.stderr(`Error: Missing chunk file: ${join21(chunksDir, chunk.filename)}
16370
+ io.stderr(`Error: Missing chunk file: ${join22(chunksDir, chunk.filename)}
16384
16371
  `);
16385
16372
  return 1;
16386
16373
  }
@@ -16413,7 +16400,7 @@ Modified chunks: ${modifiedCount} of ${manifest.chunks.length}`);
16413
16400
  NOTE: the number of lines changed. All line breaks use the most common original one.`);
16414
16401
  }
16415
16402
  }
16416
- const outputPath = options.output ? resolve7(options.output) : sourcePath;
16403
+ const outputPath = options.output ? resolve8(options.output) : sourcePath;
16417
16404
  if (readSource && existsSync8(sourcePath)) {
16418
16405
  const currentSourceHash = hash(readFileSync19(sourcePath, "utf8"));
16419
16406
  if (currentSourceHash !== manifest.sourceHash) {
@@ -16454,7 +16441,7 @@ Merged file written: ${outputPath}`);
16454
16441
  }
16455
16442
  function status(manifestFile, options, io) {
16456
16443
  const log = lineWriter(io);
16457
- const absoluteManifest = resolve7(manifestFile);
16444
+ const absoluteManifest = resolve8(manifestFile);
16458
16445
  if (!existsSync8(absoluteManifest)) {
16459
16446
  io.stderr(`Error: Manifest not found: ${absoluteManifest}
16460
16447
  `);
@@ -16467,14 +16454,14 @@ function status(manifestFile, options, io) {
16467
16454
  return 1;
16468
16455
  }
16469
16456
  const manifest = readManifest(absoluteManifest);
16470
- const chunksDir = dirname7(absoluteManifest);
16457
+ const chunksDir = dirname6(absoluteManifest);
16471
16458
  const sourcePath = resolveSource(chunksDir, manifest.sourceFile);
16472
16459
  const selfError = chunkFolderError(chunksDir, { "source file": sourcePath });
16473
16460
  if (selfError) {
16474
16461
  io.stderr(selfError);
16475
16462
  return 1;
16476
16463
  }
16477
- if (isOutside(dirname7(chunksDir), sourcePath) && !options.yes) {
16464
+ if (isOutside(dirname6(chunksDir), sourcePath) && !options.yes) {
16478
16465
  io.stderr(outsideError(sourcePath, chunksDir, "Use --yes to read it."));
16479
16466
  return 1;
16480
16467
  }
@@ -16595,7 +16582,7 @@ ${CHUNK_HELP}`);
16595
16582
  }
16596
16583
 
16597
16584
  // src/clones/index.ts
16598
- import { join as join22, resolve as resolve8 } from "node:path";
16585
+ import { join as join23, resolve as resolve9 } from "node:path";
16599
16586
 
16600
16587
  // src/clones/detect.ts
16601
16588
  function fnv1a(text) {
@@ -16729,7 +16716,7 @@ ${items.map((item) => ` ${JSON.stringify(item)}`).join(`,
16729
16716
  ]`;
16730
16717
  }
16731
16718
  function clonesJson(config, detection) {
16732
- const { out: _out, ...params } = config;
16719
+ const { out: _out, parseErrorList: _list, ...params } = config;
16733
16720
  return [
16734
16721
  "{",
16735
16722
  ` "params": ${JSON.stringify(params)},`,
@@ -16765,8 +16752,15 @@ function summaryText(config, stats, units, detection) {
16765
16752
  byFolder.set(key, (byFolder.get(key) ?? 0) + 1);
16766
16753
  }
16767
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
+ ];
16768
16761
  const lines = [
16769
- `files=${stats.files} parseErrors=${stats.parseErrors} units(minNodes=${config.minNodes})=${units.length}`,
16762
+ `files=${stats.files} parseErrors=${failed} units(minNodes=${config.minNodes})=${units.length}`,
16763
+ ...errorList,
16770
16764
  `nodes p50=${percentile(sizes, 0.5)} p90=${percentile(sizes, 0.9)} p99=${percentile(sizes, 0.99)} max=${sizes[sizes.length - 1] ?? 0}`,
16771
16765
  `exact groups=${detection.exactGroups.length} (units in them=${inGroups}) text-identical groups=${detection.textGroups.length}`,
16772
16766
  `near pairs=${detection.near.length} by jStruct: ${bands.join(", ")}`,
@@ -16787,7 +16781,8 @@ var CLONES_DEFAULTS = Object.freeze({
16787
16781
  containmentThreshold: 0.9,
16788
16782
  containmentMinShingles: 20,
16789
16783
  rareShingleLimit: 60,
16790
- minSharedShingles: 3
16784
+ minSharedShingles: 3,
16785
+ parseErrorList: 20
16791
16786
  });
16792
16787
  function mergeClonesConfig(cli, file) {
16793
16788
  const d = CLONES_DEFAULTS;
@@ -16801,7 +16796,8 @@ function mergeClonesConfig(cli, file) {
16801
16796
  containmentThreshold: cli.containmentThreshold ?? file.containmentThreshold ?? d.containmentThreshold,
16802
16797
  containmentMinShingles: cli.containmentMinShingles ?? file.containmentMinShingles ?? d.containmentMinShingles,
16803
16798
  rareShingleLimit: cli.rareShingleLimit ?? file.rareShingleLimit ?? d.rareShingleLimit,
16804
- minSharedShingles: cli.minSharedShingles ?? file.minSharedShingles ?? d.minSharedShingles
16799
+ minSharedShingles: cli.minSharedShingles ?? file.minSharedShingles ?? d.minSharedShingles,
16800
+ parseErrorList: cli.parseErrorList ?? file.parseErrorList ?? d.parseErrorList
16805
16801
  };
16806
16802
  }
16807
16803
 
@@ -17046,7 +17042,7 @@ function unitsOf(root, source, file, minNodes) {
17046
17042
  var CLONES_HELP = `Usage: repo-tools clones [root] [--out=<dir>] [--config=<path>] [--src=<path>...]
17047
17043
  [--min-nodes=<n>] [--shingle=<n>] [--near=<f>]
17048
17044
  [--containment=<f>] [--containment-min=<n>]
17049
- [--rare=<n>] [--min-shared=<n>]
17045
+ [--rare=<n>] [--min-shared=<n>] [--list-errors=<n>]
17050
17046
 
17051
17047
  List the functions of a TypeScript or JavaScript repository and find the functions
17052
17048
  that copy each other. The command only reads the repository. The result is a list
@@ -17074,6 +17070,8 @@ shingles (runs of --shingle labels) overlap enough are a near pair.
17074
17070
  pairs. Default 60.
17075
17071
  --min-shared=<n> The rare shingles that a candidate pair must share.
17076
17072
  Default 3.
17073
+ --list-errors=<n> The most paths of files with a parse error that the
17074
+ summary lists. Default 20.
17077
17075
  --root=<path> The root. The default is the current folder.
17078
17076
  --help, -h Show this help.
17079
17077
 
@@ -17093,7 +17091,8 @@ var VALUE_FLAGS2 = {
17093
17091
  "--containment": "containmentThreshold",
17094
17092
  "--containment-min": "containmentMinShingles",
17095
17093
  "--rare": "rareShingleLimit",
17096
- "--min-shared": "minSharedShingles"
17094
+ "--min-shared": "minSharedShingles",
17095
+ "--list-errors": "parseErrorList"
17097
17096
  };
17098
17097
  var NUMERIC = new Set([
17099
17098
  "minNodes",
@@ -17102,7 +17101,8 @@ var NUMERIC = new Set([
17102
17101
  "containmentThreshold",
17103
17102
  "containmentMinShingles",
17104
17103
  "rareShingleLimit",
17105
- "minSharedShingles"
17104
+ "minSharedShingles",
17105
+ "parseErrorList"
17106
17106
  ]);
17107
17107
  function parseArgs2(argv) {
17108
17108
  const options = { root: ".", config: undefined, settings: {} };
@@ -17179,39 +17179,39 @@ function selectFiles(root, src) {
17179
17179
  }
17180
17180
  async function readUnits(root, files, minNodes) {
17181
17181
  const units = [];
17182
- let parseErrors = 0;
17182
+ const parseErrorFiles = [];
17183
17183
  for (const file of files) {
17184
- const source = readSource(join22(root, file)).replace(/^\uFEFF/, "");
17184
+ const source = readSource(join23(root, file)).replace(/^\uFEFF/, "");
17185
17185
  const grammar = grammarFor(file);
17186
17186
  await loadGrammar(grammar);
17187
17187
  const tree = parserFor(grammar).parse(source);
17188
17188
  if (!tree) {
17189
- parseErrors += 1;
17189
+ parseErrorFiles.push(file);
17190
17190
  continue;
17191
17191
  }
17192
17192
  try {
17193
17193
  if (tree.rootNode.hasError)
17194
- parseErrors += 1;
17194
+ parseErrorFiles.push(file);
17195
17195
  units.push(...unitsOf(tree.rootNode, source, file, minNodes));
17196
17196
  } finally {
17197
17197
  tree.delete();
17198
17198
  }
17199
17199
  }
17200
- return { units, parseErrors };
17200
+ return { units, parseErrorFiles };
17201
17201
  }
17202
17202
  async function execute(root, config, io) {
17203
17203
  const files = selectFiles(root, config.src);
17204
17204
  if (files.length === 0) {
17205
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
17206
  }
17207
- const { units, parseErrors } = await readUnits(root, files, config.minNodes);
17207
+ const { units, parseErrorFiles } = await readUnits(root, files, config.minNodes);
17208
17208
  const detection = detect(units, config);
17209
- io.stdout(summaryText(config, { files: files.length, parseErrors }, units, detection));
17209
+ io.stdout(summaryText(config, { files: files.length, parseErrorFiles }, units, detection));
17210
17210
  if (config.out !== null) {
17211
- const out2 = resolve8(root, config.out);
17211
+ const out2 = resolve9(root, config.out);
17212
17212
  try {
17213
- writeLf(join22(out2, "inventory.jsonl"), inventoryJsonl(units));
17214
- writeLf(join22(out2, "clones.json"), clonesJson(config, detection));
17213
+ writeLf(join23(out2, "inventory.jsonl"), inventoryJsonl(units));
17214
+ writeLf(join23(out2, "clones.json"), clonesJson(config, detection));
17215
17215
  } catch {
17216
17216
  throw new Error(`cannot write the output folder '${config.out}'`);
17217
17217
  }
@@ -17227,7 +17227,7 @@ async function run4(argv, io) {
17227
17227
  const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
17228
17228
  try {
17229
17229
  const options = parseArgs2(argv);
17230
- root = resolve8(options.root);
17230
+ root = resolve9(options.root);
17231
17231
  if (!isDirectory(root))
17232
17232
  throw new Error("the root <root> is not an existing directory");
17233
17233
  const file = loadConfigSections(root, options.config).clones;
@@ -17242,7 +17242,7 @@ async function run4(argv, io) {
17242
17242
 
17243
17243
  // src/compress/index.ts
17244
17244
  import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync20, statSync as statSync8, writeFileSync as writeFileSync4 } from "node:fs";
17245
- import { basename as basename8, dirname as dirname8, extname as extname4, join as join23 } from "node:path";
17245
+ import { basename as basename8, dirname as dirname7, extname as extname4, join as join24 } from "node:path";
17246
17246
 
17247
17247
  // src/compress/formats.ts
17248
17248
  import { extname as extname3 } from "node:path";
@@ -17998,7 +17998,7 @@ function findFiles(dir, pattern, recursive, deps = defaultDeps) {
17998
17998
  const scan = (current) => {
17999
17999
  const entries = [...deps.readdir(current)].sort((a, b) => compareCodeUnits(a.name, b.name));
18000
18000
  for (const entry of entries) {
18001
- const full = join23(current, entry.name);
18001
+ const full = join24(current, entry.name);
18002
18002
  if (entry.isDirectory()) {
18003
18003
  if (recursive && !entry.name.startsWith(".") && entry.name !== "node_modules")
18004
18004
  scan(full);
@@ -18015,16 +18015,16 @@ function isDirectory2(path) {
18015
18015
  }
18016
18016
  function compactName(file) {
18017
18017
  const ext = extname4(file);
18018
- return join23(dirname8(file), `${basename8(file, ext)}.compact${ext}`);
18018
+ return join24(dirname7(file), `${basename8(file, ext)}.compact${ext}`);
18019
18019
  }
18020
18020
  function restoredName(file) {
18021
18021
  const base = basename8(file);
18022
18022
  const stripped = base.replace(/\.compact(?=\.[^.]*$|$)/i, "");
18023
- const output = stripped === base ? file : join23(dirname8(file), stripped);
18023
+ const output = stripped === base ? file : join24(dirname7(file), stripped);
18024
18024
  if (output !== file)
18025
18025
  return output;
18026
18026
  const ext = extname4(file);
18027
- return join23(dirname8(file), `${basename8(file, ext)}.restored${ext}`);
18027
+ return join24(dirname7(file), `${basename8(file, ext)}.restored${ext}`);
18028
18028
  }
18029
18029
  function assertCanRestore(output, o) {
18030
18030
  if (!o.dryRun && !o.yes && existsSync9(output)) {
@@ -18238,16 +18238,16 @@ async function run5(argv, io, deps = defaultDeps) {
18238
18238
 
18239
18239
  // src/docs/index.ts
18240
18240
  import { lstatSync as lstatSync5, readFileSync as readFileSync23, realpathSync as realpathSync3, writeFileSync as writeFileSync5 } from "node:fs";
18241
- import { basename as basename9, isAbsolute as isAbsolute4, join as join27, relative as relative5, resolve as resolve9, 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";
18242
18242
 
18243
18243
  // src/docs/analyse.ts
18244
18244
  import { readFileSync as readFileSync22 } from "node:fs";
18245
- import { join as join25 } from "node:path";
18245
+ import { join as join26 } from "node:path";
18246
18246
 
18247
18247
  // src/docs/discovery.ts
18248
18248
  import { spawnSync as spawnSync2 } from "node:child_process";
18249
18249
  import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync21, statSync as statSync9 } from "node:fs";
18250
- import { join as join24 } from "node:path";
18250
+ import { join as join25 } from "node:path";
18251
18251
  var PYTHON_SUFFIXES2 = new Set([".py"]);
18252
18252
  var TYPESCRIPT_SUFFIXES = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
18253
18253
  var SKIP_DIRS2 = new Set([
@@ -18331,8 +18331,8 @@ function walkSource(root) {
18331
18331
  const rel = `${prefix}${entry.name}`;
18332
18332
  if (entry.isDirectory()) {
18333
18333
  if (!SKIP_DIRS2.has(entry.name))
18334
- go(join24(dir, entry.name), `${rel}/`);
18335
- } else if (isSource(rel) && statOk(join24(dir, entry.name))) {
18334
+ go(join25(dir, entry.name), `${rel}/`);
18335
+ } else if (isSource(rel) && statOk(join25(dir, entry.name))) {
18336
18336
  found.push(rel);
18337
18337
  }
18338
18338
  }
@@ -18348,7 +18348,7 @@ function statOk(path) {
18348
18348
  }
18349
18349
  }
18350
18350
  function loadExclusions(root) {
18351
- const file = join24(root, CONFIG_NAME);
18351
+ const file = join25(root, CONFIG_NAME);
18352
18352
  if (!existsSync10(file) || !statOk(file))
18353
18353
  return [];
18354
18354
  let data;
@@ -18956,9 +18956,9 @@ var DECLARATIONS = {
18956
18956
  var DECLARATION_TYPES = Object.keys(DECLARATIONS);
18957
18957
  var SPACE_CLASS = "[ \\t\\n\\r\\f\\v]";
18958
18958
  var ABSTRACT_PROPERTY = new RegExp(`\\babstract(${SPACE_CLASS}*\\??${SPACE_CLASS}*:)`, "g");
18959
- 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");
18960
18960
  function sanitise(source) {
18961
- 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");
18962
18962
  }
18963
18963
  var IDENTIFIERS2 = new Set(["identifier", "shorthand_property_identifier_pattern"]);
18964
18964
  function boundIdentifiers(node, text) {
@@ -19203,7 +19203,7 @@ async function analyseFile(root, rel) {
19203
19203
  const language = languageOf(rel);
19204
19204
  let source;
19205
19205
  try {
19206
- source = readSourceText2(join25(root, rel));
19206
+ source = readSourceText2(join26(root, rel));
19207
19207
  } catch (error) {
19208
19208
  const report = newReport(rel, language);
19209
19209
  report.error = `unreadable: ${readFailure(error)}`;
@@ -19220,7 +19220,7 @@ async function analyseRepo(root) {
19220
19220
  }
19221
19221
 
19222
19222
  // src/docs/report.ts
19223
- import { join as join26 } from "node:path";
19223
+ import { join as join27 } from "node:path";
19224
19224
  function percent3(part, whole) {
19225
19225
  if (whole === 0)
19226
19226
  return 100;
@@ -19332,8 +19332,8 @@ function symbolJson(s) {
19332
19332
  };
19333
19333
  }
19334
19334
  function writeOutputs(outDir, reports, stats, provenance, rootName) {
19335
- const markdown = join26(outDir, "COVERAGE.md");
19336
- const json = join26(outDir, "coverage.json");
19335
+ const markdown = join27(outDir, "COVERAGE.md");
19336
+ const json = join27(outDir, "coverage.json");
19337
19337
  writeReport(markdown, renderMarkdown(reports, stats, provenance, rootName));
19338
19338
  const payload = {
19339
19339
  summary: statsJson(stats),
@@ -19685,7 +19685,7 @@ async function runScan(root, options, io) {
19685
19685
  if (reports.length === 0)
19686
19686
  throw new NothingToMeasure(provenance);
19687
19687
  const stats = summarise(reports);
19688
- const outDir = resolve9(options.out ?? join27(root, "docs", "code-docs"));
19688
+ const outDir = resolve10(options.out ?? join28(root, "docs", "code-docs"));
19689
19689
  const written = writeOutputs(outDir, reports, stats, provenance, basename9(root));
19690
19690
  io.stdout(`scanned ${stats.filesScanned} file(s) via ${provenance}
19691
19691
  `);
@@ -19725,7 +19725,7 @@ async function runStub(root, options, io) {
19725
19725
  const targets = report.symbols.filter((s) => s.issues.some((i2) => i2.rule === "M1"));
19726
19726
  if (targets.length === 0)
19727
19727
  continue;
19728
- const file = join27(root, report.path);
19728
+ const file = join28(root, report.path);
19729
19729
  if (leavesRoot(root, file)) {
19730
19730
  io.stdout(` SKIPPED ${report.path}: the path is a link, or it resolves outside the root
19731
19731
  `);
@@ -19860,7 +19860,7 @@ async function run6(argv, io) {
19860
19860
  const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
19861
19861
  try {
19862
19862
  const options = parseDocsArgs(argv);
19863
- root = resolve9(options.root);
19863
+ root = resolve10(options.root);
19864
19864
  if (!isDirectory(root))
19865
19865
  throw new Error("the root <root> is not an existing directory");
19866
19866
  assertReadableLanguage(root);
@@ -19879,7 +19879,7 @@ async function run6(argv, io) {
19879
19879
 
19880
19880
  // src/query/index.ts
19881
19881
  import { existsSync as existsSync12, statSync as statSync10 } from "node:fs";
19882
- import { resolve as resolve10 } from "node:path";
19882
+ import { resolve as resolve11 } from "node:path";
19883
19883
 
19884
19884
  // src/query/args.ts
19885
19885
  var MODE_FLAGS = {
@@ -20461,7 +20461,7 @@ async function run7(argv, io) {
20461
20461
  const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
20462
20462
  try {
20463
20463
  const options = parseQueryArgs(argv, process.cwd());
20464
- root = resolve10(options.root);
20464
+ root = resolve11(options.root);
20465
20465
  if (!(existsSync12(root) && statSync10(root).isDirectory())) {
20466
20466
  throw new Error("the root <root> is not an existing directory");
20467
20467
  }
@@ -20497,7 +20497,7 @@ async function run7(argv, io) {
20497
20497
 
20498
20498
  // src/ste/index.ts
20499
20499
  import { existsSync as existsSync13, lstatSync as lstatSync6, readdirSync as readdirSync5, readFileSync as readFileSync26, statSync as statSync11 } from "node:fs";
20500
- import { basename as basename10, join as join29 } from "node:path";
20500
+ import { basename as basename11, join as join30, resolve as resolve13 } from "node:path";
20501
20501
 
20502
20502
  // src/ste/markdown.ts
20503
20503
  function stripNoise(text) {
@@ -20552,7 +20552,12 @@ function wordCount(sentence) {
20552
20552
  const s = sentence.replace(CODE_SPAN, "X").replace(LINK, "$1").replace(EMPHASIS_CHARS, "").replace(LIST_MARKER, "");
20553
20553
  return pySplit(s).filter((w) => Array.from(w).some(isAlnum)).length;
20554
20554
  }
20555
- function join28(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) {
20556
20561
  const chunks = [];
20557
20562
  const lineMap = [];
20558
20563
  parts2.forEach(([n, body2], i2) => {
@@ -20582,7 +20587,7 @@ function paragraphs(text) {
20582
20587
  for (const [n, line] of stripNoise(text)) {
20583
20588
  const startsItem = LIST_ITEM.test(line);
20584
20589
  if ((n !== prevLine + 1 || startsItem) && para.length > 0) {
20585
- blocks.push(join28(para, paraStart));
20590
+ blocks.push(join29(para, paraStart));
20586
20591
  para = [];
20587
20592
  }
20588
20593
  if (para.length === 0)
@@ -20591,42 +20596,112 @@ function paragraphs(text) {
20591
20596
  prevLine = n;
20592
20597
  }
20593
20598
  if (para.length > 0)
20594
- blocks.push(join28(para, paraStart));
20599
+ blocks.push(join29(para, paraStart));
20595
20600
  return blocks;
20596
20601
  }
20597
- function checkMarkdown(name2, text) {
20598
- const problems = [];
20602
+ function checkMarkdownFindings(name2, text) {
20603
+ const found = [];
20599
20604
  const blocks = paragraphs(text);
20600
- for (const [start2, block] of blocks) {
20605
+ for (const [start2, block, lineMap] of blocks) {
20601
20606
  const procedural = PROCEDURAL_STARTS.some((p) => pyLstrip(block).startsWith(p));
20602
20607
  const limit = procedural ? MAX_WORDS_PROCEDURAL : MAX_WORDS_DESCRIPTIVE;
20608
+ const last = lineMap.length > 0 ? lineMap[lineMap.length - 1] : start2;
20603
20609
  for (const sentence of sentences(block)) {
20604
20610
  const n = wordCount(sentence);
20605
- if (n > limit) {
20611
+ if (n > limit && !isIdentifierList(sentence)) {
20606
20612
  const tier = procedural ? "procedural" : "descriptive";
20607
- 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
+ });
20608
20618
  }
20609
20619
  }
20610
20620
  }
20611
20621
  for (const [, block, lineMap] of blocks) {
20612
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
+ };
20613
20627
  for (const [phrase, better, re] of WORDY_RULES) {
20614
20628
  for (const m of bare.matchAll(re)) {
20615
- 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)}`));
20616
20630
  }
20617
20631
  }
20618
20632
  for (const m of bare.matchAll(AMBIGUOUS2)) {
20619
- 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]))}`));
20620
20634
  }
20621
20635
  for (const m of bare.matchAll(PASSIVE2)) {
20622
- 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]))}`));
20623
20637
  }
20624
20638
  }
20625
- 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);
20626
20701
  }
20627
20702
 
20628
20703
  // src/ste/index.ts
20629
- 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>]
20630
20705
  repo-tools ste --prose <file>
20631
20706
 
20632
20707
  Check prose against the mechanically decidable part of ASD-STE100 Simplified
@@ -20636,11 +20711,25 @@ cannot judge the approved dictionary.
20636
20711
 
20637
20712
  Markdown mode (the default) checks each Markdown file, or each .md file below a
20638
20713
  directory, in code-unit order of the path. A descriptive sentence may have 25
20639
- words; a procedural one (a numbered step or a check box) may have 20. Code
20640
- fences, tables, headings, quotes, comments, frontmatter, inline code and
20641
- 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
20642
20720
  "<!-- ste:historical-record -->" is skipped, and the skip is counted.
20643
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.
20644
20733
  --prose <file> Check the file as one docstring. Tag lines (@param), section
20645
20734
  headers, doctest lines and code fences are removed first. Each
20646
20735
  finding prints as "RULE<TAB>detail". The findings are advice:
@@ -20649,7 +20738,9 @@ identifiers are not checked. A file whose first five lines hold
20649
20738
 
20650
20739
  Exit codes: 0 when no finding (Markdown mode) or always (--prose mode). 1 when a
20651
20740
  Markdown finding exists. 2 on a usage error, when a path does not exist, when no
20652
- 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.
20653
20744
  `;
20654
20745
  var HISTORICAL_MARKER = "<!-- ste:historical-record -->";
20655
20746
  function readText(path) {
@@ -20678,7 +20769,7 @@ function targetsFor(path) {
20678
20769
  return;
20679
20770
  }
20680
20771
  for (const entry of entries) {
20681
- const full = join29(dir, entry.name);
20772
+ const full = join30(dir, entry.name);
20682
20773
  if (entry.isDirectory()) {
20683
20774
  if (entry.name !== ".pytest_cache")
20684
20775
  walk(full, [...parts2, entry.name]);
@@ -20691,7 +20782,7 @@ function targetsFor(path) {
20691
20782
  };
20692
20783
  if (existsSync13(path) && lstatSync6(path).isDirectory())
20693
20784
  walk(path, []);
20694
- return found.sort(comparePaths).map((parts2) => join29(path, ...parts2));
20785
+ return found.sort(comparePaths).map((parts2) => join30(path, ...parts2));
20695
20786
  }
20696
20787
  function isHistoricalRecord(path) {
20697
20788
  let text;
@@ -20713,7 +20804,7 @@ function runProse(args2, io) {
20713
20804
  try {
20714
20805
  text = readText(file);
20715
20806
  } catch {
20716
- 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
20717
20808
  `);
20718
20809
  return 2;
20719
20810
  }
@@ -20722,10 +20813,41 @@ function runProse(args2, io) {
20722
20813
  `);
20723
20814
  return 0;
20724
20815
  }
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
+ }
20725
20840
  async function run8(argv, io) {
20726
20841
  if (argv[0] === "--prose")
20727
20842
  return runProse(argv.slice(1), io);
20728
- 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) {
20729
20851
  io.stdout(`usage: repo-tools ste <file-or-directory> [...]
20730
20852
  `);
20731
20853
  return 2;
@@ -20733,12 +20855,15 @@ async function run8(argv, io) {
20733
20855
  let total = 0;
20734
20856
  let checked = 0;
20735
20857
  let skipped = 0;
20858
+ let hidden = 0;
20736
20859
  const missing = [];
20737
- for (const arg of argv) {
20860
+ const examined = new Set;
20861
+ for (const arg of paths) {
20738
20862
  if (!existsSync13(arg))
20739
20863
  missing.push(arg);
20740
20864
  for (const target of targetsFor(arg)) {
20741
- const name2 = basename10(target);
20865
+ const name2 = basename11(target);
20866
+ examined.add(pathKey(target));
20742
20867
  if (isHistoricalRecord(target)) {
20743
20868
  skipped += 1;
20744
20869
  io.stdout(`
@@ -20754,30 +20879,62 @@ async function run8(argv, io) {
20754
20879
  `);
20755
20880
  return 2;
20756
20881
  }
20757
- 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
+ }
20758
20897
  checked += 1;
20759
- total += problems.length;
20760
- 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)`;
20761
20900
  io.stdout(`
20762
20901
  === ${name2}: ${status} ===
20763
20902
  `);
20764
- for (const p of problems)
20765
- io.stdout(` ${p}
20903
+ for (const f of findings)
20904
+ io.stdout(` ${f.text}
20766
20905
  `);
20767
20906
  }
20768
20907
  }
20769
- 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("; ")}` : "";
20770
20915
  io.stdout(`
20771
20916
  TOTAL: ${total} problem(s) in ${checked} file(s)${tail}
20772
20917
  `);
20773
20918
  for (const path of missing)
20774
20919
  io.stderr(`ERROR: path does not exist: ${path}
20775
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
+ }
20776
20933
  if (checked === 0) {
20777
20934
  io.stderr(`ERROR: no Markdown file was examined. This is a failure, not a clean result -- check the path(s) given.
20778
20935
  `);
20779
20936
  }
20780
- if (missing.length > 0 || checked === 0)
20937
+ if (missing.length > 0 || checked === 0 || requiredFailed)
20781
20938
  return 2;
20782
20939
  return total > 0 ? 1 : 0;
20783
20940
  }