pqcheck 0.16.21 → 0.16.23
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/pqcheck.js +222 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/pqcheck)
|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
|
|
11
|
-
> **Latest: v0.16.
|
|
11
|
+
> **Latest: v0.16.23** — Closes the silent-no-op flag class. `--strict` now aliases `--strict-posture` in scan / deploy-check / trust-diff (audit caught the typo silently degrading to drift-only). And unknown flags now reject loudly with a closest-match suggestion + non-zero exit — `--stict-posture` (typo) → `error: did you mean --strict-posture?` instead of silently no-op'ing. Same principle applied to ourselves that Cipherwake exists to enforce for customers: a security tool can never silently proceed with weaker behaviour on an unrecognized signal. [Full changelog →](./CHANGELOG.md)
|
|
12
12
|
|
|
13
13
|
## Two ways to use it
|
|
14
14
|
|
package/bin/pqcheck.js
CHANGED
|
@@ -248,6 +248,14 @@ async function main() {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
// R86.7 — reject unknown flags loudly. Catches typo'd safety-critical
|
|
252
|
+
// flags like `--strict` (was intended --strict-posture) that previously
|
|
253
|
+
// silently no-op'd, leaving customers with a false sense of security.
|
|
254
|
+
const unknown = assertKnownFlags(args, "pqcheck <domain>");
|
|
255
|
+
if (unknown) {
|
|
256
|
+
process.exit(3);
|
|
257
|
+
}
|
|
258
|
+
|
|
251
259
|
const positional = args.filter((a) => !a.startsWith("-") && !isFlagValue(args, a));
|
|
252
260
|
const domains = [...positional, ...fileDomains]
|
|
253
261
|
.map((a) => normalizeDomain(a))
|
|
@@ -290,17 +298,29 @@ async function main() {
|
|
|
290
298
|
const fresh = args.includes("--fresh") || args.includes("--force");
|
|
291
299
|
const aiMode = parseAiMode(args);
|
|
292
300
|
const verbose = isVerboseMode(args); // v0.16.0 — opt-in detailed panel
|
|
301
|
+
// R86.4 (2026-06-03) — strict-posture opt-in. Default ship_decision is
|
|
302
|
+
// drift-only (per-deploy regression gate); --strict-posture folds the
|
|
303
|
+
// absolute posture grade into ship_decision. Recommended ONLY after a
|
|
304
|
+
// site reaches A/B posture — opting in earlier produces cry-wolf gating
|
|
305
|
+
// on every deploy since most AI-coded sites grade D/F out of the box.
|
|
306
|
+
// Accept both `--strict-posture` (explicit) and `--strict` (short alias).
|
|
307
|
+
// Audit caught a footgun where customers typed `--strict` expecting the
|
|
308
|
+
// posture gate to fire and got silent drift-only mode instead. The
|
|
309
|
+
// `onboard` subcommand uses `--strict` for a different purpose (gate exit
|
|
310
|
+
// code on step failure) but that's a separate command — the alias is
|
|
311
|
+
// scoped to scan / deploy-check / trust-diff only.
|
|
312
|
+
const strictPosture = args.includes("--strict-posture") || args.includes("--strict");
|
|
293
313
|
|
|
294
314
|
// One-shot scan(s)
|
|
295
315
|
let worstExit = 0;
|
|
296
316
|
for (const domain of domains) {
|
|
297
|
-
const exit = await runOneScan({ domain, format, quiet, threshold, webhookUrl, multi: domains.length > 1, fresh, aiMode, verbose });
|
|
317
|
+
const exit = await runOneScan({ domain, format, quiet, threshold, webhookUrl, multi: domains.length > 1, fresh, aiMode, verbose, strictPosture });
|
|
298
318
|
if (exit > worstExit) worstExit = exit;
|
|
299
319
|
}
|
|
300
320
|
process.exit(worstExit);
|
|
301
321
|
}
|
|
302
322
|
|
|
303
|
-
async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi, fresh, aiMode, verbose }) {
|
|
323
|
+
async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi, fresh, aiMode, verbose, strictPosture }) {
|
|
304
324
|
if (!quiet && format === "text") process.stderr.write(color("dim", `Scanning ${domain}${fresh ? " (forcing fresh)" : ""} ...`));
|
|
305
325
|
let report;
|
|
306
326
|
try {
|
|
@@ -511,7 +531,13 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
511
531
|
posture_findings_count: (posture.findings || []).length,
|
|
512
532
|
posture_fixes_count: (posture.fixes || []).length,
|
|
513
533
|
} : {};
|
|
514
|
-
|
|
534
|
+
// R86.2 (2026-06-03) — see combineShipDecision: posture is advisory by
|
|
535
|
+
// default, gated when --strict-posture is passed.
|
|
536
|
+
const effectiveShip = combineShipDecision(shipDecision, posture?.decision, { strict: strictPosture });
|
|
537
|
+
// R86.5 — surface posture advisory line so D/F posture is never silently
|
|
538
|
+
// blessed under the drift-only default.
|
|
539
|
+
const postureAdvisory = formatPostureAdvisoryLine(posture, strictPosture);
|
|
540
|
+
if (postureAdvisory) console.log(postureAdvisory);
|
|
515
541
|
console.log(formatAiFooterBlock({
|
|
516
542
|
status: effectiveShip,
|
|
517
543
|
domain,
|
|
@@ -522,14 +548,17 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
522
548
|
ship_decision: effectiveShip,
|
|
523
549
|
ship_decision_drift: shipDecision,
|
|
524
550
|
ship_decision_posture: posture?.decision,
|
|
551
|
+
ship_decision_mode: strictPosture ? "strict_posture" : "drift_only",
|
|
525
552
|
unreachable: unreachable ? "true" : "false",
|
|
526
553
|
top_issue: topFinding?.id || topFinding?.title || "none",
|
|
527
554
|
findings_high: findings.filter((f) => severityRank(f.severity) === 3).length,
|
|
528
555
|
findings_critical: findings.filter((f) => severityRank(f.severity) === 4).length,
|
|
529
556
|
scanned_at: new Date().toISOString(),
|
|
530
557
|
advisory_only: "true",
|
|
531
|
-
scope: "trust_surface_drift_plus_absolute_posture",
|
|
532
|
-
scope_note:
|
|
558
|
+
scope: strictPosture ? "trust_surface_drift_plus_absolute_posture" : "trust_surface_drift",
|
|
559
|
+
scope_note: strictPosture
|
|
560
|
+
? "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."
|
|
561
|
+
: "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
562
|
...postureFields,
|
|
534
563
|
}));
|
|
535
564
|
if (posture && Array.isArray(posture.fixes) && posture.fixes.length > 0) {
|
|
@@ -573,6 +602,14 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
573
602
|
} else {
|
|
574
603
|
if (multi) console.log(color("dim", `\n──── ${domain} ────`));
|
|
575
604
|
printReport(report);
|
|
605
|
+
// R86.5 — posture advisory line so D/F posture is never silently blessed
|
|
606
|
+
// in human output either. Posture is advisory, not gating.
|
|
607
|
+
const postureAdvisory = formatPostureAdvisoryLine(report.posture, strictPosture);
|
|
608
|
+
if (postureAdvisory) {
|
|
609
|
+
console.log("");
|
|
610
|
+
console.log(postureAdvisory);
|
|
611
|
+
console.log(color("dim", " See /methodology/posture-grading for the rubric + fix snippets."));
|
|
612
|
+
}
|
|
576
613
|
}
|
|
577
614
|
|
|
578
615
|
if (threshold !== null && typeof report.score === "number" && report.score >= threshold) {
|
|
@@ -724,6 +761,100 @@ function parseAiMode(args) {
|
|
|
724
761
|
return args.includes("--ai") || args.includes("--agent");
|
|
725
762
|
}
|
|
726
763
|
|
|
764
|
+
// R86.7 (2026-06-04) — close the "silent no-op on unrecognized flag" class
|
|
765
|
+
// of footguns. Audit caught --strict (intended --strict-posture) silently
|
|
766
|
+
// degrading to drift-only mode without warning — the user believed they had
|
|
767
|
+
// the hard posture gate on. Same family as false-green pins: looks like it's
|
|
768
|
+
// doing something, silently isn't. The fix: validate every --foo token in
|
|
769
|
+
// args against a known-flags whitelist and reject loudly on unknown flags
|
|
770
|
+
// with a closest-match suggestion. For a security gate, "unknown flag →
|
|
771
|
+
// silently proceed with weaker behaviour" must never happen.
|
|
772
|
+
//
|
|
773
|
+
// Scope: universal whitelist applied at scan / deploy-check / trust-diff /
|
|
774
|
+
// preview-diff entry points. False acceptance of cross-command flags (e.g.
|
|
775
|
+
// passing --preview to deploy-check) is the SAME failure mode as today
|
|
776
|
+
// (silent ignore) — typo detection is the win. Per-command whitelists
|
|
777
|
+
// would be more correct but the universal list covers the audit footgun.
|
|
778
|
+
const KNOWN_FLAGS = new Set([
|
|
779
|
+
// Global / output mode
|
|
780
|
+
"--ai", "--agent", "--verbose", "--quiet", "--help", "--version", "--debug-network",
|
|
781
|
+
// Multi-domain
|
|
782
|
+
"--file", "--watch",
|
|
783
|
+
// Scan behaviour
|
|
784
|
+
"--fresh", "--force", "--threshold", "--webhook", "--json", "--csv", "--markdown",
|
|
785
|
+
"--sarif", "--gh-action", "--multi", "--lock", "--explain", "--plan", "--stdout",
|
|
786
|
+
// Posture gating (the audit catch)
|
|
787
|
+
"--strict", "--strict-posture",
|
|
788
|
+
// Format / output format
|
|
789
|
+
"--format",
|
|
790
|
+
// Trust-diff / deploy-check / preview-diff
|
|
791
|
+
"--baseline", "--fail-on", "--fail-on-new", "--guards", "--compare-transport",
|
|
792
|
+
"--write-baseline", "--preview", "--production",
|
|
793
|
+
// Setup / onboard / install consent
|
|
794
|
+
"--auto", "--manual", "--yes", "--no-open", "--domain", "--invoked-by",
|
|
795
|
+
"--consent-phrase", "--scope",
|
|
796
|
+
"--skip-checklist", "--skip-hook", "--skip-protocol", "--skip-scan",
|
|
797
|
+
"--skip-statusline", "--skip-vendors", "--skip-vscode", "--skip-workflow",
|
|
798
|
+
]);
|
|
799
|
+
|
|
800
|
+
function levenshtein(a, b) {
|
|
801
|
+
if (a === b) return 0;
|
|
802
|
+
const m = a.length, n = b.length;
|
|
803
|
+
if (m === 0) return n;
|
|
804
|
+
if (n === 0) return m;
|
|
805
|
+
const row = new Array(m + 1);
|
|
806
|
+
for (let i = 0; i <= m; i++) row[i] = i;
|
|
807
|
+
for (let j = 1; j <= n; j++) {
|
|
808
|
+
let prev = row[0];
|
|
809
|
+
row[0] = j;
|
|
810
|
+
for (let i = 1; i <= m; i++) {
|
|
811
|
+
const tmp = row[i];
|
|
812
|
+
row[i] = a[i - 1] === b[j - 1]
|
|
813
|
+
? prev
|
|
814
|
+
: 1 + Math.min(prev, row[i], row[i - 1]);
|
|
815
|
+
prev = tmp;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return row[m];
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function suggestFlag(unknown) {
|
|
822
|
+
let best = null, bestDist = Infinity;
|
|
823
|
+
for (const known of KNOWN_FLAGS) {
|
|
824
|
+
const d = levenshtein(unknown, known);
|
|
825
|
+
if (d < bestDist) { bestDist = d; best = known; }
|
|
826
|
+
}
|
|
827
|
+
// Only suggest if reasonably close (≤ 3 edits or ≤ half the unknown's length)
|
|
828
|
+
const threshold = Math.max(3, Math.floor(unknown.length / 2));
|
|
829
|
+
return bestDist <= threshold ? best : null;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function assertKnownFlags(args, cmdName) {
|
|
833
|
+
const unknown = [];
|
|
834
|
+
for (const tok of args) {
|
|
835
|
+
if (typeof tok !== "string" || !tok.startsWith("--") || tok === "--") continue;
|
|
836
|
+
// Strip `--foo=bar` → `--foo` so equals-value forms validate against the bare flag
|
|
837
|
+
const flag = tok.includes("=") ? tok.slice(0, tok.indexOf("=")) : tok;
|
|
838
|
+
if (!KNOWN_FLAGS.has(flag)) unknown.push(flag);
|
|
839
|
+
}
|
|
840
|
+
if (unknown.length === 0) return null;
|
|
841
|
+
// Emit error to stderr with closest-match suggestion, return non-null
|
|
842
|
+
// sentinel so the caller can exit non-zero. We do NOT process.exit here
|
|
843
|
+
// because the AI-mode caller needs to emit a structured guard block
|
|
844
|
+
// before exiting (so AI agents parsing the block don't see a missing
|
|
845
|
+
// CIPHERWAKE_AI_GUARD_RESULT and fall through to a different error path).
|
|
846
|
+
for (const u of unknown) {
|
|
847
|
+
const sug = suggestFlag(u);
|
|
848
|
+
process.stderr.write(color("red", `error: unknown flag ${u} for ${cmdName}\n`));
|
|
849
|
+
if (sug) {
|
|
850
|
+
process.stderr.write(color("yellow", ` did you mean ${sug}?\n`));
|
|
851
|
+
} else {
|
|
852
|
+
process.stderr.write(color("dim", ` run \`npx pqcheck --help\` for the flag list.\n`));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return unknown;
|
|
856
|
+
}
|
|
857
|
+
|
|
727
858
|
function severityRank(s) {
|
|
728
859
|
const map = { critical: 4, high: 3, medium: 2, low: 1, info: 0, none: -1 };
|
|
729
860
|
return map[String(s || "none").toLowerCase()] ?? 0;
|
|
@@ -738,45 +869,49 @@ function highestSeverity(findings) {
|
|
|
738
869
|
return best;
|
|
739
870
|
}
|
|
740
871
|
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
//
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
872
|
+
// R86.5 (2026-06-03) — posture advisory line. Posture is a STANDING property,
|
|
873
|
+
// not a per-deploy regression signal, so gating every deploy on it is cry-wolf
|
|
874
|
+
// by construction. The drift-only ship_decision is the right default gate
|
|
875
|
+
// (fires on regression). This helper surfaces posture grade + a one-line fix
|
|
876
|
+
// nudge alongside the gate result — customer sees the grade, sees the fix
|
|
877
|
+
// path, is NOT blocked on every deploy. Only customers who have reached A/B
|
|
878
|
+
// posture opt into --strict-posture to lock that in.
|
|
879
|
+
function formatPostureAdvisoryLine(posture, strictPosture) {
|
|
880
|
+
if (!posture) return "";
|
|
881
|
+
if (strictPosture) return ""; // strict mode already gates; redundant
|
|
882
|
+
const decision = posture.decision;
|
|
883
|
+
if (decision !== "block" && decision !== "review") return ""; // A+/A — no advisory
|
|
884
|
+
const grade = posture.grade || "?";
|
|
885
|
+
const score = typeof posture.score === "number" ? posture.score : "?";
|
|
886
|
+
const fixCount = Array.isArray(posture.fixes) ? posture.fixes.length : 0;
|
|
887
|
+
const colorName = decision === "block" ? "red" : "yellow";
|
|
888
|
+
const icon = decision === "block" ? "●" : "○";
|
|
889
|
+
const nudge = fixCount > 0
|
|
890
|
+
? `${fixCount} ready-to-paste fix${fixCount === 1 ? "" : "es"} in CIPHERWAKE_POSTURE_FIXES`
|
|
891
|
+
: "see /methodology/posture-grading";
|
|
892
|
+
return color(colorName, ` ${icon} Posture: ${grade} (score ${score}) — advisory, not gating. ${nudge}.`);
|
|
758
893
|
}
|
|
759
894
|
|
|
760
|
-
// R86.2 (2026-06-03) —
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
895
|
+
// R86.2 / R86.4 (2026-06-03) — combine drift + posture into ship_decision.
|
|
896
|
+
// Default (strict=false): drift only. Posture grade is surfaced via separate
|
|
897
|
+
// fields and the advisory line but does NOT promote drift's decision —
|
|
898
|
+
// because most AI-coded sites grade D/F out of the box, making posture-gating
|
|
899
|
+
// the default would cry-wolf on every deploy of every header-less site,
|
|
900
|
+
// training people to ignore the gate. --strict-posture opts in for teams
|
|
901
|
+
// who've already fixed posture and want to prevent backsliding.
|
|
767
902
|
const SHIP_DECISION_RANK = { pass: 0, review: 1, block: 2 };
|
|
768
|
-
function combineShipDecision(driftDecision, postureDecision) {
|
|
769
|
-
if (!postureDecision) return driftDecision;
|
|
903
|
+
function combineShipDecision(driftDecision, postureDecision, { strict = false } = {}) {
|
|
904
|
+
if (!postureDecision || !strict) return driftDecision;
|
|
770
905
|
const a = SHIP_DECISION_RANK[driftDecision] ?? 0;
|
|
771
906
|
const b = SHIP_DECISION_RANK[postureDecision] ?? 0;
|
|
772
907
|
return a >= b ? driftDecision : postureDecision;
|
|
773
908
|
}
|
|
774
909
|
|
|
775
910
|
// 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;
|
|
911
|
+
// block after the AI guard block. The guard block stays compact key=value;
|
|
777
912
|
// fixes are multi-line code so they get their own block with explicit
|
|
778
|
-
// start/end markers + per-fix delimiters. Agents
|
|
779
|
-
//
|
|
913
|
+
// start/end markers + per-fix delimiters. Agents parse + apply without
|
|
914
|
+
// round-tripping to JSON.
|
|
780
915
|
function formatPostureFixesBlock(fixes) {
|
|
781
916
|
if (!Array.isArray(fixes) || fixes.length === 0) return "";
|
|
782
917
|
const lines = ["", "CIPHERWAKE_POSTURE_FIXES"];
|
|
@@ -797,6 +932,25 @@ function formatPostureFixesBlock(fixes) {
|
|
|
797
932
|
return color("dim", lines.join("\n"));
|
|
798
933
|
}
|
|
799
934
|
|
|
935
|
+
// Compute ship_decision: pass | review | block.
|
|
936
|
+
// Used in three contexts:
|
|
937
|
+
// - one-shot scan: severity-only, no diff baseline
|
|
938
|
+
// - trust-diff / preview-diff: severity + diff-since-baseline
|
|
939
|
+
// - deploy-check: same as trust-diff (it's an alias)
|
|
940
|
+
//
|
|
941
|
+
// Decision rules (advisory only):
|
|
942
|
+
// * `block` — critical severity present
|
|
943
|
+
// * `review` — high severity OR diff introduced new high-severity OR DBR drop ≥1.0
|
|
944
|
+
// * `pass` — everything else
|
|
945
|
+
function computeShipDecision({ maxSeverity, hasUnexpectedDiff, scoreDelta }) {
|
|
946
|
+
const sev = String(maxSeverity || "none").toLowerCase();
|
|
947
|
+
if (sev === "critical") return "block";
|
|
948
|
+
if (sev === "high") return "review";
|
|
949
|
+
if (hasUnexpectedDiff) return "review";
|
|
950
|
+
if (typeof scoreDelta === "number" && scoreDelta <= -1.0) return "review";
|
|
951
|
+
return "pass";
|
|
952
|
+
}
|
|
953
|
+
|
|
800
954
|
function aiBannerColor(shipDecision) {
|
|
801
955
|
if (shipDecision === "pass") return "green";
|
|
802
956
|
if (shipDecision === "block") return "red";
|
|
@@ -3319,10 +3473,16 @@ async function runScanBasedDeployCheck(domain, args) {
|
|
|
3319
3473
|
}
|
|
3320
3474
|
|
|
3321
3475
|
async function runTrustDiffCommand(args) {
|
|
3476
|
+
// R86.7 — reject unknown flags before any other parsing. Same rationale
|
|
3477
|
+
// as the bare-scan path: typo'd safety-critical flags must fail loud,
|
|
3478
|
+
// not silently degrade.
|
|
3479
|
+
if (assertKnownFlags(args, "pqcheck trust-diff")) {
|
|
3480
|
+
process.exit(3);
|
|
3481
|
+
}
|
|
3322
3482
|
const positional = args.filter((a) => !a.startsWith("-") && !isFlagValue(args, a));
|
|
3323
3483
|
if (positional.length === 0) {
|
|
3324
3484
|
console.error(color("red", "error: pqcheck trust-diff requires a domain"));
|
|
3325
|
-
console.error(color("dim", "Usage: npx pqcheck trust-diff <domain> [--baseline last-week] [--fail-on high] [--format pretty|json|sarif|github]"));
|
|
3485
|
+
console.error(color("dim", "Usage: npx pqcheck trust-diff <domain> [--baseline last-week] [--fail-on high] [--format pretty|json|sarif|github] [--strict-posture]"));
|
|
3326
3486
|
process.exit(3);
|
|
3327
3487
|
}
|
|
3328
3488
|
const domain = normalizeDomain(positional[0]);
|
|
@@ -3334,6 +3494,17 @@ async function runTrustDiffCommand(args) {
|
|
|
3334
3494
|
const baseline = parseFlag(args, "--baseline") || "last-week";
|
|
3335
3495
|
const failOn = parseFlag(args, "--fail-on") || "high";
|
|
3336
3496
|
const format = parseFlag(args, "--format") || "pretty";
|
|
3497
|
+
// R86.4 (2026-06-03) — see runOneScan for rationale. Default ship_decision
|
|
3498
|
+
// is drift-only (per-deploy regression gate); --strict-posture opts into
|
|
3499
|
+
// worst-of(drift, posture). Recommended only after a site reaches A/B
|
|
3500
|
+
// posture to lock that in.
|
|
3501
|
+
// Accept both `--strict-posture` (explicit) and `--strict` (short alias).
|
|
3502
|
+
// Audit caught a footgun where customers typed `--strict` expecting the
|
|
3503
|
+
// posture gate to fire and got silent drift-only mode instead. The
|
|
3504
|
+
// `onboard` subcommand uses `--strict` for a different purpose (gate exit
|
|
3505
|
+
// code on step failure) but that's a separate command — the alias is
|
|
3506
|
+
// scoped to scan / deploy-check / trust-diff only.
|
|
3507
|
+
const strictPosture = args.includes("--strict-posture") || args.includes("--strict");
|
|
3337
3508
|
|
|
3338
3509
|
// Build headers conditionally — Authorization is set ONLY if the user has
|
|
3339
3510
|
// an API key. Without it, the server's applyRepoQuota falls through to the
|
|
@@ -3479,7 +3650,12 @@ async function runTrustDiffCommand(args) {
|
|
|
3479
3650
|
posture_findings_count: (posture.findings || []).length,
|
|
3480
3651
|
posture_fixes_count: (posture.fixes || []).length,
|
|
3481
3652
|
} : {};
|
|
3482
|
-
|
|
3653
|
+
// R86.2 / R86.4 — see combineShipDecision: posture is advisory by default,
|
|
3654
|
+
// gated only when --strict-posture is passed.
|
|
3655
|
+
const effectiveShip = combineShipDecision(shipDecision, posture?.decision, { strict: strictPosture });
|
|
3656
|
+
// R86.5 — surface posture advisory line in default (non-strict) mode.
|
|
3657
|
+
const postureAdvisory = formatPostureAdvisoryLine(posture, strictPosture);
|
|
3658
|
+
if (postureAdvisory) console.log(postureAdvisory);
|
|
3483
3659
|
console.log(formatAiFooterBlock({
|
|
3484
3660
|
status: effectiveShip,
|
|
3485
3661
|
domain,
|
|
@@ -3491,6 +3667,7 @@ async function runTrustDiffCommand(args) {
|
|
|
3491
3667
|
ship_decision: effectiveShip,
|
|
3492
3668
|
ship_decision_drift: shipDecision,
|
|
3493
3669
|
ship_decision_posture: posture?.decision,
|
|
3670
|
+
ship_decision_mode: strictPosture ? "strict_posture" : "drift_only",
|
|
3494
3671
|
top_issue: topDelta?.id || topDelta?.type || "none",
|
|
3495
3672
|
top_issue_title: topDelta?.title || undefined,
|
|
3496
3673
|
dbr: typeof result.current_score === "number" ? result.current_score.toFixed(1) : undefined,
|
|
@@ -3499,8 +3676,10 @@ async function runTrustDiffCommand(args) {
|
|
|
3499
3676
|
quota_limit: result.quota?.monthly_limit ?? undefined,
|
|
3500
3677
|
scanned_at: new Date().toISOString(),
|
|
3501
3678
|
advisory_only: "true",
|
|
3502
|
-
scope: "trust_surface_drift_plus_absolute_posture",
|
|
3503
|
-
scope_note:
|
|
3679
|
+
scope: strictPosture ? "trust_surface_drift_plus_absolute_posture" : "trust_surface_drift",
|
|
3680
|
+
scope_note: strictPosture
|
|
3681
|
+
? "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."
|
|
3682
|
+
: "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
3683
|
...postureFields,
|
|
3505
3684
|
}));
|
|
3506
3685
|
if (posture && Array.isArray(posture.fixes) && posture.fixes.length > 0) {
|
|
@@ -3822,6 +4001,11 @@ async function runGuardsRunCommand(args) {
|
|
|
3822
4001
|
}
|
|
3823
4002
|
|
|
3824
4003
|
async function runPreviewDiffCommand(args) {
|
|
4004
|
+
// R86.7 — reject unknown flags before parsing. Closes the silent-no-op
|
|
4005
|
+
// class of footguns for the preview-diff command too.
|
|
4006
|
+
if (assertKnownFlags(args, "pqcheck preview-diff")) {
|
|
4007
|
+
process.exit(3);
|
|
4008
|
+
}
|
|
3825
4009
|
const previewUrl = parseFlag(args, "--preview");
|
|
3826
4010
|
const productionUrl = parseFlag(args, "--production");
|
|
3827
4011
|
if (!previewUrl || !productionUrl) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pqcheck",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.23",
|
|
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",
|