@danielsimonjr/repo-tools 2.2.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,66 @@ All notable changes to this project are recorded in this file. The format follow
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.2.2] - 2026-10-15
10
+
11
+ ### Changed
12
+
13
+ - The workflows pin three GitHub Actions to a newer release: `actions/upload-artifact` v7.0.1 to v7.0.2 (in `ci.yml` and
14
+ `build.yml`), `actions/download-artifact` v8.0.1 to v8.0.2 (in `build.yml`) and `actions/setup-node` v7.0.0 to v7.1.0 (in
15
+ `ci.yml`). Each pin is the commit of the upstream release tag. `build.yml` runs only on a `v*` tag, so the next release
16
+ build is the first run of the new `download-artifact` pin.
17
+
18
+ ### Fixed
19
+
20
+ - `repo-tools docs` reads a `.tsx` or `.jsx` file that has a bare `&` in JSX text (`<h3>Review & Start</h3>`).
21
+ React and `tsc` accept the text, and the grammar gave an error node, so the file was an unparsed file and its
22
+ symbols were not measured. The analyser now writes a space over a `&` that is the first character of an error
23
+ node directly inside a JSX element, which keeps the length, and parses again. Any other error stays an error.
24
+ A real syntax error beside such a text is still reported. In one application repository four files moved from
25
+ unparsed to measured, and two files with `import()` type expressions stay unparsed.
26
+ - `repo-tools docs check` reads the documentation of an overload set from its first overload. Before, the analyser read only
27
+ the comment that directly precedes a declaration, so an implementation signature that follows overload signatures counted
28
+ as undocumented (M1), although a caller sees only the overloads. A repository then needed a comment that no reader sees.
29
+ The implementation signature now takes the doc of the first overload of the same name, in a function set and in a class
30
+ member set. A doc that directly precedes the implementation still wins. A statement between the overloads and the
31
+ implementation ends the set. The `;` that ends a member signature in a class body does not end the set.
32
+ Rule M3 still compares the documented parameters with the implementation.
33
+ - `repo-tools docs` reads a file that has an `import("m")` type expression (`let v: import("fs").Dirent[]` and
34
+ `f<typeof import("m")>()`). `tsc` accepts both. The grammar reads the call as a member expression of a value, so it
35
+ cannot take `[]` after it, and it reads the call in a type argument as a comparison. Both gave error nodes, so the
36
+ file was an unparsed file. When the first parse has an error, the analyser now writes an identifier of equal length
37
+ over each `import("m")` call that holds a string literal, keeps its line breaks, and parses again. A file that
38
+ parses is never rewritten, and any other error stays an error. In the application repository of the earlier entry
39
+ the last two unparsed files move to measured.
40
+ - Two fan-out tests (`map-graph.test.ts` and `map-query.test.ts`) have an explicit 30 s timeout. Each builds a graph of about
41
+ 9,000 nodes. The graph test took 0.85 s on a fast machine and 4.57 s on a slower one, against bun's 5 s default, so it
42
+ failed there under any load. The tests check a step bound, not time, so only the timeout changes. A comment on each test records the measurement and
43
+ names no machine, because the privacy check rejects machine names.
44
+ - The privacy check accepts one finding in the history: the squash commit of #18 (`46a45d4c19af`) carries a
45
+ `Co-authored-by` trailer that GitHub added for the maintainer identity on the pull request branch, at line 53 of the
46
+ message. A commit message cannot change after a merge, so `scripts/privacy-accepted.txt` holds the entry, as it does
47
+ for `fc930a32f5d2`. Before, the `privacy` job and the test `this repository scans clean` failed on `main` after the
48
+ merge, although the pull request was green. A squash whose commits are all by the pull request opener adds no trailer.
49
+ - `repo-tools docs` keeps the doc of a declaration when a tool directive stands between the doc comment and the
50
+ declaration (`// eslint-disable-next-line`, `// @ts-expect-error`, `// prettier-ignore`, `// biome-ignore` and the
51
+ other directives of a closed list). Before, the directive hid the doc and the symbol counted as undocumented (M1),
52
+ although a directive applies to the next line and so must stay directly above the declaration; a React repository
53
+ with the `react-refresh` lint rule had four such hooks and components. A plain line comment can move, so it still
54
+ separates a doc comment from its declaration. `docs stub` also writes
55
+ a new block above the directive and not between the directive and the declaration, where the directive would apply
56
+ to the block.
57
+
58
+ ## [2.2.1] - 2026-10-08
59
+
60
+ ### Fixed
61
+
62
+ - `repo-tools map` finds the entry roots of a package that has no `src/` tree. A `bin`, `main`,
63
+ `module` or `exports` entry that names a tracked source file (`bin/tool.mjs`, `./lib/index.js`)
64
+ is now a root. Before, only entries that map into `src/` counted. A package with `bin/` and `lib/`
65
+ and no `src/` showed `entryRoots` 0, and its `unused-analysis.md` listed every file as an orphan or
66
+ test-only. A package with a `src/` tree resolves as before, so a thin launcher that fails the scan
67
+ still warns. The warning text no longer says "under src/".
68
+
9
69
  ## [2.2.0] - 2026-10-02
