dw-sdlc 0.4.0 → 0.5.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 +4 -0
- package/config/agentic.config.schema.json +1 -1
- package/dist/checks/comment-budget.js +170 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,10 @@ alone, with the same lines, exit codes and document.
|
|
|
34
34
|
{ "scripts": { "check": "dw-sdlc gate" } }
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
Under `gate.checks`, a Check the package ships is `true` for its defaults or an object of its options.
|
|
38
|
+
`comment-budget` holds each comment block in the JavaScript, TypeScript and shell (`.sh`) files under
|
|
39
|
+
its `roots` to `maxProseLines` lines of prose, 3 by default.
|
|
40
|
+
|
|
37
41
|
## Commands
|
|
38
42
|
|
|
39
43
|
| command | what it does |
|
|
@@ -331,7 +331,7 @@
|
|
|
331
331
|
"additionalProperties": false
|
|
332
332
|
},
|
|
333
333
|
"comment-budget": {
|
|
334
|
-
"description": "The comment budget: each comment block in the JavaScript and
|
|
334
|
+
"description": "The comment budget: each comment block in the JavaScript, TypeScript and shell (`.sh`) files under `roots` carries at most `maxProseLines` lines of prose, a line with a letter or a digit once the comment markers are off. A block is one `/* … */` comment or a run of `//` or `#` comments each alone on consecutive lines; a comment after code on its line is none, and so is a shell `#` that does not start a word of code.",
|
|
335
335
|
"type": ["boolean", "object"],
|
|
336
336
|
"properties": {
|
|
337
337
|
"maxProseLines": {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// `comment-budget`: a comment says what the code cannot, in a few lines. A block, one `/* … */` or a run
|
|
2
|
-
// of `//` lines alone on theirs, carries at most `maxProseLines` lines with a letter or a digit.
|
|
2
|
+
// of `//` or shell `#` lines alone on theirs, carries at most `maxProseLines` lines with a letter or a digit.
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { join, matchesGlob } from "node:path";
|
|
5
5
|
import { matching, treeFiles } from "./tree.js";
|
|
6
6
|
const SOURCE = ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"];
|
|
7
|
+
const SHELL = "**/*.sh";
|
|
7
8
|
const REMEDY = "cut it to what the code cannot say, or move the reasoning into a doc and point at it";
|
|
8
9
|
/** The characters after which a `/` opens a regular expression, not a division. */
|
|
9
10
|
const BEFORE_REGEX = new Set("(,=:[!&|?{};+-*%<>~^");
|
|
@@ -94,7 +95,170 @@ function commentsOf(text) {
|
|
|
94
95
|
}
|
|
95
96
|
return comments;
|
|
96
97
|
}
|
|
97
|
-
/** The
|
|
98
|
+
/** The characters that end a shell word, so a `#` after one opens a comment. */
|
|
99
|
+
const WORD_BREAK = new Set(" \t\n;&|()<>");
|
|
100
|
+
/** Where the line that holds `index` ends: at its newline, or at the end of `text`. */
|
|
101
|
+
function lineEnd(text, index) {
|
|
102
|
+
const newline = text.indexOf("\n", index);
|
|
103
|
+
return newline === -1 ? text.length : newline;
|
|
104
|
+
}
|
|
105
|
+
/** Where a span that `closer` ends, read from `start`, stops, past the closer; `\` escapes a character when `escapes`. */
|
|
106
|
+
function spanEnd(text, start, closer, escapes) {
|
|
107
|
+
for (let index = start; index < text.length; index += 1) {
|
|
108
|
+
if (escapes && text.charAt(index) === "\\") {
|
|
109
|
+
index += 1;
|
|
110
|
+
}
|
|
111
|
+
else if (text.charAt(index) === closer) {
|
|
112
|
+
return index + 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return text.length;
|
|
116
|
+
}
|
|
117
|
+
/** The heredoc whose `<<` is at `start`, its delimiter's quotes off, and where the delimiter's word ends. */
|
|
118
|
+
function heredocAt(text, start) {
|
|
119
|
+
const strip = text.charAt(start + 2) === "-";
|
|
120
|
+
let index = start + (strip ? 3 : 2);
|
|
121
|
+
while (text.charAt(index) === " " || text.charAt(index) === "\t") {
|
|
122
|
+
index += 1;
|
|
123
|
+
}
|
|
124
|
+
let end = index;
|
|
125
|
+
while (end < text.length && !WORD_BREAK.has(text.charAt(end))) {
|
|
126
|
+
const char = text.charAt(end);
|
|
127
|
+
end = char === "'" || char === '"' ? spanEnd(text, end + 1, char, false) : end + (char === "\\" ? 2 : 1);
|
|
128
|
+
}
|
|
129
|
+
return { delimiter: text.slice(index, end).replace(/["'\\]/g, ""), strip, end };
|
|
130
|
+
}
|
|
131
|
+
/** Where the heredoc body that starts at `start` ends: past the line that is its delimiter. */
|
|
132
|
+
function heredocEnd(text, start, { delimiter, strip }) {
|
|
133
|
+
for (let index = start; index < text.length;) {
|
|
134
|
+
const end = lineEnd(text, index);
|
|
135
|
+
const line = text.slice(index, end);
|
|
136
|
+
index = end + 1;
|
|
137
|
+
if ((strip ? line.replace(/^\t+/, "") : line) === delimiter) {
|
|
138
|
+
return index;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return text.length;
|
|
142
|
+
}
|
|
143
|
+
/** The character that opens a nesting inside each frame and the one that closes it; a double quote has none. */
|
|
144
|
+
const NESTING = {
|
|
145
|
+
code: ["(", ")"],
|
|
146
|
+
arithmetic: ["(", ")"],
|
|
147
|
+
expansion: ["{", "}"],
|
|
148
|
+
double: []
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Every `#` comment in shell `text`: a `#` that starts a word of code, never one in a quote, an
|
|
152
|
+
* expansion, an arithmetic `((…))` or a heredoc body, nor the shebang on line 1.
|
|
153
|
+
*/
|
|
154
|
+
function shellCommentsOf(text) {
|
|
155
|
+
const comments = [];
|
|
156
|
+
const newlines = [...text.matchAll(/\n/g)].map((match) => match.index);
|
|
157
|
+
const outer = [];
|
|
158
|
+
let frame = { kind: "code", depth: 0 };
|
|
159
|
+
const heredocs = [];
|
|
160
|
+
let wordStart = true;
|
|
161
|
+
let index = text.startsWith("#!") ? lineEnd(text, 0) : 0;
|
|
162
|
+
const open = (kind, length) => {
|
|
163
|
+
outer.push(frame);
|
|
164
|
+
frame = { kind, depth: 0 };
|
|
165
|
+
index += length;
|
|
166
|
+
wordStart = true;
|
|
167
|
+
};
|
|
168
|
+
const close = (length) => {
|
|
169
|
+
frame = outer.pop() ?? frame;
|
|
170
|
+
index += length;
|
|
171
|
+
wordStart = false;
|
|
172
|
+
};
|
|
173
|
+
const skipTo = (end) => {
|
|
174
|
+
index = end;
|
|
175
|
+
wordStart = false;
|
|
176
|
+
};
|
|
177
|
+
while (index < text.length) {
|
|
178
|
+
const char = text.charAt(index);
|
|
179
|
+
const next = text.charAt(index + 1);
|
|
180
|
+
const { kind } = frame;
|
|
181
|
+
const [opener, closer] = NESTING[kind];
|
|
182
|
+
if (char === "\\" && next === "\n") {
|
|
183
|
+
index += 2;
|
|
184
|
+
}
|
|
185
|
+
else if (char === "\\") {
|
|
186
|
+
skipTo(index + 2);
|
|
187
|
+
}
|
|
188
|
+
else if (kind === "double" && char === '"') {
|
|
189
|
+
close(1);
|
|
190
|
+
}
|
|
191
|
+
else if (char === "$" && text.startsWith("((", index + 1)) {
|
|
192
|
+
open("arithmetic", 3);
|
|
193
|
+
}
|
|
194
|
+
else if (char === "$" && next === "(") {
|
|
195
|
+
open("code", 2);
|
|
196
|
+
}
|
|
197
|
+
else if (char === "$" && next === "{") {
|
|
198
|
+
open("expansion", 2);
|
|
199
|
+
}
|
|
200
|
+
else if (char === "`") {
|
|
201
|
+
skipTo(spanEnd(text, index + 1, "`", true));
|
|
202
|
+
}
|
|
203
|
+
else if (kind === "double") {
|
|
204
|
+
index += 1;
|
|
205
|
+
}
|
|
206
|
+
else if (char === "$" && next === "'") {
|
|
207
|
+
skipTo(spanEnd(text, index + 2, "'", true));
|
|
208
|
+
}
|
|
209
|
+
else if (char === "'") {
|
|
210
|
+
skipTo(spanEnd(text, index + 1, "'", false));
|
|
211
|
+
}
|
|
212
|
+
else if (char === '"') {
|
|
213
|
+
open("double", 1);
|
|
214
|
+
}
|
|
215
|
+
else if (kind === "code" && char === "#" && wordStart) {
|
|
216
|
+
const end = lineEnd(text, index);
|
|
217
|
+
const line = newlines.filter((newline) => newline < index).length + 1;
|
|
218
|
+
const alone = text.slice(text.lastIndexOf("\n", index - 1) + 1, index).trim() === "";
|
|
219
|
+
comments.push({ kind: alone ? "line" : "trailing", start: line, end: line, text: text.slice(index, end) });
|
|
220
|
+
index = end;
|
|
221
|
+
}
|
|
222
|
+
else if (kind === "code" && char === "\n") {
|
|
223
|
+
index += 1;
|
|
224
|
+
for (const heredoc of heredocs.splice(0)) {
|
|
225
|
+
index = heredocEnd(text, index, heredoc);
|
|
226
|
+
}
|
|
227
|
+
wordStart = true;
|
|
228
|
+
}
|
|
229
|
+
else if (kind === "code" && text.startsWith("<<<", index)) {
|
|
230
|
+
index += 3;
|
|
231
|
+
wordStart = true;
|
|
232
|
+
}
|
|
233
|
+
else if (kind === "code" && text.startsWith("<<", index)) {
|
|
234
|
+
const { end, ...heredoc } = heredocAt(text, index);
|
|
235
|
+
heredocs.push(heredoc);
|
|
236
|
+
skipTo(end);
|
|
237
|
+
}
|
|
238
|
+
else if (kind === "code" && text.startsWith("((", index)) {
|
|
239
|
+
open("arithmetic", 2);
|
|
240
|
+
}
|
|
241
|
+
else if (char === opener) {
|
|
242
|
+
frame.depth += 1;
|
|
243
|
+
index += 1;
|
|
244
|
+
wordStart = true;
|
|
245
|
+
}
|
|
246
|
+
else if (char === closer && frame.depth > 0) {
|
|
247
|
+
frame.depth -= 1;
|
|
248
|
+
index += 1;
|
|
249
|
+
wordStart = true;
|
|
250
|
+
}
|
|
251
|
+
else if (char === closer && outer.length > 0) {
|
|
252
|
+
close(kind === "arithmetic" && next === ")" ? 2 : 1);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
wordStart = WORD_BREAK.has(char);
|
|
256
|
+
index += 1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return comments;
|
|
260
|
+
}
|
|
261
|
+
/** The comments as blocks: a run of `//` or `#` lines alone on consecutive lines is one; a comment after code is none. */
|
|
98
262
|
function blocksOf(comments) {
|
|
99
263
|
const blocks = [];
|
|
100
264
|
for (const comment of comments) {
|
|
@@ -123,8 +287,9 @@ function proseLines(block) {
|
|
|
123
287
|
}
|
|
124
288
|
function run(root, { maxProseLines, roots }) {
|
|
125
289
|
const findings = [];
|
|
126
|
-
for (const file of matching(root, treeFiles(root, roots), SOURCE)) {
|
|
127
|
-
|
|
290
|
+
for (const file of matching(root, treeFiles(root, roots), [...SOURCE, SHELL])) {
|
|
291
|
+
const text = readFileSync(join(root, file), "utf8");
|
|
292
|
+
for (const block of blocksOf(matchesGlob(file, SHELL) ? shellCommentsOf(text) : commentsOf(text))) {
|
|
128
293
|
const prose = proseLines(block.text);
|
|
129
294
|
if (prose > maxProseLines) {
|
|
130
295
|
const message = `a comment block of ${prose} prose lines, over the ${maxProseLines}-line budget`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dw-sdlc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The Gate and the agentic config of a repository: the dw-sdlc bin with the Gate, the setup, the doctor, the config check, and the Sync and setup of References; the resolver of the config's layers, its JSON Schema and the validator that reads it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|