feat: deliver policy-driven Git Validator 2.0
This commit is contained in:
@@ -75,8 +75,14 @@ async function handleShellActions(event, target, action, repository) {
|
||||
} else if (action === "git-validator-repair") {
|
||||
const check = ui.gitValidation?.checks?.[Number(target.dataset.checkIndex)];
|
||||
if (!check?.fixAction) return;
|
||||
if (!check.safe && !confirm(check.confirmation || `Apply ${check.title}?`))
|
||||
let preview;
|
||||
try {
|
||||
preview = await window.forgeflow.gitValidatorPreviewRepair(repository.fullName, check);
|
||||
} catch (error) {
|
||||
showToast("Preview failed", error.message, "error");
|
||||
return;
|
||||
}
|
||||
if (!confirm(`${check.confirmation || `Apply ${check.title}?`}\n\nReviewable change:\n${preview.diff}\n\nNothing will be committed or pushed.`)) return;
|
||||
setLoading(true, `Repairing ${check.title}…`);
|
||||
try {
|
||||
await window.forgeflow.gitValidatorRepair(repository.fullName, check);
|
||||
@@ -90,27 +96,46 @@ async function handleShellActions(event, target, action, repository) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else if (action === "git-validator-repair-safe") {
|
||||
const checks = (ui.gitValidation?.checks || []).filter(
|
||||
(check) => check.fixAction && check.safe,
|
||||
);
|
||||
setLoading(true, `Applying ${checks.length} safe Git fixes…`);
|
||||
let repaired = 0;
|
||||
} else if (action === "git-validator-policy") {
|
||||
setLoading(true, "Applying assurance policy…");
|
||||
try {
|
||||
for (const check of checks) {
|
||||
await window.forgeflow.gitValidatorRepair(repository.fullName, check);
|
||||
repaired += 1;
|
||||
}
|
||||
await refreshRepositories(false, true);
|
||||
ui.gitValidation = await window.forgeflow.gitValidatorScan(
|
||||
repository.fullName,
|
||||
);
|
||||
showToast("Safe Git fixes applied", `${repaired} repaired.`, "success");
|
||||
await window.forgeflow.gitValidatorSetPolicy(repository.fullName, { id: target.value });
|
||||
ui.gitValidation = await window.forgeflow.gitValidatorScan(repository.fullName);
|
||||
showToast("Policy updated", ui.gitValidation.policy.label, "success");
|
||||
} catch (error) {
|
||||
showToast("Safe repair stopped", error.message, "error");
|
||||
showToast("Policy update failed", error.message, "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else if (action === "git-validator-export") {
|
||||
try {
|
||||
const exported = await window.forgeflow.gitValidatorExport(repository.fullName, target.dataset.format || "markdown");
|
||||
const url = URL.createObjectURL(new Blob([exported.content], { type: exported.mimeType }));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${repository.name || "repository"}-git-assurance.${exported.extension}`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast("Report exported", link.download, "success");
|
||||
} catch (error) {
|
||||
showToast("Export failed", error.message, "error");
|
||||
}
|
||||
} else if (action === "git-validator-suppress") {
|
||||
const check = ui.gitValidation?.checks?.[Number(target.dataset.checkIndex)];
|
||||
if (!check) return;
|
||||
const reason = prompt("Reason for this temporary exception (minimum 10 characters):", "Accepted temporarily while remediation is tracked.");
|
||||
if (!reason) return;
|
||||
const author = prompt("Exception owner:", ui.boot?.state?.gitea?.user?.login || "");
|
||||
if (!author) return;
|
||||
const ticket = prompt("Ticket reference (optional):", "") || "";
|
||||
const expiresAt = new Date(Date.now() + 7 * 86_400_000).toISOString();
|
||||
try {
|
||||
await window.forgeflow.gitValidatorSuppress(repository.fullName, { checkId: check.id, reason, author, ticket, expiresAt, scope: "repository", evidence: `${ui.gitValidation.commitSha || "unknown"}:${check.id}:${check.status}` });
|
||||
ui.gitValidation = await window.forgeflow.gitValidatorScan(repository.fullName);
|
||||
showToast("Exception documented", `Expires ${formatDate(expiresAt)}.`, "success");
|
||||
} catch (error) {
|
||||
showToast("Exception rejected", error.message, "error");
|
||||
}
|
||||
} else if (action === "toggle-favorite") {
|
||||
ui.boot.state = await window.forgeflow.favoriteRepository(
|
||||
repository.fullName,
|
||||
|
||||
@@ -55,6 +55,8 @@ app.addEventListener("change", async (event) => {
|
||||
} else if (event.target.id === "action-profile-select") {
|
||||
ui.selectedProfileId = event.target.value;
|
||||
render();
|
||||
} else if (event.target.id === "validator-policy") {
|
||||
await handleShellActions(event, event.target, "git-validator-policy", selectedRepository());
|
||||
} else if (event.target.id === "profile-provider") {
|
||||
ui.modal.provider = event.target.value;
|
||||
render();
|
||||
|
||||
@@ -466,6 +466,11 @@ function createMockDeploymentBridge(context) {
|
||||
checkedAt: iso(),
|
||||
score: 78,
|
||||
grade: "Good",
|
||||
policy: { id: "standard", label: "Standard", requiredScore: 70 },
|
||||
ready: true,
|
||||
commitSha: "8cbaf303aa3bb9b4023a7c89aa13fb70ce612847",
|
||||
trend: { newlyFound: ["working-tree"], resolved: ["editorconfig"], regressions: [], suppressions: [] },
|
||||
expiredSuppressions: [],
|
||||
summary: { passed: 7, warnings: 3, errors: 0, repairable: 2 },
|
||||
checks: [
|
||||
{
|
||||
@@ -566,6 +571,18 @@ function createMockDeploymentBridge(context) {
|
||||
],
|
||||
};
|
||||
},
|
||||
async gitValidatorSetPolicy(_fullName, policy) {
|
||||
return { id: policy.id, label: policy.id[0].toUpperCase() + policy.id.slice(1) };
|
||||
},
|
||||
async gitValidatorSuppress(_fullName, suppression) {
|
||||
return { ...suppression, id: `suppression-${Date.now()}`, createdAt: iso() };
|
||||
},
|
||||
async gitValidatorPreviewRepair(_fullName, check) {
|
||||
return { checkId: check.id, action: check.fixAction, files: [".git/config"], diff: "+ reviewed configuration change\n", remoteMutation: check.fixAction === "protect-default-branch" };
|
||||
},
|
||||
async gitValidatorExport(fullName, format) {
|
||||
return { extension: format === "markdown" ? "md" : format, mimeType: "text/plain", content: `# Git assurance — ${fullName}\n` };
|
||||
},
|
||||
async gitValidatorRepair() {
|
||||
await wait(180);
|
||||
return { repaired: true };
|
||||
|
||||
@@ -390,14 +390,12 @@ function renderGitValidator(repository) {
|
||||
return `<div class="validator-empty panel">${projectIllustration("diagnostics")}<div><div class="eyebrow">Repository assurance</div><h2>Validate Git best practices</h2><p>Inspect repository identity, branch governance, tracked secrets, file hygiene and safe local synchronization settings.</p><button class="button primary" data-action="git-validator-scan">${icon("shield")}Run Git Validator</button></div></div>`;
|
||||
const tone =
|
||||
report.score >= 90 ? "success" : report.score >= 70 ? "warning" : "danger";
|
||||
const safeFixes = report.checks.filter(
|
||||
(check) => check.fixAction && check.safe,
|
||||
);
|
||||
const groups = report.checks.reduce((grouped, check) => {
|
||||
(grouped[check.category] ||= []).push(check);
|
||||
return grouped;
|
||||
}, {});
|
||||
return `<div class="validator-page"><section class="validator-hero panel ${tone}"><div class="validator-score"><strong>${report.score}</strong><span>/ 100</span></div><div><div class="eyebrow">Git assurance score</div><h2>${escapeHtml(report.grade)}</h2><p>${report.summary.passed} passed · ${report.summary.warnings} recommendations · ${report.summary.errors} critical</p></div>${projectIllustration("diagnostics")}<div class="validator-actions"><button class="button" data-action="git-validator-scan">${icon("refresh")}Scan again</button>${safeFixes.length ? `<button class="button primary" data-action="git-validator-repair-safe">${icon("wrench")}Apply ${safeFixes.length} safe fix${safeFixes.length === 1 ? "" : "es"}</button>` : ""}</div></section><div class="validator-groups">${Object.entries(
|
||||
const trend = report.trend || {};
|
||||
return `<div class="validator-page"><section class="validator-hero panel ${tone}"><div class="validator-score"><strong>${report.score}</strong><span>/ 100</span></div><div><div class="eyebrow">${escapeHtml(report.policy?.label || "Standard")} policy · ${report.ready ? "release-ready" : "review required"}</div><h2>${escapeHtml(report.grade)}</h2><p>${report.summary.passed} passed · ${report.summary.warnings} recommendations · ${report.summary.errors} critical</p><p class="meta">${trend.newlyFound?.length || 0} new · ${trend.resolved?.length || 0} resolved · ${trend.regressions?.length || 0} regressions · ${report.expiredSuppressions?.length || 0} expired exceptions</p></div>${projectIllustration("diagnostics")}<div class="validator-actions"><label class="sr-only" for="validator-policy">Assurance policy</label><select id="validator-policy" class="select" data-action="git-validator-policy">${["minimal", "standard", "strict", "production"].map((policy) => `<option value="${policy}" ${report.policy?.id === policy ? "selected" : ""}>${policy[0].toUpperCase() + policy.slice(1)}</option>`).join("")}</select><button class="button" data-action="git-validator-scan">${icon("refresh")}Scan again</button><button class="button" data-action="git-validator-export" data-format="markdown">Export report</button></div></section><div class="validator-groups">${Object.entries(
|
||||
groups,
|
||||
)
|
||||
.map(
|
||||
@@ -405,7 +403,7 @@ function renderGitValidator(repository) {
|
||||
`<section class="panel validator-group"><div class="panel-header"><h3>${escapeHtml(category)}</h3><span class="meta">${checks.filter((check) => check.status === "pass").length}/${checks.length} passed</span></div><div class="validator-checks">${checks
|
||||
.map((check) => {
|
||||
const checkIndex = report.checks.indexOf(check);
|
||||
return `<article class="validator-check ${check.status}"><span class="validator-check-icon">${icon(check.status === "pass" ? "check" : check.status === "error" ? "error" : "warning")}</span><div><strong>${escapeHtml(check.title)}</strong><p>${escapeHtml(check.detail)}</p></div>${check.fixAction ? `<button class="button ${check.safe ? "" : "primary"}" data-action="git-validator-repair" data-check-index="${checkIndex}">${icon("wrench")}${check.safe ? "Fix safely" : "Review & fix"}</button>` : `<span class="status-pill ${check.status === "pass" ? "success" : check.status === "error" ? "danger" : "warning"}">${check.status === "pass" ? "Best practice" : "Review"}</span>`}</article>`;
|
||||
return `<article class="validator-check ${check.status}"><span class="validator-check-icon">${icon(check.status === "pass" ? "check" : check.status === "error" ? "error" : "warning")}</span><div><strong>${escapeHtml(check.title)}</strong><p>${escapeHtml(check.detail)}</p>${check.suppressed ? `<small>Suppressed until ${formatDate(check.suppression.expiresAt)} · ${escapeHtml(check.suppression.reason)}</small>` : check.expiredSuppression ? `<small>Exception expired; finding is active again.</small>` : ""}</div>${check.fixAction ? `<button class="button ${check.safe ? "" : "primary"}" data-action="git-validator-repair" data-check-index="${checkIndex}">${icon("wrench")}Preview fix</button>` : check.status !== "pass" && !check.suppressed ? `<button class="button" data-action="git-validator-suppress" data-check-index="${checkIndex}">Document exception</button>` : `<span class="status-pill ${check.suppressed ? "warning" : check.status === "pass" ? "success" : check.status === "error" ? "danger" : "warning"}">${check.suppressed ? "Suppressed" : check.status === "pass" ? "Best practice" : "Review"}</span>`}</article>`;
|
||||
})
|
||||
.join("")}</div></section>`,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user