pqcheck 0.16.20 → 0.16.22
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 +1 -1
- package/bin/pqcheck.js +131 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/pqcheck)
|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
|
|
11
|
-
> **Latest: v0.16.
|
|
11
|
+
> **Latest: v0.16.22** — Drift gates, posture advises. Default `ship_decision` reverts to drift-only (the right per-deploy regression gate); posture grade is surfaced as a one-line advisory + ready-to-paste fix snippets, NOT as an auto-block. A site that's been D-posture for months hasn't regressed on today's deploy; gating it every time trains people to ignore the gate. Once your site reaches A/B posture, pass `--strict-posture` as the "lock it in, prevent backsliding" gate. New `ship_decision_drift` / `ship_decision_posture` / `ship_decision_mode` fields make both signals visible. [Full changelog →](./CHANGELOG.md)
|
|
12
12
|
|
|
13
13
|
## Two ways to use it
|
|
14
14
|
|
package/bin/pqcheck.js
CHANGED
|
@@ -290,17 +290,23 @@ async function main() {
|
|
|
290
290
|
const fresh = args.includes("--fresh") || args.includes("--force");
|
|
291
291
|
const aiMode = parseAiMode(args);
|
|
292
292
|
const verbose = isVerboseMode(args); // v0.16.0 — opt-in detailed panel
|
|
293
|
+
// R86.4 (2026-06-03) — strict-posture opt-in. Default ship_decision is
|
|
294
|
+
// drift-only (per-deploy regression gate); --strict-posture folds the
|
|
295
|
+
// absolute posture grade into ship_decision. Recommended ONLY after a
|
|
296
|
+
// site reaches A/B posture — opting in earlier produces cry-wolf gating
|
|
297
|
+
// on every deploy since most AI-coded sites grade D/F out of the box.
|
|
298
|
+
const strictPosture = args.includes("--strict-posture");
|
|
293
299
|
|
|
294
300
|
// One-shot scan(s)
|
|
295
301
|
let worstExit = 0;
|
|
296
302
|
for (const domain of domains) {
|
|
297
|
-
const exit = await runOneScan({ domain, format, quiet, threshold, webhookUrl, multi: domains.length > 1, fresh, aiMode, verbose });
|
|
303
|
+
const exit = await runOneScan({ domain, format, quiet, threshold, webhookUrl, multi: domains.length > 1, fresh, aiMode, verbose, strictPosture });
|
|
298
304
|
if (exit > worstExit) worstExit = exit;
|
|
299
305
|
}
|
|
300
306
|
process.exit(worstExit);
|
|
301
307
|
}
|
|
302
308
|
|
|
303
|
-
async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi, fresh, aiMode, verbose }) {
|
|
309
|
+
async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi, fresh, aiMode, verbose, strictPosture }) {
|
|
304
310
|
if (!quiet && format === "text") process.stderr.write(color("dim", `Scanning ${domain}${fresh ? " (forcing fresh)" : ""} ...`));
|
|
305
311
|
let report;
|
|
306
312
|
try {
|
|
@@ -511,28 +517,39 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
511
517
|
posture_findings_count: (posture.findings || []).length,
|
|
512
518
|
posture_fixes_count: (posture.fixes || []).length,
|
|
513
519
|
} : {};
|
|
520
|
+
// R86.2 (2026-06-03) — see combineShipDecision: posture is advisory by
|
|
521
|
+
// default, gated when --strict-posture is passed.
|
|
522
|
+
const effectiveShip = combineShipDecision(shipDecision, posture?.decision, { strict: strictPosture });
|
|
523
|
+
// R86.5 — surface posture advisory line so D/F posture is never silently
|
|
524
|
+
// blessed under the drift-only default.
|
|
525
|
+
const postureAdvisory = formatPostureAdvisoryLine(posture, strictPosture);
|
|
526
|
+
if (postureAdvisory) console.log(postureAdvisory);
|
|
514
527
|
console.log(formatAiFooterBlock({
|
|
515
|
-
status:
|
|
528
|
+
status: effectiveShip,
|
|
516
529
|
domain,
|
|
517
530
|
kind: "scan",
|
|
518
|
-
dbr: typeof report.score === "number" ? report.score.toFixed(1) :
|
|
519
|
-
grade: report.grade ||
|
|
531
|
+
dbr: typeof report.score === "number" ? report.score.toFixed(1) : undefined,
|
|
532
|
+
grade: report.grade || undefined,
|
|
520
533
|
max_severity: maxSev,
|
|
521
|
-
ship_decision:
|
|
534
|
+
ship_decision: effectiveShip,
|
|
535
|
+
ship_decision_drift: shipDecision,
|
|
536
|
+
ship_decision_posture: posture?.decision,
|
|
537
|
+
ship_decision_mode: strictPosture ? "strict_posture" : "drift_only",
|
|
522
538
|
unreachable: unreachable ? "true" : "false",
|
|
523
539
|
top_issue: topFinding?.id || topFinding?.title || "none",
|
|
524
540
|
findings_high: findings.filter((f) => severityRank(f.severity) === 3).length,
|
|
525
541
|
findings_critical: findings.filter((f) => severityRank(f.severity) === 4).length,
|
|
526
542
|
scanned_at: new Date().toISOString(),
|
|
527
543
|
advisory_only: "true",
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
scope: "trust_surface_drift",
|
|
533
|
-
scope_note: "ship_decision=pass means public trust surface stable. Does NOT verify app functionality. Pair with functional checks (e.g. Playwright e2e) for full deploy safety.",
|
|
544
|
+
scope: strictPosture ? "trust_surface_drift_plus_absolute_posture" : "trust_surface_drift",
|
|
545
|
+
scope_note: strictPosture
|
|
546
|
+
? "ship_decision = worst-of(drift, absolute posture) because --strict-posture is set. pass means BOTH no drift AND posture grade A+/A. ship_decision_drift and ship_decision_posture expose the two inputs separately. Cipherwake does NOT verify app functionality — pair with Playwright e2e for full deploy safety."
|
|
547
|
+
: "ship_decision reflects DRIFT only by default (per-deploy regression gate). Posture grade is surfaced via posture_decision / ship_decision_posture as advisory — D/F posture does NOT auto-block. Once your site reaches A/B posture, pass --strict-posture to lock that in. Cipherwake does NOT verify app functionality.",
|
|
534
548
|
...postureFields,
|
|
535
549
|
}));
|
|
550
|
+
if (posture && Array.isArray(posture.fixes) && posture.fixes.length > 0) {
|
|
551
|
+
console.log(formatPostureFixesBlock(posture.fixes));
|
|
552
|
+
}
|
|
536
553
|
console.log("");
|
|
537
554
|
|
|
538
555
|
// Threshold check still applies under --ai (script-pipeable). Otherwise
|
|
@@ -540,7 +557,7 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
540
557
|
if (threshold !== null && typeof report.score === "number" && report.score >= threshold) {
|
|
541
558
|
return 2;
|
|
542
559
|
}
|
|
543
|
-
return shipDecisionExitCode(
|
|
560
|
+
return shipDecisionExitCode(effectiveShip);
|
|
544
561
|
}
|
|
545
562
|
|
|
546
563
|
// Output dispatch
|
|
@@ -571,6 +588,14 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
571
588
|
} else {
|
|
572
589
|
if (multi) console.log(color("dim", `\n──── ${domain} ────`));
|
|
573
590
|
printReport(report);
|
|
591
|
+
// R86.5 — posture advisory line so D/F posture is never silently blessed
|
|
592
|
+
// in human output either. Posture is advisory, not gating.
|
|
593
|
+
const postureAdvisory = formatPostureAdvisoryLine(report.posture, strictPosture);
|
|
594
|
+
if (postureAdvisory) {
|
|
595
|
+
console.log("");
|
|
596
|
+
console.log(postureAdvisory);
|
|
597
|
+
console.log(color("dim", " See /methodology/posture-grading for the rubric + fix snippets."));
|
|
598
|
+
}
|
|
574
599
|
}
|
|
575
600
|
|
|
576
601
|
if (threshold !== null && typeof report.score === "number" && report.score >= threshold) {
|
|
@@ -736,6 +761,69 @@ function highestSeverity(findings) {
|
|
|
736
761
|
return best;
|
|
737
762
|
}
|
|
738
763
|
|
|
764
|
+
// R86.5 (2026-06-03) — posture advisory line. Posture is a STANDING property,
|
|
765
|
+
// not a per-deploy regression signal, so gating every deploy on it is cry-wolf
|
|
766
|
+
// by construction. The drift-only ship_decision is the right default gate
|
|
767
|
+
// (fires on regression). This helper surfaces posture grade + a one-line fix
|
|
768
|
+
// nudge alongside the gate result — customer sees the grade, sees the fix
|
|
769
|
+
// path, is NOT blocked on every deploy. Only customers who have reached A/B
|
|
770
|
+
// posture opt into --strict-posture to lock that in.
|
|
771
|
+
function formatPostureAdvisoryLine(posture, strictPosture) {
|
|
772
|
+
if (!posture) return "";
|
|
773
|
+
if (strictPosture) return ""; // strict mode already gates; redundant
|
|
774
|
+
const decision = posture.decision;
|
|
775
|
+
if (decision !== "block" && decision !== "review") return ""; // A+/A — no advisory
|
|
776
|
+
const grade = posture.grade || "?";
|
|
777
|
+
const score = typeof posture.score === "number" ? posture.score : "?";
|
|
778
|
+
const fixCount = Array.isArray(posture.fixes) ? posture.fixes.length : 0;
|
|
779
|
+
const colorName = decision === "block" ? "red" : "yellow";
|
|
780
|
+
const icon = decision === "block" ? "●" : "○";
|
|
781
|
+
const nudge = fixCount > 0
|
|
782
|
+
? `${fixCount} ready-to-paste fix${fixCount === 1 ? "" : "es"} in CIPHERWAKE_POSTURE_FIXES`
|
|
783
|
+
: "see /methodology/posture-grading";
|
|
784
|
+
return color(colorName, ` ${icon} Posture: ${grade} (score ${score}) — advisory, not gating. ${nudge}.`);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// R86.2 / R86.4 (2026-06-03) — combine drift + posture into ship_decision.
|
|
788
|
+
// Default (strict=false): drift only. Posture grade is surfaced via separate
|
|
789
|
+
// fields and the advisory line but does NOT promote drift's decision —
|
|
790
|
+
// because most AI-coded sites grade D/F out of the box, making posture-gating
|
|
791
|
+
// the default would cry-wolf on every deploy of every header-less site,
|
|
792
|
+
// training people to ignore the gate. --strict-posture opts in for teams
|
|
793
|
+
// who've already fixed posture and want to prevent backsliding.
|
|
794
|
+
const SHIP_DECISION_RANK = { pass: 0, review: 1, block: 2 };
|
|
795
|
+
function combineShipDecision(driftDecision, postureDecision, { strict = false } = {}) {
|
|
796
|
+
if (!postureDecision || !strict) return driftDecision;
|
|
797
|
+
const a = SHIP_DECISION_RANK[driftDecision] ?? 0;
|
|
798
|
+
const b = SHIP_DECISION_RANK[postureDecision] ?? 0;
|
|
799
|
+
return a >= b ? driftDecision : postureDecision;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// R86.3 (2026-06-03) — emit per-finding fix snippets in a separate parseable
|
|
803
|
+
// block after the AI guard block. The guard block stays compact key=value;
|
|
804
|
+
// fixes are multi-line code so they get their own block with explicit
|
|
805
|
+
// start/end markers + per-fix delimiters. Agents parse + apply without
|
|
806
|
+
// round-tripping to JSON.
|
|
807
|
+
function formatPostureFixesBlock(fixes) {
|
|
808
|
+
if (!Array.isArray(fixes) || fixes.length === 0) return "";
|
|
809
|
+
const lines = ["", "CIPHERWAKE_POSTURE_FIXES"];
|
|
810
|
+
for (let i = 0; i < fixes.length; i++) {
|
|
811
|
+
const f = fixes[i] || {};
|
|
812
|
+
lines.push(`--- FIX ${i + 1} ---`);
|
|
813
|
+
lines.push(`finding_id=${String(f.finding_id || "").replace(/[\r\n]+/g, " ")}`);
|
|
814
|
+
lines.push(`title=${String(f.title || "").replace(/[\r\n]+/g, " ")}`);
|
|
815
|
+
lines.push(`framework=${String(f.framework || "").replace(/[\r\n]+/g, " ")}`);
|
|
816
|
+
lines.push(`file_target=${String(f.file_target || "").replace(/[\r\n]+/g, " ")}`);
|
|
817
|
+
lines.push("snippet:");
|
|
818
|
+
const snippet = String(f.snippet || "");
|
|
819
|
+
for (const line of snippet.split(/\r?\n/)) {
|
|
820
|
+
lines.push(line);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
lines.push("END_CIPHERWAKE_POSTURE_FIXES");
|
|
824
|
+
return color("dim", lines.join("\n"));
|
|
825
|
+
}
|
|
826
|
+
|
|
739
827
|
// Compute ship_decision: pass | review | block.
|
|
740
828
|
// Used in three contexts:
|
|
741
829
|
// - one-shot scan: severity-only, no diff baseline
|
|
@@ -3292,6 +3380,11 @@ async function runTrustDiffCommand(args) {
|
|
|
3292
3380
|
const baseline = parseFlag(args, "--baseline") || "last-week";
|
|
3293
3381
|
const failOn = parseFlag(args, "--fail-on") || "high";
|
|
3294
3382
|
const format = parseFlag(args, "--format") || "pretty";
|
|
3383
|
+
// R86.4 (2026-06-03) — see runOneScan for rationale. Default ship_decision
|
|
3384
|
+
// is drift-only (per-deploy regression gate); --strict-posture opts into
|
|
3385
|
+
// worst-of(drift, posture). Recommended only after a site reaches A/B
|
|
3386
|
+
// posture to lock that in.
|
|
3387
|
+
const strictPosture = args.includes("--strict-posture");
|
|
3295
3388
|
|
|
3296
3389
|
// Build headers conditionally — Authorization is set ONLY if the user has
|
|
3297
3390
|
// an API key. Without it, the server's applyRepoQuota falls through to the
|
|
@@ -3437,31 +3530,41 @@ async function runTrustDiffCommand(args) {
|
|
|
3437
3530
|
posture_findings_count: (posture.findings || []).length,
|
|
3438
3531
|
posture_fixes_count: (posture.fixes || []).length,
|
|
3439
3532
|
} : {};
|
|
3533
|
+
// R86.2 / R86.4 — see combineShipDecision: posture is advisory by default,
|
|
3534
|
+
// gated only when --strict-posture is passed.
|
|
3535
|
+
const effectiveShip = combineShipDecision(shipDecision, posture?.decision, { strict: strictPosture });
|
|
3536
|
+
// R86.5 — surface posture advisory line in default (non-strict) mode.
|
|
3537
|
+
const postureAdvisory = formatPostureAdvisoryLine(posture, strictPosture);
|
|
3538
|
+
if (postureAdvisory) console.log(postureAdvisory);
|
|
3440
3539
|
console.log(formatAiFooterBlock({
|
|
3441
|
-
status:
|
|
3540
|
+
status: effectiveShip,
|
|
3442
3541
|
domain,
|
|
3443
3542
|
kind: "trust-diff",
|
|
3444
3543
|
baseline,
|
|
3445
3544
|
verdict,
|
|
3446
3545
|
delta_count: deltas.length,
|
|
3447
3546
|
max_severity: maxSev,
|
|
3448
|
-
ship_decision:
|
|
3547
|
+
ship_decision: effectiveShip,
|
|
3548
|
+
ship_decision_drift: shipDecision,
|
|
3549
|
+
ship_decision_posture: posture?.decision,
|
|
3550
|
+
ship_decision_mode: strictPosture ? "strict_posture" : "drift_only",
|
|
3449
3551
|
top_issue: topDelta?.id || topDelta?.type || "none",
|
|
3450
|
-
top_issue_title: topDelta?.title ||
|
|
3451
|
-
dbr: typeof result.current_score === "number" ? result.current_score.toFixed(1) :
|
|
3452
|
-
grade: result.current_grade ||
|
|
3453
|
-
quota_used: result.quota?.used_this_month ??
|
|
3454
|
-
quota_limit: result.quota?.monthly_limit ??
|
|
3552
|
+
top_issue_title: topDelta?.title || undefined,
|
|
3553
|
+
dbr: typeof result.current_score === "number" ? result.current_score.toFixed(1) : undefined,
|
|
3554
|
+
grade: result.current_grade || undefined,
|
|
3555
|
+
quota_used: result.quota?.used_this_month ?? undefined,
|
|
3556
|
+
quota_limit: result.quota?.monthly_limit ?? undefined,
|
|
3455
3557
|
scanned_at: new Date().toISOString(),
|
|
3456
3558
|
advisory_only: "true",
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
scope: "trust_surface_drift",
|
|
3462
|
-
scope_note: "ship_decision=pass means public trust surface stable. Does NOT verify app functionality. Pair with functional checks for full deploy safety.",
|
|
3559
|
+
scope: strictPosture ? "trust_surface_drift_plus_absolute_posture" : "trust_surface_drift",
|
|
3560
|
+
scope_note: strictPosture
|
|
3561
|
+
? "ship_decision = worst-of(drift, absolute posture) because --strict-posture is set. pass means BOTH no drift AND posture grade A+/A. ship_decision_drift and ship_decision_posture expose the two inputs separately. Cipherwake does NOT verify app functionality — pair with Playwright e2e for full deploy safety."
|
|
3562
|
+
: "ship_decision reflects DRIFT only by default (per-deploy regression gate). Posture grade is surfaced via posture_decision / ship_decision_posture as advisory — D/F posture does NOT auto-block. Once your site reaches A/B posture, pass --strict-posture to lock that in. Cipherwake does NOT verify app functionality.",
|
|
3463
3563
|
...postureFields,
|
|
3464
3564
|
}));
|
|
3565
|
+
if (posture && Array.isArray(posture.fixes) && posture.fixes.length > 0) {
|
|
3566
|
+
console.log(formatPostureFixesBlock(posture.fixes));
|
|
3567
|
+
}
|
|
3465
3568
|
console.log("");
|
|
3466
3569
|
|
|
3467
3570
|
await writeLastScanFile({
|
|
@@ -3470,13 +3573,13 @@ async function runTrustDiffCommand(args) {
|
|
|
3470
3573
|
score: typeof result.current_score === "number" ? result.current_score : null,
|
|
3471
3574
|
grade: result.current_grade || null,
|
|
3472
3575
|
max_severity: maxSev,
|
|
3473
|
-
ship_decision:
|
|
3576
|
+
ship_decision: effectiveShip,
|
|
3474
3577
|
baseline,
|
|
3475
3578
|
delta_count: deltas.length,
|
|
3476
3579
|
top_issue: topDelta?.id || topDelta?.title || null,
|
|
3477
3580
|
});
|
|
3478
3581
|
|
|
3479
|
-
process.exit(shipDecisionExitCode(
|
|
3582
|
+
process.exit(shipDecisionExitCode(effectiveShip));
|
|
3480
3583
|
}
|
|
3481
3584
|
|
|
3482
3585
|
// Format output
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pqcheck",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.22",
|
|
4
4
|
"description": "Deploy gate for AI-coded web apps. `pqcheck deploy-check --ai` returns ship_decision=pass|review|block for Claude Code / Cursor / Copilot / Aider to gate deploys before they ship. Anonymous, no signup, free for first use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-coder",
|