chore(sonar): remove the rejected Tinqer spike, clear the lint debt, and report measures in CI (#261) #262

Merged
bermudalamb merged 1 commits from chore/261-sonarqube-cleanup into main 2026-09-01 13:41:58 -05:00
11 changed files with 240 additions and 641 deletions
+18
View File
@@ -227,6 +227,24 @@ jobs:
if: always()
run: node scripts/summarize-playwright.js frontend/playwright-results.json
# The measures, printed into the log because that is the only place this
# project can read them. SonarQube 9.9 Community has no Bearer auth, so the
# official MCP cannot connect, and the host is a CI secret — so security
# hotspots, duplication, debt and coverage lived solely on a dashboard, and
# "reduce the debt" was an instruction nobody could act on without opening a
# browser. The scanner masks the URL and token; the measures are not secret.
#
# Deliberately not guarded with continue-on-error. The script exits 0 on
# every path, so it cannot fail the job anyway, and guarding it would
# oblige it to appear in the gate below — which exists to fail the job,
# the opposite of what a report should do. See #261.
- name: Report SonarQube measures
if: always()
env:
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: node scripts/summarize-sonar.js
# Last, so a failing step still produces coverage, a scan and every
# summary first. Without this step continue-on-error above would turn a
# failing suite into a passing job, which is the one way this change could
-554
View File
@@ -26,8 +26,6 @@
},
"devDependencies": {
"@eslint/js": "^9.39.5",
"@tinqerjs/pg-promise-adapter": "^0.0.27",
"@tinqerjs/tinqer": "^0.0.27",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.21",
@@ -43,7 +41,6 @@
"eslint-plugin-sonarjs": "^4.2.0",
"globals": "^17.11.0",
"jest": "^29.7.0",
"pg-promise": "^12.7.1",
"supertest": "^7.0.0",
"ts-jest": "^29.2.4",
"tsx": "^4.16.5",
@@ -635,40 +632,6 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild-kit/core-utils": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
@@ -2748,28 +2711,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz",
"integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.3"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4",
"@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
@@ -2783,358 +2724,6 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@oxc-parser/binding-android-arm-eabi": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.134.0.tgz",
"integrity": "sha512-N9Us7l/X9ZC3LA6eWSzPyduvBPXV1eRyDPwM6/UWpxwwXGsatb8131+d2L8UsmyHrixnKLHBd6UeH8wangV7fw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-android-arm64": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.134.0.tgz",
"integrity": "sha512-Ic2oPZESeCaD4+9cKRqp1GMYsTO9Q3Yi9HdY2x9x75ozbnC20sybFHzeBklmaVD9PBzd8KbkmNN0gy+SVlm7zw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-darwin-arm64": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.134.0.tgz",
"integrity": "sha512-1z9+nVJ1Awq4CPyHAthx5zOUrg5T1zc3dWt6juxwDcuejFGbdYzWJITkS1rv4DCdSTphDU3IW71MzyLV3BjRGQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-darwin-x64": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.134.0.tgz",
"integrity": "sha512-MpofofaRZnxmYrY3lE8RTLHmE1KkX2q0elEeJ8sMcLbS8At76BjYSL6axssqhx29prGGDzIZ5lYFD+IXqaTzFA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-freebsd-x64": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.134.0.tgz",
"integrity": "sha512-OqnPQY27vqWAbMnHfLcF8CVOUv2cCvdlTiqyK5qz5WCbH3XOLpYVQATv8S5UrR8bbEJCAqJLsI7W6cRFXAxCoA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-linux-arm-gnueabihf": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.134.0.tgz",
"integrity": "sha512-0eqWl+PWrcwGC3b8DCB58w3QINAuZfpX2ULTGpI01GUMBc6zDKSpttWxvqPIydxuQEkGQTQRAXLLvc+vTDZbQw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-linux-arm-musleabihf": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.134.0.tgz",
"integrity": "sha512-YToasuDmyzpyTC1ztrvkaSXz8tP+YUbx041M/4SGxaRGiyMzsKkQ869KPUTGA57A6aVsb1/DiPX8XZQQeSFkiw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-linux-arm64-gnu": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.134.0.tgz",
"integrity": "sha512-qMS7NLc2o8G7LLz53wisol+O7/YbMhtaGVhlfsTVLnrraf9kLFgzpiGjtQALLbdxX8uhx0Zmd4l+vY3s1/K4Gg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-linux-arm64-musl": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.134.0.tgz",
"integrity": "sha512-jUEsnxPXhrCYxswQLYvUOxZEE5UWFMkK5kBnrcPMj7TONz1pD0yKgPmOQCAx2LPoysJ/v2Sjg4RBUVoO2VXoZQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-linux-ppc64-gnu": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.134.0.tgz",
"integrity": "sha512-FU5xMUsXnMuWKVCGo43c6SJsnqHcsioqm32PHdDY39cIRJa/AZbo7RMYW0W5gYcNZqb8EMhELkMDwbJOFbUhtg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-linux-riscv64-gnu": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.134.0.tgz",
"integrity": "sha512-tPc7OAhslHVmNebho1RSEGL//7i7Nm39gbQ+gYreBYwzvDVqNwdQH3S13ccM+R1TpimQ+3bIyFMfnilJIGRtjQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-linux-riscv64-musl": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.134.0.tgz",
"integrity": "sha512-TtWEC4MUAHodX5a5kGsmK8g5K49V5ewWfwGrXdbw+gXWLXWXuhi+ectNELmOvYIwaqPSTAry1HNS/hfXssaPHQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-linux-s390x-gnu": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.134.0.tgz",
"integrity": "sha512-mF4uls8TA8SPXSDLpclJ6z9S+vaeUnNp95iUEfqwv9oVJUAE7B2j4crOj8UvByRXpbO5N+aAlJadQEyFlnXU6g==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-linux-x64-gnu": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.134.0.tgz",
"integrity": "sha512-hieAQplyJeCvzqdSAxpOOGvVCBVXo/ioIBxioHfsUrQu93Et7Hy52cCG/GHEnjImVyeqEIViyyjuB0nKghxz/w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-linux-x64-musl": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.134.0.tgz",
"integrity": "sha512-OQnJAK4sFuNSLgsh2s/K+14a3kwbmqf2yh1JiADU9XSfDuRooZYbAxmmBVPiyQ97+jBIIA1x2oPZeYNto3Ioow==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-openharmony-arm64": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.134.0.tgz",
"integrity": "sha512-RYLooe6g31q/PqNFN0NjR80IK/ARGsfLasAXL42LXonL+5Cyy9Or76rjBKQLAjERikJwbRU/sYW9Q5Tnpt1A4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-wasm32-wasi": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.134.0.tgz",
"integrity": "sha512-h8bmHyvc0PxA811prUjfVpJlQAurOOiRbohY4QNGjCEz+L72G9CmWl28OOz3mevrYlURgUsprNuH8hDJXh1VOw==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@napi-rs/wasm-runtime": "^1.1.4"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-win32-arm64-msvc": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.134.0.tgz",
"integrity": "sha512-EPTfanpBMLNnSAWCDYpbJp1stmsf5x6hMsAwymf2J8ylRupIvO6FtKmHBMdc/wt43y08iz6ILlzFGIXe32/Kbw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-win32-ia32-msvc": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.134.0.tgz",
"integrity": "sha512-yAwetF+fTr9lTMtmqvkkWiiMXvH/yjMxGzUDG2rTL/LV1lL37eZ2enUGIlIo0gwhBIKWgabDEidtil+kiqNpgQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-win32-x64-msvc": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.134.0.tgz",
"integrity": "sha512-4VY39G5tuGlBYiH13XbWqfLcwKygQEv0iyf8vtZ5NyLAGjI8M0MiYyLhj91GkWRznr+5VC0I+5R55HUDV/Rklw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-project/types": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.134.0.tgz",
"integrity": "sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@paralleldrive/cuid2": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
@@ -3177,37 +2766,6 @@
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="
},
"node_modules/@tinqerjs/pg-promise-adapter": {
"version": "0.0.27",
"resolved": "https://registry.npmjs.org/@tinqerjs/pg-promise-adapter/-/pg-promise-adapter-0.0.27.tgz",
"integrity": "sha512-GF2KDUWmavLDCrYRhisvwGR5cah5sSuo3loSunUVTITGvYivlrYl58zOwBKMkC0WfiVyrJ5nVAJ3E/Spt6NH9A==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@tinqerjs/tinqer": "^0.0.27"
}
},
"node_modules/@tinqerjs/tinqer": {
"version": "0.0.27",
"resolved": "https://registry.npmjs.org/@tinqerjs/tinqer/-/tinqer-0.0.27.tgz",
"integrity": "sha512-Rf9zNwstFD1mb8BO6RkRatPbx0rJ0VXHrS9pk8doeVv6sCTgo6wyFR/8hQPYe1LTblJUV3SFQNAfJpvsVbNgrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"oxc-parser": "0.134.0"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -4103,16 +3661,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/assert-options": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.3.tgz",
"integrity": "sha512-s6v4HnA+vYSGO4eZX+F+I3gvF74wPk+m6Z1Q3w1Dsg4Pnv/R24vhKAasoMVZGvDpOOfTg1Qz4ptZnEbuy95XsQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -8279,44 +7827,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/oxc-parser": {
"version": "0.134.0",
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.134.0.tgz",
"integrity": "sha512-Hs8fRG6A94BzMrMkGOtrUS7JQjmslfF+IvIXslf3QURzK3ud0QmFJRiYZjTe4TzAQnTfvlk4AwZnqIbrUjiE4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "^0.134.0"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
"@oxc-parser/binding-android-arm-eabi": "0.134.0",
"@oxc-parser/binding-android-arm64": "0.134.0",
"@oxc-parser/binding-darwin-arm64": "0.134.0",
"@oxc-parser/binding-darwin-x64": "0.134.0",
"@oxc-parser/binding-freebsd-x64": "0.134.0",
"@oxc-parser/binding-linux-arm-gnueabihf": "0.134.0",
"@oxc-parser/binding-linux-arm-musleabihf": "0.134.0",
"@oxc-parser/binding-linux-arm64-gnu": "0.134.0",
"@oxc-parser/binding-linux-arm64-musl": "0.134.0",
"@oxc-parser/binding-linux-ppc64-gnu": "0.134.0",
"@oxc-parser/binding-linux-riscv64-gnu": "0.134.0",
"@oxc-parser/binding-linux-riscv64-musl": "0.134.0",
"@oxc-parser/binding-linux-s390x-gnu": "0.134.0",
"@oxc-parser/binding-linux-x64-gnu": "0.134.0",
"@oxc-parser/binding-linux-x64-musl": "0.134.0",
"@oxc-parser/binding-openharmony-arm64": "0.134.0",
"@oxc-parser/binding-wasm32-wasi": "0.134.0",
"@oxc-parser/binding-win32-arm64-msvc": "0.134.0",
"@oxc-parser/binding-win32-ia32-msvc": "0.134.0",
"@oxc-parser/binding-win32-x64-msvc": "0.134.0"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -8523,17 +8033,6 @@
"integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
"license": "MIT"
},
"node_modules/pg-cursor": {
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.22.0.tgz",
"integrity": "sha512-knzXLKqarTjOvb3qDSW0JiGsazmxwEKXrqHfWRte7XUsOYccQRafn3BLnQobWwInkzFJSyOej8y8cQRh2z3kGw==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"pg": "^8"
}
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
@@ -8543,16 +8042,6 @@
"node": ">=4.0.0"
}
},
"node_modules/pg-minify": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/pg-minify/-/pg-minify-1.8.0.tgz",
"integrity": "sha512-jO/oJOununpx8DzKgvSsWm61P8JjwXlaxSlbbfTBo1nvSWoo/+I6qZYaSN96jm/KDwa5d+JMQwPGgcP6HXDRow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
@@ -8562,45 +8051,12 @@
"pg": ">=8.0"
}
},
"node_modules/pg-promise": {
"version": "12.7.1",
"resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-12.7.1.tgz",
"integrity": "sha512-tsemfuBRsHMGaFZKMpkmCAA9iLeHXuuTq+0DucJwJyftyXk7crlMFXq7SY0AOVelIrdSL8qwdl5omfj/R8JN6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"assert-options": "0.8.3",
"pg": "8.23.0",
"pg-minify": "1.8.0",
"spex": "4.1.0"
},
"engines": {
"node": ">=16.0"
},
"peerDependencies": {
"pg-query-stream": "4.17.0"
}
},
"node_modules/pg-protocol": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz",
"integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==",
"license": "MIT"
},
"node_modules/pg-query-stream": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/pg-query-stream/-/pg-query-stream-4.17.0.tgz",
"integrity": "sha512-69bug2CH/AJi1BN8r08eZ8tAGdbZZVp9kB0FG9OC2VkJPHIyj7d3Nk9V8tmporO60wqeWvhn7qtQ5oNimwlFug==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"pg-cursor": "^2.22.0"
},
"peerDependencies": {
"pg": "^8"
}
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
@@ -9285,16 +8741,6 @@
"source-map": "^0.6.0"
}
},
"node_modules/spex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/spex/-/spex-4.1.0.tgz",
"integrity": "sha512-ktgNAQ1X9x1A3IMChM6XBDeVjhGPbLgPQ8aEzGOaUIhZTnLeJSBApvi3gXT789hee6h73N3jOeWkXDwoPbYT/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
-3
View File
@@ -45,8 +45,6 @@
},
"devDependencies": {
"@eslint/js": "^9.39.5",
"@tinqerjs/pg-promise-adapter": "^0.0.27",
"@tinqerjs/tinqer": "^0.0.27",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.21",
@@ -62,7 +60,6 @@
"eslint-plugin-sonarjs": "^4.2.0",
"globals": "^17.11.0",
"jest": "^29.7.0",
"pg-promise": "^12.7.1",
"supertest": "^7.0.0",
"ts-jest": "^29.2.4",
"tsx": "^4.16.5",
+7 -10
View File
@@ -43,19 +43,11 @@ import { promises as fs } from 'fs';
import path from 'path';
import { pool } from './db';
import { needsProcessing, reencodeInPlace } from './imageProcessing';
import { typeForExtension } from './uploadTypes';
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/app/uploads';
const APPLY = process.argv.includes('--apply');
// The stored extension is the file's real type — uploadTypes.ts derives it from
// the validated content type on the way in, so it can be trusted on the way
// back out. Anything else is a file this application would refuse to serve.
const TYPE_FOR_EXTENSION: Readonly<Record<string, string>> = {
'.jpg': 'image/jpeg',
'.png': 'image/png',
'.webp': 'image/webp'
};
interface Totals {
seen: number;
missing: number;
@@ -78,7 +70,12 @@ async function handleRow(imagePath: string, totals: Totals): Promise<void> {
// basename only: image_path is '/uploads/<name>', and the directory it is
// served from is a server constant rather than part of the stored value.
const filePath = path.join(UPLOADS_DIR, path.basename(imagePath));
const mimetype = TYPE_FOR_EXTENSION[path.extname(filePath).toLowerCase()];
// typeForExtension rather than a copy of its table. The stored extension is
// the file's real type — uploadTypes.ts derives it from the validated content
// type on the way in — and this rewrites stored images, so a private copy
// drifting from the real one would silently skip files it should re-encode.
// It lowercases its own input, so the call site does not.
const mimetype = typeForExtension(path.extname(filePath));
if (!mimetype) {
console.warn(`[backfill] unrecognised extension, skipping: ${imagePath}`);
@@ -5,7 +5,7 @@
// match with a count equality, and array parameters. If this cannot be said
// cleanly, nothing else in the conversion matters.
import { SQL, and, eq, gte, lte, sql, inArray, exists } from 'drizzle-orm';
import { SQL, and, gte, lte, sql, inArray, exists } from 'drizzle-orm';
import { items, itemTags, favorites, categories } from './schema';
export interface SpikeFilters {
+1 -1
View File
@@ -77,7 +77,7 @@ export const tags = pgTable("tags", {
name: text().notNull(),
color: text().notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
}, (table) => [
}, () => [
uniqueIndex("tags_name_uniq").using("btree", sql`lower(name)`),
]);
-71
View File
@@ -1,71 +0,0 @@
// Spike (#216): what Tinqer can and cannot express, recorded as runnable cases.
//
// Every case is wrapped in a function rather than evaluated at module level.
// defineSelect() parses the lambda eagerly and THROWS on a shape it cannot
// handle, so a top-level call turns an unsupported query into an import-time
// crash. That is itself a finding worth keeping: the failure is a runtime throw
// at load, not a type error, so an unsupported query compiles cleanly and takes
// the process down when the module is first required.
import { createSchema, defineSelect } from '@tinqerjs/tinqer';
import { toSql } from '@tinqerjs/pg-promise-adapter';
interface Db {
items: { id: number; name: string; price_cents: number; status: string; category_id: number | null };
categories: { id: number; parent_id: number | null };
}
const schema = createSchema<Db>();
export type Outcome = { label: string; ok: boolean; sql?: string; error?: string };
function attempt(label: string, build: () => unknown, params: Record<string, unknown>): Outcome {
try {
const r = toSql(build() as never, params as never);
return { label, ok: true, sql: r.sql };
} catch (e) {
return { label, ok: false, error: (e as Error).message.split('\n')[0] };
}
}
export function runCases(): Outcome[] {
return [
// Supported.
attempt(
'compound condition',
() =>
defineSelect(schema, (q, p: { min: number; max: number }) =>
q.from('items').where((i) => i.price_cents >= p.min && i.price_cents <= p.max).select((i) => ({ id: i.id }))
),
{ min: 1, max: 2 }
),
attempt(
'array membership',
() =>
defineSelect(schema, (q, p: { statuses: string[] }) =>
q.from('items').where((i) => p.statuses.includes(i.status)).select((i) => ({ id: i.id }))
),
{ statuses: ['available', 'sold'] }
),
// NOT supported — and these are the two shapes buildItemFilterSql needs.
attempt(
'ternary (clause optional at run time)',
() =>
defineSelect(schema, (q, p: { apply: boolean; min: number }) =>
q.from('items').where((i) => (p.apply ? i.price_cents >= p.min : true)).select((i) => ({ id: i.id }))
),
{ apply: true, min: 1 }
),
attempt(
'block body with if (clause optional at run time)',
() =>
defineSelect(schema, (q, p: { apply: boolean; min: number }) => {
let x = q.from('items');
if (p.apply) x = x.where((i) => i.price_cents >= p.min);
return x.select((i) => ({ id: i.id }));
}),
{ apply: true, min: 1 }
)
];
}
+5
View File
@@ -2,6 +2,11 @@ import { Request, Response, NextFunction } from 'express';
import { pool } from '../db';
declare global {
// A namespace is the only way to spell an Express type augmentation — the
// interface has to merge into the one Express declares, and Express declares
// it inside a namespace. There is no ES module form of this, so the rule is
// disabled here rather than worked around.
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express {
interface Request {
customerId?: number;
+4
View File
@@ -118,6 +118,10 @@ router.post('/:id/set-default', requireCustomer, asyncRoute(async (req: Request,
res.json(rows[0]);
} catch (err) {
await client.query('ROLLBACK');
// Logged like the two catch blocks above it in this file, which this one
// was simply missing. Without it a failed default-address change rolls
// back and returns 500 leaving nothing behind to say why.
console.error(err);
res.status(500).json({ error: 'internal error' });
} finally {
client.release();
+8 -1
View File
@@ -56,8 +56,15 @@ function isPublicStatus(value: string): value is ItemStatus {
return value === 'available' || value === 'reserved' || value === 'sold';
}
// Sorted with an explicit comparator rather than a bare .sort(). The default
// sorts by UTF-16 code unit, which is a perfectly good total order for the
// ASCII status values passed here — but the rule exists because that stops
// being true the moment a non-ASCII value appears, and a set comparison that
// silently depends on its inputs staying ASCII is not worth keeping.
const sameSet = (a: readonly string[], b: readonly string[]) =>
a.length === b.length && [...a].sort().join() === [...b].sort().join();
a.length === b.length &&
[...a].sort((x, y) => x.localeCompare(y)).join() ===
[...b].sort((x, y) => x.localeCompare(y)).join();
// Which preset a status list corresponds to, for showing the control's current
// position. Null means no preference, which each screen renders as its default.
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env node
// Prints the project's SonarQube measures into the job log.
//
// This exists because the numbers are otherwise unreachable. The server is
// SonarQube 9.9 Community: it has no Bearer auth, so the official MCP cannot
// connect, and the host URL is a CI secret, so there is no way to query it from
// a developer's machine either. The measures lived only on a dashboard, which
// made "reduce the debt" an instruction nobody could act on without a browser
// open beside them. See #261.
//
// The scanner masks SONAR_HOST_URL and SONAR_TOKEN in the log. Measures are not
// secret, so they print fine — only the URL they came from is redacted.
//
// Authentication is HTTP Basic with the token as the username and an empty
// password. That is the documented scheme for 9.9; the `Authorization: Bearer`
// form was added later and returns 401 here.
//
// Runs with `if: always()` and deliberately WITHOUT `continue-on-error`. A
// cleanup report that can break a build is worse than no report, so instead of
// being guarded this script simply never fails: every path exits 0 and problems
// are described rather than thrown. Guarding it would also oblige it to be named
// in the workflow's final gate — workflowGate.test.ts asserts that pairing — and
// that gate exists to fail the job, which is the opposite of what is wanted for
// a report. Same shape as the summarize-jest.js steps beside it.
const https = require('https');
const http = require('http');
const fs = require('fs');
const path = require('path');
const HOST = process.env.SONAR_HOST_URL;
const TOKEN = process.env.SONAR_TOKEN;
const PROJECT = process.argv[2] || 'redefined-designs';
// Ordered so the four things the cleanup issue asks about come first, rather
// than alphabetically. Each row is [metric key, label, formatter].
const METRICS = [
['security_hotspots', 'Security hotspots', (v) => v],
['vulnerabilities', 'Vulnerabilities', (v) => v],
['bugs', 'Bugs', (v) => v],
['duplicated_lines_density', 'Duplicated lines', (v) => `${v}%`],
['duplicated_blocks', 'Duplicated blocks', (v) => v],
['sqale_index', 'Technical debt', formatDebt],
['code_smells', 'Code smells', (v) => v],
['coverage', 'Coverage', (v) => `${v}%`],
['line_coverage', 'Line coverage', (v) => `${v}%`],
['branch_coverage', 'Branch coverage', (v) => `${v}%`],
['uncovered_lines', 'Uncovered lines', (v) => v],
['ncloc', 'Lines of code', (v) => v],
['new_coverage', 'Coverage on new code', (v) => `${v}%`],
['new_duplicated_lines_density', 'Duplication on new code', (v) => `${v}%`],
['new_code_smells', 'Code smells on new code', (v) => v]
];
/** sqale_index is minutes. Days here are Sonar's 8-hour working days. */
function formatDebt(minutes) {
const total = Number(minutes);
if (!Number.isFinite(total)) return String(minutes);
const days = Math.floor(total / (8 * 60));
const hours = Math.floor((total % (8 * 60)) / 60);
const mins = total % 60;
return `${total} min (${days}d ${hours}h ${mins}m)`;
}
function get(url) {
return new Promise((resolve) => {
const client = url.startsWith('https:') ? https : http;
const request = client.get(
url,
// The empty password is deliberate: 9.9 expects `token:` rather than a
// password, and omitting the colon sends the token as a username with no
// password field at all, which it rejects.
{ auth: `${TOKEN}:`, timeout: 30000 },
(res) => {
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => resolve({ status: res.statusCode, body }));
}
);
request.on('timeout', () => {
request.destroy();
resolve({ status: 0, body: 'timed out after 30s' });
});
request.on('error', (err) => resolve({ status: 0, body: err.message }));
});
}
function parse(body) {
try {
return JSON.parse(body);
} catch {
return null;
}
}
/**
* Waits for the server to finish processing this run's analysis.
*
* The workflow does not set `sonar.qualitygate.wait`, so the scan step returns
* as soon as the report is uploaded and the server computes measures in the
* background. Querying immediately therefore returns the *previous* analysis,
* which would be indistinguishable from the current one and quietly wrong — the
* worst kind of number to publish into a cleanup report.
*
* The scanner writes the task id to .scannerwork/report-task.txt. Polling it
* costs a few seconds and makes the difference between "these are this commit's
* numbers" and "these are probably this commit's numbers".
*/
async function waitForAnalysis(base) {
const reportPath = path.join(process.cwd(), '.scannerwork', 'report-task.txt');
if (!fs.existsSync(reportPath)) {
return 'no report-task.txt — reporting the last completed analysis, which may predate this commit';
}
const taskId = /^ceTaskId=(.+)$/m.exec(fs.readFileSync(reportPath, 'utf8'))?.[1]?.trim();
if (!taskId) {
return 'no ceTaskId in report-task.txt — reporting the last completed analysis';
}
// Twenty attempts at three seconds. A minute is far longer than this project
// takes to process and short enough not to stretch the job noticeably.
for (let attempt = 0; attempt < 20; attempt++) {
const res = await get(`${base}/api/ce/task?id=${encodeURIComponent(taskId)}`);
const status = parse(res.body)?.task?.status;
if (status === 'SUCCESS') return null;
if (status === 'FAILED' || status === 'CANCELED') {
return `analysis ${status} on the server — reporting the last completed analysis instead`;
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
return 'analysis still processing after 60s — reporting the last completed analysis';
}
async function main() {
if (!HOST || !TOKEN) {
// Not an error. A fork or a run without the secrets configured reaches this
// legitimately, and saying so is more useful than a stack trace.
console.log('SonarQube measures: SONAR_HOST_URL or SONAR_TOKEN not set, skipping.');
return;
}
const base = HOST.replace(/\/+$/, '');
const staleness = await waitForAnalysis(base);
const keys = METRICS.map(([key]) => key).join(',');
const measuresUrl = `${base}/api/measures/component?component=${encodeURIComponent(PROJECT)}&metricKeys=${keys}`;
const res = await get(measuresUrl);
if (res.status !== 200) {
console.log(`SonarQube measures: request failed (status ${res.status}). ${res.body.slice(0, 300)}`);
return;
}
const json = parse(res.body);
const measures = json?.component?.measures;
if (!Array.isArray(measures)) {
console.log('SonarQube measures: unexpected response shape.');
console.log(res.body.slice(0, 300));
return;
}
// Keyed by metric so the display order above is what prints, rather than
// whatever order the API happened to return.
const byKey = new Map(measures.map((m) => [m.metric, m]));
const lines = ['', '=== SonarQube measures ===', ''];
// Said plainly rather than omitted. A reader acting on these numbers needs to
// know whether they describe this commit.
if (staleness) lines.push(`NOTE: ${staleness}`, '');
for (const [key, label, format] of METRICS) {
const measure = byKey.get(key);
// A metric can be legitimately absent — new-code metrics do not exist until
// a second analysis, and branch_coverage is missing when nothing branches.
if (!measure) {
lines.push(`${label.padEnd(26)}`);
continue;
}
const value = measure.period?.value ?? measure.value;
lines.push(`${label.padEnd(26)} ${format(value)}`);
}
const gate = await get(
`${base}/api/qualitygates/project_status?projectKey=${encodeURIComponent(PROJECT)}`
);
const gateStatus = parse(gate.body)?.projectStatus?.status;
lines.push('', `Quality gate ${gateStatus ?? 'unknown'}`, '');
console.log(lines.join('\n'));
}
main().catch((err) => {
// Reported rather than thrown, for the reason in the header: this step must
// not be able to fail the job.
console.log(`SonarQube measures: ${err instanceof Error ? err.message : String(err)}`);
});