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 CHANGED
@@ -6,6 +6,20 @@ The marketplace validates one exact commit of your plugin. Push a fix or comment
6
6
 
7
7
  [![Built for Omarchy: App](https://raw.githubusercontent.com/tcballard/omarchy-badges/75975e5b5bf75e7ede3764bcd2950046f7abfe2c/badges/v1/omarchy-app.svg)](https://github.com/tcballard/omarchy-badges) [![npm version](https://img.shields.io/npm/v/omakit)](https://www.npmjs.com/package/omakit) [![CI status](https://img.shields.io/github/actions/workflow/status/mtolhuys/omakit/ci.yml?branch=main)](https://github.com/mtolhuys/omakit/actions/workflows/ci.yml) [![Socket](https://socket.dev/api/badge/npm/package/omakit)](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
- ![inspect showing a size score and the two review classes one fixture shows](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/inspect.gif)
54
+ ![inspect showing a fixture's size score, its two long functions with their ranks, and the one review class it shows](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/inspect.gif)
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 50 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)).
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.0",
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' shares). It
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 !== 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")
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
- const SHELL_OPEN = /^\s*(?:if|for|while|until|case|select)\b/
13
- const SHELL_CLOSE = /^\s*(?:fi|done|esac)\b/
14
- const SHELL_BRANCH = /\b(?:if|elif|for|while|until|case)\b|\|\||&&|^\s*[^)]*\)\s*(?!\s*$)/
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 (line.trim() === "}" && line.startsWith(indent) && line.match(/^\s*/)[0].length === indent.length) {
240
+ if (heredoc) {
241
+ if ((heredoc.strip ? line.replace(/^\t+/, "") : line) === heredoc.word) heredoc = null
110
242
  end = at
111
- break
243
+ continue
112
244
  }
113
- if (SHELL_OPEN.test(line)) {
114
- depth += 1
115
- if (depth > deepest) deepest = depth
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
- if (SHELL_CLOSE.test(line)) depth -= 1
118
- if (SHELL_BRANCH.test(line.split("#")[0])) branches += 1
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
- const head = lines[index].match(/^(\s*)(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/)
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(line)) branches += 1
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, so the score can be read from
147
- // the document alone: `heavyShares[i]` is the i-th listed tree's.
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
- heavyShare: functionLines ? Math.round((heavyLines / functionLines) * 10000) / 10000 : 0,
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: `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. 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. 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). 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. 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, 0 with no function; the size score of omakit inspect is a tree's position among these 50 heavyShares.`,
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, 6041 functions",
92
+ sample: "50 listed trees, 6034 functions",
93
93
  trees: 50,
94
- functions: 6041,
95
- lines: 22,
94
+ functions: 6034,
95
+ lines: 25,
96
96
  branches: 6,
97
97
  depth: 2,
98
- // The histogram of each measure over the 6041 functions, value to count,
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, from which
102
- // a tree's position among listed trees is read.
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: 185, 3: 682, 4: 659, 5: 629, 6: 515, 7: 420, 8: 339, 9: 301, 10: 235, 11: 192, 12: 138, 13: 150, 14: 129, 15: 104, 16: 75, 17: 84, 18: 64, 19: 70, 20: 48, 21: 51, 22: 46, 23: 45, 24: 42, 25: 27, 26: 25, 27: 16, 28: 24, 29: 24, 30: 17, 31: 18, 32: 23, 33: 18, 34: 19, 35: 10, 36: 5, 37: 11, 38: 12, 39: 12, 40: 9, 41: 10, 42: 12, 43: 9, 44: 8, 45: 7, 46: 14, 47: 8, 48: 11, 49: 8, 50: 6, 51: 2, 52: 2, 53: 6, 54: 2, 55: 7, 56: 4, 57: 3, 58: 5, 60: 6, 61: 2, 62: 3, 63: 6, 64: 1, 65: 3, 66: 1, 67: 3, 68: 4, 69: 8, 70: 5, 71: 6, 72: 2, 73: 2, 74: 1, 75: 1, 76: 1, 77: 1, 78: 1, 79: 3, 80: 1, 81: 3, 82: 1, 83: 1, 85: 1, 87: 1, 89: 1, 90: 2, 91: 2, 92: 1, 94: 1, 96: 2, 97: 2, 98: 1, 99: 1, 100: 1, 101: 1, 102: 2, 104: 1, 108: 1, 110: 2, 111: 1, 112: 1, 113: 1, 115: 2, 116: 1, 117: 1, 119: 1, 120: 1, 121: 1, 128: 2, 129: 1, 133: 1, 134: 1, 144: 1, 145: 1, 154: 1, 161: 1, 173: 1, 253: 1, 277: 1, 292: 1, 365: 1, 495: 1 }),
105
- branches: Object.freeze({ 0: 2336, 1: 1096, 2: 807, 3: 455, 4: 339, 5: 233, 6: 171, 7: 125, 8: 90, 9: 68, 10: 47, 11: 53, 12: 24, 13: 26, 14: 18, 15: 11, 16: 17, 17: 18, 18: 12, 19: 12, 20: 11, 21: 3, 22: 5, 23: 7, 24: 8, 25: 4, 26: 1, 27: 6, 28: 4, 29: 5, 31: 1, 32: 3, 33: 3, 34: 1, 35: 1, 36: 4, 37: 1, 38: 1, 40: 1, 41: 1, 43: 2, 44: 1, 45: 2, 46: 1, 64: 1, 75: 1, 78: 1, 85: 1, 87: 2 }),
106
- depth: Object.freeze({ 0: 3794, 1: 1421, 2: 481, 3: 155, 4: 80, 5: 62, 6: 27, 7: 10, 8: 5, 9: 2, 12: 1, 13: 1, 15: 1, 18: 1 }),
107
- heavyShare: Object.freeze([0.421, 0.3361, 0.4049, 0, 0.3687, 0.4469, 0.4107, 0.6167, 0.2635, 0.8434, 0, 0, 0.193, 0.7036, 0.4506, 0.4846, 0.5047, 0.5337, 0.3628, 0.682, 0.0554, 0.7775, 0.3548, 0.3552, 0, 0.7172, 0.2542, 0.3747, 0.4275, 0.6607, 0.4322, 0.6941, 0.3247, 0.5225, 0.2545, 0.1462, 0, 0.5817, 0.5891, 0.4353, 0, 0.3479, 0.6109, 0.296, 0.3882, 0.5624, 0.6783, 0.4385, 0.6078, 0.5826]),
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 of the M12 record: 0 for a tree with
146
- * no function over the thresholds, 100 for one heavier than every listed
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 below = SIZE.distribution.heavyShare.filter((listed) => listed < share).length
151
- return Math.round((below / SIZE.trees) * 1000) / 10
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
  /**
@@ -35,11 +35,17 @@ function percent(share) {
35
35
  return `${rounded}%`
36
36
  }
37
37
 
38
- /** The score sentence after the number: the share, then the position among the listed trees the document itself carries. */
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
- const rank = (shares.filter((share) => share < size.heavyShare).length / shares.length) * 100
42
- return `${percent(size.heavyShare)} of its function lines sit in functions over the measured size, less than ${100 - Math.round(rank)} of 100 listed trees (${size.measurement})`
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
- out.push(...field("attention", `${over.length ? `long functions first, by length, then ` : ""}${plural(shown.length, "class", "classes")} reviewers raise, biggest first by share of review findings (${PATTERNS[0].measurement}); up to ${SHOWN_SITES} sites each`, c))
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.