@enwithai/product-check 0.1.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.
- package/ATTRIBUTION.md +6 -6
- package/CHANGELOG.md +66 -0
- package/NOTICE.md +5 -3
- package/README.md +34 -19
- package/SECURITY.md +5 -1
- package/dashboard/assurance.css +71 -0
- package/dashboard/assurance.js +21 -0
- package/dashboard/dashboard.css +6 -0
- package/dist/assurance/adapters.d.ts +32 -0
- package/dist/assurance/adapters.js +76 -0
- package/dist/assurance/approvals.d.ts +34 -0
- package/dist/assurance/approvals.js +133 -0
- package/dist/assurance/cli.d.ts +13 -0
- package/dist/assurance/cli.js +863 -0
- package/dist/assurance/dashboard.d.ts +18 -0
- package/dist/assurance/dashboard.js +267 -0
- package/dist/assurance/diff.d.ts +20 -0
- package/dist/assurance/diff.js +99 -0
- package/dist/assurance/evidence-log.d.ts +20 -0
- package/dist/assurance/evidence-log.js +100 -0
- package/dist/assurance/fix-packets.d.ts +12 -0
- package/dist/assurance/fix-packets.js +178 -0
- package/dist/assurance/gate.d.ts +37 -0
- package/dist/assurance/gate.js +324 -0
- package/dist/assurance/lanes.d.ts +61 -0
- package/dist/assurance/lanes.js +380 -0
- package/dist/assurance/policy.d.ts +10 -0
- package/dist/assurance/policy.js +85 -0
- package/dist/assurance/proof.d.ts +35 -0
- package/dist/assurance/proof.js +146 -0
- package/dist/assurance/register.d.ts +41 -0
- package/dist/assurance/register.js +480 -0
- package/dist/assurance/render-md.d.ts +17 -0
- package/dist/assurance/render-md.js +212 -0
- package/dist/assurance/repo-state.d.ts +15 -0
- package/dist/assurance/repo-state.js +79 -0
- package/dist/assurance/review.d.ts +52 -0
- package/dist/assurance/review.js +408 -0
- package/dist/assurance/risk.d.ts +21 -0
- package/dist/assurance/risk.js +184 -0
- package/dist/assurance/types.d.ts +573 -0
- package/dist/assurance/types.js +6 -0
- package/dist/assurance/util.d.ts +24 -0
- package/dist/assurance/util.js +89 -0
- package/dist/assurance/verification.d.ts +30 -0
- package/dist/assurance/verification.js +212 -0
- package/dist/assurance/verify.d.ts +26 -0
- package/dist/assurance/verify.js +304 -0
- package/dist/blueprint/build.d.ts +3 -1
- package/dist/blueprint/build.js +41 -5
- package/dist/cli/run.js +103 -5
- package/dist/contract/contract.d.ts +4 -0
- package/dist/contract/contract.js +16 -0
- package/dist/dashboard/render.d.ts +5 -0
- package/dist/dashboard/render.js +107 -3
- package/dist/discovery/pops.d.ts +2 -0
- package/dist/discovery/pops.js +7 -5
- package/dist/exec/process.d.ts +27 -0
- package/dist/exec/process.js +175 -0
- package/dist/importers/operational.d.ts +56 -0
- package/dist/importers/operational.js +217 -0
- package/dist/importers/reports.d.ts +24 -0
- package/dist/importers/reports.js +37 -1
- package/dist/inspectors/source/surface.d.ts +30 -0
- package/dist/inspectors/source/surface.js +529 -0
- package/dist/parse/yaml.d.ts +4 -0
- package/dist/parse/yaml.js +39 -0
- package/dist/reporting/markdown.d.ts +1 -0
- package/dist/reporting/markdown.js +18 -1
- package/dist/reporting/spec-markdown.d.ts +9 -0
- package/dist/reporting/spec-markdown.js +144 -0
- package/dist/run.d.ts +2 -0
- package/dist/run.js +52 -2
- package/dist/schema/registry.js +10 -0
- package/dist/scoring/score.js +1 -1
- package/dist/spec/build.d.ts +28 -0
- package/dist/spec/build.js +278 -0
- package/dist/types.d.ts +78 -2
- package/dist/types.js +6 -1
- package/dist/validation/catalog.d.ts +1 -1
- package/dist/validation/catalog.js +125 -2
- package/dist/validation/context.d.ts +6 -1
- package/dist/validation/import-evaluators.js +37 -3
- package/dist/validation/integrity.js +4 -0
- package/dist/validation/operational-evaluators.d.ts +3 -0
- package/dist/validation/operational-evaluators.js +153 -0
- package/dist/validation/source-evaluators.js +1 -1
- package/package.json +4 -1
- package/schemas/approval.schema.json +98 -0
- package/schemas/as-built-spec.schema.json +356 -0
- package/schemas/assurance-common.schema.json +291 -0
- package/schemas/assurance-policy.schema.json +185 -0
- package/schemas/blueprint.schema.json +3 -0
- package/schemas/change-review.schema.json +711 -0
- package/schemas/closure-register.schema.json +499 -0
- package/schemas/evidence-record.schema.json +94 -0
- package/schemas/fix-packet.schema.json +272 -0
- package/schemas/release-gate.schema.json +308 -0
- package/schemas/reviewer-findings.schema.json +49 -0
- package/schemas/scan-contract.schema.json +20 -0
- package/schemas/scorecard.schema.json +1 -1
- package/schemas/verification-run.schema.json +199 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Results } from "../types.js";
|
|
2
|
+
import type { ChangeReview, ClosureRegister, EvidenceRecord, FixPacket, ReleaseGate, VerificationRun } from "./types.js";
|
|
3
|
+
export interface DashboardBundle {
|
|
4
|
+
results: Results;
|
|
5
|
+
register: ClosureRegister | null;
|
|
6
|
+
review: ChangeReview | null;
|
|
7
|
+
proof: VerificationRun | null;
|
|
8
|
+
packets: FixPacket[];
|
|
9
|
+
gate: ReleaseGate;
|
|
10
|
+
log: EvidenceRecord[];
|
|
11
|
+
now: Date;
|
|
12
|
+
}
|
|
13
|
+
/** Agent export: fix packets with references, never evidence excerpts. */
|
|
14
|
+
export declare function agentExport(b: DashboardBundle): unknown;
|
|
15
|
+
/** Renders the dashboard. */
|
|
16
|
+
export declare function renderAssuranceDashboard(b: DashboardBundle, options: {
|
|
17
|
+
branding: boolean;
|
|
18
|
+
}): string;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 EnWithAI Technologies
|
|
3
|
+
/**
|
|
4
|
+
* The assurance dashboard (dashboard/index.html): one self-contained page rendered from the real JSON artifacts of
|
|
5
|
+
* a workspace (results, closure register, change review, proof run, fix packets, release gate). Integrity checks
|
|
6
|
+
* run first and failures are shown as a banner. No figure is invented: absent artifacts show as "not run", and a
|
|
7
|
+
* gate without executed evidence shows UNKNOWN / INSUFFICIENT EVIDENCE, never green.
|
|
8
|
+
* Views: Release gate, Coverage & phases, Findings, Closure, Risk & approvals, Tests & verification, Blueprint,
|
|
9
|
+
* Agent export. Inline scripts are allowed only by hash (CSP); no external requests.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { html } from "../reporting/escape.js";
|
|
14
|
+
import { PILLARS } from "../types.js";
|
|
15
|
+
import { checkResults } from "../validation/integrity.js";
|
|
16
|
+
import { verifyLog } from "./evidence-log.js";
|
|
17
|
+
import { checkRegister } from "./register.js";
|
|
18
|
+
import { mermaidViews } from "./render-md.js";
|
|
19
|
+
import { daysUntil } from "./util.js";
|
|
20
|
+
const ASSETS = new URL("../../dashboard/", import.meta.url);
|
|
21
|
+
const chip = (text, tone) => `<span class="chip ${tone}"><span class="dot" aria-hidden="true"></span>${html(text)}</span>`;
|
|
22
|
+
const TONE = {
|
|
23
|
+
GO: "good",
|
|
24
|
+
CONDITIONAL_GO: "warning",
|
|
25
|
+
NO_GO: "critical",
|
|
26
|
+
UNKNOWN: "unknown",
|
|
27
|
+
PASS: "good",
|
|
28
|
+
CONDITIONAL: "warning",
|
|
29
|
+
BLOCK: "critical",
|
|
30
|
+
NOT_EVALUATED: "unknown",
|
|
31
|
+
FAIL: "critical",
|
|
32
|
+
PARTIAL: "warning",
|
|
33
|
+
NOT_TESTED: "unknown",
|
|
34
|
+
BLOCKED: "unknown",
|
|
35
|
+
NOT_APPLICABLE: "neutral",
|
|
36
|
+
INCONCLUSIVE: "unknown",
|
|
37
|
+
COMPLETED: "good",
|
|
38
|
+
NOT_STARTED: "unknown",
|
|
39
|
+
NOT_SUPPORTED: "neutral",
|
|
40
|
+
CONFIRMED: "critical",
|
|
41
|
+
CORROBORATED: "serious",
|
|
42
|
+
REPRODUCED: "critical",
|
|
43
|
+
EVIDENCE_COLLECTED: "warning",
|
|
44
|
+
SUSPECTED: "unknown",
|
|
45
|
+
NEEDS_HUMAN_TRIAGE: "unknown",
|
|
46
|
+
DISPROVEN: "neutral",
|
|
47
|
+
OPEN: "serious",
|
|
48
|
+
ACKNOWLEDGED: "serious",
|
|
49
|
+
IN_PROGRESS: "warning",
|
|
50
|
+
FIX_PROPOSED: "warning",
|
|
51
|
+
RETEST_REQUIRED: "warning",
|
|
52
|
+
VERIFIED_FIXED: "good",
|
|
53
|
+
CLOSED: "good",
|
|
54
|
+
REJECTED_FALSE_POSITIVE: "neutral",
|
|
55
|
+
ACCEPTED_RISK: "warning",
|
|
56
|
+
DEFERRED: "neutral",
|
|
57
|
+
REGRESSED: "critical",
|
|
58
|
+
VALID: "good",
|
|
59
|
+
STALE: "warning",
|
|
60
|
+
ACTIVE: "warning",
|
|
61
|
+
REVIEW_DUE: "serious",
|
|
62
|
+
EXPIRED: "critical",
|
|
63
|
+
READY: "good",
|
|
64
|
+
READY_WITH_CONDITIONS: "warning",
|
|
65
|
+
NOT_READY: "critical",
|
|
66
|
+
LOW: "good",
|
|
67
|
+
MEDIUM: "warning",
|
|
68
|
+
HIGH: "critical",
|
|
69
|
+
UNDETERMINED: "unknown",
|
|
70
|
+
P0: "critical",
|
|
71
|
+
P1: "serious",
|
|
72
|
+
P2: "warning",
|
|
73
|
+
P3: "neutral",
|
|
74
|
+
AVAILABLE: "good",
|
|
75
|
+
};
|
|
76
|
+
const st = (s) => chip(s.replace(/_/g, " "), TONE[s] ?? "neutral");
|
|
77
|
+
const pct = (n) => `${Math.round(n * 100)}%`;
|
|
78
|
+
const LABEL = { customer: "Customer", technology: "Technology", trust: "Trust" };
|
|
79
|
+
const tbl = (head, rows, id) => `<div class="scroll"><table${id ? ` id="${id}"` : ""}><thead><tr>${head.map((h) => `<th>${html(h)}</th>`).join("")}</tr></thead><tbody>${rows.join("") || `<tr><td colspan="${head.length}" class="muted">None.</td></tr>`}</tbody></table></div>`;
|
|
80
|
+
const kpi = (label, value) => `<div class="card kpi"><div class="label">${html(label)}</div><div class="value">${html(value)}</div></div>`;
|
|
81
|
+
/** Agent export: fix packets with references, never evidence excerpts. */
|
|
82
|
+
export function agentExport(b) {
|
|
83
|
+
return {
|
|
84
|
+
kind: "product-check-agent-export",
|
|
85
|
+
note: "Proposals only. Apply with permission; a fix is not verified until product-check verify passes at the exact fix commit; closure needs signed human approval.",
|
|
86
|
+
target: b.gate.target,
|
|
87
|
+
head_sha: b.gate.head_sha,
|
|
88
|
+
release_decision: b.gate.decision,
|
|
89
|
+
packets: b.packets.map((p) => ({
|
|
90
|
+
fix_id: p.fix_id,
|
|
91
|
+
finding_ids: p.finding_ids,
|
|
92
|
+
severity: p.severity,
|
|
93
|
+
risk_class: p.risk_class,
|
|
94
|
+
title: p.title,
|
|
95
|
+
locations: p.traceability.locations,
|
|
96
|
+
root_cause: p.root_cause,
|
|
97
|
+
proposed_change: p.proposed_change,
|
|
98
|
+
constraints: p.constraints,
|
|
99
|
+
tests_required: p.tests_required,
|
|
100
|
+
negative_cases: p.negative_cases,
|
|
101
|
+
acceptance_conditions: p.acceptance_conditions,
|
|
102
|
+
scope_guard: p.scope_guard,
|
|
103
|
+
rollback_plan: p.rollback_plan,
|
|
104
|
+
evidence_refs: p.evidence_refs,
|
|
105
|
+
})),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Renders the dashboard. */
|
|
109
|
+
export function renderAssuranceDashboard(b, options) {
|
|
110
|
+
const { results: r, register: reg, review, gate } = b;
|
|
111
|
+
const css = readFileSync(new URL("dashboard.css", ASSETS), "utf8") +
|
|
112
|
+
readFileSync(new URL("assurance.css", ASSETS), "utf8");
|
|
113
|
+
const js = `${readFileSync(new URL("dashboard.js", ASSETS), "utf8")}\n${readFileSync(new URL("assurance.js", ASSETS), "utf8")}`;
|
|
114
|
+
const hash = createHash("sha256").update(js).digest("base64");
|
|
115
|
+
const problems = [
|
|
116
|
+
...checkResults(r).errors.map((e) => `results: ${e}`),
|
|
117
|
+
...(reg ? checkRegister(reg).map((e) => `closure register: ${e}`) : []),
|
|
118
|
+
...verifyLog(b.log).map((e) => `evidence log: ${e}`),
|
|
119
|
+
];
|
|
120
|
+
const banners = problems.length
|
|
121
|
+
? `<div class="banner error" role="alert"><strong>Integrity check failed.</strong> These artifacts were modified or are inconsistent; figures below cannot be trusted.<ul class="small">${problems
|
|
122
|
+
.slice(0, 20)
|
|
123
|
+
.map((p) => `<li>${html(p)}</li>`)
|
|
124
|
+
.join("")}</ul></div>`
|
|
125
|
+
: "";
|
|
126
|
+
const demo = r.run.demo
|
|
127
|
+
? `<div class="banner demo"><strong>DEMO DATA.</strong> Synthetic target.</div>`
|
|
128
|
+
: "";
|
|
129
|
+
// Release gate view ---------------------------------------------------------------------------------------------
|
|
130
|
+
const insufficient = gate.decision === "UNKNOWN"
|
|
131
|
+
? `<div class="insufficient"><strong>UNKNOWN / INSUFFICIENT EVIDENCE.</strong> ${html(gate.blocking_reasons[0] ?? "")}</div>`
|
|
132
|
+
: "";
|
|
133
|
+
const pillars = PILLARS.map((p) => {
|
|
134
|
+
const s = r.scorecard.pillars[p];
|
|
135
|
+
return `<div class="card"><h3>${LABEL[p]}</h3><div class="dial-value">${s.score === null ? "Not scored" : `${s.score}/100`}</div><div class="small">Coverage ${pct(s.coverage)} · confidence ${html(s.confidence)}</div><div class="meter" aria-hidden="true"><span style="width:${Math.round(s.coverage * 100)}%"></span></div>${s.state === "INSUFFICIENT_EVIDENCE" ? chip("Insufficient evidence", "unknown") : ""}</div>`;
|
|
136
|
+
}).join("");
|
|
137
|
+
const exec = `<section class="view" id="gate" aria-labelledby="h-gate"><h2 id="h-gate">Release gate</h2>
|
|
138
|
+
<div class="hero"><div><div class="small muted">Release decision</div><div class="decision tone-${TONE[gate.decision] ?? "neutral"}">${html(gate.decision.replace(/_/g, " "))}</div><div class="small muted">Head ${html(gate.head_sha?.slice(0, 12) ?? "unknown")} · evaluated ${html(gate.evaluated_at)}</div></div>
|
|
139
|
+
<div><div>Baseline product verdict: ${st(gate.product_verdict)}</div><div>Current change gate: ${gate.change_gate ? st(gate.change_gate) : chip("no change review", "neutral")}</div></div></div>
|
|
140
|
+
${insufficient}
|
|
141
|
+
<ul class="reasons">${gate.blocking_reasons.map((x) => `<li>${html(x)}</li>`).join("")}</ul>
|
|
142
|
+
<div class="grid three">${pillars}</div>
|
|
143
|
+
<div class="grid kpis">${kpi("Open P0", String(gate.open.P0))}${kpi("Open P1", String(gate.open.P1))}${kpi("Open P2", String(gate.open.P2))}${kpi("Executed checks", `${gate.executed_checks.passed}/${gate.executed_checks.count} passed`)}${kpi("Valid approvals", `${gate.approvals.valid}/${gate.approvals.required}`)}${kpi("Accepted risks", String(gate.accepted_risks.length))}</div>
|
|
144
|
+
<h3>Release conditions</h3>${tbl(["Condition", "Status", "Detail"], gate.conditions.map((c) => `<tr><td>${html(c.id)} ${html(c.title)}</td><td>${st(c.status)}</td><td class="small">${html(c.detail)}</td></tr>`))}
|
|
145
|
+
<p class="bounded">${html(r.verdict.bounded_by)}</p></section>`;
|
|
146
|
+
// Coverage & phases -----------------------------------------------------------------------------------------------
|
|
147
|
+
const cov = `<section class="view" id="coverage" aria-labelledby="h-cov"><h2 id="h-cov">Coverage & phases</h2>
|
|
148
|
+
<p>Lifecycle tier <strong>${html(r.run.tier)}</strong> (applied ${html(r.run.tiers_applied.join(", "))}). Execution phases VC-P0…P7 describe what was actually run and are separate from lifecycle tiers.</p>
|
|
149
|
+
<div class="phases">${gate.phases.map((p) => `<div class="card"><h4>${html(p.phase)} ${html(p.name)}</h4>${st(p.status)}<p class="small">${html(p.evidence.join(" "))}</p></div>`).join("")}</div>
|
|
150
|
+
<h3>Points of presence: inspected vs blocked</h3>${tbl(["PoP", "Category", "Status", "Relevant", "Limitations"], r.pop_coverage.pops.map((p) => `<tr><td>${html(p.name)}</td><td>${html(p.category)}</td><td>${st(p.status)}</td><td>${p.relevant ? "yes" : "no"}</td><td class="small">${html(p.limitations.join(" "))}</td></tr>`))}
|
|
151
|
+
<h3>Disclosed gaps</h3><ul class="small">${gate.coverage.gaps.map((g) => `<li>${html(g)}</li>`).join("") || "<li>None recorded.</li>"}</ul></section>`;
|
|
152
|
+
// Findings ------------------------------------------------------------------------------------------------------
|
|
153
|
+
const entries = reg?.entries ?? [];
|
|
154
|
+
const opened = (e) => e.transitions[0]?.at ?? reg?.created_at ?? b.now.toISOString();
|
|
155
|
+
const owner = (e) => {
|
|
156
|
+
const acc = reg?.risk_acceptances.find((a) => a.acceptance_id === e.acceptance_id);
|
|
157
|
+
const fix = reg?.fixes.find((f) => f.fix_id === e.fix_ids.at(-1));
|
|
158
|
+
return (acc?.owner ??
|
|
159
|
+
fix?.implemented_by.id ??
|
|
160
|
+
e.transitions.filter((t) => t.actor.type !== "system").at(-1)?.actor.id ??
|
|
161
|
+
"unassigned");
|
|
162
|
+
};
|
|
163
|
+
const findingRows = entries.map((e) => `<tr data-sev="${e.severity}" data-ver="${e.verification.state}" data-state="${e.state}"><td><code>${html(e.finding_id)}</code></td><td>${st(e.severity)}</td><td>${st(e.verification.state)}</td><td>${st(e.state)}</td><td>${html(e.source)}</td><td><code>${html(e.rule_or_control)}</code></td><td class="small"><code>${html(e.location)}</code></td><td>${html(owner(e))}</td><td class="num">${Math.max(0, -daysUntil(opened(e).slice(0, 10), b.now) - 1)}d</td><td>${html(e.title)}</td></tr>`);
|
|
164
|
+
const opts = (key, values) => `<label>${html(key)} <select data-key="${key}"><option value="">all</option>${values.map((v) => `<option value="${html(v.toLowerCase())}">${html(v)}</option>`).join("")}</select></label>`;
|
|
165
|
+
const triage = entries.filter((e) => e.verification.state === "NEEDS_HUMAN_TRIAGE" || e.verification.state === "SUSPECTED");
|
|
166
|
+
const fp = new Map();
|
|
167
|
+
for (const e of entries.filter((x) => x.state === "REJECTED_FALSE_POSITIVE"))
|
|
168
|
+
fp.set(e.rule_or_control, (fp.get(e.rule_or_control) ?? 0) + 1);
|
|
169
|
+
const findings = `<section class="view" id="findings" aria-labelledby="h-f"><h2 id="h-f">Findings</h2>
|
|
170
|
+
${reg ? "" : `<p class="muted">No closure register yet (product-check register). Showing nothing rather than guessing.</p>`}
|
|
171
|
+
<div class="filters" data-filter-table="tbl-findings">${opts("sev", ["P0", "P1", "P2", "P3"])}${opts("ver", ["CONFIRMED", "CORROBORATED", "REPRODUCED", "EVIDENCE_COLLECTED", "NEEDS_HUMAN_TRIAGE", "SUSPECTED"])}${opts("state", ["OPEN", "ACKNOWLEDGED", "IN_PROGRESS", "RETEST_REQUIRED", "VERIFIED_FIXED", "CLOSED", "ACCEPTED_RISK", "REGRESSED", "REJECTED_FALSE_POSITIVE"])}<label>Search <input data-key="text" type="search"></label> <span class="small muted" data-count></span></div>
|
|
172
|
+
${tbl(["Finding", "Sev", "Verification", "State", "Source", "Control/rule", "Where", "Owner", "Age", "Title"], findingRows, "tbl-findings")}
|
|
173
|
+
<h3>Awaiting corroboration (not proven defects)</h3>${tbl(["Finding", "Sev", "Title", "Checks"], triage.map((e) => `<tr><td><code>${html(e.finding_id)}</code></td><td>${st(e.severity)}</td><td>${html(e.title)}</td><td class="small">${html(e.verification.checks.map((c) => `${c.check}: ${c.result}`).join("; "))}</td></tr>`))}
|
|
174
|
+
<h3>False-positive trend by rule</h3>${tbl(["Rule/control", "Rejected as false positive"], [...fp.entries()].map(([k, n]) => `<tr><td><code>${html(k)}</code></td><td class="num">${n}</td></tr>`))}
|
|
175
|
+
${review && review.disproven.length
|
|
176
|
+
? `<h3>Disproven reviewer claims (${review.disproven.length})</h3>${tbl(["ID", "Reviewer", "Claim", "Why"], review.disproven.map((f) => `<tr><td><code>${html(f.id)}</code></td><td>${html(f.reviewer.id)} (${html(f.reviewer.type)})</td><td>${html(f.title)}</td><td class="small">${html(f.verification.checks.map((c) => c.detail).join(" "))}</td></tr>`))}`
|
|
177
|
+
: ""}</section>`;
|
|
178
|
+
// Closure funnel ----------------------------------------------------------------------------------------------------
|
|
179
|
+
const count = (states) => entries.filter((e) => states.includes(e.state)).length;
|
|
180
|
+
const steps = [
|
|
181
|
+
["Open / acknowledged", count(["OPEN", "ACKNOWLEDGED"]), ""],
|
|
182
|
+
["Remediation", count(["IN_PROGRESS", "FIX_PROPOSED"]), ""],
|
|
183
|
+
["Retest required", count(["RETEST_REQUIRED"]), ""],
|
|
184
|
+
["Verified fixed", count(["VERIFIED_FIXED"]), "done"],
|
|
185
|
+
["Closed", count(["CLOSED"]), "done"],
|
|
186
|
+
["Accepted risk", count(["ACCEPTED_RISK"]), "side"],
|
|
187
|
+
["Deferred", count(["DEFERRED"]), "side"],
|
|
188
|
+
["Regressed", count(["REGRESSED"]), "side"],
|
|
189
|
+
["Rejected (false positive)", count(["REJECTED_FALSE_POSITIVE"]), "side"],
|
|
190
|
+
];
|
|
191
|
+
const max = Math.max(1, ...steps.map((s) => s[1]));
|
|
192
|
+
const verifiedCycles = entries
|
|
193
|
+
.filter((e) => e.state === "VERIFIED_FIXED" || e.state === "CLOSED")
|
|
194
|
+
.map((e) => e.cycles + 1);
|
|
195
|
+
const closure = `<section class="view" id="closure" aria-labelledby="h-c"><h2 id="h-c">Closure</h2>
|
|
196
|
+
<p class="small">A proposed fix is not a fix: findings move to VERIFIED_FIXED only through a passing verification at the exact fix commit, and to CLOSED only with signed human approval.</p>
|
|
197
|
+
<div class="funnel">${steps.map(([label, n, cls]) => `<div class="step ${cls}"><span>${html(label)}</span><div class="bar" role="img" aria-label="${html(label)}: ${n}"><span style="width:${Math.round((n / max) * 100)}%"></span></div><span class="num">${n}</span></div>`).join("")}</div>
|
|
198
|
+
<div class="grid kpis">${kpi("Verification runs", String(reg?.verification_runs.length ?? 0))}${kpi("Avg cycles to verified fix", verifiedCycles.length ? (verifiedCycles.reduce((a, c) => a + c, 0) / verifiedCycles.length).toFixed(1) : "—")}${kpi("Escalated", String(entries.filter((e) => e.escalated).length))}${kpi("Repeated unchanged (review)", String(review?.cycle.repeated_unchanged.length ?? 0))}${kpi("Disproven claims", String(review?.disproven.length ?? 0))}${kpi("Chain length", String(reg?.chain.length ?? 0))}</div>
|
|
199
|
+
<h3>Closures</h3>${tbl(["Closure", "Finding", "Actor", "At", "Verification", "Approvals"], entries
|
|
200
|
+
.filter((e) => e.closure)
|
|
201
|
+
.map((e) => `<tr><td><code>${html(e.closure?.closure_id ?? "")}</code></td><td><code>${html(e.finding_id)}</code></td><td>${html(e.closure?.actor.id ?? "")}</td><td>${html(e.closure?.at ?? "")}</td><td><code>${html(e.closure?.verification_id ?? "")}</code></td><td class="small">${html(e.closure?.approval_ids.join(", ") ?? "")}</td></tr>`))}</section>`;
|
|
202
|
+
// Risk & approvals ------------------------------------------------------------------------------------------------
|
|
203
|
+
const risk = `<section class="view" id="risk" aria-labelledby="h-r"><h2 id="h-r">Risk & approvals</h2>
|
|
204
|
+
${review
|
|
205
|
+
? `<p>Change ${html(review.change_ref)} ${html(review.base_sha?.slice(0, 12) ?? "?")} → ${html(review.head_sha?.slice(0, 12) ?? "worktree")}: risk ${st(review.risk.level)} (treated as ${html(review.risk.effective_level)}), gate ${st(review.gate.status)}, cycle ${review.cycle.number}/${review.cycle.max}${review.cycle.escalated ? ` ${chip("escalated", "critical")}` : ""}.</p><ul class="small">${review.risk.reasons.map((x) => `<li>${html(x)}</li>`).join("")}</ul>
|
|
206
|
+
<p>Required: ${review.approvals_required.humans} human approval(s)${review.approvals_required.domain_owners.length ? `, domain owners ${html(review.approvals_required.domain_owners.join(", "))}` : ""}. Waiver: ${html(review.approvals_required.waiver.reason)}</p>
|
|
207
|
+
${tbl(["Approval", "Approver", "Status", "Reason"], review.approvals.map((a) => `<tr><td><code>${html(a.approval_id)}</code></td><td>${html(a.approver_id)}</td><td>${st(a.status)}</td><td class="small">${html(a.reason)}</td></tr>`))}
|
|
208
|
+
<h3>Lanes</h3>${tbl(["Lane", "Status", "Findings", "Notes"], review.lanes.map((l) => `<tr><td>${html(l.lane)}</td><td>${st(l.status)}</td><td class="num">${l.findings}</td><td class="small">${html(l.notes.join(" "))}</td></tr>`))}`
|
|
209
|
+
: `<p class="muted">No change review (product-check review).</p>`}
|
|
210
|
+
<h3>Accepted risks and expiry</h3>${tbl(["Acceptance", "Finding", "Owner", "Scope", "Review", "Expires", "Status"], (reg?.risk_acceptances ?? []).map((a) => {
|
|
211
|
+
const g = gate.accepted_risks.find((x) => x.acceptance_id === a.acceptance_id);
|
|
212
|
+
return `<tr><td><code>${html(a.acceptance_id)}</code></td><td><code>${html(a.finding_id)}</code></td><td>${html(a.owner)}</td><td class="small">${html(a.scope)}</td><td>${html(a.review_date)}</td><td>${html(a.expires)}</td><td>${st(g?.status ?? "ACTIVE")}</td></tr>`;
|
|
213
|
+
}))}
|
|
214
|
+
<h3>Signed approvals on record</h3>${tbl(["Approval", "Approver", "Scope", "Reviewed commit", "Decision", "Signed"], (reg?.approvals ?? []).map((a) => `<tr><td><code>${html(a.approval_id)}</code></td><td>${html(a.approver.id)} (${html(a.approver.type)})</td><td>${html(`${a.scope.type}:${a.scope.ref}`)}</td><td><code>${html(a.reviewed_sha.slice(0, 12))}</code></td><td>${html(a.decision)}</td><td>${a.signature ? "Ed25519" : chip("unsigned", "critical")}</td></tr>`))}</section>`;
|
|
215
|
+
// Tests & verification -----------------------------------------------------------------------------------------
|
|
216
|
+
const execs = [
|
|
217
|
+
...(b.proof?.executions ?? []).map((e) => ({ run: b.proof?.verification_id ?? "", e })),
|
|
218
|
+
...(reg?.verification_runs ?? []).flatMap((v) => v.executions.map((e) => ({ run: v.verification_id, e }))),
|
|
219
|
+
];
|
|
220
|
+
const tests = `<section class="view" id="tests" aria-labelledby="h-t"><h2 id="h-t">Tests & verification</h2>
|
|
221
|
+
<h3>Author-side proof</h3>${b.proof ? `<p>${st(b.proof.result)} at <code>${html(b.proof.exact_sha?.slice(0, 12) ?? "?")}</code>${b.proof.worktree_clean === false ? " (dirty worktree)" : ""}. ${html(b.proof.reasons.join(" "))}</p>` : `<p class="muted">Not run.</p>`}
|
|
222
|
+
<h3>Command executions</h3>${tbl(["Run", "Command", "Purpose", "Status", "Exit", "Duration", "Commit", "Environment", "Output digest"], execs.map(({ run, e }) => `<tr><td><code>${html(run)}</code></td><td><code>${html(e.argv.join(" "))}</code></td><td>${html(e.purpose)}</td><td>${st(e.status)}</td><td class="num">${e.exit_code ?? "—"}</td><td class="num">${e.duration_ms} ms</td><td><code>${html(e.commit_sha?.slice(0, 12) ?? "—")}</code></td><td><code>${html(e.environment_id)}</code></td><td class="small"><code>${html(e.stdout_digest?.slice(0, 12) ?? "—")}</code>${e.reason ? ` ${html(e.reason)}` : ""}</td></tr>`))}
|
|
223
|
+
<h3>Verification runs</h3>${tbl(["Run", "Findings", "Fix", "Commit", "Result", "Checks", "Reasons"], (reg?.verification_runs ?? []).map((v) => `<tr><td><code>${html(v.verification_id)}</code></td><td>${html(v.finding_ids.join(", "))}</td><td>${html(v.fix_id ?? "—")}</td><td><code>${html(v.exact_sha?.slice(0, 12) ?? "—")}</code></td><td>${st(v.result)}</td><td class="small">${v.checks.map((c) => `${html(c.check_id)} ${st(c.status)}`).join("<br>")}</td><td class="small">${html(v.reasons.join(" "))}</td></tr>`))}
|
|
224
|
+
<h3>Regression corpus (incident-derived)</h3>${tbl(["Case", "Finding", "Origin", "Command", "Added"], (reg?.regression_corpus ?? []).map((c) => `<tr><td><code>${html(c.case_id)}</code></td><td><code>${html(c.finding_id)}</code></td><td>${html(c.origin)}</td><td><code>${html(c.command.argv.join(" "))}</code></td><td>${html(c.added_at)} by ${html(c.added_by.id)}</td></tr>`))}
|
|
225
|
+
<h3>Merge & rollout watch</h3><p>${chip("NOT SUPPORTED", "neutral")} Product Check does not simulate merge queues, run canaries or read live telemetry. Rollback plan: ${gate.rollback.declared ? html(gate.rollback.ref ?? "") : chip("not declared", "warning")}.</p></section>`;
|
|
226
|
+
// Blueprint ---------------------------------------------------------------------------------------------------------
|
|
227
|
+
const v = mermaidViews(r, b.packets, reg);
|
|
228
|
+
const bp = `<section class="view" id="blueprint" aria-labelledby="h-b"><h2 id="h-b">Blueprint</h2>
|
|
229
|
+
<p class="small">Mermaid sources (paste into any Mermaid viewer; this page loads nothing external). Inferred components are labelled.</p>
|
|
230
|
+
<h3>As-is architecture and gaps</h3><button class="copy" type="button" data-copy="mm-asis" hidden>Copy</button><pre id="mm-asis">${html(v.asIs)}</pre>
|
|
231
|
+
<h3>To-be architecture</h3><button class="copy" type="button" data-copy="mm-tobe" hidden>Copy</button><pre id="mm-tobe">${html(v.toBe)}</pre>
|
|
232
|
+
<h3>Improvement backlog (fix packets)</h3>${tbl(["Packet", "Sev", "Risk", "Status", "Where", "Proposed change", "Acceptance"], b.packets.map((p) => `<tr><td><code>${html(p.fix_id)}</code></td><td>${st(p.severity)}</td><td>${st(p.risk_class)}</td><td>${html(p.status)}</td><td class="small">${p.traceability.locations.map((l) => `<code>${html(l)}</code>`).join(" ")}</td><td class="small">${html(p.proposed_change)}</td><td class="small">${html(p.acceptance_conditions.slice(0, 3).join(" · "))}</td></tr>`))}</section>`;
|
|
233
|
+
const exportJson = JSON.stringify(agentExport(b), null, 2);
|
|
234
|
+
const exp = `<section class="view" id="export" aria-labelledby="h-x"><h2 id="h-x">Agent export</h2>
|
|
235
|
+
<p class="small">Fix packets for an engineering agent (for example CrewPE as an optional executor). Evidence is referenced by ID; excerpts, command output and credentials are not included.</p>
|
|
236
|
+
<button class="copy" type="button" data-copy="agent-export" hidden>Copy JSON</button><pre class="export" id="agent-export">${html(exportJson)}</pre></section>`;
|
|
237
|
+
return `<!doctype html>
|
|
238
|
+
<html lang="en">
|
|
239
|
+
<head>
|
|
240
|
+
<meta charset="utf-8">
|
|
241
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
242
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'sha256-${hash}'; img-src data:">
|
|
243
|
+
<title>Product Check assurance: ${html(r.contract.target.name)}</title>
|
|
244
|
+
<style>${css}</style>
|
|
245
|
+
</head>
|
|
246
|
+
<body>
|
|
247
|
+
<div class="wrap">
|
|
248
|
+
<header class="top"><div><h1>Product Check assurance: ${html(r.contract.target.name)}</h1><div class="meta">Release gate ${html(gate.gate_id)} · scan ${html(r.run.run_id)} · CT3 v3.2</div></div><button class="theme-toggle" type="button" hidden>Theme: auto</button></header>
|
|
249
|
+
${banners}${demo}
|
|
250
|
+
<nav class="tabs" aria-label="Dashboard views"><a href="#gate" aria-current="page">Release gate</a><a href="#coverage">Coverage & phases</a><a href="#findings">Findings</a><a href="#closure">Closure</a><a href="#risk">Risk & approvals</a><a href="#tests">Tests & verification</a><a href="#blueprint">Blueprint</a><a href="#export">Agent export</a></nav>
|
|
251
|
+
<main>
|
|
252
|
+
${exec}
|
|
253
|
+
${cov}
|
|
254
|
+
${findings}
|
|
255
|
+
${closure}
|
|
256
|
+
${risk}
|
|
257
|
+
${tests}
|
|
258
|
+
${bp}
|
|
259
|
+
${exp}
|
|
260
|
+
</main>
|
|
261
|
+
<footer>Generated from the workspace JSON (product-check-results.json, closure-register.json, change review, proof run, fix-packets/, release-gate.json and review-evidence.jsonl); every figure is derived from those files. Evidence-bounded assurance, not a penetration test, certification or guarantee.${options.branding ? " Product Check is an EnWithAI Engineering Commons tool." : ""}</footer>
|
|
262
|
+
</div>
|
|
263
|
+
<script>${js}</script>
|
|
264
|
+
</body>
|
|
265
|
+
</html>
|
|
266
|
+
`;
|
|
267
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified diff parsing (output of `git diff`): files, statuses, renames, binary markers and every added/removed
|
|
3
|
+
* line with its new/old line number. Pure; no git process.
|
|
4
|
+
*/
|
|
5
|
+
export interface DiffLine {
|
|
6
|
+
kind: "add" | "del";
|
|
7
|
+
/** Line number in the new file (add) or old file (del). */
|
|
8
|
+
line: number;
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DiffFile {
|
|
12
|
+
path: string;
|
|
13
|
+
previous_path: string | null;
|
|
14
|
+
status: "added" | "modified" | "deleted" | "renamed" | "binary";
|
|
15
|
+
lines: DiffLine[];
|
|
16
|
+
additions: number;
|
|
17
|
+
deletions: number;
|
|
18
|
+
}
|
|
19
|
+
/** Parses a unified diff. */
|
|
20
|
+
export declare function parseUnifiedDiff(text: string): DiffFile[];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 EnWithAI Technologies
|
|
3
|
+
/**
|
|
4
|
+
* Unified diff parsing (output of `git diff`): files, statuses, renames, binary markers and every added/removed
|
|
5
|
+
* line with its new/old line number. Pure; no git process.
|
|
6
|
+
*/
|
|
7
|
+
const unquote = (p) => {
|
|
8
|
+
const s = p.trim();
|
|
9
|
+
if (s.startsWith('"') && s.endsWith('"')) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(s);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return s.slice(1, -1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return s;
|
|
18
|
+
};
|
|
19
|
+
const strip = (p) => {
|
|
20
|
+
const s = unquote(p);
|
|
21
|
+
if (s === "/dev/null")
|
|
22
|
+
return null;
|
|
23
|
+
return s.replace(/^[ab]\//, "");
|
|
24
|
+
};
|
|
25
|
+
/** Parses a unified diff. */
|
|
26
|
+
export function parseUnifiedDiff(text) {
|
|
27
|
+
const files = [];
|
|
28
|
+
let cur = null;
|
|
29
|
+
let oldLine = 0;
|
|
30
|
+
let newLine = 0;
|
|
31
|
+
let inHunk = false;
|
|
32
|
+
const finish = () => {
|
|
33
|
+
if (cur)
|
|
34
|
+
files.push(cur);
|
|
35
|
+
cur = null;
|
|
36
|
+
inHunk = false;
|
|
37
|
+
};
|
|
38
|
+
for (const raw of text.split("\n")) {
|
|
39
|
+
if (raw.startsWith("diff --git ")) {
|
|
40
|
+
finish();
|
|
41
|
+
const m = /^diff --git (?:"?a\/)(.+?)"? (?:"?b\/)(.+?)"?$/.exec(raw);
|
|
42
|
+
const path = m?.[2] ?? raw.slice(11);
|
|
43
|
+
cur = { path, previous_path: null, status: "modified", lines: [], additions: 0, deletions: 0 };
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!cur)
|
|
47
|
+
continue;
|
|
48
|
+
const c = cur;
|
|
49
|
+
if (!inHunk || raw.startsWith("@@")) {
|
|
50
|
+
if (raw.startsWith("new file mode"))
|
|
51
|
+
c.status = "added";
|
|
52
|
+
else if (raw.startsWith("deleted file mode"))
|
|
53
|
+
c.status = "deleted";
|
|
54
|
+
else if (raw.startsWith("rename from ")) {
|
|
55
|
+
c.previous_path = unquote(raw.slice(12));
|
|
56
|
+
c.status = "renamed";
|
|
57
|
+
}
|
|
58
|
+
else if (raw.startsWith("rename to "))
|
|
59
|
+
c.path = unquote(raw.slice(10));
|
|
60
|
+
else if (raw.startsWith("Binary files ") || raw.startsWith("GIT binary patch"))
|
|
61
|
+
c.status = "binary";
|
|
62
|
+
else if (raw.startsWith("--- ")) {
|
|
63
|
+
const p = strip(raw.slice(4));
|
|
64
|
+
if (p === null)
|
|
65
|
+
c.status = "added";
|
|
66
|
+
}
|
|
67
|
+
else if (raw.startsWith("+++ ")) {
|
|
68
|
+
const p = strip(raw.slice(4));
|
|
69
|
+
if (p === null)
|
|
70
|
+
c.status = "deleted";
|
|
71
|
+
else
|
|
72
|
+
c.path = p;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const h = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
|
|
76
|
+
if (h) {
|
|
77
|
+
oldLine = Number(h[1]);
|
|
78
|
+
newLine = Number(h[2]);
|
|
79
|
+
inHunk = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!inHunk)
|
|
83
|
+
continue;
|
|
84
|
+
if (raw.startsWith("+")) {
|
|
85
|
+
c.lines.push({ kind: "add", line: newLine++, text: raw.slice(1) });
|
|
86
|
+
c.additions++;
|
|
87
|
+
}
|
|
88
|
+
else if (raw.startsWith("-")) {
|
|
89
|
+
c.lines.push({ kind: "del", line: oldLine++, text: raw.slice(1) });
|
|
90
|
+
c.deletions++;
|
|
91
|
+
}
|
|
92
|
+
else if (raw.startsWith(" ") || raw === "") {
|
|
93
|
+
oldLine++;
|
|
94
|
+
newLine++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
finish();
|
|
98
|
+
return files;
|
|
99
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type EvidenceRecord } from "./types.js";
|
|
2
|
+
/** Fields the caller provides for a new record. */
|
|
3
|
+
export type EvidenceInput = Pick<EvidenceRecord, "type" | "subject" | "commit_sha" | "environment_id" | "summary" | "locator" | "line" | "payload">;
|
|
4
|
+
/** Parses a log; malformed lines are input errors. */
|
|
5
|
+
export declare function readLog(path: string): EvidenceRecord[];
|
|
6
|
+
/** Checks the chain: sequence numbers, payload digests, record digests and links. Returns the problems found. */
|
|
7
|
+
export declare function verifyLog(records: EvidenceRecord[]): string[];
|
|
8
|
+
/** Append-only writer over one log file. */
|
|
9
|
+
export declare class EvidenceLog {
|
|
10
|
+
private readonly path;
|
|
11
|
+
private readonly now;
|
|
12
|
+
private readonly records;
|
|
13
|
+
constructor(path: string, now: () => Date);
|
|
14
|
+
/** Appends a record and returns its evidence ID. */
|
|
15
|
+
append(input: EvidenceInput): string;
|
|
16
|
+
/** All records (read-only copy). */
|
|
17
|
+
all(): EvidenceRecord[];
|
|
18
|
+
/** IDs known to the log. */
|
|
19
|
+
has(id: string): boolean;
|
|
20
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 EnWithAI Technologies
|
|
3
|
+
/**
|
|
4
|
+
* review-evidence.jsonl: an append-only, hash-chained log of every piece of executed or observed evidence the
|
|
5
|
+
* assurance layer produces (command executions, diff hunks, re-reads, approvals, verifications, transitions).
|
|
6
|
+
* Each record carries the digest of its payload and of the previous record, so editing, removing or reordering a
|
|
7
|
+
* line is detected by `verifyLog`. Payloads are small and redacted; full command output is never stored.
|
|
8
|
+
*/
|
|
9
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
10
|
+
import { dirname } from "node:path";
|
|
11
|
+
import { inputError } from "../errors.js";
|
|
12
|
+
import { stableId } from "../util/hash.js";
|
|
13
|
+
import { redact } from "../util/redact.js";
|
|
14
|
+
import { ASSURANCE_SCHEMA_VERSION } from "./types.js";
|
|
15
|
+
import { digestOf } from "./util.js";
|
|
16
|
+
/** Parses a log; malformed lines are input errors. */
|
|
17
|
+
export function readLog(path) {
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
return [];
|
|
20
|
+
const lines = readFileSync(path, "utf8")
|
|
21
|
+
.split("\n")
|
|
22
|
+
.filter((l) => l.trim() !== "");
|
|
23
|
+
return lines.map((line, i) => {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(line);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw inputError(`${path}: line ${i + 1} is not JSON.`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Checks the chain: sequence numbers, payload digests, record digests and links. Returns the problems found. */
|
|
33
|
+
export function verifyLog(records) {
|
|
34
|
+
const errors = [];
|
|
35
|
+
let prev = null;
|
|
36
|
+
records.forEach((r, i) => {
|
|
37
|
+
if (r.seq !== i + 1)
|
|
38
|
+
errors.push(`record ${i + 1}: seq is ${r.seq}`);
|
|
39
|
+
if (digestOf(r.payload) !== r.payload_digest)
|
|
40
|
+
errors.push(`record ${r.seq}: payload digest mismatch`);
|
|
41
|
+
if (r.prev_digest !== prev)
|
|
42
|
+
errors.push(`record ${r.seq}: broken link to the previous record`);
|
|
43
|
+
const { digest, ...rest } = r;
|
|
44
|
+
if (digestOf(rest) !== digest)
|
|
45
|
+
errors.push(`record ${r.seq}: record digest mismatch (edited)`);
|
|
46
|
+
prev = r.digest;
|
|
47
|
+
});
|
|
48
|
+
return errors;
|
|
49
|
+
}
|
|
50
|
+
/** Append-only writer over one log file. */
|
|
51
|
+
export class EvidenceLog {
|
|
52
|
+
path;
|
|
53
|
+
now;
|
|
54
|
+
records;
|
|
55
|
+
constructor(path, now) {
|
|
56
|
+
this.path = path;
|
|
57
|
+
this.now = now;
|
|
58
|
+
this.records = readLog(path);
|
|
59
|
+
const problems = verifyLog(this.records);
|
|
60
|
+
if (problems.length > 0)
|
|
61
|
+
throw inputError(`${path} failed its integrity check; refusing to append:\n ${problems.join("\n ")}`);
|
|
62
|
+
}
|
|
63
|
+
/** Appends a record and returns its evidence ID. */
|
|
64
|
+
append(input) {
|
|
65
|
+
const seq = this.records.length + 1;
|
|
66
|
+
const prev = this.records.at(-1)?.digest ?? null;
|
|
67
|
+
const payload = JSON.parse(redact(JSON.stringify(input.payload)));
|
|
68
|
+
const payloadDigest = digestOf(payload);
|
|
69
|
+
const base = {
|
|
70
|
+
schema_version: ASSURANCE_SCHEMA_VERSION,
|
|
71
|
+
kind: "evidence-record",
|
|
72
|
+
seq,
|
|
73
|
+
evidence_id: stableId("EVR", [input.type, input.subject, payloadDigest, prev], 12),
|
|
74
|
+
recorded_at: this.now().toISOString(),
|
|
75
|
+
type: input.type,
|
|
76
|
+
subject: input.subject,
|
|
77
|
+
commit_sha: input.commit_sha,
|
|
78
|
+
environment_id: input.environment_id,
|
|
79
|
+
summary: redact(input.summary).slice(0, 500),
|
|
80
|
+
locator: input.locator,
|
|
81
|
+
line: input.line,
|
|
82
|
+
payload_digest: payloadDigest,
|
|
83
|
+
payload,
|
|
84
|
+
prev_digest: prev,
|
|
85
|
+
};
|
|
86
|
+
const record = { ...base, digest: digestOf(base) };
|
|
87
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
88
|
+
appendFileSync(this.path, `${JSON.stringify(record)}\n`);
|
|
89
|
+
this.records.push(record);
|
|
90
|
+
return record.evidence_id;
|
|
91
|
+
}
|
|
92
|
+
/** All records (read-only copy). */
|
|
93
|
+
all() {
|
|
94
|
+
return [...this.records];
|
|
95
|
+
}
|
|
96
|
+
/** IDs known to the log. */
|
|
97
|
+
has(id) {
|
|
98
|
+
return this.records.some((r) => r.evidence_id === id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-ready fix packets (VC-P4) for verified, high-priority findings. Each packet traces to source lines and/or
|
|
3
|
+
* runtime paths through evidence, states the root-cause hypothesis and its certainty, the smallest systemic change,
|
|
4
|
+
* acceptance conditions, regression and negative tests, a change-scope guard and a rollback plan. Test commands are
|
|
5
|
+
* never invented: a command appears only when the operator's policy declares it; reproduction commands are declared
|
|
6
|
+
* by the implementer when recording the fix. Packets are proposals: applying one needs permission, and a packet's
|
|
7
|
+
* implementation is not a fix until verify passes.
|
|
8
|
+
*/
|
|
9
|
+
import type { Results } from "../types.js";
|
|
10
|
+
import { type AssurancePolicy, type ChangeReview, type ClosureRegister, type FixPacket } from "./types.js";
|
|
11
|
+
/** Packets for active, verified P0–P2 entries of the register. */
|
|
12
|
+
export declare function buildFixPackets(reg: ClosureRegister, results: Results | null, reviews: ChangeReview[], policy: AssurancePolicy): FixPacket[];
|