@danielsimonjr/repo-tools 2.1.0 → 2.2.1

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