pqcheck 0.16.2 → 0.16.4
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 +69 -1
- package/bin/pqcheck.js +186 -10
- 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. |
|
|
@@ -73,7 +73,11 @@ try {
|
|
|
73
73
|
process.exit(0);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
const {
|
|
76
|
+
const {
|
|
77
|
+
domain, score, grade, ship_decision, written_at, max_severity, unreachable,
|
|
78
|
+
// v0.16.4 — preview-diff-specific fields for the 4-line render
|
|
79
|
+
kind, delta_count, diff_no_change, sector_ranking, verified_signal_categories, last_changed,
|
|
80
|
+
} = state;
|
|
77
81
|
const age = ageHours(written_at);
|
|
78
82
|
|
|
79
83
|
if (age > STALE_THRESHOLD_HOURS) {
|
|
@@ -87,6 +91,70 @@ if (age > STALE_THRESHOLD_HOURS) {
|
|
|
87
91
|
process.exit(0);
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
// v0.16.4 — preview-diff 4-line state.
|
|
95
|
+
// User direction: when the last scan was `preview-diff` (two URLs compared),
|
|
96
|
+
// render the high-yield block in the statusline. Other scan kinds keep the
|
|
97
|
+
// 1-line compact format below — the bigger block would be too noisy when
|
|
98
|
+
// it's just a routine domain scan with no diff context.
|
|
99
|
+
if (kind === "preview-diff" && !unreachable) {
|
|
100
|
+
// Line 1: brand header (hostname only).
|
|
101
|
+
const head = c(C.bold, "◆ Cipherwake") + c(C.dim, " — ") + c(C.bold, domain || "—");
|
|
102
|
+
// Line 3: decision pulse, diff-flavored copy.
|
|
103
|
+
let pulse;
|
|
104
|
+
if (ship_decision === "pass") {
|
|
105
|
+
pulse = c(C.green, "✓ PASS") + c(C.dim, " · no risky deploy-surface drift detected");
|
|
106
|
+
} else if (ship_decision === "review") {
|
|
107
|
+
pulse = c(C.yellow, "⚠ REVIEW") + c(C.dim, ` · ${delta_count || 0} public-surface change${delta_count === 1 ? "" : "s"}`);
|
|
108
|
+
} else if (ship_decision === "block") {
|
|
109
|
+
pulse = c(C.red, "⛔ BLOCK") + c(C.dim, ` · ${delta_count || 0} critical change${delta_count === 1 ? "" : "s"}`);
|
|
110
|
+
} else {
|
|
111
|
+
pulse = c(C.dim, "· no decision");
|
|
112
|
+
}
|
|
113
|
+
// Line 4: trust posture (DBR + percentile copy + stability).
|
|
114
|
+
function formatPercentile(percentile, sector) {
|
|
115
|
+
if (typeof percentile !== "number") return null;
|
|
116
|
+
const s = sector || "industry";
|
|
117
|
+
if (percentile <= 10) return `top 10% in ${s}`;
|
|
118
|
+
if (percentile <= 25) return `top 25% in ${s}`;
|
|
119
|
+
if (percentile <= 50) return `above median in ${s}`;
|
|
120
|
+
if (percentile <= 75) return `below median in ${s}`;
|
|
121
|
+
if (percentile <= 90) return `bottom 25% in ${s}`;
|
|
122
|
+
return `bottom 10% in ${s}`;
|
|
123
|
+
}
|
|
124
|
+
function formatStability(iso) {
|
|
125
|
+
if (!iso) return null;
|
|
126
|
+
const days = Math.floor((Date.now() - new Date(iso).getTime()) / 86400000);
|
|
127
|
+
if (days <= 0) return "drifted today";
|
|
128
|
+
if (days < 7) return `drifted ${days}d ago`;
|
|
129
|
+
return `stable ${days}d`;
|
|
130
|
+
}
|
|
131
|
+
const trustParts = [];
|
|
132
|
+
if (typeof score === "number") {
|
|
133
|
+
trustParts.push(`DBR ${score.toFixed(1)}${grade ? " " + grade : ""}`);
|
|
134
|
+
}
|
|
135
|
+
const pct = formatPercentile(sector_ranking?.percentile, sector_ranking?.sectorLabel || sector_ranking?.sectorName || sector_ranking?.sector);
|
|
136
|
+
if (pct) trustParts.push(pct);
|
|
137
|
+
const stab = formatStability(last_changed);
|
|
138
|
+
if (stab) trustParts.push(stab);
|
|
139
|
+
const trustLine = trustParts.length > 0
|
|
140
|
+
? c(C.green, "✓ ") + c(C.bold, "Trust posture: ") + trustParts.join(c(C.dim, " · "))
|
|
141
|
+
: null;
|
|
142
|
+
// Line 5: verified signals (or finding line when there IS drift).
|
|
143
|
+
let verifiedLine = null;
|
|
144
|
+
if (Array.isArray(verified_signal_categories) && verified_signal_categories.length > 0) {
|
|
145
|
+
const head = diff_no_change === true
|
|
146
|
+
? "Security-relevant surface unchanged"
|
|
147
|
+
: `Verified ${verified_signal_categories.length} signal${verified_signal_categories.length === 1 ? "" : "s"}`;
|
|
148
|
+
verifiedLine = c(C.green, "✓ ") + c(C.bold, head) + c(C.dim, " · " + verified_signal_categories.join(", "));
|
|
149
|
+
}
|
|
150
|
+
// Compose
|
|
151
|
+
const lines = [head, "", pulse];
|
|
152
|
+
if (trustLine) lines.push(trustLine);
|
|
153
|
+
if (verifiedLine) lines.push(verifiedLine);
|
|
154
|
+
process.stdout.write(lines.join("\n"));
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
90
158
|
// Brand-anchored layout — "Cipherwake" is always the first word after the
|
|
91
159
|
// diamond, so the customer (and their AI agent) can identify the status
|
|
92
160
|
// line's source at a glance. Trailing segments depend on what we have:
|
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.4";
|
|
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:
|
|
@@ -941,6 +941,131 @@ function formatHighYieldVerbose(report) {
|
|
|
941
941
|
return lines.join("\n");
|
|
942
942
|
}
|
|
943
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
|
+
|
|
944
1069
|
// Helper: pretty-print 1-3 alerts (when ship_decision = review/block) ABOVE
|
|
945
1070
|
// the trust-posture line, so the actionable change is the FIRST thing the
|
|
946
1071
|
// customer sees. Each alert is one line with an emoji + concise summary.
|
|
@@ -3170,11 +3295,40 @@ async function runPreviewDiffCommand(args) {
|
|
|
3170
3295
|
`Each "- CSP weakened" or "~ HSTS weakened" reduces production safety.`,
|
|
3171
3296
|
];
|
|
3172
3297
|
|
|
3173
|
-
// v0.16.
|
|
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).
|
|
3174
3303
|
const verbose = isVerboseMode(args);
|
|
3175
3304
|
const diffNoChange = !hasUnexpectedDiff;
|
|
3176
|
-
const
|
|
3177
|
-
const
|
|
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.
|
|
3178
3332
|
domain: headerDomain,
|
|
3179
3333
|
score: prevScore,
|
|
3180
3334
|
grade: result?.preview?.grade,
|
|
@@ -3185,7 +3339,7 @@ async function runPreviewDiffCommand(args) {
|
|
|
3185
3339
|
const deltaLines = summaryLines.filter((l) => !/no meaningful/i.test(l));
|
|
3186
3340
|
|
|
3187
3341
|
console.log("");
|
|
3188
|
-
console.log(formatBrandHeader(headerDomain));
|
|
3342
|
+
console.log(formatBrandHeader(headerDomain) + color("dim", " (preview ↔ production)"));
|
|
3189
3343
|
console.log("");
|
|
3190
3344
|
console.log(formatDecisionPulse({ shipDecision, alertCount: deltaLines.length, unreachable: false, diffContext: true }));
|
|
3191
3345
|
|
|
@@ -3196,12 +3350,19 @@ async function runPreviewDiffCommand(args) {
|
|
|
3196
3350
|
}
|
|
3197
3351
|
if (deltaLines.length > 3) console.log(color("dim", ` +${deltaLines.length - 3} more (run with --verbose)`));
|
|
3198
3352
|
}
|
|
3199
|
-
const tpl = formatTrustPostureLine(
|
|
3353
|
+
const tpl = formatTrustPostureLine(reportLike);
|
|
3200
3354
|
if (tpl) console.log(tpl);
|
|
3201
|
-
const vsl = formatVerifiedSignalsLine(
|
|
3355
|
+
const vsl = formatVerifiedSignalsLine(reportLike, { diffNoChange });
|
|
3202
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);
|
|
3203
3364
|
if (!verbose) {
|
|
3204
|
-
console.log(color("dim", " Run with --verbose to see
|
|
3365
|
+
console.log(color("dim", " Run with --verbose to see per-signal breakdown."));
|
|
3205
3366
|
}
|
|
3206
3367
|
|
|
3207
3368
|
// (Old banner + body removed in v0.16.2; new high-yield layout above
|
|
@@ -3229,15 +3390,30 @@ async function runPreviewDiffCommand(args) {
|
|
|
3229
3390
|
}));
|
|
3230
3391
|
console.log("");
|
|
3231
3392
|
|
|
3393
|
+
// v0.16.4 — write the data the statusline needs to render its
|
|
3394
|
+
// 4-line preview-diff state (brand header + decision pulse + trust
|
|
3395
|
+
// posture + verified signals). For non-preview-diff kinds we keep
|
|
3396
|
+
// the 1-line statusline; only preview-diff gets the bigger format.
|
|
3397
|
+
const verifiedSignalCats = (() => {
|
|
3398
|
+
try { return countVerifiedSignals(reportLike).categories; } catch { return null; }
|
|
3399
|
+
})();
|
|
3232
3400
|
await writeLastScanFile({
|
|
3233
|
-
domain: result?.production?.domain || productionUrl,
|
|
3401
|
+
domain: result?.production?.hostname || result?.production?.domain || productionUrl,
|
|
3234
3402
|
kind: "preview-diff",
|
|
3235
3403
|
preview_url: previewUrl,
|
|
3236
3404
|
production_url: productionUrl,
|
|
3237
|
-
score
|
|
3405
|
+
// v0.16.4 — score/grade come from the PRODUCTION side so the trust
|
|
3406
|
+
// posture line reflects the live customer site, not the in-flight
|
|
3407
|
+
// preview. (Was prevScore before — wrong reference.)
|
|
3408
|
+
score: typeof prodSig?.score === "number" ? prodSig.score : (typeof prevScore === "number" ? prevScore : null),
|
|
3409
|
+
grade: prodSig?.grade || null,
|
|
3238
3410
|
max_severity: maxSev,
|
|
3239
3411
|
ship_decision: shipDecision,
|
|
3240
3412
|
delta_count: summaryLines.filter((l) => !/no meaningful/i.test(l)).length,
|
|
3413
|
+
diff_no_change: diffNoChange,
|
|
3414
|
+
sector_ranking: prodSig?.sectorRanking || null,
|
|
3415
|
+
verified_signal_categories: verifiedSignalCats,
|
|
3416
|
+
last_changed: prodSig?.lastChanged || null,
|
|
3241
3417
|
});
|
|
3242
3418
|
|
|
3243
3419
|
process.exit(shipDecisionExitCode(shipDecision));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pqcheck",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.4",
|
|
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",
|