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.
- mergescout/__init__.py +1 -0
- mergescout/archive/__init__.py +0 -0
- mergescout/archive/__main__.py +30 -0
- mergescout/archive/convert.py +83 -0
- mergescout/archive/events.py +55 -0
- mergescout/archive/manifest.py +45 -0
- mergescout/archive/stream.py +83 -0
- mergescout/bench/__init__.py +1 -0
- mergescout/bench/__main__.py +135 -0
- mergescout/bench/ci.py +257 -0
- mergescout/bench/deploys.py +115 -0
- mergescout/bench/matched.py +22 -0
- mergescout/bench/prior.py +42 -0
- mergescout/bench/report.py +230 -0
- mergescout/bench/shared.py +112 -0
- mergescout/bench/stars.py +57 -0
- mergescout/cli.py +89 -0
- mergescout/config.py +35 -0
- mergescout/crawl/__init__.py +0 -0
- mergescout/crawl/__main__.py +97 -0
- mergescout/crawl/chain.py +34 -0
- mergescout/crawl/fetch.py +146 -0
- mergescout/crawl/outcomes.py +69 -0
- mergescout/crawl/queries.py +250 -0
- mergescout/crawl/runner.py +176 -0
- mergescout/crawl/seed.py +77 -0
- mergescout/crawl/state.py +169 -0
- mergescout/crawl/writer.py +87 -0
- mergescout/discover/__init__.py +0 -0
- mergescout/discover/__main__.py +58 -0
- mergescout/discover/sweep.py +286 -0
- mergescout/github/__init__.py +0 -0
- mergescout/github/graphql.py +202 -0
- mergescout/github/identities.py +35 -0
- mergescout/github/releases.py +137 -0
- mergescout/labels.py +20 -0
- mergescout/mcp_server.py +228 -0
- mergescout/measure/__init__.py +0 -0
- mergescout/measure/__main__.py +75 -0
- mergescout/measure/api_cost.py +91 -0
- mergescout/measure/count_repos.py +30 -0
- mergescout/measure/jsonl.py +14 -0
- mergescout/measure/report.py +56 -0
- mergescout/measure/sample.py +37 -0
- mergescout/owners.py +22 -0
- mergescout/setup_check.py +64 -0
- mergescout/signals/__init__.py +0 -0
- mergescout/signals/__main__.py +96 -0
- mergescout/signals/bump_ledger.py +258 -0
- mergescout/signals/bumps.py +104 -0
- mergescout/signals/check.py +141 -0
- mergescout/signals/community_bench.py +149 -0
- mergescout/signals/delivery_bench.py +103 -0
- mergescout/signals/deps_map.py +261 -0
- mergescout/signals/deps_trends.py +117 -0
- mergescout/signals/evidence.py +94 -0
- mergescout/signals/exploits.py +43 -0
- mergescout/signals/flow_bench.py +258 -0
- mergescout/signals/identity.py +73 -0
- mergescout/signals/issues.py +76 -0
- mergescout/signals/labels.py +79 -0
- mergescout/signals/osv.py +232 -0
- mergescout/signals/owners_watch.py +80 -0
- mergescout/signals/repo_stats.py +96 -0
- mergescout/signals/rules.py +70 -0
- mergescout/signals/silver.py +33 -0
- mergescout/signals/top_packages.py +26 -0
- mergescout/signals/triage.py +71 -0
- mergescout/signals/triage_math.py +120 -0
- mergescout/signals/walls.py +91 -0
- mergescout/snapshot/__init__.py +0 -0
- mergescout/snapshot/__main__.py +215 -0
- mergescout/snapshot/build.py +147 -0
- mergescout/snapshot/publish.py +70 -0
- mergescout/snapshot/schema.py +34 -0
- mergescout/snapshot/score.py +30 -0
- mergescout/store/__init__.py +0 -0
- mergescout/store/__main__.py +46 -0
- mergescout/store/sync.py +148 -0
- mergescout/timeutil.py +11 -0
- mergescout/tool/__init__.py +0 -0
- mergescout/tool/action_pack.py +62 -0
- mergescout/tool/live.py +201 -0
- mergescout/tool/queries.py +293 -0
- mergescout/tool/refs.py +31 -0
- mergescout/tool/store.py +129 -0
- mergescout/tool/tracking.py +83 -0
- stackvitals/__init__.py +2 -0
- stackvitals/__main__.py +4 -0
- stackvitals/ci/__init__.py +1 -0
- stackvitals/ci/action.py +574 -0
- stackvitals/ci/analyze.py +577 -0
- stackvitals/ci/api.py +218 -0
- stackvitals/ci/automation.py +212 -0
- stackvitals/ci/bench.py +195 -0
- stackvitals/ci/burnin.py +102 -0
- stackvitals/ci/cli.py +570 -0
- stackvitals/ci/collect.py +239 -0
- stackvitals/ci/cost.py +107 -0
- stackvitals/ci/coverage.py +155 -0
- stackvitals/ci/feedback.py +699 -0
- stackvitals/ci/fingerprint.py +220 -0
- stackvitals/ci/gitlab.py +121 -0
- stackvitals/ci/gitlab_action.py +125 -0
- stackvitals/ci/impact.py +125 -0
- stackvitals/ci/investigate.py +59 -0
- stackvitals/ci/kinds.py +52 -0
- stackvitals/ci/monitors.py +105 -0
- stackvitals/ci/owners.py +116 -0
- stackvitals/ci/quarantine_runtime.py +90 -0
- stackvitals/ci/report.py +435 -0
- stackvitals/ci/retry.py +67 -0
- stackvitals/ci/selfheal.py +102 -0
- stackvitals/ci/shards.py +120 -0
- stackvitals/ci/share.py +59 -0
- stackvitals/ci/store.py +93 -0
- stackvitals/ci/testreports.py +351 -0
- stackvitals/ci/tickets.py +106 -0
- stackvitals/ci/track_record.json +11 -0
- stackvitals/ci/triage.py +130 -0
- stackvitals/ci/triggers.py +147 -0
- stackvitals/ci/upload.py +85 -0
- stackvitals/ci/validate.py +91 -0
- stackvitals/cli.py +111 -0
- stackvitals/codeowners.py +69 -0
- stackvitals/community/__init__.py +0 -0
- stackvitals/community/action.py +140 -0
- stackvitals/community/affiliation.py +51 -0
- stackvitals/community/analyze.py +507 -0
- stackvitals/community/build.py +56 -0
- stackvitals/community/chaoss.py +70 -0
- stackvitals/community/cli.py +79 -0
- stackvitals/community/collect.py +240 -0
- stackvitals/community/context.py +54 -0
- stackvitals/community/report.py +250 -0
- stackvitals/connectors/__init__.py +244 -0
- stackvitals/connectors/ai_spend.py +190 -0
- stackvitals/connectors/base.py +82 -0
- stackvitals/connectors/check.py +686 -0
- stackvitals/connectors/copilot.py +104 -0
- stackvitals/connectors/discord.py +103 -0
- stackvitals/connectors/discourse.py +117 -0
- stackvitals/connectors/incident_io.py +106 -0
- stackvitals/connectors/jira.py +210 -0
- stackvitals/connectors/linear.py +140 -0
- stackvitals/connectors/pagerduty.py +96 -0
- stackvitals/connectors/slack.py +110 -0
- stackvitals/connectors/sonar.py +77 -0
- stackvitals/connectors/store.py +120 -0
- stackvitals/connectors/tracker.py +108 -0
- stackvitals/connectors/writeback.py +105 -0
- stackvitals/contribute/__init__.py +1 -0
- stackvitals/contribute/brief.py +297 -0
- stackvitals/contribute/cli.py +64 -0
- stackvitals/core/__init__.py +1 -0
- stackvitals/core/actors.py +103 -0
- stackvitals/core/agent_ledger.py +120 -0
- stackvitals/core/agreements.py +95 -0
- stackvitals/core/ai_outcomes.py +276 -0
- stackvitals/core/alerts.py +224 -0
- stackvitals/core/appdata.py +112 -0
- stackvitals/core/bucket.py +190 -0
- stackvitals/core/bugs.py +43 -0
- stackvitals/core/calendar.py +34 -0
- stackvitals/core/capex.py +57 -0
- stackvitals/core/change_risk.py +181 -0
- stackvitals/core/changes.py +133 -0
- stackvitals/core/chat.py +143 -0
- stackvitals/core/config.py +373 -0
- stackvitals/core/dataflow.py +168 -0
- stackvitals/core/decisions.py +275 -0
- stackvitals/core/delivery.py +954 -0
- stackvitals/core/delivery_peers.py +98 -0
- stackvitals/core/delivery_report.py +429 -0
- stackvitals/core/deliverycli.py +147 -0
- stackvitals/core/devlake_dora.sql +485 -0
- stackvitals/core/digest.py +333 -0
- stackvitals/core/explaincli.py +107 -0
- stackvitals/core/explore.py +296 -0
- stackvitals/core/export.py +342 -0
- stackvitals/core/exportcli.py +117 -0
- stackvitals/core/findings.py +350 -0
- stackvitals/core/health.py +94 -0
- stackvitals/core/identity.py +82 -0
- stackvitals/core/insights.py +365 -0
- stackvitals/core/insightscli.py +43 -0
- stackvitals/core/installdoc.py +69 -0
- stackvitals/core/investment.py +187 -0
- stackvitals/core/joins.py +43 -0
- stackvitals/core/llmsdoc.py +40 -0
- stackvitals/core/merge_queue.py +70 -0
- stackvitals/core/metrics.py +109 -0
- stackvitals/core/model.py +285 -0
- stackvitals/core/projectcli.py +66 -0
- stackvitals/core/propose.py +86 -0
- stackvitals/core/pulse.py +85 -0
- stackvitals/core/recorded.py +145 -0
- stackvitals/core/releases.py +39 -0
- stackvitals/core/requests.py +528 -0
- stackvitals/core/resync.py +35 -0
- stackvitals/core/schema.py +205 -0
- stackvitals/core/services.py +203 -0
- stackvitals/core/settings.py +96 -0
- stackvitals/core/sqlcli.py +111 -0
- stackvitals/core/stats.py +222 -0
- stackvitals/core/store.py +125 -0
- stackvitals/core/summary.py +104 -0
- stackvitals/core/time.py +29 -0
- stackvitals/core/tomledit.py +149 -0
- stackvitals/core/uicli.py +138 -0
- stackvitals/core/webapp.py +79 -0
- stackvitals/core/what_changed.py +76 -0
- stackvitals/core/workspace.py +194 -0
- stackvitals/deps/__init__.py +1 -0
- stackvitals/deps/action.py +299 -0
- stackvitals/deps/actions_vulns.py +89 -0
- stackvitals/deps/cli.py +77 -0
- stackvitals/deps/dossier.py +341 -0
- stackvitals/deps/evidence.py +44 -0
- stackvitals/deps/fixplan.py +40 -0
- stackvitals/deps/guarddog.py +32 -0
- stackvitals/deps/hygiene.py +47 -0
- stackvitals/deps/imports.py +113 -0
- stackvitals/deps/lockfiles.py +198 -0
- stackvitals/deps/lookalike.py +80 -0
- stackvitals/deps/reach.py +59 -0
- stackvitals/deps/release.py +110 -0
- stackvitals/deps/runtimes.py +161 -0
- stackvitals/deps/supply.py +454 -0
- stackvitals/deps/vulns.py +588 -0
- stackvitals/editors/__init__.py +40 -0
- stackvitals/editors/vscode/extension.js +101 -0
- stackvitals/editors/vscode/marks.js +92 -0
- stackvitals/editors/vscode/package.json +26 -0
- stackvitals/flow/__init__.py +1 -0
- stackvitals/flow/action.py +178 -0
- stackvitals/flow/analyze.py +418 -0
- stackvitals/flow/automate.py +72 -0
- stackvitals/flow/build.py +46 -0
- stackvitals/flow/cli.py +93 -0
- stackvitals/flow/code_hosts.py +146 -0
- stackvitals/flow/collect.py +451 -0
- stackvitals/flow/gitlab_mr.py +75 -0
- stackvitals/flow/knowledge.py +202 -0
- stackvitals/flow/peers.py +90 -0
- stackvitals/flow/preflight.py +210 -0
- stackvitals/flow/report.py +253 -0
- stackvitals/flow/rules.py +135 -0
- stackvitals/flow/store.py +108 -0
- stackvitals/flow/survival.py +84 -0
- stackvitals/flow/triage.py +62 -0
- stackvitals/mcp_server.py +670 -0
- stackvitals/portal/__init__.py +0 -0
- stackvitals/portal/action.py +205 -0
- stackvitals/portal/batch.py +80 -0
- stackvitals/portal/campaigns.py +104 -0
- stackvitals/portal/catalog.py +104 -0
- stackvitals/portal/checks.py +63 -0
- stackvitals/portal/cli.py +172 -0
- stackvitals/portal/descriptor.py +105 -0
- stackvitals/portal/export.py +43 -0
- stackvitals/portal/github.py +48 -0
- stackvitals/portal/golden.py +95 -0
- stackvitals/portal/history.py +30 -0
- stackvitals/portal/inventory.py +264 -0
- stackvitals/portal/owners.py +78 -0
- stackvitals/portal/pages.py +153 -0
- stackvitals/portal/rollup.py +32 -0
- stackvitals/portal/scorecard.py +366 -0
- stackvitals/security/__init__.py +2 -0
- stackvitals/security/probes.py +154 -0
- stackvitals/security/repo_checks.py +132 -0
- stackvitals/security/workflows.py +111 -0
- stackvitals/shell.py +107 -0
- stackvitals/site.py +51 -0
- stackvitals/web/app/LICENSES.md +428 -0
- stackvitals/web/app/boot.js +1 -0
- stackvitals/web/app/chunks/AddRepo-KRPZXWCY.js +1 -0
- stackvitals/web/app/chunks/SetupRunning-XWFQAKRZ.js +1 -0
- stackvitals/web/app/chunks/accuracy-data-OQTDXI7Y.js +1 -0
- stackvitals/web/app/chunks/agents-data-BAL6QAOF.js +11 -0
- stackvitals/web/app/chunks/chunk-24LFKFZ6.js +1 -0
- stackvitals/web/app/chunks/chunk-3G4SQNUC.js +1 -0
- stackvitals/web/app/chunks/chunk-3IXWEOIJ.js +154 -0
- stackvitals/web/app/chunks/chunk-3ROQFHPV.js +1 -0
- stackvitals/web/app/chunks/chunk-45PGAQRO.js +8 -0
- stackvitals/web/app/chunks/chunk-4H53OYLG.js +1 -0
- stackvitals/web/app/chunks/chunk-4LUI7O5P.js +1 -0
- stackvitals/web/app/chunks/chunk-4SQPQ5TB.js +1 -0
- stackvitals/web/app/chunks/chunk-4WKQTWUC.js +2 -0
- stackvitals/web/app/chunks/chunk-576YHDEK.js +8 -0
- stackvitals/web/app/chunks/chunk-6BDBXA6V.js +1 -0
- stackvitals/web/app/chunks/chunk-6RLVD2DT.js +1 -0
- stackvitals/web/app/chunks/chunk-763NH7PP.js +1 -0
- stackvitals/web/app/chunks/chunk-76WAVKUG.js +1 -0
- stackvitals/web/app/chunks/chunk-7II2BD5F.js +1 -0
- stackvitals/web/app/chunks/chunk-7TJCBOAY.js +1 -0
- stackvitals/web/app/chunks/chunk-ASYNLVOP.js +1 -0
- stackvitals/web/app/chunks/chunk-CTKDYZMG.js +1 -0
- stackvitals/web/app/chunks/chunk-CVB26NMC.js +1 -0
- stackvitals/web/app/chunks/chunk-G7NGB5UX.js +1 -0
- stackvitals/web/app/chunks/chunk-GPVLJRZD.js +1 -0
- stackvitals/web/app/chunks/chunk-GQYFN4PE.js +75 -0
- stackvitals/web/app/chunks/chunk-GT4EAKAO.js +2 -0
- stackvitals/web/app/chunks/chunk-IYMBFPCV.js +1 -0
- stackvitals/web/app/chunks/chunk-LAAGCPU3.js +1 -0
- stackvitals/web/app/chunks/chunk-MAPYGK3P.js +1 -0
- stackvitals/web/app/chunks/chunk-P5TXDD45.js +1 -0
- stackvitals/web/app/chunks/chunk-PABICGBL.js +1 -0
- stackvitals/web/app/chunks/chunk-RPXHAG6F.js +1 -0
- stackvitals/web/app/chunks/chunk-RQZPSJ2Y.js +1 -0
- stackvitals/web/app/chunks/chunk-S2ZBMEVA.js +1 -0
- stackvitals/web/app/chunks/chunk-S654BBEP.js +6 -0
- stackvitals/web/app/chunks/chunk-T7QVTNXF.js +1 -0
- stackvitals/web/app/chunks/chunk-THFUOCPC.js +273 -0
- stackvitals/web/app/chunks/chunk-TO4VVRZ7.js +1 -0
- stackvitals/web/app/chunks/chunk-VQ5YXO54.js +1 -0
- stackvitals/web/app/chunks/chunk-VWKSMFWJ.js +1 -0
- stackvitals/web/app/chunks/chunk-Y6LUXDM6.js +1 -0
- stackvitals/web/app/chunks/chunk-YTANGYYH.js +1 -0
- stackvitals/web/app/chunks/connect-KONYWYTT.js +1 -0
- stackvitals/web/app/chunks/connections-data-2DYS4UVW.js +32 -0
- stackvitals/web/app/chunks/logos-N5IINUAM.js +1 -0
- stackvitals/web/app/chunks/more-KMJMJQD3.js +10 -0
- stackvitals/web/app/chunks/more-PC6FVS3P.js +1 -0
- stackvitals/web/app/chunks/more-RCIVCUDE.js +2 -0
- stackvitals/web/app/chunks/more-ZKKRY6NO.css +1 -0
- stackvitals/web/app/chunks/sealbox-35YL4EUH.js +3 -0
- stackvitals/web/app/chunks/session-DYYTOE2K.js +1 -0
- stackvitals/web/app/chunks/settingsApply-2HRJEZYM.js +1 -0
- stackvitals/web/app/chunks/settingsReview-GX2FZQ4M.js +3 -0
- stackvitals/web/app/chunks/uPlot.esm-OIWT3AHJ.js +3 -0
- stackvitals/web/app/demo/ci/impact.json +1 -0
- stackvitals/web/app/demo/ci/quarantine.json +1 -0
- stackvitals/web/app/demo/ci/tickets.json +1 -0
- stackvitals/web/app/demo/mobile/alerts.json +1 -0
- stackvitals/web/app/demo/mobile/app.json +1 -0
- stackvitals/web/app/demo/mobile/community.json +1 -0
- stackvitals/web/app/demo/mobile/connectors.json +1 -0
- stackvitals/web/app/demo/mobile/data.json +1 -0
- stackvitals/web/app/demo/mobile/decisions.json +1 -0
- stackvitals/web/app/demo/mobile/delivery.json +1 -0
- stackvitals/web/app/demo/mobile/deps.json +1 -0
- stackvitals/web/app/demo/mobile/flow.json +1 -0
- stackvitals/web/app/demo/mobile/health.json +1 -0
- stackvitals/web/app/demo/mobile/insights.json +1 -0
- stackvitals/web/app/demo/mobile/pulse.json +1 -0
- stackvitals/web/app/demo/mobile/security.json +1 -0
- stackvitals/web/app/demo/mobile/summary/ci_jobs.jsonl +5 -0
- stackvitals/web/app/demo/mobile/summary/devlake_project_pr_metrics.jsonl +14 -0
- stackvitals/web/app/demo/mobile/summary/devlake_pull_requests.jsonl +14 -0
- stackvitals/web/app/demo/mobile/summary/manifest.json +1 -0
- stackvitals/web/app/demo/mobile/summary/prs.jsonl +19 -0
- stackvitals/web/app/demo/mobile/summary/tests.jsonl +1 -0
- stackvitals/web/app/demo/mobile/supply.json +1 -0
- stackvitals/web/app/demo/mobile/vulns.json +1 -0
- stackvitals/web/app/demo/payments-api/alerts.json +1 -0
- stackvitals/web/app/demo/payments-api/app.json +1 -0
- stackvitals/web/app/demo/payments-api/community.json +1 -0
- stackvitals/web/app/demo/payments-api/connectors.json +1 -0
- stackvitals/web/app/demo/payments-api/data.json +1 -0
- stackvitals/web/app/demo/payments-api/decisions.json +1 -0
- stackvitals/web/app/demo/payments-api/delivery.json +1 -0
- stackvitals/web/app/demo/payments-api/deps.json +1 -0
- stackvitals/web/app/demo/payments-api/flow.json +1 -0
- stackvitals/web/app/demo/payments-api/health.json +1 -0
- stackvitals/web/app/demo/payments-api/insights.json +1 -0
- stackvitals/web/app/demo/payments-api/pulse.json +1 -0
- stackvitals/web/app/demo/payments-api/security.json +1 -0
- stackvitals/web/app/demo/payments-api/summary/ci_jobs.jsonl +5 -0
- stackvitals/web/app/demo/payments-api/summary/devlake_project_pr_metrics.jsonl +14 -0
- stackvitals/web/app/demo/payments-api/summary/devlake_pull_requests.jsonl +14 -0
- stackvitals/web/app/demo/payments-api/summary/manifest.json +1 -0
- stackvitals/web/app/demo/payments-api/summary/prs.jsonl +22 -0
- stackvitals/web/app/demo/payments-api/summary/tests.jsonl +3 -0
- stackvitals/web/app/demo/payments-api/supply.json +1 -0
- stackvitals/web/app/demo/payments-api/vulns.json +1 -0
- stackvitals/web/app/demo/portal.json +1 -0
- stackvitals/web/app/demo/settings.json +1 -0
- stackvitals/web/app/demo/web-app/alerts.json +1 -0
- stackvitals/web/app/demo/web-app/app.json +1 -0
- stackvitals/web/app/demo/web-app/community.json +1 -0
- stackvitals/web/app/demo/web-app/connectors.json +1 -0
- stackvitals/web/app/demo/web-app/data.json +1 -0
- stackvitals/web/app/demo/web-app/decisions.json +1 -0
- stackvitals/web/app/demo/web-app/delivery.json +1 -0
- stackvitals/web/app/demo/web-app/deps.json +1 -0
- stackvitals/web/app/demo/web-app/flow.json +1 -0
- stackvitals/web/app/demo/web-app/health.json +1 -0
- stackvitals/web/app/demo/web-app/insights.json +1 -0
- stackvitals/web/app/demo/web-app/pulse.json +1 -0
- stackvitals/web/app/demo/web-app/security.json +1 -0
- stackvitals/web/app/demo/web-app/summary/ci_jobs.jsonl +5 -0
- stackvitals/web/app/demo/web-app/summary/devlake_project_pr_metrics.jsonl +14 -0
- stackvitals/web/app/demo/web-app/summary/devlake_pull_requests.jsonl +14 -0
- stackvitals/web/app/demo/web-app/summary/manifest.json +1 -0
- stackvitals/web/app/demo/web-app/summary/prs.jsonl +18 -0
- stackvitals/web/app/demo/web-app/summary/tests.jsonl +2 -0
- stackvitals/web/app/demo/web-app/supply.json +1 -0
- stackvitals/web/app/demo/web-app/vulns.json +1 -0
- stackvitals/web/app/fonts/plex-mono-400.woff2 +0 -0
- stackvitals/web/app/fonts/plex-sans-400.woff2 +0 -0
- stackvitals/web/app/fonts/plex-sans-600.woff2 +0 -0
- stackvitals/web/app/fonts/plex-sans-condensed-700.woff2 +0 -0
- stackvitals/web/app/help/accuracy.json +1 -0
- stackvitals/web/app/help/connectors.json +1 -0
- stackvitals/web/app/help/getting-started.json +1 -0
- stackvitals/web/app/help/index.json +1 -0
- stackvitals/web/app/help/modules.json +1 -0
- stackvitals/web/app/help/privacy-and-security.json +1 -0
- stackvitals/web/app/help/reference.json +1 -0
- stackvitals/web/app/main.css +1 -0
- stackvitals/web/app/main.js +1 -0
- stackvitals/web/app/pages/about.css +1 -0
- stackvitals/web/app/pages/about.js +1 -0
- stackvitals/web/app/pages/activity.css +1 -0
- stackvitals/web/app/pages/activity.js +1 -0
- stackvitals/web/app/pages/agents.css +1 -0
- stackvitals/web/app/pages/agents.js +2 -0
- stackvitals/web/app/pages/ci.css +1 -0
- stackvitals/web/app/pages/ci.js +1 -0
- stackvitals/web/app/pages/community.css +1 -0
- stackvitals/web/app/pages/community.js +1 -0
- stackvitals/web/app/pages/connections.css +1 -0
- stackvitals/web/app/pages/connections.js +6 -0
- stackvitals/web/app/pages/delivery.css +1 -0
- stackvitals/web/app/pages/delivery.js +45 -0
- stackvitals/web/app/pages/deps.css +1 -0
- stackvitals/web/app/pages/deps.js +7 -0
- stackvitals/web/app/pages/explore.css +1 -0
- stackvitals/web/app/pages/explore.js +7 -0
- stackvitals/web/app/pages/health.css +1 -0
- stackvitals/web/app/pages/health.js +1 -0
- stackvitals/web/app/pages/help.css +1 -0
- stackvitals/web/app/pages/help.js +6 -0
- stackvitals/web/app/pages/insights.css +1 -0
- stackvitals/web/app/pages/insights.js +1 -0
- stackvitals/web/app/pages/investment.css +1 -0
- stackvitals/web/app/pages/investment.js +3 -0
- stackvitals/web/app/pages/overview.css +1 -0
- stackvitals/web/app/pages/overview.js +1 -0
- stackvitals/web/app/pages/portal.css +1 -0
- stackvitals/web/app/pages/portal.js +51 -0
- stackvitals/web/app/pages/prs.css +1 -0
- stackvitals/web/app/pages/prs.js +4 -0
- stackvitals/web/app/pages/settings.css +1 -0
- stackvitals/web/app/pages/settings.js +1 -0
- stackvitals/web/app/pages/setup.css +1 -0
- stackvitals/web/app/pages/setup.js +2 -0
- stackvitals/web/app/pages/standing.css +1 -0
- stackvitals/web/app/pages/standing.js +1 -0
- stackvitals/web/app/pages/welcome-sheet.js +1 -0
- stackvitals/web/app/pages/welcome.js +1 -0
- stackvitals/web/app/settings-workflow.json +1 -0
- stackvitals/web/index.html +34 -0
- stackvitals/web/site/index.html +108 -0
- stackvitals/web/site/integrations.html +15 -0
- stackvitals/web/site/setup.html +15 -0
- stackvitals/web/site/site.css +68 -0
- stackvitals-0.1.0.dist-info/METADATA +134 -0
- stackvitals-0.1.0.dist-info/RECORD +465 -0
- stackvitals-0.1.0.dist-info/WHEEL +4 -0
- stackvitals-0.1.0.dist-info/entry_points.txt +3 -0
- stackvitals-0.1.0.dist-info/licenses/LICENSE +202 -0
- stackvitals-0.1.0.dist-info/licenses/NOTICE +16 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Decisions people make about items (quarantine a test, mute a job, snooze a finding, exclude a pull request), written on
|
|
2
|
+
GitHub as `/stackvitals` commands or as a request block from the app. The data branch keeps every decision in an
|
|
3
|
+
append-only log (who, when, why, until when, from where); the current state is the newest decision per item, and a
|
|
4
|
+
temporary one drops out on its review date, so nothing stays switched off by accident."""
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
from dataclasses import asdict, dataclass
|
|
9
|
+
from datetime import date, datetime, timedelta
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from stackvitals.core.time import iso
|
|
13
|
+
|
|
14
|
+
LOG = "decisions/log.ndjson"
|
|
15
|
+
STATE = "decisions/state.json"
|
|
16
|
+
# Which items each verb applies to, and how long a temporary decision lasts unless the command says (None: no end).
|
|
17
|
+
VERBS = {
|
|
18
|
+
"quarantine": (("test",), 30), "unquarantine": (("test",), None),
|
|
19
|
+
"mute": (("test", "job"), 30), "unmute": (("test", "job"), None),
|
|
20
|
+
"acknowledge": (("test", "job", "finding"), None), "snooze": (("finding",), 14),
|
|
21
|
+
"reopen": (("test", "job", "finding", "package"), None),
|
|
22
|
+
"exclude": (("pr",), None), "include": (("pr",), None), "accept": (("package", "rerun"), 30),
|
|
23
|
+
"confirm": (("test", "job"), None), "dispute": (("test", "job"), None),
|
|
24
|
+
}
|
|
25
|
+
KINDS = ("test", "job", "finding", "pr", "package", "rerun")
|
|
26
|
+
# Verbs that only undo an earlier decision: nothing is in force after them.
|
|
27
|
+
UNDO = {"unquarantine", "unmute", "reopen", "include"}
|
|
28
|
+
# Verbs that say whether StackVitals' verdict was right: logged with what it said then, never in force.
|
|
29
|
+
FEEDBACK = {"confirm", "dispute"}
|
|
30
|
+
_DURATION = re.compile(r"^(\d{1,3})\s*(d|day|days|w|week|weeks)$", re.I)
|
|
31
|
+
REASON_MAX = 200
|
|
32
|
+
ID_MAX = 500
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CommandError(ValueError):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Decision:
|
|
41
|
+
verb: str
|
|
42
|
+
kind: str
|
|
43
|
+
id: str
|
|
44
|
+
until: str | None
|
|
45
|
+
reason: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class Request:
|
|
50
|
+
changes: list[Decision]
|
|
51
|
+
settings: dict
|
|
52
|
+
golden: dict | None = None # a Portal golden path to run: {"path": id, "inputs": {...}}
|
|
53
|
+
check: list | str | None = None # connectors to test: "all" or names (connectors/check.py)
|
|
54
|
+
resync: list | None = None # services to read again: [{"source": "jira", "since": "2026-09-01" or None}]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _until(words: list[str], today: date) -> tuple[str | None, list[str]]:
|
|
58
|
+
"""`for 14d`, `for 2 weeks` or `until 2026-12-01` anywhere in the words; the rest is returned untouched."""
|
|
59
|
+
for i, word in enumerate(words):
|
|
60
|
+
low = word.lower()
|
|
61
|
+
if low == "until" and i + 1 < len(words):
|
|
62
|
+
try:
|
|
63
|
+
return date.fromisoformat(words[i + 1]).isoformat(), words[:i] + words[i + 2:]
|
|
64
|
+
except ValueError:
|
|
65
|
+
raise CommandError(f"`until` needs a date like 2026-12-31, not {words[i + 1]!r}.") from None
|
|
66
|
+
if low == "for" and i + 1 < len(words):
|
|
67
|
+
joined, used = words[i + 1], 2
|
|
68
|
+
if i + 2 < len(words) and not _DURATION.match(joined) and _DURATION.match(f"{joined} {words[i + 2]}"):
|
|
69
|
+
joined, used = f"{joined} {words[i + 2]}", 3
|
|
70
|
+
m = _DURATION.match(joined)
|
|
71
|
+
if m:
|
|
72
|
+
days = int(m[1]) * (7 if m[2].lower().startswith("w") else 1)
|
|
73
|
+
return (today + timedelta(days=days)).isoformat(), words[:i] + words[i + used:]
|
|
74
|
+
return None, words
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _target(word: str) -> tuple[str, str] | None:
|
|
78
|
+
if re.fullmatch(r"#\d+", word):
|
|
79
|
+
return "pr", word[1:]
|
|
80
|
+
kind, sep, ident = word.partition(":")
|
|
81
|
+
if sep and kind in KINDS and ident:
|
|
82
|
+
return kind, ident[1:] if kind == "pr" and ident.startswith("#") else ident
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _decision(verb: str, kind: str, ident: str, until: str | None, reason: str, today: date) -> Decision:
|
|
87
|
+
kinds, default_days = VERBS[verb]
|
|
88
|
+
if kind not in kinds and verb in FEEDBACK:
|
|
89
|
+
raise CommandError(f"Say which verdict: /stackvitals {verb} test:<id> or job:\"<name>\", as the comment names it.")
|
|
90
|
+
if kind not in kinds:
|
|
91
|
+
raise CommandError(f"`{verb}` is for {' or '.join(kinds)} items, not {kind}.")
|
|
92
|
+
if len(ident) > ID_MAX or any(ord(ch) < 32 or ord(ch) == 127 for ch in ident):
|
|
93
|
+
raise CommandError(f"An item id has line breaks or control characters, or is over {ID_MAX} characters.")
|
|
94
|
+
if until is None and default_days:
|
|
95
|
+
until = (today + timedelta(days=default_days)).isoformat()
|
|
96
|
+
clean = " ".join((reason or "").split())[:REASON_MAX]
|
|
97
|
+
return Decision(verb, kind, ident.strip(), until, clean)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_command(text: str, today: date, default: tuple[str, str] | None = None) -> list[Decision]:
|
|
101
|
+
"""Every line that starts with `/stackvitals <verb>`: its targets (`test:<id>`, `job:"<name>"`, `finding:<key>`,
|
|
102
|
+
`#<pr>`), else the item the issue is about; `for 14d` or `until DATE`; a reason after `because` or `--`."""
|
|
103
|
+
out = []
|
|
104
|
+
for line in (text or "").splitlines():
|
|
105
|
+
line = line.strip()
|
|
106
|
+
if not line.lower().startswith("/stackvitals"):
|
|
107
|
+
continue
|
|
108
|
+
rest = line[len("/stackvitals"):].strip()
|
|
109
|
+
reason = ""
|
|
110
|
+
for marker in (" because ", " -- "):
|
|
111
|
+
if marker in f" {rest} ":
|
|
112
|
+
rest, _, reason = f" {rest} ".partition(marker)
|
|
113
|
+
rest = rest.strip()
|
|
114
|
+
break
|
|
115
|
+
try:
|
|
116
|
+
words = shlex.split(rest)
|
|
117
|
+
except ValueError:
|
|
118
|
+
raise CommandError("A quote in that command isn't closed.") from None
|
|
119
|
+
if not words or words[0].lower() not in VERBS:
|
|
120
|
+
raise CommandError(f"Known commands: {', '.join(VERBS)}. Example: /stackvitals quarantine for 14d because ...")
|
|
121
|
+
verb = words[0].lower()
|
|
122
|
+
until, words = _until(words[1:], today)
|
|
123
|
+
targets = [t for t in (_target(w) for w in words) if t]
|
|
124
|
+
if not targets and default and default[0] == "pr" and any(w.lower() == "rerun" for w in words):
|
|
125
|
+
# `/stackvitals accept rerun` on a pull request: its green re-run is accepted (the verdict's strict mode).
|
|
126
|
+
targets = [("rerun", default[1])]
|
|
127
|
+
if not targets and default:
|
|
128
|
+
targets = [default]
|
|
129
|
+
if not targets:
|
|
130
|
+
raise CommandError(f"Say which item: /stackvitals {verb} test:<id>, job:\"<name>\", finding:<key> or #<pr>.")
|
|
131
|
+
out += [_decision(verb, kind, ident, until, reason, today) for kind, ident in targets]
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def parse_request(body: str, today: date) -> Request | None:
|
|
136
|
+
"""The fenced ```stackvitals block the app puts in a request issue (or a dispatch input): JSON with changes and
|
|
137
|
+
settings. None when there is no block."""
|
|
138
|
+
m = re.search(r"```stackvitals\s*\n(.*?)\n```", body or "", re.S)
|
|
139
|
+
if not m:
|
|
140
|
+
return None
|
|
141
|
+
try:
|
|
142
|
+
doc = json.loads(m[1])
|
|
143
|
+
except ValueError:
|
|
144
|
+
raise CommandError("The request block isn't valid JSON; nothing was changed.") from None
|
|
145
|
+
if not isinstance(doc, dict) or doc.get("version") != 1:
|
|
146
|
+
raise CommandError("The request block has an unknown version; nothing was changed.")
|
|
147
|
+
changes = []
|
|
148
|
+
for c in doc.get("changes") or []:
|
|
149
|
+
if not isinstance(c, dict) or c.get("verb") not in VERBS or not isinstance(c.get("id"), str):
|
|
150
|
+
raise CommandError("A change in the request block is incomplete; nothing was changed.")
|
|
151
|
+
until = c.get("until")
|
|
152
|
+
if until is not None:
|
|
153
|
+
try:
|
|
154
|
+
until = date.fromisoformat(str(until)).isoformat()
|
|
155
|
+
except ValueError:
|
|
156
|
+
raise CommandError(f"`until` must be a date like 2026-12-31, not {until!r}.") from None
|
|
157
|
+
changes.append(_decision(c["verb"], str(c.get("kind")), c["id"], until, str(c.get("reason") or ""), today))
|
|
158
|
+
settings = doc.get("settings") or {}
|
|
159
|
+
if not isinstance(settings, dict):
|
|
160
|
+
raise CommandError("The settings in the request block must be key: value pairs; nothing was changed.")
|
|
161
|
+
golden = doc.get("golden")
|
|
162
|
+
if golden is not None and not (isinstance(golden, dict) and isinstance(golden.get("path"), str)
|
|
163
|
+
and isinstance(golden.get("inputs") or {}, dict)):
|
|
164
|
+
raise CommandError("A golden path request needs a path and its inputs; nothing was run.")
|
|
165
|
+
check = doc.get("check")
|
|
166
|
+
if check is not None and check != "all" and not (isinstance(check, list) and check
|
|
167
|
+
and all(isinstance(x, str) and x.strip() for x in check)):
|
|
168
|
+
raise CommandError('`check` must be "all" or a list of connector names, like ["jira", "slack"]; nothing was checked.')
|
|
169
|
+
resync = doc.get("resync")
|
|
170
|
+
if resync is not None:
|
|
171
|
+
ok = isinstance(resync, list) and resync and all(isinstance(r, dict) and isinstance(r.get("source"), str) and "since" in r
|
|
172
|
+
for r in resync)
|
|
173
|
+
try:
|
|
174
|
+
for r in resync if ok else []:
|
|
175
|
+
if r["since"] is not None:
|
|
176
|
+
date.fromisoformat(str(r["since"]))
|
|
177
|
+
except ValueError:
|
|
178
|
+
ok = False
|
|
179
|
+
if not ok:
|
|
180
|
+
raise CommandError('`resync` must be a list like [{"source": "jira", "since": "2026-09-01"}] (since may be null for '
|
|
181
|
+
"the whole window); nothing was read again.")
|
|
182
|
+
resync = [{"source": r["source"], "since": r["since"]} for r in resync]
|
|
183
|
+
return Request(changes, settings, {"path": golden["path"], "inputs": golden.get("inputs") or {}} if golden else None,
|
|
184
|
+
check, resync)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class Store:
|
|
188
|
+
"""decisions/ in the data branch: the log is only appended to; state.json is rebuilt from it."""
|
|
189
|
+
|
|
190
|
+
def __init__(self, root: Path):
|
|
191
|
+
self.root = Path(root)
|
|
192
|
+
|
|
193
|
+
def log(self) -> list[dict]:
|
|
194
|
+
try:
|
|
195
|
+
text = (self.root / LOG).read_text(encoding="utf-8")
|
|
196
|
+
except OSError:
|
|
197
|
+
return []
|
|
198
|
+
return [json.loads(line) for line in text.splitlines() if line.strip()]
|
|
199
|
+
|
|
200
|
+
def record(self, decisions: list[Decision], by: str, source: str, now: datetime, seen: dict | None = None) -> list[dict]:
|
|
201
|
+
"""Append decisions; feedback rows carry what StackVitals said about the item then (`seen`, by (kind, id))."""
|
|
202
|
+
rows = [{"at": iso(now), "by": by, "source": source, **asdict(d),
|
|
203
|
+
**({"seen": (seen or {}).get((d.kind, d.id))} if d.verb in FEEDBACK else {})} for d in decisions]
|
|
204
|
+
if not rows:
|
|
205
|
+
return []
|
|
206
|
+
path = self.root / LOG
|
|
207
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
208
|
+
with path.open("a", encoding="utf-8", newline="\n") as f:
|
|
209
|
+
f.writelines(json.dumps(r, separators=(",", ":"), ensure_ascii=False) + "\n" for r in rows)
|
|
210
|
+
latest = {}
|
|
211
|
+
for row in self.log():
|
|
212
|
+
if row["verb"] not in FEEDBACK:
|
|
213
|
+
latest[f"{row['kind']}:{row['id']}"] = row
|
|
214
|
+
(self.root / STATE).write_text(json.dumps(latest, indent=1, sort_keys=True, ensure_ascii=False), encoding="utf-8")
|
|
215
|
+
return rows
|
|
216
|
+
|
|
217
|
+
def state(self, today: date) -> dict[str, dict]:
|
|
218
|
+
"""The newest decision per item, without those whose review date has passed."""
|
|
219
|
+
try:
|
|
220
|
+
latest = json.loads((self.root / STATE).read_text(encoding="utf-8"))
|
|
221
|
+
except (OSError, ValueError):
|
|
222
|
+
latest = {}
|
|
223
|
+
day = today.isoformat()
|
|
224
|
+
return {k: v for k, v in latest.items() if not v.get("until") or v["until"] >= day}
|
|
225
|
+
|
|
226
|
+
def ids(self, verb: str, kind: str, today: date) -> set[str]:
|
|
227
|
+
return {v["id"] for v in self.state(today).values() if v["verb"] == verb and v["kind"] == kind}
|
|
228
|
+
|
|
229
|
+
def feedback(self) -> list[dict]:
|
|
230
|
+
return [row for row in self.log() if row.get("verb") in FEEDBACK]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def verdict_accuracy(rows: list[dict]) -> dict:
|
|
234
|
+
"""How often people confirmed StackVitals' verdict, per the verdict it gave then (known flaky, flaked before,
|
|
235
|
+
already failing, new): the newest feedback per person and item counts once, with a 95% range."""
|
|
236
|
+
from stackvitals.core.stats import wilson
|
|
237
|
+
latest = {}
|
|
238
|
+
for r in rows:
|
|
239
|
+
latest[(r.get("by"), r.get("kind"), r.get("id"))] = r
|
|
240
|
+
out = {}
|
|
241
|
+
for r in latest.values():
|
|
242
|
+
seen = r.get("seen") or {}
|
|
243
|
+
state = seen.get("verdict") or seen.get("state") or "unknown"
|
|
244
|
+
cell = out.setdefault(state, {"confirmed": 0, "disputed": 0})
|
|
245
|
+
cell["confirmed" if r["verb"] == "confirm" else "disputed"] += 1
|
|
246
|
+
for cell in out.values():
|
|
247
|
+
n = cell["confirmed"] + cell["disputed"]
|
|
248
|
+
lo, hi = wilson(cell["confirmed"], n)
|
|
249
|
+
cell.update(n=n, share=round(cell["confirmed"] / n, 4), low=round(lo, 4), high=round(hi, 4))
|
|
250
|
+
return out
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
EXCLUDE_LABEL = "stackvitals: exclude"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def excluded_prs(state: dict, prs: list[dict]) -> set[int]:
|
|
257
|
+
"""Pull requests the team left out of the numbers: an `exclude` decision or the label, unless someone said `include`."""
|
|
258
|
+
said = {int(v["id"]): v["verb"] for v in state.values() if v.get("kind") == "pr" and str(v.get("id")).isdigit()}
|
|
259
|
+
labelled = {p["n"] for p in prs if EXCLUDE_LABEL in (p.get("lb") or [])}
|
|
260
|
+
return {n for n in labelled | set(said) if said.get(n, "exclude") == "exclude"}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def drop_prs(flow: dict, numbers: set[int]) -> dict:
|
|
264
|
+
"""The pull request history without the excluded ones, and how many were left out."""
|
|
265
|
+
if not numbers:
|
|
266
|
+
return flow
|
|
267
|
+
keep = lambda rows: [p for p in rows or [] if p.get("n") not in numbers]
|
|
268
|
+
gone = sum(1 for p in (flow.get("open") or []) + (flow.get("closed") or []) if p.get("n") in numbers)
|
|
269
|
+
return {**flow, "open": keep(flow.get("open")), "closed": keep(flow.get("closed")), "excluded": gone}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def visible(root: Path, flow: dict, today: date) -> dict:
|
|
273
|
+
"""The pull request history every module computes from: without the ones the team excluded."""
|
|
274
|
+
prs = (flow.get("open") or []) + (flow.get("closed") or [])
|
|
275
|
+
return drop_prs(flow, excluded_prs(Store(root).state(today), prs))
|