pqcheck 0.16.19 → 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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/bin/pqcheck.js +102 -14
  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.19** — `pqcheck setup` now COMPOSES with an existing Claude Code `statusLine.command` (e.g., PinnedAI's) via a new `--prepend=<cmd>` flag on `cipherwake-statusline`. Both tools' badges render in the single statusLine slot. Previously, Cipherwake politely skipped if any prior statusLine existed which meant you'd only ever see one tool's badge. Now: composed wrap with 5s timeout + graceful degradation on prepend failure + idempotent re-runs. [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
@@ -495,21 +495,46 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
495
495
  }
496
496
  }
497
497
 
498
+ // R86 — surface absolute posture grade + remediation alongside drift verdict.
499
+ // The grade= field carries DBR grade; posture_grade= is the new absolute
500
+ // posture (A+/A/B/C/D/F based on header presence/strength).
501
+ // posture_decision separates the absolute-state routing signal from the
502
+ // drift-based ship_decision so a weak-but-unchanged site gets a review
503
+ // nudge distinct from drift.
504
+ const posture = report.posture || null;
505
+ const postureFields = posture ? {
506
+ posture_grade: posture.grade,
507
+ posture_score: posture.score,
508
+ posture_decision: posture.decision,
509
+ posture_missing: (posture.missing || []).join(",") || "none",
510
+ posture_leaks: (posture.info_leaks || []).join("; ") || "none",
511
+ posture_findings_count: (posture.findings || []).length,
512
+ posture_fixes_count: (posture.fixes || []).length,
513
+ } : {};
514
+ const effectiveShip = combineShipDecision(shipDecision, posture?.decision);
498
515
  console.log(formatAiFooterBlock({
499
- status: shipDecision,
516
+ status: effectiveShip,
500
517
  domain,
501
518
  kind: "scan",
502
- dbr: typeof report.score === "number" ? report.score.toFixed(1) : "",
503
- grade: report.grade || "",
519
+ dbr: typeof report.score === "number" ? report.score.toFixed(1) : undefined,
520
+ grade: report.grade || undefined,
504
521
  max_severity: maxSev,
505
- ship_decision: shipDecision,
522
+ ship_decision: effectiveShip,
523
+ ship_decision_drift: shipDecision,
524
+ ship_decision_posture: posture?.decision,
506
525
  unreachable: unreachable ? "true" : "false",
507
526
  top_issue: topFinding?.id || topFinding?.title || "none",
508
527
  findings_high: findings.filter((f) => severityRank(f.severity) === 3).length,
509
528
  findings_critical: findings.filter((f) => severityRank(f.severity) === 4).length,
510
529
  scanned_at: new Date().toISOString(),
511
530
  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.",
533
+ ...postureFields,
512
534
  }));
535
+ if (posture && Array.isArray(posture.fixes) && posture.fixes.length > 0) {
536
+ console.log(formatPostureFixesBlock(posture.fixes));
537
+ }
513
538
  console.log("");
514
539
 
515
540
  // Threshold check still applies under --ai (script-pipeable). Otherwise
@@ -517,7 +542,7 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
517
542
  if (threshold !== null && typeof report.score === "number" && report.score >= threshold) {
518
543
  return 2;
519
544
  }
520
- return shipDecisionExitCode(shipDecision);
545
+ return shipDecisionExitCode(effectiveShip);
521
546
  }
522
547
 
523
548
  // Output dispatch
@@ -732,6 +757,46 @@ function computeShipDecision({ maxSeverity, hasUnexpectedDiff, scoreDelta }) {
732
757
  return "pass";
733
758
  }
734
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
+
735
800
  function aiBannerColor(shipDecision) {
736
801
  if (shipDecision === "pass") return "green";
737
802
  if (shipDecision === "block") return "red";
@@ -3400,24 +3465,47 @@ async function runTrustDiffCommand(args) {
3400
3465
  console.log(color("dim", " Run with --verbose to see all verified signals."));
3401
3466
  }
3402
3467
 
3468
+ // R86 — posture grade + remediation alongside drift. trust-diff API returns
3469
+ // result.current_report which contains the full scan; if so, read posture
3470
+ // from it. Otherwise gracefully omit (older API responses without posture).
3471
+ const currentReport = result.current_report || result.report || null;
3472
+ const posture = currentReport?.posture || null;
3473
+ const postureFields = posture ? {
3474
+ posture_grade: posture.grade,
3475
+ posture_score: posture.score,
3476
+ posture_decision: posture.decision,
3477
+ posture_missing: (posture.missing || []).join(",") || "none",
3478
+ posture_leaks: (posture.info_leaks || []).join("; ") || "none",
3479
+ posture_findings_count: (posture.findings || []).length,
3480
+ posture_fixes_count: (posture.fixes || []).length,
3481
+ } : {};
3482
+ const effectiveShip = combineShipDecision(shipDecision, posture?.decision);
3403
3483
  console.log(formatAiFooterBlock({
3404
- status: shipDecision,
3484
+ status: effectiveShip,
3405
3485
  domain,
3406
3486
  kind: "trust-diff",
3407
3487
  baseline,
3408
3488
  verdict,
3409
3489
  delta_count: deltas.length,
3410
3490
  max_severity: maxSev,
3411
- ship_decision: shipDecision,
3491
+ ship_decision: effectiveShip,
3492
+ ship_decision_drift: shipDecision,
3493
+ ship_decision_posture: posture?.decision,
3412
3494
  top_issue: topDelta?.id || topDelta?.type || "none",
3413
- top_issue_title: topDelta?.title || "",
3414
- dbr: typeof result.current_score === "number" ? result.current_score.toFixed(1) : "",
3415
- grade: result.current_grade || "",
3416
- quota_used: result.quota?.used_this_month ?? "",
3417
- 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,
3418
3500
  scanned_at: new Date().toISOString(),
3419
3501
  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.",
3504
+ ...postureFields,
3420
3505
  }));
3506
+ if (posture && Array.isArray(posture.fixes) && posture.fixes.length > 0) {
3507
+ console.log(formatPostureFixesBlock(posture.fixes));
3508
+ }
3421
3509
  console.log("");
3422
3510
 
3423
3511
  await writeLastScanFile({
@@ -3426,13 +3514,13 @@ async function runTrustDiffCommand(args) {
3426
3514
  score: typeof result.current_score === "number" ? result.current_score : null,
3427
3515
  grade: result.current_grade || null,
3428
3516
  max_severity: maxSev,
3429
- ship_decision: shipDecision,
3517
+ ship_decision: effectiveShip,
3430
3518
  baseline,
3431
3519
  delta_count: deltas.length,
3432
3520
  top_issue: topDelta?.id || topDelta?.title || null,
3433
3521
  });
3434
3522
 
3435
- process.exit(shipDecisionExitCode(shipDecision));
3523
+ process.exit(shipDecisionExitCode(effectiveShip));
3436
3524
  }
3437
3525
 
3438
3526
  // Format output
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pqcheck",
3
- "version": "0.16.19",
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",