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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/bin/pqcheck.js +131 -28
  3. package/package.json +1 -1
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.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: shipDecision,
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: shipDecision,
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
- // 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.",
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(shipDecision);
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: shipDecision,
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: shipDecision,
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
- // 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.",
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: shipDecision,
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(shipDecision));
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.20",
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",