From d77643c05808a3013bf40a599cd1ae9d0935502d Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:51:31 +0200 Subject: [PATCH] refactor(renderer): move diff rendering into its own module views.js sat at the project's 750-line limit, so the diff cap in the previous commit pushed it over and every further change would have meant shaving comments elsewhere. That is the file asking for decomposition, which is what the architecture audit says to do. Diff rendering is self-contained: the line cap, the line classifier and the change-map illustration depend on nothing in views.js beyond ui and escapeHtml. They now live in src/renderer/diff-view.js and are registered in index.html and in the three renderer file lists that scan the bridge surface, so anything added there is covered by the existing contract tests. views.js drops from 755 to 714 lines and no source file exceeds 750 again. The nested ternary that classified a diff line became a named function with guard clauses on the way. Co-Authored-By: Claude Opus 5 --- SOURCE_MANIFEST.txt | 17 ++++++------ reports/architecture-audit.json | 29 +++++---------------- reports/architecture-audit.md | 4 +-- scripts/verify.mjs | 2 +- src/renderer/diff-view.js | 40 +++++++++++++++++++++++++++++ src/renderer/index.html | 1 + src/renderer/views.js | 44 -------------------------------- tests/browser/forgeflow.spec.mjs | 3 +++ tests/ipc-contract.test.mjs | 2 +- tests/renderer-workflow.test.mjs | 2 +- 10 files changed, 64 insertions(+), 80 deletions(-) create mode 100644 src/renderer/diff-view.js diff --git a/SOURCE_MANIFEST.txt b/SOURCE_MANIFEST.txt index 527d8d1..922a3d5 100644 --- a/SOURCE_MANIFEST.txt +++ b/SOURCE_MANIFEST.txt @@ -114,8 +114,8 @@ abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 f018383f755352ca448e2ebb1e19b1dba412a3eb793d61e64b02953e300754fd 10538 Publish-ForgeFlow-Release.ps1 688fff7d2c989adb97ebb7fae38962656b70304a0aa5d27433c56adf7f136de0 4196 Publish-Missing-Binary-Release.ps1 42e2f375c2fa9bea138428b85eb2bb93fe8bf6e4a5fc0cd728f2c6713a019df7 10315 README.md -90e99e0fd3b92849215857137ca71307913b2558c159db86a4410e0466c249cd 15677 reports/architecture-audit.json -527aa0973a0059ae864d93a4f2393ccbda3ea725ab8036ae2d6ef323a5693fde 1372 reports/architecture-audit.md +76f3dc50b3cd60f6a078f3e37ac8fb1c7e5d5dd746000d9458c1e7189dc54804 14947 reports/architecture-audit.json +94915d088fcb8f0e66e31006283de5a43749c4028c64887305a420fa8162565e 1343 reports/architecture-audit.md 509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs d0745072321aca2c80f44460974a7926715a9f429164aaf7660dced40b52c736 4790 scripts/apply-binary-update.ps1 404863bcbe7292355662e3a326455df864d7279badc29f90866a3b837420df54 10745 scripts/apply-source-update.ps1 @@ -129,7 +129,7 @@ b83d443f5724ac15393567f3a688aed8315fbe3e5966832c864a9466e0669464 8102 c76507857292c5713e1c699cf02e24b80265da39af2cecd148034bdb874adbb6 5246 scripts/test-authenticode-chain.ps1 4393f7dc5f417e6d601a68238f4e26791799a3634acec228fe4d79deaee85eb5 3109 scripts/validate-installed-connections.cjs e6127e1e62f39c70ddb1abf72f4d7e7b8e3f19ff1f219e1a3660353c2e0cdfac 2411 scripts/verify-release-signatures.mjs -1ad4023136361431eeda4df740ec1be5d00d63be0d7a3533250da68c3971480d 21407 scripts/verify.mjs +02832d62cc6a4b9bc4e300a8c4528983272c903498eab77f97bda6467504608d 21420 scripts/verify.mjs 0b9f03ba3c67ff7cdb2916a902ad8ce25e81a7c90b210e4ae52d2ad029efabf3 2353 scripts/write-release-checksums.mjs 619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1 dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs @@ -182,14 +182,15 @@ a980f2e86d8286ea605a7259b9e9adcf3fda4f657b8d54a5d2d8765a7bbbec13 19065 094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png 813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png 02ae73c50856c827ed13e929396643699a29ac03114ceef1a8706e2e172578c5 51148 src/renderer/dialogs.js +dede1f21a06c73a2c2a462a869d27530d85f99baff202a2eb509c57436ad6aec 2732 src/renderer/diff-view.js eef2f269ba4fbb76bf66ad328d481b461255d0acb753b30878dd4d4eaac57dc6 6924 src/renderer/events.js -5b0701cea836286325a9653e265eea05547b47019549efc52edd66749625a10d 1479 src/renderer/index.html +c4a71213d412166093f7bd8254b847de4d8beb58c1aaa356a0cdc8d728080326 1524 src/renderer/index.html 06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js 870024aff376826a92c9cf7452689cc1ecc5d9034f055bea56734f3f7fcea5e5 28703 src/renderer/mock-deployment-bridge.js 81b0155dd51c7bc8871fd29bd7fdb28857cf11a86ed6d6953238c04c2774800c 20033 src/renderer/mock-repository-bridge.js 94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js 21b8f3228bddd6da06613857763282d1f88f4eeab3d063f3a63395d7b567f2e4 80004 src/renderer/styles.css -c6e4bba94b5e4c26793c7e96f287e04e833ef490970bae02bc8dea71919342cb 103195 src/renderer/views.js +f06b2ef7cc4d7f6ce83f5227212271c25d7a904f45bb7ab84f06b082bd9298b5 100508 src/renderer/views.js e9e72c072a5c5d04f59cd6763de0cfbf736c2a5ffa2f722143f3bad2bdbc630b 1411 src/shared/clone-target.cjs 5d425d5c2f939d0f6beebee7ebb0c77146cb7e318535ba7286ec7081a4dc2269 2497 src/shared/deployment-policy.cjs 029e600229714d033c28e2dcb77817aa8269847001782ae0012960e83ffd183f 3057 src/shared/git-status.cjs @@ -203,7 +204,7 @@ f8853dce6fdf360d5df2fbe2b6df3e5687630c807fee5ba8436679b34ec737ea 2436 058aeaa5d9bfe377c7e322f213c7871ecc4151b5d08ef790992f4ee28d857658 743 START-FORGEFLOW-OVERLAY.ps1 f5b0ea887fcdeadec78c1ad49b0ec7979723562f5c0b730703acb77a37281ee0 1009 tests/acceptance.test.mjs a4e5947204ff6878e601e32477bc85b53cd0153baf95a161c8935b6e5466c257 1155 tests/audit-service.test.mjs -64fd88e113f85a56090eb9943b6a3120e10ebbe98e6a1dd1c0da7f73599ba432 16645 tests/browser/forgeflow.spec.mjs +0841f6a2515508f8d28562d5c565c1519d357b8579515f890883dc2aec7d7737 16881 tests/browser/forgeflow.spec.mjs 1728c0a7abd92f4d7d9e68df32e4a6b00730555f23795e9b36416795d9d127af 5978 tests/clone-target.test.mjs ac17f8bbe9e388b80abef7792c8b184a1fd482c93f13d23a478e433961020f75 17214 tests/config-store.test.mjs f1463326aee79842d265687ae628189ce54e92544600f2bd14073780287cfb14 2502 tests/configuration-backup.test.mjs @@ -222,13 +223,13 @@ e7aebcc0d484a6a59d463d5cb26c11b3ad56e28f6535e7c38a0fe166a41565ea 13690 681ab7bcd02c4dd98d1d8d2092a3521c489d941131e7ffe5903971b940046474 2403 tests/git-workflows.test.mjs d633c59bd910008223c834c6d7f3e5666c685a0881944263ede2d42cc69d3151 18710 tests/gitea-actions.test.mjs fcc9a063882840dd89d74c2785284c8f2f6a9e5acec482b6d89ed8de62efdb85 9635 tests/inventory-classifier.test.mjs -9643622a03ea0a88fb7d72ce43e469ff4f814902f4b3d2a672990637d66ef075 2009 tests/ipc-contract.test.mjs +62b90c21c15b841af30d26ccb0b9e88d25674fa7dbff9dc231dd8a1dddc3d657 2025 tests/ipc-contract.test.mjs caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392 tests/log-redaction.test.mjs 96432a97d313f331694900bf0a2c21e38c20eac96d59147977aeed9055a9e3ad 2287 tests/partial-staging.test.mjs 1b6c920e18a248f78acaed6187197c88ec8d911b62d5e2a9f8ad57b91ae80499 11827 tests/preflight.test.mjs 7f2751ea2621f76b5427f442e931344d13e97faa7b6ef3151949bbd6a03097cf 1205 tests/process-error-policy.test.mjs 0cb884cf62c1cb02cf59a81662be055bcb5339d176de85e2a3eeb8e8573e11b3 6435 tests/production-acceptance.test.mjs -d4980aa7d2d364793f38771011383348be472d5447b6ee88a4d9e5f3a3b3ffa9 9911 tests/renderer-workflow.test.mjs +9e4064295909405674c945378062b772a370fcc26c855d343c0840f9e18bbb1d 9927 tests/renderer-workflow.test.mjs 2b4956fa4df4624a04117737e57ba74020564330ff71303b5746d8ccc881e880 854 tests/repository-matching.test.mjs e3c6e0f5c9351fce2ccf96b8abb15de404a0f935633df0b5b1c3fefa207c723b 3993 tests/repository-monitor.test.mjs 5476f3ba90bc096d4172900d9b54ada7c12da521f8627913d87794eade3cee23 13494 tests/repository-service.test.mjs diff --git a/reports/architecture-audit.json b/reports/architecture-audit.json index 8bc9756..57a4cb8 100644 --- a/reports/architecture-audit.json +++ b/reports/architecture-audit.json @@ -1,27 +1,10 @@ { - "generatedAt": "2026-08-23T12:44:33.101Z", + "generatedAt": "2026-08-23T12:51:31.496Z", "thresholds": { "preferredMaximumLines": 750, "justificationRequiredLines": 1000 }, - "over750": [ - { - "file": "src/renderer/views.js", - "lines": 759, - "branches": 57, - "functions": 159, - "ipcHandlers": 0, - "responsibilities": [ - "inventory", - "deployment", - "git", - "renderer", - "security", - "updates" - ], - "hotspotScore": 97 - } - ], + "over750": [], "over1000": [], "cyclomaticHotspots": [ { @@ -193,9 +176,9 @@ }, { "file": "src/renderer/views.js", - "lines": 759, - "branches": 57, - "functions": 159, + "lines": 715, + "branches": 53, + "functions": 152, "ipcHandlers": 0, "responsibilities": [ "inventory", @@ -205,7 +188,7 @@ "security", "updates" ], - "hotspotScore": 97 + "hotspotScore": 93 }, { "file": "src/main/ipc.cjs", diff --git a/reports/architecture-audit.md b/reports/architecture-audit.md index 4f4d9be..64ab467 100644 --- a/reports/architecture-audit.md +++ b/reports/architecture-audit.md @@ -1,12 +1,12 @@ # ForgeFlow architecture audit -Generated 2026-08-23T12:44:33.101Z. Complexity is a deterministic decision-point count used for hotspot ranking, not a claim of exact McCabe complexity. +Generated 2026-08-23T12:51:31.496Z. Complexity is a deterministic decision-point count used for hotspot ranking, not a claim of exact McCabe complexity. ## Files above 750 lines | File | Lines | Decisions | Functions | IPC handlers | Responsibilities | |---|---:|---:|---:|---:|---| -| `src/renderer/views.js` | 759 | 57 | 159 | 0 | inventory, deployment, git, renderer, security, updates | +No findings. ## Files above 1,000 lines diff --git a/scripts/verify.mjs b/scripts/verify.mjs index c96df4f..89fc3ff 100644 --- a/scripts/verify.mjs +++ b/scripts/verify.mjs @@ -325,7 +325,7 @@ if ( ) throw new Error("PowerShell update helper must start directly with param(."); -const renderer = (await Promise.all(["app.js", "views.js", "dialogs.js", "operations.js", "actions/shell.js", "actions/inventory.js", "actions/deployment-profile.js", "actions/deployment-operation.js", "actions/setup-and-settings.js", "actions/recovery.js", "actions/command.js", "events.js"].map((file) => +const renderer = (await Promise.all(["app.js", "diff-view.js", "views.js", "dialogs.js", "operations.js", "actions/shell.js", "actions/inventory.js", "actions/deployment-profile.js", "actions/deployment-operation.js", "actions/setup-and-settings.js", "actions/recovery.js", "actions/command.js", "events.js"].map((file) => readFile(path.join(root, "src/renderer", file), "utf8"), ))).join("\n"); const styles = await readFile( diff --git a/src/renderer/diff-view.js b/src/renderer/diff-view.js new file mode 100644 index 0000000..00584e4 --- /dev/null +++ b/src/renderer/diff-view.js @@ -0,0 +1,40 @@ +// Rendering a unified diff is a self-contained concern with its own size +// limits, kept out of views.js so that file stays within the project's +// architecture budget. +// A regenerated lock file runs into tens of thousands of lines, and one element +// per line freezes the window. Only the rendered view is capped. +const DIFF_RENDER_LINE_LIMIT = 2000; + +function diffAtmosphere(diff, allLines = null) { + if (!ui.selectedFile) return ""; + const lines = allLines || String(diff || "").split("\n"); + const additions = lines.filter( + (line) => line.startsWith("+") && !line.startsWith("+++"), + ).length; + const removals = lines.filter( + (line) => line.startsWith("-") && !line.startsWith("---"), + ).length; + const extension = + String(ui.selectedFile).split(".").pop()?.slice(0, 8).toUpperCase() || + "FILE"; + return ``; +} + +function diffLineType(line) { + if (line.startsWith("+") && !line.startsWith("+++")) return "add"; + if (line.startsWith("-") && !line.startsWith("---")) return "remove"; + return line.startsWith("@@") ? "hunk" : ""; +} + +function renderDiff(diff) { + if (!diff) + return '

