stackvitals 0.1.0__py3-none-any.whl

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 (465) hide show
  1. mergescout/__init__.py +1 -0
  2. mergescout/archive/__init__.py +0 -0
  3. mergescout/archive/__main__.py +30 -0
  4. mergescout/archive/convert.py +83 -0
  5. mergescout/archive/events.py +55 -0
  6. mergescout/archive/manifest.py +45 -0
  7. mergescout/archive/stream.py +83 -0
  8. mergescout/bench/__init__.py +1 -0
  9. mergescout/bench/__main__.py +135 -0
  10. mergescout/bench/ci.py +257 -0
  11. mergescout/bench/deploys.py +115 -0
  12. mergescout/bench/matched.py +22 -0
  13. mergescout/bench/prior.py +42 -0
  14. mergescout/bench/report.py +230 -0
  15. mergescout/bench/shared.py +112 -0
  16. mergescout/bench/stars.py +57 -0
  17. mergescout/cli.py +89 -0
  18. mergescout/config.py +35 -0
  19. mergescout/crawl/__init__.py +0 -0
  20. mergescout/crawl/__main__.py +97 -0
  21. mergescout/crawl/chain.py +34 -0
  22. mergescout/crawl/fetch.py +146 -0
  23. mergescout/crawl/outcomes.py +69 -0
  24. mergescout/crawl/queries.py +250 -0
  25. mergescout/crawl/runner.py +176 -0
  26. mergescout/crawl/seed.py +77 -0
  27. mergescout/crawl/state.py +169 -0
  28. mergescout/crawl/writer.py +87 -0
  29. mergescout/discover/__init__.py +0 -0
  30. mergescout/discover/__main__.py +58 -0
  31. mergescout/discover/sweep.py +286 -0
  32. mergescout/github/__init__.py +0 -0
  33. mergescout/github/graphql.py +202 -0
  34. mergescout/github/identities.py +35 -0
  35. mergescout/github/releases.py +137 -0
  36. mergescout/labels.py +20 -0
  37. mergescout/mcp_server.py +228 -0
  38. mergescout/measure/__init__.py +0 -0
  39. mergescout/measure/__main__.py +75 -0
  40. mergescout/measure/api_cost.py +91 -0
  41. mergescout/measure/count_repos.py +30 -0
  42. mergescout/measure/jsonl.py +14 -0
  43. mergescout/measure/report.py +56 -0
  44. mergescout/measure/sample.py +37 -0
  45. mergescout/owners.py +22 -0
  46. mergescout/setup_check.py +64 -0
  47. mergescout/signals/__init__.py +0 -0
  48. mergescout/signals/__main__.py +96 -0
  49. mergescout/signals/bump_ledger.py +258 -0
  50. mergescout/signals/bumps.py +104 -0
  51. mergescout/signals/check.py +141 -0
  52. mergescout/signals/community_bench.py +149 -0
  53. mergescout/signals/delivery_bench.py +103 -0
  54. mergescout/signals/deps_map.py +261 -0
  55. mergescout/signals/deps_trends.py +117 -0
  56. mergescout/signals/evidence.py +94 -0
  57. mergescout/signals/exploits.py +43 -0
  58. mergescout/signals/flow_bench.py +258 -0
  59. mergescout/signals/identity.py +73 -0
  60. mergescout/signals/issues.py +76 -0
  61. mergescout/signals/labels.py +79 -0
  62. mergescout/signals/osv.py +232 -0
  63. mergescout/signals/owners_watch.py +80 -0
  64. mergescout/signals/repo_stats.py +96 -0
  65. mergescout/signals/rules.py +70 -0
  66. mergescout/signals/silver.py +33 -0
  67. mergescout/signals/top_packages.py +26 -0
  68. mergescout/signals/triage.py +71 -0
  69. mergescout/signals/triage_math.py +120 -0
  70. mergescout/signals/walls.py +91 -0
  71. mergescout/snapshot/__init__.py +0 -0
  72. mergescout/snapshot/__main__.py +215 -0
  73. mergescout/snapshot/build.py +147 -0
  74. mergescout/snapshot/publish.py +70 -0
  75. mergescout/snapshot/schema.py +34 -0
  76. mergescout/snapshot/score.py +30 -0
  77. mergescout/store/__init__.py +0 -0
  78. mergescout/store/__main__.py +46 -0
  79. mergescout/store/sync.py +148 -0
  80. mergescout/timeutil.py +11 -0
  81. mergescout/tool/__init__.py +0 -0
  82. mergescout/tool/action_pack.py +62 -0
  83. mergescout/tool/live.py +201 -0
  84. mergescout/tool/queries.py +293 -0
  85. mergescout/tool/refs.py +31 -0
  86. mergescout/tool/store.py +129 -0
  87. mergescout/tool/tracking.py +83 -0
  88. stackvitals/__init__.py +2 -0
  89. stackvitals/__main__.py +4 -0
  90. stackvitals/ci/__init__.py +1 -0
  91. stackvitals/ci/action.py +574 -0
  92. stackvitals/ci/analyze.py +577 -0
  93. stackvitals/ci/api.py +218 -0
  94. stackvitals/ci/automation.py +212 -0
  95. stackvitals/ci/bench.py +195 -0
  96. stackvitals/ci/burnin.py +102 -0
  97. stackvitals/ci/cli.py +570 -0
  98. stackvitals/ci/collect.py +239 -0
  99. stackvitals/ci/cost.py +107 -0
  100. stackvitals/ci/coverage.py +155 -0
  101. stackvitals/ci/feedback.py +699 -0
  102. stackvitals/ci/fingerprint.py +220 -0
  103. stackvitals/ci/gitlab.py +121 -0
  104. stackvitals/ci/gitlab_action.py +125 -0
  105. stackvitals/ci/impact.py +125 -0
  106. stackvitals/ci/investigate.py +59 -0
  107. stackvitals/ci/kinds.py +52 -0
  108. stackvitals/ci/monitors.py +105 -0
  109. stackvitals/ci/owners.py +116 -0
  110. stackvitals/ci/quarantine_runtime.py +90 -0
  111. stackvitals/ci/report.py +435 -0
  112. stackvitals/ci/retry.py +67 -0
  113. stackvitals/ci/selfheal.py +102 -0
  114. stackvitals/ci/shards.py +120 -0
  115. stackvitals/ci/share.py +59 -0
  116. stackvitals/ci/store.py +93 -0
  117. stackvitals/ci/testreports.py +351 -0
  118. stackvitals/ci/tickets.py +106 -0
  119. stackvitals/ci/track_record.json +11 -0
  120. stackvitals/ci/triage.py +130 -0
  121. stackvitals/ci/triggers.py +147 -0
  122. stackvitals/ci/upload.py +85 -0
  123. stackvitals/ci/validate.py +91 -0
  124. stackvitals/cli.py +111 -0
  125. stackvitals/codeowners.py +69 -0
  126. stackvitals/community/__init__.py +0 -0
  127. stackvitals/community/action.py +140 -0
  128. stackvitals/community/affiliation.py +51 -0
  129. stackvitals/community/analyze.py +507 -0
  130. stackvitals/community/build.py +56 -0
  131. stackvitals/community/chaoss.py +70 -0
  132. stackvitals/community/cli.py +79 -0
  133. stackvitals/community/collect.py +240 -0
  134. stackvitals/community/context.py +54 -0
  135. stackvitals/community/report.py +250 -0
  136. stackvitals/connectors/__init__.py +244 -0
  137. stackvitals/connectors/ai_spend.py +190 -0
  138. stackvitals/connectors/base.py +82 -0
  139. stackvitals/connectors/check.py +686 -0
  140. stackvitals/connectors/copilot.py +104 -0
  141. stackvitals/connectors/discord.py +103 -0
  142. stackvitals/connectors/discourse.py +117 -0
  143. stackvitals/connectors/incident_io.py +106 -0
  144. stackvitals/connectors/jira.py +210 -0
  145. stackvitals/connectors/linear.py +140 -0
  146. stackvitals/connectors/pagerduty.py +96 -0
  147. stackvitals/connectors/slack.py +110 -0
  148. stackvitals/connectors/sonar.py +77 -0
  149. stackvitals/connectors/store.py +120 -0
  150. stackvitals/connectors/tracker.py +108 -0
  151. stackvitals/connectors/writeback.py +105 -0
  152. stackvitals/contribute/__init__.py +1 -0
  153. stackvitals/contribute/brief.py +297 -0
  154. stackvitals/contribute/cli.py +64 -0
  155. stackvitals/core/__init__.py +1 -0
  156. stackvitals/core/actors.py +103 -0
  157. stackvitals/core/agent_ledger.py +120 -0
  158. stackvitals/core/agreements.py +95 -0
  159. stackvitals/core/ai_outcomes.py +276 -0
  160. stackvitals/core/alerts.py +224 -0
  161. stackvitals/core/appdata.py +112 -0
  162. stackvitals/core/bucket.py +190 -0
  163. stackvitals/core/bugs.py +43 -0
  164. stackvitals/core/calendar.py +34 -0
  165. stackvitals/core/capex.py +57 -0
  166. stackvitals/core/change_risk.py +181 -0
  167. stackvitals/core/changes.py +133 -0
  168. stackvitals/core/chat.py +143 -0
  169. stackvitals/core/config.py +373 -0
  170. stackvitals/core/dataflow.py +168 -0
  171. stackvitals/core/decisions.py +275 -0
  172. stackvitals/core/delivery.py +954 -0
  173. stackvitals/core/delivery_peers.py +98 -0
  174. stackvitals/core/delivery_report.py +429 -0
  175. stackvitals/core/deliverycli.py +147 -0
  176. stackvitals/core/devlake_dora.sql +485 -0
  177. stackvitals/core/digest.py +333 -0
  178. stackvitals/core/explaincli.py +107 -0
  179. stackvitals/core/explore.py +296 -0
  180. stackvitals/core/export.py +342 -0
  181. stackvitals/core/exportcli.py +117 -0
  182. stackvitals/core/findings.py +350 -0
  183. stackvitals/core/health.py +94 -0
  184. stackvitals/core/identity.py +82 -0
  185. stackvitals/core/insights.py +365 -0
  186. stackvitals/core/insightscli.py +43 -0
  187. stackvitals/core/installdoc.py +69 -0
  188. stackvitals/core/investment.py +187 -0
  189. stackvitals/core/joins.py +43 -0
  190. stackvitals/core/llmsdoc.py +40 -0
  191. stackvitals/core/merge_queue.py +70 -0
  192. stackvitals/core/metrics.py +109 -0
  193. stackvitals/core/model.py +285 -0
  194. stackvitals/core/projectcli.py +66 -0
  195. stackvitals/core/propose.py +86 -0
  196. stackvitals/core/pulse.py +85 -0
  197. stackvitals/core/recorded.py +145 -0
  198. stackvitals/core/releases.py +39 -0
  199. stackvitals/core/requests.py +528 -0
  200. stackvitals/core/resync.py +35 -0
  201. stackvitals/core/schema.py +205 -0
  202. stackvitals/core/services.py +203 -0
  203. stackvitals/core/settings.py +96 -0
  204. stackvitals/core/sqlcli.py +111 -0
  205. stackvitals/core/stats.py +222 -0
  206. stackvitals/core/store.py +125 -0
  207. stackvitals/core/summary.py +104 -0
  208. stackvitals/core/time.py +29 -0
  209. stackvitals/core/tomledit.py +149 -0
  210. stackvitals/core/uicli.py +138 -0
  211. stackvitals/core/webapp.py +79 -0
  212. stackvitals/core/what_changed.py +76 -0
  213. stackvitals/core/workspace.py +194 -0
  214. stackvitals/deps/__init__.py +1 -0
  215. stackvitals/deps/action.py +299 -0
  216. stackvitals/deps/actions_vulns.py +89 -0
  217. stackvitals/deps/cli.py +77 -0
  218. stackvitals/deps/dossier.py +341 -0
  219. stackvitals/deps/evidence.py +44 -0
  220. stackvitals/deps/fixplan.py +40 -0
  221. stackvitals/deps/guarddog.py +32 -0
  222. stackvitals/deps/hygiene.py +47 -0
  223. stackvitals/deps/imports.py +113 -0
  224. stackvitals/deps/lockfiles.py +198 -0
  225. stackvitals/deps/lookalike.py +80 -0
  226. stackvitals/deps/reach.py +59 -0
  227. stackvitals/deps/release.py +110 -0
  228. stackvitals/deps/runtimes.py +161 -0
  229. stackvitals/deps/supply.py +454 -0
  230. stackvitals/deps/vulns.py +588 -0
  231. stackvitals/editors/__init__.py +40 -0
  232. stackvitals/editors/vscode/extension.js +101 -0
  233. stackvitals/editors/vscode/marks.js +92 -0
  234. stackvitals/editors/vscode/package.json +26 -0
  235. stackvitals/flow/__init__.py +1 -0
  236. stackvitals/flow/action.py +178 -0
  237. stackvitals/flow/analyze.py +418 -0
  238. stackvitals/flow/automate.py +72 -0
  239. stackvitals/flow/build.py +46 -0
  240. stackvitals/flow/cli.py +93 -0
  241. stackvitals/flow/code_hosts.py +146 -0
  242. stackvitals/flow/collect.py +451 -0
  243. stackvitals/flow/gitlab_mr.py +75 -0
  244. stackvitals/flow/knowledge.py +202 -0
  245. stackvitals/flow/peers.py +90 -0
  246. stackvitals/flow/preflight.py +210 -0
  247. stackvitals/flow/report.py +253 -0
  248. stackvitals/flow/rules.py +135 -0
  249. stackvitals/flow/store.py +108 -0
  250. stackvitals/flow/survival.py +84 -0
  251. stackvitals/flow/triage.py +62 -0
  252. stackvitals/mcp_server.py +670 -0
  253. stackvitals/portal/__init__.py +0 -0
  254. stackvitals/portal/action.py +205 -0
  255. stackvitals/portal/batch.py +80 -0
  256. stackvitals/portal/campaigns.py +104 -0
  257. stackvitals/portal/catalog.py +104 -0
  258. stackvitals/portal/checks.py +63 -0
  259. stackvitals/portal/cli.py +172 -0
  260. stackvitals/portal/descriptor.py +105 -0
  261. stackvitals/portal/export.py +43 -0
  262. stackvitals/portal/github.py +48 -0
  263. stackvitals/portal/golden.py +95 -0
  264. stackvitals/portal/history.py +30 -0
  265. stackvitals/portal/inventory.py +264 -0
  266. stackvitals/portal/owners.py +78 -0
  267. stackvitals/portal/pages.py +153 -0
  268. stackvitals/portal/rollup.py +32 -0
  269. stackvitals/portal/scorecard.py +366 -0
  270. stackvitals/security/__init__.py +2 -0
  271. stackvitals/security/probes.py +154 -0
  272. stackvitals/security/repo_checks.py +132 -0
  273. stackvitals/security/workflows.py +111 -0
  274. stackvitals/shell.py +107 -0
  275. stackvitals/site.py +51 -0
  276. stackvitals/web/app/LICENSES.md +428 -0
  277. stackvitals/web/app/boot.js +1 -0
  278. stackvitals/web/app/chunks/AddRepo-KRPZXWCY.js +1 -0
  279. stackvitals/web/app/chunks/SetupRunning-XWFQAKRZ.js +1 -0
  280. stackvitals/web/app/chunks/accuracy-data-OQTDXI7Y.js +1 -0
  281. stackvitals/web/app/chunks/agents-data-BAL6QAOF.js +11 -0
  282. stackvitals/web/app/chunks/chunk-24LFKFZ6.js +1 -0
  283. stackvitals/web/app/chunks/chunk-3G4SQNUC.js +1 -0
  284. stackvitals/web/app/chunks/chunk-3IXWEOIJ.js +154 -0
  285. stackvitals/web/app/chunks/chunk-3ROQFHPV.js +1 -0
  286. stackvitals/web/app/chunks/chunk-45PGAQRO.js +8 -0
  287. stackvitals/web/app/chunks/chunk-4H53OYLG.js +1 -0
  288. stackvitals/web/app/chunks/chunk-4LUI7O5P.js +1 -0
  289. stackvitals/web/app/chunks/chunk-4SQPQ5TB.js +1 -0
  290. stackvitals/web/app/chunks/chunk-4WKQTWUC.js +2 -0
  291. stackvitals/web/app/chunks/chunk-576YHDEK.js +8 -0
  292. stackvitals/web/app/chunks/chunk-6BDBXA6V.js +1 -0
  293. stackvitals/web/app/chunks/chunk-6RLVD2DT.js +1 -0
  294. stackvitals/web/app/chunks/chunk-763NH7PP.js +1 -0
  295. stackvitals/web/app/chunks/chunk-76WAVKUG.js +1 -0
  296. stackvitals/web/app/chunks/chunk-7II2BD5F.js +1 -0
  297. stackvitals/web/app/chunks/chunk-7TJCBOAY.js +1 -0
  298. stackvitals/web/app/chunks/chunk-ASYNLVOP.js +1 -0
  299. stackvitals/web/app/chunks/chunk-CTKDYZMG.js +1 -0
  300. stackvitals/web/app/chunks/chunk-CVB26NMC.js +1 -0
  301. stackvitals/web/app/chunks/chunk-G7NGB5UX.js +1 -0
  302. stackvitals/web/app/chunks/chunk-GPVLJRZD.js +1 -0
  303. stackvitals/web/app/chunks/chunk-GQYFN4PE.js +75 -0
  304. stackvitals/web/app/chunks/chunk-GT4EAKAO.js +2 -0
  305. stackvitals/web/app/chunks/chunk-IYMBFPCV.js +1 -0
  306. stackvitals/web/app/chunks/chunk-LAAGCPU3.js +1 -0
  307. stackvitals/web/app/chunks/chunk-MAPYGK3P.js +1 -0
  308. stackvitals/web/app/chunks/chunk-P5TXDD45.js +1 -0
  309. stackvitals/web/app/chunks/chunk-PABICGBL.js +1 -0
  310. stackvitals/web/app/chunks/chunk-RPXHAG6F.js +1 -0
  311. stackvitals/web/app/chunks/chunk-RQZPSJ2Y.js +1 -0
  312. stackvitals/web/app/chunks/chunk-S2ZBMEVA.js +1 -0
  313. stackvitals/web/app/chunks/chunk-S654BBEP.js +6 -0
  314. stackvitals/web/app/chunks/chunk-T7QVTNXF.js +1 -0
  315. stackvitals/web/app/chunks/chunk-THFUOCPC.js +273 -0
  316. stackvitals/web/app/chunks/chunk-TO4VVRZ7.js +1 -0
  317. stackvitals/web/app/chunks/chunk-VQ5YXO54.js +1 -0
  318. stackvitals/web/app/chunks/chunk-VWKSMFWJ.js +1 -0
  319. stackvitals/web/app/chunks/chunk-Y6LUXDM6.js +1 -0
  320. stackvitals/web/app/chunks/chunk-YTANGYYH.js +1 -0
  321. stackvitals/web/app/chunks/connect-KONYWYTT.js +1 -0
  322. stackvitals/web/app/chunks/connections-data-2DYS4UVW.js +32 -0
  323. stackvitals/web/app/chunks/logos-N5IINUAM.js +1 -0
  324. stackvitals/web/app/chunks/more-KMJMJQD3.js +10 -0
  325. stackvitals/web/app/chunks/more-PC6FVS3P.js +1 -0
  326. stackvitals/web/app/chunks/more-RCIVCUDE.js +2 -0
  327. stackvitals/web/app/chunks/more-ZKKRY6NO.css +1 -0
  328. stackvitals/web/app/chunks/sealbox-35YL4EUH.js +3 -0
  329. stackvitals/web/app/chunks/session-DYYTOE2K.js +1 -0
  330. stackvitals/web/app/chunks/settingsApply-2HRJEZYM.js +1 -0
  331. stackvitals/web/app/chunks/settingsReview-GX2FZQ4M.js +3 -0
  332. stackvitals/web/app/chunks/uPlot.esm-OIWT3AHJ.js +3 -0
  333. stackvitals/web/app/demo/ci/impact.json +1 -0
  334. stackvitals/web/app/demo/ci/quarantine.json +1 -0
  335. stackvitals/web/app/demo/ci/tickets.json +1 -0
  336. stackvitals/web/app/demo/mobile/alerts.json +1 -0
  337. stackvitals/web/app/demo/mobile/app.json +1 -0
  338. stackvitals/web/app/demo/mobile/community.json +1 -0
  339. stackvitals/web/app/demo/mobile/connectors.json +1 -0
  340. stackvitals/web/app/demo/mobile/data.json +1 -0
  341. stackvitals/web/app/demo/mobile/decisions.json +1 -0
  342. stackvitals/web/app/demo/mobile/delivery.json +1 -0
  343. stackvitals/web/app/demo/mobile/deps.json +1 -0
  344. stackvitals/web/app/demo/mobile/flow.json +1 -0
  345. stackvitals/web/app/demo/mobile/health.json +1 -0
  346. stackvitals/web/app/demo/mobile/insights.json +1 -0
  347. stackvitals/web/app/demo/mobile/pulse.json +1 -0
  348. stackvitals/web/app/demo/mobile/security.json +1 -0
  349. stackvitals/web/app/demo/mobile/summary/ci_jobs.jsonl +5 -0
  350. stackvitals/web/app/demo/mobile/summary/devlake_project_pr_metrics.jsonl +14 -0
  351. stackvitals/web/app/demo/mobile/summary/devlake_pull_requests.jsonl +14 -0
  352. stackvitals/web/app/demo/mobile/summary/manifest.json +1 -0
  353. stackvitals/web/app/demo/mobile/summary/prs.jsonl +19 -0
  354. stackvitals/web/app/demo/mobile/summary/tests.jsonl +1 -0
  355. stackvitals/web/app/demo/mobile/supply.json +1 -0
  356. stackvitals/web/app/demo/mobile/vulns.json +1 -0
  357. stackvitals/web/app/demo/payments-api/alerts.json +1 -0
  358. stackvitals/web/app/demo/payments-api/app.json +1 -0
  359. stackvitals/web/app/demo/payments-api/community.json +1 -0
  360. stackvitals/web/app/demo/payments-api/connectors.json +1 -0
  361. stackvitals/web/app/demo/payments-api/data.json +1 -0
  362. stackvitals/web/app/demo/payments-api/decisions.json +1 -0
  363. stackvitals/web/app/demo/payments-api/delivery.json +1 -0
  364. stackvitals/web/app/demo/payments-api/deps.json +1 -0
  365. stackvitals/web/app/demo/payments-api/flow.json +1 -0
  366. stackvitals/web/app/demo/payments-api/health.json +1 -0
  367. stackvitals/web/app/demo/payments-api/insights.json +1 -0
  368. stackvitals/web/app/demo/payments-api/pulse.json +1 -0
  369. stackvitals/web/app/demo/payments-api/security.json +1 -0
  370. stackvitals/web/app/demo/payments-api/summary/ci_jobs.jsonl +5 -0
  371. stackvitals/web/app/demo/payments-api/summary/devlake_project_pr_metrics.jsonl +14 -0
  372. stackvitals/web/app/demo/payments-api/summary/devlake_pull_requests.jsonl +14 -0
  373. stackvitals/web/app/demo/payments-api/summary/manifest.json +1 -0
  374. stackvitals/web/app/demo/payments-api/summary/prs.jsonl +22 -0
  375. stackvitals/web/app/demo/payments-api/summary/tests.jsonl +3 -0
  376. stackvitals/web/app/demo/payments-api/supply.json +1 -0
  377. stackvitals/web/app/demo/payments-api/vulns.json +1 -0
  378. stackvitals/web/app/demo/portal.json +1 -0
  379. stackvitals/web/app/demo/settings.json +1 -0
  380. stackvitals/web/app/demo/web-app/alerts.json +1 -0
  381. stackvitals/web/app/demo/web-app/app.json +1 -0
  382. stackvitals/web/app/demo/web-app/community.json +1 -0
  383. stackvitals/web/app/demo/web-app/connectors.json +1 -0
  384. stackvitals/web/app/demo/web-app/data.json +1 -0
  385. stackvitals/web/app/demo/web-app/decisions.json +1 -0
  386. stackvitals/web/app/demo/web-app/delivery.json +1 -0
  387. stackvitals/web/app/demo/web-app/deps.json +1 -0
  388. stackvitals/web/app/demo/web-app/flow.json +1 -0
  389. stackvitals/web/app/demo/web-app/health.json +1 -0
  390. stackvitals/web/app/demo/web-app/insights.json +1 -0
  391. stackvitals/web/app/demo/web-app/pulse.json +1 -0
  392. stackvitals/web/app/demo/web-app/security.json +1 -0
  393. stackvitals/web/app/demo/web-app/summary/ci_jobs.jsonl +5 -0
  394. stackvitals/web/app/demo/web-app/summary/devlake_project_pr_metrics.jsonl +14 -0
  395. stackvitals/web/app/demo/web-app/summary/devlake_pull_requests.jsonl +14 -0
  396. stackvitals/web/app/demo/web-app/summary/manifest.json +1 -0
  397. stackvitals/web/app/demo/web-app/summary/prs.jsonl +18 -0
  398. stackvitals/web/app/demo/web-app/summary/tests.jsonl +2 -0
  399. stackvitals/web/app/demo/web-app/supply.json +1 -0
  400. stackvitals/web/app/demo/web-app/vulns.json +1 -0
  401. stackvitals/web/app/fonts/plex-mono-400.woff2 +0 -0
  402. stackvitals/web/app/fonts/plex-sans-400.woff2 +0 -0
  403. stackvitals/web/app/fonts/plex-sans-600.woff2 +0 -0
  404. stackvitals/web/app/fonts/plex-sans-condensed-700.woff2 +0 -0
  405. stackvitals/web/app/help/accuracy.json +1 -0
  406. stackvitals/web/app/help/connectors.json +1 -0
  407. stackvitals/web/app/help/getting-started.json +1 -0
  408. stackvitals/web/app/help/index.json +1 -0
  409. stackvitals/web/app/help/modules.json +1 -0
  410. stackvitals/web/app/help/privacy-and-security.json +1 -0
  411. stackvitals/web/app/help/reference.json +1 -0
  412. stackvitals/web/app/main.css +1 -0
  413. stackvitals/web/app/main.js +1 -0
  414. stackvitals/web/app/pages/about.css +1 -0
  415. stackvitals/web/app/pages/about.js +1 -0
  416. stackvitals/web/app/pages/activity.css +1 -0
  417. stackvitals/web/app/pages/activity.js +1 -0
  418. stackvitals/web/app/pages/agents.css +1 -0
  419. stackvitals/web/app/pages/agents.js +2 -0
  420. stackvitals/web/app/pages/ci.css +1 -0
  421. stackvitals/web/app/pages/ci.js +1 -0
  422. stackvitals/web/app/pages/community.css +1 -0
  423. stackvitals/web/app/pages/community.js +1 -0
  424. stackvitals/web/app/pages/connections.css +1 -0
  425. stackvitals/web/app/pages/connections.js +6 -0
  426. stackvitals/web/app/pages/delivery.css +1 -0
  427. stackvitals/web/app/pages/delivery.js +45 -0
  428. stackvitals/web/app/pages/deps.css +1 -0
  429. stackvitals/web/app/pages/deps.js +7 -0
  430. stackvitals/web/app/pages/explore.css +1 -0
  431. stackvitals/web/app/pages/explore.js +7 -0
  432. stackvitals/web/app/pages/health.css +1 -0
  433. stackvitals/web/app/pages/health.js +1 -0
  434. stackvitals/web/app/pages/help.css +1 -0
  435. stackvitals/web/app/pages/help.js +6 -0
  436. stackvitals/web/app/pages/insights.css +1 -0
  437. stackvitals/web/app/pages/insights.js +1 -0
  438. stackvitals/web/app/pages/investment.css +1 -0
  439. stackvitals/web/app/pages/investment.js +3 -0
  440. stackvitals/web/app/pages/overview.css +1 -0
  441. stackvitals/web/app/pages/overview.js +1 -0
  442. stackvitals/web/app/pages/portal.css +1 -0
  443. stackvitals/web/app/pages/portal.js +51 -0
  444. stackvitals/web/app/pages/prs.css +1 -0
  445. stackvitals/web/app/pages/prs.js +4 -0
  446. stackvitals/web/app/pages/settings.css +1 -0
  447. stackvitals/web/app/pages/settings.js +1 -0
  448. stackvitals/web/app/pages/setup.css +1 -0
  449. stackvitals/web/app/pages/setup.js +2 -0
  450. stackvitals/web/app/pages/standing.css +1 -0
  451. stackvitals/web/app/pages/standing.js +1 -0
  452. stackvitals/web/app/pages/welcome-sheet.js +1 -0
  453. stackvitals/web/app/pages/welcome.js +1 -0
  454. stackvitals/web/app/settings-workflow.json +1 -0
  455. stackvitals/web/index.html +34 -0
  456. stackvitals/web/site/index.html +108 -0
  457. stackvitals/web/site/integrations.html +15 -0
  458. stackvitals/web/site/setup.html +15 -0
  459. stackvitals/web/site/site.css +68 -0
  460. stackvitals-0.1.0.dist-info/METADATA +134 -0
  461. stackvitals-0.1.0.dist-info/RECORD +465 -0
  462. stackvitals-0.1.0.dist-info/WHEEL +4 -0
  463. stackvitals-0.1.0.dist-info/entry_points.txt +3 -0
  464. stackvitals-0.1.0.dist-info/licenses/LICENSE +202 -0
  465. stackvitals-0.1.0.dist-info/licenses/NOTICE +16 -0
