@coreplane/switchboard 1.204.0 → 1.206.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.
- package/dist/assets/Dockerfile +6 -3
- package/dist/assets/config/config.example.yaml +56 -4
- package/dist/assets/deploy/cloudflare/coordinator.ts +86 -0
- package/dist/assets/deploy/cloudflare/shared.ts +9 -0
- package/dist/assets/deploy/cloudflare/worker.ts +142 -6
- package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +9 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +381 -7
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +22 -2
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +89 -3
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +81 -10
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +4 -1
- package/dist/assets/deploy/secrets.manifest.json +1 -1
- package/dist/assets/package-lock.json +291 -97
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +517 -0
- package/dist/assets/src/config/profile.ts +229 -0
- package/dist/assets/src/core/authz/policy.ts +15 -0
- package/dist/assets/src/core/coordinator/contract.ts +239 -0
- package/dist/assets/src/core/coordinator/driver.ts +501 -0
- package/dist/assets/src/core/coordinator/instancesRoute.ts +181 -0
- package/dist/assets/src/core/delivery.ts +829 -0
- package/dist/assets/src/core/deliverySnapshotStore.ts +260 -0
- package/dist/assets/src/core/reviewVerdict.ts +296 -0
- package/dist/assets/src/core/reviewedHead.ts +77 -0
- package/dist/assets/src/core/runEvents.ts +4 -2
- package/dist/assets/src/core/runLedger/decisions.ts +2 -1
- package/dist/assets/src/core/runLedger/types.ts +23 -1
- package/dist/assets/src/core/runRecord.ts +100 -0
- package/dist/assets/src/core/ship/contract.ts +502 -0
- package/dist/assets/src/core/ship/coordinator.ts +1039 -0
- package/dist/assets/src/core/workerError.ts +12 -0
- package/dist/assets/web/dist/.vite/manifest.json +2770 -70
- package/dist/assets/web/dist/assets/AppShell-lYVcj-k7.js +1 -0
- package/dist/assets/web/dist/assets/{CostsPage-XQ-fQzdz.js → CostsPage-DGSE48l1.js} +1 -1
- package/dist/assets/web/dist/assets/DeliveryPage-CvlWP7Eq.js +1 -0
- package/dist/assets/web/dist/assets/NotFoundPage-BzWS4Ca1.js +1 -0
- package/dist/assets/web/dist/assets/ResidentDetailPage-D3P21yeI.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-DOYqnZ1q.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-OmvrvPXY.js +12 -0
- package/dist/assets/web/dist/assets/RunRoutePage-f6TSNPeW.css +1 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DWbSQtL4.js +1 -0
- package/dist/assets/web/dist/assets/RunsTabs-DEICQ4FY.js +1 -0
- package/dist/assets/web/dist/assets/ScheduledPage-CPKfJ4mR.js +1 -0
- package/dist/assets/web/dist/assets/StatusDot-COr8jTyM.js +1 -0
- package/dist/assets/web/dist/assets/Tooltip-fOqTZkNT.js +1 -0
- package/dist/assets/web/dist/assets/abap-CLvhMVsD.js +1 -0
- package/dist/assets/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
- package/dist/assets/web/dist/assets/ada-C5qYipkI.js +1 -0
- package/dist/assets/web/dist/assets/ahk-CA9gdOdF.js +1 -0
- package/dist/assets/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
- package/dist/assets/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
- package/dist/assets/web/dist/assets/angular-html-Cw130Zyi.js +1 -0
- package/dist/assets/web/dist/assets/angular-ts-DY1m4C2s.js +1 -0
- package/dist/assets/web/dist/assets/apache-U0d_L8uA.js +1 -0
- package/dist/assets/web/dist/assets/apex-KFGFyKwG.js +1 -0
- package/dist/assets/web/dist/assets/apl-W6Ty7v05.js +1 -0
- package/dist/assets/web/dist/assets/applescript-CCn79oCD.js +1 -0
- package/dist/assets/web/dist/assets/ara-4CJ0cIlV.js +1 -0
- package/dist/assets/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
- package/dist/assets/web/dist/assets/asm-Cmm7eHzH.js +1 -0
- package/dist/assets/web/dist/assets/astro-DlLVHwVk.js +1 -0
- package/dist/assets/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
- package/dist/assets/web/dist/assets/awk-BWXHIvNe.js +1 -0
- package/dist/assets/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/dist/assets/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
- package/dist/assets/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/dist/assets/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
- package/dist/assets/web/dist/assets/bat-qdxAdBWQ.js +1 -0
- package/dist/assets/web/dist/assets/beancount-D-usSTwE.js +1 -0
- package/dist/assets/web/dist/assets/berry-DKpUyyne.js +1 -0
- package/dist/assets/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
- package/dist/assets/web/dist/assets/bicep-CUHmPFLl.js +1 -0
- package/dist/assets/web/dist/assets/bird2-CYIUgYqB.js +1 -0
- package/dist/assets/web/dist/assets/blade-Bco3tDh4.js +1 -0
- package/dist/assets/web/dist/assets/bsl-_VjlB9mv.js +1 -0
- package/dist/assets/web/dist/assets/c-1LaFY_cj.js +1 -0
- package/dist/assets/web/dist/assets/c3-D0apG41V.js +1 -0
- package/dist/assets/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
- package/dist/assets/web/dist/assets/cairo-DLTphjLi.js +1 -0
- package/dist/assets/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
- package/dist/assets/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
- package/dist/assets/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
- package/dist/assets/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
- package/dist/assets/web/dist/assets/chapel-B-OvrOL5.js +1 -0
- package/dist/assets/web/dist/assets/clarity-CtfBBspg.js +1 -0
- package/dist/assets/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
- package/dist/assets/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
- package/dist/assets/web/dist/assets/cobol-Ch3EPlHu.js +1 -0
- package/dist/assets/web/dist/assets/codeowners-C8r90Shi.js +1 -0
- package/dist/assets/web/dist/assets/codeql-oeQT6MSM.js +1 -0
- package/dist/assets/web/dist/assets/coffee-hZuITnto.js +1 -0
- package/dist/assets/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/dist/assets/web/dist/assets/coq-DuBRPxYg.js +1 -0
- package/dist/assets/web/dist/assets/cpp-B8iRVo8m.js +1 -0
- package/dist/assets/web/dist/assets/crystal-DKveI7lr.js +1 -0
- package/dist/assets/web/dist/assets/csharp-oqKa8noW.js +1 -0
- package/dist/assets/web/dist/assets/css-_2_CD-rr.js +1 -0
- package/dist/assets/web/dist/assets/csv-Dx-8-gkx.js +1 -0
- package/dist/assets/web/dist/assets/cue-CE9AQfxI.js +1 -0
- package/dist/assets/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
- package/dist/assets/web/dist/assets/d-qD-0Kul2.js +1 -0
- package/dist/assets/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
- package/dist/assets/web/dist/assets/dart-CnvKMtbv.js +1 -0
- package/dist/assets/web/dist/assets/dax-BkyTk9wS.js +1 -0
- package/dist/assets/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
- package/dist/assets/web/dist/assets/diff-woXpYk--.js +1 -0
- package/dist/assets/web/dist/assets/dist-BVjAWgkb.js +1547 -0
- package/dist/assets/web/dist/assets/docker-IyjqRm3v.js +1 -0
- package/dist/assets/web/dist/assets/dotenv-_5a1GRtc.js +1 -0
- package/dist/assets/web/dist/assets/dracula-BHWKrbxM.js +1 -0
- package/dist/assets/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
- package/dist/assets/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/dist/assets/web/dist/assets/edge-BenMDPct.js +1 -0
- package/dist/assets/web/dist/assets/elixir-CBbdEqG6.js +1 -0
- package/dist/assets/web/dist/assets/elm-DMub_2lY.js +1 -0
- package/dist/assets/web/dist/assets/emacs-lisp-c_oH4hRZ.js +1 -0
- package/dist/assets/web/dist/assets/erb-CI9He_Up.js +1 -0
- package/dist/assets/web/dist/assets/erlang-Cphh6RMH.js +1 -0
- package/dist/assets/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/dist/assets/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
- package/dist/assets/web/dist/assets/{favicon-CFfbl2AI.js → favicon-CWPcvWvp.js} +1 -1
- package/dist/assets/web/dist/assets/fennel-DQxkIbk2.js +1 -0
- package/dist/assets/web/dist/assets/fish-BJitypiv.js +1 -0
- package/dist/assets/web/dist/assets/fluent-C03EYrpw.js +1 -0
- package/dist/assets/web/dist/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
- package/dist/assets/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/dist/assets/web/dist/assets/fsharp-D13ZGOAj.js +1 -0
- package/dist/assets/web/dist/assets/gdresource-xMC7jr7x.js +1 -0
- package/dist/assets/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
- package/dist/assets/web/dist/assets/gdshader-CBce3t8t.js +1 -0
- package/dist/assets/web/dist/assets/genie-CV2tkWYe.js +1 -0
- package/dist/assets/web/dist/assets/gherkin-DExj1W_8.js +1 -0
- package/dist/assets/web/dist/assets/git-commit-BSykSTBG.js +1 -0
- package/dist/assets/web/dist/assets/git-rebase-BBfhnyF8.js +1 -0
- package/dist/assets/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
- package/dist/assets/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/dist/assets/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/dist/assets/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/dist/assets/web/dist/assets/github-light-EUqPIrTm.js +1 -0
- package/dist/assets/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
- package/dist/assets/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/dist/assets/web/dist/assets/gleam-CSRkHgEL.js +1 -0
- package/dist/assets/web/dist/assets/glimmer-js-CR9Fxrau.js +1 -0
- package/dist/assets/web/dist/assets/glimmer-ts-CwFwX_XS.js +1 -0
- package/dist/assets/web/dist/assets/glsl-Daiodp1r.js +1 -0
- package/dist/assets/web/dist/assets/gn-ilITqXS6.js +1 -0
- package/dist/assets/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
- package/dist/assets/web/dist/assets/go-rLFTqkRN.js +1 -0
- package/dist/assets/web/dist/assets/graphql-DZvrsEkB.js +1 -0
- package/dist/assets/web/dist/assets/groovy-CacY0gHj.js +1 -0
- package/dist/assets/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/dist/assets/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/dist/assets/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/dist/assets/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/dist/assets/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/dist/assets/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/dist/assets/web/dist/assets/hack-BaXOzdg7.js +1 -0
- package/dist/assets/web/dist/assets/haml-CMskq_VA.js +1 -0
- package/dist/assets/web/dist/assets/handlebars-Dhw_f7jD.js +1 -0
- package/dist/assets/web/dist/assets/haskell-D8IpX4py.js +1 -0
- package/dist/assets/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
- package/dist/assets/web/dist/assets/hcl-Dh228itO.js +1 -0
- package/dist/assets/web/dist/assets/hjson-CxZEssPk.js +1 -0
- package/dist/assets/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
- package/dist/assets/web/dist/assets/horizon-CE9ld1lL.js +1 -0
- package/dist/assets/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
- package/dist/assets/web/dist/assets/houston-CsvMBhTu.js +1 -0
- package/dist/assets/web/dist/assets/html-C9k99z0z.js +1 -0
- package/dist/assets/web/dist/assets/html-derivative-VckPItXN.js +1 -0
- package/dist/assets/web/dist/assets/http-DXU_3h9v.js +1 -0
- package/dist/assets/web/dist/assets/hurl-KU24JNdp.js +1 -0
- package/dist/assets/web/dist/assets/hxml-Dcm88srW.js +1 -0
- package/dist/assets/web/dist/assets/hy-CZbG8q4J.js +1 -0
- package/dist/assets/web/dist/assets/imba-DsUTQ-LC.js +1 -0
- package/dist/assets/web/dist/assets/indexFormat-B-pd4r7Y.js +1 -0
- package/dist/assets/web/dist/assets/ini-B5eOa1yu.js +1 -0
- package/dist/assets/web/dist/assets/java-BbZjTNgf.js +1 -0
- package/dist/assets/web/dist/assets/javascript-DXCdqcZZ.js +1 -0
- package/dist/assets/web/dist/assets/jinja-pDlsjTVp.js +1 -0
- package/dist/assets/web/dist/assets/jison-DsXDk66e.js +1 -0
- package/dist/assets/web/dist/assets/json-tCxXfRgG.js +1 -0
- package/dist/assets/web/dist/assets/json5-BR5RXkoi.js +1 -0
- package/dist/assets/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/dist/assets/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
- package/dist/assets/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/dist/assets/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
- package/dist/assets/web/dist/assets/jsx-B0ZYkCmi.js +1 -0
- package/dist/assets/web/dist/assets/julia-BLzzeI6x.js +1 -0
- package/dist/assets/web/dist/assets/just-DvD2_60c.js +1 -0
- package/dist/assets/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
- package/dist/assets/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
- package/dist/assets/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
- package/dist/assets/web/dist/assets/kdl-CsD5j6eV.js +1 -0
- package/dist/assets/web/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/dist/assets/web/dist/assets/kusto-C7mF5XQf.js +1 -0
- package/dist/assets/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
- package/dist/assets/web/dist/assets/latex-511zn3h7.js +1 -0
- package/dist/assets/web/dist/assets/lean-CewbzKMR.js +1 -0
- package/dist/assets/web/dist/assets/less-DVTAwKKz.js +1 -0
- package/dist/assets/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
- package/dist/assets/web/dist/assets/liquid-BD0BkMox.js +1 -0
- package/dist/assets/web/dist/assets/llvm-DqHOMS-s.js +1 -0
- package/dist/assets/web/dist/assets/log-BNLmms1o.js +1 -0
- package/dist/assets/web/dist/assets/logo-Cluzi2Zq.js +1 -0
- package/dist/assets/web/dist/assets/lua-NFicKm6N.js +1 -0
- package/dist/assets/web/dist/assets/luau-C2pBCSgt.js +1 -0
- package/dist/assets/web/dist/assets/main-CuENKPdD.css +1 -0
- package/dist/assets/web/dist/assets/main-DZbJaqUb.js +28 -0
- package/dist/assets/web/dist/assets/make-Dixweg8N.js +1 -0
- package/dist/assets/web/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/dist/assets/web/dist/assets/marko-1-G_nEXK.js +1 -0
- package/dist/assets/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
- package/dist/assets/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/dist/assets/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/dist/assets/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/dist/assets/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/dist/assets/web/dist/assets/matlab-D7qyCx1q.js +1 -0
- package/dist/assets/web/dist/assets/mdc-DUe3AQLp.js +1 -0
- package/dist/assets/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
- package/dist/assets/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
- package/dist/assets/web/dist/assets/min-dark-BSWPekZh.js +1 -0
- package/dist/assets/web/dist/assets/min-light-DDpmG2fV.js +1 -0
- package/dist/assets/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
- package/dist/assets/web/dist/assets/mojo-fg40_ntQ.js +1 -0
- package/dist/assets/web/dist/assets/monokai-B_bV9PQ9.js +1 -0
- package/dist/assets/web/dist/assets/moonbit-DgjMoLWr.js +1 -0
- package/dist/assets/web/dist/assets/move-BYnGhZoM.js +1 -0
- package/dist/assets/web/dist/assets/narrat-_X_XdTYD.js +1 -0
- package/dist/assets/web/dist/assets/nextflow-BJtWHP5T.js +1 -0
- package/dist/assets/web/dist/assets/nextflow-groovy-DJMQeKeT.js +1 -0
- package/dist/assets/web/dist/assets/nginx-Dg379_bA.js +1 -0
- package/dist/assets/web/dist/assets/night-owl-DhmEMT88.js +1 -0
- package/dist/assets/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/dist/assets/web/dist/assets/nim-BnFq97ZO.js +1 -0
- package/dist/assets/web/dist/assets/nix-IvuFDN5E.js +1 -0
- package/dist/assets/web/dist/assets/nord-Cb4Vim4T.js +1 -0
- package/dist/assets/web/dist/assets/nsis-CjNLYCLg.js +1 -0
- package/dist/assets/web/dist/assets/nushell-DHYfO66s.js +1 -0
- package/dist/assets/web/dist/assets/objective-c-D1A_Heim.js +1 -0
- package/dist/assets/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/dist/assets/web/dist/assets/ocaml-O90oeIOV.js +1 -0
- package/dist/assets/web/dist/assets/odin-B1RWQWA5.js +1 -0
- package/dist/assets/web/dist/assets/one-dark-pro-CLwyXe_n.js +1 -0
- package/dist/assets/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
- package/dist/assets/web/dist/assets/openscad-BUDT5pXO.js +1 -0
- package/dist/assets/web/dist/assets/org-F0uiwGvq.js +1 -0
- package/dist/assets/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
- package/dist/assets/web/dist/assets/perl-B0euczkl.js +1 -0
- package/dist/assets/web/dist/assets/php-DkL3k_n7.js +1 -0
- package/dist/assets/web/dist/assets/pierre-dark-CpLgRqie.js +1 -0
- package/dist/assets/web/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/dist/assets/web/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/dist/assets/web/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/dist/assets/web/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/dist/assets/web/dist/assets/pierre-light-CoaEpmwp.js +1 -0
- package/dist/assets/web/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/dist/assets/web/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/dist/assets/web/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/dist/assets/web/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/dist/assets/web/dist/assets/pkl-ot-7Btpt.js +1 -0
- package/dist/assets/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
- package/dist/assets/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
- package/dist/assets/web/dist/assets/po-BiJDBrnU.js +1 -0
- package/dist/assets/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
- package/dist/assets/web/dist/assets/polar-C7UOKdEL.js +1 -0
- package/dist/assets/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
- package/dist/assets/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
- package/dist/assets/web/dist/assets/powershell-CCSX75yM.js +1 -0
- package/dist/assets/web/dist/assets/prisma-DMgNhnYS.js +1 -0
- package/dist/assets/web/dist/assets/prolog-iXnhIJG7.js +1 -0
- package/dist/assets/web/dist/assets/proto-DB4EqR-F.js +1 -0
- package/dist/assets/web/dist/assets/pug-L_OjjZP4.js +1 -0
- package/dist/assets/web/dist/assets/puppet-CDv2pdJW.js +1 -0
- package/dist/assets/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
- package/dist/assets/web/dist/assets/python-gzcpVVnB.js +1 -0
- package/dist/assets/web/dist/assets/qml-BMjB00Zz.js +1 -0
- package/dist/assets/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
- package/dist/assets/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
- package/dist/assets/web/dist/assets/r-DoeLdnqR.js +1 -0
- package/dist/assets/web/dist/assets/racket-DcIDlBhZ.js +1 -0
- package/dist/assets/web/dist/assets/raku-B3gFvitq.js +1 -0
- package/dist/assets/web/dist/assets/razor-BzYNkAWy.js +1 -0
- package/dist/assets/web/dist/assets/rbs-C0MpOrj4.js +1 -0
- package/dist/assets/web/dist/assets/red-DX1BQOs8.js +1 -0
- package/dist/assets/web/dist/assets/reg-CRGYupPL.js +1 -0
- package/dist/assets/web/dist/assets/regexp-CPmElMk3.js +1 -0
- package/dist/assets/web/dist/assets/rel-BtDbiS_P.js +1 -0
- package/dist/assets/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
- package/dist/assets/web/dist/assets/ron-VUp2lXgN.js +1 -0
- package/dist/assets/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
- package/dist/assets/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
- package/dist/assets/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
- package/dist/assets/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
- package/dist/assets/web/dist/assets/rst-DrsjgfI7.js +1 -0
- package/dist/assets/web/dist/assets/ruby-cB42ppvx.js +1 -0
- package/dist/assets/web/dist/assets/rust-Cfkwpbl8.js +1 -0
- package/dist/assets/web/dist/assets/sas-R5Y3NM_I.js +1 -0
- package/dist/assets/web/dist/assets/sass-DXrisJhu.js +1 -0
- package/dist/assets/web/dist/assets/scala-BiFt_7mu.js +1 -0
- package/dist/assets/web/dist/assets/scheme-DQCgrYNe.js +1 -0
- package/dist/assets/web/dist/assets/scss-C-zWxV9s.js +1 -0
- package/dist/assets/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
- package/dist/assets/web/dist/assets/seed-DeKSHrt2.js +6 -0
- package/dist/assets/web/dist/assets/shaderlab-TOUzSsQk.js +1 -0
- package/dist/assets/web/dist/assets/shellscript-oJF96aAU.js +1 -0
- package/dist/assets/web/dist/assets/shellsession-CznECKyi.js +1 -0
- package/dist/assets/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
- package/dist/assets/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/dist/assets/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
- package/dist/assets/web/dist/assets/smithy-Dgaz7aur.js +1 -0
- package/dist/assets/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/dist/assets/web/dist/assets/solarized-dark-UdsmMDCL.js +1 -0
- package/dist/assets/web/dist/assets/solarized-light-C1IUL_tW.js +1 -0
- package/dist/assets/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
- package/dist/assets/web/dist/assets/soy-wjHLSRag.js +1 -0
- package/dist/assets/web/dist/assets/sparql-D_iOobhT.js +1 -0
- package/dist/assets/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
- package/dist/assets/web/dist/assets/sql-C_BM8IOW.js +1 -0
- package/dist/assets/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
- package/dist/assets/web/dist/assets/stata-D31HO8aQ.js +1 -0
- package/dist/assets/web/dist/assets/stylus-B6D30XZt.js +1 -0
- package/dist/assets/web/dist/assets/surrealql-CKCLyplA.js +1 -0
- package/dist/assets/web/dist/assets/svelte-3geWk2iu.js +1 -0
- package/dist/assets/web/dist/assets/swift-CyEgAFGc.js +1 -0
- package/dist/assets/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/dist/assets/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
- package/dist/assets/web/dist/assets/systemd-BxMlprV5.js +1 -0
- package/dist/assets/web/dist/assets/talonscript-CohzipZa.js +1 -0
- package/dist/assets/web/dist/assets/tasl-DMoTqEGO.js +1 -0
- package/dist/assets/web/dist/assets/tcl-CZd0xW_V.js +1 -0
- package/dist/assets/web/dist/assets/templ-DiJr_hTD.js +1 -0
- package/dist/assets/web/dist/assets/terraform-DswuEJGm.js +1 -0
- package/dist/assets/web/dist/assets/tex-BEJGWyeF.js +1 -0
- package/dist/assets/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/dist/assets/web/dist/assets/toml-CcmNWLt0.js +1 -0
- package/dist/assets/web/dist/assets/ts-tags-C8Mzdpxx.js +1 -0
- package/dist/assets/web/dist/assets/tsv-sltzmVWM.js +1 -0
- package/dist/assets/web/dist/assets/tsx-UQiSm_p3.js +1 -0
- package/dist/assets/web/dist/assets/turtle-ByJddavk.js +1 -0
- package/dist/assets/web/dist/assets/twig-_Pvzkeo9.js +1 -0
- package/dist/assets/web/dist/assets/typescript-BWMkINKK.js +1 -0
- package/dist/assets/web/dist/assets/typespec-BRdr0IET.js +1 -0
- package/dist/assets/web/dist/assets/typst-Bdg9m-e7.js +1 -0
- package/dist/assets/web/dist/assets/v-DBj4K9d_.js +1 -0
- package/dist/assets/web/dist/assets/vala-zf12oZj6.js +1 -0
- package/dist/assets/web/dist/assets/vb-DOwzaHZ0.js +1 -0
- package/dist/assets/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
- package/dist/assets/web/dist/assets/vesper-DdrHHSXu.js +1 -0
- package/dist/assets/web/dist/assets/vhdl-BroJfC0k.js +1 -0
- package/dist/assets/web/dist/assets/viml-DvXPmvsu.js +1 -0
- package/dist/assets/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/dist/assets/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/dist/assets/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
- package/dist/assets/web/dist/assets/vue-CDZrrAZ8.js +1 -0
- package/dist/assets/web/dist/assets/vue-html-tAlpNhfN.js +1 -0
- package/dist/assets/web/dist/assets/vue-vine-C27UF_rh.js +1 -0
- package/dist/assets/web/dist/assets/vyper-CgoNMtux.js +1 -0
- package/dist/assets/web/dist/assets/wallClock-61NpVkY6.js +1 -0
- package/dist/assets/web/dist/assets/wasm-BnjxR4X6.js +1 -0
- package/dist/assets/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
- package/dist/assets/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
- package/dist/assets/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
- package/dist/assets/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
- package/dist/assets/web/dist/assets/wit-DdvCle-K.js +1 -0
- package/dist/assets/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
- package/dist/assets/web/dist/assets/xml-CxeDr9Zh.js +1 -0
- package/dist/assets/web/dist/assets/xsl-Bpi0uUnM.js +1 -0
- package/dist/assets/web/dist/assets/yaml-BtUgZ1GN.js +1 -0
- package/dist/assets/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
- package/dist/assets/web/dist/assets/zig-CMLA9XwU.js +1 -0
- package/dist/cli.js +6936 -3347
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-Bw3-_TTE.js +0 -1
- package/dist/assets/web/dist/assets/DeliveryPage-BRwcQyr7.js +0 -1
- package/dist/assets/web/dist/assets/NotFoundPage-RjH-9dPy.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-BRy5wkv9.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-DAEVLN8j.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-B1KHmkZ9.js +0 -126
- package/dist/assets/web/dist/assets/RunRoutePage-CdbrbqU2.css +0 -1
- package/dist/assets/web/dist/assets/RunsIndexPage-3hUWFpFV.js +0 -1
- package/dist/assets/web/dist/assets/RunsTabs-Qn_5TVJU.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DysVvVm1.js +0 -1
- package/dist/assets/web/dist/assets/StatusDot-Bug2a6T6.js +0 -1
- package/dist/assets/web/dist/assets/Tooltip-C2eEUbwn.js +0 -1
- package/dist/assets/web/dist/assets/main-0uhp-taL.js +0 -28
- package/dist/assets/web/dist/assets/main-Xickfv8V.css +0 -1
- package/dist/assets/web/dist/assets/seed-B8mQEhhw.js +0 -6
- package/dist/assets/web/dist/assets/wallClock-DI4HIEN5.js +0 -1
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
import { systemClock } from "./trace/clock.js";
|
|
2
|
+
|
|
3
|
+
// Delivery indicators: what the run history and the pull requests' own facts
|
|
4
|
+
// say about how work reaches `main`, per week and per unit (a board issue).
|
|
5
|
+
// Nothing is written to GitHub; its facts are read on an interval and kept as a
|
|
6
|
+
// snapshot (src/core/deliverySnapshot.ts) the report is computed from. The four
|
|
7
|
+
// leading indicators the published accounts of agent-run development name, in
|
|
8
|
+
// this repository's terms:
|
|
9
|
+
//
|
|
10
|
+
// issue → merge from a unit's board issue (or the pull request's own
|
|
11
|
+
// opening when none is linked) to its merge;
|
|
12
|
+
// first-pass CI the first head's `pull_request` runs all green at their
|
|
13
|
+
// first attempt — a retry, or any red, fails it;
|
|
14
|
+
// review rounds the review agent's verdicts on the pull request; a FIX
|
|
15
|
+
// round is a verdict that followed a push since the previous
|
|
16
|
+
// one (a re-verdict with no push is a disposition round);
|
|
17
|
+
// no-human-edit the share of the review agent's findings on pull requests
|
|
18
|
+
// where every push after the first verdict was an agent's —
|
|
19
|
+
// a bot login, or a commit carrying an agent co-author (the
|
|
20
|
+
// maintainer's own agent pushes under the maintainer's login,
|
|
21
|
+
// so the login alone would call every one of those human).
|
|
22
|
+
//
|
|
23
|
+
// Plus the plain counts: pull requests merged, agent-authored ones, findings by
|
|
24
|
+
// severity (blocking ones caught), and the agent run time the history keys to a
|
|
25
|
+
// pull request. `buildDeliveryReport` is pure and does every bit of arithmetic,
|
|
26
|
+
// so the indicators are unit-tested against a recorded trunk pass; a
|
|
27
|
+
// `DeliverySource` (src/execution/githubDelivery.ts, or the in-memory one) only
|
|
28
|
+
// fetches and maps. Nothing here knows a platform SDK (AGENTS.md invariant 1).
|
|
29
|
+
|
|
30
|
+
// ---- facts (what a source assembles) ----------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export type FindingSeverity = "blocking" | "major" | "minor" | "nit" | "fyi";
|
|
33
|
+
export const FINDING_SEVERITIES: readonly FindingSeverity[] = ["blocking", "major", "minor", "nit", "fyi"];
|
|
34
|
+
|
|
35
|
+
export interface ReviewFact {
|
|
36
|
+
/** The GitHub login that posted the review. */
|
|
37
|
+
author: string;
|
|
38
|
+
state: "approved" | "changes_requested" | "commented" | "dismissed" | "pending";
|
|
39
|
+
/** ISO 8601. */
|
|
40
|
+
submittedAt: string;
|
|
41
|
+
body: string;
|
|
42
|
+
/** The head the review was attached to, when GitHub reported one. */
|
|
43
|
+
headSha?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PushFact {
|
|
47
|
+
/** Who pushed: a login for a force-push, the committer's name for a plain commit. */
|
|
48
|
+
actor: string;
|
|
49
|
+
/** ISO 8601. */
|
|
50
|
+
at: string;
|
|
51
|
+
kind: "commit" | "force";
|
|
52
|
+
/** The `Co-Authored-By:` names on the commits the push carried (a force-push: the pull request's commits). */
|
|
53
|
+
coauthors: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CiRunFact {
|
|
57
|
+
name?: string;
|
|
58
|
+
headSha: string;
|
|
59
|
+
/** The workflow's trigger (`pull_request`, `push`, `pull_request_review`, …). */
|
|
60
|
+
trigger: string;
|
|
61
|
+
/** GitHub's conclusion; null while running or when none was recorded. */
|
|
62
|
+
conclusion: string | null;
|
|
63
|
+
/** 1 on the first attempt; a rerun raises it. */
|
|
64
|
+
attempt: number;
|
|
65
|
+
/** ISO 8601. */
|
|
66
|
+
createdAt: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface LinkedIssueFact {
|
|
70
|
+
number: number;
|
|
71
|
+
title?: string;
|
|
72
|
+
/** ISO 8601. */
|
|
73
|
+
createdAt: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface PullRequestFacts {
|
|
77
|
+
number: number;
|
|
78
|
+
title: string;
|
|
79
|
+
author: string;
|
|
80
|
+
/** ISO 8601. */
|
|
81
|
+
createdAt: string;
|
|
82
|
+
/** ISO 8601 — every fact here is a MERGED pull request. */
|
|
83
|
+
mergedAt: string;
|
|
84
|
+
/** ISO 8601 — GitHub's `updated_at` of the pull request when these facts were read; an incremental
|
|
85
|
+
* read re-reads a listed row only when this moved. Absent on facts read before it was kept. */
|
|
86
|
+
updatedAt?: string;
|
|
87
|
+
/** The head CI first ran on; absent when nothing recorded a head. */
|
|
88
|
+
firstHeadSha?: string;
|
|
89
|
+
ci: CiRunFact[];
|
|
90
|
+
reviews: ReviewFact[];
|
|
91
|
+
pushes: PushFact[];
|
|
92
|
+
/** The board issue the pull request closes or names as its unit, when one is linked. */
|
|
93
|
+
issue?: LinkedIssueFact;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A finished run from the history, keyed to a pull request when its label named one. */
|
|
97
|
+
export interface RunFact {
|
|
98
|
+
agent?: string;
|
|
99
|
+
startedAt: number;
|
|
100
|
+
finishedAt: number;
|
|
101
|
+
status: string;
|
|
102
|
+
pr?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Who counts as the review agent and as an agent pusher/author. */
|
|
106
|
+
export interface DeliveryIdentities {
|
|
107
|
+
/** Logins whose reviews are verdicts — the review agent's App identity, plus config. */
|
|
108
|
+
reviewers: string[];
|
|
109
|
+
/** Logins that are agents beyond the `[bot]` suffix rule. */
|
|
110
|
+
agentLogins: string[];
|
|
111
|
+
/** Names a `Co-Authored-By:` trailer may carry for an agent-made commit (matched case-insensitively). */
|
|
112
|
+
agentCoauthors: string[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** UTC calendar days, inclusive; `weeks` is how many Monday-start weeks the range spans. */
|
|
116
|
+
export interface DeliveryRange {
|
|
117
|
+
since: string;
|
|
118
|
+
until: string;
|
|
119
|
+
weeks: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface DeliveryInput {
|
|
123
|
+
repo: string;
|
|
124
|
+
range: DeliveryRange;
|
|
125
|
+
prs: PullRequestFacts[];
|
|
126
|
+
runs?: RunFact[];
|
|
127
|
+
identities?: Partial<DeliveryIdentities>;
|
|
128
|
+
/** The source stopped before the range's end — the newest pull requests only. */
|
|
129
|
+
truncated?: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---- report (what the view and the command render) ---------------------------------------
|
|
133
|
+
|
|
134
|
+
export interface FindingCounts {
|
|
135
|
+
total: number;
|
|
136
|
+
blocking: number;
|
|
137
|
+
major: number;
|
|
138
|
+
minor: number;
|
|
139
|
+
nit: number;
|
|
140
|
+
fyi: number;
|
|
141
|
+
/** Findings on pull requests whose every post-verdict push was an agent's. */
|
|
142
|
+
noHumanEdit: number;
|
|
143
|
+
/** `noHumanEdit / total`; null with no findings. */
|
|
144
|
+
noHumanEditShare: number | null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface DeliveryIndicators {
|
|
148
|
+
prsMerged: number;
|
|
149
|
+
agentAuthoredPrs: number;
|
|
150
|
+
leadTimeHours: { median: number | null; mean: number | null };
|
|
151
|
+
firstPassCi: { passed: number; known: number; share: number | null };
|
|
152
|
+
reviewRounds: { verdicts: number; fixRounds: number; reviewed: number; perPr: number | null };
|
|
153
|
+
findings: FindingCounts;
|
|
154
|
+
agentRuns: { count: number; minutes: number };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface PullRequestRow {
|
|
158
|
+
number: number;
|
|
159
|
+
title: string;
|
|
160
|
+
author: string;
|
|
161
|
+
agentAuthored: boolean;
|
|
162
|
+
createdAt: string;
|
|
163
|
+
mergedAt: string;
|
|
164
|
+
/** The Monday (UTC date) of the week the pull request merged in. */
|
|
165
|
+
week: string;
|
|
166
|
+
issue?: LinkedIssueFact;
|
|
167
|
+
leadTimeHours: number;
|
|
168
|
+
/** null: no CI fact to judge (no first head, or nothing `pull_request`-triggered on it). */
|
|
169
|
+
firstPassCi: boolean | null;
|
|
170
|
+
reviewRounds: number;
|
|
171
|
+
fixRounds: number;
|
|
172
|
+
approved: boolean;
|
|
173
|
+
findings: Record<FindingSeverity, number>;
|
|
174
|
+
/** null: no verdict, so nothing to resolve. */
|
|
175
|
+
humanEdit: boolean | null;
|
|
176
|
+
agentRuns: { count: number; minutes: number };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface WeekRow extends DeliveryIndicators {
|
|
180
|
+
/** The Monday (UTC date) the week starts on. */
|
|
181
|
+
week: string;
|
|
182
|
+
/** The Sunday (UTC date) it ends on. */
|
|
183
|
+
until: string;
|
|
184
|
+
prs: number[];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface UnitRow extends DeliveryIndicators {
|
|
188
|
+
/** The board issue, or null for the pull requests no issue claims. */
|
|
189
|
+
issue: number | null;
|
|
190
|
+
title: string;
|
|
191
|
+
issueCreatedAt?: string;
|
|
192
|
+
prs: number[];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface DeliveryReport {
|
|
196
|
+
repo: string;
|
|
197
|
+
range: DeliveryRange;
|
|
198
|
+
weeks: WeekRow[];
|
|
199
|
+
units: UnitRow[];
|
|
200
|
+
prs: PullRequestRow[];
|
|
201
|
+
totals: DeliveryIndicators;
|
|
202
|
+
identities: DeliveryIdentities;
|
|
203
|
+
truncated: boolean;
|
|
204
|
+
/** ISO 8601 — when the pull requests' facts were read from GitHub: the snapshot's time, or
|
|
205
|
+
* the live read's. The service stamps it; a report built straight from facts has none. */
|
|
206
|
+
snapshotAt?: string;
|
|
207
|
+
/** ISO 8601 — every pull request merged at or after this instant is in the report; when the
|
|
208
|
+
* report is `truncated`, the weeks that began before it hold the newest pull requests only
|
|
209
|
+
* (`weekIncomplete`). The service stamps it from the source; a report built straight from
|
|
210
|
+
* facts has none. */
|
|
211
|
+
completeFrom?: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A week row holds the newest pull requests only — not the week's — when the report is truncated and
|
|
215
|
+
* the week began before the instant the report is complete from (the week holding that instant is
|
|
216
|
+
* partial, so it counts). A report that names no instant marks no week. */
|
|
217
|
+
export function weekIncomplete(report: Pick<DeliveryReport, "truncated" | "completeFrom">, week: string): boolean {
|
|
218
|
+
return (
|
|
219
|
+
report.truncated &&
|
|
220
|
+
report.completeFrom !== undefined &&
|
|
221
|
+
Date.parse(`${week}T00:00:00Z`) < Date.parse(report.completeFrom)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---- the arithmetic ---------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
const DAY_MS = 86_400_000;
|
|
228
|
+
const HOUR_MS = 3_600_000;
|
|
229
|
+
|
|
230
|
+
/** Add `n` calendar days to a UTC date (`YYYY-MM-DD`). */
|
|
231
|
+
function addDays(day: string, n: number): string {
|
|
232
|
+
return new Date(Date.parse(`${day}T00:00:00Z`) + n * DAY_MS).toISOString().slice(0, 10);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The Monday (UTC date) of the week an instant falls in. */
|
|
236
|
+
export function weekStartOf(iso: string): string {
|
|
237
|
+
const day = iso.slice(0, 10);
|
|
238
|
+
const dow = new Date(`${day}T00:00:00Z`).getUTCDay(); // 0 = Sunday
|
|
239
|
+
return addDays(day, -((dow + 6) % 7));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The review agent's finding lines, by severity: `- [blocking] F1 path — title`
|
|
243
|
+
* (any list marker or none; `blocker` is the same severity). Prose that
|
|
244
|
+
* repeats a finding (`**F1 (blocking)**`) is not a second finding. */
|
|
245
|
+
export function parseFindings(body: string): FindingSeverity[] {
|
|
246
|
+
const out: FindingSeverity[] = [];
|
|
247
|
+
for (const line of body.split("\n")) {
|
|
248
|
+
const m = /^\s*(?:[-*•]\s*)?\[(blocking|blocker|major|minor|nit|fyi)\]/i.exec(line);
|
|
249
|
+
if (!m) continue;
|
|
250
|
+
const s = m[1].toLowerCase();
|
|
251
|
+
out.push(s === "blocker" ? "blocking" : (s as FindingSeverity));
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const emptyCounts = (): Record<FindingSeverity, number> => ({ blocking: 0, major: 0, minor: 0, nit: 0, fyi: 0 });
|
|
257
|
+
|
|
258
|
+
function isAgentLogin(login: string, identities: DeliveryIdentities): boolean {
|
|
259
|
+
return login.endsWith("[bot]") || identities.agentLogins.includes(login);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The `Co-Authored-By:` trailer values of a commit message, as written. */
|
|
263
|
+
export function coauthorsOf(message: string): string[] {
|
|
264
|
+
const out: string[] = [];
|
|
265
|
+
for (const line of message.split("\n")) {
|
|
266
|
+
const m = /^\s*co-authored-by:\s*(.+?)\s*$/i.exec(line);
|
|
267
|
+
if (m) out.push(m[1]);
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** A co-author is an agent when it is a bot login or carries one of the configured agent names. */
|
|
273
|
+
function isAgentCoauthor(who: string, identities: DeliveryIdentities): boolean {
|
|
274
|
+
const lower = who.toLowerCase();
|
|
275
|
+
return lower.includes("[bot]") || identities.agentCoauthors.some((n) => n !== "" && lower.includes(n.toLowerCase()));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A push is an agent's when a bot (or configured agent) login made it, or a commit it carried names an agent co-author. */
|
|
279
|
+
function isAgentPush(p: PushFact, identities: DeliveryIdentities): boolean {
|
|
280
|
+
return isAgentLogin(p.actor, identities) || p.coauthors.some((c) => isAgentCoauthor(c, identities));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The triggers of a pull request's OWN checks. A `push`-triggered run on the
|
|
284
|
+
* branch may test an intermediate head or run other workflows, so it is not
|
|
285
|
+
* judged — a repository whose CI runs only on push reports first-pass CI as
|
|
286
|
+
* unknown, never as green. The fetcher picks the first head by the same set. */
|
|
287
|
+
export const CI_TRIGGERS: ReadonlySet<string> = new Set(["pull_request", "pull_request_target"]);
|
|
288
|
+
/** Conclusions that say something about the code; a skipped or cancelled run says nothing. */
|
|
289
|
+
const CI_DECISIVE: ReadonlySet<string> = new Set([
|
|
290
|
+
"success",
|
|
291
|
+
"failure",
|
|
292
|
+
"timed_out",
|
|
293
|
+
"action_required",
|
|
294
|
+
"startup_failure",
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
/** True: every decisive `pull_request` run on the first head succeeded at its
|
|
298
|
+
* first attempt. False: any red one, or any rerun. Null: nothing to judge. */
|
|
299
|
+
function firstPassCiOf(pr: PullRequestFacts): boolean | null {
|
|
300
|
+
if (!pr.firstHeadSha) return null;
|
|
301
|
+
const runs = pr.ci.filter(
|
|
302
|
+
(r) =>
|
|
303
|
+
r.headSha === pr.firstHeadSha &&
|
|
304
|
+
CI_TRIGGERS.has(r.trigger) &&
|
|
305
|
+
r.conclusion !== null &&
|
|
306
|
+
CI_DECISIVE.has(r.conclusion),
|
|
307
|
+
);
|
|
308
|
+
if (runs.length === 0) return null;
|
|
309
|
+
return runs.every((r) => r.conclusion === "success" && r.attempt <= 1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const byTime = <T extends { at: string }>(a: T, b: T): number => Date.parse(a.at) - Date.parse(b.at);
|
|
313
|
+
|
|
314
|
+
function pullRequestRow(
|
|
315
|
+
pr: PullRequestFacts,
|
|
316
|
+
runs: readonly RunFact[],
|
|
317
|
+
identities: DeliveryIdentities,
|
|
318
|
+
): PullRequestRow {
|
|
319
|
+
const verdicts = pr.reviews
|
|
320
|
+
.filter((r) => identities.reviewers.includes(r.author))
|
|
321
|
+
.map((r) => ({ ...r, at: r.submittedAt }))
|
|
322
|
+
.sort(byTime);
|
|
323
|
+
const pushes = [...pr.pushes].sort(byTime);
|
|
324
|
+
let fixRounds = 0;
|
|
325
|
+
for (let i = 1; i < verdicts.length; i++) {
|
|
326
|
+
const prev = Date.parse(verdicts[i - 1].at);
|
|
327
|
+
const cur = Date.parse(verdicts[i].at);
|
|
328
|
+
if (pushes.some((p) => Date.parse(p.at) > prev && Date.parse(p.at) <= cur)) fixRounds++;
|
|
329
|
+
}
|
|
330
|
+
const approved = verdicts.some((v) => v.state === "approved" || /^\s*LGTM\b/i.test(v.body));
|
|
331
|
+
const findings = emptyCounts();
|
|
332
|
+
for (const v of verdicts) for (const s of parseFindings(v.body)) findings[s]++;
|
|
333
|
+
const firstVerdictAt = verdicts.length > 0 ? Date.parse(verdicts[0].at) : undefined;
|
|
334
|
+
const humanEdit =
|
|
335
|
+
firstVerdictAt === undefined
|
|
336
|
+
? null
|
|
337
|
+
: pushes.some((p) => Date.parse(p.at) > firstVerdictAt && !isAgentPush(p, identities));
|
|
338
|
+
const mine = runs.filter((r) => r.pr === pr.number);
|
|
339
|
+
const from = pr.issue?.createdAt ?? pr.createdAt;
|
|
340
|
+
return {
|
|
341
|
+
number: pr.number,
|
|
342
|
+
title: pr.title,
|
|
343
|
+
author: pr.author,
|
|
344
|
+
agentAuthored: isAgentLogin(pr.author, identities),
|
|
345
|
+
createdAt: pr.createdAt,
|
|
346
|
+
mergedAt: pr.mergedAt,
|
|
347
|
+
week: weekStartOf(pr.mergedAt),
|
|
348
|
+
...(pr.issue ? { issue: pr.issue } : {}),
|
|
349
|
+
leadTimeHours: (Date.parse(pr.mergedAt) - Date.parse(from)) / HOUR_MS,
|
|
350
|
+
firstPassCi: firstPassCiOf(pr),
|
|
351
|
+
reviewRounds: verdicts.length,
|
|
352
|
+
fixRounds,
|
|
353
|
+
approved,
|
|
354
|
+
findings,
|
|
355
|
+
humanEdit,
|
|
356
|
+
agentRuns: {
|
|
357
|
+
count: mine.length,
|
|
358
|
+
minutes: mine.reduce((s, r) => s + Math.max(0, r.finishedAt - r.startedAt), 0) / 60_000,
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function median(values: number[]): number | null {
|
|
364
|
+
if (values.length === 0) return null;
|
|
365
|
+
const s = [...values].sort((a, b) => a - b);
|
|
366
|
+
const mid = Math.floor(s.length / 2);
|
|
367
|
+
return s.length % 2 === 1 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** The indicators over a set of pull request rows — a week, a unit, the whole range. */
|
|
371
|
+
export function indicatorsOf(rows: readonly PullRequestRow[]): DeliveryIndicators {
|
|
372
|
+
const lead = rows.map((r) => r.leadTimeHours);
|
|
373
|
+
const ciKnown = rows.filter((r) => r.firstPassCi !== null);
|
|
374
|
+
const ciPassed = ciKnown.filter((r) => r.firstPassCi === true);
|
|
375
|
+
const reviewed = rows.filter((r) => r.reviewRounds > 0);
|
|
376
|
+
const verdicts = rows.reduce((s, r) => s + r.reviewRounds, 0);
|
|
377
|
+
const findings: FindingCounts = { ...emptyCounts(), total: 0, noHumanEdit: 0, noHumanEditShare: null };
|
|
378
|
+
for (const r of rows) {
|
|
379
|
+
let onPr = 0;
|
|
380
|
+
for (const s of FINDING_SEVERITIES) {
|
|
381
|
+
findings[s] += r.findings[s];
|
|
382
|
+
onPr += r.findings[s];
|
|
383
|
+
}
|
|
384
|
+
findings.total += onPr;
|
|
385
|
+
if (r.humanEdit === false) findings.noHumanEdit += onPr;
|
|
386
|
+
}
|
|
387
|
+
findings.noHumanEditShare = findings.total > 0 ? findings.noHumanEdit / findings.total : null;
|
|
388
|
+
return {
|
|
389
|
+
prsMerged: rows.length,
|
|
390
|
+
agentAuthoredPrs: rows.filter((r) => r.agentAuthored).length,
|
|
391
|
+
leadTimeHours: {
|
|
392
|
+
median: median(lead),
|
|
393
|
+
mean: lead.length > 0 ? lead.reduce((s, v) => s + v, 0) / lead.length : null,
|
|
394
|
+
},
|
|
395
|
+
firstPassCi: {
|
|
396
|
+
passed: ciPassed.length,
|
|
397
|
+
known: ciKnown.length,
|
|
398
|
+
share: ciKnown.length > 0 ? ciPassed.length / ciKnown.length : null,
|
|
399
|
+
},
|
|
400
|
+
reviewRounds: {
|
|
401
|
+
verdicts,
|
|
402
|
+
fixRounds: rows.reduce((s, r) => s + r.fixRounds, 0),
|
|
403
|
+
reviewed: reviewed.length,
|
|
404
|
+
perPr: reviewed.length > 0 ? verdicts / reviewed.length : null,
|
|
405
|
+
},
|
|
406
|
+
findings,
|
|
407
|
+
agentRuns: {
|
|
408
|
+
count: rows.reduce((s, r) => s + r.agentRuns.count, 0),
|
|
409
|
+
minutes: rows.reduce((s, r) => s + r.agentRuns.minutes, 0),
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export const EMPTY_IDENTITIES: Readonly<DeliveryIdentities> = Object.freeze({
|
|
415
|
+
reviewers: [],
|
|
416
|
+
agentLogins: [],
|
|
417
|
+
agentCoauthors: [],
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
/** Every Monday from the week of `since` to the week of `until`, in order. */
|
|
421
|
+
export function weeksOf(range: DeliveryRange): string[] {
|
|
422
|
+
const out: string[] = [];
|
|
423
|
+
const last = weekStartOf(range.until);
|
|
424
|
+
for (let w = weekStartOf(range.since); w <= last; w = addDays(w, 7)) out.push(w);
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** The whole report: pure, deterministic, every number derived from the facts. */
|
|
429
|
+
export function buildDeliveryReport(input: DeliveryInput): DeliveryReport {
|
|
430
|
+
const identities: DeliveryIdentities = { ...EMPTY_IDENTITIES, ...input.identities };
|
|
431
|
+
const sinceMs = Date.parse(`${input.range.since}T00:00:00Z`);
|
|
432
|
+
const untilMs = Date.parse(`${input.range.until}T00:00:00Z`) + DAY_MS;
|
|
433
|
+
const runs = input.runs ?? [];
|
|
434
|
+
const prs = input.prs
|
|
435
|
+
.filter((pr) => {
|
|
436
|
+
const merged = Date.parse(pr.mergedAt);
|
|
437
|
+
return merged >= sinceMs && merged < untilMs;
|
|
438
|
+
})
|
|
439
|
+
.sort((a, b) => a.number - b.number)
|
|
440
|
+
.map((pr) => pullRequestRow(pr, runs, identities));
|
|
441
|
+
const weeks: WeekRow[] = weeksOf(input.range).map((week) => {
|
|
442
|
+
const rows = prs.filter((p) => p.week === week);
|
|
443
|
+
return { week, until: addDays(week, 6), prs: rows.map((p) => p.number), ...indicatorsOf(rows) };
|
|
444
|
+
});
|
|
445
|
+
const byIssue = new Map<number | null, PullRequestRow[]>();
|
|
446
|
+
for (const p of prs) {
|
|
447
|
+
const key = p.issue?.number ?? null;
|
|
448
|
+
byIssue.set(key, [...(byIssue.get(key) ?? []), p]);
|
|
449
|
+
}
|
|
450
|
+
const units: UnitRow[] = [...byIssue.entries()]
|
|
451
|
+
.sort(([a], [b]) => (a === null ? 1 : b === null ? -1 : a - b))
|
|
452
|
+
.map(([issue, rows]) => {
|
|
453
|
+
const first = rows.find((r) => r.issue !== undefined)?.issue;
|
|
454
|
+
return {
|
|
455
|
+
issue,
|
|
456
|
+
title: issue === null ? "(no issue)" : (first?.title ?? `issue ${issue}`),
|
|
457
|
+
...(first?.createdAt ? { issueCreatedAt: first.createdAt } : {}),
|
|
458
|
+
prs: rows.map((p) => p.number),
|
|
459
|
+
...indicatorsOf(rows),
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
return {
|
|
463
|
+
repo: input.repo,
|
|
464
|
+
range: input.range,
|
|
465
|
+
weeks,
|
|
466
|
+
units,
|
|
467
|
+
prs,
|
|
468
|
+
totals: indicatorsOf(prs),
|
|
469
|
+
identities,
|
|
470
|
+
truncated: input.truncated === true,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ---- range ------------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
export const DEFAULT_WEEKS = 4;
|
|
477
|
+
export const MAX_WEEKS = 26;
|
|
478
|
+
const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
479
|
+
|
|
480
|
+
/** `--weeks n` counts back from the current week's Monday (default 4, clamped
|
|
481
|
+
* 1..26); `--since YYYY-MM-DD` names the first day instead. Garbage → the
|
|
482
|
+
* default; a start after today is today. The range ends today, UTC. */
|
|
483
|
+
export function resolveDeliveryRange(
|
|
484
|
+
opts: { since?: string; weeks?: number },
|
|
485
|
+
now: Date = new Date(systemClock()),
|
|
486
|
+
): DeliveryRange {
|
|
487
|
+
const until = now.toISOString().slice(0, 10);
|
|
488
|
+
const untilWeek = weekStartOf(until);
|
|
489
|
+
if (opts.since !== undefined && ISO_DAY.test(opts.since) && Number.isFinite(Date.parse(`${opts.since}T00:00:00Z`))) {
|
|
490
|
+
const since = opts.since > until ? until : opts.since;
|
|
491
|
+
const weeks = Math.round((Date.parse(untilWeek) - Date.parse(weekStartOf(since))) / (7 * DAY_MS)) + 1;
|
|
492
|
+
return { since, until, weeks };
|
|
493
|
+
}
|
|
494
|
+
const weeks =
|
|
495
|
+
opts.weeks !== undefined && Number.isFinite(opts.weeks)
|
|
496
|
+
? Math.min(MAX_WEEKS, Math.max(1, Math.floor(opts.weeks)))
|
|
497
|
+
: DEFAULT_WEEKS;
|
|
498
|
+
return { since: addDays(untilWeek, -7 * (weeks - 1)), until, weeks };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ---- runs (what the history adds) -------------------------------------------------------
|
|
502
|
+
|
|
503
|
+
/** The pull request a run's label names (`…/pull/42`), when it names one. */
|
|
504
|
+
export function pullRequestOfRun(label: string | undefined): number | undefined {
|
|
505
|
+
const m = label === undefined ? null : /\/pull\/(\d+)\b/.exec(label);
|
|
506
|
+
return m ? Number(m[1]) : undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Finished runs of `repo` from a listing, as run facts. */
|
|
510
|
+
export function runFactsOf(
|
|
511
|
+
rows: ReadonlyArray<{
|
|
512
|
+
repo?: string;
|
|
513
|
+
agent?: string;
|
|
514
|
+
label?: string;
|
|
515
|
+
startedAt: number;
|
|
516
|
+
finishedAt?: number;
|
|
517
|
+
finished: boolean;
|
|
518
|
+
status?: string;
|
|
519
|
+
}>,
|
|
520
|
+
repo: string,
|
|
521
|
+
): RunFact[] {
|
|
522
|
+
const out: RunFact[] = [];
|
|
523
|
+
for (const r of rows) {
|
|
524
|
+
if (r.repo !== repo || !r.finished || r.finishedAt === undefined) continue;
|
|
525
|
+
const pr = pullRequestOfRun(r.label);
|
|
526
|
+
out.push({
|
|
527
|
+
...(r.agent !== undefined ? { agent: r.agent } : {}),
|
|
528
|
+
startedAt: r.startedAt,
|
|
529
|
+
finishedAt: r.finishedAt,
|
|
530
|
+
status: r.status ?? "completed",
|
|
531
|
+
...(pr !== undefined ? { pr } : {}),
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ---- text rendering (the command's chat and CLI shape) ----------------------------------
|
|
538
|
+
|
|
539
|
+
const pct = (share: number | null): string => (share === null ? "—" : `${Math.round(share * 100)}%`);
|
|
540
|
+
const hours = (h: number | null, digits = 1): string => (h === null ? "—" : `${h.toFixed(digits)} h`);
|
|
541
|
+
|
|
542
|
+
function indicatorLines(x: DeliveryIndicators): string[] {
|
|
543
|
+
const f = x.findings;
|
|
544
|
+
const severities = FINDING_SEVERITIES.filter((s) => f[s] > 0)
|
|
545
|
+
.map((s) => `${f[s]} ${s}`)
|
|
546
|
+
.join(", ");
|
|
547
|
+
return [
|
|
548
|
+
`issue → merge: median ${hours(x.leadTimeHours.median)} · mean ${hours(x.leadTimeHours.mean)}`,
|
|
549
|
+
`first-pass CI: ${x.firstPassCi.passed}/${x.firstPassCi.known} (${pct(x.firstPassCi.share)})`,
|
|
550
|
+
`review rounds: ${x.reviewRounds.perPr === null ? "—" : `${x.reviewRounds.perPr.toFixed(2)} per PR`} (${x.reviewRounds.verdicts} verdicts, ${x.reviewRounds.fixRounds} fix rounds)`,
|
|
551
|
+
`findings: ${f.total}${severities ? ` (${severities})` : ""} · ${pct(f.noHumanEditShare)} resolved with no human edit`,
|
|
552
|
+
`agent runs: ${x.agentRuns.count} · ${x.agentRuns.minutes.toFixed(1)} min`,
|
|
553
|
+
];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** `just now`, `12 minutes ago`, `3 hours ago`, `2 days ago` — how old the snapshot is, for a footer or a head line. */
|
|
557
|
+
export function snapshotAgeText(snapshotAt: string, nowMs: number): string {
|
|
558
|
+
const minutes = Math.max(0, Math.floor((nowMs - Date.parse(snapshotAt)) / 60_000));
|
|
559
|
+
if (!Number.isFinite(minutes)) return "";
|
|
560
|
+
const unit = (n: number, word: string): string => `${n} ${word}${n === 1 ? "" : "s"} ago`;
|
|
561
|
+
if (minutes < 1) return "just now";
|
|
562
|
+
if (minutes < 90) return unit(minutes, "minute");
|
|
563
|
+
const hours = Math.round(minutes / 60);
|
|
564
|
+
if (hours < 48) return unit(hours, "hour");
|
|
565
|
+
return unit(Math.round(hours / 24), "day");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** `<YYYY-MM-DD> <hh:mm> UTC` — an instant as the text spells one. */
|
|
569
|
+
const clock = (iso: string): string => `${iso.slice(0, 10)} ${iso.slice(11, 16)} UTC`;
|
|
570
|
+
|
|
571
|
+
/** `as of <YYYY-MM-DD> <hh:mm> UTC, 12 minutes ago` — the snapshot's time and age. */
|
|
572
|
+
function asOf(snapshotAt: string, nowMs: number): string {
|
|
573
|
+
return `as of ${clock(snapshotAt)}, ${snapshotAgeText(snapshotAt, nowMs)}`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** `the newest 291 pull requests only, complete from <YYYY-MM-DD> <hh:mm> UTC` — what a truncated report covers. */
|
|
577
|
+
function coverage(report: DeliveryReport): string {
|
|
578
|
+
const from = report.completeFrom === undefined ? "" : `, complete from ${clock(report.completeFrom)}`;
|
|
579
|
+
return `the newest ${report.totals.prsMerged} pull requests only${from}`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** One block per week, then the units — single-spaced lines, so chat carries them as they are. */
|
|
583
|
+
export function renderDeliveryReport(report: DeliveryReport, nowMs: number = systemClock()): string {
|
|
584
|
+
const lines: string[] = [
|
|
585
|
+
[
|
|
586
|
+
`${report.repo} · ${report.range.since} → ${report.range.until} · ${report.range.weeks} week${report.range.weeks === 1 ? "" : "s"}`,
|
|
587
|
+
...(report.snapshotAt !== undefined ? [asOf(report.snapshotAt, nowMs)] : []),
|
|
588
|
+
...(report.truncated ? [coverage(report)] : []),
|
|
589
|
+
].join(" · "),
|
|
590
|
+
];
|
|
591
|
+
for (const w of report.weeks) {
|
|
592
|
+
const week = `Week of ${w.week}${weekIncomplete(report, w.week) ? " (incomplete)" : ""}`;
|
|
593
|
+
if (w.prsMerged === 0) {
|
|
594
|
+
lines.push("", `${week}: nothing merged`);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
lines.push("", `${week}: ${w.prsMerged} merged (${w.agentAuthoredPrs} agent-authored)`, ...indicatorLines(w));
|
|
598
|
+
}
|
|
599
|
+
if (report.weeks.length > 1 && report.totals.prsMerged > 0) {
|
|
600
|
+
lines.push(
|
|
601
|
+
"",
|
|
602
|
+
`Range: ${report.totals.prsMerged} merged (${report.totals.agentAuthoredPrs} agent-authored)`,
|
|
603
|
+
...indicatorLines(report.totals),
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
if (report.units.length > 0) {
|
|
607
|
+
lines.push("", "Units:");
|
|
608
|
+
for (const u of report.units) {
|
|
609
|
+
const prs = `${u.prs.length === 1 ? "PR" : "PRs"} ${u.prs.join(", ")}`;
|
|
610
|
+
const who = u.issue === null ? "(no issue)" : `${u.issue} ${u.title}`;
|
|
611
|
+
const facts = [
|
|
612
|
+
hours(u.leadTimeHours.median),
|
|
613
|
+
`${u.reviewRounds.verdicts} round${u.reviewRounds.verdicts === 1 ? "" : "s"}`,
|
|
614
|
+
...(u.findings.blocking > 0 ? [`${u.findings.blocking} blocking`] : []),
|
|
615
|
+
];
|
|
616
|
+
lines.push(`• ${who} — ${prs} · ${facts.join(" · ")}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return lines.join("\n");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ---- config -----------------------------------------------------------------------------
|
|
623
|
+
|
|
624
|
+
export interface DeliveryConfig {
|
|
625
|
+
/** `owner/name` repositories the page serves; the first is the default. */
|
|
626
|
+
repos: string[];
|
|
627
|
+
/** Logins whose reviews are verdicts, beside the process's own App identity. */
|
|
628
|
+
reviewers: string[];
|
|
629
|
+
/** Logins that are agents beyond the `[bot]` suffix. */
|
|
630
|
+
agentLogins: string[];
|
|
631
|
+
/** Co-author names an agent-made commit carries. */
|
|
632
|
+
agentCoauthors: string[];
|
|
633
|
+
/** The snapshot the page and the command serve (src/core/deliverySnapshot.ts). */
|
|
634
|
+
snapshot: {
|
|
635
|
+
/** Minutes between two reads of a configured repository's facts from GitHub. */
|
|
636
|
+
everyMinutes: number;
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** `delivery.snapshot.everyMinutes`: the default and the bounds a value must keep. */
|
|
641
|
+
export const SNAPSHOT_EVERY_MINUTES = Object.freeze({ default: 60, min: 5, max: 1440 });
|
|
642
|
+
|
|
643
|
+
/** `owner/name`: two path segments of word characters, dots and dashes, neither of them dots alone. */
|
|
644
|
+
export const REPO_SLUG = /^(?!\.+\/)[\w.-]+\/(?!\.+$)[\w.-]+$/;
|
|
645
|
+
|
|
646
|
+
function stringList(raw: unknown, what: string, valid: (s: string) => boolean = () => true): string[] {
|
|
647
|
+
if (raw === undefined) return [];
|
|
648
|
+
if (!Array.isArray(raw) || !raw.every((s) => typeof s === "string" && s !== "" && valid(s)))
|
|
649
|
+
throw new Error(`${what} must be a list of ${what.endsWith("repos") ? "owner/name repositories" : "names"}`);
|
|
650
|
+
return raw as string[];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** `delivery.snapshot`: absent → the default interval; a value outside the bounds or not a whole number throws by name. */
|
|
654
|
+
function snapshotConfig(raw: unknown): DeliveryConfig["snapshot"] {
|
|
655
|
+
if (raw === undefined) return { everyMinutes: SNAPSHOT_EVERY_MINUTES.default };
|
|
656
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
657
|
+
throw new Error("delivery.snapshot must be a mapping");
|
|
658
|
+
const every = (raw as Record<string, unknown>).everyMinutes;
|
|
659
|
+
if (every === undefined) return { everyMinutes: SNAPSHOT_EVERY_MINUTES.default };
|
|
660
|
+
if (
|
|
661
|
+
typeof every !== "number" ||
|
|
662
|
+
!Number.isInteger(every) ||
|
|
663
|
+
every < SNAPSHOT_EVERY_MINUTES.min ||
|
|
664
|
+
every > SNAPSHOT_EVERY_MINUTES.max
|
|
665
|
+
)
|
|
666
|
+
throw new Error(
|
|
667
|
+
`delivery.snapshot.everyMinutes must be a whole number of minutes between ${SNAPSHOT_EVERY_MINUTES.min} and ${SNAPSHOT_EVERY_MINUTES.max}`,
|
|
668
|
+
);
|
|
669
|
+
return { everyMinutes: every };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Validates the `delivery:` config block. Absent → undefined (no default repository). */
|
|
673
|
+
export function parseDeliveryConfig(raw: unknown): DeliveryConfig | undefined {
|
|
674
|
+
if (raw === undefined || raw === null) return undefined;
|
|
675
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new Error("delivery: must be a mapping");
|
|
676
|
+
const r = raw as Record<string, unknown>;
|
|
677
|
+
return {
|
|
678
|
+
repos: stringList(r.repos, "delivery.repos", (s) => REPO_SLUG.test(s)),
|
|
679
|
+
reviewers: stringList(r.reviewers, "delivery.reviewers"),
|
|
680
|
+
agentLogins: stringList(r.agentLogins, "delivery.agentLogins"),
|
|
681
|
+
agentCoauthors: stringList(r.agentCoauthors, "delivery.agentCoauthors"),
|
|
682
|
+
snapshot: snapshotConfig(r.snapshot),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ---- source and service seams -----------------------------------------------------------
|
|
687
|
+
|
|
688
|
+
export interface DeliveryFetch {
|
|
689
|
+
prs: PullRequestFacts[];
|
|
690
|
+
/** The source stopped before the range's start (a page cap); the newest are in. */
|
|
691
|
+
truncated: boolean;
|
|
692
|
+
/** ISO 8601 — every pull request merged at or after this instant is in `prs`: the range's start
|
|
693
|
+
* on a complete read, the oldest update the capped listing reached otherwise. A source that does
|
|
694
|
+
* not say is complete from the range's start. */
|
|
695
|
+
completeFrom?: string;
|
|
696
|
+
/** ISO 8601 — when the facts were read from GitHub, when the source knows (a snapshot's time). */
|
|
697
|
+
fetchedAt?: string;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export interface DeliveryFetchOptions {
|
|
701
|
+
/** Read GitHub now, whatever a snapshot holds, and refresh the snapshot. */
|
|
702
|
+
fresh?: boolean;
|
|
703
|
+
/** An incremental read (the snapshot's refresh): only the pull requests touched at or after `since`
|
|
704
|
+
* — the listing is newest-touched first, so the first older row ends it — and, of those, only the
|
|
705
|
+
* rows whose update time `known` does not already hold for their number; a held row's stored facts
|
|
706
|
+
* stand. The fetch is then complete from `since`, not from the range's start. */
|
|
707
|
+
touched?: { since: string; known: ReadonlyMap<number, string> };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Where the pull requests' facts come from: GitHub in production (behind the snapshot), memory in tests. */
|
|
711
|
+
export interface DeliverySource {
|
|
712
|
+
fetchPullRequests(repo: string, range: DeliveryRange, opts?: DeliveryFetchOptions): Promise<DeliveryFetch>;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** The second implementation (AGENTS.md invariant 2) and the test double: a map of repo → facts.
|
|
716
|
+
* An incremental read answers the rows touched since the instant (by `updatedAt`, else `mergedAt`)
|
|
717
|
+
* whose update time the caller does not hold, complete from that instant. */
|
|
718
|
+
export class InMemoryDeliverySource implements DeliverySource {
|
|
719
|
+
readonly calls: Array<{
|
|
720
|
+
repo: string;
|
|
721
|
+
range: DeliveryRange;
|
|
722
|
+
fresh?: boolean;
|
|
723
|
+
touched?: DeliveryFetchOptions["touched"];
|
|
724
|
+
}> = [];
|
|
725
|
+
constructor(private byRepo: Readonly<Record<string, PullRequestFacts[]>> = {}) {}
|
|
726
|
+
/** Replace what a repository answers (a test moving GitHub on). */
|
|
727
|
+
set(repo: string, prs: PullRequestFacts[]): void {
|
|
728
|
+
this.byRepo = { ...this.byRepo, [repo]: prs };
|
|
729
|
+
}
|
|
730
|
+
fetchPullRequests(repo: string, range: DeliveryRange, opts?: DeliveryFetchOptions): Promise<DeliveryFetch> {
|
|
731
|
+
this.calls.push({
|
|
732
|
+
repo,
|
|
733
|
+
range,
|
|
734
|
+
...(opts?.fresh ? { fresh: true } : {}),
|
|
735
|
+
...(opts?.touched ? { touched: opts.touched } : {}),
|
|
736
|
+
});
|
|
737
|
+
const all = this.byRepo[repo] ?? [];
|
|
738
|
+
const touched = opts?.touched;
|
|
739
|
+
if (!touched) return Promise.resolve({ prs: all, truncated: false });
|
|
740
|
+
const since = Date.parse(touched.since);
|
|
741
|
+
const prs = all.filter(
|
|
742
|
+
(p) => Date.parse(p.updatedAt ?? p.mergedAt) >= since && touched.known.get(p.number) !== p.updatedAt,
|
|
743
|
+
);
|
|
744
|
+
return Promise.resolve({ prs, truncated: false, completeFrom: touched.since });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export interface DeliveryReportOptions {
|
|
749
|
+
since?: string;
|
|
750
|
+
weeks?: number;
|
|
751
|
+
/** Read GitHub now instead of the snapshot, and refresh it. */
|
|
752
|
+
fresh?: boolean;
|
|
753
|
+
/** The caller's finished runs of the repository over the resolved range — what the history adds. */
|
|
754
|
+
runs?: (range: DeliveryRange) => Promise<RunFact[]>;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export interface DeliveryService {
|
|
758
|
+
/** Why no report can be made at all (no GitHub credential), or undefined when one can. */
|
|
759
|
+
unavailable(): string | undefined;
|
|
760
|
+
/** The configured repositories; the first is the page's default. */
|
|
761
|
+
repos(): string[];
|
|
762
|
+
/** The report for one repository over the resolved range — from the snapshot when one fits the
|
|
763
|
+
* range, a live read otherwise or on `fresh`; stamped with when its facts were read and from
|
|
764
|
+
* when they are complete. Throws on upstream failure. */
|
|
765
|
+
report(repo: string, opts: DeliveryReportOptions): Promise<DeliveryReport>;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Why there is no delivery page: the process has no GitHub credential. */
|
|
769
|
+
export const DELIVERY_OFF_MESSAGE =
|
|
770
|
+
"Delivery indicators aren't available — this process has no GitHub credential (the GitHub App triple or GH_TOKEN); set one, then name repositories under delivery.repos.";
|
|
771
|
+
|
|
772
|
+
/** The service of a process without GitHub (a Null Object, routing-and-config item 16). */
|
|
773
|
+
export class NullDeliveryService implements DeliveryService {
|
|
774
|
+
unavailable(): string {
|
|
775
|
+
return DELIVERY_OFF_MESSAGE;
|
|
776
|
+
}
|
|
777
|
+
repos(): string[] {
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
report(_repo: string, _opts: DeliveryReportOptions): Promise<DeliveryReport> {
|
|
781
|
+
return Promise.reject(new Error(DELIVERY_OFF_MESSAGE));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export interface DeliveryServiceOptions {
|
|
786
|
+
/** The process's identities to merge under the config's — the App login the review agent posts as. */
|
|
787
|
+
identities?: () => Promise<Partial<DeliveryIdentities>>;
|
|
788
|
+
now?: () => Date;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export function createDeliveryService(
|
|
792
|
+
cfg: DeliveryConfig | undefined,
|
|
793
|
+
source: DeliverySource,
|
|
794
|
+
opts: DeliveryServiceOptions = {},
|
|
795
|
+
): DeliveryService {
|
|
796
|
+
const now = opts.now ?? (() => new Date(systemClock()));
|
|
797
|
+
return {
|
|
798
|
+
unavailable: () => undefined,
|
|
799
|
+
repos: () => [...(cfg?.repos ?? [])],
|
|
800
|
+
async report(repo, o) {
|
|
801
|
+
if (!REPO_SLUG.test(repo)) throw new Error(`repository must be owner/name, got ${JSON.stringify(repo)}`);
|
|
802
|
+
const at = now();
|
|
803
|
+
const range = resolveDeliveryRange({ since: o.since, weeks: o.weeks }, at);
|
|
804
|
+
const [fetched, process, runs] = await Promise.all([
|
|
805
|
+
source.fetchPullRequests(repo, range, o.fresh ? { fresh: true } : undefined),
|
|
806
|
+
opts.identities?.() ?? Promise.resolve<Partial<DeliveryIdentities>>({}),
|
|
807
|
+
o.runs?.(range) ?? Promise.resolve<RunFact[]>([]),
|
|
808
|
+
]);
|
|
809
|
+
const identities: DeliveryIdentities = {
|
|
810
|
+
reviewers: [...new Set([...(cfg?.reviewers ?? []), ...(process.reviewers ?? [])])],
|
|
811
|
+
agentLogins: [...new Set([...(cfg?.agentLogins ?? []), ...(process.agentLogins ?? [])])],
|
|
812
|
+
agentCoauthors: [...new Set([...(cfg?.agentCoauthors ?? []), ...(process.agentCoauthors ?? [])])],
|
|
813
|
+
};
|
|
814
|
+
return {
|
|
815
|
+
...buildDeliveryReport({
|
|
816
|
+
repo,
|
|
817
|
+
range,
|
|
818
|
+
prs: fetched.prs,
|
|
819
|
+
runs,
|
|
820
|
+
identities,
|
|
821
|
+
truncated: fetched.truncated,
|
|
822
|
+
}),
|
|
823
|
+
// A source behind a snapshot says when it read; a bare one read just now.
|
|
824
|
+
snapshotAt: fetched.fetchedAt ?? at.toISOString(),
|
|
825
|
+
...(fetched.completeFrom !== undefined ? { completeFrom: fetched.completeFrom } : {}),
|
|
826
|
+
};
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
}
|