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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// whole list, and tests/unit/pin.test.mjs fails if any module starts reading a
|
|
10
10
|
// path outside it, because on a partial clone such a read would quietly reach
|
|
11
11
|
// for the network instead of failing.
|
|
12
|
-
import { execFileSync } from "node:child_process"
|
|
12
|
+
import { execFileSync, spawnSync } from "node:child_process"
|
|
13
13
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
14
14
|
import { dirname, join, resolve } from "node:path"
|
|
15
15
|
import { omakitCacheDir } from "./paths.mjs"
|
|
@@ -178,18 +178,26 @@ export function ensurePin(repoRoot, log = () => {}, env = process.env) {
|
|
|
178
178
|
return { dir, identity, fetched: true }
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
/**
|
|
182
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Human-readable size of the pinned checkout, for `omakit pin` and `omakit
|
|
183
|
+
* doctor`. `env` is the environment `du` is found in; injectable for tests.
|
|
184
|
+
*/
|
|
185
|
+
export function pinDiskUsage(dir, env = process.env) {
|
|
183
186
|
// -H follows a symlink given on the command line (POSIX; GNU's -D). Measured
|
|
184
187
|
// without it: a checkout reached through a symlink reported 0.0 MB, the size
|
|
185
188
|
// of the link, while the directory behind it was 15 MB.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
//
|
|
190
|
+
// The total is read whether or not du exited 0. du exits 1 when a file it
|
|
191
|
+
// listed is gone by the time it reaches it, and still prints the total.
|
|
192
|
+
// Measured: with `git status` running on the pin in parallel, 1 of 40 runs
|
|
193
|
+
// warned "cannot access '.git/index.lock'" over its momentary lock file
|
|
194
|
+
// and exited 1, so doctor said "size unknown" for a checkout it had the
|
|
195
|
+
// size of. Only a run that printed no total is unknown.
|
|
196
|
+
const result = spawnSync("du", ["-skH", dir], { encoding: "utf8", env, stdio: ["ignore", "pipe", "ignore"] })
|
|
197
|
+
const output = String(result.stdout || "").trim().split(/\s+/)[0]
|
|
198
|
+
if (result.error || !/^\d+$/.test(output)) return "size unknown"
|
|
199
|
+
const mib = Number(output) / 1024
|
|
200
|
+
return `${mib < 10 ? mib.toFixed(1) : Math.round(mib)} MB on disk`
|
|
193
201
|
}
|
|
194
202
|
|
|
195
203
|
/** True when the checkout was fetched with only PIN_PATHS, as a fresh one is. */
|
|
@@ -266,12 +266,19 @@ export function renderWatchList(result, { colour = colourEnabled() } = {}) {
|
|
|
266
266
|
/** Compact batch report; exact commits and full discussion remain in JSON. */
|
|
267
267
|
export function renderWatchAll(result, { colour = colourEnabled() } = {}) {
|
|
268
268
|
const c = styler(colour)
|
|
269
|
-
const out = [...field("account", result.account, c), ...field("issues", `${result.summary.total} checked; ${result.summary.current} current, ${result.summary.stale} stale, ${result.summary.unknown} unknown`, c)
|
|
269
|
+
const out = [...field("account", result.account, c), ...field("issues", `${result.summary.total} checked; ${result.summary.current} current, ${result.summary.stale} stale, ${result.summary.unknown} unknown`, c)]
|
|
270
|
+
if (result.reviewCostSummary) {
|
|
271
|
+
const cost = result.reviewCostSummary
|
|
272
|
+
const skipped = cost.skipped.length ? `; ${cost.skipped.length} diff(s) skipped (reasons on issue rows)` : ""
|
|
273
|
+
out.push(...field("review cost", `${cost.manualQueue} of ${cost.pluginUpdates} plugin-update issue(s) on security-review-required; ${cost.docsOnly} docs-only validated diff(s) of ${cost.compared} compared${skipped}`, c))
|
|
274
|
+
}
|
|
275
|
+
out.push("")
|
|
270
276
|
for (const row of result.issues) {
|
|
271
277
|
const state = row.report?.verdict.state || "unknown"
|
|
272
278
|
const style = { current: "pass", stale: "fail", unknown: "unknown" }[state]
|
|
273
279
|
out.push(...verdict(style, state.toUpperCase(), `#${row.issue.number} ${watchIssueTitle(row.report?.read.title || row.issue.title)}`, c))
|
|
274
280
|
out.push(...field("issue", row.issue.url, c, { wrapValue: false }))
|
|
281
|
+
if (row.documentationDiff?.docsOnly === null) out.push(...field("diff skipped", watchIssueTitle(row.documentationDiff.reason), c))
|
|
275
282
|
if (row.error) {
|
|
276
283
|
out.push(...field("read error", watchIssueTitle(`${row.error.code}: ${row.error.message}`), c))
|
|
277
284
|
} else {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Review cost is advice, never a marketplace rule. Evidence: MEASUREMENTS.md M4 and M9.
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { pathToFileURL } from "node:url"
|
|
5
|
+
import { requirePin } from "./pin.mjs"
|
|
6
|
+
import { token, issue, parseIssueUrl, compareCommits } from "./github.mjs"
|
|
7
|
+
import { discoverWatchIssues, repositoryFor } from "./watch.mjs"
|
|
8
|
+
import { repositorySlug } from "./registry.mjs"
|
|
9
|
+
|
|
10
|
+
/** Outcome and update label names come from the pin, rather than a second policy. */
|
|
11
|
+
export async function reviewPolicy(repoRoot) {
|
|
12
|
+
const { dir } = requirePin(repoRoot)
|
|
13
|
+
const policy = await import(pathToFileURL(join(dir, "scripts/security-baseline-policy.mjs")).href)
|
|
14
|
+
const approval = readFileSync(join(dir, "scripts/approve-plugin-update.mjs"), "utf8")
|
|
15
|
+
const updateLabel = approval.match(/for \(const required of \["([^"]+)"/)?.[1]
|
|
16
|
+
if (!updateLabel) throw new Error("cannot read the update issue label from the pin")
|
|
17
|
+
const manual = policy.currentSecurityBaselinePolicy.maintainerVerificationOutcome
|
|
18
|
+
return { automated: policy.securityBaselineOutcome([], []), manual, updateLabel, reviewLabel: `security-${manual}` }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The same discovery as watch --all; no credential or incomplete reads are not zero issues. */
|
|
22
|
+
export async function openIssuesForRepository({ repoRoot, repository, offline = false, github = {} }) {
|
|
23
|
+
if (offline) return { count: null, reason: "not checked (--offline)" }
|
|
24
|
+
if (!(github.token || token)()) return { count: null, reason: "not checked: no GitHub credential" }
|
|
25
|
+
try {
|
|
26
|
+
const discovery = await discoverWatchIssues({ github })
|
|
27
|
+
const { dir } = requirePin(repoRoot)
|
|
28
|
+
let count = 0
|
|
29
|
+
let next = 0
|
|
30
|
+
async function worker() {
|
|
31
|
+
while (next < discovery.issues.length) {
|
|
32
|
+
const target = parseIssueUrl(discovery.issues[next++].url)
|
|
33
|
+
const subject = await (github.issue || issue)(target.owner, target.repository, target.number)
|
|
34
|
+
const parsed = await repositoryFor(dir, subject)
|
|
35
|
+
if (!parsed.url) throw new Error(`repository unknown on issue #${target.number}: ${parsed.error}`)
|
|
36
|
+
if (repositorySlug(parsed.url) === repositorySlug(repository)) count += 1
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
await Promise.all(Array.from({ length: Math.min(4, discovery.issues.length) }, worker))
|
|
40
|
+
return { count, reason: `watch --all discovery: ${count} open issue(s) for this repository` }
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return { count: null, reason: `not checked (${error.code || "issue-discovery-unavailable"}): ${error.message}` }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function reviewCostVerdict({ baseline, policy, openIssues = { count: null, reason: "not checked" }, why }) {
|
|
47
|
+
const capabilities = [...(baseline?.capabilities || [])]
|
|
48
|
+
const manual = baseline?.outcome === policy.manual
|
|
49
|
+
const automated = baseline?.outcome === policy.automated
|
|
50
|
+
const outcome = manual ? openIssues.count > 0 ? "manual queue, again" : "manual queue" : automated ? "automated" : null
|
|
51
|
+
const detail = manual
|
|
52
|
+
? `${outcome}: capabilities ${capabilities.join(", ") || "none"}. Every update of this plugin, including a docs-only one, lands in the manual queue.${openIssues.count > 0 ? ` You already have ${openIssues.count} open issue(s) for this repository.` : ""}`
|
|
53
|
+
: automated ? "automated: this update will not need a human for the security baseline." : "not checked: baseline.preflight has no passed or review-required outcome"
|
|
54
|
+
return {
|
|
55
|
+
reviewCost: { outcome, capabilities, openIssuesForRepository: openIssues.count, reason: `${detail} ${openIssues.reason}` },
|
|
56
|
+
check: {
|
|
57
|
+
id: "review.cost", source: "omakit", severity: "advisory", verdict: manual ? "fail" : automated ? "pass" : "unknown",
|
|
58
|
+
why, detail, paths: [],
|
|
59
|
+
remedy: manual && openIssues.count > 0 ? "consider batching: close or fold the open one before opening another" : null,
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A rename must be documentation at both ends; an empty diff is not a docs-only update. */
|
|
65
|
+
export function documentationPath(path) {
|
|
66
|
+
return typeof path === "string" && !path.split("/").some((part) => part === "..") &&
|
|
67
|
+
(/^docs\//i.test(path) || /\.md$/i.test(path) || /(?:^|\/)LICENSE$/i.test(path) || /\.(?:png|jpe?g|gif|webp|svg|ico|avif|bmp|tiff?)$/i.test(path))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function docsOnlyFiles(files) {
|
|
71
|
+
return Array.isArray(files) && files.length > 0 && files.every((file) =>
|
|
72
|
+
documentationPath(file.filename) && (!file.previous_filename || documentationPath(file.previous_filename)))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Select a dated validation record, never a parent guessed from Git history. */
|
|
76
|
+
export function previousValidatedCommit(registry, report, catalog = null) {
|
|
77
|
+
if (!repositorySlug(report.plugin.repository)) return null
|
|
78
|
+
const sources = Array.isArray(registry?.sources) ? registry.sources : Object.values(registry?.sources || {})
|
|
79
|
+
const source = sources.find((entry) => repositorySlug(entry.repo) === repositorySlug(report.plugin.repository))
|
|
80
|
+
if (!report.validated?.commit) return null
|
|
81
|
+
const target = report.validated.commit
|
|
82
|
+
const checkedAt = Date.parse(report.validated.checkedAt)
|
|
83
|
+
const candidates = [...(Array.isArray(source?.listingValidationHistory) ? source.listingValidationHistory : []), {
|
|
84
|
+
commit: source?.listingValidatedCommit, validatedAt: source?.listingValidatedAt,
|
|
85
|
+
}, { commit: report.previousValidated?.commit, validatedAt: report.previousValidated?.checkedAt }]
|
|
86
|
+
// The catalog also records successful upstream validation before an update
|
|
87
|
+
// is promoted to the listing. This is validation evidence, not branch HEAD.
|
|
88
|
+
for (const plugin of Array.isArray(catalog?.plugins) ? catalog.plugins : []) {
|
|
89
|
+
if (repositorySlug(plugin.repo) === repositorySlug(report.plugin.repository)) {
|
|
90
|
+
candidates.push({ commit: plugin.upstreamValidatedCommit, validatedAt: plugin.upstreamValidatedAt })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return candidates.filter((entry) => /^[a-f0-9]{40}$/i.test(entry.commit || "") && entry.commit.toLowerCase() !== target.toLowerCase() &&
|
|
94
|
+
Number.isFinite(Date.parse(entry.validatedAt)) && Date.parse(entry.validatedAt) <= checkedAt)
|
|
95
|
+
.sort((a, b) => Date.parse(a.validatedAt) - Date.parse(b.validatedAt)).at(-1)?.commit.toLowerCase() || null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function validatedDocumentationDiff({ report, registry, catalog, registryReason = null, compare = compareCommits }) {
|
|
99
|
+
const previous = previousValidatedCommit(registry, report, catalog)
|
|
100
|
+
if (!previous) return { previousCommit: null, validatedCommit: report.validated?.commit || null, docsOnly: null, files: null, source: null, reason: registryReason || "previous validated commit unknown in the marketplace registry" }
|
|
101
|
+
try {
|
|
102
|
+
const diff = await compare(report.plugin.repository, previous, report.validated.commit)
|
|
103
|
+
return { previousCommit: previous, validatedCommit: report.validated.commit, docsOnly: docsOnlyFiles(diff.files), files: diff.files.length, source: diff.url, reason: null }
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return { previousCommit: previous, validatedCommit: report.validated.commit, docsOnly: null, files: null, source: null, reason: `${error.code || "compare-unavailable"}: ${error.message}` }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -18,7 +18,7 @@ async function loadScanner(pinDir) {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* @param {{ repoRoot: string, repoUrl: string, commitSha: string, transport: "local"|"github",
|
|
21
|
-
* repoDir?: string, listedPlugins?: Array, token?: string }} options
|
|
21
|
+
* repoDir?: string, repoSubdir?: string, listedPlugins?: Array, token?: string }} options
|
|
22
22
|
*/
|
|
23
23
|
export async function runBaseline(options) {
|
|
24
24
|
const { dir: pinDir, identity } = requirePin(options.repoRoot)
|
|
@@ -34,6 +34,7 @@ export async function runBaseline(options) {
|
|
|
34
34
|
repoDir: options.repoDir,
|
|
35
35
|
repoUrl: options.repoUrl,
|
|
36
36
|
commitSha: options.commitSha,
|
|
37
|
+
subdir: options.repoSubdir || "",
|
|
37
38
|
})
|
|
38
39
|
scanOptions.fetchImpl = transport.fetchImpl
|
|
39
40
|
adapter = {
|
|
@@ -220,6 +220,9 @@ export const AUDIT_VERDICTS = Object.freeze({
|
|
|
220
220
|
unavailable: "NOT AUDITED",
|
|
221
221
|
})
|
|
222
222
|
|
|
223
|
+
/** The closing word of `omakit inspect`: a report was produced, and it decided nothing. */
|
|
224
|
+
export const INSPECT_VERDICT = "INSPECTED"
|
|
225
|
+
|
|
223
226
|
/** The width of the widest mark, "█ FAIL"; every mark is padded to it so the names beside them align. */
|
|
224
227
|
export const MARK_WIDTH = Math.max(...Object.values(STATUS).map((s) => `${s.glyph} ${s.word}`.length))
|
|
225
228
|
|
|
@@ -23,6 +23,7 @@ import { renderIssue, verifyAgainstOfficialParser } from "./issue.mjs"
|
|
|
23
23
|
import { defaultBranchHead } from "./github.mjs"
|
|
24
24
|
import { REFRESH_ACTION } from "./watch.mjs"
|
|
25
25
|
import { omakitCacheDir } from "./paths.mjs"
|
|
26
|
+
import { openIssuesForRepository, reviewCostVerdict, reviewPolicy } from "./review-cost.mjs"
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* One arrow per cause, in this order, so a person fixes the thing that is
|
|
@@ -135,6 +136,7 @@ export function reproduceCommand({ target, category, tags, pluginName, notes, su
|
|
|
135
136
|
* @param {{ repoRoot: string, target: string, category?: string, tags?: string|string[],
|
|
136
137
|
* notes?: string, suggestedTag?: string, pluginName?: string,
|
|
137
138
|
* allowDirty?: boolean, offline?: boolean,
|
|
139
|
+
* github?: object,
|
|
138
140
|
* readRegistry?: typeof liveRegistry,
|
|
139
141
|
* chooser?: (question: { contract: object, defaults: object, missing: string[] }) => Promise<{ category?: string, tags?: string[] }> }} options
|
|
140
142
|
* `readRegistry` is injectable for tests; the default reads the marketplace's
|
|
@@ -404,7 +406,7 @@ export async function submitPreflight(options) {
|
|
|
404
406
|
if (!options.offline && subject.repository.url) {
|
|
405
407
|
phase("reading the repository's default-branch HEAD")
|
|
406
408
|
try {
|
|
407
|
-
head = await defaultBranchHead(subject.repository.url)
|
|
409
|
+
head = await (options.github?.defaultBranchHead || defaultBranchHead)(subject.repository.url)
|
|
408
410
|
} catch (error) {
|
|
409
411
|
headError = { code: error.code || "head-unreadable", message: error.message }
|
|
410
412
|
}
|
|
@@ -412,9 +414,14 @@ export async function submitPreflight(options) {
|
|
|
412
414
|
const validationMatches = head ? head.commit === subject.commit.toLowerCase() : null
|
|
413
415
|
checks.push(check("submission.validation-commit", {
|
|
414
416
|
source: "omakit",
|
|
415
|
-
why: "The marketplace validates the default-branch HEAD it resolves when the issue is opened or edited, not the commit checked here.
|
|
417
|
+
why: "The marketplace validates the default-branch HEAD it resolves when the issue is opened or edited, not the commit checked here. M6 on 2026-09-15 found 326/519 readable author-fixes comparisons stale (62.8%), with 64 of 583 issues unknown, so a preflight against a commit that is not the pushed HEAD describes a tree nobody will review. Not a marketplace rule; an Omakit refusal to report on the wrong tree.",
|
|
416
418
|
severity: options.offline ? "advisory" : "blocking",
|
|
417
419
|
skipped: options.offline === true,
|
|
420
|
+
// No origin, no URL to read a HEAD from: the check waits on the one that
|
|
421
|
+
// says so. Measured on 0.4.1: it failed as a second root cause with
|
|
422
|
+
// "could not read the default-branch HEAD (unknown): " for a read that
|
|
423
|
+
// was never attempted.
|
|
424
|
+
waitedOn: [!options.offline && !subject.repository.url && "submission.repository-url"],
|
|
418
425
|
verdict: validationMatches === true,
|
|
419
426
|
detail: options.offline
|
|
420
427
|
? `not checked (--offline). Local commit ${subject.commit}.`
|
|
@@ -461,6 +468,14 @@ export async function submitPreflight(options) {
|
|
|
461
468
|
: null,
|
|
462
469
|
}))
|
|
463
470
|
|
|
471
|
+
const policy = await reviewPolicy(repoRoot)
|
|
472
|
+
const review = reviewCostVerdict({ baseline: consequence, policy,
|
|
473
|
+
openIssues: consequence?.outcome === policy.manual ? await openIssuesForRepository({ repoRoot, repository: subject.repository.url,
|
|
474
|
+
offline: options.offline === true, github: options.github }) : { count: null, reason: consequence?.outcome === policy.automated ? "open issue count not checked: automated baseline" : preflight.skipReason || preflight.refusal?.message || "baseline outcome needs findings resolved" },
|
|
475
|
+
why: `MEASUREMENTS.md M4: ${figure(figures.outcomes[policy.manual] || 0)} of ${figure(figures.withBaseline)} recorded listing baselines required review at the pin. M9: on 2026-09-15, 140 of 307 open update issues carried the manual-review label; 4 of 139 compared validated diffs were docs-only, with 1 unavailable. The baseline scans the whole snapshot, not the update diff, so unchanged capabilities also require another review. Sources and exact marketplace HEAD are recorded in MEASUREMENTS.md.`,
|
|
476
|
+
})
|
|
477
|
+
checks.push(review.check)
|
|
478
|
+
|
|
464
479
|
const blocking = checks.filter((entry) => entry.severity === "blocking" && entry.verdict === "fail")
|
|
465
480
|
const advisory = checks.filter((entry) => entry.severity === "advisory" && entry.verdict === "fail")
|
|
466
481
|
const unknown = checks.filter((entry) => entry.verdict === "unknown")
|
|
@@ -511,6 +526,7 @@ export async function submitPreflight(options) {
|
|
|
511
526
|
offline: options.offline === true,
|
|
512
527
|
}),
|
|
513
528
|
checks,
|
|
529
|
+
reviewCost: review.reviewCost,
|
|
514
530
|
outcome,
|
|
515
531
|
ready,
|
|
516
532
|
listing,
|
|
@@ -91,6 +91,27 @@ export const COMMANDS = Object.freeze([
|
|
|
91
91
|
"prints the document itself, and --out writes it to a file.",
|
|
92
92
|
],
|
|
93
93
|
},
|
|
94
|
+
{
|
|
95
|
+
signature: [
|
|
96
|
+
"omakit inspect <target> [--full] [--json] [--out <file>] [--offline]",
|
|
97
|
+
" [--allow-dirty]",
|
|
98
|
+
],
|
|
99
|
+
lines: [
|
|
100
|
+
"What a plugin tree does, as observations: every process with its argv,",
|
|
101
|
+
"every host with its timeout and size-cap flags, every write with whether",
|
|
102
|
+
"it falls under a directory the plugin controls, every timer with its",
|
|
103
|
+
"interval, and the capabilities the marketplace baseline records. Below",
|
|
104
|
+
"the facts, the review classes the marketplace's human review raised,",
|
|
105
|
+
"each with its measured share, only where the tree shows the class.",
|
|
106
|
+
"Regular expressions over QML and shell, labelled observed; runs nothing",
|
|
107
|
+
"from the tree, decides nothing, exits 0 with a report and 2 when the",
|
|
108
|
+
"target cannot be read. The report opens with a size score, 10 minus",
|
|
109
|
+
"the mean rank of the tree's functions among those in listed trees,",
|
|
110
|
+
"then what needs attention: functions over the measured size, longest",
|
|
111
|
+
"first, then the review classes by measured share, five sites each;",
|
|
112
|
+
"--full is every site with every qualifier; --json prints the document.",
|
|
113
|
+
],
|
|
114
|
+
},
|
|
94
115
|
{
|
|
95
116
|
signature: "omakit help --agent",
|
|
96
117
|
lines: [
|
|
@@ -6,15 +6,25 @@ import { runBaseline } from "./run-baseline.mjs"
|
|
|
6
6
|
import { MARKETPLACE_PIN } from "./pin.mjs"
|
|
7
7
|
import { ASSUMED_BY_ADAPTER } from "./local-transport.mjs"
|
|
8
8
|
|
|
9
|
+
/** The one assumption a subtree scan adds: the tree the official code saw is the plugin directory, not the repository root. */
|
|
10
|
+
function subtreeAssumption(subdir) {
|
|
11
|
+
return subdir ? [`tree.root=${subdir.replace(/\/+$/, "")}/ (the plugin directory below the repository root, not the root)`] : []
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
const MARKETPLACE_STATEMENT =
|
|
10
15
|
"Official baseline preview over a local snapshot. The marketplace rescans the public commit itself. This is not approval, listing, verification or a security audit."
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
|
-
* @param {{ repoRoot: string, subject: { dir: string, commit: string, repository: { url: string|null } }, listedPlugins?: Array }} options
|
|
18
|
+
* @param {{ repoRoot: string, subject: { dir: string, commit: string, repository: { url: string|null } }, listedPlugins?: Array, subdir?: string }} options
|
|
19
|
+
* `subdir`, when given, is a directory below the repository root that the
|
|
20
|
+
* local transport serves as the whole tree, so the official code scans the
|
|
21
|
+
* plugin's directory and not the repository around it; the section records
|
|
22
|
+
* it under `assumedByAdapter`. `verify` and `submit` pass none and scan the
|
|
23
|
+
* root, which for them is the repository the marketplace would fetch.
|
|
14
24
|
* @returns the `marketplaceBaseline` section: pin, transport, adapter
|
|
15
25
|
* assumptions, the official result verbatim, and the statement.
|
|
16
26
|
*/
|
|
17
|
-
export async function marketplaceBaselineSection({ repoRoot, subject, listedPlugins }) {
|
|
27
|
+
export async function marketplaceBaselineSection({ repoRoot, subject, listedPlugins, subdir = "" }) {
|
|
18
28
|
const pin = {
|
|
19
29
|
repository: MARKETPLACE_PIN.repository,
|
|
20
30
|
commit: MARKETPLACE_PIN.commit,
|
|
@@ -40,6 +50,7 @@ export async function marketplaceBaselineSection({ repoRoot, subject, listedPlug
|
|
|
40
50
|
commitSha: subject.commit,
|
|
41
51
|
transport: "local",
|
|
42
52
|
repoDir: subject.dir,
|
|
53
|
+
repoSubdir: subdir,
|
|
43
54
|
listedPlugins,
|
|
44
55
|
})
|
|
45
56
|
} catch (error) {
|
|
@@ -49,7 +60,7 @@ export async function marketplaceBaselineSection({ repoRoot, subject, listedPlug
|
|
|
49
60
|
return {
|
|
50
61
|
pin,
|
|
51
62
|
transport: "local-git",
|
|
52
|
-
assumedByAdapter: [...ASSUMED_BY_ADAPTER],
|
|
63
|
+
assumedByAdapter: [...ASSUMED_BY_ADAPTER, ...subtreeAssumption(subdir)],
|
|
53
64
|
invoked: true,
|
|
54
65
|
skipReason: null,
|
|
55
66
|
official: { error: { code: error.code, message: error.message, ...(error.details || {}) } },
|
|
@@ -61,7 +72,7 @@ export async function marketplaceBaselineSection({ repoRoot, subject, listedPlug
|
|
|
61
72
|
return {
|
|
62
73
|
pin: { ...pin, commit: run.pin.commit, baselineVersion: run.pin.baselineVersion, enforcementMode: run.pin.enforcementMode },
|
|
63
74
|
transport: "local-git",
|
|
64
|
-
assumedByAdapter: [...run.adapter.assumedByAdapter],
|
|
75
|
+
assumedByAdapter: [...run.adapter.assumedByAdapter, ...subtreeAssumption(subdir)],
|
|
65
76
|
invoked: true,
|
|
66
77
|
skipReason: null,
|
|
67
78
|
official: run.result,
|
|
@@ -9,19 +9,11 @@
|
|
|
9
9
|
// compares branch HEADs only for repositories that are already listed. So
|
|
10
10
|
// pushing a fix does nothing, and commenting "fixed in abc123" does nothing.
|
|
11
11
|
//
|
|
12
|
-
// Measured reason (docs/MEASUREMENTS.md M6): of
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// 9 had passed validation and passed the automated security baseline with zero
|
|
18
|
-
// findings and were blocked solely because their validated commit had fallen
|
|
19
|
-
// behind while they waited. 46% of the maintainer's own requests for a fresh validation never
|
|
20
|
-
// produced one; in the parked group 77% never did. The instruction that would
|
|
21
|
-
// fix this appears 22 times in the failure path of
|
|
22
|
-
// `scripts/submission-feedback.mjs` and zero times in the success path of
|
|
23
|
-
// `scripts/validate-submission.mjs`, which is the path 97 of 100 parked
|
|
24
|
-
// submissions took.
|
|
12
|
+
// Measured reason (docs/MEASUREMENTS.md M6), 2026-09-15: 326 of 519 readable
|
|
13
|
+
// author-fixes comparisons were stale (62.8%), with 64 of 583 issues unknown.
|
|
14
|
+
// The original 2026-09-12 research sampled 100 of a 464-issue queue: 68 of
|
|
15
|
+
// 93 readable pairs were stale (73.1%). It did not measure every queue item.
|
|
16
|
+
// Editing the issue body is the action the pinned workflow observes.
|
|
25
17
|
//
|
|
26
18
|
// This command reads. It never edits the issue, never comments, never labels.
|
|
27
19
|
// The action it names is the author's to take.
|
|
@@ -30,6 +22,8 @@ import { join } from "node:path"
|
|
|
30
22
|
import { pathToFileURL } from "node:url"
|
|
31
23
|
import { MARKETPLACE_PIN, requirePin } from "./pin.mjs"
|
|
32
24
|
import { authenticatedUser, repositoryIssues, defaultBranchHead, issue, issueComments, parseIssueUrl, token, GitHubError } from "./github.mjs"
|
|
25
|
+
import { liveRegistry } from "./registry.mjs"
|
|
26
|
+
import { reviewPolicy, validatedDocumentationDiff } from "./review-cost.mjs"
|
|
33
27
|
|
|
34
28
|
export class WatchError extends Error {
|
|
35
29
|
constructor(code, message) {
|
|
@@ -77,8 +71,11 @@ export async function discoverWatchIssues({ user, github = {}, onPhase = () => {
|
|
|
77
71
|
}
|
|
78
72
|
|
|
79
73
|
/** A batch keeps independent read failures visible and shares repository HEAD reads. */
|
|
80
|
-
export async function validationWatchAll({ repoRoot, discovery, github = {}, onPhase = () => {} }) {
|
|
74
|
+
export async function validationWatchAll({ repoRoot, discovery, github = {}, readRegistry = liveRegistry, onPhase = () => {} }) {
|
|
81
75
|
if (discovery.issues.length) requirePin(repoRoot)
|
|
76
|
+
const policy = discovery.issues.length ? await reviewPolicy(repoRoot) : null
|
|
77
|
+
let registry = null
|
|
78
|
+
const reviewCostSummary = { pluginUpdates: 0, manualQueue: 0, docsOnly: 0, compared: 0, skipped: [] }
|
|
82
79
|
const heads = new Map()
|
|
83
80
|
const readHead = github.defaultBranchHead || defaultBranchHead
|
|
84
81
|
const shared = { ...github, defaultBranchHead: (url) => {
|
|
@@ -102,7 +99,38 @@ export async function validationWatchAll({ repoRoot, discovery, github = {}, onP
|
|
|
102
99
|
await Promise.all(Array.from({ length: Math.min(4, discovery.issues.length) }, worker))
|
|
103
100
|
const summary = { total: results.length, current: 0, stale: 0, unknown: 0 }
|
|
104
101
|
for (const result of results) summary[result.report?.verdict.state || "unknown"] += 1
|
|
105
|
-
|
|
102
|
+
// M9: count labels on the listed issues, then compare only updates in the manual queue.
|
|
103
|
+
const manual = results.filter((row) => {
|
|
104
|
+
const labels = row.report?.read.labels || row.issue.labels
|
|
105
|
+
if (!labels.includes(policy?.updateLabel)) return false
|
|
106
|
+
reviewCostSummary.pluginUpdates += 1
|
|
107
|
+
return labels.includes(policy.reviewLabel)
|
|
108
|
+
})
|
|
109
|
+
reviewCostSummary.manualQueue = manual.length
|
|
110
|
+
if (manual.length) {
|
|
111
|
+
onPhase("reading the previous validated marketplace snapshots")
|
|
112
|
+
try { registry = await readRegistry({ repoRoot }) } catch (error) { registry = { reason: error.message } }
|
|
113
|
+
}
|
|
114
|
+
let compareNext = 0
|
|
115
|
+
async function compareWorker() {
|
|
116
|
+
while (compareNext < manual.length) {
|
|
117
|
+
const row = manual[compareNext++]
|
|
118
|
+
const diff = row.report
|
|
119
|
+
? await validatedDocumentationDiff({ report: row.report, registry: registry?.source === "head" ? registry.registry : null,
|
|
120
|
+
catalog: registry?.source === "head" ? registry.catalog : null,
|
|
121
|
+
registryReason: registry?.source === "head" ? null : `previous validated commit unknown: live registry unavailable (${registry?.reason || "unknown"})`, compare: github.compareCommits })
|
|
122
|
+
: { docsOnly: null, reason: `issue unreadable: ${row.error?.message || "unknown"}` }
|
|
123
|
+
row.documentationDiff = diff
|
|
124
|
+
if (diff.docsOnly === null) reviewCostSummary.skipped.push({ issue: row.issue.number, reason: diff.reason })
|
|
125
|
+
else {
|
|
126
|
+
reviewCostSummary.compared += 1
|
|
127
|
+
if (diff.docsOnly) reviewCostSummary.docsOnly += 1
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
await Promise.all(Array.from({ length: Math.min(4, manual.length) }, compareWorker))
|
|
132
|
+
reviewCostSummary.skipped.sort((a, b) => a.issue - b.issue)
|
|
133
|
+
return { mode: "all", account: discovery.account, marketplace: discovery.marketplace, summary, reviewCostSummary, issues: results }
|
|
106
134
|
}
|
|
107
135
|
|
|
108
136
|
// The one action that re-runs validation, in the register the marketplace itself
|
|
@@ -133,7 +161,7 @@ async function loadVerification(pinDir) {
|
|
|
133
161
|
* happens to recognise. So each form is read by the parser the marketplace
|
|
134
162
|
* itself uses for it, and the issue says which one it is.
|
|
135
163
|
*/
|
|
136
|
-
async function repositoryFor(pinDir, subject) {
|
|
164
|
+
export async function repositoryFor(pinDir, subject) {
|
|
137
165
|
const submission = await loadSubmission(pinDir)
|
|
138
166
|
const verification = await loadVerification(pinDir)
|
|
139
167
|
const title = String(subject.title || "")
|
|
@@ -221,11 +249,29 @@ export async function validationWatch({ repoRoot, issueUrl, onPhase, github = {}
|
|
|
221
249
|
baselineError = { code: error.code || "baseline-unreadable", message: error.message }
|
|
222
250
|
}
|
|
223
251
|
const fallback = validated ? null : validationCommentCommit(comments)
|
|
252
|
+
let previousValidated = null
|
|
253
|
+
if (validated) {
|
|
254
|
+
// Revalidation on an existing issue may precede the latest marker. Ignore
|
|
255
|
+
// repeated attestations of the same commit, not a different validated tree.
|
|
256
|
+
for (const comment of [...comments].reverse()) {
|
|
257
|
+
try {
|
|
258
|
+
const marker = record.findLatestSecurityBaseline([comment])
|
|
259
|
+
if (marker && marker.commitSha !== validated.commit && Date.parse(marker.checkedAt) <= Date.parse(validated.checkedAt)) {
|
|
260
|
+
previousValidated = { commit: marker.commitSha, checkedAt: marker.checkedAt, source: "security-baseline-marker" }
|
|
261
|
+
break
|
|
262
|
+
}
|
|
263
|
+
} catch { /* An incomplete run is not a previously validated commit. */ }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
224
266
|
|
|
225
267
|
const labels = (subject.labels || []).map((label) => (typeof label === "string" ? label : label?.name)).filter(Boolean)
|
|
226
268
|
const authorComments = comments.filter((comment) => comment?.user?.login === subject.user?.login)
|
|
269
|
+
// The discussion is a person's: the marketplace's own bot and any other
|
|
270
|
+
// automation (GitHub marks an app's account `type: "Bot"`, and names it
|
|
271
|
+
// `<app>[bot]`) is neither a reviewer nor the last human review.
|
|
272
|
+
const isBot = (user) => user?.type === "Bot" || /\[bot\]$/i.test(String(user?.login || ""))
|
|
227
273
|
const maintainerComments = comments.filter(
|
|
228
|
-
(comment) => comment?.user?.login && comment.user.login !== subject.user?.login && comment.user
|
|
274
|
+
(comment) => comment?.user?.login && comment.user.login !== subject.user?.login && !isBot(comment.user),
|
|
229
275
|
)
|
|
230
276
|
|
|
231
277
|
let head = null
|
|
@@ -267,6 +313,7 @@ export async function validationWatch({ repoRoot, issueUrl, onPhase, github = {}
|
|
|
267
313
|
},
|
|
268
314
|
plugin: { repository: repositoryUrl, repositoryError, form: issueKind },
|
|
269
315
|
validated,
|
|
316
|
+
previousValidated,
|
|
270
317
|
validationCommentFallback: fallback,
|
|
271
318
|
baselineError,
|
|
272
319
|
head,
|
package/tools/weigh/audit.mjs
CHANGED
|
@@ -331,7 +331,12 @@ async function sampleConfig({ label, runIndex, config, plan, env, procRoot, sign
|
|
|
331
331
|
onPhase(`run ${runIndex} of ${plan.runs}: ${label}, sampling for ${windowSeconds}s`)
|
|
332
332
|
const started = utc()
|
|
333
333
|
const t0 = Date.now()
|
|
334
|
-
|
|
334
|
+
// A pid with no stat is a process that is gone, and a gone shell has no
|
|
335
|
+
// sample: nothing is read as zero in its place. Measured on 0.4.1: a pid
|
|
336
|
+
// absent from /proc "completed" the window with 0 ticks and a Pss of
|
|
337
|
+
// null read as 0 MB, a delta of minus the whole baseline.
|
|
338
|
+
const cpu0 = cpuTicks(procRoot, pid)
|
|
339
|
+
if (cpu0 === null) return { label, run: runIndex, failed: `the shell (pid ${pid}) is not in ${procRoot}` }
|
|
335
340
|
const child0 = childTicks(procRoot, pid) ?? 0
|
|
336
341
|
const rows = []
|
|
337
342
|
const trace = []
|
|
@@ -355,7 +360,8 @@ async function sampleConfig({ label, runIndex, config, plan, env, procRoot, sign
|
|
|
355
360
|
} catch {
|
|
356
361
|
configRewritten = null
|
|
357
362
|
}
|
|
358
|
-
const cpu1 = cpuTicks(procRoot, pid)
|
|
363
|
+
const cpu1 = cpuTicks(procRoot, pid)
|
|
364
|
+
if (cpu1 === null) return { label, run: runIndex, failed: `the shell (pid ${pid}) disappeared from ${procRoot} during the window` }
|
|
359
365
|
const child1 = childTicks(procRoot, pid) ?? child0
|
|
360
366
|
const memory = { pssKb: pssKb(procRoot, pid), rssKb: rssKb(procRoot, pid), memoryAt: "window-end", ...settled, trace }
|
|
361
367
|
const seconds = (t1 - t0) / 1000
|
|
@@ -605,6 +611,13 @@ export async function measureWeigh(plan, { env = plan.env || process.env, procRo
|
|
|
605
611
|
let restore = null
|
|
606
612
|
let restoreProblem = null
|
|
607
613
|
let comeBack = null
|
|
614
|
+
// What stopped the measurement early, an interrupt or a thrown error, is
|
|
615
|
+
// kept so the restore's outcome can be judged first: a restore that failed
|
|
616
|
+
// or did not verify is the fact the person is left with, and it is what
|
|
617
|
+
// is reported, with the stop as its context. Measured on 0.4.1: an
|
|
618
|
+
// interrupt propagated past a restore whose md5 differed, and the entry
|
|
619
|
+
// point closed with "shell.json was restored".
|
|
620
|
+
let stopped = null
|
|
608
621
|
try {
|
|
609
622
|
for (let runIndex = 1; runIndex <= plan.runs; runIndex += 1) {
|
|
610
623
|
for (const { label, config } of configs) {
|
|
@@ -619,6 +632,8 @@ export async function measureWeigh(plan, { env = plan.env || process.env, procRo
|
|
|
619
632
|
samples.push(sample)
|
|
620
633
|
}
|
|
621
634
|
}
|
|
635
|
+
} catch (error) {
|
|
636
|
+
stopped = error
|
|
622
637
|
} finally {
|
|
623
638
|
onPhase("restoring shell.json and restarting the shell")
|
|
624
639
|
try {
|
|
@@ -642,7 +657,10 @@ export async function measureWeigh(plan, { env = plan.env || process.env, procRo
|
|
|
642
657
|
onLine({ state: "fail", text: `the restore failed: ${error.message}; the backup is ${backup.backupFile}` })
|
|
643
658
|
}
|
|
644
659
|
}
|
|
645
|
-
|
|
660
|
+
const because = stopped ? `. The measurement had stopped first: ${stopped.message}` : ""
|
|
661
|
+
if (restoreProblem) throw new WeighError("restore-failed", `shell.json could not be restored from ${backup.backupFile}: ${restoreProblem.message}${because}`, `Copy ${backup.backupFile} over ${plan.configFile} yourself, then run omarchy-restart-shell.`)
|
|
662
|
+
if (stopped && !restore.restored) throw new WeighError("restore-unverified", `shell.json differs from the backup after the restore; the backup ${backup.backupFile} is kept${because}`, `Compare ${backup.backupFile} with ${plan.configFile} and copy it over if the difference is not yours.`)
|
|
663
|
+
if (stopped) throw stopped
|
|
646
664
|
const ended = utc()
|
|
647
665
|
const config = {
|
|
648
666
|
path: plan.configFile,
|