No textual diff

Select another file or open the project folder for binary changes.

'; + const lines = String(diff).split("\n"); + const rendered = lines + .slice(0, DIFF_RENDER_LINE_LIMIT) + .map((line) => `${escapeHtml(line) || " "}`) + .join(""); + const hidden = Math.max(0, lines.length - DIFF_RENDER_LINE_LIMIT); + const notice = hidden ? `… ${hidden.toLocaleString()} more line${hidden === 1 ? "" : "s"} are not shown. Copy diff and the editor still give you the complete change.` : ""; + return `${rendered}${notice}${diffAtmosphere(diff, lines)}`; +} diff --git a/src/renderer/index.html b/src/renderer/index.html index 653968f..e08491a 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -21,6 +21,7 @@ + diff --git a/src/renderer/views.js b/src/renderer/views.js index ef1ffab..ffe5740 100644 --- a/src/renderer/views.js +++ b/src/renderer/views.js @@ -206,50 +206,6 @@ function linkedWorkloadsForRepository(repository) { ); } -// A regenerated lock file is an ordinary change and runs into tens of thousands -// of lines. One element per line freezes the window while it is parsed and laid -// out, so the tail is left out of the rendered view only. -const DIFF_RENDER_LINE_LIMIT = 2000; - -function diffAtmosphere(diff, allLines = null) { - if (!ui.selectedFile) return ""; - const lines = allLines || String(diff || "").split("\n"); - const additions = lines.filter( - (line) => line.startsWith("+") && !line.startsWith("+++"), - ).length; - const removals = lines.filter( - (line) => line.startsWith("-") && !line.startsWith("---"), - ).length; - const extension = - String(ui.selectedFile).split(".").pop()?.slice(0, 8).toUpperCase() || - "FILE"; - return ``; -} - -function renderDiff(diff) { - if (!diff) - return '

