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.
@@ -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
- /** Human-readable size of the pinned checkout, for `omakit pin` and `omakit doctor`. */
182
- export function pinDiskUsage(dir) {
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
- try {
187
- const output = execFileSync("du", ["-skH", dir], { encoding: "utf8" }).split(/\s+/)[0]
188
- const mib = Number(output) / 1024
189
- return `${mib < 10 ? mib.toFixed(1) : Math.round(mib)} MB on disk`
190
- } catch {
191
- return "size unknown"
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. 73% of the 464 submissions parked in the author's court have a HEAD ahead of their validated commit, 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.",
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 the 464 submissions parked in
13
- // the author's court, 73% have a default-branch HEAD ahead of the validated
14
- // commit. 47% pushed after the maintainer's review without the marketplace ever
15
- // seeing it, and 82% of those authors also commented, so they are engaged and
16
- // stuck rather than gone. Of 13 open submissions inspected with no labels left,
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
- return { mode: "all", account: discovery.account, marketplace: discovery.marketplace, summary, issues: results }
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.login !== "github-actions[bot]",
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,
@@ -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
- const cpu0 = cpuTicks(procRoot, pid) ?? 0
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) ?? cpu0
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
- if (restoreProblem) throw new WeighError("restore-failed", `shell.json could not be restored from ${backup.backupFile}: ${restoreProblem.message}`, `Copy ${backup.backupFile} over ${plan.configFile} yourself, then run omarchy-restart-shell.`)
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,