pqcheck 0.16.21 → 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 +96 -37
- 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,7 +517,13 @@ 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
|
} : {};
|
|
514
|
-
|
|
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);
|
|
515
527
|
console.log(formatAiFooterBlock({
|
|
516
528
|
status: effectiveShip,
|
|
517
529
|
domain,
|
|
@@ -522,14 +534,17 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
522
534
|
ship_decision: effectiveShip,
|
|
523
535
|
ship_decision_drift: shipDecision,
|
|
524
536
|
ship_decision_posture: posture?.decision,
|
|
537
|
+
ship_decision_mode: strictPosture ? "strict_posture" : "drift_only",
|
|
525
538
|
unreachable: unreachable ? "true" : "false",
|
|
526
539
|
top_issue: topFinding?.id || topFinding?.title || "none",
|
|
527
540
|
findings_high: findings.filter((f) => severityRank(f.severity) === 3).length,
|
|
528
541
|
findings_critical: findings.filter((f) => severityRank(f.severity) === 4).length,
|
|
529
542
|
scanned_at: new Date().toISOString(),
|
|
530
543
|
advisory_only: "true",
|
|
531
|
-
scope: "trust_surface_drift_plus_absolute_posture",
|
|
532
|
-
scope_note:
|
|
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.",
|
|
533
548
|
...postureFields,
|
|
534
549
|
}));
|
|
535
550
|
if (posture && Array.isArray(posture.fixes) && posture.fixes.length > 0) {
|
|
@@ -573,6 +588,14 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
573
588
|
} else {
|
|
574
589
|
if (multi) console.log(color("dim", `\n──── ${domain} ────`));
|
|
575
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
|
+
}
|
|
576
599
|
}
|
|
577
600
|
|
|
578
601
|
if (threshold !== null && typeof report.score === "number" && report.score >= threshold) {
|
|
@@ -738,45 +761,49 @@ function highestSeverity(findings) {
|
|
|
738
761
|
return best;
|
|
739
762
|
}
|
|
740
763
|
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
//
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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}.`);
|
|
758
785
|
}
|
|
759
786
|
|
|
760
|
-
// R86.2 (2026-06-03) —
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
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.
|
|
767
794
|
const SHIP_DECISION_RANK = { pass: 0, review: 1, block: 2 };
|
|
768
|
-
function combineShipDecision(driftDecision, postureDecision) {
|
|
769
|
-
if (!postureDecision) return driftDecision;
|
|
795
|
+
function combineShipDecision(driftDecision, postureDecision, { strict = false } = {}) {
|
|
796
|
+
if (!postureDecision || !strict) return driftDecision;
|
|
770
797
|
const a = SHIP_DECISION_RANK[driftDecision] ?? 0;
|
|
771
798
|
const b = SHIP_DECISION_RANK[postureDecision] ?? 0;
|
|
772
799
|
return a >= b ? driftDecision : postureDecision;
|
|
773
800
|
}
|
|
774
801
|
|
|
775
802
|
// R86.3 (2026-06-03) — emit per-finding fix snippets in a separate parseable
|
|
776
|
-
// block after the AI guard block. The guard block stays compact key=value;
|
|
803
|
+
// block after the AI guard block. The guard block stays compact key=value;
|
|
777
804
|
// fixes are multi-line code so they get their own block with explicit
|
|
778
|
-
// start/end markers + per-fix delimiters. Agents
|
|
779
|
-
//
|
|
805
|
+
// start/end markers + per-fix delimiters. Agents parse + apply without
|
|
806
|
+
// round-tripping to JSON.
|
|
780
807
|
function formatPostureFixesBlock(fixes) {
|
|
781
808
|
if (!Array.isArray(fixes) || fixes.length === 0) return "";
|
|
782
809
|
const lines = ["", "CIPHERWAKE_POSTURE_FIXES"];
|
|
@@ -797,6 +824,25 @@ function formatPostureFixesBlock(fixes) {
|
|
|
797
824
|
return color("dim", lines.join("\n"));
|
|
798
825
|
}
|
|
799
826
|
|
|
827
|
+
// Compute ship_decision: pass | review | block.
|
|
828
|
+
// Used in three contexts:
|
|
829
|
+
// - one-shot scan: severity-only, no diff baseline
|
|
830
|
+
// - trust-diff / preview-diff: severity + diff-since-baseline
|
|
831
|
+
// - deploy-check: same as trust-diff (it's an alias)
|
|
832
|
+
//
|
|
833
|
+
// Decision rules (advisory only):
|
|
834
|
+
// * `block` — critical severity present
|
|
835
|
+
// * `review` — high severity OR diff introduced new high-severity OR DBR drop ≥1.0
|
|
836
|
+
// * `pass` — everything else
|
|
837
|
+
function computeShipDecision({ maxSeverity, hasUnexpectedDiff, scoreDelta }) {
|
|
838
|
+
const sev = String(maxSeverity || "none").toLowerCase();
|
|
839
|
+
if (sev === "critical") return "block";
|
|
840
|
+
if (sev === "high") return "review";
|
|
841
|
+
if (hasUnexpectedDiff) return "review";
|
|
842
|
+
if (typeof scoreDelta === "number" && scoreDelta <= -1.0) return "review";
|
|
843
|
+
return "pass";
|
|
844
|
+
}
|
|
845
|
+
|
|
800
846
|
function aiBannerColor(shipDecision) {
|
|
801
847
|
if (shipDecision === "pass") return "green";
|
|
802
848
|
if (shipDecision === "block") return "red";
|
|
@@ -3334,6 +3380,11 @@ async function runTrustDiffCommand(args) {
|
|
|
3334
3380
|
const baseline = parseFlag(args, "--baseline") || "last-week";
|
|
3335
3381
|
const failOn = parseFlag(args, "--fail-on") || "high";
|
|
3336
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");
|
|
3337
3388
|
|
|
3338
3389
|
// Build headers conditionally — Authorization is set ONLY if the user has
|
|
3339
3390
|
// an API key. Without it, the server's applyRepoQuota falls through to the
|
|
@@ -3479,7 +3530,12 @@ async function runTrustDiffCommand(args) {
|
|
|
3479
3530
|
posture_findings_count: (posture.findings || []).length,
|
|
3480
3531
|
posture_fixes_count: (posture.fixes || []).length,
|
|
3481
3532
|
} : {};
|
|
3482
|
-
|
|
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);
|
|
3483
3539
|
console.log(formatAiFooterBlock({
|
|
3484
3540
|
status: effectiveShip,
|
|
3485
3541
|
domain,
|
|
@@ -3491,6 +3547,7 @@ async function runTrustDiffCommand(args) {
|
|
|
3491
3547
|
ship_decision: effectiveShip,
|
|
3492
3548
|
ship_decision_drift: shipDecision,
|
|
3493
3549
|
ship_decision_posture: posture?.decision,
|
|
3550
|
+
ship_decision_mode: strictPosture ? "strict_posture" : "drift_only",
|
|
3494
3551
|
top_issue: topDelta?.id || topDelta?.type || "none",
|
|
3495
3552
|
top_issue_title: topDelta?.title || undefined,
|
|
3496
3553
|
dbr: typeof result.current_score === "number" ? result.current_score.toFixed(1) : undefined,
|
|
@@ -3499,8 +3556,10 @@ async function runTrustDiffCommand(args) {
|
|
|
3499
3556
|
quota_limit: result.quota?.monthly_limit ?? undefined,
|
|
3500
3557
|
scanned_at: new Date().toISOString(),
|
|
3501
3558
|
advisory_only: "true",
|
|
3502
|
-
scope: "trust_surface_drift_plus_absolute_posture",
|
|
3503
|
-
scope_note:
|
|
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.",
|
|
3504
3563
|
...postureFields,
|
|
3505
3564
|
}));
|
|
3506
3565
|
if (posture && Array.isArray(posture.fixes) && posture.fixes.length > 0) {
|
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",
|