@@ -0,0 +1,52 @@
1
+ """What kind of flake a failure looks like (the environment: infrastructure, network, concurrency, timing; or the test's code:
2
+ resource leak, async wait, time, randomness, floating point, shared state, io, ordering), so people know where
3
+ to look. Measured against labelled causes in bench/accuracy/flaky/kinds.py."""
4
+ import re
5
+
6
+ # Checked in this order.
7
+ KINDS = (
8
+ # The runner, a cache or artifact store, or a service the job starts on its own machine.
9
+ ("infrastructure", re.compile(r"(?i)(ServerBusy|No space left on device|runner has received a shutdown signal|"
10
+ r"lost communication with the server|Cannot connect to the Docker daemon|"
11
+ r"failed to connect to the docker API|hosted runner encountered an error|out of memory|"
12
+ r"OOMKilled|exit code 137|Cannot allocate memory|heap out of memory|FinalizeArtifact|"
13
+ r"upload of artifact|artifact (upload|download) failed|"
14
+ r"(127\.0\.0\.1|localhost)\S{0,8}.{0,60}connection refused)")),
15
+ # ENOTFOUND matched case-sensitively: "FileNotFoundError" holds it in any case.
16
+ ("network", re.compile(r"(?i)((?-i:\bENOTFOUND\b)|EAI_AGAIN|ECONNRESET|ECONNREFUSED|ETIMEDOUT|socket hang up|Could not resolve host|"
17
+ r"Temporary failure in name resolution|Name or service not known|Failed to fetch|"
18
+ r"Failed to download(?! and build)|\b50[234] (Service Unavailable|Bad Gateway|Gateway Time-?out)|"
19
+ r"HTTP (error )?5\d\d|status code 5\d\d|returned HTTP 5\d\d|TLS handshake timeout|i/o timeout|"
20
+ r"Connection (timed out|reset|refused)|Unable to make request|toomanyrequests|Too Many Requests|"
21
+ r"rate limit|quota exceeded|Service Unavailable|net/http: request canceled|Client\.Timeout exceeded|"
22
+ r"Error response from daemon|download has timed out)")),
23
+ ("concurrency", re.compile(r"(?i)(DATA RACE|data race|deadlock|ConcurrentModificationException|race condition|"
24
+ r"concurrent map (writes|read and map write)|already borrowed)")),
25
+ ("timing", re.compile(r"(?i)(timed? ?out (after|in|waiting)|timeout of \d+ ?ms exceeded|exceeded (the )?timeout|"
26
+ r"test timed out|deadline exceeded|TimeoutError|Timeout - Async callback|waiting for .{0,80} timed out|"
27
+ r"Expected condition failed: waiting for|Matcher did not succeed in time|"
28
+ r"exceeded the maximum execution time|has timed out)")),
29
+ # Causes in the test's own code (Datadog's categories), checked after the environment's.
30
+ ("resource leak", re.compile(r"(?i)(Too many open files|EMFILE|ResourceWarning|unclosed (file|socket|transport)|"
31
+ r"address already in use|EADDRINUSE|port \d+ is already (in use|allocated)|leaked? (handle|connection))")),
32
+ ("async wait", re.compile(r"(?i)(not wrapped in act\(|Unable to find (an )?element|StaleElementReference|"
33
+ r"ElementNotInteractable|detached from (the )?DOM|element is not attached|waitFor(Element)? .{0,40}failed|"
34
+ r"Promise rejection was handled asynchronously|unhandled promise rejection)")),
35
+ ("time", re.compile(r"(?i)(time ?zone|\bDST\b|daylight saving|leap (year|second)|ZoneInfo|ZonedDateTime|"
36
+ r"datetime\.(now|utcnow)|LocalDate|Clock skew|month end|end of (the )?month)")),
37
+ ("randomness", re.compile(r"(?i)(Falsifying example|random seed|seed[=: ]+\d+|Math\.random|randomly ordered|"
38
+ r"--randomly-seed|--seed=\d+)")),
39
+ ("floating point", re.compile(r"(?i)(\d\.\d{12,}|not almost equal|assertAlmostEqual|approx\(|ULP|"
40
+ r"floating[- ]point|toBeCloseTo|within tolerance|rel_tol|abs_tol)")),
41
+ ("shared state", re.compile(r"(?i)(already registered|already exists|duplicate key|UNIQUE constraint failed|"
42
+ r"IntegrityError|state from (a |another )?(previous|other) test|test pollution|singleton)")),
43
+ ("io", re.compile(r"(?i)(FileNotFoundError|ENOENT|No such file or directory|Permission denied|EACCES|"
44
+ r"Directory not empty|ENOTEMPTY|file is being used by another process|EBUSY)")),
45
+ ("ordering", re.compile(r"(?i)(Items in the (first|second) (set|list)|Element counts were not equal|in any order|"
46
+ r"Lists differ|expected order|unordered|sort(ed)? order)")),
47
+ )
48
+
49
+
50
+ def kind_of(text: str | None) -> str | None:
51
+ """The first kind whose signs the text shows, checked in order; None when it shows none."""
52
+ return next((name for name, rx in KINDS if rx.search(text or "")), None)
@@ -0,0 +1,105 @@
1
+ """Test monitors in Trunk's documented shape: pass-on-retry (failed and passed on the same code) that heals after quiet
2
+ days, failure rate and broken with separate switch-on and switch-off thresholds, and slow and timeout-inflation labels.
3
+ Each test's status is kept in the data branch with the day it began, and every change is logged."""
4
+ from datetime import datetime, timedelta
5
+ from fnmatch import fnmatchcase
6
+
7
+ from stackvitals.core.config import DEFAULTS
8
+ from stackvitals.core.stats import median, percentile
9
+
10
+ ACTIVE = {"flaky", "broken", "suspect"}
11
+
12
+
13
+ def _iso(dt: datetime) -> str:
14
+ return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
15
+
16
+
17
+ def _cluster_start(events: list, quiet: timedelta) -> datetime | None:
18
+ # The first event of the latest unbroken stretch: flakes closer together than the heal time.
19
+ if not events:
20
+ return None
21
+ start = events[-1][0]
22
+ for i in range(len(events) - 1, 0, -1):
23
+ if events[i][0] - events[i - 1][0] >= quiet:
24
+ break
25
+ start = events[i - 1][0]
26
+ return start
27
+
28
+
29
+ def evaluate(ev_all: list, ev_win: list, hist: list[tuple[datetime, bool]], runs_since: int, now: datetime, cfg: dict,
30
+ previous: dict | None = None, pass_ms=(), fail_ms=(), test_id: str | None = None) -> dict:
31
+ """One test's state from its flake events (oldest first), its default-branch results ([(time, failed)], oldest
32
+ first) and, when kept, its previous status; plus labels and a mute."""
33
+ m = (cfg or DEFAULTS)["ci"]["monitors"]
34
+ was = set((previous or {}).get("monitors") or [])
35
+ fails = [f for _, f in hist]
36
+ b = m["broken"]
37
+ window = fails[-int(b["window_runs"]):] if b["window_runs"] else []
38
+ rate_b = sum(window) / len(window) if window else 0.0
39
+ streak = int(b["streak"])
40
+ by_streak = streak > 0 and len(fails) >= streak and all(fails[-streak:])
41
+ broken = by_streak or (len(window) >= b["window_runs"] and rate_b >= b["activate"]) or \
42
+ ("broken" in was and bool(window) and rate_b >= b["resolve"])
43
+ p = m["pass_on_retry"]
44
+ quiet = timedelta(days=p["quiet_days"])
45
+ last = ev_all[-1][0] if ev_all else None
46
+ retry = len(ev_win) >= p["min_events"] or (len(ev_all) >= p["min_events"] and last is not None and now - last < quiet)
47
+ fr = m["failure_rate"]
48
+ win = fails[-int(fr["window_runs"]):] if fr["window_runs"] else []
49
+ rate = sum(win) / len(win) if win else None
50
+ by_rate = bool(fr["enabled"]) and len(win) >= fr["min_runs"] and rate is not None and \
51
+ (rate >= fr["activate"] or ("failure_rate" in was and rate > fr["resolve"]))
52
+ monitors = [name for name, on in (("broken", broken), ("pass_on_retry", retry), ("failure_rate", by_rate)) if on]
53
+ if broken:
54
+ state = "broken"
55
+ if by_streak:
56
+ i = len(fails)
57
+ while i > 0 and fails[i - 1]:
58
+ i -= 1
59
+ since = hist[i][0]
60
+ else:
61
+ since = next(t for t, f in hist[-len(window):] if f)
62
+ elif retry or by_rate:
63
+ state = "flaky"
64
+ since = _cluster_start(ev_all, quiet) if retry else next((t for t, f in hist[-len(win):] if f), None)
65
+ elif ev_win:
66
+ state, since = "suspect", ev_win[0][0]
67
+ elif len(ev_all) >= p["min_events"] and runs_since >= p["quiet_runs"]:
68
+ state, since = "fixed", last
69
+ else:
70
+ state, since = "ok", None
71
+ out = {"state": state, "monitors": monitors, "since": _iso(since) if since else None,
72
+ "failure_rate": round(rate, 4) if rate is not None else None, "labels": []}
73
+ if previous and previous.get("state") == state and previous.get("since"):
74
+ out["since"] = previous["since"]
75
+ pass_ms, fail_ms = [x for x in pass_ms if x is not None], [x for x in fail_ms if x is not None]
76
+ if pass_ms and median(pass_ms) > m["slow"]["p50_ms"]:
77
+ out["labels"].append("slow")
78
+ t = m["timeout_inflation"]
79
+ if pass_ms and len(fail_ms) >= t["min_failures"] and median(fail_ms) >= t["ratio"] * percentile(pass_ms, 0.95):
80
+ out["labels"].append("timeout_inflation")
81
+ today = now.date().isoformat()
82
+ for pattern, until in ((cfg or DEFAULTS)["ci"].get("mute") or {}).items():
83
+ if test_id and until >= today and fnmatchcase(test_id, pattern):
84
+ out["muted_until"] = until
85
+ break
86
+ return out
87
+
88
+
89
+ def transitions(previous: dict, tests: list[dict], now: datetime) -> tuple[dict, list[dict]]:
90
+ """The status file to keep (tests that are flaky, broken or suspect, with the day it began) and the changes since the
91
+ last run, each one line for the transitions log."""
92
+ at = _iso(now)
93
+ previous = previous or {}
94
+ status, changes = {}, []
95
+ current = {t["id"]: t for t in tests}
96
+ for tid in sorted(set(current) | set(previous)):
97
+ t, prev = current.get(tid), previous.get(tid) or {}
98
+ state = t["state"] if t else "ok"
99
+ before = prev.get("state", "ok")
100
+ if state in ACTIVE:
101
+ since = prev["since"] if before == state and prev.get("since") else (t.get("since") or at)
102
+ status[tid] = {"state": state, "since": since, "monitors": t.get("monitors") or []}
103
+ if state != before:
104
+ changes.append({"at": at, "id": tid, "from": before, "to": state, "monitors": (t or {}).get("monitors") or []})
105
+ return status, changes
@@ -0,0 +1,116 @@
1
+ """Who owns a flaky test or job: CODEOWNERS (read with GitHub's rules) for the test's file, else for its workflow file.
2
+ CODEOWNERS and GitHub's error check are read once a day; the repo's file list at most once a day, for new test ids."""
3
+ import re
4
+
5
+ from stackvitals import codeowners
6
+ from stackvitals.ci.analyze import SHOWN
7
+ from stackvitals.ci.api import APIError, NotFound, OutOfBudget
8
+
9
+ SOURCE_EXT = {"py", "js", "jsx", "mjs", "cjs", "ts", "tsx", "rb", "go", "rs", "java", "kt", "kts", "scala", "cs", "php",
10
+ "swift", "ex", "exs", "c", "cc", "cpp", "m", "dart", "lua", "sh"}
11
+
12
+
13
+ def _path_like(part: str) -> bool:
14
+ return "/" in part or ("." in part and part.rsplit(".", 1)[-1] in SOURCE_EXT)
15
+
16
+
17
+ def _one(found: list[str]) -> str | None:
18
+ return found[0] if len(found) == 1 else None
19
+
20
+
21
+ def test_file(tid: str, files: list[str] | None) -> str | None:
22
+ """The repo file a test lives in, confirmed against the repo's files: the path in a pytest-style id (or the one file
23
+ ending with it, for runners started in a subfolder), or the one file matching the dotted class name
24
+ (tests.test_calc -> tests/test_calc.py, com.acme.BarTest -> .../com/acme/BarTest.java). No guess when unsure."""
25
+ if not files:
26
+ return None
27
+ parts = tid.split("::")
28
+ if _path_like(parts[0]):
29
+ if parts[0] in files:
30
+ return parts[0]
31
+ return _one([f for f in files if f.endswith("/" + parts[0])])
32
+ if len(parts) < 2 or " " in parts[0]:
33
+ return None
34
+ names = parts[0].split("$", 1)[0].split(".")
35
+ for n in range(len(names), 0, -1):
36
+ stem = "/".join(names[:n])
37
+ found = [f for f in files if f.rsplit(".", 1)[0] == stem or f.rsplit(".", 1)[0].endswith("/" + stem)]
38
+ if found:
39
+ return _one(found)
40
+ return None
41
+
42
+
43
+ def read_codeowners(api, ref: str | None, store, today: str) -> dict:
44
+ """The CODEOWNERS text GitHub uses (first of its three places), the owners its error check says it ignores, and the
45
+ lines it skips as invalid; read at most once a day. A failed read keeps the last answer and tries again next run."""
46
+ state = store.state()
47
+ kept = state.get("codeowners") or {}
48
+ answer = {"text": kept.get("text"), "unknown": set(kept.get("unknown") or []), "skip": set(kept.get("skip") or [])}
49
+ if kept.get("checked") == today:
50
+ return answer
51
+ text = None
52
+ try:
53
+ for path in codeowners.PATHS:
54
+ try:
55
+ text = api.file(path, ref).decode("utf-8", "replace")
56
+ break
57
+ except NotFound:
58
+ continue # GitHub only moves on to the next place when a file is missing
59
+ except (APIError, OutOfBudget):
60
+ return answer
61
+ unknown, skip = answer["unknown"], answer["skip"]
62
+ if text:
63
+ try:
64
+ errors = (api._get(f"/repos/{api.repo}/codeowners/errors", {"ref": ref} if ref else None) or {}).get("errors") or []
65
+ unknown = {m.group(1).lower() for e in errors if e.get("kind") == "Unknown owner"
66
+ and (m := re.search(r"make sure (\S+) exists", e.get("message") or ""))}
67
+ skip = {e["line"] for e in errors if e.get("kind") != "Unknown owner" and isinstance(e.get("line"), int)}
68
+ except (APIError, OutOfBudget):
69
+ pass # the error check is extra: without it every owner is kept as written
70
+ store.save_state({**state, "codeowners": {"checked": today, "text": text, "unknown": sorted(unknown), "skip": sorted(skip)}})
71
+ return {"text": text, "unknown": unknown, "skip": skip}
72
+
73
+
74
+ def test_files(api, ref: str | None, store, today: str, tids: list[str]) -> dict[str, str]:
75
+ """Each test's file, checked against the repo's file list (one request, at most once a day, only for new ids)."""
76
+ state = store.state()
77
+ known = dict(state.get("test_files") or {})
78
+ missing = [t for t in tids if t not in known]
79
+ if missing and state.get("tree_checked") != today:
80
+ try:
81
+ tree = api._get(f"/repos/{api.repo}/git/trees/{ref}", {"recursive": "1"}) or {}
82
+ files = [e["path"] for e in tree.get("tree") or [] if e.get("type") == "blob"]
83
+ for tid in missing:
84
+ if (path := test_file(tid, files)):
85
+ known[tid] = path
86
+ except (APIError, OutOfBudget):
87
+ pass
88
+ store.save_state({**store.state(), "test_files": known, "tree_checked": today})
89
+ return {t: known[t] for t in tids if t in known}
90
+
91
+
92
+ def attach_owners(result: dict, co: dict, files: dict[str, str]) -> None:
93
+ """Adds `owners` and `owners_from` (the path they were looked up for) to every test and job the dashboard lists."""
94
+ if not co.get("text"):
95
+ return
96
+ rules, unknown = codeowners.parse(co["text"], co.get("skip") or ()), co.get("unknown") or set()
97
+ for t in result.get("tests") or []:
98
+ if t["state"] in SHOWN:
99
+ path = files.get(t["id"])
100
+ owners = codeowners.owners_for(path, rules, unknown) if path else []
101
+ if not owners and t.get("workflow"):
102
+ path, owners = t["workflow"], codeowners.owners_for(t["workflow"], rules, unknown)
103
+ t["owners"], t["owners_from"] = owners, path
104
+ for j in result.get("jobs") or []:
105
+ if j["state"] in SHOWN and j.get("workflow"):
106
+ j["owners"], j["owners_from"] = codeowners.owners_for(j["workflow"], rules, unknown), j["workflow"]
107
+
108
+
109
+ def owners_line(item: dict, mention: bool = False) -> str:
110
+ """Markdown naming the owners; code-formatted names notify nobody unless mentions are turned on."""
111
+ if not item.get("owners"):
112
+ return ""
113
+ # A path can't close the code format early and slip a live @-mention into the issue.
114
+ where = re.sub(r"[`\r\n]", "", item.get("owners_from") or "")
115
+ names = " ".join(item["owners"]) if mention else ", ".join(f"`{o}`" for o in item["owners"])
116
+ return f"Owners (CODEOWNERS for `{where}`): {names}"
@@ -0,0 +1,90 @@
1
+ """Quarantine at test time, so a flaky test stops failing the suite without wrapping the CI command.
2
+
3
+ stackvitals ci quarantine --print jest # flags for the test runner, from the quarantine list
4
+ pytest $(stackvitals ci quarantine --print pytest) # writes .stackvitals-quarantine.json, prints -p ...
5
+
6
+ pytest runs a quarantined test as an expected failure (it still runs and reports, but can't fail the suite); Jest,
7
+ Vitest, go test, Playwright, Maven Surefire and dotnet test leave it out by name. Other runners (RSpec, Gradle): wrap the
8
+ command with `stackvitals ci quarantine -- <command>`. Only tests on the list are touched, and an empty or unreadable list touches none.
9
+ """
10
+ import json
11
+ import os
12
+ import re
13
+ from pathlib import Path
14
+
15
+
16
+ def _parts(tid: str) -> tuple[str, str]:
17
+ # "file::class::name" (JUnit, Jest) or "pkg::TestName/sub" (Go): what's before the last part, and the last part.
18
+ head, _, name = tid.rpartition("::")
19
+ return head, name
20
+
21
+
22
+ def runtime_args(ids: list[str] | set[str], runner: str) -> list[str]:
23
+ """The command-line arguments that keep quarantined tests from failing a run of `runner`."""
24
+ ids = sorted(ids or [])
25
+ if not ids:
26
+ return []
27
+ if runner == "pytest":
28
+ return ["-p", "stackvitals.ci.quarantine_runtime"]
29
+ if runner in ("jest", "vitest"):
30
+ names = sorted({re.escape(_parts(t)[1]) for t in ids if _parts(t)[1]})
31
+ flag = "--testNamePattern" if runner == "jest" else "-t"
32
+ # Runners match the full name (describe blocks first), so the test's own name is matched at the end.
33
+ return [flag, f"^(?!.*(?:{'|'.join(names)})$).*"]
34
+ if runner == "go":
35
+ # go test -skip takes one pattern per level; only top-level tests can be listed together safely.
36
+ tops = sorted({re.escape(_parts(t)[1]) for t in ids
37
+ if re.fullmatch(r"(Test|Fuzz|Example)[A-Z0-9_]\w*", _parts(t)[1] or "")})
38
+ return ["-skip", f"^(?:{'|'.join(tops)})$"] if tops else []
39
+ if runner == "playwright":
40
+ # The full title ends with the test's own name; --grep-invert leaves those out.
41
+ names = sorted({re.escape(_parts(t)[1]) for t in ids if _parts(t)[1]})
42
+ return ["--grep-invert", f"(?:{'|'.join(names)})$"]
43
+ if runner == "maven":
44
+ # Surefire 2.19+: exclusion-only patterns run everything else; the class is the last part of the classname.
45
+ pats = sorted({f"!{_parts(t)[0].split('::')[-1].split('.')[-1]}#{_parts(t)[1]}" for t in ids if _parts(t)[0] and _parts(t)[1]})
46
+ return [f"-Dtest={','.join(pats)}", "-Dsurefire.failIfNoSpecifiedTests=false"] if pats else []
47
+ if runner == "dotnet":
48
+ fqns = sorted({f"{_parts(t)[0].split('::')[-1]}.{_parts(t)[1]}" for t in ids if _parts(t)[0] and _parts(t)[1]})
49
+ return ["--filter", "&".join(f"FullyQualifiedName!={f}" for f in fqns)] if fqns else []
50
+ raise ValueError(f"runner is one of pytest, jest, vitest, go, playwright, maven, dotnet, not {runner!r}")
51
+
52
+
53
+ LIST_FILE = ".stackvitals-quarantine.json" # written by `stackvitals ci quarantine --print pytest`
54
+
55
+
56
+ def _quarantined() -> set[str]:
57
+ src = os.environ.get("STACKVITALS_QUARANTINE") or LIST_FILE
58
+ try:
59
+ items = json.loads(Path(src).read_text(encoding="utf-8"))
60
+ except (OSError, ValueError):
61
+ return set()
62
+ return {str(i) for i in items} if isinstance(items, list) else set()
63
+
64
+
65
+ def _tokens(text: str) -> list[str]:
66
+ # "tests.test_a.TestCls" and "tests/test_a.py::TestCls" both read as tests, test_a, TestCls.
67
+ return [p for p in re.split(r"[./:\\]+", text) if p and p != "py"]
68
+
69
+
70
+ def _matches(tid: str, nodeid: str, name: str) -> bool:
71
+ # Same test name, and the report's module and class end the same way as the node id's file and class (either may
72
+ # carry more folders in front, since the two can start from different roots).
73
+ head, last = _parts(tid)
74
+ if last != name:
75
+ return False
76
+ a, b = _tokens(head), _tokens(nodeid.rpartition("::")[0])
77
+ if not a or not b:
78
+ return not a
79
+ n = min(len(a), len(b))
80
+ return a[-n:] == b[-n:]
81
+
82
+
83
+ def pytest_collection_modifyitems(config, items): # pragma: no cover - exercised through pytest itself
84
+ import pytest
85
+ ids = _quarantined()
86
+ if not ids:
87
+ return
88
+ for item in items:
89
+ if any(_matches(t, item.nodeid, item.name) for t in ids):
90
+ item.add_marker(pytest.mark.xfail(reason="quarantined by StackVitals (known flaky)", strict=False))