@danielsimonjr/repo-tools 2.2.5 → 2.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,44 @@ All notable changes to this project are recorded in this file. The format follow
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.2.7] - 2026-10-22
10
+
11
+ ### Fixed
12
+
13
+ - `repo-tools ste` exits 2 when any path in the argument list does not exist, and prints `ERROR: path does not exist:
14
+ <path>` on stderr for each such path. Before, a mistyped path among valid paths was ignored and the run exited 0.
15
+ The report of each valid path is still printed.
16
+ - The warning that `repo-tools map` writes for a `package.json` entry that does not resolve is shorter (15 words).
17
+ It had 30 words, so `repo-tools ste` flagged the generated `FILE_INVENTORY.md`. A test now checks the word limit
18
+ of every warning and that the generated report passes `repo-tools ste`.
19
+ - `repo-tools map` reads a folder named `test/` as a tests folder, as version 1.x did. Before, only `tests/` counted,
20
+ so a file such as `test/setup.ts` got the area `src` in `FILE_INVENTORY.md` and counted as a source file in
21
+ `TEST_COVERAGE.md`. The disposition legend and the `--tests` message name both folders.
22
+ - The `TEST_COVERAGE.md` note on the coverage policy no longer links `COVERAGE_POLICY.md`, a file that the tool does
23
+ not write, and no longer names mathjs or AssemblyScript.
24
+ - The `Expected test` hint in `TEST_COVERAGE.md` names `tests/unit/<module>/`. It appears only when the repository
25
+ keeps tests in `tests/unit/`. Other repositories get the plain list of untested files.
26
+ - The text of `duplicate-symbols.md` and of the `duplicate-symbols.json` note no longer names `Bucket C` or
27
+ `AssemblyScript`.
28
+
29
+ ## [2.2.6] - 2026-10-22
30
+
31
+ ### Fixed
32
+
33
+ - `repo-tools map` finds the entry roots of an app whose build output is not `dist/` and whose sources are not
34
+ only under `src/`. On an Electron app (main process built to `dist-electron/`, CLI built to `dist-cli/`, renderer
35
+ loaded from `src/index.html`) it reported 74 dormant files, among them 11 orphans, and now reports 5 dormant files,
36
+ among them 3 orphans. Four sources of roots were missing or broken:
37
+ - A `tsc -p <tsconfig>` script of the root package in a repository without workspaces seeds the files of that
38
+ tsconfig. Only a workspace package did this before.
39
+ - A tsconfig that holds comments or trailing commas is read. `JSON.parse` rejected it and the whole tsconfig was
40
+ ignored without a message.
41
+ - A root build or test config names entry files with an `entry`, `input`, `setupFiles`, `globalSetup` or
42
+ `globalTeardown` key (for example `path.resolve(__dirname, 'electron/main.ts')`). Each such file is a root.
43
+ Only `new URL(...)` was read before.
44
+ - The module script of `index.html` or `src/index.html` is a root.
45
+ The ambient declaration files (`vite-env.d.ts`) stay in the list of files without an importer, as before.
46
+
9
47
  ## [2.2.5] - 2026-10-22
10
48
 
11
49
  ### Changed
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  | `query` | Answers structural questions from the graph of `map`, and writes two derived reports. |
14
14
  | `ste` | Checks Markdown or docstring prose against Simplified Technical English. |
15
15
 
16
- Status: version 2.2.5.
16
+ Status: version 2.2.7.
17
17
 
18
18
  - `map`, `check` and `query` read their settings from `repo-tools.config.json` in the root. The
19
19
  section of `map` is `map` (its old name, `depgraph`, is also read). `--config=<file>` makes any
@@ -41,7 +41,7 @@ repo-tools --version
41
41
 
42
42
  The executable holds the tool and the Bun runtime. It needs no Bun, Node or `node_modules`.
43
43
  Download the file for your platform and `SHA256SUMS` from the
