@enwithai/product-check 0.2.0 → 0.3.0

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 (63) hide show
  1. package/ATTRIBUTION.md +4 -4
  2. package/CHANGELOG.md +28 -0
  3. package/README.md +29 -16
  4. package/SECURITY.md +5 -1
  5. package/dashboard/assurance.css +71 -0
  6. package/dashboard/assurance.js +21 -0
  7. package/dist/assurance/adapters.d.ts +32 -0
  8. package/dist/assurance/adapters.js +76 -0
  9. package/dist/assurance/approvals.d.ts +34 -0
  10. package/dist/assurance/approvals.js +133 -0
  11. package/dist/assurance/cli.d.ts +13 -0
  12. package/dist/assurance/cli.js +863 -0
  13. package/dist/assurance/dashboard.d.ts +18 -0
  14. package/dist/assurance/dashboard.js +267 -0
  15. package/dist/assurance/diff.d.ts +20 -0
  16. package/dist/assurance/diff.js +99 -0
  17. package/dist/assurance/evidence-log.d.ts +20 -0
  18. package/dist/assurance/evidence-log.js +100 -0
  19. package/dist/assurance/fix-packets.d.ts +12 -0
  20. package/dist/assurance/fix-packets.js +178 -0
  21. package/dist/assurance/gate.d.ts +37 -0
  22. package/dist/assurance/gate.js +324 -0
  23. package/dist/assurance/lanes.d.ts +61 -0
  24. package/dist/assurance/lanes.js +380 -0
  25. package/dist/assurance/policy.d.ts +10 -0
  26. package/dist/assurance/policy.js +85 -0
  27. package/dist/assurance/proof.d.ts +35 -0
  28. package/dist/assurance/proof.js +146 -0
  29. package/dist/assurance/register.d.ts +41 -0
  30. package/dist/assurance/register.js +480 -0
  31. package/dist/assurance/render-md.d.ts +17 -0
  32. package/dist/assurance/render-md.js +212 -0
  33. package/dist/assurance/repo-state.d.ts +15 -0
  34. package/dist/assurance/repo-state.js +79 -0
  35. package/dist/assurance/review.d.ts +52 -0
  36. package/dist/assurance/review.js +408 -0
  37. package/dist/assurance/risk.d.ts +21 -0
  38. package/dist/assurance/risk.js +184 -0
  39. package/dist/assurance/types.d.ts +573 -0
  40. package/dist/assurance/types.js +6 -0
  41. package/dist/assurance/util.d.ts +24 -0
  42. package/dist/assurance/util.js +89 -0
  43. package/dist/assurance/verification.d.ts +30 -0
  44. package/dist/assurance/verification.js +212 -0
  45. package/dist/assurance/verify.d.ts +26 -0
  46. package/dist/assurance/verify.js +304 -0
  47. package/dist/cli/run.js +8 -1
  48. package/dist/exec/process.d.ts +27 -0
  49. package/dist/exec/process.js +175 -0
  50. package/dist/reporting/markdown.d.ts +1 -0
  51. package/dist/reporting/markdown.js +1 -1
  52. package/dist/schema/registry.js +9 -0
  53. package/package.json +1 -1
  54. package/schemas/approval.schema.json +98 -0
  55. package/schemas/assurance-common.schema.json +291 -0
  56. package/schemas/assurance-policy.schema.json +185 -0
  57. package/schemas/change-review.schema.json +711 -0
  58. package/schemas/closure-register.schema.json +499 -0
  59. package/schemas/evidence-record.schema.json +94 -0
  60. package/schemas/fix-packet.schema.json +272 -0
  61. package/schemas/release-gate.schema.json +308 -0
  62. package/schemas/reviewer-findings.schema.json +49 -0
  63. package/schemas/verification-run.schema.json +199 -0
