pqcheck 0.16.0 → 0.16.3
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/cipherwake-statusline.js +12 -0
- package/bin/pqcheck.js +269 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ You get the same scanner that powers [cipherwake.io](https://cipherwake.io), the
|
|
|
49
49
|
| `npx pqcheck <domain>` | The basic scan. Returns DBR score (0–10), letter grade (A–F), and a list of findings ranked by severity. The fastest way to ask "is my site's HTTPS posture healthy today?" |
|
|
50
50
|
| `npx pqcheck deploy-check <domain> --ai` | The flagship command. Wraps the scan with `ship_decision=pass\|review\|block` for your AI coding agent to gate the deploy announcement. Works anonymously on first use; subsequent runs compare against the previous scan. |
|
|
51
51
|
| `npx pqcheck trust-diff <domain>` | Compare today's HTTPS surface against a saved baseline (last week / last month / a saved CI baseline). For CI gates and release checklists. |
|
|
52
|
-
| `npx pqcheck preview-diff --preview <URL> --production <URL>` | Compare a Vercel/Netlify preview deployment URL to production. Surfaces new third-party scripts, header regressions, and DBR score drops *inside the PR*, before merge. |
|
|
52
|
+
| `npx pqcheck preview-diff --preview <URL> --production <URL>` | Compare a Vercel/Netlify preview deployment URL to production. Surfaces new third-party scripts, header regressions, and DBR score drops *inside the PR*, before merge. **v0.16.3** — now renders per-signal N vs N+1 status on every run (scripts, headers, cert SPKI, TLS, …) so you can see *every* check fired, not just "did something change." Add `--verbose` for the full side-by-side table. |
|
|
53
53
|
| `npx pqcheck vendors export/check/sync <domain>` | Vendor lockfile (`cipherwake.vendors.json`) + CI gate that exits non-zero when a new third-party origin appears. Like `package-lock.json` for vendor scripts. |
|
|
54
54
|
| `npx pqcheck onboard <domain>` | One command: scan → scaffold the GitHub Action → capture a vendor lockfile → set a baseline → commit + push. Zero copy-paste from docs. |
|
|
55
55
|
| **`npx pqcheck guard --domain <D> -- <cmd>`** 🆕 | **Deploy guard wrapper.** Wraps any deploy command. Runs `deploy-check` first; conditionally runs `<cmd>` based on `ship_decision`. Modes: `--gate-mode balanced` (default) / `advisory` / `strict`. ONE command instead of two — the strongest single artifact for AI-coder workflows because the AI never has to remember to chain check + deploy. |
|
|
@@ -121,6 +121,18 @@ const sevSegment = (!isUnreachable && max_severity && max_severity !== "none")
|
|
|
121
121
|
? ` · ${String(max_severity).toUpperCase()}`
|
|
122
122
|
: "";
|
|
123
123
|
|
|
124
|
+
// v0.16.2 — drift narrative suffix. "stable 14d" / "drifted 2d ago" / "now".
|
|
125
|
+
// Sourced from state.lastChanged (smartCache populates) so the customer
|
|
126
|
+
// sees time-series anchor at a glance, not just a single-point snapshot.
|
|
127
|
+
let stabilitySegment = "";
|
|
128
|
+
if (!isUnreachable && state.lastChanged) {
|
|
129
|
+
const dms = Date.now() - new Date(state.lastChanged).getTime();
|
|
130
|
+
const days = Math.floor(dms / 86400000);
|
|
131
|
+
if (days <= 0) stabilitySegment = " · drifted today";
|
|
132
|
+
else if (days < 7) stabilitySegment = ` · drifted ${days}d ago`;
|
|
133
|
+
else stabilitySegment = ` · stable ${days}d`;
|
|
134
|
+
}
|
|
135
|
+
|
|
124
136
|
process.stdout.write(
|
|
125
137
|
c(cdec, "◆") +
|
|
126
138
|
" " +
|
package/bin/pqcheck.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
})();
|
|
25
25
|
|
|
26
26
|
const API_BASE = process.env.PQCHECK_API_BASE || "https://cipherwake.io";
|
|
27
|
-
const VERSION = "0.16.
|
|
27
|
+
const VERSION = "0.16.3";
|
|
28
28
|
|
|
29
29
|
// API-key support — paid tiers (Starter $29 / Growth $79 / Scale $199) get
|
|
30
30
|
// per-account monthly quotas instead of the per-IP rate limit. Set via:
|
|
@@ -831,11 +831,13 @@ function countVerifiedSignals(report) {
|
|
|
831
831
|
if (report?.sourceMaps?.reachable) cats.push("source maps");
|
|
832
832
|
// 6. TLS posture
|
|
833
833
|
if (report?.publicSurface?.tlsVersion || report?.tlsVersion) cats.push("TLS");
|
|
834
|
-
// 7. Mixed content —
|
|
835
|
-
// publicDeps
|
|
836
|
-
if (report?.publicDeps?.fetched) cats.push("mixed-content");
|
|
834
|
+
// 7. Mixed content — v0.16.2 promoted to dedicated probe; falls back to
|
|
835
|
+
// publicDeps inference when explicit field missing.
|
|
836
|
+
if (report?.mixedContent?.probed || report?.publicDeps?.fetched) cats.push("mixed-content");
|
|
837
837
|
// 8. Subdomain scale (from CT log scan)
|
|
838
838
|
if (typeof report?.publicSurface?.subdomainCount === "number") cats.push("subdomain scale");
|
|
839
|
+
// 9. Protected paths (v0.16.2 — the headline feature)
|
|
840
|
+
if (report?.protectedPaths?.probed) cats.push("protected paths");
|
|
839
841
|
return { count: cats.length, categories: cats };
|
|
840
842
|
}
|
|
841
843
|
|
|
@@ -939,6 +941,131 @@ function formatHighYieldVerbose(report) {
|
|
|
939
941
|
return lines.join("\n");
|
|
940
942
|
}
|
|
941
943
|
|
|
944
|
+
// v0.16.3 — preview-diff per-signal comparison. Render N vs N+1 signal
|
|
945
|
+
// values for both sides side-by-side so the customer SEES that every
|
|
946
|
+
// signal was checked, even when delta_count=0. Without this, preview-diff
|
|
947
|
+
// was silent on non-changing signals and looked like a binary "did
|
|
948
|
+
// anything change" detector. With this, every preview-diff output proves
|
|
949
|
+
// the 9 signals were exercised on both URLs.
|
|
950
|
+
function formatPreviewDiffPerSignal(prev, prod, options = {}) {
|
|
951
|
+
if (!prev || !prod) return null;
|
|
952
|
+
const verbose = options.verbose === true;
|
|
953
|
+
const rows = [];
|
|
954
|
+
const push = (name, l, r) => rows.push({ name, prev: l, prod: r, same: l === r });
|
|
955
|
+
|
|
956
|
+
if (typeof prev?.score === "number" || typeof prod?.score === "number") {
|
|
957
|
+
const dbrL = typeof prev?.score === "number" ? `${prev.score.toFixed(1)}${prev.grade ? " " + prev.grade : ""}` : "—";
|
|
958
|
+
const dbrR = typeof prod?.score === "number" ? `${prod.score.toFixed(1)}${prod.grade ? " " + prod.grade : ""}` : "—";
|
|
959
|
+
push("DBR", dbrL, dbrR);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const prevScripts = prev?.publicDeps?.thirdPartyCount;
|
|
963
|
+
const prodScripts = prod?.publicDeps?.thirdPartyCount;
|
|
964
|
+
if (prevScripts !== undefined || prodScripts !== undefined) {
|
|
965
|
+
push("Scripts", String(prevScripts ?? "—"), String(prodScripts ?? "—"));
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const hdrSummary = (h) => {
|
|
969
|
+
if (!h?.reachable) return "—";
|
|
970
|
+
const flags = [];
|
|
971
|
+
flags.push(h.csp?.present ? "CSP✓" : "CSP✗");
|
|
972
|
+
flags.push(h.hsts?.present ? `HSTS${h.hsts.preload ? "+preload" : ""}✓` : "HSTS✗");
|
|
973
|
+
flags.push(h.xFrameOptions?.present ? "XFO✓" : "XFO✗");
|
|
974
|
+
return flags.join(" ");
|
|
975
|
+
};
|
|
976
|
+
if (prev?.httpHeaders || prod?.httpHeaders) {
|
|
977
|
+
push("Headers", hdrSummary(prev?.httpHeaders), hdrSummary(prod?.httpHeaders));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const ckSummary = (c) => {
|
|
981
|
+
if (!c) return "—";
|
|
982
|
+
if (!c.reachable) return "unreachable";
|
|
983
|
+
const issues = [];
|
|
984
|
+
if (c.anyMissingSecure) issues.push("Secure");
|
|
985
|
+
if (c.anyMissingHttpOnly) issues.push("HttpOnly");
|
|
986
|
+
if (c.anyMissingSameSite) issues.push("SameSite");
|
|
987
|
+
const tag = issues.length ? `miss:${issues.join(",")}` : "all flags";
|
|
988
|
+
return `${c.count ?? 0} (${tag})`;
|
|
989
|
+
};
|
|
990
|
+
if (prev?.cookies || prod?.cookies) push("Cookies", ckSummary(prev?.cookies), ckSummary(prod?.cookies));
|
|
991
|
+
|
|
992
|
+
const smSummary = (s) => {
|
|
993
|
+
if (!s) return "—";
|
|
994
|
+
if (!s.reachable) return "unreachable";
|
|
995
|
+
if (s.exposed) return `${s.exposedCount} EXPOSED`;
|
|
996
|
+
return "none exposed";
|
|
997
|
+
};
|
|
998
|
+
if (prev?.sourceMaps || prod?.sourceMaps) push("Source maps", smSummary(prev?.sourceMaps), smSummary(prod?.sourceMaps));
|
|
999
|
+
|
|
1000
|
+
const mcSummary = (m) => {
|
|
1001
|
+
if (m === null || m === undefined) return "—";
|
|
1002
|
+
if (typeof m === "number") return String(m);
|
|
1003
|
+
if (typeof m === "object") {
|
|
1004
|
+
if (typeof m.insecureCount === "number") return String(m.insecureCount);
|
|
1005
|
+
if (typeof m.count === "number") return String(m.count);
|
|
1006
|
+
}
|
|
1007
|
+
return "—";
|
|
1008
|
+
};
|
|
1009
|
+
if (prev?.mixedContent !== null || prod?.mixedContent !== null) {
|
|
1010
|
+
push("Mixed content", mcSummary(prev?.mixedContent), mcSummary(prod?.mixedContent));
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const ppSummary = (p) => {
|
|
1014
|
+
if (!p?.probed) return "—";
|
|
1015
|
+
const protected_ = p.protectedCount ?? 0;
|
|
1016
|
+
const exposed = p.exposedCount ?? 0;
|
|
1017
|
+
const total = protected_ + exposed;
|
|
1018
|
+
return total === 0 ? "0/0" : `${protected_}/${total} protected`;
|
|
1019
|
+
};
|
|
1020
|
+
if (prev?.protectedPaths || prod?.protectedPaths) {
|
|
1021
|
+
push("Protected paths", ppSummary(prev?.protectedPaths), ppSummary(prod?.protectedPaths));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const certSummary = (c) => {
|
|
1025
|
+
if (!c) return "—";
|
|
1026
|
+
if (c.spkiSha256) return `${String(c.spkiSha256).slice(0, 10)}…`;
|
|
1027
|
+
return c.issuer || "—";
|
|
1028
|
+
};
|
|
1029
|
+
if (prev?.cert || prod?.cert) push("Cert SPKI", certSummary(prev?.cert), certSummary(prod?.cert));
|
|
1030
|
+
|
|
1031
|
+
if (prev?.tlsVersion || prod?.tlsVersion) push("TLS", prev?.tlsVersion || "—", prod?.tlsVersion || "—");
|
|
1032
|
+
|
|
1033
|
+
const prevSub = prev?.publicSurface?.subdomainCount;
|
|
1034
|
+
const prodSub = prod?.publicSurface?.subdomainCount;
|
|
1035
|
+
if (prevSub !== undefined || prodSub !== undefined) {
|
|
1036
|
+
push("Subdomains", String(prevSub ?? "—"), String(prodSub ?? "—"));
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (rows.length === 0) return null;
|
|
1040
|
+
|
|
1041
|
+
if (!verbose) {
|
|
1042
|
+
// Compact 1-line summary: "✓ 9/9 signals match (preview ↔ production)"
|
|
1043
|
+
const sameCount = rows.filter((r) => r.same).length;
|
|
1044
|
+
const allMatch = sameCount === rows.length;
|
|
1045
|
+
const symbol = allMatch ? color("green", "✓ ") : color("yellow", "~ ");
|
|
1046
|
+
return symbol + color("bold", `${sameCount}/${rows.length} signals match`) + color("dim", " (preview ↔ production)");
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Verbose: per-row table
|
|
1050
|
+
const lines = [];
|
|
1051
|
+
lines.push("");
|
|
1052
|
+
lines.push(color("bold", "Per-signal verification (preview ↔ production):"));
|
|
1053
|
+
const nameWidth = Math.max(...rows.map((r) => r.name.length));
|
|
1054
|
+
const prevWidth = Math.max(...rows.map((r) => r.prev.length));
|
|
1055
|
+
for (const r of rows) {
|
|
1056
|
+
const arrow = r.same ? color("dim", "↔") : color("yellow", "→");
|
|
1057
|
+
const valueColor = r.same ? "dim" : "yellow";
|
|
1058
|
+
lines.push(
|
|
1059
|
+
" " +
|
|
1060
|
+
color("bold", r.name.padEnd(nameWidth)) + " " +
|
|
1061
|
+
color(valueColor, r.prev.padEnd(prevWidth)) + " " +
|
|
1062
|
+
arrow + " " +
|
|
1063
|
+
color(valueColor, r.prod)
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
return lines.join("\n");
|
|
1067
|
+
}
|
|
1068
|
+
|
|
942
1069
|
// Helper: pretty-print 1-3 alerts (when ship_decision = review/block) ABOVE
|
|
943
1070
|
// the trust-posture line, so the actionable change is the FIRST thing the
|
|
944
1071
|
// customer sees. Each alert is one line with an emoji + concise summary.
|
|
@@ -2762,19 +2889,39 @@ async function runScanBasedDeployCheck(domain, args) {
|
|
|
2762
2889
|
];
|
|
2763
2890
|
}
|
|
2764
2891
|
|
|
2892
|
+
// v0.16.0 high-yield rendering — same shape as runOneScan but with the
|
|
2893
|
+
// "first-deploy" context line above the brand header (since this path
|
|
2894
|
+
// fires only when there's no baseline to diff against).
|
|
2895
|
+
const verbose = isVerboseMode(args);
|
|
2896
|
+
const highSevFindings = findings.filter((f) => severityRank(f.severity) >= 3);
|
|
2897
|
+
const alertCount = highSevFindings.length;
|
|
2898
|
+
|
|
2765
2899
|
console.log("");
|
|
2766
2900
|
console.log(color("dim", ` ℹ first deploy-check for ${domain} — using current scan state (no baseline yet to diff against)`));
|
|
2767
2901
|
console.log("");
|
|
2768
|
-
console.log(
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2902
|
+
console.log(formatBrandHeader(domain));
|
|
2903
|
+
console.log("");
|
|
2904
|
+
console.log(formatDecisionPulse({ shipDecision, alertCount, unreachable, diffContext: true }));
|
|
2905
|
+
|
|
2906
|
+
if (unreachable) {
|
|
2907
|
+
console.log(formatAiBody({ topIssue, whyMatters, nextActions }));
|
|
2908
|
+
} else {
|
|
2909
|
+
if (alertCount > 0) {
|
|
2910
|
+
const alerts = formatAlertsLine(highSevFindings);
|
|
2911
|
+
if (alerts) console.log(alerts);
|
|
2912
|
+
}
|
|
2913
|
+
const tpl = formatTrustPostureLine(report);
|
|
2914
|
+
if (tpl) console.log(tpl);
|
|
2915
|
+
const vsl = formatVerifiedSignalsLine(report);
|
|
2916
|
+
if (vsl) console.log(vsl);
|
|
2917
|
+
if (verbose) {
|
|
2918
|
+
console.log("");
|
|
2919
|
+
console.log(formatHighYieldVerbose(report));
|
|
2920
|
+
} else {
|
|
2921
|
+
console.log(color("dim", " Run with --verbose to see all verified signals."));
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2778
2925
|
console.log(formatAiFooterBlock({
|
|
2779
2926
|
status: shipDecision,
|
|
2780
2927
|
domain,
|
|
@@ -2910,16 +3057,41 @@ async function runTrustDiffCommand(args) {
|
|
|
2910
3057
|
`If not intentional: revert the deploy or investigate the drift source.`,
|
|
2911
3058
|
];
|
|
2912
3059
|
|
|
2913
|
-
|
|
2914
|
-
|
|
3060
|
+
// v0.16.0 high-yield rendering for trust-diff.
|
|
3061
|
+
const verbose = isVerboseMode(args);
|
|
3062
|
+
const diffNoChange = deltas.length === 0;
|
|
3063
|
+
const fakeReport = {
|
|
2915
3064
|
domain,
|
|
2916
|
-
|
|
2917
|
-
dbr: result.current_score,
|
|
3065
|
+
score: result.current_score,
|
|
2918
3066
|
grade: result.current_grade,
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
}
|
|
2922
|
-
|
|
3067
|
+
_meta: { lastChanged: result.last_changed },
|
|
3068
|
+
sectorRanking: result.sectorRanking,
|
|
3069
|
+
};
|
|
3070
|
+
|
|
3071
|
+
console.log("");
|
|
3072
|
+
console.log(formatBrandHeader(domain));
|
|
3073
|
+
console.log("");
|
|
3074
|
+
console.log(formatDecisionPulse({ shipDecision, alertCount: deltas.length, unreachable: false, diffContext: true }));
|
|
3075
|
+
|
|
3076
|
+
if (deltas.length > 0) {
|
|
3077
|
+
// Surface top deltas as alerts
|
|
3078
|
+
const alertFindings = deltas.slice(0, 3).map((d) => ({
|
|
3079
|
+
severity: d.severity || "medium",
|
|
3080
|
+
title: d.title || d.what_changed || d.type || "drift",
|
|
3081
|
+
id: d.id,
|
|
3082
|
+
}));
|
|
3083
|
+
const alerts = formatAlertsLine(alertFindings);
|
|
3084
|
+
if (alerts) console.log(alerts);
|
|
3085
|
+
if (deltas.length > 3) console.log(color("dim", ` +${deltas.length - 3} more (run with --verbose)`));
|
|
3086
|
+
}
|
|
3087
|
+
const tpl = formatTrustPostureLine(fakeReport);
|
|
3088
|
+
if (tpl) console.log(tpl);
|
|
3089
|
+
const vsl = formatVerifiedSignalsLine(fakeReport, { diffNoChange });
|
|
3090
|
+
if (vsl) console.log(vsl);
|
|
3091
|
+
if (!verbose) {
|
|
3092
|
+
console.log(color("dim", " Run with --verbose to see all verified signals."));
|
|
3093
|
+
}
|
|
3094
|
+
|
|
2923
3095
|
console.log(formatAiFooterBlock({
|
|
2924
3096
|
status: shipDecision,
|
|
2925
3097
|
domain,
|
|
@@ -3123,16 +3295,82 @@ async function runPreviewDiffCommand(args) {
|
|
|
3123
3295
|
`Each "- CSP weakened" or "~ HSTS weakened" reduces production safety.`,
|
|
3124
3296
|
];
|
|
3125
3297
|
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3298
|
+
// v0.16.3 high-yield rendering for preview-diff. Uses the per-side
|
|
3299
|
+
// `signals` snapshot returned by /api/preview-diff so the customer
|
|
3300
|
+
// sees N vs N+1 on every signal (not just the binary "no drift").
|
|
3301
|
+
// Falls back to the v0.16.2 shape when the server hasn't deployed
|
|
3302
|
+
// the signals snapshot yet (older API response).
|
|
3303
|
+
const verbose = isVerboseMode(args);
|
|
3304
|
+
const diffNoChange = !hasUnexpectedDiff;
|
|
3305
|
+
const prevSig = result?.preview?.signals || null;
|
|
3306
|
+
const prodSig = result?.production?.signals || null;
|
|
3307
|
+
// Brand header uses the production hostname (e.g. "quantapact.com")
|
|
3308
|
+
// not the full Vercel preview URL, so the customer doesn't see
|
|
3309
|
+
// "◆ Cipherwake — https://quantapact-k3y...vercel.app".
|
|
3310
|
+
const headerDomain = result?.production?.hostname || result?.production?.domain || productionUrl;
|
|
3311
|
+
// Synthesize a report-shaped object from the production-side signals
|
|
3312
|
+
// snapshot so the existing trust-posture + verified-signals helpers
|
|
3313
|
+
// (which read .score, .grade, .sectorRanking, .cookies, etc.) work
|
|
3314
|
+
// without changes.
|
|
3315
|
+
const reportLike = prodSig ? {
|
|
3316
|
+
domain: headerDomain,
|
|
3317
|
+
score: prodSig.score,
|
|
3318
|
+
grade: prodSig.grade,
|
|
3319
|
+
_meta: { lastChanged: prodSig.lastChanged },
|
|
3320
|
+
sectorRanking: prodSig.sectorRanking,
|
|
3321
|
+
publicDeps: prodSig.publicDeps ? { fetched: true, thirdParties: new Array(prodSig.publicDeps.thirdPartyCount || 0) } : null,
|
|
3322
|
+
httpHeaders: prodSig.httpHeaders,
|
|
3323
|
+
cookies: prodSig.cookies,
|
|
3324
|
+
sourceMaps: prodSig.sourceMaps,
|
|
3325
|
+
mixedContent: prodSig.mixedContent,
|
|
3326
|
+
cert: prodSig.cert,
|
|
3327
|
+
tlsVersion: prodSig.tlsVersion,
|
|
3328
|
+
publicSurface: prodSig.publicSurface,
|
|
3329
|
+
protectedPaths: prodSig.protectedPaths,
|
|
3330
|
+
} : {
|
|
3331
|
+
// Fallback: legacy server, only score/grade available.
|
|
3332
|
+
domain: headerDomain,
|
|
3333
|
+
score: prevScore,
|
|
3131
3334
|
grade: result?.preview?.grade,
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
}
|
|
3135
|
-
|
|
3335
|
+
_meta: { lastChanged: result?.production?.lastChanged },
|
|
3336
|
+
sectorRanking: result?.production?.sectorRanking,
|
|
3337
|
+
};
|
|
3338
|
+
// Render top deltas as alert lines (1 per finding, max 3)
|
|
3339
|
+
const deltaLines = summaryLines.filter((l) => !/no meaningful/i.test(l));
|
|
3340
|
+
|
|
3341
|
+
console.log("");
|
|
3342
|
+
console.log(formatBrandHeader(headerDomain) + color("dim", " (preview ↔ production)"));
|
|
3343
|
+
console.log("");
|
|
3344
|
+
console.log(formatDecisionPulse({ shipDecision, alertCount: deltaLines.length, unreachable: false, diffContext: true }));
|
|
3345
|
+
|
|
3346
|
+
if (deltaLines.length > 0) {
|
|
3347
|
+
for (const dl of deltaLines.slice(0, 3)) {
|
|
3348
|
+
const sym = dl.startsWith("⛔") || dl.startsWith("-") ? "red" : dl.startsWith("⚠") || dl.startsWith("~") ? "yellow" : "dim";
|
|
3349
|
+
console.log(color(sym, dl));
|
|
3350
|
+
}
|
|
3351
|
+
if (deltaLines.length > 3) console.log(color("dim", ` +${deltaLines.length - 3} more (run with --verbose)`));
|
|
3352
|
+
}
|
|
3353
|
+
const tpl = formatTrustPostureLine(reportLike);
|
|
3354
|
+
if (tpl) console.log(tpl);
|
|
3355
|
+
const vsl = formatVerifiedSignalsLine(reportLike, { diffNoChange });
|
|
3356
|
+
if (vsl) console.log(vsl);
|
|
3357
|
+
// v0.16.3 — per-signal N vs N+1 comparison. Renders ALWAYS (default and
|
|
3358
|
+
// --verbose), in different shapes:
|
|
3359
|
+
// - default: 1-line "9/9 signals match (preview ↔ production)" so the
|
|
3360
|
+
// customer sees the checks fired without scrolling
|
|
3361
|
+
// - --verbose: per-row table with both sides spelled out
|
|
3362
|
+
const psl = formatPreviewDiffPerSignal(prevSig, prodSig, { verbose });
|
|
3363
|
+
if (psl) console.log(psl);
|
|
3364
|
+
if (!verbose) {
|
|
3365
|
+
console.log(color("dim", " Run with --verbose to see per-signal breakdown."));
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
// (Old banner + body removed in v0.16.2; new high-yield layout above
|
|
3369
|
+
// replaces them. AI footer still lands below for downstream agents.)
|
|
3370
|
+
if (verbose) {
|
|
3371
|
+
console.log("");
|
|
3372
|
+
console.log(formatAiBody({ topIssue, whyMatters, nextActions }));
|
|
3373
|
+
}
|
|
3136
3374
|
console.log(formatAiFooterBlock({
|
|
3137
3375
|
status: shipDecision,
|
|
3138
3376
|
domain: result?.production?.domain || productionUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pqcheck",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
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",
|