diff --git a/app.js b/app.js index 0a3660b..986b1bd 100644 --- a/app.js +++ b/app.js @@ -4665,7 +4665,7 @@ function showPaywallExhausted(count, limit, options = {}) { const resultsView = document.getElementById('results-view') if (resultsView) resultsView.classList.remove('visible') - document.body.classList.remove("results-fullscreen") + document.body.classList.remove("results-fullscreen", "results-with-sidebar") const pipelineProgress = document.getElementById('pipeline-progress') if (pipelineProgress) pipelineProgress.classList.remove('visible') @@ -5601,7 +5601,7 @@ function showPipelineProgress() { if (readyState) readyState.classList.add("hidden"); if (resultsView) resultsView.classList.remove("visible"); - document.body.classList.remove("results-fullscreen"); + document.body.classList.remove("results-fullscreen", "results-with-sidebar"); if (progress) progress.classList.add("visible"); pipelineStartTime = Date.now(); @@ -5746,10 +5746,10 @@ function renderSummaryDetail(presentation) { const score = presentation.score; const scoreTone = score == null - ? presentation.status + ? "neutral" : score >= 80 ? "pass" : score >= 60 ? "warning" : "fail"; const scoreLabel = score == null - ? "Not scored" + ? "" : score >= 80 ? "Strong" : score >= 60 ? "Needs work" : "High risk"; const findings = presentation.findings.length ? ` @@ -5757,7 +5757,10 @@ function renderSummaryDetail(presentation) { ${presentation.findings.map((finding) => `
  • ${reviewStatusIcon(finding.severity)} - ${escapeHtml(finding.message)} + + ${escapeHtml(finding.message)} + ${finding.suggestion ? `${escapeHtml(finding.suggestion)}` : ""} +
  • `).join("")} @@ -5791,7 +5794,7 @@ function renderSummaryDetail(presentation) { Score ${score == null ? "—" : escapeHtml(score)} ${score == null ? "Not scored" : "out of 100"} - ${escapeHtml(scoreLabel)} + ${scoreLabel ? `${escapeHtml(scoreLabel)}` : ""}
    Is the generated code correct? @@ -5815,9 +5818,23 @@ function renderSelectedArtifactReview(presentation) { ) || presentation.artifacts[0]; if (!selectedArtifact) return ""; + const emptyFindingState = { + pass: { + icon: "pass", + message: "No file-specific issues were found.", + }, + warning: { + icon: "warning", + message: "This file needs attention, but Code Review did not return a specific finding.", + }, + fail: { + icon: "fail", + message: "This file is blocked, but Code Review did not return a specific finding.", + }, + }[selectedArtifact.status]; const findings = selectedArtifact.findings.length ? selectedArtifact.findings.map(renderFinding).join("") - : `
    ${reviewStatusIcon("pass")} No file-specific findings were returned.
    `; + : `
    ${reviewStatusIcon(emptyFindingState.icon)} ${escapeHtml(emptyFindingState.message)}
    `; const dependencies = selectedArtifact.dependencies?.length ? selectedArtifact.dependencies.map((dependency) => `
  • ${escapeHtml(dependency.name)}${dependency.version ? ` ${escapeHtml(dependency.version)}` : ""}${dependency.reason ? `

    ${escapeHtml(dependency.reason)}

    ` : ""}
  • @@ -5936,10 +5953,7 @@ function renderBundleControls(presentation) { function updateSelectedArtifactPanels() { document.body.classList.add("results-fullscreen"); - document.body.classList.toggle( - "results-with-sidebar", - IS_DEV && new URLSearchParams(window.location.search).get("debugSidebar") === "1", - ); + document.body.classList.add("results-with-sidebar"); const resultsView = document.getElementById("results-view"); const codeOutput = document.getElementById("results-code-output"); const auditOutput = document.getElementById("results-audit-output"); @@ -5974,6 +5988,8 @@ function showResultsView(codeContent, auditContent) { } pipelineState.resultsViewMode = "summary"; document.body.classList.add("results-fullscreen"); + const legacyReviewOutput = document.getElementById("step3-output"); + if (legacyReviewOutput) legacyReviewOutput.textContent = ""; updateSelectedArtifactPanels(); updateDeployButtonVisibility(); diff --git a/index.html b/index.html index 2cce9f2..3a5eacb 100644 --- a/index.html +++ b/index.html @@ -770,6 +770,14 @@ font-size: 12px; font-weight: 700; } + .review-empty-warning { + color: #92400e; + background: #fffbeb; + } + .review-empty-fail { + color: #b91c1c; + background: #fef2f2; + } .review-muted { color: #94a3b8; font-size: 11px; } .deploy-order-list, .relationship-list, @@ -1068,11 +1076,11 @@ .file-relationship-list li { color: #475569; font-size: 11px; } .file-relationship-list li p { margin-top: 3px; color: #94a3b8; } body.results-fullscreen .sidebar { - display: none; + display: flex; } body.results-fullscreen .main-content { display: block; - width: 100vw; + width: auto; height: 100dvh; padding: 0; } @@ -1086,13 +1094,13 @@ } body.results-fullscreen.results-with-sidebar .main-content { width: auto; - height: 100vh; - padding: 36px; + height: 100dvh; + padding: 0; } body.results-fullscreen.results-with-sidebar .main-stage { - inset: 28px; - border-radius: 14px; - box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 6px 24px rgba(0,0,0,0.05); + inset: 0; + border-radius: 0; + box-shadow: none; } body.results-fullscreen .results-header { padding: 14px 28px; @@ -1149,19 +1157,25 @@ letter-spacing: -0.025em; } body.results-fullscreen .results-summary-detail { - flex: 0 1 auto; - max-height: 340px; - padding: 26px 36px 24px; + flex: 1 1 auto; + max-height: none; + padding: 28px 36px; overflow-y: auto; border-bottom: 1px solid #e5e7eb; + background: #f8fafc; } .review-summary { display: grid; grid-template-columns: minmax(0, 1fr) 184px; - gap: 48px; + gap: 36px; width: 100%; - max-width: 1280px; + max-width: none; margin: 0 auto; + padding: 24px; + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #fff; + box-shadow: 0 8px 24px -20px rgba(15, 23, 42, 0.28); } .review-summary-copy { min-width: 0; } .results-file-count { @@ -1200,6 +1214,7 @@ .review-score-pass { color: #166534; border-color: #86efac; background: #f0fdf4; } .review-score-warning { color: #9a3412; border-color: #fdba74; background: #fff7ed; } .review-score-fail { color: #991b1b; border-color: #fca5a5; background: #fef2f2; } + .review-score-neutral { color: #475569; border-color: #cbd5e1; background: #f8fafc; } .review-score-label { font-size: 11px; font-weight: 800; @@ -1274,6 +1289,19 @@ font-size: 14px; line-height: 1.45; } + .summary-findings li > span { + display: grid; + gap: 3px; + } + .summary-findings li strong { + color: #334155; + font-weight: 700; + } + .summary-findings li small { + color: #64748b; + font-size: 12px; + line-height: 1.45; + } .summary-findings .review-status-icon { margin-top: 2px; } .review-score-feedback { width: 100%; diff --git a/src/pipelineContracts.js b/src/pipelineContracts.js index f9bab48..403512c 100644 --- a/src/pipelineContracts.js +++ b/src/pipelineContracts.js @@ -38,6 +38,11 @@ export function buildReviewPrompt(generatedBundle) { return stringifyPipelinePayload({ task: "review_bundle", generatedBundle: parseJsonIfPossible(generatedBundle), + outputRequirements: { + overall: ["status", "score", "summary", "findings"], + scoreRange: [0, 100], + eachArtifact: ["id", "review.status", "review.findings"], + }, }); } diff --git a/src/pipelineContracts.test.js b/src/pipelineContracts.test.js index bdeeb53..c311815 100644 --- a/src/pipelineContracts.test.js +++ b/src/pipelineContracts.test.js @@ -36,6 +36,18 @@ test("review prompt sends generated bundle data without system instructions", () assert.equal(payload.task, "review_bundle"); assert.equal(payload.generatedBundle.artifacts[0].id, "action-a"); + assert.deepEqual(payload.outputRequirements.overall, [ + "status", + "score", + "summary", + "findings", + ]); + assert.deepEqual(payload.outputRequirements.scoreRange, [0, 100]); + assert.deepEqual(payload.outputRequirements.eachArtifact, [ + "id", + "review.status", + "review.findings", + ]); assert.doesNotMatch(prompt, /Review every generated artifact/); assert.doesNotMatch(prompt, /bundleReview/); }); diff --git a/src/reviewPresentation.js b/src/reviewPresentation.js index 79702ee..ec645ab 100644 --- a/src/reviewPresentation.js +++ b/src/reviewPresentation.js @@ -62,7 +62,7 @@ export function normalizeReviewStatus(value) { const status = String(value || "").toLowerCase(); if (/(fail|error|critical|block|reject|invalid)/.test(status)) return "fail"; if (/(warn|attention|manual|partial|incomplete|concern)/.test(status)) return "warning"; - if (/(pass|success|ready|approve|valid|clean)/.test(status)) return "pass"; + if (/(pass|success|ready|approve|valid|clean|info|notice)/.test(status)) return "pass"; return null; } @@ -177,12 +177,7 @@ function unwrapReviewPayload(reviewResult) { } const root = toObject(value); - const nested = toObject( - root.bundleReview - || root.reviewResult - || root.codeReview - || root.result, - ); + const nested = toObject(root.reviewResult || root.codeReview || root.result); return { rawText: "", root: Object.keys(nested).length ? nested : root }; } @@ -268,7 +263,8 @@ export function buildReviewPresentation({ bundle, reviewResult }) { const artifacts = asArray(safeBundle.artifacts); const { root, rawText } = unwrapReviewPayload(reviewResult); const overallReview = toObject( - root.overallReview + root.bundleReview + || root.overallReview || root.overall || root.bundleSummary || root.summaryReview diff --git a/src/reviewPresentation.test.js b/src/reviewPresentation.test.js index b248d23..801eaac 100644 --- a/src/reviewPresentation.test.js +++ b/src/reviewPresentation.test.js @@ -155,3 +155,59 @@ test("leaves structured reviews without score fields unscored", () => { assert.equal(presentation.score, null); }); + +test("preserves top-level artifact reviews alongside bundleReview", () => { + const presentation = buildReviewPresentation({ + bundle, + reviewResult: { + artifacts: [ + { + id: "format-payload", + review: { + status: "pass", + findings: [ + { + severity: "info", + message: "Formatting logic is valid.", + suggestion: "No changes required.", + }, + ], + }, + }, + { + id: "execute-webhook", + review: { + status: "pass", + findings: [ + { + severity: "info", + message: "Webhook execution is valid.", + suggestion: "No changes required.", + }, + ], + }, + }, + ], + bundleReview: { + status: "pass", + summary: "The complete bundle is ready to deploy.", + score: 96, + findings: [ + { + severity: "info", + message: "The deploy order is correct.", + suggestion: "Deploy the files in the supplied order.", + }, + ], + }, + }, + }); + + assert.equal(presentation.status, "pass"); + assert.equal(presentation.score, 96); + assert.equal(presentation.summary, "The complete bundle is ready to deploy."); + assert.equal(presentation.findings[0].message, "The deploy order is correct."); + assert.deepEqual(presentation.counts, { pass: 2, warning: 0, fail: 0 }); + assert.equal(presentation.reviewCoverage.reviewed, 2); + assert.equal(presentation.artifacts[0].findings[0].severity, "pass"); +});