@@ -0,0 +1,30 @@
1
+ import type { Finding, Results, Severity } from "../types.js";
2
+ import { type RuleHit } from "./lanes.js";
3
+ import type { DiffFile } from "./diff.js";
4
+ import type { FindingVerification, VerificationCheck, VerificationState } from "./types.js";
5
+ /** Rules evaluated over a whole changed file (re-checked with the file rules, not line by line). */
6
+ export declare const FILE_RULES: Set<string>;
7
+ /** True when a verified finding of this severity may block. P0 also blocks unverified under a fail-closed policy. */
8
+ export declare function isBlocking(severity: Severity, state: VerificationState, failClosedP0: boolean): boolean;
9
+ /** Re-checks a review hit against the file as it is at the head commit. */
10
+ export declare function recheckHit(hit: RuleHit, headText: string | null, diffFile: DiffFile | undefined): VerificationCheck;
11
+ /** Verifies a hit given the recheck and the other hits/claims of the same review. */
12
+ export declare function verifyHit(hit: RuleHit, recheck: VerificationCheck, others: {
13
+ path: string;
14
+ line: number | null;
15
+ source: string;
16
+ }[]): FindingVerification;
17
+ /** An external reviewer's claim (for example an AI code reviewer's comment). */
18
+ export interface ReviewerClaim {
19
+ reviewer: string;
20
+ type: "ai" | "human" | "imported";
21
+ path: string;
22
+ line: number | null;
23
+ title: string;
24
+ severity: Severity;
25
+ body: string;
26
+ }
27
+ /** Verifies an external claim: it can be disproven by the code, corroborated by a deterministic hit, or triaged. */
28
+ export declare function verifyClaim(claim: ReviewerClaim, headText: string | null, hits: RuleHit[]): FindingVerification;
29
+ /** Verifies a finding from a Product Check results document (baseline whole-product scan). */
30
+ export declare function verifyResultsFinding(f: Finding, results: Results, repo: string | null): FindingVerification;
@@ -0,0 +1,212 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 EnWithAI Technologies
3
+ /**
4
+ * Independent verification of alleged findings before they may influence a block decision.
5
+ *
6
+ * Checks (each recorded with its result):
7
+ * - evidence-present the finding links evidence that exists;
8
+ * - independent-reread the cited file is read again at the exact commit and the observation still holds
9
+ * (a different code path from the reviewer that raised it);
10
+ * - runtime-observation the evidence is a runtime response or an executed test;
11
+ * - second-signal another independent reviewer/lane points at the same place.
12
+ *
13
+ * States: fact + reread/runtime → CONFIRMED; inference + reread → CORROBORATED (CONFIRMED with a second signal);
14
+ * hypothesis → NEEDS_HUMAN_TRIAGE unless a second signal → CORROBORATED; reread contradicts → DISPROVEN; nothing
15
+ * re-checkable → EVIDENCE_COLLECTED; no evidence at all → SUSPECTED. Speculative or unverified reviewer claims are
16
+ * never treated as proven defects.
17
+ */
18
+ import { existsSync, readFileSync } from "node:fs";
19
+ import { isAbsolute, resolve, sep } from "node:path";
20
+ import { sha256 } from "../util/hash.js";
21
+ import { fileHits, lineHits } from "./lanes.js";
22
+ /** Rules evaluated over a whole changed file (re-checked with the file rules, not line by line). */
23
+ export const FILE_RULES = new Set([
24
+ "VC-TR-AUTHZ-REMOVED",
25
+ "VC-TR-OBJECT-AUTHZ",
26
+ "VC-OP-MIGRATION-NO-DOWN",
27
+ "VC-CR-MOCK-ONLY-TEST",
28
+ "VC-CR-NO-TESTS",
29
+ ]);
30
+ const BLOCKING_STATES = ["CONFIRMED", "CORROBORATED", "REPRODUCED"];
31
+ /** True when a verified finding of this severity may block. P0 also blocks unverified under a fail-closed policy. */
32
+ export function isBlocking(severity, state, failClosedP0) {
33
+ if (severity !== "P0" && severity !== "P1")
34
+ return false;
35
+ if (BLOCKING_STATES.includes(state))
36
+ return true;
37
+ return severity === "P0" && failClosedP0 && state !== "DISPROVEN";
38
+ }
39
+ function decide(epistemic, checks) {
40
+ const r = (name) => checks.find((c) => c.check === name)?.result;
41
+ if (r("evidence-present") === "fail")
42
+ return "SUSPECTED";
43
+ if (r("independent-reread") === "fail")
44
+ return "DISPROVEN";
45
+ const reread = r("independent-reread") === "pass";
46
+ const runtime = r("runtime-observation") === "pass";
47
+ const second = r("second-signal") === "pass";
48
+ if (epistemic === "fact")
49
+ return reread || runtime ? "CONFIRMED" : "EVIDENCE_COLLECTED";
50
+ if (epistemic === "inference") {
51
+ if ((reread || runtime) && second)
52
+ return "CONFIRMED";
53
+ if (reread || runtime || second)
54
+ return "CORROBORATED";
55
+ return "EVIDENCE_COLLECTED";
56
+ }
57
+ return second || runtime ? "CORROBORATED" : "NEEDS_HUMAN_TRIAGE";
58
+ }
59
+ /** Re-checks a review hit against the file as it is at the head commit. */
60
+ export function recheckHit(hit, headText, diffFile) {
61
+ if (hit.rule_id === "VC-CR-NO-TESTS")
62
+ return {
63
+ check: "independent-reread",
64
+ result: "not-run",
65
+ detail: "Change-level inference; no single file to re-read.",
66
+ };
67
+ if (headText === null)
68
+ return {
69
+ check: "independent-reread",
70
+ result: "not-run",
71
+ detail: "Head content unavailable (diff supplied as a file or file deleted).",
72
+ };
73
+ if (hit.rule_id === "VC-TR-AUTHZ-REMOVED") {
74
+ const still = headText.includes(hit.excerpt);
75
+ return still
76
+ ? {
77
+ check: "independent-reread",
78
+ result: "fail",
79
+ detail: "The removed auth line is still present at head (moved, not removed).",
80
+ }
81
+ : { check: "independent-reread", result: "pass", detail: "Re-read at head: the auth check is absent." };
82
+ }
83
+ const again = FILE_RULES.has(hit.rule_id) && diffFile
84
+ ? fileHits(diffFile, headText).filter((h) => h.rule_id === hit.rule_id)
85
+ : lineHits(hit.path, headText.split("\n").map((text, i) => ({ line: i + 1, text }))).filter((h) => h.rule_id === hit.rule_id && h.signature === hit.signature);
86
+ return again.length > 0
87
+ ? {
88
+ check: "independent-reread",
89
+ result: "pass",
90
+ detail: `Re-read ${hit.path} at head: the condition holds${hit.line !== null ? ` (line ${again.map((h) => h.line).join(", ")})` : ""}.`,
91
+ }
92
+ : {
93
+ check: "independent-reread",
94
+ result: "fail",
95
+ detail: `Re-read ${hit.path} at head: the condition does not hold.`,
96
+ };
97
+ }
98
+ /** Verifies a hit given the recheck and the other hits/claims of the same review. */
99
+ export function verifyHit(hit, recheck, others) {
100
+ const near = others.filter((o) => o.path === hit.path && (hit.line === null || o.line === null || Math.abs(o.line - hit.line) <= 5));
101
+ const checks = [
102
+ { check: "evidence-present", result: "pass", detail: "Diff line(s) recorded as evidence." },
103
+ recheck,
104
+ near.length > 0
105
+ ? {
106
+ check: "second-signal",
107
+ result: "pass",
108
+ detail: `Also raised by: ${[...new Set(near.map((n) => n.source))].join(", ")}.`,
109
+ }
110
+ : { check: "second-signal", result: "not-run", detail: "No other reviewer points at this location." },
111
+ ];
112
+ return { state: decide(hit.epistemic, checks), checks };
113
+ }
114
+ /** Verifies an external claim: it can be disproven by the code, corroborated by a deterministic hit, or triaged. */
115
+ export function verifyClaim(claim, headText, hits) {
116
+ const checks = [];
117
+ if (headText === null) {
118
+ checks.push({
119
+ check: "independent-reread",
120
+ result: "fail",
121
+ detail: `${claim.path} does not exist at head.`,
122
+ });
123
+ return { state: "DISPROVEN", checks };
124
+ }
125
+ const lines = headText.split("\n").length;
126
+ if (claim.line !== null && (claim.line < 1 || claim.line > lines)) {
127
+ checks.push({
128
+ check: "independent-reread",
129
+ result: "fail",
130
+ detail: `${claim.path} has ${lines} lines; line ${claim.line} does not exist.`,
131
+ });
132
+ return { state: "DISPROVEN", checks };
133
+ }
134
+ checks.push({
135
+ check: "independent-reread",
136
+ result: "pass",
137
+ detail: "The cited location exists at head; the claim itself is not machine-checked.",
138
+ });
139
+ const near = hits.filter((h) => h.path === claim.path && (claim.line === null || h.line === null || Math.abs(h.line - claim.line) <= 5));
140
+ checks.push(near.length > 0
141
+ ? {
142
+ check: "second-signal",
143
+ result: "pass",
144
+ detail: `Deterministic rule(s) agree: ${near.map((h) => h.rule_id).join(", ")}.`,
145
+ }
146
+ : { check: "second-signal", result: "not-run", detail: "No deterministic rule corroborates the claim." });
147
+ return { state: near.length > 0 ? "CORROBORATED" : "NEEDS_HUMAN_TRIAGE", checks };
148
+ }
149
+ /** Candidate absolute paths for an evidence locator. */
150
+ function locate(locator, repo, repoLabel) {
151
+ const root = resolve(repo);
152
+ const cands = isAbsolute(locator)
153
+ ? [locator]
154
+ : [
155
+ resolve(root, locator),
156
+ ...(repoLabel && locator.startsWith(`${repoLabel}/`)
157
+ ? [resolve(root, locator.slice(repoLabel.length + 1))]
158
+ : []),
159
+ ];
160
+ return cands.find((c) => c.startsWith(root + sep) && existsSync(c)) ?? null;
161
+ }
162
+ /** Verifies a finding from a Product Check results document (baseline whole-product scan). */
163
+ export function verifyResultsFinding(f, results, repo) {
164
+ const ev = f.evidence_ids
165
+ .map((id) => results.evidence.find((e) => e.id === id))
166
+ .filter((e) => e !== undefined);
167
+ const checks = [
168
+ ev.length > 0 && ev.length === f.evidence_ids.length
169
+ ? { check: "evidence-present", result: "pass", detail: `${ev.length} evidence record(s) linked.` }
170
+ : { check: "evidence-present", result: "fail", detail: "Linked evidence is missing from the results." },
171
+ ];
172
+ const runtime = ev.filter((e) => e.strength === "runtime" || e.strength === "executed-test");
173
+ checks.push(runtime.length > 0
174
+ ? {
175
+ check: "runtime-observation",
176
+ result: "pass",
177
+ detail: `Observed at runtime or by an executed test (${runtime.map((e) => e.id).join(", ")}).`,
178
+ }
179
+ : { check: "runtime-observation", result: "not-run", detail: "No runtime or executed-test evidence." });
180
+ const files = ev.filter((e) => e.kind === "source-location" && e.checksum !== null);
181
+ if (repo === null || files.length === 0) {
182
+ checks.push({
183
+ check: "independent-reread",
184
+ result: "not-run",
185
+ detail: repo === null
186
+ ? "Repository not available for a re-read."
187
+ : "No checksummed source evidence to re-read.",
188
+ });
189
+ }
190
+ else {
191
+ const label = results.contract.target.repository?.path ?? null;
192
+ const outcomes = files.map((e) => {
193
+ const p = locate(e.source.locator, repo, label);
194
+ if (!p)
195
+ return "missing";
196
+ return sha256(readFileSync(p)) === e.checksum?.value ? "match" : "changed";
197
+ });
198
+ if (outcomes.every((o) => o === "match"))
199
+ checks.push({
200
+ check: "independent-reread",
201
+ result: "pass",
202
+ detail: `Re-read ${files.length} cited file(s); checksums match the evidence.`,
203
+ });
204
+ else
205
+ checks.push({
206
+ check: "independent-reread",
207
+ result: "not-run",
208
+ detail: `Cited file(s) ${outcomes.includes("missing") ? "missing" : "changed"} since the scan; re-read is inconclusive (not a disproof).`,
209
+ });
210
+ }
211
+ return { state: decide(f.epistemic, checks), checks };
212
+ }
@@ -0,0 +1,26 @@
1
+ import type { Results, ScanContract } from "../types.js";
2
+ import type { ExecPermission } from "../exec/process.js";
3
+ import type { EvidenceLog } from "./evidence-log.js";
4
+ import { type AssurancePolicy, type ClosureRegister, type EnvironmentRecord, type VerificationRun } from "./types.js";
5
+ export interface VerifyInput {
6
+ register: ClosureRegister;
7
+ findingIds: string[];
8
+ repo: string;
9
+ head: string;
10
+ clean: boolean | null;
11
+ baseline: {
12
+ results: Results;
13
+ sha256: string;
14
+ };
15
+ contract: ScanContract | null;
16
+ extraTestResults: string[];
17
+ policy: AssurancePolicy;
18
+ permission: ExecPermission;
19
+ env: EnvironmentRecord;
20
+ log: EvidenceLog;
21
+ now: () => Date;
22
+ baseDir: string;
23
+ writeAfterResults: (verificationId: string, results: Results) => string;
24
+ }
25
+ /** Runs one verification and returns the run (the caller applies it to the register). */
26
+ export declare function verifyFindings(input: VerifyInput): Promise<VerificationRun>;
@@ -0,0 +1,304 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 EnWithAI Technologies
3
+ /**
4
+ * Closure verification (VC-P5) against the exact fix commit.
5
+ *
6
+ * Targeted reproduction, per finding:
7
+ * - baseline-scan findings: Product Check re-assesses the repository at the fix commit (offline; same contract,
8
+ * imports cleared unless re-supplied) and the finding's control must no longer produce the finding or any
9
+ * finding of equal or higher severity. A control that cannot be evaluated is NOT_TESTED, never PASS.
10
+ * - change-review findings: the rule is re-run over the whole file at the fix commit; it must no longer match.
11
+ * - the implementer's declared reproduction commands must exit 0.
12
+ * Regression: the policy's required checks and the regression corpus must pass; the re-assessment must show no new
13
+ * P0/P1 finding and no regressed control compared with the baseline.
14
+ *
15
+ * Preconditions: the repository HEAD is the fix commit, the working tree is clean, and the finding is
16
+ * RETEST_REQUIRED. Before/after results are retained (the after-results are written next to the register).
17
+ */
18
+ import { readFileSync } from "node:fs";
19
+ import { resolve, sep } from "node:path";
20
+ import { defaultContract } from "../contract/contract.js";
21
+ import { runAssessment } from "../run.js";
22
+ import { sha256 } from "../util/hash.js";
23
+ import { CONTROL_BY_ID } from "../validation/catalog.js";
24
+ import { lineHits } from "./lanes.js";
25
+ import { FILE_RULES } from "./verification.js";
26
+ import { overall, runCommands } from "./proof.js";
27
+ import { ASSURANCE_SCHEMA_VERSION, } from "./types.js";
28
+ import { seqId } from "./util.js";
29
+ const RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
30
+ /** Runs one verification and returns the run (the caller applies it to the register). */
31
+ export async function verifyFindings(input) {
32
+ const reg = input.register;
33
+ const started = input.now().toISOString();
34
+ const verificationId = seqId("VR", reg.verification_runs.length + 1);
35
+ const entries = input.findingIds.map((id) => reg.entries.find((e) => e.finding_id === id));
36
+ const reasons = [];
37
+ const limitations = [];
38
+ const checks = [];
39
+ const fixIds = [...new Set(entries.map((e) => e?.fix_ids.at(-1) ?? null))];
40
+ const fix = reg.fixes.find((f) => f.fix_id === fixIds[0]) ?? null;
41
+ let executions = [];
42
+ let afterResults = null;
43
+ const precondition = (msg) => {
44
+ reasons.push(msg);
45
+ };
46
+ if (entries.some((e) => e === undefined))
47
+ precondition("Unknown finding ID.");
48
+ else if (entries.some((e) => e?.state !== "RETEST_REQUIRED"))
49
+ precondition(`Every finding must be RETEST_REQUIRED (got ${entries.map((e) => `${e?.finding_id}=${e?.state}`).join(", ")}).`);
50
+ if (fixIds.length !== 1 || fix === null)
51
+ precondition("The findings must share one recorded fix.");
52
+ else if (fix.change_sha !== input.head)
53
+ precondition(`HEAD ${input.head.slice(0, 12)} is not the fix commit ${fix.change_sha.slice(0, 12)}; verification must run against the exact fix SHA.`);
54
+ if (input.clean !== true)
55
+ precondition(input.clean === false
56
+ ? "The working tree is not clean; results would not describe the fix commit."
57
+ : "Could not determine whether the working tree is clean.");
58
+ const run = () => ({
59
+ schema_version: ASSURANCE_SCHEMA_VERSION,
60
+ kind: "verification-run",
61
+ verification_id: verificationId,
62
+ purpose: "closure",
63
+ finding_ids: input.findingIds,
64
+ fix_id: fix?.fix_id ?? null,
65
+ exact_sha: input.head,
66
+ worktree_clean: input.clean,
67
+ environment: input.env,
68
+ started_at: started,
69
+ finished_at: input.now().toISOString(),
70
+ checks,
71
+ executions,
72
+ before_evidence_ids: [],
73
+ after_evidence_ids: [],
74
+ after_results: afterResults,
75
+ result: "INCONCLUSIVE",
76
+ reasons,
77
+ limitations,
78
+ actor: { id: "product-check", type: "system" },
79
+ });
80
+ if (reasons.length > 0) {
81
+ const r = run();
82
+ r.result = "INCONCLUSIVE";
83
+ reasons.unshift("Preconditions not met; nothing was verified.");
84
+ return r;
85
+ }
86
+ const es = entries;
87
+ // Re-assessment at the fix commit (offline) -----------------------------------------------------------------------
88
+ const base = input.baseline.results;
89
+ const contract = input.contract ?? structuredClone(base.contract);
90
+ if (!input.contract) {
91
+ contract.imports = defaultContract().imports;
92
+ limitations.push("Imported artifacts from the baseline (test results, reports, observations) were not reused; controls that need them are NOT_TESTED unless re-supplied.");
93
+ }
94
+ contract.target.repository = { path: input.repo };
95
+ contract.imports.test_results = [...contract.imports.test_results, ...input.extraTestResults];
96
+ const { results: after } = await runAssessment({
97
+ contract,
98
+ permissions: {
99
+ network: false,
100
+ exposureProbe: false,
101
+ origin: null,
102
+ appBlockedReason: contract.target.application
103
+ ? "Closure verification runs offline; supply recorded observations with --contract to re-check application controls."
104
+ : null,
105
+ },
106
+ baseline: input.baseline,
107
+ baseDir: input.baseDir,
108
+ now: input.now,
109
+ });
110
+ const path = input.writeAfterResults(verificationId, after);
111
+ const afterSha = sha256(`${JSON.stringify(after, null, 2)}\n`);
112
+ const afterEvidence = input.log.append({
113
+ type: "results-artifact",
114
+ subject: verificationId,
115
+ commit_sha: input.head,
116
+ environment_id: input.env.environment_id,
117
+ summary: `Re-assessment ${after.run.run_id} at ${input.head.slice(0, 12)}: ${after.verdict.verdict}`,
118
+ locator: path,
119
+ line: null,
120
+ payload: {
121
+ run_id: after.run.run_id,
122
+ results_sha256: afterSha,
123
+ verdict: after.verdict.verdict,
124
+ open: after.scorecard.open_findings,
125
+ },
126
+ });
127
+ afterResults = {
128
+ run_id: after.run.run_id,
129
+ results_path: path,
130
+ results_sha256: afterSha,
131
+ verdict: after.verdict.verdict,
132
+ open: after.scorecard.open_findings,
133
+ };
134
+ for (const e of es) {
135
+ if (e.source === "baseline-scan") {
136
+ const v = after.validations.find((x) => x.control_id === e.rule_or_control);
137
+ const still = after.findings.find((f) => f.id === e.source_finding_id && f.status !== "RESOLVED");
138
+ const sameOrWorse = after.findings.filter((f) => f.control_id === e.rule_or_control &&
139
+ (f.status === "OPEN" || f.status === "UNVERIFIED") &&
140
+ RANK[f.severity] <= RANK[e.severity]);
141
+ let status;
142
+ let detail;
143
+ if (!v || v.status === "NOT_TESTED" || v.status === "BLOCKED") {
144
+ status = "NOT_TESTED";
145
+ detail = `${e.rule_or_control} could not be evaluated at the fix commit${v?.reason ? `: ${v.reason}` : ""}.`;
146
+ }
147
+ else if (still) {
148
+ status = "FAIL";
149
+ detail = `${e.rule_or_control} still reports the original finding (${still.location}).`;
150
+ }
151
+ else if (sameOrWorse.length > 0) {
152
+ status = "FAIL";
153
+ detail = `The original location is gone but ${e.rule_or_control} still fails at ${sameOrWorse
154
+ .map((f) => f.location)
155
+ .slice(0, 3)
156
+ .join(", ")} (${sameOrWorse[0]?.severity}): the fix moved the problem.`;
157
+ }
158
+ else {
159
+ status = "PASS";
160
+ detail = `${e.rule_or_control} is ${v.status} at the fix commit and no longer reports the finding.`;
161
+ }
162
+ checks.push({
163
+ check_id: `targeted:${e.finding_id}:control`,
164
+ kind: "control-reassessment",
165
+ role: "targeted",
166
+ status,
167
+ detail,
168
+ execution_id: null,
169
+ evidence_ids: [afterEvidence],
170
+ });
171
+ const gate = CONTROL_BY_ID.get(e.rule_or_control)?.gate ?? null;
172
+ if (gate) {
173
+ const g = after.scorecard.gates.find((x) => x.gate_id === gate);
174
+ checks.push({
175
+ check_id: `targeted:${e.finding_id}:gate`,
176
+ kind: "gate-recheck",
177
+ role: "targeted",
178
+ status: g === undefined ? "NOT_TESTED" : g.status === "FAIL" ? "FAIL" : "PASS",
179
+ detail: g === undefined
180
+ ? `Associated hard gate ${gate} was not evaluated.`
181
+ : g.status === "UNVERIFIED"
182
+ ? `Associated hard gate ${gate} no longer fails; it stays UNVERIFIED overall because other gate controls were not testable here (disclosed by the release gate).`
183
+ : `Associated hard gate ${gate}: ${g.status}.`,
184
+ execution_id: null,
185
+ evidence_ids: [afterEvidence],
186
+ });
187
+ }
188
+ }
189
+ else {
190
+ const [path0, lineStr] = e.location.split(/:(?=\d+$)/);
191
+ const filePath = path0 ?? e.location;
192
+ let text;
193
+ try {
194
+ const root = resolve(input.repo);
195
+ const abs = resolve(root, filePath);
196
+ text = abs.startsWith(root + sep) ? readFileSync(abs, "utf8") : null;
197
+ }
198
+ catch {
199
+ text = null;
200
+ }
201
+ const still = text === null
202
+ ? []
203
+ : lineHits(filePath, text.split("\n").map((t, i) => ({ line: i + 1, text: t }))).filter((h) => h.rule_id === e.rule_or_control);
204
+ const fileRule = FILE_RULES.has(e.rule_or_control) ||
205
+ e.rule_or_control.startsWith("VC-EXT") ||
206
+ e.rule_or_control.startsWith("VC-SP");
207
+ checks.push(fileRule
208
+ ? {
209
+ check_id: `targeted:${e.finding_id}:rule`,
210
+ kind: "rule-recheck",
211
+ role: "targeted",
212
+ status: "NOT_TESTED",
213
+ detail: `${e.rule_or_control} is a change-level or external finding; a declared reproduction command is the targeted check.`,
214
+ execution_id: null,
215
+ evidence_ids: [],
216
+ }
217
+ : {
218
+ check_id: `targeted:${e.finding_id}:rule`,
219
+ kind: "rule-recheck",
220
+ role: "targeted",
221
+ status: still.length === 0 ? "PASS" : "FAIL",
222
+ detail: still.length === 0
223
+ ? `${e.rule_or_control} no longer matches ${filePath} at the fix commit${lineStr ? ` (was line ${lineStr})` : ""}.`
224
+ : `${e.rule_or_control} still matches ${filePath} at line(s) ${still.map((h) => h.line).join(", ")}.`,
225
+ execution_id: null,
226
+ evidence_ids: [],
227
+ });
228
+ }
229
+ }
230
+ // Commands --------------------------------------------------------------------------------------------------------
231
+ const ctx = {
232
+ repo: input.repo,
233
+ permission: input.permission,
234
+ policy: input.policy,
235
+ env: input.env,
236
+ sha: input.head,
237
+ log: input.log,
238
+ now: input.now,
239
+ };
240
+ const repro = runCommands(fix?.reproduction ?? [], "targeted", ctx);
241
+ const corpus = reg.regression_corpus.map((c) => c.command);
242
+ const regression = runCommands([...input.policy.required_checks, ...corpus], "regression", ctx);
243
+ executions = [...repro.executions, ...regression.executions];
244
+ checks.push(...repro.checks, ...regression.checks);
245
+ // A change-level finding with no reproduction command cannot be verified.
246
+ for (const c of checks.filter((x) => x.kind === "rule-recheck" && x.status === "NOT_TESTED")) {
247
+ if (repro.checks.some((r) => r.status === "PASS") && !repro.checks.some((r) => r.status !== "PASS")) {
248
+ c.status = "PASS";
249
+ c.detail += " Declared reproduction command(s) passed.";
250
+ }
251
+ }
252
+ // Regression vs baseline ------------------------------------------------------------------------------------------
253
+ const cmp = after.comparison;
254
+ const newCritical = (cmp?.findings_new ?? []).filter((id) => {
255
+ const f = after.findings.find((x) => x.id === id);
256
+ return f !== undefined && (f.severity === "P0" || f.severity === "P1");
257
+ });
258
+ checks.push({
259
+ check_id: "regression:no-new-critical",
260
+ kind: "no-new-critical",
261
+ role: "regression",
262
+ status: newCritical.length === 0 ? "PASS" : "FAIL",
263
+ detail: newCritical.length === 0
264
+ ? "No new P0/P1 finding compared with the baseline."
265
+ : `New P0/P1 finding(s): ${newCritical.join(", ")}.`,
266
+ execution_id: null,
267
+ evidence_ids: [afterEvidence],
268
+ });
269
+ const regressed = cmp?.regressed_controls ?? [];
270
+ checks.push({
271
+ check_id: "regression:controls",
272
+ kind: "no-regressed-controls",
273
+ role: "regression",
274
+ status: regressed.length === 0 ? "PASS" : "FAIL",
275
+ detail: regressed.length === 0
276
+ ? "No control regressed compared with the baseline."
277
+ : `Regressed control(s): ${regressed.join(", ")}.`,
278
+ execution_id: null,
279
+ evidence_ids: [afterEvidence],
280
+ });
281
+ if (input.policy.required_checks.length === 0 && corpus.length === 0)
282
+ limitations.push("No executable regression suite is declared (policy required_checks / corpus); regression rests on Product Check's re-assessment only.");
283
+ const r = run();
284
+ r.after_evidence_ids = [
285
+ afterEvidence,
286
+ ...executions.map((x) => x.evidence_id).filter((x) => x !== null),
287
+ ];
288
+ r.before_evidence_ids = es.flatMap((e) => {
289
+ const f = base.findings.find((x) => x.id === e.source_finding_id);
290
+ return f ? f.evidence_ids : [];
291
+ });
292
+ r.result = overall(checks);
293
+ const failed = checks.filter((c) => c.status === "FAIL");
294
+ const notRun = checks.filter((c) => c.status === "NOT_TESTED" || c.status === "BLOCKED");
295
+ if (failed.length > 0)
296
+ reasons.push(`Failed: ${failed.map((c) => `${c.check_id} — ${c.detail}`).join(" | ")}`);
297
+ if (notRun.length > 0)
298
+ reasons.push(`Not run: ${notRun.map((c) => `${c.check_id} — ${c.detail}`).join(" | ")}`);
299
+ if (r.result === "PASS")
300
+ reasons.push(`All ${checks.length} targeted and regression check(s) passed at ${input.head.slice(0, 12)}.`);
301
+ r.finished_at = input.now().toISOString();
302
+ r.verification_id = verificationId;
303
+ return r;
304
+ }
package/dist/cli/run.js CHANGED
@@ -26,6 +26,7 @@ import { relativize } from "../util/paths.js";
26
26
  import { CONTROLS } from "../validation/catalog.js";
