omakit 0.5.0 → 0.5.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/README.md +16 -2
- package/package.json +1 -1
- package/skills/omarchy-plugin-check/SKILL.md +1 -1
- package/tools/inspect/contract.mjs +1 -1
- package/tools/inspect/functions.mjs +236 -12
- package/tools/inspect/inspect.mjs +3 -2
- package/tools/inspect/measure-functions.mjs +12 -3
- package/tools/inspect/patterns.mjs +17 -15
- package/tools/inspect/report.mjs +15 -4
package/README.md
CHANGED
|
@@ -6,6 +6,20 @@ The marketplace validates one exact commit of your plugin. Push a fix or comment
|
|
|
6
6
|
|
|
7
7
|
[](https://github.com/tcballard/omarchy-badges) [](https://www.npmjs.com/package/omakit) [](https://github.com/mtolhuys/omakit/actions/workflows/ci.yml) [](https://socket.dev/npm/package/omakit)
|
|
8
8
|
|
|
9
|
+
## What it delivers
|
|
10
|
+
|
|
11
|
+
| Command | What you get | Read more |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `omakit submit <plugin-repo>` | Run the marketplace's own checks before you open the issue, and get the issue text ready to paste. Nothing is posted for you. | [submit](docs/SUBMIT.md) |
|
|
14
|
+
| `omakit inspect <plugin-dir>` | See what needs attention in your plugin before a reviewer does: the longest functions, and the things reviewers flag most often, each with the file and line. | [inspect](docs/INSPECT.md) |
|
|
15
|
+
| `omakit watch <issue-url>` | Know whether the commit the marketplace checked is still the one you are shipping, and what to do when it is not. | [watch](docs/VALIDATION_WATCH.md) |
|
|
16
|
+
| `omakit audit` | Find installed plugins that are running code the marketplace never checked. | [audit](docs/AUDIT.md) |
|
|
17
|
+
| `omakit weigh <plugin>` | Find out what a plugin costs the shell in memory and CPU. | [weigh](docs/WEIGH.md) |
|
|
18
|
+
| `omakit verify <plugin-repo>` | Get the marketplace's security result for your commit, exactly as it would see it. | [commands](docs/COMMANDS.md) |
|
|
19
|
+
| `omakit doctor`, `omakit setup` | Check what is installed and pinned, or set everything up once, with tab completion. | [install](docs/INSTALL.md) |
|
|
20
|
+
|
|
21
|
+
Every number a command prints has a measured origin in [MEASUREMENTS.md](docs/MEASUREMENTS.md); nothing is a guess and nothing is a grade.
|
|
22
|
+
|
|
9
23
|
## Install
|
|
10
24
|
|
|
11
25
|
```bash
|
|
@@ -37,9 +51,9 @@ Compares your installed plugin commits with the marketplace's validated commits.
|
|
|
37
51
|
|
|
38
52
|
## `omakit inspect <plugin-dir>`
|
|
39
53
|
|
|
40
|
-

|
|
41
55
|
|
|
42
|
-
Reads a plugin's tree and prints what needs attention, biggest first: a size score (the share of its function lines that sit in functions over the measured size, placed among the
|
|
56
|
+
Reads a plugin's tree and prints what needs attention, biggest first: a size score (the share of its function lines that sit in functions over the measured size, placed among the listed trees' shares, so a tree with no long function scores 10.00, [M12](docs/MEASUREMENTS.md#m12-how-long-a-plugins-functions-are-in-listed-trees)), the functions over what 90 of 100 listed functions stay under, then each review class the tree shows with the class's measured share of review findings ([M11](docs/MEASUREMENTS.md#m11-what-the-human-review-raises-by-class)) and up to five sites. No verdict, nothing run from the tree; `--full` is every site, `--json` the document. Over 18 listed plugins read at their validated commits, the extraction counted 515 process sites (57 QML `Process` blocks, 458 shell lines), 17 hosts, 63 writes and 40 timers, left 15 rows it could not resolve, and printed 73 pattern rows across 17 of the 18 ([record](docs/evidence/inspect/2026-09-15-listed-sample.json)).
|
|
43
57
|
|
|
44
58
|
## `omakit weigh <plugin>`
|
|
45
59
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omakit",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "The safe place to find out: everything knowable about an Omarchy Quattro plugin submission before you post it, on your own machine. Agent-first, read-only against the marketplace, posts nothing, zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Maarten Tolhuijs",
|
|
@@ -57,7 +57,7 @@ from M12), longest first, each with its `percentile` among them, and
|
|
|
57
57
|
those functions, and `size.score` is 10 minus the share of listed trees
|
|
58
58
|
with a strictly smaller `heavyShare`, divided by 10: 10.00 when no
|
|
59
59
|
function is over a threshold, 0.00 when the tree is heavier than every
|
|
60
|
-
listed one (`size.sample.heavyShares` holds the listed trees
|
|
60
|
+
listed one (`size.sample.heavyShares` holds the shares of the listed trees that have a function, 49 of 50). It
|
|
61
61
|
is line-weighted, so splitting a long function into short ones raises it
|
|
62
62
|
and adding small functions beside a long one barely moves it. When the
|
|
63
63
|
owner asks for simpler code, start with the top of `size.over`, and read
|
|
@@ -169,7 +169,7 @@ export function validateInspectDocument(document, known = {}) {
|
|
|
169
169
|
for (const key of ["lines", "branches", "depth"]) if (!isInt(size.thresholds?.[key]) || size.thresholds[key] < 1) problems.push(`size.thresholds.${key} is not a count`)
|
|
170
170
|
const shares = size.sample?.heavyShares
|
|
171
171
|
if (!size.sample || !isInt(size.sample.trees) || !isInt(size.sample.functions)) problems.push("size.sample is not { trees, functions, heavyShares }")
|
|
172
|
-
else if (!Array.isArray(shares) || shares.length
|
|
172
|
+
else if (!Array.isArray(shares) || !shares.length || shares.length > size.sample.trees || !shares.every((share) => typeof share === "number" && share >= 0 && share <= 1)) problems.push("size.sample.heavyShares is not one share from 0 to 1 per listed tree with a function")
|
|
173
173
|
const scorable = Array.isArray(observed.functions) && observed.functions.length > 0
|
|
174
174
|
if (typeof size.heavyShare !== "number" || size.heavyShare < 0 || size.heavyShare > 1) problems.push("size.heavyShare is not a share from 0 to 1")
|
|
175
175
|
else if (Array.isArray(observed.functions) && size.thresholds) {
|
|
@@ -9,9 +9,32 @@
|
|
|
9
9
|
import { blankComments, closingBracket, lineOf } from "./text.mjs"
|
|
10
10
|
|
|
11
11
|
const JS_BRANCH = /\b(?:if|else if|for|while|do|switch|case|catch)\b|&&|\|\||\?[^.:]/g
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// A block opens with `if`, `for`, `while`, `until`, `case` or `select` and
|
|
13
|
+
// closes with `fi`, `done` or `esac`, each counted only where a statement
|
|
14
|
+
// can start (the line's start, after `;`, `&`, `|`, `(`, `then`, `do` or
|
|
15
|
+
// `else`), over the line with its quoted text removed, so `if x; then y;
|
|
16
|
+
// fi` on one line nets zero and is not a level, `x && if y; then z; fi`
|
|
17
|
+
// nets zero too, and neither `echo "done"` nor `echo done` closes anything.
|
|
18
|
+
const SHELL_OPEN = /(?:^|[;&|(]|\b(?:then|do|else))\s*(?:if|for|while|until|case|select)\b/g
|
|
19
|
+
const SHELL_CLOSE = /(?:^|[;&|(]|\b(?:then|do|else))\s*(?:fi|done|esac)\b/g
|
|
20
|
+
// A case arm, `pattern) command` or `(pattern) command`, is a branch: a `)`
|
|
21
|
+
// with text after it on a line with no `(` before it but an opening one,
|
|
22
|
+
// so a `$(...)` or `(( ))` in a test is not one.
|
|
23
|
+
const SHELL_BRANCH = /\b(?:if|elif|for|while|until|case)\b|\|\||&&|^\s*\(?[^()]*\)\s*(?!\s*$)/
|
|
24
|
+
// A guard, not a branch: `||` or `&&` followed by one flow word (`return`,
|
|
25
|
+
// `exit`, `continue`, `break`, `true`, `false`, `:`) with an optional
|
|
26
|
+
// status (a number, `$?` or a variable), then nothing but a `;` or the `;;`
|
|
27
|
+
// that ends a case arm, as in `[[ -f $x ]] || return 1`. One guard per
|
|
28
|
+
// line, the one at its end: `x && return 0 || return 1` is a choice, and
|
|
29
|
+
// its `&&` counts. JavaScript has no such idiom, so shell alone is
|
|
30
|
+
// exempted.
|
|
31
|
+
// Matched against the trimmed end of the code, and anchored there, so a long run of spaces costs nothing.
|
|
32
|
+
const SHELL_GUARD = /(?:\|\||&&)\s*(?:return|exit|continue|break|true|false|:)(?:\s+(?:\$\?|\$\{?\w+\}?|\d+))?\s*;{0,2}$/
|
|
33
|
+
// `<<WORD`, `<<-WORD`, `<<'WORD'`, `<<"WORD"` or `<<\WORD` outside quotes and
|
|
34
|
+
// outside `(( ))`; `<<<` is a here-string and `<<` in arithmetic a shift.
|
|
35
|
+
const HEREDOC = /^<<(-?)\s*(?:(['"])([A-Za-z_][\w.-]*)\2|\\?([A-Za-z_][\w.-]*))/
|
|
36
|
+
// The line that closes a shell function: `}` alone, or `}` with a comment or a redirection after it.
|
|
37
|
+
const SHELL_END = /^\}\s*(?:#.*|[<>&|].*)?$/
|
|
15
38
|
const PY_BRANCH = /^\s*(?:if|elif|for|while|except|with)\b|\band\b|\bor\b/
|
|
16
39
|
|
|
17
40
|
/**
|
|
@@ -91,6 +114,105 @@ function jsFunctions(file) {
|
|
|
91
114
|
return rows
|
|
92
115
|
}
|
|
93
116
|
|
|
117
|
+
/**
|
|
118
|
+
* One shell line read left to right with a stack of contexts: a
|
|
119
|
+
* single-quoted string, a double-quoted string, an ANSI-C `$'...'` string,
|
|
120
|
+
* and, nested in a double-quoted string, `$(...)`, `${...}` and a
|
|
121
|
+
* backtick substitution, which is what lets `"$(printf "it's")"` and
|
|
122
|
+
* `"${x:-"it's"}"` read their inner quotes as their own. It returns the
|
|
123
|
+
* code before a `#` that starts a comment, the heredoc the line opens, and
|
|
124
|
+
* the contexts left open at its end. Quoted text is data and left out of
|
|
125
|
+
* the code, whether the quote closes on the line or spans lines (the
|
|
126
|
+
* heredoc delimiter is read here, before the quotes go, so `<<'PY'` and
|
|
127
|
+
* `<<PY` read alike), while what a substitution holds is shell and kept. A `'` inside double quotes
|
|
128
|
+
* ("Okomart's") and a `#` inside quotes (`*'#'*`) are text; a backslash
|
|
129
|
+
* escapes in code, inside double quotes and inside `$'...'`, and inside
|
|
130
|
+
* plain single quotes nothing does. `<<` in shell, at the top or inside a
|
|
131
|
+
* substitution, and outside `(( ))`, is a heredoc.
|
|
132
|
+
* @param {string} line
|
|
133
|
+
* @param {Array<{ kind: string, depth: number }>} open the contexts open from the line above, innermost last
|
|
134
|
+
* @returns {{ code: string, open: Array<{ kind: string, depth: number }>, heredoc: { word: string, strip: boolean } | null }}
|
|
135
|
+
*/
|
|
136
|
+
function scanShellLine(line, open) {
|
|
137
|
+
const stack = open.map((entry) => ({ ...entry }))
|
|
138
|
+
let code = ""
|
|
139
|
+
let heredoc = null
|
|
140
|
+
// Depth of `((` arithmetic on this line, inside which `<<` is a shift.
|
|
141
|
+
let arith = 0
|
|
142
|
+
const top = () => stack[stack.length - 1] || null
|
|
143
|
+
const isCode = (kind) => kind === "code" || kind === "$(" || kind === "${" || kind === "`"
|
|
144
|
+
// Text inside a string is data; text in shell, nested or not, is kept.
|
|
145
|
+
const keep = () => {
|
|
146
|
+
const inner = top()
|
|
147
|
+
return !inner || isCode(inner.kind)
|
|
148
|
+
}
|
|
149
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
150
|
+
const ch = line[i]
|
|
151
|
+
const context = top()
|
|
152
|
+
const kind = context ? context.kind : "code"
|
|
153
|
+
if (kind === "'") {
|
|
154
|
+
if (ch === "'") stack.pop()
|
|
155
|
+
else if (keep()) code += ch
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
if (ch === "\\") {
|
|
159
|
+
i += 1
|
|
160
|
+
if (keep()) code += ch + (line[i] ?? "")
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
if (kind === '"' || kind === "$'") {
|
|
164
|
+
if (ch === kind[kind.length - 1]) stack.pop()
|
|
165
|
+
else if (kind === '"' && ch === "$" && (line[i + 1] === "(" || line[i + 1] === "{")) {
|
|
166
|
+
stack.push({ kind: `$${line[i + 1]}`, depth: 0 })
|
|
167
|
+
code += `$${line[i + 1]}`
|
|
168
|
+
i += 1
|
|
169
|
+
// `$((` under a quote is arithmetic: its `<<` is a shift.
|
|
170
|
+
if (line[i] === "(" && line[i + 1] === "(") arith += 1
|
|
171
|
+
} else if (kind === '"' && ch === "`") {
|
|
172
|
+
stack.push({ kind: "`", depth: 0 })
|
|
173
|
+
code += ch
|
|
174
|
+
} else if (keep()) code += ch
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
// Shell code: at the top, or inside a substitution under a double quote.
|
|
178
|
+
if (kind === "$(" || kind === "${") {
|
|
179
|
+
const [opener, closer] = kind === "$(" ? ["(", ")"] : ["{", "}"]
|
|
180
|
+
if (ch === opener) context.depth += 1
|
|
181
|
+
else if (ch === closer) {
|
|
182
|
+
if (context.depth === 0) {
|
|
183
|
+
stack.pop()
|
|
184
|
+
code += ch
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
context.depth -= 1
|
|
188
|
+
}
|
|
189
|
+
} else if (kind === "`" && ch === "`") {
|
|
190
|
+
stack.pop()
|
|
191
|
+
code += ch
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
if (ch === "#" && (i === 0 || /[\s;()&|]/.test(line[i - 1]))) break
|
|
195
|
+
if (ch === "'" || ch === '"') {
|
|
196
|
+
stack.push({ kind: ch === "'" && line[i - 1] === "$" ? "$'" : ch, depth: 0 })
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
if (ch === "(" && line[i + 1] === "(") arith += 1
|
|
200
|
+
else if (ch === ")" && line[i + 1] === ")" && arith) arith -= 1
|
|
201
|
+
if (ch === "<" && line[i + 1] === "<" && line[i - 1] !== "<" && line[i + 2] !== "<" && !heredoc && !arith) {
|
|
202
|
+
const found = line.slice(i).match(HEREDOC)
|
|
203
|
+
if (found) heredoc = { word: found[3] || found[4], strip: found[1] === "-" }
|
|
204
|
+
}
|
|
205
|
+
code += ch
|
|
206
|
+
}
|
|
207
|
+
return { code, open: stack.map(({ kind, depth }) => ({ kind, depth })), heredoc }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The code with any guard tail removed, so what is left is what SHELL_BRANCH reads. */
|
|
211
|
+
function withoutGuards(code) {
|
|
212
|
+
// The one guard at the end: `a || b && return` is one guard over a real `||`.
|
|
213
|
+
return code.trimEnd().replace(SHELL_GUARD, "").trimEnd()
|
|
214
|
+
}
|
|
215
|
+
|
|
94
216
|
function shellFunctions(file) {
|
|
95
217
|
const lines = file.text.split("\n")
|
|
96
218
|
const rows = []
|
|
@@ -104,18 +226,37 @@ function shellFunctions(file) {
|
|
|
104
226
|
let depth = 0
|
|
105
227
|
let deepest = 0
|
|
106
228
|
let branches = 0
|
|
229
|
+
// Data inside the function is not shell: the body of a heredoc up to its
|
|
230
|
+
// delimiter alone on a line (leading tabs allowed after `<<-`), and
|
|
231
|
+
// quoted text, on one line or spanning lines (an awk or python program
|
|
232
|
+
// in single quotes, a remote command in double quotes). Those count
|
|
233
|
+
// toward the length and toward nothing else; the shell around them,
|
|
234
|
+
// and inside a substitution nested in them, is read. Two heredocs on
|
|
235
|
+
// one line: the first is tracked, the second's body is read as shell.
|
|
236
|
+
let heredoc = null
|
|
237
|
+
let open = []
|
|
107
238
|
for (let at = index + 1; at < lines.length; at += 1) {
|
|
108
239
|
const line = lines[at]
|
|
109
|
-
if (
|
|
240
|
+
if (heredoc) {
|
|
241
|
+
if ((heredoc.strip ? line.replace(/^\t+/, "") : line) === heredoc.word) heredoc = null
|
|
110
242
|
end = at
|
|
111
|
-
|
|
243
|
+
continue
|
|
112
244
|
}
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
245
|
+
if (!open.length && SHELL_END.test(line.trim()) && line.startsWith(indent) && line.match(/^\s*/)[0].length === indent.length) {
|
|
246
|
+
end = at
|
|
247
|
+
break
|
|
116
248
|
}
|
|
117
|
-
|
|
118
|
-
|
|
249
|
+
const scanned = scanShellLine(line, open)
|
|
250
|
+
const code = withoutGuards(scanned.code)
|
|
251
|
+
open = scanned.open
|
|
252
|
+
heredoc = scanned.heredoc
|
|
253
|
+
// A line that opens inside a string is read only after the string closes: no `if` at its start, only what the code holds.
|
|
254
|
+
// The line's net over its code: `if x; then y; fi` on one line is no level.
|
|
255
|
+
const net = (code.match(SHELL_OPEN) || []).length - (code.match(SHELL_CLOSE) || []).length
|
|
256
|
+
// Never below the body: a close the scanner misread cannot hide every later level.
|
|
257
|
+
depth = Math.max(0, depth + net)
|
|
258
|
+
if (depth > deepest) deepest = depth
|
|
259
|
+
if (SHELL_BRANCH.test(code)) branches += 1
|
|
119
260
|
end = at
|
|
120
261
|
}
|
|
121
262
|
rows.push({ file: file.path, line: index + 1, name, kind: "function", lines: end - index + 1, depth: deepest, branches })
|
|
@@ -124,11 +265,63 @@ function shellFunctions(file) {
|
|
|
124
265
|
return rows
|
|
125
266
|
}
|
|
126
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Opening brackets minus closing ones on a line, outside string literals
|
|
270
|
+
* and comments, carrying the state of a triple-quoted string across lines
|
|
271
|
+
* so a bracket inside a docstring or an SQL text counts nothing; and the
|
|
272
|
+
* code of the line with its strings and comment removed, so `and`, `or`
|
|
273
|
+
* and `if` in prose are not branches.
|
|
274
|
+
* @param {string} line
|
|
275
|
+
* @param {string|null} triple the triple quote open from the line above, or null
|
|
276
|
+
* @returns {{ balance: number, triple: string|null, continued: boolean, code: string }} `continued` when the line ends in a backslash outside a string
|
|
277
|
+
*/
|
|
278
|
+
function bracketBalance(line, triple) {
|
|
279
|
+
let balance = 0
|
|
280
|
+
let quote = triple
|
|
281
|
+
let code = ""
|
|
282
|
+
let i = 0
|
|
283
|
+
for (; i < line.length; i += 1) {
|
|
284
|
+
const ch = line[i]
|
|
285
|
+
if (quote) {
|
|
286
|
+
if (ch === "\\") i += 1
|
|
287
|
+
else if (quote.length === 3 ? line.startsWith(quote, i) : ch === quote) {
|
|
288
|
+
i += quote.length - 1
|
|
289
|
+
quote = null
|
|
290
|
+
}
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
if (ch === "#") break
|
|
294
|
+
if (ch === '"' || ch === "'") {
|
|
295
|
+
quote = line.startsWith(ch.repeat(3), i) ? ch.repeat(3) : ch
|
|
296
|
+
i += quote.length - 1
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
if (ch === "(" || ch === "[" || ch === "{") balance += 1
|
|
300
|
+
else if (ch === ")" || ch === "]" || ch === "}") balance -= 1
|
|
301
|
+
code += ch
|
|
302
|
+
}
|
|
303
|
+
// A single quote never spans a line; a triple one does.
|
|
304
|
+
const open = quote && quote.length === 3 ? quote : null
|
|
305
|
+
return { balance, triple: open, continued: !open && /\\$/.test(code.trimEnd()), code }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const PY_DEF = /^(\s*)(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/
|
|
309
|
+
const PY_CLOSER = /^\s*[)\]}]/
|
|
310
|
+
|
|
127
311
|
function pythonFunctions(file) {
|
|
128
312
|
const lines = file.text.split("\n")
|
|
129
313
|
const rows = []
|
|
314
|
+
// Whether each line starts inside a triple-quoted string, over the whole
|
|
315
|
+
// file, so a `def` quoted in a docstring's example is not a function.
|
|
316
|
+
const quoted = new Array(lines.length)
|
|
317
|
+
let moduleTriple = null
|
|
318
|
+
for (let at = 0; at < lines.length; at += 1) {
|
|
319
|
+
quoted[at] = moduleTriple !== null
|
|
320
|
+
moduleTriple = bracketBalance(lines[at], moduleTriple).triple
|
|
321
|
+
}
|
|
130
322
|
for (let index = 0; index < lines.length; index += 1) {
|
|
131
|
-
|
|
323
|
+
if (quoted[index]) continue
|
|
324
|
+
const head = lines[index].match(PY_DEF)
|
|
132
325
|
if (!head) continue
|
|
133
326
|
const base = head[1].length
|
|
134
327
|
let end = index
|
|
@@ -140,18 +333,49 @@ function pythonFunctions(file) {
|
|
|
140
333
|
// PEP 8, 2 in some trees); 4 is assumed when there is no body line.
|
|
141
334
|
let unit = 4
|
|
142
335
|
let first = true
|
|
336
|
+
// A line that starts while a bracket is open, inside a triple-quoted
|
|
337
|
+
// string, or after a line ending in a backslash is a continuation of
|
|
338
|
+
// the statement above it (the arguments of a multi-line call, a
|
|
339
|
+
// docstring, a split condition): it counts toward the length and the
|
|
340
|
+
// branches, never toward depth, and never sets the unit. The def's own
|
|
341
|
+
// parameter list, when it spans lines, is a continuation of the def.
|
|
342
|
+
// The balance is over `(`, `[` and `{` minus their closers, outside
|
|
343
|
+
// string literals, the way braceDepth skips quotes. A miscount cannot
|
|
344
|
+
// run past the function: a bracket or backslash continuation ends at
|
|
345
|
+
// the first line at the def's indent or shallower that is not a
|
|
346
|
+
// closing bracket, whatever the balance says; a triple-quoted string
|
|
347
|
+
// runs to its close, since an SQL or help text inside it may sit at
|
|
348
|
+
// column 0.
|
|
349
|
+
let state = bracketBalance(lines[index], null)
|
|
350
|
+
let balance = Math.max(0, state.balance)
|
|
351
|
+
let triple = state.triple
|
|
352
|
+
let continued = state.continued
|
|
143
353
|
for (let at = index + 1; at < lines.length; at += 1) {
|
|
144
354
|
const line = lines[at]
|
|
145
355
|
if (!line.trim()) continue
|
|
146
356
|
const indent = line.match(/^\s*/)[0].length
|
|
357
|
+
const continuation = balance > 0 || triple !== null || continued
|
|
358
|
+
if (continuation && triple === null && indent <= base && !PY_CLOSER.test(line)) break
|
|
359
|
+
state = bracketBalance(line, triple)
|
|
360
|
+
if (continuation) {
|
|
361
|
+
balance = Math.max(0, balance + state.balance)
|
|
362
|
+
triple = state.triple
|
|
363
|
+
continued = state.continued
|
|
364
|
+
if (PY_BRANCH.test(state.code)) branches += 1
|
|
365
|
+
end = at
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
147
368
|
if (indent <= base) break
|
|
369
|
+
balance = Math.max(0, state.balance)
|
|
370
|
+
triple = state.triple
|
|
371
|
+
continued = state.continued
|
|
148
372
|
if (first) {
|
|
149
373
|
unit = indent - base
|
|
150
374
|
first = false
|
|
151
375
|
}
|
|
152
376
|
const level = Math.max(0, Math.floor((indent - base) / unit) - 1)
|
|
153
377
|
if (level > deepest) deepest = level
|
|
154
|
-
if (PY_BRANCH.test(
|
|
378
|
+
if (PY_BRANCH.test(state.code)) branches += 1
|
|
155
379
|
end = at
|
|
156
380
|
}
|
|
157
381
|
rows.push({ file: file.path, line: index + 1, name: head[2], kind: "function", lines: end - index + 1, depth: deepest, branches })
|
|
@@ -143,8 +143,9 @@ export async function inspectPlugin({ repoRoot, target, offline = false, allowDi
|
|
|
143
143
|
// 100 functions in listed trees stay under; never a judgement.
|
|
144
144
|
size: {
|
|
145
145
|
measurement: SIZE.measurement,
|
|
146
|
-
// The listed trees' own heavy shares,
|
|
147
|
-
// the
|
|
146
|
+
// The listed trees' own heavy shares, in the record's row order and
|
|
147
|
+
// only for the trees with a function, so the score can be read from
|
|
148
|
+
// the document alone.
|
|
148
149
|
sample: { trees: SIZE.trees, functions: SIZE.functions, heavyShares: [...SIZE.distribution.heavyShare] },
|
|
149
150
|
thresholds: { lines: SIZE.lines, branches: SIZE.branches, depth: SIZE.depth },
|
|
150
151
|
// The share of this tree's function lines inside functions over the
|
|
@@ -98,14 +98,23 @@ export function buildRecord(trees, { fetchFailed = [], date = new Date().toISOSt
|
|
|
98
98
|
medianLines: median(tree.functions.map((row) => row.lines)),
|
|
99
99
|
functionLines,
|
|
100
100
|
heavyLines,
|
|
101
|
-
|
|
101
|
+
// null, not 0, with no function: a tree with nothing to measure did not measure light.
|
|
102
|
+
heavyShare: functionLines ? Math.round((heavyLines / functionLines) * 10000) / 10000 : null,
|
|
102
103
|
}
|
|
103
104
|
})
|
|
104
105
|
return {
|
|
105
106
|
measurement: "M12",
|
|
106
107
|
date,
|
|
107
108
|
command: "node tools/inspect/measure-functions.mjs",
|
|
108
|
-
method:
|
|
109
|
+
method: [
|
|
110
|
+
`extractFunctions from tools/inspect/functions.mjs over the first ${trees.length} distinct repositories in the pinned catalog's order whose listing is community, laid out as a root plugin and carrying a validated commit (the 2026-09-15 inspect record's rule), each fetched read-only at that commit in reviewer mode.`,
|
|
111
|
+
"A function is a `function name(`, a named arrow function (`const load = (rows) => {`, `this.load = rows => {`), a method shorthand `load(rows) {` inside an object literal or a class, or a multi-line `onSomething: {` handler in QML and JavaScript; a `name() {` or `function name` block in shell; a `def` in Python. Anonymous callbacks are not counted.",
|
|
112
|
+
"Lines are first to last line inclusive; depth is the deepest nesting below the body, the body itself at 0 in every language, a brace that opens an object or array literal or an inline arrow body not a level; branches count if, else if, for, while, switch, case, catch, &&, || and ?: (their shell and Python equivalents).",
|
|
113
|
+
"In shell, a `||` or `&&` followed by one flow word (return, exit, continue, break, true, false, :) with an optional status (a number, `$?` or a variable) and nothing else on the line but a `;` or `;;` is a guard and not a branch, one per line at its end, so `x && return 0 || return 1` keeps its `&&`; the body of a heredoc (`<<WORD`, `<<-WORD`, quoted or backslashed, outside quotes and outside arithmetic) up to its delimiter alone on a line, and a quoted string that spans lines from its opening quote to the line that closes it, count toward the length and toward nothing else, while a `$(...)`, `${...}` or backtick substitution nested in a string reads its own quotes and, spanning lines, is shell and read as shell; a case arm is a `)` with text after it on a line with no `(` before it but its own opening one; a block opens with `if`, `for`, `while`, `until`, `case` or `select` and closes with `fi`, `done` or `esac`, each counted only where a statement can start over the line with its quoted text removed, so a block that opens and closes on one line is no level and a keyword inside a string or as an argument is nothing.",
|
|
114
|
+
"In Python, a line that starts while a bracket is open, inside a triple-quoted string, or after a line ending in a backslash is a continuation of the statement above it, and a def's own parameter list spanning lines is a continuation of the def: it counts toward the length and the branches and never toward nesting; a bracket or backslash continuation ends at the first line at the def's indent that is not a closing bracket, a triple-quoted string at its close. Branch words inside strings, docstrings and comments are prose, not branches, and a def quoted in a docstring is not a function.",
|
|
115
|
+
"Quantiles are nearest-rank over every function in the sample pooled, not per tree. `distribution` is the histogram of each measure over the same functions, value to count, from which a function's percentile rank is read.",
|
|
116
|
+
"Per tree, functionLines is the lines inside every function, heavyLines the lines inside functions over any p90 threshold of this record, and heavyShare their ratio, null with no function; the size score of omakit inspect is a tree's position among the non-null heavyShares.",
|
|
117
|
+
].join(" "),
|
|
109
118
|
sample: { trees: trees.length, functions: pooled.length, fetchFailed },
|
|
110
119
|
quantiles,
|
|
111
120
|
distribution: { lines: histogram(lines), branches: histogram(branches), depth: histogram(depth) },
|
|
@@ -114,7 +123,7 @@ export function buildRecord(trees, { fetchFailed = [], date = new Date().toISOSt
|
|
|
114
123
|
}
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
const PREVIOUS = "Re-measured on 2026-09-16 after three extraction fixes, so the quantiles are not comparable with the 0.4.3 record (this file at commit 6619c26: 50 trees, 6040 functions, p90 22 lines, 6 branches, nesting 3; and before it 18 trees, 715 functions, p90 18 lines, 7 branches, nesting 2): a Python body now starts at depth 0 the way a brace body does, where it started at 1; a brace that opens an object or array literal is no longer a nesting level; and a named arrow function or a method shorthand is a function, where only `function name(` and `onSomething: {` were."
|
|
126
|
+
const PREVIOUS = "Re-measured on 2026-09-16 after three extraction fixes, so the quantiles are not comparable with the 0.4.3 record (this file at commit 6619c26: 50 trees, 6040 functions, p90 22 lines, 6 branches, nesting 3; and before it 18 trees, 715 functions, p90 18 lines, 7 branches, nesting 2): a Python body now starts at depth 0 the way a brace body does, where it started at 1; a brace that opens an object or array literal is no longer a nesting level; and a named arrow function or a method shorthand is a function, where only `function name(` and `onSomething: {` were. Re-measured again for 0.5.1 after more (the 0.5.0 record is this file at commit 7df451a: 6041 functions, p90 22 lines, 6 branches, nesting 2, six heavyShares of 0 among 50): a Python continuation line inside an open bracket, a triple-quoted string or after a backslash no longer counts as nesting, and a def whose parameter list spans lines is read through to its body where before only the signature was, which is what moved the line p90 from 22 to 25; branch words in Python strings and docstrings are prose; a shell heredoc body and a quoted program spanning lines are no longer read as shell; a shell guard (`|| return 1`) is no longer a branch; and a tree with no function carries heavyShare null instead of 0, so it no longer lifts every other tree's rank. The earlier 0.5.1 cuts, never released, are in this file's history, each corrected by the next: one read a `\"` inside a string and a `\"\"\"` inside a bracket wrongly and stretched two listed Python functions (101 to 133 and 128 to 162 lines); one still read `<<` in arithmetic and a quote inside `$(...)` inside a string wrongly, and counted docstring prose as branches; one counted `if x; then y; fi` on one line as a nesting level; and one closed a block on `done` as a plain argument and peeled two guards from one line."
|
|
118
127
|
|
|
119
128
|
export async function measureFunctions(repoRoot, { count = TREES, cacheRoot = omakitCacheDir(), log = () => {} } = {}) {
|
|
120
129
|
const pin = requirePin(repoRoot)
|
|
@@ -89,22 +89,23 @@ function secretLogs(files) {
|
|
|
89
89
|
*/
|
|
90
90
|
export const SIZE = Object.freeze({
|
|
91
91
|
measurement: "M12",
|
|
92
|
-
sample: "50 listed trees,
|
|
92
|
+
sample: "50 listed trees, 6034 functions",
|
|
93
93
|
trees: 50,
|
|
94
|
-
functions:
|
|
95
|
-
lines:
|
|
94
|
+
functions: 6034,
|
|
95
|
+
lines: 25,
|
|
96
96
|
branches: 6,
|
|
97
97
|
depth: 2,
|
|
98
|
-
// The histogram of each measure over the
|
|
98
|
+
// The histogram of each measure over the 6034 functions, value to count,
|
|
99
99
|
// from which a function's percentile rank among listed functions is read;
|
|
100
100
|
// and each listed tree's heavyShare, the share of its function lines in
|
|
101
|
-
// functions over the thresholds, in the record's row order
|
|
102
|
-
// a tree
|
|
101
|
+
// functions over the thresholds, in the record's row order and only for
|
|
102
|
+
// the trees with a function (a tree with nothing to measure has no
|
|
103
|
+
// share), from which a tree's position among listed trees is read.
|
|
103
104
|
distribution: Object.freeze({
|
|
104
|
-
lines: Object.freeze({ 1: 330, 2:
|
|
105
|
-
branches: Object.freeze({ 0:
|
|
106
|
-
depth: Object.freeze({ 0:
|
|
107
|
-
heavyShare: Object.freeze([0.421, 0.3361, 0.4049, 0, 0.
|
|
105
|
+
lines: Object.freeze({ 1: 330, 2: 157, 3: 667, 4: 620, 5: 584, 6: 508, 7: 413, 8: 337, 9: 301, 10: 238, 11: 195, 12: 138, 13: 152, 14: 131, 15: 106, 16: 75, 17: 86, 18: 62, 19: 71, 20: 57, 21: 51, 22: 56, 23: 47, 24: 44, 25: 33, 26: 28, 27: 24, 28: 26, 29: 27, 30: 17, 31: 21, 32: 22, 33: 18, 34: 23, 35: 11, 36: 5, 37: 16, 38: 13, 39: 13, 40: 9, 41: 13, 42: 12, 43: 11, 44: 9, 45: 8, 46: 14, 47: 8, 48: 11, 49: 7, 50: 6, 51: 1, 52: 2, 53: 6, 54: 3, 55: 7, 56: 4, 57: 3, 58: 5, 59: 3, 60: 8, 61: 5, 62: 3, 63: 6, 64: 2, 65: 3, 66: 1, 67: 3, 68: 4, 69: 9, 70: 7, 71: 7, 72: 3, 73: 3, 74: 3, 75: 1, 76: 1, 77: 2, 78: 2, 79: 3, 80: 3, 81: 4, 82: 3, 83: 1, 85: 1, 86: 3, 87: 3, 88: 1, 89: 1, 90: 3, 91: 2, 92: 2, 93: 2, 94: 1, 96: 3, 97: 2, 98: 1, 99: 1, 100: 1, 101: 3, 102: 2, 104: 2, 107: 2, 108: 1, 109: 2, 110: 2, 111: 2, 112: 2, 113: 2, 115: 3, 116: 1, 117: 2, 119: 1, 120: 1, 121: 1, 125: 1, 126: 1, 128: 2, 129: 1, 133: 1, 134: 1, 142: 1, 144: 1, 145: 1, 154: 1, 155: 1, 161: 2, 173: 1, 217: 2, 253: 1, 272: 1, 277: 1, 290: 1, 292: 1, 298: 1, 325: 1, 365: 1, 495: 1 }),
|
|
106
|
+
branches: Object.freeze({ 0: 2302, 1: 1130, 2: 828, 3: 457, 4: 344, 5: 221, 6: 180, 7: 127, 8: 83, 9: 70, 10: 48, 11: 48, 12: 24, 13: 30, 14: 20, 15: 10, 16: 14, 17: 10, 18: 12, 19: 8, 20: 6, 21: 4, 22: 5, 23: 5, 24: 7, 25: 4, 26: 2, 27: 7, 28: 5, 29: 2, 31: 1, 32: 2, 33: 2, 35: 1, 36: 3, 38: 1, 39: 1, 40: 1, 41: 1, 43: 1, 45: 2, 55: 1, 64: 1, 75: 1, 78: 1, 85: 1 }),
|
|
107
|
+
depth: Object.freeze({ 0: 3854, 1: 1478, 2: 482, 3: 140, 4: 47, 5: 27, 6: 5, 7: 1 }),
|
|
108
|
+
heavyShare: Object.freeze([0.421, 0.3361, 0.4049, 0, 0.2979, 0.3647, 0.4107, 0.6167, 0.2635, 0.8434, 0, 0, 0.193, 0.6399, 0.4063, 0.4769, 0.4322, 0.5337, 0.3628, 0.6626, 0.0554, 0.7775, 0.3548, 0.3285, 0, 0.6801, 0.2222, 0.3084, 0.3705, 0.6468, 0.3928, 0.6786, 0.3247, 0.5225, 0, 0.1462, 0, 0.5817, 0.5891, 0.3835, 0.2491, 0.5627, 0.2439, 0.3643, 0.6719, 0.6525, 0.4385, 0.565, 0.5672]),
|
|
108
109
|
}),
|
|
109
110
|
})
|
|
110
111
|
|
|
@@ -142,13 +143,14 @@ export function heavyShare(functions) {
|
|
|
142
143
|
|
|
143
144
|
/**
|
|
144
145
|
* The share of listed trees whose heavyShare is strictly smaller than this
|
|
145
|
-
* one, as a percentage over the trees
|
|
146
|
-
* no function over the thresholds, 100 for one heavier than
|
|
147
|
-
* tree.
|
|
146
|
+
* one, as a percentage over the listed trees that have a share: 0 for a
|
|
147
|
+
* tree with no function over the thresholds, 100 for one heavier than
|
|
148
|
+
* every listed tree.
|
|
148
149
|
*/
|
|
149
150
|
export function treeRank(share) {
|
|
150
|
-
const
|
|
151
|
-
|
|
151
|
+
const shares = SIZE.distribution.heavyShare
|
|
152
|
+
const below = shares.filter((listed) => listed < share).length
|
|
153
|
+
return Math.round((below / shares.length) * 1000) / 10
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
/**
|
package/tools/inspect/report.mjs
CHANGED
|
@@ -35,11 +35,17 @@ function percent(share) {
|
|
|
35
35
|
return `${rounded}%`
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* The score sentence after the number: the share, then the position among
|
|
40
|
+
* the listed trees the document itself carries. The rank counts the listed
|
|
41
|
+
* trees with a strictly smaller share, so the tree is "no heavier than"
|
|
42
|
+
* the rest: a tie is level, not lighter.
|
|
43
|
+
*/
|
|
39
44
|
function scoreText(size) {
|
|
40
45
|
const shares = size.sample.heavyShares
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
if (!shares.length) return `${percent(size.heavyShare)} of its function lines sit in functions over the measured size; no listed tree to place it among (${size.measurement})`
|
|
47
|
+
const lighter = shares.filter((share) => share < size.heavyShare).length
|
|
48
|
+
return `${percent(size.heavyShare)} of its function lines sit in functions over the measured size, no heavier than ${shares.length - lighter} of ${shares.length} listed trees (${size.measurement})`
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
/** Under --allow-dirty: what the checkout holds that the tree at the commit does not. */
|
|
@@ -271,7 +277,12 @@ export function renderInspect(document, { colour = colourEnabled(), full = false
|
|
|
271
277
|
if (!ranked.length && !over.length) {
|
|
272
278
|
out.push(...field("attention", `nothing: no function over the size of ${SIZE.sample} (${SIZE.measurement}), and none of the ${PATTERNS.length} classes reviewers raise shows in this tree (${PATTERNS[0]?.measurement || "M11"})`, c))
|
|
273
279
|
} else {
|
|
274
|
-
|
|
280
|
+
const classes = shown.length
|
|
281
|
+
? `${plural(shown.length, "class", "classes")} reviewers raise, biggest first by share of review findings (${PATTERNS[0].measurement}); up to ${SHOWN_SITES} sites each`
|
|
282
|
+
: below.length
|
|
283
|
+
? `only classes under ${Math.round(MIN_SHARE * 100)}% of review findings show (${PATTERNS[0].measurement}), counted below`
|
|
284
|
+
: `none of the classes reviewers raise shows in this tree (${PATTERNS[0].measurement})`
|
|
285
|
+
out.push(...field("attention", `${over.length ? `long functions first, by length${shown.length ? ", then " : "; "}` : ""}${classes}`, c))
|
|
275
286
|
}
|
|
276
287
|
// Long functions first: what the person asked about, so the order is a
|
|
277
288
|
// preference and the heading says whose thresholds it uses.
|