@danielsimonjr/repo-tools 2.2.4 → 2.2.5

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,33 @@ All notable changes to this project are recorded in this file. The format follow
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.2.5] - 2026-10-22
10
+
11
+ ### Changed
12
+
13
+ - The C# test-coverage report states its basis. A C# `using` usually names a namespace, so a test covers every file
14
+ that declares the namespace, and a covered file is not proof that a test uses the file. A test in the same namespace,
15
+ or in a nested namespace, needs no `using` and does not count, so the percentage can be too high or too low.
16
+ `TEST_COVERAGE.md` now has a note under the summary table that says "counted by namespace import".
17
+ `test-coverage.json` now has `metadata.coverageBasis` (`namespace-import`) and `metadata.coverageCaveat`. The
18
+ percentage does not change. The reports of TypeScript, Python and Rust have no note and no new key, and the golden
19
+ files do not change.
20
+ `importsNameSymbols` in `src/map/artifacts.ts` is now the one rule for "does an import name a symbol"; the
21
+ unused-analysis caveat and its export classification use it as well.
22
+ - `docs/design.md` passes `repo-tools ste` with 0 problems. The check reported 4 sentences of 31 to 43 words in
23
+ section 16.3. Each of the 4 sentences is now a group of shorter sentences. No fact changes.
24
+
25
+ ### Fixed
26
+
27
+ - `repo-tools docs` fails on a repository that has more files of a language it cannot read than TypeScript,
28
+ JavaScript and Python together. It printed `PASS -- 0/0 exported symbols documented, 0 MUST issues` and exited 0
29
+ for IronClaw, a Rust repository. `docs check src` measured the 1 JavaScript file below `src` and none of the 247
30
+ Rust files. `scan`, `check` and `stub` now exit 1 and print
31
+ `this repo is rust: 247 rust file(s) against 1 TypeScript, JavaScript or Python file(s)`. `map` already refused
32
+ such a repository. `docs` uses the file walk of `map`, so both commands give a repository the same language. A tie
33
+ is measured. A TypeScript repository with a few Rust files is measured as before. A repository of Go files only
34
+ now names Go. It said before that it found no source file.
35
+
9
36
  ## [2.2.4] - 2026-10-19
10
37
 