27
27
  import { checkResults } from "../validation/integrity.js";
28
28
  import { STAGES, TIER_INFO } from "../validation/tiers.js";
29
+ import { ASSURANCE_COMMANDS, ASSURANCE_HELP, runAssurance } from "../assurance/cli.js";
29
30
  import { TOOL_VERSION } from "../version.js";
30
31
  /** Text printed by `product-check --help`. */
31
32
  export const HELP = `Product Check ${TOOL_VERSION} — does this software work, can it be trusted, and what needs to improve?
@@ -74,6 +75,8 @@ Output:
74
75
  --fail-on <verdicts> Exit 6 when the verdict is one of these (e.g. NOT_READY,INCONCLUSIVE)
75
76
  --no-dashboard --no-branding --quiet
76
77
 
78
+ ${ASSURANCE_HELP}
79
+
77
80
  Exit codes: 0 ok · 1 internal error · 2 usage/config · 3 input · 4 safety refusal · 5 invalid artifact · 6 --fail-on matched
78
81
  Local-first: no telemetry, no account, no model calls. Network only to an authorized origin with passive GETs.
79
82
  `;
@@ -472,6 +475,10 @@ function commandValidate(files, io) {
472
475
  }
473
476
  /** Parses arguments and runs one command. */
474
477
  async function main(argv, io) {
478
+ const first = argv[0] ?? "";
479
+ if (ASSURANCE_COMMANDS.includes(first) ||
480
+ ((first === "dashboard" || first === "blueprint") && argv.includes("--from")))
481
+ return runAssurance(first, argv.slice(1), io);
475
482
  const { values, positionals } = (() => {
476
483
  try {
477
484
  return parseArgs({ args: argv, allowPositionals: true, strict: true, options: OPTIONS });
@@ -581,7 +588,7 @@ async function main(argv, io) {
581
588
  return ExitCode.Ok;
582
589
  }
583
590
  default:
584
- throw usageError(`Unknown command: ${command}`, "Commands: run, retest, scan, spec, init, validate, report, blueprint, dashboard, controls, schema.");
591
+ throw usageError(`Unknown command: ${command}`, `Commands: run, retest, scan, spec, init, validate, report, blueprint, dashboard, controls, schema, ${ASSURANCE_COMMANDS.join(", ")}.`);
585
592
  }
586
593
  }
587
594
  /** Runs the CLI and returns the process exit code. Never throws. */
@@ -0,0 +1,27 @@
1
+ import type { CommandExecution, CommandSpec } from "../assurance/types.js";
2
+ /** Result of a git read. */
3
+ export interface GitResult {
4
+ ok: boolean;
5
+ stdout: string;
6
+ stderr: string;
7
+ }
8
+ /** Runs a read-only git subcommand in `repo`. */
9
+ export declare function gitRead(repo: string, args: string[], maxBuffer?: number): GitResult;
10
+ /** Authorization for command execution, decided once per CLI invocation. */
11
+ export interface ExecPermission {
12
+ allowed: boolean;
13
+ reason: string;
14
+ }
15
+ /** Inputs to one execution. */
16
+ export interface ExecInput {
17
+ spec: CommandSpec;
18
+ repo: string;
19
+ permission: ExecPermission;
20
+ envAllowlist: readonly string[];
21
+ maxOutputBytes: number;
22
+ environmentId: string;
23
+ commitSha: string | null;
24
+ now: () => Date;
25
+ }
26
+ /** Executes one declared command (argv, no shell). Never throws for command failures. */
27
+ export declare function execCommand(input: ExecInput): CommandExecution;