@maycombhq/atticus 0.0.0-stage → 2.0.0-beta.3
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/LICENSE +177 -0
- package/README.md +17 -2
- package/THIRD-PARTY-LICENSES.txt +2607 -0
- package/dist/bin.js +47 -0
- package/dist/chunk-23W7VWKR.js +2609 -0
- package/dist/chunk-2C5B464C.js +106 -0
- package/dist/chunk-2LLFKP7C.js +53 -0
- package/dist/chunk-2MD7TXSG.js +166 -0
- package/dist/chunk-2UIQBDJO.js +131 -0
- package/dist/chunk-32MPK524.js +378 -0
- package/dist/chunk-3ALD2W2A.js +95 -0
- package/dist/chunk-3G4YI4ZU.js +16 -0
- package/dist/chunk-4AK7IBVP.js +915 -0
- package/dist/chunk-4BMDGTED.js +69 -0
- package/dist/chunk-4NDAAFQT.js +103 -0
- package/dist/chunk-4Q3NN7VF.js +32 -0
- package/dist/chunk-4RS5KZQD.js +25 -0
- package/dist/chunk-4SKZDI62.js +22 -0
- package/dist/chunk-55YODD3N.js +385 -0
- package/dist/chunk-56YQ6ROQ.js +3479 -0
- package/dist/chunk-5A5L3T2U.js +583 -0
- package/dist/chunk-5FEOD73Z.js +374 -0
- package/dist/chunk-5GDRCDQB.js +32 -0
- package/dist/chunk-5WS2YOGD.js +95 -0
- package/dist/chunk-65NYPGRC.js +766 -0
- package/dist/chunk-6BVONWJ7.js +50 -0
- package/dist/chunk-6D4FRTFF.js +53 -0
- package/dist/chunk-6GPJOFV4.js +149 -0
- package/dist/chunk-6JCPI55B.js +92 -0
- package/dist/chunk-6JUBZBMW.js +56 -0
- package/dist/chunk-6SCVQZX5.js +2522 -0
- package/dist/chunk-6TACQN3N.js +41 -0
- package/dist/chunk-72TXNLOC.js +496 -0
- package/dist/chunk-76WKMA4J.js +14 -0
- package/dist/chunk-7EFILTCI.js +67 -0
- package/dist/chunk-7EKXJZ2Y.js +2902 -0
- package/dist/chunk-7EOBHUMC.js +133 -0
- package/dist/chunk-7JUKDRSZ.js +1726 -0
- package/dist/chunk-7NN5VWDC.js +43 -0
- package/dist/chunk-7UFQZXZA.js +5949 -0
- package/dist/chunk-A3KPZJ5M.js +131 -0
- package/dist/chunk-A6AC72A5.js +48 -0
- package/dist/chunk-A7KZKIZX.js +166 -0
- package/dist/chunk-ABWHYWNI.js +101 -0
- package/dist/chunk-AFFIK6JI.js +46 -0
- package/dist/chunk-AHOQZ4OK.js +37 -0
- package/dist/chunk-AVJDSJ2F.js +18 -0
- package/dist/chunk-AXXCPAP6.js +203 -0
- package/dist/chunk-AY3QXYMB.js +41 -0
- package/dist/chunk-B2OKWJIU.js +143 -0
- package/dist/chunk-B632FH3I.js +251 -0
- package/dist/chunk-BLW4SXCU.js +38 -0
- package/dist/chunk-BVS2K73A.js +33 -0
- package/dist/chunk-BWZ77RQT.js +25 -0
- package/dist/chunk-BYBUDX4T.js +163 -0
- package/dist/chunk-BYZCY6SP.js +230 -0
- package/dist/chunk-C6EKFH7J.js +169 -0
- package/dist/chunk-C6MZFGN7.js +271 -0
- package/dist/chunk-CAQZUNOH.js +1150 -0
- package/dist/chunk-CD4XHYGY.js +856 -0
- package/dist/chunk-CEZQOLEO.js +1217 -0
- package/dist/chunk-CHWTETW7.js +128 -0
- package/dist/chunk-CMV7TGU6.js +31 -0
- package/dist/chunk-CNUKCS2T.js +2741 -0
- package/dist/chunk-CP3L3FH7.js +549 -0
- package/dist/chunk-CPGCA6GZ.js +41 -0
- package/dist/chunk-CWM6KM5G.js +315 -0
- package/dist/chunk-CXDSTRUJ.js +873 -0
- package/dist/chunk-D3PO4DOZ.js +261 -0
- package/dist/chunk-DFBCSHVT.js +69 -0
- package/dist/chunk-DHPACDG7.js +104 -0
- package/dist/chunk-DPC5Y5DL.js +3898 -0
- package/dist/chunk-DPUYDDQG.js +164 -0
- package/dist/chunk-DQN7TF5D.js +325 -0
- package/dist/chunk-EADP3PEN.js +450 -0
- package/dist/chunk-ED2LWWIP.js +878 -0
- package/dist/chunk-EHVTK3UE.js +116 -0
- package/dist/chunk-EPTBZ3GP.js +101 -0
- package/dist/chunk-EXIMEJ2X.js +545 -0
- package/dist/chunk-EYWIDJFU.js +130 -0
- package/dist/chunk-F44OMYIN.js +54 -0
- package/dist/chunk-F7HYOUFF.js +287 -0
- package/dist/chunk-FALXWAMD.js +182 -0
- package/dist/chunk-FBNPE7HS.js +49 -0
- package/dist/chunk-FSWUD5QB.js +108 -0
- package/dist/chunk-FVQISWPW.js +296 -0
- package/dist/chunk-G2B4ZQGH.js +240 -0
- package/dist/chunk-GK6XK5SP.js +1227 -0
- package/dist/chunk-GLEBK5L3.js +34 -0
- package/dist/chunk-GNB5QDDI.js +25 -0
- package/dist/chunk-GQ4N4WKF.js +1142 -0
- package/dist/chunk-GRMR5TRQ.js +119 -0
- package/dist/chunk-HC6XKREB.js +334 -0
- package/dist/chunk-HFQCFSJR.js +60 -0
- package/dist/chunk-HJDICQA4.js +102 -0
- package/dist/chunk-HLJAXMM4.js +233 -0
- package/dist/chunk-HVIMMDVB.js +162 -0
- package/dist/chunk-HZVLAPN5.js +32 -0
- package/dist/chunk-I3E7CX4H.js +155 -0
- package/dist/chunk-IQFTZ37C.js +2919 -0
- package/dist/chunk-ITY3VJRX.js +285 -0
- package/dist/chunk-JBTK5OP2.js +55 -0
- package/dist/chunk-JPGCERE2.js +26 -0
- package/dist/chunk-JQDNHIBC.js +38 -0
- package/dist/chunk-JRBCWLOR.js +307 -0
- package/dist/chunk-JRZQA2JM.js +212 -0
- package/dist/chunk-KCBJCA66.js +45 -0
- package/dist/chunk-KJPMEAI4.js +705 -0
- package/dist/chunk-KQDZXGNR.js +14 -0
- package/dist/chunk-KUUQR4HA.js +264 -0
- package/dist/chunk-L24C55VN.js +7923 -0
- package/dist/chunk-L3BX5ZIB.js +400 -0
- package/dist/chunk-L6SGQIR3.js +471 -0
- package/dist/chunk-LBENYT3H.js +66 -0
- package/dist/chunk-LNAJW6FB.js +47 -0
- package/dist/chunk-MC3H7736.js +19910 -0
- package/dist/chunk-MMOSZPWJ.js +273 -0
- package/dist/chunk-MXCHWULV.js +285 -0
- package/dist/chunk-N3E56YPN.js +344 -0
- package/dist/chunk-NHFQMOVK.js +180 -0
- package/dist/chunk-NQU6KNQF.js +27 -0
- package/dist/chunk-OIK5YPTN.js +391 -0
- package/dist/chunk-OLWFG75W.js +4330 -0
- package/dist/chunk-ONPUIE7Y.js +29 -0
- package/dist/chunk-OW7D2PJL.js +376 -0
- package/dist/chunk-P63VL4HE.js +2357 -0
- package/dist/chunk-PA4YNUMU.js +45 -0
- package/dist/chunk-PAMPPUR6.js +4500 -0
- package/dist/chunk-PBS3OFNN.js +113 -0
- package/dist/chunk-PFT3VTS4.js +214 -0
- package/dist/chunk-PFTA6BZF.js +49 -0
- package/dist/chunk-PNN5KLAY.js +119 -0
- package/dist/chunk-PRPPGYZG.js +104 -0
- package/dist/chunk-PUKEZ37F.js +666 -0
- package/dist/chunk-Q3HOFXZJ.js +76 -0
- package/dist/chunk-Q5AMFEWD.js +119 -0
- package/dist/chunk-QKRNMBJO.js +80 -0
- package/dist/chunk-QSU47IQ5.js +36 -0
- package/dist/chunk-QSXK2RGH.js +688 -0
- package/dist/chunk-QXNWYUYI.js +52 -0
- package/dist/chunk-R6RY4Q5T.js +51 -0
- package/dist/chunk-RCU3MPY2.js +1496 -0
- package/dist/chunk-RLIB4JUN.js +174 -0
- package/dist/chunk-RYSCLMU3.js +1991 -0
- package/dist/chunk-RZUZ4RJJ.js +415 -0
- package/dist/chunk-S2A7YC3Q.js +41433 -0
- package/dist/chunk-S2FBBX7D.js +540 -0
- package/dist/chunk-S34B4QPI.js +430 -0
- package/dist/chunk-SETWVXOJ.js +2078 -0
- package/dist/chunk-SMA4N4AC.js +211 -0
- package/dist/chunk-SP2I63OO.js +252 -0
- package/dist/chunk-STHSY74Q.js +242 -0
- package/dist/chunk-STUPFGXY.js +3206 -0
- package/dist/chunk-T5I4S2W3.js +34 -0
- package/dist/chunk-T6AIP7H5.js +3851 -0
- package/dist/chunk-TANFV3MZ.js +531 -0
- package/dist/chunk-TAVDMUV3.js +189 -0
- package/dist/chunk-TBSQJ6IV.js +324 -0
- package/dist/chunk-TIZHP7KE.js +408 -0
- package/dist/chunk-TV45EUGM.js +51 -0
- package/dist/chunk-TW2KPPUC.js +4087 -0
- package/dist/chunk-V2YTAIQO.js +1075 -0
- package/dist/chunk-V5PKHHQQ.js +1963 -0
- package/dist/chunk-VF4IOZZO.js +184 -0
- package/dist/chunk-VJTINHZI.js +48 -0
- package/dist/chunk-VKLILA2D.js +205 -0
- package/dist/chunk-VOGCSYOI.js +16 -0
- package/dist/chunk-VPTTCYOC.js +16101 -0
- package/dist/chunk-WY6PP6AA.js +53 -0
- package/dist/chunk-WYFIXP54.js +174 -0
- package/dist/chunk-WZ2BXIGG.js +34 -0
- package/dist/chunk-XJNDSYGO.js +147 -0
- package/dist/chunk-XPCM366L.js +70 -0
- package/dist/chunk-XSQKGVYA.js +69 -0
- package/dist/chunk-XUPVB7TL.js +207 -0
- package/dist/chunk-XZRON6IV.js +131 -0
- package/dist/chunk-Y4NG2FP5.js +94 -0
- package/dist/chunk-YEDDOYSC.js +27809 -0
- package/dist/chunk-YGMCXNTY.js +41 -0
- package/dist/chunk-YJKPTX6J.js +228 -0
- package/dist/chunk-YMZ3TNYB.js +18 -0
- package/dist/chunk-Z6LF7HQN.js +22 -0
- package/dist/chunk-Z7SG32ZY.js +55 -0
- package/dist/chunk-ZDPAS56H.js +76 -0
- package/dist/chunk-ZJSP24X4.js +1247 -0
- package/dist/chunk-ZMAINWBW.js +18 -0
- package/dist/chunk-ZMFPW66A.js +22 -0
- package/dist/chunk-ZUIAOKXI.js +1611 -0
- package/dist/chunk-ZVM6AKKM.js +47 -0
- package/dist/files.json +1888 -0
- package/dist/fixtures/forge/README.md +19 -0
- package/dist/fixtures/forge/expected/discrepancies.yaml +54 -0
- package/dist/fixtures/forge/expected/inputs.yaml +38 -0
- package/dist/fixtures/forge/expected/scan.yaml +342 -0
- package/dist/fixtures/forge/inputs/diagrams/forge-context.drawio +23 -0
- package/dist/fixtures/forge/inputs/partner-specs/northwind-pay-openapi.yaml +11 -0
- package/dist/fixtures/forge/inputs/tickets/ado-stories.json +4 -0
- package/dist/fixtures/forge/inputs/tickets/closed-service-requests.json +5 -0
- package/dist/fixtures/forge/inputs/wiki/integration-patterns.md +11 -0
- package/dist/fixtures/forge/inputs/wiki/ledger-solution-spec.md +9 -0
- package/dist/fixtures/forge/inputs/wiki/snow-firewall-request.md +4 -0
- package/dist/fixtures/forge/metarepo/judgments/reaches/84af5b6af24f7ca8c23013f3.yaml +17 -0
- package/dist/fixtures/forge/metarepo/judgments/reaches/c3917555edebddce8cecb647.yaml +17 -0
- package/dist/fixtures/forge/metarepo/judgments/reaches/c5fafd6601f8ca9e34e0bbd0.yaml +17 -0
- package/dist/fixtures/forge/metarepo/org-pack/agents/architecture-critic.yaml +9 -0
- package/dist/fixtures/forge/metarepo/org-pack/agents/story-writer.yaml +10 -0
- package/dist/fixtures/forge/metarepo/org-pack/checklists/architecture.yaml +115 -0
- package/dist/fixtures/forge/metarepo/org-pack/dialects/forge.yaml +111 -0
- package/dist/fixtures/forge/metarepo/org-pack/gates.yaml +22 -0
- package/dist/fixtures/forge/metarepo/org-pack/pack.yaml +11 -0
- package/dist/fixtures/forge/metarepo/org-pack/patterns/A.EB.PUB.yaml +12 -0
- package/dist/fixtures/forge/metarepo/org-pack/patterns/D.JDBC.yaml +12 -0
- package/dist/fixtures/forge/metarepo/org-pack/patterns/S.DIR.BFF.yaml +12 -0
- package/dist/fixtures/forge/metarepo/org-pack/patterns/S.GW.SOR.yaml +13 -0
- package/dist/fixtures/forge/metarepo/org-pack/patterns/S.GW.VND.yaml +14 -0
- package/dist/fixtures/forge/metarepo/org-pack/process/change-templates/feature.md +37 -0
- package/dist/fixtures/forge/metarepo/org-pack/process/skills/add-forge-endpoint/SKILL.md +17 -0
- package/dist/fixtures/forge/metarepo/org-pack/process/snow/firewall-rule.yaml +14 -0
- package/dist/fixtures/forge/metarepo/org-pack/process/snow/gateway-route.yaml +13 -0
- package/dist/fixtures/forge/metarepo/org-pack/profiles/forge/profile.yaml +12 -0
- package/dist/fixtures/forge/metarepo/org-pack/profiles/forge/repos.manifest.yaml +44 -0
- package/dist/fixtures/forge/metarepo/org-pack/roles.yaml +14 -0
- package/dist/fixtures/forge/metarepo/org-pack/runtime.yaml +14 -0
- package/dist/fixtures/forge/metarepo/org-pack/sources.yaml +3 -0
- package/dist/fixtures/forge/repos/forge-events/topics/payments.initiated.yaml +4 -0
- package/dist/fixtures/forge/repos/forge-events/topics/transfers.completed.yaml +4 -0
- package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/ledger-v1.xml +3 -0
- package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/policies/SpikeArrest.xml +3 -0
- package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/policies/VerifyOAuth.xml +4 -0
- package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/proxies/default.xml +14 -0
- package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/targets/ledger.xml +5 -0
- package/dist/fixtures/forge/repos/forge-infra/alerts/ledger-api.yaml +5 -0
- package/dist/fixtures/forge/repos/forge-infra/k8s/ledger-api.yaml +15 -0
- package/dist/fixtures/forge/repos/forge-infra/k8s/portal-bff.yaml +7 -0
- package/dist/fixtures/forge/repos/forge-infra/terraform/ledger-db.tf +9 -0
- package/dist/fixtures/forge/repos/forge-ledger/api/ledger-openapi.yaml +31 -0
- package/dist/fixtures/forge/repos/forge-ledger/forge.yaml +4 -0
- package/dist/fixtures/forge/repos/forge-ledger/src/main/java/forge/ledger/LedgerEndpoints.java +13 -0
- package/dist/fixtures/forge/repos/forge-ledger/src/main/java/forge/ledger/LedgerEvents.java +11 -0
- package/dist/fixtures/forge/repos/forge-ledger/src/main/java/forge/ledger/NorthwindClient.java +10 -0
- package/dist/fixtures/forge/repos/forge-ledger/src/main/resources/application.yml +3 -0
- package/dist/fixtures/forge/repos/forge-ledger/src/main/resources/db/migration/V1__create_account.sql +5 -0
- package/dist/fixtures/forge/repos/forge-ledger/src/main/resources/db/migration/V2__create_entry.sql +7 -0
- package/dist/fixtures/forge/repos/forge-portal-bff/pom.xml +1 -0
- package/dist/fixtures/forge/repos/forge-portal-bff/src/main/java/forge/portal/AuditPublisher.java +12 -0
- package/dist/fixtures/forge/repos/forge-portal-bff/src/main/java/forge/portal/BalanceController.java +16 -0
- package/dist/fixtures/forge/repos/forge-portal-bff/src/main/java/forge/portal/LedgerClient.java +13 -0
- package/dist/fixtures/forge/repos/forge-portal-bff/src/main/java/forge/portal/PaymentEvents.java +13 -0
- package/dist/fixtures/forge/repos/forge-portal-bff/src/main/resources/application.yml +8 -0
- package/dist/fixtures/forge/repos/forge-portal-ui/package.json +1 -0
- package/dist/fixtures/forge/repos/forge-portal-ui/src/api/balance.ts +5 -0
- package/dist/fixtures/forge/repos/forge-reports-job/src/main/java/forge/reports/DailyEntriesJob.java +19 -0
- package/dist/fixtures/forge/repos/forge-reports-job/src/main/resources/application.yml +3 -0
- package/dist/fixtures/forge-change/README.md +41 -0
- package/dist/fixtures/forge-change/expected/findings.yaml +301 -0
- package/dist/fixtures/forge-change/expected/stories.yaml +101 -0
- package/dist/fixtures/forge-change/judgments/personal-data/0b5f3dbf5257a22275d8c74b.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/2759a66770134c417440bf2d.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/31b1ea2f14d0737f1aca79fd.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/49afd70c08228e65ad6ca1ed.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/761cabcb8a39f5e5866d2dd6.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/adae902b90cc8ffa0c765145.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/ba11bb933105ec9493e065dd.yaml +18 -0
- package/dist/fixtures/forge-change/judgments/personal-data/bdbcd1cdac3021c68063c3e7.yaml +18 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/brief.md +35 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/change.yaml +120 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/data.md +18 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/design.md +29 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/interfaces.md +17 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/mappings.md +26 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/proposal.md +35 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/snow.md +12 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/specs/transfer-limits/spec.md +19 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories/s1-refuse-over-limit.md +17 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories/s2-show-remaining.md +16 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories/s3-audit-refusals.md +18 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories/s4-velocity-check.md +16 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories.md +8 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/tasks.md +12 -0
- package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/ux.md +7 -0
- package/dist/fixtures/forge-change/org-pack/agents/code-and-test-critic.yaml +13 -0
- package/dist/fixtures/forge-change/org-pack/agents/complete-critic.yaml +12 -0
- package/dist/fixtures/forge-change/org-pack/agents/safe-critic.yaml +12 -0
- package/dist/fixtures/forge-change/org-pack/agents/ship-critic.yaml +11 -0
- package/dist/fixtures/forge-change/org-pack/agents/true-critic.yaml +11 -0
- package/dist/fixtures/forge-change/org-pack/checklists/review.yaml +54 -0
- package/dist/fixtures/forge-change/org-pack/gates.yaml +25 -0
- package/dist/fixtures/forge-change/org-pack/teams/team-ledger/checklists/ledger-team.yaml +227 -0
- package/dist/fixtures/forge-change/recordings/complete-critic/2ad84f84bf39c1d5412697e69fe23034d6983ec45a00314bf0975e627589a3c5.json +12 -0
- package/dist/fixtures/forge-change/recordings/complete-critic/417c98d8e54a4387d0fd93441c49f44c2868958739070d855c1dce0c41e24db3.json +12 -0
- package/dist/fixtures/forge-change/recordings/complete-critic/50203ee22e9cb3ef047295edb6538fe7d5695e1e4b139f3c929013dc6ce47021.json +12 -0
- package/dist/fixtures/forge-change/recordings/complete-critic/89f18d67f5d9c325bf35e2a9e020f9ddb6ee13b8523315037f0905cc57c040b1.json +12 -0
- package/dist/fixtures/forge-change/recordings/complete-critic/b4097867d26ae8eadc8b9efa8cdd86897013358035f0b40086c299a59c4ba121.json +12 -0
- package/dist/fixtures/forge-change/recordings/safe-critic/302d06f49664dd67ad2e21ab9cf74c1b0f7d3f750709c6ed8eff2b70983274e2.json +12 -0
- package/dist/fixtures/forge-change/recordings/safe-critic/32f1049c9a5ab4a759543290db4644ec6e221cc45fdfdcf06193837c6a8ce0b9.json +12 -0
- package/dist/fixtures/forge-change/recordings/safe-critic/4e4a84bfc04b9f26ca4250319a879901ff7a1c081c71e46a117675fee05026b0.json +12 -0
- package/dist/fixtures/forge-change/recordings/safe-critic/604af0fcdd6df6b76208fa88305fdc15c37606c2a4d1ae816477e46ac8c2dd04.json +12 -0
- package/dist/fixtures/forge-change/recordings/safe-critic/f0bf50af11f14c50d07904f0dffe1c8c20fb0cd89770645a8824c1d30a443955.json +12 -0
- package/dist/fixtures/forge-change/recordings/ship-critic/350954b58833b2c04880c67db45a1b328b7fd541d9f0d943b63905b8875488a6.json +12 -0
- package/dist/fixtures/forge-change/recordings/ship-critic/4af95c6c96a206777cec9de1f9c2d8c55a36ddc9b8db2f81d6eb83217b835efa.json +12 -0
- package/dist/fixtures/forge-change/recordings/ship-critic/76c34522a49cf7587a4e5689a5030d384603fa780b8aba97d27815cbc829683e.json +12 -0
- package/dist/fixtures/forge-change/recordings/ship-critic/a3b2cd1ace48587e5994333db03b34c3a0f9dec4f1932f1730ddfb687f6918c8.json +12 -0
- package/dist/fixtures/forge-change/recordings/ship-critic/de288a4fa64d0fc20890f9a787080ce6eac5203b7b9b72d884df11e8cf111835.json +12 -0
- package/dist/fixtures/forge-change/recordings/story-writer/1ddb00eac2aaf91a5ec5d9dfd220116be9f75aec6546a4a3a3b050ed89af2864.json +12 -0
- package/dist/packages/assistant-kit/frameworks-kit/README.md +86 -0
- package/dist/packages/assistant-kit/frameworks-kit/checklist.md +11 -0
- package/dist/packages/assistant-kit/frameworks-kit/examples/tide-lantern.yaml +56 -0
- package/dist/packages/assistant-kit/frameworks-kit/examples/tide-platform.yaml +43 -0
- package/dist/packages/assistant-kit/frameworks-kit/prompts/write-framework-model.md +12 -0
- package/dist/packages/assistant-kit/kit/README.md +98 -0
- package/dist/packages/assistant-kit/kit/c2-reference.md +334 -0
- package/dist/packages/assistant-kit/kit/checklist.md +39 -0
- package/dist/packages/assistant-kit/kit/examples/house-http-client.yaml +102 -0
- package/dist/packages/assistant-kit/kit/examples/tide-apigee.yaml +56 -0
- package/dist/packages/assistant-kit/kit/examples/tide-apim.yaml +32 -0
- package/dist/packages/assistant-kit/kit/examples/tide-kong.yaml +44 -0
- package/dist/packages/assistant-kit/kit/prompts/write-dialect.md +34 -0
- package/dist/packages/assistant-kit/kit/worked-example.md +198 -0
- package/dist/packages/conformance/sample/README.md +4 -0
- package/dist/packages/conformance/sample/inputs/notes.md +3 -0
- package/dist/packages/conformance/sample/inputs/systems.csv +3 -0
- package/dist/packages/conformance/sample/repo/api/greeter.proto +6 -0
- package/dist/packages/conformance/sample/repo/api/openapi.yaml +10 -0
- package/dist/packages/conformance/sample/repo/api/schema.graphql +3 -0
- package/dist/packages/conformance/sample/repo/db/V1__create_greeting.sql +4 -0
- package/dist/packages/conformance/sample/repo/package.json +7 -0
- package/dist/packages/conformance/sample/repo/src/main/java/sample/HelloController.java +10 -0
- package/dist/packages/conformance/sample/repo/web/app.ts +4 -0
- package/dist/packages/evals/datasets/critic-lenses.yaml +62 -0
- package/dist/packages/evals/datasets/dialect-writer.yaml +14 -0
- package/dist/packages/evals/datasets/feature-namer.yaml +34 -0
- package/dist/packages/evals/datasets/glossary-definer.yaml +25 -0
- package/dist/packages/evals/datasets/story-writer.yaml +13 -0
- package/dist/packages/evals/datasets/template-writer.yaml +16 -0
- package/dist/packages/evals/datasets/test-writer.yaml +18 -0
- package/dist/packages/executors/reference/reference-executor.mjs +233 -0
- package/dist/packages/extractors/dialects/argocd.yaml +45 -0
- package/dist/packages/extractors/dialects/fixtures/jaxrs/accounts-service/src/main/java/forge/accounts/AccountResource.java +47 -0
- package/dist/packages/extractors/dialects/fixtures/jaxrs/accounts-service/src/main/java/forge/accounts/AuditClient.java +12 -0
- package/dist/packages/extractors/dialects/fixtures/jaxrs/accounts-service/src/main/java/forge/accounts/LedgerClient.java +17 -0
- package/dist/packages/extractors/dialects/fixtures/micronaut/statements-service/src/main/java/forge/statements/LedgerClient.java +14 -0
- package/dist/packages/extractors/dialects/fixtures/micronaut/statements-service/src/main/java/forge/statements/RatesClient.java +11 -0
- package/dist/packages/extractors/dialects/fixtures/micronaut/statements-service/src/main/java/forge/statements/StatementController.java +36 -0
- package/dist/packages/extractors/dialects/fixtures/micronaut/statements-service/src/main/resources/application.yml +7 -0
- package/dist/packages/extractors/dialects/fixtures/quarkus-reactive-messaging/fraud-service/src/main/java/forge/fraud/FraudScorer.java +27 -0
- package/dist/packages/extractors/dialects/fixtures/quarkus-reactive-messaging/fraud-service/src/main/resources/application.properties +4 -0
- package/dist/packages/extractors/dialects/fixtures/terraform-aws/forge-aws-infra/envs/prod/api.tf +38 -0
- package/dist/packages/extractors/dialects/fixtures/terraform-aws/forge-aws-infra/envs/prod/main.tf +95 -0
- package/dist/packages/extractors/dialects/fixtures/terraform-aws/forge-aws-infra/envs/prod/terraform.tfvars +1 -0
- package/dist/packages/extractors/dialects/fixtures/terraform-aws/forge-aws-infra/envs/prod/variables.tf +4 -0
- package/dist/packages/extractors/dialects/fixtures/terraform-azure/forge-azure-infra/main.tf +102 -0
- package/dist/packages/extractors/dialects/fixtures/terraform-gcp/forge-gcp-infra/main.tf +51 -0
- package/dist/packages/extractors/dialects/harness.yaml +101 -0
- package/dist/packages/extractors/dialects/jaxrs.yaml +155 -0
- package/dist/packages/extractors/dialects/micronaut.yaml +207 -0
- package/dist/packages/extractors/dialects/quarkus-reactive-messaging.yaml +158 -0
- package/dist/packages/extractors/dialects/tekton.yaml +71 -0
- package/dist/packages/extractors/dialects/terraform-aws.yaml +267 -0
- package/dist/packages/extractors/dialects/terraform-azure.yaml +212 -0
- package/dist/packages/extractors/dialects/terraform-gcp.yaml +109 -0
- package/dist/packages/extractors/grammars/README.md +11 -0
- package/dist/packages/extractors/grammars/tree-sitter-groovy.LICENSE +21 -0
- package/dist/packages/extractors/grammars/tree-sitter-groovy.wasm +0 -0
- package/dist/packages/extractors/grammars/vendored.json +11 -0
- package/dist/packages/learn/lessons/architect-diagrams-and-code.yaml +69 -0
- package/dist/packages/learn/lessons/business-analyst-story-bar.yaml +94 -0
- package/dist/packages/learn/lessons/delivery-lead-release-readiness.yaml +44 -0
- package/dist/packages/learn/lessons/designer-features-in-user-words.yaml +57 -0
- package/dist/packages/learn/lessons/developer-find-your-way.yaml +60 -0
- package/dist/packages/learn/lessons/everyone-first-hour.yaml +74 -0
- package/dist/packages/learn/lessons/platform-lead-coverage-and-defences.yaml +58 -0
- package/dist/packages/learn/lessons/product-owner-first-change.yaml +167 -0
- package/dist/packages/learn/lessons/qa-tests-and-trace.yaml +53 -0
- package/dist/packages/learn/lessons/team-lead-initiative.yaml +146 -0
- package/dist/packages/naysayer/constitution/constitution.yaml +286 -0
- package/dist/packages/naysayer/starter/review.yaml +256 -0
- package/dist/packages/sdk/npm/index.js +8892 -0
- package/dist/packages/speckit/rules/drift.yaml +78 -0
- package/dist/packages/taskrun/rules/conform.yaml +38 -0
- package/dist/packages/testgen/rules/trace.yaml +49 -0
- package/dist/packages/ui/dist/assets/elk-api-tma0MCuh.js +2 -0
- package/dist/packages/ui/dist/assets/elk-worker.min--dAJroGl.js +6401 -0
- package/dist/packages/ui/dist/assets/elk-worker.min-DdYmS5kP.js +2 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
- package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
- package/dist/packages/ui/dist/assets/index-B54fm9FQ.js +29 -0
- package/dist/packages/ui/dist/assets/index-DD_hufQz.css +1 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/packages/ui/dist/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-devanagari-600-normal-BtY48IV_.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-devanagari-600-normal-DhS7ScYx.woff2 +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-devanagari-700-normal-BQQOj9BB.woff2 +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-devanagari-700-normal-DBRwWuxA.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-600-normal-CXCVEoA9.woff2 +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-600-normal-Dx02qI2_.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-700-normal-BPDUZo87.woff2 +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-700-normal-e0Uj8ZlN.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-ext-600-normal-BleIgkPp.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-ext-600-normal-C9ZjJQAt.woff2 +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-ext-700-normal-BPaQ7tlD.woff +0 -0
- package/dist/packages/ui/dist/assets/rajdhani-latin-ext-700-normal-S6aYESQK.woff2 +0 -0
- package/dist/packages/ui/dist/favicon.svg +1 -0
- package/dist/packages/ui/dist/index.html +15 -0
- package/dist/third-party.json +575 -0
- package/dist/vendor/@fission-ai/openspec/bin/openspec.js +85986 -0
- package/dist/vendor/@fission-ai/openspec/package.json +82 -0
- package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/schema.yaml +248 -0
- package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/templates/design.md +21 -0
- package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/templates/proposal.md +34 -0
- package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/templates/spec.md +13 -0
- package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/templates/tasks.md +11 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-c_sharp.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-go.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-java.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-javascript.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-kotlin.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-php.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-python.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-ruby.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-rust.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-scala.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-tsx.wasm +0 -0
- package/dist/vendor/tree-sitter-wasms/out/tree-sitter-typescript.wasm +0 -0
- package/dist/vendor/web-tree-sitter/tree-sitter.wasm +0 -0
- package/package.json +36 -3
|
@@ -0,0 +1,1991 @@
|
|
|
1
|
+
import { createRequire as __atticusCreateRequire } from "node:module";
|
|
2
|
+
import { fileURLToPath as __atticusFileURLToPath } from "node:url";
|
|
3
|
+
var require = __atticusCreateRequire(import.meta.url);
|
|
4
|
+
var __atticusUrl = (p) => new URL("./" + p, import.meta.url).href;
|
|
5
|
+
var __atticusPath = (p) => __atticusFileURLToPath(__atticusUrl(p));
|
|
6
|
+
import {
|
|
7
|
+
addWorkingDays,
|
|
8
|
+
dayOf,
|
|
9
|
+
initiativeStatus,
|
|
10
|
+
laterOf,
|
|
11
|
+
leadTimeFor,
|
|
12
|
+
readPlan,
|
|
13
|
+
readView,
|
|
14
|
+
workingDays
|
|
15
|
+
} from "./chunk-ZUIAOKXI.js";
|
|
16
|
+
import {
|
|
17
|
+
traceMatrix
|
|
18
|
+
} from "./chunk-6SCVQZX5.js";
|
|
19
|
+
import {
|
|
20
|
+
SYNC_FILE
|
|
21
|
+
} from "./chunk-VPTTCYOC.js";
|
|
22
|
+
import {
|
|
23
|
+
contentLines,
|
|
24
|
+
docOf,
|
|
25
|
+
hopNotes,
|
|
26
|
+
hopRoutes,
|
|
27
|
+
normalizePath,
|
|
28
|
+
readChangeFiles,
|
|
29
|
+
readDataChanges,
|
|
30
|
+
readHops,
|
|
31
|
+
readRound,
|
|
32
|
+
readRoundMeta,
|
|
33
|
+
readSnowDrafts,
|
|
34
|
+
readStoryIndex,
|
|
35
|
+
readTasks,
|
|
36
|
+
reviewedGates,
|
|
37
|
+
roundsOf,
|
|
38
|
+
sectionLines,
|
|
39
|
+
sectionMatching,
|
|
40
|
+
tableWith
|
|
41
|
+
} from "./chunk-7UFQZXZA.js";
|
|
42
|
+
import {
|
|
43
|
+
changeFolder,
|
|
44
|
+
changeReport,
|
|
45
|
+
changeStore,
|
|
46
|
+
loadChangeContext
|
|
47
|
+
} from "./chunk-56YQ6ROQ.js";
|
|
48
|
+
import {
|
|
49
|
+
fileStepStore,
|
|
50
|
+
stepOfProvisional
|
|
51
|
+
} from "./chunk-7JUKDRSZ.js";
|
|
52
|
+
import {
|
|
53
|
+
readServiceRequestExports
|
|
54
|
+
} from "./chunk-23W7VWKR.js";
|
|
55
|
+
import {
|
|
56
|
+
readChangeRepos
|
|
57
|
+
} from "./chunk-KJPMEAI4.js";
|
|
58
|
+
import {
|
|
59
|
+
BREAKING_SPEC,
|
|
60
|
+
NAMES_SECRET_SPEC,
|
|
61
|
+
PERSONAL_DATA_SPEC,
|
|
62
|
+
UNDOES_SPEC,
|
|
63
|
+
personalItem,
|
|
64
|
+
settle,
|
|
65
|
+
tokenFormatIn,
|
|
66
|
+
verdictOf
|
|
67
|
+
} from "./chunk-CNUKCS2T.js";
|
|
68
|
+
import {
|
|
69
|
+
ChangeOutcomes,
|
|
70
|
+
WORKSPACE_SCHEMA_VERSION,
|
|
71
|
+
external_exports,
|
|
72
|
+
fileJudgments,
|
|
73
|
+
inputsDirOf,
|
|
74
|
+
readRunSummary,
|
|
75
|
+
require_dist,
|
|
76
|
+
toCanonicalYaml
|
|
77
|
+
} from "./chunk-S2A7YC3Q.js";
|
|
78
|
+
import {
|
|
79
|
+
__toESM
|
|
80
|
+
} from "./chunk-TV45EUGM.js";
|
|
81
|
+
|
|
82
|
+
// packages/release/dist/errors.js
|
|
83
|
+
var ReleaseError = class extends Error {
|
|
84
|
+
code;
|
|
85
|
+
constructor(message, code = "invalid") {
|
|
86
|
+
super(message);
|
|
87
|
+
this.code = code;
|
|
88
|
+
this.name = "ReleaseError";
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// packages/release/dist/context.js
|
|
93
|
+
var import_yaml = __toESM(require_dist(), 1);
|
|
94
|
+
import { readFile } from "node:fs/promises";
|
|
95
|
+
import { join } from "node:path";
|
|
96
|
+
var REQUESTS_FILE = "snow.md";
|
|
97
|
+
var SyncEntries = external_exports.object({
|
|
98
|
+
requests: external_exports.array(external_exports.object({ key: external_exports.string(), id: external_exports.string() })).default([])
|
|
99
|
+
});
|
|
100
|
+
async function readServiceRequests(dir, files, templates, steps, exported) {
|
|
101
|
+
const drafts = readSnowDrafts(files);
|
|
102
|
+
if (drafts.length === 0)
|
|
103
|
+
return [];
|
|
104
|
+
let synced = [];
|
|
105
|
+
try {
|
|
106
|
+
synced = SyncEntries.parse((0, import_yaml.parse)(await readFile(join(dir, SYNC_FILE), "utf8"))).requests;
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
return drafts.map((d) => {
|
|
110
|
+
const h = d.heading.toLowerCase();
|
|
111
|
+
const t = templates.find((x) => [x.requestType, x.name, x.id].some((n) => h.includes(n.toLowerCase())));
|
|
112
|
+
const fields = t ? t.fields.map((f) => ({
|
|
113
|
+
name: f.name,
|
|
114
|
+
value: d.fields.get(f.name.toLowerCase())?.value ?? "",
|
|
115
|
+
required: f.required
|
|
116
|
+
})) : [...d.fields].map(([name, v2]) => ({ name, value: v2.value, required: false }));
|
|
117
|
+
const fromHeading = /:\s*(\S+)\s*$/.exec(d.heading)?.[1] ?? "";
|
|
118
|
+
const recordField = t?.fields.find((f) => f.from === "${record}");
|
|
119
|
+
const record = fromHeading || (recordField ? d.fields.get(recordField.name.toLowerCase())?.value ?? "" : "");
|
|
120
|
+
const unblocks = [...d.fields].filter(([k]) => /^unblocks?\b/.test(k)).flatMap(([, v2]) => v2.value.match(/\d+(?:\.\d+)+|\b\d+\b/g) ?? []);
|
|
121
|
+
const lead = t ? leadTimeFor({ id: t.id, kind: "request", template: t.id, days: t.leadTimeDays }, void 0, { templates, requests: exported, journey: "change" }) : void 0;
|
|
122
|
+
const base = {
|
|
123
|
+
heading: d.heading,
|
|
124
|
+
line: d.line,
|
|
125
|
+
...t ? { template: t.id } : {},
|
|
126
|
+
requestType: t?.requestType ?? d.heading,
|
|
127
|
+
record,
|
|
128
|
+
...lead ? { leadTimeDays: lead.days, leadTimeSource: lead.source } : {},
|
|
129
|
+
approvers: t?.approvers ?? [],
|
|
130
|
+
fields,
|
|
131
|
+
missing: fields.filter((f) => f.required && !f.value.trim()).map((f) => f.name),
|
|
132
|
+
unblocks
|
|
133
|
+
};
|
|
134
|
+
const entry = t ? synced.find((e) => e.key === `${t.id}|${fromHeading}`) : void 0;
|
|
135
|
+
if (!entry)
|
|
136
|
+
return { ...base, state: "to-file" };
|
|
137
|
+
const stepId = stepOfProvisional(entry.id);
|
|
138
|
+
if (!stepId)
|
|
139
|
+
return { ...base, state: "filed", number: entry.id };
|
|
140
|
+
const s = steps.find((x) => x.id === stepId);
|
|
141
|
+
const v = s?.state === "done" ? s.recorded?.values : void 0;
|
|
142
|
+
return typeof v?.requestNumber === "string" ? {
|
|
143
|
+
...base,
|
|
144
|
+
state: "filed",
|
|
145
|
+
step: stepId,
|
|
146
|
+
number: v.requestNumber,
|
|
147
|
+
...typeof v.url === "string" ? { url: v.url } : {},
|
|
148
|
+
...s?.recorded ? { filedAt: s.recorded.at } : {}
|
|
149
|
+
} : { ...base, state: "waiting", step: stepId };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function readRounds(root, change) {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const gate of await reviewedGates(root, change))
|
|
155
|
+
for (const round2 of await roundsOf(root, change, gate)) {
|
|
156
|
+
const meta = await readRoundMeta(root, change, gate, round2);
|
|
157
|
+
const r = await readRound(root, change, gate, round2);
|
|
158
|
+
out.push({ gate, round: round2, ...meta ? { meta } : {}, findings: r.findings });
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
var iso = (d) => d.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
163
|
+
async function loadRelease(root, changeId, o = {}) {
|
|
164
|
+
const now = typeof o.now === "string" ? o.now : iso(o.now ?? /* @__PURE__ */ new Date());
|
|
165
|
+
let change;
|
|
166
|
+
try {
|
|
167
|
+
change = await changeStore(root).read(changeId);
|
|
168
|
+
} catch {
|
|
169
|
+
throw new ReleaseError(`There is no change ${changeId}.`, "not-found");
|
|
170
|
+
}
|
|
171
|
+
const dir = changeFolder(root, change.id);
|
|
172
|
+
const [ctx, files] = await Promise.all([loadChangeContext(root), readChangeFiles(dir)]);
|
|
173
|
+
const problems = [...ctx.problems];
|
|
174
|
+
const records = ctx.records;
|
|
175
|
+
const key = (k, id) => `${k}|${id}`;
|
|
176
|
+
const byKey = new Map(records.map((r) => [key(r.kind, r.id), r]));
|
|
177
|
+
const repos = ctx.orgPack.profiles.flatMap((p) => p.repos?.repos ?? []).map((r) => ({
|
|
178
|
+
id: r.id,
|
|
179
|
+
type: r.type,
|
|
180
|
+
owner: r.owner,
|
|
181
|
+
...r.component ? { component: r.component } : {},
|
|
182
|
+
...r.system ? { system: r.system } : {}
|
|
183
|
+
})).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
184
|
+
let changeRepos;
|
|
185
|
+
try {
|
|
186
|
+
changeRepos = await readChangeRepos(root, change.id);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
problems.push(e.message);
|
|
189
|
+
}
|
|
190
|
+
let steps = [];
|
|
191
|
+
try {
|
|
192
|
+
steps = await fileStepStore(root).list({ change: change.id });
|
|
193
|
+
} catch (e) {
|
|
194
|
+
problems.push(`A step file can't be read: ${e.message}`);
|
|
195
|
+
}
|
|
196
|
+
const exported = await readServiceRequestExports({ root, inputsDir: inputsDirOf(root) });
|
|
197
|
+
const requests = await readServiceRequests(dir, files, ctx.orgPack.serviceRequests, steps, exported.requests);
|
|
198
|
+
const rounds = await readRounds(root, change.id);
|
|
199
|
+
const latest = [];
|
|
200
|
+
for (const gate of [...new Set(rounds.map((r) => r.gate))]) {
|
|
201
|
+
const last = rounds.filter((r) => r.gate === gate).at(-1);
|
|
202
|
+
if (last)
|
|
203
|
+
latest.push(...last.findings);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
root,
|
|
207
|
+
now,
|
|
208
|
+
today: dayOf(now),
|
|
209
|
+
change,
|
|
210
|
+
dir,
|
|
211
|
+
files,
|
|
212
|
+
orgPack: ctx.orgPack,
|
|
213
|
+
policy: ctx.policy,
|
|
214
|
+
records,
|
|
215
|
+
get: (kind, id) => byKey.get(key(kind, id)),
|
|
216
|
+
all: (kind) => records.filter((r) => r.kind === kind),
|
|
217
|
+
repos,
|
|
218
|
+
...changeRepos ? { changeRepos } : {},
|
|
219
|
+
steps,
|
|
220
|
+
requests,
|
|
221
|
+
rounds,
|
|
222
|
+
latest,
|
|
223
|
+
exportedRequests: exported.requests,
|
|
224
|
+
problems,
|
|
225
|
+
judging: {
|
|
226
|
+
store: o.judgments ?? fileJudgments(root),
|
|
227
|
+
...o.runtime ? { runtime: o.runtime } : {}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// packages/release/dist/judged.js
|
|
233
|
+
function breakingItem(h, notes) {
|
|
234
|
+
return {
|
|
235
|
+
inputs: {
|
|
236
|
+
v: 1,
|
|
237
|
+
change: h.change,
|
|
238
|
+
operation: h.operation,
|
|
239
|
+
caller: h.caller,
|
|
240
|
+
callee: h.callee,
|
|
241
|
+
notes
|
|
242
|
+
},
|
|
243
|
+
texts: { notes },
|
|
244
|
+
facts: [
|
|
245
|
+
`operation: ${h.operation}`,
|
|
246
|
+
`caller: ${h.caller}`,
|
|
247
|
+
`callee: ${h.callee}`,
|
|
248
|
+
`change: ${h.change.replace(/^planned-/, "")}`
|
|
249
|
+
]
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function undoesItem(sentence, step, others) {
|
|
253
|
+
return {
|
|
254
|
+
inputs: {
|
|
255
|
+
v: 1,
|
|
256
|
+
sentence,
|
|
257
|
+
step: step.key,
|
|
258
|
+
names: [...new Set(step.names)].sort(),
|
|
259
|
+
others: [...new Set(others.filter((n) => !step.names.includes(n)))].sort()
|
|
260
|
+
},
|
|
261
|
+
texts: { sentence },
|
|
262
|
+
facts: [
|
|
263
|
+
`the step: ${step.kind} in ${step.repo}`,
|
|
264
|
+
...step.what.map((w) => `it deploys: ${w}`),
|
|
265
|
+
`its names: ${[...new Set(step.names)].join(", ")}`
|
|
266
|
+
]
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function secretItem(line) {
|
|
270
|
+
return { inputs: { v: 1, line }, texts: { line } };
|
|
271
|
+
}
|
|
272
|
+
async function judgedFor(ctx, spec, items) {
|
|
273
|
+
const results = items.length ? await settle(spec, items, {
|
|
274
|
+
store: ctx.judging.store,
|
|
275
|
+
runtime: ctx.judging.runtime,
|
|
276
|
+
change: ctx.change.id,
|
|
277
|
+
now: new Date(ctx.now)
|
|
278
|
+
}) : /* @__PURE__ */ new Map();
|
|
279
|
+
return (item) => verdictOf(results, spec, item.inputs);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// packages/release/dist/model.js
|
|
283
|
+
var cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
284
|
+
var sortStrings = (xs) => [...new Set(xs)].sort(cmp);
|
|
285
|
+
var EVENT = /^(publish|subscribe|emit|event|consume)$/i;
|
|
286
|
+
function buildModel(ctx) {
|
|
287
|
+
const profileSystems = new Map(ctx.orgPack.profiles.flatMap((p) => p.profile?.systems ?? []).map((s) => [s.id, s]));
|
|
288
|
+
const repoOfComponent = (id) => ctx.get("Component", id)?.repo ?? ctx.repos.find((r) => (r.component ?? r.id) === id)?.id ?? ctx.repos.find((r) => r.id === id)?.id;
|
|
289
|
+
const componentsOfRepo = (repo) => {
|
|
290
|
+
const r = ctx.repos.find((x) => x.id === repo);
|
|
291
|
+
return sortStrings([
|
|
292
|
+
...r?.component ? [r.component] : [],
|
|
293
|
+
...ctx.all("Component").filter((c) => c.repo === repo).map((c) => c.id)
|
|
294
|
+
]);
|
|
295
|
+
};
|
|
296
|
+
const systemOf = (id) => {
|
|
297
|
+
const c = ctx.get("Component", id);
|
|
298
|
+
if (c)
|
|
299
|
+
return c.system;
|
|
300
|
+
if (ctx.get("System", id) || profileSystems.has(id))
|
|
301
|
+
return id;
|
|
302
|
+
const t = ctx.get("Table", id);
|
|
303
|
+
if (t?.ownerComponent)
|
|
304
|
+
return systemOf(t.ownerComponent);
|
|
305
|
+
const s = ctx.get("DataStore", id);
|
|
306
|
+
if (s?.ownerComponent)
|
|
307
|
+
return systemOf(s.ownerComponent);
|
|
308
|
+
return ctx.repos.find((r) => (r.component ?? r.id) === id)?.system;
|
|
309
|
+
};
|
|
310
|
+
const boundaryOf = (s) => s ? ctx.get("System", s)?.boundary ?? profileSystems.get(s)?.boundary : void 0;
|
|
311
|
+
const systemName = (s) => ctx.get("System", s)?.name ?? profileSystems.get(s)?.name ?? s;
|
|
312
|
+
const evidenceRepo = (records, src) => sortStrings(records.flatMap((r) => r.evidence.filter((e) => e.source === src && e.repo).map((e) => e.repo)))[0];
|
|
313
|
+
const gatewayRepo = evidenceRepo(ctx.all("GatewayRoute"), "gateway") ?? ctx.repos.find((r) => /gateway/i.test(r.type))?.id;
|
|
314
|
+
const topics = ctx.all("Endpoint").filter((e) => e.endpointType === "event-topic");
|
|
315
|
+
const eventsRepo = evidenceRepo(topics, "event-config") ?? ctx.repos.find((r) => /event/i.test(r.type))?.id;
|
|
316
|
+
const storeOwner = (store) => {
|
|
317
|
+
const component = ctx.get("DataStore", store)?.ownerComponent;
|
|
318
|
+
const repo = component ? repoOfComponent(component) : void 0;
|
|
319
|
+
return { ...component ? { component } : {}, ...repo ? { repo } : {} };
|
|
320
|
+
};
|
|
321
|
+
const touched = new Map(ctx.change.touches.interfaces.map((t) => [t.id, t.status]));
|
|
322
|
+
const hops = readHops(ctx.files).map((h) => {
|
|
323
|
+
const record = h.id && h.id !== "new" ? ctx.get("Interface", h.id) : void 0;
|
|
324
|
+
const routes = sortStrings([
|
|
325
|
+
...record?.route ? [record.route] : [],
|
|
326
|
+
...hopRoutes(ctx.files, h)
|
|
327
|
+
]);
|
|
328
|
+
const isEvent = ["event-bus", "pubsub", "mq"].includes(record?.via ?? "") || /^A\./.test(h.pattern) || EVENT.test(h.method ?? "");
|
|
329
|
+
const topic = isEvent ? record?.normalizedPath ?? (EVENT.test(h.method ?? "") ? h.path : void 0) : void 0;
|
|
330
|
+
const callerSystem = systemOf(h.caller);
|
|
331
|
+
const calleeSystem = systemOf(h.callee);
|
|
332
|
+
const callerRepo = repoOfComponent(h.caller);
|
|
333
|
+
const calleeRepo = repoOfComponent(h.callee);
|
|
334
|
+
const callerBoundary = boundaryOf(callerSystem);
|
|
335
|
+
const calleeBoundary = boundaryOf(calleeSystem);
|
|
336
|
+
return {
|
|
337
|
+
...h,
|
|
338
|
+
change: /^planned-/.test(h.status) ? h.status : touched.get(h.id) ?? h.status,
|
|
339
|
+
...record ? { record } : {},
|
|
340
|
+
routes,
|
|
341
|
+
newRoutes: routes.filter((r) => !ctx.get("GatewayRoute", r)),
|
|
342
|
+
...callerRepo ? { callerRepo } : {},
|
|
343
|
+
...calleeRepo ? { calleeRepo } : {},
|
|
344
|
+
...callerSystem ? { callerSystem } : {},
|
|
345
|
+
...calleeSystem ? { calleeSystem } : {},
|
|
346
|
+
...callerBoundary ? { callerBoundary } : {},
|
|
347
|
+
...calleeBoundary ? { calleeBoundary } : {},
|
|
348
|
+
...topic ? { topic } : {},
|
|
349
|
+
newTopic: !!topic && !topics.some((t) => t.pathOrTopic === topic),
|
|
350
|
+
notes: hopNotes(ctx.files, h)
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
return {
|
|
354
|
+
hops,
|
|
355
|
+
data: readDataChanges(ctx.files),
|
|
356
|
+
repoOfComponent,
|
|
357
|
+
componentsOfRepo,
|
|
358
|
+
systemOf,
|
|
359
|
+
boundaryOf,
|
|
360
|
+
systemName,
|
|
361
|
+
...gatewayRepo ? { gatewayRepo } : {},
|
|
362
|
+
...eventsRepo ? { eventsRepo } : {},
|
|
363
|
+
storeOwner
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
var opOf = (h) => h.operation || (h.path ?? "");
|
|
367
|
+
function sectionOf(ctx, file, re) {
|
|
368
|
+
const doc = docOf(ctx.files, file);
|
|
369
|
+
const s = doc?.sections.find((x) => re.test(x.heading));
|
|
370
|
+
if (!doc || !s)
|
|
371
|
+
return void 0;
|
|
372
|
+
const lines2 = sectionLines(doc, s);
|
|
373
|
+
return {
|
|
374
|
+
heading: s.heading,
|
|
375
|
+
source: `${file}:${s.line}`,
|
|
376
|
+
text: lines2.map((l) => l.text).join("\n").trim(),
|
|
377
|
+
lines: lines2
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function sectionsOf(ctx, file, re) {
|
|
381
|
+
const doc = docOf(ctx.files, file);
|
|
382
|
+
if (!doc)
|
|
383
|
+
return [];
|
|
384
|
+
return doc.sections.filter((x) => re.test(x.heading)).map((s) => {
|
|
385
|
+
const lines2 = sectionLines(doc, s);
|
|
386
|
+
return {
|
|
387
|
+
heading: s.heading,
|
|
388
|
+
source: `${file}:${s.line}`,
|
|
389
|
+
text: lines2.map((l) => l.text).join("\n").trim(),
|
|
390
|
+
lines: lines2
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function sentences(lines2) {
|
|
395
|
+
const out = [];
|
|
396
|
+
let cur = "";
|
|
397
|
+
let start = 0;
|
|
398
|
+
const flush = () => {
|
|
399
|
+
const t = cur.trim();
|
|
400
|
+
if (t)
|
|
401
|
+
out.push({ n: start, text: t });
|
|
402
|
+
cur = "";
|
|
403
|
+
};
|
|
404
|
+
for (const l of lines2) {
|
|
405
|
+
const text = l.text.trim();
|
|
406
|
+
if (/^([-*+]|\d+\.)\s+/.test(text) || text.startsWith("|")) {
|
|
407
|
+
flush();
|
|
408
|
+
start = l.n;
|
|
409
|
+
cur = text.replace(/^([-*+]|\d+\.)\s+/, "");
|
|
410
|
+
flush();
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
for (const part of text.split(/(?<=[.!?])\s+/)) {
|
|
414
|
+
if (!cur)
|
|
415
|
+
start = l.n;
|
|
416
|
+
cur = cur ? `${cur} ${part}` : part;
|
|
417
|
+
if (/[.!?]$/.test(part))
|
|
418
|
+
flush();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
flush();
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
function hasWord(text, word) {
|
|
425
|
+
if (!word)
|
|
426
|
+
return false;
|
|
427
|
+
const esc3 = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
428
|
+
return new RegExp(`(^|[^A-Za-z0-9_-])${esc3}($|[^A-Za-z0-9_-])`, "i").test(text);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// packages/release/dist/plan.js
|
|
432
|
+
var RANK = { migration: 0, topic: 1, "gateway-route": 2, code: 3 };
|
|
433
|
+
var cmp2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
434
|
+
function requestWait(ctx, r, why) {
|
|
435
|
+
const base = {
|
|
436
|
+
heading: r.heading,
|
|
437
|
+
...r.template ? { template: r.template } : {},
|
|
438
|
+
requestType: r.requestType,
|
|
439
|
+
...r.record ? { record: r.record } : {},
|
|
440
|
+
state: r.state,
|
|
441
|
+
...r.number ? { number: r.number } : {},
|
|
442
|
+
missing: r.missing,
|
|
443
|
+
source: `${REQUESTS_FILE}:${r.line}`,
|
|
444
|
+
why
|
|
445
|
+
};
|
|
446
|
+
if (r.leadTimeDays === void 0)
|
|
447
|
+
return base;
|
|
448
|
+
const from = r.state === "filed" && r.filedAt ? dayOf(r.filedAt) : ctx.today;
|
|
449
|
+
const due = addWorkingDays(from, Math.ceil(r.leadTimeDays));
|
|
450
|
+
const readyBy = laterOf(due, ctx.today);
|
|
451
|
+
return {
|
|
452
|
+
...base,
|
|
453
|
+
leadTimeDays: r.leadTimeDays,
|
|
454
|
+
...r.leadTimeSource ? { leadTimeSource: r.leadTimeSource } : {},
|
|
455
|
+
waitDays: workingDays(ctx.today, readyBy),
|
|
456
|
+
readyBy
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
var SECTIONS = [
|
|
460
|
+
{
|
|
461
|
+
name: "rollout",
|
|
462
|
+
re: /roll-?out|release plan|deploy|cut-?over/i,
|
|
463
|
+
gap: 'design.md has no rollout section: add "Rollout and rollback" with the order and any feature flags.'
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: "rollback",
|
|
467
|
+
re: /roll-?back|back-?out/i,
|
|
468
|
+
gap: "design.md says nothing about rolling back: say how each step is undone, and when to decide."
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: "readiness",
|
|
472
|
+
re: /readiness|ready|pre-?release|go-?live|operational/i,
|
|
473
|
+
gap: "design.md has no readiness section: say what must be true before go-live (runbooks, on-call, alerts, sign-offs)."
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "verification",
|
|
477
|
+
re: /verif|smoke|post-?(deploy|release)|validation/i,
|
|
478
|
+
gap: "design.md has no verification section: say how the team checks each step worked in production."
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: "hypercare",
|
|
482
|
+
re: /hyper-?care|early life|warranty|aftercare|monitoring after/i,
|
|
483
|
+
gap: "design.md has no hypercare section: say who watches what, and for how long, after release."
|
|
484
|
+
}
|
|
485
|
+
];
|
|
486
|
+
async function deploymentPlan(ctx) {
|
|
487
|
+
const m = buildModel(ctx);
|
|
488
|
+
const gaps = [];
|
|
489
|
+
const notes = [];
|
|
490
|
+
const units = /* @__PURE__ */ new Map();
|
|
491
|
+
const edges = [];
|
|
492
|
+
const unplaced = /* @__PURE__ */ new Map();
|
|
493
|
+
const external = [];
|
|
494
|
+
const externalCaller = /* @__PURE__ */ new Map();
|
|
495
|
+
const tasks = readTasks(ctx.files);
|
|
496
|
+
const stories = readStoryIndex(ctx.files);
|
|
497
|
+
const unit = (key, kind, repo) => {
|
|
498
|
+
let u = units.get(key);
|
|
499
|
+
if (!u) {
|
|
500
|
+
u = { key, kind, repo, what: [], sources: /* @__PURE__ */ new Set(), names: [repo], tasks: [] };
|
|
501
|
+
units.set(key, u);
|
|
502
|
+
}
|
|
503
|
+
return u;
|
|
504
|
+
};
|
|
505
|
+
const codeUnit = (repo) => {
|
|
506
|
+
const u = unit(`code:${repo}`, "code", repo);
|
|
507
|
+
for (const c of m.componentsOfRepo(repo))
|
|
508
|
+
if (!u.names.includes(c))
|
|
509
|
+
u.names.push(c);
|
|
510
|
+
return u;
|
|
511
|
+
};
|
|
512
|
+
const addWhat = (u, w, source) => {
|
|
513
|
+
if (!u.what.includes(w))
|
|
514
|
+
u.what.push(w);
|
|
515
|
+
u.sources.add(source);
|
|
516
|
+
};
|
|
517
|
+
for (const d of m.data) {
|
|
518
|
+
const owner = m.storeOwner(d.store);
|
|
519
|
+
if (!owner.repo) {
|
|
520
|
+
unplaced.set(d.store, `${d.store} has no owning component with a repo in the catalog, so its migration can't be placed.`);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const u = unit(`migration:${d.store}`, "migration", owner.repo);
|
|
524
|
+
addWhat(u, `${d.store}.${d.table}: ${d.change.trim() || "change"} (${d.migration || "no migration file named"})`, `data.md:${d.line}`);
|
|
525
|
+
for (const n of [d.migration, d.table, d.tableId, d.store].filter(Boolean))
|
|
526
|
+
if (!u.names.includes(n))
|
|
527
|
+
u.names.push(n);
|
|
528
|
+
if (!d.migration)
|
|
529
|
+
gaps.push(`data.md:${d.line}: the change to ${d.store}.${d.table} names no migration file, so the migration step can't say what runs.`);
|
|
530
|
+
const t = ctx.get("Table", d.tableId);
|
|
531
|
+
const users = sortStrings([
|
|
532
|
+
...t?.readers ?? [],
|
|
533
|
+
...t?.writers ?? [],
|
|
534
|
+
...owner.component ? [owner.component] : []
|
|
535
|
+
]);
|
|
536
|
+
for (const c of users) {
|
|
537
|
+
const repo = m.repoOfComponent(c);
|
|
538
|
+
if (!repo)
|
|
539
|
+
continue;
|
|
540
|
+
codeUnit(repo);
|
|
541
|
+
edges.push({
|
|
542
|
+
from: u.key,
|
|
543
|
+
to: `code:${repo}`,
|
|
544
|
+
why: `${c} reads or writes ${d.store}.${d.table}, so the migration (expand) runs first`
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
for (const h of m.hops) {
|
|
549
|
+
const live = /^planned-(new|modify)$/.test(h.change);
|
|
550
|
+
const op = opOf(h);
|
|
551
|
+
const src = `interfaces.md:${h.line}`;
|
|
552
|
+
const callerInternal = h.callerBoundary !== "vendor";
|
|
553
|
+
const calleeInternal = h.calleeBoundary !== "vendor";
|
|
554
|
+
const verb = h.change === "planned-new" ? "new" : h.change === "planned-modify" ? "changed" : h.change;
|
|
555
|
+
if (live && callerInternal) {
|
|
556
|
+
if (h.callerRepo)
|
|
557
|
+
addWhat(codeUnit(h.callerRepo), `${h.caller} calls ${op} on ${h.callee} (${verb})`, src);
|
|
558
|
+
else
|
|
559
|
+
unplaced.set(h.caller, placeWhy(ctx, h.caller, `caller of ${op}`));
|
|
560
|
+
}
|
|
561
|
+
if (live && calleeInternal) {
|
|
562
|
+
if (h.calleeRepo)
|
|
563
|
+
addWhat(codeUnit(h.calleeRepo), `${h.callee} serves ${op} (${verb})`, src);
|
|
564
|
+
else
|
|
565
|
+
unplaced.set(h.callee, placeWhy(ctx, h.callee, `callee of ${op}`));
|
|
566
|
+
}
|
|
567
|
+
if (live && !calleeInternal && h.calleeSystem) {
|
|
568
|
+
const what = `${op} must be live on ${m.systemName(h.calleeSystem)}'s side (a vendor: nothing of theirs deploys here)`;
|
|
569
|
+
external.push({ system: h.calleeSystem, name: m.systemName(h.calleeSystem), what });
|
|
570
|
+
if (h.callerRepo)
|
|
571
|
+
externalCaller.set(what, `code:${h.callerRepo}`);
|
|
572
|
+
}
|
|
573
|
+
for (const r of h.newRoutes) {
|
|
574
|
+
if (!m.gatewayRepo) {
|
|
575
|
+
unplaced.set(r, `${r} is a new gateway route but no repo holds gateway proxies, so Atticus can't place it.`);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const u = unit(`route:${r}`, "gateway-route", m.gatewayRepo);
|
|
579
|
+
addWhat(u, `New gateway route ${r} for ${op}`, src);
|
|
580
|
+
if (!u.names.includes(r))
|
|
581
|
+
u.names.push(r);
|
|
582
|
+
if (h.callerRepo && callerInternal)
|
|
583
|
+
edges.push({
|
|
584
|
+
from: u.key,
|
|
585
|
+
to: `code:${h.callerRepo}`,
|
|
586
|
+
why: `${h.caller} calls ${op} through ${r}, so the route goes first`
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
if (h.topic && h.newTopic && live) {
|
|
590
|
+
if (!m.eventsRepo) {
|
|
591
|
+
unplaced.set(h.topic, `${h.topic} is a new topic but no repo holds topic definitions, so Atticus can't place it.`);
|
|
592
|
+
} else {
|
|
593
|
+
const u = unit(`topic:${h.topic}`, "topic", m.eventsRepo);
|
|
594
|
+
addWhat(u, `New topic ${h.topic}`, src);
|
|
595
|
+
if (!u.names.includes(h.topic))
|
|
596
|
+
u.names.push(h.topic);
|
|
597
|
+
if (h.callerRepo)
|
|
598
|
+
edges.push({
|
|
599
|
+
from: u.key,
|
|
600
|
+
to: `code:${h.callerRepo}`,
|
|
601
|
+
why: `${h.caller} publishes to ${h.topic}, so the topic goes first`
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (live && h.callerRepo && h.calleeRepo && h.callerRepo !== h.calleeRepo && callerInternal && calleeInternal)
|
|
606
|
+
edges.push({
|
|
607
|
+
from: `code:${h.calleeRepo}`,
|
|
608
|
+
to: `code:${h.callerRepo}`,
|
|
609
|
+
why: `${h.caller} calls ${h.callee}'s ${verb} ${op}, so ${h.callee} deploys first`
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
for (const t of tasks) {
|
|
613
|
+
if (!ctx.repos.some((r) => r.id === t.repo) && !m.componentsOfRepo(t.repo).length)
|
|
614
|
+
continue;
|
|
615
|
+
const u = codeUnit(t.repo);
|
|
616
|
+
const num = /\[[ xX]?\]\s*(\d+(?:\.\d+)*)/.exec(t.text)?.[1];
|
|
617
|
+
if (num && !u.tasks.includes(num))
|
|
618
|
+
u.tasks.push(num);
|
|
619
|
+
addWhat(u, `Task ${t.text.replace(/^\s*[-*]\s+\[[ xX]?\]\s*/, "")}`, `tasks.md:${t.line}`);
|
|
620
|
+
}
|
|
621
|
+
for (const s of stories)
|
|
622
|
+
for (const repo of s.repos) {
|
|
623
|
+
if (!ctx.repos.some((r) => r.id === repo))
|
|
624
|
+
continue;
|
|
625
|
+
addWhat(codeUnit(repo), `Story ${s.id}`, `stories.md:${s.line}`);
|
|
626
|
+
}
|
|
627
|
+
for (const repo of [
|
|
628
|
+
...ctx.changeRepos?.repos.map((r) => r.id) ?? [],
|
|
629
|
+
...ctx.change.touches.repos
|
|
630
|
+
])
|
|
631
|
+
if (!units.has(`code:${repo}`))
|
|
632
|
+
addWhat(codeUnit(repo), "Named in the change's repos", "repos.yaml");
|
|
633
|
+
for (const s of stories)
|
|
634
|
+
for (const dep of s.dependsOn) {
|
|
635
|
+
const d = stories.find((x) => x.id === dep);
|
|
636
|
+
if (!d)
|
|
637
|
+
continue;
|
|
638
|
+
for (const a of d.repos)
|
|
639
|
+
for (const b of s.repos)
|
|
640
|
+
if (a !== b && units.has(`code:${a}`) && units.has(`code:${b}`))
|
|
641
|
+
edges.push({
|
|
642
|
+
from: `code:${a}`,
|
|
643
|
+
to: `code:${b}`,
|
|
644
|
+
why: `story ${s.id} depends on ${d.id}`
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
const keys = [...units.keys()];
|
|
648
|
+
const preds = new Map(keys.map((k) => [k, []]));
|
|
649
|
+
for (const e of edges)
|
|
650
|
+
if (units.has(e.from) && units.has(e.to) && e.from !== e.to && !preds.get(e.to).some((x) => x.from === e.from && x.why === e.why))
|
|
651
|
+
preds.get(e.to).push(e);
|
|
652
|
+
const prio = (k) => {
|
|
653
|
+
const u = units.get(k);
|
|
654
|
+
return [RANK[u.kind], u.repo, k];
|
|
655
|
+
};
|
|
656
|
+
const byPrio = (a, b) => {
|
|
657
|
+
const [ra, pa, ka] = prio(a);
|
|
658
|
+
const [rb, pb, kb] = prio(b);
|
|
659
|
+
return ra !== rb ? ra - rb : pa !== pb ? cmp2(pa, pb) : cmp2(ka, kb);
|
|
660
|
+
};
|
|
661
|
+
const order = [];
|
|
662
|
+
const left = new Set(keys);
|
|
663
|
+
while (left.size) {
|
|
664
|
+
const ready = [...left].filter((k) => preds.get(k).every((e) => !left.has(e.from))).sort(byPrio);
|
|
665
|
+
if (ready.length) {
|
|
666
|
+
order.push(ready[0]);
|
|
667
|
+
left.delete(ready[0]);
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const next = [...left].sort(byPrio)[0];
|
|
671
|
+
const stuck = preds.get(next).filter((e) => left.has(e.from)).map((e) => units.get(e.from).repo);
|
|
672
|
+
gaps.push(`${units.get(next).repo} and ${sortStrings(stuck).join(", ")} depend on each other, so no order satisfies both; agree one and write it in design.md.`);
|
|
673
|
+
order.push(next);
|
|
674
|
+
left.delete(next);
|
|
675
|
+
}
|
|
676
|
+
const numOf = new Map(order.map((k, i) => [k, i + 1]));
|
|
677
|
+
const rollbackLines = [
|
|
678
|
+
...sectionsOf(ctx, "design.md", /roll|release|deploy|back-?out/i).flatMap((s) => sentences(s.lines).map((x) => ({ ...x, file: "design.md" }))),
|
|
679
|
+
...sectionsOf(ctx, "data.md", /.*/).flatMap((s) => sentences(s.lines).map((x) => ({ ...x, file: "data.md" })))
|
|
680
|
+
].filter((x) => !/^\|[\s|:-]*\|?$/.test(x.text));
|
|
681
|
+
const allNames = [...units.values()].flatMap((u) => u.names);
|
|
682
|
+
const pairs = [...units.values()].flatMap((u) => rollbackLines.map((x) => ({
|
|
683
|
+
u,
|
|
684
|
+
x,
|
|
685
|
+
item: undoesItem(x.text, { key: u.key, kind: u.kind, repo: u.repo, what: u.what, names: u.names }, allNames)
|
|
686
|
+
})));
|
|
687
|
+
const undoes = await judgedFor(ctx, UNDOES_SPEC, pairs.map((p) => p.item));
|
|
688
|
+
const labelOf = (u) => u.kind === "code" ? u.repo : u.what[0] ?? u.repo;
|
|
689
|
+
const rollbackOf = (u) => {
|
|
690
|
+
const mine = pairs.filter((p) => p.u === u);
|
|
691
|
+
const fit = mine.filter((p) => undoes(p.item)?.verdict === "yes").map((p) => p.x);
|
|
692
|
+
if (fit.length > 0)
|
|
693
|
+
return {
|
|
694
|
+
text: fit.map((x) => x.text).join(" "),
|
|
695
|
+
source: sortStrings(fit.map((x) => `${x.file}:${x.n}`)).join(", ")
|
|
696
|
+
};
|
|
697
|
+
const open = mine.filter((p) => {
|
|
698
|
+
const v = undoes(p.item)?.verdict;
|
|
699
|
+
return v === void 0 || v === "unsure";
|
|
700
|
+
});
|
|
701
|
+
if (open.length > 0)
|
|
702
|
+
return {
|
|
703
|
+
gap: `No rollback step judged for ${labelOf(u)} yet: ${open.length === 1 ? "a sentence" : `${open.length} sentences`} of design.md or data.md may say how to undo it, but ${open.some((p) => undoes(p.item)) ? "a judge couldn't tell" : "no model was asked"}, so a person checks ${sortStrings([...new Set(open.map((p) => `${p.x.file}:${p.x.n}`))]).join(", ")}.`
|
|
704
|
+
};
|
|
705
|
+
return {
|
|
706
|
+
gap: `No rollback step written for ${labelOf(u)}: say how to undo it in design.md under "Rollout and rollback"${u.kind === "migration" ? " or in data.md" : ""}.`
|
|
707
|
+
};
|
|
708
|
+
};
|
|
709
|
+
const waitsOf = /* @__PURE__ */ new Map();
|
|
710
|
+
const before = [];
|
|
711
|
+
for (const r of ctx.requests) {
|
|
712
|
+
const route = r.record ? units.get(`route:${r.record}`) : void 0;
|
|
713
|
+
const taskUnit = !route ? [...units.values()].find((u) => r.unblocks.some((t) => u.tasks.includes(t))) : void 0;
|
|
714
|
+
const target = route ?? taskUnit;
|
|
715
|
+
const why = route ? `it deploys ${r.record}` : taskUnit ? `it unblocks task ${r.unblocks.join(", ")} in ${taskUnit.repo}` : "no step names it, so it is needed before the first step";
|
|
716
|
+
const w = requestWait(ctx, r, why);
|
|
717
|
+
if (r.leadTimeDays === void 0)
|
|
718
|
+
gaps.push(`${REQUESTS_FILE}:${r.line}: "${r.heading}" matches no request template, so it has no lead time to forecast.`);
|
|
719
|
+
if (target)
|
|
720
|
+
waitsOf.set(target.key, [...waitsOf.get(target.key) ?? [], w]);
|
|
721
|
+
else
|
|
722
|
+
before.push(w);
|
|
723
|
+
}
|
|
724
|
+
const beforeReady = before.reduce((d, w) => laterOf(d, w.readyBy ?? ctx.today), ctx.today);
|
|
725
|
+
const earliestOf = /* @__PURE__ */ new Map();
|
|
726
|
+
const steps = order.map((k) => {
|
|
727
|
+
const u = units.get(k);
|
|
728
|
+
const p = preds.get(k);
|
|
729
|
+
const waits = waitsOf.get(k) ?? [];
|
|
730
|
+
const earliest = [
|
|
731
|
+
beforeReady,
|
|
732
|
+
...waits.map((w) => w.readyBy ?? ctx.today),
|
|
733
|
+
...p.map((e) => earliestOf.get(e.from) ?? ctx.today)
|
|
734
|
+
].reduce((a, b) => laterOf(a, b), ctx.today);
|
|
735
|
+
earliestOf.set(k, earliest);
|
|
736
|
+
const why = p.length ? sortStrings(p.map((e) => `After step ${numOf.get(e.from)}: ${e.why}.`)) : [
|
|
737
|
+
u.kind === "migration" ? "First: the schema is expanded before any code reads it." : u.kind === "gateway-route" || u.kind === "topic" ? "Nothing in this change has to be live before it." : "No dependency in interfaces.md, data.md or stories.md has to deploy before it."
|
|
738
|
+
];
|
|
739
|
+
return {
|
|
740
|
+
n: numOf.get(k),
|
|
741
|
+
repo: u.repo,
|
|
742
|
+
kind: u.kind,
|
|
743
|
+
what: u.what,
|
|
744
|
+
why,
|
|
745
|
+
after: sortStrings(p.map((e) => String(numOf.get(e.from)))).map(Number).sort((a, b) => a - b),
|
|
746
|
+
rollback: rollbackOf(u),
|
|
747
|
+
waitsOn: waits,
|
|
748
|
+
earliest,
|
|
749
|
+
waitDays: workingDays(ctx.today, earliest),
|
|
750
|
+
sources: sortStrings(u.sources)
|
|
751
|
+
};
|
|
752
|
+
});
|
|
753
|
+
for (const s of steps)
|
|
754
|
+
if (s.rollback.gap)
|
|
755
|
+
gaps.push(s.rollback.gap);
|
|
756
|
+
for (const x of external) {
|
|
757
|
+
const caller = externalCaller.get(x.what);
|
|
758
|
+
if (caller && numOf.has(caller))
|
|
759
|
+
x.beforeStep = numOf.get(caller);
|
|
760
|
+
}
|
|
761
|
+
const sections = SECTIONS.map((d) => {
|
|
762
|
+
const s = sectionOf(ctx, "design.md", d.re);
|
|
763
|
+
if (!s || !s.text)
|
|
764
|
+
return { name: d.name, gap: d.gap };
|
|
765
|
+
const inSection = pairs.filter((p) => p.x.file === "design.md" && s.lines.some((l) => l.n === p.x.n));
|
|
766
|
+
if (d.name === "rollback" && inSection.length > 0 && inSection.every((p) => undoes(p.item)?.verdict === "no"))
|
|
767
|
+
return {
|
|
768
|
+
name: d.name,
|
|
769
|
+
heading: s.heading,
|
|
770
|
+
source: s.source,
|
|
771
|
+
gap: `design.md "${s.heading}" gives the order but no way back: say how each step is undone, and when to decide.`
|
|
772
|
+
};
|
|
773
|
+
return { name: d.name, heading: s.heading, source: s.source, text: s.text };
|
|
774
|
+
});
|
|
775
|
+
for (const s of sections)
|
|
776
|
+
if (s.gap)
|
|
777
|
+
gaps.push(s.gap);
|
|
778
|
+
const rollout = sections.find((s) => s.name === "rollout");
|
|
779
|
+
if (rollout?.text) {
|
|
780
|
+
const mentions = [];
|
|
781
|
+
for (const s of steps) {
|
|
782
|
+
const u = units.get(order[s.n - 1]);
|
|
783
|
+
const words = s.kind === "migration" ? [...u.names, "migration"] : u.names;
|
|
784
|
+
for (const w of words) {
|
|
785
|
+
const at = rollout.text.toLowerCase().search(new RegExp(`(^|[^a-z0-9_-])${w.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
|
|
786
|
+
if (at >= 0) {
|
|
787
|
+
mentions.push({ at, step: s.n, name: w });
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
mentions.sort((a, b) => a.at - b.at);
|
|
793
|
+
for (let i = 0; i + 1 < mentions.length; i++)
|
|
794
|
+
for (let j = i + 1; j < mentions.length; j++)
|
|
795
|
+
if (mentions[i].step > mentions[j].step)
|
|
796
|
+
notes.push(`design.md (${rollout.source}) puts ${mentions[i].name} before ${mentions[j].name}; the dependencies put ${mentions[j].name} first (step ${mentions[j].step}).`);
|
|
797
|
+
const named = new Set(mentions.map((x) => x.step));
|
|
798
|
+
for (const s of steps)
|
|
799
|
+
if (!named.has(s.n) && s.kind !== "migration")
|
|
800
|
+
notes.push(`design.md's rollout doesn't mention step ${s.n} (${s.repo}).`);
|
|
801
|
+
}
|
|
802
|
+
for (const [id, why] of [...unplaced].sort(([a], [b]) => cmp2(a, b)))
|
|
803
|
+
gaps.push(why || id);
|
|
804
|
+
const findings = ctx.latest.filter((f) => f.finding.lens === "release" && f.finding.state === "open").map((f) => ({
|
|
805
|
+
gate: f.finding.gate,
|
|
806
|
+
id: f.finding.id,
|
|
807
|
+
rule: f.finding.rule,
|
|
808
|
+
severity: f.finding.severity,
|
|
809
|
+
state: f.finding.state,
|
|
810
|
+
summary: f.finding.summary,
|
|
811
|
+
path: f.path
|
|
812
|
+
}));
|
|
813
|
+
if (ctx.rounds.length === 0)
|
|
814
|
+
notes.push("No critic round has run on this change yet, so the release lens has nothing to add.");
|
|
815
|
+
return {
|
|
816
|
+
change: ctx.change.id,
|
|
817
|
+
title: ctx.change.title,
|
|
818
|
+
state: ctx.change.state,
|
|
819
|
+
today: ctx.today,
|
|
820
|
+
steps,
|
|
821
|
+
before,
|
|
822
|
+
external: external.filter((x, i, a) => a.findIndex((y) => y.what === x.what) === i),
|
|
823
|
+
unplaced: [...unplaced].map(([id, why]) => ({ id, why })).sort((a, b) => cmp2(a.id, b.id)),
|
|
824
|
+
sections,
|
|
825
|
+
findings,
|
|
826
|
+
earliest: steps.reduce((d, s) => laterOf(d, s.earliest), beforeReady),
|
|
827
|
+
gaps: [...new Set(gaps)],
|
|
828
|
+
notes: [...new Set(notes)]
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function placeWhy(ctx, id, role) {
|
|
832
|
+
return ctx.get("Component", id) ? `${id} (${role}) has no repo in the catalog or the repos manifest, so Atticus can't place its deploy.` : `${id} (${role}) isn't in the catalog or the repos manifest, so Atticus can't place its deploy.`;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// packages/release/dist/forecast.js
|
|
836
|
+
var RELEASED = /* @__PURE__ */ new Set(["released", "post-release-review", "archived"]);
|
|
837
|
+
var MILESTONES = ["release-ready", "released"];
|
|
838
|
+
async function releaseForecast(ctx, plan) {
|
|
839
|
+
const p = plan ?? await deploymentPlan(ctx);
|
|
840
|
+
const notes = [];
|
|
841
|
+
const tasks = readTasks(ctx.files);
|
|
842
|
+
const done = tasks.filter((t) => /\[[xX]\]/.test(t.text));
|
|
843
|
+
const open = tasks.filter((t) => !/\[[xX]\]/.test(t.text));
|
|
844
|
+
const taskNum = (t) => /\[[ xX]?\]\s*(\d+(?:\.\d+)*)/.exec(t.text)?.[1];
|
|
845
|
+
const waitingOnRequests = ctx.requests.flatMap((r) => r.unblocks).filter((n) => open.some((t) => taskNum(t) === n));
|
|
846
|
+
const taskView = {
|
|
847
|
+
total: tasks.length,
|
|
848
|
+
open: open.length,
|
|
849
|
+
done: done.length,
|
|
850
|
+
waitingOnRequests: [...new Set(waitingOnRequests)].sort()
|
|
851
|
+
};
|
|
852
|
+
if (RELEASED.has(ctx.change.state)) {
|
|
853
|
+
const at = ctx.change.history.filter((h) => h.to === "released").at(-1)?.at;
|
|
854
|
+
return {
|
|
855
|
+
change: ctx.change.id,
|
|
856
|
+
state: ctx.change.state,
|
|
857
|
+
today: ctx.today,
|
|
858
|
+
...at ? { ready: at.slice(0, 10) } : {},
|
|
859
|
+
basis: "released",
|
|
860
|
+
status: "released",
|
|
861
|
+
waitDays: 0,
|
|
862
|
+
drivers: [],
|
|
863
|
+
tasks: taskView,
|
|
864
|
+
notes: at ? [`Released on ${at.slice(0, 10)} (change.yaml history).`] : []
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
const drivers = [];
|
|
868
|
+
for (const w of [...p.before, ...p.steps.flatMap((s) => s.waitsOn)])
|
|
869
|
+
if (w.readyBy)
|
|
870
|
+
drivers.push({
|
|
871
|
+
what: `${w.requestType}${w.record ? ` for ${w.record}` : ""} (${w.state === "filed" ? `filed${w.number ? ` as ${w.number}` : ""}` : w.state === "waiting" ? "waiting for a person to file it" : "not filed yet"})`,
|
|
872
|
+
until: w.readyBy,
|
|
873
|
+
days: w.waitDays ?? 0,
|
|
874
|
+
source: `${w.source}; lead time ${w.leadTimeDays} working days (${w.leadTimeSource})`
|
|
875
|
+
});
|
|
876
|
+
let ready = p.earliest;
|
|
877
|
+
let basis = drivers.length ? "requests" : "today";
|
|
878
|
+
let target;
|
|
879
|
+
let initiative;
|
|
880
|
+
const view = await readView(ctx.root);
|
|
881
|
+
const owning = view.all("Initiative").filter((i) => i.changes.includes(ctx.change.id)).sort((a, b) => a.id < b.id ? -1 : 1);
|
|
882
|
+
if (owning.length > 1)
|
|
883
|
+
notes.push(`${ctx.change.id} is in ${owning.length} initiatives (${owning.map((i) => i.id).join(", ")}); the forecast uses ${owning[0].id}.`);
|
|
884
|
+
if (owning[0]) {
|
|
885
|
+
try {
|
|
886
|
+
const st = await initiativeStatus(ctx.root, owning[0].id, { now: new Date(ctx.now) });
|
|
887
|
+
const planned = await readPlan(ctx.root, owning[0].id);
|
|
888
|
+
const milestoneOf = new Map((planned?.items ?? []).filter((i) => i.change === ctx.change.id && MILESTONES.includes(i.milestone ?? "")).map((i) => [i.key, i.milestone]));
|
|
889
|
+
const item = st.items.filter((i) => milestoneOf.has(i.key)).sort((a, b) => a.due < b.due ? -1 : 1)[0];
|
|
890
|
+
initiative = {
|
|
891
|
+
id: st.initiative,
|
|
892
|
+
title: st.title,
|
|
893
|
+
status: st.status,
|
|
894
|
+
...st.targetRelease ? { targetRelease: st.targetRelease } : {},
|
|
895
|
+
...st.projectedRelease ? { projectedRelease: st.projectedRelease } : {},
|
|
896
|
+
criticalPath: st.criticalPath,
|
|
897
|
+
...item ? {
|
|
898
|
+
item: {
|
|
899
|
+
key: item.key,
|
|
900
|
+
milestone: milestoneOf.get(item.key),
|
|
901
|
+
due: item.due,
|
|
902
|
+
projectedFinish: item.projectedFinish,
|
|
903
|
+
status: item.status
|
|
904
|
+
}
|
|
905
|
+
} : {}
|
|
906
|
+
};
|
|
907
|
+
const src = `initiatives/${st.initiative}/plan.yaml`;
|
|
908
|
+
if (item) {
|
|
909
|
+
drivers.push({
|
|
910
|
+
what: `Initiative ${st.initiative}: ${item.what}`,
|
|
911
|
+
until: item.projectedFinish,
|
|
912
|
+
days: workingDays(ctx.today, item.projectedFinish),
|
|
913
|
+
source: `${src} item ${item.key} (critical path: ${st.criticalPath.join(" \u2192 ") || "none"})`
|
|
914
|
+
});
|
|
915
|
+
ready = laterOf(ready, item.projectedFinish);
|
|
916
|
+
basis = "initiative";
|
|
917
|
+
target = { date: item.due, source: `${src} item ${item.key} due` };
|
|
918
|
+
} else if (st.targetRelease) {
|
|
919
|
+
target = { date: st.targetRelease, source: `${src} targetRelease` };
|
|
920
|
+
notes.push(`Initiative ${st.initiative} has no release milestone for ${ctx.change.id}; the target is the initiative's release.`);
|
|
921
|
+
}
|
|
922
|
+
notes.push(...st.notes);
|
|
923
|
+
} catch (e) {
|
|
924
|
+
notes.push(`Initiative ${owning[0].id}: ${e.message}`);
|
|
925
|
+
}
|
|
926
|
+
} else
|
|
927
|
+
notes.push(`${ctx.change.id} isn't part of an initiative, so it has no target date.`);
|
|
928
|
+
if (open.length)
|
|
929
|
+
notes.push(`${open.length} of ${tasks.length} tasks are open. tasks.md carries no estimates, so they can't move the day: it is the earliest day the dates allow, not a promise.`);
|
|
930
|
+
if (!drivers.length)
|
|
931
|
+
notes.push("Nothing with a lead time stands in the way: no service request waits.");
|
|
932
|
+
for (const g of p.gaps.filter((x) => /no lead time/.test(x)))
|
|
933
|
+
notes.push(g);
|
|
934
|
+
const status = !target ? "no-target" : ctx.today > target.date ? "late" : ready > target.date ? "at-risk" : "on-track";
|
|
935
|
+
return {
|
|
936
|
+
change: ctx.change.id,
|
|
937
|
+
state: ctx.change.state,
|
|
938
|
+
today: ctx.today,
|
|
939
|
+
ready,
|
|
940
|
+
basis,
|
|
941
|
+
...target ? { target } : {},
|
|
942
|
+
status,
|
|
943
|
+
waitDays: workingDays(ctx.today, ready),
|
|
944
|
+
drivers: drivers.sort((a, b) => a.until > b.until ? -1 : a.until < b.until ? 1 : 0),
|
|
945
|
+
tasks: taskView,
|
|
946
|
+
...initiative ? { initiative } : {},
|
|
947
|
+
notes
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// packages/release/dist/outcomes.js
|
|
952
|
+
var import_yaml2 = __toESM(require_dist(), 1);
|
|
953
|
+
import { mkdir, readFile as readFile2, rename, rm, writeFile } from "node:fs/promises";
|
|
954
|
+
import { dirname, join as join2 } from "node:path";
|
|
955
|
+
var OUTCOMES_FILE = "review/outcomes.yaml";
|
|
956
|
+
var MEASURABLE = /* @__PURE__ */ new Set(["released", "post-release-review", "archived"]);
|
|
957
|
+
var SECTION = [/success/i, /measures?|kpis?|metrics?/i, /outcome/i];
|
|
958
|
+
function measureId(text) {
|
|
959
|
+
const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
960
|
+
if (slug.length <= 60)
|
|
961
|
+
return slug || "measure";
|
|
962
|
+
const cut = slug.slice(0, 61);
|
|
963
|
+
return cut.slice(0, cut.lastIndexOf("-") > 20 ? cut.lastIndexOf("-") : 60);
|
|
964
|
+
}
|
|
965
|
+
var tidy = (s) => {
|
|
966
|
+
const t = s.trim().replace(/[.;]+$/, "").trim();
|
|
967
|
+
return t ? t[0].toUpperCase() + t.slice(1) : t;
|
|
968
|
+
};
|
|
969
|
+
function measuresOf(ctx) {
|
|
970
|
+
const doc = docOf(ctx.files, "brief.md");
|
|
971
|
+
if (!doc)
|
|
972
|
+
return { measures: [] };
|
|
973
|
+
const s = SECTION.map((re) => doc.sections.find((x) => re.test(x.heading))).find(Boolean);
|
|
974
|
+
if (!s)
|
|
975
|
+
return { measures: [] };
|
|
976
|
+
const out = [];
|
|
977
|
+
for (const sen of sentences(sectionLines(doc, s)))
|
|
978
|
+
for (const part of sen.text.split(/;\s+/)) {
|
|
979
|
+
const measure = tidy(part);
|
|
980
|
+
if (!measure)
|
|
981
|
+
continue;
|
|
982
|
+
let id = measureId(measure);
|
|
983
|
+
for (let k = 2; out.some((m) => m.id === id); k++)
|
|
984
|
+
id = `${measureId(measure)}-${k}`;
|
|
985
|
+
out.push({ id, n: out.length + 1, measure, source: `brief.md:${sen.n}` });
|
|
986
|
+
}
|
|
987
|
+
return { measures: out, source: `brief.md:${s.line}` };
|
|
988
|
+
}
|
|
989
|
+
async function readOutcomes(dir) {
|
|
990
|
+
let text;
|
|
991
|
+
try {
|
|
992
|
+
text = await readFile2(join2(dir, OUTCOMES_FILE), "utf8");
|
|
993
|
+
} catch {
|
|
994
|
+
return void 0;
|
|
995
|
+
}
|
|
996
|
+
const r = ChangeOutcomes.safeParse((0, import_yaml2.parse)(text));
|
|
997
|
+
if (!r.success)
|
|
998
|
+
throw new ReleaseError(`${OUTCOMES_FILE}: ${r.error.issues.map((i) => `${i.path.join(".")} ${i.message}`).join("; ")}`, "invalid");
|
|
999
|
+
return r.data;
|
|
1000
|
+
}
|
|
1001
|
+
async function outcomesView(ctx) {
|
|
1002
|
+
const { measures, source } = measuresOf(ctx);
|
|
1003
|
+
const file = await readOutcomes(ctx.dir);
|
|
1004
|
+
const recorded = new Map((file?.measures ?? []).map((m) => [m.id, m]));
|
|
1005
|
+
const gaps = [];
|
|
1006
|
+
if (!source)
|
|
1007
|
+
gaps.push('brief.md has no "Success measures" section, so there is nothing to measure after release. Add one: how the team will know the change worked.');
|
|
1008
|
+
const canRecord = MEASURABLE.has(ctx.change.state) && measures.length > 0;
|
|
1009
|
+
const why = !measures.length ? "The brief names no success measures." : canRecord ? void 0 : `Outcomes are measured after release; ${ctx.change.id} is ${ctx.change.state}.`;
|
|
1010
|
+
return {
|
|
1011
|
+
change: ctx.change.id,
|
|
1012
|
+
title: ctx.change.title,
|
|
1013
|
+
state: ctx.change.state,
|
|
1014
|
+
...source ? { source } : {},
|
|
1015
|
+
measures: measures.map((m) => {
|
|
1016
|
+
const values = recorded.get(m.id)?.values ?? [];
|
|
1017
|
+
return { ...m, values, ...values.length ? { latest: values.at(-1) } : {} };
|
|
1018
|
+
}),
|
|
1019
|
+
orphans: (file?.measures ?? []).filter((x) => !measures.some((m) => m.id === x.id)).map((x) => ({ id: x.id, measure: x.measure, values: x.values })),
|
|
1020
|
+
canRecord,
|
|
1021
|
+
...why ? { why } : {},
|
|
1022
|
+
gaps
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function findMeasure(measures, ref) {
|
|
1026
|
+
const r = ref.trim();
|
|
1027
|
+
if (/^\d+$/.test(r))
|
|
1028
|
+
return measures.find((m) => m.n === Number(r));
|
|
1029
|
+
return measures.find((m) => m.id === r) ?? measures.find((m) => m.measure.toLowerCase() === r.toLowerCase()) ?? measures.find((m) => m.id === measureId(r));
|
|
1030
|
+
}
|
|
1031
|
+
async function recordOutcome(ctx, input) {
|
|
1032
|
+
const { measures } = measuresOf(ctx);
|
|
1033
|
+
if (!measures.length)
|
|
1034
|
+
throw new ReleaseError(`brief.md of ${ctx.change.id} names no success measures, so there is nothing to record.`, "refused");
|
|
1035
|
+
if (!MEASURABLE.has(ctx.change.state))
|
|
1036
|
+
throw new ReleaseError(`Outcomes are measured after release; ${ctx.change.id} is ${ctx.change.state}.`, "refused");
|
|
1037
|
+
const m = findMeasure(measures, input.measure);
|
|
1038
|
+
if (!m)
|
|
1039
|
+
throw new ReleaseError(`brief.md has no measure "${input.measure}". Its measures: ${measures.map((x) => `${x.n} ${x.id}`).join("; ")}.`, "refused");
|
|
1040
|
+
const by = input.by.replace(/^@/, "");
|
|
1041
|
+
const value = {
|
|
1042
|
+
value: input.value.trim(),
|
|
1043
|
+
by,
|
|
1044
|
+
at: ctx.now,
|
|
1045
|
+
...input.note?.trim() ? { note: input.note.trim() } : {}
|
|
1046
|
+
};
|
|
1047
|
+
const file = await readOutcomes(ctx.dir) ?? {
|
|
1048
|
+
schemaVersion: WORKSPACE_SCHEMA_VERSION,
|
|
1049
|
+
change: ctx.change.id,
|
|
1050
|
+
measures: []
|
|
1051
|
+
};
|
|
1052
|
+
const existing = file.measures.find((x) => x.id === m.id);
|
|
1053
|
+
const next = {
|
|
1054
|
+
...file,
|
|
1055
|
+
measures: existing ? file.measures.map((x) => x.id === m.id ? { ...x, values: [...x.values, value] } : x) : [...file.measures, { id: m.id, measure: m.measure, values: [value] }]
|
|
1056
|
+
};
|
|
1057
|
+
const parsed = ChangeOutcomes.safeParse(next);
|
|
1058
|
+
if (!parsed.success)
|
|
1059
|
+
throw new ReleaseError(parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "), "refused");
|
|
1060
|
+
const path = join2(ctx.dir, OUTCOMES_FILE);
|
|
1061
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1062
|
+
const tmp = `${path}.tmp`;
|
|
1063
|
+
try {
|
|
1064
|
+
await writeFile(tmp, toCanonicalYaml(parsed.data, [
|
|
1065
|
+
"schemaVersion",
|
|
1066
|
+
"change",
|
|
1067
|
+
"id",
|
|
1068
|
+
"measure",
|
|
1069
|
+
"value",
|
|
1070
|
+
"by",
|
|
1071
|
+
"at"
|
|
1072
|
+
]), "utf8");
|
|
1073
|
+
await rename(tmp, path);
|
|
1074
|
+
} catch (e) {
|
|
1075
|
+
await rm(tmp, { force: true });
|
|
1076
|
+
throw e;
|
|
1077
|
+
}
|
|
1078
|
+
return {
|
|
1079
|
+
measure: m,
|
|
1080
|
+
value: parsed.data.measures.find((x) => x.id === m.id).values.at(-1),
|
|
1081
|
+
written: OUTCOMES_FILE
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// packages/release/dist/evidence.js
|
|
1086
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
1087
|
+
import { createHash } from "node:crypto";
|
|
1088
|
+
import { copyFile, mkdir as mkdir2, readdir, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
1089
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
1090
|
+
async function walk(dir, rel = "") {
|
|
1091
|
+
let entries;
|
|
1092
|
+
try {
|
|
1093
|
+
entries = await readdir(join3(dir, rel), { withFileTypes: true });
|
|
1094
|
+
} catch {
|
|
1095
|
+
return [];
|
|
1096
|
+
}
|
|
1097
|
+
const out = [];
|
|
1098
|
+
for (const e of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
1099
|
+
const p = rel ? `${rel}/${e.name}` : e.name;
|
|
1100
|
+
if (e.isDirectory())
|
|
1101
|
+
out.push(...await walk(dir, p));
|
|
1102
|
+
else if (e.isFile() && !e.name.endsWith(".tmp"))
|
|
1103
|
+
out.push(p);
|
|
1104
|
+
}
|
|
1105
|
+
return out;
|
|
1106
|
+
}
|
|
1107
|
+
async function changeFiles(dir) {
|
|
1108
|
+
const out = [];
|
|
1109
|
+
for (const path of await walk(dir)) {
|
|
1110
|
+
const buf = await readFile3(join3(dir, path));
|
|
1111
|
+
out.push({ path, sha256: createHash("sha256").update(buf).digest("hex"), bytes: buf.length });
|
|
1112
|
+
}
|
|
1113
|
+
return out;
|
|
1114
|
+
}
|
|
1115
|
+
var esc = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
1116
|
+
var day = (iso2) => iso2 ? iso2.slice(0, 10) : "";
|
|
1117
|
+
async function evidencePack(ctx) {
|
|
1118
|
+
const files = await changeFiles(ctx.dir);
|
|
1119
|
+
const c = ctx.change;
|
|
1120
|
+
const md = [];
|
|
1121
|
+
const h2 = (t) => {
|
|
1122
|
+
if (md.at(-1) !== "")
|
|
1123
|
+
md.push("");
|
|
1124
|
+
md.push(`## ${t}`, "");
|
|
1125
|
+
};
|
|
1126
|
+
md.push(`# Evidence pack: ${c.id}`, "", `${c.title}. Generated ${ctx.now} by Atticus from the files of the change folder (\`openspec/changes/${c.id}/\`); nothing here is typed by hand, and every item names the file it came from.`);
|
|
1127
|
+
h2("Change summary");
|
|
1128
|
+
let risk;
|
|
1129
|
+
try {
|
|
1130
|
+
const rep = await changeReport(ctx.root, c.id);
|
|
1131
|
+
risk = `${rep.risk.level} (score ${rep.risk.score})`;
|
|
1132
|
+
} catch {
|
|
1133
|
+
risk = "couldn't be assessed";
|
|
1134
|
+
}
|
|
1135
|
+
md.push(`| | |`, `| --- | --- |`, `| Change | ${c.id} |`, `| Title | ${esc(c.title)} |`, `| Type | ${c.type} |`, `| State | ${c.state} |`, `| Risk | ${risk} |`, `| Systems | ${c.systems.join(", ") || "none named"} |`, `| Owners | ${Object.entries(c.owners).map(([r, p]) => `${r}: ${p.join(", ")}`).join("; ")} |`, `| Created | ${c.createdAt} by ${c.createdBy} |`, "", "Source: change.yaml");
|
|
1136
|
+
const problem = sectionOf(ctx, "brief.md", /^problem/i);
|
|
1137
|
+
const outcome = sectionOf(ctx, "brief.md", /^outcome/i);
|
|
1138
|
+
for (const s of [problem, outcome])
|
|
1139
|
+
if (s)
|
|
1140
|
+
md.push("", `**${s.heading}.** ${s.text.replace(/\n/g, " ")} (${s.source})`);
|
|
1141
|
+
if (c.history.length) {
|
|
1142
|
+
md.push("", "State history:", "");
|
|
1143
|
+
for (const x of c.history)
|
|
1144
|
+
md.push(`- ${x.at}: ${x.from} \u2192 ${x.to} by ${x.by}${x.pr ? ` (PR #${x.pr})` : ""}${x.reason ? `: ${x.reason}` : ""}`);
|
|
1145
|
+
md.push("", "Source: change.yaml history");
|
|
1146
|
+
}
|
|
1147
|
+
h2("Artifacts and their sha-256");
|
|
1148
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
1149
|
+
for (const hf of c.handoffs)
|
|
1150
|
+
for (const a of hf.artifacts)
|
|
1151
|
+
recorded.set(a.path, { phase: hf.phase, sha256: a.sha256 });
|
|
1152
|
+
if (recorded.size)
|
|
1153
|
+
md.push("| File | Bytes | sha-256 | Against the hand-off |", "| --- | --- | --- | --- |");
|
|
1154
|
+
else
|
|
1155
|
+
md.push("| File | Bytes | sha-256 |", "| --- | --- | --- |");
|
|
1156
|
+
for (const f of files) {
|
|
1157
|
+
const r = recorded.get(f.path);
|
|
1158
|
+
const against = !r ? "not in a hand-off" : r.sha256 === f.sha256 ? `matches ${r.phase}` : `changed since ${r.phase} (${r.sha256.slice(0, 12)}\u2026)`;
|
|
1159
|
+
md.push(`| ${f.path} | ${f.bytes} | \`${f.sha256}\` |${recorded.size ? ` ${against} |` : ""}`);
|
|
1160
|
+
}
|
|
1161
|
+
if (!recorded.size)
|
|
1162
|
+
md.push("", "_No hand-off recorded artifact hashes, so there is nothing to compare against._");
|
|
1163
|
+
h2("Critic rounds and findings");
|
|
1164
|
+
const rounds = ctx.rounds.filter((r) => r.gate !== "drift");
|
|
1165
|
+
if (!rounds.length)
|
|
1166
|
+
md.push("_No critic round has run on this change yet._");
|
|
1167
|
+
for (const r of rounds) {
|
|
1168
|
+
const open = r.findings.filter((f) => f.finding.state === "open").length;
|
|
1169
|
+
md.push(`### ${r.gate} gate, round ${r.round}${r.meta?.createdAt ? ` (${r.meta.createdAt})` : ""}`, "", `${r.findings.length} finding${r.findings.length === 1 ? "" : "s"}, ${open} open. Source: review/${r.gate}/${r.round}/`, "");
|
|
1170
|
+
if (r.findings.length) {
|
|
1171
|
+
md.push("| Finding | Severity | Rule | State | Summary | Resolution | Source |", "| --- | --- | --- | --- | --- | --- | --- |");
|
|
1172
|
+
for (const { finding: f, path } of r.findings) {
|
|
1173
|
+
const res = f.resolution ? `${f.resolution.action} by ${f.resolution.by} ${day(f.resolution.at)}${f.resolution.reason ? `: ${esc(f.resolution.reason)}` : ""}${f.resolution.askedOf ? ` (asked of ${f.resolution.askedOf})` : ""}` : "";
|
|
1174
|
+
md.push(`| ${f.id} | ${f.severity} | ${f.rule} | ${f.state} | ${esc(f.summary)} | ${res} | ${path} |`);
|
|
1175
|
+
}
|
|
1176
|
+
md.push("");
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
h2("Waivers");
|
|
1180
|
+
const waived = ctx.rounds.flatMap((r) => r.findings).filter((f) => f.finding.state === "waived");
|
|
1181
|
+
const uniq = waived.filter((w, i, a) => a.findIndex((x) => x.finding.gate === w.finding.gate && x.finding.rule === w.finding.rule && x.finding.summary === w.finding.summary) === i);
|
|
1182
|
+
if (!uniq.length)
|
|
1183
|
+
md.push(rounds.length ? "_No finding is waived._" : "_No critic round yet, so nothing is waived._");
|
|
1184
|
+
for (const { finding: f, path } of uniq)
|
|
1185
|
+
md.push(`- ${f.summary} Rule ${f.rule} (${f.layer}), waived by ${f.resolution?.by ?? "?"} on ${day(f.resolution?.at)}: ${f.resolution?.reason ?? "no reason recorded"} (${path})`);
|
|
1186
|
+
h2("Approvals (hand-off PRs)");
|
|
1187
|
+
if (!c.handoffs.length)
|
|
1188
|
+
md.push("_No hand-off PR yet._");
|
|
1189
|
+
for (const hf of c.handoffs) {
|
|
1190
|
+
md.push(`- ${hf.phase}: PR #${hf.pr.number} ${hf.pr.url}, ${hf.state}, raised by ${hf.by} ${day(hf.openedAt)}; ${hf.approvals.length ? `approved by ${hf.approvals.map((a) => `${a.by}${a.roles?.length ? ` as ${a.roles.join(", ")}` : ""} (${a.at})`).join("; ")}` : "no approvals yet"}.`);
|
|
1191
|
+
}
|
|
1192
|
+
if (c.handoffs.length)
|
|
1193
|
+
md.push("", "Source: change.yaml handoffs");
|
|
1194
|
+
h2("Test trace");
|
|
1195
|
+
try {
|
|
1196
|
+
const t = await traceMatrix(ctx.root, c.id);
|
|
1197
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
1198
|
+
for (const g of t.gaps)
|
|
1199
|
+
byKind.set(g.kind, (byKind.get(g.kind) ?? 0) + 1);
|
|
1200
|
+
const tested = t.rows.filter((r) => r.tests.length).length;
|
|
1201
|
+
md.push(`${t.requirements.length} requirement${t.requirements.length === 1 ? "" : "s"}, ${t.stories.length} stor${t.stories.length === 1 ? "y" : "ies"}, ${t.rows.length} trace row${t.rows.length === 1 ? "" : "s"} of which ${tested} ha${tested === 1 ? "s" : "ve"} a test; ${t.found.length} test file${t.found.length === 1 ? "" : "s"} found in the repos.`);
|
|
1202
|
+
if (t.gaps.length) {
|
|
1203
|
+
md.push("", "Gaps:", "");
|
|
1204
|
+
for (const [k, n] of [...byKind].sort())
|
|
1205
|
+
md.push(`- ${k}: ${n}`);
|
|
1206
|
+
}
|
|
1207
|
+
md.push("", "Source: computed as `atticus trace` does, from specs/, stories/, tests/ and Run with AI records");
|
|
1208
|
+
} catch (e) {
|
|
1209
|
+
md.push(`_The trace couldn't be computed: ${e.message}_`);
|
|
1210
|
+
}
|
|
1211
|
+
h2("Service requests and manual steps");
|
|
1212
|
+
if (!ctx.requests.length)
|
|
1213
|
+
md.push(`_${REQUESTS_FILE} drafts no service request._`);
|
|
1214
|
+
for (const r of ctx.requests)
|
|
1215
|
+
md.push(`- ${r.heading}: ${r.state === "filed" ? `filed${r.number ? ` as ${r.number}` : ""}${r.filedAt ? ` on ${day(r.filedAt)}` : ""}` : r.state === "waiting" ? `waiting for a person to file it (step ${r.step})` : "not filed"}${r.missing.length ? `; missing ${r.missing.join(", ")}` : ""} (${REQUESTS_FILE}:${r.line})`);
|
|
1216
|
+
const steps = [...ctx.steps].sort((a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : a.id < b.id ? -1 : 1);
|
|
1217
|
+
if (steps.length) {
|
|
1218
|
+
md.push("", "| Step | System | What | State | Done by | Source |", "| --- | --- | --- | --- | --- | --- |");
|
|
1219
|
+
for (const s of steps)
|
|
1220
|
+
md.push(`| ${s.id} | ${s.system} | ${esc(s.title)} | ${s.state} | ${s.recorded ? `${s.recorded.by} ${s.recorded.at}${s.recorded.via === "executor" ? ` via ${s.recorded.executor ?? "an executor"}` : ""}` : ""} | steps/${s.id}.yaml |`);
|
|
1221
|
+
} else
|
|
1222
|
+
md.push("", "_No manual step for this change._");
|
|
1223
|
+
h2("Drift");
|
|
1224
|
+
const drift = ctx.rounds.filter((r) => r.gate === "drift").at(-1);
|
|
1225
|
+
if (!drift)
|
|
1226
|
+
md.push("_No drift check has run on this change (it runs in build and qa, and after each scan)._");
|
|
1227
|
+
else {
|
|
1228
|
+
const open = drift.findings.filter((f) => f.finding.state === "open");
|
|
1229
|
+
md.push(`Newest round ${drift.round}${drift.meta?.createdAt ? ` (${drift.meta.createdAt})` : ""}: ${open.length ? `${open.length} open drift finding${open.length === 1 ? "" : "s"}` : "no open drift"}. Source: review/drift/${drift.round}/`);
|
|
1230
|
+
for (const f of open)
|
|
1231
|
+
md.push(`- ${f.finding.summary} (${f.path})`);
|
|
1232
|
+
}
|
|
1233
|
+
h2("AI run summaries");
|
|
1234
|
+
const runs = [];
|
|
1235
|
+
for (const f of files.filter((x) => /(^|\/)runs\/[^/]+\.ya?ml$/.test(x.path))) {
|
|
1236
|
+
try {
|
|
1237
|
+
const s = readRunSummary((0, import_yaml3.parse)(await readFile3(join3(ctx.dir, f.path), "utf8")));
|
|
1238
|
+
if (s.success)
|
|
1239
|
+
runs.push(`- ${s.data.runId}: ${s.data.agent} on ${s.data.runtime}, ${s.data.state}, ${s.data.startedAt}${s.data.usage ? `, ${s.data.usage.inputTokens} in / ${s.data.usage.outputTokens} out tokens` : ""}; context sha-256 \`${s.data.contextManifestSha256.slice(0, 12)}\u2026\` (${f.path})`);
|
|
1240
|
+
} catch {
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
for (const r of rounds)
|
|
1244
|
+
for (const l of r.meta?.lenses ?? [])
|
|
1245
|
+
runs.push(`- ${r.gate} round ${r.round}, ${l.lens} lens: ${l.agent}, ${l.state}${l.reason ? ` (${l.reason})` : ""}${l.promptSha256 ? `; prompt sha-256 \`${l.promptSha256.slice(0, 12)}\u2026\`` : ""} (review/${r.gate}/${r.round}/meta/round.yaml)`);
|
|
1246
|
+
md.push(...runs.length ? runs : ["_No AI run is recorded for this change._"]);
|
|
1247
|
+
h2("Outcomes");
|
|
1248
|
+
const o = await outcomesView(ctx);
|
|
1249
|
+
if (!o.measures.length)
|
|
1250
|
+
md.push(`_${o.gaps[0] ?? "The brief names no success measures."}_`);
|
|
1251
|
+
for (const m of o.measures)
|
|
1252
|
+
md.push(`- ${m.measure} (${m.source}): ${m.latest ? `${m.latest.value}, measured by ${m.latest.by} on ${day(m.latest.at)} (review/outcomes.yaml)` : "not measured yet"}`);
|
|
1253
|
+
return {
|
|
1254
|
+
change: c.id,
|
|
1255
|
+
title: c.title,
|
|
1256
|
+
generatedAt: ctx.now,
|
|
1257
|
+
files,
|
|
1258
|
+
markdown: md.join("\n") + "\n"
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
async function writeEvidencePack(ctx, out) {
|
|
1262
|
+
const pack = await evidencePack(ctx);
|
|
1263
|
+
await mkdir2(out, { recursive: true });
|
|
1264
|
+
const written = ["evidence.md", "manifest.json"];
|
|
1265
|
+
await writeFile2(join3(out, "evidence.md"), pack.markdown, "utf8");
|
|
1266
|
+
await writeFile2(join3(out, "manifest.json"), JSON.stringify({ change: pack.change, generatedAt: pack.generatedAt, files: pack.files }, null, 2) + "\n", "utf8");
|
|
1267
|
+
for (const f of pack.files) {
|
|
1268
|
+
const to = join3(out, "files", ...f.path.split("/"));
|
|
1269
|
+
await mkdir2(dirname2(to), { recursive: true });
|
|
1270
|
+
await copyFile(join3(ctx.dir, ...f.path.split("/")), to);
|
|
1271
|
+
written.push(`files/${f.path}`);
|
|
1272
|
+
}
|
|
1273
|
+
return written;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// packages/release/dist/notes.js
|
|
1277
|
+
var AUDIENCES = ["business", "technical"];
|
|
1278
|
+
var lines = (text) => (text ?? "").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1279
|
+
var bullets = (text) => lines(text).map((l) => l.replace(/^([-*+]|\d+\.)\s+/, ""));
|
|
1280
|
+
function breakingOf(change, said, j) {
|
|
1281
|
+
if (j?.by === "veto")
|
|
1282
|
+
return change === "planned-retire" ? { breaking: "yes", why: "The operation is retired.", judgedBy: "catalog" } : {
|
|
1283
|
+
breaking: "no",
|
|
1284
|
+
why: "A new operation in this change, so no existing caller breaks.",
|
|
1285
|
+
judgedBy: "catalog"
|
|
1286
|
+
};
|
|
1287
|
+
if (!said)
|
|
1288
|
+
return {
|
|
1289
|
+
breaking: "check",
|
|
1290
|
+
why: "interfaces.md doesn't say what changes for callers.",
|
|
1291
|
+
judgedBy: "not judged"
|
|
1292
|
+
};
|
|
1293
|
+
const by = !j ? "not judged" : j.judgment?.confirmedBy.by === "person" ? "person" : "model";
|
|
1294
|
+
if (j?.verdict === "yes")
|
|
1295
|
+
return { breaking: "yes", why: said, judgedBy: by };
|
|
1296
|
+
if (j?.verdict === "no")
|
|
1297
|
+
return { breaking: "no", why: said, judgedBy: by };
|
|
1298
|
+
return {
|
|
1299
|
+
breaking: "check",
|
|
1300
|
+
why: j ? `A judge couldn't tell whether callers must change, so a person checks: ${said}` : `Not judged: no model was asked whether callers must change, so a person checks: ${said}`,
|
|
1301
|
+
judgedBy: by
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
async function releaseNotes(ctx, audience = "business") {
|
|
1305
|
+
const m = buildModel(ctx);
|
|
1306
|
+
const problem = sectionOf(ctx, "brief.md", /^problem|^why/i);
|
|
1307
|
+
const outcome = sectionOf(ctx, "brief.md", /^outcome/i);
|
|
1308
|
+
const scope = sectionOf(ctx, "brief.md", /^scope/i);
|
|
1309
|
+
const questions = sectionOf(ctx, "brief.md", /open questions/i);
|
|
1310
|
+
const whyP = sectionOf(ctx, "proposal.md", /^why/i);
|
|
1311
|
+
const what = sectionOf(ctx, "proposal.md", /what changes/i);
|
|
1312
|
+
const impact = sectionOf(ctx, "proposal.md", /^impact/i);
|
|
1313
|
+
const systemsTable = tableWith(docOf(ctx.files, "proposal.md"), ["why"]);
|
|
1314
|
+
const who = [];
|
|
1315
|
+
let whoSource;
|
|
1316
|
+
if (systemsTable) {
|
|
1317
|
+
whoSource = `proposal.md:${systemsTable.line}`;
|
|
1318
|
+
for (const r of systemsTable.rows) {
|
|
1319
|
+
const idCell = Object.entries(r.cells).find(([k]) => /system/.test(k))?.[1] ?? "";
|
|
1320
|
+
const id = idCell.split(/\s+/)[0] ?? "";
|
|
1321
|
+
if (!id)
|
|
1322
|
+
continue;
|
|
1323
|
+
const how = Object.entries(r.cells).find(([k]) => /new|modified|retired/.test(k))?.[1] ?? "";
|
|
1324
|
+
who.push(`${m.systemName(id)}${how ? ` (${how})` : ""}: ${r.cells.why || "no reason given"}`);
|
|
1325
|
+
}
|
|
1326
|
+
} else
|
|
1327
|
+
for (const s of ctx.change.systems) {
|
|
1328
|
+
who.push(m.systemName(s));
|
|
1329
|
+
whoSource = "change.yaml";
|
|
1330
|
+
}
|
|
1331
|
+
const outOfScope = scope ? sentences(scope.lines).map((s) => /out of scope:?\s*(.*)$/i.exec(s.text)?.[1]?.trim()).filter((x) => !!x).map((x) => x[0].toUpperCase() + x.slice(1).replace(/\.$/, "")) : [];
|
|
1332
|
+
const planned = m.hops.filter((h) => /^planned-/.test(h.change));
|
|
1333
|
+
const saidOf = (h) => h.notes.map((n) => n.text.replace(/^\s*[-*]\s+/, "").trim()).join(" ");
|
|
1334
|
+
const itemOf = (h) => breakingItem({ change: h.change, operation: opOf(h), caller: h.caller, callee: h.callee }, saidOf(h));
|
|
1335
|
+
const judged = await judgedFor(ctx, BREAKING_SPEC, planned.filter((h) => saidOf(h) || h.change === "planned-new" || h.change === "planned-retire").map(itemOf));
|
|
1336
|
+
const api = planned.map((h) => {
|
|
1337
|
+
const b = breakingOf(h.change, saidOf(h), judged(itemOf(h)));
|
|
1338
|
+
return {
|
|
1339
|
+
operation: opOf(h),
|
|
1340
|
+
caller: h.caller,
|
|
1341
|
+
callee: h.calleeBoundary === "vendor" && h.calleeSystem ? `${h.callee} (${m.systemName(h.calleeSystem)}, a vendor)` : h.callee,
|
|
1342
|
+
change: h.change.replace(/^planned-/, ""),
|
|
1343
|
+
...b,
|
|
1344
|
+
source: `interfaces.md:${h.line}`
|
|
1345
|
+
};
|
|
1346
|
+
});
|
|
1347
|
+
const data = m.data.map((d) => ({
|
|
1348
|
+
table: `${d.store}.${d.table}`,
|
|
1349
|
+
change: d.change.trim(),
|
|
1350
|
+
...d.migration ? { migration: d.migration } : {},
|
|
1351
|
+
source: `data.md:${d.line}`
|
|
1352
|
+
}));
|
|
1353
|
+
const limitations = ctx.latest.filter((f) => f.finding.state === "waived").map((f) => ({
|
|
1354
|
+
summary: f.finding.summary,
|
|
1355
|
+
...f.finding.resolution?.reason ? { reason: f.finding.resolution.reason } : {},
|
|
1356
|
+
...f.finding.resolution ? { by: f.finding.resolution.by, at: f.finding.resolution.at } : {},
|
|
1357
|
+
source: f.path
|
|
1358
|
+
}));
|
|
1359
|
+
const sections = [];
|
|
1360
|
+
const add = (heading, items, source, empty) => sections.push({
|
|
1361
|
+
heading,
|
|
1362
|
+
items,
|
|
1363
|
+
...source ? { source } : {},
|
|
1364
|
+
...items.length ? {} : { empty }
|
|
1365
|
+
});
|
|
1366
|
+
const why = problem ?? whyP;
|
|
1367
|
+
add("Why", lines(why?.text), why?.source, "The brief doesn't say why yet.");
|
|
1368
|
+
const changes = what ? bullets(what.text) : lines(outcome?.text);
|
|
1369
|
+
add("What changes", changes, what?.source ?? outcome?.source, "Neither the proposal nor the brief says what changes yet.");
|
|
1370
|
+
add("Who it affects", who, whoSource, "The proposal names no systems.");
|
|
1371
|
+
if (audience === "technical") {
|
|
1372
|
+
add("API changes", api.map((a) => `${a.operation} (${a.caller} \u2192 ${a.callee}): ${a.change}; breaking: ${a.breaking}${a.judgedBy === "not judged" ? " (not judged)" : ""}. ${a.why}`), api.length ? "interfaces.md" : void 0, "interfaces.md lists no new or changed interfaces.");
|
|
1373
|
+
add("Data changes", data.map((d) => `${d.table}: ${d.change}${d.migration ? ` (${d.migration})` : " (no migration named)"}`), data.length ? "data.md" : void 0, "data.md lists no data changes.");
|
|
1374
|
+
if (impact)
|
|
1375
|
+
add("Impact", lines(impact.text), impact.source, "");
|
|
1376
|
+
}
|
|
1377
|
+
add("Not included", outOfScope, outOfScope.length ? scope?.source : void 0, "The brief doesn't say what is out of scope.");
|
|
1378
|
+
add("Known limitations", limitations.map((l) => `${l.summary}${l.reason ? ` Waived${l.by ? ` by ${l.by}` : ""}${l.at ? ` on ${l.at.slice(0, 10)}` : ""}: ${l.reason}` : ""} (${l.source})`), void 0, ctx.rounds.length ? "No finding is waived." : "No critic round has run yet, so no finding is waived.");
|
|
1379
|
+
if (audience === "business")
|
|
1380
|
+
add("Still open", bullets(questions?.text), questions?.source, "The brief has no open questions.");
|
|
1381
|
+
const md = [
|
|
1382
|
+
`# Release notes: ${ctx.change.title}`,
|
|
1383
|
+
"",
|
|
1384
|
+
`For ${audience} readers. Change \`${ctx.change.id}\`, generated ${ctx.today} from the change folder; every line says where it came from.`
|
|
1385
|
+
];
|
|
1386
|
+
for (const s of sections) {
|
|
1387
|
+
md.push("", `## ${s.heading}`, "");
|
|
1388
|
+
if (s.heading === "API changes" && api.length) {
|
|
1389
|
+
md.push("| Operation | Caller \u2192 callee | Change | Breaking | Why | Source |");
|
|
1390
|
+
md.push("| --- | --- | --- | --- | --- | --- |");
|
|
1391
|
+
for (const a of api)
|
|
1392
|
+
md.push(`| ${a.operation} | ${a.caller} \u2192 ${a.callee} | ${a.change} | ${a.breaking} (${a.judgedBy === "catalog" ? "by its change kind" : a.judgedBy === "not judged" ? "not judged" : a.breaking === "check" ? "a judge couldn't tell" : `judged by a ${a.judgedBy}`}) | ${a.why.replace(/\|/g, "\\|")} | ${a.source} |`);
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
if (s.heading === "Data changes" && data.length) {
|
|
1396
|
+
md.push("| Table | Change | Migration | Source |", "| --- | --- | --- | --- |");
|
|
1397
|
+
for (const d of data)
|
|
1398
|
+
md.push(`| ${d.table} | ${d.change.replace(/\|/g, "\\|")} | ${d.migration ?? "none named"} | ${d.source} |`);
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
if (!s.items.length) {
|
|
1402
|
+
md.push(`_${s.empty}_`);
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
if (s.heading === "Why" || s.heading === "Impact")
|
|
1406
|
+
md.push(s.items.join(" "));
|
|
1407
|
+
else
|
|
1408
|
+
for (const i of s.items)
|
|
1409
|
+
md.push(`- ${i}`);
|
|
1410
|
+
if (s.source)
|
|
1411
|
+
md.push("", `Source: ${s.source}`);
|
|
1412
|
+
}
|
|
1413
|
+
return {
|
|
1414
|
+
change: ctx.change.id,
|
|
1415
|
+
title: ctx.change.title,
|
|
1416
|
+
audience,
|
|
1417
|
+
today: ctx.today,
|
|
1418
|
+
sections,
|
|
1419
|
+
api,
|
|
1420
|
+
data,
|
|
1421
|
+
limitations,
|
|
1422
|
+
markdown: md.join("\n") + "\n"
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// packages/release/dist/security.js
|
|
1427
|
+
var SECURITY_RULES = /* @__PURE__ */ new Set([
|
|
1428
|
+
"data-columns-classified",
|
|
1429
|
+
"data-retention-stated",
|
|
1430
|
+
"interface-pattern-approved",
|
|
1431
|
+
"interface-ends-in-catalog",
|
|
1432
|
+
"vendor-operation-known",
|
|
1433
|
+
"no-embedded-instructions"
|
|
1434
|
+
]);
|
|
1435
|
+
var SECURITY_WORDS = /\b(auth\w*|secur\w*|secret|personal|privacy|classif\w*|encrypt\w*|tls|mtls|credential|token|vendor|retention|pii)\b/i;
|
|
1436
|
+
function authFromText(text) {
|
|
1437
|
+
if (/\bm-?tls\b|mutual tls|client cert/i.test(text))
|
|
1438
|
+
return "mtls";
|
|
1439
|
+
if (/\boauth/i.test(text))
|
|
1440
|
+
return "oauth2";
|
|
1441
|
+
if (/\bapi[- ]?key/i.test(text))
|
|
1442
|
+
return "api-key";
|
|
1443
|
+
if (/\bbasic auth/i.test(text))
|
|
1444
|
+
return "basic";
|
|
1445
|
+
if (/\b(no auth|unauthenticated|without auth)/i.test(text))
|
|
1446
|
+
return "none";
|
|
1447
|
+
return void 0;
|
|
1448
|
+
}
|
|
1449
|
+
function authOf(ctx, h) {
|
|
1450
|
+
if (h.record?.auth)
|
|
1451
|
+
return { auth: h.record.auth, source: `catalog Interface ${h.record.id}` };
|
|
1452
|
+
const ep = ctx.all("Endpoint").find((e) => e.component === h.callee && (e.method ?? "").toUpperCase() === (h.method ?? "") && (e.normalizedPath ?? normalizePath(e.pathOrTopic)) === (h.path ? normalizePath(h.path) : ""));
|
|
1453
|
+
if (ep?.auth)
|
|
1454
|
+
return { auth: ep.auth, source: `catalog Endpoint ${ep.id}` };
|
|
1455
|
+
for (const n of h.notes) {
|
|
1456
|
+
const a = authFromText(n.text);
|
|
1457
|
+
if (a)
|
|
1458
|
+
return { auth: a, source: `interfaces.md:${n.n}` };
|
|
1459
|
+
}
|
|
1460
|
+
for (const r of h.routes) {
|
|
1461
|
+
const route = ctx.get("GatewayRoute", r);
|
|
1462
|
+
for (const p of route?.policies ?? []) {
|
|
1463
|
+
const a = authFromText(p.replace(/([a-z])([A-Z])/g, "$1 $2"));
|
|
1464
|
+
if (a)
|
|
1465
|
+
return { auth: a, source: `catalog GatewayRoute ${r} policy ${p}` };
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return { auth: "not stated" };
|
|
1469
|
+
}
|
|
1470
|
+
function classifier(ctx, m) {
|
|
1471
|
+
const levels = ctx.orgPack.pack?.classification ?? [
|
|
1472
|
+
"public",
|
|
1473
|
+
"internal",
|
|
1474
|
+
"confidential",
|
|
1475
|
+
"restricted"
|
|
1476
|
+
];
|
|
1477
|
+
const personal = ctx.policy.privacy.classifications;
|
|
1478
|
+
const terms = [...personal, ...levels];
|
|
1479
|
+
const doc = docOf(ctx.files, "data.md");
|
|
1480
|
+
const sec = sectionMatching(doc, /classif/i);
|
|
1481
|
+
const lines2 = doc && sec ? sectionLines(doc, sec) : [];
|
|
1482
|
+
const termIn = (text) => terms.find((t) => hasWord(text, t));
|
|
1483
|
+
return {
|
|
1484
|
+
levels,
|
|
1485
|
+
personal,
|
|
1486
|
+
of(table, column) {
|
|
1487
|
+
const col = ctx.all("Column").find((c) => c.name === column && (c.table.endsWith(`.${table}`) || c.table === table));
|
|
1488
|
+
if (col?.classification)
|
|
1489
|
+
return { classification: col.classification, source: `catalog Column ${col.id}` };
|
|
1490
|
+
const own = lines2.find((l) => hasWord(l.text, column) && termIn(l.text));
|
|
1491
|
+
if (own)
|
|
1492
|
+
return { classification: termIn(own.text), source: `data.md:${own.n}` };
|
|
1493
|
+
const tableWide = lines2.find((l) => hasWord(l.text, table) && /\bcolumns\b/i.test(l.text) && termIn(l.text));
|
|
1494
|
+
if (tableWide)
|
|
1495
|
+
return { classification: termIn(tableWide.text), source: `data.md:${tableWide.n}` };
|
|
1496
|
+
void m;
|
|
1497
|
+
return { classification: "not classified yet" };
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
async function securityReview(ctx) {
|
|
1502
|
+
const m = buildModel(ctx);
|
|
1503
|
+
const c = classifier(ctx, m);
|
|
1504
|
+
const gaps = [];
|
|
1505
|
+
const planned = m.hops.filter((h) => /^planned-/.test(h.change));
|
|
1506
|
+
const interfaces = planned.map((h) => {
|
|
1507
|
+
const a = authOf(ctx, h);
|
|
1508
|
+
const crosses = h.callerSystem && h.calleeSystem && h.callerSystem !== h.calleeSystem ? `${m.systemName(h.callerSystem)} \u2192 ${m.systemName(h.calleeSystem)}${h.calleeBoundary ? ` (${h.calleeBoundary})` : ""}` : !h.calleeSystem ? `${h.callee} isn't in the catalog` : void 0;
|
|
1509
|
+
if (a.auth === "not stated")
|
|
1510
|
+
gaps.push(`interfaces.md:${h.line}: ${opOf(h)} (${h.caller} \u2192 ${h.callee}) says nothing about auth, and the catalog has none.`);
|
|
1511
|
+
return {
|
|
1512
|
+
operation: opOf(h),
|
|
1513
|
+
caller: h.caller,
|
|
1514
|
+
callee: h.callee,
|
|
1515
|
+
change: h.change.replace(/^planned-/, ""),
|
|
1516
|
+
auth: a.auth,
|
|
1517
|
+
...a.source ? { authSource: a.source } : {},
|
|
1518
|
+
...crosses ? { crosses } : {},
|
|
1519
|
+
source: `interfaces.md:${h.line}`
|
|
1520
|
+
};
|
|
1521
|
+
});
|
|
1522
|
+
const columnItem = (table, column, type) => personalItem({
|
|
1523
|
+
column,
|
|
1524
|
+
table,
|
|
1525
|
+
...type ? { type } : {},
|
|
1526
|
+
classifiedPersonal: c.personal.includes(c.of(table, column).classification)
|
|
1527
|
+
});
|
|
1528
|
+
const mappedItem = (src) => {
|
|
1529
|
+
const dot = /^([a-z0-9_]+)\.([a-z0-9_]+)$/i.exec(src.trim());
|
|
1530
|
+
return dot ? columnItem(dot[1], dot[2]) : personalItem({ column: src, table: "(mapping)" });
|
|
1531
|
+
};
|
|
1532
|
+
const mdoc = docOf(ctx.files, "mappings.md");
|
|
1533
|
+
const mappedSources = (mdoc?.tables ?? []).filter((t) => t.headers.includes("source field")).flatMap((t) => t.rows.map((r) => r.cells["source field"] ?? "")).filter(Boolean);
|
|
1534
|
+
const personalOf = await judgedFor(ctx, PERSONAL_DATA_SPEC, [
|
|
1535
|
+
...m.data.flatMap((d) => d.newColumns.map((col) => columnItem(d.table, col.name, col.type))),
|
|
1536
|
+
...mappedSources.map(mappedItem)
|
|
1537
|
+
]);
|
|
1538
|
+
const personalWord = (item) => {
|
|
1539
|
+
const v = personalOf(item)?.verdict;
|
|
1540
|
+
return v === "yes" || v === "no" || v === "unsure" ? v : "not judged";
|
|
1541
|
+
};
|
|
1542
|
+
const fields = [];
|
|
1543
|
+
for (const d of m.data)
|
|
1544
|
+
for (const col of d.newColumns) {
|
|
1545
|
+
const k = c.of(d.table, col.name);
|
|
1546
|
+
const personal = personalWord(columnItem(d.table, col.name, col.type));
|
|
1547
|
+
fields.push({
|
|
1548
|
+
field: `${d.store}.${d.table}.${col.name}`,
|
|
1549
|
+
classification: k.classification,
|
|
1550
|
+
...k.source ? { classificationSource: k.source } : {},
|
|
1551
|
+
personal,
|
|
1552
|
+
personalLooking: personal === "yes",
|
|
1553
|
+
source: `data.md:${d.line}`
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
for (const f of fields) {
|
|
1557
|
+
if (f.classification === "not classified yet")
|
|
1558
|
+
gaps.push(`${f.field} (${f.source}) is not classified yet.`);
|
|
1559
|
+
else if (f.personal === "yes" && !c.personal.includes(f.classification))
|
|
1560
|
+
gaps.push(`${f.field} is judged personal data but is classified ${f.classification} (${f.classificationSource}).`);
|
|
1561
|
+
}
|
|
1562
|
+
const unjudged = fields.filter((f) => (f.personal === "not judged" || f.personal === "unsure") && !c.personal.includes(f.classification));
|
|
1563
|
+
if (unjudged.length)
|
|
1564
|
+
gaps.push(`Not judged whether ${unjudged.map((f) => f.field).join(", ")} ${unjudged.length === 1 ? "holds" : "hold"} personal data (${unjudged.some((f) => f.personal === "unsure") ? "a judge couldn't tell" : "no model was asked"}), so a person checks ${unjudged.length === 1 ? "it" : "them"} against the personal classes (${c.personal.join(", ")}).`);
|
|
1565
|
+
const crossings = [];
|
|
1566
|
+
const level = (cl) => c.levels.indexOf(cl);
|
|
1567
|
+
const sensitiveFrom = Math.max(1, level("confidential"));
|
|
1568
|
+
for (const h of planned) {
|
|
1569
|
+
if (!h.callerSystem || !h.calleeSystem || h.callerSystem === h.calleeSystem)
|
|
1570
|
+
continue;
|
|
1571
|
+
const s = mdoc?.sections.find((x) => x.heading.trim().toLowerCase() === opOf(h).toLowerCase());
|
|
1572
|
+
const t = s && mdoc ? mdoc.tables.find((x) => x.line > s.line && x.line <= s.end && x.headers.includes("source field")) : void 0;
|
|
1573
|
+
const out = (t?.rows ?? []).map((r) => {
|
|
1574
|
+
const src = r.cells["source field"] ?? "";
|
|
1575
|
+
const dot = /^([a-z0-9_]+)\.([a-z0-9_]+)$/i.exec(src.trim());
|
|
1576
|
+
const k = dot ? c.of(dot[1], dot[2]) : { classification: "not a catalog field" };
|
|
1577
|
+
const personal = src ? personalWord(mappedItem(src)) : "not judged";
|
|
1578
|
+
const sensitive = c.personal.includes(k.classification) || personal === "yes" || level(k.classification) >= sensitiveFrom ? "yes" : level(k.classification) >= 0 && personal === "no" ? "no" : "unknown";
|
|
1579
|
+
return {
|
|
1580
|
+
field: src,
|
|
1581
|
+
classification: k.classification,
|
|
1582
|
+
...k.source ? { classificationSource: k.source } : {},
|
|
1583
|
+
personal,
|
|
1584
|
+
personalLooking: personal === "yes",
|
|
1585
|
+
...r.cells["rule or transformation"] ? { transform: r.cells["rule or transformation"] } : {},
|
|
1586
|
+
sensitive,
|
|
1587
|
+
source: `mappings.md:${r.line}`
|
|
1588
|
+
};
|
|
1589
|
+
});
|
|
1590
|
+
const note = s && mdoc && !t ? sectionLines(mdoc, s).map((l) => l.text.trim()).join(" ") : void 0;
|
|
1591
|
+
if (!s)
|
|
1592
|
+
gaps.push(`${opOf(h)} crosses from ${m.systemName(h.callerSystem)} to ${m.systemName(h.calleeSystem)} but mappings.md doesn't say which fields it sends.`);
|
|
1593
|
+
crossings.push({
|
|
1594
|
+
operation: opOf(h),
|
|
1595
|
+
from: m.systemName(h.callerSystem),
|
|
1596
|
+
to: m.systemName(h.calleeSystem),
|
|
1597
|
+
toBoundary: h.calleeBoundary ?? "unknown",
|
|
1598
|
+
fields: out,
|
|
1599
|
+
...note ? { note } : {},
|
|
1600
|
+
source: t ? `mappings.md:${t.line}` : s ? `mappings.md:${s.line}` : `interfaces.md:${h.line}`
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
const vendorIds = sortStrings([
|
|
1604
|
+
...ctx.change.systems.filter((s) => m.boundaryOf(s) === "vendor"),
|
|
1605
|
+
...planned.filter((h) => h.calleeBoundary === "vendor" && h.calleeSystem).map((h) => h.calleeSystem)
|
|
1606
|
+
]);
|
|
1607
|
+
const plannedIds = new Set(planned.map((h) => h.record?.id).filter(Boolean));
|
|
1608
|
+
const vendors = vendorIds.map((v) => {
|
|
1609
|
+
const existing = ctx.all("Interface").filter((i) => m.systemOf(i.to) === v && !plannedIds.has(i.id)).length;
|
|
1610
|
+
return {
|
|
1611
|
+
system: v,
|
|
1612
|
+
name: m.systemName(v),
|
|
1613
|
+
isNew: existing === 0,
|
|
1614
|
+
operations: planned.filter((h) => h.calleeSystem === v).map((h) => opOf(h)),
|
|
1615
|
+
existing
|
|
1616
|
+
};
|
|
1617
|
+
});
|
|
1618
|
+
const secrets = [];
|
|
1619
|
+
for (const i of interfaces) {
|
|
1620
|
+
if (i.change === "retire")
|
|
1621
|
+
continue;
|
|
1622
|
+
if (i.auth === "mtls")
|
|
1623
|
+
secrets.push({
|
|
1624
|
+
what: `A client certificate for ${i.caller} to call ${i.operation} (mTLS)`,
|
|
1625
|
+
source: i.authSource ?? i.source
|
|
1626
|
+
});
|
|
1627
|
+
if (i.auth === "api-key")
|
|
1628
|
+
secrets.push({
|
|
1629
|
+
what: `An API key for ${i.caller} to call ${i.operation}`,
|
|
1630
|
+
source: i.authSource ?? i.source
|
|
1631
|
+
});
|
|
1632
|
+
if (i.auth === "oauth2") {
|
|
1633
|
+
const h = planned.find((x) => opOf(x) === i.operation && x.caller === i.caller);
|
|
1634
|
+
const grant = ctx.all("AccessGrant").find((g) => g.subject === i.caller && h.routes.some((r) => (ctx.get("GatewayRoute", r)?.proxy ?? r) === g.resource));
|
|
1635
|
+
secrets.push(grant ? {
|
|
1636
|
+
what: `OAuth client ${i.caller} with scopes ${grant.scopesOrPermissions.join(", ")} (existing grant)`,
|
|
1637
|
+
source: `catalog AccessGrant ${grant.id}`
|
|
1638
|
+
} : {
|
|
1639
|
+
what: `An OAuth client for ${i.caller} to call ${i.operation}: no access grant in the catalog yet`,
|
|
1640
|
+
source: i.authSource ?? i.source
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
const candidates = [...ctx.files.docs].flatMap(([path, doc]) => contentLines(doc).filter((l) => !/^\s*#/.test(l.text) && !/^\s*\|[\s|:-]*\|?\s*$/.test(l.text)).map((l) => ({ path, doc, l, item: secretItem(l.text.trim()) })));
|
|
1645
|
+
const secretOf = await judgedFor(ctx, NAMES_SECRET_SPEC, candidates.map((x) => x.item));
|
|
1646
|
+
const secretsNotJudged = [];
|
|
1647
|
+
for (const { path, doc, l, item } of candidates) {
|
|
1648
|
+
const v = secretOf(item);
|
|
1649
|
+
if (!v || v.verdict === "unsure")
|
|
1650
|
+
secretsNotJudged.push(`${path}:${l.n}`);
|
|
1651
|
+
if (v?.verdict !== "yes")
|
|
1652
|
+
continue;
|
|
1653
|
+
const format = tokenFormatIn(l.text);
|
|
1654
|
+
if (format) {
|
|
1655
|
+
gaps.push(`${path}:${l.n} holds what looks like ${format}: take it out of the change folder and rotate it.`);
|
|
1656
|
+
secrets.push({
|
|
1657
|
+
what: `${format}, written in the change (not shown here)`,
|
|
1658
|
+
source: `${path}:${l.n}`
|
|
1659
|
+
});
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
const row = doc.tables.flatMap((t) => t.rows).find((r) => r.line === l.n);
|
|
1663
|
+
const what = !row ? l.text.trim().replace(/^[-*]\s+/, "") : row.cells["source field"] && row.cells["target field"] ? `${row.cells["source field"]} \u2192 ${row.cells["target field"]}: ${Object.entries(row.cells).filter(([k, v2]) => v2 && k !== "source field" && k !== "target field").map(([, v2]) => v2).join("; ")}` : Object.values(row.cells).filter(Boolean).join("; ");
|
|
1664
|
+
secrets.push({ what, source: `${path}:${l.n}` });
|
|
1665
|
+
}
|
|
1666
|
+
const uniqueSecrets = secrets.filter((x, i, a) => a.findIndex((y) => y.what === x.what && y.source === x.source) === i);
|
|
1667
|
+
const touchedComponents = new Set(planned.flatMap((h) => [h.caller, h.callee]));
|
|
1668
|
+
const settings = ctx.all("Setting").filter((s) => touchedComponents.has(s.component) && (!s.target || touchedComponents.has(s.target))).map((s) => ({
|
|
1669
|
+
id: s.id,
|
|
1670
|
+
key: s.key,
|
|
1671
|
+
value: s.valueOrRef,
|
|
1672
|
+
component: s.component,
|
|
1673
|
+
...s.target ? { target: s.target } : {}
|
|
1674
|
+
}));
|
|
1675
|
+
const findings = ctx.latest.filter((f) => f.finding.state === "open" && (SECURITY_RULES.has(f.finding.rule) || SECURITY_WORDS.test(f.finding.summary))).map((f) => ({
|
|
1676
|
+
gate: f.finding.gate,
|
|
1677
|
+
id: f.finding.id,
|
|
1678
|
+
rule: f.finding.rule,
|
|
1679
|
+
severity: f.finding.severity,
|
|
1680
|
+
summary: f.finding.summary,
|
|
1681
|
+
path: f.path
|
|
1682
|
+
}));
|
|
1683
|
+
const gates = ctx.orgPack.gates.gates.filter((g) => g.phase === "design" || g.phase === "release").map((g) => {
|
|
1684
|
+
const h = ctx.change.handoffs.filter((x) => x.phase === g.phase).at(-1);
|
|
1685
|
+
return {
|
|
1686
|
+
gate: g.id,
|
|
1687
|
+
phase: g.phase,
|
|
1688
|
+
...h ? {
|
|
1689
|
+
handoff: {
|
|
1690
|
+
state: h.state,
|
|
1691
|
+
pr: h.pr.url,
|
|
1692
|
+
approvals: h.approvals.map((a) => `${a.by} ${a.at.slice(0, 10)}`)
|
|
1693
|
+
}
|
|
1694
|
+
} : {}
|
|
1695
|
+
};
|
|
1696
|
+
});
|
|
1697
|
+
const signOff = {
|
|
1698
|
+
gates,
|
|
1699
|
+
how: gates.length ? `Reviewers approve the ${gates.map((g) => g.phase).join(" and ")} hand-off PRs on the Git host with this draft in hand; merging a PR is its sign-off, and atticus change sync records it. The Git host's rules (branch protection, CODEOWNERS) decide who may approve. There is no separate security approval.` : "gates.yaml has no design or release gate, so no hand-off PR carries this review yet."
|
|
1700
|
+
};
|
|
1701
|
+
const review = {
|
|
1702
|
+
change: ctx.change.id,
|
|
1703
|
+
title: ctx.change.title,
|
|
1704
|
+
today: ctx.today,
|
|
1705
|
+
levels: c.levels,
|
|
1706
|
+
interfaces,
|
|
1707
|
+
fields,
|
|
1708
|
+
crossings,
|
|
1709
|
+
vendors,
|
|
1710
|
+
secrets: uniqueSecrets,
|
|
1711
|
+
secretsNotJudged,
|
|
1712
|
+
settings,
|
|
1713
|
+
findings,
|
|
1714
|
+
signOff,
|
|
1715
|
+
gaps: [...new Set(gaps)]
|
|
1716
|
+
};
|
|
1717
|
+
return { ...review, markdown: securityMarkdown(review, ctx.rounds.length > 0) };
|
|
1718
|
+
}
|
|
1719
|
+
var esc2 = (s) => s.replace(/\|/g, "\\|");
|
|
1720
|
+
function securityMarkdown(r, reviewed) {
|
|
1721
|
+
const md = [
|
|
1722
|
+
`# Security review draft: ${r.title}`,
|
|
1723
|
+
"",
|
|
1724
|
+
`Change \`${r.change}\`, generated ${r.today} from the catalog and the change folder. A draft for the reviewers of the hand-off PRs: every line says where it came from, and anything Atticus couldn't find is listed as a gap.`,
|
|
1725
|
+
"",
|
|
1726
|
+
"## New or changed interfaces",
|
|
1727
|
+
""
|
|
1728
|
+
];
|
|
1729
|
+
if (r.interfaces.length) {
|
|
1730
|
+
md.push("| Operation | Caller \u2192 callee | Change | Auth | Auth from | Crosses | Source |", "| --- | --- | --- | --- | --- | --- | --- |");
|
|
1731
|
+
for (const i of r.interfaces)
|
|
1732
|
+
md.push(`| ${i.operation} | ${i.caller} \u2192 ${i.callee} | ${i.change} | ${i.auth} | ${i.authSource ?? "nowhere"} | ${i.crosses ?? "no"} | ${i.source} |`);
|
|
1733
|
+
} else
|
|
1734
|
+
md.push("_interfaces.md lists no new or changed interfaces._");
|
|
1735
|
+
md.push("", "## Data fields touched", "", `Classification levels (org pack): ${r.levels.join(", ")}.`, "");
|
|
1736
|
+
if (r.fields.length) {
|
|
1737
|
+
md.push("| Field | Classification | From | Personal data | Source |", "| --- | --- | --- | --- | --- |");
|
|
1738
|
+
for (const f of r.fields)
|
|
1739
|
+
md.push(`| ${f.field} | ${f.classification} | ${f.classificationSource ?? "nowhere"} | ${f.personal} | ${f.source} |`);
|
|
1740
|
+
} else
|
|
1741
|
+
md.push("_data.md adds no fields._");
|
|
1742
|
+
md.push("", "## Data crossing a boundary", "");
|
|
1743
|
+
if (r.crossings.length)
|
|
1744
|
+
for (const x of r.crossings) {
|
|
1745
|
+
md.push(`### ${x.operation}: ${x.from} \u2192 ${x.to} (${x.toBoundary})`, "");
|
|
1746
|
+
if (x.fields.length) {
|
|
1747
|
+
md.push("| Field | Classification | Transformation | Sensitive | Source |", "| --- | --- | --- | --- | --- |");
|
|
1748
|
+
for (const f of x.fields)
|
|
1749
|
+
md.push(`| ${esc2(f.field)} | ${f.classification} | ${esc2(f.transform ?? "")} | ${f.sensitive} | ${f.source} |`);
|
|
1750
|
+
} else if (x.note)
|
|
1751
|
+
md.push(`${x.note} (${x.source})`);
|
|
1752
|
+
else
|
|
1753
|
+
md.push(`_mappings.md doesn't say which fields it sends (${x.source})._`);
|
|
1754
|
+
md.push("");
|
|
1755
|
+
}
|
|
1756
|
+
else
|
|
1757
|
+
md.push("_No new or changed call crosses a system boundary._", "");
|
|
1758
|
+
md.push("## Vendors", "");
|
|
1759
|
+
if (r.vendors.length)
|
|
1760
|
+
for (const v of r.vendors)
|
|
1761
|
+
md.push(`- ${v.name} (${v.system}): ${v.isNew ? "a new vendor" : `already called (${v.existing} interface${v.existing === 1 ? "" : "s"} in the catalog)`}; new or changed: ${v.operations.join(", ") || "none"}.`);
|
|
1762
|
+
else
|
|
1763
|
+
md.push("_The change calls no vendor._");
|
|
1764
|
+
md.push("", "## Secrets and settings touched", "");
|
|
1765
|
+
if (r.secrets.length)
|
|
1766
|
+
for (const s of r.secrets)
|
|
1767
|
+
md.push(`- ${s.what} (${s.source})`);
|
|
1768
|
+
else if (!r.secretsNotJudged.length)
|
|
1769
|
+
md.push("_No credential or secret is named or implied._");
|
|
1770
|
+
if (r.secretsNotJudged.length)
|
|
1771
|
+
md.push(...r.secrets.length ? [""] : [], `_Not judged: ${r.secretsNotJudged.length} ${r.secretsNotJudged.length === 1 ? "line" : "lines"} of the change ${r.secretsNotJudged.length === 1 ? "wasn't" : "weren't"} read for secrets (no model was asked, or a judge couldn't tell), so a person reads ${r.secretsNotJudged.length === 1 ? "it" : "them"} for keys, certificates and passwords: ${r.secretsNotJudged.slice(0, 10).join(", ")}${r.secretsNotJudged.length > 10 ? ` and ${r.secretsNotJudged.length - 10} more` : ""}._`);
|
|
1772
|
+
if (r.settings.length) {
|
|
1773
|
+
md.push("", "Settings in force on the touched calls:", "");
|
|
1774
|
+
for (const s of r.settings)
|
|
1775
|
+
md.push(`- ${s.key} = ${s.value} on ${s.component}${s.target ? ` \u2192 ${s.target}` : ""} (catalog Setting ${s.id})`);
|
|
1776
|
+
}
|
|
1777
|
+
md.push("", "## Open security-relevant findings", "");
|
|
1778
|
+
if (r.findings.length)
|
|
1779
|
+
for (const f of r.findings)
|
|
1780
|
+
md.push(`- ${f.severity}: ${f.summary} (${f.rule}, ${f.path})`);
|
|
1781
|
+
else
|
|
1782
|
+
md.push(reviewed ? "_None open._" : "_No critic round has run yet, so there are no findings to show._");
|
|
1783
|
+
md.push("", "## Sign-off", "");
|
|
1784
|
+
md.push(`- How: ${r.signOff.how}`);
|
|
1785
|
+
for (const g of r.signOff.gates)
|
|
1786
|
+
md.push(`- ${g.gate} gate: ${g.handoff ? `hand-off PR ${g.handoff.pr} is ${g.handoff.state}${g.handoff.approvals.length ? `, approved by ${g.handoff.approvals.join("; ")}` : ""}` : "no hand-off PR yet"}.`);
|
|
1787
|
+
md.push("", "## Gaps", "");
|
|
1788
|
+
if (r.gaps.length)
|
|
1789
|
+
for (const g of r.gaps)
|
|
1790
|
+
md.push(`- ${g}`);
|
|
1791
|
+
else
|
|
1792
|
+
md.push("_None._");
|
|
1793
|
+
return md.join("\n") + "\n";
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// packages/release/dist/estimate.js
|
|
1797
|
+
var CALLS = /* @__PURE__ */ new Set(["call-out", "publish"]);
|
|
1798
|
+
var OP = {
|
|
1799
|
+
added: "add",
|
|
1800
|
+
changed: "change",
|
|
1801
|
+
removed: "remove"
|
|
1802
|
+
};
|
|
1803
|
+
function classOf(s, boundaryOf) {
|
|
1804
|
+
const parts = [OP[s.mark] ?? "keep", s.kind];
|
|
1805
|
+
if (s.to && CALLS.has(s.kind)) {
|
|
1806
|
+
if (boundaryOf(s.to.component) === "vendor")
|
|
1807
|
+
parts.push("vendor");
|
|
1808
|
+
if (s.to.new)
|
|
1809
|
+
parts.push("new-entry");
|
|
1810
|
+
}
|
|
1811
|
+
if (s.store?.new)
|
|
1812
|
+
parts.push("new-table");
|
|
1813
|
+
else if (s.store?.migration)
|
|
1814
|
+
parts.push("migration");
|
|
1815
|
+
return parts.join("/");
|
|
1816
|
+
}
|
|
1817
|
+
function rangeOf(cls, table) {
|
|
1818
|
+
for (const c of table?.classes ?? []) {
|
|
1819
|
+
if (c.match === "*" || c.match === cls)
|
|
1820
|
+
return c.days;
|
|
1821
|
+
if (c.match.endsWith("/*") && `${cls}/`.startsWith(c.match.slice(0, -1)))
|
|
1822
|
+
return c.days;
|
|
1823
|
+
}
|
|
1824
|
+
return void 0;
|
|
1825
|
+
}
|
|
1826
|
+
function seeded(text) {
|
|
1827
|
+
let a = 2166136261;
|
|
1828
|
+
for (const ch of text)
|
|
1829
|
+
a = Math.imul(a ^ ch.charCodeAt(0), 16777619);
|
|
1830
|
+
return () => {
|
|
1831
|
+
a = a + 1831565813 | 0;
|
|
1832
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
1833
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
1834
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
var Z80 = 0.8416;
|
|
1838
|
+
var SAMPLES = 4e3;
|
|
1839
|
+
function sampler(next) {
|
|
1840
|
+
return (d) => {
|
|
1841
|
+
const sigma = (Math.log(d.p80) - Math.log(d.p20)) / (2 * Z80);
|
|
1842
|
+
const u = Math.max(next(), 1e-12);
|
|
1843
|
+
const n = Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * next());
|
|
1844
|
+
return Math.exp(Math.log(d.p50) + sigma * n);
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
var round = (n) => Math.round(n * 10) / 10;
|
|
1848
|
+
function quantiles(xs) {
|
|
1849
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
1850
|
+
const at = (q) => s[Math.min(s.length - 1, Math.floor(q * s.length))];
|
|
1851
|
+
return { p20: round(at(0.2)), p50: round(at(0.5)), p80: round(at(0.8)) };
|
|
1852
|
+
}
|
|
1853
|
+
function estimateChange(input) {
|
|
1854
|
+
const units = [];
|
|
1855
|
+
const drivers = /* @__PURE__ */ new Map();
|
|
1856
|
+
const factor = (id) => input.table?.risks[id];
|
|
1857
|
+
const drive = (id, what, count, source) => {
|
|
1858
|
+
if (!count)
|
|
1859
|
+
return;
|
|
1860
|
+
const d = drivers.get(id);
|
|
1861
|
+
if (d)
|
|
1862
|
+
d.count += count;
|
|
1863
|
+
else {
|
|
1864
|
+
const w = factor(id);
|
|
1865
|
+
drivers.set(id, {
|
|
1866
|
+
id,
|
|
1867
|
+
what,
|
|
1868
|
+
count,
|
|
1869
|
+
...w && w > 1 ? { widens: w } : {},
|
|
1870
|
+
...source ? { source } : {}
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
for (const f of input.flows) {
|
|
1875
|
+
const unit = (u) => {
|
|
1876
|
+
const days = rangeOf(u.class, input.table);
|
|
1877
|
+
units.push({ flow: f.flow, repo: f.repo, ...u, ...days ? { days } : {}, source: f.file });
|
|
1878
|
+
};
|
|
1879
|
+
if (f.change === "remove") {
|
|
1880
|
+
unit({ what: `${f.flow} goes`, class: "remove-flow" });
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
if (f.change === "new")
|
|
1884
|
+
unit({ what: `${f.flow} is new`, class: `new-flow/${f.trigger ?? "endpoint"}` });
|
|
1885
|
+
for (const s of f.steps) {
|
|
1886
|
+
if (s.mark === "same")
|
|
1887
|
+
continue;
|
|
1888
|
+
const step = s.target ?? s.today;
|
|
1889
|
+
unit({
|
|
1890
|
+
...step !== void 0 ? { step } : {},
|
|
1891
|
+
what: s.what ?? s.kind,
|
|
1892
|
+
class: classOf(s, input.boundaryOf)
|
|
1893
|
+
});
|
|
1894
|
+
if (s.mark === "removed")
|
|
1895
|
+
continue;
|
|
1896
|
+
if (s.to && CALLS.has(s.kind) && s.to.new && input.boundaryOf(s.to.component) === "vendor")
|
|
1897
|
+
drive("new-vendor-call", "New calls to a vendor", 1, f.file);
|
|
1898
|
+
if (s.store?.new)
|
|
1899
|
+
drive("new-table", "New tables", 1, f.file);
|
|
1900
|
+
else if (s.store?.migration)
|
|
1901
|
+
drive("migration", "Migrations", 1, f.file);
|
|
1902
|
+
}
|
|
1903
|
+
const unconfirmed = f.steps.filter((s) => (s.mark === "changed" || s.mark === "removed") && (!s.confirmedBy || s.confirmedBy === "ai")).length;
|
|
1904
|
+
drive("unconfirmed-step", "Changed steps no check or person confirmed", unconfirmed, f.file);
|
|
1905
|
+
}
|
|
1906
|
+
drive("open-blocking", "Open blocking findings", input.openBlocking ?? 0);
|
|
1907
|
+
drive("open-questions", "Open questions", input.openQuestions ?? 0);
|
|
1908
|
+
drive("conflicts", "Other open changes on the same parts", input.conflicts ?? 0);
|
|
1909
|
+
const repos = [...new Set(input.flows.map((f) => f.repo))].sort();
|
|
1910
|
+
if (repos.length > 1)
|
|
1911
|
+
drive("repos", "Repos to change", repos.length);
|
|
1912
|
+
const ORDER = [
|
|
1913
|
+
"new-vendor-call",
|
|
1914
|
+
"new-table",
|
|
1915
|
+
"migration",
|
|
1916
|
+
"unconfirmed-step",
|
|
1917
|
+
"open-blocking",
|
|
1918
|
+
"open-questions",
|
|
1919
|
+
"conflicts",
|
|
1920
|
+
"repos"
|
|
1921
|
+
];
|
|
1922
|
+
const listed = ORDER.flatMap((id) => drivers.has(id) ? [drivers.get(id)] : []);
|
|
1923
|
+
const classed = units.filter((u) => u.days);
|
|
1924
|
+
let effort;
|
|
1925
|
+
const perRepo = repos.map((repo) => ({
|
|
1926
|
+
repo,
|
|
1927
|
+
units: units.filter((u) => u.repo === repo).length
|
|
1928
|
+
}));
|
|
1929
|
+
if (classed.length) {
|
|
1930
|
+
const draw = sampler(seeded(input.change));
|
|
1931
|
+
const totals = [];
|
|
1932
|
+
const byRepo = new Map(repos.map((r) => [r, []]));
|
|
1933
|
+
const extra = input.table?.perExtraRepo;
|
|
1934
|
+
for (let i = 0; i < SAMPLES; i++) {
|
|
1935
|
+
let total = 0;
|
|
1936
|
+
for (const r of repos) {
|
|
1937
|
+
let sum = 0;
|
|
1938
|
+
for (const u of classed)
|
|
1939
|
+
if (u.repo === r)
|
|
1940
|
+
sum += draw(u.days);
|
|
1941
|
+
byRepo.get(r).push(sum);
|
|
1942
|
+
total += sum;
|
|
1943
|
+
}
|
|
1944
|
+
if (extra)
|
|
1945
|
+
for (let k = 1; k < repos.length; k++)
|
|
1946
|
+
total += draw(extra);
|
|
1947
|
+
totals.push(total);
|
|
1948
|
+
}
|
|
1949
|
+
const widen = listed.reduce((w, d) => w * (d.widens ?? 1), 1);
|
|
1950
|
+
const q = quantiles(totals);
|
|
1951
|
+
effort = { ...q, p80: round(q.p80 * widen) };
|
|
1952
|
+
for (const p of perRepo)
|
|
1953
|
+
if (classed.some((u) => u.repo === p.repo))
|
|
1954
|
+
p.effort = quantiles(byRepo.get(p.repo));
|
|
1955
|
+
}
|
|
1956
|
+
const calendar = {
|
|
1957
|
+
...effort ? { effortDays: effort.p80 } : {},
|
|
1958
|
+
...input.waitDays !== void 0 ? { waitDays: input.waitDays } : {}
|
|
1959
|
+
};
|
|
1960
|
+
if (effort && input.waitDays !== void 0)
|
|
1961
|
+
calendar.longer = effort.p80 > input.waitDays ? "work" : "waiting";
|
|
1962
|
+
return {
|
|
1963
|
+
change: input.change,
|
|
1964
|
+
units,
|
|
1965
|
+
needsClass: units.length - classed.length,
|
|
1966
|
+
...effort ? { effort } : {},
|
|
1967
|
+
perRepo,
|
|
1968
|
+
drivers: listed,
|
|
1969
|
+
calendar,
|
|
1970
|
+
calibration: {
|
|
1971
|
+
calibrated: false,
|
|
1972
|
+
...input.table ? { source: input.table.source } : {}
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
export {
|
|
1978
|
+
ReleaseError,
|
|
1979
|
+
readServiceRequests,
|
|
1980
|
+
loadRelease,
|
|
1981
|
+
deploymentPlan,
|
|
1982
|
+
releaseForecast,
|
|
1983
|
+
outcomesView,
|
|
1984
|
+
recordOutcome,
|
|
1985
|
+
evidencePack,
|
|
1986
|
+
writeEvidencePack,
|
|
1987
|
+
AUDIENCES,
|
|
1988
|
+
releaseNotes,
|
|
1989
|
+
securityReview,
|
|
1990
|
+
estimateChange
|
|
1991
|
+
};
|