feat: deliver policy-driven Git Validator 2.0

This commit is contained in:
NuklearRabbit
2026-07-29 18:24:27 +02:00
parent 7b05c953b6
commit a0733875c4
18 changed files with 502 additions and 133 deletions
+53 -50
View File
@@ -15,40 +15,41 @@ ca32a76e708d565c4af659f0f4d2615fc32114c3f75aec1454862a3ed1e72c41 2263
25048ed854e8ce8fece115e555c98d25507b002f8019b6ae717b54604c868c50 46223 build/icon.ico 25048ed854e8ce8fece115e555c98d25507b002f8019b6ae717b54604c868c50 46223 build/icon.ico
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 build/icon.png 16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 build/icon.png
5f4aca19a35cbcaffa1a6993ce96b7d66052ec2b286022f2af74594e8a310568 15712 CHANGELOG.md 5f4aca19a35cbcaffa1a6993ce96b7d66052ec2b286022f2af74594e8a310568 15712 CHANGELOG.md
1101ae8a14a4f0cf0a56a3a73a60e7a231e07ea7872d936904c605abf6298a34 288319 coverage/tmp/coverage-16016-1785339516187-0.json 04a3058e507c938447d35564e503cf89b76670d8ea45f5209db074dfa2625b91 303090 coverage/tmp/coverage-14228-1785341379444-0.json
d655dca9239887ef791e144308830fe97ae99b3531449944254d9733b27a1cae 111306 coverage/tmp/coverage-20500-1785339516683-0.json 0f86a35f2e6a48c0789263a4e36190760b0dfa45ce9a1128555b4dd77da2df5b 352006 coverage/tmp/coverage-14596-1785341377618-0.json
9f13cddf4882c263f5da10e82eda888aa80d60eba5a4880646fde183ea41451f 329357 coverage/tmp/coverage-23076-1785339516366-0.json 4ae93dacf27d8ec7e84bd00ab52398a0d99cecb25cf2244be8e6d89824c3d5db 345122 coverage/tmp/coverage-15236-1785341392510-0.json
bd1b7d71c9b2d6a89c7b96c0da51d634d7e8f9e335822f9601998eb7d1264b66 367435 coverage/tmp/coverage-30956-1785339516246-0.json 2607418ea469b58eb6190e4cabde794c0773b7746afb0fdd2d401c99a353752a 358520 coverage/tmp/coverage-17880-1785341380606-0.json
a27032411a329ff88d2ca8b0365f1c9c04f86951763bfdbdc6b1c76dcb4dbb10 327419 coverage/tmp/coverage-33844-1785339515162-0.json ea98763aaacb035c1c75fca8f50784fc0cc89b2dedf2ddfa80c0eb32d633af27 342639 coverage/tmp/coverage-19480-1785341377315-0.json
59c785d1cabcee6d1069421425732d67ede026fdf907922724b894ec60f23f7c 303089 coverage/tmp/coverage-36076-1785339516543-0.json fdaef396ca19f7e1f62ea7e6f2172fa615c7e7f26b157e80c896199f5ba3e77e 329358 coverage/tmp/coverage-19612-1785341379233-0.json
6ccdab75e41e28365282114305041c342bf0924b488673d4afbebe8dc0b2bf4d 426817 coverage/tmp/coverage-39452-1785339516633-0.json d2808be6bce7d7d3d4662def01522170b8cef6c802813db92ade8c27b335fbbe 346825 coverage/tmp/coverage-21808-1785341377499-0.json
7019945829625166aafaf3fac38ed2095c9b6ccef4db451b103c8e641c5b31c4 301396 coverage/tmp/coverage-39516-1785339515312-0.json 7f788bab2eb9ada8e3c3f7fc1d6948f001018721eccb54e87b98a8916e30695b 299000 coverage/tmp/coverage-24368-1785341377951-0.json
204f45a7f69e83e727b5d06713b59fe7cf76fa2b8acfb9909b8a226cfebe6919 299000 coverage/tmp/coverage-41400-1785339515472-0.json d3584daefb19adb8e0e7cd3ca6ffc7f6992636a692a4a3b186da94281950aa8f 302346 coverage/tmp/coverage-25148-1785341378806-0.json
28305a502a85cc95ad37adab0c80abe7ad9923e1fb7739c8b3c01694cfd5303a 335944 coverage/tmp/coverage-43744-1785339515697-0.json e4045202501f53544351ae8f0aba6cdcccef9d08d735f4d0c83634af5d3033b3 344499 coverage/tmp/coverage-29628-1785341379071-0.json
61d18db7fece14e6ffc192cd0c93556c176276c5738438a6cbc96bac4b6ebab7 282471 coverage/tmp/coverage-44320-1785339516169-0.json bf66bf3c80bfed0d145dc2a73363e47089a74858d3cece5dbef2756bfbfd623b 360893 coverage/tmp/coverage-31472-1785341379151-0.json
10ebff421ee2f56d9a2b3ea87ec43d397609111a1e028a810472757dc1fbc6c2 346825 coverage/tmp/coverage-44612-1785339515256-0.json 0d52bfc33883af8fccdc37c436edd95eb83f54c65de87fa09ad03719995574e4 335635 coverage/tmp/coverage-36800-1785341386017-0.json
f9179452c930d41972809015ef90e2d5cdbfd9a460f50420fb2131869f510f8d 354683 coverage/tmp/coverage-50832-1785339515755-0.json bcb44d6e6369e3cb5fd5306a02097fadd44e6d85eeac5490e7660f841ba86ea3 288320 coverage/tmp/coverage-37176-1785341378929-0.json
6265af5161de8807eff28cba2834f6a29c93216b4b4ac3fb0955f14e7a8d677e 89173 coverage/tmp/coverage-52036-1785339515055-0.json da4b2512633624ef659682bf16af77d92799bd228e0d69a985fa23246460ce09 111308 coverage/tmp/coverage-44380-1785341379522-0.json
bd8a4f824da29801902c1f305d3804f249efb6233d44f902c04a0c35e2d4ba76 342639 coverage/tmp/coverage-52864-1785339515537-0.json 96a9f4b2bb3a429dbe5286c7f6c13c39b0e0de882d46724853083ea3d9ac9f6e 317635 coverage/tmp/coverage-44612-1785341382147-0.json
ca7431ed712d082aca6348fdeda4c67207fdab56656e0feda5fad74e60ccf4db 336007 coverage/tmp/coverage-53260-1785339515757-0.json 705a92db802fac29c0bbe5aa5727ad35350ddfd03bc98efc24d68529f5a41a99 360058 coverage/tmp/coverage-44984-1785341422918-0.json
f834e80b44c37263816fa5ac9f29870658690453aa826db1f8a6841e5ed554b2 335634 coverage/tmp/coverage-53596-1785339519653-0.json 279e70e06e6366bf15bb60f6079c38580d006832ba200ba5cebd9bac8a4e4f02 321057 coverage/tmp/coverage-48188-1785341379614-0.json
b8c977c8dc0f34e968c59071c376ec1e19ddc45440e3ff96d844f48eda12be05 284302 coverage/tmp/coverage-55056-1785339515769-0.json 20758be193868d8f9275dd1eaf35b6bfc652f4682ebc89e4c4aa481e63dd1664 301398 coverage/tmp/coverage-49592-1785341377478-0.json
40cad1955f780f9fd42cdcbe61d04acba2d3f7662bb27639f16e5f0820df83b3 321055 coverage/tmp/coverage-55928-1785339516726-0.json a4275d02b93b13deadcc554844b321fe186ad272e676f93ccf8f84772bc2760c 284667 coverage/tmp/coverage-49952-1785341378477-0.json
b9559b72bc441f91eed9e65122636e5e76c8ffcb2dbfe0501fea8860d6868b12 320075 coverage/tmp/coverage-56416-1785339517848-0.json d704dd8bab1e3fcf52d4e4762e6a3e4eb9818073a2fc1d622a2afe4425ec9cc4 336008 coverage/tmp/coverage-50036-1785341378337-0.json
2e027822e3475abb5caef8d1326f76d5e2f0a0512656bfd9747135a86bf1e4f3 344498 coverage/tmp/coverage-56544-1785339516170-0.json e4dc4ec12d779464d974f7d5c9713f63db6776596698fdc4ce03d18e773a34d9 354684 coverage/tmp/coverage-50728-1785341378056-0.json
30eada55184a94693954401a5cb9e1f37ebaee78075ed569b7ad0762aae0d123 344612 coverage/tmp/coverage-56608-1785339518494-0.json 0487ef221d132f9ada2b57cb216fb0ff167f903cea8df46c61b39b9fa52bd38a 282952 coverage/tmp/coverage-51204-1785341379590-0.json
09ac59a2ff03d78b2b025a266af53e27b67c6f33a562afba9da62b89938c8991 318324 coverage/tmp/coverage-57192-1785339516055-0.json 42be90ace3af568e4eace7f1a3a5974856360707839c793b3b092ede0f6c5ad1 344613 coverage/tmp/coverage-51236-1785341383324-0.json
1b35c0e476c27889b191455271562550e146e0b4129721e8938836e9e5e25414 344933 coverage/tmp/coverage-57204-1785339516743-0.json 1b5a01cc5508a78e28b27f7a46878d54aaca027deb0e93f34f445d9dd32116f1 335945 coverage/tmp/coverage-54004-1785341378407-0.json
9fb213f290c39ec6c4d89dab2e86dde5109551a0c0f64b6b1cc020557e4a515d 302345 coverage/tmp/coverage-57868-1785339516192-0.json b46e7b4d5f95b167803a7d0481163a01026b5ac2e9c32e2ba40afa8a4791163e 428588 coverage/tmp/coverage-55224-1785341380041-0.json
993ece7e08147c4d88bce3fe791aacb6af20c487a9ad3f59f0d8ed25a0661892 304507 coverage/tmp/coverage-58136-1785339515403-0.json b175e49795c65f6011dbf04aefd75cb7ebee5c2b248edb50e7df1780f2a599b8 367436 coverage/tmp/coverage-55472-1785341379481-0.json
344bd1ec56b83794737ea3c0c889fe5629a0baf4993d550048deb6fe1c2f1f03 352004 coverage/tmp/coverage-58704-1785339515317-0.json 2a53a441228fb74c1f7a488d49c1545e53640af5c1ca720352919c86527e5c19 344934 coverage/tmp/coverage-56192-1785341380236-0.json
3106fd06d0269f55a759c491d8d73a844ad9b13db96a0ed839407630daad1cd4 310268 coverage/tmp/coverage-58996-1785339516528-0.json dd2c111bc050afda47ed7379f88651cda579a965cfed56ddf2b3ca0e5d748ee1 304508 coverage/tmp/coverage-5636-1785341377677-0.json
2a1b4078c040a602b68d7c684e7b4a70b330ea424866bc043baa5f3f7eb867aa 360892 coverage/tmp/coverage-59356-1785339516204-0.json 8ce33572e4a3ba6e63b85155dc0ebba61b3e6fd203ef17b762735639dab72467 360058 coverage/tmp/coverage-56392-1785341444543-0.json
deb5210ec65db9e7aaf85bb4b3bda1ec5c9fdc89709b048712ca4a82836f84b7 358381 coverage/tmp/coverage-59756-1785339516948-0.json 9efac9c2f67e00fbc2ddd22ff17886ffa212b55b76e21597df1a85417e55ef0c 89175 coverage/tmp/coverage-56440-1785341377010-0.json
3d435d5982a18f5876080c2cf0e181beb0e3759d838c429e1d1d4e6dcbe97634 302512 coverage/tmp/coverage-59972-1785339516032-0.json 4b6892c586091b5bf3eead8d8687a318ef2d82874fc0c9efaab25430ee819600 296835 coverage/tmp/coverage-58592-1785341378685-0.json
0dbfa219b3a04e5854b09c803e5092b09320b2c87ebfb9c6ed69f88c2730944c 360066 coverage/tmp/coverage-59988-1785339547987-0.json 81f7395f307d0c4776331a5125653388650dea21876ace362ec92e3ce1efefda 318326 coverage/tmp/coverage-58856-1785341378523-0.json
bdce38d30ef6798ae45146d58a16f794910c9016fe3f0591aa6b65613cc600a0 345121 coverage/tmp/coverage-60804-1785339522999-0.json 5370cdb8a3384f62e68314bd4bf0efa78d9db57ced656b011d3b1e995369596e 327419 coverage/tmp/coverage-59512-1785341377212-0.json
de23d1f8a3a19c8297c97fc44d0cccd2dff77242d7789112c5177738c0ab40a7 296834 coverage/tmp/coverage-61136-1785339515987-0.json 7e7571c84f4bcdcdd39ba6fcaa2ac9ea1583eeff6cf59ea4c46c601add76e40a 310269 coverage/tmp/coverage-60040-1785341379655-0.json
d19fda134998419dd45ecd943d587c26cf80742b140de46449f8ec4bcf9191a9 302513 coverage/tmp/coverage-61124-1785341378630-0.json
21cb96e7afe71b1dc791c818dedd244d92f9a6ed4d9ffbb3022ccb187e1bdf0f 852 docs/ACCEPTANCE.md 21cb96e7afe71b1dc791c818dedd244d92f9a6ed4d9ffbb3022ccb187e1bdf0f 852 docs/ACCEPTANCE.md
a17f95d96d3c9fbc69d870874e6fbb7472091adefc454b24f835db1279511d72 8296 docs/ARCHITECTURE.md a17f95d96d3c9fbc69d870874e6fbb7472091adefc454b24f835db1279511d72 8296 docs/ARCHITECTURE.md
72e846f591c47a0291e7466e58e052d3d5afcf551c4e6c848632ac3c552a1244 3043 docs/CURRENT_STATE.md 72e846f591c47a0291e7466e58e052d3d5afcf551c4e6c848632ac3c552a1244 3043 docs/CURRENT_STATE.md
@@ -118,17 +119,17 @@ c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532
106538d4a14a5a7b13419f9520c582b19809e8fafe2cb8c7dce2bc3e600dd10a 397 examples/server/nginx-forgeflow-status.conf 106538d4a14a5a7b13419f9520c582b19809e8fafe2cb8c7dce2bc3e600dd10a 397 examples/server/nginx-forgeflow-status.conf
2dff25fb39ce8fc7844026a50524b23f241bec5b614eb05371c7f908a080f69a 398 examples/server/status-example.json 2dff25fb39ce8fc7844026a50524b23f241bec5b614eb05371c7f908a080f69a 398 examples/server/status-example.json
4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE 4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE
910e179eb2743989725e7fe18efd291ff098b4335aae4b9eb530b579efa6fd3f 13654 main.cjs 1f0f388df4397e548887bbc7579fd3c864581b86469c01703201ece7a6cbf931 13667 main.cjs
91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md 91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md
f3e2a262e251eb165f342dc3c184625ec10d56e536b5a987199b9944f463eee9 181069 package-lock.json f3e2a262e251eb165f342dc3c184625ec10d56e536b5a987199b9944f463eee9 181069 package-lock.json
43725a143cb927cc70bd823d789788d785d872119c008043c270bbf73948851f 4882 package.json 43725a143cb927cc70bd823d789788d785d872119c008043c270bbf73948851f 4882 package.json
86876cd502f51ad65e9c9280b189a80579df99ba65c81daf338048693d6ec78f 11732 preload.cjs 69318fdf054be7aa2fe86ead9847da9da65745d8d5de548c8346f3ba0afc4892 12175 preload.cjs
abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 PUBLISH-AND-ENABLE-UPDATE.cmd abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 PUBLISH-AND-ENABLE-UPDATE.cmd
f018383f755352ca448e2ebb1e19b1dba412a3eb793d61e64b02953e300754fd 10538 Publish-ForgeFlow-Release.ps1 f018383f755352ca448e2ebb1e19b1dba412a3eb793d61e64b02953e300754fd 10538 Publish-ForgeFlow-Release.ps1
688fff7d2c989adb97ebb7fae38962656b70304a0aa5d27433c56adf7f136de0 4196 Publish-Missing-Binary-Release.ps1 688fff7d2c989adb97ebb7fae38962656b70304a0aa5d27433c56adf7f136de0 4196 Publish-Missing-Binary-Release.ps1
794bbe1937077788f34c64398fd73dc9a3c43d095084aa32673f3e968b115de2 9150 README.md 794bbe1937077788f34c64398fd73dc9a3c43d095084aa32673f3e968b115de2 9150 README.md
adbed8dcc399e1c55393391f81ca6a8789840ce17ee942980d2593813aee166d 13680 reports/architecture-audit.json 46e2f05dc20703e869bcf863fbeea8a2e470a957b880e008f15c879a076512d0 14009 reports/architecture-audit.json
9683d4988b4fbdfeef6cbb8c50e515e34c3c09bc55fb5f5913c6bb14e592f0c2 1105 reports/architecture-audit.md 0a6f4dba118d8e86f34d616f2c0a546fe8bfb9fecced92a951bf58452c412a55 1114 reports/architecture-audit.md
509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs 509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs
00d57bda5af8c8eda294b72d18b318f024a307b81b0d9205a0821f5240151e31 3814 scripts/apply-binary-update.ps1 00d57bda5af8c8eda294b72d18b318f024a307b81b0d9205a0821f5240151e31 3814 scripts/apply-binary-update.ps1
f8359a69d20deb2dfe10042d1bec7b12a95e76e58e36bc5f265f073c3111d056 10287 scripts/apply-source-update.ps1 f8359a69d20deb2dfe10042d1bec7b12a95e76e58e36bc5f265f073c3111d056 10287 scripts/apply-source-update.ps1
@@ -145,7 +146,7 @@ e8c5d00737d4c5e2c37ff683e569e8d7a0510be233db3840460c78be69f2c4bc 17163
0b9f03ba3c67ff7cdb2916a902ad8ce25e81a7c90b210e4ae52d2ad029efabf3 2353 scripts/write-release-checksums.mjs 0b9f03ba3c67ff7cdb2916a902ad8ce25e81a7c90b210e4ae52d2ad029efabf3 2353 scripts/write-release-checksums.mjs
619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1 619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1
dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs
565787043e9825859301c203a269ca801e0c2c417f37417f0dde17238433d712 30410 src/main/config-store.cjs 04b97b9b02ad8746d2229d40ba0585088118ecd18e1b5f3a1911a4f3b63831d6 32271 src/main/config-store.cjs
2fb04b1494b39f5d7c0720fa5fd298cd46fa85dc1b696d77657592347fcf1819 2731 src/main/configuration-backup.cjs 2fb04b1494b39f5d7c0720fa5fd298cd46fa85dc1b696d77657592347fcf1819 2731 src/main/configuration-backup.cjs
86e9fc2eda66b4b563f6c4bbb87d3e8514340d484fb503b73137e63b6b05c3c9 14597 src/main/deploy-key-lifecycle-service.cjs 86e9fc2eda66b4b563f6c4bbb87d3e8514340d484fb503b73137e63b6b05c3c9 14597 src/main/deploy-key-lifecycle-service.cjs
7cbfe51973d6607203cb197652ed7f296a3f6b6b644df876957117866a47d802 2159 src/main/deployment-identity.cjs 7cbfe51973d6607203cb197652ed7f296a3f6b6b644df876957117866a47d802 2159 src/main/deployment-identity.cjs
@@ -153,14 +154,15 @@ dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412
c157640e76d558906a9aa9881eda811196623ef1c65fa3467f32f0f84b0ddd0c 15095 src/main/diagnostics-service.cjs c157640e76d558906a9aa9881eda811196623ef1c65fa3467f32f0f84b0ddd0c 15095 src/main/diagnostics-service.cjs
a2ef47d5330095b92c2bd22fcc39962091881f9cb60d02e261eb1dd1bd693170 1974 src/main/external-tools-service.cjs a2ef47d5330095b92c2bd22fcc39962091881f9cb60d02e261eb1dd1bd693170 1974 src/main/external-tools-service.cjs
0b7476c2cfe1872601978c20a466c20fe58be35e81b2303e38a753fea62bbc27 32548 src/main/git-service.cjs 0b7476c2cfe1872601978c20a466c20fe58be35e81b2303e38a753fea62bbc27 32548 src/main/git-service.cjs
3ce45837099ac7bddc024974bd839575b4b765a7df9055e7d45ef889dc85bf7f 15623 src/main/git-validator-service.cjs b23dfa041d9f4597d144601ab8569e69ba748a0d7a075c3ef01154eacc2640ae 7018 src/main/git-validator-policy.cjs
ca050e9820528b555e6a1dfd89ac8be2f9a0cec6e91254897e6e3b6e116a7eb2 26317 src/main/git-validator-service.cjs
2faaef0eeec1e473db82b94243674e3812ef5869358b0ec6abf8d645a8794623 20768 src/main/gitea-service.cjs 2faaef0eeec1e473db82b94243674e3812ef5869358b0ec6abf8d645a8794623 20768 src/main/gitea-service.cjs
8c3c7b9bf0893276f5ab520efba17fc143c64377128d754411cd911180025c4a 5743 src/main/inventory-classifier.cjs 8c3c7b9bf0893276f5ab520efba17fc143c64377128d754411cd911180025c4a 5743 src/main/inventory-classifier.cjs
dafdb09133d2b6ec2161a3f0b09354551e54fc606c8107976fca37405643be91 3404 src/main/inventory-review-service.cjs dafdb09133d2b6ec2161a3f0b09354551e54fc606c8107976fca37405643be91 3404 src/main/inventory-review-service.cjs
00989577aed509a7ddfdf9f4df09a196a393a85b5ea59b21089002215e69f065 25949 src/main/ipc.cjs 00989577aed509a7ddfdf9f4df09a196a393a85b5ea59b21089002215e69f065 25949 src/main/ipc.cjs
0eb1cfdcd3a37a0ec9502bf753966f87230c03580335798bef9265add42ee6fa 12530 src/main/ipc/deployment-handlers.cjs 0eb1cfdcd3a37a0ec9502bf753966f87230c03580335798bef9265add42ee6fa 12530 src/main/ipc/deployment-handlers.cjs
dc9b5971c9fefe8c374aa31916f5513601ce86003fd48b1d0e51330a909ae3a5 3442 src/main/ipc/operations-handlers.cjs dc9b5971c9fefe8c374aa31916f5513601ce86003fd48b1d0e51330a909ae3a5 3442 src/main/ipc/operations-handlers.cjs
7c6628caf16228500f15309766cdbbe54f8ea8238a32052123abdc445bb7d51f 14478 src/main/ipc/repository-handlers.cjs 2e2d0b26480b395906b7881e50b596c9a7701e6e534497d04d541bf3f3b3c057 15673 src/main/ipc/repository-handlers.cjs
62f2c80c8210e19370b8556b1f296cbae50dae6b758a39e209f8fb461691fd4c 4235 src/main/log-redaction.cjs 62f2c80c8210e19370b8556b1f296cbae50dae6b758a39e209f8fb461691fd4c 4235 src/main/log-redaction.cjs
958595a99fb242c127f475f3d8622bdba4c07b2d658703f69fe3992227a9107e 12909 src/main/preflight-service.cjs 958595a99fb242c127f475f3d8622bdba4c07b2d658703f69fe3992227a9107e 12909 src/main/preflight-service.cjs
3096b4181566cb93a27e56e248c92105d4f4df5aee39d73c6c7d8ae8c2231bc0 1570 src/main/process-runner.cjs 3096b4181566cb93a27e56e248c92105d4f4df5aee39d73c6c7d8ae8c2231bc0 1570 src/main/process-runner.cjs
@@ -168,12 +170,12 @@ e89b54e7e3174b4b0a1dcd9058d8344e29431f9d16d0e6bb8d11559b691440a0 2508
17e2a53f61cd7faba461b9f332967143087eaac95b72001462292976278ca305 7782 src/main/repository-service.cjs 17e2a53f61cd7faba461b9f332967143087eaac95b72001462292976278ca305 7782 src/main/repository-service.cjs
52b6d88ed1f5c904a13cdde92e5f96d1e2b5971ceef49862152197353cdc6490 27928 src/main/server-inventory.cjs 52b6d88ed1f5c904a13cdde92e5f96d1e2b5971ceef49862152197353cdc6490 27928 src/main/server-inventory.cjs
afef3841a3948b2121f8fba809aae4ea3da71bd2fda86973ba50200a5b1f89b2 14894 src/main/ssh-service.cjs afef3841a3948b2121f8fba809aae4ea3da71bd2fda86973ba50200a5b1f89b2 14894 src/main/ssh-service.cjs
e880894fb9626d804617e8e993b88e9cdc8b9972baad4c3751e86932da45b7b3 25506 src/main/unraid-access-methods.cjs 90504e27bfabcd2f927ba29930fad9bb65520fdf871a2a9e49cbcefcb837d84c 25492 src/main/unraid-access-methods.cjs
2ede80cd1565a7f2c282cc58d35dc0889d58d7465346bc723026b9c8be4df0ac 9501 src/main/unraid-deploy-key-host.cjs 2ede80cd1565a7f2c282cc58d35dc0889d58d7465346bc723026b9c8be4df0ac 9501 src/main/unraid-deploy-key-host.cjs
803a079499f7b8148495209dea43b505f6eb9bb587de19e82054e47183186c6e 30461 src/main/unraid-deployment-methods.cjs 803a079499f7b8148495209dea43b505f6eb9bb587de19e82054e47183186c6e 30461 src/main/unraid-deployment-methods.cjs
2e63fdc0be0bf4d8e5c3d7d45ff5811d786b0821a08b82f511f1301696e12c9d 17157 src/main/unraid-deployment-service.cjs 2e63fdc0be0bf4d8e5c3d7d45ff5811d786b0821a08b82f511f1301696e12c9d 17157 src/main/unraid-deployment-service.cjs
08f082a2d902ce65dcf65e20a2f90631542b515eecb31cfe09520b5b0ec32ba4 35212 src/main/unraid-inventory-methods.cjs de0378e1344ff102552b01018451346c30c16a76eb00e7c4349c82f7b39d52a6 35210 src/main/unraid-inventory-methods.cjs
b385460ea1ea53bb9f10463a0b036a0128efe35ef4f8a453e76181d990ef68e9 26487 src/main/unraid-preflight-methods.cjs 9e682411f73450f595b5cc4dfb28939c6c58b9547d0a91e287c3efb4955e8840 26299 src/main/unraid-preflight-methods.cjs
d45220176aed72d692f9ae5534f9d40bcc359a2d08e025e74a3b3b505b8b9ed4 16559 src/main/unraid-runtime-methods.cjs d45220176aed72d692f9ae5534f9d40bcc359a2d08e025e74a3b3b505b8b9ed4 16559 src/main/unraid-runtime-methods.cjs
d4b3a07eeca687a49544a94ea574f7c6f7e0bc3aa9311b614d5244a468ca4a1b 11307 src/main/unraid-state-methods.cjs d4b3a07eeca687a49544a94ea574f7c6f7e0bc3aa9311b614d5244a468ca4a1b 11307 src/main/unraid-state-methods.cjs
b654a9e45044ad32c61fabe4a6d897288615ec83739b53e3241ff881e32f56bd 21677 src/main/update-service.cjs b654a9e45044ad32c61fabe4a6d897288615ec83739b53e3241ff881e32f56bd 21677 src/main/update-service.cjs
@@ -183,21 +185,21 @@ d0bf607dd1de9d55f2947d0adf0997cd3ca5c269d10a5362cc1d8bc4d1a2a8ae 6706
48bed91dd2a85bb51ee7307f7acc3b79c881ce8cf63b22ba79d5d079b265eb4b 7785 src/renderer/actions/inventory.js 48bed91dd2a85bb51ee7307f7acc3b79c881ce8cf63b22ba79d5d079b265eb4b 7785 src/renderer/actions/inventory.js
4227a05a20580a31127d2c929640defc3d36e8e3e89d6be830aab1940da81082 12267 src/renderer/actions/recovery.js 4227a05a20580a31127d2c929640defc3d36e8e3e89d6be830aab1940da81082 12267 src/renderer/actions/recovery.js
cdfaacdcd5ae04b0e5c79fefa21f5e09d5c810bcea504c5b6e1d6b744182ff84 15567 src/renderer/actions/setup-and-settings.js cdfaacdcd5ae04b0e5c79fefa21f5e09d5c810bcea504c5b6e1d6b744182ff84 15567 src/renderer/actions/setup-and-settings.js
b17be89568a5602d67dc226bf8ca4464731c88664de8c72a880493c96abc1963 16825 src/renderer/actions/shell.js 5d8110918b2957889047e38eb4ab2953b2b4476394d9328bd40ae6e4246e65ef 18584 src/renderer/actions/shell.js
d33bdc89a17fbe921dacbb035dee84c9cc10c161259a02772196d9f26a17c4e9 24089 src/renderer/app.js 100077a82f14d5252753d017369ecef48ed6d00532166ae5a0b356ead549e02b 24087 src/renderer/app.js
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png 16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png 813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png
094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png 094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png 813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png
5d6c87d51e55c9e976cf83ce242be0c28cf831d9c52082f02e745193500e6348 47992 src/renderer/dialogs.js 5d6c87d51e55c9e976cf83ce242be0c28cf831d9c52082f02e745193500e6348 47992 src/renderer/dialogs.js
7abdd25671edcc63b134a13f013890c67a5988d2d12c0e969506d75abe83ec3c 5630 src/renderer/events.js 977af9585074f0a404830c5a93de6939b4897d2eb98ba8ae75c3cf0023dea673 5782 src/renderer/events.js
a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html
06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js 06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js
6da0d3e9fcbe6040cf30bdefb566dd8a7a26df2352233124abd8a011034f477e 26667 src/renderer/mock-deployment-bridge.js 19603be9f471ba695c66f17fc027ab427b68ce146af80d52e3c94624a5ba1c35 27712 src/renderer/mock-deployment-bridge.js
26065ffa2359cd27b9c9b5b9fb67bcad83ba0f118960c7c024e7e9392dbb16a3 20032 src/renderer/mock-repository-bridge.js 26065ffa2359cd27b9c9b5b9fb67bcad83ba0f118960c7c024e7e9392dbb16a3 20032 src/renderer/mock-repository-bridge.js
94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js 94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js
45692591428575b518678a6b548c25d3de95f568541e5648b0f06f42c48b5bbf 77872 src/renderer/styles.css 45692591428575b518678a6b548c25d3de95f568541e5648b0f06f42c48b5bbf 77872 src/renderer/styles.css
314d0db4d5ae2042ab76516daf452bc7c49c3e667dfc06c731dd3f47b153e12a 93872 src/renderer/views.js 1703e64533b7e2717b27c5776296c7dd76331e6f97e8005aea9fd688f1aee3ae 94834 src/renderer/views.js
0a1e9d9d6cd4d190eb7f85dbc6668d80600b1cf2749cc0c2c51cc428f506f20d 1121 src/shared/clone-target.cjs 0a1e9d9d6cd4d190eb7f85dbc6668d80600b1cf2749cc0c2c51cc428f506f20d 1121 src/shared/clone-target.cjs
5d425d5c2f939d0f6beebee7ebb0c77146cb7e318535ba7286ec7081a4dc2269 2497 src/shared/deployment-policy.cjs 5d425d5c2f939d0f6beebee7ebb0c77146cb7e318535ba7286ec7081a4dc2269 2497 src/shared/deployment-policy.cjs
029e600229714d033c28e2dcb77817aa8269847001782ae0012960e83ffd183f 3057 src/shared/git-status.cjs 029e600229714d033c28e2dcb77817aa8269847001782ae0012960e83ffd183f 3057 src/shared/git-status.cjs
@@ -220,6 +222,7 @@ fae3634bae871abade4d487b94b4741b50e787804dbd6135249f634fdd83c6d0 3800
dd121d96ca265a027cd415a52064500a4541b2f8a662f4f4b25f2f996d52b5da 762 tests/external-tools.test.mjs dd121d96ca265a027cd415a52064500a4541b2f8a662f4f4b25f2f996d52b5da 762 tests/external-tools.test.mjs
e7aebcc0d484a6a59d463d5cb26c11b3ad56e28f6535e7c38a0fe166a41565ea 13690 tests/git-integration.test.mjs e7aebcc0d484a6a59d463d5cb26c11b3ad56e28f6535e7c38a0fe166a41565ea 13690 tests/git-integration.test.mjs
5ea94c6b241a02060d531fad94e449eecd3772eed2137581d4e2babfb09e56db 1239 tests/git-status.test.mjs 5ea94c6b241a02060d531fad94e449eecd3772eed2137581d4e2babfb09e56db 1239 tests/git-status.test.mjs
c00bbd8eae5cef7856c8283d6b40dedb81083bf57ad762e89ab79e0f312da351 3271 tests/git-validator-policy.test.mjs
73d00729696e5067ba33dd6d43b018d89ce7fdd561a60ab66648d3283fb54d21 5370 tests/git-validator.test.mjs 73d00729696e5067ba33dd6d43b018d89ce7fdd561a60ab66648d3283fb54d21 5370 tests/git-validator.test.mjs
681ab7bcd02c4dd98d1d8d2092a3521c489d941131e7ffe5903971b940046474 2403 tests/git-workflows.test.mjs 681ab7bcd02c4dd98d1d8d2092a3521c489d941131e7ffe5903971b940046474 2403 tests/git-workflows.test.mjs
771eeb4fa5443d581991cedb4107d7c414ce1b7d2e14bac7cf719ec8ba131647 8969 tests/gitea-actions.test.mjs 771eeb4fa5443d581991cedb4107d7c414ce1b7d2e14bac7cf719ec8ba131647 8969 tests/gitea-actions.test.mjs
@@ -228,7 +231,7 @@ e7aebcc0d484a6a59d463d5cb26c11b3ad56e28f6535e7c38a0fe166a41565ea 13690
caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392 tests/log-redaction.test.mjs caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392 tests/log-redaction.test.mjs
96432a97d313f331694900bf0a2c21e38c20eac96d59147977aeed9055a9e3ad 2287 tests/partial-staging.test.mjs 96432a97d313f331694900bf0a2c21e38c20eac96d59147977aeed9055a9e3ad 2287 tests/partial-staging.test.mjs
c0f8f5a3784835f19d9ff1015185ccb385840b6fa1c9ec19f233393a7d952b65 3718 tests/preflight.test.mjs c0f8f5a3784835f19d9ff1015185ccb385840b6fa1c9ec19f233393a7d952b65 3718 tests/preflight.test.mjs
676a5a791e94dcda11ee1f1826d5c4de2f94389072b4f53a5228a57cb0c5d9e0 9493 tests/renderer-workflow.test.mjs 629ba26395c0b49cc5fdee6b0646d75369eb6338e1cc7b59509938f97eea08ec 9601 tests/renderer-workflow.test.mjs
2b4956fa4df4624a04117737e57ba74020564330ff71303b5746d8ccc881e880 854 tests/repository-matching.test.mjs 2b4956fa4df4624a04117737e57ba74020564330ff71303b5746d8ccc881e880 854 tests/repository-matching.test.mjs
f679072548554a64974f0452337ce5e7b0c567343c287223770cc0974b905348 1068 tests/repository-monitor.test.mjs f679072548554a64974f0452337ce5e7b0c567343c287223770cc0974b905348 1068 tests/repository-monitor.test.mjs
75b5b83836c75675bb9a48fe4363fcb8a24fc425e6af6f822d7955c6f3c79eac 2265 tests/repository-service.test.mjs 75b5b83836c75675bb9a48fe4363fcb8a24fc425e6af6f822d7955c6f3c79eac 2265 tests/repository-service.test.mjs
+1
View File
@@ -300,6 +300,7 @@ app
git, git,
gitea, gitea,
diagnostics, diagnostics,
store,
}); });
repositoryMonitor = new RepositoryMonitor({ repositoryMonitor = new RepositoryMonitor({
store, store,
+4
View File
@@ -142,6 +142,10 @@ contextBridge.exposeInMainWorld(
troubleshooterRepair: (issue) => invoke('troubleshooter:repair', { issue }), troubleshooterRepair: (issue) => invoke('troubleshooter:repair', { issue }),
troubleshooterAutoRepair: (issues) => invoke('troubleshooter:auto-repair', { issues }), troubleshooterAutoRepair: (issues) => invoke('troubleshooter:auto-repair', { issues }),
gitValidatorScan: (fullName) => invoke('git-validator:scan', { fullName }), gitValidatorScan: (fullName) => invoke('git-validator:scan', { fullName }),
gitValidatorSetPolicy: (fullName, policy) => invoke('git-validator:set-policy', { fullName, policy }),
gitValidatorSuppress: (fullName, suppression) => invoke('git-validator:suppress', { fullName, suppression }),
gitValidatorPreviewRepair: (fullName, check) => invoke('git-validator:preview-repair', { fullName, check }),
gitValidatorExport: (fullName, format = 'json') => invoke('git-validator:export', { fullName, format }),
gitValidatorRepair: (fullName, check) => invoke('git-validator:repair', { fullName, check }), gitValidatorRepair: (fullName, check) => invoke('git-validator:repair', { fullName, check }),
diagnosticsStatus: () => invoke('diagnostics:status'), diagnosticsStatus: () => invoke('diagnostics:status'),
clearDiagnostics: () => invoke('diagnostics:clear'), clearDiagnostics: () => invoke('diagnostics:clear'),
+62 -46
View File
@@ -1,5 +1,5 @@
{ {
"generatedAt": "2026-07-29T16:10:50.148Z", "generatedAt": "2026-07-29T16:20:01.552Z",
"thresholds": { "thresholds": {
"preferredMaximumLines": 750, "preferredMaximumLines": 750,
"justificationRequiredLines": 1000 "justificationRequiredLines": 1000
@@ -7,6 +7,21 @@
"over750": [], "over750": [],
"over1000": [], "over1000": [],
"cyclomaticHotspots": [ "cyclomaticHotspots": [
{
"file": "src/renderer/actions/shell.js",
"lines": 506,
"branches": 98,
"functions": 84,
"ipcHandlers": 0,
"responsibilities": [
"inventory",
"deployment",
"git",
"renderer",
"updates"
],
"hotspotScore": 128
},
{ {
"file": "src/main/server-inventory.cjs", "file": "src/main/server-inventory.cjs",
"lines": 578, "lines": 578,
@@ -22,20 +37,6 @@
], ],
"hotspotScore": 119 "hotspotScore": 119
}, },
{
"file": "src/renderer/actions/shell.js",
"lines": 481,
"branches": 89,
"functions": 81,
"ipcHandlers": 0,
"responsibilities": [
"inventory",
"deployment",
"git",
"renderer"
],
"hotspotScore": 109
},
{ {
"file": "src/main/git-service.cjs", "file": "src/main/git-service.cjs",
"lines": 632, "lines": 632,
@@ -49,6 +50,21 @@
} }
], ],
"mixedResponsibilityModules": [ "mixedResponsibilityModules": [
{
"file": "src/renderer/actions/shell.js",
"lines": 506,
"branches": 98,
"functions": 84,
"ipcHandlers": 0,
"responsibilities": [
"inventory",
"deployment",
"git",
"renderer",
"updates"
],
"hotspotScore": 128
},
{ {
"file": "src/main/server-inventory.cjs", "file": "src/main/server-inventory.cjs",
"lines": 578, "lines": 578,
@@ -66,7 +82,7 @@
}, },
{ {
"file": "src/renderer/app.js", "file": "src/renderer/app.js",
"lines": 669, "lines": 667,
"branches": 74, "branches": 74,
"functions": 110, "functions": 110,
"ipcHandlers": 0, "ipcHandlers": 0,
@@ -80,20 +96,6 @@
], ],
"hotspotScore": 114 "hotspotScore": 114
}, },
{
"file": "src/renderer/actions/shell.js",
"lines": 481,
"branches": 89,
"functions": 81,
"ipcHandlers": 0,
"responsibilities": [
"inventory",
"deployment",
"git",
"renderer"
],
"hotspotScore": 109
},
{ {
"file": "src/main/ipc.cjs", "file": "src/main/ipc.cjs",
"lines": 749, "lines": 749,
@@ -128,8 +130,8 @@
}, },
{ {
"file": "src/renderer/views.js", "file": "src/renderer/views.js",
"lines": 695, "lines": 693,
"branches": 52, "branches": 53,
"functions": 138, "functions": 138,
"ipcHandlers": 0, "ipcHandlers": 0,
"responsibilities": [ "responsibilities": [
@@ -140,7 +142,7 @@
"security", "security",
"updates" "updates"
], ],
"hotspotScore": 92 "hotspotScore": 93
}, },
{ {
"file": "src/renderer/actions/setup-and-settings.js", "file": "src/renderer/actions/setup-and-settings.js",
@@ -203,7 +205,7 @@
}, },
{ {
"file": "main.cjs", "file": "main.cjs",
"lines": 442, "lines": 443,
"branches": 33, "branches": 33,
"functions": 53, "functions": 53,
"ipcHandlers": 0, "ipcHandlers": 0,
@@ -236,9 +238,9 @@
}, },
{ {
"file": "src/main/config-store.cjs", "file": "src/main/config-store.cjs",
"lines": 615, "lines": 652,
"branches": 47, "branches": 47,
"functions": 82, "functions": 86,
"ipcHandlers": 0, "ipcHandlers": 0,
"responsibilities": [ "responsibilities": [
"inventory", "inventory",
@@ -341,9 +343,9 @@
}, },
{ {
"file": "src/renderer/events.js", "file": "src/renderer/events.js",
"lines": 170, "lines": 172,
"branches": 30, "branches": 31,
"functions": 25, "functions": 26,
"ipcHandlers": 0, "ipcHandlers": 0,
"responsibilities": [ "responsibilities": [
"inventory", "inventory",
@@ -352,6 +354,20 @@
"renderer", "renderer",
"security" "security"
], ],
"hotspotScore": 61
},
{
"file": "src/main/git-validator-service.cjs",
"lines": 588,
"branches": 40,
"functions": 74,
"ipcHandlers": 0,
"responsibilities": [
"deployment",
"git",
"security",
"updates"
],
"hotspotScore": 60 "hotspotScore": 60
}, },
{ {
@@ -429,9 +445,9 @@
}, },
{ {
"file": "preload.cjs", "file": "preload.cjs",
"lines": 158, "lines": 162,
"branches": 2, "branches": 2,
"functions": 119, "functions": 123,
"ipcHandlers": 0, "ipcHandlers": 0,
"responsibilities": [ "responsibilities": [
"inventory", "inventory",
@@ -446,9 +462,9 @@
}, },
{ {
"file": "src/renderer/mock-deployment-bridge.js", "file": "src/renderer/mock-deployment-bridge.js",
"lines": 661, "lines": 678,
"branches": 11, "branches": 11,
"functions": 83, "functions": 87,
"ipcHandlers": 0, "ipcHandlers": 0,
"responsibilities": [ "responsibilities": [
"inventory", "inventory",
@@ -655,10 +671,10 @@
}, },
{ {
"file": "src/main/ipc/repository-handlers.cjs", "file": "src/main/ipc/repository-handlers.cjs",
"lines": 390, "lines": 411,
"branches": 16, "branches": 16,
"functions": 75, "functions": 79,
"ipcHandlers": 47, "ipcHandlers": 51,
"responsibilities": [ "responsibilities": [
"git", "git",
"ipc" "ipc"
+2 -2
View File
@@ -1,6 +1,6 @@
# ForgeFlow architecture audit # ForgeFlow architecture audit
Generated 2026-07-29T16:10:50.148Z. Complexity is a deterministic decision-point count used for hotspot ranking, not a claim of exact McCabe complexity. Generated 2026-07-29T16:20:01.552Z. Complexity is a deterministic decision-point count used for hotspot ranking, not a claim of exact McCabe complexity.
## Files above 750 lines ## Files above 750 lines
@@ -18,8 +18,8 @@ No findings.
| File | Lines | Decisions | Functions | IPC handlers | Responsibilities | | File | Lines | Decisions | Functions | IPC handlers | Responsibilities |
|---|---:|---:|---:|---:|---| |---|---:|---:|---:|---:|---|
| `src/renderer/actions/shell.js` | 506 | 98 | 84 | 0 | inventory, deployment, git, renderer, updates |
| `src/main/server-inventory.cjs` | 578 | 89 | 104 | 0 | inventory, deployment, git, security, updates | | `src/main/server-inventory.cjs` | 578 | 89 | 104 | 0 | inventory, deployment, git, security, updates |
| `src/renderer/actions/shell.js` | 481 | 89 | 81 | 0 | inventory, deployment, git, renderer |
| `src/main/git-service.cjs` | 632 | 98 | 109 | 0 | git | | `src/main/git-service.cjs` | 632 | 98 | 109 | 0 | git |
## Interpretation ## Interpretation
+38 -1
View File
@@ -7,7 +7,7 @@ const { safeStorage } = require('electron');
const { assertHttpUrl, assertWorkflowFileName, assertBranchName, assertEnvironmentName, assertCloneRemote, assertRepositoryRelativePath, assertRepositoryRelativePaths } = require('../shared/validation.cjs'); const { assertHttpUrl, assertWorkflowFileName, assertBranchName, assertEnvironmentName, assertCloneRemote, assertRepositoryRelativePath, assertRepositoryRelativePaths } = require('../shared/validation.cjs');
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
schemaVersion: 12, schemaVersion: 13,
setupComplete: false, setupComplete: false,
appearance: 'dark', appearance: 'dark',
gitea: { baseUrl: '', user: null, encryptedToken: null }, gitea: { baseUrl: '', user: null, encryptedToken: null },
@@ -16,6 +16,7 @@ const DEFAULT_CONFIG = {
deploymentProfiles: {}, deploymentProfiles: {},
deploymentStates: {}, deploymentStates: {},
inventoryReviewDecisions: {}, inventoryReviewDecisions: {},
gitValidator: { policies: {}, suppressions: {}, trends: {} },
favorites: [], favorites: [],
updates: { updates: {
owner: 'Jens', owner: 'Jens',
@@ -67,6 +68,11 @@ class ConfigStore {
workspaceRoots: uniqueStrings(source.workspaceRoots), workspaceRoots: uniqueStrings(source.workspaceRoots),
repositoryMappings: source.repositoryMappings && typeof source.repositoryMappings === 'object' ? source.repositoryMappings : {}, repositoryMappings: source.repositoryMappings && typeof source.repositoryMappings === 'object' ? source.repositoryMappings : {},
inventoryReviewDecisions: source.inventoryReviewDecisions && typeof source.inventoryReviewDecisions === 'object' ? structuredClone(source.inventoryReviewDecisions) : {}, inventoryReviewDecisions: source.inventoryReviewDecisions && typeof source.inventoryReviewDecisions === 'object' ? structuredClone(source.inventoryReviewDecisions) : {},
gitValidator: {
policies: source.gitValidator?.policies && typeof source.gitValidator.policies === 'object' ? structuredClone(source.gitValidator.policies) : {},
suppressions: source.gitValidator?.suppressions && typeof source.gitValidator.suppressions === 'object' ? structuredClone(source.gitValidator.suppressions) : {},
trends: source.gitValidator?.trends && typeof source.gitValidator.trends === 'object' ? structuredClone(source.gitValidator.trends) : {}
},
deploymentProfiles: source.deploymentProfiles && typeof source.deploymentProfiles === 'object' deploymentProfiles: source.deploymentProfiles && typeof source.deploymentProfiles === 'object'
? Object.fromEntries(Object.entries(source.deploymentProfiles).map(([key, profiles]) => [key, (Array.isArray(profiles) ? profiles : []).map((profile) => { ? Object.fromEntries(Object.entries(source.deploymentProfiles).map(([key, profiles]) => [key, (Array.isArray(profiles) ? profiles : []).map((profile) => {
if (!profile || typeof profile !== 'object' || profile.provider !== 'ssh-unraid') return profile; if (!profile || typeof profile !== 'object' || profile.provider !== 'ssh-unraid') return profile;
@@ -144,6 +150,36 @@ class ConfigStore {
return this.saveQueue; return this.saveQueue;
} }
getGitValidatorState(fullName) {
const key = String(fullName || '').toLowerCase();
return {
policy: structuredClone(this.data.gitValidator.policies[key] || { id: 'standard' }),
suppressions: structuredClone(this.data.gitValidator.suppressions[key] || []),
trends: structuredClone(this.data.gitValidator.trends[key] || [])
};
}
async setGitValidatorPolicy(fullName, policy) {
const key = String(fullName || '').toLowerCase();
this.data.gitValidator.policies[key] = structuredClone(policy);
await this.save();
return this.getGitValidatorState(key);
}
async addGitValidatorSuppression(fullName, suppression) {
const key = String(fullName || '').toLowerCase();
this.data.gitValidator.suppressions[key] = [...(this.data.gitValidator.suppressions[key] || []), structuredClone(suppression)].slice(-250);
await this.save();
return this.getGitValidatorState(key);
}
async appendGitValidatorTrend(fullName, trend) {
const key = String(fullName || '').toLowerCase();
this.data.gitValidator.trends[key] = [...(this.data.gitValidator.trends[key] || []), structuredClone(trend)].slice(-100);
await this.save();
return this.getGitValidatorState(key);
}
async createRecoverySnapshot(reason = 'configuration-change') { async createRecoverySnapshot(reason = 'configuration-change') {
await this.saveQueue.catch(() => {}); await this.saveQueue.catch(() => {});
const safeReason = String(reason || 'configuration-change').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'configuration-change'; const safeReason = String(reason || 'configuration-change').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'configuration-change';
@@ -602,6 +638,7 @@ class ConfigStore {
repositoryMappings: { ...this.data.repositoryMappings }, repositoryMappings: { ...this.data.repositoryMappings },
deploymentProfiles: structuredClone(this.data.deploymentProfiles), deploymentProfiles: structuredClone(this.data.deploymentProfiles),
deploymentStates: structuredClone(this.data.deploymentStates), deploymentStates: structuredClone(this.data.deploymentStates),
gitValidator: structuredClone(this.data.gitValidator),
favorites: [...this.data.favorites], favorites: [...this.data.favorites],
updates: { ...this.data.updates }, updates: { ...this.data.updates },
servers: this.data.servers.map((server) => this.getPublicServer(server)), servers: this.data.servers.map((server) => this.getPublicServer(server)),
+87
View File
@@ -0,0 +1,87 @@
"use strict";
const crypto = require("node:crypto");
const PROFILE_DEFINITIONS = Object.freeze({
minimal: { label: "Minimal", requiredScore: 55, severities: ["error"], allowSuppressions: true, maxSuppressionDays: 180 },
standard: { label: "Standard", requiredScore: 70, severities: ["error", "warning"], allowSuppressions: true, maxSuppressionDays: 90 },
strict: { label: "Strict", requiredScore: 82, severities: ["error", "warning"], allowSuppressions: true, maxSuppressionDays: 30 },
production: { label: "Production", requiredScore: 90, severities: ["error", "warning"], allowSuppressions: true, maxSuppressionDays: 14 },
});
function normalizePolicy(policy = {}) {
const id = String(policy.id || policy.profile || "standard").toLowerCase();
const base = PROFILE_DEFINITIONS[id] || PROFILE_DEFINITIONS.standard;
const custom = id === "organization" ? policy : {};
return {
id,
label: custom.label || base.label || "Organization custom",
requiredScore: Math.min(100, Math.max(0, Number(custom.requiredScore ?? base.requiredScore ?? 80))),
enabledChecks: Array.isArray(custom.enabledChecks) ? [...new Set(custom.enabledChecks.map(String))] : null,
severityOverrides: custom.severityOverrides && typeof custom.severityOverrides === "object" ? { ...custom.severityOverrides } : {},
blockingChecks: [...new Set((custom.blockingChecks || policy.blockingChecks || []).map(String))],
allowSuppressions: custom.allowSuppressions ?? base.allowSuppressions ?? true,
maxSuppressionDays: Math.max(1, Number(custom.maxSuppressionDays ?? base.maxSuppressionDays ?? 30)),
};
}
function validateSuppression(input, policy, now = new Date()) {
if (!policy.allowSuppressions) throw new Error("The selected policy does not allow suppressions.");
const checkId = String(input?.checkId || "").trim();
const reason = String(input?.reason || "").trim();
const author = String(input?.author || "").trim();
const scope = String(input?.scope || "repository").trim();
const evidence = String(input?.evidence || "").trim();
const expiresAt = new Date(input?.expiresAt || "");
if (!checkId || reason.length < 10 || !author || !evidence) throw new Error("A suppression requires a check ID, author, evidence and a reason of at least 10 characters.");
if (!Number.isFinite(expiresAt.getTime()) || expiresAt <= now) throw new Error("A suppression expiry must be in the future.");
const maximum = new Date(now.getTime() + policy.maxSuppressionDays * 86_400_000);
if (expiresAt > maximum) throw new Error(`This policy permits suppressions for at most ${policy.maxSuppressionDays} days.`);
return {
id: crypto.randomUUID(), checkId, reason, author,
createdAt: now.toISOString(), ticket: String(input.ticket || "").trim() || null,
expiresAt: expiresAt.toISOString(), scope, evidence,
};
}
function applyPolicy(checks, policyInput, suppressions = [], now = new Date()) {
const policy = normalizePolicy(policyInput);
const enabled = policy.enabledChecks ? new Set(policy.enabledChecks) : null;
const relevant = checks.filter((check) => !enabled || enabled.has(check.id)).map((check) => {
const status = policy.severityOverrides[check.id] || check.status;
const suppression = suppressions.find((item) => item.checkId === check.id && new Date(item.expiresAt) > now);
const expiredSuppression = suppressions.find((item) => item.checkId === check.id && new Date(item.expiresAt) <= now);
return {
...check,
status,
suppressed: Boolean(suppression),
suppression: suppression || null,
expiredSuppression: expiredSuppression || null,
blocking: !suppression && status !== "pass" && (status === "error" || policy.blockingChecks.includes(check.id)),
};
});
return { policy, checks: relevant };
}
function buildTrend(previous, report) {
const prior = new Map((previous?.checks || []).map((check) => [check.id, check]));
const current = new Map(report.checks.map((check) => [check.id, check]));
const active = (check) => check && check.status !== "pass" && !check.suppressed;
const newlyFound = [...current.values()].filter((check) => active(check) && !active(prior.get(check.id))).map((check) => check.id);
const resolved = [...prior.values()].filter((check) => active(check) && !active(current.get(check.id))).map((check) => check.id);
const regressions = [...current.values()].filter((check) => active(check) && prior.get(check.id)?.status === "warning" && check.status === "error").map((check) => check.id);
return { score: report.score, categories: report.categories, newlyFound, resolved, regressions, suppressions: report.checks.filter((check) => check.suppressed).map((check) => check.id), checkedAt: report.checkedAt, commitSha: report.commitSha || null, checks: report.checks.map(({ id, status, suppressed }) => ({ id, status, suppressed })) };
}
function exportReport(report, format = "json") {
if (format === "json") return { extension: "json", mimeType: "application/json", content: `${JSON.stringify(report, null, 2)}\n` };
const rows = report.checks.map((check) => `| ${check.id} | ${check.category} | ${check.status}${check.suppressed ? " (suppressed)" : ""} | ${String(check.detail).replace(/\|/g, "\\|")} |`).join("\n");
const markdown = `# Git assurance report — ${report.repository}\n\nPolicy: **${report.policy.label}** · Score: **${report.score}/100** · Commit: \`${report.commitSha || "unknown"}\`\n\n| Check | Category | Status | Evidence |\n|---|---|---|---|\n${rows}\n`;
if (format === "markdown") return { extension: "md", mimeType: "text/markdown", content: markdown };
if (format !== "html") throw new Error("Unsupported Git Validator export format.");
const escape = (value) => String(value).replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]);
const htmlRows = report.checks.map((check) => `<tr><td>${escape(check.id)}</td><td>${escape(check.category)}</td><td>${escape(check.status)}${check.suppressed ? " (suppressed)" : ""}</td><td>${escape(check.detail)}</td></tr>`).join("");
return { extension: "html", mimeType: "text/html", content: `<!doctype html><html lang="en"><meta charset="utf-8"><title>Git assurance — ${escape(report.repository)}</title><style>body{font:15px system-ui;max-width:1100px;margin:40px auto;padding:0 24px;color:#172033}table{border-collapse:collapse;width:100%}th,td{padding:10px;border:1px solid #ccd4e0;text-align:left}th{background:#edf2f7}</style><h1>Git assurance — ${escape(report.repository)}</h1><p>Policy: <strong>${escape(report.policy.label)}</strong> · Score: <strong>${report.score}/100</strong> · Commit: <code>${escape(report.commitSha || "unknown")}</code></p><table><thead><tr><th>Check</th><th>Category</th><th>Status</th><th>Evidence</th></tr></thead><tbody>${htmlRows}</tbody></table></html>` };
}
module.exports = { PROFILE_DEFINITIONS, normalizePolicy, validateSuppression, applyPolicy, buildTrend, exportReport };
+106 -3
View File
@@ -4,6 +4,7 @@ const fs = require("node:fs/promises");
const path = require("node:path"); const path = require("node:path");
const { run } = require("./process-runner.cjs"); const { run } = require("./process-runner.cjs");
const { normalizeRemoteUrl } = require("../shared/repository-match.cjs"); const { normalizeRemoteUrl } = require("../shared/repository-match.cjs");
const { applyPolicy, buildTrend, exportReport, normalizePolicy, validateSuppression } = require("./git-validator-policy.cjs");
const RECOMMENDED_GITIGNORE = `# Local configuration and secrets const RECOMMENDED_GITIGNORE = `# Local configuration and secrets
.env .env
@@ -67,6 +68,7 @@ function result(id, category, title, status, detail, options = {}) {
fixAction: options.fixAction || null, fixAction: options.fixAction || null,
safe: options.safe === true, safe: options.safe === true,
confirmation: options.confirmation || null, confirmation: options.confirmation || null,
evidence: options.evidence || null,
}; };
} }
@@ -84,10 +86,11 @@ function isSensitiveTrackedPath(filePath) {
} }
class GitValidatorService { class GitValidatorService {
constructor({ git, gitea, diagnostics }) { constructor({ git, gitea, diagnostics, store }) {
this.git = git; this.git = git;
this.gitea = gitea; this.gitea = gitea;
this.diagnostics = diagnostics; this.diagnostics = diagnostics;
this.store = store;
} }
async config(root, key, { local = true } = {}) { async config(root, key, { local = true } = {}) {
@@ -176,7 +179,7 @@ class GitValidatorService {
{ weight: 35 }, { weight: 35 },
), ),
); );
return this.summarize(repository, checks); return this.finalize(repository, checks, null);
} }
const root = await this.git.ensureRepository(repository.localPath); const root = await this.git.ensureRepository(repository.localPath);
@@ -378,7 +381,107 @@ class GitValidatorService {
{ weight: 7 }, { weight: 7 },
), ),
); );
return this.summarize(repository, checks); await this.addAssuranceChecks(root, tracked, lowerFiles, checks);
return this.finalize(repository, checks, status);
}
async addAssuranceChecks(root, tracked, lowerFiles, checks) {
const has = (...patterns) => lowerFiles.some((file) => patterns.some((pattern) => pattern.test(file)));
const fileCheck = (id, category, title, patterns, detail, weight = 5) => {
const present = has(...patterns);
checks.push(result(id, category, title, present ? "pass" : "warning", present ? `${title} is versioned.` : detail, { weight }));
};
fileCheck("security-policy", "Security", "Security policy", [/(^|\/)security\.md$/], "Add SECURITY.md with supported versions and private disclosure instructions.", 8);
fileCheck("codeowners", "Governance", "Code ownership", [/(^|\/)codeowners$/], "Add CODEOWNERS for security-sensitive and release paths.", 6);
fileCheck("license", "Governance", "Repository license", [/(^|\/)(license|copying)(\.[^/]+)?$/], "Document the repository license or private-use terms.", 5);
fileCheck("changelog", "Release readiness", "Changelog", [/(^|\/)changelog(\.[^/]+)?$/], "Add a changelog that maps releases to user-visible changes.", 7);
fileCheck("contributing", "Collaboration", "Contribution guide", [/(^|\/)contributing(\.[^/]+)?$/], "Add contribution, test and review instructions.", 4);
fileCheck("issue-templates", "Collaboration", "Issue templates", [/^\.gitea\/issue_template\//, /^\.github\/issue_template\//], "Add structured issue templates.", 3);
fileCheck("pull-request-template", "Collaboration", "Pull request template", [/(^|\/)pull_request_template\.md$/], "Add a pull request checklist for tests, risk and rollback.", 4);
fileCheck("runtime-pinning", "Reproducibility", "Runtime version pinning", [/(^|\/)(\.nvmrc|\.node-version|\.tool-versions|mise\.toml)$/], "Pin the runtime version used by developers and CI.", 7);
fileCheck("build-instructions", "Reproducibility", "Build instructions", [/(^|\/)(readme|building|build)(\.[^/]+)?$/], "Document a clean, reproducible build command.", 6);
const generated = tracked.filter((file) => /(^|\/)(dist|build|coverage|\.cache)\//i.test(file));
checks.push(result("generated-artifacts", "Performance and hygiene", "Generated output is not tracked", generated.length ? "warning" : "pass", generated.length ? `${generated.length} generated-path file(s) are tracked; review ${generated.slice(0, 5).join(", ")}.` : "No common generated output directories are tracked.", { weight: 8, evidence: generated.slice(0, 20) }));
const executables = tracked.filter((file) => /\.(exe|dll|msi|scr|com|bat|cmd|ps1)$/i.test(file));
checks.push(result("executable-artifacts", "Security", "Executable artifacts are intentional", executables.length ? "warning" : "pass", executables.length ? `Review executable content: ${executables.slice(0, 8).join(", ")}.` : "No executable-shaped artifacts are tracked.", { weight: 8, evidence: executables.slice(0, 20) }));
const workflowFiles = tracked.filter((file) => /^\.(gitea|github)\/workflows\/[^/]+\.ya?ml$/i.test(file));
const workflowText = (await Promise.all(workflowFiles.slice(0, 40).map((file) => fs.readFile(path.join(root, file), "utf8").catch(() => "")))).join("\n");
const unpinned = [...workflowText.matchAll(/uses:\s*[^\s@]+@([^\s#]+)/g)].map((match) => match[1]).filter((ref) => !/^[0-9a-f]{40}$/i.test(ref));
checks.push(result("pinned-actions", "Security", "External CI actions are commit-pinned", unpinned.length ? "warning" : "pass", unpinned.length ? `${unpinned.length} action reference(s) use mutable tags or branches.` : "External actions are commit-pinned or no external actions are used.", { weight: 9, evidence: unpinned.slice(0, 20) }));
const broadPermissions = /permissions:\s*(write-all|write)/i.test(workflowText) || /contents:\s*write/i.test(workflowText);
checks.push(result("workflow-permissions", "Security", "Workflow permissions use least privilege", broadPermissions ? "error" : "pass", broadPermissions ? "A workflow requests broad write permissions; scope permissions per job and capability." : "No broad workflow write permission was detected.", { weight: 12 }));
const [commitSignature, tagSignature, recentSubjects] = await Promise.all([
run("git", ["log", "-1", "--format=%G?"], { cwd: root, timeout: 10_000, allowExitCodes: [128] }).then((value) => value.stdout.trim()).catch(() => "N"),
run("git", ["tag", "--points-at", "HEAD", "--format=%(contents:signature)"], { cwd: root, timeout: 10_000, allowExitCodes: [128] }).then((value) => value.stdout.trim()).catch(() => ""),
run("git", ["log", "-20", "--format=%s"], { cwd: root, timeout: 10_000, allowExitCodes: [128] }).then((value) => value.stdout.trim().split(/\r?\n/).filter(Boolean)).catch(() => []),
]);
checks.push(result("signed-commits", "Governance", "Latest commit is signed", /[GUYX]/.test(commitSignature) ? "pass" : "warning", /[GUYX]/.test(commitSignature) ? "Git reports a cryptographic signature on HEAD." : "HEAD has no verifiable Git signature.", { weight: 6 }));
checks.push(result("signed-tags", "Governance", "Release tags are signed", tagSignature ? "pass" : "warning", tagSignature ? "HEAD has a signed tag." : "HEAD has no signed release tag.", { weight: 5 }));
const conventional = recentSubjects.length > 0 && recentSubjects.every((subject) => /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?:\s.+/i.test(subject));
checks.push(result("conventional-commits", "Governance", "Recent commits follow a convention", conventional ? "pass" : "warning", conventional ? `${recentSubjects.length} recent commit subject(s) follow Conventional Commits.` : "Some recent commit subjects do not follow Conventional Commits.", { weight: 4 }));
const releaseFiles = {
"release-checksums": /(^|\/)(sha256sums|checksums?)(\.[^/]+)?$/,
"release-provenance": /(^|\/)(provenance|attestation)(\.[^/]+)?$/,
"release-sbom": /(^|\/)(sbom)(\.[^/]+)?$/,
};
for (const [id, pattern] of Object.entries(releaseFiles)) fileCheck(id, "Release readiness", id.replace(/^release-/, "Release "), [pattern], `No ${id.replace(/^release-/, "")} artifact is versioned or generated in the repository.`, 4);
checks.push(result("signing-readiness", "Release readiness", "Signing policy is documented", has(/(^|\/)(security|release|signing)(\.[^/]+)?$/) ? "pass" : "warning", has(/(^|\/)(security|release|signing)(\.[^/]+)?$/) ? "Signing guidance is present." : "Document signing identity, verification and timestamp requirements.", { weight: 6 }));
}
async finalize(repository, checks, status) {
const repositoryState = this.store?.getGitValidatorState?.(repository.fullName) || { policy: { id: "standard" }, suppressions: [], trends: [] };
const { policy, checks: governedChecks } = applyPolicy(checks, repositoryState.policy, repositoryState.suppressions);
const report = this.summarize(repository, governedChecks);
report.policy = policy;
report.commitSha = status?.head || status?.branch?.oid || null;
report.categories = Object.fromEntries([...new Set(governedChecks.map((check) => check.category))].map((category) => {
const categoryChecks = governedChecks.filter((check) => check.category === category);
return [category, Math.round(categoryChecks.filter((check) => check.status === "pass" || check.suppressed).length / categoryChecks.length * 100)];
}));
report.ready = report.score >= policy.requiredScore && !governedChecks.some((check) => check.blocking);
report.expiredSuppressions = governedChecks.filter((check) => check.expiredSuppression).map((check) => check.id);
report.trend = buildTrend(repositoryState.trends.at(-1), report);
if (this.store?.appendGitValidatorTrend) await this.store.appendGitValidatorTrend(repository.fullName, report.trend);
return report;
}
async setPolicy(repository, policyInput) {
const policy = normalizePolicy(policyInput);
if (!this.store?.setGitValidatorPolicy) throw new Error("Git Validator policy persistence is unavailable.");
await this.store.setGitValidatorPolicy(repository.fullName, policy);
return policy;
}
async suppress(repository, input) {
const state = this.store?.getGitValidatorState?.(repository.fullName) || { policy: { id: "standard" } };
const suppression = validateSuppression(input, normalizePolicy(state.policy));
await this.store.addGitValidatorSuppression(repository.fullName, suppression);
return suppression;
}
export(report, format) { return exportReport(report, format); }
async previewRepair(repository, check) {
if (!check?.fixAction) throw new Error("This validator check has no repair action.");
const root = repository.localPath ? await this.git.ensureRepository(repository.localPath) : null;
const fileDefinitions = {
"add-gitignore": [".gitignore", RECOMMENDED_GITIGNORE],
"add-gitattributes": [".gitattributes", RECOMMENDED_GITATTRIBUTES],
"add-editorconfig": [".editorconfig", RECOMMENDED_EDITORCONFIG],
};
if (fileDefinitions[check.fixAction]) {
const [name, content] = fileDefinitions[check.fixAction];
if (await fs.stat(path.join(root, name)).catch(() => null)) throw new Error(`${name} already exists; rescan before repairing.`);
return { checkId: check.id, action: check.fixAction, files: [name], diff: `diff --git a/${name} b/${name}\nnew file mode 100644\n--- /dev/null\n+++ b/${name}\n${content.split("\n").filter((line, index, lines) => index < lines.length - 1).map((line) => `+${line}`).join("\n")}\n`, remoteMutation: false };
}
if (check.fixAction === "configure-local-safety") return { checkId: check.id, action: check.fixAction, files: [".git/config"], diff: "+ fetch.prune = true\n+ pull.ff = only\n+ rebase.autoStash = true\n", remoteMutation: false };
if (check.fixAction === "align-origin") return { checkId: check.id, action: check.fixAction, files: [".git/config"], diff: `- origin = current\n+ origin = ${repository.preferredCloneUrl || repository.cloneUrl || repository.sshUrl}\n`, remoteMutation: false };
if (check.fixAction === "protect-default-branch") return { checkId: check.id, action: check.fixAction, files: [], diff: `Gitea policy change:\n+ protect ${repository.defaultBranch || "main"}\n+ block force pushes\n+ require pull request review\n`, remoteMutation: true };
throw new Error("Unsupported Git Validator repair action.");
} }
summarize(repository, checks) { summarize(repository, checks) {
+21
View File
@@ -359,6 +359,27 @@ function registerRepositoryIpc({
}); });
return report; return report;
}); });
register("git-validator:set-policy", async ({ fullName, policy }) => {
const repository = await resolveRepository({ fullName });
const saved = await gitValidator.setPolicy(repository, policy);
await audit.append("git-validator.policy.changed", { repository: repository.fullName, policy: saved.id });
return saved;
});
register("git-validator:suppress", async ({ fullName, suppression }) => {
const repository = await resolveRepository({ fullName });
const saved = await gitValidator.suppress(repository, suppression);
await audit.append("git-validator.finding.suppressed", { repository: repository.fullName, checkId: saved.checkId, expiresAt: saved.expiresAt, ticket: saved.ticket });
return saved;
});
register("git-validator:preview-repair", async ({ fullName, check }) => {
const repository = await resolveRepository({ fullName });
return gitValidator.previewRepair(repository, check);
});
register("git-validator:export", async ({ fullName, format = "json" }) => {
const repository = await resolveRepository({ fullName });
const report = await gitValidator.scan(repository);
return gitValidator.export(report, format);
});
register("git-validator:repair", async ({ fullName, check }) => { register("git-validator:repair", async ({ fullName, check }) => {
const repository = await resolveRepository({ fullName }); const repository = await resolveRepository({ fullName });
const allowed = new Set([ const allowed = new Set([
+1 -1
View File
@@ -106,7 +106,7 @@ function createUnraidAccessMethods({ shellQuote, path, bash, inventoryRemoteIden
const deployKey = await this.gitea.ensureReadOnlyDeployKey({ const deployKey = await this.gitea.ensureReadOnlyDeployKey({
owner, owner,
repo, repo,
title: `ForgeFlow · ${server.name} · read-only`, title: `ForgeFlow · ${server.name} · read-only`,
publicKey, publicKey,
}); });
const probeCommand = `${this.serverGitEnvironment(repository, profile, server)} git ls-remote --exit-code ${shellQuote(remote)} ${shellQuote(`refs/heads/${profile.branch}`)}`; const probeCommand = `${this.serverGitEnvironment(repository, profile, server)} git ls-remote --exit-code ${shellQuote(remote)} ${shellQuote(`refs/heads/${profile.branch}`)}`;
+1 -1
View File
@@ -199,7 +199,7 @@ function createUnraidInventoryMethods({
const profileId = `${idPrefix}-${crypto.createHash("sha256").update(`${server.id}:${repository.fullName}:${workload.workloadId}`).digest("hex").slice(0, 20)}`; const profileId = `${idPrefix}-${crypto.createHash("sha256").update(`${server.id}:${repository.fullName}:${workload.workloadId}`).digest("hex").slice(0, 20)}`;
return { return {
id: profileId, id: profileId,
name: `${server.name} · ${workload.displayName}`, name: `${server.name} · ${workload.displayName}`,
environment: "production", environment: "production",
provider: "ssh-unraid", provider: "ssh-unraid",
branch: workload.metadata?.branch || repository.defaultBranch || "main", branch: workload.metadata?.branch || repository.defaultBranch || "main",
+5 -5
View File
@@ -288,7 +288,7 @@ function createUnraidPreflightMethods({
connectionCapabilities = connection.capabilities || {}; connectionCapabilities = connection.capabilities || {};
checks.push({ checks.push({
id: "ssh", id: "ssh",
label: "Desktop → Unraid SSH", label: "Desktop Unraid SSH",
status: "pass", status: "pass",
detail: `${server.username}@${server.host}:${server.port}`, detail: `${server.username}@${server.host}:${server.port}`,
}); });
@@ -327,7 +327,7 @@ function createUnraidPreflightMethods({
} catch (error) { } catch (error) {
checks.push({ checks.push({
id: "ssh", id: "ssh",
label: "Desktop → Unraid SSH", label: "Desktop Unraid SSH",
status: "fail", status: "fail",
detail: error.message, detail: error.message,
}); });
@@ -349,7 +349,7 @@ function createUnraidPreflightMethods({
const access = await this.probeServerGitAccess({ repository, profile, server }); const access = await this.probeServerGitAccess({ repository, profile, server });
checks.push({ checks.push({
id: "server-git-access", id: "server-git-access",
label: "Unraid → Gitea read access", label: "Unraid Gitea read access",
status: access.ready ? "pass" : "fail", status: access.ready ? "pass" : "fail",
detail: access.ready detail: access.ready
? `Read-only deploy key verified${access.remoteSha ? ` at ${access.remoteSha.slice(0, 7)}` : ""}.` ? `Read-only deploy key verified${access.remoteSha ? ` at ${access.remoteSha.slice(0, 7)}` : ""}.`
@@ -359,7 +359,7 @@ function createUnraidPreflightMethods({
}); });
} else checks.push({ } else checks.push({
id: "transfer-path", id: "transfer-path",
label: "Desktop → Unraid transfer", label: "Desktop Unraid transfer",
status: "pass", status: "pass",
detail: "Files are copied over the configured server connection. No Gitea credential is required on Unraid.", detail: "Files are copied over the configured server connection. No Gitea credential is required on Unraid.",
}); });
@@ -387,7 +387,7 @@ function createUnraidPreflightMethods({
id: `write-path:${target.id}`, id: `write-path:${target.id}`,
label: target.label, label: target.label,
status: "fail", status: "fail",
detail: `${target.path} · owner ${target.owner || "unknown"}:${target.group || "unknown"} · mode ${target.mode || "unknown"}. ${target.detail}`, detail: `${target.path} · owner ${target.owner || "unknown"}:${target.group || "unknown"} · mode ${target.mode || "unknown"}. ${target.detail}`,
repairAction: "repair-deployment-write-access", repairAction: "repair-deployment-write-access",
repairLabel: "Fix write access", repairLabel: "Fix write access",
}); });
+42 -17
View File
@@ -75,8 +75,14 @@ async function handleShellActions(event, target, action, repository) {
} else if (action === "git-validator-repair") { } else if (action === "git-validator-repair") {
const check = ui.gitValidation?.checks?.[Number(target.dataset.checkIndex)]; const check = ui.gitValidation?.checks?.[Number(target.dataset.checkIndex)];
if (!check?.fixAction) return; 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; 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}`); setLoading(true, `Repairing ${check.title}`);
try { try {
await window.forgeflow.gitValidatorRepair(repository.fullName, check); await window.forgeflow.gitValidatorRepair(repository.fullName, check);
@@ -90,27 +96,46 @@ async function handleShellActions(event, target, action, repository) {
} finally { } finally {
setLoading(false); setLoading(false);
} }
} else if (action === "git-validator-repair-safe") { } else if (action === "git-validator-policy") {
const checks = (ui.gitValidation?.checks || []).filter( setLoading(true, "Applying assurance policy…");
(check) => check.fixAction && check.safe,
);
setLoading(true, `Applying ${checks.length} safe Git fixes…`);
let repaired = 0;
try { try {
for (const check of checks) { await window.forgeflow.gitValidatorSetPolicy(repository.fullName, { id: target.value });
await window.forgeflow.gitValidatorRepair(repository.fullName, check); ui.gitValidation = await window.forgeflow.gitValidatorScan(repository.fullName);
repaired += 1; showToast("Policy updated", ui.gitValidation.policy.label, "success");
}
await refreshRepositories(false, true);
ui.gitValidation = await window.forgeflow.gitValidatorScan(
repository.fullName,
);
showToast("Safe Git fixes applied", `${repaired} repaired.`, "success");
} catch (error) { } catch (error) {
showToast("Safe repair stopped", error.message, "error"); showToast("Policy update failed", error.message, "error");
} finally { } finally {
setLoading(false); 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") { } else if (action === "toggle-favorite") {
ui.boot.state = await window.forgeflow.favoriteRepository( ui.boot.state = await window.forgeflow.favoriteRepository(
repository.fullName, repository.fullName,
+2
View File
@@ -55,6 +55,8 @@ app.addEventListener("change", async (event) => {
} else if (event.target.id === "action-profile-select") { } else if (event.target.id === "action-profile-select") {
ui.selectedProfileId = event.target.value; ui.selectedProfileId = event.target.value;
render(); render();
} else if (event.target.id === "validator-policy") {
await handleShellActions(event, event.target, "git-validator-policy", selectedRepository());
} else if (event.target.id === "profile-provider") { } else if (event.target.id === "profile-provider") {
ui.modal.provider = event.target.value; ui.modal.provider = event.target.value;
render(); render();
+17
View File
@@ -466,6 +466,11 @@ function createMockDeploymentBridge(context) {
checkedAt: iso(), checkedAt: iso(),
score: 78, score: 78,
grade: "Good", 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 }, summary: { passed: 7, warnings: 3, errors: 0, repairable: 2 },
checks: [ 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() { async gitValidatorRepair() {
await wait(180); await wait(180);
return { repaired: true }; return { repaired: true };
+3 -5
View File
@@ -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>`; 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 = const tone =
report.score >= 90 ? "success" : report.score >= 70 ? "warning" : "danger"; 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) => { const groups = report.checks.reduce((grouped, check) => {
(grouped[check.category] ||= []).push(check); (grouped[check.category] ||= []).push(check);
return grouped; 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, groups,
) )
.map( .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 `<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) => { .map((check) => {
const checkIndex = report.checks.indexOf(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>`, .join("")}</div></section>`,
) )
+53
View File
@@ -0,0 +1,53 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const {
normalizePolicy,
validateSuppression,
applyPolicy,
buildTrend,
exportReport,
} = require("../src/main/git-validator-policy.cjs");
test("Git Validator policies enforce score, blockers and enabled checks", () => {
const policy = normalizePolicy({ id: "organization", label: "ITWorx", requiredScore: 88, enabledChecks: ["security", "readme"], blockingChecks: ["security"] });
const governed = applyPolicy([
{ id: "security", status: "warning", category: "Security", weight: 10 },
{ id: "readme", status: "pass", category: "Collaboration", weight: 5 },
{ id: "ignored", status: "error", category: "Other", weight: 5 },
], policy, []);
assert.equal(governed.policy.requiredScore, 88);
assert.deepEqual(governed.checks.map((check) => check.id), ["security", "readme"]);
assert.equal(governed.checks[0].blocking, true);
});
test("suppressions require accountable evidence and reactivate after expiry", () => {
const now = new Date("2026-07-01T00:00:00.000Z");
const suppression = validateSuppression({ checkId: "signed-tags", reason: "Tracked under release hardening", author: "Jens", ticket: "FF-42", expiresAt: "2026-07-08T00:00:00.000Z", scope: "repository", evidence: "sha:abc" }, normalizePolicy({ id: "standard" }), now);
let governed = applyPolicy([{ id: "signed-tags", status: "warning", category: "Governance", weight: 5 }], { id: "standard" }, [suppression], now);
assert.equal(governed.checks[0].suppressed, true);
governed = applyPolicy(governed.checks, { id: "standard" }, [suppression], new Date("2026-07-09T00:00:00.000Z"));
assert.equal(governed.checks[0].suppressed, false);
assert.equal(governed.checks[0].expiredSuppression.id, suppression.id);
assert.throws(() => validateSuppression({ checkId: "x", reason: "short", author: "a", expiresAt: "2026-07-02", evidence: "x" }, normalizePolicy(), now), /requires/);
});
test("trends report new, resolved and regressed findings without false precision", () => {
const previous = { checks: [{ id: "a", status: "warning" }, { id: "b", status: "error" }, { id: "c", status: "pass" }] };
const report = { score: 74, categories: { Security: 50 }, checkedAt: "2026-07-02T00:00:00Z", commitSha: "abc", checks: [{ id: "a", status: "error" }, { id: "b", status: "pass" }, { id: "c", status: "warning", suppressed: true }] };
const trend = buildTrend(previous, report);
assert.deepEqual(trend.regressions, ["a"]);
assert.deepEqual(trend.resolved.sort(), ["b"]);
assert.deepEqual(trend.suppressions, ["c"]);
});
test("reports export as JSON, Markdown and standalone escaped HTML", () => {
const report = { repository: "jens/<app>", score: 80, commitSha: "abc", policy: { label: "Production" }, checks: [{ id: "readme", category: "Collaboration", status: "pass", detail: "Safe & ready" }] };
assert.doesNotThrow(() => JSON.parse(exportReport(report, "json").content));
assert.match(exportReport(report, "markdown").content, /\| readme \|/);
const html = exportReport(report, "html").content;
assert.match(html, /<!doctype html>/);
assert.match(html, /jens\/&lt;app&gt;/);
});
+4 -2
View File
@@ -221,12 +221,14 @@ test("Git Validator exposes scored best-practice checks and bounded repairs", as
"utf8", "utf8",
); );
assert.match(renderer, /function renderGitValidator/); assert.match(renderer, /function renderGitValidator/);
assert.match(renderer, /git-validator-repair-safe/); assert.match(renderer, /gitValidatorPreviewRepair/);
assert.match(renderer, /check\.safe/); assert.match(renderer, /git-validator-suppress/);
assert.match(renderer, /git-validator-policy/);
assert.match(styles, /\.validator-score/); assert.match(styles, /\.validator-score/);
assert.match(styles, /@container \(max-width: 900px\)/); assert.match(styles, /@container \(max-width: 900px\)/);
assert.match(preload, /gitValidatorScan/); assert.match(preload, /gitValidatorScan/);
assert.match(preload, /gitValidatorRepair/); assert.match(preload, /gitValidatorRepair/);
assert.match(preload, /gitValidatorExport/);
}); });
test("renderer guards accessible names, labels and uncertain inventory evidence", async () => { test("renderer guards accessible names, labels and uncertain inventory evidence", async () => {