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 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.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)
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
- const effectiveShip = combineShipDecision(shipDecision, posture?.decision);
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: "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.",
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
- // Compute ship_decision: pass | review | block.
742
- // Used in three contexts:
743
- // - one-shot scan: severity-only, no diff baseline
744
- // - trust-diff / preview-diff: severity + diff-since-baseline
745
- // - deploy-check: same as trust-diff (it's an alias)
746
- //
747
- // Decision rules (advisory only):
748
- // * `block` — critical severity present
749
- // * `review` high severity OR diff introduced new high-severity OR DBR drop ≥1.0
750
- // * `pass` — everything else
751
- function computeShipDecision({ maxSeverity, hasUnexpectedDiff, scoreDelta }) {
752
- const sev = String(maxSeverity || "none").toLowerCase();
753
- if (sev === "critical") return "block";
754
- if (sev === "high") return "review";
755
- if (hasUnexpectedDiff) return "review";
756
- if (typeof scoreDelta === "number" && scoreDelta <= -1.0) return "review";
757
- return "pass";
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) — 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.
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; the
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 can parse the fixes
779
- // independently and apply them without round-tripping to the JSON response.
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
- const effectiveShip = combineShipDecision(shipDecision, posture?.decision);
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: "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.",
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.21",
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",