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 CHANGED
@@ -8,7 +8,7 @@
8
8
  [![npm downloads](https://img.shields.io/npm/dm/pqcheck.svg?style=flat-square&color=06b6d4)](https://www.npmjs.com/package/pqcheck)
9
9
  [![license](https://img.shields.io/npm/l/pqcheck.svg?style=flat-square&color=06b6d4)](./LICENSE)
10
10
 
11
- > **Latest: v0.16.20** — `pqcheck deploy-check --ai` now adds an absolute posture grade (A+ F, strict SSL-Labs rubric over CSP / HSTS / X-Frame-Options / X-Content-Type-Options / Referrer-Policy / Permissions-Policy + `x-powered-by` leak) alongside the existing drift-only `ship_decision`. Grade D/F sets `posture_decision=block` so AI coders gate posture failures even on a clean drift. Adds `scope_note` (drift pass functional verification) and surfaces ready-to-paste fix snippets (Next.js / vercel.json / Express). Watched-domain monitoring also gains `posture_regression` + `cert_expiring` alerts. [Full changelog →](./CHANGELOG.md)
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: shipDecision,
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: shipDecision,
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
- // R85 (2026-06-03) — scope honesty disclaimer. pass = trust surface
529
- // stable, NOT app functionality. Agents that follow the protocol must
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(shipDecision);
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: shipDecision,
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: shipDecision,
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
- // R85 — scope honesty disclaimer (per build brief 2026-06-03).
3458
- // ship_decision=pass means trust surface stable, NOT app functions.
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: shipDecision,
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(shipDecision));
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.20",
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",