nollm 0.3.0 → 0.4.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/README.md +2 -1
- package/package.json +1 -1
- package/src/diff.js +15 -12
- package/src/files.js +11 -9
- package/src/rules.js +31 -4
- package/src/shape.js +135 -2
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ Files of other types, binary files, lockfiles, minified files, and files over 2
|
|
|
136
136
|
| `chat-opener` | Lines that start with "Great question", "Certainly", "Let me", and more |
|
|
137
137
|
| `chat-closer` | "Hope this helps", "Let me know if", "Feel free to", and more |
|
|
138
138
|
| `ai-disclosure` | "As an AI", "my training data", and more |
|
|
139
|
-
| `contrast-cliche` | "not just X, but Y"
|
|
139
|
+
| `contrast-cliche` | "not just X, but Y", "it's not X, it's Y", and "X, not Y: the rest" |
|
|
140
140
|
| `rhetorical-question` | "Why? Because" and "The result?" |
|
|
141
141
|
| `emoji-list` | List items that start with an emoji |
|
|
142
142
|
| `no-short-term-relevance` | Comments that stop making sense once the change lands: "no longer", "no behavior change", "for now", "Previously," |
|
|
@@ -144,6 +144,7 @@ Files of other types, binary files, lockfiles, minified files, and files over 2
|
|
|
144
144
|
| `quoted-error` | Comments that quote an error message: `"Cannot read properties of..."` |
|
|
145
145
|
| `dramatic-verb` | blows up, dies with, falls over, chokes on, and friends |
|
|
146
146
|
| `parenthetical-aside` | Asides like `(and their compiled handles)` |
|
|
147
|
+
| `mid-phrase-break` | A line that stops mid phrase, on a word such as the, of, or that |
|
|
147
148
|
| `long-sentence` | A sentence over 30 words |
|
|
148
149
|
| `wall-of-text` | A paragraph over 120 words or 7 sentences |
|
|
149
150
|
| `uniform-paragraphs` | Three or more paragraphs in a row of about the same length |
|
package/package.json
CHANGED
package/src/diff.js
CHANGED
|
@@ -22,14 +22,14 @@ export const ALL_LINES = true;
|
|
|
22
22
|
/**
|
|
23
23
|
* The lines a branch adds or changes, per file.
|
|
24
24
|
*
|
|
25
|
-
* Returns a Map from path to a Set of line numbers in the new file,
|
|
26
|
-
* ALL_LINES when the whole file is new.
|
|
25
|
+
* Returns a Map from path to a Set of line numbers in the new file,
|
|
26
|
+
* or to ALL_LINES when the whole file is new.
|
|
27
27
|
* Paths are relative to cwd, so they match what collectFiles returns.
|
|
28
28
|
*
|
|
29
29
|
* The comparison starts at the merge base of base and the working tree, so
|
|
30
30
|
* commits that land on base after the branch point stay out of the result.
|
|
31
|
-
* Uncommitted edits and files git does not track yet count as part of the
|
|
32
|
-
*
|
|
31
|
+
* Uncommitted edits and files git does not track yet count as part of the branch,
|
|
32
|
+
* so the same call works before a push.
|
|
33
33
|
*
|
|
34
34
|
* Files with no added or changed lines are left out. So are deleted files.
|
|
35
35
|
*/
|
|
@@ -124,9 +124,10 @@ async function real(path) {
|
|
|
124
124
|
/**
|
|
125
125
|
* Says what is wrong with a base ref before git says it less clearly.
|
|
126
126
|
*
|
|
127
|
-
* A checkout that fetched one branch, or fetched to a shallow depth,
|
|
128
|
-
* usual reason a ref is missing.
|
|
129
|
-
*
|
|
127
|
+
* A checkout that fetched one branch, or fetched to a shallow depth,
|
|
128
|
+
* is the usual reason a ref is missing.
|
|
129
|
+
* CI does both by default,
|
|
130
|
+
* so the ref a branch is compared against is often the one that is missing.
|
|
130
131
|
*/
|
|
131
132
|
async function checkBase(dir) {
|
|
132
133
|
if ((await tryGit(["rev-parse", "--is-inside-work-tree"], dir)) === null) {
|
|
@@ -166,9 +167,10 @@ async function noMergeBase(base, dir) {
|
|
|
166
167
|
/**
|
|
167
168
|
* How to fetch a missing ref. "origin/develop" needs "origin develop".
|
|
168
169
|
*
|
|
169
|
-
* A branch name may hold a slash of its own,
|
|
170
|
-
* remote only when the repository lists it as one.
|
|
171
|
-
* remotes gives nothing to check against,
|
|
170
|
+
* A branch name may hold a slash of its own,
|
|
171
|
+
* so the first part counts as a remote only when the repository lists it as one.
|
|
172
|
+
* A repository with no remotes gives nothing to check against,
|
|
173
|
+
* so the usual reading wins.
|
|
172
174
|
*/
|
|
173
175
|
async function fetchArgs(base, cwd) {
|
|
174
176
|
const cut = base.indexOf("/");
|
|
@@ -220,8 +222,9 @@ async function untracked(cwd) {
|
|
|
220
222
|
/**
|
|
221
223
|
* Reads a unified diff produced with --unified=0 and --no-prefix.
|
|
222
224
|
*
|
|
223
|
-
* Every file starts with a "diff --git" line,
|
|
224
|
-
* next "+++" is a header
|
|
225
|
+
* Every file starts with a "diff --git" line,
|
|
226
|
+
* so that line marks where the next "+++" is a header,
|
|
227
|
+
* and not a line of added content that starts with "++".
|
|
225
228
|
*/
|
|
226
229
|
function parse(patch) {
|
|
227
230
|
const changed = new Map();
|
package/src/files.js
CHANGED
|
@@ -30,12 +30,13 @@ const EVERYTHING = ["**/*", "**/.*", "**/.*/**"];
|
|
|
30
30
|
* git only knows about its own work tree, so roots outside cwd are always
|
|
31
31
|
* walked.
|
|
32
32
|
*
|
|
33
|
-
* The ignore option takes patterns in .gitignore syntax.
|
|
34
|
-
* project, so they apply under cwd
|
|
33
|
+
* The ignore option takes patterns in .gitignore syntax.
|
|
34
|
+
* They describe the project, so they apply under cwd
|
|
35
|
+
* and leave paths outside cwd alone.
|
|
35
36
|
*
|
|
36
|
-
* Roots and cwd are compared after their symlinks are followed.
|
|
37
|
-
* temp directory is reached through /var and lives in /private/var,
|
|
38
|
-
* without this a path under cwd would look like a path outside it.
|
|
37
|
+
* Roots and cwd are compared after their symlinks are followed.
|
|
38
|
+
* On macOS a temp directory is reached through /var and lives in /private/var,
|
|
39
|
+
* so without this a path under cwd would look like a path outside it.
|
|
39
40
|
*/
|
|
40
41
|
export async function collectFiles(
|
|
41
42
|
roots,
|
|
@@ -113,11 +114,12 @@ async function fromWalk(roots, base) {
|
|
|
113
114
|
* so an ignored tree is never opened.
|
|
114
115
|
*
|
|
115
116
|
* A .gitignore applies to the directory that holds it and to everything below.
|
|
116
|
-
* So each entry is matched against the chain of files
|
|
117
|
-
*
|
|
117
|
+
* So each entry is matched against the chain of files
|
|
118
|
+
* from the root down to its own directory.
|
|
119
|
+
* Chains are built once per directory and kept.
|
|
118
120
|
*
|
|
119
|
-
* glob asks this question synchronously, so the reads are synchronous.
|
|
120
|
-
* one small file per directory, which is what the walk read before.
|
|
121
|
+
* glob asks this question synchronously, so the reads are synchronous.
|
|
122
|
+
* It is one small file per directory, which is what the walk read before.
|
|
121
123
|
*/
|
|
122
124
|
function skips(root) {
|
|
123
125
|
const chains = new Map();
|
package/src/rules.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
longSentences,
|
|
3
|
+
midPhraseBreaks,
|
|
4
|
+
uniformParagraphs,
|
|
5
|
+
uniformSentences,
|
|
6
|
+
wallOfText,
|
|
7
|
+
} from "./shape.js";
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* A rule is a regular expression plus a message,
|
|
@@ -28,6 +34,23 @@ function escape(text) {
|
|
|
28
34
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
29
35
|
}
|
|
30
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Three shapes of one move: naming the thing it is not.
|
|
39
|
+
*
|
|
40
|
+
* not just X, but Y
|
|
41
|
+
* it is not X, it is Y
|
|
42
|
+
* X, not Y: the rest
|
|
43
|
+
*
|
|
44
|
+
* The third wants a label colon, so "a, not https://b" stays out. It also
|
|
45
|
+
* stays on one line, so a wrapped block comment cannot join two sentences
|
|
46
|
+
* into a match.
|
|
47
|
+
*/
|
|
48
|
+
const CONTRAST_CLICHES = [
|
|
49
|
+
String.raw`\bnot (?:just|only|merely|simply) [^.\n]{1,60}?,? but(?: also)?\b`,
|
|
50
|
+
String.raw`\bit'?s not (?:just |about )?[^.\n]{1,40}?[,;] it'?s\b`,
|
|
51
|
+
String.raw`[^\s,:][^,:\n]{0,29},[ \t]+not[ \t]+[^,:\n]{1,30}:(?=\s|$)`,
|
|
52
|
+
];
|
|
53
|
+
|
|
31
54
|
const BANNED_WORDS = [
|
|
32
55
|
"genuinely",
|
|
33
56
|
"fails loudly",
|
|
@@ -508,9 +531,8 @@ export const rules = [
|
|
|
508
531
|
},
|
|
509
532
|
{
|
|
510
533
|
id: "contrast-cliche",
|
|
511
|
-
message: "Contrast cliche
|
|
512
|
-
pattern:
|
|
513
|
-
/\b(?:not (?:just|only|merely|simply) [^.\n]{1,60}?,? but(?: also)?\b|it'?s not (?:just |about )?[^.\n]{1,40}?[,;] it'?s\b)/gi,
|
|
534
|
+
message: "Contrast cliche. State the thing directly",
|
|
535
|
+
pattern: new RegExp(CONTRAST_CLICHES.join("|"), "gi"),
|
|
514
536
|
},
|
|
515
537
|
{
|
|
516
538
|
id: "rhetorical-question",
|
|
@@ -562,6 +584,11 @@ export const rules = [
|
|
|
562
584
|
pattern: new RegExp(String.raw`${COMMENT_START}(?:${words(WHAT_COMMENT_STARTS)})\b`, "gmi"),
|
|
563
585
|
scope: "comments",
|
|
564
586
|
},
|
|
587
|
+
{
|
|
588
|
+
id: "mid-phrase-break",
|
|
589
|
+
message: "Line break in the middle of a phrase. Break where there is a pause",
|
|
590
|
+
check: midPhraseBreaks,
|
|
591
|
+
},
|
|
565
592
|
{
|
|
566
593
|
id: "long-sentence",
|
|
567
594
|
message: "Long sentence. One idea per sentence",
|
package/src/shape.js
CHANGED
|
@@ -11,6 +11,86 @@ const TRAILER = /\s*(?:\*\/|-->|"""|''')\s*$/;
|
|
|
11
11
|
const LIST_ITEM = /^(?:[-*+]|\d+[.)])\s+|^@\w+/;
|
|
12
12
|
const SENTENCE_END = /[.!?]+(?:["')\]]+)?(?:\s+|$)/;
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Words that cannot end a thought.
|
|
16
|
+
* They point at whatever comes next,
|
|
17
|
+
* so a line that stops on one stopped in the middle of a phrase.
|
|
18
|
+
*
|
|
19
|
+
* Words that can stand at the end of a clause stay out, however often they
|
|
20
|
+
* also appear mid phrase. "Yes it can" and "give it to her" are ordinary,
|
|
21
|
+
* so can and her are not here.
|
|
22
|
+
*/
|
|
23
|
+
export const DANGLING_WORDS = [
|
|
24
|
+
"a",
|
|
25
|
+
"an",
|
|
26
|
+
"the",
|
|
27
|
+
"of",
|
|
28
|
+
"to",
|
|
29
|
+
"in",
|
|
30
|
+
"on",
|
|
31
|
+
"at",
|
|
32
|
+
"by",
|
|
33
|
+
"for",
|
|
34
|
+
"with",
|
|
35
|
+
"from",
|
|
36
|
+
"into",
|
|
37
|
+
"onto",
|
|
38
|
+
"upon",
|
|
39
|
+
"over",
|
|
40
|
+
"under",
|
|
41
|
+
"about",
|
|
42
|
+
"across",
|
|
43
|
+
"after",
|
|
44
|
+
"before",
|
|
45
|
+
"between",
|
|
46
|
+
"during",
|
|
47
|
+
"through",
|
|
48
|
+
"toward",
|
|
49
|
+
"towards",
|
|
50
|
+
"within",
|
|
51
|
+
"without",
|
|
52
|
+
"against",
|
|
53
|
+
"among",
|
|
54
|
+
"around",
|
|
55
|
+
"beyond",
|
|
56
|
+
"per",
|
|
57
|
+
"via",
|
|
58
|
+
"and",
|
|
59
|
+
"or",
|
|
60
|
+
"but",
|
|
61
|
+
"nor",
|
|
62
|
+
"that",
|
|
63
|
+
"which",
|
|
64
|
+
"who",
|
|
65
|
+
"whom",
|
|
66
|
+
"whose",
|
|
67
|
+
"if",
|
|
68
|
+
"when",
|
|
69
|
+
"while",
|
|
70
|
+
"because",
|
|
71
|
+
"although",
|
|
72
|
+
"though",
|
|
73
|
+
"unless",
|
|
74
|
+
"until",
|
|
75
|
+
"since",
|
|
76
|
+
"whether",
|
|
77
|
+
"is",
|
|
78
|
+
"are",
|
|
79
|
+
"was",
|
|
80
|
+
"were",
|
|
81
|
+
"has",
|
|
82
|
+
"have",
|
|
83
|
+
"had",
|
|
84
|
+
"its",
|
|
85
|
+
"their",
|
|
86
|
+
"your",
|
|
87
|
+
"our",
|
|
88
|
+
"my",
|
|
89
|
+
"every",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const DANGLING_END = new RegExp(String.raw`\b(${DANGLING_WORDS.join("|")})\s*$`, "i");
|
|
93
|
+
|
|
14
94
|
export const WALL_WORDS = 120;
|
|
15
95
|
export const WALL_SENTENCES = 7;
|
|
16
96
|
export const LONG_SENTENCE_WORDS = 30;
|
|
@@ -23,8 +103,8 @@ export const UNIFORM_SENTENCE_VARIATION = 0.2;
|
|
|
23
103
|
/**
|
|
24
104
|
* Groups segments into paragraphs.
|
|
25
105
|
*
|
|
26
|
-
* Pass inComments: true for comment segments,
|
|
27
|
-
* comment markers are removed before counting.
|
|
106
|
+
* Pass inComments: true for comment segments,
|
|
107
|
+
* so comment markers are removed before counting.
|
|
28
108
|
*
|
|
29
109
|
* Each paragraph has:
|
|
30
110
|
* line, column → where it starts
|
|
@@ -260,3 +340,56 @@ function preview(text) {
|
|
|
260
340
|
const words = text.split(/\s+/, 6);
|
|
261
341
|
return words.join(" ") + (words.length === 6 ? "..." : "");
|
|
262
342
|
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Line breaks that land in the middle of a phrase.
|
|
346
|
+
*
|
|
347
|
+
* A break reads as a pause, so the line before it should be able to stop.
|
|
348
|
+
* A line ending on a word that points at the next one cannot.
|
|
349
|
+
*
|
|
350
|
+
* Only a break the next line carries on counts.
|
|
351
|
+
* A blank line, a heading, a table, a fence, or a new list item
|
|
352
|
+
* each end the thought by themselves, so the line before one is skipped.
|
|
353
|
+
*/
|
|
354
|
+
export function midPhraseBreaks(segments, scope) {
|
|
355
|
+
const inComments = scope === "comments";
|
|
356
|
+
const found = [];
|
|
357
|
+
let fence = false;
|
|
358
|
+
let previous = null;
|
|
359
|
+
|
|
360
|
+
for (let i = 0; i < segments.length; i++) {
|
|
361
|
+
const segment = segments[i];
|
|
362
|
+
const text = inComments ? strip(segment.text) : segment.text.trim();
|
|
363
|
+
|
|
364
|
+
if (text.startsWith("```") || text.startsWith("~~~")) {
|
|
365
|
+
fence = !fence;
|
|
366
|
+
previous = null;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (fence || text.length === 0 || text.startsWith("#") || text.startsWith("|")) {
|
|
371
|
+
previous = null;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const carriesOn = previous !== null && segment.line === previous.line + 1;
|
|
376
|
+
if (carriesOn && !LIST_ITEM.test(text)) {
|
|
377
|
+
const match = DANGLING_END.exec(previous.text);
|
|
378
|
+
if (match) {
|
|
379
|
+
found.push({
|
|
380
|
+
line: previous.line,
|
|
381
|
+
column: previous.column + match.index,
|
|
382
|
+
text: match[1],
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
previous = {
|
|
388
|
+
text,
|
|
389
|
+
line: segment.line,
|
|
390
|
+
column: segment.column + segment.text.indexOf(text),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return found;
|
|
395
|
+
}
|