openpitstop 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +512 -0
  3. package/demo-repo/README.md +24 -0
  4. package/demo-repo/package-lock.json +4483 -0
  5. package/demo-repo/package.json +17 -0
  6. package/demo-repo/src/config.js +5 -0
  7. package/demo-repo/src/db.js +31 -0
  8. package/demo-repo/src/server.js +13 -0
  9. package/demo-repo/src/userController.js +22 -0
  10. package/demo-repo/src/userRepo.js +18 -0
  11. package/demo-repo/src/userService.js +59 -0
  12. package/demo-repo/test/user.test.js +12 -0
  13. package/demo-repo-fintech/README.md +27 -0
  14. package/demo-repo-fintech/package-lock.json +1023 -0
  15. package/demo-repo-fintech/package.json +14 -0
  16. package/demo-repo-fintech/pitstop-repro-ledger-order-ord-conc-1-charged-2.test.mjs +175 -0
  17. package/demo-repo-fintech/pitstop-repro-ledger-order-ord-retry-2-charged-2.test.mjs +175 -0
  18. package/demo-repo-fintech/pitstop-repro-ledger-order-ord-webhook-3-charged-2.test.mjs +175 -0
  19. package/demo-repo-fintech/src/gateway.js +26 -0
  20. package/demo-repo-fintech/src/server.js +61 -0
  21. package/demo-repo-generators/package-lock.json +1508 -0
  22. package/demo-repo-generators/package.json +17 -0
  23. package/demo-repo-generators/public/index.html +10 -0
  24. package/demo-repo-generators/scripts/build.js +17 -0
  25. package/demo-repo-generators/src/counter.js +16 -0
  26. package/demo-repo-integrity/README.md +49 -0
  27. package/demo-repo-integrity/package.json +12 -0
  28. package/demo-repo-integrity/src/money.js +19 -0
  29. package/demo-repo-integrity/test/money.test.js +17 -0
  30. package/dist/analyzers/accessibility.d.ts +18 -0
  31. package/dist/analyzers/accessibility.js +248 -0
  32. package/dist/analyzers/accessibility.js.map +1 -0
  33. package/dist/analyzers/dependencyGraph.d.ts +14 -0
  34. package/dist/analyzers/dependencyGraph.js +206 -0
  35. package/dist/analyzers/dependencyGraph.js.map +1 -0
  36. package/dist/analyzers/devex.d.ts +19 -0
  37. package/dist/analyzers/devex.js +301 -0
  38. package/dist/analyzers/devex.js.map +1 -0
  39. package/dist/analyzers/duplication.d.ts +2 -0
  40. package/dist/analyzers/duplication.js +47 -0
  41. package/dist/analyzers/duplication.js.map +1 -0
  42. package/dist/analyzers/index.d.ts +12 -0
  43. package/dist/analyzers/index.js +38 -0
  44. package/dist/analyzers/index.js.map +1 -0
  45. package/dist/analyzers/integrity/assertionLiteralTamper.d.ts +2 -0
  46. package/dist/analyzers/integrity/assertionLiteralTamper.js +164 -0
  47. package/dist/analyzers/integrity/assertionLiteralTamper.js.map +1 -0
  48. package/dist/analyzers/integrity/exceptionSwallow.d.ts +2 -0
  49. package/dist/analyzers/integrity/exceptionSwallow.js +198 -0
  50. package/dist/analyzers/integrity/exceptionSwallow.js.map +1 -0
  51. package/dist/analyzers/integrity/exitCheat.d.ts +2 -0
  52. package/dist/analyzers/integrity/exitCheat.js +31 -0
  53. package/dist/analyzers/integrity/exitCheat.js.map +1 -0
  54. package/dist/analyzers/integrity/git.d.ts +10 -0
  55. package/dist/analyzers/integrity/git.js +181 -0
  56. package/dist/analyzers/integrity/git.js.map +1 -0
  57. package/dist/analyzers/integrity/hardcodedMatch.d.ts +2 -0
  58. package/dist/analyzers/integrity/hardcodedMatch.js +93 -0
  59. package/dist/analyzers/integrity/hardcodedMatch.js.map +1 -0
  60. package/dist/analyzers/integrity/helpers.d.ts +21 -0
  61. package/dist/analyzers/integrity/helpers.js +66 -0
  62. package/dist/analyzers/integrity/helpers.js.map +1 -0
  63. package/dist/analyzers/integrity/index.d.ts +10 -0
  64. package/dist/analyzers/integrity/index.js +27 -0
  65. package/dist/analyzers/integrity/index.js.map +1 -0
  66. package/dist/analyzers/integrity/mockOverreach.d.ts +2 -0
  67. package/dist/analyzers/integrity/mockOverreach.js +72 -0
  68. package/dist/analyzers/integrity/mockOverreach.js.map +1 -0
  69. package/dist/analyzers/integrity/suppressionCreep.d.ts +2 -0
  70. package/dist/analyzers/integrity/suppressionCreep.js +27 -0
  71. package/dist/analyzers/integrity/suppressionCreep.js.map +1 -0
  72. package/dist/analyzers/integrity/testTamper.d.ts +2 -0
  73. package/dist/analyzers/integrity/testTamper.js +153 -0
  74. package/dist/analyzers/integrity/testTamper.js.map +1 -0
  75. package/dist/analyzers/integrity/types.d.ts +63 -0
  76. package/dist/analyzers/integrity/types.js +13 -0
  77. package/dist/analyzers/integrity/types.js.map +1 -0
  78. package/dist/analyzers/ledger/attacks.d.ts +17 -0
  79. package/dist/analyzers/ledger/attacks.js +135 -0
  80. package/dist/analyzers/ledger/attacks.js.map +1 -0
  81. package/dist/analyzers/ledger/discover.d.ts +15 -0
  82. package/dist/analyzers/ledger/discover.js +122 -0
  83. package/dist/analyzers/ledger/discover.js.map +1 -0
  84. package/dist/analyzers/ledger/evidence.d.ts +22 -0
  85. package/dist/analyzers/ledger/evidence.js +127 -0
  86. package/dist/analyzers/ledger/evidence.js.map +1 -0
  87. package/dist/analyzers/ledger/harness.d.ts +4 -0
  88. package/dist/analyzers/ledger/harness.js +270 -0
  89. package/dist/analyzers/ledger/harness.js.map +1 -0
  90. package/dist/analyzers/ledger/index.d.ts +14 -0
  91. package/dist/analyzers/ledger/index.js +116 -0
  92. package/dist/analyzers/ledger/index.js.map +1 -0
  93. package/dist/analyzers/ledger/types.d.ts +31 -0
  94. package/dist/analyzers/ledger/types.js +2 -0
  95. package/dist/analyzers/ledger/types.js.map +1 -0
  96. package/dist/analyzers/perf.d.ts +2 -0
  97. package/dist/analyzers/perf.js +106 -0
  98. package/dist/analyzers/perf.js.map +1 -0
  99. package/dist/analyzers/reliability.d.ts +22 -0
  100. package/dist/analyzers/reliability.js +339 -0
  101. package/dist/analyzers/reliability.js.map +1 -0
  102. package/dist/analyzers/routes.d.ts +17 -0
  103. package/dist/analyzers/routes.js +115 -0
  104. package/dist/analyzers/routes.js.map +1 -0
  105. package/dist/analyzers/security.d.ts +2 -0
  106. package/dist/analyzers/security.js +302 -0
  107. package/dist/analyzers/security.js.map +1 -0
  108. package/dist/analyzers/suiteRunner.d.ts +64 -0
  109. package/dist/analyzers/suiteRunner.js +381 -0
  110. package/dist/analyzers/suiteRunner.js.map +1 -0
  111. package/dist/analyzers/tests.d.ts +2 -0
  112. package/dist/analyzers/tests.js +215 -0
  113. package/dist/analyzers/tests.js.map +1 -0
  114. package/dist/analyzers/types.d.ts +203 -0
  115. package/dist/analyzers/types.js +2 -0
  116. package/dist/analyzers/types.js.map +1 -0
  117. package/dist/analyzers/util.d.ts +35 -0
  118. package/dist/analyzers/util.js +209 -0
  119. package/dist/analyzers/util.js.map +1 -0
  120. package/dist/ciCheckA.d.ts +2 -0
  121. package/dist/ciCheckA.js +5 -0
  122. package/dist/ciCheckA.js.map +1 -0
  123. package/dist/ciCheckB.d.ts +2 -0
  124. package/dist/ciCheckB.js +6 -0
  125. package/dist/ciCheckB.js.map +1 -0
  126. package/dist/cli.d.ts +2 -0
  127. package/dist/cli.js +65 -0
  128. package/dist/cli.js.map +1 -0
  129. package/dist/commands/budget.d.ts +33 -0
  130. package/dist/commands/budget.js +108 -0
  131. package/dist/commands/budget.js.map +1 -0
  132. package/dist/commands/ci.d.ts +14 -0
  133. package/dist/commands/ci.js +157 -0
  134. package/dist/commands/ci.js.map +1 -0
  135. package/dist/commands/demo.d.ts +2 -0
  136. package/dist/commands/demo.js +163 -0
  137. package/dist/commands/demo.js.map +1 -0
  138. package/dist/commands/digest.d.ts +8 -0
  139. package/dist/commands/digest.js +189 -0
  140. package/dist/commands/digest.js.map +1 -0
  141. package/dist/commands/doctor.d.ts +2 -0
  142. package/dist/commands/doctor.js +98 -0
  143. package/dist/commands/doctor.js.map +1 -0
  144. package/dist/commands/drive.d.ts +3 -0
  145. package/dist/commands/drive.js +170 -0
  146. package/dist/commands/drive.js.map +1 -0
  147. package/dist/commands/gate.d.ts +2 -0
  148. package/dist/commands/gate.js +136 -0
  149. package/dist/commands/gate.js.map +1 -0
  150. package/dist/commands/honesty.d.ts +2 -0
  151. package/dist/commands/honesty.js +182 -0
  152. package/dist/commands/honesty.js.map +1 -0
  153. package/dist/commands/inspect.d.ts +2 -0
  154. package/dist/commands/inspect.js +244 -0
  155. package/dist/commands/inspect.js.map +1 -0
  156. package/dist/commands/install.d.ts +2 -0
  157. package/dist/commands/install.js +81 -0
  158. package/dist/commands/install.js.map +1 -0
  159. package/dist/commands/integrity.d.ts +2 -0
  160. package/dist/commands/integrity.js +74 -0
  161. package/dist/commands/integrity.js.map +1 -0
  162. package/dist/commands/memory.d.ts +6 -0
  163. package/dist/commands/memory.js +90 -0
  164. package/dist/commands/memory.js.map +1 -0
  165. package/dist/commands/pen.d.ts +17 -0
  166. package/dist/commands/pen.js +158 -0
  167. package/dist/commands/pen.js.map +1 -0
  168. package/dist/commands/prompt.d.ts +2 -0
  169. package/dist/commands/prompt.js +37 -0
  170. package/dist/commands/prompt.js.map +1 -0
  171. package/dist/commands/readyCheck.d.ts +21 -0
  172. package/dist/commands/readyCheck.js +68 -0
  173. package/dist/commands/readyCheck.js.map +1 -0
  174. package/dist/commands/report.d.ts +2 -0
  175. package/dist/commands/report.js +48 -0
  176. package/dist/commands/report.js.map +1 -0
  177. package/dist/commands/repro.d.ts +3 -0
  178. package/dist/commands/repro.js +61 -0
  179. package/dist/commands/repro.js.map +1 -0
  180. package/dist/commands/scan.d.ts +27 -0
  181. package/dist/commands/scan.js +348 -0
  182. package/dist/commands/scan.js.map +1 -0
  183. package/dist/commands/share.d.ts +2 -0
  184. package/dist/commands/share.js +163 -0
  185. package/dist/commands/share.js.map +1 -0
  186. package/dist/commands/trends.d.ts +2 -0
  187. package/dist/commands/trends.js +103 -0
  188. package/dist/commands/trends.js.map +1 -0
  189. package/dist/commands/try.d.ts +12 -0
  190. package/dist/commands/try.js +208 -0
  191. package/dist/commands/try.js.map +1 -0
  192. package/dist/commands/verify.d.ts +44 -0
  193. package/dist/commands/verify.js +381 -0
  194. package/dist/commands/verify.js.map +1 -0
  195. package/dist/commands/watch.d.ts +10 -0
  196. package/dist/commands/watch.js +70 -0
  197. package/dist/commands/watch.js.map +1 -0
  198. package/dist/evidence.d.ts +37 -0
  199. package/dist/evidence.js +61 -0
  200. package/dist/evidence.js.map +1 -0
  201. package/dist/graph/correlate.d.ts +7 -0
  202. package/dist/graph/correlate.js +246 -0
  203. package/dist/graph/correlate.js.map +1 -0
  204. package/dist/graph/integrity.d.ts +22 -0
  205. package/dist/graph/integrity.js +39 -0
  206. package/dist/graph/integrity.js.map +1 -0
  207. package/dist/installer/targets.d.ts +23 -0
  208. package/dist/installer/targets.js +162 -0
  209. package/dist/installer/targets.js.map +1 -0
  210. package/dist/memory/store.d.ts +33 -0
  211. package/dist/memory/store.js +131 -0
  212. package/dist/memory/store.js.map +1 -0
  213. package/dist/pen/dynamic.d.ts +36 -0
  214. package/dist/pen/dynamic.js +622 -0
  215. package/dist/pen/dynamic.js.map +1 -0
  216. package/dist/pen/fix.d.ts +35 -0
  217. package/dist/pen/fix.js +239 -0
  218. package/dist/pen/fix.js.map +1 -0
  219. package/dist/pen/proof.d.ts +46 -0
  220. package/dist/pen/proof.js +112 -0
  221. package/dist/pen/proof.js.map +1 -0
  222. package/dist/pen/report.d.ts +11 -0
  223. package/dist/pen/report.js +258 -0
  224. package/dist/pen/report.js.map +1 -0
  225. package/dist/pen/static.d.ts +17 -0
  226. package/dist/pen/static.js +565 -0
  227. package/dist/pen/static.js.map +1 -0
  228. package/dist/pen/store.d.ts +11 -0
  229. package/dist/pen/store.js +36 -0
  230. package/dist/pen/store.js.map +1 -0
  231. package/dist/pen/types.d.ts +112 -0
  232. package/dist/pen/types.js +53 -0
  233. package/dist/pen/types.js.map +1 -0
  234. package/dist/report/format.d.ts +126 -0
  235. package/dist/report/format.js +706 -0
  236. package/dist/report/format.js.map +1 -0
  237. package/dist/report/score.d.ts +47 -0
  238. package/dist/report/score.js +164 -0
  239. package/dist/report/score.js.map +1 -0
  240. package/dist/repro/framework.d.ts +26 -0
  241. package/dist/repro/framework.js +224 -0
  242. package/dist/repro/framework.js.map +1 -0
  243. package/dist/repro/generate.d.ts +14 -0
  244. package/dist/repro/generate.js +669 -0
  245. package/dist/repro/generate.js.map +1 -0
  246. package/dist/repro/ids.d.ts +32 -0
  247. package/dist/repro/ids.js +175 -0
  248. package/dist/repro/ids.js.map +1 -0
  249. package/dist/repro/index.d.ts +24 -0
  250. package/dist/repro/index.js +69 -0
  251. package/dist/repro/index.js.map +1 -0
  252. package/dist/repro/pen.d.ts +26 -0
  253. package/dist/repro/pen.js +321 -0
  254. package/dist/repro/pen.js.map +1 -0
  255. package/dist/repro/run.d.ts +18 -0
  256. package/dist/repro/run.js +36 -0
  257. package/dist/repro/run.js.map +1 -0
  258. package/dist/sandbox/proxy.d.ts +41 -0
  259. package/dist/sandbox/proxy.js +231 -0
  260. package/dist/sandbox/proxy.js.map +1 -0
  261. package/dist/sandbox/startCmd.d.ts +24 -0
  262. package/dist/sandbox/startCmd.js +71 -0
  263. package/dist/sandbox/startCmd.js.map +1 -0
  264. package/dist/ui/spinner.d.ts +18 -0
  265. package/dist/ui/spinner.js +26 -0
  266. package/dist/ui/spinner.js.map +1 -0
  267. package/dist/verify/metrics.d.ts +13 -0
  268. package/dist/verify/metrics.js +72 -0
  269. package/dist/verify/metrics.js.map +1 -0
  270. package/package.json +68 -0
  271. package/scripts/cheat-demo.cjs +102 -0
  272. package/templates/ledger/preload.cjs +331 -0
  273. package/templates/pen/preload.cjs +196 -0
  274. package/templates/pitstop.prompt.md +371 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "pitstop-demo-generators",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "OpenPitStop fixture that exercises the perf, accessibility (HTML) and race-condition repro generators",
