omakit 0.4.1 → 0.4.3
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 +32 -92
- package/package.json +1 -1
- package/skills/omarchy-plugin-check/SKILL.md +59 -3
- package/skills/omarchy-plugin-submit/SKILL.md +2 -2
- package/skills/omarchy-plugin-validation-watch/SKILL.md +4 -4
- package/tools/inspect/contract.mjs +245 -0
- package/tools/inspect/functions.mjs +129 -0
- package/tools/inspect/hosts.mjs +141 -0
- package/tools/inspect/inspect.mjs +170 -0
- package/tools/inspect/patterns.mjs +326 -0
- package/tools/inspect/processes.mjs +298 -0
- package/tools/inspect/report.mjs +342 -0
- package/tools/inspect/text.mjs +496 -0
- package/tools/inspect/timers.mjs +65 -0
- package/tools/inspect/walk.mjs +102 -0
- package/tools/inspect/writes.mjs +194 -0
- package/tools/marketplace/README.md +25 -2
- package/tools/marketplace/cli.mjs +84 -13
- package/tools/marketplace/github.mjs +13 -2
- package/tools/marketplace/local-transport.mjs +16 -4
- package/tools/marketplace/measure-review-cost.mjs +39 -0
- package/tools/marketplace/measure-staleness.mjs +76 -0
- package/tools/marketplace/options.mjs +1 -0
- package/tools/marketplace/pin.mjs +18 -10
- package/tools/marketplace/report.mjs +8 -1
- package/tools/marketplace/review-cost.mjs +107 -0
- package/tools/marketplace/run-baseline.mjs +2 -1
- package/tools/marketplace/style.mjs +3 -0
- package/tools/marketplace/submit.mjs +18 -2
- package/tools/marketplace/usage.mjs +21 -0
- package/tools/marketplace/verify.mjs +15 -4
- package/tools/marketplace/watch.mjs +64 -17
- package/tools/weigh/audit.mjs +21 -3
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// The report for a person, drawn only with the shared vocabulary of
|
|
2
|
+
// style.mjs: `░ info` for a fact, `▒ ?` for a fact the extraction could
|
|
3
|
+
// not read, `▓ note` for a pattern row, `▔ skip` for the baseline under
|
|
4
|
+
// --offline, and the one closing word INSPECTED. It never draws `▁ ok` or
|
|
5
|
+
// `█ FAIL`, because it has no verdict to attach them to. Every section
|
|
6
|
+
// line says what was observed, and a section with nothing in it says
|
|
7
|
+
// "observed nothing of this kind", never "clean".
|
|
8
|
+
//
|
|
9
|
+
// Two views of one document. The default is what needs attention, biggest
|
|
10
|
+
// first: one block per review class this tree shows, ordered by the
|
|
11
|
+
// class's share of review findings in the M11 sample, which is the one
|
|
12
|
+
// measured number that says how much reviewers care; under each, up to
|
|
13
|
+
// five sites with the fact at each in one line; classes under five percent
|
|
14
|
+
// counted on one line. Measured before this: a listed tree with four shell
|
|
15
|
+
// scripts printed 372 process rows of two lines each, and the two rows a
|
|
16
|
+
// reviewer would act on sat under 750 lines of argv. `--full` is the
|
|
17
|
+
// exhaustive view, every site of every kind with every qualifier; `--json`
|
|
18
|
+
// is the document itself, which carries everything either view shows.
|
|
19
|
+
|
|
20
|
+
import { colourEnabled, field, GUTTER, INSPECT_VERDICT, mark, outputColumns, styler, verdict, wrap } from "../marketplace/style.mjs"
|
|
21
|
+
import { withHomeAbbreviated } from "../marketplace/paths.mjs"
|
|
22
|
+
import { PATTERNS, SIZE } from "./patterns.mjs"
|
|
23
|
+
import { toolOf } from "./processes.mjs"
|
|
24
|
+
|
|
25
|
+
const NOTHING = "observed nothing of this kind"
|
|
26
|
+
|
|
27
|
+
const plural = (count, word, words = `${word}s`) => `${count} ${count === 1 ? word : words}`
|
|
28
|
+
const site = (row) => `${row.file}:${row.line}`
|
|
29
|
+
|
|
30
|
+
/** A row: a mark, the site in bold, then the text, wrapped under the gutter. */
|
|
31
|
+
function row(state, head, text, c, extra = []) {
|
|
32
|
+
const lines = wrap(text || "(nothing)", { indent: GUTTER, first: GUTTER + head.length + 2 }, c)
|
|
33
|
+
const out = [`${mark(state, c)}${c("name", head)} ${lines[0].trimStart()}`, ...lines.slice(1)]
|
|
34
|
+
for (const line of extra) out.push(...wrap(line, { indent: GUTTER }, c))
|
|
35
|
+
return out
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function processRow(process, c) {
|
|
39
|
+
if (process.argvForm === "computed") {
|
|
40
|
+
return row("unknown", site(process), `command: ${process.commandText}`, c, ["argv not resolvable statically"])
|
|
41
|
+
}
|
|
42
|
+
const notes = []
|
|
43
|
+
notes.push(`argv ${process.argvForm}${process.declaredIn === "shell" ? " (shell line)" : ""}${process.detached ? ", detached" : ""}`)
|
|
44
|
+
if (process.expressions.length) notes.push(`${plural(process.expressions.length, "element")} computed (${process.expressions.map((entry) => entry.text).join(", ")})`)
|
|
45
|
+
if (process.shellWrapper) notes.push("shell wrapper, the script inside is not followed")
|
|
46
|
+
if (process.pipedFrom) notes.push(`piped from ${process.pipedFrom.argv0} on line ${process.pipedFrom.line}`)
|
|
47
|
+
if (process.declaredIn === "qml" && !process.detached) {
|
|
48
|
+
const deadline = process.deadline
|
|
49
|
+
notes.push(deadline.observed
|
|
50
|
+
? `deadline observed (${deadline.via}${deadline.ms !== null ? ` ${deadline.ms} ms` : ""})`
|
|
51
|
+
: "no deadline observed")
|
|
52
|
+
const output = process.output
|
|
53
|
+
if (output.collector === "none") notes.push("no collector")
|
|
54
|
+
else notes.push(`output through ${output.collector}, ${output.capObserved ? `cap observed (${output.via})` : "no cap observed"}`)
|
|
55
|
+
} else if (process.deadline.observed) notes.push(`deadline observed (${process.deadline.via}${process.deadline.ms !== null ? ` ${process.deadline.ms} ms` : ""})`)
|
|
56
|
+
return row("info", site(process), argvText(process), c, [notes.join("; ")])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The argv as a list: literals quoted, a computed element as the source text it was read from. */
|
|
60
|
+
function argvText(process) {
|
|
61
|
+
const computed = new Set(process.expressions.map((entry) => entry.index))
|
|
62
|
+
return `[${process.argv.map((word, index) => (computed.has(index) ? word : JSON.stringify(word))).join(", ")}]`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hostRow(host, c) {
|
|
66
|
+
const via = host.tool ? `via ${host.tool}` : "no tool observed on the line"
|
|
67
|
+
const notes = [
|
|
68
|
+
host.timeout.observed ? `timeout observed (${host.timeout.via})` : "no timeout observed",
|
|
69
|
+
host.sizeCap.observed ? `size cap observed (${host.sizeCap.via})` : "no size cap observed",
|
|
70
|
+
]
|
|
71
|
+
if (host.tool === "curl") {
|
|
72
|
+
notes.push(host.flags.includes("-q") ? "-q observed" : "-q not observed")
|
|
73
|
+
notes.push(host.flags.includes("-L") || host.flags.includes("--location") ? "-L observed" : "-L not observed")
|
|
74
|
+
} else if (host.flags.length) notes.push(`flags ${host.flags.join(" ")}`)
|
|
75
|
+
if (host.privateAddress) notes.push("private or loopback address")
|
|
76
|
+
const lines = wrap(`${host.scheme} ${site(host)} ${via}`, { indent: GUTTER, first: GUTTER + host.host.length + 2 }, c)
|
|
77
|
+
return [`${mark("info", c)}${c("name", host.host)} ${lines[0].trimStart()}`, ...lines.slice(1), ...wrap(notes.join("; "), { indent: GUTTER }, c)]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeRow(write, c) {
|
|
81
|
+
const what = write.via === "FileView" ? `FileView path: ${write.path}` : `${write.via} ${write.path}`
|
|
82
|
+
const where = write.controlledDirectory === "observed"
|
|
83
|
+
? `observed (${write.controlledBy})`
|
|
84
|
+
: write.controlledDirectory === "not-observed"
|
|
85
|
+
? `not observed${write.temp ? ` (${write.canonicalPath.split("/").slice(0, 2).join("/")} is shared)` : ""}`
|
|
86
|
+
: "unknown (path not readable as a literal prefix)"
|
|
87
|
+
const notes = [`under a directory the plugin controls: ${where}`]
|
|
88
|
+
if (write.mode) notes.push(`mode ${write.mode}`)
|
|
89
|
+
return row("info", site(write), what, c, [notes.join("; ")])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function timerRow(timer, c) {
|
|
93
|
+
if (timer.intervalMs === null) {
|
|
94
|
+
return row("unknown", site(timer), `interval: ${timer.intervalText ?? "not declared"}`, c, ["interval not resolvable statically"])
|
|
95
|
+
}
|
|
96
|
+
const parts = [`interval ${timer.intervalMs} ms`]
|
|
97
|
+
parts.push(timer.repeat === true ? "repeat" : timer.repeat === false ? "single shot" : "repeat bound to an expression")
|
|
98
|
+
if (timer.running === true) parts.push("running")
|
|
99
|
+
else if (timer.running === null) parts.push("running bound to an expression")
|
|
100
|
+
if (timer.triggeredOnStart === true) parts.push("triggeredOnStart")
|
|
101
|
+
if (timer.startedBy) parts.push(`started by ${timer.startedBy}`)
|
|
102
|
+
else if (timer.running === false) parts.push("no start observed")
|
|
103
|
+
return row("info", site(timer), parts.join(", "), c)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function baselineLines(section, c) {
|
|
107
|
+
const out = []
|
|
108
|
+
if (section.skipped) {
|
|
109
|
+
out.push(...field("capabilities", "marketplace baseline not run", c))
|
|
110
|
+
out.push(`${mark("skipped", c)}${wrap(`skipped (${section.reason})`, { indent: GUTTER }, c)[0].trimStart()}`)
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
out.push(...field("capabilities", `marketplace baseline at pin ${section.pin.commit.slice(0, 8)}, ${section.transport === "local-git" ? "local transport" : `transport ${section.transport}`}`, c))
|
|
114
|
+
if (!section.invoked) {
|
|
115
|
+
out.push(`${mark("unknown", c)}${wrap(`not run: ${section.skipReason}`, { indent: GUTTER }, c)[0].trimStart()}`)
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
118
|
+
const official = section.official
|
|
119
|
+
if (official?.error) {
|
|
120
|
+
out.push(...row("unknown", official.error.code, official.error.message, c))
|
|
121
|
+
return out
|
|
122
|
+
}
|
|
123
|
+
const capabilities = official.capabilities || []
|
|
124
|
+
const findings = official.findings || []
|
|
125
|
+
const evidence = (entries) => entries.flatMap((entry) => entry.evidence || [])
|
|
126
|
+
const files = (entries) => [...new Set(evidence(entries).map((entry) => entry.path))]
|
|
127
|
+
if (capabilities.length) {
|
|
128
|
+
const sites = evidence(capabilities)
|
|
129
|
+
out.push(...wrap(`observed: ${capabilities.map((entry) => entry.id).join(", ")} (${plural(sites.length, "evidence site")}, ${files(capabilities).join(", ")})`, { indent: GUTTER, first: 0 }, c)
|
|
130
|
+
.map((line, index) => (index === 0 ? `${mark("info", c)}${line}` : line)))
|
|
131
|
+
} else {
|
|
132
|
+
out.push(`${mark("info", c)}observed: no capability recorded`)
|
|
133
|
+
}
|
|
134
|
+
for (const finding of findings) {
|
|
135
|
+
const sites = (finding.evidence || []).map((entry) => `${entry.path}:${entry.line}`)
|
|
136
|
+
out.push(...wrap(`observed: finding ${finding.ruleId} (${sites.join(", ")})`, { indent: GUTTER, first: 0 }, c)
|
|
137
|
+
.map((line, index) => (index === 0 ? `${mark("info", c)}${line}` : line)))
|
|
138
|
+
}
|
|
139
|
+
out.push(...wrap(`official result: ${official.outcome} (verbatim in --json under marketplaceBaseline)`, { indent: GUTTER }, c))
|
|
140
|
+
return out
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function patternLines(document, c) {
|
|
144
|
+
if (!PATTERNS.length) return []
|
|
145
|
+
const out = []
|
|
146
|
+
const width = Math.max(...PATTERNS.map((pattern) => pattern.label.length)) + 1
|
|
147
|
+
out.push(...field("patterns", document.patterns.length
|
|
148
|
+
? `of what the marketplace's human review raised, in a ${PATTERNS[0].sample} (${PATTERNS[0].measurement})`
|
|
149
|
+
: `none of the ${PATTERNS.length} classes the marketplace's human review raised, in a ${PATTERNS[0].sample} (${PATTERNS[0].measurement}), shows its precondition here`, c))
|
|
150
|
+
for (const entry of document.patterns) {
|
|
151
|
+
const pattern = PATTERNS.find((candidate) => candidate.id === entry.id)
|
|
152
|
+
const label = pattern.label.padEnd(width)
|
|
153
|
+
const lines = wrap(entry.observation, { indent: GUTTER, first: GUTTER + label.length + 1 }, c)
|
|
154
|
+
out.push(`${mark("advisory", c)}${c("name", label)} ${lines[0].trimStart()}`, ...lines.slice(1))
|
|
155
|
+
out.push(...wrap(`about ${Math.round(entry.share * 100)} of every 100 review findings in the sample (${entry.measurement})`, { indent: GUTTER }, c))
|
|
156
|
+
}
|
|
157
|
+
out.push("")
|
|
158
|
+
const lookedFor = document.lookedFor.map((id) => PATTERNS.find((pattern) => pattern.id === id)?.notObserved || id)
|
|
159
|
+
out.push(...field("not observed", lookedFor.length ? lookedFor.join(", ") : "every pattern's precondition was observed", c))
|
|
160
|
+
return out
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The default view: what needs attention, biggest first. One block per
|
|
165
|
+
* review class this tree shows, ordered by the class's share of review
|
|
166
|
+
* findings in the M11 sample, which is the one measured number that says
|
|
167
|
+
* how much reviewers care; under each, the sites that show it, up to
|
|
168
|
+
* SHOWN_SITES, with the fact at each site in one line. Classes under
|
|
169
|
+
* MIN_SHARE are counted on one line and not listed. Nothing else is
|
|
170
|
+
* printed: the counts of what was observed go in the closing line, and
|
|
171
|
+
* `--full` has every site of every kind.
|
|
172
|
+
*/
|
|
173
|
+
const MIN_SHARE = 0.05
|
|
174
|
+
const SHOWN_SITES = 5
|
|
175
|
+
|
|
176
|
+
/** A shell word for the eye: quoted only when it holds a space or a quote, an expression element as its source text. */
|
|
177
|
+
function commandLine(process) {
|
|
178
|
+
const computed = new Set(process.expressions.map((entry) => entry.index))
|
|
179
|
+
return process.argv.map((word, index) => (computed.has(index) || !/[\s"'`]/.test(word) ? word : JSON.stringify(word))).join(" ")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The fact at a site, in one line: the write there for the file class, the
|
|
184
|
+
* host there for the egress class, otherwise the command, then the host,
|
|
185
|
+
* then the write, whichever the site has.
|
|
186
|
+
*/
|
|
187
|
+
function factAt(document, at, patternId) {
|
|
188
|
+
const same = (row) => `${row.file}:${row.line}` === at
|
|
189
|
+
const process = () => {
|
|
190
|
+
const row = document.observed.processes.find(same)
|
|
191
|
+
return row ? (row.argvForm === "computed" ? `command: ${row.commandText}` : commandLine(row) || "[] (an empty argv)") : ""
|
|
192
|
+
}
|
|
193
|
+
const host = () => {
|
|
194
|
+
const row = document.observed.hosts.find(same)
|
|
195
|
+
return row ? `${row.scheme}://${row.host}${row.tool ? ` via ${row.tool}` : ""}` : ""
|
|
196
|
+
}
|
|
197
|
+
const write = () => {
|
|
198
|
+
const row = document.observed.writes.find(same)
|
|
199
|
+
return row ? `${row.via} ${row.canonicalPath ?? row.path}` : ""
|
|
200
|
+
}
|
|
201
|
+
const order = patternId === "file-and-state-boundary" ? [write, process, host] : patternId === "network-egress" ? [host, process, write] : [process, host, write]
|
|
202
|
+
for (const fact of order) {
|
|
203
|
+
const text = fact()
|
|
204
|
+
if (text) return text
|
|
205
|
+
}
|
|
206
|
+
return ""
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Text cut to a width with three dots, for a command a person only needs to recognise; --full has it whole. */
|
|
210
|
+
function cut(text, width) {
|
|
211
|
+
return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 3))}...`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function baselineText(section) {
|
|
215
|
+
if (section.skipped) return `skipped (${section.reason})`
|
|
216
|
+
if (!section.invoked) return `not run: ${section.skipReason}`
|
|
217
|
+
const official = section.official
|
|
218
|
+
if (official?.error) return `the official code refused the snapshot: ${official.error.code}`
|
|
219
|
+
const names = [...(official.capabilities || []).map((entry) => entry.id), ...(official.findings || []).map((entry) => entry.ruleId)]
|
|
220
|
+
return `${official.outcome} at pin ${section.pin.commit.slice(0, 8)}${names.length ? `: ${names.join(", ")}` : ""}`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {object} document the document of docs/INSPECT.md
|
|
225
|
+
* @param {{ colour?: boolean, full?: boolean }} [options] `full` is the
|
|
226
|
+
* exhaustive view behind `--full`; the default is what needs attention,
|
|
227
|
+
* biggest first.
|
|
228
|
+
* @returns {string}
|
|
229
|
+
*/
|
|
230
|
+
export function renderInspect(document, { colour = colourEnabled(), full = false } = {}) {
|
|
231
|
+
if (full) return renderFull(document, { colour })
|
|
232
|
+
const c = styler(colour)
|
|
233
|
+
const out = []
|
|
234
|
+
const counts = document.counts
|
|
235
|
+
out.push(...field("subject", `${withHomeAbbreviated(document.subject.dir)} at ${document.subject.commit ? document.subject.commit.slice(0, 8) : "no commit"}`, c))
|
|
236
|
+
out.push(...field("baseline", baselineText(document.marketplaceBaseline), c))
|
|
237
|
+
const score = document.size.score
|
|
238
|
+
out.push(...field("size score", score === null
|
|
239
|
+
? `none: no function to rank`
|
|
240
|
+
: `${score.toFixed(2)} of 10; 10 minus the mean rank of its ${plural(counts.functions, "function")} among ${SIZE.functions} in ${document.size.sample.trees} listed trees (${SIZE.measurement}), so a tree of median functions scores 5.00`, c))
|
|
241
|
+
out.push("")
|
|
242
|
+
|
|
243
|
+
const ranked = [...document.patterns].sort((a, b) => b.share - a.share)
|
|
244
|
+
const shown = ranked.filter((entry) => entry.share >= MIN_SHARE)
|
|
245
|
+
const below = ranked.filter((entry) => entry.share < MIN_SHARE)
|
|
246
|
+
const width = outputColumns()
|
|
247
|
+
const over = document.size.over
|
|
248
|
+
if (!ranked.length && !over.length) {
|
|
249
|
+
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))
|
|
250
|
+
} else {
|
|
251
|
+
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))
|
|
252
|
+
}
|
|
253
|
+
// Long functions first: what the person asked about, so the order is a
|
|
254
|
+
// preference and the heading says whose thresholds it uses.
|
|
255
|
+
if (over.length) {
|
|
256
|
+
out.push("")
|
|
257
|
+
const thresholds = `${SIZE.lines} lines, ${SIZE.branches} branches or nesting ${SIZE.depth}`
|
|
258
|
+
const heading = wrap(`${plural(over.length, "function")} over what 90 of 100 functions in ${SIZE.sample} stay under: ${thresholds} (${SIZE.measurement}); rank is the share of listed functions smaller than it`, { indent: GUTTER, first: GUTTER + "long functions".length + 2 }, c)
|
|
259
|
+
out.push(`${mark("advisory", c)}${c("name", "long functions")} ${heading[0].trimStart()}`, ...heading.slice(1))
|
|
260
|
+
const top = over.slice(0, SHOWN_SITES)
|
|
261
|
+
const siteWidth = Math.max(...top.map((entry) => `${entry.file}:${entry.line}`.length))
|
|
262
|
+
const nameWidth = Math.max(...top.map((entry) => entry.name.length))
|
|
263
|
+
for (const entry of top) {
|
|
264
|
+
const at = `${entry.file}:${entry.line}`
|
|
265
|
+
out.push(`${" ".repeat(GUTTER)}${c("name", at.padEnd(siteWidth))} ${entry.name.padEnd(nameWidth)} ${entry.lines} lines, ${entry.branches} branches, nesting ${entry.depth}, ${c("label", `rank ${Math.round(entry.percentile)}`)}`)
|
|
266
|
+
}
|
|
267
|
+
if (over.length > SHOWN_SITES) out.push(...wrap(`and ${over.length - SHOWN_SITES} more (--full)`, { indent: GUTTER }).map((line) => c("label", line)))
|
|
268
|
+
}
|
|
269
|
+
for (const entry of shown) {
|
|
270
|
+
const pattern = PATTERNS.find((candidate) => candidate.id === entry.id)
|
|
271
|
+
out.push("")
|
|
272
|
+
const share = `${Math.round(entry.share * 100)} of 100 findings`
|
|
273
|
+
const heading = wrap(entry.summary, { indent: GUTTER, first: GUTTER + pattern.label.length + 2 + share.length + 2 }, c)
|
|
274
|
+
out.push(`${mark("advisory", c)}${c("name", pattern.label)} ${c("label", share)} ${heading[0].trimStart()}`, ...heading.slice(1))
|
|
275
|
+
// A site cited twice by one class (curl from PATH and curl without -q) is one line.
|
|
276
|
+
const sites = [...new Set(entry.sites.map((site_) => `${site_.file}:${site_.line}`))]
|
|
277
|
+
const siteWidth = Math.max(...sites.slice(0, SHOWN_SITES).map((at) => at.length))
|
|
278
|
+
for (const at of sites.slice(0, SHOWN_SITES)) {
|
|
279
|
+
const fact = factAt(document, at, entry.id)
|
|
280
|
+
const room = width - GUTTER - siteWidth - 2
|
|
281
|
+
out.push(`${" ".repeat(GUTTER)}${c("name", at.padEnd(siteWidth))}${fact ? ` ${cut(fact, room)}` : ""}`)
|
|
282
|
+
}
|
|
283
|
+
if (sites.length > SHOWN_SITES) out.push(...wrap(`and ${sites.length - SHOWN_SITES} more (--full)`, { indent: GUTTER }).map((line) => c("label", line)))
|
|
284
|
+
}
|
|
285
|
+
if (below.length) {
|
|
286
|
+
out.push("")
|
|
287
|
+
out.push(...field(`under ${Math.round(MIN_SHARE * 100)}%`, below.map((entry) => `${PATTERNS.find((candidate) => candidate.id === entry.id)?.label || entry.id} (${plural(entry.observedCount, "site")})`).join(", "), c))
|
|
288
|
+
}
|
|
289
|
+
out.push("")
|
|
290
|
+
const processes = `${plural(counts.processes.total, "process", "processes")}${counts.processes.total ? ` (${split(counts.processes)})` : ""}`
|
|
291
|
+
out.push(...verdict("info", INSPECT_VERDICT, `${processes}, ${plural(counts.hosts, "host")}, ${plural(counts.writes, "write")}, ${plural(counts.timers, "timer")} observed; static; --full for every site, --json for the document`, c))
|
|
292
|
+
return out.join("\n")
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The exhaustive view: every site, every qualifier, the whole not observed and not visible lists. */
|
|
296
|
+
function renderFull(document, { colour }) {
|
|
297
|
+
const c = styler(colour)
|
|
298
|
+
const out = []
|
|
299
|
+
const read = document.subject.filesRead
|
|
300
|
+
const kinds = Object.entries(read).filter(([, count]) => count > 0).map(([kind, count]) => `${count} ${kind}`)
|
|
301
|
+
const total = Object.values(read).reduce((sum, count) => sum + count, 0)
|
|
302
|
+
out.push(...field("subject", `${withHomeAbbreviated(document.subject.dir)} at ${document.subject.commit ? document.subject.commit.slice(0, 8) : "no commit"}, ${plural(total, "file")} read${kinds.length ? ` (${kinds.join(", ")})` : ""}`, c))
|
|
303
|
+
out.push(...field("method", document.method, c))
|
|
304
|
+
out.push("")
|
|
305
|
+
|
|
306
|
+
const sections = [
|
|
307
|
+
["processes", document.observed.processes, processRow],
|
|
308
|
+
["hosts", document.observed.hosts, hostRow],
|
|
309
|
+
["writes", document.observed.writes, writeRow],
|
|
310
|
+
["timers", document.observed.timers, timerRow],
|
|
311
|
+
]
|
|
312
|
+
for (const [name, rows, render] of sections) {
|
|
313
|
+
out.push(...field(name, rows.length ? `observed ${rows.length}${name === "processes" ? `, ${split(document.counts.processes)}` : ""}` : NOTHING, c))
|
|
314
|
+
for (const entry of rows) out.push(...render(entry, c))
|
|
315
|
+
out.push("")
|
|
316
|
+
}
|
|
317
|
+
const functions = document.observed.functions
|
|
318
|
+
const over = document.size.over
|
|
319
|
+
out.push(...field("functions", functions.length
|
|
320
|
+
? `observed ${functions.length}, ${over.length} over what 90 of 100 functions in ${SIZE.sample} stay under (${SIZE.lines} lines, ${SIZE.branches} branches or nesting ${SIZE.depth}; ${SIZE.measurement}), longest first`
|
|
321
|
+
: NOTHING, c))
|
|
322
|
+
for (const entry of over) out.push(...row("info", `${entry.file}:${entry.line}`, `${entry.name}, ${plural(entry.lines, "line")}, ${plural(entry.branches, "branch", "branches")}, nesting ${entry.depth}, over ${entry.percentile} of 100 listed`, c))
|
|
323
|
+
if (functions.length) out.push(...wrap(`size score ${document.size.score.toFixed(2)} of 10: 10 minus the mean rank of the ${functions.length} among ${SIZE.functions} listed functions (${SIZE.measurement})`, { indent: GUTTER }, c))
|
|
324
|
+
out.push("")
|
|
325
|
+
out.push(...baselineLines(document.marketplaceBaseline, c))
|
|
326
|
+
out.push("")
|
|
327
|
+
const patterns = patternLines(document, c)
|
|
328
|
+
if (patterns.length) out.push(...patterns)
|
|
329
|
+
out.push(...field("not visible", document.notVisible.join(", "), c))
|
|
330
|
+
out.push("")
|
|
331
|
+
const counts = document.counts
|
|
332
|
+
const processes = `${plural(counts.processes.total, "process", "processes")}${counts.processes.total ? ` (${split(counts.processes)})` : ""}`
|
|
333
|
+
out.push(...verdict("info", INSPECT_VERDICT, `${processes}, ${plural(counts.hosts, "host")}, ${plural(counts.writes, "write")}, ${plural(counts.timers, "timer")}; static, see docs/INSPECT.md`, c))
|
|
334
|
+
return out.join("\n")
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** The process split in words: how many are QML Process sites and how many are shell lines. */
|
|
338
|
+
function split({ qml, shell }) {
|
|
339
|
+
if (!shell) return qml === 1 ? "in qml" : "all in qml"
|
|
340
|
+
if (!qml) return shell === 1 ? "a shell line" : "all shell lines"
|
|
341
|
+
return `${qml} in qml, ${plural(shell, "shell line")}`
|
|
342
|
+
}
|