@danielsimonjr/repo-tools 2.1.0 → 2.2.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.
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.1.0",
5
+ version: "2.2.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",
@@ -35,7 +35,9 @@ var package_default = {
35
35
  privacy: "bun scripts/privacy-check.ts",
36
36
  build: "bun scripts/build.ts",
37
37
  compile: "bun scripts/build.ts --compile",
38
- smoke: "bun scripts/smoke.ts"
38
+ smoke: "bun scripts/smoke.ts",
39
+ "audit:ledger": "bun scripts/fix-ledger-audit.ts",
40
+ "check:design": "bun scripts/design-refs.ts"
39
41
  },
40
42
  devDependencies: {
41
43
  "@biomejs/biome": "2.5.14",
@@ -52,7 +54,7 @@ var package_default = {
52
54
  };
53
55
 
54
56
  // src/check/index.ts
55
- import { readdirSync as readdirSync5, readFileSync as readFileSync17 } from "node:fs";
57
+ import { readdirSync as readdirSync2, readFileSync as readFileSync17 } from "node:fs";
56
58
  import { join as join20, resolve as resolve5 } from "node:path";
57
59
 
58
60
  // src/config.ts
@@ -1747,13 +1749,7 @@ function outputPath(outputDir, relPath) {
1747
1749
  }
1748
1750
 
1749
1751
  // src/map/artifacts.ts
1750
- import {
1751
- mkdirSync as mkdirSync2,
1752
- readdirSync as readdirSync4,
1753
- readFileSync as readFileSync9,
1754
- statSync as statSync5,
1755
- writeFileSync as writeFileSync2
1756
- } from "node:fs";
1752
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, statSync as statSync5, writeFileSync as writeFileSync2 } from "node:fs";
1757
1753
  import { basename as basename4, dirname as dirname6, join as join12 } from "node:path";
1758
1754
 
1759
1755
  // src/map/cycles.ts
