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"); }