pqcheck 0.16.20 → 0.16.21
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 +70 -26
- 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.21** — `ship_decision` now folds posture into the headline routing decision (worst-of drift + posture) — closes a footgun where a clean-drift F-posture deploy would have shipped under the original "`pass` → announce" protocol. Posture rubric recalibrated for partial credit: a site with valid HSTS preload + everything else missing now scores ~29 / grade D, not 0 / F. Fix snippets now emit inline in a `CIPHERWAKE_POSTURE_FIXES` block — no JSON round-trip needed to read them. Empty legacy `grade=` field dropped from the AI block. [Full changelog →](./CHANGELOG.md)
|
|
12
12
|
|
|
13
13
|
## Two ways to use it
|
|
14
14
|
|
package/bin/pqcheck.js
CHANGED
|
@@ -511,28 +511,30 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
511
511
|
posture_findings_count: (posture.findings || []).length,
|
|
512
512
|
posture_fixes_count: (posture.fixes || []).length,
|
|
513
513
|
} : {};
|
|
514
|
+
const effectiveShip = combineShipDecision(shipDecision, posture?.decision);
|
|
514
515
|
console.log(formatAiFooterBlock({
|
|
515
|
-
status:
|
|
516
|
+
status: effectiveShip,
|
|
516
517
|
domain,
|
|
517
518
|
kind: "scan",
|
|
518
|
-
dbr: typeof report.score === "number" ? report.score.toFixed(1) :
|
|
519
|
-
grade: report.grade ||
|
|
519
|
+
dbr: typeof report.score === "number" ? report.score.toFixed(1) : undefined,
|
|
520
|
+
grade: report.grade || undefined,
|
|
520
521
|
max_severity: maxSev,
|
|
521
|
-
ship_decision:
|
|
522
|
+
ship_decision: effectiveShip,
|
|
523
|
+
ship_decision_drift: shipDecision,
|
|
524
|
+
ship_decision_posture: posture?.decision,
|
|
522
525
|
unreachable: unreachable ? "true" : "false",
|
|
523
526
|
top_issue: topFinding?.id || topFinding?.title || "none",
|
|
524
527
|
findings_high: findings.filter((f) => severityRank(f.severity) === 3).length,
|
|
525
528
|
findings_critical: findings.filter((f) => severityRank(f.severity) === 4).length,
|
|
526
529
|
scanned_at: new Date().toISOString(),
|
|
527
530
|
advisory_only: "true",
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
// see this so they don't announce a deploy whose signup is broken
|
|
531
|
-
// (the exact bug the user reported on socialideagen 2026-06-02).
|
|
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.",
|
|
531
|
+
scope: "trust_surface_drift_plus_absolute_posture",
|
|
532
|
+
scope_note: "ship_decision = worst-of(drift, absolute posture). 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.",
|
|
534
533
|
...postureFields,
|
|
535
534
|
}));
|
|
535
|
+
if (posture && Array.isArray(posture.fixes) && posture.fixes.length > 0) {
|
|
536
|
+
console.log(formatPostureFixesBlock(posture.fixes));
|
|
537
|
+
}
|
|
536
538
|
console.log("");
|
|
537
539
|
|
|
538
540
|
// Threshold check still applies under --ai (script-pipeable). Otherwise
|
|
@@ -540,7 +542,7 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
540
542
|
if (threshold !== null && typeof report.score === "number" && report.score >= threshold) {
|
|
541
543
|
return 2;
|
|
542
544
|
}
|
|
543
|
-
return shipDecisionExitCode(
|
|
545
|
+
return shipDecisionExitCode(effectiveShip);
|
|
544
546
|
}
|
|
545
547
|
|
|
546
548
|
// Output dispatch
|
|
@@ -755,6 +757,46 @@ function computeShipDecision({ maxSeverity, hasUnexpectedDiff, scoreDelta }) {
|
|
|
755
757
|
return "pass";
|
|
756
758
|
}
|
|
757
759
|
|
|
760
|
+
// R86.2 (2026-06-03) — fold absolute posture grade into ship_decision so an
|
|
761
|
+
// AI agent following the original "ship_decision=pass → safe to announce"
|
|
762
|
+
// protocol does NOT ship a site with F-grade posture just because nothing
|
|
763
|
+
// drifted since last scan. The headline decision must reflect both drift
|
|
764
|
+
// and absolute state.
|
|
765
|
+
//
|
|
766
|
+
// Order: block > review > pass. Worst-of-both wins.
|
|
767
|
+
const SHIP_DECISION_RANK = { pass: 0, review: 1, block: 2 };
|
|
768
|
+
function combineShipDecision(driftDecision, postureDecision) {
|
|
769
|
+
if (!postureDecision) return driftDecision;
|
|
770
|
+
const a = SHIP_DECISION_RANK[driftDecision] ?? 0;
|
|
771
|
+
const b = SHIP_DECISION_RANK[postureDecision] ?? 0;
|
|
772
|
+
return a >= b ? driftDecision : postureDecision;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// 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; the
|
|
777
|
+
// fixes are multi-line code so they get their own block with explicit
|
|
778
|
+
// start/end markers + per-fix delimiters. Agents can parse the fixes
|
|
779
|
+
// independently and apply them without round-tripping to the JSON response.
|
|
780
|
+
function formatPostureFixesBlock(fixes) {
|
|
781
|
+
if (!Array.isArray(fixes) || fixes.length === 0) return "";
|
|
782
|
+
const lines = ["", "CIPHERWAKE_POSTURE_FIXES"];
|
|
783
|
+
for (let i = 0; i < fixes.length; i++) {
|
|
784
|
+
const f = fixes[i] || {};
|
|
785
|
+
lines.push(`--- FIX ${i + 1} ---`);
|
|
786
|
+
lines.push(`finding_id=${String(f.finding_id || "").replace(/[\r\n]+/g, " ")}`);
|
|
787
|
+
lines.push(`title=${String(f.title || "").replace(/[\r\n]+/g, " ")}`);
|
|
788
|
+
lines.push(`framework=${String(f.framework || "").replace(/[\r\n]+/g, " ")}`);
|
|
789
|
+
lines.push(`file_target=${String(f.file_target || "").replace(/[\r\n]+/g, " ")}`);
|
|
790
|
+
lines.push("snippet:");
|
|
791
|
+
const snippet = String(f.snippet || "");
|
|
792
|
+
for (const line of snippet.split(/\r?\n/)) {
|
|
793
|
+
lines.push(line);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
lines.push("END_CIPHERWAKE_POSTURE_FIXES");
|
|
797
|
+
return color("dim", lines.join("\n"));
|
|
798
|
+
}
|
|
799
|
+
|
|
758
800
|
function aiBannerColor(shipDecision) {
|
|
759
801
|
if (shipDecision === "pass") return "green";
|
|
760
802
|
if (shipDecision === "block") return "red";
|
|
@@ -3437,31 +3479,33 @@ async function runTrustDiffCommand(args) {
|
|
|
3437
3479
|
posture_findings_count: (posture.findings || []).length,
|
|
3438
3480
|
posture_fixes_count: (posture.fixes || []).length,
|
|
3439
3481
|
} : {};
|
|
3482
|
+
const effectiveShip = combineShipDecision(shipDecision, posture?.decision);
|
|
3440
3483
|
console.log(formatAiFooterBlock({
|
|
3441
|
-
status:
|
|
3484
|
+
status: effectiveShip,
|
|
3442
3485
|
domain,
|
|
3443
3486
|
kind: "trust-diff",
|
|
3444
3487
|
baseline,
|
|
3445
3488
|
verdict,
|
|
3446
3489
|
delta_count: deltas.length,
|
|
3447
3490
|
max_severity: maxSev,
|
|
3448
|
-
ship_decision:
|
|
3491
|
+
ship_decision: effectiveShip,
|
|
3492
|
+
ship_decision_drift: shipDecision,
|
|
3493
|
+
ship_decision_posture: posture?.decision,
|
|
3449
3494
|
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 ??
|
|
3495
|
+
top_issue_title: topDelta?.title || undefined,
|
|
3496
|
+
dbr: typeof result.current_score === "number" ? result.current_score.toFixed(1) : undefined,
|
|
3497
|
+
grade: result.current_grade || undefined,
|
|
3498
|
+
quota_used: result.quota?.used_this_month ?? undefined,
|
|
3499
|
+
quota_limit: result.quota?.monthly_limit ?? undefined,
|
|
3455
3500
|
scanned_at: new Date().toISOString(),
|
|
3456
3501
|
advisory_only: "true",
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
// Agents must see this so they don't announce a deploy whose signup
|
|
3460
|
-
// is broken (the exact bug socialideagen had on 2026-06-02).
|
|
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.",
|
|
3502
|
+
scope: "trust_surface_drift_plus_absolute_posture",
|
|
3503
|
+
scope_note: "ship_decision = worst-of(drift, absolute posture). 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.",
|
|
3463
3504
|
...postureFields,
|
|
3464
3505
|
}));
|
|
3506
|
+
if (posture && Array.isArray(posture.fixes) && posture.fixes.length > 0) {
|
|
3507
|
+
console.log(formatPostureFixesBlock(posture.fixes));
|
|
3508
|
+
}
|
|
3465
3509
|
console.log("");
|
|
3466
3510
|
|
|
3467
3511
|
await writeLastScanFile({
|
|
@@ -3470,13 +3514,13 @@ async function runTrustDiffCommand(args) {
|
|
|
3470
3514
|
score: typeof result.current_score === "number" ? result.current_score : null,
|
|
3471
3515
|
grade: result.current_grade || null,
|
|
3472
3516
|
max_severity: maxSev,
|
|
3473
|
-
ship_decision:
|
|
3517
|
+
ship_decision: effectiveShip,
|
|
3474
3518
|
baseline,
|
|
3475
3519
|
delta_count: deltas.length,
|
|
3476
3520
|
top_issue: topDelta?.id || topDelta?.title || null,
|
|
3477
3521
|
});
|
|
3478
3522
|
|
|
3479
|
-
process.exit(shipDecisionExitCode(
|
|
3523
|
+
process.exit(shipDecisionExitCode(effectiveShip));
|
|
3480
3524
|
}
|
|
3481
3525
|
|
|
3482
3526
|
// Format output
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pqcheck",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.21",
|
|
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",
|