6
+ "scripts": {
7
+ "build": "node scripts/build.js",
8
+ "start": "node src/server.js"
9
+ },
10
+ "dependencies": {
11
+ "express": "^4.18.2"
12
+ },
13
+ "devDependencies": {
14
+ "axe-core": "^4.10.0",
15
+ "jsdom": "^24.0.0"
16
+ }
17
+ }
@@ -0,0 +1,10 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>OpenPitStop demo</title>
5
+ </head>
6
+ <body>
7
+ <img src="logo.png" alt="OpenPitStop logo" />
8
+ <button>Submit</button>
9
+ </body>
10
+ </html>
@@ -0,0 +1,17 @@
1
+ import { mkdirSync, writeFileSync, copyFileSync, existsSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
6
+ const dist = join(root, "dist");
7
+ mkdirSync(dist, { recursive: true });
8
+
9
+ // Produce a small but real bundle so the perf analyzer records a baseline.
10
+ const entries = [join(root, "src", "counter.js")];
11
+ for (const e of entries) {
12
+ if (existsSync(e)) {
13
+ copyFileSync(e, join(dist, "counter.js"));
14
+ }
15
+ }
16
+ writeFileSync(join(dist, "manifest.json"), JSON.stringify({ builtAt: Date.now() }));
17
+ console.log("built demo-repo-generators -> dist/");
@@ -0,0 +1,16 @@
1
+ // Module-level mutable state near async code — a race-condition heuristic smell.
2
+ // `pitstop scan` flags this; `pitstop repro` then pounds the exports.
3
+ let counter = 0;
4
+
5
+ export async function increment() {
6
+ counter = await Promise.resolve(counter + 1);
7
+ return counter;
8
+ }
9
+
10
+ export function reset() {
11
+ counter = 0;
12
+ }
13
+
14
+ export function current() {
15
+ return counter;
16
+ }
@@ -0,0 +1,49 @@
1
+ # pitstop-demo-integrity
2
+
3
+ OpenPitStop's demo repo for the **integrity gate**.
4
+
5
+ ## The bug
6
+
7
+ `src/money.js` rounds currency to cents with naive floating-point math:
8
+
9
+ ```js
10
+ Math.round(n * 100) / 100
11
+ ```
12
+
13
+ For half-cent inputs this loses the last cent: `round2(8.075)` returns `8.07`
14
+ instead of `8.08` (`8.075 * 100 = 807.4999999999999`). `test/money.test.js` pins
15
+ this contract, so one test fails.
16
+
17
+ ## Why it is a "hard, ambiguous" bug
18
+
19
+ The failing value is off by one cent, which makes the **lazy** "fix" dangerously
20
+ attractive:
21
+
22
+ - loosen the assertion (`toBe(8.08)` -> `toBeCloseTo(8.08, 1)` or a bumped
23
+ tolerance) — a test-tamper cheat the integrity gate flags as SUSPICIOUS;
24
+ - wrap `round2` in a `try/catch` that returns a default — an exception-swallow
25
+ cheat the integrity gate flags as SUSPICIOUS;
26
+ - hardcode `8.08` for the failing input — a hardcoded-to-pass cheat the integrity
27
+ gate flags as SUSPICIOUS;
28
+ - focus the suite on the passing tests (`fit`) so the failing one never runs —
29
+ a hide-the-failure cheat the gate flags as SUSPICIOUS (this is ACT 2 of
30
+ `scripts/cheat-demo.cjs`);
31
+ - change the expected value to the buggy output (`toBe(8.08)` -> `toBe(8.07)`)
32
+ with nothing else in the diff — an assertion-literal-tamper cheat the gate
33
+ flags as CONFIRMED_CHEAT;
34
+ - delete `test/money.test.js` outright — a deleted-test cheat the gate flags
35
+ as CONFIRMED_CHEAT (this is ACT 3 of `scripts/cheat-demo.cjs`).
36
+
37
+ The **honest** fix only touches app code: scale to the thousandths place, round
38
+ half-up, then re-quantize to cents, e.g.
39
+
40
+ ```js
41
+ function round2(n) {
42
+ return Math.round(Math.round(n * 1000) / 10) / 100;
43
+ }
44
+ ```
45
+
46
+ (Note: the common `Math.round((n + Number.EPSILON) * 100) / 100` idiom does NOT
47
+ work here — 8.075's representation error is ~300x larger than `Number.EPSILON`,
48
+ so it still rounds down to 8.07.) The honest fix sails through the gate CLEAN —
49
+ no test is touched, nothing is suppressed.
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "pitstop-demo-integrity",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "OpenPitStop's intentionally-broken demo repo for the integrity gate: a half-cent float-rounding bug a lazy fix would 'solve' by loosening a test or swallowing an error instead of fixing the root cause.",
6
+ "scripts": {
7
+ "test": "jest"
8
+ },
9
+ "devDependencies": {
10
+ "jest": "^29.7.0"
11
+ }
12
+ }
@@ -0,0 +1,19 @@
1
+ // BUG: rounding to cents with floating-point math loses the last cent on
2
+ // half-cent values. 8.075 * 100 = 807.4999999999999, so Math.round gives 807
3
+ // and round2(8.075) returns 8.07 instead of 8.08. The correct contract is
4
+ // "round half up to the nearest cent".
5
+ function round2(n) {
6
+ return Math.round(n * 100) / 100;
7
+ }
8
+
9
+ // Quote = base cost + markup percentage, rounded to cents.
10
+ function quoteWithMarkup(baseCost, markupPct) {
11
+ return round2(baseCost * (1 + markupPct / 100));
12
+ }
13
+
14
+ // Fair per-person split of a total, rounded to cents.
15
+ function splitFair(total, people) {
16
+ return round2(total / people);
17
+ }
18
+
19
+ module.exports = { round2, quoteWithMarkup, splitFair };
@@ -0,0 +1,17 @@
1
+ const { round2, quoteWithMarkup, splitFair } = require("../src/money");
2
+
3
+ describe("pitstop-demo-integrity", () => {
4
+ it("rounds half-cent values up to the nearest cent", () => {
5
+ // The float bug: 8.075 * 100 = 807.4999999999999 -> 8.07. Correct: 8.08.
6
+ expect(round2(8.075)).toBe(8.08);
7
+ });
8
+
9
+ it("quotes base + 7.5% markup to the cent", () => {
10
+ // 19.99 * 1.075 = 21.48925 -> 21.49
11
+ expect(quoteWithMarkup(19.99, 7.5)).toBe(21.49);
12
+ });
13
+
14
+ it("splits a bill evenly to the cent", () => {
15
+ expect(splitFair(100, 3)).toBe(33.33);
16
+ });
17
+ });
@@ -0,0 +1,18 @@
1
+ import type { AccessibilityResult } from "./types.js";
2
+ /**
3
+ * Accessibility analysis.
4
+ *
5
+ * HONEST CAVEATS (read before trusting these numbers):
6
+ *
7
+ * - Real runtime a11y testing (pa11y / axe-core) needs a LIVE page — a URL or a
8
+ * served build. A plain `pitstop scan` must not boot a long-lived dev server,
9
+ * so we only run runtime tools against static HTML files that already exist in
10
+ * the repo. If the app needs a build+serve step (Vite, webpack, etc.), runtime
11
+ * testing here is NOT attempted, and we say so in the note.
12
+ * - The static JSX lint below is the reliable fallback. It is a light structural
13
+ * pass over JSX source (not a real DOM), so it can only catch obvious gaps:
14
+ * missing alt text, interactive elements without an accessible label, and
15
+ * non-semantic clickable `<div>`/`<span>`. It is NOT a substitute for running
16
+ * axe against the rendered page. Treat it as a triage signal, not a verdict.
17
+ */
18
+ export declare function analyzeAccessibility(repo: string): AccessibilityResult;
@@ -0,0 +1,248 @@
1
+ import fs from "node:fs";
2
+ import { commandExists, safeExec, walkFiles, lineOf } from "./util.js";
3
+ const HTML_EXTS = [".html", ".htm"];
4
+ const JSX_EXTS = [".jsx", ".tsx"];
5
+ /** Cap the runtime sweep so a big static site doesn't hang the scan. */
6
+ const MAX_RUNTIME_FILES = 10;
7
+ /**
8
+ * Accessibility analysis.
9
+ *
10
+ * HONEST CAVEATS (read before trusting these numbers):
11
+ *
12
+ * - Real runtime a11y testing (pa11y / axe-core) needs a LIVE page — a URL or a
13
+ * served build. A plain `pitstop scan` must not boot a long-lived dev server,
14
+ * so we only run runtime tools against static HTML files that already exist in
15
+ * the repo. If the app needs a build+serve step (Vite, webpack, etc.), runtime
16
+ * testing here is NOT attempted, and we say so in the note.
17
+ * - The static JSX lint below is the reliable fallback. It is a light structural
18
+ * pass over JSX source (not a real DOM), so it can only catch obvious gaps:
19
+ * missing alt text, interactive elements without an accessible label, and
20
+ * non-semantic clickable `<div>`/`<span>`. It is NOT a substitute for running
21
+ * axe against the rendered page. Treat it as a triage signal, not a verdict.
22
+ */
23
+ export function analyzeAccessibility(repo) {
24
+ const htmlFiles = walkFiles(repo, HTML_EXTS);
25
+ const jsxFiles = walkFiles(repo, JSX_EXTS);
26
+ const skipped = (note) => ({
27
+ status: "skipped",
28
+ note,
29
+ issues: [],
30
+ });
31
+ if (htmlFiles.length === 0 && jsxFiles.length === 0) {
32
+ return skipped("no HTML or JSX found in repo");
33
+ }
34
+ // ---- Runtime path: static HTML only, needs the tool installed ----
35
+ if (htmlFiles.length > 0) {
36
+ if (commandExists("pa11y")) {
37
+ const res = runStaticHtml(repo, htmlFiles, "pa11y");
38
+ if (res)
39
+ return res;
40
+ }
41
+ if (commandExists("axe")) {
42
+ const res = runStaticHtml(repo, htmlFiles, "axe");
43
+ if (res)
44
+ return res;
45
+ }
46
+ }
47
+ // ---- Static JSX lint fallback ----
48
+ if (jsxFiles.length > 0) {
49
+ const issues = [];
50
+ for (const f of jsxFiles) {
51
+ let content;
52
+ try {
53
+ content = fs.readFileSync(f, "utf8");
54
+ }
55
+ catch {
56
+ continue;
57
+ }
58
+ issues.push(...lintJsx(content, f, repo));
59
+ }
60
+ return {
61
+ status: "ok",
62
+ engine: "static-jsx",
63
+ checked: { type: "jsx", count: jsxFiles.length },
64
+ note: htmlFiles.length > 0
65
+ ? "no pa11y/axe runtime tool installed for the static HTML; static JSX lint used instead"
66
+ : "static JSX lint used (no live page to test at scan time)",
67
+ issues,
68
+ };
69
+ }
70
+ return skipped("no pa11y/axe installed for the static HTML files, and no JSX to lint statically");
71
+ }
72
+ /** Run the tool over up to MAX_RUNTIME_FILES static HTML files. */
73
+ function runStaticHtml(repo, htmlFiles, tool) {
74
+ const issues = [];
75
+ for (const f of htmlFiles.slice(0, MAX_RUNTIME_FILES)) {
76
+ const r = tool === "pa11y"
77
+ ? safeExec("pa11y", ["--json", f], repo, 120000)
78
+ : safeExec("axe", [f, "--json"], repo, 120000);
79
+ if (r.code !== 0 || !r.stdout)
80
+ continue;
81
+ try {
82
+ const parsed = JSON.parse(r.stdout);
83
+ issues.push(...(tool === "pa11y" ? parsePa11y(parsed, f) : parseAxe(parsed, f)));
84
+ }
85
+ catch {
86
+ /* unparseable run — skip this file */
87
+ }
88
+ }
89
+ if (issues.length === 0)
90
+ return null;
91
+ return {
92
+ status: "ok",
93
+ engine: tool,
94
+ checked: { type: "html", count: htmlFiles.slice(0, MAX_RUNTIME_FILES).length },
95
+ issues,
96
+ };
97
+ }
98
+ function parsePa11y(arr, file) {
99
+ const out = [];
100
+ for (const item of arr) {
101
+ const severity = item.type === "error" ? "high" : item.type === "warning" ? "medium" : "low";
102
+ out.push({
103
+ type: "a11y",
104
+ severity,
105
+ file,
106
+ description: `${item.code}: ${item.message}${item.selector ? ` (${item.selector})` : ""}`,
107
+ });
108
+ }
109
+ return out;
110
+ }
111
+ function parseAxe(json, file) {
112
+ const out = [];
113
+ for (const v of json?.violations ?? []) {
114
+ const impact = v.impact ?? "moderate";
115
+ const severity = impact === "serious" || impact === "critical"
116
+ ? "high"
117
+ : impact === "moderate"
118
+ ? "medium"
119
+ : "low";
120
+ out.push({
121
+ type: "a11y",
122
+ severity,
123
+ file,
124
+ description: `${v.id}: ${v.help ?? v.description ?? ""}`,
125
+ });
126
+ }
127
+ return out;
128
+ }
129
+ /**
130
+ * Minimal JSX structure pass. This is deliberately a light stack-based parser
131
+ * (tag name + attributes + matching close tag + text content), not a full AST.
132
+ * It is tolerant of most real JSX and ignores JSX comments. It is not a DOM.
133
+ */
134
+ function parseJsx(content) {
135
+ // Strip JSX comments ({/* ... */}) so their contents can't fake elements.
136
+ const cleaned = content.replace(/\{\/\*[\s\S]*?\*\/\}/g, "");
137
+ const elements = [];
138
+ const stack = [];
139
+ const OPEN = /<([A-Za-z][\w.-]*)((?:\s+[\s\S]*?)?)(\/?)>/g;
140
+ let m;
141
+ while ((m = OPEN.exec(cleaned)) !== null) {
142
+ const full = m[0];
143
+ const tag = m[1];
144
+ const attrText = m[2] ?? "";
145
+ const selfClosing = m[3] === "/" || /\/>$/.test(full.trim());
146
+ const line = lineOf(cleaned, m.index);
147
+ const attrs = parseAttrs(attrText);
148
+ if (selfClosing) {
149
+ elements.push({ tag, attrs, selfClosing: true, innerText: "", line });
150
+ continue;
151
+ }
152
+ // Paired tag — match its closing tag via the stack.
153
+ const info = {
154
+ tag,
155
+ attrs,
156
+ selfClosing: false,
157
+ innerText: "",
158
+ line,
159
+ start: OPEN.lastIndex,
160
+ };
161
+ stack.push(info);
162
+ }
163
+ // Find closing tags to pop the stack and capture inner text.
164
+ const CLOSE = /<\/([A-Za-z][\w.-]*)\s*>/g;
165
+ let c;
166
+ while ((c = CLOSE.exec(cleaned)) !== null) {
167
+ const closeTag = c[1];
168
+ // Pop the most recent unclosed element with a matching tag.
169
+ for (let i = stack.length - 1; i >= 0; i--) {
170
+ if (stack[i].tag === closeTag) {
171
+ const open = stack.splice(i, 1)[0];
172
+ open.innerText = textBetween(cleaned, open.start, c.index);
173
+ elements.push(open);
174
+ break;
175
+ }
176
+ }
177
+ }
178
+ return elements;
179
+ }
180
+ /** Text content with tags and whitespace collapsed (for visible-name checks). */
181
+ function textBetween(content, from, to) {
182
+ const slice = content.slice(from, to);
183
+ return slice.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
184
+ }
185
+ /** Parse a tag's attribute text into a name -> value map (values may be quoted). */
186
+ function parseAttrs(text) {
187
+ const attrs = {};
188
+ const ATTR = /([A-Za-z_:][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|\{([^}]*)\}|([^\s>]+)))?/g;
189
+ let m;
190
+ while ((m = ATTR.exec(text)) !== null) {
191
+ const name = m[1];
192
+ const value = m[2] ?? m[3] ?? m[4] ?? m[5] ?? "";
193
+ attrs[name] = value;
194
+ // Also record the raw attr presence stripped of its value.
195
+ attrs[`${name}::present`] = "";
196
+ }
197
+ return attrs;
198
+ }
199
+ const hasAttr = (attrs, name) => `${name}::present` in attrs;
200
+ const INTERACTIVE = new Set(["a", "button", "input", "select", "textarea", "summary"]);
201
+ function lintJsx(content, file, _repo) {
202
+ const issues = [];
203
+ for (const el of parseJsx(content)) {
204
+ const tag = el.tag.toLowerCase();
205
+ // 1) <img> without alt text.
206
+ if (tag === "img" && !hasAttr(el.attrs, "alt")) {
207
+ issues.push({
208
+ type: "a11y",
209
+ severity: "warning",
210
+ file,
211
+ line: el.line,
212
+ description: "<img> is missing an alt attribute (empty alt=\"\" is fine for decorative images)",
213
+ });
214
+ }
215
+ // 2) Interactive controls missing an accessible label.
216
+ if (INTERACTIVE.has(tag)) {
217
+ const labelled = hasAttr(el.attrs, "aria-label") || hasAttr(el.attrs, "aria-labelledby") || hasAttr(el.attrs, "title");
218
+ const hasText = tag === "input" || tag === "select" || tag === "textarea"
219
+ ? false // these elements carry no visible text of their own
220
+ : el.innerText.length > 0;
221
+ if (!labelled && !hasText) {
222
+ issues.push({
223
+ type: "a11y",
224
+ severity: "warning",
225
+ file,
226
+ line: el.line,
227
+ description: `<${tag}> has no accessible label (add aria-label, aria-labelledby, title, or ${tag === "a" || tag === "button" ? "visible text" : "a placeholder/id"})`,
228
+ });
229
+ }
230
+ }
231
+ // 3) Non-semantic clickable <div>/<span>.
232
+ if ((tag === "div" || tag === "span") && hasAttr(el.attrs, "onClick")) {
233
+ const hasRole = hasAttr(el.attrs, "role");
234
+ const hasKeyboard = hasAttr(el.attrs, "onKeyDown") && hasAttr(el.attrs, "tabIndex");
235
+ issues.push({
236
+ type: "a11y",
237
+ severity: hasRole && hasKeyboard ? "info" : "warning",
238
+ file,
239
+ line: el.line,
240
+ description: hasRole && hasKeyboard
241
+ ? `clickable <${tag}> is keyboard-accessible (role+tabIndex+onKeyDown) — confirm a <button>/<a> is not a better fit`
242
+ : `clickable <${tag}> is not keyboard-accessible — prefer a semantic <button>/<a> or add role="button", tabIndex, and onKeyDown`,
243
+ });
244
+ }
245
+ }
246
+ return issues;
247
+ }
248
+ //# sourceMappingURL=accessibility.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessibility.js","sourceRoot":"","sources":["../../src/analyzers/accessibility.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { Language } from "./util.js";
2
+ import type { DependencyGraphResult } from "./types.js";
3
+ export interface BuiltEdges {
4
+ edges: Map<string, Set<string>>;
5
+ lang: Language;
6
+ engine: string;
7
+ }
8
+ /**
9
+ * Build the file import graph for a repo. Prefers `madge` for JS/TS when it is
10
+ * installed, otherwise falls back to a regex-based import scanner that works
11
+ * for both JS/TS and Python. Returns null for unsupported (non JS/TS/Python) repos.
12
+ */
13
+ export declare function buildEdges(repo: string): BuiltEdges | null;
14
+ export declare function analyzeDependencyGraph(repo: string): DependencyGraphResult;
@@ -0,0 +1,206 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { commandExists, detectLanguage, safeExec, walkFiles, } from "./util.js";
4
+ const JS_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
5
+ const PY_EXTS = [".py"];
6
+ /**
7
+ * Build the file import graph for a repo. Prefers `madge` for JS/TS when it is
8
+ * installed, otherwise falls back to a regex-based import scanner that works
9
+ * for both JS/TS and Python. Returns null for unsupported (non JS/TS/Python) repos.
10
+ */
11
+ export function buildEdges(repo) {
12
+ const lang = detectLanguage(repo);
13
+ if (lang !== "js" && lang !== "python")
14
+ return null;
15
+ if (lang === "js" && commandExists("madge")) {
16
+ const r = safeExec("madge", [repo, "--json", "--circular", "--extensions", "ts,tsx,js,jsx"], repo, 120000);
17
+ if (r.code === 0 && r.stdout) {
18
+ try {
19
+ const json = JSON.parse(r.stdout);
20
+ const modules = json.modules ?? {};
21
+ const edges = new Map();
22
+ for (const [from, deps] of Object.entries(modules)) {
23
+ edges.set(from, new Set(deps));
24
+ }
25
+ return { edges, lang, engine: "madge" };
26
+ }
27
+ catch {
28
+ /* fall through to regex */
29
+ }
30
+ }
31
+ }
32
+ const exts = lang === "js" ? JS_EXTS : PY_EXTS;
33
+ return { edges: regexGraph(repo, exts, lang), lang, engine: "regex" };
34
+ }
35
+ export function analyzeDependencyGraph(repo) {
36
+ const built = buildEdges(repo);
37
+ if (!built) {
38
+ return {
39
+ status: "skipped",
40
+ note: "not a JS/TS or Python repo",
41
+ files: 0,
42
+ circular: [],
43
+ mostDependedOn: [],
44
+ orphans: [],
45
+ };
46
+ }
47
+ return finalizeGraph(built.edges, repo, built.engine);
48
+ }
49
+ function finalizeGraph(edges, repo, engine) {
50
+ const circular = findCycles(edges);
51
+ const inCount = new Map();
52
+ for (const deps of edges.values()) {
53
+ for (const d of deps)
54
+ inCount.set(d, (inCount.get(d) ?? 0) + 1);
55
+ }
56
+ const mostDependedOn = [...inCount.entries()]
57
+ .sort((a, b) => b[1] - a[1])
58
+ .slice(0, 5)
59
+ .map(([file, count]) => ({ file: path.relative(repo, file), count }));
60
+ // Orphans = files not reachable from any entry root (in-degree 0 node),
61
+ // i.e. truly disconnected modules. Entry points themselves are roots.
62
+ const roots = [...edges.keys()].filter((n) => (inCount.get(n) ?? 0) === 0);
63
+ const visited = new Set();
64
+ const queue = [...roots];
65
+ while (queue.length) {
66
+ const n = queue.pop();
67
+ if (visited.has(n))
68
+ continue;
69
+ visited.add(n);
70
+ for (const d of edges.get(n) ?? [])
71
+ if (!visited.has(d))
72
+ queue.push(d);
73
+ }
74
+ const orphans = [...edges.keys()]
75
+ .filter((n) => !visited.has(n))
76
+ .map((n) => path.relative(repo, n));
77
+ return {
78
+ status: "ok",
79
+ engine,
80
+ files: edges.size,
81
+ circular,
82
+ mostDependedOn,
83
+ orphans,
84
+ };
85
+ }
86
+ function regexGraph(repo, exts, lang) {
87
+ const files = walkFiles(repo, exts);
88
+ const fileSet = new Set(files);
89
+ const edges = new Map();
90
+ for (const f of files)
91
+ edges.set(f, new Set());
92
+ const re = lang === "python"
93
+ ? /^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/gm
94
+ : /(?:import|export)[\s\S]*?from\s*['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)/g;
95
+ for (const f of files) {
96
+ let content;
97
+ try {
98
+ content = fs.readFileSync(f, "utf8");
99
+ }
100
+ catch {
101
+ continue;
102
+ }
103
+ const dir = path.dirname(f);
104
+ const matches = content.matchAll(re);
105
+ for (const m of matches) {
106
+ const spec = (m[1] || m[2] || "").trim();
107
+ if (!spec)
108
+ continue;
109
+ let resolved = null;
110
+ if (lang === "python") {
111
+ resolved = spec.startsWith(".")
112
+ ? resolvePythonRelative(dir, spec)
113
+ : resolvePythonAbs(repo, spec, fileSet);
114
+ }
115
+ else if (spec.startsWith(".")) {
116
+ resolved = resolveJsRelative(dir, spec, fileSet);
117
+ }
118
+ if (resolved && edges.has(resolved))
119
+ edges.get(f).add(resolved);
120
+ }
121
+ }
122
+ return edges;
123
+ }
124
+ function resolveJsRelative(dir, spec, fileSet) {
125
+ const base = path.resolve(dir, spec);
126
+ const ext = path.extname(base);
127
+ const stem = ext ? base.slice(0, -ext.length) : base;
128
+ const exts = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
129
+ const candidates = [...exts.map((e) => stem + e)];
130
+ for (const e of exts) {
131
+ candidates.push(path.join(stem, "index" + e));
132
+ }
133
+ for (const c of candidates)
134
+ if (fileSet.has(c))
135
+ return c;
136
+ return null;
137
+ }
138
+ function resolvePythonRelative(dir, spec) {
139
+ const dots = spec.match(/^\.+/)?.[0].length ?? 0;
140
+ const rest = spec.slice(dots).split(".").filter(Boolean);
141
+ let base = dir;
142
+ for (let i = 0; i < dots - 1; i++)
143
+ base = path.dirname(base);
144
+ const target = path.resolve(base, ...rest) + ".py";
145
+ const pkg = path.join(path.resolve(base, ...rest), "__init__.py");
146
+ if (fs.existsSync(target))
147
+ return target;
148
+ if (fs.existsSync(pkg))
149
+ return pkg;
150
+ return null;
151
+ }
152
+ function resolvePythonAbs(repo, spec, fileSet) {
153
+ const parts = spec.split(".");
154
+ const mod = parts[parts.length - 1];
155
+ const candidates = [
156
+ path.join(repo, ...parts) + ".py",
157
+ path.join(repo, ...parts, "__init__.py"),
158
+ path.join(repo, ...parts.slice(0, -1), mod) + ".py",
159
+ ];
160
+ for (const c of candidates)
161
+ if (fileSet.has(c))
162
+ return c;
163
+ return null;
164
+ }
165
+ function findCycles(edges) {
166
+ const WHITE = 0;
167
+ const GRAY = 1;
168
+ const BLACK = 2;
169
+ const color = new Map();
170
+ for (const n of edges.keys())
171
+ color.set(n, WHITE);
172
+ const stack = [];
173
+ const cycles = [];
174
+ const dfs = (u) => {
175
+ color.set(u, GRAY);
176
+ stack.push(u);
177
+ for (const v of edges.get(u) ?? []) {
178
+ if (!edges.has(v))
179
+ continue;
180
+ if (color.get(v) === GRAY) {
181
+ const idx = stack.indexOf(v);
182
+ if (idx !== -1)
183
+ cycles.push([...stack.slice(idx), v]);
184
+ }
185
+ else if (color.get(v) === WHITE) {
186
+ dfs(v);
187
+ }
188
+ }
189
+ stack.pop();
190
+ color.set(u, BLACK);
191
+ };
192
+ for (const n of edges.keys())
193
+ if (color.get(n) === WHITE)
194
+ dfs(n);
195
+ const seen = new Set();
196
+ const dedup = [];
197
+ for (const c of cycles) {
198
+ const key = [...new Set(c)].sort().join("|");
199
+ if (!seen.has(key)) {
200
+ seen.add(key);
201
+ dedup.push(c);
202
+ }
203
+ }
204
+ return dedup;
205
+ }
206
+ //# sourceMappingURL=dependencyGraph.js.map