11
38
  ### Fixed
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.4.
16
+ Status: version 2.2.5.
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.4):
44
+ [GitHub release](https://github.com/danielsimonjr/repo-tools/releases/tag/v2.2.5):
45
45
 
46
46
  | Platform | File |
47
47
  |---|---|
@@ -238,7 +238,9 @@ The rules are:
238
238
  | S6 | SHOULD | The prose follows Simplified Technical English (the rules of `repo-tools ste`). |
239
239
 
240
240
  Only a MUST issue changes the exit code of `check`. A file that does not parse is also a failure,
241
- because an unknown file is not a clean file. A repository with no source file fails too.
241
+ because an unknown file is not a clean file. A repository with no source file fails too. So does a
242
+ repository with more files of another language (Rust or Go, for example) than TypeScript, JavaScript
243
+ and Python together. The command names the language and exits 1.
242
244
 
243
245
  `stub` is a dry run until you pass `--apply`. Each stub holds the marker `TODO:`, so `check` fails
244
246
  until you replace the stub with real prose. The command parses a file again after the change.
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.4",
5
+ version: "2.2.5",
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",
@@ -13114,6 +13114,9 @@ var IMPORTS_NAME_SYMBOLS = {
13114
13114
  csharp: false,
13115
13115
  rust: true
13116
13116
  };
13117
+ function importsNameSymbols(language) {
13118
+ return IMPORTS_NAME_SYMBOLS[language] ?? true;
13119
+ }
13117
13120
  var PUBLIC_API_LANGUAGES = new Set(["rust"]);
13118
13121
  var PUBLIC_API_CAVEAT = "A `pub` item with no user inside this crate is normally PUBLIC API, not dead code. `unused-analysis` sees only the repository. A library crate's public surface exists for DOWNSTREAM consumers. These consumers are not in the graph. Therefore, 'no importer here' is the expected state for most public items. The state is not a finding. Measured on one real crate: 2,635 of 2,739 exports (96%) had no in-crate user, and essentially all of them were live API. Treat this list as 'candidates to check against the crate's intended surface', never as a deletion list.";
13119
13122
  var NAMESPACE_IMPORT_CAVEAT = "In this language, an import names a namespace, not the symbols that it brings in (C#'s `using App.Hosting;`). The import edges therefore cannot show the use of one symbol. Usage here is a whole-identifier text search across every scanned file instead. The search is the same class of TEXT-LEVEL heuristic as the `referencedInModule` split above. The limit is the same: a name in the comment or string literal of another file counts as a use. The search is conservative. It UNDER-reports dead code and never invents it. That direction is safe for a list whose entries read as deletion candidates. Neither the import edges nor the text search can see a type that only reflection reaches (`Activator.CreateInstance`, DI-by-convention) or that only a .csproj/XAML file names.";
@@ -13144,7 +13147,7 @@ function identifierUsers(graph) {
13144
13147
  }
13145
13148
  function caveatsFor(graph) {
13146
13149
  const caveats = [...UNUSED_CAVEATS];
13147
- if (!(IMPORTS_NAME_SYMBOLS[graph.language] ?? true))
13150
+ if (!importsNameSymbols(graph.language))
13148
13151
  caveats.push(NAMESPACE_IMPORT_CAVEAT);
13149
13152
  if (PUBLIC_API_LANGUAGES.has(graph.language))
13150
13153
  caveats.push(PUBLIC_API_CAVEAT);
@@ -13185,7 +13188,7 @@ function classifyUnusedExports(graph) {
13185
13188
  const unreferenced = {};
13186
13189
  const referencedInModule = {};
13187
13190
  const unclassified = {};
13188
- const namesSymbols = IMPORTS_NAME_SYMBOLS[graph.language] ?? true;
13191
+ const namesSymbols = importsNameSymbols(graph.language);
13189
13192
  const users = namesSymbols ? new Map : identifierUsers(graph);
13190
13193
  const push = (bucket, path, name2) => {
13191
13194
  const list = bucket[path] ?? [];
@@ -13530,6 +13533,8 @@ import { basename as basename5 } from "node:path";
13530
13533
  function percent(tested, total) {
13531
13534
  return total > 0 ? (tested / total * 100).toFixed(1) : "0";
13532
13535
  }
13536
+ var NAMESPACE_IMPORT_BASIS = "namespace-import";
13537
+ var NAMESPACE_IMPORT_COVERAGE_CAVEAT = "Coverage is counted by namespace import. A C# `using` directive usually names a namespace, not a type, so a test covers every file that declares the namespace. A namespace import does not show that a test uses the file. A test in the same namespace, or in a nested namespace, needs no `using` and does not count. The percentage can be too high or too low.";
13533
13538
  function generateTestCoverageMarkdown(coverage, options = {}) {
13534
13539
  const hint = options.expectedTestHint ?? true;
13535
13540
  const lines = [];
@@ -13548,9 +13553,11 @@ function generateTestCoverageMarkdown(coverage, options = {}) {
13548
13553
  lines.push(`| Source Files without Tests | ${totalUntested} |`);
13549
13554
  lines.push(`| Coverage (raw, direct-import) | **${percent(totalTested, totalSource)}%** |`);
13550
13555
  const b = coverage.policyBreakdown;
13556
+ const basisNote = options.namespaceImportBasis ? [`> ${NAMESPACE_IMPORT_COVERAGE_CAVEAT}`, ""] : [];
13551
13557
  if (coverage.policy) {
13552
13558
  lines.push(`| Coverage (effective, active code only) | **${b.effectivePercent}%** (${b.testedActive} / ${b.activeFiles}) |`);
13553
13559
  lines.push("");
13560
+ lines.push(...basisNote);
13554
13561
  const policyPath = options.policyPath ?? "docs/architecture/coverage-policy.json";
13555
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.");
13556
13563
  lines.push("");
@@ -13566,6 +13573,8 @@ function generateTestCoverageMarkdown(coverage, options = {}) {
13566
13573
  }
13567
13574
  lines.push(`| **Active (real gap — needs a test)** | ${b.byCategory.active_untested ?? 0} | These are the files that should grow a direct-import test. |`);
13568
13575
  lines.push("");
13576
+ } else if (basisNote.length > 0) {
13577
+ lines.push("", ...basisNote);
13569
13578
  }
13570
13579
  lines.push("---");
13571
13580
  lines.push("");
@@ -13621,7 +13630,7 @@ function generateTestCoverageMarkdown(coverage, options = {}) {
13621
13630
  return lines.join(`
13622
13631
  `);
13623
13632
  }
13624
- function generateTestCoverageJson(coverage) {
13633
+ function generateTestCoverageJson(coverage, options = {}) {
13625
13634
  const coverageMapObj = {};
13626
13635
  for (const [source, tests] of coverage.coverageMap)
13627
13636
  coverageMapObj[source] = tests;
@@ -13636,6 +13645,10 @@ function generateTestCoverageJson(coverage) {
13636
13645
  testedCount: coverage.testedFiles.length,
13637
13646
  untestedCount: coverage.untestedFiles.length,
13638
13647
  coveragePercent: percent(coverage.testedFiles.length, coverage.sourceFiles.length),
13648
+ ...options.namespaceImportBasis ? {
13649
+ coverageBasis: NAMESPACE_IMPORT_BASIS,
13650
+ coverageCaveat: NAMESPACE_IMPORT_COVERAGE_CAVEAT
13651
+ } : {},
13639
13652
  effectiveCoverage: {
13640
13653
  policyLoaded: coverage.policy !== null,
13641
13654
  activeFiles: b.activeFiles,
@@ -14140,12 +14153,14 @@ function emitTestCoverage(graph, root, outDir, options = {}) {
14140
14153
  const coverage = analyzeTestCoverage(sources, tests, root, options.policyPath ?? join16(root, "docs", "architecture", "coverage-policy.json"));
14141
14154
  const mdPath = join16(outDir, "TEST_COVERAGE.md");
14142
14155
  const jsonPath = join16(outDir, "test-coverage.json");
14156
+ const namespaceImportBasis = !importsNameSymbols(graph.language);
14143
14157
  const markdown = generateTestCoverageMarkdown(coverage, {
14144
14158
  expectedTestHint: graph.language === "typescript",
14145
- policyPath: options.policyNote ?? "docs/architecture/coverage-policy.json"
14159
+ policyPath: options.policyNote ?? "docs/architecture/coverage-policy.json",
14160
+ namespaceImportBasis
14146
14161
  });
14147
14162
  writeReport(mdPath, withBanner(markdown, { command: MAP_REGENERATE_COMMAND, ...options.banner }));
14148
- writeReport(jsonPath, JSON.stringify(generateTestCoverageJson(coverage), null, 2));
14163
+ writeReport(jsonPath, JSON.stringify(generateTestCoverageJson(coverage, { namespaceImportBasis }), null, 2));
14149
14164
  return [mdPath, jsonPath];
14150
14165
  }
14151
14166
 
@@ -17517,6 +17532,22 @@ class ConfigError extends Error {
17517
17532
  this.name = "ConfigError";
17518
17533
  }
17519
17534
  }
17535
+
17536
+ class UnreadableRepoLanguage extends Error {
17537
+ constructor(message) {
17538
+ super(message);
17539
+ this.name = "UnreadableRepoLanguage";
17540
+ }
17541
+ }
17542
+ function assertReadableLanguage(root) {
17543
+ const counts = countLanguages(root);
17544
+ const readable = (counts.typescript ?? 0) + (counts.python ?? 0);
17545
+ const unreadable = Object.entries(counts).filter(([language]) => language !== "typescript" && language !== "python").sort(([a, x], [b, y]) => y - x || (a < b ? -1 : a > b ? 1 : 0));
17546
+ const [language, n] = unreadable[0] ?? ["", 0];
17547
+ if (n > readable) {
17548
+ throw new UnreadableRepoLanguage(`this repo is ${language}: ${n} ${language} file(s) against ${readable} TypeScript, ` + "JavaScript or Python file(s). repo-tools docs reads TypeScript, JavaScript and Python " + "only, so a PASS here would describe the wrong files. It fails rather than print a gate " + "that measured nothing.");
17549
+ }
17550
+ }
17520
17551
  function languageOf(path) {
17521
17552
  const suffix = suffixOf(path.slice(path.lastIndexOf("/") + 1));
17522
17553
  if (PYTHON_SUFFIXES2.has(suffix))
@@ -18711,7 +18742,9 @@ var DOCS_HELP = `Usage: repo-tools docs scan [root] [--out=<dir>]
18711
18742
  Measure, stub and gate the documentation comments of the TypeScript, JavaScript
18712
18743
  and Python files of a repository. The files are the files that git tracks. A
18713
18744
  file in a skipped folder (node_modules, dist, build, bundle, out, vendor) or
18714
- with a generated name (.d.ts, .min.js, _pb2.py) is not read.
18745
+ with a generated name (.d.ts, .min.js, _pb2.py) is not read. A repository with
18746
+ more files of another language (for example Rust or Go) than TypeScript,
18747
+ JavaScript and Python together is refused: the run names the language and exits 1.
18715
18748
 
18716
18749
  Actions:
18717
18750
  scan Measure the coverage. Write COVERAGE.md and coverage.json.
@@ -19085,6 +19118,7 @@ async function run5(argv, io) {
19085
19118
  root = resolve8(options.root);
19086
19119
  if (!isDirectory(root))
19087
19120
  throw new Error("the root <root> is not an existing directory");
19121
+ assertReadableLanguage(root);
19088
19122
  if (options.action === "scan")
19089
19123
  return await runScan(root, options, io);
19090
19124
  if (options.action === "stub")
@@ -19094,7 +19128,7 @@ async function run5(argv, io) {
19094
19128
  const message = error instanceof Error ? error.message : String(error);
19095
19129
  stderr(`repo-tools docs: ${message}
19096
19130
  `);
19097
- return error instanceof NothingToMeasure ? 1 : 2;
19131
+ return error instanceof NothingToMeasure || error instanceof UnreadableRepoLanguage ? 1 : 2;
19098
19132
  }
19099
19133
  }
19100
19134
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielsimonjr/repo-tools",
3
- "version": "2.2.4",
3
+ "version": "2.2.5",
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",