@@ -5996,6 +5992,7 @@ function detectWorkspaces(root, warn = () => {}) {
5996
5992
  // src/py.ts
5997
5993
  var SPACE_BODY = "\\t\\n\\v\\f\\r\\x1c-\\x1f \\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000";
5998
5994
  var S = `[${SPACE_BODY}]`;
5995
+ var NON_SPACE = `[^${SPACE_BODY}]`;
5999
5996
  var W = "[\\p{L}\\p{N}_]";
6000
5997
  var B = `(?:(?<=${W})(?!${W})|(?<!${W})(?=${W}))`;
6001
5998
  var D = "\\p{Nd}";
@@ -6280,7 +6277,7 @@ function toParsedFiles(graph, root, options = {}) {
6280
6277
 
6281
6278
  // src/map/discovery.ts
6282
6279
  import { spawnSync } from "node:child_process";
6283
- import { lstatSync as lstatSync3, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
6280
+ import { lstatSync as lstatSync3, readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
6284
6281
  import { join as join10 } from "node:path";
6285
6282
  var SOURCE_SUFFIXES = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
6286
6283
  var PYTHON_SUFFIXES = new Set([".py"]);
@@ -6381,7 +6378,7 @@ function isReparsePoint(path) {
6381
6378
  }
6382
6379
  function entries(dir) {
6383
6380
  try {
6384
- return readdirSync2(dir, { withFileTypes: true });
6381
+ return readdirHook.entries(dir);
6385
6382
  } catch {
6386
6383
  return [];
6387
6384
  }
@@ -6600,7 +6597,7 @@ function discover(root, language = detectLanguage(root), skippedLinks) {
6600
6597
  }
6601
6598
 
6602
6599
  // src/map/graph.ts
6603
- import { readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
6600
+ import { readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
6604
6601
  import { basename as basename3, join as join11, resolve as resolve3 } from "node:path";
6605
6602
 
6606
6603
  // node_modules/smol-toml/dist/error.js
@@ -7579,6 +7576,9 @@ import { fileURLToPath } from "node:url";
7579
7576
  // node_modules/tree-sitter-python/tree-sitter-python.wasm
7580
7577
  var tree_sitter_python_default = "./tree-sitter-python-d56ewc8c.wasm";
7581
7578
 
7579
+ // node_modules/tree-sitter-typescript/tree-sitter-tsx.wasm
7580
+ var tree_sitter_tsx_default = "./tree-sitter-tsx-bmyq0z5a.wasm";
7581
+
7582
7582
  // node_modules/tree-sitter-typescript/tree-sitter-typescript.wasm
7583
7583
  var tree_sitter_typescript_default = "./tree-sitter-typescript-zxjzwt75.wasm";
7584
7584
 
@@ -10867,6 +10867,7 @@ var web_tree_sitter_default2 = "./web-tree-sitter-grrd7nak.wasm";
10867
10867
  // src/map/grammars.ts
10868
10868
  var WASM = {
10869
10869
  typescript: tree_sitter_typescript_default,
10870
+ tsx: tree_sitter_tsx_default,
10870
10871
  python: tree_sitter_python_default
10871
10872
  };
10872
10873
  function assetBytes(path) {
@@ -12431,7 +12432,7 @@ function rglobParts(root, suffix) {
12431
12432
  const go = (dir, parts2) => {
12432
12433
  let entries;
12433
12434
  try {
12434
- entries = readdirSync3(dir, { withFileTypes: true });
12435
+ entries = readdirHook.entries(dir);
12435
12436
  } catch {
12436
12437
  return;
12437
12438
  }
@@ -12931,7 +12932,7 @@ function csprojParts(root) {
12931
12932
  const go = (dir, parts2) => {
12932
12933
  let entries;
12933
12934
  try {
12934
- entries = readdirSync4(dir, { withFileTypes: true });
12935
+ entries = readdirHook.entries(dir);
12935
12936
  } catch {
12936
12937
  return;
12937
12938
  }
@@ -12966,7 +12967,7 @@ function csprojMap(root) {
12966
12967
  }
12967
12968
  function subdirs(dir) {
12968
12969
  try {
12969
- return readdirSync4(dir, { withFileTypes: true }).filter((e) => e.isDirectory() || e.isSymbolicLink() && statSync5(join12(dir, e.name)).isDirectory()).map((e) => e.name).sort(compareCodeUnits);
12970
+ return readdirHook.entries(dir).filter((e) => e.isDirectory() || e.isSymbolicLink() && statSync5(join12(dir, e.name)).isDirectory()).map((e) => e.name).sort(compareCodeUnits);
12970
12971
  } catch {
12971
12972
  return [];
12972
12973
  }
@@ -15074,7 +15075,7 @@ function readDocs(dir, label) {
15074
15075
  }
15075
15076
  let entries;
15076
15077
  try {
15077
- entries = readdirSync5(dir);
15078
+ entries = readdirSync2(dir);
15078
15079
  } catch {
15079
15080
  throw new Error(`cannot read the docs folder ${label}`);
15080
15081
  }
@@ -16459,7 +16460,7 @@ ${CHUNK_HELP}`);
16459
16460
  }
16460
16461
 
16461
16462
  // src/compress/index.ts
16462
- import { existsSync as existsSync9, readdirSync as readdirSync6, readFileSync as readFileSync20, statSync as statSync8, writeFileSync as writeFileSync4 } from "node:fs";
16463
+ import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync20, statSync as statSync8, writeFileSync as writeFileSync4 } from "node:fs";
16463
16464
  import { basename as basename8, dirname as dirname8, extname as extname4, join as join22 } from "node:path";
16464
16465
 
16465
16466
  // src/compress/formats.ts
@@ -17091,7 +17092,7 @@ Exit codes: 0 when all files are done, 1 on an error. An unknown option, an opti
17091
17092
  a value, a missing input file and two inputs without --batch are errors. No file is written.
17092
17093
  `;
17093
17094
  var defaultDeps = {
17094
- readdir: (dir) => readdirSync6(dir, { withFileTypes: true })
17095
+ readdir: (dir) => readdirSync3(dir, { withFileTypes: true })
17095
17096
  };
17096
17097
  function parseArgs2(args2) {
17097
17098
  const o = {
@@ -17454,244 +17455,1776 @@ async function run4(argv, io, deps = defaultDeps) {
17454
17455
  }
17455
17456
  }
17456
17457
 
17457
- // src/query/index.ts
17458
- import { existsSync as existsSync11, statSync as statSync9 } from "node:fs";
17459
- import { resolve as resolve8 } from "node:path";
17458
+ // src/docs/index.ts
17459
+ import { lstatSync as lstatSync5, readFileSync as readFileSync23, realpathSync as realpathSync3, writeFileSync as writeFileSync5 } from "node:fs";
17460
+ import { basename as basename9, isAbsolute as isAbsolute4, join as join26, relative as relative5, resolve as resolve8, sep as sep4 } from "node:path";
17460
17461
 
17461
- // src/query/args.ts
17462
- var MODE_FLAGS = {
17463
- "--emit": { name: "emit" },
17464
- "--check-browser-safety": { name: "check-browser-safety" }
17465
- };
17466
- var VALUE_FLAGS2 = new Set(["--root", "--config", "--out", "--node-runtime"]);
17467
- var COMMAND_USAGE = {
17468
- dependents: "<file>",
17469
- "symbol-users": "<symbol>",
17470
- "is-public": "<pkg> <symbol>",
17471
- "node-safety": "[pkg]",
17472
- cycles: ""
17473
- };
17474
- function makeCommand(name2, args2) {
17475
- const usage = COMMAND_USAGE[name2];
17476
- if (usage === undefined) {
17477
- throw new Error(`unknown command '${name2}' (see repo-tools query --help)`);
17478
- }
17479
- const [a, b] = args2;
17480
- const need = (count) => {
17481
- if (args2.length < count)
17482
- throw new Error(`${name2} needs ${usage}`);
17483
- };
17484
- const atMost = (count) => {
17485
- if (args2.length > count) {
17486
- throw new Error(count === 0 ? `${name2} takes no more arguments` : `${name2} takes ${usage}, not more arguments`);
17487
- }
17488
- };
17489
- switch (name2) {
17490
- case "dependents":
17491
- need(1);
17492
- atMost(1);
17493
- return { name: name2, file: a };
17494
- case "symbol-users":
17495
- need(1);
17496
- atMost(1);
17497
- return { name: name2, symbol: a };
17498
- case "is-public":
17499
- need(2);
17500
- atMost(2);
17501
- return { name: name2, pkg: a, symbol: b };
17502
- case "node-safety":
17503
- atMost(1);
17504
- return a === undefined ? { name: name2 } : { name: name2, pkg: a };
17505
- default:
17506
- atMost(0);
17507
- return { name: "cycles" };
17462
+ // src/docs/analyse.ts
17463
+ import { readFileSync as readFileSync22 } from "node:fs";
17464
+ import { join as join24 } from "node:path";
17465
+
17466
+ // src/docs/discovery.ts
17467
+ import { spawnSync as spawnSync2 } from "node:child_process";
17468
+ import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync21, statSync as statSync9 } from "node:fs";
17469
+ import { join as join23 } from "node:path";
17470
+ var PYTHON_SUFFIXES2 = new Set([".py"]);
17471
+ var TYPESCRIPT_SUFFIXES = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
17472
+ var SKIP_DIRS2 = new Set([
17473
+ "node_modules",
17474
+ ".git",
17475
+ "dist",
17476
+ "build",
17477
+ "bundle",
17478
+ "out",
17479
+ "coverage",
17480
+ "__pycache__",
17481
+ ".venv",
17482
+ "venv",
17483
+ ".mypy_cache",
17484
+ ".pytest_cache",
17485
+ "vendor",
17486
+ ".next",
17487
+ ".turbo",
17488
+ "target"
17489
+ ]);
17490
+ var SKIP_SUFFIXES = [".d.ts", ".min.js", ".bundle.js", "_pb2.py", ".g.ts"];
17491
+ var CONFIG_NAME = ".code-docs.json";
17492
+
17493
+ class ConfigError extends Error {
17494
+ constructor(message) {
17495
+ super(message);
17496
+ this.name = "ConfigError";
17508
17497
  }
17509
17498
  }
17510
- function list2(flag, value) {
17511
- const items = value.split(",");
17512
- if (items.some((item) => item === ""))
17513
- throw new Error(`flag ${flag} holds an empty item`);
17514
- return items;
17499
+ function languageOf(path) {
17500
+ const suffix = suffixOf(path.slice(path.lastIndexOf("/") + 1));
17501
+ if (PYTHON_SUFFIXES2.has(suffix))
17502
+ return "python";
17503
+ if (TYPESCRIPT_SUFFIXES.has(suffix))
17504
+ return "typescript";
17505
+ return "";
17515
17506
  }
17516
- function parseQueryArgs(argv, cwd) {
17517
- let root;
17518
- let config;
17519
- let out2;
17520
- let nodeRuntimes;
17521
- let mode;
17522
- let components = false;
17523
- const words = [];
17524
- for (const arg of argv) {
17525
- if (!arg.startsWith("-")) {
17526
- words.push(arg);
17527
- continue;
17528
- }
17529
- const eq = arg.indexOf("=");
17530
- const name2 = eq === -1 ? arg : arg.slice(0, eq);
17531
- const value = eq === -1 ? undefined : arg.slice(eq + 1);
17532
- if (name2 === "--components") {
17533
- if (value !== undefined)
17534
- throw new Error("flag --components takes no value");
17535
- components = true;
17536
- continue;
17537
- }
17538
- const modeCommand = MODE_FLAGS[name2];
17539
- if (modeCommand !== undefined) {
17540
- if (value !== undefined)
17541
- throw new Error(`flag ${name2} takes no value`);
17542
- if (mode !== undefined)
17543
- throw new Error("use one command or mode in a run");
17544
- mode = modeCommand;
17545
- continue;
17546
- }
17547
- if (!VALUE_FLAGS2.has(name2)) {
17548
- throw new Error(`unknown flag '${name2}' (see repo-tools query --help)`);
17549
- }
17550
- if (value === undefined || value === "") {
17551
- throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
17552
- }
17553
- if (name2 === "--root") {
17554
- if (root !== undefined)
17555
- throw new Error("the root is set twice");
17556
- root = value;
17557
- } else if (name2 === "--config") {
17558
- if (isAbsolutePath(value))
17559
- throw new Error(`flag --config ${ABSOLUTE_PATH_TEXT}`);
17560
- config = value;
17561
- } else if (name2 === "--out") {
17562
- if (isAbsolutePath(value))
17563
- throw new Error(`flag --out ${ABSOLUTE_PATH_TEXT}`);
17564
- out2 = value;
17565
- } else {
17566
- nodeRuntimes = list2(name2, value);
17567
- }
17568
- }
17569
- const [first, ...rest] = words;
17570
- let command;
17571
- if (mode !== undefined) {
17572
- if (first !== undefined) {
17573
- if (COMMAND_USAGE[first] !== undefined)
17574
- throw new Error("use one command or mode in a run");
17575
- throw new Error(`unknown command '${first}' (see repo-tools query --help)`);
17507
+ function isSource(path) {
17508
+ if (path.split("/").some((part) => SKIP_DIRS2.has(part)))
17509
+ return false;
17510
+ if (SKIP_SUFFIXES.some((suffix) => path.endsWith(suffix)))
17511
+ return false;
17512
+ return languageOf(path) !== "";
17513
+ }
17514
+ function gitFiles(root, args2) {
17515
+ const run2 = spawnSync2("git", ["-C", root, "ls-files", "-z", ...args2], {
17516
+ stdio: ["ignore", "pipe", "ignore"],
17517
+ timeout: 120000,
17518
+ maxBuffer: 1 << 30
17519
+ });
17520
+ if (run2.error || run2.status !== 0 || !run2.stdout)
17521
+ return null;
17522
+ return new TextDecoder("utf-8").decode(run2.stdout).split("\x00").filter(Boolean);
17523
+ }
17524
+ function walkSource(root) {
17525
+ const found = [];
17526
+ const go = (dir, prefix) => {
17527
+ let entries;
17528
+ try {
17529
+ entries = readdirSync4(dir, { withFileTypes: true });
17530
+ } catch {
17531
+ return;
17576
17532
  }
17577
- command = mode;
17578
- } else {
17579
- if (first === undefined) {
17580
- throw new Error("a command is missing (see repo-tools query --help)");
17533
+ for (const entry of entries) {
17534
+ const rel = `${prefix}${entry.name}`;
17535
+ if (entry.isDirectory()) {
17536
+ if (!SKIP_DIRS2.has(entry.name))
17537
+ go(join23(dir, entry.name), `${rel}/`);
17538
+ } else if (isSource(rel) && statOk(join23(dir, entry.name))) {
17539
+ found.push(rel);
17540
+ }
17581
17541
  }
17582
- command = makeCommand(first, rest);
17583
- }
17584
- if (components) {
17585
- if (command.name !== "cycles")
17586
- throw new Error("flag --components applies to cycles only");
17587
- command = { name: "cycles", components: true };
17588
- }
17589
- return {
17590
- command,
17591
- root: root ?? cwd,
17592
- ...config === undefined ? {} : { config },
17593
- ...out2 === undefined ? {} : { out: out2 },
17594
- ...nodeRuntimes === undefined ? {} : { nodeRuntimes }
17595
17542
  };
17543
+ go(root, "");
17544
+ return found;
17596
17545
  }
17597
-
17598
- // src/map/query.ts
17599
- import { readFileSync as readFileSync21 } from "node:fs";
17600
- var SUPPORTED_SCHEMA_MAJOR = Number(SCHEMA_VERSION.split(".")[0]);
17601
- function loadGraph(graphPath) {
17602
- const data = JSON.parse(readFileSync21(graphPath, "utf8"));
17603
- if (data === null || typeof data !== "object" || !("modules" in data)) {
17604
- throw new Error(`${graphPath}: no 'modules' key found -- this is not a map dependency-graph.json, or it is ` + "from an incompatible schema. Cannot answer any query against it.");
17605
- }
17606
- const warnings = [];
17607
- const version = data.metadata?.schemaVersion;
17608
- if (version === undefined || version === null) {
17609
- warnings.push(`${graphPath}: metadata.schemaVersion is missing -- proceeding as if it matches this ` + `module's schema (${SCHEMA_VERSION}), but an older or newer artifact whose shape has ` + "actually changed may produce a silently wrong answer.");
17610
- } else if (Number(String(version).split(".")[0]) !== SUPPORTED_SCHEMA_MAJOR) {
17611
- throw new Error(`${graphPath}: schemaVersion '${String(version)}' is not compatible with this module's ` + `supported major version ${SUPPORTED_SCHEMA_MAJOR} (SCHEMA_VERSION='${SCHEMA_VERSION}'). ` + "Refusing to query a schema this module was not written against rather than risk a " + "silently wrong answer.");
17546
+ function statOk(path) {
17547
+ try {
17548
+ return statSync9(path).isFile();
17549
+ } catch {
17550
+ return false;
17612
17551
  }
17613
- return { data, warnings };
17614
17552
  }
17615
- function entries2(data) {
17616
- if (!data.modules) {
17617
- throw new Error("graph data has no 'modules' key -- cannot answer; pass data returned by loadGraph(), or " + "a document with the same shape");
17553
+ function loadExclusions(root) {
17554
+ const file = join23(root, CONFIG_NAME);
17555
+ if (!existsSync10(file) || !statOk(file))
17556
+ return [];
17557
+ let data;
17558
+ try {
17559
+ data = JSON.parse(readFileSync21(file, "utf8"));
17560
+ } catch (error) {
17561
+ throw new ConfigError(`${CONFIG_NAME}: invalid JSON (${error.message})`);
17618
17562
  }
17619
- return Object.values(data.modules).flatMap((area) => Object.entries(area));
17620
- }
17621
- var sorted = (items) => items.sort(compareCodeUnits);
17622
- function dependents(data, file) {
17623
- const all = entries2(data);
17624
- if (!all.some(([path]) => path === file)) {
17625
- throw new Error(`'${file}' is not a file in this graph -- cannot answer 'dependents' for an unknown path. ` + "Check the path is repo-relative POSIX and this is the graph for the right repo/checkout. " + "(A file that IS known but genuinely has no importers returns [] normally, not an error.)");
17563
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
17564
+ throw new ConfigError(`${CONFIG_NAME}: the top level must be an object`);
17626
17565
  }
17627
- return sorted(all.filter(([, entry]) => (entry.internalDependencies ?? []).some((d) => d.file === file)).map(([path]) => path));
17566
+ const list = data.exclude ?? [];
17567
+ if (!Array.isArray(list))
17568
+ throw new ConfigError(`${CONFIG_NAME}: 'exclude' must be an array`);
17569
+ return list.map((entry, i2) => {
17570
+ const record = typeof entry === "object" && entry !== null ? entry : {};
17571
+ const { path, reason } = record;
17572
+ if (typeof path !== "string" || path === "") {
17573
+ throw new ConfigError(`${CONFIG_NAME}: exclude[${i2}] needs a string 'path'`);
17574
+ }
17575
+ if (typeof reason !== "string" || reason.trim() === "") {
17576
+ throw new ConfigError(`${CONFIG_NAME}: exclude[${i2}] (${path}) needs a non-empty 'reason'`);
17577
+ }
17578
+ return {
17579
+ prefix: `${path.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")}/`,
17580
+ reason: reason.trim()
17581
+ };
17582
+ });
17628
17583
  }
17629
- function symbolUsers(data, symbol) {
17630
- return sorted(entries2(data).filter(([, entry]) => (entry.internalDependencies ?? []).some((d) => (d.imports ?? []).includes(symbol))).map(([path]) => path));
17584
+ function applyExclusions(files, how, root) {
17585
+ const exclusions = loadExclusions(root);
17586
+ if (exclusions.length === 0)
17587
+ return { files, provenance: how };
17588
+ const kept = files.filter((f) => !exclusions.some((e) => `${f}/`.startsWith(e.prefix)));
17589
+ const named = exclusions.map((e) => `${e.prefix} (${e.reason})`).join("; ");
17590
+ return {
17591
+ files: kept,
17592
+ provenance: `${how} -- ${files.length - kept.length} file(s) excluded by ${CONFIG_NAME}: ${named}`
17593
+ };
17631
17594
  }
17632
- function cycles(data, limits = {}) {
17633
- const all = entries2(data);
17634
- const known = new Set(all.map(([path]) => path));
17635
- const edges = new Map;
17636
- for (const [path, entry] of all) {
17637
- const targets = (entry.internalDependencies ?? []).map((d) => d.file).filter((f) => f !== undefined && known.has(f));
17638
- edges.set(path, [...new Set(targets)]);
17595
+ function discover2(root) {
17596
+ const tracked = gitFiles(root, []);
17597
+ if (tracked === null) {
17598
+ const files = sortCodeUnits(walkSource(root));
17599
+ return applyExclusions(files, "filesystem walk (NOT a git repo -- may include generated files)", root);
17639
17600
  }
17640
- const result = simpleCycles(edges, limits);
17641
- if (result.truncated) {
17642
- result.warning = `cycles: simple-cycle enumeration hit its safety cap (found ${result.cycles.length} ` + `cycles / ${result.steps} backtracking steps) and stopped early -- the returned list is a ` + "FLOOR (at least this many simple cycles exist), not an exact total.";
17601
+ const files = sortCodeUnits(tracked.filter(isSource));
17602
+ const untracked = (gitFiles(root, ["--others", "--exclude-standard"]) ?? []).filter(isSource);
17603
+ if (untracked.length > 0 && files.length === 0) {
17604
+ return applyExclusions(sortCodeUnits(untracked), `filesystem walk -- git tracks NO source here, ${untracked.length} untracked file(s) found; ` + "commit them or the gate measures nothing", root);
17643
17605
  }
17644
- return result;
17645
- }
17646
-
17647
- // src/query/graph.ts
17648
- function fileEntriesOf(graph) {
17649
- const pairs = [];
17650
- for (const files of Object.values(graph.modules)) {
17651
- for (const [file, entry] of Object.entries(files))
17652
- pairs.push([file, entry]);
17606
+ if (untracked.length > 0) {
17607
+ return applyExclusions(files, `git ls-files (${files.length} tracked) -- WARNING: ${untracked.length} untracked source ` + "file(s) were NOT measured", root);
17653
17608
  }
17654
- return pairs;
17609
+ return applyExclusions(files, "git ls-files", root);
17655
17610
  }
17656
- function buildForward(fileEntries, allFiles) {
17657
- const forward = new Map;
17658
- for (const [file, entry] of fileEntries) {
17659
- const targets = new Set;
17660
- for (const dep of entry.internalDependencies ?? []) {
17661
- if (dep.file !== undefined && allFiles.has(dep.file))
17662
- targets.add(dep.file);
17663
- }
17664
- forward.set(file, targets);
17665
- }
17666
- return forward;
17611
+
17612
+ // src/docs/model.ts
17613
+ var STUB_MARKER = "TODO:";
17614
+ function isParsed(report) {
17615
+ return report.error === "";
17667
17616
  }
17668
- function invert(forward) {
17669
- const reverse = new Map;
17670
- for (const [file, targets] of forward) {
17671
- for (const target of targets) {
17672
- const importers = reverse.get(target) ?? new Set;
17673
- importers.add(file);
17674
- reverse.set(target, importers);
17675
- }
17676
- }
17677
- const out2 = {};
17678
- for (const target of sortCodeUnits([...reverse.keys()])) {
17679
- out2[target] = sortCodeUnits([...reverse.get(target) ?? []]);
17680
- }
17681
- return out2;
17617
+ function newReport(path, language) {
17618
+ return { path, language, symbols: [], dialects: new Set, error: "" };
17682
17619
  }
17683
17620
 
17684
- // src/query/load.ts
17685
- import { existsSync as existsSync10, readFileSync as readFileSync22 } from "node:fs";
17686
- var RUN_MAP = "run repo-tools map first";
17687
- function reportRel(out2, name2) {
17688
- return `${out2.replace(/\\/g, "/").replace(/\/+$/, "")}/${name2}`;
17621
+ // src/ste/rules.ts
17622
+ var IRREGULAR_PARTICIPLES = "read|built|sent|held|kept|made|found|put|set|run|lost|left|met|paid|told" + "|thrown|drawn|given|taken|shown|known|grown|blown" + "|cut|meant|dealt|felt|sold|said|spent|split|shut|hit|let|bred|fed|led";
17623
+ var DETERMINERS = "the|a|an|its|their|this|that|our|your|each|every" + "|his|her|my|these|those";
17624
+ var PASSIVE_SOURCE = `${B}(?:${ci("is|are|was|were|be|been|being")})${S}+` + `(?:${W}+(?:${ci("ed|en")})|(?:${ci(IRREGULAR_PARTICIPLES)}))${S}+` + `by${S}+(?:(?:${ci(DETERMINERS)})${B}|[A-Z\`])`;
17625
+ var AMBIGUOUS_VERBS = "is|are|was|were|be|will|can|may|must|does|do|has|have|should|shall" + "|disables?|enables?|causes?|returns?|means?|allows?|prevents?|makes?" + "|sets?|gets?|adds?|removes?|creates?|requires?|affects?|applies|happens?";
17626
+ var AMBIGUOUS_SOURCE = `(?:^|(?<=[.!?,;:])${S})${S}*(?:(?:${ci("and|but|or|so|then|yet")})${S}+)?` + `(${ci("it|this|that|these|those")})${S}+(?:${AMBIGUOUS_VERBS})${B}`;
17627
+ var WORDY = [
17628
+ ["in order to", "to"],
17629
+ ["at this point in time", "now"],
17630
+ ["in the event that", "if"],
17631
+ ["due to the fact that", "because"],
17632
+ ["a number of", "some"],
17633
+ ["utilise", "use"],
17634
+ ["utilize", "use"],
17635
+ ["prior to", "before"],
17636
+ ["subsequent to", "after"],
17637
+ ["in the case of", "for"],
17638
+ ["in spite of the fact that", "although"],
17639
+ ["for the purpose of", "to"],
17640
+ ["has the ability to", "can"],
17641
+ ["is able to", "can"]
17642
+ ];
17643
+ var IDENTIFIER_SOURCE = "`[^`]*`" + `|${B}${W}+\\(\\)` + `|${B}${W}*[a-z]${W}*[A-Z]${W}*${B}` + `|${B}${W}+_${W}+${B}` + `|${B}${W}+\\.${W}+${B}`;
17644
+ var MAX_WORDS_PROCEDURAL = 20;
17645
+ var MAX_WORDS_DESCRIPTIVE = 25;
17646
+ function rule(source, flags2 = "") {
17647
+ return new RegExp(source, `u${flags2}`);
17648
+ }
17649
+ function stripIdentifiers(text) {
17650
+ return text.replace(rule(IDENTIFIER_SOURCE, "g"), " ");
17651
+ }
17652
+
17653
+ // src/ste/prose.ts
17654
+ var PASSIVE = rule(PASSIVE_SOURCE);
17655
+ var AMBIGUOUS = rule(AMBIGUOUS_SOURCE);
17656
+ var SENTENCE_END = rule(`(?<=[.!?])${S}+`);
17657
+ function sentencesOf(text) {
17658
+ const collapsed = pySplit(text).join(" ");
17659
+ return collapsed.split(SENTENCE_END).map(pyStrip).filter((s) => s !== "");
17660
+ }
17661
+ function clip(text, limit = 60) {
17662
+ return codePointLength(text) <= limit ? text : `${headCodePoints(text, limit - 1)}…`;
17663
+ }
17664
+ function checkProse(text) {
17665
+ const findings = [];
17666
+ for (const sentence of sentencesOf(text)) {
17667
+ const words = pySplit(sentence);
17668
+ if (words.length > MAX_WORDS_PROCEDURAL) {
17669
+ findings.push([
17670
+ "STE-LEN",
17671
+ `sentence has ${words.length} words (limit ${MAX_WORDS_PROCEDURAL}): '${clip(sentence)}'`
17672
+ ]);
17673
+ }
17674
+ const passive = PASSIVE.exec(sentence);
17675
+ if (passive) {
17676
+ findings.push(["STE-VOICE", `possible passive voice '${passive[0]}' -- prefer active`]);
17677
+ }
17678
+ const ambiguous = AMBIGUOUS.exec(sentence);
17679
+ if (ambiguous) {
17680
+ findings.push(["STE-REF", `ambiguous '${ambiguous[1]}' -- name the thing it refers to`]);
17681
+ }
17682
+ }
17683
+ const prose = stripIdentifiers(text);
17684
+ for (const [phrase, better] of WORDY) {
17685
+ if (rule(`${B}${ci(reEscape(phrase))}${B}`).test(prose)) {
17686
+ findings.push(["STE-WORD", `'${phrase}' -- prefer '${better}'`]);
17687
+ }
17688
+ }
17689
+ return findings;
17690
+ }
17691
+ var SECTION_HEADER = /^(Args|Arguments|Returns|Raises|Yields|Examples|Attributes|Parameters|Notes?|See Also):?$/u;
17692
+ function proseOf(doc) {
17693
+ const out2 = [];
17694
+ let inFence = false;
17695
+ for (const raw of pySplitlines(doc)) {
17696
+ const line = pyStrip(pyStrip(raw).replace(/^\*+/, ""));
17697
+ if (line.startsWith("```")) {
17698
+ inFence = !inFence;
17699
+ continue;
17700
+ }
17701
+ if (inFence || line.startsWith("@") || line.startsWith(">>>"))
17702
+ continue;
17703
+ if (SECTION_HEADER.test(line))
17704
+ continue;
17705
+ if (/^-{3,}$/u.test(line))
17706
+ continue;
17707
+ out2.push(line);
17708
+ }
17709
+ return out2.filter((p) => p !== "").join(" ");
17710
+ }
17711
+
17712
+ // src/docs/rules.ts
17713
+ var TEST_PATH = /(^|\/)(tests?|__tests__|spec)\/|(^|\/)test_[^/]*$|_test\.[^/]+$|\.(test|spec)\.[^/]+$/;
17714
+ function isTestFile2(path) {
17715
+ return TEST_PATH.test(path.replace(/\\/g, "/"));
17716
+ }
17717
+ function applyRules(sym, doc, path, options) {
17718
+ if (sym.exported && !sym.hasDoc && !isTestFile2(path)) {
17719
+ sym.issues.push({ rule: "M1", tier: "MUST", detail: `exported symbol has no ${options.noun}` });
17720
+ return;
17721
+ }
17722
+ if (!sym.hasDoc)
17723
+ return;
17724
+ if (!sym.summary) {
17725
+ sym.issues.push({ rule: "M2", tier: "MUST", detail: `${options.noun} has no summary line` });
17726
+ }
17727
+ for (const p of sym.docParams) {
17728
+ if (!sym.params.includes(p)) {
17729
+ sym.issues.push({
17730
+ rule: "M3",
17731
+ tier: "MUST",
17732
+ detail: `documents parameter '${p}' which is not in the signature`
17733
+ });
17734
+ }
17735
+ }
17736
+ if (doc.includes(STUB_MARKER)) {
17737
+ sym.issues.push({
17738
+ rule: "M4",
17739
+ tier: "MUST",
17740
+ detail: "unfilled stub marker left by `stub`"
17741
+ });
17742
+ }
17743
+ if (sym.summary && !sym.summary.endsWith(".")) {
17744
+ sym.issues.push({ rule: "S2", tier: "SHOULD", detail: "summary does not end with a period" });
17745
+ }
17746
+ if (options.restatesType !== null) {
17747
+ sym.issues.push({ rule: "S5", tier: "SHOULD", detail: options.restatesType });
17748
+ }
17749
+ for (const [rule, detail] of checkProse(options.prose)) {
17750
+ sym.issues.push({ rule: `S6/${rule}`, tier: "SHOULD", detail });
17751
+ }
17752
+ }
17753
+
17754
+ // src/docs/python.ts
17755
+ var GOOGLE = new RegExp(`^${S}*(Args|Arguments|Returns|Raises|Yields):${S}*$`, "mu");
17756
+ var NUMPY = new RegExp(`^${S}*Parameters${S}*\\n${S}*-{3,}${S}*$`, "mu");
17757
+ var REST_TYPE = `(?:[^:\\n]*${S})?`;
17758
+ var REST = new RegExp(`^${S}*:param${S}+${REST_TYPE}\\*{0,2}${W}+${S}*:`, "mu");
17759
+ var GOOGLE_PARAM = new RegExp(`^${S}{1,8}(\\*{0,2}${W}+)${S}*(?:\\([^)]*\\))?${S}*:`, "gmu");
17760
+ var NUMPY_PARAM = new RegExp(`^${S}*(\\*{0,2}${W}+)${S}*:${S}*${NON_SPACE}`, "gmu");
17761
+ var REST_PARAM = new RegExp(`^${S}*:param${S}+${REST_TYPE}(\\*{0,2}${W}+)${S}*:`, "gmu");
17762
+ var SECTION_HEADERS = new Set([
17763
+ "Args",
17764
+ "Arguments",
17765
+ "Returns",
17766
+ "Raises",
17767
+ "Yields",
17768
+ "Examples",
17769
+ "Attributes",
17770
+ "Note",
17771
+ "Notes",
17772
+ "Parameters",
17773
+ "See Also"
17774
+ ]);
17775
+ function detectDialect(doc) {
17776
+ if (NUMPY.test(doc))
17777
+ return "numpy";
17778
+ if (GOOGLE.test(doc))
17779
+ return "google";
17780
+ if (REST.test(doc))
17781
+ return "rest";
17782
+ return "";
17783
+ }
17784
+ var isRule = (line) => /^-+$/.test(pyStrip(line));
17785
+ function section2(doc, header) {
17786
+ const lines = doc.split(`
17787
+ `);
17788
+ const out2 = [];
17789
+ let inside = false;
17790
+ for (let i2 = 0;i2 < lines.length; i2++) {
17791
+ const stripped = pyStrip(lines[i2] ?? "");
17792
+ const next = lines[i2 + 1];
17793
+ const underlined = next !== undefined && isRule(next);
17794
+ if (!inside) {
17795
+ if (stripped.replace(/:+$/, "") === header && stripped.endsWith(":"))
17796
+ inside = true;
17797
+ else if (stripped === header && underlined)
17798
+ inside = true;
17799
+ continue;
17800
+ }
17801
+ if (SECTION_HEADERS.has(stripped.replace(/:+$/, "")) && (stripped.endsWith(":") || underlined)) {
17802
+ break;
17803
+ }
17804
+ out2.push(lines[i2] ?? "");
17805
+ }
17806
+ return out2.join(`
17807
+ `);
17808
+ }
17809
+ function namesOf(pattern, block) {
17810
+ return [...block.matchAll(pattern)].map((m) => (m[1] ?? "").replace(/^\*+/, ""));
17811
+ }
17812
+ function documentedParams(doc, dialect) {
17813
+ if (dialect === "rest")
17814
+ return namesOf(REST_PARAM, doc);
17815
+ if (dialect === "numpy")
17816
+ return namesOf(NUMPY_PARAM, section2(doc, "Parameters"));
17817
+ if (dialect === "google") {
17818
+ return namesOf(GOOGLE_PARAM, section2(doc, "Args") || section2(doc, "Arguments"));
17819
+ }
17820
+ return [];
17821
+ }
17822
+ function expandTabs(text) {
17823
+ let out2 = "";
17824
+ let column = 0;
17825
+ for (const ch of text) {
17826
+ if (ch === "\t") {
17827
+ const width = 8 - column % 8;
17828
+ out2 += " ".repeat(width);
17829
+ column += width;
17830
+ } else {
17831
+ out2 += ch;
17832
+ column = ch === `
17833
+ ` || ch === "\r" ? 0 : column + 1;
17834
+ }
17835
+ }
17836
+ return out2;
17837
+ }
17838
+ function cleandoc(doc) {
17839
+ const lines = expandTabs(doc).split(`
17840
+ `);
17841
+ const stripSpaces = (line) => line.replace(/^ +/, "");
17842
+ let margin = Number.POSITIVE_INFINITY;
17843
+ for (const line of lines.slice(1)) {
17844
+ const content = stripSpaces(line).length;
17845
+ if (content > 0)
17846
+ margin = Math.min(margin, line.length - content);
17847
+ }
17848
+ lines[0] = stripSpaces(lines[0] ?? "");
17849
+ if (margin < Number.POSITIVE_INFINITY) {
17850
+ for (let i2 = 1;i2 < lines.length; i2++)
17851
+ lines[i2] = (lines[i2] ?? "").slice(margin);
17852
+ }
17853
+ while (lines.length > 0 && lines[lines.length - 1] === "")
17854
+ lines.pop();
17855
+ while (lines.length > 0 && lines[0] === "")
17856
+ lines.shift();
17857
+ return lines.join(`
17858
+ `);
17859
+ }
17860
+ var ESCAPES = {
17861
+ "\\": "\\",
17862
+ "'": "'",
17863
+ '"': '"',
17864
+ a: "\x07",
17865
+ b: "\b",
17866
+ f: "\f",
17867
+ n: `
17868
+ `,
17869
+ r: "\r",
17870
+ t: "\t",
17871
+ v: "\v"
17872
+ };
17873
+ function decodeEscapes(body2) {
17874
+ return body2.replace(/\\(\r\n|[\n\r]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|[0-7]{1,3}|N\{[^}]*\}|[\s\S])/g, (whole, seq) => {
17875
+ if (seq === `
17876
+ ` || seq === "\r" || seq === `\r
17877
+ `)
17878
+ return "";
17879
+ const simple = ESCAPES[seq];
17880
+ if (simple !== undefined)
17881
+ return simple;
17882
+ if (/^[0-7]+$/.test(seq))
17883
+ return String.fromCharCode(Number.parseInt(seq, 8));
17884
+ if (/^[xuU][0-9a-fA-F]+$/.test(seq)) {
17885
+ const code = Number.parseInt(seq.slice(1), 16);
17886
+ return code <= 1114111 ? String.fromCodePoint(code) : whole;
17887
+ }
17888
+ return whole;
17889
+ });
17890
+ }
17891
+ function stringValue(node) {
17892
+ const m = /^([A-Za-z]*)('''|"""|'|")/.exec(node.text);
17893
+ if (!m)
17894
+ return null;
17895
+ const prefix = (m[1] ?? "").toLowerCase();
17896
+ const quote = m[2] ?? "";
17897
+ if (/[bft]/.test(prefix))
17898
+ return null;
17899
+ const body2 = node.text.slice(prefix.length + quote.length, node.text.length - quote.length);
17900
+ return prefix.includes("r") ? body2 : decodeEscapes(body2);
17901
+ }
17902
+ function constantString(node) {
17903
+ if (node.type === "string")
17904
+ return stringValue(node);
17905
+ if (node.type === "concatenated_string") {
17906
+ let joined = "";
17907
+ for (const part of node.namedChildren) {
17908
+ if (part.type !== "string")
17909
+ return null;
17910
+ const value = stringValue(part);
17911
+ if (value === null)
17912
+ return null;
17913
+ joined += value;
17914
+ }
17915
+ return joined;
17916
+ }
17917
+ if (node.type === "parenthesized_expression" && node.namedChildren.length === 1) {
17918
+ const inner = node.namedChildren[0];
17919
+ return inner ? constantString(inner) : null;
17920
+ }
17921
+ return null;
17922
+ }
17923
+ function docstringOf(def) {
17924
+ const body2 = def.childForFieldName("body");
17925
+ const first = body2?.namedChildren.find((c) => c.type !== "comment");
17926
+ if (first?.type !== "expression_statement")
17927
+ return "";
17928
+ const value = first.namedChildren[0];
17929
+ if (!value || first.namedChildren.length !== 1)
17930
+ return "";
17931
+ const text = constantString(value);
17932
+ return text === null ? "" : cleandoc(text);
17933
+ }
17934
+ function parameterName(param) {
17935
+ switch (param.type) {
17936
+ case "identifier":
17937
+ return { name: param.text, star: "" };
17938
+ case "typed_parameter": {
17939
+ const first = param.namedChildren[0];
17940
+ return first ? parameterName(first) : null;
17941
+ }
17942
+ case "default_parameter":
17943
+ case "typed_default_parameter": {
17944
+ const name2 = param.childForFieldName("name");
17945
+ return name2?.type === "identifier" ? { name: name2.text, star: "" } : null;
17946
+ }
17947
+ case "list_splat_pattern":
17948
+ case "dictionary_splat_pattern": {
17949
+ const inner = param.namedChildren.find((c) => c.type === "identifier");
17950
+ return inner ? { name: inner.text, star: param.type === "list_splat_pattern" ? "*" : "**" } : null;
17951
+ }
17952
+ default:
17953
+ return null;
17954
+ }
17955
+ }
17956
+ function paramsOf(def) {
17957
+ const list = def.childForFieldName("parameters");
17958
+ const plain = [];
17959
+ let vararg;
17960
+ let kwarg;
17961
+ for (const param of list?.namedChildren ?? []) {
17962
+ const found = parameterName(param);
17963
+ if (!found)
17964
+ continue;
17965
+ if (found.star === "*")
17966
+ vararg = found.name;
17967
+ else if (found.star === "**")
17968
+ kwarg = found.name;
17969
+ else
17970
+ plain.push(found.name);
17971
+ }
17972
+ const names = [...plain, ...vararg ? [vararg] : [], ...kwarg ? [kwarg] : []];
17973
+ return names.filter((n) => n !== "self" && n !== "cls");
17974
+ }
17975
+ function isExported(name2, moduleAll) {
17976
+ if (moduleAll !== null)
17977
+ return moduleAll.has(name2);
17978
+ if (name2.startsWith("__") && name2.endsWith("__"))
17979
+ return true;
17980
+ return !name2.startsWith("_");
17981
+ }
17982
+ function literalNames(value) {
17983
+ if (!["list", "tuple", "set"].includes(value.type))
17984
+ return null;
17985
+ const names = new Set;
17986
+ for (const item of value.namedChildren) {
17987
+ if (item.type === "comment")
17988
+ continue;
17989
+ const text = constantString(item);
17990
+ if (text === null)
17991
+ return null;
17992
+ names.add(text);
17993
+ }
17994
+ return names;
17995
+ }
17996
+ function moduleAllOf(root) {
17997
+ let found = null;
17998
+ for (const stmt of root.namedChildren) {
17999
+ const assign = stmt.type === "expression_statement" ? stmt.namedChildren[0] : undefined;
18000
+ if (assign?.type !== "assignment" || assign.childForFieldName("type"))
18001
+ continue;
18002
+ const left = assign.childForFieldName("left");
18003
+ const right = assign.childForFieldName("right");
18004
+ if (left?.type !== "identifier" || left.text !== "__all__" || !right)
18005
+ continue;
18006
+ found = literalNames(right);
18007
+ }
18008
+ return found;
18009
+ }
18010
+ function firstErrorLine(root) {
18011
+ let node = root;
18012
+ for (;; ) {
18013
+ const inner = node.children.find((c) => c.hasError || c.isMissing);
18014
+ if (!inner)
18015
+ return node.startPosition.row + 1;
18016
+ node = inner;
18017
+ }
18018
+ }
18019
+ var PYTHON_2_STATEMENTS = ["print_statement", "exec_statement"];
18020
+ function legacyStatement(root) {
18021
+ return root.descendantsOfType(PYTHON_2_STATEMENTS).find((n) => !n.namedChildren.some((c) => c.type === "chevron"));
18022
+ }
18023
+ function indentationError(root) {
18024
+ for (const scope of [root, ...root.descendantsOfType(["block"])]) {
18025
+ const isBlock = scope.type === "block";
18026
+ const statements = scope.namedChildren.filter((c) => c.type !== "comment");
18027
+ if (isBlock && statements.length === 0) {
18028
+ return `IndentationError: expected an indented block at line ${scope.startPosition.row + 2}`;
18029
+ }
18030
+ const inline = isBlock && scope.previousSibling?.endPosition.row === scope.startPosition.row;
18031
+ let expected = isBlock ? -1 : 0;
18032
+ let previousEnd = inline ? scope.startPosition.row : -1;
18033
+ for (const stmt of statements) {
18034
+ const { row, column } = stmt.startPosition;
18035
+ if (row !== previousEnd) {
18036
+ if (expected < 0)
18037
+ expected = column;
18038
+ else if (column !== expected) {
18039
+ const kind = isBlock ? "inconsistent indentation" : "unexpected indent";
18040
+ return `IndentationError: ${kind} at line ${row + 1}`;
18041
+ }
18042
+ }
18043
+ previousEnd = stmt.endPosition.row;
18044
+ }
18045
+ }
18046
+ return null;
18047
+ }
18048
+ var LEADING_ZERO = /^0[0-9_]*[1-9][0-9_]*$/;
18049
+ function syntaxError(root) {
18050
+ if (root.hasError)
18051
+ return `SyntaxError: invalid syntax at line ${firstErrorLine(root)}`;
18052
+ const legacy = legacyStatement(root);
18053
+ if (legacy)
18054
+ return `SyntaxError: Python 2 statement at line ${legacy.startPosition.row + 1}`;
18055
+ const indent = indentationError(root);
18056
+ if (indent)
18057
+ return indent;
18058
+ const octal = root.descendantsOfType(["integer"]).find((n) => LEADING_ZERO.test(n.text));
18059
+ if (octal) {
18060
+ const line = octal.startPosition.row + 1;
18061
+ return `SyntaxError: leading zeros in decimal integer literals are not permitted at line ${line}`;
18062
+ }
18063
+ return null;
18064
+ }
18065
+ function parsesAsPython(text) {
18066
+ const tree = parserFor("python").parse(text.replace(/^\uFEFF/, ""));
18067
+ if (!tree)
18068
+ return false;
18069
+ try {
18070
+ return syntaxError(tree.rootNode) === null;
18071
+ } finally {
18072
+ tree.delete();
18073
+ }
18074
+ }
18075
+ function summaryOf(doc) {
18076
+ return pySplitlines(pyStrip(doc))[0] ?? "";
18077
+ }
18078
+ function restatesTypes(doc) {
18079
+ const header = new RegExp(`^${S}*(Args|Parameters):?${S}*$`, "mu");
18080
+ const typed = new RegExp(`^${S}{1,8}\\*{0,2}${W}+${S}*\\([^)]+\\)${S}*:`, "mu");
18081
+ return header.test(doc) && doc.includes("(") && typed.test(doc);
18082
+ }
18083
+ async function analysePython(path, source) {
18084
+ const report = newReport(path, "python");
18085
+ await loadGrammar("python");
18086
+ const tree = parserFor("python").parse(source);
18087
+ if (!tree) {
18088
+ report.error = "SyntaxError: the parser returned no tree";
18089
+ return report;
18090
+ }
18091
+ try {
18092
+ const root = tree.rootNode;
18093
+ const failure = syntaxError(root);
18094
+ if (failure) {
18095
+ report.error = failure;
18096
+ return report;
18097
+ }
18098
+ const moduleAll = moduleAllOf(root);
18099
+ for (const def of root.descendantsOfType(["function_definition", "class_definition"])) {
18100
+ const doc = docstringOf(def);
18101
+ const dialect = detectDialect(doc);
18102
+ const isClass = def.type === "class_definition";
18103
+ const name2 = def.childForFieldName("name")?.text ?? "<anonymous>";
18104
+ const line = def.startPosition.row + 1;
18105
+ const sym = {
18106
+ file: path,
18107
+ line,
18108
+ anchorLine: line,
18109
+ anchorColumn: 0,
18110
+ name: name2,
18111
+ kind: isClass ? "class" : "function",
18112
+ exported: isExported(name2, moduleAll),
18113
+ hasDoc: pyStrip(doc) !== "",
18114
+ params: isClass ? [] : paramsOf(def),
18115
+ docParams: doc ? documentedParams(doc, dialect) : [],
18116
+ summary: summaryOf(doc),
18117
+ dialect,
18118
+ issues: []
18119
+ };
18120
+ applyRules(sym, doc, path, {
18121
+ noun: "docstring",
18122
+ restatesType: restatesTypes(doc) ? "docstring restates types already in the signature" : null,
18123
+ prose: proseOf(doc)
18124
+ });
18125
+ report.symbols.push(sym);
18126
+ if (dialect)
18127
+ report.dialects.add(dialect);
18128
+ }
18129
+ } finally {
18130
+ tree.delete();
18131
+ }
18132
+ return report;
18133
+ }
18134
+
18135
+ // src/docs/typescript.ts
18136
+ var NAME = "[\\p{L}\\p{N}_$]";
18137
+ var TSDOC_PARAM = new RegExp(`^${S}*\\*?${S}*@param${S}+(?:\\{[^}]*\\}${S}*)?\\[?(${NAME}+)`, "gmu");
18138
+ var JSDOC_TYPED_PARAM = new RegExp(`@param${S}+\\{[^}]+\\}`, "u");
18139
+ var JSDOC_TYPED_TAG = new RegExp(`@(?:param|returns?|type|typedef|property|prop|throws|this)${S}+\\{[^}]+\\}`, "u");
18140
+ var TSDOC_TAG = new RegExp(`@param${S}+${NAME}+${S}+-${S}|@(?:returns?|throws)${S}+(?!\\{)${NON_SPACE}|@(?:remarks|typeParam|defaultValue)${B}`, "u");
18141
+ function dialectOf(doc) {
18142
+ if (!doc)
18143
+ return "";
18144
+ if (JSDOC_TYPED_TAG.test(doc))
18145
+ return "jsdoc";
18146
+ if (TSDOC_TAG.test(doc))
18147
+ return "tsdoc";
18148
+ return "";
18149
+ }
18150
+ var DECLARATIONS = {
18151
+ function_declaration: "function",
18152
+ generator_function_declaration: "function",
18153
+ class_declaration: "class",
18154
+ abstract_class_declaration: "class",
18155
+ interface_declaration: "interface",
18156
+ type_alias_declaration: "type",
18157
+ method_definition: "method"
18158
+ };
18159
+ var DECLARATION_TYPES = Object.keys(DECLARATIONS);
18160
+ function grammarFor(path) {
18161
+ return path.endsWith(".tsx") || path.endsWith(".jsx") ? "tsx" : "typescript";
18162
+ }
18163
+ var SPACE_CLASS = "[ \\t\\n\\r\\f\\v]";
18164
+ var ABSTRACT_PROPERTY = new RegExp(`\\babstract(${SPACE_CLASS}*\\??${SPACE_CLASS}*:)`, "g");
18165
+ var EXPORT_TYPE_STAR = new RegExp(`(\\bexport${SPACE_CLASS}+)type(${SPACE_CLASS}*\\*)`, "g");
18166
+ function sanitise(source) {
18167
+ return source.replace(ABSTRACT_PROPERTY, "abstrac_$1").replace(EXPORT_TYPE_STAR, "$1 $2");
18168
+ }
18169
+ var IDENTIFIERS = new Set(["identifier", "shorthand_property_identifier_pattern"]);
18170
+ function boundIdentifiers(node, text) {
18171
+ if (IDENTIFIERS.has(node.type))
18172
+ return [text(node)];
18173
+ const pattern = node.childForFieldName("pattern");
18174
+ if (pattern)
18175
+ return boundIdentifiers(pattern, text);
18176
+ const into = (child) => child ? boundIdentifiers(child, text) : [];
18177
+ switch (node.type) {
18178
+ case "object_pattern":
18179
+ case "array_pattern":
18180
+ return node.namedChildren.flatMap((child) => boundIdentifiers(child, text));
18181
+ case "assignment_pattern":
18182
+ case "object_assignment_pattern":
18183
+ return into(node.childForFieldName("left"));
18184
+ case "pair_pattern":
18185
+ return into(node.childForFieldName("value"));
18186
+ case "rest_pattern":
18187
+ return into(node.namedChildren[0] ?? null);
18188
+ default: {
18189
+ const first = node.namedChildren.find((c) => IDENTIFIERS.has(c.type));
18190
+ return first ? [text(first)] : [];
18191
+ }
18192
+ }
18193
+ }
18194
+ function paramsOf2(node, text) {
18195
+ const list = node.childForFieldName("parameters");
18196
+ return (list?.namedChildren ?? []).flatMap((param) => boundIdentifiers(param, text));
18197
+ }
18198
+ function anchorOf(node) {
18199
+ let target = node;
18200
+ while (target.parent?.type === "export_statement")
18201
+ target = target.parent;
18202
+ return target;
18203
+ }
18204
+ function leadingDoc(anchor, text) {
18205
+ const prev = anchor.previousSibling;
18206
+ if (prev?.type !== "comment")
18207
+ return "";
18208
+ const comment = text(prev);
18209
+ return comment.startsWith("/**") ? comment : "";
18210
+ }
18211
+ function isExported2(node) {
18212
+ let parent = node.parent;
18213
+ while (parent) {
18214
+ if (parent.type === "export_statement")
18215
+ return true;
18216
+ if (["program", "statement_block", "class_body"].includes(parent.type))
18217
+ break;
18218
+ parent = parent.parent;
18219
+ }
18220
+ return false;
18221
+ }
18222
+ function summaryOf2(doc) {
18223
+ for (const raw of pySplitlines(doc)) {
18224
+ const line = pyStrip(pyStrip(pyStrip(raw.replace(/\*+\/\s*$/, "")).replace(/^[/*]+/, "")).replace(/^\*+/, ""));
18225
+ if (line === "" || line.startsWith("@"))
18226
+ continue;
18227
+ return line;
18228
+ }
18229
+ return "";
18230
+ }
18231
+ function proseOfComment(doc) {
18232
+ const lines = pySplitlines(doc).map((l) => l.replace(/^\s*\/\*+/, "").replace(/\*+\/\s*$/, ""));
18233
+ return proseOf(lines.join(`
18234
+ `));
18235
+ }
18236
+ function parsesAsTypeScript(path, text) {
18237
+ const tree = parserFor(grammarFor(path)).parse(sanitise(text.replace(/^\uFEFF/, "")));
18238
+ if (!tree)
18239
+ return false;
18240
+ try {
18241
+ return !tree.rootNode.hasError;
18242
+ } finally {
18243
+ tree.delete();
18244
+ }
18245
+ }
18246
+ async function analyseTypeScript(path, source) {
18247
+ const report = newReport(path, "typescript");
18248
+ const grammar = grammarFor(path);
18249
+ await loadGrammar(grammar);
18250
+ const tree = parserFor(grammar).parse(sanitise(source));
18251
+ if (!tree) {
18252
+ report.error = "parse error (the parser returned no tree)";
18253
+ return report;
18254
+ }
18255
+ try {
18256
+ if (tree.rootNode.hasError) {
18257
+ report.error = "parse error (tree-sitter reported ERROR nodes)";
18258
+ return report;
18259
+ }
18260
+ const text = (node) => source.slice(node.startIndex, node.endIndex);
18261
+ for (const node of tree.rootNode.descendantsOfType(DECLARATION_TYPES)) {
18262
+ const kind = DECLARATIONS[node.type];
18263
+ const anchor = anchorOf(node);
18264
+ const doc = leadingDoc(anchor, text);
18265
+ const dialect = dialectOf(doc);
18266
+ const typeLike = kind === "class" || kind === "interface" || kind === "type";
18267
+ const line = node.startPosition.row + 1;
18268
+ const nameNode = node.childForFieldName("name");
18269
+ const sym = {
18270
+ file: path,
18271
+ line,
18272
+ anchorLine: anchor.startPosition.row + 1,
18273
+ anchorColumn: anchor.startPosition.column,
18274
+ name: nameNode ? text(nameNode) : "<anonymous>",
18275
+ kind,
18276
+ exported: isExported2(node),
18277
+ hasDoc: doc !== "",
18278
+ params: typeLike ? [] : paramsOf2(node, text),
18279
+ docParams: doc ? [...doc.matchAll(TSDOC_PARAM)].map((m) => m[1] ?? "") : [],
18280
+ summary: summaryOf2(doc),
18281
+ dialect,
18282
+ issues: []
18283
+ };
18284
+ const typed = /\.tsx?$/.test(path) && JSDOC_TYPED_PARAM.test(doc);
18285
+ applyRules(sym, doc, path, {
18286
+ noun: "doc comment",
18287
+ restatesType: typed ? "@param restates a type already in the TypeScript signature" : null,
18288
+ prose: proseOfComment(doc)
18289
+ });
18290
+ report.symbols.push(sym);
18291
+ if (dialect)
18292
+ report.dialects.add(dialect);
18293
+ }
18294
+ } finally {
18295
+ tree.delete();
18296
+ }
18297
+ report.symbols.sort((a, b) => a.line - b.line);
18298
+ return report;
18299
+ }
18300
+
18301
+ // src/docs/analyse.ts
18302
+ var decoder2 = new TextDecoder("utf-8", { fatal: true });
18303
+ function readFailure(error) {
18304
+ const code = error.code;
18305
+ if (code === "ENOENT")
18306
+ return "the file is tracked but missing from the work tree";
18307
+ if (error instanceof TypeError)
18308
+ return "the file is not valid UTF-8";
18309
+ return code ?? String(error);
18310
+ }
18311
+ function readSourceText2(path) {
18312
+ return decoder2.decode(readFileSync22(path)).replace(/\r\n?/g, `
18313
+ `);
18314
+ }
18315
+ async function analyseFile(root, rel) {
18316
+ const language = languageOf(rel);
18317
+ let source;
18318
+ try {
18319
+ source = readSourceText2(join24(root, rel));
18320
+ } catch (error) {
18321
+ const report = newReport(rel, language);
18322
+ report.error = `unreadable: ${readFailure(error)}`;
18323
+ return report;
18324
+ }
18325
+ return language === "python" ? analysePython(rel, source) : analyseTypeScript(rel, source);
18326
+ }
18327
+ async function analyseRepo(root) {
18328
+ const { files, provenance } = discover2(root);
18329
+ const reports = [];
18330
+ for (const rel of files)
18331
+ reports.push(await analyseFile(root, rel));
18332
+ return { reports, provenance };
18333
+ }
18334
+
18335
+ // src/docs/report.ts
18336
+ import { join as join25 } from "node:path";
18337
+ function percent3(part, whole) {
18338
+ if (whole === 0)
18339
+ return 100;
18340
+ const x = 100 * part / whole;
18341
+ const quarters = x * 4;
18342
+ if (Number.isInteger(quarters) && quarters % 2 !== 0) {
18343
+ const floor = Math.floor(x * 10);
18344
+ return (floor % 2 === 0 ? floor : floor + 1) / 10;
18345
+ }
18346
+ return Number(x.toFixed(1));
18347
+ }
18348
+ function summarise(reports) {
18349
+ const parsed = reports.filter(isParsed);
18350
+ const symbols = parsed.flatMap((r) => r.symbols);
18351
+ const exported = symbols.filter((s) => s.exported);
18352
+ const documented = exported.filter((s) => s.hasDoc);
18353
+ const issues = symbols.flatMap((s) => s.issues);
18354
+ return {
18355
+ filesScanned: reports.length,
18356
+ filesParsed: parsed.length,
18357
+ filesUnparsed: reports.length - parsed.length,
18358
+ symbols: symbols.length,
18359
+ exportedSymbols: exported.length,
18360
+ exportedDocumented: documented.length,
18361
+ exportedDocumentedPct: percent3(documented.length, exported.length),
18362
+ mustIssues: issues.filter((i2) => i2.tier === "MUST").length,
18363
+ shouldIssues: issues.filter((i2) => i2.tier === "SHOULD").length,
18364
+ filesMixedDialect: parsed.filter((r) => r.dialects.size > 1).length
18365
+ };
18366
+ }
18367
+ var TABLE_LIMIT = 200;
18368
+ var pctText = (pct) => pct.toFixed(1);
18369
+ function renderMarkdown(reports, stats, provenance, rootName) {
18370
+ const lines = [
18371
+ "# Code documentation coverage",
18372
+ "",
18373
+ `**Repo:** \`${rootName}\``,
18374
+ "",
18375
+ "Generated by `repo-tools docs scan`. The rules are in the README of `repo-tools`.",
18376
+ "",
18377
+ "## Summary",
18378
+ "",
18379
+ "| Metric | Value |",
18380
+ "|---|---:|",
18381
+ `| Files scanned | ${stats.filesScanned} |`,
18382
+ `| Files parsed | ${stats.filesParsed} |`,
18383
+ `| **Files UNPARSED** (unknown, not clean) | **${stats.filesUnparsed}** |`,
18384
+ `| Symbols found | ${stats.symbols} |`,
18385
+ `| Exported symbols | ${stats.exportedSymbols} |`,
18386
+ `| Exported documented | ${stats.exportedDocumented} |`,
18387
+ `| Exported documented % | ${pctText(stats.exportedDocumentedPct)} |`,
18388
+ `| MUST issues (gated) | ${stats.mustIssues} |`,
18389
+ `| SHOULD issues (advisory) | ${stats.shouldIssues} |`,
18390
+ `| Files with mixed dialect | ${stats.filesMixedDialect} |`,
18391
+ "",
18392
+ "> The percentage is over **exported** symbols only. Private helpers are not",
18393
+ "> gated -- the public surface is a contract, internals are judgement.",
18394
+ ""
18395
+ ];
18396
+ const mustRows = reports.filter(isParsed).flatMap((r) => r.symbols.flatMap((s) => s.issues.map((i2) => ({ s, i: i2 })))).filter(({ i: i2 }) => i2.tier === "MUST");
18397
+ if (mustRows.length > 0) {
18398
+ lines.push("## MUST issues", "", "| File | Line | Symbol | Rule | Detail |", "|---|---:|---|---|---|");
18399
+ for (const { s, i: i2 } of mustRows.slice(0, TABLE_LIMIT)) {
18400
+ lines.push(`| \`${s.file}\` | ${s.line} | \`${s.name}\` | ${i2.rule} | ${i2.detail} |`);
18401
+ }
18402
+ if (mustRows.length > TABLE_LIMIT) {
18403
+ lines.push(`| … | | | | **${mustRows.length - TABLE_LIMIT} more not shown -- this table is TRUNCATED** |`);
18404
+ }
18405
+ lines.push("");
18406
+ }
18407
+ const unparsed = reports.filter((r) => !isParsed(r));
18408
+ if (unparsed.length > 0) {
18409
+ lines.push("## Unparsed files", "", "These were NOT measured. They are unknown, not clean.", "");
18410
+ for (const r of unparsed)
18411
+ lines.push(`- \`${r.path}\` -- ${r.error}`);
18412
+ lines.push("");
18413
+ }
18414
+ lines.push("## Verification", "", `- files_scanned: ${stats.filesScanned}`, `- files_parsed: ${stats.filesParsed}`, `- files_unparsed: ${stats.filesUnparsed}`, `- exported_symbols: ${stats.exportedSymbols}`, `- exported_documented: ${stats.exportedDocumented}`, `- must_issues: ${stats.mustIssues}`, `- discovery: ${provenance}`, "", "Run `repo-tools docs check .` to gate the repository. The command measures the code again and", "exits 1 on a MUST issue or on an unparsed file. It does not read this file.", "");
18415
+ return lines.join(`
18416
+ `);
18417
+ }
18418
+ function statsJson(stats) {
18419
+ return {
18420
+ files_scanned: stats.filesScanned,
18421
+ files_parsed: stats.filesParsed,
18422
+ files_unparsed: stats.filesUnparsed,
18423
+ symbols: stats.symbols,
18424
+ exported_symbols: stats.exportedSymbols,
18425
+ exported_documented: stats.exportedDocumented,
18426
+ exported_documented_pct: stats.exportedDocumentedPct,
18427
+ must_issues: stats.mustIssues,
18428
+ should_issues: stats.shouldIssues,
18429
+ files_mixed_dialect: stats.filesMixedDialect
18430
+ };
18431
+ }
18432
+ function symbolJson(s) {
18433
+ return {
18434
+ file: s.file,
18435
+ line: s.line,
18436
+ name: s.name,
18437
+ kind: s.kind,
18438
+ exported: s.exported,
18439
+ has_doc: s.hasDoc,
18440
+ params: s.params,
18441
+ doc_params: s.docParams,
18442
+ summary: s.summary,
18443
+ dialect: s.dialect,
18444
+ issues: s.issues
18445
+ };
18446
+ }
18447
+ function writeOutputs(outDir, reports, stats, provenance, rootName) {
18448
+ const markdown = join25(outDir, "COVERAGE.md");
18449
+ const json = join25(outDir, "coverage.json");
18450
+ writeReport(markdown, renderMarkdown(reports, stats, provenance, rootName));
18451
+ const payload = {
18452
+ summary: statsJson(stats),
18453
+ provenance,
18454
+ files: reports.map((r) => ({
18455
+ path: r.path,
18456
+ language: r.language,
18457
+ error: r.error,
18458
+ dialects: [...r.dialects].sort(),
18459
+ symbols: r.symbols.map(symbolJson)
18460
+ }))
18461
+ };
18462
+ writeReport(json, toJson(payload));
18463
+ return { markdown, json };
18464
+ }
18465
+
18466
+ // src/docs/stub.ts
18467
+ function splitLines(text) {
18468
+ return text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
18469
+ }
18470
+ var indentOf = (line) => line.slice(0, line.length - pyLstrip(line).length);
18471
+ var OPENERS = "([{";
18472
+ var CLOSERS = ")]}";
18473
+ function codeOf(line, open) {
18474
+ let code = "";
18475
+ let string = open;
18476
+ let i2 = 0;
18477
+ while (i2 < line.length) {
18478
+ const ch = line.charAt(i2);
18479
+ if (string !== "") {
18480
+ if (ch === "\\")
18481
+ i2 += 2;
18482
+ else if (line.startsWith(string, i2)) {
18483
+ i2 += string.length;
18484
+ string = "";
18485
+ } else
18486
+ i2 += 1;
18487
+ } else if (ch === "#") {
18488
+ break;
18489
+ } else if (ch === '"' || ch === "'") {
18490
+ string = line.startsWith(ch.repeat(3), i2) ? ch.repeat(3) : ch;
18491
+ i2 += string.length;
18492
+ } else {
18493
+ code += ch;
18494
+ i2 += 1;
18495
+ }
18496
+ }
18497
+ return { code, open: string.length === 3 ? string : "" };
18498
+ }
18499
+ function headerEnd(lines, start2) {
18500
+ let depth = 0;
18501
+ let open = "";
18502
+ for (let i2 = start2;i2 < Math.min(lines.length, start2 + 60); i2++) {
18503
+ const line = codeOf(lines[i2] ?? "", open);
18504
+ open = line.open;
18505
+ for (const ch of line.code) {
18506
+ if (OPENERS.includes(ch))
18507
+ depth += 1;
18508
+ else if (CLOSERS.includes(ch))
18509
+ depth -= 1;
18510
+ }
18511
+ if (depth <= 0 && line.code.trimEnd().endsWith(":"))
18512
+ return i2;
18513
+ }
18514
+ return start2;
18515
+ }
18516
+ function planPython(sym, lines) {
18517
+ if (sym.hasDoc)
18518
+ return null;
18519
+ const start2 = sym.line - 1;
18520
+ if (start2 >= lines.length)
18521
+ return null;
18522
+ const end = headerEnd(lines, start2);
18523
+ const body2 = `${indentOf(lines[end] ?? "")} `;
18524
+ const out2 = [`${body2}"""${STUB_MARKER} state what this does, in one active-voice sentence.`];
18525
+ if (sym.params.length > 0 || sym.kind === "function")
18526
+ out2.push("");
18527
+ if (sym.params.length > 0) {
18528
+ out2.push(`${body2}Args:`);
18529
+ for (const p of sym.params) {
18530
+ out2.push(`${body2} ${p}: ${STUB_MARKER} state what it is. Do not repeat the type.`);
18531
+ }
18532
+ out2.push("");
18533
+ }
18534
+ if (sym.kind === "function") {
18535
+ out2.push(`${body2}Returns:`, `${body2} ${STUB_MARKER} state what the caller gets back.`);
18536
+ }
18537
+ out2.push(`${body2}"""`);
18538
+ return { lineIndex: end + 1, text: `${out2.join(`
18539
+ `)}
18540
+ `, symbol: sym.name };
18541
+ }
18542
+ function startsItsLine(sym, lines) {
18543
+ const line = lines[sym.anchorLine - 1] ?? "";
18544
+ return line.slice(0, sym.anchorColumn).trim() === "";
18545
+ }
18546
+ function planTypeScript(sym, lines) {
18547
+ if (sym.hasDoc)
18548
+ return null;
18549
+ const index = sym.anchorLine - 1;
18550
+ if (index >= lines.length)
18551
+ return null;
18552
+ const indent = indentOf(lines[index] ?? "");
18553
+ const out2 = [
18554
+ `${indent}/**`,
18555
+ `${indent} * ${STUB_MARKER} state what this does, in one active-voice sentence.`
18556
+ ];
18557
+ if (sym.params.length > 0) {
18558
+ out2.push(`${indent} *`);
18559
+ for (const p of sym.params) {
18560
+ out2.push(`${indent} * @param ${p} - ${STUB_MARKER} state what it is. Do not repeat the type.`);
18561
+ }
18562
+ }
18563
+ if (sym.kind === "function" || sym.kind === "method") {
18564
+ out2.push(`${indent} * @returns ${STUB_MARKER} state what the caller gets back.`);
18565
+ }
18566
+ out2.push(`${indent} */`);
18567
+ return { lineIndex: index, text: `${out2.join(`
18568
+ `)}
18569
+ `, symbol: sym.name };
18570
+ }
18571
+ function applyInsertions(original, insertions, verify) {
18572
+ if (insertions.length === 0)
18573
+ return { ok: false, text: original, message: "no insertions" };
18574
+ const eol = original.includes(`\r
18575
+ `) ? `\r
18576
+ ` : `
18577
+ `;
18578
+ const lines = splitLines(original);
18579
+ for (const ins of [...insertions].sort((a, b) => b.lineIndex - a.lineIndex)) {
18580
+ const index = Math.max(0, Math.min(ins.lineIndex, lines.length));
18581
+ lines.splice(index, 0, eol === `
18582
+ ` ? ins.text : ins.text.replace(/\n/g, eol));
18583
+ }
18584
+ const updated = lines.join("");
18585
+ if (!verify(updated)) {
18586
+ return {
18587
+ ok: false,
18588
+ text: original,
18589
+ message: "REVERTED: the file would no longer parse after insertion"
18590
+ };
18591
+ }
18592
+ return { ok: true, text: updated, message: `inserted ${insertions.length} stub(s)` };
18593
+ }
18594
+
18595
+ // src/docs/index.ts
18596
+ var DOCS_HELP = `Usage: repo-tools docs scan [root] [--out=<dir>]
18597
+ repo-tools docs stub [root] [--apply] [--path=<path>]
18598
+ repo-tools docs check [root] [--paths <path>... | --paths-from=<file>]
18599
+
18600
+ Measure, stub and gate the documentation comments of the TypeScript, JavaScript
18601
+ and Python files of a repository. The files are the files that git tracks. A
18602
+ file in a skipped folder (node_modules, dist, build, bundle, out, vendor) or
18603
+ with a generated name (.d.ts, .min.js, _pb2.py) is not read.
18604
+
18605
+ Actions:
18606
+ scan Measure the coverage. Write COVERAGE.md and coverage.json.
18607
+ stub Plan a skeleton comment for each exported symbol that has none. The
18608
+ default is a dry run. Every stub holds the marker TODO:, which check
18609
+ fails on.
18610
+ check The gate. Exit 1 on a MUST issue or on a file that does not parse.
18611
+
18612
+ Options:
18613
+ --root=<path> The repository (default: the current folder). An argument
18614
+ that is not a flag also sets the root.
18615
+ --out=<dir> scan: the output folder (default: docs/code-docs below
18616
+ the root). A relative path is relative to the current
18617
+ folder.
18618
+ --apply stub: write the stubs. A file that would not parse after
18619
+ the change is not changed.
18620
+ --path=<path> stub: only this file, or the files below this folder. The
18621
+ root itself ('.') selects every file.
18622
+ --paths <path>... check: gate only these files, for example the files of a
18623
+ pull request. Put the root before this flag. The flag
18624
+ takes every argument up to the next flag. No path means
18625
+ that the gate checks nothing, and it passes.
18626
+ --paths-from=<file> check: read the paths from a file, one path on each line,
18627
+ as 'git diff --name-only' writes them. The quoted form
18628
+ that git writes for a non-ASCII name is decoded.
18629
+ --help, -h Show this help.
18630
+
18631
+ A flag with a value takes '--name=value' or '--name value'.
18632
+
18633
+ The MUST rules decide the exit code:
18634
+ M1 An exported symbol has a doc comment. A test file is exempt.
18635
+ M2 The doc comment has a summary line.
18636
+ M3 The doc comment names no parameter that the signature lacks.
18637
+ M4 The doc comment holds no TODO: marker from stub.
18638
+ M5 A file uses one doc dialect only (google, numpy, rest, tsdoc or jsdoc).
18639
+ The SHOULD rules are reported and never gate: S2 (the summary ends with a full
18640
+ stop), S5 (the comment repeats a type that the signature holds), and S6 (the
18641
+ prose follows Simplified Technical English).
18642
+
18643
+ ${CONFIG_NAME} at the root excludes paths from every action. Each entry needs a
18644
+ reason, and the output names each exclusion:
18645
+ { "exclude": [ { "path": "assembly/", "reason": "AssemblyScript" } ] }
18646
+
18647
+ Exit codes: 0 when the action succeeded and the gate passed. 1 when the gate
18648
+ failed, or when the root holds no source file to measure. 2 on a usage error,
18649
+ when the root is not a folder, or when ${CONFIG_NAME} is malformed.
18650
+ `;
18651
+ var VALUE_FLAGS2 = {
18652
+ "--root": "all",
18653
+ "--out": "scan",
18654
+ "--path": "stub",
18655
+ "--paths-from": "check"
18656
+ };
18657
+ function parseDocsArgs(argv) {
18658
+ const action = argv[0];
18659
+ if (action !== "scan" && action !== "stub" && action !== "check") {
18660
+ throw new Error(action === undefined ? "an action is required: scan, stub or check (see repo-tools docs --help)" : `unknown action '${action}': use scan, stub or check`);
18661
+ }
18662
+ const options = {
18663
+ action,
18664
+ root: ".",
18665
+ out: undefined,
18666
+ apply: false,
18667
+ path: undefined,
18668
+ paths: undefined,
18669
+ pathsFrom: undefined
18670
+ };
18671
+ let rootSet = false;
18672
+ const setRoot = (value) => {
18673
+ if (rootSet)
18674
+ throw new Error("the root is set twice (use --root=<path> or one positional path)");
18675
+ rootSet = true;
18676
+ options.root = value;
18677
+ };
18678
+ const forbid = (flag, owner) => {
18679
+ if (owner !== "all" && owner !== action)
18680
+ throw new Error(`flag ${flag} applies to '${owner}' only`);
18681
+ };
18682
+ for (let i2 = 1;i2 < argv.length; i2++) {
18683
+ const arg = argv[i2] ?? "";
18684
+ if (!arg.startsWith("-")) {
18685
+ setRoot(arg);
18686
+ continue;
18687
+ }
18688
+ const eq = arg.indexOf("=");
18689
+ const name2 = eq === -1 ? arg : arg.slice(0, eq);
18690
+ if (name2 === "--apply") {
18691
+ forbid(name2, "stub");
18692
+ if (eq !== -1)
18693
+ throw new Error("flag --apply takes no value");
18694
+ options.apply = true;
18695
+ } else if (name2 === "--paths") {
18696
+ forbid(name2, "check");
18697
+ options.paths ??= [];
18698
+ if (eq !== -1) {
18699
+ options.paths.push(arg.slice(eq + 1));
18700
+ } else {
18701
+ while (i2 + 1 < argv.length && !(argv[i2 + 1] ?? "").startsWith("-"))
18702
+ options.paths.push(argv[++i2] ?? "");
18703
+ }
18704
+ } else if (Object.hasOwn(VALUE_FLAGS2, name2)) {
18705
+ forbid(name2, VALUE_FLAGS2[name2]);
18706
+ let value = eq === -1 ? undefined : arg.slice(eq + 1);
18707
+ if (eq === -1 && i2 + 1 < argv.length && !(argv[i2 + 1] ?? "").startsWith("-"))
18708
+ value = argv[++i2];
18709
+ if (value === undefined || value === "") {
18710
+ throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
18711
+ }
18712
+ if (name2 === "--root")
18713
+ setRoot(value);
18714
+ else if (name2 === "--out")
18715
+ options.out = value;
18716
+ else if (name2 === "--path")
18717
+ options.path = value;
18718
+ else
18719
+ options.pathsFrom = value;
18720
+ } else {
18721
+ throw new Error(`unknown flag '${name2}' (see repo-tools docs --help)`);
18722
+ }
18723
+ }
18724
+ if (options.paths !== undefined && options.pathsFrom !== undefined) {
18725
+ throw new Error("use --paths or --paths-from, not both");
18726
+ }
18727
+ return options;
18728
+ }
18729
+ function relativeToRoot(root, path) {
18730
+ let q = toPosix(path);
18731
+ if (isAbsolute4(path)) {
18732
+ const rel = toPosix(relative5(root, path));
18733
+ if (rel !== ".." && !rel.startsWith("../") && !isAbsolute4(rel))
18734
+ q = rel;
18735
+ }
18736
+ return q.replace(/^(\.\/)+/, "");
18737
+ }
18738
+ function leavesRoot(root, file) {
18739
+ try {
18740
+ if (lstatSync5(file).isSymbolicLink())
18741
+ return true;
18742
+ const rel = relative5(realpathSync3.native(root), realpathSync3.native(file));
18743
+ return rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute4(rel);
18744
+ } catch {
18745
+ return true;
18746
+ }
18747
+ }
18748
+ var GIT_ESCAPES = {
18749
+ a: 7,
18750
+ b: 8,
18751
+ t: 9,
18752
+ n: 10,
18753
+ v: 11,
18754
+ f: 12,
18755
+ r: 13,
18756
+ '"': 34,
18757
+ "\\": 92
18758
+ };
18759
+ function unquoteGitPath(line) {
18760
+ if (line.length < 2 || !line.startsWith('"') || !line.endsWith('"'))
18761
+ return line;
18762
+ const chars = Array.from(line.slice(1, -1));
18763
+ const encoder = new TextEncoder;
18764
+ const bytes = [];
18765
+ for (let i2 = 0;i2 < chars.length; i2++) {
18766
+ const ch = chars[i2] ?? "";
18767
+ const octal = chars.slice(i2 + 1, i2 + 4).join("");
18768
+ const single = GIT_ESCAPES[chars[i2 + 1] ?? ""];
18769
+ if (ch !== "\\") {
18770
+ bytes.push(...encoder.encode(ch));
18771
+ } else if (/^[0-7]{3}$/.test(octal)) {
18772
+ bytes.push(Number.parseInt(octal, 8));
18773
+ i2 += 3;
18774
+ } else if (single !== undefined) {
18775
+ bytes.push(single);
18776
+ i2 += 1;
18777
+ } else {
18778
+ bytes.push(...encoder.encode(ch));
18779
+ }
18780
+ }
18781
+ return new TextDecoder().decode(Uint8Array.from(bytes));
18782
+ }
18783
+ function shown2(root, path) {
18784
+ const rel = relative5(root, path);
18785
+ return rel.startsWith("..") || isAbsolute4(rel) ? path : `<root>/${toPosix(rel)}`;
18786
+ }
18787
+
18788
+ class NothingToMeasure extends Error {
18789
+ constructor(provenance) {
18790
+ super("no TypeScript, JavaScript or Python source file was found below <root> " + `(${provenance}) -- nothing to measure. This is a failure, not a pass: check the path, ` + "or add the files to git.");
18791
+ this.name = "NothingToMeasure";
18792
+ }
18793
+ }
18794
+ async function runScan(root, options, io) {
18795
+ const { reports, provenance } = await analyseRepo(root);
18796
+ if (reports.length === 0)
18797
+ throw new NothingToMeasure(provenance);
18798
+ const stats = summarise(reports);
18799
+ const outDir = resolve8(options.out ?? join26(root, "docs", "code-docs"));
18800
+ const written = writeOutputs(outDir, reports, stats, provenance, basename9(root));
18801
+ io.stdout(`scanned ${stats.filesScanned} file(s) via ${provenance}
18802
+ `);
18803
+ io.stdout(` exported symbols : ${stats.exportedSymbols}
18804
+ `);
18805
+ io.stdout(` documented : ${stats.exportedDocumented} (${pctText(stats.exportedDocumentedPct)}%)
18806
+ `);
18807
+ io.stdout(` MUST issues : ${stats.mustIssues}
18808
+ `);
18809
+ io.stdout(` SHOULD issues : ${stats.shouldIssues}
18810
+ `);
18811
+ if (stats.filesUnparsed > 0) {
18812
+ io.stdout(` UNPARSED (unknown) : ${stats.filesUnparsed} <- not counted as clean
18813
+ `);
18814
+ }
18815
+ io.stdout(`wrote ${shown2(root, written.markdown)}
18816
+ `);
18817
+ io.stdout(`wrote ${shown2(root, written.json)}
18818
+ `);
18819
+ return 0;
18820
+ }
18821
+ function inScope(report, scope) {
18822
+ const prefix = `${scope.replace(/\/+$/, "")}/`;
18823
+ return report.path.startsWith(prefix) || report.path === scope;
18824
+ }
18825
+ async function runStub(root, options, io) {
18826
+ const analysis = await analyseRepo(root);
18827
+ if (analysis.reports.length === 0)
18828
+ throw new NothingToMeasure(analysis.provenance);
18829
+ const scope = options.path === undefined ? undefined : relativeToRoot(root, options.path).replace(/\/+$/, "");
18830
+ const reports = scope === undefined || scope === "" || scope === "." ? analysis.reports : analysis.reports.filter((r) => inScope(r, scope));
18831
+ let planned = 0;
18832
+ let changed = 0;
18833
+ for (const report of reports) {
18834
+ if (!isParsed(report))
18835
+ continue;
18836
+ const targets = report.symbols.filter((s) => s.issues.some((i2) => i2.rule === "M1"));
18837
+ if (targets.length === 0)
18838
+ continue;
18839
+ const file = join26(root, report.path);
18840
+ if (leavesRoot(root, file)) {
18841
+ io.stdout(` SKIPPED ${report.path}: the path is a link, or it resolves outside the root
18842
+ `);
18843
+ continue;
18844
+ }
18845
+ const raw = readFileSync23(file, "utf8");
18846
+ if (/\r(?!\n)/.test(raw)) {
18847
+ io.stdout(` SKIPPED ${report.path}: the file holds a carriage return with no line feed
18848
+ `);
18849
+ continue;
18850
+ }
18851
+ const lines = splitLines(raw);
18852
+ const python = report.language === "python";
18853
+ const insertions = [];
18854
+ for (const symbol of targets) {
18855
+ if (!python && !startsItsLine(symbol, lines)) {
18856
+ io.stdout(` cannot place a stub for ${report.path}:${symbol.anchorLine} ${symbol.name}: ` + `another token precedes the declaration on its line
18857
+ `);
18858
+ continue;
18859
+ }
18860
+ const insertion = (python ? planPython : planTypeScript)(symbol, lines);
18861
+ if (insertion !== null)
18862
+ insertions.push(insertion);
18863
+ }
18864
+ planned += insertions.length;
18865
+ if (!options.apply) {
18866
+ for (const ins of insertions) {
18867
+ io.stdout(` would document ${report.path}:${ins.lineIndex + 1} ${ins.symbol}
18868
+ `);
18869
+ }
18870
+ continue;
18871
+ }
18872
+ const verify = report.language === "python" ? parsesAsPython : (text) => parsesAsTypeScript(report.path, text);
18873
+ const result = applyInsertions(raw, insertions, verify);
18874
+ if (result.ok)
18875
+ writeFileSync5(file, result.text);
18876
+ io.stdout(` ${result.ok ? "wrote" : "SKIPPED"} ${report.path}: ${result.message}
18877
+ `);
18878
+ if (result.ok)
18879
+ changed += 1;
18880
+ }
18881
+ const mode = options.apply ? "APPLIED" : "DRY RUN (pass --apply to write)";
18882
+ io.stdout(`${mode}: ${planned} stub(s) planned across ${reports.length} file(s); ${changed} file(s) modified
18883
+ `);
18884
+ if (planned > 0 && !options.apply) {
18885
+ io.stdout("Every stub carries a TODO: marker, which `check` FAILS on -- fill them in.\n");
18886
+ }
18887
+ return 0;
18888
+ }
18889
+ function requestedPaths(root, options) {
18890
+ let raw = options.paths;
18891
+ if (options.pathsFrom !== undefined) {
18892
+ let text;
18893
+ try {
18894
+ text = new TextDecoder("utf-8", { fatal: true }).decode(readFileSync23(options.pathsFrom));
18895
+ } catch {
18896
+ throw new Error(`cannot read the paths file ${basename9(options.pathsFrom)} as UTF-8 text`);
18897
+ }
18898
+ raw = text.split(/\r\n|\r|\n/).map((l) => l.trim()).filter((l) => l !== "").map(unquoteGitPath);
18899
+ }
18900
+ return raw?.map((p) => relativeToRoot(root, p));
18901
+ }
18902
+ var pyList = (items) => `[${items.sort().map((s) => `'${s}'`).join(", ")}]`;
18903
+ async function runCheck(root, options, io) {
18904
+ const scope = requestedPaths(root, options);
18905
+ const { reports: all, provenance } = await analyseRepo(root);
18906
+ if (all.length === 0 && scope === undefined)
18907
+ throw new NothingToMeasure(provenance);
18908
+ const stats = summarise(all);
18909
+ const wanted = scope === undefined ? undefined : new Set(scope);
18910
+ const reports = wanted === undefined ? all : all.filter((r) => wanted.has(r.path));
18911
+ const failures = [];
18912
+ for (const report of reports) {
18913
+ if (!isParsed(report)) {
18914
+ failures.push(`${report.path}: UNPARSED -- ${report.error}`);
18915
+ continue;
18916
+ }
18917
+ if (report.dialects.size > 1) {
18918
+ failures.push(`${report.path}: M5 mixed docstring dialects ${pyList([...report.dialects])}`);
18919
+ }
18920
+ for (const sym of report.symbols) {
18921
+ for (const issue of sym.issues) {
18922
+ if (issue.tier === "MUST") {
18923
+ failures.push(`${sym.file}:${sym.line} ${sym.name}: ${issue.rule} ${issue.detail}`);
18924
+ }
18925
+ }
18926
+ }
18927
+ }
18928
+ if (wanted === undefined) {
18929
+ io.stdout(`checked ${stats.filesScanned} file(s) via ${provenance}
18930
+ `);
18931
+ } else {
18932
+ io.stdout(`checked ${reports.length} changed source file(s) of ${stats.filesScanned} tracked (${provenance})
18933
+ `);
18934
+ const known = new Set(all.map((r) => r.path));
18935
+ const missed = [...wanted].filter((p) => languageOf(p) !== "" && !known.has(p));
18936
+ if (missed.length > 0) {
18937
+ io.stdout(`note: ${missed.length} requested source path(s) were not measured (not tracked, excluded, ` + `deleted, or in a skipped folder): ${missed.slice(0, 10).join(", ")}${missed.length > 10 ? ", ..." : ""}
18938
+ `);
18939
+ }
18940
+ }
18941
+ if (failures.length === 0) {
18942
+ if (wanted === undefined) {
18943
+ io.stdout(`PASS -- ${stats.exportedDocumented}/${stats.exportedSymbols} exported symbols documented, 0 MUST issues
18944
+ `);
18945
+ if (stats.shouldIssues > 0) {
18946
+ io.stdout(` (${stats.shouldIssues} SHOULD issue(s) reported, not gated)
18947
+ `);
18948
+ }
18949
+ } else {
18950
+ io.stdout(`PASS -- 0 MUST issue(s) in the changed files
18951
+ `);
18952
+ }
18953
+ return 0;
18954
+ }
18955
+ io.stdout(`FAIL -- ${failures.length} MUST issue(s):
18956
+ `);
18957
+ for (const line of failures.slice(0, 100))
18958
+ io.stdout(` ${line}
18959
+ `);
18960
+ if (failures.length > 100)
18961
+ io.stdout(` ... and ${failures.length - 100} more (TRUNCATED)
18962
+ `);
18963
+ return 1;
18964
+ }
18965
+ async function run5(argv, io) {
18966
+ if (wantsHelp(argv)) {
18967
+ io.stdout(DOCS_HELP);
18968
+ return 0;
18969
+ }
18970
+ let root;
18971
+ const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
18972
+ try {
18973
+ const options = parseDocsArgs(argv);
18974
+ root = resolve8(options.root);
18975
+ if (!isDirectory(root))
18976
+ throw new Error("the root <root> is not an existing directory");
18977
+ if (options.action === "scan")
18978
+ return await runScan(root, options, io);
18979
+ if (options.action === "stub")
18980
+ return await runStub(root, options, io);
18981
+ return await runCheck(root, options, io);
18982
+ } catch (error) {
18983
+ const message = error instanceof Error ? error.message : String(error);
18984
+ stderr(`repo-tools docs: ${message}
18985
+ `);
18986
+ return error instanceof NothingToMeasure ? 1 : 2;
18987
+ }
18988
+ }
18989
+
18990
+ // src/query/index.ts
18991
+ import { existsSync as existsSync12, statSync as statSync10 } from "node:fs";
18992
+ import { resolve as resolve9 } from "node:path";
18993
+
18994
+ // src/query/args.ts
18995
+ var MODE_FLAGS = {
18996
+ "--emit": { name: "emit" },
18997
+ "--check-browser-safety": { name: "check-browser-safety" }
18998
+ };
18999
+ var VALUE_FLAGS3 = new Set(["--root", "--config", "--out", "--node-runtime"]);
19000
+ var COMMAND_USAGE = {
19001
+ dependents: "<file>",
19002
+ "symbol-users": "<symbol>",
19003
+ "is-public": "<pkg> <symbol>",
19004
+ "node-safety": "[pkg]",
19005
+ cycles: ""
19006
+ };
19007
+ function makeCommand(name2, args2) {
19008
+ const usage = COMMAND_USAGE[name2];
19009
+ if (usage === undefined) {
19010
+ throw new Error(`unknown command '${name2}' (see repo-tools query --help)`);
19011
+ }
19012
+ const [a, b] = args2;
19013
+ const need = (count) => {
19014
+ if (args2.length < count)
19015
+ throw new Error(`${name2} needs ${usage}`);
19016
+ };
19017
+ const atMost = (count) => {
19018
+ if (args2.length > count) {
19019
+ throw new Error(count === 0 ? `${name2} takes no more arguments` : `${name2} takes ${usage}, not more arguments`);
19020
+ }
19021
+ };
19022
+ switch (name2) {
19023
+ case "dependents":
19024
+ need(1);
19025
+ atMost(1);
19026
+ return { name: name2, file: a };
19027
+ case "symbol-users":
19028
+ need(1);
19029
+ atMost(1);
19030
+ return { name: name2, symbol: a };
19031
+ case "is-public":
19032
+ need(2);
19033
+ atMost(2);
19034
+ return { name: name2, pkg: a, symbol: b };
19035
+ case "node-safety":
19036
+ atMost(1);
19037
+ return a === undefined ? { name: name2 } : { name: name2, pkg: a };
19038
+ default:
19039
+ atMost(0);
19040
+ return { name: "cycles" };
19041
+ }
19042
+ }
19043
+ function list2(flag, value) {
19044
+ const items = value.split(",");
19045
+ if (items.some((item) => item === ""))
19046
+ throw new Error(`flag ${flag} holds an empty item`);
19047
+ return items;
19048
+ }
19049
+ function parseQueryArgs(argv, cwd) {
19050
+ let root;
19051
+ let config;
19052
+ let out2;
19053
+ let nodeRuntimes;
19054
+ let mode;
19055
+ let components = false;
19056
+ const words = [];
19057
+ for (const arg of argv) {
19058
+ if (!arg.startsWith("-")) {
19059
+ words.push(arg);
19060
+ continue;
19061
+ }
19062
+ const eq = arg.indexOf("=");
19063
+ const name2 = eq === -1 ? arg : arg.slice(0, eq);
19064
+ const value = eq === -1 ? undefined : arg.slice(eq + 1);
19065
+ if (name2 === "--components") {
19066
+ if (value !== undefined)
19067
+ throw new Error("flag --components takes no value");
19068
+ components = true;
19069
+ continue;
19070
+ }
19071
+ const modeCommand = MODE_FLAGS[name2];
19072
+ if (modeCommand !== undefined) {
19073
+ if (value !== undefined)
19074
+ throw new Error(`flag ${name2} takes no value`);
19075
+ if (mode !== undefined)
19076
+ throw new Error("use one command or mode in a run");
19077
+ mode = modeCommand;
19078
+ continue;
19079
+ }
19080
+ if (!VALUE_FLAGS3.has(name2)) {
19081
+ throw new Error(`unknown flag '${name2}' (see repo-tools query --help)`);
19082
+ }
19083
+ if (value === undefined || value === "") {
19084
+ throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
19085
+ }
19086
+ if (name2 === "--root") {
19087
+ if (root !== undefined)
19088
+ throw new Error("the root is set twice");
19089
+ root = value;
19090
+ } else if (name2 === "--config") {
19091
+ if (isAbsolutePath(value))
19092
+ throw new Error(`flag --config ${ABSOLUTE_PATH_TEXT}`);
19093
+ config = value;
19094
+ } else if (name2 === "--out") {
19095
+ if (isAbsolutePath(value))
19096
+ throw new Error(`flag --out ${ABSOLUTE_PATH_TEXT}`);
19097
+ out2 = value;
19098
+ } else {
19099
+ nodeRuntimes = list2(name2, value);
19100
+ }
19101
+ }
19102
+ const [first, ...rest] = words;
19103
+ let command;
19104
+ if (mode !== undefined) {
19105
+ if (first !== undefined) {
19106
+ if (COMMAND_USAGE[first] !== undefined)
19107
+ throw new Error("use one command or mode in a run");
19108
+ throw new Error(`unknown command '${first}' (see repo-tools query --help)`);
19109
+ }
19110
+ command = mode;
19111
+ } else {
19112
+ if (first === undefined) {
19113
+ throw new Error("a command is missing (see repo-tools query --help)");
19114
+ }
19115
+ command = makeCommand(first, rest);
19116
+ }
19117
+ if (components) {
19118
+ if (command.name !== "cycles")
19119
+ throw new Error("flag --components applies to cycles only");
19120
+ command = { name: "cycles", components: true };
19121
+ }
19122
+ return {
19123
+ command,
19124
+ root: root ?? cwd,
19125
+ ...config === undefined ? {} : { config },
19126
+ ...out2 === undefined ? {} : { out: out2 },
19127
+ ...nodeRuntimes === undefined ? {} : { nodeRuntimes }
19128
+ };
19129
+ }
19130
+
19131
+ // src/map/query.ts
19132
+ import { readFileSync as readFileSync24 } from "node:fs";
19133
+ var SUPPORTED_SCHEMA_MAJOR = Number(SCHEMA_VERSION.split(".")[0]);
19134
+ function loadGraph(graphPath) {
19135
+ const data = JSON.parse(readFileSync24(graphPath, "utf8"));
19136
+ if (data === null || typeof data !== "object" || !("modules" in data)) {
19137
+ throw new Error(`${graphPath}: no 'modules' key found -- this is not a map dependency-graph.json, or it is ` + "from an incompatible schema. Cannot answer any query against it.");
19138
+ }
19139
+ const warnings = [];
19140
+ const version = data.metadata?.schemaVersion;
19141
+ if (version === undefined || version === null) {
19142
+ warnings.push(`${graphPath}: metadata.schemaVersion is missing -- proceeding as if it matches this ` + `module's schema (${SCHEMA_VERSION}), but an older or newer artifact whose shape has ` + "actually changed may produce a silently wrong answer.");
19143
+ } else if (Number(String(version).split(".")[0]) !== SUPPORTED_SCHEMA_MAJOR) {
19144
+ throw new Error(`${graphPath}: schemaVersion '${String(version)}' is not compatible with this module's ` + `supported major version ${SUPPORTED_SCHEMA_MAJOR} (SCHEMA_VERSION='${SCHEMA_VERSION}'). ` + "Refusing to query a schema this module was not written against rather than risk a " + "silently wrong answer.");
19145
+ }
19146
+ return { data, warnings };
19147
+ }
19148
+ function entries2(data) {
19149
+ if (!data.modules) {
19150
+ throw new Error("graph data has no 'modules' key -- cannot answer; pass data returned by loadGraph(), or " + "a document with the same shape");
19151
+ }
19152
+ return Object.values(data.modules).flatMap((area) => Object.entries(area));
19153
+ }
19154
+ var sorted = (items) => items.sort(compareCodeUnits);
19155
+ function dependents(data, file) {
19156
+ const all = entries2(data);
19157
+ if (!all.some(([path]) => path === file)) {
19158
+ throw new Error(`'${file}' is not a file in this graph -- cannot answer 'dependents' for an unknown path. ` + "Check the path is repo-relative POSIX and this is the graph for the right repo/checkout. " + "(A file that IS known but genuinely has no importers returns [] normally, not an error.)");
19159
+ }
19160
+ return sorted(all.filter(([, entry]) => (entry.internalDependencies ?? []).some((d) => d.file === file)).map(([path]) => path));
19161
+ }
19162
+ function symbolUsers(data, symbol) {
19163
+ return sorted(entries2(data).filter(([, entry]) => (entry.internalDependencies ?? []).some((d) => (d.imports ?? []).includes(symbol))).map(([path]) => path));
19164
+ }
19165
+ function cycles(data, limits = {}) {
19166
+ const all = entries2(data);
19167
+ const known = new Set(all.map(([path]) => path));
19168
+ const edges = new Map;
19169
+ for (const [path, entry] of all) {
19170
+ const targets = (entry.internalDependencies ?? []).map((d) => d.file).filter((f) => f !== undefined && known.has(f));
19171
+ edges.set(path, [...new Set(targets)]);
19172
+ }
19173
+ const result = simpleCycles(edges, limits);
19174
+ if (result.truncated) {
19175
+ result.warning = `cycles: simple-cycle enumeration hit its safety cap (found ${result.cycles.length} ` + `cycles / ${result.steps} backtracking steps) and stopped early -- the returned list is a ` + "FLOOR (at least this many simple cycles exist), not an exact total.";
19176
+ }
19177
+ return result;
19178
+ }
19179
+
19180
+ // src/query/graph.ts
19181
+ function fileEntriesOf(graph) {
19182
+ const pairs = [];
19183
+ for (const files of Object.values(graph.modules)) {
19184
+ for (const [file, entry] of Object.entries(files))
19185
+ pairs.push([file, entry]);
19186
+ }
19187
+ return pairs;
19188
+ }
19189
+ function buildForward(fileEntries, allFiles) {
19190
+ const forward = new Map;
19191
+ for (const [file, entry] of fileEntries) {
19192
+ const targets = new Set;
19193
+ for (const dep of entry.internalDependencies ?? []) {
19194
+ if (dep.file !== undefined && allFiles.has(dep.file))
19195
+ targets.add(dep.file);
19196
+ }
19197
+ forward.set(file, targets);
19198
+ }
19199
+ return forward;
19200
+ }
19201
+ function invert(forward) {
19202
+ const reverse = new Map;
19203
+ for (const [file, targets] of forward) {
19204
+ for (const target of targets) {
19205
+ const importers = reverse.get(target) ?? new Set;
19206
+ importers.add(file);
19207
+ reverse.set(target, importers);
19208
+ }
19209
+ }
19210
+ const out2 = {};
19211
+ for (const target of sortCodeUnits([...reverse.keys()])) {
19212
+ out2[target] = sortCodeUnits([...reverse.get(target) ?? []]);
19213
+ }
19214
+ return out2;
19215
+ }
19216
+
19217
+ // src/query/load.ts
19218
+ import { existsSync as existsSync11, readFileSync as readFileSync25 } from "node:fs";
19219
+ var RUN_MAP = "run repo-tools map first";
19220
+ function reportRel(out2, name2) {
19221
+ return `${out2.replace(/\\/g, "/").replace(/\/+$/, "")}/${name2}`;
17689
19222
  }
17690
19223
  function loadQueryInput(root, out2) {
17691
19224
  const rel = reportRel(out2, "dependency-graph.json");
17692
19225
  const shown = `the dependency graph <root>/${rel}`;
17693
19226
  const path = resolveUnderRoot(root, rel);
17694
- if (!existsSync10(path))
19227
+ if (!existsSync11(path))
17695
19228
  throw new Error(`${shown} does not exist; ${RUN_MAP}`);
17696
19229
  let loaded;
17697
19230
  try {
@@ -17728,13 +19261,13 @@ function loadSurfaces(input) {
17728
19261
  const rel = reportRel(input.out, "package-export-surfaces.json");
17729
19262
  const shown = `the export-surfaces report <root>/${rel}`;
17730
19263
  const path = resolveUnderRoot(input.root, rel);
17731
- if (!existsSync10(path)) {
19264
+ if (!existsSync11(path)) {
17732
19265
  const why = input.language === "csharp" ? " (map writes none for C#)" : `; ${RUN_MAP}`;
17733
19266
  throw new Error(`${shown} does not exist${why}`);
17734
19267
  }
17735
19268
  let parsed;
17736
19269
  try {
17737
- parsed = JSON.parse(readFileSync22(path, "utf8"));
19270
+ parsed = JSON.parse(readFileSync25(path, "utf8"));
17738
19271
  } catch {
17739
19272
  throw new Error(`${shown} is not valid JSON; ${RUN_MAP}`);
17740
19273
  }
@@ -18033,13 +19566,13 @@ report, a path that is not a file of the graph, a browser-safety command on a
18033
19566
  graph that is not TypeScript/JavaScript, or a browser-safety leak with
18034
19567
  --check-browser-safety.
18035
19568
  `;
18036
- async function run5(argv, io) {
19569
+ async function run6(argv, io) {
18037
19570
  let root;
18038
19571
  const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
18039
19572
  try {
18040
19573
  const options = parseQueryArgs(argv, process.cwd());
18041
- root = resolve8(options.root);
18042
- if (!(existsSync11(root) && statSync9(root).isDirectory())) {
19574
+ root = resolve9(options.root);
19575
+ if (!(existsSync12(root) && statSync10(root).isDirectory())) {
18043
19576
  throw new Error("the root <root> is not an existing directory");
18044
19577
  }
18045
19578
  const config = mergeQueryConfig({ out: options.out, nodeRuntimes: options.nodeRuntimes }, loadConfigSections(root, options.config));
@@ -18073,40 +19606,8 @@ async function run5(argv, io) {
18073
19606
  }
18074
19607
 
18075
19608
  // src/ste/index.ts
18076
- import { existsSync as existsSync12, lstatSync as lstatSync5, readdirSync as readdirSync7, readFileSync as readFileSync23, statSync as statSync10 } from "node:fs";
18077
- import { basename as basename9, join as join24 } from "node:path";
18078
-
18079
- // src/ste/rules.ts
18080
- var IRREGULAR_PARTICIPLES = "read|built|sent|held|kept|made|found|put|set|run|lost|left|met|paid|told" + "|thrown|drawn|given|taken|shown|known|grown|blown" + "|cut|meant|dealt|felt|sold|said|spent|split|shut|hit|let|bred|fed|led";
18081
- var DETERMINERS = "the|a|an|its|their|this|that|our|your|each|every" + "|his|her|my|these|those";
18082
- var PASSIVE_SOURCE = `${B}(?:${ci("is|are|was|were|be|been|being")})${S}+` + `(?:${W}+(?:${ci("ed|en")})|(?:${ci(IRREGULAR_PARTICIPLES)}))${S}+` + `by${S}+(?:(?:${ci(DETERMINERS)})${B}|[A-Z\`])`;
18083
- var AMBIGUOUS_VERBS = "is|are|was|were|be|will|can|may|must|does|do|has|have|should|shall" + "|disables?|enables?|causes?|returns?|means?|allows?|prevents?|makes?" + "|sets?|gets?|adds?|removes?|creates?|requires?|affects?|applies|happens?";
18084
- var AMBIGUOUS_SOURCE = `(?:^|(?<=[.!?,;:])${S})${S}*(?:(?:${ci("and|but|or|so|then|yet")})${S}+)?` + `(${ci("it|this|that|these|those")})${S}+(?:${AMBIGUOUS_VERBS})${B}`;
18085
- var WORDY = [
18086
- ["in order to", "to"],
18087
- ["at this point in time", "now"],
18088
- ["in the event that", "if"],
18089
- ["due to the fact that", "because"],
18090
- ["a number of", "some"],
18091
- ["utilise", "use"],
18092
- ["utilize", "use"],
18093
- ["prior to", "before"],
18094
- ["subsequent to", "after"],
18095
- ["in the case of", "for"],
18096
- ["in spite of the fact that", "although"],
18097
- ["for the purpose of", "to"],
18098
- ["has the ability to", "can"],
18099
- ["is able to", "can"]
18100
- ];
18101
- var IDENTIFIER_SOURCE = "`[^`]*`" + `|${B}${W}+\\(\\)` + `|${B}${W}*[a-z]${W}*[A-Z]${W}*${B}` + `|${B}${W}+_${W}+${B}` + `|${B}${W}+\\.${W}+${B}`;
18102
- var MAX_WORDS_PROCEDURAL = 20;
18103
- var MAX_WORDS_DESCRIPTIVE = 25;
18104
- function rule(source, flags2 = "") {
18105
- return new RegExp(source, `u${flags2}`);
18106
- }
18107
- function stripIdentifiers(text) {
18108
- return text.replace(rule(IDENTIFIER_SOURCE, "g"), " ");
18109
- }
19609
+ import { existsSync as existsSync13, lstatSync as lstatSync6, readdirSync as readdirSync5, readFileSync as readFileSync26, statSync as statSync11 } from "node:fs";
19610
+ import { basename as basename10, join as join28 } from "node:path";
18110
19611
 
18111
19612
  // src/ste/markdown.ts
18112
19613
  function stripNoise(text) {
@@ -18149,10 +19650,10 @@ function proseOnly(line) {
18149
19650
  }
18150
19651
  var EMPHASIS = /\*\*|__/gu;
18151
19652
  var DECIMAL2 = rule(`(${D})\\.(${D})`, "g");
18152
- var SENTENCE_END = rule(`(?<=[.!?])${S}+`);
19653
+ var SENTENCE_END2 = rule(`(?<=[.!?])${S}+`);
18153
19654
  function sentences(block) {
18154
19655
  const guarded = block.replace(EMPHASIS, "").replace(DECIMAL2, "$1<DOT>$2");
18155
- return guarded.split(SENTENCE_END).map((part) => part.replaceAll("<DOT>", "."));
19656
+ return guarded.split(SENTENCE_END2).map((part) => part.replaceAll("<DOT>", "."));
18156
19657
  }
18157
19658
  var CODE_SPAN = /`[^`]*`/gu;
18158
19659
  var EMPHASIS_CHARS = /[*_]/gu;
@@ -18161,7 +19662,7 @@ function wordCount(sentence) {
18161
19662
  const s = sentence.replace(CODE_SPAN, "X").replace(LINK, "$1").replace(EMPHASIS_CHARS, "").replace(LIST_MARKER, "");
18162
19663
  return pySplit(s).filter((w) => Array.from(w).some(isAlnum)).length;
18163
19664
  }
18164
- function join23(parts2, start2) {
19665
+ function join27(parts2, start2) {
18165
19666
  const chunks = [];
18166
19667
  const lineMap = [];
18167
19668
  parts2.forEach(([n, body2], i2) => {
@@ -18181,8 +19682,8 @@ function lineAt2(index, lineMap) {
18181
19682
  var LIST_ITEM = rule(`^${S}*(?:[-*+]|${D}+\\.)${S}`);
18182
19683
  var PROCEDURAL_STARTS = ["- [", "1.", "2.", "3.", "4.", "5.", "6."];
18183
19684
  var WORDY_RULES = WORDY.map(([phrase, better]) => [phrase, better, rule(`(?<![\\p{L}\\p{N}_-])${reEscape(phrase)}${B}`, "g")]);
18184
- var AMBIGUOUS = rule(AMBIGUOUS_SOURCE, "g");
18185
- var PASSIVE = rule(PASSIVE_SOURCE, "g");
19685
+ var AMBIGUOUS2 = rule(AMBIGUOUS_SOURCE, "g");
19686
+ var PASSIVE2 = rule(PASSIVE_SOURCE, "g");
18186
19687
  function paragraphs(text) {
18187
19688
  const blocks = [];
18188
19689
  let para = [];
@@ -18191,7 +19692,7 @@ function paragraphs(text) {
18191
19692
  for (const [n, line] of stripNoise(text)) {
18192
19693
  const startsItem = LIST_ITEM.test(line);
18193
19694
  if ((n !== prevLine + 1 || startsItem) && para.length > 0) {
18194
- blocks.push(join23(para, paraStart));
19695
+ blocks.push(join27(para, paraStart));
18195
19696
  para = [];
18196
19697
  }
18197
19698
  if (para.length === 0)
@@ -18200,7 +19701,7 @@ function paragraphs(text) {
18200
19701
  prevLine = n;
18201
19702
  }
18202
19703
  if (para.length > 0)
18203
- blocks.push(join23(para, paraStart));
19704
+ blocks.push(join27(para, paraStart));
18204
19705
  return blocks;
18205
19706
  }
18206
19707
  function checkMarkdown(name2, text) {
@@ -18224,75 +19725,16 @@ function checkMarkdown(name2, text) {
18224
19725
  problems.push(`${name2}:${lineAt2(m.index, lineMap)}: wordy: ${pyRepr(phrase)} - prefer ${pyRepr(better)}`);
18225
19726
  }
18226
19727
  }
18227
- for (const m of bare.matchAll(AMBIGUOUS)) {
19728
+ for (const m of bare.matchAll(AMBIGUOUS2)) {
18228
19729
  problems.push(`${name2}:${lineAt2(m.index, lineMap)}: ambiguous reference: ${pyRepr(pyStrip(m[0]))}`);
18229
19730
  }
18230
- for (const m of bare.matchAll(PASSIVE)) {
19731
+ for (const m of bare.matchAll(PASSIVE2)) {
18231
19732
  problems.push(`${name2}:${lineAt2(m.index, lineMap)}: passive voice: ${pyRepr(pyStrip(m[0]))}`);
18232
19733
  }
18233
19734
  }
18234
19735
  return problems;
18235
19736
  }
18236
19737
 
18237
- // src/ste/prose.ts
18238
- var PASSIVE2 = rule(PASSIVE_SOURCE);
18239
- var AMBIGUOUS2 = rule(AMBIGUOUS_SOURCE);
18240
- var SENTENCE_END2 = rule(`(?<=[.!?])${S}+`);
18241
- function sentencesOf(text) {
18242
- const collapsed = pySplit(text).join(" ");
18243
- return collapsed.split(SENTENCE_END2).map(pyStrip).filter((s) => s !== "");
18244
- }
18245
- function clip(text, limit = 60) {
18246
- return codePointLength(text) <= limit ? text : `${headCodePoints(text, limit - 1)}…`;
18247
- }
18248
- function checkProse(text) {
18249
- const findings = [];
18250
- for (const sentence of sentencesOf(text)) {
18251
- const words = pySplit(sentence);
18252
- if (words.length > MAX_WORDS_PROCEDURAL) {
18253
- findings.push([
18254
- "STE-LEN",
18255
- `sentence has ${words.length} words (limit ${MAX_WORDS_PROCEDURAL}): '${clip(sentence)}'`
18256
- ]);
18257
- }
18258
- const passive = PASSIVE2.exec(sentence);
18259
- if (passive) {
18260
- findings.push(["STE-VOICE", `possible passive voice '${passive[0]}' -- prefer active`]);
18261
- }
18262
- const ambiguous = AMBIGUOUS2.exec(sentence);
18263
- if (ambiguous) {
18264
- findings.push(["STE-REF", `ambiguous '${ambiguous[1]}' -- name the thing it refers to`]);
18265
- }
18266
- }
18267
- const prose = stripIdentifiers(text);
18268
- for (const [phrase, better] of WORDY) {
18269
- if (rule(`${B}${ci(reEscape(phrase))}${B}`).test(prose)) {
18270
- findings.push(["STE-WORD", `'${phrase}' -- prefer '${better}'`]);
18271
- }
18272
- }
18273
- return findings;
18274
- }
18275
- var SECTION_HEADER = /^(Args|Arguments|Returns|Raises|Yields|Examples|Attributes|Parameters|Notes?|See Also):?$/u;
18276
- function proseOf(doc) {
18277
- const out2 = [];
18278
- let inFence = false;
18279
- for (const raw of pySplitlines(doc)) {
18280
- const line = pyStrip(pyStrip(raw).replace(/^\*+/, ""));
18281
- if (line.startsWith("```")) {
18282
- inFence = !inFence;
18283
- continue;
18284
- }
18285
- if (inFence || line.startsWith("@") || line.startsWith(">>>"))
18286
- continue;
18287
- if (SECTION_HEADER.test(line))
18288
- continue;
18289
- if (/^-{3,}$/u.test(line))
18290
- continue;
18291
- out2.push(line);
18292
- }
18293
- return out2.filter((p) => p !== "").join(" ");
18294
- }
18295
-
18296
19738
  // src/ste/index.ts
18297
19739
  var STE_HELP = `Usage: repo-tools ste <file-or-directory> [...]
18298
19740
  repo-tools ste --prose <file>
@@ -18321,7 +19763,7 @@ or when a file is not UTF-8.
18321
19763
  `;
18322
19764
  var HISTORICAL_MARKER = "<!-- ste:historical-record -->";
18323
19765
  function readText(path) {
18324
- const bytes = readFileSync23(path);
19766
+ const bytes = readFileSync26(path);
18325
19767
  const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
18326
19768
  return text.replace(/\r\n?/g, `
18327
19769
  `);
@@ -18335,36 +19777,36 @@ function comparePaths(a, b) {
18335
19777
  return a.length - b.length;
18336
19778
  }
18337
19779
  function targetsFor(path) {
18338
- if (existsSync12(path) && statSync10(path).isFile())
19780
+ if (existsSync13(path) && statSync11(path).isFile())
18339
19781
  return [path];
18340
19782
  const found = [];
18341
19783
  const walk = (dir, parts2) => {
18342
19784
  let entries;
18343
19785
  try {
18344
- entries = readdirSync7(dir, { withFileTypes: true });
19786
+ entries = readdirSync5(dir, { withFileTypes: true });
18345
19787
  } catch {
18346
19788
  return;
18347
19789
  }
18348
19790
  for (const entry of entries) {
18349
- const full = join24(dir, entry.name);
19791
+ const full = join28(dir, entry.name);
18350
19792
  if (entry.isDirectory()) {
18351
19793
  if (entry.name !== ".pytest_cache")
18352
19794
  walk(full, [...parts2, entry.name]);
18353
19795
  } else if (entry.name.endsWith(".md")) {
18354
- const isFile = entry.isFile() || entry.isSymbolicLink() && statSync10(full).isFile();
19796
+ const isFile = entry.isFile() || entry.isSymbolicLink() && statSync11(full).isFile();
18355
19797
  if (isFile)
18356
19798
  found.push([...parts2, entry.name]);
18357
19799
  }
18358
19800
  }
18359
19801
  };
18360
- if (existsSync12(path) && lstatSync5(path).isDirectory())
19802
+ if (existsSync13(path) && lstatSync6(path).isDirectory())
18361
19803
  walk(path, []);
18362
- return found.sort(comparePaths).map((parts2) => join24(path, ...parts2));
19804
+ return found.sort(comparePaths).map((parts2) => join28(path, ...parts2));
18363
19805
  }
18364
19806
  function isHistoricalRecord(path) {
18365
19807
  let text;
18366
19808
  try {
18367
- text = new TextDecoder("utf-8").decode(readFileSync23(path));
19809
+ text = new TextDecoder("utf-8").decode(readFileSync26(path));
18368
19810
  } catch {
18369
19811
  return false;
18370
19812
  }
@@ -18381,7 +19823,7 @@ function runProse(args2, io) {
18381
19823
  try {
18382
19824
  text = readText(file);
18383
19825
  } catch {
18384
- io.stderr(`repo-tools ste: cannot read ${basename9(file)} as UTF-8 text
19826
+ io.stderr(`repo-tools ste: cannot read ${basename10(file)} as UTF-8 text
18385
19827
  `);
18386
19828
  return 2;
18387
19829
  }
@@ -18390,7 +19832,7 @@ function runProse(args2, io) {
18390
19832
  `);
18391
19833
  return 0;
18392
19834
  }
18393
- async function run6(argv, io) {
19835
+ async function run7(argv, io) {
18394
19836
  if (argv[0] === "--prose")
18395
19837
  return runProse(argv.slice(1), io);
18396
19838
  if (argv.length === 0) {
@@ -18403,7 +19845,7 @@ async function run6(argv, io) {
18403
19845
  let skipped = 0;
18404
19846
  for (const arg of argv) {
18405
19847
  for (const target of targetsFor(arg)) {
18406
- const name2 = basename9(target);
19848
+ const name2 = basename10(target);
18407
19849
  if (isHistoricalRecord(target)) {
18408
19850
  skipped += 1;
18409
19851
  io.stdout(`
@@ -18462,6 +19904,11 @@ ${MAP_HELP}`,
18462
19904
  help: CHECK_HELP,
18463
19905
  run: run2
18464
19906
  },
19907
+ docs: {
19908
+ summary: "Measure, stub and gate the doc comments of TypeScript, JavaScript and Python source.",
19909
+ help: DOCS_HELP,
19910
+ run: run5
19911
+ },
18465
19912
  chunk: {
18466
19913
  summary: "Split a large file into chunks, merge the chunks back, or show changed chunks.",
18467
19914
  help: CHUNK_HELP,
@@ -18475,12 +19922,12 @@ ${MAP_HELP}`,
18475
19922
  query: {
18476
19923
  summary: "Answer structural questions from the depgraph reports, or write derived reports.",
18477
19924
  help: QUERY_HELP,
18478
- run: run5
19925
+ run: run6
18479
19926
  },
18480
19927
  ste: {
18481
19928
  summary: "Check Markdown or docstring prose against Simplified Technical English.",
18482
19929
  help: STE_HELP,
18483
- run: run6
19930
+ run: run7
18484
19931
  }
18485
19932
  };
18486
19933
  var SUBCOMMANDS = Object.keys(REGISTRY);