@danielsimonjr/repo-tools 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +5 -4
- package/dist/cli.js +472 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,42 @@ All notable changes to this project are recorded in this file. The format follow
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.1.0] - 2026-09-25
|
|
10
|
+
|
|
11
|
+
The STE release. `repo-tools ste` checks Markdown, and docstring prose with `--prose`, against the
|
|
12
|
+
mechanically decidable part of ASD-STE100. It replaces the Python checker of the architecture-docs
|
|
13
|
+
skill and the docstring harness of the code-docs skill. The port gives the same 362 findings as
|
|
14
|
+
`ste_check.py` on 111 Markdown files. `ste` runs on Node 20, 22 and 24. A static test keeps the
|
|
15
|
+
tool offline.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Tests: `tests/unit/node-compat.test.ts` keeps regex modifier groups (`(?i:...)`) out of `src/`.
|
|
20
|
+
Node 20 and Node 22 cannot compile one. A rule regex compiles when its module loads. Thus one such
|
|
21
|
+
group stopped every subcommand of the Node bundle, although Bun and Node 24 ran it. The STE
|
|
22
|
+
rules now spell each case-insensitive word as letter classes (`ci`). The classes also match the
|
|
23
|
+
letters that Python's case-insensitive match accepts (U+0131 and U+0130 for `i`, U+017F for
|
|
24
|
+
`s`, U+212A for `k`). The STE output on Node 20 is byte-identical to the output on Bun.
|
|
25
|
+
- `repo-tools ste`: checks Markdown against the mechanically decidable part of ASD-STE100
|
|
26
|
+
(sentence length in two tiers, wordy forms, ambiguous references, the passive voice with an
|
|
27
|
+
agent). The subcommand is a port of `ste_check.py` and `ste_rules.py` from the architecture-docs
|
|
28
|
+
skill.
|
|
29
|
+
`repo-tools ste --prose <file>` is the docstring harness of the code-docs skill, over the same
|
|
30
|
+
one rule module. `docs/design.md` has a table of every behavioral difference between the two
|
|
31
|
+
harnesses; a run of both on the same text proves each row. The port and `ste_check.py` give the
|
|
32
|
+
same 362 findings, byte for byte, on 111 Markdown files. The corpus holds the architecture docs
|
|
33
|
+
of memoryjs, Mathts and universal-physics-tensor, the repo-tools docs, and 17 files of a
|
|
34
|
+
private repository. Only the file order differs: the port sorts in code-unit order on every
|
|
35
|
+
operating system.
|
|
36
|
+
- `docs/design.md`: 8 STE findings fixed (the new checker found them); the document is STE-clean.
|
|
37
|
+
- Tests: `tests/unit/offline.test.ts` keeps the tool offline. It fails when a file in `src/`
|
|
38
|
+
imports a network module (`http`, `https`, `http2`, `net`, `dgram`, `tls`, `dns`, `undici`,
|
|
39
|
+
`ws`, with or without `node:`) or uses a network global (`fetch`, `WebSocket`,
|
|
40
|
+
`XMLHttpRequest`, `EventSource`, `Bun.connect`, `Bun.listen`, `Bun.serve`, `Bun.udpSocket`,
|
|
41
|
+
`navigator.sendBeacon`). repo-tools reads private repositories, so it must never send their
|
|
42
|
+
content. 18 positive controls prove that the scan finds each form, and a planted `fetch` in
|
|
43
|
+
`src/` fails the test.
|
|
44
|
+
|
|
9
45
|
## [1.0.0] - 2026-09-24
|
|
10
46
|
|
|
11
47
|
The first release. One command, `repo-tools`, replaces the copies of the repository tools that
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# repo-tools
|
|
2
2
|
|
|
3
|
-
`repo-tools` is one command-line tool with
|
|
3
|
+
`repo-tools` is one command-line tool with five subcommands for repositories:
|
|
4
4
|
|
|
5
5
|
| Subcommand | Job |
|
|
6
6
|
|---|---|
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
| `chunk` | Splits a large file into chunks, merges the chunks back, and shows which chunks changed. |
|
|
9
9
|
| `compress` | Writes a compact copy of a file for a model context, and restores it. |
|
|
10
10
|
| `query` | Answers structural questions from the depgraph reports, and writes two derived reports. |
|
|
11
|
+
| `ste` | Checks Markdown or docstring prose against Simplified Technical English. |
|
|
11
12
|
|
|
12
|
-
Status: version 1.
|
|
13
|
+
Status: version 1.1.0.
|
|
13
14
|
|
|
14
|
-
- All
|
|
15
|
+
- All five subcommands work.
|
|
15
16
|
- `depgraph` and `query` read their settings from `repo-tools.config.json` in the root.
|
|
16
17
|
`--config=<file>` makes either subcommand read another file.
|
|
17
18
|
- `compress -d` restores JSON files only.
|
|
@@ -33,7 +34,7 @@ repo-tools --version
|
|
|
33
34
|
|
|
34
35
|
The executable holds the tool and the Bun runtime. It needs no Bun, Node or `node_modules`.
|
|
35
36
|
Download the file for your platform and `SHA256SUMS` from the
|
|
36
|
-
[GitHub release](https://github.com/danielsimonjr/repo-tools/releases/tag/v1.
|
|
37
|
+
[GitHub release](https://github.com/danielsimonjr/repo-tools/releases/tag/v1.1.0):
|
|
37
38
|
|
|
38
39
|
| Platform | File |
|
|
39
40
|
|---|---|
|
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: "1.
|
|
5
|
+
version: "1.1.0",
|
|
6
6
|
description: "One CLI for repository analysis: dependency graphs creation and query with per-export API facts, file chunking, and context compression. TypeScript on Bun, compiled to a single executable.",
|
|
7
7
|
type: "module",
|
|
8
8
|
license: "MIT",
|
|
@@ -10971,6 +10971,472 @@ async function run4(argv, io) {
|
|
|
10971
10971
|
}
|
|
10972
10972
|
}
|
|
10973
10973
|
|
|
10974
|
+
// src/ste/index.ts
|
|
10975
|
+
import { existsSync as existsSync14, lstatSync as lstatSync3, readdirSync as readdirSync3, readFileSync as readFileSync14, statSync as statSync7 } from "node:fs";
|
|
10976
|
+
import { basename as basename6, join as join15 } from "node:path";
|
|
10977
|
+
|
|
10978
|
+
// src/ste/py.ts
|
|
10979
|
+
var SPACE_BODY = "\\t\\n\\v\\f\\r\\x1c-\\x1f \\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000";
|
|
10980
|
+
var S = `[${SPACE_BODY}]`;
|
|
10981
|
+
var W = "[\\p{L}\\p{N}_]";
|
|
10982
|
+
var B = `(?:(?<=${W})(?!${W})|(?<!${W})(?=${W}))`;
|
|
10983
|
+
var D = "\\p{Nd}";
|
|
10984
|
+
var EXTRA_CASES = {
|
|
10985
|
+
i: String.fromCharCode(305, 304),
|
|
10986
|
+
s: String.fromCharCode(383),
|
|
10987
|
+
k: String.fromCharCode(8490)
|
|
10988
|
+
};
|
|
10989
|
+
function ci(pattern) {
|
|
10990
|
+
let out = "";
|
|
10991
|
+
for (const ch of pattern) {
|
|
10992
|
+
const lower = ch.toLowerCase();
|
|
10993
|
+
if (lower >= "a" && lower <= "z" && lower.length === 1) {
|
|
10994
|
+
out += `[${lower}${lower.toUpperCase()}${EXTRA_CASES[lower] ?? ""}]`;
|
|
10995
|
+
} else
|
|
10996
|
+
out += ch;
|
|
10997
|
+
}
|
|
10998
|
+
return out;
|
|
10999
|
+
}
|
|
11000
|
+
var SPACE_RUN = new RegExp(`${S}+`, "u");
|
|
11001
|
+
var LEADING_SPACE = new RegExp(`^${S}+`, "u");
|
|
11002
|
+
var TRAILING_SPACE = new RegExp(`${S}+$`, "u");
|
|
11003
|
+
function pyStrip(text) {
|
|
11004
|
+
return text.replace(LEADING_SPACE, "").replace(TRAILING_SPACE, "");
|
|
11005
|
+
}
|
|
11006
|
+
function pyLstrip(text) {
|
|
11007
|
+
return text.replace(LEADING_SPACE, "");
|
|
11008
|
+
}
|
|
11009
|
+
function pySplit(text) {
|
|
11010
|
+
return text.split(SPACE_RUN).filter((part) => part !== "");
|
|
11011
|
+
}
|
|
11012
|
+
var LINE_ENDS = new Set([10, 11, 12, 13, 28, 29, 30, 133, 8232, 8233]);
|
|
11013
|
+
var CR = 13;
|
|
11014
|
+
var LF = 10;
|
|
11015
|
+
function pySplitlines(text) {
|
|
11016
|
+
const lines = [];
|
|
11017
|
+
let start = 0;
|
|
11018
|
+
for (let i = 0;i < text.length; i++) {
|
|
11019
|
+
const c = text.charCodeAt(i);
|
|
11020
|
+
if (!LINE_ENDS.has(c))
|
|
11021
|
+
continue;
|
|
11022
|
+
lines.push(text.slice(start, i));
|
|
11023
|
+
if (c === CR && text.charCodeAt(i + 1) === LF)
|
|
11024
|
+
i += 1;
|
|
11025
|
+
start = i + 1;
|
|
11026
|
+
}
|
|
11027
|
+
if (start < text.length)
|
|
11028
|
+
lines.push(text.slice(start));
|
|
11029
|
+
return lines;
|
|
11030
|
+
}
|
|
11031
|
+
function isAlnum(ch) {
|
|
11032
|
+
return /^[\p{L}\p{N}]$/u.test(ch);
|
|
11033
|
+
}
|
|
11034
|
+
function headCodePoints(text, n) {
|
|
11035
|
+
return Array.from(text).slice(0, n).join("");
|
|
11036
|
+
}
|
|
11037
|
+
function codePointLength(text) {
|
|
11038
|
+
return Array.from(text).length;
|
|
11039
|
+
}
|
|
11040
|
+
var NOT_PRINTABLE = /^[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Cs}\p{Zl}\p{Zp}\p{Zs}]$/u;
|
|
11041
|
+
function pyRepr(text) {
|
|
11042
|
+
const quote = text.includes("'") && !text.includes('"') ? '"' : "'";
|
|
11043
|
+
let out = quote;
|
|
11044
|
+
for (const ch of text) {
|
|
11045
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
11046
|
+
if (ch === "\\")
|
|
11047
|
+
out += "\\\\";
|
|
11048
|
+
else if (ch === quote)
|
|
11049
|
+
out += `\\${quote}`;
|
|
11050
|
+
else if (ch === `
|
|
11051
|
+
`)
|
|
11052
|
+
out += "\\n";
|
|
11053
|
+
else if (ch === "\r")
|
|
11054
|
+
out += "\\r";
|
|
11055
|
+
else if (ch === "\t")
|
|
11056
|
+
out += "\\t";
|
|
11057
|
+
else if (ch !== " " && NOT_PRINTABLE.test(ch)) {
|
|
11058
|
+
if (cp < 256)
|
|
11059
|
+
out += `\\x${cp.toString(16).padStart(2, "0")}`;
|
|
11060
|
+
else if (cp < 65536)
|
|
11061
|
+
out += `\\u${cp.toString(16).padStart(4, "0")}`;
|
|
11062
|
+
else
|
|
11063
|
+
out += `\\U${cp.toString(16).padStart(8, "0")}`;
|
|
11064
|
+
} else
|
|
11065
|
+
out += ch;
|
|
11066
|
+
}
|
|
11067
|
+
return out + quote;
|
|
11068
|
+
}
|
|
11069
|
+
function reEscape(text) {
|
|
11070
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11071
|
+
}
|
|
11072
|
+
|
|
11073
|
+
// src/ste/rules.ts
|
|
11074
|
+
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";
|
|
11075
|
+
var DETERMINERS = "the|a|an|its|their|this|that|our|your|each|every" + "|his|her|my|these|those";
|
|
11076
|
+
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\`])`;
|
|
11077
|
+
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?";
|
|
11078
|
+
var AMBIGUOUS_SOURCE = `(?:^|(?<=[.!?,;:])${S})${S}*(?:(?:${ci("and|but|or|so|then|yet")})${S}+)?` + `(${ci("it|this|that|these|those")})${S}+(?:${AMBIGUOUS_VERBS})${B}`;
|
|
11079
|
+
var WORDY = [
|
|
11080
|
+
["in order to", "to"],
|
|
11081
|
+
["at this point in time", "now"],
|
|
11082
|
+
["in the event that", "if"],
|
|
11083
|
+
["due to the fact that", "because"],
|
|
11084
|
+
["a number of", "some"],
|
|
11085
|
+
["utilise", "use"],
|
|
11086
|
+
["utilize", "use"],
|
|
11087
|
+
["prior to", "before"],
|
|
11088
|
+
["subsequent to", "after"],
|
|
11089
|
+
["in the case of", "for"],
|
|
11090
|
+
["in spite of the fact that", "although"],
|
|
11091
|
+
["for the purpose of", "to"],
|
|
11092
|
+
["has the ability to", "can"],
|
|
11093
|
+
["is able to", "can"]
|
|
11094
|
+
];
|
|
11095
|
+
var IDENTIFIER_SOURCE = "`[^`]*`" + `|${B}${W}+\\(\\)` + `|${B}${W}*[a-z]${W}*[A-Z]${W}*${B}` + `|${B}${W}+_${W}+${B}` + `|${B}${W}+\\.${W}+${B}`;
|
|
11096
|
+
var MAX_WORDS_PROCEDURAL = 20;
|
|
11097
|
+
var MAX_WORDS_DESCRIPTIVE = 25;
|
|
11098
|
+
function rule(source, flags = "") {
|
|
11099
|
+
return new RegExp(source, `u${flags}`);
|
|
11100
|
+
}
|
|
11101
|
+
function stripIdentifiers(text) {
|
|
11102
|
+
return text.replace(rule(IDENTIFIER_SOURCE, "g"), " ");
|
|
11103
|
+
}
|
|
11104
|
+
|
|
11105
|
+
// src/ste/markdown.ts
|
|
11106
|
+
function stripNoise(text) {
|
|
11107
|
+
const out = [];
|
|
11108
|
+
let inFence = false;
|
|
11109
|
+
const lines = pySplitlines(text);
|
|
11110
|
+
let start = 0;
|
|
11111
|
+
if (lines.length > 0 && pyStrip(lines[0]) === "---") {
|
|
11112
|
+
for (let j = 1;j < lines.length; j++) {
|
|
11113
|
+
if (pyStrip(lines[j]) === "---") {
|
|
11114
|
+
start = j + 1;
|
|
11115
|
+
break;
|
|
11116
|
+
}
|
|
11117
|
+
}
|
|
11118
|
+
}
|
|
11119
|
+
for (let i = start;i < lines.length; i++) {
|
|
11120
|
+
const line = lines[i];
|
|
11121
|
+
if (pyLstrip(line).startsWith("```")) {
|
|
11122
|
+
inFence = !inFence;
|
|
11123
|
+
continue;
|
|
11124
|
+
}
|
|
11125
|
+
if (inFence)
|
|
11126
|
+
continue;
|
|
11127
|
+
const s = pyStrip(line);
|
|
11128
|
+
if (s === "" || s.startsWith("#") || s.startsWith("|") || s.startsWith(">"))
|
|
11129
|
+
continue;
|
|
11130
|
+
if (s.startsWith("---") || s.startsWith("<!--"))
|
|
11131
|
+
continue;
|
|
11132
|
+
if (s.startsWith("- Not:"))
|
|
11133
|
+
continue;
|
|
11134
|
+
out.push([i + 1, line]);
|
|
11135
|
+
}
|
|
11136
|
+
return out;
|
|
11137
|
+
}
|
|
11138
|
+
var LINK = /\[([^\]]*)\]\([^)]*\)/gu;
|
|
11139
|
+
var IDENTIFIER = rule(IDENTIFIER_SOURCE, "g");
|
|
11140
|
+
function proseOnly(line) {
|
|
11141
|
+
const s = line.replace(LINK, (m, text) => text + " ".repeat(m.length - text.length));
|
|
11142
|
+
return s.replace(IDENTIFIER, (m) => " ".repeat(m.length));
|
|
11143
|
+
}
|
|
11144
|
+
var EMPHASIS = /\*\*|__/gu;
|
|
11145
|
+
var DECIMAL2 = rule(`(${D})\\.(${D})`, "g");
|
|
11146
|
+
var SENTENCE_END = rule(`(?<=[.!?])${S}+`);
|
|
11147
|
+
function sentences(block) {
|
|
11148
|
+
const guarded = block.replace(EMPHASIS, "").replace(DECIMAL2, "$1<DOT>$2");
|
|
11149
|
+
return guarded.split(SENTENCE_END).map((part) => part.replaceAll("<DOT>", "."));
|
|
11150
|
+
}
|
|
11151
|
+
var CODE_SPAN = /`[^`]*`/gu;
|
|
11152
|
+
var EMPHASIS_CHARS = /[*_]/gu;
|
|
11153
|
+
var LIST_MARKER = rule(`^${S}*[-*${D}.]+${S}*`);
|
|
11154
|
+
function wordCount(sentence) {
|
|
11155
|
+
const s = sentence.replace(CODE_SPAN, "X").replace(LINK, "$1").replace(EMPHASIS_CHARS, "").replace(LIST_MARKER, "");
|
|
11156
|
+
return pySplit(s).filter((w) => Array.from(w).some(isAlnum)).length;
|
|
11157
|
+
}
|
|
11158
|
+
function join14(parts, start) {
|
|
11159
|
+
const chunks = [];
|
|
11160
|
+
const lineMap = [];
|
|
11161
|
+
parts.forEach(([n, body], i) => {
|
|
11162
|
+
if (i > 0) {
|
|
11163
|
+
chunks.push(" ");
|
|
11164
|
+
lineMap.push(n);
|
|
11165
|
+
}
|
|
11166
|
+
chunks.push(body);
|
|
11167
|
+
for (let k = 0;k < body.length; k++)
|
|
11168
|
+
lineMap.push(n);
|
|
11169
|
+
});
|
|
11170
|
+
return [start, chunks.join(""), lineMap];
|
|
11171
|
+
}
|
|
11172
|
+
function lineAt2(index, lineMap) {
|
|
11173
|
+
return lineMap.length > 0 ? lineMap[Math.min(index, lineMap.length - 1)] : 0;
|
|
11174
|
+
}
|
|
11175
|
+
var LIST_ITEM = rule(`^${S}*(?:[-*+]|${D}+\\.)${S}`);
|
|
11176
|
+
var PROCEDURAL_STARTS = ["- [", "1.", "2.", "3.", "4.", "5.", "6."];
|
|
11177
|
+
var WORDY_RULES = WORDY.map(([phrase, better]) => [phrase, better, rule(`(?<![\\p{L}\\p{N}_-])${reEscape(phrase)}${B}`, "g")]);
|
|
11178
|
+
var AMBIGUOUS = rule(AMBIGUOUS_SOURCE, "g");
|
|
11179
|
+
var PASSIVE = rule(PASSIVE_SOURCE, "g");
|
|
11180
|
+
function paragraphs(text) {
|
|
11181
|
+
const blocks = [];
|
|
11182
|
+
let para = [];
|
|
11183
|
+
let paraStart = 0;
|
|
11184
|
+
let prevLine = -2;
|
|
11185
|
+
for (const [n, line] of stripNoise(text)) {
|
|
11186
|
+
const startsItem = LIST_ITEM.test(line);
|
|
11187
|
+
if ((n !== prevLine + 1 || startsItem) && para.length > 0) {
|
|
11188
|
+
blocks.push(join14(para, paraStart));
|
|
11189
|
+
para = [];
|
|
11190
|
+
}
|
|
11191
|
+
if (para.length === 0)
|
|
11192
|
+
paraStart = n;
|
|
11193
|
+
para.push([n, pyStrip(line)]);
|
|
11194
|
+
prevLine = n;
|
|
11195
|
+
}
|
|
11196
|
+
if (para.length > 0)
|
|
11197
|
+
blocks.push(join14(para, paraStart));
|
|
11198
|
+
return blocks;
|
|
11199
|
+
}
|
|
11200
|
+
function checkMarkdown(name, text) {
|
|
11201
|
+
const problems = [];
|
|
11202
|
+
const blocks = paragraphs(text);
|
|
11203
|
+
for (const [start, block] of blocks) {
|
|
11204
|
+
const procedural = PROCEDURAL_STARTS.some((p) => pyLstrip(block).startsWith(p));
|
|
11205
|
+
const limit = procedural ? MAX_WORDS_PROCEDURAL : MAX_WORDS_DESCRIPTIVE;
|
|
11206
|
+
for (const sentence of sentences(block)) {
|
|
11207
|
+
const n = wordCount(sentence);
|
|
11208
|
+
if (n > limit) {
|
|
11209
|
+
const tier = procedural ? "procedural" : "descriptive";
|
|
11210
|
+
problems.push(`${name}:~${start}: ${n} words (>${limit} ${tier}): ${headCodePoints(pyStrip(sentence), 90)}`);
|
|
11211
|
+
}
|
|
11212
|
+
}
|
|
11213
|
+
}
|
|
11214
|
+
for (const [, block, lineMap] of blocks) {
|
|
11215
|
+
const bare = proseOnly(block);
|
|
11216
|
+
for (const [phrase, better, re] of WORDY_RULES) {
|
|
11217
|
+
for (const m of bare.matchAll(re)) {
|
|
11218
|
+
problems.push(`${name}:${lineAt2(m.index, lineMap)}: wordy: ${pyRepr(phrase)} - prefer ${pyRepr(better)}`);
|
|
11219
|
+
}
|
|
11220
|
+
}
|
|
11221
|
+
for (const m of bare.matchAll(AMBIGUOUS)) {
|
|
11222
|
+
problems.push(`${name}:${lineAt2(m.index, lineMap)}: ambiguous reference: ${pyRepr(pyStrip(m[0]))}`);
|
|
11223
|
+
}
|
|
11224
|
+
for (const m of bare.matchAll(PASSIVE)) {
|
|
11225
|
+
problems.push(`${name}:${lineAt2(m.index, lineMap)}: passive voice: ${pyRepr(pyStrip(m[0]))}`);
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
11228
|
+
return problems;
|
|
11229
|
+
}
|
|
11230
|
+
|
|
11231
|
+
// src/ste/prose.ts
|
|
11232
|
+
var PASSIVE2 = rule(PASSIVE_SOURCE);
|
|
11233
|
+
var AMBIGUOUS2 = rule(AMBIGUOUS_SOURCE);
|
|
11234
|
+
var SENTENCE_END2 = rule(`(?<=[.!?])${S}+`);
|
|
11235
|
+
function sentencesOf(text) {
|
|
11236
|
+
const collapsed = pySplit(text).join(" ");
|
|
11237
|
+
return collapsed.split(SENTENCE_END2).map(pyStrip).filter((s) => s !== "");
|
|
11238
|
+
}
|
|
11239
|
+
function clip(text, limit = 60) {
|
|
11240
|
+
return codePointLength(text) <= limit ? text : `${headCodePoints(text, limit - 1)}…`;
|
|
11241
|
+
}
|
|
11242
|
+
function checkProse(text) {
|
|
11243
|
+
const findings = [];
|
|
11244
|
+
for (const sentence of sentencesOf(text)) {
|
|
11245
|
+
const words = pySplit(sentence);
|
|
11246
|
+
if (words.length > MAX_WORDS_PROCEDURAL) {
|
|
11247
|
+
findings.push([
|
|
11248
|
+
"STE-LEN",
|
|
11249
|
+
`sentence has ${words.length} words (limit ${MAX_WORDS_PROCEDURAL}): '${clip(sentence)}'`
|
|
11250
|
+
]);
|
|
11251
|
+
}
|
|
11252
|
+
const passive = PASSIVE2.exec(sentence);
|
|
11253
|
+
if (passive) {
|
|
11254
|
+
findings.push(["STE-VOICE", `possible passive voice '${passive[0]}' -- prefer active`]);
|
|
11255
|
+
}
|
|
11256
|
+
const ambiguous = AMBIGUOUS2.exec(sentence);
|
|
11257
|
+
if (ambiguous) {
|
|
11258
|
+
findings.push(["STE-REF", `ambiguous '${ambiguous[1]}' -- name the thing it refers to`]);
|
|
11259
|
+
}
|
|
11260
|
+
}
|
|
11261
|
+
const prose = stripIdentifiers(text);
|
|
11262
|
+
for (const [phrase, better] of WORDY) {
|
|
11263
|
+
if (rule(`${B}${ci(reEscape(phrase))}${B}`).test(prose)) {
|
|
11264
|
+
findings.push(["STE-WORD", `'${phrase}' -- prefer '${better}'`]);
|
|
11265
|
+
}
|
|
11266
|
+
}
|
|
11267
|
+
return findings;
|
|
11268
|
+
}
|
|
11269
|
+
var SECTION_HEADER = /^(Args|Arguments|Returns|Raises|Yields|Examples|Attributes|Parameters|Notes?|See Also):?$/u;
|
|
11270
|
+
function proseOf(doc) {
|
|
11271
|
+
const out = [];
|
|
11272
|
+
let inFence = false;
|
|
11273
|
+
for (const raw of pySplitlines(doc)) {
|
|
11274
|
+
const line = pyStrip(pyStrip(raw).replace(/^\*+/, ""));
|
|
11275
|
+
if (line.startsWith("```")) {
|
|
11276
|
+
inFence = !inFence;
|
|
11277
|
+
continue;
|
|
11278
|
+
}
|
|
11279
|
+
if (inFence || line.startsWith("@") || line.startsWith(">>>"))
|
|
11280
|
+
continue;
|
|
11281
|
+
if (SECTION_HEADER.test(line))
|
|
11282
|
+
continue;
|
|
11283
|
+
if (/^-{3,}$/u.test(line))
|
|
11284
|
+
continue;
|
|
11285
|
+
out.push(line);
|
|
11286
|
+
}
|
|
11287
|
+
return out.filter((p) => p !== "").join(" ");
|
|
11288
|
+
}
|
|
11289
|
+
|
|
11290
|
+
// src/ste/index.ts
|
|
11291
|
+
var STE_HELP = `Usage: repo-tools ste <file-or-directory> [...]
|
|
11292
|
+
repo-tools ste --prose <file>
|
|
11293
|
+
|
|
11294
|
+
Check prose against the mechanically decidable part of ASD-STE100 Simplified
|
|
11295
|
+
Technical English: sentence length, wordy forms, ambiguous references (a
|
|
11296
|
+
demonstrative with no noun) and the passive voice with an agent. The checker
|
|
11297
|
+
cannot judge the approved dictionary.
|
|
11298
|
+
|
|
11299
|
+
Markdown mode (the default) checks each Markdown file, or each .md file below a
|
|
11300
|
+
directory, in code-unit order of the path. A descriptive sentence may have 25
|
|
11301
|
+
words; a procedural one (a numbered step or a check box) may have 20. Code
|
|
11302
|
+
fences, tables, headings, quotes, comments, frontmatter, inline code and
|
|
11303
|
+
identifiers are not checked. A file whose first five lines hold
|
|
11304
|
+
"<!-- ste:historical-record -->" is skipped, and the skip is counted.
|
|
11305
|
+
|
|
11306
|
+
--prose <file> Check the file as one docstring. Tag lines (@param), section
|
|
11307
|
+
headers, doctest lines and code fences are removed first. Each
|
|
11308
|
+
finding prints as "RULE<TAB>detail". The findings are advice:
|
|
11309
|
+
the exit code is 0.
|
|
11310
|
+
--help, -h Show this help.
|
|
11311
|
+
|
|
11312
|
+
Exit codes: 0 when no finding (Markdown mode) or always (--prose mode). 1 when a
|
|
11313
|
+
Markdown finding exists. 2 on a usage error, when no Markdown file was examined,
|
|
11314
|
+
or when a file is not UTF-8.
|
|
11315
|
+
`;
|
|
11316
|
+
var HISTORICAL_MARKER = "<!-- ste:historical-record -->";
|
|
11317
|
+
function readText(path) {
|
|
11318
|
+
const bytes = readFileSync14(path);
|
|
11319
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
11320
|
+
return text.replace(/\r\n?/g, `
|
|
11321
|
+
`);
|
|
11322
|
+
}
|
|
11323
|
+
function comparePaths(a, b) {
|
|
11324
|
+
for (let i = 0;i < Math.min(a.length, b.length); i++) {
|
|
11325
|
+
const c = compareCodeUnits(a[i], b[i]);
|
|
11326
|
+
if (c !== 0)
|
|
11327
|
+
return c;
|
|
11328
|
+
}
|
|
11329
|
+
return a.length - b.length;
|
|
11330
|
+
}
|
|
11331
|
+
function targetsFor(path) {
|
|
11332
|
+
if (existsSync14(path) && statSync7(path).isFile())
|
|
11333
|
+
return [path];
|
|
11334
|
+
const found = [];
|
|
11335
|
+
const walk = (dir, parts) => {
|
|
11336
|
+
let entries;
|
|
11337
|
+
try {
|
|
11338
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
11339
|
+
} catch {
|
|
11340
|
+
return;
|
|
11341
|
+
}
|
|
11342
|
+
for (const entry of entries) {
|
|
11343
|
+
const full = join15(dir, entry.name);
|
|
11344
|
+
if (entry.isDirectory()) {
|
|
11345
|
+
if (entry.name !== ".pytest_cache")
|
|
11346
|
+
walk(full, [...parts, entry.name]);
|
|
11347
|
+
} else if (entry.name.endsWith(".md")) {
|
|
11348
|
+
const isFile = entry.isFile() || entry.isSymbolicLink() && statSync7(full).isFile();
|
|
11349
|
+
if (isFile)
|
|
11350
|
+
found.push([...parts, entry.name]);
|
|
11351
|
+
}
|
|
11352
|
+
}
|
|
11353
|
+
};
|
|
11354
|
+
if (existsSync14(path) && lstatSync3(path).isDirectory())
|
|
11355
|
+
walk(path, []);
|
|
11356
|
+
return found.sort(comparePaths).map((parts) => join15(path, ...parts));
|
|
11357
|
+
}
|
|
11358
|
+
function isHistoricalRecord(path) {
|
|
11359
|
+
let text;
|
|
11360
|
+
try {
|
|
11361
|
+
text = new TextDecoder("utf-8").decode(readFileSync14(path));
|
|
11362
|
+
} catch {
|
|
11363
|
+
return false;
|
|
11364
|
+
}
|
|
11365
|
+
return text.split(/\r\n|\r|\n/).slice(0, 5).some((line) => line.trim() === HISTORICAL_MARKER);
|
|
11366
|
+
}
|
|
11367
|
+
function runProse(args, io) {
|
|
11368
|
+
const [file, ...extra] = args;
|
|
11369
|
+
if (file === undefined || extra.length > 0) {
|
|
11370
|
+
io.stderr(`repo-tools ste: --prose needs exactly one file
|
|
11371
|
+
`);
|
|
11372
|
+
return 2;
|
|
11373
|
+
}
|
|
11374
|
+
let text;
|
|
11375
|
+
try {
|
|
11376
|
+
text = readText(file);
|
|
11377
|
+
} catch {
|
|
11378
|
+
io.stderr(`repo-tools ste: cannot read ${basename6(file)} as UTF-8 text
|
|
11379
|
+
`);
|
|
11380
|
+
return 2;
|
|
11381
|
+
}
|
|
11382
|
+
for (const [rule, detail] of checkProse(proseOf(text)))
|
|
11383
|
+
io.stdout(`${rule} ${detail}
|
|
11384
|
+
`);
|
|
11385
|
+
return 0;
|
|
11386
|
+
}
|
|
11387
|
+
async function run5(argv, io) {
|
|
11388
|
+
if (argv[0] === "--prose")
|
|
11389
|
+
return runProse(argv.slice(1), io);
|
|
11390
|
+
if (argv.length === 0) {
|
|
11391
|
+
io.stdout(`usage: repo-tools ste <file-or-directory> [...]
|
|
11392
|
+
`);
|
|
11393
|
+
return 2;
|
|
11394
|
+
}
|
|
11395
|
+
let total = 0;
|
|
11396
|
+
let checked = 0;
|
|
11397
|
+
let skipped = 0;
|
|
11398
|
+
for (const arg of argv) {
|
|
11399
|
+
for (const target of targetsFor(arg)) {
|
|
11400
|
+
const name = basename6(target);
|
|
11401
|
+
if (isHistoricalRecord(target)) {
|
|
11402
|
+
skipped += 1;
|
|
11403
|
+
io.stdout(`
|
|
11404
|
+
=== ${name}: SKIPPED (historical record) ===
|
|
11405
|
+
`);
|
|
11406
|
+
continue;
|
|
11407
|
+
}
|
|
11408
|
+
let text;
|
|
11409
|
+
try {
|
|
11410
|
+
text = readText(target);
|
|
11411
|
+
} catch {
|
|
11412
|
+
io.stderr(`repo-tools ste: cannot read ${name} as UTF-8 text
|
|
11413
|
+
`);
|
|
11414
|
+
return 2;
|
|
11415
|
+
}
|
|
11416
|
+
const problems = checkMarkdown(name, text);
|
|
11417
|
+
checked += 1;
|
|
11418
|
+
total += problems.length;
|
|
11419
|
+
const status = problems.length === 0 ? "OK" : `${problems.length} problem(s)`;
|
|
11420
|
+
io.stdout(`
|
|
11421
|
+
=== ${name}: ${status} ===
|
|
11422
|
+
`);
|
|
11423
|
+
for (const p of problems)
|
|
11424
|
+
io.stdout(` ${p}
|
|
11425
|
+
`);
|
|
11426
|
+
}
|
|
11427
|
+
}
|
|
11428
|
+
const tail = skipped > 0 ? `; ${skipped} skipped as historical record(s)` : "";
|
|
11429
|
+
io.stdout(`
|
|
11430
|
+
TOTAL: ${total} problem(s) in ${checked} file(s)${tail}
|
|
11431
|
+
`);
|
|
11432
|
+
if (checked === 0) {
|
|
11433
|
+
io.stderr(`ERROR: no Markdown file was examined. This is a failure, not a clean result -- check the path(s) given.
|
|
11434
|
+
`);
|
|
11435
|
+
return 2;
|
|
11436
|
+
}
|
|
11437
|
+
return total > 0 ? 1 : 0;
|
|
11438
|
+
}
|
|
11439
|
+
|
|
10974
11440
|
// src/cli.ts
|
|
10975
11441
|
var REGISTRY = {
|
|
10976
11442
|
depgraph: {
|
|
@@ -10992,6 +11458,11 @@ var REGISTRY = {
|
|
|
10992
11458
|
summary: "Answer structural questions from the depgraph reports, or write derived reports.",
|
|
10993
11459
|
help: QUERY_HELP,
|
|
10994
11460
|
run: run4
|
|
11461
|
+
},
|
|
11462
|
+
ste: {
|
|
11463
|
+
summary: "Check Markdown or docstring prose against Simplified Technical English.",
|
|
11464
|
+
help: STE_HELP,
|
|
11465
|
+
run: run5
|
|
10995
11466
|
}
|
|
10996
11467
|
};
|
|
10997
11468
|
var SUBCOMMANDS = Object.keys(REGISTRY);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danielsimonjr/repo-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "One CLI for repository analysis: dependency graphs creation and query with per-export API facts, file chunking, and context compression. TypeScript on Bun, compiled to a single executable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|