10
70
 
11
71
  `repo-tools docs` is the new gate of the doc comments of a repository. It ports `code_docs.py` of
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.0.
16
+ Status: version 2.2.2.
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.0):
44
+ [GitHub release](https://github.com/danielsimonjr/repo-tools/releases/tag/v2.2.2):
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.0",
5
+ version: "2.2.2",
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",
@@ -12324,6 +12324,13 @@ function launcherEntry(root, spec, known) {
12324
12324
  }
12325
12325
  return null;
12326
12326
  }
12327
+ function declaredSource(spec, known) {
12328
+ for (const path of known)
12329
+ if (path.startsWith("src/"))
12330
+ return null;
12331
+ const posix = lstripDotSlash(spec.replaceAll("\\", "/"));
12332
+ return known.has(posix) ? posix : null;
12333
+ }
12327
12334
  var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
12328
12335
  function packageJsonRoots(root, known) {
12329
12336
  let pkg;
@@ -12351,9 +12358,9 @@ function packageJsonRoots(root, known) {
12351
12358
  for (const spec of raw) {
12352
12359
  if (spec.endsWith(".json"))
12353
12360
  continue;
12354
- const mapped = mapDistToSrc(spec, known) ?? launcherEntry(root, spec, known);
12361
+ const mapped = mapDistToSrc(spec, known) ?? launcherEntry(root, spec, known) ?? declaredSource(spec, known);
12355
12362
  if (mapped === null) {
12356
- warnings.push(`package.json entry ${pyRepr(spec)} did not resolve to a known source file under src/ -- ` + "ignored; if no other declared entry resolves either, the entry-point root falls back to " + "a conventional src/index.* file instead");
12363
+ 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");
12357
12364
  continue;
12358
12365
  }
12359
12366
  if (!roots.includes(mapped))
@@ -18201,13 +18208,60 @@ function anchorOf(node) {
18201
18208
  target = target.parent;
18202
18209
  return target;
18203
18210
  }
18204
- function leadingDoc(anchor, text) {
18205
- const prev = anchor.previousSibling;
18211
+ var DIRECTIVE = new RegExp(`^//${SPACE_CLASS}*(?:eslint-(?:disable|enable)(?:-next-line|-line)?|@ts-(?:ignore|expect-error|nocheck|check)|` + "prettier-ignore|biome-ignore|oxlint-disable|deno-lint-ignore|tslint:(?:disable|enable)|" + `(?:istanbul|c8|v8)${SPACE_CLASS}+ignore)(?![\\w-])`);
18212
+ function isDirective(node, text) {
18213
+ return node.type === "comment" && DIRECTIVE.test(text(node));
18214
+ }
18215
+ function docBefore(anchor, text) {
18216
+ let prev = anchor.previousSibling;
18217
+ while (prev && isDirective(prev, text))
18218
+ prev = prev.previousSibling;
18206
18219
  if (prev?.type !== "comment")
18207
18220
  return "";
18208
18221
  const comment = text(prev);
18209
18222
  return comment.startsWith("/**") ? comment : "";
18210
18223
  }
18224
+ function stubAnchor(anchor, source, text) {
18225
+ let first = anchor;
18226
+ for (let prev = anchor.previousSibling;prev && isDirective(prev, text); ) {
18227
+ if (prev.endPosition.row + 1 < first.startPosition.row)
18228
+ break;
18229
+ const lineStart = source.lastIndexOf(`
18230
+ `, prev.startIndex - 1) + 1;
18231
+ if (source.slice(lineStart, prev.startIndex).trim() !== "")
18232
+ break;
18233
+ first = prev;
18234
+ prev = prev.previousSibling;
18235
+ }
18236
+ return first;
18237
+ }
18238
+ var SIGNATURES = new Set(["function_signature", "method_signature"]);
18239
+ function signatureName(sibling, text) {
18240
+ const inner = sibling.type === "export_statement" ? sibling.childForFieldName("declaration") : sibling;
18241
+ if (!inner || !SIGNATURES.has(inner.type))
18242
+ return null;
18243
+ const name2 = inner.childForFieldName("name");
18244
+ return name2 ? text(name2) : null;
18245
+ }
18246
+ function leadingDoc(anchor, node, text) {
18247
+ const own = docBefore(anchor, text);
18248
+ if (own || node.type !== "function_declaration" && node.type !== "method_definition") {
18249
+ return own;
18250
+ }
18251
+ const name2 = node.childForFieldName("name");
18252
+ if (!name2)
18253
+ return "";
18254
+ const implementation = text(name2);
18255
+ let first = null;
18256
+ for (let sibling = anchor.previousSibling;sibling; sibling = sibling.previousSibling) {
18257
+ if (sibling.type === "comment" || sibling.type === ";")
18258
+ continue;
18259
+ if (signatureName(sibling, text) !== implementation)
18260
+ break;
18261
+ first = sibling;
18262
+ }
18263
+ return first ? docBefore(first, text) : "";
18264
+ }
18211
18265
  function isExported2(node) {
18212
18266
  let parent = node.parent;
18213
18267
  while (parent) {
@@ -18233,8 +18287,53 @@ function proseOfComment(doc) {
18233
18287
  return proseOf(lines.join(`
18234
18288
  `));
18235
18289
  }
18290
+ var JSX_PARENTS = new Set(["jsx_element", "jsx_fragment"]);
18291
+ var MAX_REPAIRS = 16;
18292
+ var IMPORT_CALL = new RegExp(`\\bimport${SPACE_CLASS}*\\(${SPACE_CLASS}*(?:"[^"\\n]*"|'[^'\\n]*')${SPACE_CLASS}*\\)`, "g");
18293
+ function blankImportCalls(text) {
18294
+ return text.replace(IMPORT_CALL, (call) => {
18295
+ let name2 = true;
18296
+ let out2 = "";
18297
+ for (const ch of call) {
18298
+ if (/\s/u.test(ch)) {
18299
+ name2 = false;
18300
+ out2 += ch;
18301
+ } else {
18302
+ out2 += (name2 ? "_" : " ").repeat(ch.length);
18303
+ }
18304
+ }
18305
+ return out2;
18306
+ });
18307
+ }
18308
+ function parseSource(parser, text) {
18309
+ let current = sanitise(text);
18310
+ let tree = parser.parse(current);
18311
+ let importsBlanked = false;
18312
+ for (let pass = 0;tree?.rootNode.hasError && pass < MAX_REPAIRS; pass++) {
18313
+ const marks = [];
18314
+ for (const node of tree.rootNode.descendantsOfType("ERROR")) {
18315
+ const start2 = node.startIndex;
18316
+ if (current[start2] === "&" && node.parent && JSX_PARENTS.has(node.parent.type)) {
18317
+ marks.push(start2);
18318
+ }
18319
+ }
18320
+ let next = current;
18321
+ for (const at of marks)
18322
+ next = `${next.slice(0, at)} ${next.slice(at + 1)}`;
18323
+ if (!importsBlanked) {
18324
+ importsBlanked = true;
18325
+ next = blankImportCalls(next);
18326
+ }
18327
+ if (next === current)
18328
+ break;
18329
+ tree.delete();
18330
+ current = next;
18331
+ tree = parser.parse(current);
18332
+ }
18333
+ return tree;
18334
+ }
18236
18335
  function parsesAsTypeScript(path, text) {
18237
- const tree = parserFor(grammarFor(path)).parse(sanitise(text.replace(/^\uFEFF/, "")));
18336
+ const tree = parseSource(parserFor(grammarFor(path)), text.replace(/^\uFEFF/, ""));
18238
18337
  if (!tree)
18239
18338
  return false;
18240
18339
  try {
@@ -18247,7 +18346,7 @@ async function analyseTypeScript(path, source) {
18247
18346
  const report = newReport(path, "typescript");
18248
18347
  const grammar = grammarFor(path);
18249
18348
  await loadGrammar(grammar);
18250
- const tree = parserFor(grammar).parse(sanitise(source));
18349
+ const tree = parseSource(parserFor(grammar), source);
18251
18350
  if (!tree) {
18252
18351
  report.error = "parse error (the parser returned no tree)";
18253
18352
  return report;
@@ -18261,7 +18360,8 @@ async function analyseTypeScript(path, source) {
18261
18360
  for (const node of tree.rootNode.descendantsOfType(DECLARATION_TYPES)) {
18262
18361
  const kind = DECLARATIONS[node.type];
18263
18362
  const anchor = anchorOf(node);
18264
- const doc = leadingDoc(anchor, text);
18363
+ const lead = stubAnchor(anchor, source, text);
18364
+ const doc = leadingDoc(anchor, node, text);
18265
18365
  const dialect = dialectOf(doc);
18266
18366
  const typeLike = kind === "class" || kind === "interface" || kind === "type";
18267
18367
  const line = node.startPosition.row + 1;
@@ -18269,8 +18369,8 @@ async function analyseTypeScript(path, source) {
18269
18369
  const sym = {
18270
18370
  file: path,
18271
18371
  line,
18272
- anchorLine: anchor.startPosition.row + 1,
18273
- anchorColumn: anchor.startPosition.column,
18372
+ anchorLine: lead.startPosition.row + 1,
18373
+ anchorColumn: lead.startPosition.column,
18274
18374
  name: nameNode ? text(nameNode) : "<anonymous>",
18275
18375
  kind,
18276
18376
  exported: isExported2(node),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielsimonjr/repo-tools",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
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",