44
- [GitHub release](https://github.com/danielsimonjr/repo-tools/releases/tag/v2.2.5):
44
+ [GitHub release](https://github.com/danielsimonjr/repo-tools/releases/tag/v2.2.7):
45
45
 
46
46
  | Platform | File |
47
47
  |---|---|
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@danielsimonjr/repo-tools",
5
- version: "2.2.5",
5
+ version: "2.2.7",
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",
@@ -974,7 +974,7 @@ function mergeDepgraphConfig(cli, file) {
974
974
  // src/depgraph/args.ts
975
975
  var SCAN_SCOPE_FLAGS = {
976
976
  "--src": "the census finds the source files itself; the area of a file comes from its path",
977
- "--tests": "a test file is found by its name or its tests/ folder",
977
+ "--tests": "a test file is found by its name or by its test/ or tests/ folder",
978
978
  "--exclude": "the census reads the files that git tracks; list a file in .gitignore instead",
979
979
  "--also-exclude": "the census reads the files that git tracks; list a file in .gitignore instead",
980
980
  "--all": "the graph always holds every file; the disposition of each file shows reachability",
@@ -1372,7 +1372,7 @@ var TYPE_DUP_CATEGORIES = [
1372
1372
  { cat: "type", key: "types" },
1373
1373
  { cat: "enum", key: "enums" }
1374
1374
  ];
1375
- var DUPLICATE_SYMBOLS_NOTE = "This report groups names by OWN definition, not by call graph, then classifies each " + "flagged name (see DupEntryTag): TRUE_DUPLICATE (the actionable merge targets), " + "DISPATCH_VARIANT (>=2 mathTyped(...) registrations of the same public name — distinct " + "dispatch surfaces, Bucket C delegation candidates, 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: hot-path is* guards, AssemblyScript mirrors, 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.";
1375
+ var DUPLICATE_SYMBOLS_NOTE = "This report groups names by OWN definition, not by call graph, then classifies each " + "flagged name (see DupEntryTag): TRUE_DUPLICATE (the actionable merge targets), " + "DISPATCH_VARIANT (>=2 mathTyped(...) registrations of the same public name — distinct " + "dispatch surfaces, not copy-paste bodies), " + "ALIAS_DELEGATION (a const-alias forward to an imported symbol, not an independent body — " + "excluded once fewer than 2 real bodies remain), and ALLOWLISTED (matches " + "duplicate-allowlist.json: for example hot-path is* guards, mirrors in another language, " + "per-package VERSION strings). NOT detected: same-file typed-dispatch overload polymorphism for " + "different argument shapes within one registration — a human still triages TRUE_DUPLICATE " + "entries using the defining files + public flags before merging anything.";
1376
1376
  var DUP_ENTRY_TAG_SORT_ORDER = {
1377
1377
  TRUE_DUPLICATE: 0,
1378
1378
  DISPATCH_VARIANT: 1,
@@ -2066,13 +2066,55 @@ function tsupConfigEntries(root, pkgDir) {
2066
2066
  }
2067
2067
  return out2;
2068
2068
  }
2069
+ function stringEnd(text, start2) {
2070
+ let j = start2 + 1;
2071
+ while (j < text.length && text.charAt(j) !== '"')
2072
+ j += text.charAt(j) === "\\" ? 2 : 1;
2073
+ return Math.min(j + 1, text.length);
2074
+ }
2075
+ function stripJsonc(text) {
2076
+ let plain = "";
2077
+ for (let i2 = 0;i2 < text.length; ) {
2078
+ const c = text.charAt(i2);
2079
+ if (c === '"') {
2080
+ const end = stringEnd(text, i2);
2081
+ plain += text.slice(i2, end);
2082
+ i2 = end;
2083
+ } else if (c === "/" && text.charAt(i2 + 1) === "/") {
2084
+ while (i2 < text.length && text.charAt(i2) !== `
2085
+ `)
2086
+ i2++;
2087
+ } else if (c === "/" && text.charAt(i2 + 1) === "*") {
2088
+ const end = text.indexOf("*/", i2 + 2);
2089
+ i2 = end === -1 ? text.length : end + 2;
2090
+ } else {
2091
+ plain += c;
2092
+ i2++;
2093
+ }
2094
+ }
2095
+ let out2 = "";
2096
+ for (let i2 = 0;i2 < plain.length; ) {
2097
+ const c = plain.charAt(i2);
2098
+ if (c === '"') {
2099
+ const end = stringEnd(plain, i2);
2100
+ out2 += plain.slice(i2, end);
2101
+ i2 = end;
2102
+ } else if (c === "," && /^\s*[}\]]/.test(plain.slice(i2 + 1))) {
2103
+ i2++;
2104
+ } else {
2105
+ out2 += c;
2106
+ i2++;
2107
+ }
2108
+ }
2109
+ return out2;
2110
+ }
2069
2111
  function seedTsconfigEntries(root, pkgDir, cfgRel, add) {
2070
2112
  const cfgPath = join7(root, pkgDir, cfgRel);
2071
2113
  if (!existsSync4(cfgPath))
2072
2114
  return;
2073
2115
  let cfg;
2074
2116
  try {
2075
- cfg = JSON.parse(readFileSync3(cfgPath, "utf-8"));
2117
+ cfg = JSON.parse(stripJsonc(readFileSync3(cfgPath, "utf-8")));
2076
2118
  } catch {
2077
2119
  return;
2078
2120
  }
@@ -2226,6 +2268,8 @@ function selfPackage(root) {
2226
2268
  entryFiles: packageEntryFiles(root, pkg)
2227
2269
  };
2228
2270
  }
2271
+ var CONFIG_ENTRY_KEY = /\b(?:entry|input|setupFiles|globalSetup|globalTeardown)["']?\s*:\s*(\[[^\]]*\]|\{[^}]*\}|[\w$.]+\([^)]*\)|['"`][^'"`]*['"`])/g;
2272
+ var SOURCE_LITERAL = /['"`]([^'"`]+\.[cm]?[jt]sx?)['"`]/g;
2229
2273
  function configReferencedEntries(root) {
2230
2274
  const out2 = new Set;
2231
2275
  let names;
@@ -2247,9 +2291,36 @@ function configReferencedEntries(root) {
2247
2291
  for (const m of code.matchAll(/new\s+URL\(\s*['"`]([^'"`]*?src\/[\w./-]+\.ts)['"`]/g)) {
2248
2292
  out2.add((m[1] ?? "").replace(/^\.\//, ""));
2249
2293
  }
2294
+ for (const key of code.matchAll(CONFIG_ENTRY_KEY)) {
2295
+ for (const lit of (key[1] ?? "").matchAll(SOURCE_LITERAL)) {
2296
+ out2.add(toPosix(lit[1] ?? "").replace(/^\.\//, ""));
2297
+ }
2298
+ }
2250
2299
  }
2251
2300
  return [...out2];
2252
2301
  }
2302
+ function htmlReferencedEntries(root) {
2303
+ const out2 = [];
2304
+ for (const page of ["index.html", "src/index.html"]) {
2305
+ let html;
2306
+ try {
2307
+ html = readFileSync3(join7(root, page), "utf-8");
2308
+ } catch {
2309
+ continue;
2310
+ }
2311
+ for (const m of html.matchAll(/<script\b[^>]*?\bsrc\s*=\s*["']([^"']+)["']/gi)) {
2312
+ const src = m[1] ?? "";
2313
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(src) || !/\.[cm]?[jt]sx?$/.test(src))
2314
+ continue;
2315
+ const fromPage = toPosix(join7(dirname4(page), src)).replace(/^\.\//, "");
2316
+ const candidates = src.startsWith("/") ? [src.slice(1), fromPage] : [fromPage];
2317
+ const found = candidates.find((c) => existsSync4(join7(root, c)));
2318
+ if (found !== undefined && !out2.includes(found))
2319
+ out2.push(found);
2320
+ }
2321
+ }
2322
+ return out2;
2323
+ }
2253
2324
  function runtimeLaunchedEntries(root, parsed) {
2254
2325
  const known = new Set(parsed.map((f) => f.path));
2255
2326
  const out2 = new Set;
@@ -6307,7 +6378,7 @@ function classifyArea(relPath) {
6307
6378
  }
6308
6379
  if (CONFIG_PATTERN.test(relPath))
6309
6380
  return "config";
6310
- if (isTestFile || /(^|\/)tests\//.test(relPath))
6381
+ if (isTestFile || /(^|\/)tests?\//.test(relPath))
6311
6382
  return "tests";
6312
6383
  if (/^(bench|benchmarks)\//.test(relPath) || /\.bench\.\w+$/.test(relPath))
6313
6384
  return "benchmarks";
@@ -12364,7 +12435,7 @@ function packageJsonRoots(root, known) {
12364
12435
  continue;
12365
12436
  const mapped = mapDistToSrc(spec, known) ?? launcherEntry(root, spec, known) ?? declaredSource(spec, known);
12366
12437
  if (mapped === null) {
12367
- warnings.push(`package.json entry ${pyRepr(spec)} did not resolve to a known source file -- ` + "ignored; if no other declared entry resolves either, the entry-point root falls back to " + "a conventional src/index.* file instead");
12438
+ warnings.push(`package.json entry ${pyRepr(spec)} did not resolve to a known source file. The entry is ignored.`);
12368
12439
  continue;
12369
12440
  }
12370
12441
  if (!roots.includes(mapped))
@@ -12513,7 +12584,8 @@ function findRoots(root, known, language) {
12513
12584
  return rustRoots(known);
12514
12585
  const [rootRoots, rootWarnings] = packageJsonRoots(root, known);
12515
12586
  const [wsRoots, wsWarnings] = workspaceRoots(root, known);
12516
- const roots = [...new Set([...rootRoots, ...wsRoots])];
12587
+ const scriptRoots = detectWorkspaces(root).size === 0 ? rootPackageEntries(root).filter((e) => known.has(e)) : [];
12588
+ const roots = [...new Set([...rootRoots, ...wsRoots, ...scriptRoots])];
12517
12589
  const warnings = [...rootWarnings, ...wsWarnings];
12518
12590
  if (roots.length > 0)
12519
12591
  return [roots, warnings];
@@ -12689,7 +12761,7 @@ async function buildGraph(root) {
12689
12761
  const [declaredRoots, rootWarnings] = findRoots(root, known, language);
12690
12762
  const roots = [...declaredRoots];
12691
12763
  if (language === "typescript") {
12692
- for (const entry of configReferencedEntries(root)) {
12764
+ for (const entry of [...configReferencedEntries(root), ...htmlReferencedEntries(root)]) {
12693
12765
  if (known.has(entry) && !roots.includes(entry))
12694
12766
  roots.push(entry);
12695
12767
  }
@@ -13559,7 +13631,7 @@ function generateTestCoverageMarkdown(coverage, options = {}) {
13559
13631
  lines.push("");
13560
13632
  lines.push(...basisNote);
13561
13633
  const policyPath = options.policyPath ?? "docs/architecture/coverage-policy.json";
13562
- lines.push("> The raw figure counts every source file the CDG tool finds, including code that is " + "intentionally not direct-imported by a vitest `*.test.ts` (synced mathjs categories, " + "AssemblyScript sources, type-only barrels, …). The **effective** figure excludes those " + `per \`${policyPath}\` so the number reflects the genuinely-active hand-written code only. ` + "See [`COVERAGE_POLICY.md`](./COVERAGE_POLICY.md) for the policy.");
13634
+ lines.push("> The raw figure counts every source file the map finds, including files that no test " + "imports directly by design (for example generated code, type-only barrels and code in " + `another language). The **effective** figure excludes the files that \`${policyPath}\` ` + "classifies, so it shows the active code only.");
13563
13635
  lines.push("");
13564
13636
  lines.push("### Untested-file breakdown by category");
13565
13637
  lines.push("");
@@ -14155,7 +14227,7 @@ function emitTestCoverage(graph, root, outDir, options = {}) {
14155
14227
  const jsonPath = join16(outDir, "test-coverage.json");
14156
14228
  const namespaceImportBasis = !importsNameSymbols(graph.language);
14157
14229
  const markdown = generateTestCoverageMarkdown(coverage, {
14158
- expectedTestHint: graph.language === "typescript",
14230
+ expectedTestHint: graph.language === "typescript" && tests.some((t) => t.path.startsWith("tests/unit/")),
14159
14231
  policyPath: options.policyNote ?? "docs/architecture/coverage-policy.json",
14160
14232
  namespaceImportBasis
14161
14233
  });
@@ -14215,17 +14287,14 @@ function generateDuplicateSymbolsMarkdown(report) {
14215
14287
  md += "Each name carries one tag (see `DupEntryTag`), so the actionable subset is clear:\n\n";
14216
14288
  md += "- `TRUE_DUPLICATE`: a real merge target.\n";
14217
14289
  md += "- `DISPATCH_VARIANT`: two or more `mathTyped(...)` registrations of the same public name. ";
14218
- md += "The registrations are distinct dispatch surfaces and Bucket C delegation candidates, ";
14219
- md += `not copy-paste bodies.
14290
+ md += `The registrations are distinct dispatch surfaces, not copy-paste bodies.
14220
14291
  `;
14221
14292
  md += "- `ALIAS_DELEGATION`: a `const X = importedY` forward. ";
14222
14293
  md += `The entry is excluded once fewer than two real bodies remain.
14223
14294
  `;
14224
14295
  md += "- `ALLOWLISTED`: the name matches `duplicate-allowlist.json`. ";
14225
- md += "Examples are hot-path `is*` guards, AssemblyScript mirrors and per-package `VERSION` ";
14226
- md += `strings.
14227
-
14228
- `;
14296
+ md += "Examples are hot-path `is*` guards, mirrors in another language and per-package ";
14297
+ md += "`VERSION` strings.\n\n";
14229
14298
  md += `> **Note:** ${report.note}
14230
14299
 
14231
14300
  `;
@@ -14241,7 +14310,7 @@ function generateDuplicateSymbolsMarkdown(report) {
14241
14310
 
14242
14311
  `;
14243
14312
  md += renderTaggedSection("TRUE_DUPLICATE — actionable merge targets", report.runtime, "TRUE_DUPLICATE");
14244
- md += renderTaggedSection("DISPATCH_VARIANT — distinct public typed-dispatch surfaces (Bucket C candidates)", report.runtime, "DISPATCH_VARIANT");
14313
+ md += renderTaggedSection("DISPATCH_VARIANT — distinct public typed-dispatch surfaces", report.runtime, "DISPATCH_VARIANT");
14245
14314
  md += renderTaggedSection("ALIAS_DELEGATION — const-alias forwards (not independent bodies)", report.runtime, "ALIAS_DELEGATION");
14246
14315
  md += renderTaggedSection("ALLOWLISTED — accepted layering (see duplicate-allowlist.json)", report.runtime, "ALLOWLISTED");
14247
14316
  md += `## Type duplicates (lower priority)
@@ -14260,7 +14329,7 @@ var DISPOSITION_LEGEND = [
14260
14329
  ["build-entry", "A `src`-area file that is an entry root."],
14261
14330
  ["test-only", "A `src`-area file that only a test reaches."],
14262
14331
  ["orphan", "A `src`-area file that nothing reaches. Delete it, or wire it to a root."],
14263
- ["test", "A file in a `tests/` folder, or a `*.test.ts` or `*.spec.ts` file."],
14332
+ ["test", "A file in a `test/` or `tests/` folder, or a `*.test.ts` or `*.spec.ts` file."],
14264
14333
  ["tool", "A file in a `tools/` or `scripts/` folder outside `src/`."],
14265
14334
  ["config", "A `*.config.*` file of TypeScript or JavaScript."],
14266
14335
  ["example", "A file in `examples/` or `docs/`."],
@@ -19903,8 +19972,8 @@ identifiers are not checked. A file whose first five lines hold
19903
19972
  --help, -h Show this help.
19904
19973
 
19905
19974
  Exit codes: 0 when no finding (Markdown mode) or always (--prose mode). 1 when a
19906
- Markdown finding exists. 2 on a usage error, when no Markdown file was examined,
19907
- or when a file is not UTF-8.
19975
+ Markdown finding exists. 2 on a usage error, when a path does not exist, when no
19976
+ Markdown file was examined, or when a file is not UTF-8.
19908
19977
  `;
19909
19978
  var HISTORICAL_MARKER = "<!-- ste:historical-record -->";
19910
19979
  function readText(path) {
@@ -19988,7 +20057,10 @@ async function run7(argv, io) {
19988
20057
  let total = 0;
19989
20058
  let checked = 0;
19990
20059
  let skipped = 0;
20060
+ const missing = [];
19991
20061
  for (const arg of argv) {
20062
+ if (!existsSync13(arg))
20063
+ missing.push(arg);
19992
20064
  for (const target of targetsFor(arg)) {
19993
20065
  const name2 = basename10(target);
19994
20066
  if (isHistoricalRecord(target)) {
@@ -20021,12 +20093,16 @@ async function run7(argv, io) {
20021
20093
  const tail = skipped > 0 ? `; ${skipped} skipped as historical record(s)` : "";
20022
20094
  io.stdout(`
20023
20095
  TOTAL: ${total} problem(s) in ${checked} file(s)${tail}
20096
+ `);
20097
+ for (const path of missing)
20098
+ io.stderr(`ERROR: path does not exist: ${path}
20024
20099
  `);
20025
20100
  if (checked === 0) {
20026
20101
  io.stderr(`ERROR: no Markdown file was examined. This is a failure, not a clean result -- check the path(s) given.
20027
20102
  `);
20028
- return 2;
20029
20103
  }
20104
+ if (missing.length > 0 || checked === 0)
20105
+ return 2;
20030
20106
  return total > 0 ? 1 : 0;
20031
20107
  }
20032
20108
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielsimonjr/repo-tools",
3
- "version": "2.2.5",
3
+ "version": "2.2.7",
4
4
  "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.",
5
5
  "type": "module",
6
6
  "license": "MIT",