No textual diff

Select another file or open the project folder for binary changes.

'; - const lines = String(diff).split("\n"); - const rendered = lines - .slice(0, DIFF_RENDER_LINE_LIMIT) - .map((line) => { - const type = - line.startsWith("+") && !line.startsWith("+++") - ? "add" - : line.startsWith("-") && !line.startsWith("---") - ? "remove" - : line.startsWith("@@") - ? "hunk" - : ""; - return `${escapeHtml(line) || " "}`; - }) - .join(""); - const hidden = Math.max(0, lines.length - DIFF_RENDER_LINE_LIMIT); - const notice = hidden - ? `… ${hidden.toLocaleString()} more line${hidden === 1 ? "" : "s"} are not shown. Copy diff and the editor still give you the complete change.` - : ""; - return `${rendered}${notice}${diffAtmosphere(diff, lines)}`; -} function fileStatusCode(file) { if (file.conflict) return "U"; if (file.untracked) return "?"; diff --git a/tests/browser/forgeflow.spec.mjs b/tests/browser/forgeflow.spec.mjs index 1ecafd6..34f510c 100644 --- a/tests/browser/forgeflow.spec.mjs +++ b/tests/browser/forgeflow.spec.mjs @@ -281,6 +281,9 @@ test("a very large diff is capped instead of freezing the window", async ({ page }); expect(selected, "the demo needs a repository with local changes").not.toBeNull(); await expect(page.locator(".diff-view")).toBeVisible(); + // Selecting a repository loads its diff asynchronously; that load would + // otherwise overwrite the diff injected below. + await expect.poll(() => page.evaluate(() => Boolean(ui.diff) && !ui.diff.startsWith("Loading"))).toBe(true); const measured = await page.evaluate(() => { const newline = String.fromCharCode(10); diff --git a/tests/ipc-contract.test.mjs b/tests/ipc-contract.test.mjs index c2cb3a7..d1034b2 100644 --- a/tests/ipc-contract.test.mjs +++ b/tests/ipc-contract.test.mjs @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; async function rendererSource() { - return (await Promise.all(["app.js", "views.js", "dialogs.js", "operations.js", "actions/shell.js", "actions/inventory.js", "actions/deployment-profile.js", "actions/deployment-operation.js", "actions/setup-and-settings.js", "actions/recovery.js", "actions/command.js", "events.js"].map((file) => readFile(new URL(`../src/renderer/${file}`, import.meta.url), "utf8")))).join("\n"); + return (await Promise.all(["app.js", "diff-view.js", "views.js", "dialogs.js", "operations.js", "actions/shell.js", "actions/inventory.js", "actions/deployment-profile.js", "actions/deployment-operation.js", "actions/setup-and-settings.js", "actions/recovery.js", "actions/command.js", "events.js"].map((file) => readFile(new URL(`../src/renderer/${file}`, import.meta.url), "utf8")))).join("\n"); } test("every preload invoke channel has a registered IPC handler", async () => { diff --git a/tests/renderer-workflow.test.mjs b/tests/renderer-workflow.test.mjs index 8e5d1c9..acc4296 100644 --- a/tests/renderer-workflow.test.mjs +++ b/tests/renderer-workflow.test.mjs @@ -2,7 +2,7 @@ import test from "node:test"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; -const rendererFiles = ["app.js", "views.js", "dialogs.js", "operations.js", "actions/shell.js", "actions/inventory.js", "actions/deployment-profile.js", "actions/deployment-operation.js", "actions/setup-and-settings.js", "actions/recovery.js", "actions/command.js", "events.js"]; +const rendererFiles = ["app.js", "diff-view.js", "views.js", "dialogs.js", "operations.js", "actions/shell.js", "actions/inventory.js", "actions/deployment-profile.js", "actions/deployment-operation.js", "actions/setup-and-settings.js", "actions/recovery.js", "actions/command.js", "events.js"]; async function rendererSource() { return (await Promise.all(rendererFiles.map((file) => readFile(new URL(`../src/renderer/${file}`, import.meta.url), "utf8")))).join("\n"); }