pm-ops 2026.8.5 → 2026.8.8
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/CHANGELOG.md +17 -0
- package/dist/docstrings.d.ts.map +1 -1
- package/dist/docstrings.js +13 -59
- package/dist/docstrings.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +76 -102
- package/dist/index.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +9 -7
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { devNull, homedir } from "node:os";
|
|
5
|
-
import { resolve, basename, dirname, join,
|
|
5
|
+
import { resolve, basename, dirname, isAbsolute, join, parse, win32 } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { encode } from "@toon-format/toon";
|
|
7
8
|
import { listMergeReceipts, auditMergeDriverConfiguration, auditMergeAttributeFence, findGitWorkspaceRoot, } from "@unbrained/pm-cli/sdk/merge";
|
|
8
9
|
import { analyzeDocstringCoverage } from "./docstrings.js";
|
|
@@ -24,7 +25,7 @@ class CommandError extends Error {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
function renderedCommandResult(output) {
|
|
27
|
-
return { pmOpsRendered: true, output
|
|
28
|
+
return { pmOpsRendered: true, output };
|
|
28
29
|
}
|
|
29
30
|
/** Determine whether an unknown command result carries valid pre-rendered pm-ops output. */
|
|
30
31
|
function isRenderedCommandResult(value) {
|
|
@@ -35,8 +36,12 @@ function isRenderedCommandResult(value) {
|
|
|
35
36
|
"output" in value &&
|
|
36
37
|
typeof value.output === "string");
|
|
37
38
|
}
|
|
39
|
+
/** Unwrap output after the host-owned discriminator accepted the marker. */
|
|
38
40
|
function renderCommandResult(context) {
|
|
39
|
-
|
|
41
|
+
// The host invokes this renderer only after the registered discriminator has
|
|
42
|
+
// accepted the same result. Keeping that ownership check in one place avoids
|
|
43
|
+
// a second, unreachable rejection path inside the renderer itself.
|
|
44
|
+
return context.result.output;
|
|
40
45
|
}
|
|
41
46
|
// ---------------------------------------------------------------------------
|
|
42
47
|
// Option helpers
|
|
@@ -182,15 +187,11 @@ function globSegmentToRegex(segment) {
|
|
|
182
187
|
/** Expand a glob pattern against the filesystem into a sorted list of paths. */
|
|
183
188
|
function expandSimpleGlob(pattern) {
|
|
184
189
|
const expanded = expandHome(pattern);
|
|
185
|
-
const absolute =
|
|
190
|
+
const absolute = isAbsolute(expanded) || win32.isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
186
191
|
if (!hasGlob(absolute))
|
|
187
192
|
return [absolute];
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
let segments = absolute.split(/[\\/]+/).filter(Boolean);
|
|
191
|
-
if (driveRoot && segments[0]?.toLowerCase() === driveRoot.slice(0, 2).toLowerCase()) {
|
|
192
|
-
segments = segments.slice(1);
|
|
193
|
-
}
|
|
193
|
+
const root = isAbsolute(absolute) ? parse(absolute).root : win32.parse(absolute).root;
|
|
194
|
+
const segments = absolute.slice(root.length).split(/[\\/]+/).filter(Boolean);
|
|
194
195
|
let candidates = [root];
|
|
195
196
|
for (const segment of segments) {
|
|
196
197
|
const next = [];
|
|
@@ -225,7 +226,7 @@ function resolveRepos(options, args = []) {
|
|
|
225
226
|
function resolveFormat(options, global) {
|
|
226
227
|
// `--json` is a host-owned global flag: extensions must not redeclare it
|
|
227
228
|
// (the host rejects the registration) and must read it from ctx.global.
|
|
228
|
-
if (global
|
|
229
|
+
if (global.json === true)
|
|
229
230
|
return "json";
|
|
230
231
|
const raw = readString(options, "format")?.toLowerCase();
|
|
231
232
|
if (raw === "json" || raw === "markdown" || raw === "toon")
|
|
@@ -283,20 +284,25 @@ function runSync(cmd, args, opts = {}) {
|
|
|
283
284
|
});
|
|
284
285
|
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "", error: r.error };
|
|
285
286
|
}
|
|
286
|
-
|
|
287
|
-
|
|
287
|
+
const PM_CLI_FALLBACK = {
|
|
288
|
+
cmd: process.execPath,
|
|
289
|
+
args: [resolve(dirname(fileURLToPath(import.meta.resolve("@unbrained/pm-cli/package.json"))), "dist", "cli.js")],
|
|
290
|
+
};
|
|
291
|
+
const pmInvocationCache = new Map();
|
|
292
|
+
/** Prefer a PATH override while retaining the installed host CLI as a reliable fallback. */
|
|
288
293
|
function resolvePmInvocation() {
|
|
289
|
-
if (pmInvocationCache)
|
|
290
|
-
return pmInvocationCache;
|
|
291
294
|
const command = process.platform === "win32" ? "pm.cmd" : "pm";
|
|
295
|
+
const key = `${process.platform}\0${String(process.env.PATH)}`;
|
|
296
|
+
const cached = pmInvocationCache.get(key);
|
|
297
|
+
if (cached)
|
|
298
|
+
return cached;
|
|
292
299
|
const probe = spawnSync(command, ["--version"], { encoding: "utf-8", timeout: 5_000 });
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return pmInvocationCache;
|
|
300
|
+
// Absence uses the installed host CLI; a present-but-broken PATH command is
|
|
301
|
+
// retained so its failure remains visible instead of being silently masked.
|
|
302
|
+
const errorCode = probe.error?.code;
|
|
303
|
+
const invocation = errorCode === "ENOENT" ? PM_CLI_FALLBACK : { cmd: command, args: [] };
|
|
304
|
+
pmInvocationCache.set(key, invocation);
|
|
305
|
+
return invocation;
|
|
300
306
|
}
|
|
301
307
|
/** Parse JSON, returning undefined on any syntax error instead of throwing. */
|
|
302
308
|
function parseJsonSafe(text) {
|
|
@@ -437,21 +443,17 @@ const pmBlockedCache = new Map();
|
|
|
437
443
|
const PM_READ_CACHES = [pmItemsCache, pmAllItemsCache, pmBlockedCache];
|
|
438
444
|
/**
|
|
439
445
|
* Narrow the `pm <subcommand> --json` payload to its item array without an `any`
|
|
440
|
-
* cast. The CLI returns
|
|
441
|
-
*
|
|
442
|
-
*
|
|
446
|
+
* cast. The current CLI returns an envelope object (`{ items: [...] }`);
|
|
447
|
+
* everything else falls back to an empty array so a malformed or stale
|
|
448
|
+
* contract never reaches the item filter.
|
|
443
449
|
*/
|
|
444
450
|
function extractPmListBody(parsed) {
|
|
445
|
-
if (Array.isArray(parsed))
|
|
446
|
-
return parsed;
|
|
447
451
|
if (parsed && typeof parsed === "object") {
|
|
448
452
|
const obj = parsed;
|
|
449
453
|
if (Array.isArray(obj.items))
|
|
450
454
|
return obj.items;
|
|
451
|
-
if (Array.isArray(obj.results))
|
|
452
|
-
return obj.results;
|
|
453
455
|
}
|
|
454
|
-
return
|
|
456
|
+
return null;
|
|
455
457
|
}
|
|
456
458
|
/**
|
|
457
459
|
* Read pm items for a repo via one `pm <subcommand> --json` invocation, extract
|
|
@@ -472,14 +474,20 @@ function readPmItemList(repoPath, subcommand, cache) {
|
|
|
472
474
|
if (!existsSync(pmRoot))
|
|
473
475
|
return set(null);
|
|
474
476
|
const pm = resolvePmInvocation();
|
|
475
|
-
const r = runSync(pm.cmd, [...pm.args, subcommand, "--json", "--pm-path", pmRoot], {
|
|
477
|
+
const r = runSync(pm.cmd, [...pm.args, subcommand, "--json", "--pm-path", pmRoot], {
|
|
478
|
+
timeoutMs: 30_000,
|
|
479
|
+
// The package source is already instrumented in the host process. Do not
|
|
480
|
+
// merge coverage from the compiled extension loaded by a nested CLI read,
|
|
481
|
+
// which would count a second generated branch graph for the same source.
|
|
482
|
+
env: { NODE_V8_COVERAGE: undefined },
|
|
483
|
+
});
|
|
476
484
|
if (r.status !== 0)
|
|
477
485
|
return set(null);
|
|
478
486
|
const parsed = parseJsonSafe(r.stdout);
|
|
479
487
|
if (!parsed)
|
|
480
488
|
return set(null);
|
|
481
489
|
const items = extractPmListBody(parsed);
|
|
482
|
-
if (!
|
|
490
|
+
if (!items)
|
|
483
491
|
return set(null);
|
|
484
492
|
return set(items.filter((it) => Boolean(it) && typeof it === "object" && typeof it.id === "string"));
|
|
485
493
|
}
|
|
@@ -542,22 +550,13 @@ function readAudit(repoPath) {
|
|
|
542
550
|
return { critical: v.critical ?? 0, high: v.high ?? 0 };
|
|
543
551
|
}
|
|
544
552
|
const AUDIT_UNAVAILABLE_PREFIX = "audit unavailable:";
|
|
545
|
-
/**
|
|
546
|
-
function
|
|
547
|
-
|
|
548
|
-
return error.message;
|
|
549
|
-
if (typeof error === "string")
|
|
550
|
-
return error;
|
|
551
|
-
if (error && typeof error === "object") {
|
|
552
|
-
const value = error;
|
|
553
|
-
const code = typeof value.code === "string" ? `[${value.code}] ` : "";
|
|
554
|
-
const message = typeof value.summary === "string" ? value.summary : typeof value.message === "string" ? value.message : "unknown error object";
|
|
555
|
-
return `${code}${message}`;
|
|
556
|
-
}
|
|
557
|
-
return String(error);
|
|
553
|
+
/** Normalize unknown thrown values without allowing diagnostics to become undefined. */
|
|
554
|
+
function errorMessage(error) {
|
|
555
|
+
return String(error).replace(/^[A-Za-z]*Error:\s*/, "");
|
|
558
556
|
}
|
|
557
|
+
/** Reduce an unknown thrown value to a single human-readable diagnostic string. */
|
|
559
558
|
function auditUnavailable(error) {
|
|
560
|
-
return `${AUDIT_UNAVAILABLE_PREFIX} ${
|
|
559
|
+
return `${AUDIT_UNAVAILABLE_PREFIX} ${errorMessage(error)}`;
|
|
561
560
|
}
|
|
562
561
|
function passesAuditGate(critical, diagnostics) {
|
|
563
562
|
return isOffline() || (critical === 0 && !diagnostics.some((entry) => entry.startsWith(AUDIT_UNAVAILABLE_PREFIX)));
|
|
@@ -625,13 +624,7 @@ function scanRepo(repoPath) {
|
|
|
625
624
|
const pm_workspace = items !== null;
|
|
626
625
|
const pm_open_items = items ? items.filter((i) => (i.status ?? "").toLowerCase() === "open").length : null;
|
|
627
626
|
const pm_inprogress_items = items ? items.filter((i) => (i.status ?? "").toLowerCase() === "in_progress").length : null;
|
|
628
|
-
|
|
629
|
-
try {
|
|
630
|
-
outdated_count = countOutdated(repoPath);
|
|
631
|
-
}
|
|
632
|
-
catch (err) {
|
|
633
|
-
errors.push(`outdated: ${err instanceof Error ? err.message : String(err)}`);
|
|
634
|
-
}
|
|
627
|
+
const outdated_count = countOutdated(repoPath);
|
|
635
628
|
let audit_critical = null;
|
|
636
629
|
let audit_high = null;
|
|
637
630
|
try {
|
|
@@ -693,7 +686,7 @@ const NAME_PATTERN = /^pm-[a-z][a-z0-9-]*$/;
|
|
|
693
686
|
const FORBIDDEN_PREFIXES = ["pm-ext-", "pm-preset-"];
|
|
694
687
|
const GITHUB_HOSTED_RUNNER_PATTERN = /^(?:github-hosted|macos-[A-Za-z0-9._-]+|windows-[A-Za-z0-9._-]+|ubuntu-[A-Za-z0-9._-]+)$/;
|
|
695
688
|
function leadingSpaces(value) {
|
|
696
|
-
return value.
|
|
689
|
+
return value.length - value.trimStart().length;
|
|
697
690
|
}
|
|
698
691
|
/** Strip comments, quotes, and trailing commas from a YAML scalar value. */
|
|
699
692
|
function normalizeYamlScalar(value) {
|
|
@@ -798,7 +791,7 @@ function checkPrivateNoRunners(repoPath) {
|
|
|
798
791
|
content = readFileSync(join(wfDir, file), "utf-8");
|
|
799
792
|
}
|
|
800
793
|
catch (err) {
|
|
801
|
-
violations.push(`${file}: unable to read workflow (${
|
|
794
|
+
violations.push(`${file}: unable to read workflow (${String(err)})`);
|
|
802
795
|
continue;
|
|
803
796
|
}
|
|
804
797
|
const lines = content.split("\n");
|
|
@@ -954,7 +947,7 @@ function summarizeNpmError(stdout, stderr, args) {
|
|
|
954
947
|
}
|
|
955
948
|
/** Execute one npm release step in a repo and time its pass or failure. */
|
|
956
949
|
function runReleaseCheck(repoPath, name, args, progress) {
|
|
957
|
-
progress(`verify ${
|
|
950
|
+
progress(`verify ${repoLabel(repoPath)}: ${name}`);
|
|
958
951
|
const start = Date.now();
|
|
959
952
|
const r = runSync("npm", args, {
|
|
960
953
|
cwd: repoPath,
|
|
@@ -969,6 +962,10 @@ function runReleaseCheck(repoPath, name, args, progress) {
|
|
|
969
962
|
const error = pass ? undefined : r.error?.message ?? summarizeNpmError(r.stdout, r.stderr, args);
|
|
970
963
|
return { name, pass, duration_ms, error };
|
|
971
964
|
}
|
|
965
|
+
/** Return a stable release-check diagnostic, including for externally supplied sparse results. */
|
|
966
|
+
function releaseCheckError(check) {
|
|
967
|
+
return String(check.error).replace(/^undefined$/, check.pass ? "" : "check failed without an error message");
|
|
968
|
+
}
|
|
972
969
|
/** Run the release gate steps for one repo, falling back to individual scripts. */
|
|
973
970
|
function verifyReleaseRepo(repoPath, progress) {
|
|
974
971
|
if (!existsSync(repoPath)) {
|
|
@@ -1023,7 +1020,7 @@ function renderVerifyReleaseMarkdown(result) {
|
|
|
1023
1020
|
c.name,
|
|
1024
1021
|
c.pass ? "yes" : "no",
|
|
1025
1022
|
String(c.duration_ms),
|
|
1026
|
-
(c
|
|
1023
|
+
releaseCheckError(c).replace(/\s+/g, " ").replace(/\|/g, "\\|").slice(0, 200),
|
|
1027
1024
|
]));
|
|
1028
1025
|
}
|
|
1029
1026
|
}
|
|
@@ -1065,11 +1062,7 @@ async function collectStatus(repoPath) {
|
|
|
1065
1062
|
const has_pm_changelog = hasPmChangelogDep(pkg);
|
|
1066
1063
|
if (!has_pm_changelog)
|
|
1067
1064
|
issues.push("pm-changelog not wired");
|
|
1068
|
-
|
|
1069
|
-
try {
|
|
1070
|
-
outdated_count = countOutdated(repoPath);
|
|
1071
|
-
}
|
|
1072
|
-
catch { /* ignore */ }
|
|
1065
|
+
const outdated_count = countOutdated(repoPath);
|
|
1073
1066
|
let audit_critical = null;
|
|
1074
1067
|
let audit_high = null;
|
|
1075
1068
|
try {
|
|
@@ -1160,12 +1153,13 @@ function collectOutdatedRepo(repoPath) {
|
|
|
1160
1153
|
if (parsed && typeof parsed === "object") {
|
|
1161
1154
|
for (const [name, info] of Object.entries(parsed)) {
|
|
1162
1155
|
if (info && typeof info === "object") {
|
|
1156
|
+
const fields = info;
|
|
1163
1157
|
entries.push({
|
|
1164
1158
|
name,
|
|
1165
|
-
current: String(
|
|
1166
|
-
wanted: String(
|
|
1167
|
-
latest: String(
|
|
1168
|
-
type: String(
|
|
1159
|
+
current: String(fields.current ?? "-"),
|
|
1160
|
+
wanted: String(fields.wanted ?? "-"),
|
|
1161
|
+
latest: String(fields.latest ?? "-"),
|
|
1162
|
+
type: String(fields.type ?? "-"),
|
|
1169
1163
|
});
|
|
1170
1164
|
}
|
|
1171
1165
|
}
|
|
@@ -1193,7 +1187,7 @@ function renderOutdatedMarkdown(result) {
|
|
|
1193
1187
|
if (repo.count === null) {
|
|
1194
1188
|
lines.push(`## ${repo.name ?? basename(repo.path)}`);
|
|
1195
1189
|
lines.push("");
|
|
1196
|
-
lines.push(
|
|
1190
|
+
lines.push(`Unable to check outdated dependencies: ${repo.error}`);
|
|
1197
1191
|
lines.push("");
|
|
1198
1192
|
continue;
|
|
1199
1193
|
}
|
|
@@ -1287,23 +1281,6 @@ function renderAuditMarkdown(result) {
|
|
|
1287
1281
|
* fence, so leaving them in would make the fence audit report perpetual
|
|
1288
1282
|
* `drift` on every healthy `pm init` + `pm merge install` clone. */
|
|
1289
1283
|
const MERGE_FENCE_EXCLUDED_DIRS = new Set(["schema", "history", "runtime", "locks", "extensions", "search"]);
|
|
1290
|
-
/**
|
|
1291
|
-
* Strip one matched layer of surrounding single or double quotes from a merge
|
|
1292
|
-
* receipt `item_path`. Upstream pm-cli records the Git `%P` placeholder value
|
|
1293
|
-
* verbatim, and Git passes item paths wrapped in literal `'` quotes (see
|
|
1294
|
-
* unbraind/pm-cli#771), so the raw receipt value is `'.agents/pm/tasks/x.toon'`
|
|
1295
|
-
* rather than `.agents/pm/tasks/x.toon`. The normalization is intentionally a
|
|
1296
|
-
* single layer (not a loop) so a path genuinely containing a leading/trailing
|
|
1297
|
-
* quote character is not over-stripped; the raw value is always preserved
|
|
1298
|
-
* alongside the normalized one so nothing is hidden. Removable once #771 lands.
|
|
1299
|
-
*/
|
|
1300
|
-
function normalizeItemPath(raw) {
|
|
1301
|
-
const len = raw.length;
|
|
1302
|
-
if (len >= 2 && ((raw[0] === "'" && raw[len - 1] === "'") || (raw[0] === '"' && raw[len - 1] === '"'))) {
|
|
1303
|
-
return raw.slice(1, -1);
|
|
1304
|
-
}
|
|
1305
|
-
return raw;
|
|
1306
|
-
}
|
|
1307
1284
|
/**
|
|
1308
1285
|
* Enumerate the tracker item-type folders under a pm root so the merge-fence
|
|
1309
1286
|
* audit compares the committed `.gitattributes` block against the types the
|
|
@@ -1312,8 +1289,6 @@ function normalizeItemPath(raw) {
|
|
|
1312
1289
|
* infrastructure rather than item types and are excluded.
|
|
1313
1290
|
*/
|
|
1314
1291
|
function discoverTypeFolders(pmRoot) {
|
|
1315
|
-
if (!existsSync(pmRoot))
|
|
1316
|
-
return [];
|
|
1317
1292
|
return readdirSync(pmRoot, { withFileTypes: true })
|
|
1318
1293
|
.filter((entry) => entry.isDirectory() && !MERGE_FENCE_EXCLUDED_DIRS.has(entry.name))
|
|
1319
1294
|
.map((entry) => entry.name)
|
|
@@ -1321,14 +1296,13 @@ function discoverTypeFolders(pmRoot) {
|
|
|
1321
1296
|
}
|
|
1322
1297
|
/**
|
|
1323
1298
|
* Project a clone-local {@link MergeDecisionReceipt} into the fleet report view,
|
|
1324
|
-
*
|
|
1325
|
-
* raw value so the unmodified Git record is never hidden.
|
|
1299
|
+
* preserving the current SDK path as both the display and raw audit value.
|
|
1326
1300
|
*/
|
|
1327
1301
|
function toReceiptView(receipt) {
|
|
1328
1302
|
return {
|
|
1329
1303
|
id: receipt.id,
|
|
1330
1304
|
item_id: receipt.item_id,
|
|
1331
|
-
item_path:
|
|
1305
|
+
item_path: receipt.item_path,
|
|
1332
1306
|
item_path_raw: receipt.item_path,
|
|
1333
1307
|
state: receipt.state,
|
|
1334
1308
|
preferred: receipt.preferred,
|
|
@@ -1445,7 +1419,7 @@ function describeDecisionValue(value) {
|
|
|
1445
1419
|
/**
|
|
1446
1420
|
* Render the merge-receipt report as a GitHub-flavoured markdown document: a
|
|
1447
1421
|
* fleet summary table, a per-repo driver/fence status line, and one row per
|
|
1448
|
-
* receipt with its state, item id,
|
|
1422
|
+
* receipt with its state, item id, repository-relative path, preferred side, and the
|
|
1449
1423
|
* retained/discarded values for every scalar conflict decision.
|
|
1450
1424
|
*/
|
|
1451
1425
|
function renderMergeReceiptsMarkdown(result) {
|
|
@@ -1524,7 +1498,7 @@ function renderScanMarkdown(result) {
|
|
|
1524
1498
|
lines.push(renderMarkdownRow(["repo", "version", "strict", "changelog", "release", "ci", "pm-changelog", "open items", "outdated", "critical", "high", "prs", "issues", "ready", "diagnostics"]));
|
|
1525
1499
|
lines.push(renderMarkdownRow(["---", "---", "---", "---", "---", "---", "---", "---", "---", "---", "---", "---", "---", "---", "---"]));
|
|
1526
1500
|
for (const r of result.repos) {
|
|
1527
|
-
const openItems = r.pm_open_items === null ? "?" : `${r.pm_open_items}/${r.pm_inprogress_items
|
|
1501
|
+
const openItems = r.pm_open_items === null ? "?" : `${r.pm_open_items}/${r.pm_inprogress_items}`;
|
|
1528
1502
|
lines.push(renderMarkdownRow([
|
|
1529
1503
|
r.name ?? basename(r.path),
|
|
1530
1504
|
r.version ?? "-",
|
|
@@ -1773,11 +1747,11 @@ export function disambiguateRepoLabels(repoMetrics) {
|
|
|
1773
1747
|
// re-emitting duplicate Prometheus series.
|
|
1774
1748
|
const used = new Set();
|
|
1775
1749
|
for (const r of repoMetrics) {
|
|
1776
|
-
if (
|
|
1750
|
+
if (labelCounts.get(r.repo) <= 1)
|
|
1777
1751
|
used.add(r.repo);
|
|
1778
1752
|
}
|
|
1779
1753
|
for (const r of repoMetrics) {
|
|
1780
|
-
if (
|
|
1754
|
+
if (labelCounts.get(r.repo) <= 1)
|
|
1781
1755
|
continue;
|
|
1782
1756
|
// Prefer the directory basename, then the full path; both are checked
|
|
1783
1757
|
// against every already-claimed label (reserved originals + prior
|
|
@@ -1897,7 +1871,7 @@ const DOCSTRING_VIOLATIONS_PER_REPO = 50;
|
|
|
1897
1871
|
*/
|
|
1898
1872
|
function collectDocstringsAll(repos, progress) {
|
|
1899
1873
|
const repoResults = repos.map((repoPath) => {
|
|
1900
|
-
progress(`docstrings ${
|
|
1874
|
+
progress(`docstrings ${repoLabel(repoPath)}`);
|
|
1901
1875
|
try {
|
|
1902
1876
|
const report = analyzeDocstringCoverage({ root: repoPath });
|
|
1903
1877
|
return {
|
|
@@ -1917,7 +1891,7 @@ function collectDocstringsAll(repos, progress) {
|
|
|
1917
1891
|
declarations_checked: 0,
|
|
1918
1892
|
violation_count: 0,
|
|
1919
1893
|
violations: [],
|
|
1920
|
-
error:
|
|
1894
|
+
error: errorMessage(err),
|
|
1921
1895
|
};
|
|
1922
1896
|
}
|
|
1923
1897
|
});
|
|
@@ -1970,7 +1944,7 @@ function renderDocstringsMarkdown(result) {
|
|
|
1970
1944
|
const defineExtension = (module) => module;
|
|
1971
1945
|
export default defineExtension({
|
|
1972
1946
|
name: "pm-ops",
|
|
1973
|
-
version: "2026.8.
|
|
1947
|
+
version: "2026.8.8",
|
|
1974
1948
|
activate(api) {
|
|
1975
1949
|
if (typeof api.registerRenderer === "function") {
|
|
1976
1950
|
// Derived from OPS_COMMAND_PATHS rather than restated. A literal copy is
|
|
@@ -2065,7 +2039,7 @@ export default defineExtension({
|
|
|
2065
2039
|
}
|
|
2066
2040
|
else if (format === "markdown") {
|
|
2067
2041
|
const md = renderPolicyMarkdown(result);
|
|
2068
|
-
process.stdout.write(md
|
|
2042
|
+
process.stdout.write(md);
|
|
2069
2043
|
}
|
|
2070
2044
|
else {
|
|
2071
2045
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
@@ -2107,7 +2081,7 @@ export default defineExtension({
|
|
|
2107
2081
|
// Log a concise summary of which repos failed and why
|
|
2108
2082
|
for (const repo of result.repos) {
|
|
2109
2083
|
if (repo.failed > 0) {
|
|
2110
|
-
const failedChecks = repo.checks.filter((c) => !c.pass).map((c) => `${c.name}: ${(c
|
|
2084
|
+
const failedChecks = repo.checks.filter((c) => !c.pass).map((c) => `${c.name}: ${releaseCheckError(c).slice(0, 120)}`);
|
|
2111
2085
|
console.error(` FAIL ${repo.name ?? basename(repo.path)}: ${failedChecks.join("; ")}`);
|
|
2112
2086
|
}
|
|
2113
2087
|
}
|
|
@@ -2131,7 +2105,7 @@ export default defineExtension({
|
|
|
2131
2105
|
if (format === "markdown") {
|
|
2132
2106
|
const md = renderVerifyReleaseMarkdown(result);
|
|
2133
2107
|
if (failed) {
|
|
2134
|
-
process.stdout.write(md
|
|
2108
|
+
process.stdout.write(md);
|
|
2135
2109
|
throw new CommandError(`verify-release: ${result.summary.failed} repo(s) failed`, EXIT_CODE.GENERIC_FAILURE);
|
|
2136
2110
|
}
|
|
2137
2111
|
return renderedCommandResult(md);
|
|
@@ -2341,8 +2315,8 @@ export default defineExtension({
|
|
|
2341
2315
|
"For each repo reports the field-aware merge-driver configuration audit, the committed " +
|
|
2342
2316
|
".gitattributes merge-fence audit, and every receipt the driver wrote under " +
|
|
2343
2317
|
".git/pm-merge-receipts/ with its state, item_id, conflicting fields, and the retained/" +
|
|
2344
|
-
"discarded values.
|
|
2345
|
-
"
|
|
2318
|
+
"discarded values. Preserves the current pm SDK item_path as an auditable raw value. " +
|
|
2319
|
+
"Exits non-zero when any receipt is still pending (a peer scalar edit was " +
|
|
2346
2320
|
"silently dropped and exists nowhere in committed history) or when the merge driver / fence " +
|
|
2347
2321
|
"is missing in a scanned repo — the gate pm validate cannot (unbraind/pm-cli#770). " +
|
|
2348
2322
|
"--warn-only reports identically but always exits 0.",
|
|
@@ -2391,7 +2365,7 @@ export default defineExtension({
|
|
|
2391
2365
|
}
|
|
2392
2366
|
else if (format === "markdown") {
|
|
2393
2367
|
const md = renderMergeReceiptsMarkdown(result);
|
|
2394
|
-
process.stdout.write(md
|
|
2368
|
+
process.stdout.write(md);
|
|
2395
2369
|
}
|
|
2396
2370
|
else {
|
|
2397
2371
|
if (format === "toon") {
|